├── .nvmrc ├── .husky ├── .gitignore └── pre-commit ├── src ├── controls │ ├── common.css │ ├── loading-indicator │ │ ├── types.ts │ │ ├── main.ts │ │ └── LoadingIndicatorControl.svelte │ └── bearings │ │ ├── main.ts │ │ ├── types.ts │ │ └── BearingsControl.svelte ├── tsconfig.json ├── env.d.ts ├── README.md ├── main.ts └── directions │ ├── helpers.ts │ ├── layers.ts │ ├── utils.ts │ ├── types.ts │ └── events.ts ├── .github ├── FUNDING.yml ├── release.yml ├── workflows │ ├── build-deploy-demo.yml │ ├── pull-request-checks.yml │ └── build-publish-lib.yml └── dependabot.yml ├── demo ├── env.d.ts ├── src │ ├── assets │ │ ├── map │ │ │ ├── images │ │ │ │ ├── routeline.png │ │ │ │ ├── balloon-snappoint.png │ │ │ │ ├── balloon-waypoint.png │ │ │ │ ├── direction-arrow.png │ │ │ │ └── balloon-hoverpoint.png │ │ │ ├── style │ │ │ │ ├── icons │ │ │ │ │ ├── star-11.svg │ │ │ │ │ └── circle-11.svg │ │ │ │ ├── .travis.yml │ │ │ │ └── README.md │ │ │ ├── custom-directions.ts │ │ │ ├── restyling-example-layers.ts │ │ │ └── distance-measurement-directions.ts │ │ └── styles │ │ │ └── index.css │ ├── main.ts │ ├── examples │ │ ├── README.md │ │ ├── 9 Loading Indicator Control.svelte │ │ ├── 8 Aborting Requests.svelte │ │ ├── 3 Mapbox Directions API and Congestions.svelte │ │ ├── 4 Origin and Destination.svelte │ │ ├── 5 Show Routes' Directions.svelte │ │ ├── 6 Touch-Friendly Features.svelte │ │ ├── 12 Distance Measurement.svelte │ │ ├── 2 Programmatical Control.svelte │ │ ├── 13 Load and Save.svelte │ │ ├── 1 User Interaction.svelte │ │ ├── 11 Restyling.svelte │ │ ├── 14 Multiple profiles.svelte │ │ ├── 10 Bearings Support and Control.svelte │ │ └── 7 Events.svelte │ ├── App.svelte │ ├── router.ts │ ├── components │ │ └── AppSidebar.svelte │ └── Menu.svelte ├── index.html └── README.md ├── doc ├── images │ ├── public-filter.png │ ├── demo-screenshot-1.png │ ├── demo-screenshot-2.png │ ├── demo-screenshot-3.png │ ├── complex-customization.png │ └── public-protected-filter.png ├── README.md ├── MAIN.md ├── BASIC_USAGE.md ├── CONTROLS.md └── CUSTOMIZATION.md ├── postcss.config.cjs ├── typedoc.json ├── CODE_OF_CONDUCT.md ├── tsconfig.node.json ├── .prettierignore ├── svelte.config.cjs ├── .gitignore ├── vite.demo.config.ts ├── tailwind.config.cjs ├── tsconfig.json ├── vite.lib.config.ts ├── tsconfig.lib.json ├── .eslintrc.cjs ├── LICENSE ├── README.md ├── package.json └── CONTRIBUTING.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /src/controls/common.css: -------------------------------------------------------------------------------- 1 | /* Don't strip out! */ 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [maplibre] 2 | open_collective: maplibre -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.lib.json" 3 | } 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /demo/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /doc/images/public-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/HEAD/doc/images/public-filter.png -------------------------------------------------------------------------------- /doc/images/demo-screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/HEAD/doc/images/demo-screenshot-1.png -------------------------------------------------------------------------------- /doc/images/demo-screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/HEAD/doc/images/demo-screenshot-2.png -------------------------------------------------------------------------------- /doc/images/demo-screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/HEAD/doc/images/demo-screenshot-3.png -------------------------------------------------------------------------------- /doc/images/complex-customization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/HEAD/doc/images/complex-customization.png -------------------------------------------------------------------------------- /doc/images/public-protected-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/HEAD/doc/images/public-protected-filter.png -------------------------------------------------------------------------------- /demo/src/assets/map/images/routeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/HEAD/demo/src/assets/map/images/routeline.png -------------------------------------------------------------------------------- /demo/src/assets/map/images/balloon-snappoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/HEAD/demo/src/assets/map/images/balloon-snappoint.png -------------------------------------------------------------------------------- /demo/src/assets/map/images/balloon-waypoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/HEAD/demo/src/assets/map/images/balloon-waypoint.png -------------------------------------------------------------------------------- /demo/src/assets/map/images/direction-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/HEAD/demo/src/assets/map/images/direction-arrow.png -------------------------------------------------------------------------------- /demo/src/assets/map/images/balloon-hoverpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/HEAD/demo/src/assets/map/images/balloon-hoverpoint.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require("tailwindcss"); 2 | const autoprefixer = require("autoprefixer"); 3 | 4 | module.exports = { 5 | plugins: [tailwindcss(), autoprefixer], 6 | }; 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/maplibre/maplibre/blob/main/CODE_OF_CONDUCT.md) 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/src/App.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore everything recursively... 2 | * 3 | 4 | # But not the .{ts,js,cjs,html,css,svelte,json,md} files 5 | !*.ts 6 | !*.js 7 | !*.cjs 8 | !*.html 9 | !*.css 10 | !*.svelte 11 | !*.json 12 | !*.md 13 | 14 | # But still ignore dist and docs 15 | dist/** 16 | docs/** 17 | 18 | # Check subdirectories too 19 | !*/ -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | docs 14 | stats.html 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MapLibreGlDirections Demo 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | This is the Demo project's source files. It's used for 2 purposes: 2 | 3 | 1. As a test-stand for manual testing when developing the plugin 4 | 2. As a demo-website for the demonstration purposes 5 | 6 | Serve it locally with `npm run dev:demo`. Check whether it builds correctly with `npm run build:demo`. 7 | 8 | The resulting static-website is put inside the `/docs` folder and is combined with the API documentation static-website automatically when the `main` branch is pushed. 9 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - "release:ignore" 5 | 6 | categories: 7 | - title: Major Updates (Breaking Changes ⚠) 8 | labels: 9 | - "semver:major" 10 | 11 | - title: Minor Updates (New Features 🎉 and other non-breaking API changes) 12 | labels: 13 | - "semver:minor" 14 | 15 | - title: Patches (Bug Fixes 🐛, refactoring, and other changes that don't affect the existing API) 16 | labels: 17 | - "semver:patch" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/src/assets/map/style/icons/star-11.svg: -------------------------------------------------------------------------------- 1 | star-11.svg -------------------------------------------------------------------------------- /demo/src/assets/map/style/icons/circle-11.svg: -------------------------------------------------------------------------------- 1 | circle-11.svg -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/build-deploy-demo.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy the Demo project 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build-and-deploy-demo: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🛎️ 12 | uses: actions/checkout@v3 13 | 14 | - name: Install and Build 🔧 15 | run: | 16 | npm ci 17 | npm link 18 | npm link @maplibre/maplibre-gl-directions 19 | npm run build 20 | 21 | - name: Deploy 🚀 22 | uses: JamesIves/github-pages-deploy-action@v4.3.0 23 | with: 24 | branch: gh-pages 25 | folder: docs -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | target-branch: "staging" 13 | open-pull-requests-limit: 20 14 | labels: 15 | - "dependencies" 16 | - "semver:patch" 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-checks.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Checks 2 | on: 3 | pull_request: 4 | types: [opened, labeled, unlabeled, synchronize] 5 | 6 | jobs: 7 | check-labels: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: mheap/github-action-required-labels@v1 11 | with: 12 | mode: exactly 13 | count: 1 14 | labels: "semver:major, semver:minor, semver:patch, release:ignore" 15 | 16 | check-build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 🛎️ 20 | uses: actions/checkout@v3 21 | 22 | - name: Use Node 16 x64 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: 16 26 | architecture: x64 27 | registry-url: "https://registry.npmjs.org" 28 | 29 | - name: Try to Build 30 | run: | 31 | npm ci 32 | npm link 33 | npm link @maplibre/maplibre-gl-directions 34 | npm run build -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 ` 9 | 10 | 28 | 29 | 39 | -------------------------------------------------------------------------------- /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/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 { MapLibreGlDirectionsNonCancelableEvent } 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 MapLibreGlDirectionsNonCancelableEvent("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/Menu.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | Examples 9 | 10 |
    11 | {#each examples.sort((a, b) => a.index - b.index) as example} 12 |
  • 13 | {example.name} 14 |
  • 15 | {/each} 16 |
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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/controls/loading-indicator/LoadingIndicatorControl.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | {#if loading} 20 | 28 | 29 | 34 | 35 | 36 | 41 | 46 | 54 | 55 | 56 | {/if} 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/build-publish-lib.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish the Library 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: Choose a release type 7 | required: true 8 | type: choice 9 | options: 10 | - patch 11 | - minor 12 | - major 13 | 14 | jobs: 15 | build-and-deploy-lib: 16 | runs-on: ubuntu-latest 17 | defaults: 18 | run: 19 | shell: bash 20 | 21 | steps: 22 | - name: Checkout 🛎️ 23 | uses: actions/checkout@v3 24 | with: 25 | token: ${{ secrets.GH_TOKEN }} 26 | 27 | - name: Use Node 16 x64 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 16 31 | architecture: x64 32 | registry-url: "https://registry.npmjs.org" 33 | 34 | - name: Install and Link 🔗 35 | run: | 36 | npm ci 37 | npm link 38 | npm link @maplibre/maplibre-gl-directions 39 | 40 | - name: Build 🔧 41 | run: | 42 | npm run build 43 | 44 | - name: Bump Version 45 | run: | 46 | git config user.name github-actions 47 | git config user.email github-actions@github.com 48 | npm version ${{ github.event.inputs.version }} --no-git-tag-version --tag-version-prefix=v -m "Update version (${{ github.event.inputs.version }})" 49 | git push && git push --tags 50 | 51 | - name: Get Version 52 | id: package-version 53 | uses: martinbeentjes/npm-get-version-action@v1.1.0 54 | 55 | - name: Publish to NPM 56 | run: | 57 | npm publish --access public --non-interactive 58 | env: 59 | NODE_AUTH_TOKEN: ${{ secrets.NPM_ORG_TOKEN }} 60 | 61 | - name: Create Archive 62 | run: | 63 | zip -r dist dist 64 | 65 | - name: Create GitHub Release 66 | id: create_github_release 67 | uses: ncipollo/release-action@v1.10.0 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | with: 71 | tag: v${{ steps.package-version.outputs.current-version }} 72 | name: v${{ steps.package-version.outputs.current-version }} 73 | generateReleaseNotes: true 74 | artifacts: "./dist.zip" 75 | artifactContentType: "application/zip" 76 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | type AnyMapLibreGlDirectionsEvent, 13 | type MapLibreGlDirectionsEventData, 14 | type MapLibreGlDirectionsAddWaypointData, 15 | type MapLibreGlDirectionsRemoveWaypointData, 16 | type MapLibreGlDirectionsMoveWaypointData, 17 | type MapLibreGlDirectionsCreateHoverpointData, 18 | type MapLibreGlDirectionsRoutingData, 19 | MapLibreGlDirectionsCancelableEvent, 20 | MapLibreGlDirectionsNonCancelableEvent, 21 | } from "./directions/events"; 22 | import layersFactory from "./directions/layers"; 23 | import type { LayerSpecification, MapMouseEvent, MapTouchEvent } from "maplibre-gl"; 24 | import * as utils from "./directions/utils"; 25 | import type { Feature, Point, LineString } from "geojson"; 26 | 27 | import LoadingIndicatorControl from "./controls/loading-indicator/main"; 28 | import type { LoadingIndicatorControlConfiguration } from "./controls/loading-indicator/types"; 29 | import BearingsControl from "./controls/bearings/main"; 30 | import type { BearingsControlConfiguration } from "./controls/bearings/types"; 31 | import "./controls/common.css"; 32 | 33 | export default MapLibreGlDirections; 34 | export type { MapLibreGlDirectionsConfiguration }; 35 | export type { MapLibreGlDirectionsEventType }; 36 | export { layersFactory }; 37 | 38 | /** 39 | * @protected 40 | */ 41 | export type { 42 | Directions, 43 | Route, 44 | Leg, 45 | Snappoint, 46 | AnyMapLibreGlDirectionsEvent, 47 | MapLibreGlDirectionsEventData, 48 | MapLibreGlDirectionsAddWaypointData, 49 | MapLibreGlDirectionsRemoveWaypointData, 50 | MapLibreGlDirectionsMoveWaypointData, 51 | MapLibreGlDirectionsCreateHoverpointData, 52 | MapLibreGlDirectionsRoutingData, 53 | }; 54 | 55 | /** 56 | * @protected 57 | */ 58 | export { MapLibreGlDirectionsCancelableEvent, MapLibreGlDirectionsNonCancelableEvent }; 59 | 60 | /** 61 | * @protected 62 | * @see {@link https://maplibre.org/maplibre-gl-js-docs/style-spec/layers/|Layers | Style Specification} 63 | */ 64 | export type { LayerSpecification }; 65 | 66 | /** 67 | * @protected 68 | */ 69 | export type { MapMouseEvent, MapTouchEvent }; 70 | 71 | /** 72 | * @protected 73 | */ 74 | export { utils }; 75 | /** 76 | * @protected 77 | */ 78 | export type { Feature, Point, PointType, LineString }; 79 | 80 | export { LoadingIndicatorControl }; 81 | export type { LoadingIndicatorControlConfiguration }; 82 | 83 | export { BearingsControl }; 84 | export type { BearingsControlConfiguration }; 85 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MapLibre GL Directions 2 | 3 | A plugin to show routing directions on a MapLibre GL JS map. Supports any [OSRM](http://project-osrm.org/) or [Mapbox Directions API](https://docs.mapbox.com/api/navigation/directions/) compatible Routing-provider. 4 | 5 | ![1st Demo Screenshot](https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/main/doc/images/demo-screenshot-1.png) 6 | ![2nd Demo Screenshot](https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/main/doc/images/demo-screenshot-2.png) 7 | ![3rd Demo Screenshot](https://raw.githubusercontent.com/maplibre/maplibre-gl-directions/main/doc/images/demo-screenshot-3.png) 8 | 9 | [Live Demo](https://maplibre.org/maplibre-gl-directions/#/). 10 | 11 | --- 12 | 13 | ## Features 14 | 15 | ### Different Routing-providers 16 | 17 | The plugin supports any OSRM- or Mapbox Directions API-compatible Routing-provider out of the box! 18 | 19 | ### Sane Defaults 20 | 21 | Works without any configuration at all out of the box, though at the same time configurable enough to support most of the imaginable scenarios. 22 | 23 | ### User interaction 24 | 25 | Add waypoints by clicking the map, click a waypoint to remove it, drag waypoints to move them, add waypoints in-between existing ones by dragging the selected route line, change the selected route by clicking an alternative route line or completely disable the user interaction with a single call. Everything is touch-friendly! 26 | 27 | ### Congestions 28 | 29 | Supports the Mapbox Directions API congestions (both plain and numeric!) 30 | 31 | ### Bearings 32 | 33 | Supports the waypoints' bearings settings with the help of a custom Control. 34 | 35 | ### Multiple routing profiles per single directions request 36 | 37 | Originally, backends (e.g. OSRM and Mapbox Directions API) don't support multiple routing profiles per single routing request. But the plugin overcomes the limitation, and it becomes possible to retrieve directions for a walk to a bus stop, then riding on a bus and then again walking from the next bus stop to the final destination all in one request! 38 | 39 | ### Customization 40 | 41 | The powerful customization interface allows to customize everything starting from visual aspects all the way up to request logic. 42 | 43 | ### Standard Controls 44 | 45 | Provides standard map-controls. Currently, there're only 2 of them (loading-indicator and bearings), but there are more to come. 46 | 47 | ### TypeScript support 48 | 49 | The plugin is written 100% in TypeScript and therefore ships with built-in types. 50 | 51 | ## Installation 52 | 53 | ```shell 54 | npm i @maplibre/maplibre-gl-directions 55 | ``` 56 | 57 | ## Usage 58 | 59 | ```typescript 60 | // Import the plugin 61 | import MapLibreGlDirections, { LoadingIndicatorControl } from "@maplibre/maplibre-gl-directions"; 62 | 63 | // Make sure to create a MapLibreGlDirections instance only after the map is loaded 64 | map.on("load", () => { 65 | // Create an instance of the default class 66 | const directions = new MapLibreGlDirections(map); 67 | 68 | // Enable interactivity (if needed) 69 | directions.interactive = true; 70 | 71 | // Optionally add the standard loading-indicator control 72 | map.addControl(new LoadingIndicatorControl(directions)); 73 | 74 | // Set the waypoints programmatically 75 | directions.setWaypoints([ 76 | [-73.8271025, 40.8032906], 77 | [-73.8671258, 40.82234996], 78 | ]); 79 | 80 | // Remove waypoints 81 | directions.removeWaypoint(0); 82 | 83 | // Add waypoints 84 | directions.addWaypoint([-73.8671258, 40.82234996], 0); 85 | 86 | // Remove everything plugin-related from the map 87 | directions.clear(); 88 | }); 89 | ``` 90 | 91 | Check out the [Demo](https://maplibre.org/maplibre-gl-directions/#/) or dive right into the [API Docs](https://maplibre.org/maplibre-gl-directions/api) for more! 92 | 93 | ## Future plans 94 | 95 | - Implement default control 96 | - Write tests 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@maplibre/maplibre-gl-directions", 3 | "version": "0.9.1", 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 | -------------------------------------------------------------------------------- /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/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/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/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/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/7 Events.svelte: -------------------------------------------------------------------------------- 1 | 134 | 135 | 136 | {meta.name} 137 | 138 |

139 | This example listens for all the available events and logs them below. Interact with the map to see the emitted 140 | events. 141 |

142 | 143 | 147 | 148 | {#if preventDefault} 149 | 153 | {/if} 154 | 155 | 156 | While the "Prevent Default" checkbox above is selected, all the subsequent cancelable events will have their default 157 | behavior prevented by calling the event's preventDefault() method. Such events will be displayed below 158 | as a strikethrough text. 159 | 160 | {#if preventDefault} 161 | Checking the "Force-allow adding waypoints" will make adding waypoints ignore its 162 | preventDefault() invocations 163 | {/if} 164 | 165 | 166 | {#if messages.length} 167 | 168 | {/if} 169 | 170 |
    171 | {#each messages as message} 172 |
  1. {@html message}
  2. 173 | {/each} 174 |
175 |
176 | 177 |
178 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | ## Prepare the Development Environment 4 | 5 | 1. Make sure you've got Node (v. ^16) and NPM (v. ^8) installed 6 | 2. Fork the repo 7 | 3. Clone the fork 8 | 4. Install the dependencies: `npm i` 9 | 5. Run `npm run env:prep` 10 | 6. Introduce some changes (see the section below) 11 | 7. Make sure the `npm run build` passes 12 | 8. Commit and push the changes 13 | 9. Create a PR 14 | 15 | _In case of any troubles, refer to the troubleshooting instructions at the bottom of this page._ 16 | 17 | The step 5 is a shorthand for `npm run build:lib && npm link && npm link @maplibre/maplibre-gl-directions` where the last two commands must be performed in order to have the `@maplibre/maplibre-gl-directions` as a local symlinked dependency, because the Demo project uses not the library sources, but the locally-built `/dist` folder to make sure that the instance being tested is the same instance which is deployed to the end user. 18 | 19 | ## Project Structure 20 | 21 | The source files of the library itself are located under the `/src` folder. The `/src/main.ts` is the main entry point. The `/src/directions` folder contains everything related to the plugin's main purpose. The `/src/control` folder contains all the UI-Control-related source code. 22 | 23 | TypeDoc is responsible for building all the API documentation. The API documentation is built from the library sources and from the files located under the `/doc` folder. 24 | 25 | The Demo project's source files are located under the `/demo` folder. See its README for more information. 26 | 27 | ## Making Changes 28 | 29 | _All the processes mentioned in this section are described in more detail in the section below._ 30 | 31 | Changes to the library sources should be introduced while there's a running `dev:lib` process. You may also want to have a running `dev:doc` process to see how correctly your doc-comments are parsed. Notice however that `dev:doc` does not serve the built documentation static-website somewhere, it just continuously rebuilds it. 32 | 33 | The Demo project might be used as a test-stand while working on the library sources. The `dev:demo` process serves the demo project at `localhost:3000` (by default). The Demo project uses the built `/dist` folder to refer to the library (instead of using sources from the `/src` directly), so you may also want to have a running `dev:lib` at the same time. 34 | 35 | You may keep any examples you add when creating a PR if you think the changes you were working on are worth a separate example. An additional example by itself (without modifying the library sources) is a good PR candidate as well. The only thing you should remember when introducing any changes to the Demo project is that they are about to be seen by any other person, a potential end-user, so don't go too crazy and provide human-readable explanations of what the example is really meant to show. 36 | 37 | ## NPM Scripts 38 | 39 | - `npm run prepare` - is run automatically when the project is being prepared (namely, when `npm i` is run). Configures the Husky's checks. 40 | 41 | - `npm run env:prep` - builds the library and self-links it for the Demo project. 42 | 43 | - `npm run dev:lib` - starts a vite-powered development server for the library source files. Continuously rebuilds the contents of the `/dist` folder while you make changes to the `/src` folder. **Does not rebuild types!** Must be restarted in order to rebuild them. 44 | 45 | - `npm run dev:doc` - continuously rebuilds the documentation static-website while you make changes to the library source files and puts the results under the `/docs/api`. 46 | 47 | - `npm run dev:demo` - starts a vite-powered development server for the Demo project. The Demo project targets the library from the `/dist` folder via a symlinked `@maplibre/maplibre-gl-directions` package. 48 | 49 | - `npm run build` - Combines `npm run lint`, `npm run build:lib`, `npm run build:doc` and `npm run build:demo` into a single call. 50 | 51 | 1. `npm run build:lib` - builds the library (the `/src` folder contents) and outputs the resulting es-module and its type declarations into the `/dist` folder. 52 | 53 | 2. `npm run build:doc` - builds the documentation (the `/doc` folder contents and the source code comments) using the TypeDoc compiler and outputs the resulting static-website into the `/docs/api` folder. 54 | 55 | 3. `npm run build:demo` - builds the Demo project (the `/demo` folder contents) and outputs the resulting static-website into the `/docs` folder. 56 | 57 | - `npm run format` - formats all the files that are not ignored by the `.prettierignore` and rewrites the files in-place. 58 | 59 | - `npm run prelint` - is run automatically each time when `npm run lint` is called. 60 | 61 | - `npm run lint` - lints and fixes the contents all the .ts, .js, .cjs and .svelte files and rewrites the files in-place. 62 | 63 | - `npm run check:lib` - checks the `/src` folder contents using the `svelte-check`. 64 | 65 | - `npm run check:demo` - checks the `/demo` folder contents using the `svelte-check`. 66 | 67 | - `npm run check` - combines `npm run lint`, `npm run check:lib` and `npm run check:demo` into a single call. Is run automatically if there were changes to the `/src` or `/demo` folders before you commit the changes and aborts the commit if there are errors that could not be automatically fixed by eslint. 68 | 69 | Since the deployment process is configured to be performed automatically, you don't have to make sure that the `/docs` folder is up-to-date (it's actually ignored by Git). You run `npm run build` manually only to make sure that the build doesn't fail. 70 | 71 | ## CI/CD Setup 72 | 73 | We use GitHub Actions for deployment. There are several tasks that get performed independently: 74 | 75 | - Build and Deploy the Demo project 76 | - Build and Publish the Library 77 | - Pull Request Checks 78 | 79 | ### Build and Deploy the Demo project 80 | 81 | Deploys the `/docs` folder to GitHub Pages each time the main branch is pushed (building the folder from sources beforehand). The GitHub Pages is configured in such a way so that the deployed site is actually available under the maplibre.org domain. Does not require any special access tokens. 82 | 83 | ### Build and Publish the Library 84 | 85 | Triggered manually whenever the team decides it's the time to release a new version of the library. Builds the sources from the target branch, runs all the checks, prepares the build-artifacts, updates the version in `package.json` (and automatically commits the changes), prepares the release notes and releases the new version to NPM all in one action. 86 | 87 | At the time of writing this ignores all the changes to the files made by `eslint --fix` and `prettier --write`, but there's a plan to fix that (#108). 88 | 89 | Requires a special access token (a secret) with slightly bumped permissions in order for github actions bot to be able to commit and push the updated `package.json` version of the library. The token is called "GH_TOKEN" and is a personal access token with the following permissions granted: admin:org, admin:org_hook, admin:public_key, admin:repo_hook, repo, workflow, write:packages. 90 | 91 | Also requires the "NPM_ORG_TOKEN" token which is an organization secret to be able to release the package under the "@maplibre" namespace. 92 | 93 | ### Pull Request Checks 94 | 95 | Whenever there's a new PR, ensures that: 96 | 97 | - The build passes 98 | - The PR has at least one of the following labels: either `release:ignore`, or `semver:patch`, or `semver:minor`, or `semver:major`. 99 | 100 | `semver-*` labels are used to indicate the backwards-compatibility of the proposed changes in order for GitHub to be able to automatically build the release notes. The `release:ignore` label should be used in cases when there's no need to mention the PR in release notes. 101 | 102 | Please, note that there's no protection from releasing a minor or a patch version when there was a `semver:major` PR merged. The maintainers should track the changes themselves. In general, it's a good idea to release new versions as soon as there are some changes available. 103 | 104 | ## Troubleshooting 105 | 106 | ### "Failed to resolve import "@maplibre/maplibre-gl-directions" from "demo/<...>". Does the file exist? 107 | 108 | That happens when the package is not self-symlinked. Perhaps you did `npm i` or installed some new dependencies (NPM removes all the symlinked deps after updating the `node_modules`). 109 | 110 | **Solution**: run `npm run env:prep` once again. 111 | 112 | ### `npm run build` fails without a meaningful reason 113 | 114 | This may happen if you have already had the project on your machine and recently pulled the upstream changes. Due to some package version updates the build may start to fail without any reasonable explanation of why is it so. 115 | 116 | **Solution**: run `npm ci` instead of `npm i` and try again. Another solution might be to completely delete the `node_modules` folder and reinstall the dependencies from scratch. 117 | -------------------------------------------------------------------------------- /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/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/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/events.ts: -------------------------------------------------------------------------------- 1 | import type { Map, MapMouseEvent, MapTouchEvent } from "maplibre-gl"; 2 | import type { Directions } from "./types"; 3 | 4 | /** 5 | * The base class that provides event functionality (`on`, `off`, `once` and `fire`). 6 | */ 7 | export class MapLibreGlDirectionsEvented { 8 | constructor(map: Map) { 9 | this.map = map; 10 | } 11 | 12 | protected readonly map: Map; 13 | 14 | private listeners: ListenersStore = {}; 15 | private oneTimeListeners: ListenersStore = {}; 16 | 17 | /** 18 | * Fires an event and notifies all listeners. 19 | * 20 | * @param event The event object to fire. 21 | * @returns `false` if the event's `preventDefault()` method was called, `true` otherwise. 22 | */ 23 | protected fire(event: MapLibreGlDirectionsEventType[T]): boolean { 24 | event.target = this.map; 25 | 26 | const type: T = event.type as T; 27 | 28 | // Fire one-time listeners. 29 | const oneTime = { ...this.oneTimeListeners[type] }; 30 | if (oneTime && oneTime.length > 0) { 31 | // Clear the original listeners map. 32 | this.oneTimeListeners[type] = []; 33 | 34 | // Fire all listeners that were in the list. 35 | oneTime.forEach((listener) => { 36 | listener(event); 37 | }); 38 | } 39 | 40 | // Fire persistent listeners. 41 | const persistent = this.listeners[type]; 42 | if (persistent) { 43 | // Iterate over a copy in case listeners remove themselves (`off`). 44 | [...persistent].forEach((listener) => { 45 | listener(event); 46 | }); 47 | } 48 | 49 | // Callers can check the method's result to act accordingly when it's needed to cancel some operation as a result of 50 | // default action being prevented. 51 | return !event.defaultPrevented; 52 | } 53 | 54 | /** 55 | * Registers an event listener. 56 | * 57 | * @param type The event type to listen for. 58 | * @param listener The listener function. 59 | * @returns `this` for method chaining. 60 | */ 61 | on(type: T, listener: MapLibreGlDirectionsEventListener) { 62 | this.listeners[type] = this.listeners[type] ?? []; 63 | this.listeners[type]!.push(listener); 64 | 65 | return this; 66 | } 67 | 68 | /** 69 | * Un-registers an event listener. 70 | * 71 | * @param type The event type. 72 | * @param listener The listener function to remove. 73 | * @returns `this` for method chaining. 74 | */ 75 | off(type: T, listener: MapLibreGlDirectionsEventListener) { 76 | const index = this.listeners[type]?.indexOf(listener); 77 | if (index !== undefined && index > -1) { 78 | this.listeners[type]?.splice(index, 1); 79 | } 80 | 81 | return this; 82 | } 83 | 84 | /** 85 | * Registers an event listener to be invoked only once. 86 | * 87 | * @param type The event type to listen for. 88 | * @param listener The listener function. 89 | * @returns `this` for method chaining. 90 | */ 91 | once(type: T, listener: MapLibreGlDirectionsEventListener) { 92 | this.oneTimeListeners[type] = this.oneTimeListeners[type] ?? []; 93 | this.oneTimeListeners[type]!.push(listener); 94 | 95 | return this; 96 | } 97 | } 98 | 99 | /** 100 | * Internal type for storing listeners. 101 | */ 102 | type ListenersStore = Partial<{ 103 | [T in keyof MapLibreGlDirectionsEventType]: MapLibreGlDirectionsEventListener[]; 104 | }>; 105 | 106 | /** 107 | * Defines the function signature for an event listener. 108 | */ 109 | export type MapLibreGlDirectionsEventListener = ( 110 | event: MapLibreGlDirectionsEventType[T], 111 | ) => void; 112 | 113 | /** 114 | * Base marker interface for all event data payloads. 115 | * Events with no data use this directly. 116 | */ 117 | export interface MapLibreGlDirectionsEventData { 118 | // Intentionally empty. This ensures a common base type without allowing `any`. 119 | } 120 | 121 | /** 122 | * Data payload for the `addwaypoint` and `beforeaddwaypoint` events. 123 | */ 124 | export interface MapLibreGlDirectionsAddWaypointData extends MapLibreGlDirectionsEventData { 125 | /** The index at which the waypoint was added. */ 126 | index: number; 127 | /** The added waypoint's coordinate. */ 128 | coordinates: [number, number]; 129 | } 130 | 131 | /** 132 | * Data payload for the `removewaypoint` and `beforeremovewaypoint` events. 133 | */ 134 | export interface MapLibreGlDirectionsRemoveWaypointData extends MapLibreGlDirectionsEventData { 135 | /** The index of the waypoint that was removed. */ 136 | index: number; 137 | } 138 | 139 | /** 140 | * Data payload for the `movewaypoint` and `beforemovewaypoint` events. 141 | */ 142 | export interface MapLibreGlDirectionsMoveWaypointData extends MapLibreGlDirectionsEventData { 143 | /** The index of the waypoint that was moved. */ 144 | index: number; 145 | /** The coordinates from which the waypoint was moved. */ 146 | initialCoordinates?: [number, number]; 147 | /** 148 | * The coordinates to which the waypoint was moved. 149 | * 150 | * Only present for the `movewaypoint` event. 151 | */ 152 | newCoordinates?: [number, number]; 153 | } 154 | 155 | /** 156 | * Data payload for the `beforecreatehoverpoint` event. 157 | */ 158 | export interface MapLibreGlDirectionsCreateHoverpointData extends MapLibreGlDirectionsEventData { 159 | /** 160 | * The index of the snappoint after which the hoverpoint is about to be added. The arrival index will be this plus 161 | * one. 162 | */ 163 | departSnappointIndex: number; 164 | } 165 | 166 | /** 167 | * Data payload for routing-related events. 168 | */ 169 | export interface MapLibreGlDirectionsRoutingData extends MapLibreGlDirectionsEventData { 170 | /** 171 | * The server's response. 172 | * 173 | * Only present for the `fetchroutesend` event, and even then might be `undefined` if the request has failed. 174 | */ 175 | directions?: Directions; 176 | } 177 | 178 | /** 179 | * The "any" event type exported to be used by clients when they need a type for a generic event listener. 180 | * 181 | * This is also used for the `originalEvent` property. 182 | */ 183 | export type AnyMapLibreGlDirectionsEvent = MapLibreGlDirectionsEvent< 184 | keyof MapLibreGlDirectionsEventType, 185 | MapLibreGlDirectionsEventData 186 | >; 187 | 188 | /** 189 | * The base event object, containing all common logic. 190 | * This is an abstract class, and it's not intended to be instantiated directly. 191 | * 192 | * @template T - The event type string (e.g., "addwaypoint"). 193 | * @template D - The data payload interface for this event type. 194 | */ 195 | export abstract class MapLibreGlDirectionsEvent< 196 | T extends keyof MapLibreGlDirectionsEventType, 197 | D extends MapLibreGlDirectionsEventData = MapLibreGlDirectionsEventData, 198 | > { 199 | /** 200 | * @private 201 | */ 202 | protected constructor( 203 | type: T, 204 | originalEvent: MapMouseEvent | MapTouchEvent | AnyMapLibreGlDirectionsEvent | undefined, 205 | data: D, 206 | cancelable: boolean, // Internal flag set by subclasses. 207 | ) { 208 | this.type = type; 209 | this.originalEvent = originalEvent; 210 | this.data = data; 211 | this._cancelable = cancelable; 212 | } 213 | 214 | readonly type: T; 215 | target!: Map; 216 | originalEvent?: MapMouseEvent | MapTouchEvent | AnyMapLibreGlDirectionsEvent; 217 | data: D; 218 | 219 | protected readonly _cancelable: boolean; 220 | protected _defaultPrevented: boolean = false; 221 | 222 | /** 223 | * Whether `preventDefault()` has been called on this event. 224 | * This is readable by the `fire` method. 225 | */ 226 | get defaultPrevented(): boolean { 227 | return this._defaultPrevented; 228 | } 229 | } 230 | 231 | /** 232 | * A non-cancelable event. 233 | * It does NOT have the `preventDefault()` method. 234 | */ 235 | export class MapLibreGlDirectionsNonCancelableEvent< 236 | T extends keyof MapLibreGlDirectionsEventType, 237 | D extends MapLibreGlDirectionsEventData = MapLibreGlDirectionsEventData, 238 | > extends MapLibreGlDirectionsEvent { 239 | /** 240 | * @private 241 | */ 242 | constructor( 243 | type: T, 244 | originalEvent: MapMouseEvent | MapTouchEvent | AnyMapLibreGlDirectionsEvent | undefined, 245 | data: D, 246 | ) { 247 | super(type, originalEvent, data, false); // Always pass `false`. 248 | } 249 | 250 | declare readonly _cancelable: false; 251 | } 252 | 253 | /** 254 | * A cancelable event. 255 | * `preventDefault()` will stop the default action. 256 | */ 257 | export class MapLibreGlDirectionsCancelableEvent< 258 | T extends keyof MapLibreGlDirectionsEventType, 259 | D extends MapLibreGlDirectionsEventData = MapLibreGlDirectionsEventData, 260 | > extends MapLibreGlDirectionsEvent { 261 | /** 262 | * @private 263 | */ 264 | constructor( 265 | type: T, 266 | originalEvent: MapMouseEvent | MapTouchEvent | AnyMapLibreGlDirectionsEvent | undefined, 267 | data: D, 268 | ) { 269 | super(type, originalEvent, data, true); // Always pass `true`. 270 | } 271 | 272 | declare readonly _cancelable: true; 273 | 274 | /** 275 | * Prevents the default action associated with this event. 276 | */ 277 | preventDefault() { 278 | // This sets the protected property on the base class. 279 | this._defaultPrevented = true; 280 | } 281 | } 282 | 283 | /** 284 | * A registry mapping all supported event type strings to their 285 | * corresponding event object type (Cancelable or NonCancelable). 286 | */ 287 | export interface MapLibreGlDirectionsEventType { 288 | /** 289 | * Fired *before* a waypoint is added. 290 | * 291 | * This event is **cancelable**. 292 | * 293 | * Fired from `_addWaypoint`. 294 | */ 295 | beforeaddwaypoint: MapLibreGlDirectionsCancelableEvent<"beforeaddwaypoint", MapLibreGlDirectionsAddWaypointData>; 296 | 297 | /** 298 | * Fired *after* a waypoint is added and drawn on the map, but before a new routes fetch has been triggered. 299 | * 300 | * This event is **not** cancelable. 301 | * 302 | * Fired from `_addWaypoint`. 303 | */ 304 | addwaypoint: MapLibreGlDirectionsNonCancelableEvent<"addwaypoint", MapLibreGlDirectionsAddWaypointData>; 305 | 306 | /** 307 | * Fired *before* a waypoint is removed. 308 | * 309 | * This event is **cancelable**. 310 | * 311 | * Fired from `_removeWaypoint`. 312 | */ 313 | beforeremovewaypoint: MapLibreGlDirectionsCancelableEvent< 314 | "beforeremovewaypoint", 315 | MapLibreGlDirectionsRemoveWaypointData 316 | >; 317 | 318 | /** 319 | * Fired *after* a waypoint is removed and the changes are drawn on the map, but before a new routes fetch has been 320 | * triggered. 321 | * 322 | * This event is **not** cancelable. 323 | * 324 | * Fired from `_removeWaypoint`. 325 | */ 326 | removewaypoint: MapLibreGlDirectionsNonCancelableEvent<"removewaypoint", MapLibreGlDirectionsRemoveWaypointData>; 327 | 328 | /** 329 | * Fired *before* a waypoint has been moved by dragging. 330 | * 331 | * This event is **cancelable**. 332 | * 333 | * Fired from `onDragDown`. 334 | */ 335 | beforemovewaypoint: MapLibreGlDirectionsCancelableEvent<"beforemovewaypoint", MapLibreGlDirectionsMoveWaypointData>; 336 | 337 | /** 338 | * Fired *after* a waypoint has been moved by dragging. 339 | * 340 | * This event is **not** cancelable. 341 | * 342 | * Fired from `onDragUp` and `liveRefresh`. 343 | */ 344 | movewaypoint: MapLibreGlDirectionsNonCancelableEvent<"movewaypoint", MapLibreGlDirectionsMoveWaypointData>; 345 | 346 | /** 347 | * Fired right *before* a hoverpoint is created after starting to drag a routeline. 348 | * 349 | * This event is **cancelable**. 350 | * 351 | * Fired from `onMove`. 352 | */ 353 | beforecreatehoverpoint: MapLibreGlDirectionsCancelableEvent< 354 | "beforecreatehoverpoint", 355 | MapLibreGlDirectionsCreateHoverpointData 356 | >; 357 | 358 | /** 359 | * Fired *after* waypoints are set programmatically. 360 | * 361 | * This event is **not** cancelable. 362 | * 363 | * Fired from `setWaypoints`. 364 | */ 365 | setwaypoints: MapLibreGlDirectionsNonCancelableEvent<"setwaypoints", MapLibreGlDirectionsEventData>; 366 | 367 | /** 368 | * Fired *after* waypoints' bearings are rotated. 369 | * 370 | * This event is **not** cancelable. 371 | * 372 | * Fired from `waypointsBearings` setter. 373 | */ 374 | rotatewaypoints: MapLibreGlDirectionsNonCancelableEvent<"rotatewaypoints", MapLibreGlDirectionsEventData>; 375 | 376 | /** 377 | * Fired when a routing request is about to be made. 378 | * 379 | * This event is **cancelable**. 380 | * 381 | * Fired from `fetchDirections`. 382 | */ 383 | fetchroutesstart: MapLibreGlDirectionsCancelableEvent<"fetchroutesstart", MapLibreGlDirectionsEventData>; 384 | 385 | /** 386 | * Fired *after* a routing request has finished (successfully or not). 387 | * 388 | * Check `event.data.directions` for the response. 389 | * 390 | * This event is **not** cancelable. 391 | * 392 | * Fired from `fetchDirections`. 393 | */ 394 | fetchroutesend: MapLibreGlDirectionsNonCancelableEvent<"fetchroutesend", MapLibreGlDirectionsRoutingData>; 395 | } 396 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------