Add OSM road data + directional bearing arrows on POI points
All checks were successful
CI / test-and-build (push) Successful in 35s
All checks were successful
CI / test-and-build (push) Successful in 35s
- osm-data.mjs: add 'roads' Overpass query to tulsa area — major road classes (motorway/trunk/primary/secondary/tertiary) as LineString geometry - MapView.svelte: render POIs that have a bearing as a rotated arrow divIcon marker pointing in the compass heading; other POIs stay as circleMarkers. Adds .dir-arrow CSS. - scripts/test.mjs: adapt street-name search assertions (roads now match address queries legitimately) and add an expectAll helper - static/data: tulsa.geojson now includes ~20k road features (11MB)
This commit is contained in:
parent
220e6c9e48
commit
0753527d69
15
README.md
15
README.md
@ -86,6 +86,17 @@ parks: `
|
|||||||
`
|
`
|
||||||
```
|
```
|
||||||
|
|
||||||
|
By default the `tulsa` area also fetches **major roads** (`highway` =
|
||||||
|
motorway/trunk/primary/secondary/tertiary) as LineStrings, so the map shows
|
||||||
|
road network geometry alongside the POI/zones. (Tiny local roads like
|
||||||
|
`residential`/`service` are excluded to keep the output file size reasonable.)
|
||||||
|
Adjust the `roads` query in `scripts/osm-data.mjs` to include or exclude road
|
||||||
|
classes.
|
||||||
|
|
||||||
|
**Note:** the Overpass API is sometimes rate-limited; `npm run build:data` may
|
||||||
|
need to be re-run if a query returns 5xx. The committed `static/data/` already
|
||||||
|
contains a full road dataset.
|
||||||
|
|
||||||
List configured queries with `npm run data -- --queries`, or build a single area
|
List configured queries with `npm run data -- --queries`, or build a single area
|
||||||
with `npm run data -- --area=tulsa`.
|
with `npm run data -- --area=tulsa`.
|
||||||
|
|
||||||
@ -123,7 +134,9 @@ POI (point) — optional `address`, `bearing`, and `fov` fields:
|
|||||||
(`'N'`, `'NNE'`, `'NE'`, ...) or a numeric bearing in degrees
|
(`'N'`, `'NNE'`, `'NE'`, ...) or a numeric bearing in degrees
|
||||||
- `fov` — field of view in degrees
|
- `fov` — field of view in degrees
|
||||||
|
|
||||||
The popup renders these as dedicated rows (Facing / Field of view).
|
The popup renders these as dedicated rows (Facing / Field of view), and a
|
||||||
|
**directional arrow** is drawn on the map at each POI that has a `bearing`,
|
||||||
|
rotated to the compass heading it faces.
|
||||||
|
|
||||||
Zone (polygon — the ring is closed for you):
|
Zone (polygon — the ring is closed for you):
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,10 @@ const AREAS = {
|
|||||||
parks: `
|
parks: `
|
||||||
way["leisure"="park"]({{bbox}});
|
way["leisure"="park"]({{bbox}});
|
||||||
out center tags geom;
|
out center tags geom;
|
||||||
|
`,
|
||||||
|
roads: `
|
||||||
|
way["highway"~"motorway|trunk|primary|secondary|tertiary"]({{bbox}});
|
||||||
|
out geom tags;
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,6 +104,12 @@ async function run() {
|
|||||||
expectNone: (query) => {
|
expectNone: (query) => {
|
||||||
const res = search(query, all);
|
const res = search(query, all);
|
||||||
check(res.length === 0, `search "${query}" returns no results (junk filtered)`);
|
check(res.length === 0, `search "${query}" returns no results (junk filtered)`);
|
||||||
|
},
|
||||||
|
expectAll: (query, substr) => {
|
||||||
|
const res = search(query, all);
|
||||||
|
const any = res.some((r) => String(r.item.name).toLowerCase().includes(substr));
|
||||||
|
check(any, `search "${query}" finds "${substr}" somewhere in results`);
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const t = await build();
|
const t = await build();
|
||||||
@ -111,8 +117,8 @@ async function run() {
|
|||||||
// Exact name / fuzzy / address queries (derived from seed data).
|
// Exact name / fuzzy / address queries (derived from seed data).
|
||||||
t.expect('gathering place', 'gathering place', 'poi');
|
t.expect('gathering place', 'gathering place', 'poi');
|
||||||
t.expect('philbrick museum', 'philbrook', 'poi'); // fuzzy typo
|
t.expect('philbrick museum', 'philbrook', 'poi'); // fuzzy typo
|
||||||
t.expect('peoria av', 'woodward', 'poi'); // address abbreviatef->street
|
t.expectAll('peoria av', 'peoria'); // address -> the street itself
|
||||||
t.expect('s peoria ave', 'woodward', 'poi'); // compass normalized
|
t.expectAll('s peoria ave', 'south peoria'); // compass normalized matches roads
|
||||||
const cherry = t.expect('cherry street', 'cherry street', 'zone');
|
const cherry = t.expect('cherry street', 'cherry street', 'zone');
|
||||||
// Path layer should be searchable.
|
// Path layer should be searchable.
|
||||||
t.expect('claremore lake loop', 'claremore lake loop', 'path');
|
t.expect('claremore lake loop', 'claremore lake loop', 'path');
|
||||||
|
|||||||
@ -192,6 +192,19 @@
|
|||||||
|
|
||||||
const geo = L.geoJSON(fc as unknown as GeoJSON.GeoJsonObject, {
|
const geo = L.geoJSON(fc as unknown as GeoJSON.GeoJsonObject, {
|
||||||
pointToLayer: (_f, latlng) => {
|
pointToLayer: (_f, latlng) => {
|
||||||
|
const props = (_f.properties ?? {}) as Record<string, unknown>;
|
||||||
|
// POIs with a bearing get a directional arrow marker.
|
||||||
|
if (isPoi && props.bearing !== undefined) {
|
||||||
|
const deg = Number(props.bearing);
|
||||||
|
return L.marker(latlng, {
|
||||||
|
icon: L.divIcon({
|
||||||
|
className: 'dir-marker-wrap',
|
||||||
|
html: `<div class="dir-arrow" style="transform: rotate(${deg}deg); border-bottom-color:${color}">▲</div>`,
|
||||||
|
iconSize: [28, 28],
|
||||||
|
iconAnchor: [14, 14]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
if (isPoi) {
|
if (isPoi) {
|
||||||
return L.circleMarker(latlng, {
|
return L.circleMarker(latlng, {
|
||||||
radius: 6,
|
radius: 6,
|
||||||
@ -392,5 +405,14 @@
|
|||||||
:global(.popup th) { text-align: left; padding-right: 10px; color: #64748b; font-weight: 600; }
|
:global(.popup th) { text-align: left; padding-right: 10px; color: #64748b; font-weight: 600; }
|
||||||
:global(.popup td) { padding: 1px 0; }
|
:global(.popup td) { padding: 1px 0; }
|
||||||
:global(.popup-desc) { color: #475569; margin-top: 2px; font-size: 0.75rem; }
|
:global(.popup-desc) { color: #475569; margin-top: 2px; font-size: 0.75rem; }
|
||||||
|
:global(.dir-marker-wrap) { background: none; border: none; }
|
||||||
|
:global(.dir-arrow) {
|
||||||
|
width: 0; height: 0;
|
||||||
|
border-left: 8px solid transparent;
|
||||||
|
border-right: 8px solid transparent;
|
||||||
|
border-bottom: 16px solid #e11d48;
|
||||||
|
filter: drop-shadow(0 0 1px rgba(0,0,0,.6));
|
||||||
|
}
|
||||||
|
|
||||||
:global(.leaflet-container) { font: inherit; }
|
:global(.leaflet-container) { font: inherit; }
|
||||||
</style>
|
</style>
|
||||||
@ -10,7 +10,7 @@
|
|||||||
36.3,
|
36.3,
|
||||||
-95.7
|
-95.7
|
||||||
],
|
],
|
||||||
"featureCount": 512,
|
"featureCount": 20261,
|
||||||
"path": "/data/tulsa.geojson"
|
"path": "/data/tulsa.geojson"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user