Skip to content

Commit df8e29d

Browse files
authored
Merge pull request #242 from Kitware/issue-201-measurement-tool
Create a slidable ruler for frequency measurements
2 parents 417ef7f + 8470fba commit df8e29d

File tree

8 files changed

+368
-31
lines changed

8 files changed

+368
-31
lines changed

client/src/components/geoJS/LayerManager.vue

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import TimeLayer from "./layers/timeLayer";
1212
import FreqLayer from "./layers/freqLayer";
1313
import SpeciesLayer from "./layers/speciesLayer";
1414
import SpeciesSequenceLayer from "./layers/speciesSequenceLayer";
15+
import MeasureToolLayer from "./layers/measureToolLayer";
1516
import { cloneDeep } from "lodash";
1617
import useState from "@use/useState";
1718
export default defineComponent({
@@ -63,6 +64,8 @@ export default defineComponent({
6364
configuration,
6465
colorScheme,
6566
backgroundColor,
67+
measuring,
68+
frequencyRulerY,
6669
} = useState();
6770
const selectedAnnotationId: Ref<null | number> = ref(null);
6871
const hoveredAnnotationId: Ref<null | number> = ref(null);
@@ -79,6 +82,7 @@ export default defineComponent({
7982
let freqLayer: FreqLayer;
8083
let speciesLayer: SpeciesLayer;
8184
let speciesSequenceLayer: SpeciesSequenceLayer;
85+
let measureToolLayer: MeasureToolLayer;
8286
const displayError = ref(false);
8387
const errorMsg = ref("");
8488
@@ -284,6 +288,10 @@ export default defineComponent({
284288
}
285289
}
286290
}
291+
if (type === "measure:dragged") {
292+
const { yValue } = data;
293+
frequencyRulerY.value = yValue || 0;
294+
}
287295
};
288296
289297
const getDataForLayers = () => {
@@ -510,6 +518,25 @@ export default defineComponent({
510518
speciesLayer.spectroInfo = props.spectroInfo;
511519
speciesLayer.setScaledDimensions(props.scaledWidth, props.scaledHeight);
512520
521+
if (!measureToolLayer) {
522+
measureToolLayer = new MeasureToolLayer(
523+
props.geoViewerRef,
524+
event,
525+
props.spectroInfo,
526+
measuring.value,
527+
frequencyRulerY.value
528+
);
529+
measureToolLayer.setScaledDimensions(props.scaledWidth, props.scaledHeight);
530+
}
531+
measureToolLayer.redraw();
532+
watch(measuring, () => {
533+
if (measuring.value) {
534+
measureToolLayer.enableDrawing();
535+
} else {
536+
measureToolLayer.disableDrawing();
537+
}
538+
});
539+
513540
timeLayer.setDisplaying({ pulse: configuration.value.display_pulse_annotations, sequence: configuration.value.display_sequence_annotations });
514541
timeLayer.formatData(localAnnotations.value, sequenceAnnotations.value);
515542
freqLayer.formatData(localAnnotations.value);
@@ -615,6 +642,10 @@ export default defineComponent({
615642
);
616643
sequenceAnnotationLayer.redraw();
617644
}
645+
if (measureToolLayer) {
646+
measureToolLayer.setScaledDimensions(props.scaledWidth, props.scaledHeight);
647+
measureToolLayer.redraw();
648+
}
618649
// Triggers the Axis redraw when zoomed in and the axis is at the bottom/top
619650
legendLayer?.onPan();
620651
});
@@ -649,7 +680,7 @@ export default defineComponent({
649680
// convert rgb(0 0 0) to rgb(0, 0, 0)
650681
backgroundColor.value = backgroundColor.value.replace(/rgb\((\d+)\s+(\d+)\s+(\d+)\)/, 'rgb($1, $2, $3)');
651682
}
652-
683+
653684
const backgroundRgbColor = d3.color(backgroundColor.value) as d3.RGBColor;
654685
const redStops: number[] = [backgroundRgbColor.r / 255];
655686
const greenStops: number[] = [backgroundRgbColor.g / 255];
@@ -676,11 +707,13 @@ export default defineComponent({
676707
}
677708
if (timeLayer) {
678709
timeLayer.setTextColor(textColor);
679-
}
710+
}
680711
if (speciesSequenceLayer) {
681712
speciesSequenceLayer.setTextColor(textColor);
682713
}
683-
714+
if (measureToolLayer) {
715+
measureToolLayer.setTextColor(textColor);
716+
}
684717
}
685718
686719
watch([backgroundColor, colorScheme], updateColorFilter);

client/src/components/geoJS/layers/freqLayer.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,7 @@
22
import { SpectrogramAnnotation } from "../../../api/api";
33
import { SpectroInfo, spectroToGeoJSon } from "../geoJSUtils";
44
import BaseTextLayer from "./baseTextLayer";
5-
import { LayerStyle } from "./types";
6-
7-
interface LineData {
8-
line: GeoJSON.LineString;
9-
thicker?: boolean;
10-
grid?: boolean;
11-
}
12-
13-
interface TextData {
14-
text: string;
15-
x: number;
16-
y: number;
17-
offsetY?: number;
18-
offsetX?: number;
19-
}
5+
import { LayerStyle, LineData, TextData } from "./types";
206

217
export default class FreqLayer extends BaseTextLayer<TextData> {
228
lineData: LineData[];
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import geo, { GeoEvent } from 'geojs';
2+
import { SpectroInfo } from '../geoJSUtils';
3+
import { LayerStyle, LineData, TextData } from './types';
4+
import BaseTextLayer from './baseTextLayer';
5+
6+
function _determineRulerColor(isDragging: boolean, isDarkMode: boolean) {
7+
if (isDarkMode) {
8+
return isDragging ? 'orange' : 'yellow';
9+
}
10+
return isDragging ? 'cyan' : 'blue';
11+
}
12+
13+
export default class MeasureToolLayer extends BaseTextLayer<TextData> {
14+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
15+
frequencyRulerLayer: any;
16+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17+
pointAnnotation: any;
18+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
19+
lineAnnotation: any;
20+
21+
rulerOn: boolean;
22+
dragging: boolean;
23+
yValue: number;
24+
hovering: boolean;
25+
26+
moveHandler: (e: GeoEvent) => void;
27+
mousedownHandler: (e: GeoEvent) => void;
28+
hoverHandler: (e: GeoEvent) => void;
29+
mouseupHandler: () => void;
30+
31+
constructor(
32+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
33+
geoViewerRef: any,
34+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
35+
event: (name: string, data: any) => void,
36+
spectroInfo: SpectroInfo,
37+
measuring?: boolean,
38+
yValue?: number
39+
) {
40+
super(geoViewerRef, event, spectroInfo);
41+
42+
const textLayer = this.geoViewerRef.createLayer('feature', {
43+
features: ['text']
44+
});
45+
this.textLayer = textLayer
46+
.createFeature("text")
47+
.text((data: TextData) => data.text)
48+
.style(this.createTextStyle())
49+
.position((data: TextData) => ({
50+
x: data.x,
51+
y: data.y,
52+
}));
53+
54+
const frequencyRulerLayer = this.geoViewerRef.createLayer("feature", {
55+
features: ['point', 'line'],
56+
});
57+
this.frequencyRulerLayer = frequencyRulerLayer;
58+
this.rulerOn = false;
59+
this.pointAnnotation= null;
60+
this.lineAnnotation = null;
61+
this.dragging = false;
62+
this.yValue = yValue || 0;
63+
this.color = 'white';
64+
this.hovering = false;
65+
66+
this.textStyle = this.createTextStyle();
67+
this.rulerOn = measuring || false;
68+
69+
this.moveHandler = (e: GeoEvent) => {
70+
if (e && this.dragging) {
71+
this.updateRuler(e.mouse.geo.y);
72+
}
73+
};
74+
this.hoverHandler = (e: GeoEvent) => {
75+
if (e) {
76+
const gcs = this.geoViewerRef.displayToGcs(e.map);
77+
const p = this.pointAnnotation.data()[0];
78+
const dx = Math.abs(gcs.x - p.x);
79+
const dy = Math.abs(gcs.y - p.y);
80+
if (Math.sqrt(dx*dx + dy*dy) < 20 || dy < 10) {
81+
this.event('update:cursor', { cursor: 'grab' });
82+
this.hovering = true;
83+
return;
84+
} else {
85+
this.event('update:cursor', { cursor: 'default' });
86+
}
87+
}
88+
this.hovering = false;
89+
};
90+
this.mousedownHandler = (e: GeoEvent) => {
91+
if (this.hovering && e.buttons.left) {
92+
this.geoViewerRef.interactor().addAction({
93+
action: 'dragpoint',
94+
name: 'drag point with mouse',
95+
owner: 'MeasureToolLayer',
96+
input: 'left',
97+
});
98+
this.dragging = true;
99+
this.event('update:cursor', { cursor: 'grabbing' });
100+
}
101+
};
102+
this.mouseupHandler = () => {
103+
this.dragging = false;
104+
this.geoViewerRef.interactor().removeAction(undefined, undefined, 'MeasureToolLayer');
105+
this.updateRuler(this.yValue);
106+
this.event('update:cursor', { cursor: 'grab' });
107+
};
108+
109+
if (this.rulerOn) {
110+
this.enableDrawing();
111+
}
112+
}
113+
114+
enableDrawing() {
115+
this.rulerOn = true;
116+
// Frequency ruler
117+
this.lineAnnotation = this.frequencyRulerLayer.createFeature('line')
118+
.data([[
119+
{x: 0, y: this.yValue},
120+
{x: this.spectroInfo.width, y: this.yValue},
121+
]])
122+
.style(this.createLineStyle());
123+
this.pointAnnotation = this.frequencyRulerLayer.createFeature('point')
124+
.data([{x: 0, y: this.yValue}])
125+
.style(this.createPointStyle());
126+
this.geoViewerRef.geoOn(geo.event.mousedown, this.mousedownHandler);
127+
this.geoViewerRef.geoOn(geo.event.actionmove, this.moveHandler);
128+
this.geoViewerRef.geoOn(geo.event.mouseup, this.mouseupHandler);
129+
this.geoViewerRef.geoOn(geo.event.mousemove, this.hoverHandler);
130+
this.frequencyRulerLayer.draw();
131+
this.updateRuler(this.yValue);
132+
}
133+
134+
_getTextCoordinates(): { x: number, y: number } {
135+
const bounds = this.geoViewerRef.bounds();
136+
const startX = 0;
137+
const endX = ((this.compressedView
138+
? this.scaledWidth
139+
: this.spectroInfo.width
140+
) || this.spectroInfo.width);
141+
const left = Math.max(startX, bounds.left);
142+
const right = Math.min(endX, bounds.right);
143+
return { x: (left + right) / 2, y: this.yValue };
144+
}
145+
146+
updateRuler(newY: number) {
147+
if (newY < 0) {
148+
return;
149+
}
150+
this.event("measure:dragged", { yValue: newY });
151+
this.yValue = newY;
152+
const spectroWidth = this.compressedView ? this.scaledWidth : this.spectroInfo.width;
153+
this.lineAnnotation
154+
.data([[
155+
{x: 0, y: this.yValue},
156+
{x: (spectroWidth || this.spectroInfo.width), y: this.yValue},
157+
]])
158+
.style(this.createLineStyle());
159+
this.pointAnnotation
160+
.data([{x: 0, y: this.yValue}])
161+
.style(this.createPointStyle());
162+
this.frequencyRulerLayer.draw();
163+
const height = Math.max(this.scaledHeight, this.spectroInfo.height);
164+
const frequency = height - this.yValue >= 0
165+
? ((height - newY) * (this.spectroInfo.high_freq - this.spectroInfo.low_freq)) / height / 1000 + this.spectroInfo.low_freq / 1000
166+
: -1;
167+
const textValue = `${frequency.toFixed(1)}KHz`;
168+
const { x: textX, y: textY } = this._getTextCoordinates();
169+
this.textData = [
170+
{
171+
text: textValue,
172+
x: textX,
173+
y: textY,
174+
offsetY: 20,
175+
},
176+
];
177+
this.textLayer.data(this.textData).draw();
178+
}
179+
180+
disableDrawing() {
181+
this.rulerOn = false;
182+
this.textData = [];
183+
this.textLayer.data(this.textData).draw();
184+
this.clearRulerLayer();
185+
this.geoViewerRef.geoOff(geo.event.mousedown, this.mousedownHandler);
186+
this.geoViewerRef.geoOff(geo.event.mouseup, this.mouseupHandler);
187+
this.geoViewerRef.geoOff(geo.event.actionmove, this.moveHandler);
188+
this.geoViewerRef.geoOff(geo.event.mousemove, this.hoverHandler);
189+
}
190+
191+
clearRulerLayer() {
192+
this.pointAnnotation?.data([]);
193+
this.lineAnnotation?.data([]);
194+
this.textLayer?.data([]).draw();
195+
this.frequencyRulerLayer?.draw();
196+
}
197+
198+
destroy() {
199+
super.destroy();
200+
this.textData = [];
201+
if (this.frequencyRulerLayer) {
202+
this.geoViewerRef.deleteLater(this.frequencyRulerLayer);
203+
}
204+
}
205+
206+
setScaledDimensions(scaledWidth: number, scaledHeight: number) {
207+
// Get the frequency represented by the current ruler
208+
const height = Math.max(this.scaledHeight, this.spectroInfo.height);
209+
const frequency = height - this.yValue >= 0
210+
? ((height - this.yValue) * (this.spectroInfo.high_freq - this.spectroInfo.low_freq)) / height / 1000 + this.spectroInfo.low_freq / 1000
211+
: -1;
212+
// Get the new yValue needed based on the updated scaled height
213+
const newY = scaledHeight - ((frequency - (this.spectroInfo.low_freq / 1000)) * scaledHeight * 1000) / (this.spectroInfo.high_freq - this.spectroInfo.low_freq);
214+
this.yValue = newY;
215+
super.setScaledDimensions(scaledWidth, scaledHeight);
216+
this.clearRulerLayer();
217+
if (this.rulerOn) {
218+
this.enableDrawing();
219+
this.updateRuler(this.yValue);
220+
}
221+
}
222+
223+
redraw() {
224+
if (this.rulerOn) {
225+
this.updateRuler(this.yValue);
226+
}
227+
super.redraw();
228+
}
229+
230+
createTextStyle(): LayerStyle<TextData> {
231+
return {
232+
color: () => _determineRulerColor(this.dragging, this.color === 'white'),
233+
offset: (data: TextData) => ({
234+
x: data.offsetX || 0,
235+
y: data.offsetY || 0,
236+
}),
237+
textAlign: 'center',
238+
textScaled: this.textScaled,
239+
textBaseline: 'bottom',
240+
};
241+
}
242+
243+
createPointStyle(): LayerStyle<LineData> {
244+
return {
245+
radius: 10,
246+
fillColor: () => _determineRulerColor(this.dragging, this.color === 'white'),
247+
strokeColor: () => _determineRulerColor(this.dragging, this.color === 'white'),
248+
stroke: true,
249+
strokeWidth: 5,
250+
};
251+
}
252+
253+
createLineStyle(): LayerStyle<LineData> {
254+
return {
255+
strokeColor: () => _determineRulerColor(this.dragging, this.color === 'white'),
256+
strokeWidth: 2,
257+
};
258+
}
259+
260+
}

0 commit comments

Comments
 (0)