From baf091639b53e49574358ab464a76e7e690b15ac Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Thu, 25 Jan 2024 12:03:22 -0500 Subject: [PATCH 1/9] adding legend capabilities --- client/src/components/SpectrogramViewer.vue | 6 + client/src/components/ThumbnailViewer.vue | 1 + client/src/components/geoJS/LayerManager.vue | 23 ++ client/src/components/geoJS/geoJSUtils.ts | 7 +- .../components/geoJS/layers/legendLayer.ts | 313 ++++++++++++++++++ client/src/views/Spectrogram.vue | 11 +- 6 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 client/src/components/geoJS/layers/legendLayer.ts diff --git a/client/src/components/SpectrogramViewer.vue b/client/src/components/SpectrogramViewer.vue index a9e54d7..a4e867c 100644 --- a/client/src/components/SpectrogramViewer.vue +++ b/client/src/components/SpectrogramViewer.vue @@ -29,6 +29,10 @@ export default defineComponent({ recordingId: { type: String as PropType, required: true, + }, + grid: { + type: Boolean, + default: true, } }, emits: ['update:annotation', 'create:annotation', 'selected', 'geoViewerRef'], @@ -63,6 +67,7 @@ export default defineComponent({ }; + return { containerRef, geoViewerRef: geoJS.getGeoViewer(), @@ -87,6 +92,7 @@ export default defineComponent({ :spectro-info="spectroInfo" :annotations="annotations" :selected-id="selectedId" + :grid="grid" @selected="$emit('selected',$event)" @update:annotation="updateAnnotation($event)" @create:annotation="createAnnotation($event)" diff --git a/client/src/components/ThumbnailViewer.vue b/client/src/components/ThumbnailViewer.vue index eb54e84..3734033 100644 --- a/client/src/components/ThumbnailViewer.vue +++ b/client/src/components/ThumbnailViewer.vue @@ -152,6 +152,7 @@ export default defineComponent({ :spectro-info="spectroInfo" :annotations="annotations" :selected-id="selectedId" + thumbnail @selected="$emit('selected',$event)" /> diff --git a/client/src/components/geoJS/LayerManager.vue b/client/src/components/geoJS/LayerManager.vue index bd746ae..498826e 100644 --- a/client/src/components/geoJS/LayerManager.vue +++ b/client/src/components/geoJS/LayerManager.vue @@ -4,6 +4,7 @@ import { SpectrogramAnnotation } from "../../api/api"; import { geojsonToSpectro, SpectroInfo } from "./geoJSUtils"; import EditAnnotationLayer from "./layers/editAnnotationLayer"; import RectangleLayer from "./layers/rectangleLayer"; +import LegendLayer from "./layers/legendLayer"; import { cloneDeep } from "lodash"; import useState from "../../use/useState"; export default defineComponent({ @@ -26,6 +27,14 @@ export default defineComponent({ type: Number as PropType, default: null, }, + thumbnail: { + type: Boolean, + default: false, + }, + grid: { + type: Boolean, + default: true, + } }, emits: ['selected', 'update:annotation', 'create:annotation'], setup(props, { emit }) { @@ -37,6 +46,7 @@ export default defineComponent({ const editingAnnotation: Ref = ref(null); let rectAnnotationLayer: RectangleLayer; let editAnnotationLayer: EditAnnotationLayer; + let legendLayer: LegendLayer; const displayError = ref(false); const errorMsg = ref(''); // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any @@ -161,6 +171,9 @@ export default defineComponent({ rectAnnotationLayer.formatData(localAnnotations.value, selectedAnnotationId.value); rectAnnotationLayer.redraw(); } + if (!props.thumbnail) { + legendLayer.redraw(); + } if (editing.value && editingAnnotation.value) { setTimeout(() => { editAnnotationLayer.changeData(editingAnnotation.value); @@ -185,6 +198,16 @@ export default defineComponent({ editAnnotationLayer = new EditAnnotationLayer(props.geoViewerRef, event, props.spectroInfo); rectAnnotationLayer.formatData(localAnnotations.value, selectedAnnotationId.value); rectAnnotationLayer.redraw(); + if (!props.thumbnail) { + legendLayer = new LegendLayer(props.geoViewerRef, event, props.spectroInfo); + legendLayer.redraw(); + } + } + }); + watch(() => props.grid, () => { + if (!props.thumbnail && legendLayer) { + legendLayer.setGridEnabled(props.grid); + triggerUpdate(); } }); watch( diff --git a/client/src/components/geoJS/geoJSUtils.ts b/client/src/components/geoJS/geoJSUtils.ts index e619d24..a83742b 100644 --- a/client/src/components/geoJS/geoJSUtils.ts +++ b/client/src/components/geoJS/geoJSUtils.ts @@ -118,7 +118,12 @@ const useGeoJS = () => { const { width: mapWidth } = geoViewer.value.camera().viewport; const bounds = !thumbnail.value - ? { left: 0, top: 0, right: mapWidth, bottom: originalBounds.bottom } + ? { + left: -125, // Making sure the legend is on the screen + top: 0, + right: mapWidth, + bottom: originalBounds.bottom, + } : originalBounds; const zoomAndCenter = geoViewer.value.zoomAndCenterFromBounds(bounds, 0); geoViewer.value.zoom(zoomAndCenter.zoom); diff --git a/client/src/components/geoJS/layers/legendLayer.ts b/client/src/components/geoJS/layers/legendLayer.ts new file mode 100644 index 0000000..76d918e --- /dev/null +++ b/client/src/components/geoJS/layers/legendLayer.ts @@ -0,0 +1,313 @@ +/* eslint-disable class-methods-use-this */ +import { SpectroInfo } 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 LegendLayer { + 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; + + gridLines: LineData[]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + event: (name: string, data: any) => void; + + spectroInfo: SpectroInfo; + + textStyle: LayerStyle; + lineStyle: LayerStyle; + + axisBuffer: number; + + gridEnabled: boolean; + + // 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.axisBuffer = 5; + this.event = event; + this.gridEnabled = true; + //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(); + this.gridLines = []; + this.createLabels(); + + } + + drawXAxisLabels() { + // Now we need tick marks for ms along the X-axis + const time = this.spectroInfo.end_time - this.spectroInfo.start_time; + const timeToPixels = this.spectroInfo.width / time; + + // every 100 ms a small tick and every 1000 a big tick + for (let i = 0; i < time; i += 10) { + const length = i % 1000 === 0 ? this.axisBuffer * 8 : this.axisBuffer * 4; + if (i % 50 === 0) { + this.lineData.push({ + line: { + type: "LineString", + coordinates: [ + [i * timeToPixels, this.spectroInfo.height + this.axisBuffer], + [i * timeToPixels, this.spectroInfo.height + length], + ], + }, + thicker: i % 1000 === 0, + }); + this.textData.push({ + text: `${i}ms`, + x: i * timeToPixels, + y: this.spectroInfo.height + length, + offsetX: 3, + offsetY: 8, + }); + } + this.gridLines.push({ + line: { + type: "LineString", + coordinates: [ + [i * timeToPixels, 0], + [i * timeToPixels, this.spectroInfo.height + length], + ], + }, + grid: true, + }); + } + } + + drawXAxisLabelsCompressed() { + // For compressed we need to draw based on the start/endTimes instead of the standard + const time = this.spectroInfo.end_time - this.spectroInfo.start_time; + const timeToPixels = this.spectroInfo.width / time; + + const { start_times, end_times } = this.spectroInfo; + if (start_times && end_times) { + // We need a pixel time to map to the 0 position + let pixelOffset = 0; + for (let i =0; i< start_times.length; i+= 1) { + const length = this.axisBuffer * 8; + const start_time = start_times[i]; + const end_time = end_times[i]; + this.lineData.push({ + line: { + type: "LineString", + coordinates: [ + [(0 + pixelOffset), this.spectroInfo.height + this.axisBuffer], + [(0 * timeToPixels) + pixelOffset, this.spectroInfo.height + length], + ], + }, + thicker:true, + }); + this.lineData.push({ + line: { + type: "LineString", + coordinates: [ + [((end_time - start_time) * timeToPixels) + pixelOffset, this.spectroInfo.height + this.axisBuffer], + [((end_time - start_time) * timeToPixels) + pixelOffset, this.spectroInfo.height + length], + ], + }, + thicker:true, + }); + this.lineData.push({ + line: { + type: "LineString", + coordinates: [ + [((end_time - start_time) * timeToPixels) + pixelOffset, this.spectroInfo.height + this.axisBuffer], + [((end_time - start_time) * timeToPixels) + pixelOffset, 0], + ], + }, + grid:true, + }); + + // Need to decide what text to add to the label + // this.textData.push({ + // text: `${start_time}ms`, + // x: (0 + pixelOffset), + // y: this.spectroInfo.height + length, + // offsetX: 3, + // offsetY: i > 0 ? 24: 8, + // }); + // this.textData.push({ + // text: `${end_time}ms`, + // x: ((start_time - end_time) * timeToPixels) + pixelOffset, + // y: this.spectroInfo.height + length, + // offsetX: 3, + // offsetY: i != start_times.length -1 ? 8 : 24, + // }); + // Need to add the current + pixelOffset += (end_time - start_time) * timeToPixels; + } + } + + } + + createLabels() { + // Take spectro Info and create lines for X/Y axis + const xAxis: GeoJSON.LineString = { + type: "LineString", + coordinates: [ + [0 - this.axisBuffer, this.spectroInfo.height + this.axisBuffer], + [this.spectroInfo.width, this.spectroInfo.height + this.axisBuffer], + ], + }; + const yAxis: GeoJSON.LineString = { + type: "LineString", + coordinates: [ + [0 - this.axisBuffer, 0], + [0 - this.axisBuffer, this.spectroInfo.height + this.axisBuffer], + ], + }; + this.lineData.push({ line: xAxis }); + this.lineData.push({ line: yAxis }); + + // Lets do the vertical Hz axis now + const hz = this.spectroInfo.high_freq - this.spectroInfo.low_freq; + const hzToPixels = this.spectroInfo.height / hz; + for (let i = 0; i < hz; i += 10000) { + const length = i % 10000 === 0 ? this.axisBuffer * 8 : this.axisBuffer * 4; + this.lineData.push({ + line: { + type: "LineString", + coordinates: [ + [0 - this.axisBuffer, this.spectroInfo.height - i * hzToPixels], + [0 - this.axisBuffer - length, this.spectroInfo.height - i * hzToPixels], + ], + }, + thicker: i % 10000 === 0, + }); + this.textData.push({ + text: `${i / 1000}KHz`, + x: 0 - this.axisBuffer - length, + y: this.spectroInfo.height - i * hzToPixels, + offsetX: -25, + offsetY: 0, + }); + this.gridLines.push({ + line: { + type: "LineString", + coordinates: [ + [0 - this.axisBuffer - length, this.spectroInfo.height - i * hzToPixels], + [this.spectroInfo.width, this.spectroInfo.height - i * hzToPixels], + ], + }, + grid: true, + }); + } + if (this.spectroInfo.start_times && this.spectroInfo.end_times) { + this.drawXAxisLabelsCompressed(); + } else { + this.drawXAxisLabels(); + } + } + + redraw() { + // add some styles + const lineData = this.gridEnabled ? this.lineData.concat(this.gridLines) : this.lineData; + this.lineLayer + .data(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(); + } + + setGridEnabled(val: boolean) { + this.gridEnabled = val; + this.redraw(); + } + + 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/views/Spectrogram.vue b/client/src/views/Spectrogram.vue index fa6a0c7..acf6bb7 100644 --- a/client/src/views/Spectrogram.vue +++ b/client/src/views/Spectrogram.vue @@ -30,6 +30,7 @@ export default defineComponent({ const speciesList: Ref = ref([]); const loadedImage = ref(false); const compressed = ref(false); + const gridEnabled = ref(true); const getAnnotationsList= async (annotationId?: number) => { const response = await getAnnotations(props.id); annotations.value = response.data; @@ -82,6 +83,7 @@ export default defineComponent({ speciesList, selectedAnnotation, parentGeoViewerRef, + gridEnabled, }; }, }); @@ -97,6 +99,7 @@ export default defineComponent({ :recording-id="id" :annotations="annotations" :selected-id="selectedId" + :grid="gridEnabled" @selected="setSelection($event)" @create:annotation="getAnnotationsList($event)" @geo-viewer-ref="setParentGeoViewer($event)" @@ -113,12 +116,18 @@ export default defineComponent({ /> - + Date: Fri, 26 Jan 2024 14:19:16 -0500 Subject: [PATCH 2/9] add hover state and improve legend --- client/src/components/SpectrogramViewer.vue | 87 ++++++++++++++----- .../components/geoJS/layers/legendLayer.ts | 46 ++++++---- client/src/views/Spectrogram.vue | 25 ++++++ 3 files changed, 120 insertions(+), 38 deletions(-) diff --git a/client/src/components/SpectrogramViewer.vue b/client/src/components/SpectrogramViewer.vue index a4e867c..266cad0 100644 --- a/client/src/components/SpectrogramViewer.vue +++ b/client/src/components/SpectrogramViewer.vue @@ -1,13 +1,15 @@