Skip to content

Commit

Permalink
initialize geoJS annotation viewer
Browse files Browse the repository at this point in the history
  • Loading branch information
BryonLewis committed Jan 8, 2024
1 parent 505001f commit 074bd28
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 6 deletions.
7 changes: 5 additions & 2 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ export interface Species {
}

export interface SpectrogramAnnotation {
offset: number,
frequency: number,
start_time: number;
end_time: number;
low_freq: number;
high_feq: number;
id: number;
}


Expand Down
37 changes: 37 additions & 0 deletions client/src/components/geoJS/LayerManager.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { SpectroInfo } from './geoJSUtils';
import RectangleLayer from './layers/rectangleLayer';
export default defineComponent({
name:'LayerManager',
props: {
geoJSRef: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Object as PropType<any>,
required: true,
},
spectroInfo: {
type: Object as PropType<SpectroInfo>,
required: true,
}
},
setup(props) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const event = (type: string, data: any) => {
// Will handle clicking, selecting and editing here
//console.log(type);
//console.log(data);
};
const rectAnnotationLayer = new RectangleLayer(props.geoJSRef, event, props.spectroInfo);
return {
};
}
});
</script>

<template>
<div />
</template>
92 changes: 88 additions & 4 deletions client/src/components/geoJS/geoJSUtils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@

import { defineComponent, PropType, ref, Ref, watch } from "vue";
import geo, { GeoEvent } from "geojs";
import { ref, Ref } from "vue";
import geo from "geojs";

const useGeoJS = () => {

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const geoViewer: Ref<any> = ref();
const container: Ref<HTMLElement| undefined> = ref();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let quadFeature: any;
let originalBounds = {
left: 0,
Expand Down Expand Up @@ -142,6 +144,88 @@ const useGeoJS = () => {
};
};

import { SpectrogramAnnotation } from "../../api/api";

export interface SpectroInfo {
width: number;
height: number;
start_time: number;
end_time: number;
low_freq: number;
high_freq: number;
}
function spectroToGeoJSon(annotation: SpectrogramAnnotation, spectroInfo: SpectroInfo): GeoJSON.Polygon {
//scale pixels to time and frequency ranges
const widthScale = spectroInfo.width / (spectroInfo.end_time - spectroInfo.start_time);
const heightScale = spectroInfo.height / (spectroInfo.high_freq - spectroInfo.low_freq);
// Now we remap our annotation to pixel coordinates
const low_freq = annotation.low_freq * heightScale;
const high_freq = annotation.high_feq * heightScale;
const start_time = annotation.start_time * widthScale;
const end_time = annotation.end_time * widthScale;

return {
type: 'Polygon',
coordinates: [
[
[start_time, high_freq],
[start_time, low_freq],
[end_time, low_freq],
[end_time, high_freq],
[start_time, high_freq],
],
],

};
}

/* beginning at bottom left, rectangle is defined clockwise */
function geojsonToSpectro(geojson: GeoJSON.Feature<GeoJSON.Polygon>, spectroInfo: SpectroInfo): { start_time: number, end_time: number, low_freq: number, high_freq: number} {
const coords = geojson.geometry.coordinates[0];
const widthScale = spectroInfo.width / (spectroInfo.end_time - spectroInfo.start_time);
const heightScale = spectroInfo.height / (spectroInfo.high_freq - spectroInfo.low_freq);
const start_time = coords[1][0] / widthScale;
const end_time = coords[3][0] / widthScale;
const low_freq = coords[1][1] / heightScale;
const high_freq = coords[3][1] / heightScale;
return {
start_time,
end_time,
low_freq,
high_freq
};
}


/**
* This will take the current geoJSON Coordinates for a rectangle and reorder it
* to keep the vertices index the same with respect to how geoJS uses it
* Example: UL, LL, LR, UR, UL
*/
function reOrdergeoJSON(coords: GeoJSON.Position[]) {
let x1 = Infinity;
let x2 = -Infinity;
let y1 = Infinity;
let y2 = -Infinity;
coords.forEach((coord) => {
x1 = Math.min(x1, coord[0]);
x2 = Math.max(x2, coord[0]);
y1 = Math.min(y1, coord[1]);
y2 = Math.max(y2, coord[1]);
});
return [
[x1, y2],
[x1, y1],
[x2, y1],
[x2, y2],
[x1, y2],
];
}


export {
useGeoJS,
};
spectroToGeoJSon,
geojsonToSpectro,
reOrdergeoJSON,
useGeoJS,
};
177 changes: 177 additions & 0 deletions client/src/components/geoJS/layers/rectangleLayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/* eslint-disable class-methods-use-this */
import geo, { GeoEvent } from 'geojs';
import { SpectroInfo, spectroToGeoJSon } from '../geoJSUtils';
import { SpectrogramAnnotation } from '../../../api/api';

interface RectGeoJSData{
id: number;
selected: boolean;
editing: boolean | string;
polygon: GeoJSON.Polygon;
}

// eslint-disable-next-line max-len
export type StyleFunction<T, D> = T | ((point: [number, number], index: number, data: D) => T | undefined);
export type ObjectFunction<T, D> = T | ((data: D, index: number) => T | undefined);
export type PointFunction<T, D> = T | ((data: D) => T | undefined);

