Skip to content

Commit dedd216

Browse files
committed
add ptas example
1 parent fb6b307 commit dedd216

File tree

8 files changed

+746
-0
lines changed

8 files changed

+746
-0
lines changed

examples/ptas.html

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="utf-8" />
6+
<meta name="viewport" content="width=device-width,initial-scale=1" />
7+
<title>Public Transport Accessibility — Glyph Example</title>
8+
<script src="https://unpkg.com/maplibre-gl@4.0.0/dist/maplibre-gl.js"></script>
9+
<link href="https://unpkg.com/maplibre-gl@4.0.0/dist/maplibre-gl.css" rel="stylesheet" />
10+
<style>
11+
body {
12+
margin: 0;
13+
font-family: Arial, sans-serif;
14+
}
15+
16+
#map {
17+
width: 100vw;
18+
height: 100vh;
19+
}
20+
21+
.controls {
22+
position: absolute;
23+
right: 10px;
24+
top: 10px;
25+
background: white;
26+
padding: 10px;
27+
border-radius: 6px;
28+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
29+
z-index: 1000
30+
}
31+
32+
.controls label {
33+
display: block;
34+
font-size: 13px;
35+
margin-top: 6px
36+
}
37+
38+
.controls input[type=range] {
39+
width: 200px
40+
}
41+
42+
.legend {
43+
margin-top: 8px;
44+
font-size: 12px;
45+
}
46+
47+
.legend .bar {
48+
width: 180px;
49+
height: 12px;
50+
border-radius: 3px;
51+
margin-top: 6px;
52+
border: 1px solid rgba(0, 0, 0, 0.12);
53+
}
54+
55+
.legend .row {
56+
display: flex;
57+
align-items: center;
58+
gap: 8px;
59+
}
60+
61+
.legend .label {
62+
width: 90px;
63+
}
64+
</style>
65+
</head>
66+
67+
<body>
68+
<div id="map"></div>
69+
<div class="controls">
70+
<div><strong>Public Transport Accessibility</strong></div>
71+
<label>Time bin: <span id="minuteLabel">120</span> min</label>
72+
<input id="timeSlider" type="range" min="0" max="7" step="1" value="7" />
73+
<label>Normalization:
74+
<select id="normalizationSelect">
75+
<option value="max-local">Max (local)</option>
76+
<option value="max-global">Max (global)</option>
77+
<option value="z-score">Z-score</option>
78+
<option value="percentile">Percentile</option>
79+
</select>
80+
</label>
81+
<label><input id="cumulativeToggle" type="checkbox" /> Show cumulative layer</label>
82+
<label><input id="sparkToggle" type="checkbox" /> Show sparkline</label>
83+
<div style="margin-top:8px">Glyph size: <input id="glyphSize" type="range" min="0.3" max="1.2" step="0.1"
84+
value="0.8" /></div>
85+
<div class="legend" id="legend">
86+
<div><strong>Legend</strong></div>
87+
<div class="row">
88+
<div class="label">Cells (primary)</div>
89+
<div class="bar" id="legend-primary" style="background: linear-gradient(90deg, rgb(0,200,50), rgb(255,0,50));">
90+
</div>
91+
</div>
92+
<div class="row">
93+
<div class="label">Cumulative</div>
94+
<div class="bar" id="legend-cumulative"
95+
style="background: linear-gradient(90deg, rgb(0,120,255), rgb(200,0,0));"></div>
96+
</div>
97+
<div style="margin-top:6px; font-size:11px; color:#333;">Normalization: <span id="legend-norm">Max (local)</span>
98+
</div>
99+
<div style="margin-top:4px; font-size:11px; color:#333;">Cumulative = per-feature sum across selected time bins,
100+
averaged per cell.</div>
101+
</div>
102+
</div>
103+
104+
<script type="module">
105+
import { ScreenGridLayerGL } from '../src/index.js';
106+
import '../src/glyphs/PublicTransportGlyph.js';
107+
108+
const MINUTES = [15, 30, 45, 60, 75, 90, 105, 120];
109+
110+
const map = new maplibregl.Map({
111+
container: 'map',
112+
style: {
113+
version: 8,
114+
sources: {
115+
'carto-dark': {
116+
type: 'raster',
117+
tiles: [
118+
'https://a.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
119+
'https://b.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
120+
'https://c.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
121+
'https://d.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png'
122+
],
123+
tileSize: 256,
124+
attribution: '© <a href="https://carto.com/">CARTO</a>'
125+
}
126+
},
127+
layers: [{ id: 'carto-dark', type: 'raster', source: 'carto-dark' }]
128+
},
129+
center: [-1.25, 52.5],
130+
zoom: 6
131+
});
132+
133+
const minuteLabel = document.getElementById('minuteLabel');
134+
const timeSlider = document.getElementById('timeSlider');
135+
const sparkToggle = document.getElementById('sparkToggle');
136+
const glyphSizeInput = document.getElementById('glyphSize');
137+
138+
let gridLayer = null;
139+
let cumulativeLayer = null;
140+
let data = null;
141+
let currentGlobalMax = null;
142+
143+
async function loadData() {
144+
const url = 'data/public_transport_accessibility.json';
145+
try {
146+
const res = await fetch(url);
147+
data = await res.json();
148+
console.log('Loaded accessibility data, records:', data.length);
149+
setupLayer();
150+
} catch (e) {
151+
console.error('Failed to load data', e);
152+
}
153+
}
154+
155+
function setupLayer() {
156+
if (!data) return;
157+
if (gridLayer && map.getLayer(gridLayer.id)) map.removeLayer(gridLayer.id);
158+
159+
const cellSize = 60;
160+
const timeIndex = Number(timeSlider.value);
161+
const showSpark = sparkToggle.checked;
162+
const glyphSize = Number(glyphSizeInput.value);
163+
const normalization = document.getElementById('normalizationSelect').value;
164+
const cumulativeOn = false; // primary layer is per-time by default
165+
166+
gridLayer = new ScreenGridLayerGL({
167+
id: 'pta-glyph-layer',
168+
data,
169+
// data is expected to be features with a coordinate property named `centroid` or `COORDINATES`
170+
getPosition: (d) => {
171+
if (!d) return null;
172+
// If the feature already provides a centroid array
173+
if (Array.isArray(d.centroid) && d.centroid.length >= 2) return d.centroid;
174+
// If the feature provides a simple COORDINATES array
175+
if (Array.isArray(d.COORDINATES) && d.COORDINATES.length >= 2) return d.COORDINATES;
176+
// If GeoJSON geometry is a Point / MultiPoint, extract first coordinate pair
177+
if (d.geometry && d.geometry.type === 'Point' && Array.isArray(d.geometry.coordinates)) return d.geometry.coordinates;
178+
if (d.geometry && d.geometry.type === 'MultiPoint' && Array.isArray(d.geometry.coordinates) && d.geometry.coordinates[0]) return d.geometry.coordinates[0];
179+
// If the properties provide centroids as numbers (e.g., `cent_long` / `cent_lat`), use them
180+
if (d.properties && typeof d.properties.cent_long === 'number' && typeof d.properties.cent_lat === 'number') return [d.properties.cent_long, d.properties.cent_lat];
181+
// Fallback: try top-level cent_long / cent_lat
182+
if (typeof d.cent_long === 'number' && typeof d.cent_lat === 'number') return [d.cent_long, d.cent_lat];
183+
return null;
184+
},
185+
getWeight: () => 1,
186+
cellSizePixels: cellSize,
187+
colorScale: (v) => [255 * v, 200 * (1 - v), 50, 200],
188+
enableGlyphs: true,
189+
glyphSize: glyphSize,
190+
glyph: 'public-transport',
191+
glyphConfig: { timeIndex: timeIndex, showSparkline: showSpark, debug: true },
192+
normalizationFunction: normalization,
193+
normalizationContext: (normalization === 'max-global' && currentGlobalMax) ? { globalMax: currentGlobalMax } : {},
194+
showBackground: true,
195+
onAggregate: (grid) => {
196+
console.log('Aggregated', grid.cols, 'x', grid.rows);
197+
// compute global max for max-global normalization
198+
try {
199+
const values = (grid && grid.grid) ? grid.grid.map(v => (typeof v === 'number' ? v : (v && typeof v === 'object' ? Object.values(v).filter(n => typeof n === 'number').reduce((s, n) => s + n, 0) : 0))) : [];
200+
const gm = values.length ? Math.max(...values) : 0;
201+
currentGlobalMax = gm || 0;
202+
if (gridLayer) gridLayer.setConfig({ normalizationContext: (document.getElementById('normalizationSelect').value === 'max-global') ? { globalMax: currentGlobalMax } : {} });
203+
if (cumulativeLayer) cumulativeLayer.setConfig({ normalizationContext: (document.getElementById('normalizationSelect').value === 'max-global') ? { globalMax: currentGlobalMax } : {} });
204+
} catch (e) {
205+
console.warn('Failed to compute global max', e);
206+
}
207+
},
208+
onHover: ({ cell }) => {
209+
// optional: show hover details
210+
// console.log('hover', cell);
211+
}
212+
});
213+
214+
map.addLayer(gridLayer);
215+
216+
// prepare cumulative layer but do not add to map by default
217+
cumulativeLayer = new ScreenGridLayerGL({
218+
id: 'pta-cumulative-layer',
219+
data,
220+
getPosition: gridLayer.config.getPosition,
221+
getWeight: () => 1,
222+
cellSizePixels: cellSize,
223+
colorScale: (v) => [200 * v, 120 * (1 - v), 255 * (1 - v), 200],
224+
enableGlyphs: true,
225+
glyphSize: glyphSize,
226+
glyph: 'public-transport',
227+
glyphConfig: { timeIndex: timeIndex, showSparkline: false, cumulative: true },
228+
normalizationFunction: document.getElementById('normalizationSelect').value,
229+
normalizationContext: (document.getElementById('normalizationSelect').value === 'max-global' && currentGlobalMax) ? { globalMax: currentGlobalMax } : {},
230+
showBackground: false,
231+
onAggregate: (grid) => { /* reuse global max update via primary layer */ }
232+
});
233+
}
234+
235+
map.on('load', () => {
236+
loadData();
237+
});
238+
239+
timeSlider.addEventListener('input', () => {
240+
const idx = Number(timeSlider.value);
241+
minuteLabel.textContent = MINUTES[idx];
242+
if (gridLayer) gridLayer.setConfig({ glyphConfig: { timeIndex: idx, showSparkline: sparkToggle.checked } });
243+
if (cumulativeLayer) cumulativeLayer.setConfig({ glyphConfig: { timeIndex: idx, cumulative: true } });
244+
});
245+
246+
sparkToggle.addEventListener('change', () => {
247+
const idx = Number(timeSlider.value);
248+
if (gridLayer) gridLayer.setConfig({ glyphConfig: { timeIndex: idx, showSparkline: sparkToggle.checked } });
249+
});
250+
251+
glyphSizeInput.addEventListener('input', () => {
252+
const v = Number(glyphSizeInput.value);
253+
if (gridLayer) gridLayer.setConfig({ glyphSize: v });
254+
if (cumulativeLayer) cumulativeLayer.setConfig({ glyphSize: v });
255+
});
256+
257+
// normalization selector
258+
const normSelect = document.getElementById('normalizationSelect');
259+
normSelect.addEventListener('change', () => {
260+
const val = normSelect.value;
261+
if (gridLayer) gridLayer.setConfig({ normalizationFunction: val, normalizationContext: (val === 'max-global' && currentGlobalMax) ? { globalMax: currentGlobalMax } : {} });
262+
if (cumulativeLayer) cumulativeLayer.setConfig({ normalizationFunction: val, normalizationContext: (val === 'max-global' && currentGlobalMax) ? { globalMax: currentGlobalMax } : {} });
263+
// update legend text
264+
const legendNorm = document.getElementById('legend-norm');
265+
if (legendNorm) {
266+
const mapping = { 'max-local': 'Max (local)', 'max-global': 'Max (global)', 'z-score': 'Z-score', 'percentile': 'Percentile' };
267+
legendNorm.textContent = mapping[val] || val;
268+
}
269+
});
270+
271+
// cumulative toggle
272+
const cumToggle = document.getElementById('cumulativeToggle');
273+
cumToggle.addEventListener('change', () => {
274+
if (cumToggle.checked) {
275+
if (cumulativeLayer && !map.getLayer(cumulativeLayer.id)) map.addLayer(cumulativeLayer);
276+
// highlight legend
277+
const el = document.getElementById('legend-cumulative'); if (el) el.style.outline = '2px solid rgba(255,255,255,0.12)';
278+
} else {
279+
if (cumulativeLayer && map.getLayer(cumulativeLayer.id)) map.removeLayer(cumulativeLayer.id);
280+
const el = document.getElementById('legend-cumulative'); if (el) el.style.outline = 'none';
281+
}
282+
// trigger repaint
283+
if (gridLayer && map) map.triggerRepaint();
284+
});
285+
286+
// Initialize legend text on load
287+
const legendNormInit = document.getElementById('legend-norm'); if (legendNormInit) legendNormInit.textContent = document.getElementById('normalizationSelect').selectedOptions[0].textContent;
288+
</script>
289+
</body>
290+
291+
</html>

