diff --git a/client/src/components/SpectrogramViewer.vue b/client/src/components/SpectrogramViewer.vue index 6e9b22c..562852e 100644 --- a/client/src/components/SpectrogramViewer.vue +++ b/client/src/components/SpectrogramViewer.vue @@ -32,10 +32,6 @@ export default defineComponent({ type: String as PropType, required: true, }, - grid: { - type: Boolean, - default: false, - }, }, emits: ["update:annotation", "create:annotation", "selected", "geoViewerRef", "hoverData", 'set-mode',], setup(props, { emit }) { @@ -198,7 +194,6 @@ export default defineComponent({ :spectro-info="spectroInfo" :annotations="annotations" :selected-id="selectedId" - :grid="grid" @selected="clickSelected($event)" @update:annotation="updateAnnotation($event)" @create:annotation="createAnnotation($event)" diff --git a/client/src/components/geoJS/LayerManager.vue b/client/src/components/geoJS/LayerManager.vue index a65a5ae..54ac52b 100644 --- a/client/src/components/geoJS/LayerManager.vue +++ b/client/src/components/geoJS/LayerManager.vue @@ -5,6 +5,9 @@ import { geojsonToSpectro, SpectroInfo } from "./geoJSUtils"; import EditAnnotationLayer from "./layers/editAnnotationLayer"; import RectangleLayer from "./layers/rectangleLayer"; import LegendLayer from "./layers/legendLayer"; +import TimeLayer from "./layers/timeLayer"; +import FreqLayer from "./layers/freqLayer"; +import SpeciesLayer from "./layers/speciesLayer"; import { cloneDeep } from "lodash"; import useState from "../../use/useState"; export default defineComponent({ @@ -31,14 +34,10 @@ export default defineComponent({ type: Boolean, default: false, }, - grid: { - type: Boolean, - default: false, - } }, emits: ['selected', 'update:annotation', 'create:annotation', 'set-cursor', 'set-mode'], setup(props, { emit }) { - const { annotationState, setAnnotationState } = useState(); + const { annotationState, setAnnotationState, layerVisibility, } = useState(); const selectedAnnotationId: Ref = ref(null); const hoveredAnnotationId: Ref = ref(null); const localAnnotations: Ref = ref(cloneDeep(props.annotations)); @@ -47,6 +46,9 @@ export default defineComponent({ let rectAnnotationLayer: RectangleLayer; let editAnnotationLayer: EditAnnotationLayer; let legendLayer: LegendLayer; + let timeLayer: TimeLayer; + let freqLayer: FreqLayer; + let speciesLayer: SpeciesLayer; const displayError = ref(false); const errorMsg = ref(''); @@ -180,7 +182,30 @@ export default defineComponent({ rectAnnotationLayer.redraw(); } if (!props.thumbnail) { + if (layerVisibility.value.includes('grid')) { + legendLayer.setGridEnabled(true); + } else { + legendLayer.setGridEnabled(false); + } legendLayer.redraw(); + if (layerVisibility.value.includes('time')) { + timeLayer.formatData(localAnnotations.value); + timeLayer.redraw(); + } else { + timeLayer.disable(); + } + if (layerVisibility.value.includes('freq')) { + freqLayer.formatData(localAnnotations.value); + freqLayer.redraw(); + } else { + freqLayer.disable(); + } + if (layerVisibility.value.includes('species')) { + speciesLayer.formatData(localAnnotations.value); + speciesLayer.redraw(); + } else { + speciesLayer.disable(); + } } if (editing.value && editingAnnotation.value) { setTimeout(() => { @@ -201,6 +226,7 @@ export default defineComponent({ nextTick(() => { if (editAnnotationLayer && editAnnotationLayer.getMode() === 'disabled' && props.selectedId === null) { emit('set-mode', 'disabled'); + editAnnotationLayer.featureLayer.clear(); } }); @@ -218,13 +244,23 @@ export default defineComponent({ rectAnnotationLayer.redraw(); if (!props.thumbnail) { legendLayer = new LegendLayer(props.geoViewerRef, event, props.spectroInfo); + timeLayer = new TimeLayer(props.geoViewerRef, event, props.spectroInfo); + timeLayer.formatData(localAnnotations.value); + freqLayer = new FreqLayer(props.geoViewerRef, event, props.spectroInfo); + freqLayer.formatData(localAnnotations.value); + speciesLayer = new SpeciesLayer(props.geoViewerRef, event, props.spectroInfo); + speciesLayer.formatData(localAnnotations.value); + + legendLayer.redraw(); + timeLayer.disable(); + freqLayer.disable(); + speciesLayer.disable(); } } }); - watch(() => props.grid, () => { + watch(layerVisibility, () => { if (!props.thumbnail && legendLayer) { - legendLayer.setGridEnabled(props.grid); triggerUpdate(); } }); @@ -272,5 +308,5 @@ export default defineComponent({ -
+./layers/timeLalyer \ No newline at end of file diff --git a/client/src/components/geoJS/layers/editAnnotationLayer.ts b/client/src/components/geoJS/layers/editAnnotationLayer.ts index 6b20e02..18a7863 100644 --- a/client/src/components/geoJS/layers/editAnnotationLayer.ts +++ b/client/src/components/geoJS/layers/editAnnotationLayer.ts @@ -303,7 +303,6 @@ export default class EditAnnotationLayer { this.skipNextExternalUpdate = false; this.featureLayer.removeAllAnnotations(false, true); this.setMode(null); - this.featureLayer.clear(); this.shapeInProgress = null; if (this.selectedHandleIndex !== -1) { this.selectedHandleIndex = -1; diff --git a/client/src/components/geoJS/layers/freqLayer.ts b/client/src/components/geoJS/layers/freqLayer.ts new file mode 100644 index 0000000..48f92ba --- /dev/null +++ b/client/src/components/geoJS/layers/freqLayer.ts @@ -0,0 +1,192 @@ +/* eslint-disable class-methods-use-this */ +import { SpectrogramAnnotation } from "../../../api/api"; +import { SpectroInfo, spectroToGeoJSon } from "../geoJSUtils"; +import { LayerStyle } from "./types"; + +interface LineData { + line: GeoJSON.LineString; + thicker?: boolean; + grid?: boolean; +} + +interface TextData { + text: string; + x: number; + y: number; + offsetY?: number; + offsetX?: number; +} + +export default class FreqLayer { + lineData: LineData[]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + lineLayer: any; + + textData: TextData[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + textLayer: any; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + geoViewerRef: any; + + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + event: (name: string, data: any) => void; + + spectroInfo: SpectroInfo; + + textStyle: LayerStyle; + lineStyle: LayerStyle; + + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + geoViewerRef: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + event: (name: string, data: any) => void, + spectroInfo: SpectroInfo + ) { + this.geoViewerRef = geoViewerRef; + this.lineData = []; + this.spectroInfo = spectroInfo; + this.textData = []; + this.event = event; + //Only initialize once, prevents recreating Layer each edit + const layer = this.geoViewerRef.createLayer("feature", { + features: ["text", "line"], + }); + this.textLayer = layer + .createFeature("text") + .text((data: TextData) => data.text) + .position((data: TextData) => ({ x: data.x, y: data.y })); + + this.lineLayer = layer.createFeature("line"); + + this.textStyle = this.createTextStyle(); + this.lineStyle = this.createLineStyle(); + } + + + formatData(annotationData: SpectrogramAnnotation[]) { + this.textData = []; + this.lineData = []; + const lineDist = 16; + annotationData.forEach((annotation: SpectrogramAnnotation) => { + const polygon = spectroToGeoJSon(annotation, this.spectroInfo); + const {low_freq, high_freq } = annotation; + const [xmin, ymin] = polygon.coordinates[0][0]; + const [xmax, ymax] = polygon.coordinates[0][2]; + // For the compressed view we need to filter out default or NaN numbers + if (Number.isNaN(xmax) || Number.isNaN(xmin) || Number.isNaN(ymax) || Number.isNaN(ymin)) { + return; + } + if (xmax === -1 && ymin === -1 && ymax === -1 && xmin === -1) { + return; + } + // We create two small lines for the beginning/end of annotation + this.lineData.push({ + line: { + type: "LineString", + coordinates: [ + [xmax, ymin], + [xmax + lineDist, ymin], + ], + }, + thicker: true, + }); + this.lineData.push({ + line: { + type: "LineString", + coordinates: [ + [xmax, ymax], + [xmax + lineDist, ymax], + ], + }, + thicker: true, + }); + // Now we need to create the text Labels + this.textData.push({ + text: `${(low_freq/1000).toFixed(1)}KHz`, + x: xmax + lineDist, + y: ymin , + offsetX: 0, + offsetY: 0, + }); + this.textData.push({ + text: `${(high_freq/1000).toFixed(1)}KHz`, + x: xmax + lineDist, + y: ymax, + offsetX: 0, + offsetY: 0, + }); + }); + } + + redraw() { + // add some styles + this.lineLayer + .data(this.lineData) + .line((d: LineData) => d.line.coordinates) + .style(this.createLineStyle()) + .draw(); + this.textLayer.data(this.textData).style(this.createTextStyle()).draw(); + } + + disable() { + this.lineLayer.data([]).draw(); + this.textLayer.data([]).draw(); + } + + + createLineStyle(): LayerStyle { + return { + ...{ + strokeColor: "#00FFFF", + strokeWidth: 2.0, + antialiasing: 0, + stroke: true, + uniformPolygon: true, + fill: false, + }, + strokeOpacity: (_point, _index, data) => { + // Reduce the rectangle opacity if a polygon is also drawn + if (data.grid) { + return 0.5; + } + return 1.0; + }, + + strokeWidth: (_point, _index, data) => { + if (data.thicker) { + return 4.0; + } + if (data.grid) { + return 1.0; + } + return 2.0; + }, + }; + } + createTextStyle(): LayerStyle { + return { + ...{ + strokeColor: "yellow", + strokeWidth: 2.0, + antialiasing: 0, + stroke: true, + uniformPolygon: true, + fill: false, + }, + color: () => { + return "white"; + }, + offset: (data) => ({ + x: data.offsetX || 0, + y: data.offsetY || 0, + }), + textAlign: 'starts', + }; + } +} diff --git a/client/src/components/geoJS/layers/rectangleLayer.ts b/client/src/components/geoJS/layers/rectangleLayer.ts index 81c6c4e..b6a2ff7 100644 --- a/client/src/components/geoJS/layers/rectangleLayer.ts +++ b/client/src/components/geoJS/layers/rectangleLayer.ts @@ -123,8 +123,8 @@ export default class RectangleLayer { } if (xmax === -1 && ymin === -1 && ymax === -1 && xmin === -1) { return; - } - const newAnnotation: RectGeoJSData = { + } + const newAnnotation: RectGeoJSData = { id: annotation.id, selected: annotation.id === selectedIndex, editing: annotation.editing, diff --git a/client/src/components/geoJS/layers/speciesLayer.ts b/client/src/components/geoJS/layers/speciesLayer.ts new file mode 100644 index 0000000..6575680 --- /dev/null +++ b/client/src/components/geoJS/layers/speciesLayer.ts @@ -0,0 +1,120 @@ +/* eslint-disable class-methods-use-this */ +import { SpectrogramAnnotation } from "../../../api/api"; +import { SpectroInfo, spectroToGeoJSon } from "../geoJSUtils"; +import { LayerStyle } from "./types"; + +interface TextData { + text: string; + x: number; + y: number; + offsetY?: number; + offsetX?: number; +} + +export default class SpeciesLayer { + + textData: TextData[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + textLayer: any; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + geoViewerRef: any; + + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + event: (name: string, data: any) => void; + + spectroInfo: SpectroInfo; + + textStyle: LayerStyle; + + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + geoViewerRef: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + event: (name: string, data: any) => void, + spectroInfo: SpectroInfo + ) { + this.geoViewerRef = geoViewerRef; + this.spectroInfo = spectroInfo; + this.textData = []; + this.event = event; + //Only initialize once, prevents recreating Layer each edit + const layer = this.geoViewerRef.createLayer("feature", { + features: ["text"], + }); + this.textLayer = layer + .createFeature("text") + .text((data: TextData) => data.text) + .position((data: TextData) => ({ x: data.x, y: data.y })); + + + this.textStyle = this.createTextStyle(); + } + + + formatData(annotationData: SpectrogramAnnotation[]) { + this.textData = []; + annotationData.forEach((annotation: SpectrogramAnnotation) => { + const polygon = spectroToGeoJSon(annotation, this.spectroInfo); + const [xmin, ymin] = polygon.coordinates[0][0]; + const [xmax, ymax] = polygon.coordinates[0][2]; + // For the compressed view we need to filter out default or NaN numbers + if (Number.isNaN(xmax) || Number.isNaN(xmin) || Number.isNaN(ymax) || Number.isNaN(ymin)) { + return; + } + if (xmax === -1 && ymin === -1 && ymax === -1 && xmin === -1) { + return; + } + let textOffset = 0; + const species = annotation.species; + if (species) { + for (let i =0; i< species.length; i += 1) { + const specie = species[i]; + this.textData.push({ + text: `${specie.common_name}`, + x: xmin + (xmax-xmin) /2.0, + y: ymax , + offsetX:0, + offsetY: -5 + textOffset, + }); + textOffset -= 15; + + } + } + }); + } + + redraw() { + // add some styles + this.textLayer.data(this.textData).style(this.createTextStyle()).draw(); + } + + disable() { + this.textLayer.data([]).draw(); + } + + + createTextStyle(): LayerStyle { + return { + ...{ + strokeColor: "yellow", + strokeWidth: 2.0, + antialiasing: 0, + stroke: true, + uniformPolygon: true, + fill: false, + }, + color: () => { + return "white"; + }, + offset: (data) => ({ + x: data.offsetX || 0, + y: data.offsetY || 0, + }), + textAlign: 'center', + }; + } +} diff --git a/client/src/components/geoJS/layers/timeLayer.ts b/client/src/components/geoJS/layers/timeLayer.ts new file mode 100644 index 0000000..80edb2c --- /dev/null +++ b/client/src/components/geoJS/layers/timeLayer.ts @@ -0,0 +1,191 @@ +/* eslint-disable class-methods-use-this */ +import { SpectrogramAnnotation } from "../../../api/api"; +import { SpectroInfo, spectroToGeoJSon } from "../geoJSUtils"; +import { LayerStyle } from "./types"; + +interface LineData { + line: GeoJSON.LineString; + thicker?: boolean; + grid?: boolean; +} + +interface TextData { + text: string; + x: number; + y: number; + offsetY?: number; + offsetX?: number; +} + +export default class TimeLayer { + lineData: LineData[]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + lineLayer: any; + + textData: TextData[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + textLayer: any; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + geoViewerRef: any; + + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + event: (name: string, data: any) => void; + + spectroInfo: SpectroInfo; + + textStyle: LayerStyle; + lineStyle: LayerStyle; + + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + geoViewerRef: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + event: (name: string, data: any) => void, + spectroInfo: SpectroInfo + ) { + this.geoViewerRef = geoViewerRef; + this.lineData = []; + this.spectroInfo = spectroInfo; + this.textData = []; + this.event = event; + //Only initialize once, prevents recreating Layer each edit + const layer = this.geoViewerRef.createLayer("feature", { + features: ["text", "line"], + }); + this.textLayer = layer + .createFeature("text") + .text((data: TextData) => data.text) + .position((data: TextData) => ({ x: data.x, y: data.y })); + + this.lineLayer = layer.createFeature("line"); + + this.textStyle = this.createTextStyle(); + this.lineStyle = this.createLineStyle(); + } + + + formatData(annotationData: SpectrogramAnnotation[]) { + this.textData = []; + this.lineData = []; + const lineDist = 12; + annotationData.forEach((annotation: SpectrogramAnnotation) => { + const polygon = spectroToGeoJSon(annotation, this.spectroInfo); + const {start_time, end_time } = annotation; + const [xmin, ymin] = polygon.coordinates[0][0]; + const [xmax, ymax] = polygon.coordinates[0][2]; + // For the compressed view we need to filter out default or NaN numbers + if (Number.isNaN(xmax) || Number.isNaN(xmin) || Number.isNaN(ymax) || Number.isNaN(ymin)) { + return; + } + if (xmax === -1 && ymin === -1 && ymax === -1 && xmin === -1) { + return; + } + // We create two small lines for the beginning/end of annotation + this.lineData.push({ + line: { + type: "LineString", + coordinates: [ + [xmin, ymin], + [xmin, ymin + lineDist], + ], + }, + thicker: true, + }); + this.lineData.push({ + line: { + type: "LineString", + coordinates: [ + [xmax, ymin], + [xmax, ymin + lineDist], + ], + }, + thicker: true, + }); + // Now we need to create the text Labels + this.textData.push({ + text: `${start_time}ms`, + x: xmin, + y: ymin + lineDist, + offsetX: 0, + offsetY: 5, + }); + this.textData.push({ + text: `${end_time}ms`, + x: xmax, + y: ymin + lineDist, + offsetX: 0, + offsetY: 5, + }); + }); + } + + redraw() { + // add some styles + this.lineLayer + .data(this.lineData) + .line((d: LineData) => d.line.coordinates) + .style(this.createLineStyle()) + .draw(); + this.textLayer.data(this.textData).style(this.createTextStyle()).draw(); + } + + disable() { + this.lineLayer.data([]).draw(); + this.textLayer.data([]).draw(); + } + + + createLineStyle(): LayerStyle { + return { + ...{ + strokeColor: "#00FFFF", + strokeWidth: 2.0, + antialiasing: 0, + stroke: true, + uniformPolygon: true, + fill: false, + }, + strokeOpacity: (_point, _index, data) => { + // Reduce the rectangle opacity if a polygon is also drawn + if (data.grid) { + return 0.5; + } + return 1.0; + }, + + strokeWidth: (_point, _index, data) => { + if (data.thicker) { + return 4.0; + } + if (data.grid) { + return 1.0; + } + return 2.0; + }, + }; + } + createTextStyle(): LayerStyle { + return { + ...{ + strokeColor: "yellow", + strokeWidth: 2.0, + antialiasing: 0, + stroke: true, + uniformPolygon: true, + fill: false, + }, + color: () => { + return "white"; + }, + offset: (data) => ({ + x: data.offsetX || 0, + y: data.offsetY || 0, + }), + }; + } +} diff --git a/client/src/use/useState.ts b/client/src/use/useState.ts index ac9ca6d..698a91c 100644 --- a/client/src/use/useState.ts +++ b/client/src/use/useState.ts @@ -1,15 +1,32 @@ import { ref, Ref } from 'vue'; +import { cloneDeep } from 'lodash'; const annotationState: Ref = ref(''); +type LayersVis = 'time' | 'freq' | 'species' |'grid'; +const layerVisibility: Ref = ref([]); type AnnotationState = '' | 'editing' | 'creating'; export default function useState() { const setAnnotationState = (state: AnnotationState) => { annotationState.value = state; }; - return { + function toggleLayerVisibility(value: LayersVis) { + const index = layerVisibility.value.indexOf(value); + const clone = cloneDeep(layerVisibility.value); + if (index === -1) { + // If the value is not present, add it + clone.push(value); + } else { + // If the value is present, remove it + clone.splice(index, 1); + } + layerVisibility.value = clone; + } + return { annotationState, setAnnotationState, + toggleLayerVisibility, + layerVisibility, }; } diff --git a/client/src/views/Spectrogram.vue b/client/src/views/Spectrogram.vue index f1685b0..bc72ccc 100644 --- a/client/src/views/Spectrogram.vue +++ b/client/src/views/Spectrogram.vue @@ -14,7 +14,7 @@ import AnnotationList from "../components/AnnotationList.vue"; import AnnotationEditor from "../components/AnnotationEditor.vue"; import ThumbnailViewer from "../components/ThumbnailViewer.vue"; import { watch } from "vue"; - +import useState from "../use/useState"; export default defineComponent({ name: "Spectrogram", components: { @@ -30,6 +30,7 @@ export default defineComponent({ }, }, setup(props) { + const { toggleLayerVisibility, layerVisibility } = useState(); const image: Ref = ref(new Image()); const spectroInfo: Ref = ref(); const annotations: Ref = ref([]); @@ -89,6 +90,9 @@ export default defineComponent({ } return null; }); + watch(gridEnabled, () => { + toggleLayerVisibility('grid'); + }); onMounted(() => loadData()); // eslint-disable-next-line @typescript-eslint/no-explicit-any const parentGeoViewerRef: Ref = ref(null); @@ -99,7 +103,7 @@ export default defineComponent({ const timeRef = ref(0); const freqRef = ref(0); - const setHoverData = ({time, freq}: {time: number, freq: number}) => { + const setHoverData = ({ time, freq }: { time: number; freq: number }) => { timeRef.value = time; freqRef.value = freq; }; @@ -134,10 +138,12 @@ export default defineComponent({ setParentGeoViewer, setHoverData, setMode, + toggleLayerVisibility, speciesList, selectedAnnotation, parentGeoViewerRef, gridEnabled, + layerVisibility, timeRef, freqRef, mode, @@ -170,6 +176,48 @@ export default defineComponent({ {{ mode }} + + + Turn Species Label On/Off + + + + Turn Time Label On/Off + + + + Turn Time Label On/Off +