mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 12:57:08 +00:00
618 lines
18 KiB
Vue
618 lines
18 KiB
Vue
<script>
|
||
// Important info about performance:
|
||
// https://deck.gl/docs/developer-guide/performance#supply-attributes-directly
|
||
|
||
import { Deck, WebMercatorViewport, FlyToInterpolator, CompositeLayer } from '@deck.gl/core';
|
||
import { GeoJsonLayer, LineLayer, PathLayer, BitmapLayer, ScatterplotLayer, ColumnLayer, IconLayer } from '@deck.gl/layers';
|
||
import {HeatmapLayer} from '@deck.gl/aggregation-layers';
|
||
import { TileLayer, MVTLayer } from '@deck.gl/geo-layers';
|
||
|
||
|
||
//import { json } from 'd3-fetch';
|
||
import * as d3a from 'd3-array';
|
||
|
||
import { DougalBinaryBundle, DougalBinaryChunkSequential, DougalBinaryChunkInterleaved } from '@dougal/binary';
|
||
import { DougalShotLayer } from '@/lib/deck.gl';
|
||
import { DougalSequenceLayer, DougalEventsLayer } from '@/lib/deck.gl';
|
||
import DougalBinaryLoader from '@/lib/deck.gl/DougalBinaryLoader';
|
||
|
||
import { colors } from 'vuetify/lib'
|
||
|
||
|
||
function hexToArray (hex, defaultValue = [ 0xc0, 0xc0, 0xc0, 0xff ]) {
|
||
|
||
if (typeof hex != "string" || hex.length < 6) {
|
||
return defaultValue;
|
||
}
|
||
|
||
if (hex[0] == "#") {
|
||
hex = hex.slice(1); // remove the '#' character
|
||
}
|
||
|
||
return [
|
||
parseInt(hex.slice(0, 2), 16),
|
||
parseInt(hex.slice(2, 4), 16),
|
||
parseInt(hex.slice(4, 6), 16),
|
||
hex.length > 6 ? parseInt(hex.slice(6, 8), 16) : 255
|
||
];
|
||
}
|
||
|
||
function namedColourToArray (name) {
|
||
const parts = name.split(/\s+/).map( (s, i) =>
|
||
i
|
||
? s.replace("-", "")
|
||
: s.replace(/-([a-z])/g, (match, group1) => group1.toUpperCase())
|
||
);
|
||
parts[0]
|
||
if (parts.length == 1) parts[1] = "base";
|
||
const hex = parts.reduce((acc, key) => acc[key], colors);
|
||
return hexToArray(hex);
|
||
}
|
||
|
||
export default {
|
||
name: "MapLayersMixin",
|
||
|
||
data () {
|
||
|
||
return {
|
||
|
||
COLOUR_SCALE_1: [
|
||
// negative
|
||
[65, 182, 196],
|
||
[127, 205, 187],
|
||
[199, 233, 180],
|
||
[237, 248, 177],
|
||
|
||
// positive
|
||
[255, 255, 204],
|
||
[255, 237, 160],
|
||
[254, 217, 118],
|
||
[254, 178, 76],
|
||
[253, 141, 60],
|
||
[252, 78, 42],
|
||
[227, 26, 28],
|
||
[189, 0, 38],
|
||
[128, 0, 38]
|
||
]
|
||
|
||
};
|
||
|
||
},
|
||
|
||
methods: {
|
||
|
||
normalisedColourScale(v, scale = this.COLOUR_SCALE_1, min = 0, max = 1) {
|
||
const range = max-min;
|
||
const i = Math.min(scale.length, Math.max(Math.round((v-min) / range * scale.length), 0));
|
||
//console.log(`v=${v}, scale.length=${scale.length}, min=${min}, max=${max}, i=${i}, → ${scale[i]}`);
|
||
return scale[i];
|
||
},
|
||
|
||
osmLayer (options = {}) {
|
||
return new TileLayer({
|
||
id: "osm",
|
||
// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
|
||
data: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||
|
||
minZoom: 0,
|
||
maxZoom: 19,
|
||
tileSize: 256,
|
||
|
||
renderSubLayers: props => {
|
||
const {
|
||
bbox: {west, south, east, north}
|
||
} = props.tile;
|
||
|
||
return new BitmapLayer(props, {
|
||
data: null,
|
||
image: props.data,
|
||
bounds: [west, south, east, north]
|
||
});
|
||
},
|
||
...options
|
||
})
|
||
},
|
||
|
||
|
||
// OSM tiles layer. Handy to make water transparent
|
||
// but not super reliable yet
|
||
|
||
osmVectorLayer (options = {}) {
|
||
return new MVTLayer({
|
||
id: 'osm',
|
||
data: 'https://vector.openstreetmap.org/shortbread_v1/{z}/{x}/{y}.mvt',
|
||
minZoom: 0,
|
||
maxZoom: 14,
|
||
getFillColor: feature => {
|
||
const layer = feature.properties.layerName;
|
||
//console.log("layer =", layer, feature.properties.kind);
|
||
switch (layer) {
|
||
case "ocean":
|
||
return [0, 0, 0, 0];
|
||
case "land":
|
||
return [ 0x54, 0x6E, 0x7A, 255 ];
|
||
default:
|
||
return [ 240, 240, 240, 255 ];
|
||
}
|
||
},
|
||
getLineColor: feature => {
|
||
if (feature.properties.layer === 'water') {
|
||
return [0, 0, 0, 0]; // No outline for water
|
||
}
|
||
return [192, 192, 192, 255]; // Default line color for roads, etc.
|
||
},
|
||
getLineWidth: feature => {
|
||
if (feature.properties.highway) {
|
||
return feature.properties.highway === 'motorway' ? 6 : 3; // Example road widths
|
||
}
|
||
return 1;
|
||
},
|
||
stroked: true,
|
||
filled: true,
|
||
pickable: true
|
||
});
|
||
},
|
||
|
||
|
||
openSeaMapLayer (options = {}) {
|
||
return new TileLayer({
|
||
id: "sea",
|
||
data: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
|
||
|
||
minZoom: 0,
|
||
maxZoom: 19,
|
||
tileSize: 256,
|
||
|
||
renderSubLayers: props => {
|
||
const {
|
||
bbox: {west, south, east, north}
|
||
} = props.tile;
|
||
|
||
return new BitmapLayer(props, {
|
||
data: null,
|
||
image: props.data,
|
||
bounds: [west, south, east, north]
|
||
});
|
||
},
|
||
...options
|
||
})
|
||
},
|
||
|
||
|
||
// Norwegian nautical charts
|
||
// As of 2025, not available for some weird reason
|
||
nauLayer (options = {}) {
|
||
return new TileLayer({
|
||
id: "nau",
|
||
// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
|
||
data: 'https://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=sjokartraster&zoom={z}&x={x}&y={y}',
|
||
|
||
minZoom: 0,
|
||
maxZoom: 19,
|
||
tileSize: 256,
|
||
|
||
renderSubLayers: props => {
|
||
const {
|
||
bbox: {west, south, east, north}
|
||
} = props.tile;
|
||
|
||
return new BitmapLayer(props, {
|
||
data: null,
|
||
image: props.data,
|
||
bounds: [west, south, east, north]
|
||
});
|
||
},
|
||
...options
|
||
})
|
||
},
|
||
|
||
vesselTrackPointsLayer (options = {}) {
|
||
return new ScatterplotLayer({
|
||
id: 'navp',
|
||
data: `/api/navdata?limit=10000`,
|
||
getPosition: (d) => ([d.longitude, d.latitude]),
|
||
getRadius: d => (d.speed),
|
||
radiusScale: 1,
|
||
lineWidthMinPixels: 2,
|
||
getFillColor: d => d.guns
|
||
? d.lineStatus == "online"
|
||
? [0xaa, 0x00, 0xff] // Online
|
||
: [0xd5, 0x00, 0xf9] // Soft start or guns otherwise active
|
||
: [0xea, 0x80, 0xfc], // Offline, guns inactive
|
||
getLineColor: [127, 65, 90],
|
||
getColor: [ 255, 0, 0 ],
|
||
getPointRadius: 12,
|
||
radiusUnits: "pixels",
|
||
pointRadiusMinPixels: 4,
|
||
stroked: false,
|
||
filled: true,
|
||
pickable: true,
|
||
...options
|
||
})
|
||
},
|
||
|
||
vesselTrackLinesLayer (options = {}) {
|
||
return new LineLayer({
|
||
id: 'navl',
|
||
data: `/api/navdata?v=${Date.now()}`, // NOTE Not too sure about this
|
||
lineWidthMinPixels: 2,
|
||
getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||
getSourcePosition: (obj, i) => i.index < i.data?.length ? [i.data[i.index]?.longitude, i.data[i.index]?.latitude] : null,
|
||
getTargetPosition: (obj, i) => i.index < i.data?.length ? [i.data[i.index+1]?.longitude, i.data[i.index+1]?.latitude] : null,
|
||
getLineWidth: 3,
|
||
getPointRadius: 2,
|
||
radiusUnits: "pixels",
|
||
pointRadiusMinPixels: 2,
|
||
pickable: true,
|
||
...options
|
||
})
|
||
},
|
||
|
||
eventsLogLayer (options = {}) {
|
||
|
||
const labelColour = (d, i, t, c = [127, 65, 90]) => {
|
||
const label = d?.properties?.labels?.[0];
|
||
const colour = this.labels[label]?.view?.colour ?? "#cococo";
|
||
|
||
if (colour) {
|
||
if (colour[0] == "#") {
|
||
c = hexToArray(colour);
|
||
} else {
|
||
c = namedColourToArray(colour);
|
||
}
|
||
} else {
|
||
//return [127, 65, 90];
|
||
}
|
||
|
||
if (t != null) {
|
||
c[3] = t;
|
||
}
|
||
|
||
return c;
|
||
};
|
||
|
||
return new DougalEventsLayer({
|
||
id: 'log',
|
||
data: `/api/project/${this.$route.params.project}/event?mime=application/geo%2Bjson`,
|
||
lineWidthMinPixels: 2,
|
||
getPosition: d => d.geometry.coordinates,
|
||
jitter: 0.00015,
|
||
getElevation: d => Math.min(Math.max(d.properties.remarks?.length || 10, 10), 200),
|
||
getFillColor: (d, i) => labelColour(d, i, 200),
|
||
getLineColor: (d, i) => labelColour(d, i, 200),
|
||
radius: 0.001,
|
||
radiusScale: 1,
|
||
// This just won't work with radiusUnits = "pixels".
|
||
// See: https://grok.com/share/c2hhcmQtMw%3D%3D_16578be4-20fd-4000-a765-f082503d0495
|
||
radiusUnits: "pixels",
|
||
radiusMinPixels: 1.5,
|
||
radiusMaxPixels: 2.5,
|
||
|
||
pickable: true,
|
||
...options
|
||
})
|
||
|
||
},
|
||
|
||
preplotSaillinesLinesLayer (options = {}) {
|
||
return new GeoJsonLayer({
|
||
id: 'psll',
|
||
data: `/api/project/${this.$route.params.project}/gis/preplot/line?class=V&v=${this.lineTStamp?.valueOf()}`,
|
||
lineWidthMinPixels: 1,
|
||
getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||
getLineWidth: 1,
|
||
getPointRadius: 2,
|
||
radiusUnits: "pixels",
|
||
pointRadiusMinPixels: 2,
|
||
pickable: true,
|
||
...options
|
||
})
|
||
},
|
||
|
||
preplotLinesLayer (options = {}) {
|
||
return new GeoJsonLayer({
|
||
id: 'ppll',
|
||
data: `/api/project/${this.$route.params.project}/gis/preplot/line?v=${this.lineTStamp?.valueOf()}`,
|
||
lineWidthMinPixels: 1,
|
||
getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||
getLineWidth: 1,
|
||
getPointRadius: 2,
|
||
radiusUnits: "pixels",
|
||
pointRadiusMinPixels: 2,
|
||
pickable: true,
|
||
...options
|
||
})
|
||
},
|
||
|
||
plannedLinesLinesLayer (options = {}) {
|
||
return new PathLayer({
|
||
id: 'planl',
|
||
data: [...this.plannedSequences], // Create new array to trigger Deck.gl update
|
||
dataTransform: (sequences) => {
|
||
// Raise the data 10 m above ground so that it's visible over heatmaps, etc.
|
||
return sequences.map( seq => ({
|
||
...seq,
|
||
geometry: {
|
||
...seq.geometry,
|
||
coordinates: seq.geometry.coordinates.map( pos => [...pos, 10] )
|
||
}
|
||
}))
|
||
},
|
||
getPath: d => d.geometry.coordinates,
|
||
//getSourcePosition: d => d.geometry.coordinates[0],
|
||
//getTargetPosition: d => d.geometry.coordinates[1],
|
||
widthUnits: "meters",
|
||
widthMinPixels: 4,
|
||
getWidth: 25,
|
||
//getLineWidth: 10,
|
||
getColor: (d) => {
|
||
const k = (d?.azimuth??0)/360*255;
|
||
return [ k, 128, k, 200 ];
|
||
},
|
||
stroked: true,
|
||
pickable: true,
|
||
...options
|
||
});
|
||
},
|
||
|
||
rawSequencesLinesLayer (options = {}) {
|
||
return new GeoJsonLayer({
|
||
id: 'seqrl',
|
||
data: `/api/project/${this.$route.params.project}/gis/raw/line?v=${this.sequenceTStamp?.valueOf()}`,
|
||
lineWidthMinPixels: 1,
|
||
getLineColor: (d) => d.properties.ntbp ? [0xe6, 0x51, 0x00, 200] : [0xff, 0x98, 0x00, 200],
|
||
getLineWidth: 1,
|
||
getPointRadius: 2,
|
||
radiusUnits: "pixels",
|
||
pointRadiusMinPixels: 2,
|
||
pickable: true,
|
||
...options
|
||
})
|
||
},
|
||
|
||
finalSequencesLinesLayer (options = {}) {
|
||
return new GeoJsonLayer({
|
||
id: 'seqfl',
|
||
data: `/api/project/${this.$route.params.project}/gis/final/line?v=${this.sequenceTStamp?.valueOf()}`,
|
||
lineWidthMinPixels: 1,
|
||
getLineColor: (d) => d.properties.pending ? [0xa7, 0xff, 0xab, 200] : [0x00, 0x96, 0x88, 200],
|
||
getLineWidth: 1,
|
||
getPointRadius: 2,
|
||
radiusUnits: "pixels",
|
||
pointRadiusMinPixels: 2,
|
||
pickable: true,
|
||
...options
|
||
})
|
||
},
|
||
|
||
preplotSaillinesPointLayer (options = {}) {
|
||
return new DougalSequenceLayer({
|
||
id: 'pslp',
|
||
data: `/api/project/${this.$route.params.project}/line/sail`, // API endpoint returning binary data
|
||
loaders: [DougalBinaryLoader],
|
||
loadOptions: {
|
||
fetch: {
|
||
method: 'GET',
|
||
headers: { Accept: 'application/vnd.aaltronav.dougal+octet-stream' }
|
||
}
|
||
},
|
||
getRadius: 2,
|
||
getFillColor: (d, {data, index}) => data.attributes.value2.value[index] ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||
//getFillColor: [0, 120, 220, 200],
|
||
pickable: true,
|
||
...options
|
||
});
|
||
},
|
||
|
||
preplotPointsLayer (options = {}) {
|
||
return new DougalSequenceLayer({
|
||
id: 'pplp',
|
||
data: `/api/project/${this.$route.params.project}/line/source`, // API endpoint returning binary data
|
||
loaders: [DougalBinaryLoader],
|
||
loadOptions: {
|
||
fetch: {
|
||
method: 'GET',
|
||
headers: { Accept: 'application/vnd.aaltronav.dougal+octet-stream' }
|
||
}
|
||
},
|
||
getRadius: 2,
|
||
getFillColor: (d, {data, index}) => data.attributes.value2.value[index] ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||
//getFillColor: [0, 120, 220, 200],
|
||
pickable: true,
|
||
...options
|
||
});
|
||
},
|
||
|
||
plannedLinesPointsLayer (options = {}) {
|
||
},
|
||
|
||
rawSequencesPointsLayer (options = {}) {
|
||
return new DougalSequenceLayer({
|
||
id: 'seqrp',
|
||
data: `/api/project/${this.$route.params.project}/sequence?type=2`, // API endpoint returning binary data
|
||
loaders: [DougalBinaryLoader],
|
||
loadOptions: {
|
||
fetch: {
|
||
method: 'GET',
|
||
headers: { Accept: 'application/vnd.aaltronav.dougal+octet-stream' }
|
||
}
|
||
},
|
||
getRadius: 2,
|
||
getFillColor: [0, 120, 220, 200],
|
||
pickable: true,
|
||
...options
|
||
});
|
||
},
|
||
|
||
heatmapLayer(options = {}) {
|
||
const { positions, values } = this.sequenceBinaryData;
|
||
if (!positions?.length || !values?.length) {
|
||
console.warn('No valid data for heatmapLayer');
|
||
|
||
return new HeatmapLayer({
|
||
id: 'seqrh',
|
||
data: [],
|
||
...options
|
||
});
|
||
}
|
||
|
||
|
||
let weights, offset = 0, scaler = 1;
|
||
let colorDomain = null;
|
||
let aggregation = "MEAN";
|
||
let transform = (v) => v;
|
||
|
||
switch (this.heatmapValue) {
|
||
case "total_error":
|
||
weights = Float32Array.from(values[3], (ei, i) => {
|
||
const ej = values[4][i];
|
||
return Math.sqrt(ei * ei + ej * ej) / 10; // Euclidean distance in meters
|
||
});
|
||
colorDomain = [2, 20];
|
||
break;
|
||
case "delta_j":
|
||
weights = values[3];
|
||
scaler = 0.1;
|
||
colorDomain = [2, 20];
|
||
break;
|
||
case "delta_i":
|
||
weights = values[4];
|
||
scaler = 0.1;
|
||
colorDomain = [0.5, 5];
|
||
break;
|
||
case "delta_μ":
|
||
weights = values[5];
|
||
scaler = 0.1;
|
||
break;
|
||
case "delta_σ":
|
||
weights = values[6];
|
||
scaler = 0.1;
|
||
colorDomain = [ 0.1, 1.5 ];
|
||
break;
|
||
case "delta_R":
|
||
weights = values[7];
|
||
scaler = 0.1;
|
||
colorDomain = [ 0.5, 1.0 ];
|
||
break;
|
||
case "press_μ":
|
||
weights = values[8];
|
||
offset = -2000;
|
||
colorDomain = [ 5, 50 ];
|
||
break;
|
||
case "press_σ":
|
||
weights = values[9];
|
||
colorDomain = [ 1, 19 ];
|
||
break;
|
||
case "press_R":
|
||
weights = values[10];
|
||
colorDomain = [ 3, 50 ];
|
||
break;
|
||
case "depth_μ":
|
||
weights = values[11];
|
||
offset = -6;
|
||
scaler = 0.1;
|
||
colorDomain = [ 0.1, 1 ];
|
||
break;
|
||
case "depth_σ":
|
||
weights = values[12];
|
||
scaler = 0.1;
|
||
break;
|
||
case "depth_R":
|
||
weights = values[13];
|
||
scaler = 0.1;
|
||
break;
|
||
case "fill_μ":
|
||
weights = values[14];
|
||
colorDomain = [ 300, 1000 ];
|
||
break;
|
||
case "fill_σ":
|
||
weights = values[15];
|
||
offset = -250;
|
||
colorDomain = [ 0, 250 ];
|
||
break;
|
||
case "fill_R":
|
||
weights = values[16];
|
||
offset = -500;
|
||
colorDomain = [ 0, 500 ];
|
||
break;
|
||
case "delay_μ":
|
||
weights = values[17];
|
||
offset = -150;
|
||
colorDomain = [ 1.5, 25 ];
|
||
//transform = (v) => {console.log("τ(v)", v); return v;};
|
||
break;
|
||
case "delay_σ":
|
||
weights = values[18];
|
||
break;
|
||
case "delay_R":
|
||
weights = values[19];
|
||
break;
|
||
case "no_fire":
|
||
weights = values[20];
|
||
transform = v => v >> 4;
|
||
aggregation = "SUM";
|
||
colorDomain = [ 0.1, 1.5 ];
|
||
break;
|
||
case "autofire":
|
||
weights = values[20];
|
||
transform = v => v & 0xf;
|
||
aggregation = "SUM";
|
||
colorDomain = [ 0.5, 1.5 ];
|
||
break;
|
||
case "misfire":
|
||
weights = values[20];
|
||
aggregation = "SUM";
|
||
colorDomain = [ 0.5, 1.5 ];
|
||
break;
|
||
}
|
||
|
||
|
||
const stats = {
|
||
min: d3a.min(weights),
|
||
mode: d3a.mode(weights),
|
||
mean: d3a.mean(weights),
|
||
max: d3a.max(weights),
|
||
sd: d3a.deviation(weights),
|
||
};
|
||
const sr0 = [ stats.mean - 2.1*stats.sd, stats.mean + 2.1*stats.sd ];
|
||
const sr1 = [ stats.mode - 2.1*stats.sd, stats.mode + 2.1*stats.sd ];
|
||
|
||
console.log('Positions sample:', positions.slice(0, 10));
|
||
console.log('Weights sample:', weights.slice(0, 10));
|
||
console.log("Mode:", this.heatmapValue);
|
||
console.log('Weight stats:', stats);
|
||
console.log("Suggested ranges");
|
||
console.log(sr0);
|
||
console.log(sr1);
|
||
console.log("Actual ranges");
|
||
console.log(colorDomain);
|
||
|
||
return new HeatmapLayer({
|
||
id: 'seqrh',
|
||
data: {
|
||
length: weights.length,
|
||
positions,
|
||
weights
|
||
/*
|
||
attributes: {
|
||
getPosition: { value: positions, type: 'float32', size: 2 },
|
||
getWeight: { value: weights, type: 'float32', size: 1 }
|
||
}
|
||
*/
|
||
},
|
||
getPosition: (d, {index, data}) => [ data.positions[index*2], data.positions[index*2+1] ],
|
||
getWeight: (d, {index, data}) => transform(Math.abs(data.weights[index] * scaler + offset)),
|
||
colorDomain,
|
||
radiusPixels: 25,
|
||
aggregation,
|
||
pickable: false,
|
||
...options
|
||
});
|
||
},
|
||
|
||
},
|
||
|
||
}
|
||
|
||
</script>
|