├── .eslintrc.cjs ├── .github ├── FUNDING.yml ├── dependabot.yml ├── release.yml └── workflows │ ├── build-deploy-demo.yml │ ├── build-publish-lib.yml │ └── pull-request-checks.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .nvmrc ├── .prettierignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── demo ├── README.md ├── env.d.ts ├── index.html └── src │ ├── App.svelte │ ├── Menu.svelte │ ├── assets │ ├── map │ │ ├── custom-directions.ts │ │ ├── distance-measurement-directions.ts │ │ ├── images │ │ │ ├── balloon-hoverpoint.png │ │ │ ├── balloon-snappoint.png │ │ │ ├── balloon-waypoint.png │ │ │ ├── direction-arrow.png │ │ │ └── routeline.png │ │ ├── restyling-example-layers.ts │ │ └── style │ │ │ ├── .travis.yml │ │ │ ├── README.md │ │ │ ├── icons │ │ │ ├── circle-11.svg │ │ │ └── star-11.svg │ │ │ └── style.json │ └── styles │ │ └── index.css │ ├── components │ └── AppSidebar.svelte │ ├── examples │ ├── 1 User Interaction.svelte │ ├── 10 Bearings Support and Control.svelte │ ├── 11 Restyling.svelte │ ├── 12 Distance Measurement.svelte │ ├── 13 Load and Save.svelte │ ├── 14 Multiple profiles.svelte │ ├── 2 Programmatical Control.svelte │ ├── 3 Mapbox Directions API and Congestions.svelte │ ├── 4 Origin and Destination.svelte │ ├── 5 Show Routes' Directions.svelte │ ├── 6 Touch-Friendly Features.svelte │ ├── 7 Events.svelte │ ├── 8 Aborting Requests.svelte │ ├── 9 Loading Indicator Control.svelte │ └── README.md │ ├── main.ts │ └── router.ts ├── doc ├── BASIC_USAGE.md ├── CONTROLS.md ├── CUSTOMIZATION.md ├── MAIN.md ├── README.md └── images │ ├── complex-customization.png │ ├── demo-screenshot-1.png │ ├── demo-screenshot-2.png │ ├── demo-screenshot-3.png │ ├── public-filter.png │ └── public-protected-filter.png ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── src ├── README.md ├── controls │ ├── bearings │ │ ├── BearingsControl.svelte │ │ ├── main.ts │ │ └── types.ts │ ├── common.css │ └── loading-indicator │ │ ├── LoadingIndicatorControl.svelte │ │ ├── main.ts │ │ └── types.ts ├── directions │ ├── events.ts │ ├── helpers.ts │ ├── layers.ts │ ├── main.ts │ ├── types.ts │ └── utils.ts ├── env.d.ts ├── main.ts └── tsconfig.json ├── svelte.config.cjs ├── tailwind.config.cjs ├── tsconfig.json ├── tsconfig.lib.json ├── tsconfig.node.json ├── typedoc.json ├── vite.demo.config.ts └── vite.lib.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:svelte/recommended"], 3 | parser: "@typescript-eslint/parser", 4 | parserOptions: { 5 | ecmaVersion: 2021, 6 | sourceType: "module", 7 | tsconfigRootDir: __dirname, 8 | project: ["./tsconfig.json"], 9 | extraFileExtensions: [".svelte"], 10 | }, 11 | env: { 12 | es6: true, 13 | browser: true, 14 | }, 15 | overrides: [ 16 | { 17 | files: ["*.svelte"], 18 | parser: "svelte-eslint-parser", 19 | // Parse the ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/src/App.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /demo/src/Menu.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | Examples 9 | 10 | 17 | 18 | 19 |
20 |
23 |

Please, choose an example at the sidebar on the left

