diff --git a/package.json b/package.json index b10563e0..a84ec2dc 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "license:": "AGPL-3.0-or-later", "dependencies": { + "@mapbox/vector-tile": "^1.3.1", "@material-ui/core": "^4.9.11", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^5.5.0", @@ -14,6 +15,7 @@ "@types/jest": "^25.2.1", "@types/mapbox-gl": "^1.9.0", "@types/node": "^13.13.3", + "@types/pbf": "^3.0.2", "@types/react": "^16.9.34", "@types/react-dom": "^16.9.6", "@types/react-map-gl": "^5.2.2", @@ -23,6 +25,7 @@ "@urbica/react-map-gl": "^1.13.0", "mapbox-gl": "^1.9.1", "notistack": "^0.9.11", + "pbf": "^3.2.1", "plannerjs": "https://github.com/gatesolve/planner.js#build-for-gatesolve", "react": "^16.13.1", "react-autosuggest-geocoder": "https://github.com/gatesolve/react-autosuggest-geocoder#fix/modify-for-gatesolve", diff --git a/src/@mapbox__vector-tile.d.ts b/src/@mapbox__vector-tile.d.ts new file mode 100644 index 00000000..976ddbb8 --- /dev/null +++ b/src/@mapbox__vector-tile.d.ts @@ -0,0 +1 @@ +declare module "@mapbox/vector-tile"; diff --git a/src/App.tsx b/src/App.tsx index 60bc84bc..a74237d8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,7 +29,7 @@ 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 { addImageSVG, getMapSize, queryFeatures } from "./mapbox-utils"; import routableTilesToGeoJSON from "./RoutableTilesToGeoJSON"; import { getVisibleTiles } from "./minimal-xyz-viewer"; @@ -315,6 +315,16 @@ const App: React.FC = () => { useEffect(() => { if (!state.destination) return; // Nothing to do yet + queryFeatures(destinationToLatLng(state.destination)).then((result) => { + setState( + (prevState): State => { + return { + ...prevState, + highlights: result, + }; + } + ); + }); queryEntrances(state.destination).then((result) => { setState( (prevState): State => { diff --git a/src/mapbox-utils.ts b/src/mapbox-utils.ts index b07ba91e..24288557 100644 --- a/src/mapbox-utils.ts +++ b/src/mapbox-utils.ts @@ -1,5 +1,10 @@ import { Map } from "mapbox-gl"; +import { VectorTile } from "@mapbox/vector-tile"; +import Protobuf from "pbf"; + +import type { Feature, FeatureCollection } from "geojson"; + // eslint-disable-next-line import/prefer-default-export export const addImageSVG = ( mapboxgl: Map, @@ -40,3 +45,71 @@ export const getMapSize = ( width: mapboxgl?.getContainer()?.clientWidth, height: mapboxgl?.getContainer()?.clientHeight, }); + +export const queryFeatures = async ([lat, lng]: [number, number]): Promise< + FeatureCollection +> => { + const TILE_SIZE = 512; + const WEBMERCATOR_R = 6378137.0; + const DIAMETER = WEBMERCATOR_R * 2 * Math.PI; + + function mercatorProject(lonlat: [number, number]): [number, number] { + const x = (DIAMETER * lonlat[0]) / 360.0; + const sinlat = Math.sin((lonlat[1] * Math.PI) / 180.0); + const y = + (DIAMETER * Math.log((1 + sinlat) / (1 - sinlat))) / (4 * Math.PI); + return [DIAMETER / 2 + x, DIAMETER - (DIAMETER / 2 + y)]; + } + + function getTile( + lonlat: [number, number], + zoom: number + ): [number, number, number, number] { + const centerm = mercatorProject(lonlat); + const centerpx = [ + (centerm[0] * TILE_SIZE * 2 ** zoom) / DIAMETER, + (centerm[1] * TILE_SIZE * 2 ** zoom) / DIAMETER, + ]; + const x = Math.floor(centerpx[0] / TILE_SIZE); + const y = Math.floor(centerpx[1] / TILE_SIZE); + return [ + x, + y, + (centerpx[0] - x * TILE_SIZE) / TILE_SIZE, + (centerpx[1] - y * TILE_SIZE) / TILE_SIZE, + ]; + } + + function pointInBox( + point: [number, number], + box: [number, number, number, number] + ): boolean { + return ( + box[0] < point[0] && + point[0] < box[2] && + box[1] < point[1] && + point[1] < box[3] + ); + } + + const coords = getTile([lng, lat], 14); + const tileCoords = [coords[2] * 4096, coords[3] * 4096] as [number, number]; + + const data = await fetch( + `https://api.digitransit.fi/map/v1/hsl-vector-map/14/${coords[0]}/${coords[1]}.pbf` + ); + const tile = new VectorTile(new Protobuf(await data.arrayBuffer())); + const results = []; + for (let i = 0; i < tile.layers.building.length; i += 1) { + const feature = tile.layers.building.feature(i); + if (pointInBox(tileCoords, feature.bbox())) { + // eslint-disable-next-line no-console + console.log("selected building", feature); + results.push(feature.toGeoJSON(coords[0], coords[1], 14)); + } + } + return { + type: "FeatureCollection" as "FeatureCollection", + features: results as Array, + }; +}; diff --git a/yarn.lock b/yarn.lock index acc4cd22..d9c54c15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3131,6 +3131,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/pbf@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.2.tgz#8d291ad68b4b8c533e96c174a2e3e6399a59ed61" + integrity sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ== + "@types/prop-types@*": version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"