Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions client/dive-common/components/Attributes/AttributeEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import {
import { Attribute } from 'vue-media-annotator/use/AttributeTypes';
import { usePrompt } from 'dive-common/vue-utilities/prompt-service';
import { useTrackStyleManager } from 'vue-media-annotator/provides';
import {
isReservedAttributeName,
RESERVED_DETECTION_ATTRIBUTES,
RESERVED_TRACK_ATTRIBUTES,
} from 'vue-media-annotator/utils';
import AttributeRendering from './AttributeRendering.vue';
import AttributeValueColors from './AttributeValueColors.vue';
import AttributeNumberValueColors from './AttributeNumberValueColors.vue';
Expand Down Expand Up @@ -205,6 +210,10 @@ export default defineComponent({
typeChange,
numericChange,
launchColorEditor,
//utils
isReservedAttributeName,
RESERVED_DETECTION_ATTRIBUTES,
RESERVED_TRACK_ATTRIBUTES,
};
},
});
Expand Down Expand Up @@ -245,8 +254,15 @@ export default defineComponent({
<v-text-field
v-model="baseSettings.name"
label="Name"
:rules="[v => !!v || 'Name is required', v => !v.includes(' ')
|| 'No spaces', v => v !== 'userAttributes' || 'Reserved Name']"
:rules="[
v => !!v || 'Name is required',
v => !v.includes(' ') || 'No spaces',
v => v !== 'userAttributes' || 'Reserved Name',
v => !isReservedAttributeName(v, baseSettings.belongs)
|| `Reserved name. ${baseSettings.belongs === 'detection'
? `Reserved detection attributes: ${RESERVED_DETECTION_ATTRIBUTES.join(', ')}`
: `Reserved track attributes: ${RESERVED_TRACK_ATTRIBUTES.join(', ')}`}`,
]"
required
/>
<v-select
Expand Down
22 changes: 20 additions & 2 deletions client/dive-common/use/useModeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import {
} from 'vue';
import { uniq, flatMapDeep, flattenDeep } from 'lodash';
import Track, { TrackId } from 'vue-media-annotator/track';
import { RectBounds, updateBounds } from 'vue-media-annotator/utils';
import {
RectBounds,
updateBounds,
validateRotation,
ROTATION_ATTRIBUTE_NAME,
} from 'vue-media-annotator/utils';
import { EditAnnotationTypes, VisibleAnnotationTypes } from 'vue-media-annotator/layers';
import { AggregateMediaController } from 'vue-media-annotator/components/annotators/mediaControllerType';