24 | 25 | or navigate to 26 | 27 |
28 | GitHub 29 | API 30 |
31 |
32 |
33 | 34 | 44 | -------------------------------------------------------------------------------- /demo/src/assets/map/custom-directions.ts: -------------------------------------------------------------------------------- 1 | import type maplibregl from "maplibre-gl"; 2 | import type { MapLibreGlDirectionsConfiguration, Feature, LineString, Point } from "@maplibre/maplibre-gl-directions"; 3 | import MapLibreGlDirections from "@maplibre/maplibre-gl-directions"; 4 | import { MapLibreGlDirectionsWaypointEvent } from "@maplibre/maplibre-gl-directions"; 5 | 6 | export default class CustomMapLibreGlDirections extends MapLibreGlDirections { 7 | constructor(map: maplibregl.Map, configuration?: Partial) { 8 | super(map, configuration); 9 | } 10 | 11 | // augmented public interface 12 | 13 | get waypointsFeatures() { 14 | return this._waypoints; 15 | } 16 | 17 | setWaypointsFeatures(waypointsFeatures: Feature[]) { 18 | this._waypoints = waypointsFeatures; 19 | 20 | this.assignWaypointsCategories(); 21 | 22 | const waypointEvent = new MapLibreGlDirectionsWaypointEvent("setwaypoints", undefined); 23 | this.fire(waypointEvent); 24 | 25 | this.draw(); 26 | } 27 | 28 | get snappointsFeatures() { 29 | return this.snappoints; 30 | } 31 | 32 | setSnappointsFeatures(snappointsFeatures: Feature[]) { 33 | this.snappoints = snappointsFeatures; 34 | this.draw(); 35 | } 36 | 37 | get routelinesFeatures() { 38 | return this.routelines; 39 | } 40 | 41 | setRoutelinesFeatures(routelinesFeatures: Feature[][]) { 42 | this.routelines = routelinesFeatures; 43 | this.draw(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /demo/src/assets/map/distance-measurement-directions.ts: -------------------------------------------------------------------------------- 1 | import type maplibregl from "maplibre-gl"; 2 | import type { 3 | MapLibreGlDirectionsConfiguration, 4 | Route, 5 | Feature, 6 | Point, 7 | LineString, 8 | } from "@maplibre/maplibre-gl-directions"; 9 | import MapLibreGlDirections from "@maplibre/maplibre-gl-directions"; 10 | import { utils } from "@maplibre/maplibre-gl-directions"; 11 | 12 | export default class DistanceMeasurementMapLibreGlDirections extends MapLibreGlDirections { 13 | constructor(map: maplibregl.Map, configuration?: Partial) { 14 | super(map, configuration); 15 | } 16 | 17 | // here we save the original method to be able to use it in the re-defined one. For some methods (namely those 18 | // that are defined as methods and not as properties) you can instead call their "super" counterparts, but for the 19 | // methods as `buildRoutelines` it's impossible due to restrictions implied by the language itself, so that's the 20 | // only reasonable way to be able to use the original functionality as a part of the re-defined method 21 | originalBuildRoutelines = utils.buildRoutelines; 22 | 23 | // re-defining the original `buildRoutelines` method 24 | protected buildRoutelines = ( 25 | requestOptions: MapLibreGlDirectionsConfiguration["requestOptions"], 26 | routes: Route[], 27 | selectedRouteIndex: number, 28 | snappoints: Feature[], 29 | ): Feature[][] => { 30 | // first we call the original method. It returns the built routelines 31 | const routelines = this.originalBuildRoutelines(requestOptions, routes, selectedRouteIndex, snappoints); 32 | 33 | // then we modify the routelines adding to each route leg a property that stores the leg's distance 34 | routelines[0].forEach((leg, index) => { 35 | if (leg.properties) leg.properties.distance = routes[0].legs[index].distance as number; 36 | }); 37 | 38 | // and returning the modified routelines 39 | return routelines; 40 | }; 41 | } 42 | 43 | // using a different source name. That might become useful if you'd like to use the Distance Measurement Directions 44 | // instance along with a normal Directions instance on the same map 45 | const sourceName = "distance-measurement-maplibre-gl-directions"; 46 | 47 | const config: Partial = { 48 | sourceName, 49 | layers: [ 50 | { 51 | id: `${sourceName}-snapline`, 52 | type: "line", 53 | source: sourceName, 54 | layout: { 55 | "line-cap": "round", 56 | "line-join": "round", 57 | }, 58 | paint: { 59 | "line-dasharray": [3, 3], 60 | "line-color": "#34343f", 61 | "line-width": 2, 62 | }, 63 | filter: ["==", ["get", "type"], "SNAPLINE"], 64 | }, 65 | { 66 | id: `${sourceName}-routeline`, 67 | type: "line", 68 | source: sourceName, 69 | layout: { 70 | "line-cap": "butt", 71 | "line-join": "round", 72 | }, 73 | paint: { 74 | "line-color": "#212121", 75 | "line-opacity": 0.85, 76 | "line-width": 3, 77 | }, 78 | filter: ["==", ["get", "route"], "SELECTED"], 79 | }, 80 | { 81 | id: `${sourceName}-routeline-distance`, 82 | type: "symbol", 83 | source: sourceName, 84 | layout: { 85 | "symbol-placement": "line-center", 86 | "text-field": "{distance}m", 87 | "text-size": 16, 88 | "text-ignore-placement": true, 89 | "text-allow-overlap": true, 90 | "text-overlap": "always", 91 | }, 92 | paint: { 93 | "text-color": "#212121", 94 | "text-halo-color": "#ffffff", 95 | "text-halo-width": 1, 96 | }, 97 | filter: ["==", ["get", "route"], "SELECTED"], 98 | }, 99 | { 100 | id: `${sourceName}-hoverpoint`, 101 | type: "circle", 102 | source: sourceName, 103 | paint: { 104 | "circle-radius": 9, 105 | "circle-color": "#212121", 106 | }, 107 | filter: ["==", ["get", "type"], "HOVERPOINT"], 108 | }, 109 | { 110 | id: `${sourceName}-snappoint`, 111 | type: "circle", 112 | source: sourceName, 113 | paint: { 114 | "circle-radius": ["case", ["boolean", ["get", "highlight"], false], 9, 7], 115 | "circle-color": ["case", ["boolean", ["get", "highlight"], false], "#313131", "#494949"], 116 | }, 117 | filter: ["==", ["get", "type"], "SNAPPOINT"], 118 | }, 119 | { 120 | id: `${sourceName}-waypoint`, 121 | type: "circle", 122 | source: sourceName, 123 | paint: { 124 | "circle-radius": ["case", ["boolean", ["get", "highlight"], false], 9, 7], 125 | "circle-color": ["case", ["boolean", ["get", "highlight"], false], "#212121", "#2c2c2c"], 126 | }, 127 | filter: ["==", ["get", "type"], "WAYPOINT"], 128 | }, 129 | ] as maplibregl.LayerSpecification[], 130 | // don't forget to update the sensitive layers 131 | sensitiveSnappointLayers: [`${sourceName}-snappoint`], 132 | sensitiveWaypointLayers: [`${sourceName}-waypoint`], 133 | sensitiveRoutelineLayers: [`${sourceName}-routeline`], 134 | sensitiveAltRoutelineLayers: [], 135 | }; 136 | 137 | export { config }; 138 | -------------------------------------------------------------------------------- /demo/src/assets/map/images/balloon-hoverpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/d829ccd0311e64aadf75f9900a3e76549cd402aa/demo/src/assets/map/images/balloon-hoverpoint.png -------------------------------------------------------------------------------- /demo/src/assets/map/images/balloon-snappoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/d829ccd0311e64aadf75f9900a3e76549cd402aa/demo/src/assets/map/images/balloon-snappoint.png -------------------------------------------------------------------------------- /demo/src/assets/map/images/balloon-waypoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/d829ccd0311e64aadf75f9900a3e76549cd402aa/demo/src/assets/map/images/balloon-waypoint.png -------------------------------------------------------------------------------- /demo/src/assets/map/images/direction-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/d829ccd0311e64aadf75f9900a3e76549cd402aa/demo/src/assets/map/images/direction-arrow.png -------------------------------------------------------------------------------- /demo/src/assets/map/images/routeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/d829ccd0311e64aadf75f9900a3e76549cd402aa/demo/src/assets/map/images/routeline.png -------------------------------------------------------------------------------- /demo/src/assets/map/restyling-example-layers.ts: -------------------------------------------------------------------------------- 1 | import type { LayerSpecification } from "maplibre-gl"; 2 | 3 | // The following layers are used in the "Restyling" example. 4 | export const layers = [ 5 | { 6 | id: "maplibre-gl-directions-snapline", 7 | type: "line", 8 | source: "maplibre-gl-directions", 9 | layout: { 10 | "line-cap": "round", 11 | "line-join": "round", 12 | }, 13 | paint: { 14 | "line-dasharray": [2, 2], 15 | "line-color": "#ffffff", 16 | "line-opacity": 0.65, 17 | "line-width": 2, 18 | }, 19 | filter: ["==", ["get", "type"], "SNAPLINE"], 20 | }, 21 | 22 | { 23 | id: "maplibre-gl-directions-alt-routeline", 24 | type: "line", 25 | source: "maplibre-gl-directions", 26 | layout: { 27 | "line-cap": "butt", 28 | "line-join": "round", 29 | }, 30 | paint: { 31 | "line-pattern": "routeline", 32 | "line-width": 12, 33 | "line-opacity": 0.5, 34 | }, 35 | filter: ["==", ["get", "route"], "ALT"], 36 | }, 37 | 38 | { 39 | id: "maplibre-gl-directions-routeline", 40 | type: "line", 41 | source: "maplibre-gl-directions", 42 | layout: { 43 | "line-cap": "butt", 44 | "line-join": "round", 45 | }, 46 | paint: { 47 | "line-pattern": "routeline", 48 | "line-width": 12, 49 | }, 50 | filter: ["==", ["get", "route"], "SELECTED"], 51 | }, 52 | 53 | { 54 | id: "maplibre-gl-directions-hoverpoint", 55 | type: "symbol", 56 | source: "maplibre-gl-directions", 57 | layout: { 58 | "icon-image": "balloon-hoverpoint", 59 | "icon-anchor": "bottom", 60 | "icon-ignore-placement": true, 61 | "icon-overlap": "always", 62 | }, 63 | filter: ["==", ["get", "type"], "HOVERPOINT"], 64 | }, 65 | 66 | { 67 | id: "maplibre-gl-directions-snappoint", 68 | type: "symbol", 69 | source: "maplibre-gl-directions", 70 | layout: { 71 | "icon-image": "balloon-snappoint", 72 | "icon-anchor": "bottom", 73 | "icon-ignore-placement": true, 74 | "icon-overlap": "always", 75 | }, 76 | filter: ["==", ["get", "type"], "SNAPPOINT"], 77 | }, 78 | 79 | { 80 | id: "maplibre-gl-directions-waypoint", 81 | type: "symbol", 82 | source: "maplibre-gl-directions", 83 | layout: { 84 | "icon-image": "balloon-waypoint", 85 | "icon-anchor": "bottom", 86 | "icon-ignore-placement": true, 87 | "icon-overlap": "always", 88 | }, 89 | filter: ["==", ["get", "type"], "WAYPOINT"], 90 | }, 91 | ] as LayerSpecification[]; 92 | -------------------------------------------------------------------------------- /demo/src/assets/map/style/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: required 3 | node_js: 4 | - "4" 5 | branches: 6 | only: 7 | - master 8 | - /^v\d+(\.\d+)*$/ 9 | addons: 10 | apt: 11 | sources: 12 | - ubuntu-toolchain-r-test 13 | - llvm-toolchain-precise-3.5 14 | packages: 15 | - libstdc++6 16 | script: 17 | - "git clone https://github.com/klokantech/gl-style-package-spec.git" 18 | - cd gl-style-package-spec 19 | - bash ./task/run.sh 20 | env: 21 | global: 22 | - secure: >- 23 | XeQHO3LqTXJtOhko9TT54HlBkvUM60ToGviYzNZzzrvM6W1ReURGjOovRXf0hZY3RO5W2zdBRy7FmFo6F2uO4c1BespNpMrgqY8tbZdk1LNvhrpaLjGEZPAcX32JnFDnEJs1USZHNQlhF5blHg76R/6QyNxt7uJGy7um86B2PWZORGjnky7Ct6/6FIIYToK3V2qrnVsasL0I7M5jEMnPQ6Bh/DjGGmIR9q3mbAFb9DSmqKoVoZX7uAv4hbilM7milRkWhUtHHhxOUNWPoQChSdOkAYXZj1FZ5eFToOdwqCQdr/YdXsnKcLgp4w+oadnjcBHeq8WRzKqrcabHeBEGqc9OApryaAzubd+1r4pXTQcYcDuZTftGtMt6ZFlwH4FMMfofuzPFU0nvoh6H29Qlk8u75h9TjV5sTHA5VRzS9vQ5Tvo2UFhi50xyzbm0Ra2sQAHH9sw8wkg6hrLtqppIJ4mEVmvJg3Rk15au2XSbZixfzBE6fpVf1SnFxwKGV4H/Zc1zJHFlDA5v9FTDSq51fdHxUPiDX98U6FfURJ4HuGhinhADmYoRSdhn6e0ls3QKn+jZ1B3htrgJhSLQ8ZLj3yNFsqRsPYfSLf2S8R5Yn6zT5bypbyddk5jZUKW5eOcbru89UXtSyBlpnb73CJkIxwrxzkcwDa6JHtw8E/IeObY= 24 | deploy: 25 | provider: releases 26 | api_key: "${GITHUB_TOKEN}" 27 | file: "build/${TRAVIS_TAG}.zip" 28 | skip_cleanup: true 29 | "on": 30 | tags: true 31 | services: 32 | - docker 33 | -------------------------------------------------------------------------------- /demo/src/assets/map/style/README.md: -------------------------------------------------------------------------------- 1 | # Fiord Color 2 | 3 | [![Build Status](https://travis-ci.org/openmaptiles/fiord-color-gl-style.svg?branch=master)](https://travis-ci.org/openmaptiles/fiord-color-gl-style) 4 | 5 | A basemap style useful with fiord color ideal as background map for data visualizations. It is using the vector tile 6 | schema of [OpenMapTiles](https://github.com/openmaptiles/openmaptiles). 7 | 8 | ## Preview 9 | 10 | **[:globe_with_meridians: Browse the map](https://openmaptiles.github.io/fiord-color-gl-style/)** 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ## Edit the Style 21 | 22 | Use the [Maputnik CLI](http://openmaptiles.org/docs/style/maputnik/) to edit and develop the style. 23 | After you've started Maputnik open the editor on `localhost:8000`. 24 | 25 | ``` 26 | maputnik --watch --file style.json 27 | ``` 28 | 29 | ## License 30 | 31 | - [ ] Clarify license 32 | -------------------------------------------------------------------------------- /demo/src/assets/map/style/icons/circle-11.svg: -------------------------------------------------------------------------------- 1 | circle-11.svg -------------------------------------------------------------------------------- /demo/src/assets/map/style/icons/star-11.svg: -------------------------------------------------------------------------------- 1 | star-11.svg -------------------------------------------------------------------------------- /demo/src/assets/map/style/style.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "Fiord Color", 4 | "metadata": { 5 | "mapbox:autocomposite": false, 6 | "mapbox:groups": { 7 | "101da9f13b64a08fa4b6ac1168e89e5f": { "collapsed": true, "name": "Places" }, 8 | "a14c9607bc7954ba1df7205bf660433f": { 9 | "collapsed": true, 10 | "name": "Boundaries" 11 | }, 12 | "b6371a3f2f5a9932464fa3867530a2e5": { 13 | "collapsed": true, 14 | "name": "Transportation" 15 | } 16 | }, 17 | "mapbox:type": "template", 18 | "openmaptiles:version": "3.x" 19 | }, 20 | "center": [0.41134944662525186, -1.7053025658242404e-13], 21 | "zoom": 1.3869401319571246, 22 | "bearing": 0, 23 | "pitch": 0, 24 | "sources": { 25 | "openmaptiles": { 26 | "type": "vector", 27 | "url": "https://api.maptiler.com/tiles/v3/tiles.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL" 28 | } 29 | }, 30 | "sprite": "https://openmaptiles.github.io/fiord-color-gl-style/sprite", 31 | "glyphs": "https://api.maptiler.com/fonts/{fontstack}/{range}.pbf?key=get_your_own_OpIi9ZULNHzrESv6T2vL", 32 | "layers": [ 33 | { 34 | "id": "background", 35 | "type": "background", 36 | "paint": { "background-color": "#45516E" } 37 | }, 38 | { 39 | "id": "water", 40 | "type": "fill", 41 | "source": "openmaptiles", 42 | "source-layer": "water", 43 | "filter": ["==", "$type", "Polygon"], 44 | "layout": { "visibility": "visible" }, 45 | "paint": { "fill-antialias": false, "fill-color": "#38435C" } 46 | }, 47 | { 48 | "id": "landcover_ice_shelf", 49 | "type": "fill", 50 | "source": "openmaptiles", 51 | "source-layer": "landcover", 52 | "maxzoom": 8, 53 | "filter": ["all", ["==", "$type", "Polygon"], ["==", "subclass", "ice_shelf"]], 54 | "layout": { "visibility": "visible" }, 55 | "paint": { "fill-color": "hsl(232, 33%, 34%)", "fill-opacity": 0.4 } 56 | }, 57 | { 58 | "id": "landuse_residential", 59 | "type": "fill", 60 | "source": "openmaptiles", 61 | "source-layer": "landuse", 62 | "maxzoom": 16, 63 | "filter": ["all", ["==", "$type", "Polygon"], ["==", "subclass", "residential"]], 64 | "layout": { "visibility": "visible" }, 65 | "paint": { 66 | "fill-color": "rgb(234, 234, 230)", 67 | "fill-opacity": { 68 | "base": 0.6, 69 | "stops": [ 70 | [8, 0.8], 71 | [9, 0.6] 72 | ] 73 | } 74 | } 75 | }, 76 | { 77 | "id": "landcover_wood", 78 | "type": "fill", 79 | "source": "openmaptiles", 80 | "source-layer": "landcover", 81 | "minzoom": 10, 82 | "filter": ["all", ["==", "$type", "Polygon"], ["==", "class", "wood"]], 83 | "layout": { "visibility": "visible" }, 84 | "paint": { 85 | "fill-color": "hsla(232, 18%, 30%, 0.57)", 86 | "fill-opacity": { 87 | "base": 1, 88 | "stops": [ 89 | [9, 0], 90 | [12, 1] 91 | ] 92 | } 93 | } 94 | }, 95 | { 96 | "id": "park", 97 | "type": "fill", 98 | "source": "openmaptiles", 99 | "source-layer": "park", 100 | "filter": ["==", "$type", "Polygon"], 101 | "layout": { "visibility": "visible" }, 102 | "paint": { "fill-color": "hsl(204, 17%, 35%)", "fill-opacity": 0.3 } 103 | }, 104 | { 105 | "id": "park_outline", 106 | "type": "line", 107 | "source": "openmaptiles", 108 | "source-layer": "park", 109 | "filter": ["==", "$type", "Polygon"], 110 | "layout": {}, 111 | "paint": { "line-color": "hsl(205, 49%, 31%)", "line-dasharray": [2, 2] } 112 | }, 113 | { 114 | "id": "waterway", 115 | "type": "line", 116 | "source": "openmaptiles", 117 | "source-layer": "waterway", 118 | "filter": ["==", "$type", "LineString"], 119 | "layout": { "visibility": "visible" }, 120 | "paint": { "line-color": "hsl(232, 23%, 28%)", "line-opacity": 0.6 } 121 | }, 122 | { 123 | "id": "water_name", 124 | "type": "symbol", 125 | "source": "openmaptiles", 126 | "source-layer": "water_name", 127 | "filter": ["==", "$type", "LineString"], 128 | "layout": { 129 | "symbol-placement": "line", 130 | "symbol-spacing": 500, 131 | "text-field": "{name:latin}\n{name:nonlatin}", 132 | "text-font": ["Metropolis Medium Italic", "Noto Sans Italic"], 133 | "text-rotation-alignment": "map", 134 | "text-size": 12, 135 | "visibility": "visible" 136 | }, 137 | "paint": { 138 | "text-color": "hsl(223, 21%, 52%)", 139 | "text-halo-blur": 0, 140 | "text-halo-color": "hsl(232, 5%, 19%)", 141 | "text-halo-width": 1 142 | } 143 | }, 144 | { 145 | "id": "building", 146 | "type": "fill", 147 | "source": "openmaptiles", 148 | "source-layer": "building", 149 | "minzoom": 12, 150 | "filter": ["==", "$type", "Polygon"], 151 | "paint": { 152 | "fill-antialias": false, 153 | "fill-color": "hsla(232, 47%, 18%, 0.65)", 154 | "fill-opacity": 0.25 155 | } 156 | }, 157 | { 158 | "id": "tunnel_motorway_casing", 159 | "type": "line", 160 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 161 | "source": "openmaptiles", 162 | "source-layer": "transportation", 163 | "minzoom": 6, 164 | "filter": [ 165 | "all", 166 | ["==", "$type", "LineString"], 167 | ["all", ["==", "brunnel", "tunnel"], ["==", "class", "motorway"]] 168 | ], 169 | "layout": { 170 | "line-cap": "butt", 171 | "line-join": "miter", 172 | "visibility": "visible" 173 | }, 174 | "paint": { 175 | "line-color": "#3C4357", 176 | "line-opacity": 1, 177 | "line-width": { 178 | "base": 1.4, 179 | "stops": [ 180 | [5.8, 0], 181 | [6, 3], 182 | [20, 40] 183 | ] 184 | } 185 | } 186 | }, 187 | { 188 | "id": "tunnel_motorway_inner", 189 | "type": "line", 190 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 191 | "source": "openmaptiles", 192 | "source-layer": "transportation", 193 | "minzoom": 6, 194 | "filter": [ 195 | "all", 196 | ["==", "$type", "LineString"], 197 | ["all", ["==", "brunnel", "tunnel"], ["==", "class", "motorway"]] 198 | ], 199 | "layout": { 200 | "line-cap": "round", 201 | "line-join": "round", 202 | "visibility": "visible" 203 | }, 204 | "paint": { 205 | "line-color": "hsl(224, 18%, 21%)", 206 | "line-width": { 207 | "base": 1.4, 208 | "stops": [ 209 | [4, 2], 210 | [6, 1.3], 211 | [20, 30] 212 | ] 213 | } 214 | } 215 | }, 216 | { 217 | "id": "aeroway-taxiway", 218 | "type": "line", 219 | "metadata": { "mapbox:group": "1444849345966.4436" }, 220 | "source": "openmaptiles", 221 | "source-layer": "aeroway", 222 | "minzoom": 12, 223 | "filter": ["all", ["in", "class", "taxiway"]], 224 | "layout": { 225 | "line-cap": "round", 226 | "line-join": "round", 227 | "visibility": "visible" 228 | }, 229 | "paint": { 230 | "line-color": "hsl(224, 22%, 45%)", 231 | "line-opacity": 1, 232 | "line-width": { 233 | "base": 1.55, 234 | "stops": [ 235 | [13, 1.8], 236 | [20, 20] 237 | ] 238 | } 239 | } 240 | }, 241 | { 242 | "id": "aeroway-runway-casing", 243 | "type": "line", 244 | "metadata": { "mapbox:group": "1444849345966.4436" }, 245 | "source": "openmaptiles", 246 | "source-layer": "aeroway", 247 | "minzoom": 11, 248 | "filter": ["all", ["in", "class", "runway"]], 249 | "layout": { 250 | "line-cap": "round", 251 | "line-join": "round", 252 | "visibility": "visible" 253 | }, 254 | "paint": { 255 | "line-color": "hsl(224, 22%, 45%)", 256 | "line-opacity": 1, 257 | "line-width": { 258 | "base": 1.5, 259 | "stops": [ 260 | [11, 6], 261 | [17, 55] 262 | ] 263 | } 264 | } 265 | }, 266 | { 267 | "id": "aeroway-area", 268 | "type": "fill", 269 | "metadata": { "mapbox:group": "1444849345966.4436" }, 270 | "source": "openmaptiles", 271 | "source-layer": "aeroway", 272 | "minzoom": 4, 273 | "filter": ["all", ["==", "$type", "Polygon"], ["in", "class", "runway", "taxiway"]], 274 | "layout": { "visibility": "visible" }, 275 | "paint": { "fill-color": "hsl(224, 20%, 29%)", "fill-opacity": 1 } 276 | }, 277 | { 278 | "id": "aeroway-runway", 279 | "type": "line", 280 | "metadata": { "mapbox:group": "1444849345966.4436" }, 281 | "source": "openmaptiles", 282 | "source-layer": "aeroway", 283 | "minzoom": 11, 284 | "maxzoom": 24, 285 | "filter": ["all", ["in", "class", "runway"], ["==", "$type", "LineString"]], 286 | "layout": { 287 | "line-cap": "round", 288 | "line-join": "round", 289 | "visibility": "visible" 290 | }, 291 | "paint": { 292 | "line-color": "hsl(224, 20%, 29%)", 293 | "line-opacity": 1, 294 | "line-width": { 295 | "base": 1.5, 296 | "stops": [ 297 | [11, 4], 298 | [17, 50] 299 | ] 300 | } 301 | } 302 | }, 303 | { 304 | "id": "road_area_pier", 305 | "type": "fill", 306 | "metadata": {}, 307 | "source": "openmaptiles", 308 | "source-layer": "transportation", 309 | "filter": ["all", ["==", "$type", "Polygon"], ["==", "class", "pier"]], 310 | "layout": { "visibility": "visible" }, 311 | "paint": { "fill-antialias": true, "fill-color": "#45516E" } 312 | }, 313 | { 314 | "id": "road_pier", 315 | "type": "line", 316 | "metadata": {}, 317 | "source": "openmaptiles", 318 | "source-layer": "transportation", 319 | "filter": ["all", ["==", "$type", "LineString"], ["in", "class", "pier"]], 320 | "layout": { "line-cap": "round", "line-join": "round" }, 321 | "paint": { 322 | "line-color": "#45516E", 323 | "line-width": { 324 | "base": 1.2, 325 | "stops": [ 326 | [15, 1], 327 | [17, 4] 328 | ] 329 | } 330 | } 331 | }, 332 | { 333 | "id": "highway_path", 334 | "type": "line", 335 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 336 | "source": "openmaptiles", 337 | "source-layer": "transportation", 338 | "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "path"]], 339 | "layout": { 340 | "line-cap": "round", 341 | "line-join": "round", 342 | "visibility": "visible" 343 | }, 344 | "paint": { 345 | "line-color": "hsl(211, 29%, 38%)", 346 | "line-dasharray": [2, 2], 347 | "line-opacity": 1, 348 | "line-width": { 349 | "base": 1.2, 350 | "stops": [ 351 | [12, 0.5], 352 | [20, 4] 353 | ] 354 | } 355 | } 356 | }, 357 | { 358 | "id": "highway_minor", 359 | "type": "line", 360 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 361 | "source": "openmaptiles", 362 | "source-layer": "transportation", 363 | "minzoom": 8, 364 | "filter": ["all", ["==", "$type", "LineString"], ["in", "class", "minor", "service", "track"]], 365 | "layout": { 366 | "line-cap": "round", 367 | "line-join": "round", 368 | "visibility": "visible" 369 | }, 370 | "paint": { 371 | "line-color": "hsl(224, 22%, 45%)", 372 | "line-opacity": 0.9, 373 | "line-width": { 374 | "base": 1.55, 375 | "stops": [ 376 | [13, 1.8], 377 | [20, 20] 378 | ] 379 | } 380 | } 381 | }, 382 | { 383 | "id": "highway_major_casing", 384 | "type": "line", 385 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 386 | "source": "openmaptiles", 387 | "source-layer": "transportation", 388 | "minzoom": 11, 389 | "filter": ["all", ["==", "$type", "LineString"], ["in", "class", "primary", "secondary", "tertiary", "trunk"]], 390 | "layout": { 391 | "line-cap": "butt", 392 | "line-join": "miter", 393 | "visibility": "visible" 394 | }, 395 | "paint": { 396 | "line-color": "hsl(224, 22%, 45%)", 397 | "line-dasharray": [12, 0], 398 | "line-width": { 399 | "base": 1.3, 400 | "stops": [ 401 | [10, 3], 402 | [20, 23] 403 | ] 404 | } 405 | } 406 | }, 407 | { 408 | "id": "highway_major_inner", 409 | "type": "line", 410 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 411 | "source": "openmaptiles", 412 | "source-layer": "transportation", 413 | "minzoom": 11, 414 | "filter": ["all", ["==", "$type", "LineString"], ["in", "class", "primary", "secondary", "tertiary", "trunk"]], 415 | "layout": { 416 | "line-cap": "round", 417 | "line-join": "round", 418 | "visibility": "visible" 419 | }, 420 | "paint": { 421 | "line-color": "#3C4357", 422 | "line-width": { 423 | "base": 1.3, 424 | "stops": [ 425 | [10, 2], 426 | [20, 20] 427 | ] 428 | } 429 | } 430 | }, 431 | { 432 | "id": "highway_major_subtle", 433 | "type": "line", 434 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 435 | "source": "openmaptiles", 436 | "source-layer": "transportation", 437 | "maxzoom": 11, 438 | "filter": ["all", ["==", "$type", "LineString"], ["in", "class", "primary", "secondary", "tertiary", "trunk"]], 439 | "layout": { 440 | "line-cap": "round", 441 | "line-join": "round", 442 | "visibility": "visible" 443 | }, 444 | "paint": { "line-color": "#3D4355", "line-opacity": 0.6, "line-width": 2 } 445 | }, 446 | { 447 | "id": "highway_motorway_casing", 448 | "type": "line", 449 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 450 | "source": "openmaptiles", 451 | "source-layer": "transportation", 452 | "minzoom": 6, 453 | "filter": [ 454 | "all", 455 | ["==", "$type", "LineString"], 456 | ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"]] 457 | ], 458 | "layout": { 459 | "line-cap": "butt", 460 | "line-join": "miter", 461 | "visibility": "visible" 462 | }, 463 | "paint": { 464 | "line-color": "hsl(224, 22%, 45%)", 465 | "line-dasharray": [2, 0], 466 | "line-opacity": 1, 467 | "line-width": { 468 | "base": 1.4, 469 | "stops": [ 470 | [5.8, 0], 471 | [6, 3], 472 | [20, 40] 473 | ] 474 | } 475 | } 476 | }, 477 | { 478 | "id": "highway_motorway_inner", 479 | "type": "line", 480 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 481 | "source": "openmaptiles", 482 | "source-layer": "transportation", 483 | "minzoom": 6, 484 | "filter": [ 485 | "all", 486 | ["==", "$type", "LineString"], 487 | ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"]] 488 | ], 489 | "layout": { 490 | "line-cap": "round", 491 | "line-join": "round", 492 | "visibility": "visible" 493 | }, 494 | "paint": { 495 | "line-color": { 496 | "base": 1, 497 | "stops": [ 498 | [5.8, "hsla(0, 0%, 85%, 0.53)"], 499 | [6, "hsl(224, 20%, 29%)"] 500 | ] 501 | }, 502 | "line-width": { 503 | "base": 1.4, 504 | "stops": [ 505 | [4, 2], 506 | [6, 1.3], 507 | [20, 30] 508 | ] 509 | } 510 | } 511 | }, 512 | { 513 | "id": "highway_motorway_subtle", 514 | "type": "line", 515 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 516 | "source": "openmaptiles", 517 | "source-layer": "transportation", 518 | "maxzoom": 6, 519 | "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "motorway"]], 520 | "layout": { 521 | "line-cap": "round", 522 | "line-join": "round", 523 | "visibility": "visible" 524 | }, 525 | "paint": { 526 | "line-color": "hsla(239, 45%, 69%, 0.2)", 527 | "line-width": { 528 | "base": 1.4, 529 | "stops": [ 530 | [4, 2], 531 | [6, 1.3] 532 | ] 533 | } 534 | } 535 | }, 536 | { 537 | "id": "railway_transit", 538 | "type": "line", 539 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 540 | "source": "openmaptiles", 541 | "source-layer": "transportation", 542 | "minzoom": 16, 543 | "filter": [ 544 | "all", 545 | ["==", "$type", "LineString"], 546 | ["all", ["==", "class", "transit"], ["!in", "brunnel", "tunnel"]] 547 | ], 548 | "layout": { "line-join": "round", "visibility": "visible" }, 549 | "paint": { "line-color": "hsl(200, 65%, 11%)", "line-width": 3 } 550 | }, 551 | { 552 | "id": "railway_transit_dashline", 553 | "type": "line", 554 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 555 | "source": "openmaptiles", 556 | "source-layer": "transportation", 557 | "minzoom": 16, 558 | "filter": [ 559 | "all", 560 | ["==", "$type", "LineString"], 561 | ["all", ["==", "class", "transit"], ["!in", "brunnel", "tunnel"]] 562 | ], 563 | "layout": { "line-join": "round", "visibility": "visible" }, 564 | "paint": { 565 | "line-color": "hsl(193, 63%, 26%)", 566 | "line-dasharray": [3, 3], 567 | "line-width": 2 568 | } 569 | }, 570 | { 571 | "id": "railway_service", 572 | "type": "line", 573 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 574 | "source": "openmaptiles", 575 | "source-layer": "transportation", 576 | "minzoom": 16, 577 | "filter": ["all", ["==", "$type", "LineString"], ["all", ["==", "class", "rail"], ["has", "service"]]], 578 | "layout": { "line-join": "round", "visibility": "visible" }, 579 | "paint": { "line-color": "hsl(200, 65%, 11%)", "line-width": 3 } 580 | }, 581 | { 582 | "id": "railway_service_dashline", 583 | "type": "line", 584 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 585 | "source": "openmaptiles", 586 | "source-layer": "transportation", 587 | "minzoom": 16, 588 | "filter": ["all", ["==", "$type", "LineString"], ["all", ["==", "class", "rail"], ["has", "service"]]], 589 | "layout": { "line-join": "round", "visibility": "visible" }, 590 | "paint": { 591 | "line-color": "hsl(193, 63%, 26%)", 592 | "line-dasharray": [3, 3], 593 | "line-width": 2 594 | } 595 | }, 596 | { 597 | "id": "railway", 598 | "type": "line", 599 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 600 | "source": "openmaptiles", 601 | "source-layer": "transportation", 602 | "minzoom": 13, 603 | "filter": ["all", ["==", "$type", "LineString"], ["all", ["!has", "service"], ["==", "class", "rail"]]], 604 | "layout": { "line-join": "round", "visibility": "visible" }, 605 | "paint": { 606 | "line-color": "hsl(200, 10%, 18%)", 607 | "line-width": { 608 | "base": 1.3, 609 | "stops": [ 610 | [16, 3], 611 | [20, 7] 612 | ] 613 | } 614 | } 615 | }, 616 | { 617 | "id": "railway_dashline", 618 | "type": "line", 619 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 620 | "source": "openmaptiles", 621 | "source-layer": "transportation", 622 | "minzoom": 13, 623 | "filter": ["all", ["==", "$type", "LineString"], ["all", ["!has", "service"], ["==", "class", "rail"]]], 624 | "layout": { "line-join": "round", "visibility": "visible" }, 625 | "paint": { 626 | "line-color": "hsl(224, 20%, 41%)", 627 | "line-dasharray": [3, 3], 628 | "line-width": { 629 | "base": 1.3, 630 | "stops": [ 631 | [16, 1.5], 632 | [20, 6] 633 | ] 634 | } 635 | } 636 | }, 637 | { 638 | "id": "highway_name_other", 639 | "type": "symbol", 640 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 641 | "source": "openmaptiles", 642 | "source-layer": "transportation_name", 643 | "filter": ["all", ["!=", "class", "motorway"], ["==", "$type", "LineString"]], 644 | "layout": { 645 | "symbol-placement": "line", 646 | "symbol-spacing": 350, 647 | "text-field": "{name:latin} {name:nonlatin}", 648 | "text-font": ["Metropolis Regular", "Noto Sans Regular"], 649 | "text-max-angle": 30, 650 | "text-pitch-alignment": "viewport", 651 | "text-rotation-alignment": "map", 652 | "text-size": 10, 653 | "text-transform": "uppercase", 654 | "visibility": "visible" 655 | }, 656 | "paint": { 657 | "text-color": "hsl(223, 31%, 61%)", 658 | "text-halo-blur": 0, 659 | "text-halo-color": "hsl(232, 9%, 23%)", 660 | "text-halo-width": 2, 661 | "text-opacity": 1, 662 | "text-translate": [0, 0] 663 | } 664 | }, 665 | { 666 | "id": "highway_ref", 667 | "type": "symbol", 668 | "metadata": { "mapbox:group": "b6371a3f2f5a9932464fa3867530a2e5" }, 669 | "source": "openmaptiles", 670 | "source-layer": "transportation_name", 671 | "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "motorway"]], 672 | "layout": { 673 | "symbol-placement": "line", 674 | "symbol-spacing": 350, 675 | "text-field": "{ref}", 676 | "text-font": ["Metropolis Light", "Noto Sans Regular"], 677 | "text-pitch-alignment": "viewport", 678 | "text-rotation-alignment": "viewport", 679 | "text-size": 10, 680 | "visibility": "none" 681 | }, 682 | "paint": { 683 | "text-color": "hsl(215, 57%, 77%)", 684 | "text-halo-blur": 1, 685 | "text-halo-color": "hsl(209, 64%, 19%)", 686 | "text-halo-width": 1, 687 | "text-opacity": 1, 688 | "text-translate": [0, 2] 689 | } 690 | }, 691 | { 692 | "id": "boundary_state", 693 | "type": "line", 694 | "metadata": { "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" }, 695 | "source": "openmaptiles", 696 | "source-layer": "boundary", 697 | "filter": ["==", "admin_level", 4], 698 | "layout": { 699 | "line-cap": "round", 700 | "line-join": "round", 701 | "visibility": "visible" 702 | }, 703 | "paint": { 704 | "line-blur": 0.4, 705 | "line-color": "hsla(195, 47%, 62%, 0.26)", 706 | "line-dasharray": [2, 2], 707 | "line-opacity": 1, 708 | "line-width": { 709 | "base": 1.3, 710 | "stops": [ 711 | [3, 1], 712 | [22, 15] 713 | ] 714 | } 715 | } 716 | }, 717 | { 718 | "id": "boundary_country_z0-4", 719 | "type": "line", 720 | "metadata": { "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" }, 721 | "source": "openmaptiles", 722 | "source-layer": "boundary", 723 | "maxzoom": 5, 724 | "filter": ["all", ["==", "admin_level", 2], ["!has", "claimed_by"]], 725 | "layout": { "line-cap": "round", "line-join": "round" }, 726 | "paint": { 727 | "line-blur": { 728 | "base": 1, 729 | "stops": [ 730 | [0, 0.4], 731 | [22, 4] 732 | ] 733 | }, 734 | "line-color": "hsl(214, 63%, 76%)", 735 | "line-opacity": 0.56, 736 | "line-width": { 737 | "base": 1.1, 738 | "stops": [ 739 | [3, 1], 740 | [22, 20] 741 | ] 742 | } 743 | } 744 | }, 745 | { 746 | "id": "boundary_country_z5-", 747 | "type": "line", 748 | "metadata": { "mapbox:group": "a14c9607bc7954ba1df7205bf660433f" }, 749 | "source": "openmaptiles", 750 | "source-layer": "boundary", 751 | "minzoom": 5, 752 | "filter": ["==", "admin_level", 2], 753 | "layout": { "line-cap": "round", "line-join": "round" }, 754 | "paint": { 755 | "line-blur": { 756 | "base": 1, 757 | "stops": [ 758 | [0, 0.4], 759 | [22, 4] 760 | ] 761 | }, 762 | "line-color": "hsl(214, 63%, 76%)", 763 | "line-opacity": 0.56, 764 | "line-width": { 765 | "base": 1.1, 766 | "stops": [ 767 | [3, 1], 768 | [22, 20] 769 | ] 770 | } 771 | } 772 | }, 773 | { 774 | "id": "place_other", 775 | "type": "symbol", 776 | "metadata": { "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" }, 777 | "source": "openmaptiles", 778 | "source-layer": "place", 779 | "maxzoom": 14, 780 | "filter": ["all", ["in", "class", "hamlet", "neighbourhood", "isolated_dwelling"], ["==", "$type", "Point"]], 781 | "layout": { 782 | "text-anchor": "center", 783 | "text-field": "{name:latin}\n{name:nonlatin}", 784 | "text-font": ["Metropolis Regular", "Noto Sans Regular"], 785 | "text-justify": "center", 786 | "text-offset": [0.5, 0], 787 | "text-size": 10, 788 | "text-transform": "uppercase", 789 | "visibility": "visible" 790 | }, 791 | "paint": { 792 | "text-color": "hsl(195, 37%, 73%)", 793 | "text-halo-blur": 1, 794 | "text-halo-color": "hsla(228, 60%, 21%, 0.7)", 795 | "text-halo-width": 1, 796 | "text-opacity": 0.6 797 | } 798 | }, 799 | { 800 | "id": "place_suburb", 801 | "type": "symbol", 802 | "metadata": { "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" }, 803 | "source": "openmaptiles", 804 | "source-layer": "place", 805 | "maxzoom": 15, 806 | "filter": ["all", ["==", "$type", "Point"], ["==", "class", "suburb"]], 807 | "layout": { 808 | "text-anchor": "center", 809 | "text-field": "{name:latin}\n{name:nonlatin}", 810 | "text-font": ["Metropolis Regular", "Noto Sans Regular"], 811 | "text-justify": "center", 812 | "text-offset": [0.5, 0], 813 | "text-size": 10, 814 | "text-transform": "uppercase", 815 | "visibility": "visible" 816 | }, 817 | "paint": { 818 | "text-color": "hsl(195, 41%, 49%)", 819 | "text-halo-blur": 1, 820 | "text-halo-color": "hsla(228, 60%, 21%, 0.7)", 821 | "text-halo-width": 1 822 | } 823 | }, 824 | { 825 | "id": "place_village", 826 | "type": "symbol", 827 | "metadata": { "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" }, 828 | "source": "openmaptiles", 829 | "source-layer": "place", 830 | "maxzoom": 14, 831 | "filter": ["all", ["==", "$type", "Point"], ["==", "class", "village"]], 832 | "layout": { 833 | "icon-size": 0.4, 834 | "text-anchor": "left", 835 | "text-field": "{name:latin}\n{name:nonlatin}", 836 | "text-font": ["Metropolis Regular", "Noto Sans Regular"], 837 | "text-justify": "left", 838 | "text-offset": [0.5, 0.2], 839 | "text-size": 10, 840 | "text-transform": "uppercase", 841 | "visibility": "visible" 842 | }, 843 | "paint": { 844 | "icon-opacity": 0.7, 845 | "text-color": "hsl(195, 41%, 49%)", 846 | "text-halo-blur": 1, 847 | "text-halo-color": "hsla(228, 60%, 21%, 0.7)", 848 | "text-halo-width": 1 849 | } 850 | }, 851 | { 852 | "id": "place_town", 853 | "type": "symbol", 854 | "metadata": { "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" }, 855 | "source": "openmaptiles", 856 | "source-layer": "place", 857 | "maxzoom": 15, 858 | "filter": ["all", ["==", "$type", "Point"], ["==", "class", "town"]], 859 | "layout": { 860 | "icon-image": { 861 | "base": 1, 862 | "stops": [ 863 | [0, "circle-11"], 864 | [9, ""] 865 | ] 866 | }, 867 | "icon-size": 0.4, 868 | "text-anchor": { 869 | "base": 1, 870 | "stops": [ 871 | [0, "left"], 872 | [8, "center"] 873 | ] 874 | }, 875 | "text-field": "{name:latin}\n{name:nonlatin}", 876 | "text-font": ["Metropolis Regular", "Noto Sans Regular"], 877 | "text-justify": "left", 878 | "text-offset": [0.5, 0.2], 879 | "text-size": 10, 880 | "text-transform": "uppercase", 881 | "visibility": "visible" 882 | }, 883 | "paint": { 884 | "icon-opacity": 0.7, 885 | "text-color": "hsl(195, 25%, 76%)", 886 | "text-halo-blur": 1, 887 | "text-halo-color": "hsla(228, 60%, 21%, 0.7)", 888 | "text-halo-width": 1 889 | } 890 | }, 891 | { 892 | "id": "place_city", 893 | "type": "symbol", 894 | "metadata": { "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" }, 895 | "source": "openmaptiles", 896 | "source-layer": "place", 897 | "maxzoom": 14, 898 | "filter": ["all", ["==", "$type", "Point"], ["all", ["==", "class", "city"], [">", "rank", 3]]], 899 | "layout": { 900 | "icon-size": 0.4, 901 | "text-anchor": { 902 | "base": 1, 903 | "stops": [ 904 | [0, "left"], 905 | [8, "center"] 906 | ] 907 | }, 908 | "text-field": "{name:latin}\n{name:nonlatin}", 909 | "text-font": ["Metropolis Regular", "Noto Sans Regular"], 910 | "text-justify": "left", 911 | "text-offset": [0.5, 0.2], 912 | "text-size": 10, 913 | "text-transform": "uppercase", 914 | "visibility": "visible" 915 | }, 916 | "paint": { 917 | "icon-opacity": 0.7, 918 | "text-color": "hsl(195, 25%, 76%)", 919 | "text-halo-blur": 1, 920 | "text-halo-color": "hsla(228, 60%, 21%, 0.7)", 921 | "text-halo-width": 1 922 | } 923 | }, 924 | { 925 | "id": "place_city_large", 926 | "type": "symbol", 927 | "metadata": { "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" }, 928 | "source": "openmaptiles", 929 | "source-layer": "place", 930 | "maxzoom": 12, 931 | "filter": ["all", ["==", "$type", "Point"], ["all", ["<=", "rank", 3], ["==", "class", "city"]]], 932 | "layout": { 933 | "icon-size": 0.4, 934 | "text-anchor": { 935 | "base": 1, 936 | "stops": [ 937 | [0, "left"], 938 | [8, "center"] 939 | ] 940 | }, 941 | "text-field": "{name:latin}\n{name:nonlatin}", 942 | "text-font": ["Metropolis Regular", "Noto Sans Regular"], 943 | "text-justify": "left", 944 | "text-offset": [0.5, 0.2], 945 | "text-size": 14, 946 | "text-transform": "uppercase", 947 | "visibility": "visible" 948 | }, 949 | "paint": { 950 | "icon-opacity": 0.7, 951 | "text-color": "hsl(195, 25%, 76%)", 952 | "text-halo-blur": 1, 953 | "text-halo-color": "hsla(228, 60%, 21%, 0.7)", 954 | "text-halo-width": 1 955 | } 956 | }, 957 | { 958 | "id": "place_state", 959 | "type": "symbol", 960 | "metadata": { "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" }, 961 | "source": "openmaptiles", 962 | "source-layer": "place", 963 | "maxzoom": 12, 964 | "filter": ["all", ["==", "$type", "Point"], ["==", "class", "state"]], 965 | "layout": { 966 | "text-field": "{name:latin}\n{name:nonlatin}", 967 | "text-font": ["Metropolis Regular", "Noto Sans Regular"], 968 | "text-size": 10, 969 | "text-transform": "uppercase", 970 | "visibility": "visible" 971 | }, 972 | "paint": { 973 | "text-color": "rgb(113, 129, 144)", 974 | "text-halo-blur": 1, 975 | "text-halo-color": "hsla(228, 60%, 21%, 0.7)", 976 | "text-halo-width": 1 977 | } 978 | }, 979 | { 980 | "id": "place_country_other", 981 | "type": "symbol", 982 | "metadata": { "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" }, 983 | "source": "openmaptiles", 984 | "source-layer": "place", 985 | "maxzoom": 8, 986 | "filter": ["all", ["==", "$type", "Point"], ["==", "class", "country"], ["!has", "iso_a2"]], 987 | "layout": { 988 | "text-field": "{name:latin}", 989 | "text-font": ["Metropolis Light Italic", "Noto Sans Italic"], 990 | "text-size": { 991 | "base": 1, 992 | "stops": [ 993 | [0, 9], 994 | [6, 11] 995 | ] 996 | }, 997 | "text-transform": "uppercase", 998 | "visibility": "visible" 999 | }, 1000 | "paint": { 1001 | "text-color": { 1002 | "base": 1, 1003 | "stops": [ 1004 | [3, "rgb(157,169,177)"], 1005 | [4, "rgb(153, 153, 153)"] 1006 | ] 1007 | }, 1008 | "text-halo-color": "hsla(228, 60%, 21%, 0.7)", 1009 | "text-halo-width": 1.4, 1010 | "text-opacity": 1 1011 | } 1012 | }, 1013 | { 1014 | "id": "place_country_minor", 1015 | "type": "symbol", 1016 | "metadata": { "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" }, 1017 | "source": "openmaptiles", 1018 | "source-layer": "place", 1019 | "maxzoom": 8, 1020 | "filter": ["all", ["==", "$type", "Point"], ["==", "class", "country"], [">=", "rank", 2], ["has", "iso_a2"]], 1021 | "layout": { 1022 | "text-field": "{name:latin}", 1023 | "text-font": ["Metropolis Regular", "Noto Sans Regular"], 1024 | "text-size": { 1025 | "base": 1, 1026 | "stops": [ 1027 | [0, 10], 1028 | [6, 12] 1029 | ] 1030 | }, 1031 | "text-transform": "uppercase", 1032 | "visibility": "visible" 1033 | }, 1034 | "paint": { 1035 | "text-color": { 1036 | "base": 1, 1037 | "stops": [ 1038 | [3, "rgb(157,169,177)"], 1039 | [4, "rgb(153, 153, 153)"] 1040 | ] 1041 | }, 1042 | "text-halo-color": "hsla(228, 60%, 21%, 0.7)", 1043 | "text-halo-width": 1.4, 1044 | "text-opacity": 1 1045 | } 1046 | }, 1047 | { 1048 | "id": "place_country_major", 1049 | "type": "symbol", 1050 | "metadata": { "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" }, 1051 | "source": "openmaptiles", 1052 | "source-layer": "place", 1053 | "maxzoom": 6, 1054 | "filter": ["all", ["==", "$type", "Point"], ["<=", "rank", 1], ["==", "class", "country"], ["has", "iso_a2"]], 1055 | "layout": { 1056 | "text-anchor": "center", 1057 | "text-field": "{name:latin}", 1058 | "text-font": ["Metropolis Regular", "Noto Sans Regular"], 1059 | "text-size": { 1060 | "base": 1.4, 1061 | "stops": [ 1062 | [0, 10], 1063 | [3, 12], 1064 | [4, 14] 1065 | ] 1066 | }, 1067 | "text-transform": "uppercase", 1068 | "visibility": "visible" 1069 | }, 1070 | "paint": { 1071 | "text-color": { 1072 | "base": 1, 1073 | "stops": [ 1074 | [3, "rgb(157,169,177)"], 1075 | [4, "rgb(153, 153, 153)"] 1076 | ] 1077 | }, 1078 | "text-halo-color": "hsla(228, 60%, 21%, 0.7)", 1079 | "text-halo-width": 1.4, 1080 | "text-opacity": 1 1081 | } 1082 | }, 1083 | { 1084 | "id": "place_continent", 1085 | "type": "symbol", 1086 | "metadata": { "mapbox:group": "101da9f13b64a08fa4b6ac1168e89e5f" }, 1087 | "source": "openmaptiles", 1088 | "source-layer": "place", 1089 | "maxzoom": 6, 1090 | "filter": ["all", ["==", "$type", "Point"], ["==", "class", "continent"]], 1091 | "layout": { 1092 | "text-anchor": "center", 1093 | "text-field": "{name:latin}", 1094 | "text-font": ["Metropolis Regular", "Noto Sans Regular"], 1095 | "text-size": { 1096 | "base": 1.4, 1097 | "stops": [ 1098 | [0, 10], 1099 | [3, 12], 1100 | [4, 14] 1101 | ] 1102 | }, 1103 | "text-transform": "uppercase", 1104 | "visibility": "visible" 1105 | }, 1106 | "paint": { 1107 | "text-color": "hsl(0, 0%, 100%)", 1108 | "text-halo-color": "hsla(228, 60%, 21%, 0.7)", 1109 | "text-halo-width": 1.4, 1110 | "text-opacity": { 1111 | "base": 1, 1112 | "stops": [ 1113 | [0, 0.6], 1114 | [3, 0] 1115 | ] 1116 | } 1117 | } 1118 | } 1119 | ], 1120 | "id": "ciwlw4z7800092qmvzlut41tx" 1121 | } 1122 | -------------------------------------------------------------------------------- /demo/src/assets/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | * { 7 | box-sizing: border-box; 8 | } 9 | 10 | html, 11 | body, 12 | #app, 13 | main { 14 | @apply h-full bg-slate-800 text-slate-200 font-sans; 15 | } 16 | 17 | main > * { 18 | @apply w-full bg-slate-700 rounded-3xl; 19 | } 20 | 21 | a { 22 | @apply underline underline-offset-4 hover:text-slate-400; 23 | } 24 | 25 | a[target="_blank"]::after { 26 | content: " 🔗"; 27 | } 28 | 29 | h1 { 30 | @apply text-5xl; 31 | } 32 | 33 | ul { 34 | @apply list-disc list-inside; 35 | } 36 | 37 | ol { 38 | @apply list-decimal list-inside; 39 | } 40 | 41 | ul li { 42 | @apply my-2 ml-6 -indent-6; 43 | } 44 | 45 | ol li { 46 | @apply my-2 ml-4 -indent-4; 47 | } 48 | 49 | small { 50 | @apply text-sm text-slate-400; 51 | } 52 | 53 | input[type="checkbox"] { 54 | @apply transition-all w-6 h-6 bg-slate-500 hover:bg-slate-400 active:bg-slate-600 border-none rounded-md shadow-lg hover:shadow-xl active:shadow-md focus:ring-0 focus:ring-offset-0 disabled:opacity-50 disabled:shadow-none disabled:pointer-events-none; 55 | } 56 | 57 | input[type="checkbox"]:checked { 58 | @apply transition-all bg-accent-500 hover:bg-accent-400 active:bg-accent-600 focus:bg-accent-500 disabled:opacity-50 disabled:shadow-none disabled:pointer-events-none; 59 | } 60 | 61 | button { 62 | @apply transition-all p-5 bg-accent-500 hover:bg-accent-400 active:bg-accent-600 rounded-full shadow-lg hover:shadow-xl active:shadow-md active:bg-blend-darken disabled:opacity-50 disabled:shadow-none disabled:pointer-events-none; 63 | } 64 | 65 | select { 66 | @apply transition-all px-3 py-2 bg-accent-500 hover:bg-accent-400 active:bg-accent-600 border-none rounded-md shadow-lg hover:shadow-xl active:shadow-md focus:ring-0 focus:ring-offset-0 disabled:opacity-50 disabled:shadow-none disabled:pointer-events-none; 67 | } 68 | 69 | input[type="number"] { 70 | @apply transition-all bg-slate-100 text-slate-900 border-none rounded-md shadow-lg hover:shadow-xl active:shadow-md focus:ring-0 focus:ring-offset-0 focus:outline-accent-500 focus:outline-offset-0 disabled:opacity-50 disabled:shadow-none disabled:pointer-events-none; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /demo/src/components/AppSidebar.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 28 | 29 | 39 | -------------------------------------------------------------------------------- /demo/src/examples/1 User Interaction.svelte: -------------------------------------------------------------------------------- 1 | 78 | 79 | 80 | {meta.name} 81 | 82 | {#if message} 83 |

{message}

84 | {/if} 85 | 86 | 90 | 91 | 95 | 96 | 100 | 101 | 105 | 106 |
    107 |
  • Click somewhere on the map to add a waypoint
  • 108 |
  • Click a waypoint to remove it and its related snappoint
  • 109 |
  • Click a snappoint to remove it and its related waypoint
  • 110 |
  • Drag a waypoint somewhere to move it
  • 111 |
  • Drag a routeline somewhere to add a waypoint in-between the 2 nearest ones
  • 112 |
  • 113 | Click an alternative routeline to select it
    114 | Note, there's usually no alternative routelines in the server response if there are more than 116 | 2 waypoints 118 |
  • 119 |
120 |
121 | 122 |
123 | -------------------------------------------------------------------------------- /demo/src/examples/10 Bearings Support and Control.svelte: -------------------------------------------------------------------------------- 1 | 68 | 69 | 70 | {meta.name} 71 | 72 |

73 | The bearings support 74 | allows to control in which direction the route would be continued from a given waypoint. 75 |

76 | 77 |

78 | In order to enable support for this API option on the plugin level, pass the bearings: true option to 79 | the plugin's configuration object. When this is done, each request would contain the bearings field. The 80 | problem with that is that the values for the waypoints' bearings are not populated correctly since we need some way to 81 | assign these bearings values to our waypoints. 82 |

83 | 84 |

Luckily, that's possible to achieve using the built-in Bearings Control.

85 | 86 | 90 | 91 | 95 | 96 | 100 | 101 | 105 | 106 | 110 | 111 | 115 | 116 | 120 | 121 | 129 | 130 | 138 | 139 | 147 | 148 | 156 | 157 | 161 | 162 | 166 |
167 | 168 |
169 | -------------------------------------------------------------------------------- /demo/src/examples/11 Restyling.svelte: -------------------------------------------------------------------------------- 1 | 85 | 86 | 87 | {meta.name} 88 | 89 |

90 | It's completely up to you how to style the Directions' features shown on the map. You can either use the default 91 | styles provided by the plugin (see other examples), easily modify the default features' dimensions (see the 92 | Touch-Friendly Features example) or 93 | define your custom features' styles from scratch. 94 |

95 | 96 |

This example demonstrates the last option.

97 |
98 | 99 |
100 | -------------------------------------------------------------------------------- /demo/src/examples/12 Distance Measurement.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 | 51 | {meta.name} 52 | 53 |

54 | Total Route Distance: 55 | {#if totalDistance} 56 | {totalDistance}m 57 | {:else} 58 | unknown 59 | {/if} 60 |

61 | 62 | Note that you might want to zoom in and out the map to toggle the distance-annotations visibility 65 | 66 |

67 | This is an example of how one could use the plugin's extensibility interfaces to create a distance measurement tool 68 | out of it. 69 |

70 | 71 |

72 | Here we create a subclass of the MapLibreGlDirections main super class and augment the original 73 | buildRoutelines 74 | method to write each route leg's distance into a respective feature's properties object. These saved distances 75 | are then used by an additional "symbol" layer that displays them along the respective route's lines on the map. 76 |

77 | 78 |

79 | The total distance comes from the response's specific field and is updated each time there's the "fetchroutesend" or 80 | the "removewaypoint" event fired. 81 |

82 |
83 | 84 |
85 | -------------------------------------------------------------------------------- /demo/src/examples/13 Load and Save.svelte: -------------------------------------------------------------------------------- 1 | 61 | 62 | 63 | {meta.name} 64 | 65 | 66 | 67 | 68 | 69 | This example uses the localStorage to save routes, but you are obviously not restricted to it. There might 71 | instead be a file or a serverside-database or whatever else 73 | 74 |

75 | If you want to save/load the route, the obvious way to do so would be to save the list of waypoints whenever the 76 | route is updated and to make a new routing-request with the saved waypoints' coordinates whenever you need to load 77 | it. 78 |

79 | 80 |

81 | But what if the underlying roads networks changes for some reason? You'll get a different route for the same set of 82 | waypoints. Moreover, if there are some severe construction works going on, you run into a risk of not getting any 83 | routes whatsoever. 84 |

85 | 86 |

87 | Sometimes it's actually a good idea to save the route as a list of GeoJSON Features and be able to load these saved 88 | features whenever there's a need. This example shows how that could be done. 89 |

90 |
91 | 92 |
93 | -------------------------------------------------------------------------------- /demo/src/examples/14 Multiple profiles.svelte: -------------------------------------------------------------------------------- 1 | 84 | 85 | 86 | {meta.name} 87 |

88 | This example showcases routing with multiple profiles. Segments corresponding to different profiles are displayed in 89 | different colors. Plugin provides default styles for typical OSRM profiles: car, bike, foot. Styles can be changed per profile via general style customization approach (consult the 93 | Restyling example). In case different profiles are used you can similarly 94 | style map features corresponding to each profile by targeting profile property of a feature (see 95 | default styles). 98 |

99 | 100 | Note that interactivity is not supported for multiple profiles 101 | 102 |

Used profiles:

103 |
    104 | {#each displayedProfiles as profile} 105 |
  • {@html profile}
  • 106 | {/each} 107 |
108 | 109 |
110 | 111 | 112 |
113 |
114 | 115 |
116 | 117 | 122 | -------------------------------------------------------------------------------- /demo/src/examples/2 Programmatical Control.svelte: -------------------------------------------------------------------------------- 1 | 77 | 78 | 79 | {meta.name} 80 | 81 | Note that interactivity is disabled for this example 82 | 83 |
84 |

Set waypoints to a predefined set

85 | 86 |
87 | 88 |
89 |

Add a random waypoint at some random index

90 | 91 |
92 | 93 |
94 |

Delete a random waypoint

95 | 96 |
97 | 98 |
99 |

Clear the map from all the stuff added by the plugin

100 | 101 |
102 |
103 | 104 |
105 | -------------------------------------------------------------------------------- /demo/src/examples/3 Mapbox Directions API and Congestions.svelte: -------------------------------------------------------------------------------- 1 | 58 | 59 | 60 | {meta.name} 61 | 62 | 69 | 70 |

This example makes POST requests to the official Mapbox Directions API with the following options:

71 | 72 |
    73 |
  • 74 | annotations={annotations} 75 |
  • 76 |
  • overview=full
  • 77 |
  • alternatives=true
  • 78 |
79 |
80 | 81 |
82 | -------------------------------------------------------------------------------- /demo/src/examples/4 Origin and Destination.svelte: -------------------------------------------------------------------------------- 1 | 71 | 72 | 73 | {meta.name} 74 | 75 |

76 | In this example the default layers used by the plugin are augmented with an additional "symbol" layer which is only 77 | rendered for the ORIGIN and DESTINATION waypoints. 78 |

79 | 80 |

81 | Note how you don't need to re-define all the layers from scratch thanks to the exported 82 | layersFactory function that returns all the default layers allowing for their augmentation and modification 83 |

84 |
85 | 86 |
87 | -------------------------------------------------------------------------------- /demo/src/examples/5 Show Routes' Directions.svelte: -------------------------------------------------------------------------------- 1 | 68 | 69 | 70 | {meta.name} 71 | 72 |

Another example that demonstrates the ease of extending the original styles provided by the plugin.

73 | 74 |

This time a "symbol" layer is added that shows the direction the selected route goes in.

75 | 76 | Note that you have to manually load and add the images you intend to use for the custom layers you 78 | add 80 |
81 | 82 |
83 | -------------------------------------------------------------------------------- /demo/src/examples/6 Touch-Friendly Features.svelte: -------------------------------------------------------------------------------- 1 | 56 | 57 | 58 | {meta.name} 59 | 60 | 64 | 65 |

66 | Sometimes it's pretty hard to aim exactly at the selected route line to add a waypoint by dragging it when using the 67 | plugin on a touch device. 68 |

69 | 70 |

71 | The example shows how one could use the layersFactory's input parameters to handle that case by 72 | increasing the points by 1.5 and the lines by 2 times when the map is used on a touch-enabled device. 73 |

74 | 75 | Note that you can either load the page on a touch-enabled device or toggle the checkbox above: both 77 | options apply the same effect 79 |
80 | 81 |
82 | -------------------------------------------------------------------------------- /demo/src/examples/7 Events.svelte: -------------------------------------------------------------------------------- 1 | 82 | 83 | 84 | {meta.name} 85 | 86 |

87 | This example listens for all the available events and logs them below. Interact with the map to see the emitted 88 | events. 89 |

90 | 91 |
    92 | {#each messages as message} 93 |
  1. {@html message}
  2. 94 | {/each} 95 |
96 | 97 | {#if messages.length} 98 | 99 | {/if} 100 |
101 | 102 |
103 | -------------------------------------------------------------------------------- /demo/src/examples/8 Aborting Requests.svelte: -------------------------------------------------------------------------------- 1 | 55 | 56 | 57 | {meta.name} 58 | 59 | 60 | 61 |

62 | Instead of aborting routing-requests manually, you can set the requestTimeout configuration option to a 63 | number of ms that a routing-request is allowed to take before getting automatically aborted. 64 |

65 | 66 | 70 | 71 | Note that you may need to manually 73 | enable network throttling for the setting above to take effect 79 |
80 | 81 |
82 | -------------------------------------------------------------------------------- /demo/src/examples/9 Loading Indicator Control.svelte: -------------------------------------------------------------------------------- 1 | 51 | 52 | 53 | {meta.name} 54 | 55 | 64 | 65 |

66 | The LoadingIndicatorControl adds a simple spinning loader-icon which automatically appears whenever there's 67 | an ongoing routing-request. 68 |

69 |
70 | 71 |
72 | -------------------------------------------------------------------------------- /demo/src/examples/README.md: -------------------------------------------------------------------------------- 1 | The prefix number for the files in this folder specifies the order the examples appear in the Demo project. It's displayed neither in the paths nor in the example names. 2 | -------------------------------------------------------------------------------- /demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from "./App.svelte"; 2 | import "./assets/styles/index.css"; 3 | 4 | export default new App({ 5 | target: document.getElementById("app")!, 6 | }); 7 | -------------------------------------------------------------------------------- /demo/src/router.ts: -------------------------------------------------------------------------------- 1 | import Menu from "./Menu.svelte"; 2 | import type { ComponentType } from "svelte"; 3 | 4 | export const examples = Object.entries(import.meta.glob("./examples/**.svelte", { eager: true })).map( 5 | ([path, component]) => { 6 | const parsedFileName = path.match(/\/(\d+)\s([^/]+)\./)!; 7 | const index = parseInt(parsedFileName[1]); 8 | const name = parsedFileName[2]; 9 | 10 | return { 11 | path: "/examples/" + name.toLowerCase().replaceAll(/\s/g, "-"), 12 | index, 13 | name: name, 14 | component: component, 15 | sourceUrl: `https://github.com/maplibre/maplibre-gl-directions/tree/main/demo/src/${path}`, 16 | }; 17 | }, 18 | ); 19 | 20 | const routes: Record = {}; 21 | 22 | examples.forEach((example) => { 23 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 24 | // @ts-ignore 25 | routes[example.path] = example.component.default; 26 | }); 27 | 28 | routes["*"] = Menu; 29 | 30 | export { routes }; 31 | -------------------------------------------------------------------------------- /doc/BASIC_USAGE.md: -------------------------------------------------------------------------------- 1 | Start by importing the plugin. Then, when the map is loaded, create an instance of the imported {@link default|`MapLibreGlDirections`} class passing to the constructor a map instance and optionally a {@link MapLibreGlDirectionsConfiguration|configuration object}. 2 | 3 | ```typescript 4 | import MapLibreGlDirections from "@maplibre/maplibre-gl-directions"; 5 | 6 | map.on("load", () => { 7 | const directions = new MapLibreGlDirections(map, { 8 | // optional configuration 9 | }); 10 | }); 11 | ``` 12 | 13 | If needed, enable the interactivity. 14 | 15 | ```typescript 16 | directions.interactive = true; 17 | ``` 18 | 19 | Use the plugin's public interface to set, add and remove waypoints. 20 | 21 | ```typescript 22 | // Set the waypoints programmatically 23 | directions.setWaypoints([ 24 | [-73.8271025, 40.8032906], 25 | [-73.8671258, 40.82234996], 26 | ]); 27 | 28 | // Remove the first waypoint 29 | directions.removeWaypoint(0); 30 | 31 | // Add a waypoint at index 0 32 | directions.addWaypoint([-73.8671258, 40.82234996], 0); 33 | ``` 34 | 35 | Listen to the plugin's events. 36 | 37 | ```typescript 38 | directions.on("movewaypoint", () => { 39 | console.log("A waypoint has been moved!"); 40 | }); 41 | ``` 42 | 43 | Call the {@link clear|`clear`} method to remove all the plugin's traces from the map. 44 | 45 | ```typescript 46 | directions.clear(); 47 | ``` 48 | 49 | If you need to completely disable the plugin, make sure to call the {@link destroy|`destroy`} method first. 50 | 51 | ```typescript 52 | directions.destroy(); 53 | directions = undefined; 54 | ``` 55 | -------------------------------------------------------------------------------- /doc/CONTROLS.md: -------------------------------------------------------------------------------- 1 | ### `LoadingIndicatorControl` 2 | 3 | The {@link LoadingIndicatorControl} adds a spinning wheel that appears whenever there's an ongoing routing requests and automatically disappears as soon as the request is finished. 4 | 5 | The loading indicator's appearance is configurable via the {@link LoadingIndicatorControlConfiguration} object that is (optionally) passed as the second argument to the constructor. 6 | 7 | See the respective {@link https://maplibre.org/maplibre-gl-directions/#/examples/loading-indicator-control|Demo}. 8 | 9 | ### `BearingsControl` 10 | 11 | The {@link BearingsControl} is a built-in control for manipulating waypoints' bearings values when the respective `bearings` option is set to `true` for a given Directions instance. 12 | 13 | The loading indicator's appearance and behavior are configurable via the {@link BearingsControlConfiguration} object that is (optionally) passed as the second argument to the constructor. 14 | 15 | See the respective {@link https://maplibre.org/maplibre-gl-directions/#/examples/bearings-support-and-control|Demo}. 16 | 17 | Here's the list of CSS classes available for the end user to style the component according to one's needs: 18 | 19 | - `maplibre-gl-directions-bearings-control` 20 | - `maplibre-gl-directions-bearings-control__list` 21 | - `maplibre-gl-directions-bearings-control__list-item` 22 | - `maplibre-gl-directions-bearings-control__list-item--enabled` 23 | - `maplibre-gl-directions-bearings-control__list-item--disabled` 24 | - `maplibre-gl-directions-bearings-control__number` 25 | - `maplibre-gl-directions-bearings-control__checkbox` 26 | - `maplibre-gl-directions-bearings-control__waypoint-image` 27 | - `maplibre-gl-directions-bearings-control__input` 28 | - `maplibre-gl-directions-bearings-control__text` 29 | 30 | ### `DirectionsControl` 31 | 32 | WIP (1.x milestone). 33 | -------------------------------------------------------------------------------- /doc/CUSTOMIZATION.md: -------------------------------------------------------------------------------- 1 | For the sakes of your convenience, make sure you've enabled the "Inherited/Protected/External" filter: 2 | 3 | ![Enabling the "Inherited/Protected/External" filter](https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/main/doc/images/public-protected-filter.png) 4 | 5 | Here's an example of what can potentially be done after investing some time into customization: straight-lined routing, distance measurement, multiple Directions' instances running in parallel on the same map with a possibility to toggle between them, different types of Waypoints and Snappoints and so on: 6 | 7 | ![A Complex Customization Example](https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/main/doc/images/complex-customization.png) 8 | 9 | In short, all the plugin's customization possibilities fall down into 2 categories: visual-only customization and behavioral customization. 10 | 11 | ## Visual-only customization 12 | 13 | Visual-only customization is done by modifying the style layers used by the plugin. You can either remove certain layers altogether, or instead add additional custom ones, or modify existing layers, or refuse from using or modifying the existing layers and instead define new layers from scratch. Or you can combine these different approaches to achieve the look-and-feel you aim towards. 14 | 15 | When you create an instance of Directions, you're allowed to provide the constructor with the {@link MapLibreGlDirectionsConfiguration|configuration object}. One of the configuration options is the {@link layers|`layers`} array. 16 | 17 | By default (if you don't provide this configuration option), the plugin uses the default layers that are generated by the {@link layersFactory|`layersFactory`} function. But you can instead provide a plain array of {@link LayerSpecification} objects. See the {@link https://maplibre.org/maplibre-gl-directions/#/examples/restyling|Restyling} example for a demo. 18 | 19 | When re-defining the layers, you must respect the following rules: 20 | 21 | 1. There must be at least one layer for Waypoints 22 | 2. There must be at least one layer for Snappoints 23 | 3. There must be at least one layer for the Hoverpoint 24 | 4. There must be at least one layer for Routelines 25 | 5. There must be at least one layer for Alternative Routelines (if you plan to enable the respective request option) 26 | 27 | If you think you don't need some of these layers, you must still provide them, but use some styling that would allow to actually hide the features represented by the layer. For instance, using the {@link https://maplibre.org/maplibre-gl-js-docs/style-spec/layers/#layout-background-visibility|`visibility`} property. 28 | 29 | Waypoints, Snappoints and the Hoverpoint represent (obviously enough) Point GeoJSON Features. So you would most probably like to use either "symbol" or "circle" layer types for those. 30 | 31 | Routelines and Alternative Routelines represent GeoJSON LineString Features and therefore must be represented with layers of type "line". 32 | 33 | You can also optionally provide one or more layers for the snaplines (the lines that connect Waypoints to their related snappoints and the Hoverpoint to its related Waypoints). 34 | 35 | To filter out the features that are only applicable for the given layers, you can use the following filters: 36 | 37 | 1. For Snaplines: `["==", ["get", "type"], "SNAPLINE"]` (meaning: all the features where `feature.properties.type === "SNAPLINE"`) 38 | 2. For Alternative Routelines: `["==", ["get", "route"], "ALT"]` (meaning: all the features where `feature.properties.route === "ALT"`) 39 | 3. For Routelines (i.e. the selected Routeline): `["==", ["get", "route"], "SELECTED"]` (meaning: all the features where `feature.properties.route === "SELECTED"`) 40 | 4. For the Hoverpoint: `["==", ["get", "type"], "HOVERPOINT"]` (meaning: all the features where `feature.properties.type === "HOVERPOINT"`) 41 | 5. For Snappoints: `["==", ["get", "type"], "SNAPPOINT"]` (meaning: all the features where `feature.properties.type === "SNAPPOINT"`) 42 | 6. For Waypoints: `["==", ["get", "type"], "WAYPOINT"]` (meaning: all the features where `feature.properties.type === "WAYPOINT"`) 43 | 44 | Note that the order the layers appear in the array determines the order the features will appear on the map. You are free to use any order that applies better for your exact case, but by default the layers come in the order they're listed above: the Waypoints' layers are the foremost ones. 45 | 46 | Here's the example of the layers re-definition for the {@link https://maplibre.org/maplibre-gl-directions/#/examples/restyling|Restyling} example: 47 | 48 | ```typescript 49 | layers: [ 50 | { 51 | id: "maplibre-gl-directions-snapline", 52 | type: "line", 53 | source: "maplibre-gl-directions", 54 | layout: { 55 | "line-cap": "round", 56 | "line-join": "round", 57 | }, 58 | paint: { 59 | "line-dasharray": [2, 2], 60 | "line-color": "#ffffff", 61 | "line-opacity": 0.65, 62 | "line-width": 2, 63 | }, 64 | filter: ["==", ["get", "type"], "SNAPLINE"], 65 | }, 66 | 67 | { 68 | id: "maplibre-gl-directions-alt-routeline", 69 | type: "line", 70 | source: "maplibre-gl-directions", 71 | layout: { 72 | "line-cap": "butt", 73 | "line-join": "round", 74 | }, 75 | paint: { 76 | "line-pattern": "routeline", 77 | "line-width": 8, 78 | "line-opacity": 0.5, 79 | }, 80 | filter: ["==", ["get", "route"], "ALT"], 81 | }, 82 | 83 | { 84 | id: "maplibre-gl-directions-routeline", 85 | type: "line", 86 | source: "maplibre-gl-directions", 87 | layout: { 88 | "line-cap": "butt", 89 | "line-join": "round", 90 | }, 91 | paint: { 92 | "line-pattern": "routeline", 93 | "line-width": 8, 94 | }, 95 | filter: ["==", ["get", "route"], "SELECTED"], 96 | }, 97 | 98 | { 99 | id: "maplibre-gl-directions-hoverpoint", 100 | type: "symbol", 101 | source: "maplibre-gl-directions", 102 | layout: { 103 | "icon-image": "balloon-hoverpoint", 104 | "icon-anchor": "bottom", 105 | "icon-ignore-placement": true, 106 | "icon-overlap": "always", 107 | }, 108 | filter: ["==", ["get", "type"], "HOVERPOINT"], 109 | }, 110 | 111 | { 112 | id: "maplibre-gl-directions-snappoint", 113 | type: "symbol", 114 | source: "maplibre-gl-directions", 115 | layout: { 116 | "icon-image": "balloon-snappoint", 117 | "icon-anchor": "bottom", 118 | "icon-ignore-placement": true, 119 | "icon-overlap": "always", 120 | }, 121 | filter: ["==", ["get", "type"], "SNAPPOINT"], 122 | }, 123 | 124 | { 125 | id: "maplibre-gl-directions-waypoint", 126 | type: "symbol", 127 | source: "maplibre-gl-directions", 128 | layout: { 129 | "icon-image": "balloon-waypoint", 130 | "icon-anchor": "bottom", 131 | "icon-ignore-placement": true, 132 | "icon-overlap": "always", 133 | }, 134 | filter: ["==", ["get", "type"], "WAYPOINT"], 135 | }, 136 | ]; 137 | ``` 138 | 139 | As you can see, each layer type is represented by one layer: one for Snaplines, one for the Hoverpoint and so on. But you're not restricted to one layer-per-feature. Each feature could easily be represented by multiple layers. By the way, that's exactly the way the things are implemented by default. E.g. each Waypoint is by default represented by 2 layers: one for the casing ("halo", as the MapLibre spec calls it) and one for the main, central circle. 140 | 141 | By default, the plugin expects you to provide casings for Waypoints, Snappoints, Hoverpoint, and all the Routelines. The thing here is that all these features are made interactive (except for the Hoverpoint's casing) because the user would probably like to be able not to aim exactly at the very center of a Waypoint to be able to move it, but also to be able to drag the Waypoint by it casing. Here comes the concept of sensitive layers. 142 | 143 | If you decide to deviate from the default model where there are 2 layers for Waypoints, 2 layers for Snappoints, 2 layers for Routelines and 2 layers for Alternative Routelines, you must manually specify which layers should be considered sensitive for each group of these features. Please, see the {@link https://maplibre.org/maplibre-gl-directions/#/examples/restyling|Restyling} example for details. Namely, take a look at the source code for the example. 144 | 145 | Originally, the definitions of the sensitive layers look like these: 146 | 147 | 1. `sensitiveWaypointLayers: ["maplibre-gl-directions-waypoint", "maplibre-gl-directions-waypoint-casing"]` 148 | 2. `sensitiveSnappointLayers: ["maplibre-gl-directions-snappoint", "maplibre-gl-directions-snappoint-casing"]` 149 | 3. `sensitiveRoutelineLayers: ["maplibre-gl-directions-routeline", "maplibre-gl-directions-routeline-casing"]` 150 | 4. `sensitiveAltRoutelineLayers: ["maplibre-gl-directions-alt-routeline", "maplibre-gl-directions-alt-routeline-casing"]` 151 | 152 | If you, e.g., decide to use the only `"my-waypoint"` layer to represent all the Waypoints, you must update the `sensitiveWaypointLayers` option's value respectfully: `sensitiveWaypointLayers: ["my-waypoint"]`. 153 | 154 | Also, don't forget to make sure that all the custom icons and images you use for your custom layers are loaded and added to the map before you create an instance of Directions. 155 | 156 | Another example of your interest might be the {@link https://maplibre.org/maplibre-gl-directions/#/examples/show-routes'-directions|Show Routes' Directions} one. It shows how to add an additional "symbol" layer to show arrows that represent the route's direction. 157 | 158 | ## Behavioral customization 159 | 160 | Behavioral customization allows you (jokes aside) to customize the plugin's behavior. It might be some minor customization (like saving some additional information for each waypoint in order to be able to somehow manipulate that saved data later) or some more complex cases like allowing for different types of waypoints - routed and straight-lined waypoints, though we won't cover the last case in this guide at least because it requires some severe updates on the back-end-side. 161 | 162 | Behavioral customization in its main comes down to 2 different strategies. In order to pick the most appropriate one, ask yourself a question: does the plugin's public interface provide enough data to satisfy my case? 163 | 164 | If the answer is "yes", then in most cases all you'd need is to listen to events and react to them appropriately. But if you need some additional data that comes from the server, or some intrinsic plugin's properties, you'd need to extend the {@link default|`MapLibreGlDirections`} superclass with a subclass: 165 | 166 | ```typescript 167 | import MapLibreGlDirections from "@maplibre/maplibre-gl-directions"; 168 | 169 | class MyCustomDirections extends MapLibreGlDirections { 170 | constructor(map: maplibregl.Map, configuration?: Partial) { 171 | super(map, configuration); 172 | } 173 | } 174 | ``` 175 | 176 | Then, instead of creating an instance of the {@link default} class, you create an instance of your custom class: 177 | 178 | ```typescript 179 | const directions = new MyCustomDirections(map, { 180 | requestOptions: { 181 | alternatives: "true", 182 | }, 183 | }); 184 | ``` 185 | 186 | In that subclass you're free to augment the default implementation the way you need, to remove methods and properties, to create your own custom ones, to modify and extend the built-in standard ones and more. 187 | 188 | There are 2 examples available at the moment that cover the subclass-extensibility case. The fist one is the {@link https://maplibre.org/maplibre-gl-directions/#/examples/distance-measurement|Distance Measurement}. It shows how to extend the {@link default} superclass with a subclass in a way so that the instance produced by the last would allow you to display each route leg's distance along the respective routeline. It also uses the {@link removewaypoint|`removewaypoint`} and {@link fetchroutesend|`fetchroutesend`} events to read the response's total distance field to be able to display it in the UI. 189 | 190 | The second example is called {@link https://maplibre.org/maplibre-gl-directions/#/examples/load-and-save|Load and Save}. It considers the case when you need to be able to load and save the (pre)built route as a collection of GeoJSON Features. 191 | 192 | The only thing that you should be aware of when trying to extend the plugin's default functionality with a subclass is that there exist two different approaches of extending the default methods. 193 | 194 | The thing here is that some methods of the main class are defined on it as usual normal methods, and some other being not exactly methods in common sense, but rather properties which hold functions in them. 195 | 196 | There's (almost) no difference from the architectual-design perspective, but the language still implies some restrictions over semantics for extensibility. 197 | 198 | Namely, normally you're allowed to re-define some existing method of a superclass like this if your goal is to also make use of the super method's functionality: 199 | 200 | ```typescript 201 | existingSuperMethod() { 202 | const originalResult = super.existingSuperMethod(); 203 | // ... do other stuff with the result 204 | } 205 | ``` 206 | 207 | But in cases with e.g. utility-methods of the plugin that becomes impossible, and what you need to do instead is to first save the original implementation somewhere (let's say as a class field) and then to manually call it where appropriate as if it was a super-call: 208 | 209 | ```typescript 210 | // where the `utils` comes from the `import { utils } from "@maplibre/maplibre-gl-directions"` 211 | originalBuildRoutelines = utils.buildRoutelines; 212 | 213 | protected buildRoutelines = ( 214 | requestOptions: MapLibreGlDirectionsConfiguration["requestOptions"], 215 | routes: Route[], 216 | selectedRouteIndex: number, 217 | snappoints: Feature[], 218 | ): Feature[][] => { 219 | // first we call the original method. It returns the built routelines 220 | const routelines = this.originalBuildRoutelines(requestOptions, routes, selectedRouteIndex, snappoints); 221 | 222 | // modify these routelines the way you need 223 | // ... 224 | 225 | // and don't forget to return the resulting modified routelines 226 | return routelines; 227 | } 228 | ``` 229 | 230 | See the examples' source codes to dive deeper into the implementation details. There are a lot of possibilities, and it's a really tricky business to describe each possible detail here in the docs. Feel free to experiment and ask a question either in the MapLibre's official channel in Slack or even open an issue (or a new discussion) in the plugin's GitHub repo. 231 | -------------------------------------------------------------------------------- /doc/MAIN.md: -------------------------------------------------------------------------------- 1 | # MapLibreGlDirections 2 | 3 | For the sakes of your convenience, make sure you've enabled the "Inherited" filter only: 4 | 5 | ![Enabling the "Inherited" filter only](https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/main/doc/images/public-filter.png) 6 | 7 | --- 8 | 9 | ## Basic Usage 10 | 11 | [[include:BASIC_USAGE.md]] 12 | 13 | --- 14 | 15 | ## Controls 16 | 17 | [[include:CONTROLS.md]] 18 | 19 | --- 20 | 21 | ## Customization 22 | 23 | [[include:CUSTOMIZATION.md]] 24 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | The files in this folder are used by TypeDoc to build the API documentation for the plugin. The MAIN.md file is a composition of all the other files in the same folder where each one represents a separate section of the main page of the API documentation. 2 | -------------------------------------------------------------------------------- /doc/images/complex-customization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/d829ccd0311e64aadf75f9900a3e76549cd402aa/doc/images/complex-customization.png -------------------------------------------------------------------------------- /doc/images/demo-screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/d829ccd0311e64aadf75f9900a3e76549cd402aa/doc/images/demo-screenshot-1.png -------------------------------------------------------------------------------- /doc/images/demo-screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/d829ccd0311e64aadf75f9900a3e76549cd402aa/doc/images/demo-screenshot-2.png -------------------------------------------------------------------------------- /doc/images/demo-screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/d829ccd0311e64aadf75f9900a3e76549cd402aa/doc/images/demo-screenshot-3.png -------------------------------------------------------------------------------- /doc/images/public-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/d829ccd0311e64aadf75f9900a3e76549cd402aa/doc/images/public-filter.png -------------------------------------------------------------------------------- /doc/images/public-protected-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/d829ccd0311e64aadf75f9900a3e76549cd402aa/doc/images/public-protected-filter.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@maplibre/maplibre-gl-directions", 3 | "version": "0.8.0", 4 | "type": "module", 5 | "main": "./dist/maplibre-gl-directions.js", 6 | "module": "./dist/maplibre-gl-directions.js", 7 | "types": "./dist/src/main.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "license": "MIT", 12 | "homepage": "https://maplibre.org/maplibre-gl-directions/#/", 13 | "repository": "https://github.com/maplibre/maplibre-gl-directions", 14 | "keywords": [ 15 | "directions", 16 | "osrm", 17 | "routing", 18 | "mapbox", 19 | "maplibre" 20 | ], 21 | "prettier": { 22 | "tabWidth": 2, 23 | "semi": true, 24 | "singleQuote": false, 25 | "quoteProps": "as-needed", 26 | "trailingComma": "all", 27 | "bracketSpacing": true, 28 | "printWidth": 120, 29 | "plugins": [ 30 | "prettier-plugin-svelte" 31 | ], 32 | "svelteSortOrder": "options-scripts-markup-styles", 33 | "svelteStrictMode": false, 34 | "svelteIndentScriptAndStyle": true 35 | }, 36 | "scripts": { 37 | "prepare": "husky install", 38 | "env:prep": "npm run build:lib && npm link && npm link @maplibre/maplibre-gl-directions", 39 | "dev:lib": "npm run check:lib && npm run tsc:lib && vite build --watch --mode development --config vite.lib.config.ts", 40 | "dev:doc": "typedoc --tsconfig tsconfig.lib.json --watch", 41 | "dev:demo": "npm run check:demo && npm run tsc:demo && vite --config vite.demo.config.ts", 42 | "build": "npm run lint && npm run build:lib && npm run build:doc && npm run build:demo", 43 | "build:lib": "npm run check:lib && npm run tsc:lib && vite build --config vite.lib.config.ts", 44 | "build:doc": "typedoc --tsconfig tsconfig.lib.json", 45 | "build:demo": "npm run check:demo && npm run tsc:demo && vite build --config vite.demo.config.ts --base /maplibre-gl-directions/", 46 | "format": "prettier --write .", 47 | "prelint": "npm run format", 48 | "tsc:lib": "tsc --project ./tsconfig.lib.json", 49 | "tsc:lib:watch": "npm run tsc:lib -- --watch", 50 | "tsc:demo": "tsc --project ./tsconfig.json", 51 | "tsc:demo:watch": "npm run tsc:demo -- --watch", 52 | "lint": "eslint --fix './{demo,src}/**/*.{ts,js,cjs,svelte}'", 53 | "check:lib": "svelte-check --tsconfig ../tsconfig.lib.json --workspace src", 54 | "check:demo": "svelte-check --tsconfig ../tsconfig.json --workspace demo", 55 | "check": "npm run lint && npm run check:lib && npm run check:demo" 56 | }, 57 | "lint-staged": { 58 | "./{src,demo}/**/*.{ts,js,cjs,svelte}": [ 59 | "npm run check" 60 | ] 61 | }, 62 | "dependencies": { 63 | "@placemarkio/polyline": "^1.2.0", 64 | "nanoid": "^5.0.6" 65 | }, 66 | "peerDependencies": { 67 | "maplibre-gl": "^5.0.0" 68 | }, 69 | "devDependencies": { 70 | "@sveltejs/vite-plugin-svelte": "^2.5.3", 71 | "@tailwindcss/forms": "^0.5.7", 72 | "@tsconfig/svelte": "^5.0.4", 73 | "@types/geojson": "^7946.0.13", 74 | "@types/lodash": "^4.17.0", 75 | "@types/mapbox__point-geometry": "^0.1.4", 76 | "@types/mapbox__vector-tile": "^1.3.4", 77 | "@types/node": "^16", 78 | "@typescript-eslint/parser": "^7.7.0", 79 | "@typescript-eslint/eslint-plugin": "^7.7.0", 80 | "autoprefixer": "^10.4.19", 81 | "eslint": "^8.56.0", 82 | "eslint-plugin-svelte": "^2.35.1", 83 | "husky": "^9.0.11", 84 | "lint-staged": "^15.2.0", 85 | "lodash": "^4.17.21", 86 | "maplibre-gl": "^5.1.0", 87 | "postcss": "^8.4.38", 88 | "postcss-load-config": "^5.0.3", 89 | "prettier": "^3.1.1", 90 | "prettier-plugin-svelte": "^3.2.2", 91 | "rollup-plugin-visualizer": "^5.9.2", 92 | "svelte": "^4.2.8", 93 | "svelte-check": "^3.6.8", 94 | "svelte-preprocess": "^5.0.4", 95 | "svelte-spa-router": "^3.3.0", 96 | "tailwindcss": "^3.4.3", 97 | "tslib": "^2.6.2", 98 | "typedoc": "^0.24.8", 99 | "typescript": "^5.1.6", 100 | "vite": "^4.5.3" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require("tailwindcss"); 2 | const autoprefixer = require("autoprefixer"); 3 | 4 | module.exports = { 5 | plugins: [tailwindcss(), autoprefixer], 6 | }; 7 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | The source code of the plugin lives here. The API documentation for it is parsed from the comments left inside these source files. The `directions` folder holds the code which serves the plugin's main purpose: routing and user-interaction. The `controls` folder at the time being contains the source code of the only plugin's control - the loading indicator control. 2 | 3 | The `main.ts` file is the main entry point of the library. 4 | -------------------------------------------------------------------------------- /src/controls/bearings/BearingsControl.svelte: -------------------------------------------------------------------------------- 1 | 107 | 108 |
113 |
114 | {#each waypointsBearings as waypointBearing, i} 115 |
123 | {i + 1}. 124 | 129 |
onImageMousedown(e, i)} role="spinbutton" tabindex="0"> 130 | 139 | 150 | 151 | 152 |
153 | 162 | ° 163 | ± 164 | {#if configuration.fixedDegrees} 165 | {configuration.fixedDegrees}° 166 | {:else} 167 | 176 | ° 177 | {/if} 178 |
179 | {/each} 180 |
181 |
182 | -------------------------------------------------------------------------------- /src/controls/bearings/main.ts: -------------------------------------------------------------------------------- 1 | import type { IControl } from "maplibre-gl"; 2 | import BearingsControlComponent from "./BearingsControl.svelte"; 3 | import { BearingsControlDefaultConfiguration } from "./types"; 4 | import type { BearingsControlConfiguration } from "./types"; 5 | import type MapLibreGlDirections from "../../directions/main"; 6 | 7 | /** 8 | * Creates an instance of BearingsControl that could be added to the map using the 9 | * {@link https://maplibre.org/maplibre-gl-js-docs/api/map/#map#addcontrol|`addControl`} method. 10 | * 11 | * @example 12 | * ```typescript 13 | * import MapLibreGlDirections, { BearingsControl } from "@maplibre/maplibre-gl-directions"; 14 | * map.addControl(new BearingsControl(new MapLibreGlDirections(map))); 15 | * ``` 16 | */ 17 | export default class BearingsControl implements IControl { 18 | constructor(directions: MapLibreGlDirections, configuration?: Partial) { 19 | this.directions = directions; 20 | this.configuration = Object.assign({}, BearingsControlDefaultConfiguration, configuration); 21 | } 22 | 23 | private controlElement!: HTMLElement; 24 | private readonly directions: MapLibreGlDirections; 25 | private readonly configuration: BearingsControlConfiguration; 26 | 27 | /** 28 | * @private 29 | */ 30 | onAdd() { 31 | this.controlElement = document.createElement("div"); 32 | 33 | new BearingsControlComponent({ 34 | target: this.controlElement, 35 | props: { 36 | directions: this.directions, 37 | configuration: this.configuration, 38 | }, 39 | }); 40 | 41 | return this.controlElement; 42 | } 43 | 44 | /** 45 | * @private 46 | */ 47 | onRemove() { 48 | this.controlElement.remove(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/controls/bearings/types.ts: -------------------------------------------------------------------------------- 1 | export interface BearingsControlConfiguration { 2 | /** 3 | * Whether the bearings support is enabled by default for new waypoints. 4 | * 5 | * @default `false` 6 | */ 7 | defaultEnabled: boolean; 8 | 9 | /** 10 | * Debounce requests by the specified amount of milliseconds. 11 | * 12 | * @default `150` 13 | */ 14 | debounceTimeout: number; 15 | 16 | /** 17 | * The default angle for a waypoint when it's added. 18 | * 19 | * @default `0` 20 | */ 21 | angleDefault: number; 22 | 23 | /** 24 | * Minimal allowed angle for a waypoint (affects the control's respective numeric input behavior). 25 | * 26 | * @default `0` 27 | */ 28 | angleMin: number; 29 | 30 | /** 31 | * Maximal allowed angle for a waypoint (affects the control's respective numeric input behavior). 32 | * 33 | * @default `359` 34 | */ 35 | angleMax: number; 36 | 37 | /** 38 | * How many degrees to add/remove to/from the bearing's angle value when the control's respective numeric input's 39 | * up/down button is clicked. 40 | * 41 | * @default `1` 42 | */ 43 | angleStep: number; 44 | 45 | /** 46 | * Whether to allow changing the bearings' degrees. When 0 - allow to change degrees, when any other value - use that 47 | * value instead. 48 | * 49 | * @default `0` 50 | */ 51 | fixedDegrees: number; 52 | 53 | /** 54 | * The default degree for a waypoint when it's added. 55 | * 56 | * @default `45` 57 | */ 58 | degreesDefault: number; 59 | 60 | /** 61 | * Minimal allowed degree for a waypoint (affects the control's respective numeric input behavior). 62 | * 63 | * @default `15` 64 | */ 65 | degreesMin: number; 66 | 67 | /** 68 | * Maximal allowed degree for a waypoint (affects the control's respective numeric input behavior). 69 | * 70 | * @default `360` 71 | */ 72 | degreesMax: number; 73 | 74 | /** 75 | * How many degrees to add/remove to/from the bearing's degrees value when the control's respective numeric input's 76 | * up/down button is clicked. 77 | * 78 | * @default `15` 79 | */ 80 | degreesStep: number; 81 | 82 | /** 83 | * Whether the waypoint-images in the control should be rotated according to the map's current bearing. 84 | * 85 | * @default `false` 86 | */ 87 | respectMapBearing: boolean; 88 | 89 | /** 90 | * The size of the waypoint-images in the control (in pixels). 91 | * 92 | * @default `50` 93 | */ 94 | imageSize: number; 95 | } 96 | 97 | export const BearingsControlDefaultConfiguration: BearingsControlConfiguration = { 98 | defaultEnabled: false, 99 | debounceTimeout: 150, 100 | angleDefault: 0, 101 | angleMin: 0, 102 | angleMax: 359, 103 | angleStep: 1, 104 | fixedDegrees: 0, 105 | degreesDefault: 45, 106 | degreesMin: 15, 107 | degreesMax: 360, 108 | degreesStep: 15, 109 | respectMapBearing: false, 110 | imageSize: 50, 111 | }; 112 | -------------------------------------------------------------------------------- /src/controls/common.css: -------------------------------------------------------------------------------- 1 | /* Don't strip out! */ 2 | -------------------------------------------------------------------------------- /src/controls/loading-indicator/LoadingIndicatorControl.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | {#if loading} 20 | 28 | 29 | 34 | 35 | 36 | 41 | 46 | 54 | 55 | 56 | {/if} 57 | -------------------------------------------------------------------------------- /src/controls/loading-indicator/main.ts: -------------------------------------------------------------------------------- 1 | import type { IControl } from "maplibre-gl"; 2 | import LoadingIndicatorControlComponent from "./LoadingIndicatorControl.svelte"; 3 | import { LoadingIndicatorControlDefaultConfiguration } from "./types"; 4 | import type { LoadingIndicatorControlConfiguration } from "./types"; 5 | import type MapLibreGlDirections from "../../directions/main"; 6 | 7 | /** 8 | * Creates an instance of LoadingControl that could be added to the map using the 9 | * {@link https://maplibre.org/maplibre-gl-js-docs/api/map/#map#addcontrol|`addControl`} method. 10 | * 11 | * @example 12 | * ```typescript 13 | * import MapLibreGlDirections, { LoadingControl } from "@maplibre/maplibre-gl-directions"; 14 | * map.addControl(new LoadingControl(new MapLibreGlDirections(map))); 15 | * ``` 16 | */ 17 | export default class LoadingControl implements IControl { 18 | constructor(directions: MapLibreGlDirections, configuration?: Partial) { 19 | this.directions = directions; 20 | this.configuration = Object.assign({}, LoadingIndicatorControlDefaultConfiguration, configuration); 21 | } 22 | 23 | private controlElement!: HTMLElement; 24 | private readonly directions: MapLibreGlDirections; 25 | private readonly configuration: LoadingIndicatorControlConfiguration; 26 | 27 | /** 28 | * @private 29 | */ 30 | onAdd() { 31 | this.controlElement = document.createElement("div"); 32 | 33 | new LoadingIndicatorControlComponent({ 34 | target: this.controlElement, 35 | props: { 36 | directions: this.directions, 37 | configuration: this.configuration, 38 | }, 39 | }); 40 | 41 | return this.controlElement; 42 | } 43 | 44 | /** 45 | * @private 46 | */ 47 | onRemove() { 48 | this.controlElement.remove(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/controls/loading-indicator/types.ts: -------------------------------------------------------------------------------- 1 | export interface LoadingIndicatorControlConfiguration { 2 | /** 3 | * Fill-color for the loader. Any valid CSS-value. 4 | * 5 | * @default "#6d26d7" 6 | */ 7 | fill: string; 8 | 9 | /** 10 | * The size of the loader. Any valid CSS-value. 11 | * 12 | * @default "24px" 13 | */ 14 | size: string; 15 | 16 | /** 17 | * Class-string passed as-is to the `class=""` attribute of the loader SVG. 18 | * 19 | * @default "" 20 | */ 21 | class: string; 22 | } 23 | 24 | export const LoadingIndicatorControlDefaultConfiguration: LoadingIndicatorControlConfiguration = { 25 | fill: "#6d26d7", 26 | size: "24px", 27 | class: "", 28 | }; 29 | -------------------------------------------------------------------------------- /src/directions/events.ts: -------------------------------------------------------------------------------- 1 | import type { Map, MapMouseEvent, MapTouchEvent } from "maplibre-gl"; 2 | import type { Directions } from "./types"; 3 | 4 | export class MapLibreGlDirectionsEvented { 5 | constructor(map: Map) { 6 | this.map = map; 7 | } 8 | 9 | protected readonly map: Map; 10 | 11 | private listeners: ListenersStore = {}; 12 | private oneTimeListeners: ListenersStore = {}; 13 | 14 | protected fire(event: MapLibreGlDirectionsEventType[T]) { 15 | event.target = this.map; 16 | 17 | const type: T = event.type as T; 18 | 19 | this.listeners[type]?.forEach((listener) => listener(event)); 20 | this.oneTimeListeners[type]?.forEach((listener) => { 21 | listener(event); 22 | 23 | const index = this.oneTimeListeners[type]?.indexOf(listener); 24 | if (index !== undefined && ~index) this.oneTimeListeners[type]?.splice(index, 1); 25 | }); 26 | } 27 | 28 | /** 29 | * Registers an event listener. 30 | */ 31 | on(type: T, listener: MapLibreGlDirectionsEventListener) { 32 | this.listeners[type] = this.listeners[type] ?? []; 33 | this.listeners[type]!.push(listener); 34 | } 35 | 36 | /** 37 | * Un-registers an event listener. 38 | */ 39 | off(type: T, listener: (e: MapLibreGlDirectionsEventType[T]) => void) { 40 | const index = this.listeners[type]?.indexOf(listener); 41 | if (index !== undefined && ~index) this.listeners[type]?.splice(index, 1); 42 | } 43 | 44 | /** 45 | * Registers an event listener to be invoked only once. 46 | */ 47 | once(type: T, listener: MapLibreGlDirectionsEventListener) { 48 | this.oneTimeListeners[type] = this.oneTimeListeners[type] ?? []; 49 | this.oneTimeListeners[type]!.push(listener); 50 | } 51 | } 52 | 53 | /** 54 | * Supported event types. 55 | */ 56 | export interface MapLibreGlDirectionsEventType { 57 | /** 58 | * Emitted after the waypoints are set using the {@link default.setWaypoints|`setWaypoints`} method. 59 | */ 60 | setwaypoints: MapLibreGlDirectionsWaypointEvent; 61 | 62 | /** 63 | * Emitted after the waypoints' bearings values are changed using the 64 | * {@link default.waypointsBearings|`waypointsBearings`} setter. 65 | */ 66 | rotatewaypoints: MapLibreGlDirectionsWaypointEvent; 67 | 68 | /** 69 | * Emitted when a waypoint is added. 70 | */ 71 | addwaypoint: MapLibreGlDirectionsWaypointEvent; 72 | 73 | /** 74 | * Emitted when a waypoint is removed. 75 | */ 76 | removewaypoint: MapLibreGlDirectionsWaypointEvent; 77 | 78 | /** 79 | * Emitted when a waypoint is moved. __Note__ that the event is not emitted if the waypoint has been dragged for an 80 | * amount of pixels less than specified by the {@link MapLibreGlDirectionsConfiguration.dragThreshold|`dragThreshold`} 81 | * configuration property. 82 | */ 83 | movewaypoint: MapLibreGlDirectionsWaypointEvent; 84 | 85 | /** 86 | * Emitted when there appears an ongoing routing-request. 87 | */ 88 | fetchroutesstart: MapLibreGlDirectionsRoutingEvent; 89 | 90 | /** 91 | * Emitted after the ongoing routing-request has finished. 92 | */ 93 | fetchroutesend: MapLibreGlDirectionsRoutingEvent; 94 | } 95 | 96 | export type MapLibreGlDirectionsEventListener = ( 97 | event: MapLibreGlDirectionsEventType[T], 98 | ) => void; 99 | 100 | type ListenersStore = Partial<{ 101 | [T in keyof MapLibreGlDirectionsEventType]: MapLibreGlDirectionsEventListener[]; 102 | }>; 103 | 104 | export interface MapLibreGlDirectionsEvent { 105 | type: T; 106 | target: Map; 107 | originalEvent: TOrig; 108 | } 109 | 110 | export interface MapLibreGlDirectionsWaypointEventData { 111 | /** 112 | * Index of the added/removed/moved waypoint. 113 | * 114 | * Never presents for {@link MapLibreGlDirectionsEventType.setwaypoints|`setwaypoints`} and 115 | * {@link MapLibreGlDirectionsEventType.rotatewaypoints|`rotatewaypoints`} events. 116 | */ 117 | index: number; 118 | 119 | /** 120 | * Coordinates from which the waypoint has been moved. 121 | * 122 | * Only presents when it's the {@link MapLibreGlDirectionsEventType.movewaypoint|`movewaypoint`} event. 123 | */ 124 | initialCoordinates: [number, number]; 125 | } 126 | 127 | export class MapLibreGlDirectionsWaypointEvent 128 | implements 129 | MapLibreGlDirectionsEvent< 130 | MapMouseEvent | MapTouchEvent | undefined, 131 | "setwaypoints" | "rotatewaypoints" | "addwaypoint" | "removewaypoint" | "movewaypoint" 132 | > 133 | { 134 | /** 135 | * @private 136 | */ 137 | constructor( 138 | type: "setwaypoints" | "rotatewaypoints" | "addwaypoint" | "removewaypoint" | "movewaypoint", 139 | originalEvent: MapMouseEvent | MapTouchEvent | undefined, 140 | data?: Partial, 141 | ) { 142 | this.type = type; 143 | this.originalEvent = originalEvent; 144 | this.data = data; 145 | } 146 | 147 | type; 148 | target!: Map; 149 | originalEvent: MapMouseEvent | MapTouchEvent | undefined; 150 | data?: Partial; 151 | } 152 | 153 | export type MapLibreGlDirectionsRoutingEventData = Directions; 154 | 155 | export class MapLibreGlDirectionsRoutingEvent 156 | implements MapLibreGlDirectionsEvent 157 | { 158 | /** 159 | * @private 160 | */ 161 | constructor( 162 | type: "fetchroutesstart" | "fetchroutesend", 163 | originalEvent: MapLibreGlDirectionsWaypointEvent, 164 | data?: MapLibreGlDirectionsRoutingEventData, 165 | ) { 166 | this.type = type; 167 | this.originalEvent = originalEvent; 168 | this.data = data; 169 | } 170 | 171 | type; 172 | target!: Map; 173 | originalEvent: MapLibreGlDirectionsWaypointEvent; 174 | /** 175 | * The server's response. 176 | * 177 | * Only presents when it's the {@link MapLibreGlDirectionsEventType.fetchroutesend|`fetchroutesend`} event, but might 178 | * be `undefined` in case the request to fetch directions failed. 179 | * 180 | * @see http://project-osrm.org/docs/v5.24.0/api/#responses 181 | */ 182 | data?: MapLibreGlDirectionsRoutingEventData; 183 | } 184 | -------------------------------------------------------------------------------- /src/directions/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { GeoJSONGeometry, Geometry, Leg, MapLibreGlDirectionsConfiguration, PolylineGeometry } from "./types"; 2 | import { decode } from "@placemarkio/polyline"; 3 | import type { Position, Feature, Point } from "geojson"; 4 | 5 | /** 6 | * Decodes the geometry of a route to the form of a coordinates array. 7 | */ 8 | export function geometryDecoder( 9 | requestOptions: MapLibreGlDirectionsConfiguration["requestOptions"], 10 | geometry: Geometry, 11 | ): Position[] { 12 | if (requestOptions.geometries === "geojson") { 13 | return (geometry as GeoJSONGeometry).coordinates; 14 | } else if (requestOptions.geometries === "polyline6") { 15 | return decode(geometry as PolylineGeometry, 6); 16 | } else { 17 | return decode(geometry as PolylineGeometry, 5); 18 | } 19 | } 20 | 21 | /** 22 | * Decodes the congestion level of a specific segment of a route leg. 23 | */ 24 | export function congestionLevelDecoder( 25 | requestOptions: MapLibreGlDirectionsConfiguration["requestOptions"], 26 | annotation: Leg["annotation"] | undefined, 27 | segmentIndex: number, 28 | ): number { 29 | if (requestOptions.annotations?.includes("congestion_numeric")) { 30 | return annotation?.congestion_numeric?.[segmentIndex] ?? 0; 31 | } else if (requestOptions.annotations?.includes("congestion")) { 32 | switch (annotation?.congestion?.[segmentIndex] ?? "") { 33 | case "unknown": 34 | return 0; 35 | case "low": 36 | return 1; 37 | case "moderate": 38 | return 34; 39 | case "heavy": 40 | return 77; 41 | case "severe": 42 | return 100; 43 | default: 44 | return 0; 45 | } 46 | } else { 47 | return 0; 48 | } 49 | } 50 | 51 | /** 52 | * Compares two coordinates and returns `true` if they are equal taking into account that there's an allowable error in 53 | * 0.00001 degree when using "polyline" geometries (5 fractional-digits precision). 54 | */ 55 | export function coordinatesComparator( 56 | requestOptions: MapLibreGlDirectionsConfiguration["requestOptions"], 57 | a: Position, 58 | b: Position, 59 | ): boolean { 60 | if (!requestOptions.geometries || requestOptions.geometries === "polyline") { 61 | return Math.abs(a[0] - b[0]) <= 0.00001 && Math.abs(a[1] - b[1]) <= 0.00001; 62 | } else { 63 | return a[0] === b[0] && a[1] === b[1]; 64 | } 65 | } 66 | 67 | /** 68 | * Gets coordinates of a point feature 69 | */ 70 | export function getWaypointsCoordinates(waypoints: Feature[]): [number, number][] { 71 | return waypoints.map((waypoint) => { 72 | return [waypoint.geometry.coordinates[0], waypoint.geometry.coordinates[1]]; 73 | }); 74 | } 75 | 76 | /** 77 | * Gets bearings out of properties of point features 78 | */ 79 | export function getWaypointsBearings(waypoints: Feature[]): ([number, number] | undefined)[] { 80 | return waypoints.map((waypoint) => { 81 | return Array.isArray(waypoint.properties?.bearing) 82 | ? [waypoint.properties?.bearing[0], waypoint.properties?.bearing[1]] 83 | : undefined; 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /src/directions/layers.ts: -------------------------------------------------------------------------------- 1 | import type { LayerSpecification, LineLayerSpecification } from "maplibre-gl"; 2 | import type { CircleLayerSpecification } from "@maplibre/maplibre-gl-style-spec"; 3 | 4 | export const colors = { 5 | snapline: "#34343f", 6 | altRouteline: "#9e91be", 7 | routelineFoot: "#3665ff", 8 | routelineBike: "#63c4ff", 9 | routeline: "#7b51f8", 10 | congestionLow: "#42c74c", 11 | congestionHigh: "#d72359", 12 | hoverpoint: "#30a856", 13 | snappoint: "#cb3373", 14 | snappointHighlight: "#e50d3f", 15 | waypointFoot: "#3665ff", 16 | waypointFootHighlight: "#0942ff", 17 | waypointBike: "#63c4ff", 18 | waypointBikeHighlight: "#0bb8ff", 19 | waypoint: "#7b51f8", 20 | waypointHighlight: "#6d26d7", 21 | }; 22 | 23 | const routelineColor: NonNullable["line-color"] = [ 24 | "case", 25 | ["==", ["get", "profile", ["get", "arriveSnappointProperties"]], "foot"], 26 | colors.routelineFoot, 27 | ["==", ["get", "profile", ["get", "arriveSnappointProperties"]], "bike"], 28 | colors.routelineBike, 29 | [ 30 | "interpolate-hcl", 31 | ["linear"], 32 | ["get", "congestion"], 33 | 0, 34 | colors.routeline, 35 | 1, 36 | colors.congestionLow, 37 | 100, 38 | colors.congestionHigh, 39 | ], 40 | ]; 41 | 42 | const waypointColor: NonNullable["circle-color"] = [ 43 | "case", 44 | ["==", ["get", "profile"], "foot"], 45 | ["case", ["boolean", ["get", "highlight"], false], colors.waypointFootHighlight, colors.waypointFoot], 46 | ["==", ["get", "profile"], "bike"], 47 | ["case", ["boolean", ["get", "highlight"], false], colors.waypointBikeHighlight, colors.waypointBike], 48 | ["case", ["boolean", ["get", "highlight"], false], colors.waypointHighlight, colors.waypoint], 49 | ]; 50 | 51 | const snappointColor: NonNullable["circle-color"] = [ 52 | "case", 53 | ["boolean", ["get", "highlight"], false], 54 | colors.snappointHighlight, 55 | colors.snappoint, 56 | ]; 57 | 58 | /** 59 | * Builds the 60 | * {@link https://github.com/smellyshovel/maplibre-gl-directions/blob/main/src/directions/layers.ts#L3|standard 61 | * `MapLibreGlDirections` layers} with optionally scaled features. 62 | * 63 | * @param pointsScalingFactor A number to multiply the initial points' dimensions by 64 | * @param linesScalingFactor A number to multiply the initial lines' dimensions by 65 | * @param sourceName A name of the source used by the instance and layers names' prefix 66 | */ 67 | export default function layersFactory( 68 | pointsScalingFactor = 1, 69 | linesScalingFactor = 1, 70 | sourceName = "maplibre-gl-directions", 71 | ): LayerSpecification[] { 72 | const pointCasingCircleRadius: NonNullable["circle-radius"] = [ 73 | "interpolate", 74 | ["exponential", 1.5], 75 | ["zoom"], 76 | // don't forget it's the radius! The visible value is diameter (which is 2x) 77 | // on zoom levels 0-5 should be 5px more than the routeline casing. 7 + 5 = 12. 78 | // When highlighted should be +2px more. 12 + 2 = 14 79 | 0, 80 | // highlighted to default ratio (epsilon) = 14 / 12 ~= 1.16 81 | [ 82 | "case", 83 | ["boolean", ["get", "highlight"], ["==", ["get", "type"], "HOVERPOINT"]], 84 | 14 * pointsScalingFactor, 85 | 12 * pointsScalingFactor, 86 | ], 87 | 5, 88 | [ 89 | "case", 90 | ["boolean", ["get", "highlight"], ["==", ["get", "type"], "HOVERPOINT"]], 91 | 14 * pointsScalingFactor, 92 | 12 * pointsScalingFactor, 93 | ], 94 | // exponentially grows on zoom levels 5-18 finally becoming the same 5px wider than the routeline's casing on 95 | // the same zoom level: 23 + 5 = 28px 96 | 18, 97 | // highlighted = default ~= 33 98 | [ 99 | "case", 100 | ["boolean", ["get", "highlight"], ["==", ["get", "type"], "HOVERPOINT"]], 101 | 33 * pointsScalingFactor, 102 | 28 * pointsScalingFactor, 103 | ], 104 | ]; 105 | 106 | const pointCircleRadius: NonNullable["circle-radius"] = [ 107 | "interpolate", 108 | ["exponential", 1.5], 109 | ["zoom"], 110 | // on zoom levels 0-5 - 5px smaller than the casing. 12 - 5 = 7. 111 | 0, 112 | // feature to casing ratio (psi) = 7 / 12 ~= 0.58 113 | // highlighted to default ratio (epsilon) = 9 / 7 ~= 1.28 114 | [ 115 | "case", 116 | ["boolean", ["get", "highlight"], ["==", ["get", "type"], "HOVERPOINT"]], 117 | 9 * pointsScalingFactor, 118 | 7 * pointsScalingFactor, 119 | ], 120 | 5, 121 | [ 122 | "case", 123 | ["boolean", ["get", "highlight"], ["==", ["get", "type"], "HOVERPOINT"]], 124 | 9 * pointsScalingFactor, 125 | 7 * pointsScalingFactor, 126 | ], 127 | // exponentially grows on zoom levels 5-18 finally becoming psi times the casing 128 | 18, 129 | // psi * 28 ~= 16 130 | // when highlighted multiply by epsilon ~= 21 131 | [ 132 | "case", 133 | ["boolean", ["get", "highlight"], ["==", ["get", "type"], "HOVERPOINT"]], 134 | 21 * pointsScalingFactor, 135 | 16 * pointsScalingFactor, 136 | ], 137 | ]; 138 | 139 | const lineWidth: NonNullable["line-width"] = [ 140 | "interpolate", 141 | ["exponential", 1.5], 142 | ["zoom"], 143 | // on zoom levels 0-5 - 4px smaller than the casing (2px on each side). 7 - 4 = 3. 144 | // Doesn't change when highlighted 145 | 0, 146 | // feature to casing ratio (psi) = 3 / 7 ~= 0.42 147 | 3 * linesScalingFactor, 148 | 5, 149 | 3 * linesScalingFactor, 150 | // exponentially grows on zoom levels 5-18 finally becoming psi times the casing 151 | 18, 152 | // psi * 23 ~= 10 153 | 10 * linesScalingFactor, 154 | ]; 155 | 156 | const lineCasingWidth: NonNullable["line-width"] = [ 157 | "interpolate", 158 | ["exponential", 1.5], 159 | ["zoom"], 160 | // on zoom levels 0-5 - 7px by default and 10px when highlighted 161 | 0, 162 | // highlighted to default ratio (epsilon) = 10 / 7 ~= 1.42 163 | ["case", ["boolean", ["get", "highlight"], false], 10 * linesScalingFactor, 7 * linesScalingFactor], 164 | 5, 165 | ["case", ["boolean", ["get", "highlight"], false], 10 * linesScalingFactor, 7 * linesScalingFactor], 166 | // exponentially grows on zoom levels 5-18 finally becoming 32px when highlighted 167 | 18, 168 | // default = 32 / epsilon ~= 23 169 | ["case", ["boolean", ["get", "highlight"], false], 32 * linesScalingFactor, 23 * linesScalingFactor], 170 | ]; 171 | 172 | return [ 173 | { 174 | id: `${sourceName}-snapline`, 175 | type: "line", 176 | source: sourceName, 177 | layout: { 178 | "line-cap": "round", 179 | "line-join": "round", 180 | }, 181 | paint: { 182 | "line-dasharray": [3, 3], 183 | "line-color": colors.snapline, 184 | "line-opacity": 0.65, 185 | "line-width": 3, 186 | }, 187 | filter: ["==", ["get", "type"], "SNAPLINE"], 188 | }, 189 | 190 | { 191 | id: `${sourceName}-alt-routeline-casing`, 192 | type: "line", 193 | source: sourceName, 194 | layout: { 195 | "line-cap": "butt", 196 | "line-join": "round", 197 | }, 198 | paint: { 199 | "line-color": colors.altRouteline, 200 | "line-opacity": 0.55, 201 | "line-width": lineCasingWidth, 202 | }, 203 | filter: ["==", ["get", "route"], "ALT"], 204 | }, 205 | { 206 | id: `${sourceName}-alt-routeline`, 207 | type: "line", 208 | source: sourceName, 209 | layout: { 210 | "line-cap": "butt", 211 | "line-join": "round", 212 | }, 213 | paint: { 214 | "line-color": colors.altRouteline, 215 | "line-opacity": 0.85, 216 | "line-width": lineWidth, 217 | }, 218 | filter: ["==", ["get", "route"], "ALT"], 219 | }, 220 | 221 | { 222 | id: `${sourceName}-routeline-casing`, 223 | type: "line", 224 | source: sourceName, 225 | layout: { 226 | "line-cap": "butt", 227 | "line-join": "round", 228 | }, 229 | paint: { 230 | "line-color": routelineColor, 231 | "line-opacity": 0.55, 232 | "line-width": lineCasingWidth, 233 | }, 234 | filter: ["==", ["get", "route"], "SELECTED"], 235 | }, 236 | { 237 | id: `${sourceName}-routeline`, 238 | type: "line", 239 | source: sourceName, 240 | layout: { 241 | "line-cap": "butt", 242 | "line-join": "round", 243 | }, 244 | paint: { 245 | "line-color": routelineColor, 246 | "line-opacity": 0.85, 247 | "line-width": lineWidth, 248 | }, 249 | filter: ["==", ["get", "route"], "SELECTED"], 250 | }, 251 | 252 | { 253 | id: `${sourceName}-hoverpoint-casing`, 254 | type: "circle", 255 | source: sourceName, 256 | paint: { 257 | "circle-radius": pointCasingCircleRadius, 258 | "circle-color": colors.hoverpoint, 259 | "circle-opacity": 0.65, 260 | }, 261 | filter: ["==", ["get", "type"], "HOVERPOINT"], 262 | }, 263 | { 264 | id: `${sourceName}-hoverpoint`, 265 | type: "circle", 266 | source: sourceName, 267 | paint: { 268 | // same as snappoint, but always hig(since it's always highlighted while present on the map) 269 | "circle-radius": pointCircleRadius, 270 | "circle-color": colors.hoverpoint, 271 | }, 272 | filter: ["==", ["get", "type"], "HOVERPOINT"], 273 | }, 274 | 275 | { 276 | id: `${sourceName}-snappoint-casing`, 277 | type: "circle", 278 | source: sourceName, 279 | paint: { 280 | "circle-radius": pointCasingCircleRadius, 281 | "circle-color": snappointColor, 282 | "circle-opacity": 0.65, 283 | }, 284 | filter: ["==", ["get", "type"], "SNAPPOINT"], 285 | }, 286 | { 287 | id: `${sourceName}-snappoint`, 288 | type: "circle", 289 | source: sourceName, 290 | paint: { 291 | "circle-radius": pointCircleRadius, 292 | "circle-color": snappointColor, 293 | }, 294 | filter: ["==", ["get", "type"], "SNAPPOINT"], 295 | }, 296 | 297 | { 298 | id: `${sourceName}-waypoint-casing`, 299 | type: "circle", 300 | source: sourceName, 301 | paint: { 302 | "circle-radius": pointCasingCircleRadius, 303 | "circle-color": waypointColor, 304 | "circle-opacity": 0.65, 305 | }, 306 | filter: ["==", ["get", "type"], "WAYPOINT"], 307 | }, 308 | 309 | { 310 | id: `${sourceName}-waypoint`, 311 | type: "circle", 312 | source: sourceName, 313 | paint: { 314 | "circle-radius": pointCircleRadius, 315 | "circle-color": waypointColor, 316 | }, 317 | filter: ["==", ["get", "type"], "WAYPOINT"], 318 | }, 319 | ] satisfies LayerSpecification[]; 320 | } 321 | -------------------------------------------------------------------------------- /src/directions/types.ts: -------------------------------------------------------------------------------- 1 | import type { LayerSpecification } from "maplibre-gl"; 2 | 3 | /** 4 | * The {@link default|MapLibreGlDirections} configuration object's interface. 5 | */ 6 | export interface MapLibreGlDirectionsConfiguration { 7 | /** 8 | * An API-provider URL to make the routing requests to. 9 | * 10 | * Any {@link http://project-osrm.org/|OSRM}-compatible or 11 | * {@link https://docs.mapbox.com/api/navigation/directions/|Mapbox Directions API}-compatible API-provider is 12 | * supported. 13 | * 14 | * @default `"https://router.project-osrm.org/route/v1"` 15 | * 16 | * @example 17 | * ``` 18 | * api: "https://router.project-osrm.org/route/v1" 19 | * ``` 20 | * 21 | * @example 22 | * ``` 23 | * api: "https://api.mapbox.com/directions/v5" 24 | * ``` 25 | */ 26 | api: string; 27 | 28 | /** 29 | * A routing profile to use. The value depends on the API-provider of choice. 30 | * 31 | * @see {@link http://project-osrm.org/docs/v5.24.0/api/#requests|OSRM #Requests} 32 | * @see {@link https://docs.mapbox.com/api/navigation/directions/#routing-profiles|Mapbox Direction API #Routing profiles} 33 | * 34 | * @default `"driving"` 35 | * 36 | * @example 37 | * ``` 38 | * api: "https://router.project-osrm.org/route/v1", 39 | * profile: "driving" 40 | * ``` 41 | * 42 | * @example 43 | * ``` 44 | * api: "https://api.mapbox.com/directions/v5", 45 | * profile: "mapbox/driving-traffic" 46 | * ``` 47 | */ 48 | profile: string; 49 | 50 | /** 51 | * A list of the request-payload parameters that are passed along with routing requests. 52 | * 53 | * __Note__ that the `access-token` request-parameter has a special treatment when used along with 54 | * {@link makePostRequest|`makePostRequest: true`}. It's automatically removed from the `FormData` and passed as a URL 55 | * query-parameter as the Mapbox Directions API {@link https://docs.mapbox.com/api/navigation/http-post/|requires}. 56 | * 57 | * @default `{}` 58 | * 59 | * @example 60 | * ``` 61 | * requestOptions: { 62 | * overview: "full", 63 | * steps: "true" 64 | * } 65 | * ``` 66 | * 67 | * @example 68 | * ``` 69 | * api: "https://api.mapbox.com/directions/v5", 70 | * profile: "mapbox/driving-traffic", 71 | * requestOptions: { 72 | * access_token: "", 73 | * annotations: "congestion", 74 | * geometries: "polyline6" 75 | * } 76 | * ``` 77 | */ 78 | requestOptions: Partial>; 79 | 80 | /** 81 | * A timeout in ms after which a still-unresolved routing-request automatically gets aborted. 82 | * 83 | * @default `null` (no timeout) 84 | * 85 | * @example 86 | * ``` 87 | * // abort requests that take longer then 5s to complete 88 | * requestTimeout: 5000 89 | * ``` 90 | */ 91 | requestTimeout: number | null; 92 | 93 | /** 94 | * Whether to make a {@link https://docs.mapbox.com/api/navigation/http-post/|POST request} instead of a GET one. 95 | * 96 | * __Note__ that this is only supported by the Mapbox Directions API. Don't set the value to `true` if using an 97 | * OSRM-compatible API-provider. 98 | * 99 | * @default `false` 100 | * 101 | * @example 102 | * ``` 103 | * api: "https://api.mapbox.com/directions/v5", 104 | * profile: "mapbox/driving-traffic", 105 | * makePostRequest: true 106 | * ``` 107 | */ 108 | makePostRequest: boolean; 109 | 110 | /** 111 | * A name of the source used by the instance. Also used as a prefix for the default layers' names. 112 | * 113 | * __Note__ that if you decide to set this field to some custom value, you'd also need to update the following 114 | * settings accordingly: {@link sensitiveWaypointLayers}, {@link sensitiveSnappointLayers}, 115 | * {@link sensitiveRoutelineLayers} and {@link sensitiveAltRoutelineLayers}. 116 | * 117 | * @default `"maplibre-gl-directions"` 118 | * 119 | * @example 120 | * ``` 121 | * sourceName: "my-directions" 122 | * ``` 123 | */ 124 | sourceName: string; 125 | 126 | /** 127 | * The layers used by the plugin. 128 | * 129 | * @default The value returned by the {@link layersFactory|`layersFactory`} invoked with the passed 130 | * {@link pointsScalingFactor|`options.pointsScalingFactor`} and 131 | * {@link linesScalingFactor|`options.linesScalingFactor`} 132 | * 133 | * __Note__ that you don't have to create layers with the {@link layersFactory|`layersFactory`}. Any 134 | * `LayerSpecification[]` value is OK. 135 | * 136 | * __Note__ that if you add custom layers then you'd most probably want to register them as sensitive layers using 137 | * the {@link sensitiveWaypointLayers|`options.sensitiveWaypointLayers`}, 138 | * {@link sensitiveSnappointLayers|`options.sensitiveSnappointLayers`}, 139 | * {@link sensitiveAltRoutelineLayers|`options.sensitiveAltRoutelineLayers`} and 140 | * {@link sensitiveRoutelineLayers|`options.sensitiveRoutelineLayers`} options. 141 | * 142 | * @example 143 | * ``` 144 | * // Use the default layers with all the points increased by 1.5 times and all the lines increased by 2 times and an additional `"my-custom-layer"` layer. 145 | * { 146 | * layers: layersFactory(1.5, 2).concat([ 147 | * { 148 | * id: "my-custom-layer", 149 | * // ... 150 | * } 151 | * ]) 152 | * } 153 | * ``` 154 | */ 155 | layers: LayerSpecification[]; 156 | 157 | /** 158 | * A factor by which all the default points' dimensions should be increased. The value is passed as is to the 159 | * {@link layersFactory|`layersFactory`}'s first argument. 160 | * 161 | * __Note__ that the option has no effect when the `layers` option is provided. 162 | * 163 | * @default `1` 164 | * 165 | * @example 166 | * ``` 167 | * // Increase all the points by 1.5 times when the map is used on a touch-enabled device 168 | * linesScalingFactor: isTouchDevice ? 1.5 : 1 169 | * ``` 170 | */ 171 | pointsScalingFactor: number; 172 | 173 | /** 174 | * A factor by which all the default lines' dimensions should be increased. The value is passed as is to the 175 | * {@link layersFactory|`layersFactory`}'s second argument. 176 | * 177 | * __Note__ that the option has no effect on the snaplines. 178 | * 179 | * __Note__ that the option has no effect when the `layers` option is provided. 180 | * 181 | * @default `1` 182 | * 183 | * @example 184 | * ``` 185 | * // Increase all the lines by 2 times when the map is used on a touch-enabled device 186 | * linesScalingFactor: isTouchDevice ? 2 : 1 187 | * ``` 188 | */ 189 | linesScalingFactor: number; 190 | 191 | /** 192 | * IDs of the layers that are used to represent the waypoints which should be interactive. 193 | * 194 | * @default `["maplibre-gl-directions-waypoint", "maplibre-gl-directions-waypoint-casing"]` 195 | * 196 | * @example 197 | * ``` 198 | * sensitiveSnappointLayers: [ 199 | * "maplibre-gl-directions-waypoint", 200 | * "maplibre-gl-directions-waypoint-casing", 201 | * "my-custom-waypoint-layer" 202 | * ] 203 | * ``` 204 | */ 205 | sensitiveWaypointLayers: string[]; 206 | 207 | /** 208 | * IDs of the layers that are used to represent the snappoints which should be interactive. 209 | * 210 | * @default `["maplibre-gl-directions-snappoint", "maplibre-gl-directions-snappoint-casing"]` 211 | * 212 | * @example 213 | * ``` 214 | * sensitiveSnappointLayers: [ 215 | * "maplibre-gl-directions-snappoint", 216 | * "maplibre-gl-directions-snappoint-casing", 217 | * "my-custom-snappoint-layer" 218 | * ] 219 | * ``` 220 | */ 221 | sensitiveSnappointLayers: string[]; 222 | 223 | /** 224 | * IDs of the layers that are used to represent the selected route line which should be interactive. 225 | * 226 | * @default `["maplibre-gl-directions-routeline", "maplibre-gl-directions-routeline-casing"]` 227 | * 228 | * @example 229 | * ``` 230 | * sensitiveRoutelineLayers: [ 231 | * "maplibre-gl-directions-routeline", 232 | * "maplibre-gl-directions-routeline-casing", 233 | * "my-custom-routeline-layer" 234 | * ] 235 | * ``` 236 | */ 237 | sensitiveRoutelineLayers: string[]; 238 | 239 | /** 240 | * IDs of the layers that are used to represent the alternative route lines which should be interactive. 241 | * 242 | * @default `["maplibre-gl-directions-alt-routeline", "maplibre-gl-directions-alt-routeline-casing"]` 243 | * 244 | * @example 245 | * ``` 246 | * sensitiveAltRoutelineLayers: [ 247 | * "maplibre-gl-directions-alt-routeline", 248 | * "maplibre-gl-directions-alt-routeline-casing", 249 | * "my-custom-alt-routeline-layer" 250 | * ] 251 | * ``` 252 | */ 253 | sensitiveAltRoutelineLayers: string[]; 254 | 255 | /** 256 | * A minimal amount of pixels a waypoint or the hoverpoint must be dragged in order for the drag-event to be 257 | * respected, and for network requests to be made when using {@link refreshOnMove|`refreshOnMove: true`}. Should be a number >= `0`. 258 | * Any negative value is treated as `0`. 259 | * 260 | * @default `10` 261 | * 262 | * @example 263 | * ``` 264 | * // Don't respect drag-events where a point was dragged for less than 5px away from its initial location 265 | * dragThreshold: 5 266 | * ``` 267 | */ 268 | dragThreshold: number; 269 | 270 | /** 271 | * Whether to update a route while dragging a waypoint/hoverpoint instead of only when dropping it 272 | * 273 | * @default `false` 274 | * 275 | * @example 276 | * ``` 277 | * // make the route update while dragging 278 | * refreshOnMove: true 279 | * ``` 280 | */ 281 | refreshOnMove: boolean; 282 | 283 | /** 284 | * Whether to support waypoints' {@link https://docs.mapbox.com/api/navigation/directions/#optional-parameters|bearings}. 285 | * 286 | * @see {@link http://project-osrm.org/docs/v5.24.0/api/#requests|OSRM #Requests} 287 | * @see {@link https://docs.mapbox.com/api/navigation/directions/#optional-parameters|Mapbox Direction API #Optional parameters} 288 | * 289 | * @default `false` 290 | * 291 | * @example 292 | * ``` 293 | * // enable the bearings support 294 | * bearings: true 295 | * ``` 296 | */ 297 | bearings: boolean; 298 | } 299 | 300 | export const MapLibreGlDirectionsDefaultConfiguration: Omit = { 301 | api: "https://router.project-osrm.org/route/v1", 302 | profile: "driving", 303 | requestOptions: {}, 304 | requestTimeout: null, // can't use Infinity here because of this: https://github.com/denysdovhan/wtfjs/issues/61#issuecomment-325321753 305 | makePostRequest: false, 306 | sourceName: "maplibre-gl-directions", 307 | pointsScalingFactor: 1, 308 | linesScalingFactor: 1, 309 | sensitiveWaypointLayers: ["maplibre-gl-directions-waypoint", "maplibre-gl-directions-waypoint-casing"], 310 | sensitiveSnappointLayers: ["maplibre-gl-directions-snappoint", "maplibre-gl-directions-snappoint-casing"], 311 | sensitiveRoutelineLayers: ["maplibre-gl-directions-routeline", "maplibre-gl-directions-routeline-casing"], 312 | sensitiveAltRoutelineLayers: ["maplibre-gl-directions-alt-routeline", "maplibre-gl-directions-alt-routeline-casing"], 313 | dragThreshold: 10, 314 | refreshOnMove: false, 315 | bearings: false, 316 | }; 317 | 318 | export type PointType = "WAYPOINT" | "SNAPPOINT" | "HOVERPOINT" | string; 319 | 320 | // server response. Only the necessary for the plugin fields 321 | 322 | export interface Directions { 323 | code: "Ok" | string; 324 | message?: string; 325 | routes: Route[]; 326 | waypoints: Snappoint[]; 327 | } 328 | 329 | export type Geometry = PolylineGeometry | GeoJSONGeometry; 330 | export type GeoJSONGeometry = { 331 | coordinates: [number, number][]; 332 | }; 333 | export type PolylineGeometry = string; 334 | 335 | export interface Route { 336 | [P: string]: unknown; 337 | geometry: Geometry; 338 | legs: Leg[]; 339 | } 340 | 341 | export interface Leg { 342 | [P: string]: unknown; 343 | annotation?: { 344 | congestion?: ("unknown" | "low" | "moderate" | "heavy" | "severe")[]; 345 | congestion_numeric?: (number | null)[]; 346 | }; 347 | } 348 | 349 | export interface Snappoint { 350 | [P: string]: unknown; 351 | location: [number, number]; 352 | } 353 | -------------------------------------------------------------------------------- /src/directions/utils.ts: -------------------------------------------------------------------------------- 1 | import type { MapLibreGlDirectionsConfiguration, PointType, Route } from "./types"; 2 | import type { Feature, LineString, Point } from "geojson"; 3 | import { MapLibreGlDirectionsDefaultConfiguration } from "./types"; 4 | import layersFactory from "./layers"; 5 | import { nanoid } from "nanoid"; 6 | import { congestionLevelDecoder, coordinatesComparator, geometryDecoder } from "./helpers"; 7 | 8 | /** 9 | * @protected 10 | * 11 | * Takes a missing or an incomplete {@link MapLibreGlDirectionsConfiguration|configuration object}, augments it with the 12 | * default values and returns the complete configuration object. 13 | */ 14 | export function buildConfiguration( 15 | customConfiguration?: Partial, 16 | ): MapLibreGlDirectionsConfiguration { 17 | const layers = layersFactory( 18 | customConfiguration?.pointsScalingFactor, 19 | customConfiguration?.linesScalingFactor, 20 | customConfiguration?.sourceName, 21 | ); 22 | return Object.assign({}, MapLibreGlDirectionsDefaultConfiguration, { layers }, customConfiguration); 23 | } 24 | 25 | export type RequestData = { 26 | method: "get" | "post"; 27 | url: string; 28 | payload: URLSearchParams; 29 | }; 30 | 31 | /** 32 | * @protected 33 | * 34 | * Builds the routing-request method, URL and payload based on the provided 35 | * {@link MapLibreGlDirectionsConfiguration|configuration} and the waypoints' coordinates. 36 | */ 37 | export function buildRequest( 38 | configuration: MapLibreGlDirectionsConfiguration, 39 | waypointsCoordinates: [number, number][], 40 | waypointsBearings?: ([number, number] | undefined)[], 41 | ): RequestData { 42 | const method = configuration.makePostRequest ? "post" : "get"; 43 | 44 | let url: string; 45 | let payload: URLSearchParams; 46 | 47 | if (method === "get") { 48 | url = `${configuration.api}/${configuration.profile}/${waypointsCoordinates.join(";")}`; 49 | payload = new URLSearchParams(configuration.requestOptions as Record); 50 | } else { 51 | url = `${configuration.api}/${configuration.profile}${ 52 | configuration.requestOptions.access_token ? `?access_token=${configuration.requestOptions.access_token}` : "" 53 | }`; 54 | 55 | const formData = new FormData(); 56 | 57 | Object.entries(configuration.requestOptions as Record).forEach(([key, value]) => { 58 | if (key !== "access_token") { 59 | formData.set(key, value); 60 | } 61 | }); 62 | 63 | formData.set("coordinates", waypointsCoordinates.join(";")); 64 | 65 | // the URLSearchParams constructor works perfectly fine with FormData, so ignore the TypeScript's complaint 66 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 67 | // @ts-ignore 68 | payload = new URLSearchParams(formData); 69 | } 70 | 71 | if (configuration.bearings && waypointsBearings) { 72 | payload.set( 73 | "bearings", 74 | waypointsBearings 75 | .map((waypointBearing) => { 76 | if (waypointBearing) { 77 | return `${waypointBearing[0]},${waypointBearing[1]}`; 78 | } else { 79 | return ""; 80 | } 81 | }) 82 | .join(";"), 83 | ); 84 | } 85 | 86 | return { 87 | method, 88 | url, 89 | payload, 90 | }; 91 | } 92 | 93 | /** 94 | * @protected 95 | * 96 | * Creates a {@link Feature|GeoJSON Point Feature} of one of the ${@link PointType|known types} with a given 97 | * coordinate. 98 | */ 99 | export function buildPoint( 100 | coordinate: [number, number], 101 | type: PointType, 102 | properties?: Record, 103 | ): Feature { 104 | return { 105 | type: "Feature", 106 | geometry: { 107 | type: "Point", 108 | coordinates: coordinate, 109 | }, 110 | properties: { 111 | type, 112 | id: nanoid(), 113 | ...(properties ?? {}), 114 | }, 115 | }; 116 | } 117 | 118 | /** 119 | * @protected 120 | * 121 | * Creates a ${@link Feature|GeoJSON LineString Features} array where each feature represents a 122 | * line connecting a waypoint with its respective snappoint and the hoverpoint with its respective snappoints. 123 | */ 124 | export function buildSnaplines( 125 | waypointsCoordinates: [number, number][], 126 | snappointsCoordinates: [number, number][], 127 | hoverpointCoordinates: [number, number] | undefined, 128 | departSnappointIndex: number, // might be -1 129 | showHoverpointSnaplines = false, 130 | ): Feature[] { 131 | if (waypointsCoordinates.length !== snappointsCoordinates.length) return []; 132 | 133 | const snaplines = waypointsCoordinates.map((waypointCoordinates, index) => { 134 | return { 135 | type: "Feature", 136 | geometry: { 137 | type: "LineString", 138 | coordinates: [ 139 | [waypointCoordinates[0], waypointCoordinates[1]], 140 | [snappointsCoordinates[index][0], snappointsCoordinates[index][1]], 141 | ], 142 | }, 143 | properties: { 144 | type: "SNAPLINE", 145 | }, 146 | } as Feature; 147 | }); 148 | 149 | if (~departSnappointIndex && hoverpointCoordinates !== undefined && showHoverpointSnaplines) { 150 | snaplines.push({ 151 | type: "Feature", 152 | geometry: { 153 | type: "LineString", 154 | coordinates: [ 155 | [hoverpointCoordinates[0], hoverpointCoordinates[1]], 156 | [snappointsCoordinates[departSnappointIndex][0], snappointsCoordinates[departSnappointIndex][1]], 157 | ], 158 | }, 159 | properties: { 160 | type: "SNAPLINE", 161 | }, 162 | }); 163 | 164 | snaplines.push({ 165 | type: "Feature", 166 | geometry: { 167 | type: "LineString", 168 | coordinates: [ 169 | [hoverpointCoordinates[0], hoverpointCoordinates[1]], 170 | [snappointsCoordinates[departSnappointIndex + 1][0], snappointsCoordinates[departSnappointIndex + 1][1]], 171 | ], 172 | }, 173 | properties: { 174 | type: "SNAPLINE", 175 | }, 176 | }); 177 | } 178 | 179 | return snaplines; 180 | } 181 | 182 | /** 183 | * @protected 184 | * 185 | * Creates route lines from the server response. 186 | * 187 | * Each route line is an array of legs, where each leg is an array of segments. A segment is a 188 | * {@link Feature|GeoJSON LineString Feature}. Route legs are divided into segments by their congestion 189 | * levels. If there's no congestions, each route leg consists of a single segment. 190 | */ 191 | export function buildRoutelines( 192 | requestOptions: MapLibreGlDirectionsConfiguration["requestOptions"], 193 | routes: Route[], 194 | selectedRouteIndex: number, 195 | snappoints: Feature[], 196 | ): Feature[][] { 197 | // do the following stuff for each route (there are multiple when `alternatives=true` request option is set) 198 | return routes.map((route, routeIndex) => { 199 | // a list of coordinates pairs (longitude-latitude) the route goes by 200 | const coordinates = geometryDecoder(requestOptions, route.geometry); 201 | 202 | // get coordinates from the snappoint-features 203 | const snappointsCoordinates = snappoints.map((snappoint) => snappoint.geometry.coordinates); 204 | 205 | // add a variable to watch the current index to start the search from 206 | let currentIndex = 0; 207 | 208 | // indices of coordinate pairs that match existing snappoints (except for the first one) 209 | const snappointsCoordinatesIndices = snappointsCoordinates 210 | .map((snappointLngLat, index) => { 211 | // use the currentIndex to start the search from the place where the last snappoint's coordinate was found 212 | const waypointCoordinatesIndex = coordinates.slice(currentIndex).findIndex((lngLat) => { 213 | // there might be an error in 0.00001 degree between snappoint and decoded coordinate when using the 214 | // "polyline" geometries. The comparator neglects that 215 | return coordinatesComparator(requestOptions, lngLat, snappointLngLat as [number, number]); 216 | }); 217 | 218 | const isLast = index === snappointsCoordinates.length - 1; 219 | 220 | // update the current index if something's found 221 | if (waypointCoordinatesIndex !== -1) { 222 | currentIndex += waypointCoordinatesIndex; 223 | } else if (isLast) { 224 | return coordinates.length - 1; 225 | } 226 | 227 | return currentIndex; 228 | }) 229 | .slice(1); // because the first one is always 0 (first leg always starts with the first snappoint) 230 | 231 | // split the coordinates array by legs. Each leg consists of coordinates between snappoints 232 | let initialIndex = 0; 233 | const legsCoordinates = snappointsCoordinatesIndices.map((waypointCoordinatesIndex) => { 234 | return coordinates.slice(initialIndex, (initialIndex = waypointCoordinatesIndex + 1)); 235 | }); 236 | 237 | // an array to store the resulting route's features in 238 | const features: Feature[] = []; 239 | 240 | legsCoordinates.forEach((legCoordinates, legIndex) => { 241 | const legId = nanoid(); 242 | 243 | // for each pair of leg's coordinates 244 | legCoordinates.forEach((lngLat, i) => { 245 | // find the previous segment 246 | const previousSegment = features[features.length - 1]; 247 | // determine the current segment's congestion level 248 | const segmentCongestion = congestionLevelDecoder(requestOptions, route.legs[legIndex]?.annotation, i); 249 | 250 | // only allow to continue the previous segment if it exists and if it's the same leg and if it's the same 251 | // congestion level 252 | if ( 253 | legIndex === previousSegment?.properties?.legIndex && 254 | previousSegment.properties?.congestion === segmentCongestion 255 | ) { 256 | previousSegment.geometry.coordinates.push(lngLat); 257 | } else { 258 | const departSnappointProperties = snappoints[legIndex].properties ?? {}; 259 | const arriveSnappointProperties = snappoints[legIndex + 1].properties ?? {}; 260 | 261 | const segment = { 262 | type: "Feature", 263 | geometry: { 264 | type: "LineString", 265 | coordinates: [], 266 | }, 267 | properties: { 268 | id: legId, // used to highlight the whole leg when hovered, not a single segment 269 | routeIndex, // used to switch between alternative and selected routes 270 | route: routeIndex === selectedRouteIndex ? "SELECTED" : "ALT", 271 | legIndex, // used across forEach iterations to check whether it's safe to continue a segment 272 | congestion: segmentCongestion, // the current segment's congestion level 273 | departSnappointProperties, // include depart and arrive snappoints' properties to allow customization... 274 | arriveSnappointProperties, // ...of behavior via a subclass 275 | }, 276 | } as Feature; 277 | 278 | // a new segment starts with previous segment's last coordinate 279 | if (previousSegment) { 280 | segment.geometry.coordinates.push( 281 | previousSegment.geometry.coordinates[previousSegment.geometry.coordinates.length - 1], 282 | ); 283 | } 284 | 285 | segment.geometry.coordinates.push(lngLat); 286 | 287 | features.push(segment); 288 | } 289 | }); 290 | }); 291 | 292 | return features; 293 | }); 294 | } 295 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import MapLibreGlDirections from "./directions/main"; 2 | import type { 3 | MapLibreGlDirectionsConfiguration, 4 | PointType, 5 | Directions, 6 | Route, 7 | Leg, 8 | Snappoint, 9 | } from "./directions/types"; 10 | import { 11 | type MapLibreGlDirectionsEventType, 12 | MapLibreGlDirectionsWaypointEvent, 13 | type MapLibreGlDirectionsWaypointEventData, 14 | MapLibreGlDirectionsRoutingEvent, 15 | type MapLibreGlDirectionsRoutingEventData, 16 | } from "./directions/events"; 17 | import layersFactory from "./directions/layers"; 18 | import type { LayerSpecification, MapMouseEvent, MapTouchEvent } from "maplibre-gl"; 19 | import * as utils from "./directions/utils"; 20 | import type { Feature, Point, LineString } from "geojson"; 21 | 22 | import LoadingIndicatorControl from "./controls/loading-indicator/main"; 23 | import type { LoadingIndicatorControlConfiguration } from "./controls/loading-indicator/types"; 24 | import BearingsControl from "./controls/bearings/main"; 25 | import type { BearingsControlConfiguration } from "./controls/bearings/types"; 26 | import "./controls/common.css"; 27 | 28 | export default MapLibreGlDirections; 29 | export type { MapLibreGlDirectionsConfiguration }; 30 | export type { MapLibreGlDirectionsEventType }; 31 | export { layersFactory }; 32 | 33 | /** 34 | * @protected 35 | */ 36 | export type { 37 | Directions, 38 | Route, 39 | Leg, 40 | Snappoint, 41 | MapLibreGlDirectionsWaypointEventData, 42 | MapLibreGlDirectionsRoutingEventData, 43 | }; 44 | 45 | /** 46 | * @protected 47 | */ 48 | export { MapLibreGlDirectionsWaypointEvent, MapLibreGlDirectionsRoutingEvent }; 49 | 50 | /** 51 | * @protected 52 | * @see {@link https://maplibre.org/maplibre-gl-js-docs/style-spec/layers/|Layers | Style Specification} 53 | */ 54 | export type { LayerSpecification }; 55 | 56 | /** 57 | * @protected 58 | */ 59 | export type { MapMouseEvent, MapTouchEvent }; 60 | 61 | /** 62 | * @protected 63 | */ 64 | export { utils }; 65 | /** 66 | * @protected 67 | */ 68 | export type { Feature, Point, PointType, LineString }; 69 | 70 | export { LoadingIndicatorControl }; 71 | export type { LoadingIndicatorControlConfiguration }; 72 | 73 | export { BearingsControl }; 74 | export type { BearingsControlConfiguration }; 75 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.lib.json" 3 | } 4 | -------------------------------------------------------------------------------- /svelte.config.cjs: -------------------------------------------------------------------------------- 1 | const sveltePreprocess = require("svelte-preprocess"); 2 | 3 | module.exports = { 4 | // consult https://github.com/sveltejs/svelte-preprocess for more information about preprocessors 5 | preprocess: [ 6 | sveltePreprocess({ 7 | typescript: true, 8 | postcss: true, 9 | }), 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const defaultTheme = require("tailwindcss/defaultTheme"); 2 | 3 | module.exports = { 4 | content: ["./demo/index.html", "./demo/**/*.svelte", "./src/controls/**/*.svelte"], 5 | theme: { 6 | extend: { 7 | fontFamily: { 8 | sans: ["Noto Sans", ...defaultTheme.fontFamily.sans], 9 | }, 10 | colors: { 11 | accent: { 12 | 400: "#7b32e7", 13 | 500: "#6d26d7", 14 | 600: "#6127b7", 15 | }, 16 | }, 17 | }, 18 | }, 19 | plugins: [ 20 | require("@tailwindcss/forms")({ 21 | strategy: "base", 22 | }), 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "strict": true, 6 | "target": "esnext", 7 | "useDefineForClassFields": true, 8 | "module": "esnext", 9 | "resolveJsonModule": true, 10 | "baseUrl": ".", 11 | "allowJs": true, 12 | "checkJs": true, 13 | "isolatedModules": true, 14 | "paths": { 15 | "@placemarkio/polyline": ["node_modules/@placemarkio/polyline/dist/index.d.ts"] 16 | }, 17 | "types": [] 18 | }, 19 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.svelte", "demo/**/*.d.ts", "demo/**/*.ts", "demo/**/*.svelte"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "strict": true, 8 | "target": "esnext", 9 | "useDefineForClassFields": true, 10 | "composite": true, 11 | "module": "esnext", 12 | "resolveJsonModule": true, 13 | "baseUrl": ".", 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true, 17 | "paths": { 18 | "@placemarkio/polyline": ["node_modules/@placemarkio/polyline/dist/index.d.ts"] 19 | }, 20 | "types": [] 21 | }, 22 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.svelte"], 23 | "references": [ 24 | { 25 | "path": "./tsconfig.node.json" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "composite": true, 5 | "module": "esnext", 6 | "moduleResolution": "node" 7 | }, 8 | "include": ["vite.lib.config.ts", "vite.demo.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/main.ts"], 3 | "readme": "doc/MAIN.md", 4 | "includes": "doc", 5 | "out": "docs/api", 6 | "includeVersion": true, 7 | "githubPages": true 8 | } 9 | -------------------------------------------------------------------------------- /vite.demo.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 | import { resolve } from "path"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [svelte({ configFile: "../svelte.config.cjs" })], 8 | 9 | root: "demo", 10 | 11 | build: { 12 | outDir: "../docs", 13 | }, 14 | resolve: { 15 | alias: { 16 | src: resolve(__dirname, "./src"), 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /vite.lib.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 | import { visualizer } from "rollup-plugin-visualizer"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [svelte({ configFile: "svelte.config.cjs" }), visualizer()], 8 | 9 | build: { 10 | outDir: "dist", 11 | emptyOutDir: false, 12 | sourcemap: true, 13 | 14 | lib: { 15 | entry: "src/main.ts", 16 | formats: ["es", "cjs"], 17 | }, 18 | 19 | rollupOptions: { 20 | output: { 21 | // Because the plugin provides both the default and named exports. 22 | exports: "named", 23 | }, 24 | }, 25 | }, 26 | }); 27 | --------------------------------------------------------------------------------