mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 10:17:07 +00:00
Further refactor Map component.
Map.sequencesBinaryData is now a single object instead of an array of objects. DougalSequenceLayer has been greatly simplified. It now inherits from ScatterplotLayer rather than CompositeLayer. DougalEventsLayer added. It shows either a ScatteplotLayer or a ColumnsLayer depending on zoom level.
This commit is contained in:
142
lib/www/client/source/src/lib/deck.gl/DougalEventsLayer.js
Normal file
142
lib/www/client/source/src/lib/deck.gl/DougalEventsLayer.js
Normal file
@@ -0,0 +1,142 @@
|
||||
// Ref.: https://deck.gl/docs/developer-guide/custom-layers/composite-layers
|
||||
import { CompositeLayer } from '@deck.gl/core';
|
||||
import { GeoJsonLayer, ColumnLayer } from '@deck.gl/layers';
|
||||
|
||||
class DougalEventsLayer extends CompositeLayer {
|
||||
static layerName = "DougalEventsLayer";
|
||||
|
||||
static defaultProps = {
|
||||
columnsZoom: 11, // Threshold zoom level for switching layers
|
||||
jitter: 0, // Add a small amount of jitter so that columns do not overlap.
|
||||
// GeoJsonLayer props
|
||||
getLineColor: [127, 65, 90],
|
||||
getFillColor: [127, 65, 90],
|
||||
getPointRadius: 2,
|
||||
radiusUnits: "pixels",
|
||||
pointRadiusMinPixels: 2,
|
||||
lineWidthMinPixels: 2,
|
||||
// ColumnLayer props
|
||||
getPosition: { type: 'accessor', value: d => d.geometry.coordinates },
|
||||
getElevation: { type: 'accessor', value: d => Math.min(Math.max(d.properties.remarks?.length || 10, 10), 200) },
|
||||
diskResolution: 20,
|
||||
radius: 5,
|
||||
radiusUnits: "pixels",
|
||||
radiusScale: 1,
|
||||
elevationScale: 1,
|
||||
filled: true,
|
||||
stroked: false,
|
||||
extruded: true,
|
||||
wireframe: false,
|
||||
material: true,
|
||||
getFillColor: [255, 0, 0, 200],
|
||||
getLineColor: [255, 0, 0, 200],
|
||||
getLineWidth: 2,
|
||||
pickable: true
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.uid = "el-" + Math.random().toString().slice(2);
|
||||
// Initialize state with current zoom
|
||||
this.state = {
|
||||
zoom: this.context?.viewport?.zoom || 0
|
||||
};
|
||||
}
|
||||
|
||||
shouldUpdateState({ changeFlags }) {
|
||||
// Always update if viewport changed (including zoom)
|
||||
if (changeFlags.viewportChanged) {
|
||||
return true;
|
||||
}
|
||||
return super.shouldUpdateState({ changeFlags });
|
||||
}
|
||||
|
||||
updateState({ props, oldProps, context, changeFlags }) {
|
||||
// Check if zoom has changed
|
||||
const newZoom = context.viewport?.zoom || 0;
|
||||
if (newZoom !== this.state.zoom) {
|
||||
this.setState({ zoom: newZoom });
|
||||
this.setNeedsRedraw(); // Trigger re-render of sublayers
|
||||
console.log(`Zoom changed to ${newZoom}, triggering redraw`);
|
||||
}
|
||||
}
|
||||
|
||||
getPickingInfo({ info, mode, sourceLayer }) {
|
||||
if (info.index >= 0) {
|
||||
info.object = {
|
||||
type: sourceLayer.constructor.layerName,
|
||||
...info.object // Merge default picking info (GeoJSON feature or ColumnLayer object)
|
||||
};
|
||||
//console.log(`Picked ${info.object.type}, index ${info.index}`);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
renderLayers() {
|
||||
const { zoom } = this.state;
|
||||
const sublayers = [];
|
||||
|
||||
if (zoom >= this.props.columnsZoom) {
|
||||
// Render ColumnLayer at high zoom
|
||||
const data = Array.isArray(this.props.data) ? this.props.data : this.props.data.features || [];
|
||||
|
||||
const positionFn = this.props.jitter
|
||||
? (d, info) => {
|
||||
let pos;
|
||||
if (typeof this.props.getPosition == 'function') {
|
||||
pos = this.props.getPosition(d, info);
|
||||
} else {
|
||||
pos = this.props.getPosition;
|
||||
}
|
||||
return pos.map( i => i + (Math.random() - 0.5) * this.props.jitter )
|
||||
}
|
||||
: this.props.getPosition;
|
||||
|
||||
sublayers.push(
|
||||
new ColumnLayer(this.getSubLayerProps({
|
||||
id: `${this.uid}-column`,
|
||||
data,
|
||||
visible: this.props.visible,
|
||||
getPosition: positionFn,
|
||||
getElevation: this.props.getElevation,
|
||||
diskResolution: this.props.diskResolution,
|
||||
radius: this.props.radius,
|
||||
radiusUnits: this.props.radiusUnits,
|
||||
radiusScale: this.props.radiusScale,
|
||||
elevationScale: this.props.elevationScale,
|
||||
filled: this.props.filled,
|
||||
stroked: this.props.stroked,
|
||||
extruded: this.props.extruded,
|
||||
wireframe: this.props.wireframe,
|
||||
material: this.props.material,
|
||||
getFillColor: this.props.getFillColor,
|
||||
getLineColor: this.props.getLineColor,
|
||||
getLineWidth: this.props.getLineWidth,
|
||||
pickable: this.props.pickable
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
// Render GeoJsonLayer at low zoom
|
||||
sublayers.push(
|
||||
new GeoJsonLayer(this.getSubLayerProps({
|
||||
id: `${this.uid}-geojson`,
|
||||
data: this.props.data,
|
||||
visible: this.props.visible,
|
||||
getLineColor: this.props.getLineColor,
|
||||
getFillColor: this.props.getFillColor,
|
||||
getPointRadius: this.props.getPointRadius,
|
||||
radiusUnits: this.props.radiusUnits,
|
||||
pointRadiusMinPixels: this.props.pointRadiusMinPixels,
|
||||
lineWidthMinPixels: this.props.lineWidthMinPixels,
|
||||
pickable: this.props.pickable
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Rendering ${sublayers.length} sublayer(s) at zoom ${zoom}`);
|
||||
|
||||
return sublayers;
|
||||
}
|
||||
}
|
||||
|
||||
export default DougalEventsLayer;
|
||||
@@ -1,12 +1,11 @@
|
||||
// Ref.: https://deck.gl/docs/developer-guide/custom-layers/composite-layers
|
||||
import { CompositeLayer } from '@deck.gl/core';
|
||||
import { GeoJsonLayer, LineLayer, BitmapLayer, ScatterplotLayer, IconLayer } from '@deck.gl/layers';
|
||||
import {HeatmapLayer} from '@deck.gl/aggregation-layers';
|
||||
// Ref.: https://deck.gl/docs/developer-guide/custom-layers/layer-lifecycle
|
||||
import { ScatterplotLayer } from '@deck.gl/layers';
|
||||
|
||||
class DougalSequenceLayer extends CompositeLayer {
|
||||
class DougalSequenceLayer extends ScatterplotLayer {
|
||||
static layerName = "DougalSequenceLayer";
|
||||
|
||||
static defaultProps = {
|
||||
...ScatterplotLayer.defaultProps,
|
||||
valueIndex: 0,
|
||||
radiusUnits: "pixels",
|
||||
radiusScale: 1,
|
||||
@@ -18,161 +17,59 @@ class DougalSequenceLayer extends CompositeLayer {
|
||||
radiusMaxPixels: 50,
|
||||
lineWidthMinPixels: 1,
|
||||
lineWidthMaxPixels: 50,
|
||||
// billboard: this.props.billboard,
|
||||
// antialiasing: this.props.antialiasing,
|
||||
getPosition: {type: 'accessor', value: d => d.positions},
|
||||
getPosition: { type: 'accessor', value: d => d.positions },
|
||||
getRadius: 5,
|
||||
//getColor: this.props.getColor,
|
||||
getFillColor: [255, 0, 0, 200],
|
||||
getLineColor: [255, 0, 0, 200],
|
||||
getLineWidth: 2,
|
||||
pickable: true
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// A unique id (prefix) for this layer
|
||||
this.uid = "sl-" + Math.random().toString().slice(2);
|
||||
}
|
||||
|
||||
getPickingInfo({ info, mode, sourceLayer }) {
|
||||
console.log("picking info", info);
|
||||
initializeState(context) {
|
||||
super.initializeState(context);
|
||||
}
|
||||
|
||||
getPickingInfo({ info, mode }) {
|
||||
const index = info.index;
|
||||
if (index >= 0) {
|
||||
const d = sourceLayer.props.data.attributes;
|
||||
console.log(index, d);
|
||||
|
||||
const d = this.props.data.attributes;
|
||||
if (d) {
|
||||
info.object = {
|
||||
udv: d.udv ?? 2, // FIXME must add
|
||||
i: d.value0.value[index],
|
||||
j: d.value1.value[index],
|
||||
udv: d.udv ?? 2,
|
||||
i: d.value0.value[index],
|
||||
j: d.value1.value[index],
|
||||
ts: Number(d.value2.value[index]),
|
||||
//λ: d.value3[k*2+0],
|
||||
//φ: d.value3[k*2+1],
|
||||
εi: d.value3.value[index] / 10,
|
||||
εj: d.value4.value[index] / 10,
|
||||
delta_μ: d.value5.value[index] / 10,
|
||||
delta_σ: d.value6.value[index] / 10,
|
||||
delta_R: d.value7.value[index] / 10,
|
||||
press_μ: d.value8.value[index],
|
||||
press_σ: d.value9.value[index],
|
||||
press_R: d.value10.value[index],
|
||||
depth_μ: d.value11.value[index] / 10,
|
||||
depth_σ: d.value12.value[index] / 10,
|
||||
depth_R: d.value13.value[index] / 10,
|
||||
fill_μ: d.value14.value[index],
|
||||
fill_σ: d.value15.value[index],
|
||||
fill_R: d.value16.value[index],
|
||||
delay_μ: d.value17.value[index] / 10,
|
||||
delay_σ: d.value18.value[index] / 10,
|
||||
delay_R: d.value19.value[index] / 10,
|
||||
/*
|
||||
nofire: d.value20.value[index] >> 4,
|
||||
εi: d.value3.value[index] / 10,
|
||||
εj: d.value4.value[index] / 10,
|
||||
delta_μ: d.value5.value[index] / 10,
|
||||
delta_σ: d.value6.value[index] / 10,
|
||||
delta_R: d.value7.value[index] / 10,
|
||||
press_μ: d.value8.value[index],
|
||||
press_σ: d.value9.value[index],
|
||||
press_R: d.value10.value[index],
|
||||
depth_μ: d.value11.value[index] / 10,
|
||||
depth_σ: d.value12.value[index] / 10,
|
||||
depth_R: d.value13.value[index] / 10,
|
||||
fill_μ: d.value14.value[index],
|
||||
fill_σ: d.value15.value[index],
|
||||
fill_R: d.value16.value[index],
|
||||
delay_μ: d.value17.value[index] / 10,
|
||||
delay_σ: d.value18.value[index] / 10,
|
||||
delay_R: d.value19.value[index] / 10,
|
||||
nofire: d.value20.value[index] >> 4,
|
||||
autofire: d.value20.value[index] & 0xf
|
||||
*/
|
||||
}
|
||||
};
|
||||
console.log(`Picked sequence ${info.object.i}, point ${info.object.j}, udv ${info.object.udv}`);
|
||||
} else {
|
||||
console.log(`No data found index = ${index}`);
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
renderLayers () {
|
||||
// return [
|
||||
// // List of sublayers. One per sequence (+ one per data item?)
|
||||
// ]
|
||||
|
||||
|
||||
const subLayers = [];
|
||||
let count=0;
|
||||
for (const {positions, values} of this.props.data) {
|
||||
const length = positions.length / 2;
|
||||
|
||||
// CHANGE: Build binary attributes object
|
||||
const attributes = {
|
||||
getPosition: {
|
||||
value: positions,
|
||||
type: "float32",
|
||||
size: 2
|
||||
}
|
||||
};
|
||||
|
||||
// CHANGE: Add each TypedArray in values as a custom attribute 'value0', 'value1', etc.
|
||||
// Note: If BigUint64Array or unsupported types, convert here (e.g., to Float64Array for timestamps)
|
||||
// Example: if (values[k] instanceof BigUint64Array) values[k] = Float64Array.from(values[k], BigInt => Number(BigInt));
|
||||
values.forEach((valArray, k) => {
|
||||
attributes[`value${k}`] = {
|
||||
value: valArray,
|
||||
size: 1
|
||||
};
|
||||
});
|
||||
|
||||
const subLayer = new ScatterplotLayer(this.getSubLayerProps({
|
||||
id: `${this.uid}-${count++}-scatter`,
|
||||
data: {
|
||||
length,
|
||||
attributes
|
||||
},
|
||||
radiusUnits: this.props.radiusUnits,
|
||||
radiusScale: this.props.radiusScale,
|
||||
lineWidthUnits: this.props.lineWidthUnits,
|
||||
lineWidthScale: this.props.lineWidthScale,
|
||||
stroked: this.props.stroked,
|
||||
filled: this.props.filled,
|
||||
radiusMinPixels: this.props.radiusMinPixels,
|
||||
radiusMaxPixels: this.props.radiusMaxPixels,
|
||||
lineWidthMinPixels: this.props.lineWidthMinPixels,
|
||||
lineWidthMaxPixels: this.props.lineWidthMaxPixels,
|
||||
billboard: this.props.billboard,
|
||||
antialiasing: this.props.antialiasing,
|
||||
getPosition: this.props.getPosition, // CHANGE: Default to d => d.positions; user can override with function accessing d.valueX
|
||||
getRadius: this.props.getRadius, // CHANGE: Can now be function d => d.value0 (or string 'value0' for direct mapping)
|
||||
//getColor: this.props.getColor,
|
||||
getFillColor: this.props.getFillColor ?? this.props.getColor, // CHANGE: Can now be function d => d.value1 * ...
|
||||
getLineColor: this.props.getLineColor,
|
||||
getLineWidth: this.props.getLineWidth,
|
||||
|
||||
updateTriggers: {
|
||||
getPosition: this.props.updateTriggers.getPosition,
|
||||
getRadius: this.props.updateTriggers.getRadius,
|
||||
getColor: this.props.updateTriggers.getColor,
|
||||
getFillColor: this.props.updateTriggers.getFillColor,
|
||||
getLineColor: this.props.updateTriggers.getLineColor,
|
||||
getLineWidth: this.props.updateTriggers.getLineWidth,
|
||||
}
|
||||
}));
|
||||
|
||||
subLayers.push(subLayer);
|
||||
}
|
||||
|
||||
// console.log(`Rendering ${subLayers.length} sublayers`);
|
||||
|
||||
return subLayers;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
function GunPressureAvgHeatmap (data, options = {}) {
|
||||
let id = options.id
|
||||
? options.id
|
||||
: "gun-pressure-avg-heatmap"+Math.random().toString().slice(2);
|
||||
|
||||
return new HeatmapLayer({
|
||||
id,
|
||||
data: {
|
||||
length,
|
||||
attributes: {
|
||||
getPosition,
|
||||
getWeight
|
||||
}
|
||||
}
|
||||
})
|
||||
}*/
|
||||
|
||||
export default DougalSequenceLayer;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
|
||||
import DougalSequenceLayer from './DougalSequenceLayer'
|
||||
import DougalEventsLayer from './DougalEventsLayer'
|
||||
|
||||
export {
|
||||
DougalSequenceLayer
|
||||
DougalSequenceLayer,
|
||||
DougalEventsLayer
|
||||
};
|
||||
|
||||
@@ -96,15 +96,6 @@
|
||||
<label for="lyr-crosshairs" title="Show or hide the crosshairs position marker">Crosshairs marker</label>
|
||||
</template>
|
||||
</form>
|
||||
<!--
|
||||
<h3 class="mt-3" title="At least one of raw or final will always be selected">Sequence data</h3>
|
||||
<form>
|
||||
<input id="sd-raw" type="checkbox" value="R" v-model="sequenceDataTypes"/>
|
||||
<label for="sd-raw">Show raw</label>
|
||||
<input id="sd-final" type="checkbox" value="F" v-model="sequenceDataTypes"/>
|
||||
<label for="sd-final">Show final</label>
|
||||
</form>
|
||||
-->
|
||||
|
||||
<!-- QC data: This section is meant to show (some) QC results in a graphical way,
|
||||
as 3D columns with lengths proportional to the QC values. Not implemented
|
||||
@@ -365,14 +356,13 @@ export default {
|
||||
return {
|
||||
layerSelection: [],
|
||||
layersAvailable: {},
|
||||
sequenceDataTypes: [ "P", "F" ],
|
||||
sequenceDataElements: [],
|
||||
sequenceDataTStamp: null,
|
||||
loadingProgress: null,
|
||||
viewState: {},
|
||||
viewStateDefaults: {
|
||||
//maxZoom: 18,
|
||||
//maxPitch: 85
|
||||
maxPitch: 89
|
||||
},
|
||||
|
||||
crosshairsPosition: [],
|
||||
@@ -383,27 +373,6 @@ export default {
|
||||
filterVisible: false,
|
||||
|
||||
error: null,
|
||||
/*
|
||||
layerDefinitions: [
|
||||
{
|
||||
id: "osm",
|
||||
name: "OpenStreetMap"
|
||||
},
|
||||
{
|
||||
id: "sea",
|
||||
name: "OpenSeaMap"
|
||||
},
|
||||
{
|
||||
id: "nau",
|
||||
name: "Nautical charts (NO)",
|
||||
title: "Scan of Norway's nautical charts"
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
|
||||
|
||||
]
|
||||
*/
|
||||
};
|
||||
},
|
||||
|
||||
@@ -413,129 +382,96 @@ export default {
|
||||
return this.sequenceDataElements?.map( el => el.data );
|
||||
},
|
||||
|
||||
sequenceBinaryData () {
|
||||
return this.sequenceDataElements?.sort( (a, b) => a-b )?.map( ({data}) => {
|
||||
sequenceBinaryData() {
|
||||
const sequences = this.sequenceDataElements?.sort((a, b) => a.sequence - b.sequence) || [];
|
||||
if (!sequences.length) {
|
||||
console.warn('No sequence data available');
|
||||
return { positions: new Float32Array(0), values: [], udv: 2 };
|
||||
}
|
||||
|
||||
const bundle = DougalBinaryBundle.clone(data);
|
||||
|
||||
// Total point count
|
||||
const totalCount = bundle.chunks().reduce( (acc, cur) => acc += cur.jCount, 0 );
|
||||
|
||||
// There is an implicit assumption that all chunks in a bundle
|
||||
// have the same ΔelemCount, elemCount, and udv.
|
||||
const ΔelemCount = bundle.chunks()[0].ΔelemCount;
|
||||
const elemCount = bundle.chunks()[0].elemCount;
|
||||
const udv = bundle.chunks()[0].udv;
|
||||
|
||||
const ΣelemCount = ΔelemCount + elemCount + 2;
|
||||
|
||||
const values = new Array(ΣelemCount);
|
||||
values[0] = new Uint16Array(totalCount); // i values
|
||||
values[1] = new Uint32Array(totalCount); // j values
|
||||
|
||||
const positions = new Float32Array(totalCount*2);
|
||||
|
||||
console.log(`totalCount = ${totalCount}, ΔelemCount = ${ΔelemCount}, elemCount = ${elemCount}, ΣelemCount = ${ΣelemCount}`);
|
||||
|
||||
let offset = 0;
|
||||
for (const chunk of bundle.chunks()) {
|
||||
let k = 0;
|
||||
|
||||
console.log(`offset = ${offset}, k = ${k}, jCount = ${chunk.jCount}`);
|
||||
// Populate the positions, which are assumed to always be at indices 0, 1
|
||||
|
||||
const λarray = chunk.elem(0);
|
||||
const φarray = chunk.elem(1);
|
||||
|
||||
// Interleave lons and lats
|
||||
for (let i = 0; i < λarray.length; i++) {
|
||||
positions[offset*2 + i*2] = λarray[i];
|
||||
positions[offset*2 + i*2+1] = φarray[i]
|
||||
}
|
||||
|
||||
|
||||
// Populate the i values
|
||||
//console.log(`values[${k}]: ${values[k] ? "✔" : "✘"}`);
|
||||
values[k++].set((new Uint16Array(chunk.jCount)).fill(chunk.i), offset);
|
||||
|
||||
// Populate the j values
|
||||
//console.log(`values[${k}]: ${values[k] ? "✔" : "✘"}`);
|
||||
values[k++].set(Uint32Array.from({length: chunk.jCount}, (_, i) => chunk.j0 + i * chunk.Δj), offset);
|
||||
|
||||
|
||||
// Populate values with each of Δelem first followed by elem
|
||||
|
||||
for (let j = 0; j < ΔelemCount; j++) {
|
||||
const buffer = chunk.Δelem(j);
|
||||
//console.log(`values[${k}]: ${values[k] ? "✔" : "✘"}`);
|
||||
//console.log(`values[${k}] ← ΔelemCount(${j}) ${buffer.constructor.name}`);
|
||||
if (!values[k]) values[k] = new buffer.constructor(totalCount);
|
||||
values[k++].set(buffer, offset);
|
||||
}
|
||||
|
||||
// Start at 2 as 0, 1 are assumed to be λ, φ
|
||||
for (let j = 2; j < elemCount; j++) {
|
||||
const buffer = chunk.elem(j);
|
||||
//console.log(`values[${k}]: ${values[k] ? "✔" : "✘"}`);
|
||||
//console.log(`values[${k}] ← elemCount(${j}) ${buffer.constructor.name}`);
|
||||
if (!values[k]) values[k] = new buffer.constructor(totalCount);
|
||||
values[k++].set(buffer, offset);
|
||||
}
|
||||
|
||||
offset += chunk.jCount;
|
||||
|
||||
if (offset > totalCount) throw new Error('Overflow condition: offset > totalCount');
|
||||
// Validate first sequence to get array sizes
|
||||
let firstBundle;
|
||||
try {
|
||||
firstBundle = DougalBinaryBundle.clone(sequences[0].data);
|
||||
if (!firstBundle.chunks || typeof firstBundle.chunks !== 'function') {
|
||||
throw new Error('Invalid DougalBinaryBundle: chunks method missing');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to process first sequence:', e);
|
||||
return { positions: new Float32Array(0), values: [], udv: 2 };
|
||||
}
|
||||
|
||||
return { positions, values, udv };
|
||||
const totalCount = sequences.reduce((acc, { data }) => {
|
||||
try {
|
||||
const bundle = DougalBinaryBundle.clone(data);
|
||||
return acc + bundle.chunks().reduce((sum, chunk) => sum + chunk.jCount, 0);
|
||||
} catch (e) {
|
||||
console.warn('Skipping invalid sequence data:', e);
|
||||
return acc;
|
||||
}
|
||||
}, 0);
|
||||
|
||||
if (totalCount === 0) {
|
||||
console.warn('No valid points found in sequences');
|
||||
return { positions: new Float32Array(0), values: [], udv: 2 };
|
||||
}
|
||||
|
||||
const ΔelemCount = firstBundle.chunks()[0].ΔelemCount;
|
||||
const elemCount = firstBundle.chunks()[0].elemCount;
|
||||
const positions = new Float32Array(totalCount * 2);
|
||||
const values = new Array(ΔelemCount + elemCount + 2);
|
||||
for (let k = 0; k < values.length; k++) {
|
||||
values[k] = new (k === 0 ? Uint16Array : k === 1 ? Uint32Array : k === 2 ? BigUint64Array : Float32Array)(totalCount);
|
||||
}
|
||||
|
||||
let offset = 0;
|
||||
let udv = 2;
|
||||
sequences.forEach(({ data, sequence }) => {
|
||||
try {
|
||||
const bundle = DougalBinaryBundle.clone(data);
|
||||
const chunks = bundle.chunks();
|
||||
if (!chunks.length) {
|
||||
console.warn(`No chunks in sequence ${sequence}`);
|
||||
return;
|
||||
}
|
||||
udv = chunks[0].udv;
|
||||
|
||||
let chunkOffset = offset;
|
||||
for (const chunk of chunks) {
|
||||
const λarray = chunk.elem(0);
|
||||
const φarray = chunk.elem(1);
|
||||
for (let i = 0; i < λarray.length; i++) {
|
||||
positions[chunkOffset * 2 + i * 2] = λarray[i];
|
||||
positions[chunkOffset * 2 + i * 2 + 1] = φarray[i];
|
||||
}
|
||||
|
||||
values[0].set(new Uint16Array(chunk.jCount).fill(chunk.i), chunkOffset);
|
||||
values[1].set(Uint32Array.from({ length: chunk.jCount }, (_, i) => chunk.j0 + i * chunk.Δj), chunkOffset);
|
||||
|
||||
for (let j = 0; j < chunk.ΔelemCount; j++) {
|
||||
values[2 + j].set(chunk.Δelem(j), chunkOffset);
|
||||
}
|
||||
for (let j = 2; j < chunk.elemCount; j++) {
|
||||
values[2 + chunk.ΔelemCount + j - 2].set(chunk.elem(j), chunkOffset);
|
||||
}
|
||||
|
||||
chunkOffset += chunk.jCount;
|
||||
}
|
||||
|
||||
offset += chunkOffset - offset;
|
||||
} catch (e) {
|
||||
console.warn(`Error processing sequence ${sequence}:`, e);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
sequenceDataPreplots () {
|
||||
return {
|
||||
tstamp: this.sequenceDataTStamp,
|
||||
sequences: this.sequenceDataElements.filter(i => i.type == "P")
|
||||
};
|
||||
},
|
||||
if (offset !== totalCount) {
|
||||
console.warn(`Offset mismatch: ${offset} ≠ ${totalCount}`);
|
||||
}
|
||||
|
||||
sequenceDataRaw () {
|
||||
return {
|
||||
tstamp: this.sequenceDataTStamp,
|
||||
sequences: this.sequenceDataElements.filter(i => i.type == "R")
|
||||
};
|
||||
console.log(`Concatenated ${totalCount} points, ${values.length} value arrays`);
|
||||
return { positions, values, udv };
|
||||
},
|
||||
|
||||
sequenceDataFinal () {
|
||||
return {
|
||||
tstamp: this.sequenceDataTStamp,
|
||||
sequences: this.sequenceDataElements.filter(i => i.type == "F")
|
||||
};
|
||||
},
|
||||
|
||||
sequenceDataPostplots () {
|
||||
const fn = i => i.type != "P" && this.sequenceDataTypes.includes(i.type);
|
||||
return {
|
||||
tstamp: this.sequenceDataTStamp,
|
||||
sequences: this.sequenceDataElements.filter(fn)
|
||||
};
|
||||
},
|
||||
*/
|
||||
|
||||
/*
|
||||
fullProspectRaw () {
|
||||
// Get raw data, so type = "R"
|
||||
const sequences = this.sequenceDataElements.filter( i => i.type == "R" );
|
||||
return [...sequences.map( i => toJSON(i.data) )].flat();
|
||||
},
|
||||
|
||||
fullProspectFinal () {
|
||||
// Get raw data, so type = "R"
|
||||
const sequences = this.sequenceDataElements.filter( i => i.type == "F" );
|
||||
return [...sequences.map( i => toJSON(i.data) )].flat();
|
||||
},
|
||||
*/
|
||||
|
||||
...mapGetters(['user', 'loading', 'serverEvent', 'lineName', 'serverEvent']),
|
||||
...mapState({projectSchema: state => state.project.projectSchema})
|
||||
},
|
||||
@@ -552,31 +488,12 @@ export default {
|
||||
deep: true
|
||||
},
|
||||
|
||||
sequenceDataTypes (val, old) {
|
||||
// Ensure that at least one of raw or final is always selected
|
||||
if (!val.includes("R") && !val.includes("F")) {
|
||||
if (old.includes("F")) {
|
||||
this.sequenceDataTypes.push("R");
|
||||
} else {
|
||||
this.sequenceDataTypes.push("F");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
sequenceDataElements () {
|
||||
//console.log("seq data changed");
|
||||
this.sequenceDataTStamp = Date.now();
|
||||
//this.render();
|
||||
},
|
||||
|
||||
sequenceDataPreplots () {
|
||||
this.render();
|
||||
},
|
||||
|
||||
sequenceDataPostplots () {
|
||||
this.render();
|
||||
},
|
||||
|
||||
$route (to, from) {
|
||||
if (to.name == "map") {
|
||||
this.decodeURLHash();
|
||||
@@ -595,6 +512,11 @@ export default {
|
||||
for (const name in this.layersAvailable) {
|
||||
//console.log("Visible", name, this.layerSelection.includes(name));
|
||||
const fn = this.layersAvailable[name];
|
||||
|
||||
if (typeof fn != 'function') {
|
||||
throw new Error(`Layer ${name}: expected a function, got ${typeof fn}`);
|
||||
}
|
||||
|
||||
layers.push(fn({
|
||||
visible: this.layerSelection.includes(name)
|
||||
}));
|
||||
@@ -680,10 +602,10 @@ export default {
|
||||
|
||||
let data = layer.props.data;
|
||||
|
||||
// Handle DougalSequenceLayer (CompositeLayer)
|
||||
// Handle DougalSequenceLayer
|
||||
if (layer.constructor.layerName === 'DougalSequenceLayer') {
|
||||
// Iterate over sequence data (array of {positions, values, udv})
|
||||
data.forEach(({ positions }) => {
|
||||
const { positions } = data;
|
||||
if (positions && positions.length) {
|
||||
for (let i = 0; i < positions.length; i += 2) {
|
||||
const lon = positions[i];
|
||||
const lat = positions[i + 1];
|
||||
@@ -693,8 +615,8 @@ export default {
|
||||
λ1 = Math.max(λ1, lon);
|
||||
φ1 = Math.max(φ1, lat);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (layer.constructor.layerName === 'ScatterplotLayer') {
|
||||
// Direct point data (e.g., navp)
|
||||
if (Array.isArray(data)) {
|
||||
@@ -708,7 +630,7 @@ export default {
|
||||
}
|
||||
});
|
||||
} else if (data && data.attributes && data.attributes.getPosition) {
|
||||
// Binary data (e.g., DougalSequenceLayer sublayers)
|
||||
// Binary data
|
||||
const positions = data.attributes.getPosition.value;
|
||||
for (let i = 0; i < data.length * 2; i += 2) {
|
||||
const lon = positions[i];
|
||||
@@ -723,7 +645,7 @@ export default {
|
||||
}
|
||||
} else if (layer.constructor.layerName === 'GeoJsonLayer') {
|
||||
// Extract points from GeoJSON features
|
||||
let featureCollections = Array.isArray(data) ? data : [data]; // Handle both array and single object
|
||||
let featureCollections = Array.isArray(data) ? data : [data];
|
||||
featureCollections.forEach(item => {
|
||||
let features = item;
|
||||
if (item?.type === 'FeatureCollection') {
|
||||
@@ -882,85 +804,6 @@ export default {
|
||||
console.log("passed for await", deck);
|
||||
},
|
||||
|
||||
dataChunks (lonIndex = 0, latIndex = 1, valueIndex = 1, udvFilter = null) {
|
||||
|
||||
// Collect data from all bundles across all buffers
|
||||
let totalCount = 0;
|
||||
const chunkData = [];
|
||||
let elemCountChecked = false; // To validate indices against elemCount once
|
||||
let minValue = Infinity;
|
||||
let maxValue = -Infinity;
|
||||
const chunkRecordMap = []; // For picking: map point indices to chunk and record index
|
||||
|
||||
for (const buffer of this.sequenceData) {
|
||||
// Clone the buffer into our class
|
||||
const bundle = DougalBinaryBundle.clone(buffer);
|
||||
|
||||
const chunks = bundle.chunks();
|
||||
|
||||
chunks.forEach(chunk => {
|
||||
// Check udv filter
|
||||
if (udvFilter !== null) {
|
||||
let match = false;
|
||||
if (Array.isArray(udvFilter)) {
|
||||
match = udvFilter.includes(chunk.udv);
|
||||
} else if (typeof udvFilter === 'function') {
|
||||
match = udvFilter(chunk.udv);
|
||||
} else {
|
||||
match = chunk.udv === udvFilter;
|
||||
}
|
||||
if (!match) {
|
||||
console.log(`Skipping chunk with udv=${chunk.udv}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Log chunk type for debugging
|
||||
const chunkType = chunk instanceof DougalBinaryChunkSequential ? 'sequential (0x11)' : 'interleaved (0x12)';
|
||||
console.log(`Processing chunk: type=${chunkType}, udv=${chunk.udv}, jCount=${chunk.jCount}`);
|
||||
|
||||
// Validate indices against chunk.elemCount (once, assuming consistent structure)
|
||||
if (!elemCountChecked) {
|
||||
const ec = chunk.elemCount;
|
||||
if (lonIndex < 0 || lonIndex >= ec || latIndex < 0 || latIndex >= ec) {
|
||||
throw new Error(`Invalid lonIndex (${lonIndex}) or latIndex (${latIndex}); must be between 0 and ${ec - 1}`);
|
||||
}
|
||||
if (valueIndex !== null && (valueIndex < 0 || valueIndex >= ec)) {
|
||||
throw new Error(`Invalid valueIndex (${valueIndex}); must be between 0 and ${ec - 1}`);
|
||||
}
|
||||
elemCountChecked = true;
|
||||
}
|
||||
|
||||
const lons = chunk.elem(lonIndex); // Float32Array
|
||||
const lats = chunk.elem(latIndex); // Float32Array
|
||||
|
||||
let values = null;
|
||||
if (valueIndex !== null) {
|
||||
values = chunk.elem(valueIndex); // TypedArray for rendering value
|
||||
// Clamp negative values for radius
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (values[i] < 0) {
|
||||
console.warn(`Negative value (${values[i]}) at valueIndex ${valueIndex}; clamping to 0 for radius`);
|
||||
values[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const count = chunk.jCount;
|
||||
// Map point indices to chunk and record index for picking
|
||||
for (let i = 0; i < count; i++) {
|
||||
chunkRecordMap[totalCount + i] = { chunk, recordIndex: i };
|
||||
}
|
||||
totalCount += count;
|
||||
|
||||
chunkData.push({ lons, lats, values, count });
|
||||
});
|
||||
}
|
||||
|
||||
// Update state with chunkRecordMap for getPickingInfo
|
||||
return chunkRecordMap;
|
||||
},
|
||||
|
||||
async initLayers (gl) {
|
||||
//console.log("SHOULD BE INITIALISING LAYERS HERE", gl);
|
||||
this.decodeURL();
|
||||
@@ -1096,25 +939,6 @@ export default {
|
||||
console.log("TODO: Should switch to legacy map view");
|
||||
}
|
||||
|
||||
const COLOR_SCALE = [
|
||||
// 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]
|
||||
];
|
||||
|
||||
this.layersAvailable.osm = this.osmLayer;
|
||||
|
||||
this.layersAvailable.sea = this.openSeaMapLayer;
|
||||
@@ -1137,7 +961,9 @@ export default {
|
||||
|
||||
this.layersAvailable.seqrp = this.rawSequencesPointsLayer;
|
||||
|
||||
//this.layersAvailable.seqrp = this.rawSequencesPointsGunDataLayer;
|
||||
//this.layersAvailable.seqfp = this.rawSequencesPointsGunDataLayer;
|
||||
|
||||
this.layersAvailable.seqrh = this.rawSequencesIJErrorLayer;
|
||||
|
||||
this.layersAvailable.crosshairs = (options = {}) => {
|
||||
return new IconLayer({
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// https://deck.gl/docs/developer-guide/performance#supply-attributes-directly
|
||||
|
||||
import { Deck, WebMercatorViewport, FlyToInterpolator, CompositeLayer } from '@deck.gl/core';
|
||||
import { GeoJsonLayer, LineLayer, BitmapLayer, ScatterplotLayer, IconLayer } from '@deck.gl/layers';
|
||||
import { GeoJsonLayer, LineLayer, BitmapLayer, ScatterplotLayer, ColumnLayer, IconLayer } from '@deck.gl/layers';
|
||||
import {HeatmapLayer} from '@deck.gl/aggregation-layers';
|
||||
import { TileLayer, MVTLayer } from '@deck.gl/geo-layers';
|
||||
|
||||
@@ -13,7 +13,7 @@ import * as d3a from 'd3-array';
|
||||
|
||||
import { DougalBinaryBundle, DougalBinaryChunkSequential, DougalBinaryChunkInterleaved } from '@dougal/binary';
|
||||
import { DougalShotLayer } from '@/lib/deck.gl';
|
||||
import DougalSequenceLayer from '@/lib/deck.gl/DougalSequenceLayer';
|
||||
import { DougalSequenceLayer, DougalEventsLayer } from '@/lib/deck.gl';
|
||||
|
||||
import { colors } from 'vuetify/lib'
|
||||
|
||||
@@ -53,6 +53,29 @@ export default {
|
||||
|
||||
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]
|
||||
]
|
||||
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
@@ -63,6 +86,13 @@ export default {
|
||||
|
||||
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",
|
||||
@@ -90,7 +120,7 @@ export default {
|
||||
|
||||
|
||||
// OSM tiles layer. Handy to make water transparent
|
||||
// but super reliable yet
|
||||
// but not super reliable yet
|
||||
|
||||
osmVectorLayer (options = {}) {
|
||||
return new MVTLayer({
|
||||
@@ -225,35 +255,48 @@ export default {
|
||||
|
||||
eventsLogLayer (options = {}) {
|
||||
|
||||
const labelColour = (d, i) => {
|
||||
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] == "#") {
|
||||
return hexToArray(colour);
|
||||
c = hexToArray(colour);
|
||||
} else {
|
||||
return namedColourToArray(colour);
|
||||
c = namedColourToArray(colour);
|
||||
}
|
||||
} else {
|
||||
return [127, 65, 90];
|
||||
//return [127, 65, 90];
|
||||
}
|
||||
|
||||
if (t != null) {
|
||||
c[3] = t;
|
||||
}
|
||||
|
||||
return c;
|
||||
};
|
||||
|
||||
return new GeoJsonLayer({
|
||||
return new DougalEventsLayer({
|
||||
id: 'log',
|
||||
data: `/api/project/${this.$route.params.project}/event?mime=application/geo%2Bjson`,
|
||||
lineWidthMinPixels: 2,
|
||||
//getLineColor: [127, 65, 90],
|
||||
getFillColor: (d, i) => labelColour(d, i),
|
||||
getLineColor: (d, i) => labelColour(d, i),
|
||||
getPointRadius: 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",
|
||||
pointRadiusMinPixels: 2,
|
||||
radiusMinPixels: 1.5,
|
||||
radiusMaxPixels: 2.5,
|
||||
|
||||
pickable: true,
|
||||
...options
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
preplotSaillinesLinesLayer (options = {}) {
|
||||
@@ -344,9 +387,51 @@ export default {
|
||||
},
|
||||
|
||||
rawSequencesPointsLayer (options = {}) {
|
||||
const { positions, values, udv } = this.sequenceBinaryData;
|
||||
if (!positions.length || !values.length) {
|
||||
console.warn('No valid data for seqrp');
|
||||
|
||||
return new DougalSequenceLayer({
|
||||
id: 'seqrp',
|
||||
data: { length: 0, attributes: {} },
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
const pointCount = positions.length / 2;
|
||||
if (!values.every(arr => arr.length === pointCount)) {
|
||||
console.warn(`Length mismatch in seqrp: positions/2 (${pointCount}) ≠ values (${values.map(arr => arr.length)})`);
|
||||
}
|
||||
|
||||
const attributes = {
|
||||
getPosition: {
|
||||
value: positions,
|
||||
type: 'float32',
|
||||
size: 2
|
||||
},
|
||||
udv
|
||||
};
|
||||
|
||||
values.forEach((valArray, k) => {
|
||||
let value = valArray;
|
||||
if (valArray instanceof BigUint64Array) {
|
||||
value = Float64Array.from(valArray, v => Number(v));
|
||||
}
|
||||
attributes[`value${k}`] = {
|
||||
value,
|
||||
type: value instanceof Float64Array ? 'float64' :
|
||||
value instanceof Uint16Array ? 'uint16' :
|
||||
value instanceof Uint32Array ? 'uint32' : 'float32',
|
||||
size: 1
|
||||
};
|
||||
});
|
||||
|
||||
return new DougalSequenceLayer({
|
||||
id: 'seqrp',
|
||||
data: this.sequenceBinaryData,
|
||||
data: {
|
||||
length: pointCount,
|
||||
attributes
|
||||
},
|
||||
getRadius: 2,
|
||||
getFillColor: [0, 120, 220, 200],
|
||||
pickable: true,
|
||||
@@ -354,23 +439,54 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
rawSequencesPointsGunDataLayer (options = {}) {
|
||||
return new DougalSequenceLayer({
|
||||
id: 'seqrp',
|
||||
data: this.sequenceBinaryData,
|
||||
getRadius: (d, i) => i?.data?.attributes?.value9.value[i?.index],
|
||||
//getRadius: (d, i) => {console.log(d, i); return 1;},
|
||||
//getRadius: 2,
|
||||
radiusMinPixels: 1,
|
||||
//autoScale: true,
|
||||
//getFillColor: gunPressureColours,
|
||||
getFillColor: [0, 120, 220, 200],
|
||||
pickable: true,
|
||||
rawSequencesIJErrorLayer(options = {}) {
|
||||
const { positions, values } = this.sequenceBinaryData;
|
||||
if (!positions.length || !values[3] || !values[4]) {
|
||||
console.warn('No valid data for rawSequencesIJErrorLayer');
|
||||
|
||||
return new HeatmapLayer({
|
||||
id: 'seqrh',
|
||||
data: [],
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
const weights = Float32Array.from(values[3], (ei, i) => {
|
||||
const ej = values[4][i];
|
||||
return Math.sqrt(ei * ei + ej * ej) / 10; // Euclidean distance in meters
|
||||
});
|
||||
|
||||
console.log('Positions sample:', positions.slice(0, 10));
|
||||
console.log('Weights sample:', weights.slice(0, 10));
|
||||
console.log('Weight stats:', {
|
||||
min: Math.min(...weights),
|
||||
max: Math.max(...weights),
|
||||
mean: weights.reduce((sum, v) => sum + v, 0) / weights.length
|
||||
});
|
||||
|
||||
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}) => data.weights[index],
|
||||
colorDomain: [2, 20],
|
||||
radiusPixels: 25,
|
||||
aggregation: 'MEAN',
|
||||
pickable: false,
|
||||
...options
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user