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:
D. Berge
2025-08-02 16:00:54 +02:00
parent 44113c89c0
commit 2bcdee03d5
5 changed files with 421 additions and 438 deletions

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

View File

@@ -1,12 +1,11 @@
// Ref.: https://deck.gl/docs/developer-guide/custom-layers/composite-layers // Ref.: https://deck.gl/docs/developer-guide/custom-layers/layer-lifecycle
import { CompositeLayer } from '@deck.gl/core'; import { ScatterplotLayer } from '@deck.gl/layers';
import { GeoJsonLayer, LineLayer, BitmapLayer, ScatterplotLayer, IconLayer } from '@deck.gl/layers';
import {HeatmapLayer} from '@deck.gl/aggregation-layers';
class DougalSequenceLayer extends CompositeLayer { class DougalSequenceLayer extends ScatterplotLayer {
static layerName = "DougalSequenceLayer"; static layerName = "DougalSequenceLayer";
static defaultProps = { static defaultProps = {
...ScatterplotLayer.defaultProps,
valueIndex: 0, valueIndex: 0,
radiusUnits: "pixels", radiusUnits: "pixels",
radiusScale: 1, radiusScale: 1,
@@ -18,161 +17,59 @@ class DougalSequenceLayer extends CompositeLayer {
radiusMaxPixels: 50, radiusMaxPixels: 50,
lineWidthMinPixels: 1, lineWidthMinPixels: 1,
lineWidthMaxPixels: 50, lineWidthMaxPixels: 50,
// billboard: this.props.billboard, getPosition: { type: 'accessor', value: d => d.positions },
// antialiasing: this.props.antialiasing,
getPosition: {type: 'accessor', value: d => d.positions},
getRadius: 5, getRadius: 5,
//getColor: this.props.getColor,
getFillColor: [255, 0, 0, 200], getFillColor: [255, 0, 0, 200],
getLineColor: [255, 0, 0, 200], getLineColor: [255, 0, 0, 200],
getLineWidth: 2, getLineWidth: 2,
pickable: true pickable: true
} }
constructor (props) { constructor(props) {
super(props); super(props);
// A unique id (prefix) for this layer
this.uid = "sl-" + Math.random().toString().slice(2);
} }
getPickingInfo({ info, mode, sourceLayer }) { initializeState(context) {
console.log("picking info", info); super.initializeState(context);
}
getPickingInfo({ info, mode }) {
const index = info.index; const index = info.index;
if (index >= 0) { if (index >= 0) {
const d = sourceLayer.props.data.attributes; const d = this.props.data.attributes;
console.log(index, d);
if (d) { if (d) {
info.object = { info.object = {
udv: d.udv ?? 2, // FIXME must add udv: d.udv ?? 2,
i: d.value0.value[index], i: d.value0.value[index],
j: d.value1.value[index], j: d.value1.value[index],
ts: Number(d.value2.value[index]), ts: Number(d.value2.value[index]),
//λ: d.value3[k*2+0], εi: d.value3.value[index] / 10,
//φ: d.value3[k*2+1], εj: d.value4.value[index] / 10,
εi: d.value3.value[index] / 10, delta: d.value5.value[index] / 10,
εj: d.value4.value[index] / 10, delta_σ: d.value6.value[index] / 10,
delta_μ: d.value5.value[index] / 10, delta_R: d.value7.value[index] / 10,
delta_σ: d.value6.value[index] / 10, press_μ: d.value8.value[index],
delta_R: d.value7.value[index] / 10, press_σ: d.value9.value[index],
press_μ: d.value8.value[index], press_R: d.value10.value[index],
press_σ: d.value9.value[index], depth_μ: d.value11.value[index] / 10,
press_R: d.value10.value[index], depth_σ: d.value12.value[index] / 10,
depth_μ: d.value11.value[index] / 10, depth_R: d.value13.value[index] / 10,
depth_σ: d.value12.value[index] / 10, fill_μ: d.value14.value[index],
depth_R: d.value13.value[index] / 10, fill_σ: d.value15.value[index],
fill_μ: d.value14.value[index], fill_R: d.value16.value[index],
fill_σ: d.value15.value[index], delay_μ: d.value17.value[index] / 10,
fill_R: d.value16.value[index], delay_σ: d.value18.value[index] / 10,
delay_μ: d.value17.value[index] / 10, delay_R: d.value19.value[index] / 10,
delay_σ: d.value18.value[index] / 10, nofire: d.value20.value[index] >> 4,
delay_R: d.value19.value[index] / 10,
/*
nofire: d.value20.value[index] >> 4,
autofire: d.value20.value[index] & 0xf autofire: d.value20.value[index] & 0xf
*/ };
} console.log(`Picked sequence ${info.object.i}, point ${info.object.j}, udv ${info.object.udv}`);
} else { } else {
console.log(`No data found index = ${index}`); console.log(`No data found index = ${index}`);
} }
} }
return info; 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; export default DougalSequenceLayer;

View File

@@ -1,6 +1,8 @@
import DougalSequenceLayer from './DougalSequenceLayer' import DougalSequenceLayer from './DougalSequenceLayer'
import DougalEventsLayer from './DougalEventsLayer'
export { export {
DougalSequenceLayer DougalSequenceLayer,
DougalEventsLayer
}; };

View File

@@ -96,15 +96,6 @@
<label for="lyr-crosshairs" title="Show or hide the crosshairs position marker">Crosshairs marker</label> <label for="lyr-crosshairs" title="Show or hide the crosshairs position marker">Crosshairs marker</label>
</template> </template>
</form> </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, <!-- 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 as 3D columns with lengths proportional to the QC values. Not implemented
@@ -365,14 +356,13 @@ export default {
return { return {
layerSelection: [], layerSelection: [],
layersAvailable: {}, layersAvailable: {},
sequenceDataTypes: [ "P", "F" ],
sequenceDataElements: [], sequenceDataElements: [],
sequenceDataTStamp: null, sequenceDataTStamp: null,
loadingProgress: null, loadingProgress: null,
viewState: {}, viewState: {},
viewStateDefaults: { viewStateDefaults: {
//maxZoom: 18, //maxZoom: 18,
//maxPitch: 85 maxPitch: 89
}, },
crosshairsPosition: [], crosshairsPosition: [],
@@ -383,27 +373,6 @@ export default {
filterVisible: false, filterVisible: false,
error: null, 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 ); return this.sequenceDataElements?.map( el => el.data );
}, },
sequenceBinaryData () { sequenceBinaryData() {
return this.sequenceDataElements?.sort( (a, b) => a-b )?.map( ({data}) => { 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); // Validate first sequence to get array sizes
let firstBundle;
// Total point count try {
const totalCount = bundle.chunks().reduce( (acc, cur) => acc += cur.jCount, 0 ); firstBundle = DougalBinaryBundle.clone(sequences[0].data);
if (!firstBundle.chunks || typeof firstBundle.chunks !== 'function') {
// There is an implicit assumption that all chunks in a bundle throw new Error('Invalid DougalBinaryBundle: chunks method missing');
// 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');
} }
} 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);
}
}); });
},
/* if (offset !== totalCount) {
sequenceDataPreplots () { console.warn(`Offset mismatch: ${offset}${totalCount}`);
return { }
tstamp: this.sequenceDataTStamp,
sequences: this.sequenceDataElements.filter(i => i.type == "P")
};
},
sequenceDataRaw () { console.log(`Concatenated ${totalCount} points, ${values.length} value arrays`);
return { return { positions, values, udv };
tstamp: this.sequenceDataTStamp,
sequences: this.sequenceDataElements.filter(i => i.type == "R")
};
}, },
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']), ...mapGetters(['user', 'loading', 'serverEvent', 'lineName', 'serverEvent']),
...mapState({projectSchema: state => state.project.projectSchema}) ...mapState({projectSchema: state => state.project.projectSchema})
}, },
@@ -552,31 +488,12 @@ export default {
deep: true 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 () { sequenceDataElements () {
//console.log("seq data changed"); //console.log("seq data changed");
this.sequenceDataTStamp = Date.now(); this.sequenceDataTStamp = Date.now();
//this.render(); //this.render();
}, },
sequenceDataPreplots () {
this.render();
},
sequenceDataPostplots () {
this.render();
},
$route (to, from) { $route (to, from) {
if (to.name == "map") { if (to.name == "map") {
this.decodeURLHash(); this.decodeURLHash();
@@ -595,6 +512,11 @@ export default {
for (const name in this.layersAvailable) { for (const name in this.layersAvailable) {
//console.log("Visible", name, this.layerSelection.includes(name)); //console.log("Visible", name, this.layerSelection.includes(name));
const fn = this.layersAvailable[name]; const fn = this.layersAvailable[name];
if (typeof fn != 'function') {
throw new Error(`Layer ${name}: expected a function, got ${typeof fn}`);
}
layers.push(fn({ layers.push(fn({
visible: this.layerSelection.includes(name) visible: this.layerSelection.includes(name)
})); }));
@@ -680,10 +602,10 @@ export default {
let data = layer.props.data; let data = layer.props.data;
// Handle DougalSequenceLayer (CompositeLayer) // Handle DougalSequenceLayer
if (layer.constructor.layerName === 'DougalSequenceLayer') { if (layer.constructor.layerName === 'DougalSequenceLayer') {
// Iterate over sequence data (array of {positions, values, udv}) const { positions } = data;
data.forEach(({ positions }) => { if (positions && positions.length) {
for (let i = 0; i < positions.length; i += 2) { for (let i = 0; i < positions.length; i += 2) {
const lon = positions[i]; const lon = positions[i];
const lat = positions[i + 1]; const lat = positions[i + 1];
@@ -693,8 +615,8 @@ export default {
λ1 = Math.max(λ1, lon); λ1 = Math.max(λ1, lon);
φ1 = Math.max(φ1, lat); φ1 = Math.max(φ1, lat);
} }
}; }
}); }
} else if (layer.constructor.layerName === 'ScatterplotLayer') { } else if (layer.constructor.layerName === 'ScatterplotLayer') {
// Direct point data (e.g., navp) // Direct point data (e.g., navp)
if (Array.isArray(data)) { if (Array.isArray(data)) {
@@ -708,7 +630,7 @@ export default {
} }
}); });
} else if (data && data.attributes && data.attributes.getPosition) { } else if (data && data.attributes && data.attributes.getPosition) {
// Binary data (e.g., DougalSequenceLayer sublayers) // Binary data
const positions = data.attributes.getPosition.value; const positions = data.attributes.getPosition.value;
for (let i = 0; i < data.length * 2; i += 2) { for (let i = 0; i < data.length * 2; i += 2) {
const lon = positions[i]; const lon = positions[i];
@@ -723,7 +645,7 @@ export default {
} }
} else if (layer.constructor.layerName === 'GeoJsonLayer') { } else if (layer.constructor.layerName === 'GeoJsonLayer') {
// Extract points from GeoJSON features // 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 => { featureCollections.forEach(item => {
let features = item; let features = item;
if (item?.type === 'FeatureCollection') { if (item?.type === 'FeatureCollection') {
@@ -882,85 +804,6 @@ export default {
console.log("passed for await", deck); 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) { async initLayers (gl) {
//console.log("SHOULD BE INITIALISING LAYERS HERE", gl); //console.log("SHOULD BE INITIALISING LAYERS HERE", gl);
this.decodeURL(); this.decodeURL();
@@ -1096,25 +939,6 @@ export default {
console.log("TODO: Should switch to legacy map view"); 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.osm = this.osmLayer;
this.layersAvailable.sea = this.openSeaMapLayer; this.layersAvailable.sea = this.openSeaMapLayer;
@@ -1137,7 +961,9 @@ export default {
this.layersAvailable.seqrp = this.rawSequencesPointsLayer; this.layersAvailable.seqrp = this.rawSequencesPointsLayer;
//this.layersAvailable.seqrp = this.rawSequencesPointsGunDataLayer; //this.layersAvailable.seqfp = this.rawSequencesPointsGunDataLayer;
this.layersAvailable.seqrh = this.rawSequencesIJErrorLayer;
this.layersAvailable.crosshairs = (options = {}) => { this.layersAvailable.crosshairs = (options = {}) => {
return new IconLayer({ return new IconLayer({

View File

@@ -3,7 +3,7 @@
// https://deck.gl/docs/developer-guide/performance#supply-attributes-directly // https://deck.gl/docs/developer-guide/performance#supply-attributes-directly
import { Deck, WebMercatorViewport, FlyToInterpolator, CompositeLayer } from '@deck.gl/core'; 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 {HeatmapLayer} from '@deck.gl/aggregation-layers';
import { TileLayer, MVTLayer } from '@deck.gl/geo-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 { DougalBinaryBundle, DougalBinaryChunkSequential, DougalBinaryChunkInterleaved } from '@dougal/binary';
import { DougalShotLayer } from '@/lib/deck.gl'; 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' import { colors } from 'vuetify/lib'
@@ -53,6 +53,29 @@ export default {
data () { 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: { computed: {
@@ -63,6 +86,13 @@ export default {
methods: { 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 = {}) { osmLayer (options = {}) {
return new TileLayer({ return new TileLayer({
id: "osm", id: "osm",
@@ -90,7 +120,7 @@ export default {
// OSM tiles layer. Handy to make water transparent // OSM tiles layer. Handy to make water transparent
// but super reliable yet // but not super reliable yet
osmVectorLayer (options = {}) { osmVectorLayer (options = {}) {
return new MVTLayer({ return new MVTLayer({
@@ -225,35 +255,48 @@ export default {
eventsLogLayer (options = {}) { eventsLogLayer (options = {}) {
const labelColour = (d, i) => { const labelColour = (d, i, t, c = [127, 65, 90]) => {
const label = d?.properties?.labels?.[0]; const label = d?.properties?.labels?.[0];
const colour = this.labels[label]?.view?.colour ?? "#cococo"; const colour = this.labels[label]?.view?.colour ?? "#cococo";
if (colour) { if (colour) {
if (colour[0] == "#") { if (colour[0] == "#") {
return hexToArray(colour); c = hexToArray(colour);
} else { } else {
return namedColourToArray(colour); c = namedColourToArray(colour);
} }
} else { } 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', id: 'log',
data: `/api/project/${this.$route.params.project}/event?mime=application/geo%2Bjson`, data: `/api/project/${this.$route.params.project}/event?mime=application/geo%2Bjson`,
lineWidthMinPixels: 2, lineWidthMinPixels: 2,
//getLineColor: [127, 65, 90], getPosition: d => d.geometry.coordinates,
getFillColor: (d, i) => labelColour(d, i), jitter: 0.00015,
getLineColor: (d, i) => labelColour(d, i), getElevation: d => Math.min(Math.max(d.properties.remarks?.length || 10, 10), 200),
getPointRadius: 2, 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", radiusUnits: "pixels",
pointRadiusMinPixels: 2, radiusMinPixels: 1.5,
radiusMaxPixels: 2.5,
pickable: true, pickable: true,
...options ...options
}) })
}, },
preplotSaillinesLinesLayer (options = {}) { preplotSaillinesLinesLayer (options = {}) {
@@ -344,9 +387,51 @@ export default {
}, },
rawSequencesPointsLayer (options = {}) { 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({ return new DougalSequenceLayer({
id: 'seqrp', id: 'seqrp',
data: this.sequenceBinaryData, data: {
length: pointCount,
attributes
},
getRadius: 2, getRadius: 2,
getFillColor: [0, 120, 220, 200], getFillColor: [0, 120, 220, 200],
pickable: true, pickable: true,
@@ -354,23 +439,54 @@ export default {
}); });
}, },
rawSequencesPointsGunDataLayer (options = {}) { rawSequencesIJErrorLayer(options = {}) {
return new DougalSequenceLayer({ const { positions, values } = this.sequenceBinaryData;
id: 'seqrp', if (!positions.length || !values[3] || !values[4]) {
data: this.sequenceBinaryData, console.warn('No valid data for rawSequencesIJErrorLayer');
getRadius: (d, i) => i?.data?.attributes?.value9.value[i?.index],
//getRadius: (d, i) => {console.log(d, i); return 1;}, return new HeatmapLayer({
//getRadius: 2, id: 'seqrh',
radiusMinPixels: 1, data: [],
//autoScale: true, ...options
//getFillColor: gunPressureColours, });
getFillColor: [0, 120, 220, 200], }
pickable: true,
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 ...options
}); });
}, },
}, },
} }