From fd0908ff3725e1a2f48f003d8333aa5f85f74e99 Mon Sep 17 00:00:00 2001 From: melitele Date: Sun, 18 Jan 2026 00:31:03 -0700 Subject: [PATCH] use `updateData` method of GeoJSON source to handle updates --- lib/feature/source.js | 122 ++++++++++++++++++++++++++++++------- test/feature/source.js | 134 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 227 insertions(+), 29 deletions(-) diff --git a/lib/feature/source.js b/lib/feature/source.js index 06817eb..80886ab 100644 --- a/lib/feature/source.js +++ b/lib/feature/source.js @@ -1,54 +1,130 @@ -const sources = new Map(); +const sources = new Set(); const updates = new Map(); -function updateMap(map, id, data, timeout = 0) { - if (updates.get(id)) { +function updateMap(map, id, { data, diff, add, remove } = {}, timeout = 0) { + const update = updates.get(id); + if (update) { + if (data) { + delete update.diff; + update.data = data; + return; + } + if (add) { + if (!update.diff) { + update.diff = { add: [add] }; + return; + } + diffAdd(update.diff, add); + return; + } + if (remove) { + if (!update.diff) { + update.diff = { remove: [remove] }; + return; + } + diffRemove(update.diff, remove); + return; + } return; } - updates.set( - id, - setTimeout(() => { + if (add || remove) { + diff = {}; + if (add) { + diff.add = [add]; + } + if (remove) { + diff.remove = [remove]; + } + } + updates.set(id, { + data, + diff, + timeout: setTimeout(() => { + const { data, diff } = updates.get(id); updates.delete(id); if (!map?._m) { // there is no map instance + sources.delete(id); return; } const source = map._m.getSource(id); if (!source) { // map is not ready yet - return updateMap(map, id, data, 2 * timeout + 100); + return updateMap(map, id, { data, diff }, 2 * timeout + 100); + } + if (!(data || diff)) { + source.updateData({}); + return; + } + if (data) { + source.setData(data); + } + if (diff) { + source.updateData(diff); } - source.setData(data); }, timeout) - ); + }); } export function addToSource(map, id, feature) { - let data = sources.get(id); - if (!data) { - data = { + if (!sources.has(id)) { + sources.add(id); + const data = { type: 'FeatureCollection', features: [] }; - sources.set(id, data); + updateMap(map, id, { data }); } - data.features.push(feature); - updateMap(map, id, data); + updateMap(map, id, { add: feature }); } export function removeFromSource(map, id, featureId) { - const data = sources.get(id); - if (!data) { + if (!sources.has(id)) { return; } - const idx = data.features.findIndex(f => f.id === featureId); - if (idx === -1) { + updateMap(map, id, { remove: featureId }); +} + +export function refresh(map) { + sources.forEach(id => updateMap(map, id)); +} + +function diffAdd(diff, feature) { + const index = diff.remove?.indexOf(feature.id); + if (index > -1) { + diff.remove.splice(index, 1); + if (diff.remove.length === 0) { + delete diff.remove; + } + diff.update ??= []; + diff.update.push({ + id: feature.id, + newGeometry: feature.geometry, + removeAllProperties: true, + addOrUpdateProperties: Object.entries(feature.properties || {}).map(([key, value]) => ({ key, value })) + }); return; } - data.features.splice(idx, 1); - updateMap(map, id, data); + diff.add ??= []; + diff.add.push(feature); } -export function refresh(map) { - sources.forEach((data, id) => updateMap(map, id, data)); +function diffRemove(diff, featureId) { + const indexAdd = diff.add?.findIndex(f => f.id === featureId); + if (indexAdd > -1) { + diff.add.splice(indexAdd, 1); + if (diff.add.length === 0) { + delete diff.add; + } + return; + } + const indexUpdate = diff.update?.findIndex(f => f.id === featureId); + if (indexUpdate > -1) { + diff.update.splice(indexUpdate, 1); + if (diff.update.length === 0) { + delete diff.update; + } + } + diff.remove ??= []; + diff.remove.push(featureId); } diff --git a/test/feature/source.js b/test/feature/source.js index 188e306..7a29c15 100644 --- a/test/feature/source.js +++ b/test/feature/source.js @@ -1,18 +1,31 @@ import test from 'node:test'; -import { addToSource } from '../../lib/feature/source.js'; +import { addToSource, removeFromSource } from '../../lib/feature/source.js'; -test('addToSource', async t => { - const data = { +const data = [ + { + id: 1, + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [0, 0] + }, + properties: { name: 'Feature 1' } + }, + { + id: 2, type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0] } - }; + } +]; +test('addToSource', async t => { await t.test('map not ready', async t => { let ready = false; let newData; + let newDiff; const map = { _m: { getSource() { @@ -22,19 +35,128 @@ test('addToSource', async t => { return { setData(data) { newData = data; + }, + updateData(diff) { + newDiff = diff; } }; } } }; - addToSource(map, 'source-id', data); + addToSource(map, 'source-id-a-1', data[0]); t.assert.equal(newData, undefined); + t.assert.equal(newDiff, undefined); await new Promise(resolve => setTimeout(resolve, 1)); ready = true; await new Promise(resolve => setTimeout(resolve, 101)); t.assert.deepEqual(newData, { type: 'FeatureCollection', - features: [data] + features: [] }); + t.assert.deepEqual(newDiff, { add: [data[0]] }); + }); + + await t.test('add second feature', async t => { + let newData; + let newDiff; + const map = { + _m: { + getSource() { + return { + setData(data) { + newData = data; + }, + updateData(diff) { + newDiff = diff; + } + }; + } + } + }; + addToSource(map, 'source-id-a-2', data[0]); + addToSource(map, 'source-id-a-2', data[1]); + await new Promise(resolve => setTimeout(resolve, 1)); + t.assert.deepEqual(newData, { + type: 'FeatureCollection', + features: [] + }); + t.assert.deepEqual(newDiff, { add: [data[0], data[1]] }); + }); + + await t.test('add feature immediately after removing', async t => { + let newDiff; + const map = { + _m: { + getSource() { + return { + setData() {}, + updateData(diff) { + newDiff = diff; + } + }; + } + } + }; + addToSource(map, 'source-id-a-3', data[0]); + await new Promise(resolve => setTimeout(resolve, 1)); + removeFromSource(map, 'source-id-a-3', data[0].id); + addToSource(map, 'source-id-a-3', data[0]); + await new Promise(resolve => setTimeout(resolve, 1)); + t.assert.deepEqual(newDiff, { + update: [ + { + addOrUpdateProperties: [{ key: 'name', value: 'Feature 1' }], + id: 1, + newGeometry: { + coordinates: [0, 0], + type: 'Point' + }, + removeAllProperties: true + } + ] + }); + }); +}); + +test('removeFromSource', async t => { + await t.test('remove feature', async t => { + let newDiff; + const map = { + _m: { + getSource() { + return { + setData() {}, + updateData(diff) { + newDiff = diff; + } + }; + } + } + }; + addToSource(map, 'source-id-r-1', data[0]); + await new Promise(resolve => setTimeout(resolve, 1)); + removeFromSource(map, 'source-id-r-1', data[0].id); + await new Promise(resolve => setTimeout(resolve, 1)); + t.assert.deepEqual(newDiff, { remove: [data[0].id] }); + }); + + await t.test('remove feature immediately after adding', async t => { + let newDiff; + const map = { + _m: { + getSource() { + return { + setData() {}, + updateData(diff) { + newDiff = diff; + } + }; + } + } + }; + addToSource(map, 'source-id-r-2', data[0]); + removeFromSource(map, 'source-id-r-2', data[0].id); + await new Promise(resolve => setTimeout(resolve, 1)); + t.assert.deepEqual(newDiff, {}); }); });