Skip to content

Commit 3967c74

Browse files
committed
Visualise routable tiles as GeoJSON layers
1 parent f2f20d0 commit 3967c74

5 files changed

Lines changed: 197 additions & 0 deletions

File tree

.eslintignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ src/react-app-env.d.ts
88
# This file was created by Create React App so let's not lint it until we need
99
# to touch it.
1010
src/serviceWorker.ts
11+
12+
# Modules from external sources
13+
src/minimal-xyz-viewer.js
14+
src/RoutableTilesToGeoJSON.js

src/App.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ import {
1919
routePointLayer,
2020
routePointSymbolLayer,
2121
routeLineLayer,
22+
routableTilesLayer,
2223
} from "./map-style";
2324
import PinMarker from "./components/PinMarker";
2425
import calculatePlan, { geometryToGeoJSON } from "./planner";
2526
import { queryEntrances, ElementWithCoordinates } from "./overpass";
27+
import routableTilesToGeoJSON from "./RoutableTilesToGeoJSON";
28+
import { getVisibleTiles } from "./minimal-xyz-viewer";
2629
import "./App.css";
2730

2831
interface State {
@@ -31,6 +34,7 @@ interface State {
3134
destination: ElementWithCoordinates;
3235
entrances: Array<ElementWithCoordinates>;
3336
route: FeatureCollection;
37+
routableTiles: Map<string, FeatureCollection | null>;
3438
}
3539

3640
const latLngToDestination = (
@@ -59,6 +63,7 @@ const initialState: State = {
5963
bearing: 0,
6064
pitch: 0,
6165
},
66+
routableTiles: new Map(),
6267
};
6368

6469
const transformRequest = (originalURL?: string): MapRequest => {
@@ -115,6 +120,69 @@ const App: React.FC = () => {
115120

116121
const [state, setState] = useState(initialState);
117122

123+
useEffect(() => {
124+
if (
125+
!state.viewport.zoom ||
126+
!state.viewport.width ||
127+
!state.viewport.height
128+
) {
129+
return; // Nothing to do yet
130+
}
131+
if (state.viewport.zoom < 12) return; // minzoom
132+
133+
const zoomOffset = 1;
134+
const zoomMultiplier = 2 ** (14 - zoomOffset) - state.viewport.zoom;
135+
const visibleTiles = getVisibleTiles(
136+
zoomMultiplier * state.viewport.width,
137+
zoomMultiplier * state.viewport.height,
138+
[state.viewport.longitude, state.viewport.latitude],
139+
14
140+
);
141+
142+
// Initialise the new Map with nulls and available tiles from previous
143+
const routableTiles = new Map();
144+
visibleTiles.forEach(({ x, y }) => {
145+
const key = `${x}-${y}`;
146+
routableTiles.set(key, state.routableTiles.get(key) || null);
147+
});
148+
149+
setState(
150+
(prevState: State): State => {
151+
return {
152+
...prevState,
153+
routableTiles,
154+
};
155+
}
156+
);
157+
158+
visibleTiles.map(async ({ x, y }) => {
159+
const key = `${x}-${y}`;
160+
if (routableTiles.get(key) !== null) return; // We already have the tile
161+
// fetch tile
162+
const response = await fetch(
163+
`https://tile.olmap.org/routable-tiles/14/${x}/${y}`
164+
);
165+
const body = await response.json();
166+
// convert to geojson
167+
const geoJSON = routableTilesToGeoJSON(body) as FeatureCollection;
168+
// add to tiles if still needed based on latest state
169+
setState(
170+
(prevState: State): State => {
171+
if (prevState.routableTiles.get(key) !== null) {
172+
return prevState; // This tile is not needed anymore
173+
}
174+
const newRoutableTiles = new Map(prevState.routableTiles);
175+
newRoutableTiles.set(key, geoJSON);
176+
return {
177+
...prevState,
178+
routableTiles: newRoutableTiles,
179+
};
180+
}
181+
);
182+
});
183+
}, [state.viewport]); // eslint-disable-line react-hooks/exhaustive-deps
184+
// XXX: state.routableTiles is missing above as we only use it as a cache here
185+
118186
useEffect(() => {
119187
if (urlMatch) {
120188
const origin = parseLatLng(urlMatch.params.from);
@@ -313,6 +381,24 @@ const App: React.FC = () => {
313381
}
314382
}}
315383
/>
384+
{Array.from(
385+
state.routableTiles.entries(),
386+
([coords, tile]) =>
387+
tile && (
388+
<Source
389+
key={coords}
390+
id={`source-${coords}`}
391+
type="geojson"
392+
data={tile}
393+
>
394+
<Layer
395+
// eslint-disable-next-line react/jsx-props-no-spreading
396+
{...routableTilesLayer}
397+
id={coords}
398+
/>
399+
</Source>
400+
)
401+
)}
316402
<Source type="geojson" data={state.route}>
317403
<Layer
318404
// eslint-disable-next-line react/jsx-props-no-spreading
@@ -327,6 +413,7 @@ const App: React.FC = () => {
327413
{...routePointSymbolLayer}
328414
/>
329415
</Source>
416+
330417
<PinMarker
331418
marker={{
332419
draggable: true,

src/RoutableTilesToGeoJSON.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
var extractWays = function (json, nodes) {
2+
return json["@graph"]
3+
.filter((item) => {
4+
return item["@type"] === "osm:Way";
5+
})
6+
.map((item) => {
7+
//Transform osm:hasNodes to a linestring style thing
8+
if (!item["osm:hasNodes"]) {
9+
item["osm:hasNodes"] = [];
10+
} else if (typeof item["osm:hasNodes"] === "string") {
11+
item["osm:hasNodes"] = [item["osm:hasNodes"]];
12+
}
13+
item["osm:hasNodes"] = item["osm:hasNodes"].map((node) => {
14+
return nodes[node];
15+
});
16+
let geometry = {
17+
type: "LineString",
18+
coordinates: item["osm:hasNodes"],
19+
};
20+
return {
21+
id: item["@id"],
22+
//layer: item['osm:highway'],
23+
type: "Feature",
24+
properties: {
25+
highway: item["osm:highway"],
26+
name: item["rdfs:label"] ? item["rdfs:label"] : "",
27+
},
28+
geometry: geometry,
29+
};
30+
});
31+
};
32+
33+
module.exports = function (json) {
34+
// Normalize feature getters into actual instanced features
35+
var feats = [];
36+
var nodes = {};
37+
for (var i = 0; i < json["@graph"].length; i++) {
38+
let o = json["@graph"][i];
39+
if (o["geo:lat"] && o["geo:long"]) {
40+
nodes[o["@id"]] = [o["geo:long"], o["geo:lat"]];
41+
let feature = {
42+
id: o["@id"],
43+
type: "Feature",
44+
geometry: {
45+
type: "Point",
46+
coordinates: [o["geo:long"], o["geo:lat"]],
47+
},
48+
};
49+
feats.push(feature);
50+
}
51+
}
52+
let ways = extractWays(json, nodes);
53+
return {
54+
type: "FeatureCollection",
55+
features: ways,
56+
};
57+
};

src/map-style.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ export const routeLineLayer = {
1010
"line-color": ["get", "color"] as Expression,
1111
},
1212
};
13+
export const routableTilesLayer = {
14+
id: "routable-tiles-line",
15+
type: "line",
16+
paint: {
17+
"line-opacity": ["coalesce", ["get", "opacity"], 0.5] as Expression,
18+
"line-width": 2,
19+
"line-color": "black",
20+
},
21+
};
1322

1423
export const routePointLayer = {
1524
id: "route-point",

src/minimal-xyz-viewer.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const TILE_SIZE = 256;
2+
const WEBMERCATOR_R = 6378137.0;
3+
const DIAMETER = WEBMERCATOR_R * 2 * Math.PI;
4+
5+
function mercatorProject(lonlat) {
6+
var x = (DIAMETER * lonlat[0]) / 360.0;
7+
var sinlat = Math.sin((lonlat[1] * Math.PI) / 180.0);
8+
var y = (DIAMETER * Math.log((1 + sinlat) / (1 - sinlat))) / (4 * Math.PI);
9+
return [DIAMETER / 2 + x, DIAMETER - (DIAMETER / 2 + y)];
10+
}
11+
// console.log(Mercator.project([-3,41]))
12+
13+
export function getVisibleTiles(clientWidth, clientHeight, center, zoom) {
14+
var centerm = mercatorProject(center);
15+
// zoom + centerm -> centerpx
16+
var centerpx = [
17+
(centerm[0] * TILE_SIZE * Math.pow(2, zoom)) / DIAMETER,
18+
(centerm[1] * TILE_SIZE * Math.pow(2, zoom)) / DIAMETER,
19+
];
20+
21+
// xmin, ymin, xmax, ymax
22+
var bbox = [
23+
Math.floor((centerpx[0] - clientWidth / 2) / TILE_SIZE),
24+
Math.floor((centerpx[1] - clientHeight / 2) / TILE_SIZE),
25+
Math.ceil((centerpx[0] + clientWidth / 2) / TILE_SIZE),
26+
Math.ceil((centerpx[1] + clientHeight / 2) / TILE_SIZE),
27+
];
28+
var tiles = [];
29+
//xmin, ymin, xmax, ymax
30+
for (let x = bbox[0]; x < bbox[2]; ++x) {
31+
for (let y = bbox[1]; y < bbox[3]; ++y) {
32+
var [px, py] = [
33+
x * TILE_SIZE - centerpx[0] + clientWidth / 2,
34+
y * TILE_SIZE - centerpx[1] + clientHeight / 2,
35+
];
36+
tiles.push({ x, y, zoom, px, py });
37+
}
38+
}
39+
return tiles;
40+
}

0 commit comments

Comments
 (0)