export interface LayerStyle<D> {
strokeWidth?: StyleFunction<number, D> | PointFunction<number, D>;
strokeOffset?: StyleFunction<number, D> | PointFunction<string, D>;
strokeOpacity?: StyleFunction<number, D> | PointFunction<string, D>;
strokeColor?: StyleFunction<string, D> | PointFunction<string, D>;
fillColor?: StyleFunction<string, D> | PointFunction<string, D>;
fillOpacity?: StyleFunction<number, D> | PointFunction<number, D>;
position?: (point: [number, number]) => { x: number; y: number };
color?: (data: D) => string;
textOpacity?: (data: D) => number;
fontSize?: (data: D) => string | undefined;
offset?: (data: D) => { x: number; y: number };
fill?: ObjectFunction<boolean, D> | boolean;
radius?: PointFunction<number, D> | number;
textAlign?: ((data: D) => string) | string;
textScaled?: ((data: D) => number | undefined) | number | undefined;
[x: string]: unknown;
}


export default class RectangleLayer{
formattedData: RectGeoJSData[];

drawingOther: boolean; //drawing another type of annotation at the same time?

hoverOn: boolean; //to turn over annnotations on
// eslint-disable-next-line @typescript-eslint/no-explicit-any
featureLayer: any;

selectedIndex: number[]; // sparse array

// 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;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(geoViewerRef: any, event: (name: string, data: any) => void, spectroInfo: SpectroInfo) {
this.geoViewerRef = geoViewerRef;
this.drawingOther = false;
this.spectroInfo = spectroInfo;
this.formattedData = [];
this.hoverOn = false;
this.selectedIndex = [];
this.event = event;
//Only initialize once, prevents recreating Layer each edit
this.initialize();
}

initialize() {
const layer = this.geoViewerRef.value.createLayer('feature', {
features: ['polygon'],
});
this.featureLayer = layer
.createFeature('polygon', { selectionAPI: true })
.geoOn(geo.event.feature.mouseclick, (e: GeoEvent) => {
/**
* Handle clicking on individual annotations, if DrawingOther is true we use the
* Rectangle type if only the polygon is visible we use the polygon bounds
* */
if (e.mouse.buttonsDown.left) {
if (!e.data.editing || (e.data.editing && !e.data.selected)) {
this.event('annotation-clicked', { id: e.data.id, edit: false });
}
} else if (e.mouse.buttonsDown.right) {
if (!e.data.editing || (e.data.editing && !e.data.selected)) {
this.event('annotation-right-clicked', { id: e.data.id, edit: true });
}
}
});
this.featureLayer.geoOn(
geo.event.feature.mouseclick_order,
this.featureLayer.mouseOverOrderClosestBorder,
);
this.featureLayer.geoOn(geo.event.mouseclick, (e: GeoEvent) => {
// If we aren't clicking on an annotation we can deselect the current track
if (this.featureLayer.pointSearch(e.geo).found.length === 0) {
this.event('annotation-clicked', { id: null, edit: false });
}
});
}

hoverAnnotations(e: GeoEvent) {
const { found } = this.featureLayer.pointSearch(e.mouse.geo);
this.event('annotation-hover', {id: found, pos: e.mouse.geo});
}

setHoverAnnotations(val: boolean) {
if (!this.hoverOn && val) {
this.featureLayer.geoOn(
geo.event.feature.mouseover,
(e: GeoEvent) => this.hoverAnnotations(e),
);
this.featureLayer.geoOn(
geo.event.feature.mouseoff,
(e: GeoEvent) => this.hoverAnnotations(e),
);
this.hoverOn = true;
} else if (this.hoverOn && !val) {
this.featureLayer.geoOff(geo.event.feature.mouseover);
this.featureLayer.geoOff(geo.event.feature.mouseoff);
this.hoverOn = false;
}
}

/**
* Used to set the drawingOther parameter used to change styling if other types are drawn
* and also handle selection clicking between different types
* @param val - determines if we are drawing other types of annotations
*/
setDrawingOther(val: boolean) {
this.drawingOther = val;
}


formatData(annotationData: SpectrogramAnnotation[]) {
const arr: RectGeoJSData[] = [];
annotationData.forEach((annotation: SpectrogramAnnotation, ) => {
const polygon = spectroToGeoJSon(annotation, this.spectroInfo);
const newAnnotation: RectGeoJSData = {
id: annotation.id,
selected: false,
editing: false,
polygon,
};
arr.push(newAnnotation);

});
return arr;
}

redraw() {
this.featureLayer
.data(this.formattedData)
.polygon((d: RectGeoJSData) => d.polygon.coordinates[0])
.draw();
}

disable() {
this.featureLayer
.data([])
.draw();
}

createStyle(): LayerStyle<RectGeoJSData> {
return {
// Style conversion to get array objects to work in geoJS
position: (point) => ({ x: point[0], y: point[1] }),
strokeColor: (_point, _index, data) => {
if (data.selected) {
return 'cyan';
}
return 'red';
},
};
}
}

0 comments on commit 074bd28

Please sign in to comment.