scripts/testGlyphIntegration.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Renderer } from '../src/canvas/Renderer.js';
2+
import { GlyphRegistry } from '../src/glyphs/GlyphRegistry.js';
3+
import '../src/glyphs/PublicTransportGlyph.js';
4+
import fs from 'fs';
5+
6+
if (typeof global.window === 'undefined') global.window = { devicePixelRatio: 1 };
7+
8+
const data = JSON.parse(fs.readFileSync(new URL('../examples/data/public_transport_accessibility.json', import.meta.url)));
9+
console.log('Loaded data length', data.length);
10+
11+
// Build simple 4x4 grid, 2x2 cells for simplicity
12+
const cols = 2; const rows = 2; const cellSizePixels = 60; const width = cols * cellSizePixels; const height = rows * cellSizePixels;
13+
const grid = [0,0,0,0];
14+
const cellData = [[],[],[],[]];
15+
16+
// Place some sample features into cell 0 and 3
17+
for (let i=0;i<10;i++) {
18+
const item = data[i % data.length];
19+
cellData[0].push({ data: item, weight: 1 });
20+
grid[0] += 1;
21+
}
22+
for (let i=10;i<20;i++) {
23+
const item = data[i % data.length];
24+
cellData[3].push({ data: item, weight: 1 });
25+
grid[3] += 1;
26+
}
27+
28+
const aggregationResult = { cols, rows, width, height, cellSizePixels, grid, cellData };
29+
30+
const ctx = {
31+
clearRect: () => {},
32+
fillRect: () => {},
33+
beginPath: () => {},
34+
arc: () => {},
35+
fill: () => {},
36+
stroke: () => {},
37+
moveTo: () => {},
38+
lineTo: () => {},
39+
save: () => {},
40+
restore: () => {},
41+
set fillStyle(v){},
42+
set strokeStyle(v){},
43+
set lineWidth(v){},
44+
set lineCap(v){},
45+
set globalAlpha(v){}
46+
};
47+
48+
const plugin = GlyphRegistry.get('public-transport');
49+
if (!plugin) {
50+
console.error('public-transport plugin not found');
51+
process.exit(1);
52+
}
53+
54+
const glyphCfg = { timeIndex: 7, showSparkline: true, debug: true };
55+
56+
let invoked = false;
57+
58+
const onDrawCell = (ctxArg, x, y, normVal, info) => {
59+
try {
60+
plugin.draw(ctxArg, x, y, normVal, info, glyphCfg);
61+
invoked = true;
62+
} catch (e) {
63+
console.error('draw threw', e);
64+
}
65+
};
66+
67+
Renderer.render(aggregationResult, ctx, { colorScale: (v) => [255 * v, 100, 200, 200], enableGlyphs: true, onDrawCell, glyphSize: 0.8, normalizationFunction: 'max-local', showBackground: true });
68+
69+
console.log('invoked?', invoked);

scripts/testGlyphRegistry.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { GlyphRegistry } from '../src/glyphs/GlyphRegistry.js';
2+
import '../src/glyphs/PublicTransportGlyph.js';
3+
4+
console.log('Registered glyphs:', GlyphRegistry.list());
5+
6+
const glyph = GlyphRegistry.get('public-transport');
7+
console.log('public-transport found:', !!glyph);
8+
console.log('glyph keys:', Object.keys(glyph || {}));
9+
10+
if (glyph && typeof glyph.draw === 'function') {
11+
console.log('draw is function');
12+
} else {
13+
console.log('draw missing or not function');
14+
}

0 commit comments

Comments
 (0)