Expand Down Expand Up @@ -395,7 +400,7 @@ export default function useModeManager({
creating = newCreatingValue;
}

function handleUpdateRectBounds(frameNum: number, flickNum: number, bounds: RectBounds) {
function handleUpdateRectBounds(frameNum: number, flickNum: number, bounds: RectBounds, rotation?: number) {
if (selectedTrackId.value !== null) {
const track = cameraStore.getPossibleTrack(selectedTrackId.value, selectedCamera.value);
if (track) {
Expand All @@ -409,6 +414,19 @@ export default function useModeManager({
keyframe: true,
interpolate: _shouldInterpolate(interpolate),
});

// Save rotation as detection attribute if provided
const normalizedRotation = validateRotation(rotation);
if (normalizedRotation !== undefined) {
track.setFeatureAttribute(frameNum, ROTATION_ATTRIBUTE_NAME, normalizedRotation);
} else {
// Remove rotation attribute if rotation is 0 or undefined
const feature = track.features[frameNum];
if (feature && feature.attributes && ROTATION_ATTRIBUTE_NAME in feature.attributes) {
track.setFeatureAttribute(frameNum, ROTATION_ATTRIBUTE_NAME, undefined);
}
}

newTrackSettingsAfterLogic(track);
}
}
Expand Down
8 changes: 6 additions & 2 deletions client/src/components/LayerManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import TextLayer, { FormatTextRow } from '../layers/AnnotationLayers/TextLayer';
import AttributeLayer from '../layers/AnnotationLayers/AttributeLayer';
import AttributeBoxLayer from '../layers/AnnotationLayers/AttributeBoxLayer';
import type { AnnotationId } from '../BaseAnnotation';
import { geojsonToBound } from '../utils';
import { geojsonToBound, isRotationValue, ROTATION_ATTRIBUTE_NAME } from '../utils';
import { VisibleAnnotationTypes } from '../layers';
import UILayer from '../layers/UILayers/UILayer';
import ToolTipWidget from '../layers/UILayers/ToolTipWidget.vue';
Expand Down Expand Up @@ -465,8 +465,12 @@ export default defineComponent({
) => {
if (type === 'rectangle') {
const bounds = geojsonToBound(data as GeoJSON.Feature<GeoJSON.Polygon>);
// Extract rotation from properties if it exists
const rotation = data.properties && isRotationValue(data.properties?.[ROTATION_ATTRIBUTE_NAME])
? data.properties[ROTATION_ATTRIBUTE_NAME] as number
: undefined;
cb();
handler.updateRectBounds(frameNumberRef.value, flickNumberRef.value, bounds);
handler.updateRectBounds(frameNumberRef.value, flickNumberRef.value, bounds, rotation);
} else {
handler.updateGeoJSON(mode, frameNumberRef.value, flickNumberRef.value, data, key, cb);
}
Expand Down
18 changes: 17 additions & 1 deletion client/src/layers/AnnotationLayers/RectangleLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
import geo, { GeoEvent } from 'geojs';

import { cloneDeep } from 'lodash';
import { boundToGeojson } from '../../utils';
import {
boundToGeojson,
applyRotationToPolygon,
getRotationFromAttributes,
hasSignificantRotation,
} from '../../utils';
import BaseLayer, { LayerStyle, BaseLayerParams } from '../BaseLayer';
import { FrameDataTrack } from '../LayerTypes';
import LineLayer from './LineLayer';
Expand All @@ -16,6 +21,7 @@ interface RectGeoJSData{
hasPoly: boolean;
set?: string;
dashed?: boolean;
rotation?: number;
}

export default class RectangleLayer extends BaseLayer<RectGeoJSData> {
Expand Down Expand Up @@ -111,6 +117,15 @@ export default class RectangleLayer extends BaseLayer<RectGeoJSData> {
const filtered = track.features.geometry.features.filter((feature) => feature.geometry && feature.geometry.type === 'Polygon');
hasPoly = filtered.length > 0;
}

// Get rotation from attributes if it exists
const rotation = getRotationFromAttributes(track.features.attributes);

// Apply rotation to polygon if rotation exists
if (hasSignificantRotation(rotation)) {
polygon = applyRotationToPolygon(polygon, track.features.bounds, rotation!);
}

const dashed = !!(track.set && comparisonSets?.includes(track.set));
if (dashed) {
const temp = cloneDeep(polygon);
Expand All @@ -130,6 +145,7 @@ export default class RectangleLayer extends BaseLayer<RectGeoJSData> {
hasPoly,
set: track.set,
dashed,
rotation,
};
arr.push(annotation);
}
Expand Down
1 change: 1 addition & 0 deletions client/src/layers/BaseLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface LayerStyle<D> {
textOpacity?: (data: D) => number;
fontSize?: (data: D) => string | undefined;
offset?: (data: D) => { x: number; y: number };
rotation?: (data: D) => number;
fill?: ObjectFunction<boolean, D> | boolean;
radius?: PointFunction<number, D> | number;
textAlign?: ((data: D) => string) | string;
Expand Down
137 changes: 123 additions & 14 deletions client/src/layers/EditAnnotationLayer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
/*eslint class-methods-use-this: "off"*/
import geo, { GeoEvent } from 'geojs';
import { boundToGeojson, reOrdergeoJSON } from '../utils';
import {
boundToGeojson,
reOrdergeoJSON,
rotatedPolygonToAxisAlignedBbox,
applyRotationToPolygon,
getRotationFromAttributes,
hasSignificantRotation,
ROTATION_ATTRIBUTE_NAME,
} from '../utils';
import { FrameDataTrack } from './LayerTypes';
import BaseLayer, { BaseLayerParams, LayerStyle } from './BaseLayer';

Expand Down Expand Up @@ -219,6 +227,8 @@ export default class EditAnnotationLayer extends BaseLayer<GeoJSON.Feature> {
this.annotator.setCursor(rectVertex[e.handle.handle.index]);
} else if (e.handle.handle.type === 'edge') {
this.annotator.setCursor(rectEdge[e.handle.handle.index]);
} else if (e.handle.handle.type === 'rotate') {
this.annotator.setCursor('grab');
}
} else if (e.handle.handle.type === 'vertex') {
this.annotator.setCursor('grab');
Expand All @@ -229,6 +239,8 @@ export default class EditAnnotationLayer extends BaseLayer<GeoJSON.Feature> {
this.annotator.setCursor('move');
} else if (e.handle.handle.type === 'resize') {
this.annotator.setCursor('nwse-resize');
} else if (e.handle.handle.type === 'rotate') {
this.annotator.setCursor('grab');
}
} else if (this.getMode() !== 'creation') {
this.annotator.setCursor('default');
Expand Down Expand Up @@ -378,6 +390,17 @@ export default class EditAnnotationLayer extends BaseLayer<GeoJSON.Feature> {
let geoJSONData: GeoJSON.Point | GeoJSON.Polygon | GeoJSON.LineString | undefined;
if (this.type === 'rectangle') {
geoJSONData = boundToGeojson(track.features.bounds);

// Restore rotation if it exists
const rotation = getRotationFromAttributes(track.features.attributes);
if (hasSignificantRotation(rotation)) {
// Apply rotation to restore the rotated rectangle for editing
geoJSONData = applyRotationToPolygon(
geoJSONData,
track.features.bounds,
rotation || 0,
);
}
} else {
// TODO: this assumes only one polygon
geoJSONData = this.getGeoJSONData(track);
Expand All @@ -390,6 +413,10 @@ export default class EditAnnotationLayer extends BaseLayer<GeoJSON.Feature> {
geometry: geoJSONData,
properties: {
annotationType: typeMapper.get(this.type),
// Preserve rotation in properties
...(getRotationFromAttributes(track.features.attributes) !== undefined
? { [ROTATION_ATTRIBUTE_NAME]: getRotationFromAttributes(track.features.attributes) }
: {}),
},
};

Expand Down Expand Up @@ -429,9 +456,27 @@ export default class EditAnnotationLayer extends BaseLayer<GeoJSON.Feature> {
if (e.annotation.state() === 'done' && this.getMode() === 'creation') {
const geoJSONData = [e.annotation.geojson()];
if (this.type === 'rectangle') {
geoJSONData[0].geometry.coordinates[0] = reOrdergeoJSON(
geoJSONData[0].geometry.coordinates[0] as GeoJSON.Position[],
);
const coords = geoJSONData[0].geometry.coordinates[0] as GeoJSON.Position[];
const rotationInfo = rotatedPolygonToAxisAlignedBbox(coords);
const isRotated = hasSignificantRotation(rotationInfo.rotation);

// Convert to axis-aligned for storage
const axisAlignedCoords = [
[rotationInfo.bounds[0], rotationInfo.bounds[3]],
[rotationInfo.bounds[0], rotationInfo.bounds[1]],
[rotationInfo.bounds[2], rotationInfo.bounds[1]],
[rotationInfo.bounds[2], rotationInfo.bounds[3]],
[rotationInfo.bounds[0], rotationInfo.bounds[3]],
];
geoJSONData[0].geometry.coordinates[0] = axisAlignedCoords;

// Store rotation if significant
if (isRotated) {
geoJSONData[0].properties = {
...geoJSONData[0].properties,
[ROTATION_ATTRIBUTE_NAME]: rotationInfo.rotation,
};
}
}
this.formattedData = geoJSONData;
// The new annotation is in a state without styling, so apply local stypes
Expand Down Expand Up @@ -466,31 +511,95 @@ export default class EditAnnotationLayer extends BaseLayer<GeoJSON.Feature> {
);
if (this.formattedData.length > 0) {
if (this.type === 'rectangle') {
/* Updating the corners for the proper cursor icons
Also allows for regrabbing of the handle */
newGeojson.geometry.coordinates[0] = reOrdergeoJSON(
newGeojson.geometry.coordinates[0] as GeoJSON.Position[],
);
const coords = newGeojson.geometry.coordinates[0] as GeoJSON.Position[];

// Calculate rotation info once and reuse
const rotationInfo = rotatedPolygonToAxisAlignedBbox(coords);
const isRotated = hasSignificantRotation(rotationInfo.rotation);

// During editing, keep the rotated polygon as-is so corners don't snap
// Only reorder if not rotated
if (!isRotated) {
// No rotation, use reordered coordinates
newGeojson.geometry.coordinates[0] = reOrdergeoJSON(coords);
}
// If rotated, keep the rotated coordinates for editing

// The corners need to update for the indexes to update
// coordinates are in a different system than display
const coords = newGeojson.geometry.coordinates[0].map(
(coord) => ({ x: coord[0], y: coord[1] }),
// Use the actual coordinates (rotated or not) to set corners correctly
const currentCoords = newGeojson.geometry.coordinates[0] as GeoJSON.Position[];
const cornerCoords = currentCoords.map(
(coord: GeoJSON.Position) => ({ x: coord[0], y: coord[1] }),
);
// only use the 4 coords instead of 5
const remapped = this.annotator.geoViewerRef.value.worldToGcs(coords.splice(0, 4));
const remapped = this.annotator.geoViewerRef.value.worldToGcs(cornerCoords.splice(0, 4));
e.annotation.options('corners', remapped);
//This will retrigger highlighting of the current handle after releasing the mouse
setTimeout(() => this.annotator.geoViewerRef
.value.interactor().retriggerMouseMove(), 0);

// For rectangles, convert to axis-aligned bounds with rotation when saving
if (isRotated) {
// Convert to axis-aligned for storage, but keep rotation in properties
const axisAlignedCoords = [
[rotationInfo.bounds[0], rotationInfo.bounds[3]],
[rotationInfo.bounds[0], rotationInfo.bounds[1]],
[rotationInfo.bounds[2], rotationInfo.bounds[1]],
[rotationInfo.bounds[2], rotationInfo.bounds[3]],
[rotationInfo.bounds[0], rotationInfo.bounds[3]],
];
newGeojson.geometry.coordinates[0] = axisAlignedCoords;
newGeojson.properties = {
...newGeojson.properties,
[ROTATION_ATTRIBUTE_NAME]: rotationInfo.rotation,
};
} else {
// No rotation, ensure coordinates are properly ordered
newGeojson.geometry.coordinates[0] = reOrdergeoJSON(coords);
}
}

// update existing feature
this.formattedData[0].geometry = newGeojson.geometry;
if (newGeojson.properties) {
this.formattedData[0].properties = {
...this.formattedData[0].properties,
...newGeojson.properties,
};
}
} else {
// create new feature
// For rectangles, convert to axis-aligned bounds with rotation when saving
if (this.type === 'rectangle') {
const coords = newGeojson.geometry.coordinates[0] as GeoJSON.Position[];
const rotationInfo = rotatedPolygonToAxisAlignedBbox(coords);
const isRotated = hasSignificantRotation(rotationInfo.rotation);

if (isRotated) {
// Convert to axis-aligned for storage, but keep rotation in properties
const axisAlignedCoords = [
[rotationInfo.bounds[0], rotationInfo.bounds[3]],
[rotationInfo.bounds[0], rotationInfo.bounds[1]],
[rotationInfo.bounds[2], rotationInfo.bounds[1]],
[rotationInfo.bounds[2], rotationInfo.bounds[3]],
[rotationInfo.bounds[0], rotationInfo.bounds[3]],
];
newGeojson.geometry.coordinates[0] = axisAlignedCoords;
newGeojson.properties = {
...(newGeojson.properties || {}),
[ROTATION_ATTRIBUTE_NAME]: rotationInfo.rotation,
};
} else {
// No rotation, ensure coordinates are properly ordered
newGeojson.geometry.coordinates[0] = reOrdergeoJSON(coords);
}
}

this.formattedData = [{
...newGeojson,
properties: {
annotationType: this.type,
...(newGeojson.properties || {}),
},
type: 'Feature',
}];
Expand Down Expand Up @@ -552,7 +661,7 @@ export default class EditAnnotationLayer extends BaseLayer<GeoJSON.Feature> {
if (this.type === 'rectangle') {
return {
handles: {
rotate: false,
rotate: true,
},
};
}
Expand Down
1 change: 1 addition & 0 deletions client/src/provides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export interface Handler {
frameNum: number,
flickNum: number,
bounds: RectBounds,
rotation?: number,
): void;
/* update geojson for track */
updateGeoJSON(
Expand Down
Loading
Loading