Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 99 additions & 23 deletions lib/feature/source.js
Original file line number Diff line number Diff line change
@@ -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);
}
134 changes: 128 additions & 6 deletions test/feature/source.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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, {});
});
});