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

@@ -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({