-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbasic.js
More file actions
137 lines (119 loc) · 7 KB
/
Copy pathbasic.js
File metadata and controls
137 lines (119 loc) · 7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// A tour of the in-process index. Run it: node examples/basic.js
import { NetCluster } from '../src/index.js'; // 'netcluster-js' once installed
const index = new NetCluster({ radius: 40, maxZoom: 16 });
// --- a small fleet around Sao Paulo -----------------------------------------
const FLEET = 1000;
const pos = new Float64Array(FLEET * 2);
for (let i = 0; i < FLEET; i++) {
pos[i * 2] = -46.63 + (Math.random() - 0.5) * 0.25;
pos[i * 2 + 1] = -23.55 + (Math.random() - 0.5) * 0.25;
index.insert(`vehicle-${i}`, pos[i * 2], pos[i * 2 + 1], { plate: `ABC-${String(i).padStart(4, '0')}` });
}
console.log(`${index.size} vehicles indexed`);
// --- devices report new positions. No rebuild, ever. -------------------------
index.moveTo('vehicle-0', -46.64, -23.56); // this is the whole API for it
// Timed over many reports, because one call is mostly JIT warm-up. Each vehicle
// creeps a few metres, which is what a real position report looks like -- a
// random teleport across the whole city would cost several times more.
const STEP = 12 / 111320; // ~12 metres, in degrees
const report = (i) => {
pos[i * 2] += (Math.random() - 0.5) * 2 * STEP;
pos[i * 2 + 1] += (Math.random() - 0.5) * 2 * STEP;
index.moveTo(`vehicle-${i}`, pos[i * 2], pos[i * 2 + 1]);
};
const MOVES = 50_000;
for (let i = 0; i < MOVES; i++) report(i % FLEET); // warm up
const t0 = process.hrtime.bigint();
for (let i = 0; i < MOVES; i++) report(i % FLEET);
console.log(`${MOVES.toLocaleString()} position reports at ` +
`${(Number(process.hrtime.bigint() - t0) / 1000 / MOVES).toFixed(2)} us each, no rebuild`);
// --- what to draw in this viewport, at this zoom ------------------------------
const bbox = [-47, -24, -46, -23];
for (const z of [8, 11, 14]) {
const features = index.getClusters(bbox, z);
const markers = features.filter(f => f.properties.cluster).length;
const singles = features.length - markers;
console.log(`zoom ${String(z).padStart(2)}: ${features.length} features ` +
`(${markers} clusters + ${singles} single vehicles)`);
}
// --- drill into one cluster ---------------------------------------------------
// A cluster id is read off a feature. It is NOT a device id.
const clusters = index.getClusters(bbox, 11);
const cluster = clusters.find(f => f.properties.cluster);
const clusterId = cluster.properties.cluster_id;
console.log(`\ncluster ${clusterId} holds ${cluster.properties.point_count} vehicles`);
console.log(` splits apart at zoom ${index.getClusterExpansionZoom(clusterId)}`);
console.log(` breaks into ${index.getChildren(clusterId).length} sub-clusters`);
console.log(` first 3 members: ${index.getLeaves(clusterId, 3).map(f => f.properties.plate).join(', ')}`);
// --- which marker is a given vehicle drawn inside? ----------------------------
console.log(`\nvehicle-0 is drawn inside cluster ${index.representative('vehicle-0', 11)} at zoom 11`);
// --- vector tile ---------------------------------------------------------------
const tile = index.getTile(11, 758, 1161);
console.log(`tile 11/758/1161: ${tile ? tile.features.length + ' features' : 'empty'}`);
// --- a device goes offline -----------------------------------------------------
index.remove('vehicle-0');
console.log(`\nafter removing vehicle-0: ${index.size} vehicles`);
console.log(`representative() now returns ${index.representative('vehicle-0', 11)} (gone)`);
// --- GeoJSON in, GeoJSON out ---------------------------------------------------
// Whatever is producing your points -- a .geojson file, a PostGIS query, a
// Mapbox source -- is probably already emitting this shape. load() reads it and
// drops the wrappers, so the index costs what it would have cost had you called
// insert() directly.
index.load({
type: 'FeatureCollection',
features: [
{ type: 'Feature', id: 'van-1', properties: { plate: 'GEO-001' },
geometry: { type: 'Point', coordinates: [-46.6400, -23.5600] } },
// properties may be null, and a third coordinate is altitude: allowed by the
// spec, ignored by clustering
{ type: 'Feature', id: 'van-2', properties: null,
geometry: { type: 'Point', coordinates: [-46.6410, -23.5610, 720] } },
],
});
console.log(`\nafter load(): ${index.size} vehicles, van-1 registered = ${index.has('van-1')}`);
// getClusters returns bare features; getFeatureCollection wraps them, which is
// what map.getSource(id).setData() and L.geoJSON() want.
const fc = index.getFeatureCollection(bbox, 11);
console.log(`getFeatureCollection: ${fc.type} of ${fc.features.length} features`);
// --- filtering on more than one property ---------------------------------------
// The map needs "show me client 7's vehicles that are en route". That is two
// filters at once, and a vehicle can belong to several clients, so neither fits a
// single category. Declare the properties and the combinations you will ask for.
const fleet = new NetCluster({
radius: 40,
maxZoom: 16,
dimensions: {
// `multi` because one vehicle can be operated for several clients
client: { values: 40, multi: true },
status: ['idle', 'enroute', 'loading'],
},
// The combinations a query may name. Each one is stored separately, so this
// list is what filtering costs -- declare the ones your UI actually offers.
filters: [['client'], ['status'], ['client', 'status']],
});
fleet.insert('truck-1', -46.6333, -23.5505, { client: [7, 22], status: 'enroute' });
fleet.insert('truck-2', -46.6340, -23.5510, { client: [7], status: 'idle' });
fleet.insert('truck-3', -46.6350, -23.5520, { client: [3], status: 'enroute' });
const total = (fs) => fs.reduce((a, f) => a + (f.properties.point_count ?? 1), 0);
const world = [-180, -85, 180, 85];
console.log('\nfiltering:');
console.log(` everything ${total(fleet.getClusters(world, 16))}`);
console.log(` client 7 ${total(fleet.getClusters(world, 16, { client: 7 }))}`);
console.log(` en route ${total(fleet.getClusters(world, 16, { status: 'enroute' }))}`);
console.log(` client 7 AND en route ${total(fleet.getClusters(world, 16, { client: 7, status: 'enroute' }))}`);
// A status change does not move the vehicle, so report it at the same position
// with the new value and the index re-files it.
fleet.moveTo('truck-2', -46.6340, -23.5510, { client: [7], status: 'enroute' });
console.log(` after truck-2 departs ${total(fleet.getClusters(world, 16, { client: 7, status: 'enroute' }))}`);
// A bare position report keeps the values it already had -- positions arrive far
// more often than values change, and re-sending them every time would be wasteful
// and easy to get wrong.
fleet.moveTo('truck-2', -46.6341, -23.5511);
console.log(` after it moves again ${total(fleet.getClusters(world, 16, { client: 7, status: 'enroute' }))}`);
// Asking for a combination you did not declare is an error, never an empty map:
// a filter that silently matches nothing looks exactly like a quiet fleet.
try {
fleet.getClusters(world, 16, { plate: 'ABC1234' });
} catch (e) {
console.log(` undeclared filter ${e.message.slice(0, 60)}...`);
}