Skip to content
Open
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
4 changes: 4 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ src/serviceWorker.ts

# Copied from @urbica/react-map-gl
src/__mocks__/mapbox-gl.js

# Modules from external sources
src/minimal-xyz-viewer.js
src/RoutableTilesToGeoJSON.js
92 changes: 92 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ import {
routeImaginaryLineLayer,
allEntrancesLayer,
allEntrancesSymbolLayer,
routableTilesLayer,
} from "./map-style";
import Pin, { pinAsSVG } from "./components/Pin";
import UserPosition from "./components/UserPosition";
import GeolocateControl from "./components/GeolocateControl";
import calculatePlan, { geometryToGeoJSON } from "./planner";
import { queryEntrances, ElementWithCoordinates } from "./overpass";
import { addImageSVG, getMapSize } from "./mapbox-utils";
import routableTilesToGeoJSON from "./RoutableTilesToGeoJSON";
import { getVisibleTiles } from "./minimal-xyz-viewer";

import "./App.css";
import "./components/PinMarker.css";

Expand All @@ -46,6 +50,7 @@ interface State {
geolocationPosition: LatLng | null;
popupCoordinates: LatLng | null;
snackbar?: ReactText;
routableTiles: Map<string, FeatureCollection | null>;
}

const latLngToDestination = (latLng: LatLng): ElementWithCoordinates => ({
Expand Down Expand Up @@ -74,6 +79,7 @@ const initialState: State = {
isGeolocating: false,
geolocationPosition: null,
popupCoordinates: null,
routableTiles: new Map(),
};

const metropolitanAreaCenter = [60.17066815612902, 24.941510260105133];
Expand Down Expand Up @@ -173,6 +179,73 @@ const App: React.FC = () => {
);
};

useEffect(() => {
if (!map.current || !state.viewport.zoom) {
return; // Nothing to do yet
}
if (state.viewport.zoom < 12) return; // minzoom

const { width: mapWidth, height: mapHeight } = getMapSize(
map.current.getMap()
);

// Calculate multiplier for under- or over-zoom
const tilesetZoomLevel = 14;
const zoomOffset = 1; // tiles are 512px (double the standard size)
const zoomMultiplier =
2 ** (tilesetZoomLevel - zoomOffset - state.viewport.zoom);

const visibleTiles = getVisibleTiles(
zoomMultiplier * mapWidth,
zoomMultiplier * mapHeight,
[state.viewport.longitude, state.viewport.latitude],
tilesetZoomLevel
);

// Initialise the new Map with nulls and available tiles from previous
const routableTiles = new Map();
visibleTiles.forEach(({ zoom, x, y }) => {
const key = `${zoom}/${x}/${y}`;
routableTiles.set(key, state.routableTiles.get(key) || null);
});

setState(
(prevState: State): State => {
return {
...prevState,
routableTiles,
};
}
);

visibleTiles.map(async ({ zoom, x, y }) => {
const key = `${zoom}/${x}/${y}`;
if (routableTiles.get(key) !== null) return; // We already have the tile
// Fetch the tile
const response = await fetch(
`https://tile.olmap.org/routable-tiles/${zoom}/${x}/${y}`
);
const body = await response.json();
// Convert the tile to GeoJSON
const geoJSON = routableTilesToGeoJSON(body) as FeatureCollection;
// Add the tile if still needed based on latest state
setState(
(prevState: State): State => {
if (prevState.routableTiles.get(key) !== null) {
return prevState; // This tile is not needed anymore
}
const newRoutableTiles = new Map(prevState.routableTiles);
newRoutableTiles.set(key, geoJSON);
return {
...prevState,
routableTiles: newRoutableTiles,
};
}
);
});
}, [map.current, state.viewport]); // eslint-disable-line react-hooks/exhaustive-deps
// XXX: state.routableTiles is missing above as we only use it as a cache here

useEffect(() => {
/**
* FIXME: urbica/react-map-gl does not expose fitBounds and its viewport
Expand Down Expand Up @@ -548,6 +621,25 @@ const App: React.FC = () => {
<UserPosition dataTestId="user-marker" />
</Marker>
)}
{Array.from(
state.routableTiles.entries(),
([coords, tile]) =>
tile && (
<Source
key={coords}
id={`source-${coords}`}
type="geojson"
data={tile}
>
<Layer
// eslint-disable-next-line react/jsx-props-no-spreading
{...routableTilesLayer}
id={coords}
source={`source-${coords}`}
/>
</Source>
)
)}
<Source
id="osm-qa-tiles"
type="vector"
Expand Down
59 changes: 59 additions & 0 deletions src/RoutableTilesToGeoJSON.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Source: https://github.com/openplannerteam/leaflet-routable-tiles/blob/master/lib/RoutableTilesToGeoJSON.js

var extractWays = function (json, nodes) {
return json["@graph"]
.filter((item) => {
return item["@type"] === "osm:Way";
})
.map((item) => {
//Transform osm:hasNodes to a linestring style thing
if (!item["osm:hasNodes"]) {
item["osm:hasNodes"] = [];
} else if (typeof item["osm:hasNodes"] === "string") {
item["osm:hasNodes"] = [item["osm:hasNodes"]];
}
item["osm:hasNodes"] = item["osm:hasNodes"].map((node) => {
return nodes[node];
});
let geometry = {
type: "LineString",
coordinates: item["osm:hasNodes"],
};
return {
id: item["@id"],
//layer: item['osm:highway'],
type: "Feature",
properties: {
highway: item["osm:highway"],
name: item["rdfs:label"] ? item["rdfs:label"] : "",
},
geometry: geometry,
};
});
};

module.exports = function (json) {
// Normalize feature getters into actual instanced features
var feats = [];
var nodes = {};
for (var i = 0; i < json["@graph"].length; i++) {
let o = json["@graph"][i];
if (o["geo:lat"] && o["geo:long"]) {
nodes[o["@id"]] = [o["geo:long"], o["geo:lat"]];
let feature = {
id: o["@id"],
type: "Feature",
geometry: {
type: "Point",
coordinates: [o["geo:long"], o["geo:lat"]],
},
};
feats.push(feature);
}
}
let ways = extractWays(json, nodes);
return {
type: "FeatureCollection",
features: ways,
};
};
9 changes: 9 additions & 0 deletions src/map-style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ export const routeImaginaryLineLayer: LayerProps = {
},
filter: ["coalesce", ["get", "imaginary"], false],
};
export const routableTilesLayer = {
id: "routable-tiles-line",
type: "line",
paint: {
"line-opacity": ["coalesce", ["get", "opacity"], 0.5],
"line-width": 2,
"line-color": "black",
},
};

export const routePointLayer: LayerProps = {
id: "route-point",
Expand Down
42 changes: 42 additions & 0 deletions src/minimal-xyz-viewer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Source: https://gist.github.com/jsanz/e8549c7bffd442235a942695ffdaf77d

const TILE_SIZE = 256;
const WEBMERCATOR_R = 6378137.0;
const DIAMETER = WEBMERCATOR_R * 2 * Math.PI;

function mercatorProject(lonlat) {
var x = (DIAMETER * lonlat[0]) / 360.0;
var sinlat = Math.sin((lonlat[1] * Math.PI) / 180.0);
var y = (DIAMETER * Math.log((1 + sinlat) / (1 - sinlat))) / (4 * Math.PI);
return [DIAMETER / 2 + x, DIAMETER - (DIAMETER / 2 + y)];
}
// console.log(Mercator.project([-3,41]))

export function getVisibleTiles(clientWidth, clientHeight, center, zoom) {
var centerm = mercatorProject(center);
// zoom + centerm -> centerpx
var centerpx = [
(centerm[0] * TILE_SIZE * Math.pow(2, zoom)) / DIAMETER,
(centerm[1] * TILE_SIZE * Math.pow(2, zoom)) / DIAMETER,
];

// xmin, ymin, xmax, ymax
var bbox = [
Math.floor((centerpx[0] - clientWidth / 2) / TILE_SIZE),
Math.floor((centerpx[1] - clientHeight / 2) / TILE_SIZE),
Math.ceil((centerpx[0] + clientWidth / 2) / TILE_SIZE),
Math.ceil((centerpx[1] + clientHeight / 2) / TILE_SIZE),
];
var tiles = [];
//xmin, ymin, xmax, ymax
for (let x = bbox[0]; x < bbox[2]; ++x) {
for (let y = bbox[1]; y < bbox[3]; ++y) {
var [px, py] = [
x * TILE_SIZE - centerpx[0] + clientWidth / 2,
y * TILE_SIZE - centerpx[1] + clientHeight / 2,
];
tiles.push({ x, y, zoom, px, py });
}
}
return tiles;
}