diff --git a/package-lock.json b/package-lock.json index 551ebdc1..2c1a3bfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "graphhopper-maps", - "version": "0.0.1", + "version": "2.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "graphhopper-maps", - "version": "0.0.1", + "version": "2.0.3", "license": "Apache-2.0", "dependencies": { "custom-model-editor": "github:graphhopper/custom-model-editor#5ebd80570329f7abfc95c39624be1f1a379cf392", "geojson": "^0.5.0", + "maplibre-gl": "^5.16.0", "ol": "10.6.1", "ol-mapbox-style": "13.1.0", "react": "^19.1.1", @@ -3148,12 +3149,53 @@ "node": ">= 0.6" } }, + "node_modules/@mapbox/point-geometry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", + "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz", + "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==", + "license": "BSD-2-Clause" + }, "node_modules/@mapbox/unitbezier": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", "license": "BSD-2-Clause" }, + "node_modules/@mapbox/vector-tile": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz", + "integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^4.0.1" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/geojson-vt": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.0.4.tgz", + "integrity": "sha512-HYv3POhMRCdhP3UPPATM/hfcy6/WuVIf5FKboH8u/ZuFMTnAIcSVlq5nfOqroLokd925w2QtE7YwquFOIacwVQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/@maplibre/maplibre-gl-style-spec": { "version": "23.3.0", "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-23.3.0.tgz", @@ -3174,6 +3216,36 @@ "gl-style-validate": "dist/gl-style-validate.mjs" } }, + "node_modules/@maplibre/mlt": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.8.tgz", + "integrity": "sha512-8vtfYGidr1rNkv5IwIoU2lfe3Oy+Wa8HluzQYcQi9cveU9K3pweAal/poQj4GJ0K/EW4bTQp2wVAs09g2yDRZg==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0" + } + }, + "node_modules/@maplibre/vt-pbf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.0.tgz", + "integrity": "sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/vector-tile": "^2.0.4", + "@maplibre/geojson-vt": "^5.0.4", + "@types/geojson": "^7946.0.16", + "@types/supercluster": "^7.1.3", + "pbf": "^4.0.1", + "supercluster": "^8.0.1" + } + }, + "node_modules/@maplibre/vt-pbf/node_modules/@maplibre/geojson-vt": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz", + "integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==", + "license": "ISC" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -3672,7 +3744,6 @@ "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "dev": true, "license": "MIT" }, "node_modules/@types/html-minifier-terser": { @@ -3890,6 +3961,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -6804,6 +6884,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -8422,6 +8508,12 @@ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -8598,6 +8690,60 @@ "integrity": "sha512-kvsEfzvLik34BiFj+S19bv5d70l9qSdkUzrq99dvZ9d5POaLyB4vJMQmq3BoJ5D6lFG1GYnMM7o7cm5Jh8YEEg==", "license": "BSD-2-Clause" }, + "node_modules/maplibre-gl": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.21.0.tgz", + "integrity": "sha512-n0v4J/Ge0EG8ix/z3TY3ragtJYMqzbtSnj1riOC0OwQbzwp0lUF2maS1ve1z8HhitQCKtZZiZJhb8to36aMMfQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/tiny-sdf": "^2.0.7", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^2.0.4", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/geojson-vt": "^6.0.4", + "@maplibre/maplibre-gl-style-spec": "^24.7.0", + "@maplibre/mlt": "^1.1.8", + "@maplibre/vt-pbf": "^4.3.0", + "@types/geojson": "^7946.0.16", + "earcut": "^3.0.2", + "gl-matrix": "^3.4.4", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^4.0.1", + "potpack": "^2.1.0", + "quickselect": "^3.0.0", + "tinyqueue": "^3.0.0" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "node_modules/maplibre-gl/node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "24.7.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.7.0.tgz", + "integrity": "sha512-Ed7rcKYU5iELfablg9Mj+TVCsXsPBgdMyXPRAxb2v7oWg9YJnpQdZ5msDs1LESu/mtXy3Z48Vdppv2t/x5kAhw==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, "node_modules/matchmediaquery": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.4.2.tgz", @@ -8803,6 +8949,12 @@ "multicast-dns": "cli.js" } }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -9554,6 +9706,12 @@ "dev": true, "license": "MIT" }, + "node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, "node_modules/prettier": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", @@ -10928,6 +11086,15 @@ "webpack": "^5.27.0" } }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index 945490db..18874f91 100644 --- a/package.json +++ b/package.json @@ -5,16 +5,18 @@ "type": "git", "url": "git+https://github.com/graphhopper/graphhopper-maps.git" }, - "version": "0.0.1", + "version": "2.0.3", "license": "Apache-2.0", "scripts": { "format": "prettier --write \"src/**/*.{css,ts,tsx,html}\" \"test/**/*.{js,ts,jsx,tsx}\" \"jest.config.js\" \"webpack.*.js\" \"config.js\"", "serve": "webpack serve --config webpack.dev.js", "build": "webpack --config webpack.prod.js", + "fdroid": "webpack --config webpack.fdroid.js", "build-debug": "webpack --config webpack.dev.js", "test": "jest" }, "dependencies": { + "maplibre-gl": "^5.16.0", "custom-model-editor": "github:graphhopper/custom-model-editor#5ebd80570329f7abfc95c39624be1f1a379cf392", "geojson": "^0.5.0", "ol": "10.6.1", diff --git a/src/App.module.css b/src/App.module.css index 5538675c..2031f16b 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -12,6 +12,32 @@ user-select: none; /* firefox does not ignore -webkit-user-select so make this explicit, see #217 */ } +.appNaviWrapper { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + display: grid; + grid-template-columns: 30rem 1fr; + grid-template-rows: 1fr auto; + justify-content: space-between; + overflow: auto; + -webkit-user-select: none; + user-select: none; /* firefox does not ignore -webkit-user-select so make this explicit, see #217 */ +} + +@media (max-width: 44rem) { + .appWrapper { + grid-template-columns: 100%; + grid-template-rows: auto 0 1fr auto auto; + } + + .appNaviWrapper { + grid-template-columns: 100%; + grid-template-rows: 1fr auto; + } +} + .map { grid-column: 1 / span 3; grid-row: 1 / span 1; @@ -213,7 +239,6 @@ grid-template-rows: auto 0 1fr auto; } } - /* mapilion */ @font-face { font-family: 'Open Sans'; @@ -236,16 +261,16 @@ src: url(./fonts/RobotoCondensed-Regular.ttf) format('truetype'); } -.sidebarCloseButton { +.sidebarCloseButton svg { position: absolute; - color: lightgray !important; /* unsure why !important is necessary here but not in ErrorMessage.module.css for same */ + fill: lightgray; - height: 10px; - width: 10px; + height: 12px; + width: 12px; - right: 7px; - top: 6px; + right: 6px; + top: 5px; } .sidebarCloseButton:hover { diff --git a/src/App.tsx b/src/App.tsx index 65fabf87..221712c2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import { getQueryStore, getRouteStore, getSettingsStore, + getTurnNavigationStore, getCurrentLocationStore, } from '@/stores/Stores' import MapComponent from '@/map/MapComponent' @@ -29,6 +30,7 @@ import { ErrorStoreState } from '@/stores/ErrorStore' import { CurrentLocationStoreState } from '@/stores/CurrentLocationStore' import Search from '@/sidebar/search/Search' import ErrorMessage from '@/sidebar/ErrorMessage' +import { TurnNavigationStoreState } from './stores/TurnNavigationStore' import useBackgroundLayer from '@/layers/UseBackgroundLayer' import useQueryPointsLayer from '@/layers/UseQueryPointsLayer' import usePathsLayer from '@/layers/UsePathsLayer' @@ -41,10 +43,12 @@ import useRoutingGraphLayer from '@/layers/UseRoutingGraphLayer' import useUrbanDensityLayer from '@/layers/UseUrbanDensityLayer' import useMapBorderLayer from '@/layers/UseMapBorderLayer' import RoutingProfiles from '@/sidebar/search/routingProfiles/RoutingProfiles' +import PlainButton from '@/PlainButton' +import TurnNavigation from '@/turnNavigation/TurnNavigation' import MapPopups from '@/map/MapPopups' +import useNavigationLocationLayer from '@/layers/NavigationLocationLayer' import Menu from '@/sidebar/menu.svg' import Cross from '@/sidebar/times-solid.svg' -import PlainButton from '@/PlainButton' import useAreasLayer from '@/layers/UseAreasLayer' import useExternalMVTLayer from '@/layers/UseExternalMVTLayer' import LocationButton from '@/map/LocationButton' @@ -60,6 +64,7 @@ export default function App() { const [query, setQuery] = useState(getQueryStore().state) const [info, setInfo] = useState(getApiInfoStore().state) const [route, setRoute] = useState(getRouteStore().state) + const [turnNavigation, setTurnNavigation] = useState(getTurnNavigationStore().state) const [error, setError] = useState(getErrorStore().state) const [mapOptions, setMapOptions] = useState(getMapOptionsStore().state) const [pathDetails, setPathDetails] = useState(getPathDetailsStore().state) @@ -76,6 +81,7 @@ export default function App() { const onRouteChanged = () => setRoute(getRouteStore().state) const onErrorChanged = () => setError(getErrorStore().state) const onMapOptionsChanged = () => setMapOptions(getMapOptionsStore().state) + const onTurnNavigationChanged = () => setTurnNavigation(getTurnNavigationStore().state) const onPathDetailsChanged = () => setPathDetails(getPathDetailsStore().state) const onMapFeaturesChanged = () => setMapFeatures(getMapFeatureStore().state) const onPOIsChanged = () => setPOIs(getPOIsStore().state) @@ -87,6 +93,7 @@ export default function App() { getRouteStore().register(onRouteChanged) getErrorStore().register(onErrorChanged) getMapOptionsStore().register(onMapOptionsChanged) + getTurnNavigationStore().register(onTurnNavigationChanged) getPathDetailsStore().register(onPathDetailsChanged) getMapFeatureStore().register(onMapFeaturesChanged) getPOIsStore().register(onPOIsChanged) @@ -97,6 +104,7 @@ export default function App() { onRouteChanged() onErrorChanged() onMapOptionsChanged() + onTurnNavigationChanged() onPathDetailsChanged() onMapFeaturesChanged() onPOIsChanged() @@ -109,6 +117,7 @@ export default function App() { getRouteStore().deregister(onRouteChanged) getErrorStore().deregister(onErrorChanged) getMapOptionsStore().deregister(onMapOptionsChanged) + getTurnNavigationStore().deregister(onTurnNavigationChanged) getPathDetailsStore().deregister(onPathDetailsChanged) getMapFeatureStore().deregister(onMapFeaturesChanged) getPOIsStore().deregister(onPOIsChanged) @@ -127,16 +136,17 @@ export default function App() { const [pathDisplayMode, setPathDisplayMode] = useState('normal') const showPaths = pathDisplayMode !== 'hidden' const inclineOnMap = pathDisplayMode === 'incline' - usePathsLayer(map, route.routingResult.paths, route.selectedPath, query.queryPoints, showPaths) + usePathsLayer(map, route.routingResult.paths, route.selectedPath, query.queryPoints, turnNavigation, showPaths) useQueryPointsLayer(map, query.queryPoints) usePathDetailsLayer(map, pathDetails, showPaths) + useNavigationLocationLayer(map, turnNavigation) usePOIsLayer(map, pois) useCurrentLocationLayer(map, currentLocation) const isSmallScreen = useMediaQuery({ query: '(max-width: 44rem)' }) return ( -
+
- {isSmallScreen ? ( + {turnNavigation.showUI ? ( + <> + +
+ +
+ + ) : isSmallScreen ? ( void } @@ -232,6 +252,7 @@ function LargeScreenLayout({ mapOptions, encodedValues, drawAreas, + turnNavigation, currentLocation, pathDisplayMode, onCyclePathDisplay, @@ -239,6 +260,7 @@ function LargeScreenLayout({ const inclineOnMap = pathDisplayMode === 'incline' const [showSidebar, setShowSidebar] = useState(true) const [showCustomModelBox, setShowCustomModelBox] = useState(false) + const [elevationState, setElevationState] = useState<'compact' | 'expanded' | 'closed'>('closed') const hasRoute = route.selectedPath.points.coordinates.length > 0 const routeRequestPending = query.currentRequest.subRequests.some(r => r.state === RequestState.SENT) @@ -281,7 +303,12 @@ function LargeScreenLayout({ drawAreas={drawAreas} /> )} - +
{!error.isDismissed && }
@@ -368,6 +396,7 @@ function SmallScreenLayout({ mapOptions, encodedValues, drawAreas, + turnNavigation, currentLocation, pathDisplayMode, onCyclePathDisplay, @@ -392,6 +421,7 @@ function SmallScreenLayout({ route={route} error={error} encodedValues={encodedValues} + turnNavigationSettings={turnNavigation.settings} drawAreas={drawAreas} map={map} /> @@ -444,6 +474,7 @@ function SmallScreenLayout({ selectedPath={route.selectedPath} currentRequest={query.currentRequest} profile={query.routingProfile.name} + turnNavigation={turnNavigation} inclineOnMap={inclineOnMap} /> diff --git a/src/Converters.ts b/src/Converters.ts index fb435e70..fccafbd8 100644 --- a/src/Converters.ts +++ b/src/Converters.ts @@ -1,6 +1,6 @@ -import { GeocodingHit } from '@/api/graphhopper' +import {GeocodingHit} from '@/api/graphhopper' -import { Coordinate } from '@/utils' +import {Coordinate} from '@/utils' export function milliSecondsToText(ms: number) { const hours = Math.floor(ms / 3600000) @@ -12,7 +12,8 @@ export function milliSecondsToText(ms: number) { return (hourText ? hourText + ' ' : '') + minutes + ' min' } -let distanceFormat: Intl.NumberFormat = new Intl.NumberFormat('en', { maximumFractionDigits: 1 }) +let distanceFormat: Intl.NumberFormat = new Intl.NumberFormat('en', {maximumFractionDigits: 1}) + export function setDistanceFormat(_distanceFormat: Intl.NumberFormat) { distanceFormat = _distanceFormat } @@ -51,6 +52,22 @@ export function metersToShortText(meters: number, showDistanceInMiles: boolean) } } +export function kmToMPHIfMiles(value: number, showDistanceInMiles: boolean, roundTo10 = false) { + return showDistanceInMiles + ? roundTo10 + ? Math.round(value / 1.60934 / 10.0) * 10 + : Math.round(value / 1.60934) + : Math.round(value) +} + +export function meterToFt(value: number) { + return value / 0.3048 +} + +export function meterToMiles(value: number) { + return value / 1609.34 +} + export function hitToItem(hit: GeocodingHit) { const mainText = hit.street && hit.name.indexOf(hit.street) >= 0 diff --git a/src/SpeechSynthesizer.ts b/src/SpeechSynthesizer.ts new file mode 100644 index 00000000..576d253d --- /dev/null +++ b/src/SpeechSynthesizer.ts @@ -0,0 +1,24 @@ +export interface SpeechSynthesizer { + synthesize(text: string): void +} + +export class SpeechSynthesizerImpl implements SpeechSynthesizer { + private readonly locale: string + private readonly speechSynthesisAPIAvailable: boolean + + constructor(locale: string) { + this.locale = locale + this.speechSynthesisAPIAvailable = 'speechSynthesis' in window + } + + synthesize(text: string) { + if (this.speechSynthesisAPIAvailable) { + let utterance = new SpeechSynthesisUtterance(text) + utterance.lang = this.locale + if (speechSynthesis.pending) speechSynthesis.cancel() + speechSynthesis.speak(utterance) + } else { + console.log('no speechSynthesis API available') + } + } +} diff --git a/src/actions/Actions.ts b/src/actions/Actions.ts index bff40e7c..26c6595e 100644 --- a/src/actions/Actions.ts +++ b/src/actions/Actions.ts @@ -2,6 +2,7 @@ import { Action } from '@/stores/Dispatcher' import { QueryPoint } from '@/stores/QueryStore' import { ApiInfo, Bbox, Path, RoutingArgs, RoutingProfile, RoutingResult } from '@/api/graphhopper' import { PathDetailsPoint } from '@/stores/PathDetailsStore' +import { TNSettingsState } from '@/stores/TurnNavigationStore' import { ChartPathDetail } from '@/pathDetails/elevationWidget/types' import { POI } from '@/stores/POIsStore' import { Settings } from '@/stores/SettingsStore' @@ -15,6 +16,50 @@ export class InfoReceived implements Action { } } +export class TurnNavigationStop implements Action {} + +export class TurnNavigationStart implements Action {} + +export class LocationUpdateSync implements Action { + readonly enableViewSync: boolean + + constructor(enableViewSync: boolean) { + this.enableViewSync = enableViewSync + } +} + +export class LocationUpdate implements Action { + readonly coordinate: Coordinate + readonly speed: number // in meter/sec + readonly heading: number + readonly syncView: boolean + + constructor(coordinate: Coordinate, syncView: boolean, speed: number, heading: number) { + this.coordinate = coordinate + this.speed = speed + this.syncView = syncView + this.heading = heading + } +} + +export class TurnNavigationSettingsUpdate implements Action { + readonly settings: TNSettingsState + + constructor(settings: TNSettingsState) { + this.settings = settings + } +} + +export class TurnNavigationReroutingFailed implements Action {} +export class TurnNavigationReroutingTimeResetForTest implements Action {} +export class TurnNavigationRerouting implements Action { + readonly path: Path + + constructor(path: Path) { + this.path = path + } +} + export class SetPoint implements Action { readonly point: QueryPoint readonly zoomResponse: boolean @@ -158,9 +203,11 @@ export class DismissLastError implements Action {} export class SelectMapLayer implements Action { readonly layer: string + readonly forNavigation: boolean - constructor(layer: string) { + constructor(layer: string, forNavigation: boolean = false) { this.layer = layer + this.forNavigation = forNavigation } } @@ -258,6 +305,9 @@ export class InstructionClicked implements Action { } } +export class ToggleVectorTilesForNavigation implements Action {} +export class ToggleFullScreenForNavigation implements Action {} + export class UpdateSettings implements Action { readonly updatedSettings: Partial diff --git a/src/api/Api.ts b/src/api/Api.ts index 3c79b855..4d5df4e2 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -301,6 +301,14 @@ export class ApiImpl implements Api { request['timeout_ms'] = 10000 } + if (args.heading) { + // for navigation we use heading => we have to disable CH + request['ch.disable'] = true + request.headings = [args.heading] + request.heading_penalty = 120 + request['timeout_ms'] = 10000 + } + if ( args.points.length <= 2 && args.maxAlternativeRoutes > 1 && @@ -344,7 +352,7 @@ export class ApiImpl implements Api { } } - private static decodeResult(result: RawResult, is3D: boolean) { + public static decodeResult(result: RawResult, is3D: boolean) { return result.paths .map((path: RawPath) => { return { diff --git a/src/api/graphhopper.d.ts b/src/api/graphhopper.d.ts index dad29b6d..06440947 100644 --- a/src/api/graphhopper.d.ts +++ b/src/api/graphhopper.d.ts @@ -7,6 +7,7 @@ export type Bbox = [number, number, number, number] export interface RoutingArgs { readonly points: [number, number][] + readonly heading?: number readonly profile: string readonly maxAlternativeRoutes: number readonly customModel: CustomModel | null @@ -20,6 +21,8 @@ export interface RoutingRequest { points_encoded_multiplier: number instructions: boolean elevation: boolean + headings?: number[] + heading_penalty?: number 'alternative_route.max_paths'?: number 'alternative_route.max_weight_factor'?: number 'ch.disable'?: boolean @@ -94,16 +97,19 @@ export interface Instruction { readonly points: number[][] readonly sign: number readonly text: string + readonly street_name: string readonly motorway_junction: string readonly time: number } interface Details { readonly street_name: [number, number, string][] + readonly surface: [number, number, string][] + readonly road_environment: [number, number, string][] + readonly road_class: [number, number, string][] readonly toll: [number, number, string][] readonly max_speed: [number, number, number][] - readonly road_class: [number, number, string][] - readonly road_environment: [number, number, string][] + readonly average_speed: [number, number, number][] readonly road_access: [number, number, string][] readonly surface: [number, number, string][] readonly bike_network: [number, number, string][] diff --git a/src/custom.d.ts b/src/custom.d.ts index c1194472..3858015c 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -3,6 +3,10 @@ declare module '*.svg' declare module '*.png' declare module 'custom-model-editor/src/index' +interface Window { + ghSaveFile: ({ fileName: string, mimeType: string, fileContents: xmlString }) => Promise +} + declare module 'config' { interface ProfileGroup { readonly options: { profile: string }[] @@ -11,6 +15,7 @@ declare module 'config' { const routingApi: string const geocodingApi: string const defaultTiles: string + const navigationTiles: string const keys: { graphhopper: string omniscale: string diff --git a/src/index.html b/src/index.html index 95b82adf..bcc7b9ac 100644 --- a/src/index.html +++ b/src/index.html @@ -3,18 +3,25 @@ - + + - + - GraphHopper Maps | Route Planner + GraphHopper Maps | Route Planner and GPS Navigation