├── .github ├── README.md ├── dependabot.yml └── workflows │ └── auto-merge.yml ├── .gitignore ├── LICENSE ├── README.md ├── SUMMARY.md ├── docs ├── examples.md ├── header.png ├── start.md └── styles.md ├── package.json ├── pnpm-lock.yaml ├── rollup.config.js ├── src ├── components │ ├── Atmosphere │ │ ├── README.md │ │ └── index.tsx │ ├── Camera │ │ ├── README.md │ │ └── index.tsx │ ├── Control │ │ ├── README.md │ │ └── index.tsx │ ├── Draw │ │ ├── README.md │ │ └── index.tsx │ ├── Image │ │ ├── README.md │ │ └── index.tsx │ ├── Layer │ │ ├── README.md │ │ └── index.tsx │ ├── Layer3D │ │ ├── README.md │ │ └── index.tsx │ ├── Light │ │ ├── README.md │ │ └── index.tsx │ ├── MapGL │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ ├── index.test.tsx │ │ └── index.tsx │ ├── MapProvider │ │ └── index.tsx │ ├── Marker │ │ ├── README.md │ │ └── index.tsx │ ├── Popup │ │ ├── README.md │ │ └── index.tsx │ ├── Source │ │ ├── README.md │ │ └── index.tsx │ └── Terrain │ │ ├── README.md │ │ ├── index.test.tsx │ │ └── index.tsx ├── events.ts ├── index.tsx ├── mapStyles.ts ├── styles.ts └── vitest.ts ├── tsconfig.json └── vite.config.ts /.github/README.md: -------------------------------------------------------------------------------- 1 | [![Banner](https://assets.solidjs.com/banner?project=solid-map-gl&background=tiles&type=Mapping%20Plugin)](https://gis-hub.gitbook.io/solid-map-gl) 2 | 3 | # **_Solid Map GL_** for Mapbox & MapLibre 4 | 5 | [![npm](https://img.shields.io/npm/v/solid-map-gl)](https://www.npmjs.com/package/solid-map-gl) 6 | [![downloads](https://img.shields.io/npm/dt/solid-map-gl)](https://www.npmjs.com/package/solid-map-gl) 7 | [![licence](https://img.shields.io/npm/l/solid-map-gl?color=blue)](LICENSE/) 8 | [![size](https://img.shields.io/bundlephobia/min/solid-map-gl)](https://bundlephobia.com/package/solid-map-gl) 9 | [![treeshaking](https://img.shields.io/badge/treeshaking-supported-success)](https://bundlephobia.com/package/solid-map-gl) 10 | ![ts](https://img.shields.io/badge/types-included-blue?logo=typescript&logoColor=white) 11 | 12 | [SolidJS](https://www.solidjs.com/) Component Library for [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) and [MapLibre GL.](https://maplibre.org/projects/maplibre-gl-js/) Both libraries render interactive maps from vector tiles and Map styles using WebGL. This project is intended to be as close as possible to the [Mapbox GL JS API.](https://docs.mapbox.com/mapbox-gl-js/api/) 13 | 14 | ## [Documentation & Examples](https://gis-hub.gitbook.io/solid-map-gl) 15 | 16 | [![Gallery](/docs/header.png)](https://gis-hub.gitbook.io/solid-map-gl) 17 | 18 | ## [Getting Started](https://gis-hub.gitbook.io/solid-map-gl/start) 19 | 20 | ### Installation 21 | 22 | ```shell 23 | pnpm add mapbox-gl solid-map-gl 24 | yarn add mapbox-gl solid-map-gl 25 | npm i mapbox-gl solid-map-gl 26 | ``` 27 | 28 | #### Use with [Solid Start](https://github.com/solidjs/solid-start) 29 | 30 | ```shell 31 | pnpm create solid && pnpm i 32 | pnpm add mapbox-gl solid-map-gl 33 | pnpm dev 34 | ``` 35 | 36 | ## [Components](https://gis-hub.gitbook.io/solid-map-gl/components) 37 | 38 | | Component | Description | 39 | | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | 40 | | [MapGL](https://gis-hub.gitbook.io/solid-map-gl/components/mapgl) | Represents map on the page | 41 | | [Source](https://gis-hub.gitbook.io/solid-map-gl/components/source) | [Sources](https://docs.mapbox.com/mapbox-gl-js/api/#sources) specify the geographic features to be rendered on the map | 42 | | [Layer](https://gis-hub.gitbook.io/solid-map-gl/components/layer) | [Layers](https://docs.mapbox.com/mapbox-gl-js/style-spec/#layers) specify the `Sources` | 43 | | [Layer3D](https://gis-hub.gitbook.io/solid-map-gl/components/layer3d) | Component for [BabylonJS](https://www.babylonjs.com/) or [ThreeJS](https://threejs.org/) | 44 | | [Atmosphere](https://gis-hub.gitbook.io/solid-map-gl/components/atmosphere) | Specify the Atmosphere | 45 | | [Light](https://gis-hub.gitbook.io/solid-map-gl/components/light) | Specify the Light Source | 46 | | [Terrain](https://gis-hub.gitbook.io/solid-map-gl/components/terrain) | Specify the Terrain | 47 | | [Image](https://gis-hub.gitbook.io/solid-map-gl/components/image) | Adds an image to the map style | 48 | | [Popup](https://gis-hub.gitbook.io/solid-map-gl/components/popup) | Component for [Mapbox GL JS Popup](https://docs.mapbox.com/mapbox-gl-js/api/#popup) | 49 | | [Marker](https://gis-hub.gitbook.io/solid-map-gl/components/marker) | Component for [Mapbox GL JS Marker](https://docs.mapbox.com/mapbox-gl-js/api/#marker) | 50 | | [Control](https://gis-hub.gitbook.io/solid-map-gl/components/control) | Represents the map's control | 51 | | [Camera](https://gis-hub.gitbook.io/solid-map-gl/components/camera) | Map's camera view | 52 | | [Draw](https://gis-hub.gitbook.io/solid-map-gl/components/draw) | Draw Control view | 53 | 54 | ## Usage with [Mapbox](https://docs.mapbox.com/mapbox-gl-js/guides/) 55 | 56 | Pass the _Mapbox access token_ via ` options` or `.env` file as `VITE_MAPBOX_ACCESS_TOKEN` 57 | 58 | ```jsx 59 | import { render } from "solid-js/web"; 60 | import { Component, createSignal } from "solid-js"; 61 | import MapGL, { Viewport, Source, Layer } from "solid-map-gl"; 62 | import 'mapbox-gl/dist/mapbox-gl.css' 63 | 64 | const App: Component = () => { 65 | const [viewport, setViewport] = createSignal({ 66 | center: [-122.45, 37.78], 67 | zoom: 11, 68 | } as Viewport); 69 | 70 | return ( 71 | setViewport(evt)} 75 | > 76 | 82 | 91 | 92 | 93 | ); 94 | }; 95 | 96 | render(() => , document.getElementById("app")!); 97 | ``` 98 | 99 | ## Usage with [MapLibre](https://maplibre.org/maplibre-gl-js-docs/api/) 100 | 101 | Install MapLibre package and placeholder Mapbox package 102 | 103 | ```shell 104 | pnpm add solid-map-gl maplibre-gl mapbox-gl@npm:empty-npm-package@1.0.0 105 | yarn add solid-map-gl maplibre-gl mapbox-gl@npm:empty-npm-package@1.0.0 106 | npm i solid-map-gl maplibre-gl mapbox-gl@npm:empty-npm-package@1.0.0 107 | ``` 108 | 109 | ```jsx 110 | import { render } from "solid-js/web"; 111 | import { Component, createSignal } from "solid-js"; 112 | import MapGL, { Viewport } from "solid-map-gl"; 113 | import * as maplibre from 'maplibre-gl' 114 | import 'maplibre-gl/dist/maplibre-gl.css' 115 | 116 | const App: Component = () => { 117 | const [viewport, setViewport] = createSignal({ 118 | center: [-122.45, 37.78], 119 | zoom: 11, 120 | } as Viewport); 121 | 122 | return ( 123 | setViewport(evt)} 128 | /> 129 | ); 130 | }; 131 | 132 | render(() => , document.getElementById("app")!); 133 | ``` 134 | 135 | ## Roadmap 136 | 137 | - [x] Basic Mapbox GL Functionality 138 | - [x] Include Map Controls 139 | - [x] Include Fog, Sky, and Terrain 140 | - [x] Include Popup and Markers 141 | - [x] Minify bundle & reduce size 142 | - [x] Add basemap switching 143 | - [x] Include event handling 144 | - [x] Sync Maps 145 | - [x] Add MapLibre support 146 | - [x] Add debug functionality 147 | - [x] Add draw functionality 148 | - [x] Add 3D library support 149 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | 6 | updates: 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | 12 | - package-ecosystem: "npm" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Auto approve and merge PRs by dependabot 2 | 3 | on: pull_request 4 | 5 | permissions: 6 | pull-requests: write 7 | 8 | jobs: 9 | automerge: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v1.3.5 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Approve dependabot PR 19 | run: gh pr review --approve "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | - name: Auto merge dependabot PR 24 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 25 | run: gh pr merge --auto --squash "$PR_URL" 26 | env: 27 | PR_URL: ${{github.event.pull_request.html_url}} 28 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # build 4 | /dist 5 | 6 | # dependencies 7 | /node_modules 8 | 9 | # misc 10 | .env 11 | .DS_Store 12 | .env.local 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | .idea 17 | Thumbs.db 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | yarn.lock 23 | 24 | pnpm-lock.yaml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 GIShub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | Solid Map GL provides Mapbox & MapLibre functionality within SolidJS 4 | applications 5 | cover: docs/docs/header.png 6 | coverY: -136.56381942189023 7 | layout: landing 8 | --- 9 | 10 | # Introduction 11 | 12 | [SolidJS](https://www.solidjs.com/) Component Library for [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) and [MapLibre GL.](https://maplibre.org/projects/maplibre-gl-js/) Both libraries render interactive maps from vector tiles and Map styles using WebGL. This project is intended to be as close as possible to the [Mapbox GL JS API.](https://docs.mapbox.com/mapbox-gl-js/api/) 13 | 14 | {% content-ref url="docs/start.md" %} 15 | [start.md](docs/start.md) 16 | {% endcontent-ref %} 17 | 18 | {% content-ref url="docs/examples.md" %} 19 | [examples.md](docs/examples.md) 20 | {% endcontent-ref %} 21 | 22 | ## Simple Demo 23 | 24 | {% embed url="https://stackblitz.com/edit/solid-map-gl-intro?embed=1&hideExplorer=1&hideNavigation=1&hidedevtools=1&view=preview&file=src%2FApp.tsx" %} 25 | 26 | #### Roadmap 27 | 28 | * [x] Basic Mapbox GL Functionality 29 | * [x] Include Map Controls 30 | * [x] Include Fog, Sky, and Terrain 31 | * [x] Include Popup and Markers 32 | * [x] Minify bundle & reduce size 33 | * [x] Add basemap switching 34 | * [x] Include event handling 35 | * [x] Sync Maps 36 | * [x] Add MapLibre support 37 | * [x] Add draw functionality 38 | * [x] Add 3D Layer support 39 | * [ ] Add debug console 40 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [Introduction](README.md) 4 | * [🚀 Getting Started](docs/start.md) 5 | 6 | ## Components 7 | 8 | * [MapGL](src/components/MapGL/README.md) 9 | * [Source](src/components/Source/README.md) 10 | * [Layer](src/components/Layer/README.md) 11 | * [Layer3D](src/components/Layer3D/README.md) 12 | * [Control](src/components/Control/README.md) 13 | * [Image](src/components/Image/README.md) 14 | * [Marker](src/components/Marker/README.md) 15 | * [Popup](src/components/Popup/README.md) 16 | * [Terrain](src/components/Terrain/README.md) 17 | * [Atmosphere](src/components/Atmosphere/README.md) 18 | * [Light](src/components/Light/README.md) 19 | * [Camera](src/components/Camera/README.md) 20 | * [Draw](src/components/Draw/README.md) 21 | 22 | *** 23 | 24 | * [🗺 Styles](docs/styles.md) 25 | * [⚙ Examples](docs/examples.md) 26 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: List of example links run in SolidJS playground 3 | --- 4 | 5 | # ⚙ Examples 6 | 7 | {% tabs %} 8 | {% tab title="Map" %} 9 | 10 | - [Static Map](https://stackblitz.com/edit/solid-map-gl-static?embed=1&file=src%2Findex.tsx) 11 | - [Interactive Map](https://stackblitz.com/edit/solid-map-gl-interactive?embed=1&file=src%2Findex.tsx) 12 | - [Change Vector 'Base Map'](https://stackblitz.com/edit/solid-map-gl-vector-base?embed=1&file=src%2Findex.tsx) 13 | - [Toggle Dark Theme Map](https://stackblitz.com/edit/solid-map-gl-dark-theme?embed=1&file=src%2Findex.tsx) 14 | - [Change Projection](https://stackblitz.com/edit/solid-map-gl-projection?embed=1&file=src%2Findex.tsx) 15 | - [Display Mouse Coordinates](https://stackblitz.com/edit/solid-map-gl-mouse?embed=1&file=src%2Findex.tsx) 16 | - [Sync Maps](https://stackblitz.com/edit/solid-map-gl-sync?embed=1&file=src%2Findex.tsx) 17 | - [Swipe Maps](https://stackblitz.com/edit/solid-map-gl-swipe?embed=1&file=src%2Findex.tsx) 18 | - [Map Context](https://stackblitz.com/edit/solid-map-gl-fit-bounds?embed=1&file=src%2Findex.tsx) 19 | - [Use MapLibre](https://stackblitz.com/edit/solid-map-gl-use-maplibre?embed=1&file=src%2Findex.tsx) 20 | - ~~Debug Maps~~ 21 | {% endtab %} 22 | 23 | {% tab title="Source" %} 24 | 25 | - [Add GeoJSON Source](https://stackblitz.com/edit/solid-map-gl-geojson?embed=1&file=src%2Findex.tsx) 26 | - [Add GeoJSON URL Source](https://stackblitz.com/edit/solid-map-gl-url?embed=1&file=src%2Findex.tsx) 27 | - [Updating GeoJSON Source](https://stackblitz.com/edit/solid-map-gl-update?embed=1&file=src%2Findex.tsx) 28 | - [Add Vector Source](https://stackblitz.com/edit/solid-map-gl-vector?embed=1&file=src%2Findex.tsx) 29 | - [Add Raster Source](https://stackblitz.com/edit/solid-map-gl-raster?embed=1&file=src%2Findex.tsx) 30 | - [Change Raster 'Base Map'](https://stackblitz.com/edit/solid-map-gl-toggle-raster?embed=1&file=src%2Findex.tsx) 31 | - [Add Image Source](https://stackblitz.com/edit/solid-map-gl-image-source?embed=1&file=src%2Findex.tsx) 32 | - [Add Image](https://stackblitz.com/edit/solid-map-gl-image?embed=1&file=src%2Findex.tsx) 33 | - [Add Video](https://stackblitz.com/edit/solid-map-gl-video?embed=1&file=src%2Findex.tsx) 34 | - [Use ProtoMaps](https://stackblitz.com/edit/solid-map-gl-protomaps?file=src%2Findex.tsx) 35 | {% endtab %} 36 | 37 | {% tab title="Layer" %} 38 | 39 | - [Add Heatmap](https://stackblitz.com/edit/solid-map-gl-heatmap?embed=1&file=src%2Findex.tsx) 40 | - [Add Custom Layer](https://stackblitz.com/edit/solid-map-gl-custom-layer?embed=1&file=src%2Findex.tsx) 41 | - [Dynamic Pattern](https://stackblitz.com/edit/solid-map-gl-pattern?embed=1&file=src%2Findex.tsx) 42 | {% endtab %} 43 | 44 | {% tab title="Overlay" %} 45 | 46 | - [Dynamic Controls / Traffic / Geocoder](https://stackblitz.com/edit/solid-map-gl-controls?embed=1&file=src%2Findex.tsx) 47 | - [Dynamic Marker / Popup](https://stackblitz.com/edit/solid-map-gl-marker-popup?embed=1&file=src/index.tsx) 48 | {% endtab %} 49 | 50 | {% tab title="3D" %} 51 | 52 | - [3D Overlays](https://stackblitz.com/edit/solid-map-gl-3d-overlay?embed=1&file=src%2Findex.tsx) 53 | - [Add Hillshade](https://stackblitz.com/edit/solid-map-gl-hillshade?embed=1&file=src%2Findex.tsx) 54 | - [Add Globe](https://stackblitz.com/edit/solid-map-gl-globe?embed=1&file=src%2Findex.tsx) 55 | - [Query Elevation](https://stackblitz.com/edit/solid-map-gl-query-elevation?embed=1&file=src%2Findex.tsx) 56 | - [Add 3D Layer with BabylonJS](https://stackblitz.com/edit/solid-map-gl-3d-babylon?embed=1&file=src%2Findex.tsx) 57 | - [Add 3D Layer with ThreeJS](https://stackblitz.com/edit/solid-map-gl-3d-three?embed=1&file=src%2Findex.tsx) 58 | 59 | - ~~Camera Transition~~ 60 | {% endtab %} 61 | {% endtabs %} 62 | -------------------------------------------------------------------------------- /docs/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GIShub4/solid-map-gl/fc803444ec0b52e1e9cfc9883e243b0d7c3003c1/docs/header.png -------------------------------------------------------------------------------- /docs/start.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Installation and basic usage 3 | --- 4 | 5 | # 🚀 Getting Started 6 | 7 | `solid-map-gl` requires `mapbox-gl` or `maplibre-gl` as peer dependency 8 | 9 | {% tabs %} 10 | {% tab title="Existing project" %} 11 | ```shell 12 | pnpm add mapbox-gl solid-map-gl 13 | yarn add mapbox-gl solid-map-gl 14 | npm i mapbox-gl solid-map-gl 15 | ``` 16 | {% endtab %} 17 | 18 | {% tab title="Solid Start" %} 19 | ```shell 20 | pnpm create solid && pnpm i 21 | pnpm add mapbox-gl solid-map-gl 22 | pnpm dev 23 | ``` 24 | {% endtab %} 25 | 26 | {% tab title="With MapLibre project" %} 27 | ```shell 28 | pnpm create solid && pnpm i 29 | # Install MapLibre package and placeholder Mapbox package 30 | pnpm add solid-map-gl maplibre-gl mapbox-gl@npm:empty-npm-package@1.0.0 31 | pnpm dev 32 | ``` 33 | {% endtab %} 34 | {% endtabs %} 35 | 36 | {% hint style="danger" %} 37 | If you use `vite` and get the following error: 38 | 39 | 'mapbox-gl.js' does not provide an export named 'default' 40 | 41 | add this to your `vite.config.ts` file: 42 | 43 | `optimizeDeps: {include: ['mapbox-gl']}` 44 | {% endhint %} 45 | 46 | ## Usage 47 | 48 | To use any of Mapbox’s tools, APIs, or SDKs, you’ll need a Mapbox [access token](https://www.mapbox.com/help/define-access-token/). Mapbox uses access tokens to associate requests to API resources with your account. You can find all your access tokens, create new ones, or delete existing ones on your [API access tokens page](https://www.mapbox.com/studio/account/tokens/). Then pass the *Mapbox access token* via ` options` or `.env` file as `VITE_MAPBOX_ACCESS_TOKEN` 49 | 50 | ### **Static Map** 51 | 52 | By default, `MapGL` component renders in a static mode. That means that the user cannot interact with the map. 53 | 54 | ```jsx 55 | import { Component } from "solid-js"; 56 | import MapGL from "solid-map-gl"; 57 | 58 | const App: Component = () => ( 59 | 69 | ); 70 | ``` 71 | 72 | ### **Interactive Map** 73 | 74 | In most cases, you will want the user to interact with the map. To do this, you need to provide `onViewportChange` handler, that will update the map's viewport state. 75 | 76 | ```jsx 77 | import { Component, createSignal } from "solid-js"; 78 | import MapGL, { Viewport } from "solid-map-gl"; 79 | 80 | const App: Component = () => { 81 | const [viewport, setViewport] = createSignal({ 82 | center: [-122.41, 37.78], 83 | zoom: 11, 84 | } as Viewport); 85 | 86 | return ( 87 | setViewport(evt)} 94 | > 95 | ); 96 | }; 97 | ``` 98 | -------------------------------------------------------------------------------- /docs/styles.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: List of available Map Styles 3 | --- 4 | 5 | # 🗺 Styles 6 | 7 | ## Vector Base Maps 8 | 9 | Additionally to the default [Mapbox referencing of map styles](https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#vector) you can use one of the following shortcuts. 10 | 11 | ```jsx 12 | 13 | ``` 14 | 15 | ### Mapbox 16 | 17 | {% tabs %} 18 | {% tab title="Base" %} 19 | 20 | - mb:standard 21 | - mb:light 22 | - mb:dark 23 | - mb:street 24 | - mb:outdoor 25 | - mb:sat 26 | - mb:sat_street 27 | - mb:nav 28 | - mb:nav_night 29 | - mb:nav_base 30 | - mb:nav_base_night 31 | {% endtab %} 32 | 33 | {% tab title="Map Design" %} 34 | 35 | - mb:basic 36 | - mb:monochrome 37 | - mb:leshine 38 | - mb:icecream 39 | - mb:cali 40 | - mb:northstar 41 | - mb:mineral 42 | - mb:moonlight 43 | - mb:frank 44 | - mb:minimo 45 | - mb:decimal 46 | - mb:stand 47 | - mb:blueprint 48 | - mb:bubble 49 | - mb:pencil 50 | {% endtab %} 51 | 52 | {% tab title="Community" %} 53 | 54 | - mb:swiss_ski 55 | - mb:vintage 56 | - mb:whaam 57 | - mb:neon 58 | - mb:camoflauge 59 | - mb:emerald 60 | - mb:runner 61 | - mb:x_ray 62 | {% endtab %} 63 | 64 | {% tab title="HERE Maps" %} 65 | 66 | - here:base 67 | - here:day 68 | - here:night 69 | {% endtab %} 70 | 71 | {% tab title="Esri" %} 72 | Please check if you can use [esri maps](https://doc.arcgis.com/en/arcgis-online/reference/terms-of-use.htm) here. 73 | 74 | - esri:blueprint 75 | - esri:charted_territory 76 | - esri:colored_pencil 77 | - esri:community 78 | - esri:mid_century 79 | - esri:modern_antique 80 | - esri:nat_geo 81 | - esri:newspaper 82 | - esri:open_street_map 83 | - esri:light_gray_canvas 84 | - esri:dark_gray_canvas 85 | - esri:human_geo_light 86 | - esri:human_geo_dark 87 | - esri:world_navigation 88 | - esri:world_street 89 | - esri:world_street_night 90 | - esri:world_terrain 91 | - esri:world_terrain_hybrid 92 | - esri:world_topographic 93 | - esri:chromium 94 | - esri:dreamcatcher 95 | - esri:seahaven 96 | - esri:sangria 97 | - esri:mercurial 98 | - esri:imagery 99 | - esri:imagery_hybrid 100 | - esri:firefly 101 | - esri:firefly_hybrid 102 | - esri:oceans 103 | {% endtab %} 104 | {% endtabs %} 105 | 106 | ## Raster Tile Base Maps 107 | 108 | Additionally to the default [Mapbox referencing of raster styles ](https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#raster)you can use one of the following shortcuts. 109 | 110 | ```jsx 111 | 118 | 123 | 124 | ``` 125 | 126 | {% tabs %} 127 | {% tab title="OSM" %} 128 | 129 | - osm:org 130 | - osm:human 131 | - osm:cycle 132 | - open:topo 133 | {% endtab %} 134 | 135 | {% tab title="Carto" %} 136 | 137 | - carto:voyager 138 | - carto:positron 139 | - carto:dark 140 | {% endtab %} 141 | 142 | {% tab title="Stamen" %} 143 | 144 | - stamen:toner 145 | - stamen:toner_lite 146 | - stamen:watercolor 147 | - stamen:terrain 148 | {% endtab %} 149 | 150 | {% tab title="Thunder Forest" %} 151 | To use these maps, get an API key from [Thunder Forest ](https://manage.thunderforest.com/dashboard)and add to the `Source` 152 | 153 | - tf:cycle 154 | - tf:trans 155 | - tf:trans_dark 156 | - tf:landscape 157 | - tf:outdoors 158 | - tf:neighbourhood 159 | - tf:spinal 160 | - tf:pioneer 161 | - tf:atlas 162 | - tf:mobile 163 | {% endtab %} 164 | {% endtabs %} 165 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.11.3", 3 | "name": "solid-map-gl", 4 | "description": "Solid Component Library for Mapbox GL JS", 5 | "info": "Solid Component Library for Mapbox GL JS. Mapbox GL JS is a JavaScript library that renders interactive maps from vector tiles and Mapbox styles using WebGL. This project is intended to be as close as possible to the Mapbox GL JS API.", 6 | "homepage": "https://gis-hub.gitbook.io/solid-map-gl", 7 | "author": "kaihuebner", 8 | "contributors": [ 9 | { 10 | "name": "Kai Huebner", 11 | "email": "kai.huebner@gmail.com" 12 | } 13 | ], 14 | "keywords": [ 15 | "gis", 16 | "map", 17 | "webgl", 18 | "mapbox-gl", 19 | "mapbox-gl-js", 20 | "mapbox", 21 | "maplibre", 22 | "solidjs" 23 | ], 24 | "license": "MIT", 25 | "type": "module", 26 | "files": [ 27 | "dist" 28 | ], 29 | "source": "dist/index/index.jsx", 30 | "main": "dist/esm/index.js", 31 | "module": "dist/esm/index.js", 32 | "types": "dist/types/index.d.ts", 33 | "exports": { 34 | ".": { 35 | "solid": "./dist/source/index.jsx", 36 | "default": "./dist/esm/index.js", 37 | "types": "./dist/types/index.d.ts" 38 | } 39 | }, 40 | "scripts": { 41 | "build": "rollup -c", 42 | "watch": "rollup -c -w", 43 | "pub": "rollup -c && npm version patch && npm publish", 44 | "test": "vitest", 45 | "coverage": "vitest run --coverage" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "git+https://github.com/gishub4/solid-map-gl.git" 50 | }, 51 | "browserList": [ 52 | "defaults", 53 | "not ie 11" 54 | ], 55 | "devDependencies": { 56 | "@rollup/plugin-terser": "^0.4.4", 57 | "@solidjs/testing-library": "^0.8.8", 58 | "@testing-library/jest-dom": "^6.4.5", 59 | "@types/mapbox-gl": "^3.1.0", 60 | "@vitest/coverage-v8": "^1.6.0", 61 | "jsdom": "^24.1.0", 62 | "jsdom-worker": "^0.3.0", 63 | "rollup": "^4.18.0", 64 | "rollup-plugin-import-css": "^3.5.0", 65 | "rollup-preset-solid": "^2.0.1", 66 | "typescript": "^5.4.5", 67 | "vite": "^5.2.11", 68 | "vite-plugin-solid": "^2.10.2", 69 | "vitest": "^1.6.0" 70 | }, 71 | "peerDependencies": { 72 | "@babylonjs/core": "^6.24.0", 73 | "mapbox-gl": "*", 74 | "solid-js": "*", 75 | "three": "^0.157.0" 76 | }, 77 | "peerDependenciesMeta": { 78 | "@babylonjs/core": { 79 | "optional": true 80 | }, 81 | "three": { 82 | "optional": true 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import withSolid from 'rollup-preset-solid' 2 | import css from 'rollup-plugin-import-css' 3 | import terser from '@rollup/plugin-terser' 4 | 5 | export default withSolid({ 6 | input: 'src/index.tsx', 7 | plugins: [css({ output: 'styles.css', minify: true }), terser()], 8 | }) 9 | -------------------------------------------------------------------------------- /src/components/Atmosphere/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Enhance your map with atmospheric effects using the Atmosphere component. 3 | --- 4 | 5 | # Atmosphere 6 | 7 | The Atmosphere component allows you to add and customize atmospheric effects like fog, helping to set the mood or highlight certain areas on your map. It provides an easy way to control the appearance of the atmosphere using a declarative syntax. 8 | 9 | ## Props 10 | 11 | The component accepts the following properties (props): 12 | 13 | | Name | Type | Description | Default | 14 | | ----- | ------ | ---------------------------------------------------------------------- | ------- | 15 | | style | object | An object specifying the style properties of the atmosphere element according to Mapbox's [Atmosphere style specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/atmosphere). | `{}` (The default atmosphere style used by the map style) | 16 | 17 | > Note: All properties are optional. If omitted, the map's default atmosphere style will be applied. 18 | 19 | ## Example 20 | 21 | To utilize the Atmosphere component, import it from `solid-map-gl` and include it within your `MapGL` component. Below is a simple example demonstrating how to add the Atmosphere component to your map and specify its style properties: 22 | 23 | ```jsx 24 | import { Component, createSignal } from "solid-js"; 25 | import MapGL, { Viewport, Atmosphere } from "solid-map-gl"; 26 | import 'mapbox-gl/dist/mapbox-gl.css'; 27 | 28 | const App: Component = () => { 29 | // Create a state hook for the viewport settings 30 | const [viewport, setViewport] = createSignal({ 31 | center: [0, 52], // Longitude, Latitude 32 | zoom: 6, // Zoom level 33 | pitch: 100 // Map pitch in degrees 34 | } as Viewport); 35 | 36 | // Define the style for the atmosphere effect 37 | const atmosphereStyle = { 38 | color: 'white', // The color of the fog 39 | horizonBlend: 0.1, // Blend factor of the fog over the horizon line 40 | intensity: 0.5 // The intensity of the fog 41 | }; 42 | 43 | // Render the map with the Atmosphere component 44 | return ( 45 | setViewport(newViewport)} // Handler for viewport changes 49 | > 50 | // Atmosphere component with custom style 51 | 52 | ); 53 | }; 54 | ``` 55 | 56 | Through this example, the `Atmosphere` component is applied to the map, enhancing the visual depth and adding an ethereal effect that blends the horizon with the sky. Customize the `atmosphereStyle` object to experiment with different looks and find the perfect ambiance for your map. 57 | -------------------------------------------------------------------------------- /src/components/Atmosphere/index.tsx: -------------------------------------------------------------------------------- 1 | import { onCleanup, createEffect, VoidComponent } from "solid-js"; 2 | import { useMapContext } from "../MapProvider"; 3 | import type { Fog } from "mapbox-gl"; 4 | 5 | interface AtmosphereProps { 6 | /** Fog/Atmosphere Specifications */ 7 | style?: Fog; 8 | } 9 | 10 | export const Atmosphere: VoidComponent = (props) => { 11 | const [ctx] = useMapContext(); 12 | 13 | // Add or Update Atmosphere Layer 14 | createEffect(() => ctx.map.setFog(props.style || {})); 15 | 16 | // Remove Atmosphere Layer 17 | onCleanup(() => { 18 | if (ctx.map.getFog()) ctx.map.setFog(null); 19 | }); 20 | 21 | return null; 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Camera/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Represents the map's camera 3 | --- 4 | 5 | # Camera 6 | 7 | ## Props 8 | 9 | | Name | Type | Description | 10 | | ---------------- | ----------------- | --------------------------------------- | 11 | | rotateGlobe | object \| boolean | rotate map when in globe view | 12 | | rotateViewport | object \| boolean | rotate map around center | 13 | | reverse | boolean | reverses the rotation direction | 14 | | resetWhenStopped | boolean | returns to origin when rotation stopped | 15 | | translate | object | translate viewport in 3D space | 16 | 17 | _\*required_\ 18 | _^default_ 19 | 20 | ## Example 21 | 22 | ```jsx 23 | import { Component, createSignal } from "solid-js"; 24 | import MapGL, { Viewport, Camera } from "solid-map-gl"; 25 | import 'mapbox-gl/dist/mapbox-gl.css'; 26 | 27 | const App: Component = () => { 28 | const [viewport, setViewport] = createSignal({ 29 | center: [0, 52], 30 | zoom: 6, 31 | } as Viewport); 32 | 33 | return ( 34 | setViewport(evt)} 38 | > 39 | 40 | 41 | ); 42 | }; 43 | ``` 44 | -------------------------------------------------------------------------------- /src/components/Camera/index.tsx: -------------------------------------------------------------------------------- 1 | import { createEffect, Component, onCleanup, createSignal } from 'solid-js' 2 | import { useMapContext } from '../MapProvider' 3 | import type { LngLatLike } from 'mapbox-gl' 4 | 5 | // linearly interpolate between two positions [x,y,z] based on time 6 | const lerp = (a: number[], b: number[], t: number): number[] => 7 | a.map((_, idx) => (1.0 - t) * a[idx] + t * b[idx]) 8 | 9 | // sphericaly interpolate between two positions [x,y,z] based on time 10 | const slerp = (a: number[], b: number[], t: number): number[] => { 11 | const dotProduct = a.map((_, idx) => a[idx] * b[idx]).reduce((m, n) => m + n) 12 | const theta = Math.acos(dotProduct) 13 | 14 | return a.map( 15 | (_, idx) => 16 | (Math.sin((1 - t) * theta) / Math.sin(theta)) * a[idx] + 17 | (Math.sin(t * theta) / Math.sin(theta)) * b[idx] 18 | ) 19 | } 20 | 21 | // calculations from: https://easings.net 22 | const easeQuad = { 23 | in: (x: number): number => x * x, 24 | out: (x: number): number => 1 - (1 - x) * (1 - x), 25 | inOut: (x: number): number => 26 | x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2, 27 | } 28 | 29 | /** Options for rotating the globe. */ 30 | interface RotateGlobeOptions { 31 | /** The number of seconds per revolution. */ 32 | secPerRev?: number 33 | /** The maximum zoom level for spinning the globe. */ 34 | maxSpinZoom?: number 35 | /** The zoom level at which the spinning slows down. */ 36 | slowSpinZoom?: number 37 | } 38 | 39 | /** Options for rotating the viewport. */ 40 | interface RotateViewportOptions { 41 | /** The number of seconds per revolution. */ 42 | secPerRev?: number 43 | /** The pitch angle in degrees. */ 44 | pitch?: number 45 | /** The center of rotation. */ 46 | around?: LngLatLike 47 | } 48 | 49 | /** Props for the component. */ 50 | type Props = { 51 | /** Whether to rotate the globe. */ 52 | rotateGlobe?: boolean | RotateGlobeOptions 53 | /** Whether to rotate the viewport. */ 54 | rotateViewport?: boolean | RotateViewportOptions 55 | /** Whether to reverse the rotation direction. */ 56 | reverse?: boolean 57 | /** Whether to reset the rotation when stopped. */ 58 | resetWhenStopped?: boolean 59 | /** Translation options. */ 60 | translate?: { 61 | /** The type of translation. */ 62 | type?: 'line' | 'sphere' 63 | /** The starting position. */ 64 | start: [number, number, number] 65 | /** The ending position. */ 66 | end: [number, number, number] 67 | /** The target position. */ 68 | target: LngLatLike 69 | /** The ending target position. */ 70 | targetEnd?: LngLatLike 71 | /** The easing function to use. */ 72 | easing?: 'in' | 'out' | 'inOut' 73 | /** Whether to loop the translation. */ 74 | loop?: boolean 75 | /** The duration of the translation in milliseconds. */ 76 | duration: number 77 | } 78 | /** The children to render. */ 79 | children?: any 80 | } 81 | 82 | export const Camera: Component = (props) => { 83 | const [ctx] = useMapContext() 84 | let animationTime = 0.1 85 | let isReverse = false 86 | 87 | // Handle User Interaction 88 | const [userInteraction, setUserInteraction] = createSignal(false) 89 | ;['mousedown', 'touchstart', 'wheel'].forEach((event) => 90 | ctx.map.on(event, (evt) => !evt.rotate && setUserInteraction(true)) 91 | ) 92 | ;['moveend', 'mouseup', 'touchend'].forEach((event) => 93 | ctx.map.on(event, (evt) => !evt.rotate && setUserInteraction(false)) 94 | ) 95 | 96 | const updateCameraPosition = async ( 97 | [lng, lat, alt]: number[], 98 | target: LngLatLike 99 | ) => { 100 | const camera = ctx.map.getFreeCameraOptions() 101 | camera.position = window.MapLib.MercatorCoordinate.fromLngLat( 102 | [lng, lat], 103 | alt 104 | ) 105 | camera.lookAtPoint(target) 106 | ctx.map.setFreeCameraOptions(camera) 107 | } 108 | 109 | const frame = () => { 110 | props.translate && window.requestAnimationFrame(frame) 111 | const params = [ 112 | props.translate.start, 113 | props.translate.end, 114 | props.translate.easing 115 | ? easeQuad[props.translate.easing](animationTime) 116 | : animationTime, 117 | ] as const 118 | const position = 119 | props.translate.type === 'line' ? lerp(...params) : slerp(...params) 120 | 121 | updateCameraPosition(position, props.translate.target) 122 | 123 | if (!props.translate.loop && (animationTime > 1.0 || animationTime < 0.0)) 124 | isReverse = !isReverse 125 | animationTime = isReverse ? animationTime - 0.001 : animationTime + 0.001 126 | } 127 | 128 | createEffect(() => { 129 | props.translate && window.requestAnimationFrame(frame) 130 | }) 131 | 132 | const options = { duration: 1000, easing: (n) => n } 133 | 134 | const rotateGlobe = (): void => { 135 | if (userInteraction()) return 136 | const { 137 | secPerRev = 120, 138 | maxSpinZoom = 5, 139 | slowSpinZoom = 3, 140 | }: RotateGlobeOptions = props.rotateGlobe as RotateGlobeOptions 141 | 142 | const zoom: number = ctx.map.getZoom() 143 | if (zoom > maxSpinZoom) return 144 | let distancePerSecond: number = 360 / secPerRev 145 | if (zoom > slowSpinZoom) 146 | distancePerSecond *= (maxSpinZoom - zoom) / (maxSpinZoom - slowSpinZoom) 147 | const center: mapboxgl.LngLat = ctx.map.getCenter() 148 | center.lng = props.reverse 149 | ? center.lng + distancePerSecond 150 | : center.lng - distancePerSecond 151 | ctx.map.easeTo({ center, ...options }, { rotate: true }) 152 | } 153 | 154 | const rotateViewport = (): void => { 155 | if (!props.rotateViewport || userInteraction()) { 156 | ctx.map.stop() 157 | return 158 | } 159 | const rotateViewport = props.rotateViewport as RotateViewportOptions 160 | const secPerRev = rotateViewport?.secPerRev || 60 161 | const bearing = props.reverse 162 | ? ctx.map.getBearing() + 360 / secPerRev 163 | : ctx.map.getBearing() - 360 / secPerRev 164 | const pitch = rotateViewport?.pitch || 60 165 | const around = rotateViewport?.around 166 | ctx.map.easeTo({ bearing, pitch, around, ...options }, { rotate: true }) 167 | } 168 | 169 | const onEnd = () => (props.rotateGlobe ? rotateGlobe() : rotateViewport()) 170 | ctx.map.on('moveend', onEnd).on('dragend', onEnd) 171 | 172 | let originalCenter: mapboxgl.LngLatLike 173 | createEffect(() => { 174 | if (props.rotateGlobe == undefined) return 175 | if (props.rotateGlobe) { 176 | originalCenter = ctx.map.getCenter() 177 | rotateGlobe() 178 | } else { 179 | props.resetWhenStopped 180 | ? ctx.map.stop().easeTo({ center: originalCenter }) 181 | : ctx.map.stop() 182 | } 183 | }) 184 | 185 | createEffect(() => { 186 | if (props.rotateViewport == undefined) return 187 | props.rotateViewport 188 | ? rotateViewport() 189 | : props.resetWhenStopped 190 | ? ctx.map.stop().resetNorthPitch() 191 | : ctx.map.stop() 192 | }) 193 | 194 | onCleanup(() => ctx.map.off('moveend', onEnd).off('dragend', onEnd)) 195 | 196 | return props.children 197 | } 198 | -------------------------------------------------------------------------------- /src/components/Control/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Provides interface components for map controls such as navigation buttons, scale bars, etc. 3 | --- 4 | 5 | # Control Component 6 | 7 | The Control component is used to add various control elements to your map, which enhance the user's interaction with the map. 8 | 9 | ## Props 10 | 11 | The following table lists the props for the `Control` component: 12 | 13 | | Name | Type | Description | Default Value | 14 | | -------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | 15 | | type\* | string | Specifies the type of control to add. Accepted values: `navigation`, `scale`, `attribution`, `fullscreen`, `geolocate`, `language`, `traffic`. | — | 16 | | options | object | Additional options for the control as documented in [Control Options](https://docs.mapbox.com/mapbox-gl-js/api/markers/). | {} | 17 | | position | string | Determines the position of the control on the map. Choices: `top-left`, `top-right`, `bottom-left`, `bottom-right`. | `top-right` | 18 | 19 | _\* indicates a required property._ 20 | 21 | #### Optional Dependencies 22 | 23 | To use some of the control types, you will need to install optional dependencies. Below are the instructions for installing these dependencies using `pnpm`. 24 | 25 | {% tabs %} 26 | {% tab title="Traffic" %} 27 | 28 | ``` 29 | pnpm add @mapbox/mapbox-gl-traffic 30 | ``` 31 | 32 | {% endtab %} 33 | 34 | {% tab title="Language" %} 35 | 36 | ``` 37 | pnpm add @mapbox/mapbox-gl-language 38 | ``` 39 | 40 | {% endtab %} 41 | {% endtabs %} 42 | 43 | ## Example 44 | 45 | Here is an example of how to integrate the `Control` component within the `MapGL` component from `solid-map-gl`. 46 | 47 | ```jsx 48 | import { Component, createSignal } from "solid-js"; 49 | import MapGL, { Viewport, Control } from "solid-map-gl"; 50 | import 'mapbox-gl/dist/mapbox-gl.css'; 51 | 52 | const App: Component = (props) => { 53 | const [viewport, setViewport] = createSignal({ 54 | center: [0, 52], 55 | zoom: 6, 56 | } as Viewport); 57 | 58 | return ( 59 | setViewport(evt)} 63 | > 64 | 65 | 66 | 67 | ); 68 | }; 69 | ``` 70 | -------------------------------------------------------------------------------- /src/components/Control/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createSignal, 3 | createEffect, 4 | onCleanup, 5 | splitProps, 6 | untrack, 7 | VoidComponent, 8 | } from "solid-js"; 9 | import { useMapContext } from "../MapProvider"; 10 | import type { Options as AttributionOptions } from "mapbox-gl/src/ui/control/attribution_control"; 11 | import type { Options as FullscreenOptions } from "mapbox-gl/src/ui/control/fullscreen_control"; 12 | import type { Options as GeolocateOptions } from "mapbox-gl/src/ui/control/geolocate_control"; 13 | import type { Options as NavigationOptions } from "mapbox-gl/src/ui/control/navigation_control"; 14 | import type { Options as ScaleOptions } from "mapbox-gl/src/ui/control/scale_control"; 15 | 16 | type ControlType = 17 | | "navigation" 18 | | "scale" 19 | | "attribution" 20 | | "fullscreen" 21 | | "geolocate" 22 | | "logo" 23 | | "terrain"; 24 | 25 | type Props = { 26 | type?: ControlType; 27 | options?: 28 | | NavigationOptions 29 | | ScaleOptions 30 | | AttributionOptions 31 | | FullscreenOptions 32 | | GeolocateOptions 33 | | object; 34 | custom?: any; 35 | position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; 36 | }; 37 | 38 | export const Control: VoidComponent = (props) => { 39 | const [ctx] = useMapContext(); 40 | const [update, create] = splitProps(props, ["position"]); 41 | const [control, setControl] = createSignal(null); 42 | 43 | const controlClasses = new Map([ 44 | ["navigation", window.MapLib.NavigationControl], 45 | ["scale", window.MapLib.ScaleControl], 46 | ["attribution", window.MapLib.AttributionControl], 47 | ["geolocate", window.MapLib.GeolocateControl], 48 | ["fullscreen", window.MapLib.FullscreenControl], 49 | ["logo", window.MapLib.LogoControl], 50 | ["terrain", window.MapLib.TerrainControl], 51 | ]); 52 | 53 | // Add Control 54 | createEffect(() => { 55 | untrack( 56 | () => 57 | control() && 58 | ctx.map.hasControl(control()) && 59 | ctx.map.removeControl(control()), 60 | ); 61 | setControl( 62 | create.custom || 63 | new (controlClasses.get(create.type || "navigation"))(create.options), 64 | ); 65 | }); 66 | 67 | // Update Position 68 | createEffect(() => { 69 | ctx.map.hasControl(control()) && ctx.map.removeControl(control()); 70 | ctx.map.addControl(control(), update.position); 71 | }); 72 | 73 | onCleanup(() => { 74 | ctx.map.hasControl(control()) && ctx.map.removeControl(control()); 75 | }); 76 | 77 | return null; 78 | }; 79 | -------------------------------------------------------------------------------- /src/components/Draw/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Draw Component 3 | --- 4 | 5 | # Draw 6 | 7 | ## Props 8 | 9 | | Name | Type | Description | 10 | | ----------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | 11 | | lib\* | object | Draw Library from `import` | 12 | | options | object | [Draw Options](https://github.com/mapbox/mapbox-gl-draw/blob/main/docs/API.md#options) | 13 | | position | string | 'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' | 14 | | getInstance | function | access to the Draw Object to run [API Methods](https://github.com/mapbox/mapbox-gl-draw/blob/main/docs/API.md#api-methods) | 15 | | on[Event] | function | Called when event is fired at draw control | 16 | 17 | _\*required_ 18 | 19 | ## Example 20 | 21 | ```jsx 22 | import { Component, createSignal } from "solid-js"; 23 | import MapGL, { Viewport, Source, Image, Layer } from "solid-map-gl"; 24 | import 'mapbox-gl/dist/mapbox-gl.css'; 25 | import MapboxDraw from "@mapbox/mapbox-gl-draw"; 26 | import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css' 27 | 28 | const App: Component = () => { 29 | const [viewport, setViewport] = createSignal({ 30 | center: [-77.4144, 25.0759], 31 | zoom: 6, 32 | } as Viewport); 33 | 34 | return ( 35 | setViewport(evt)} 39 | > 40 | console.log(event)} 49 | getInstance={draw => draw.add({ type: 'Point', coordinates: [0, 0] })} 50 | /> 51 | 52 | ); 53 | }; 54 | ``` 55 | -------------------------------------------------------------------------------- /src/components/Draw/index.tsx: -------------------------------------------------------------------------------- 1 | import { onCleanup, VoidComponent } from 'solid-js' 2 | import { useMapContext } from '../MapProvider' 3 | import { drawEvents } from '../../events' 4 | import type { drawEventTypes } from '../../events' 5 | 6 | type Props = { 7 | /** Draw Library */ 8 | lib: any 9 | /** Draw Options */ 10 | options?: object 11 | /** Draw Control Position */ 12 | position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' 13 | /** Draw Control Instance */ 14 | getInstance?: (object) => void 15 | } & drawEventTypes 16 | 17 | export const Draw: VoidComponent = (props: Props) => { 18 | const [ctx] = useMapContext() 19 | 20 | // Add Draw Control 21 | const draw = new props.lib(props.options) 22 | ctx.map.addControl(draw, props.position || 'top-right') 23 | props.getInstance && props.getInstance(draw) 24 | 25 | // Hook up events 26 | const eventList: Record void> = {} 27 | drawEvents.forEach(item => { 28 | if (props[item]) { 29 | const event = `draw.${item.slice(2).toLowerCase()}` 30 | const fn = evt => props[item](evt) 31 | eventList[event] = fn 32 | ctx.map.on(event, fn) 33 | } 34 | }) 35 | 36 | // Remove Draw Control 37 | onCleanup(() => { 38 | // Remove Events 39 | Object.keys(eventList).forEach(event => ctx.map.off(event, eventList[event])) 40 | // Remove Control 41 | ctx.map?.removeControl(draw) 42 | }) 43 | 44 | return null 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Image/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Image Component 3 | --- 4 | 5 | # Image 6 | 7 | ## Props 8 | 9 | | Name | Type | Description | 10 | | -------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | 11 | | id\* | string | ID to reference image in layer style | 12 | | source\* | [HTMLImageElement, SVGElement, ImageBitmap, ImageData](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#addimage) \| string | ImageBitmap or [Loadable Image URL](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#loadimage) | 13 | | options | object or { stroke \| fill \| transform } | [Add image options](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#addimage) or overwrite SVG properties | 14 | 15 | _\*required_ 16 | 17 | ## Example 18 | 19 | ```jsx 20 | import { Component, createSignal } from "solid-js"; 21 | import MapGL, { Viewport, Source, Image, Layer } from "solid-map-gl"; 22 | import 'mapbox-gl/dist/mapbox-gl.css'; 23 | 24 | const App: Component = () => { 25 | const [viewport, setViewport] = createSignal({ 26 | center: [-77.4144, 25.0759], 27 | zoom: 6, 28 | } as Viewport); 29 | 30 | return ( 31 | setViewport(evt)} 35 | > 36 | 40 | 57 | 66 | 67 | 68 | ); 69 | }; 70 | ``` 71 | -------------------------------------------------------------------------------- /src/components/Image/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | onCleanup, 3 | createSignal, 4 | createEffect, 5 | VoidComponent, 6 | untrack, 7 | } from 'solid-js' 8 | import { useMapContext } from '../MapProvider' 9 | import type { 10 | StyleImageInterface, 11 | StyleImageMetadata, 12 | } from 'mapbox-gl/src/style/style_image' 13 | 14 | const PATTERN = { 15 | diagonal_l: { size: 20, path: 'M20 0 0 20M-10 10 10-10M10 30 30 10' }, 16 | diagonal_r: { size: 20, path: 'M0 0 20 20M30 10 10-10M10 30-10 10' }, 17 | horizontal: { size: 14, path: 'M7 0V20' }, 18 | vertical: { size: 14, path: 'M0 7H20' }, 19 | cross: { size: 18, path: 'M9 0V18M0 9H18' }, 20 | hash: { size: 30, path: 'M15 0 30 15 15 30 0 15Z' }, 21 | chevron_h: { size: 20, path: 'M-5 5 0 10 10 0 20 10 25 5M0 30 10 20 20 30' }, 22 | chevron_v: { size: 20, path: 'M5-5 10 0 0 10 10 20 5 25M25 15 20 10 25 5' }, 23 | square: { size: 20, path: 'M8 8H12V12H8Z', fill: true }, 24 | hex: { size: 50, path: 'M0 0V50L50 25ZM50 0V50L0 25ZM25 0V50' }, 25 | circle: { 26 | size: 25, 27 | path: 'M8 10A2.5 2.5 0 118.01 10M18 15A2.5 2.5 0 1018.01 15', 28 | fill: true, 29 | }, 30 | } 31 | 32 | export const patternList = Object.keys(PATTERN) 33 | 34 | export type Color = 35 | | `#${string}` 36 | | `rgb(${number}, ${number}, ${number})` 37 | | `rgba(${number}, ${number}, ${number}, ${number})` 38 | | `hsl(${number}, ${number}%, ${number}%)` 39 | | `hsla(${number}, ${number}%, ${number}%, ${number})` 40 | 41 | type Props = { 42 | id: string 43 | /** The unique identifier for the image. */ 44 | source?: 45 | | HTMLImageElement 46 | | ImageBitmap 47 | | ImageData 48 | | SVGElement 49 | | { width: number; height: number; data: Uint8Array | Uint8ClampedArray } 50 | | StyleImageInterface 51 | | string 52 | /** The image to be used for the image component. */ 53 | options?: StyleImageMetadata & { 54 | fill?: Color 55 | stroke?: Color 56 | transform?: string 57 | } 58 | /** The options for the image */ 59 | pattern?: { 60 | type: string 61 | color: Color 62 | background: Color 63 | lineWith: number 64 | } 65 | /** The pattern to be used for the image component. */ 66 | } 67 | 68 | export const MGL_Image: VoidComponent = props => { 69 | const [ctx] = useMapContext() 70 | const [size, setSize] = createSignal({ width: 0, height: 0 }) 71 | 72 | const debug = (text, value?) => { 73 | ctx.map.debug && 74 | console.debug('%c[MapGL]', 'color: #10b981', text, value || '') 75 | } 76 | 77 | // Remove Image 78 | onCleanup(() => { 79 | ctx.map?.hasImage(props.id) && ctx.map?.removeImage(props.id) 80 | debug('Remove Image:', props.id) 81 | }) 82 | 83 | // Add or Update Image 84 | createEffect(() => { 85 | if (!props.id) throw new Error('Image - ID is required') 86 | if (!props.source && !props.pattern) 87 | throw new Error('Image - Image or Pattern is required') 88 | 89 | const ops = props.pattern 90 | ? { pixelRatio: 2, ...props.options } 91 | : props.options 92 | 93 | _loadImage(props.source || _createPattern(props.pattern), data => { 94 | const { width, height } = data 95 | if (ctx.map && !ctx.map.hasImage(props.id)) 96 | ctx.map.addImage(props.id, data, ops) 97 | if ( 98 | !props.pattern && 99 | untrack(() => width === size().width && height === size().height) 100 | ) { 101 | ctx.map.updateImage(props.id, data) 102 | ctx.map.triggerRepaint() 103 | } else { 104 | ctx.map.removeImage(props.id) 105 | ctx.map.addImage(props.id, data, ops) 106 | } 107 | setSize({ width, height }) 108 | debug('Add Image:', props.id) 109 | ctx.map.on('style.load', () => { 110 | if (ctx.map.hasImage(props.id)) return 111 | ctx.map.addImage(props.id, data, ops) 112 | debug('Re-Add Image:', props.id) 113 | }) 114 | }) 115 | }) 116 | 117 | // Load Image / SVG 118 | const _loadImage = (image, callback) => { 119 | if (typeof image == 'string' && image?.trimStart().startsWith(' { 131 | if (error) { 132 | if (typeof image == 'string' && image?.trimEnd().endsWith('.svg')) { 133 | image = await (await fetch(image)).text() 134 | image = new DOMParser().parseFromString(image, 'image/svg+xml') 135 | .childNodes[0] 136 | image.setAttribute('fill', props.options.fill) 137 | image.setAttribute('stroke', props.options.stroke) 138 | image.setAttribute('transform', props.options.transform) 139 | image = new XMLSerializer().serializeToString(image) 140 | } 141 | const img = new Image() 142 | img.crossOrigin = 'Anonymous' 143 | img.onload = () => { 144 | const canvas = document.createElement('canvas') 145 | canvas.width = Math.max(img.width, 50) 146 | canvas.height = Math.max(img.height, 50) 147 | const ctx = canvas.getContext('2d') 148 | ctx.imageSmoothingEnabled = true 149 | ctx.drawImage(img, 0, 0, canvas.width, canvas.height) 150 | return callback(ctx.getImageData(0, 0, canvas.width, canvas.height)) 151 | } 152 | img.src = image.trimStart().startsWith(' { 162 | const p = PATTERN[pattern.type] 163 | return { 164 | width: p.size, 165 | height: p.size, 166 | data: new Uint8Array(p.size * p.size * 4), 167 | onAdd: function () { 168 | let canvas = document.createElement('canvas') 169 | canvas.width = canvas.height = p.size 170 | this.ctx = canvas.getContext('2d', { willReadFrequently: true }) 171 | }, 172 | render: function () { 173 | this.ctx.fillStyle = pattern.background || 'transparent' 174 | this.ctx.fillRect(0, 0, this.width, this.height) 175 | this.ctx.strokeStyle = this.ctx.fillStyle = pattern.color || 'black' 176 | this.ctx.lineWidth = pattern.lineWith || 1 177 | this.ctx.stroke(new Path2D(p.path)) 178 | if (p.fill) this.ctx.fill(new Path2D(p.path)) 179 | this.data = this.ctx.getImageData(0, 0, this.width, this.height).data 180 | return true 181 | }, 182 | } 183 | } 184 | 185 | return null 186 | } 187 | -------------------------------------------------------------------------------- /src/components/Layer/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Layer Component 3 | --- 4 | 5 | # Layer 6 | 7 | ## Props 8 | 9 | | Name | Type | Description | 10 | | ------------ | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | 11 | | id | string | only required if referenced outside of nested layer | 12 | | style | object | [Layer Style Object](https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/) | 13 | | customLayer | [CustomLayerInterface](https://docs.mapbox.com/mapbox-gl-js/api/properties/#customlayerinterface) | To include external layers e.g. [deck.gl](https://deck.gl/) | 14 | | filter | [FilterSpecification](https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/) | Filter expression | 15 | | visible | boolean | Show/Hide Layer | 16 | | sourceId | string | Required for Vector Sources | 17 | | beforeType | string | background \| fill \| line \| symbol \| raster \| circle \| fill-extrusion \| heatmap \| hillshade \| sky | 18 | | beforeId | string | Id of Layer to insert Layer before | 19 | | featureState | object | Define Feature State | 20 | 21 | _\*required_ 22 | 23 | ## Examples 24 | 25 | ### Circle Layer 26 | 27 | ```jsx 28 | import { Component, createSignal } from "solid-js"; 29 | import MapGL, { Viewport, Source, Layer } from "solid-map-gl"; 30 | import 'mapbox-gl/dist/mapbox-gl.css'; 31 | 32 | const App: Component = (props) => { 33 | const [viewport, setViewport] = createSignal({ 34 | center: [-122.45, 37.78], 35 | zoom: 6, 36 | } as Viewport); 37 | 38 | return ( 39 | setViewport(evt)} 43 | > 44 | 50 | 59 | 60 | 61 | ); 62 | }; 63 | ``` 64 | -------------------------------------------------------------------------------- /src/components/Layer/index.tsx: -------------------------------------------------------------------------------- 1 | import { onCleanup, createEffect, Component, createUniqueId } from "solid-js"; 2 | import { useMapContext } from "../MapProvider"; 3 | import { useSourceId } from "../Source"; 4 | import { layerEvents } from "../../events"; 5 | import { baseStyle, layoutStyles } from "../../styles"; 6 | import type { layerEventTypes } from "../../events"; 7 | import type { 8 | FilterSpecification, 9 | StyleSpecification, 10 | } from "mapbox-gl/src/style-spec/types.js"; 11 | import type { CustomLayerInterface } from "mapbox-gl/src/style/style_layer/custom_style_layer"; 12 | 13 | const diff = ( 14 | newProps: StyleSpecification = {}, 15 | prevProps: StyleSpecification = {}, 16 | ): [string, any][] => { 17 | const keys = new Set([...Object.keys(newProps), ...Object.keys(prevProps)]); 18 | return [...keys].reduce((acc, key: string) => { 19 | const value = newProps[key]; 20 | if (value !== prevProps[key]) { 21 | acc.push([key, value]); 22 | } 23 | return acc; 24 | }, []); 25 | }; 26 | 27 | type Props = { 28 | id?: string; 29 | /** A string that uniquely identifies the layer. If not provided, a unique ID will be generated. */ 30 | style?: StyleSpecification; 31 | /** A Mapbox Style Specification object that defines the visual appearance of the layer. */ 32 | customLayer?: CustomLayerInterface; 33 | /** An object that implements the `CustomLayerInterface` interface, which allows you to create custom layers using WebGL. */ 34 | filter?: FilterSpecification; 35 | /** A Mapbox filter specification that defines which features of the layer to include or exclude from the layer. */ 36 | visible?: boolean; 37 | /** A boolean that determines whether the layer is visible or not. */ 38 | sourceId?: string; 39 | /** A string that specifies the ID of the source that the layer uses for its data. */ 40 | slot?: "bottom" | "middle" | "top" | string; 41 | /** A string that specifies the slot to which the layer belongs. */ 42 | beforeType?: 43 | | "background" 44 | | "fill" 45 | | "line" 46 | | "symbol" 47 | | "raster" 48 | | "circle" 49 | | "fill-extrusion" 50 | | "heatmap" 51 | | "hillshade" 52 | | "sky" 53 | | string; 54 | /** A string that specifies the type of layer before which the current layer should be inserted. */ 55 | beforeId?: string; 56 | /** A string that specifies the ID of the layer before which the current layer should be inserted. */ 57 | featureState?: { id: number | string; state: object }; 58 | /** An object that specifies the state of a feature in the layer. The object consists of an ID (either a number or a string) and an object containing the state. */ 59 | children?: any; 60 | /** Any content that should be rendered within the layer. */ 61 | } & layerEventTypes; 62 | 63 | const newKey = (key, type) => 64 | (key.startsWith(type) || key.startsWith("icon") || key.startsWith("text") 65 | ? "" 66 | : type + "-") + key.replace(/[A-Z]/g, (s) => "-" + s.toLowerCase()); 67 | 68 | const updateStyle = (oldStyle) => { 69 | if (!oldStyle) return; 70 | let layout = {}; 71 | let paint = {}; 72 | let style = {}; 73 | 74 | Object.entries(oldStyle).forEach(([key, value]) => { 75 | if (baseStyle.includes(key)) style[key] = value; 76 | else { 77 | const nk = newKey(key, oldStyle.type); 78 | layoutStyles.includes(nk) ? (layout[nk] = value) : (paint[nk] = value); 79 | } 80 | }); 81 | if (oldStyle.paint) 82 | Object.entries(oldStyle.paint).forEach( 83 | ([key, value]) => (paint[newKey(key, oldStyle.type)] = value), 84 | ); 85 | if (oldStyle.layout) 86 | Object.entries(oldStyle.layout).forEach( 87 | ([key, value]) => (layout[newKey(key, oldStyle.type)] = value), 88 | ); 89 | return { ...style, paint, layout } as StyleSpecification; 90 | }; 91 | 92 | export const Layer: Component = (props) => { 93 | const [ctx] = useMapContext(); 94 | const sourceId: string = 95 | props.sourceId || props.style?.source || useSourceId(); 96 | const layerId: string = props.id || props.customLayer?.id || createUniqueId(); 97 | 98 | const debug = (text, value?) => { 99 | (ctx.map.debug || ctx.map.debugEvents) && 100 | console.debug("%c[MapGL]", "color: #10b981", text, value || ""); 101 | }; 102 | 103 | // Add Layer 104 | ctx.map.addLayer( 105 | props.customLayer || { 106 | ...updateStyle(props.style), 107 | id: layerId, 108 | source: sourceId, 109 | slot: props.slot || "", 110 | metadata: { 111 | smg: { beforeType: props.beforeType, beforeId: props.beforeId }, 112 | }, 113 | }, 114 | props.beforeType 115 | ? ctx.map.getStyle().layers.find((l) => l.type === props.beforeType)?.id 116 | : props.beforeId, 117 | ); 118 | ctx.map.layerIdList.push(layerId); 119 | if (props.customLayer) ctx.map.fire("load"); 120 | debug("Add Layer:", layerId); 121 | 122 | // Hook up events 123 | layerEvents.forEach((item) => { 124 | if (props[item]) { 125 | const event = item.slice(2).toLowerCase(); 126 | ctx.map.on(event, layerId, (evt) => { 127 | evt.clickOnLayer = true; 128 | props[item](evt); 129 | ctx.map.debugEvent && 130 | debug(`Layer '${event}' event on '${layerId}':`, evt); 131 | }); 132 | } 133 | }); 134 | 135 | // Update Style 136 | createEffect((prev: StyleSpecification) => { 137 | const style = updateStyle(props.style); 138 | if (style === prev) return; 139 | 140 | if (style.layout !== prev?.layout) 141 | diff(style.layout, prev?.layout).forEach(([key, value]) => 142 | ctx.map.setLayoutProperty(layerId, key, value, { validate: false }), 143 | ); 144 | 145 | if (style.paint !== prev?.paint) 146 | diff(style.paint, prev?.paint).forEach(([key, value]) => 147 | ctx.map.setPaintProperty(layerId, key, value, { validate: false }), 148 | ); 149 | 150 | if (style.minzoom !== prev?.minzoom || style.maxzoom !== prev?.maxzoom) 151 | ctx.map.setLayerZoomRange(layerId, style.minzoom, style.maxzoom); 152 | 153 | if (style.filter !== prev?.filter) 154 | ctx.map.setFilter(layerId, style.filter, { validate: false }); 155 | 156 | debug("Update Layer Style:", layerId); 157 | return style; 158 | }, updateStyle(props.style)); 159 | 160 | // Update Visibility 161 | createEffect((prev: boolean) => { 162 | if (props.visible === prev) return; 163 | 164 | ctx.map.setLayoutProperty( 165 | layerId, 166 | "visibility", 167 | props.visible ? "visible" : "none", 168 | { validate: false }, 169 | ); 170 | debug(`Update Visibility (${layerId}):`, props.visible.toString()); 171 | return props.visible; 172 | }, props.visible); 173 | 174 | // Update Filter 175 | createEffect(async () => { 176 | if (!props.filter) return; 177 | 178 | !ctx.map.isStyleLoaded() && (await ctx.map.once("styledata")); 179 | ctx.map.setFilter(layerId, props.filter); 180 | debug(`Update Filter (${layerId}):`, props.filter); 181 | }); 182 | 183 | // Update Feature State 184 | createEffect(async () => { 185 | if (!props.featureState || props.featureState.id === null) return; 186 | 187 | !ctx.map.isStyleLoaded() && (await ctx.map.once("styledata")); 188 | 189 | ctx.map.removeFeatureState({ 190 | source: sourceId, 191 | sourceLayer: props.style["source-layer"], 192 | }); 193 | ctx.map.setFeatureState( 194 | { 195 | source: sourceId, 196 | sourceLayer: props.style["source-layer"], 197 | id: props.featureState.id, 198 | }, 199 | props.featureState.state, 200 | ); 201 | }); 202 | 203 | //Remove Layer 204 | onCleanup(() => ctx.map?.getLayer(layerId) && ctx.map?.removeLayer(layerId)); 205 | 206 | return props.children; 207 | }; 208 | -------------------------------------------------------------------------------- /src/components/Layer3D/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Layer 3D Component 3 | --- 4 | 5 | # Layer3D 6 | 7 | ## Props 8 | 9 | | Name | Type | Description | 10 | | ------------ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | 11 | | origin | object [x: number, y:number, z:number] | The origin of the 3D model | 12 | | babylon | boolean | Flag to indicate using [BabylonJS](https://www.babylonjs.com/) otherwise [ThreeJS](https://threejs.org/) library | 13 | | defaultLight | boolean | Add default light to scene | 14 | | onAdd | function | Function to create the scene after layer is added | 15 | | onRender | function | Function to implement functionality on each render cycle | 16 | 17 | _\*required_ 18 | 19 | ## Example 20 | 21 | ```jsx 22 | import { Component, createSignal } from "solid-js"; 23 | import MapGL, { Viewport, Layer3D } from "solid-map-gl"; 24 | import 'mapbox-gl/dist/mapbox-gl.css'; 25 | import { SceneLoader } from '@babylonjs/core/Loading'; 26 | import '@babylonjs/loaders'; 27 | 28 | const [viewport, setViewport] = createSignal({ 29 | center: [148.9819, -35.3981], 30 | zoom: 18, 31 | pitch: 60, 32 | } as Viewport); 33 | 34 | const App: Component = () => ( 35 | setViewport(evt)} 39 | > 40 | { 45 | SceneLoader.LoadAssetContainerAsync( 46 | 'https://docs.mapbox.com/mapbox-gl-js/assets/34M_17/', 47 | '34M_17.gltf', 48 | scene 49 | ).then((modelContainer: any) => modelContainer.addAllToScene()); 50 | }} 51 | /> 52 | 53 | ); 54 | ``` 55 | -------------------------------------------------------------------------------- /src/components/Layer3D/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | onCleanup, 3 | Component, 4 | createUniqueId, 5 | createSignal, 6 | onMount, 7 | createContext, 8 | useContext, 9 | JSX, 10 | } from "solid-js"; 11 | import { unwrap } from "solid-js/store"; 12 | import { useMapContext } from "../MapProvider"; 13 | 14 | declare global { 15 | interface Window { 16 | BABYLON?: any; 17 | THREE?: any; 18 | } 19 | } 20 | 21 | type Props = { 22 | id?: string; 23 | origin?: [number, number, number?]; // [lng, lat, altitude] 24 | defaultLight?: boolean; 25 | babylon?: boolean; 26 | onAdd?: (scene: any, map: any, gl: any) => void; 27 | onRender?: (gl: any, matrix: any) => void; 28 | children?: JSX.Element | JSX.Element[]; 29 | /** A string that specifies the type of layer before which the current layer should be inserted. */ 30 | beforeId?: string; 31 | disableProjectionMatrix?: boolean; 32 | }; 33 | 34 | const LayerContext = createContext(); 35 | export const useScene = (): any => useContext(LayerContext); 36 | 37 | export const Layer3D: Component = (props) => { 38 | const [ctx] = useMapContext(); 39 | const [scene, setScene] = createSignal(null); 40 | 41 | onMount(async () => { 42 | props.id = props.id || createUniqueId(); 43 | 44 | const worldOriginMercator = window.MapLib.MercatorCoordinate.fromLngLat( 45 | [props.origin[0] || 0, props.origin[1] || 0], 46 | props.origin[2] || 0, 47 | ); 48 | const worldScale = worldOriginMercator.meterInMercatorCoordinateUnits(); 49 | const { x, y, z } = worldOriginMercator; 50 | 51 | let worldMatrix = null; 52 | let BABYLON = null; 53 | let THREE = null; 54 | 55 | if (props.babylon) { 56 | BABYLON = await import("@babylonjs/core"); 57 | worldMatrix = BABYLON.Matrix.Compose( 58 | new BABYLON.Vector3(worldScale, worldScale, worldScale), 59 | BABYLON.Quaternion.FromEulerAngles(Math.PI / 2, 0, 0), 60 | new BABYLON.Vector3(x, y, z), 61 | ); 62 | } else { 63 | THREE = await import("three"); 64 | worldMatrix = { 65 | translateX: x, 66 | translateY: y, 67 | translateZ: z, 68 | rotateX: Math.PI / 2, 69 | rotateY: 0, 70 | rotateZ: 0, 71 | scale: worldScale, 72 | }; 73 | } 74 | 75 | ctx.map.addLayer( 76 | { 77 | id: props.id, 78 | type: "custom", 79 | renderingMode: "3d", 80 | onAdd(map, gl) { 81 | if (props.babylon) { 82 | const engine = new BABYLON.Engine( 83 | gl, 84 | true, 85 | { 86 | useHighPrecisionMatrix: true, 87 | }, 88 | true, 89 | ); 90 | this.scene = new BABYLON.Scene(engine); 91 | this.scene.autoClear = false; 92 | this.scene.autoClearDepthAndStencil = false; 93 | // this.scene.detachControl(); 94 | this.scene.preventDefaultOnPointerDown = false; 95 | this.scene.preventDefaultOnPointerUp = false; 96 | this.scene.beforeRender = () => engine.wipeCaches(true); 97 | this.scene.activeCamera = new BABYLON.UniversalCamera( 98 | "Camera", 99 | BABYLON.Vector3.Zero(), 100 | this.scene, 101 | ); 102 | // this.scene.activeCamera.setTarget(BABYLON.Vector3.Zero()); 103 | // this.camera.inputs.addMouseWheel() 104 | // this.scene.activeCamera.attachControl(gl, true); 105 | this.scene.ambientColor = new BABYLON.Color3(1, 1, 1); 106 | if (props.defaultLight) 107 | new BABYLON.HemisphericLight( 108 | "Light", 109 | new BABYLON.Vector3(100, 10, 0), 110 | this.scene, 111 | ); 112 | } else { 113 | this.camera = new THREE.Camera(); 114 | this.scene = new THREE.Scene(); 115 | if (props.defaultLight) { 116 | this.light = new THREE.DirectionalLight(0xffffff); 117 | this.light.position.set(100, 0, 0).normalize(); 118 | this.scene.add(this.light); 119 | } 120 | 121 | this.renderer = new THREE.WebGLRenderer({ 122 | canvas: map.getCanvas(), 123 | context: gl, 124 | antialias: true, 125 | }); 126 | this.renderer.autoClear = false; 127 | } 128 | this.map = map; 129 | props.onAdd && props.onAdd(this.scene, this.map, gl); 130 | setScene(this.scene); 131 | }, 132 | render(gl, matrix) { 133 | if (props.babylon) { 134 | this.scene.activeCamera.freezeProjectionMatrix( 135 | props.disableProjectionMatrix 136 | ? BABYLON.Matrix.FromArray(matrix) 137 | : worldMatrix.multiply(BABYLON.Matrix.FromArray(matrix)), 138 | ); 139 | // const { x, y, z } = this.map.getFreeCameraOptions().position; 140 | // this.scene.activeCamera.setPosition(new BABYLON.Vector3(x, y, z)); 141 | // console.log(this.scene.activeCamera); 142 | // this.camera.position = BABYLON.Vector3.TransformCoordinates( 143 | // new BABYLON.Vector3(x, y, z), 144 | // this.scene.activeCamera.getProjectionMatrix().clone().invert(), 145 | // ); 146 | this.scene.render(false); 147 | } else { 148 | const rotationX = new THREE.Matrix4().makeRotationAxis( 149 | new THREE.Vector3(1, 0, 0), 150 | worldMatrix.rotateX, 151 | ); 152 | const rotationY = new THREE.Matrix4().makeRotationAxis( 153 | new THREE.Vector3(0, 1, 0), 154 | worldMatrix.rotateY, 155 | ); 156 | const rotationZ = new THREE.Matrix4().makeRotationAxis( 157 | new THREE.Vector3(0, 0, 1), 158 | worldMatrix.rotateZ, 159 | ); 160 | 161 | const m = new THREE.Matrix4().fromArray(matrix); 162 | const l = new THREE.Matrix4() 163 | .makeTranslation( 164 | worldMatrix.translateX, 165 | worldMatrix.translateY, 166 | worldMatrix.translateZ, 167 | ) 168 | .scale( 169 | new THREE.Vector3( 170 | worldMatrix.scale, 171 | -worldMatrix.scale, 172 | worldMatrix.scale, 173 | ), 174 | ) 175 | .multiply(rotationX) 176 | .multiply(rotationY) 177 | .multiply(rotationZ); 178 | this.camera.projectionMatrix = m.multiply(l); 179 | this.renderer.resetState(); 180 | this.renderer.render(this.scene, this.camera); 181 | } 182 | props.onRender && props.onRender(gl, matrix); 183 | ctx.map.triggerRepaint(); 184 | }, 185 | }, 186 | props.beforeId, 187 | ); 188 | }); 189 | 190 | onCleanup(() => { 191 | ctx.map.getLayer(props.id) && ctx.map.removeLayer(props.id); 192 | }); 193 | 194 | return ( 195 | 196 | {scene() && props.children} 197 | 198 | ); 199 | }; 200 | -------------------------------------------------------------------------------- /src/components/Light/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Light Layer Component 3 | --- 4 | 5 | # Light 6 | 7 | ## Props 8 | 9 | | Name | Type | Description | 10 | | ----- | ------ | -------------------------------------------------------------------------- | 11 | | style | object | [Light style specs](https://docs.mapbox.com/mapbox-gl-js/style-spec/light) | 12 | 13 | _\*required_\ 14 | _^default_ 15 | 16 | ## Example 17 | 18 | ```jsx 19 | import { Component, createSignal } from "solid-js"; 20 | import MapGL, { Viewport, Light } from "solid-map-gl"; 21 | import 'mapbox-gl/dist/mapbox-gl.css'; 22 | 23 | const App: Component = () => { 24 | const [viewport, setViewport] = createSignal({ 25 | center: [0, 52], 26 | zoom: 6, 27 | pitch: 100 28 | } as Viewport); 29 | 30 | return ( 31 | setViewport(evt)} 35 | > 36 | 41 | 42 | ); 43 | }; 44 | ``` 45 | -------------------------------------------------------------------------------- /src/components/Light/index.tsx: -------------------------------------------------------------------------------- 1 | import { onCleanup, createEffect, VoidComponent } from 'solid-js' 2 | import { useMapContext } from '../MapProvider' 3 | import type { Light as LightSpecification } from 'mapbox-gl' 4 | 5 | type Props = { 6 | /** Light Specifications */ 7 | style: LightSpecification 8 | } 9 | export const Light: VoidComponent = (props: Props) => { 10 | const [ctx] = useMapContext() 11 | 12 | // Add or Update Light Layer 13 | createEffect(() => ctx.map.setLight(props.style || {})) 14 | 15 | // Remove Light Layer 16 | onCleanup(() => ctx.map?.setLight(null)) 17 | 18 | return null 19 | } 20 | -------------------------------------------------------------------------------- /src/components/MapGL/README.md: -------------------------------------------------------------------------------- 1 | # Map 2 | 3 | ## Props 4 | 5 | | Name | Type | Description | 6 | | ----------------- | ------------------------------- | -------------------------------------------------------------------------------------------- | 7 | | mapLib | module | Pass [MapLibre](https://maplibre.org/) package to use instead of Mapbox library | 8 | | style | string | CSS style for map container | 9 | | class | string | CSS class for map container | 10 | | classList | string\[] | SolidJS classList attached to map container | 11 | | viewport | object | Current viewport of the map, contains: `latitude, longitude, zoom, ...` | 12 | | onViewportChange | Viewport | Set the map viewport | 13 | | options | object | [Mapbox map parameter](https://docs.mapbox.com/mapbox-gl-js/api/map/#map-parameters) | 14 | | config | object | Sets configuration in Mapbox Standard Style | 15 | | transitionType | string | flyTo^, easeTo, jumpTo | 16 | | on\[Event] | Event | Any [Map Event](https://docs.mapbox.com/mapbox-gl-js/api/map/#map-events) - eg.: onMouseMove | 17 | | onUserInteraction | boolean | Event Listeners for user interactions with the map | 18 | | cursorStyle | string | Map cursor | 19 | | darkStyle | object \| string | Map style when application or browser is in dark mode | 20 | | disableResize | boolean | disable listener for resizing map container | 21 | | debug | boolean | Enable debug messages | 22 | | apikey | string | apikey for vectortile services | 23 | 24 | _\*required_\ 25 | _^default_ 26 | 27 | ## Examples 28 | 29 | ### Static Map 30 | 31 | By default, `MapGL` component renders in a static mode. That means that the user cannot interact with the map. 32 | 33 | ```jsx 34 | import { Component } from 'solid-js' 35 | import MapGL from 'solid-map-gl' 36 | import 'mapbox-gl/dist/mapbox-gl.css' 37 | 38 | const App: Component = () => ( 39 | 46 | ) 47 | ``` 48 | 49 | ### **Interactive Map** 50 | 51 | In most cases, you will want the user to interact with the map. To do this, you need to provide `onViewportChange` handler, that will update the map's viewport state. 52 | 53 | ```jsx 54 | import { Component, createSignal } from "solid-js"; 55 | import MapGL, { Viewport } from "solid-map-gl"; 56 | import 'mapbox-gl/dist/mapbox-gl.css'; 57 | 58 | const App: Component = () => { 59 | const [viewport, setViewport] = createSignal({ 60 | center: [-122.41, 37.78], 61 | zoom: 11, 62 | } as Viewport); 63 | 64 | return ( 65 | setViewport(evt)} 69 | > 70 | ); 71 | }; 72 | ``` 73 | 74 | ### **Changing Map Style** 75 | 76 | ```jsx 77 | import { Component, createSignal } from "solid-js"; 78 | import MapGL, { Viewport } from "solid-map-gl"; 79 | import 'mapbox-gl/dist/mapbox-gl.css'; 80 | 81 | const App: Component = () => { 82 | const [viewport, setViewport] = createSignal({ 83 | center: [-122.45, 37.78], 84 | zoom: 11, 85 | } as Viewport); 86 | const [style, setStyle] = createSignal('basic'); 87 | 88 | return ( 89 | setViewport(evt)} 93 | > 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | ); 104 | }; 105 | ``` 106 | -------------------------------------------------------------------------------- /src/components/MapGL/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Map > renders 1`] = `"
"`; 4 | -------------------------------------------------------------------------------- /src/components/MapGL/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { render } from '@solidjs/testing-library' 3 | import MapGL from '../..' 4 | 5 | describe('Map', () => { 6 | it('renders', () => { 7 | const { container, unmount } = render(() => ( 8 | 9 | )) 10 | expect(container.innerHTML).toMatchSnapshot() 11 | unmount() 12 | }) 13 | 14 | it('renders with static viewport', () => 15 | render(() => )) 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/MapGL/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createSignal, 3 | createEffect, 4 | onMount, 5 | onCleanup, 6 | Component, 7 | on, 8 | } from "solid-js"; 9 | import { MapProvider } from "../MapProvider"; 10 | import { mapEvents } from "../../events"; 11 | import { vectorStyleList } from "../../mapStyles"; 12 | import type { mapEventTypes } from "../../events"; 13 | import type mapboxgl from "mapbox-gl"; 14 | import type { MapboxOptions } from "mapbox-gl/src/ui/map"; 15 | import type { LngLatLike } from "mapbox-gl/src/geo/lng_lat.js"; 16 | import type { LngLatBounds } from "mapbox-gl/src/geo/lng_lat_bounds.js"; 17 | import type { PaddingOptions } from "mapbox-gl/src/geo/edge_insets.js"; 18 | import type { StyleSpecification } from "mapbox-gl/src/style-spec/types.js"; 19 | import type { JSX } from "solid-js"; 20 | 21 | declare global { 22 | interface Window { 23 | MapLib?: any; 24 | } 25 | } 26 | 27 | export type Map = mapboxgl.Map & { 28 | debug: boolean; 29 | debugEvents: boolean; 30 | sourceIdList: string[]; 31 | layerIdList: string[]; 32 | }; 33 | 34 | export type Viewport = { 35 | id?: string; 36 | point?: { x: number; y: number }; 37 | center?: LngLatLike; 38 | bounds?: LngLatBounds; 39 | zoom?: number; 40 | pitch?: number; 41 | bearing?: number; 42 | padding?: PaddingOptions; 43 | inTransit?: boolean; 44 | }; 45 | 46 | type Props = { 47 | /** ID for the map container element */ 48 | id?: string; 49 | /** Map Container CSS Style */ 50 | style?: JSX.CSSProperties; 51 | /** Map Container CSS Class */ 52 | class?: string; 53 | /** SolidJS Class List for Map Container */ 54 | classList?: { 55 | [k: string]: boolean | undefined; 56 | }; 57 | /** Current Map View */ 58 | viewport?: Viewport; 59 | /** Mapbox Options 60 | * @see https://docs.mapbox.com/mapbox-gl-js/api/map/#map-parameters 61 | */ 62 | options?: MapboxOptions; 63 | /** Mapbox Style Configuration 64 | * @see https://docs.mapbox.com/mapbox-gl-js/guides/styles/#configure-a-style 65 | */ 66 | config?: { 67 | id?: string; 68 | lightPreset?: "dawn" | "day" | "dusk" | "night" | string; 69 | showPlaceLabels?: boolean; 70 | showRoadLabels?: boolean; 71 | showPointOfInterestLabels?: boolean; 72 | showTransitLabels?: boolean; 73 | font?: string[]; 74 | [key: string]: boolean | string | string[]; 75 | }; 76 | /** Type for pan, move and zoom transitions */ 77 | transitionType?: "flyTo" | "easeTo" | "jumpTo" | string; 78 | /** Event listener for Viewport updates */ 79 | onViewportChange?: (viewport: Viewport) => void; 80 | /** Event listener for User Interaction */ 81 | onUserInteraction?: (user: boolean) => void; 82 | /** Displays Map Tile Borders */ 83 | showTileBoundaries?: boolean; 84 | /** Displays Wireframe if Terrain is visible */ 85 | showTerrainWireframe?: boolean; 86 | /** Displays Borders if Padding is set */ 87 | showPadding?: boolean; 88 | /** Displays Label Collision Boxes */ 89 | showCollisionBoxes?: boolean; 90 | /** Displays all feature outlines even if normally not drawn by style rules */ 91 | showOverdrawInspector?: boolean; 92 | /** Mouse Cursor Style */ 93 | cursorStyle?: string; 94 | //** Dark Map Style */ 95 | darkStyle?: StyleSpecification | string; 96 | //** Disable automatic map resize */ 97 | disableResize?: boolean; 98 | //** MapLibre library */ 99 | mapLib?: any; 100 | //** APIkey for vector service */ 101 | apikey?: string; 102 | //** Debug Message Mode */ 103 | debug?: boolean; 104 | //** Debug Events */ 105 | debugEvents?: boolean; 106 | ref?: HTMLDivElement; 107 | /** Children within the Map Container */ 108 | children?: any; 109 | } & mapEventTypes; 110 | 111 | /** Creates a new Map Container */ 112 | export const MapGL: Component = (props) => { 113 | let map: Map; 114 | let mapRef: HTMLDivElement; 115 | let resizeObserver: ResizeObserver; 116 | let mutationObserver: MutationObserver; 117 | 118 | const [mapLoaded, setMapLoaded] = createSignal(null); 119 | const [darkMode, setDarkMode] = createSignal( 120 | (typeof window !== "undefined" && 121 | window.matchMedia("(prefers-color-scheme: dark)").matches) || 122 | (typeof document !== "undefined" && 123 | document.body.classList.contains("dark")), 124 | ); 125 | const [internal, setInternal] = createSignal(false); 126 | 127 | const debug = (text: string, value?: any) => 128 | (props.debug || props.debugEvents) && 129 | console.debug("%c[MapGL]", "color: #0ea5e9", text, value || ""); 130 | 131 | const getStyle = (light: any, dark: any) => { 132 | const style = darkMode() && dark ? dark : light; 133 | return typeof style === "string" || style instanceof String 134 | ? style 135 | ?.split(":") 136 | .reduce((p, c) => p && p[c], vectorStyleList) 137 | ?.replace( 138 | "{apikey}", 139 | //@ts-ignore 140 | props.apikey || import.meta.env.VITE_VECTOR_API_KEY, 141 | ) || style 142 | : style; 143 | }; 144 | 145 | onMount(async () => { 146 | let mapLib = props.mapLib || (await import("mapbox-gl")); 147 | if (!mapLib.Map) mapLib = window["maplibregl"] || window["mapboxgl"]; 148 | 149 | if (typeof mapLib.supported === "function" && !mapLib.supported()) 150 | throw new Error("Mapbox GL not supported"); 151 | 152 | debug(`Map (v${mapLib.version}) loading...`); 153 | map = new mapLib.Map({ 154 | accessToken: 155 | //@ts-ignore 156 | props.options?.accessToken || import.meta.env.VITE_MAPBOX_ACCESS_TOKEN, 157 | interactive: props.options?.interactive || !!props.onViewportChange, 158 | ...props.options, 159 | ...props.viewport, 160 | projection: props.options?.projection, 161 | container: mapRef, 162 | style: getStyle(props.options?.style, props.darkStyle), 163 | fitBoundsOptions: { padding: props.viewport?.padding }, 164 | } as MapboxOptions); 165 | 166 | map.debug = props.debug; 167 | map.debugEvents = props.debugEvents; 168 | map.sourceIdList = []; 169 | map.layerIdList = []; 170 | window.MapLib = mapLib; 171 | 172 | // Hook up events 173 | mapEvents.forEach((item) => { 174 | const prop = props[item]; 175 | if (prop) { 176 | const event = item.slice(2).toLowerCase(); 177 | if (typeof prop === "function") { 178 | map.on(event, (evt) => { 179 | setTimeout(() => { 180 | if (evt.clickOnLayer) return; 181 | prop(evt); 182 | props.debugEvents && debug(`Map '${event}' event:`, evt); 183 | }, 0); 184 | }); 185 | } else { 186 | Object.keys(prop).forEach((layerId) => { 187 | map.on(event as any, layerId, (evt) => { 188 | setTimeout(() => { 189 | if (evt.clickOnLayer) return; 190 | prop[layerId](evt); 191 | props.debugEvents && 192 | debug(`Map '${event}' event on '${layerId}':`, evt); 193 | }, 0); 194 | }); 195 | }); 196 | } 197 | } 198 | }); 199 | 200 | map.once("load", () => { 201 | setMapLoaded(map); 202 | debug("Map loaded"); 203 | 204 | // Handle User Interaction 205 | ["mousedown", "touchstart", "wheel"].forEach((event) => 206 | map.on(event, (evt) => !evt.rotate && props.onUserInteraction?.(true)), 207 | ); 208 | ["moveend", "mouseup", "touchend"].forEach((event) => 209 | map.on(event, (evt) => !evt.rotate && props.onUserInteraction?.(false)), 210 | ); 211 | 212 | // Listen to dark theme changes 213 | const darkTheme = 214 | typeof window !== "undefined" && 215 | window?.matchMedia("(prefers-color-scheme: dark)"); 216 | darkTheme?.addEventListener("change", () => { 217 | setDarkMode(darkTheme.matches); 218 | debug("Set dark theme to:", darkTheme.matches?.toString()); 219 | }); 220 | mutationObserver = new MutationObserver(() => { 221 | const darkTheme = document.body.classList.contains("dark"); 222 | setDarkMode(darkTheme); 223 | debug("Set theme to:", darkTheme); 224 | }); 225 | mutationObserver.observe(document.body, { attributes: true }); 226 | 227 | // Listen to map container size changes 228 | if (!props.disableResize) { 229 | resizeObserver = new ResizeObserver(() => { 230 | setTimeout(() => { 231 | map?.resize(); 232 | }, 0); 233 | debug("Map resized"); 234 | }); 235 | resizeObserver.observe(mapRef as Element); 236 | } 237 | 238 | // Update Configuration 239 | createEffect(() => { 240 | for (const key in props.config) { 241 | if (!key || key === "id") continue; 242 | const id = props.config?.id || "basemap"; 243 | const value = props.config[key]; 244 | //@ts-ignore 245 | map?.setConfigProperty(id, key, value); 246 | debug(`Set Config (${id}:${key}) to:`, value); 247 | } 248 | }); 249 | 250 | // Update Viewport 251 | map.on("move", (event) => { 252 | const viewport: Viewport = { 253 | ...props.viewport, 254 | id: props.id, 255 | point: { 256 | x: (event as any).originalEvent?.x, 257 | y: (event as any).originalEvent?.y, 258 | }, 259 | center: props.viewport?.center?.lat 260 | ? map.getCenter() 261 | : [map.getCenter().lng, map.getCenter().lat], 262 | zoom: map.getZoom(), 263 | pitch: map.getPitch(), 264 | bearing: map.getBearing(), 265 | inTransit: true, 266 | bounds: null, 267 | // !internal() 268 | // ? props.viewport?.center?.lat 269 | // ? map.getBounds() 270 | // : [ 271 | // [ 272 | // map.getBounds().getNorthEast().lng, 273 | // map.getBounds().getNorthEast().lat, 274 | // ], 275 | // [ 276 | // map.getBounds().getSouthWest().lng, 277 | // map.getBounds().getSouthWest().lat, 278 | // ], 279 | // ] 280 | // : null, 281 | }; 282 | setInternal(true); 283 | !event.viewport && props.onViewportChange?.(viewport); 284 | }); 285 | 286 | map.on("moveend", (event) => { 287 | !event.rotate && 288 | props.onViewportChange?.({ ...props.viewport, inTransit: false }); 289 | setInternal(false); 290 | }); 291 | }); 292 | }); 293 | 294 | // Hook up viewport event 295 | createEffect( 296 | on( 297 | () => props.viewport, 298 | (vp) => { 299 | if (props.id !== vp?.id || internal()) return; 300 | const viewport = { 301 | ...vp, 302 | ...(vp.bounds 303 | ? map.cameraForBounds(vp.bounds, { 304 | bearing: vp?.bearing, 305 | pitch: vp?.pitch, 306 | padding: vp?.padding, 307 | }) 308 | : null), 309 | }; 310 | map.stop()[props.transitionType || "flyTo"](viewport); 311 | debug( 312 | `Update Viewport (${props.transitionType || "flyTo"}):`, 313 | viewport, 314 | ); 315 | }, 316 | { defer: true }, 317 | ), 318 | ); 319 | 320 | // Update Projection 321 | createEffect(() => { 322 | const proj = props.options?.projection; 323 | if (!map || !proj) return; 324 | map.setProjection(proj); 325 | debug("Set Projection to:", proj); 326 | }); 327 | 328 | // Update Cursor 329 | createEffect(() => { 330 | const cur = props.cursorStyle; 331 | if (!map || !cur) return; 332 | map.getCanvas().style.cursor = cur; 333 | debug("Set Cursor to:", cur); 334 | }); 335 | 336 | const insertLayers = (list, layers) => { 337 | layers.forEach((layer) => { 338 | const index = list.findIndex((i) => 339 | layer.metadata.smg.beforeType 340 | ? i.type === layer.metadata.smg.beforeType 341 | : i.id === layer.metadata.smg.beforeId, 342 | ); 343 | list = 344 | index === -1 345 | ? [...list, layer] 346 | : [...list.slice(0, index), layer, ...list.slice(index + 1)]; 347 | }); 348 | return list; 349 | }; 350 | 351 | // Update map style 352 | createEffect((prev) => { 353 | const style = getStyle(props.options?.style, props.darkStyle); 354 | if (map?.isStyleLoaded() && prev !== style) { 355 | const oldStyle = map.getStyle(); 356 | const oldLayers = oldStyle.layers.filter((l) => 357 | map.layerIdList.includes(l.id), 358 | ); 359 | const oldSources = Object.keys(oldStyle.sources) 360 | .filter((s) => map.sourceIdList.includes(s)) 361 | .reduce((obj, key) => ({ ...obj, [key]: oldStyle.sources[key] }), {}); 362 | 363 | map.setStyle(style); 364 | map.once("styledata", () => { 365 | if (!oldLayers) return; 366 | const newStyle = map.getStyle(); 367 | map.setStyle({ 368 | ...newStyle, 369 | sources: { ...newStyle.sources, ...oldSources }, 370 | layers: insertLayers(newStyle.layers, oldLayers), 371 | fog: oldStyle.fog, 372 | terrain: oldStyle.terrain, 373 | }); 374 | debug("Set Mapstyle to:", style); 375 | }); 376 | } 377 | return style; 378 | }, props.options?.style); 379 | 380 | // Update debug features 381 | [ 382 | "showTileBoundaries", 383 | "showTerrainWireframe", 384 | "showCollisionBoxes", 385 | "showPadding", 386 | "showOverdrawInspector", 387 | ].forEach((item) => { 388 | createEffect(() => { 389 | const prop = props[item]; 390 | if (!map || !prop) return; 391 | map[item] = prop; 392 | debug(`Set ${item} to:`, prop); 393 | }); 394 | }); 395 | 396 | onCleanup(() => { 397 | resizeObserver?.disconnect(); 398 | mutationObserver?.disconnect(); 399 | map?.remove(); 400 | debug("Map removed"); 401 | }); 402 | 403 | return ( 404 |
419 | {mapLoaded() && ( 420 | 421 | 422 |
{props.children}
423 |
424 | )} 425 |
426 | ); 427 | }; 428 | -------------------------------------------------------------------------------- /src/components/MapProvider/index.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, ParentComponent } from 'solid-js' 2 | import { createStore } from 'solid-js/store' 3 | import type { Map } from '../MapGL' 4 | 5 | const [state, setState] = createStore({ map: null }) 6 | 7 | export const MapContext = createContext([state]) 8 | 9 | export const useMapContext = () => useContext(MapContext) 10 | 11 | export const MapProvider: ParentComponent<{ 12 | map?: Map 13 | }> = (props) => { 14 | props.map && setState('map', props.map) 15 | 16 | return ( 17 | {props.children} 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Marker/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Marker Component 3 | --- 4 | 5 | # Marker 6 | 7 | ## Props 8 | 9 | | Prop | Type | Description | 10 | | ----------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | 11 | | options | object | [Marker Parameters](https://docs.mapbox.com/mapbox-gl-js/api/markers/#marker-parameters) | 12 | | popup | object | [Popup Parameters](https://docs.mapbox.com/mapbox-gl-js/api/markers/#popup-parameters) | 13 | | lngLat\* | [LngLatLike](https://docs.mapbox.com/mapbox-gl-js/api/geography/#lnglatlike) | Marker Location | 14 | | showPopup | boolean | Is Popup showing | 15 | | children | [HTML Element \| String](https://developer.mozilla.org/en-US/docs/Web/API/Element) | Popup Content | 16 | | onOpen | function | Called when Popup opens | 17 | | onClose | function | Called when Popup closes | 18 | | onDragStart | function | Called when Marker drag starts | 19 | | onDragEnd | function | Called when Marker finshed dragging | 20 | | onDrag | function | [LngLatLike](https://docs.mapbox.com/mapbox-gl-js/api/geography/#lnglatlike) position when Marker is dragged | 21 | 22 | _\*required_ 23 | 24 | ## Example 25 | 26 | ```jsx 27 | import { Component, createSignal } from "solid-js"; 28 | import MapGL, { Viewport, Marker } from "solid-map-gl"; 29 | import 'mapbox-gl/dist/mapbox-gl.css'; 30 | 31 | const App: Component = () => { 32 | const [viewport, setViewport] = createSignal({ 33 | center: [0, 52], 34 | zoom: 6, 35 | } as Viewport); 36 | 37 | return ( 38 | setViewport(evt)} 42 | > 43 | 44 | Hi there! 👋 45 | 46 | 47 | ); 48 | }; 49 | ``` 50 | -------------------------------------------------------------------------------- /src/components/Marker/index.tsx: -------------------------------------------------------------------------------- 1 | import { onCleanup, createEffect, Component, splitProps } from 'solid-js' 2 | import { useMapContext } from '../MapProvider' 3 | import type { 4 | Popup as PopupType, 5 | PopupOptions, 6 | Marker as MarkerType, 7 | MarkerOptions, 8 | LngLatLike, 9 | } from 'mapbox-gl' 10 | 11 | type Props = { 12 | /** The geographical location to place the marker */ 13 | lngLat: LngLatLike 14 | /** Options for the Mapbox GL JS marker */ 15 | options?: MarkerOptions 16 | /** Options for the Mapbox GL JS popup that is associated with the marker */ 17 | popup?: PopupOptions 18 | /** Whether the marker's associated popup should be open */ 19 | showPopup?: boolean 20 | /** Whether the marker is draggable */ 21 | draggable?: boolean 22 | /** A function that is called when the marker's associated popup is opened */ 23 | onOpen?: () => void 24 | /** A function that is called when the marker's associated popup is closed */ 25 | onClose?: () => void 26 | /** A function that is called when the marker starts being dragged */ 27 | onDragStart?: () => void 28 | /** A function that is called when the marker stops being dragged */ 29 | onDragEnd?: () => void 30 | /** A function that is called when the marker is being dragged */ 31 | onDrag?: (lngLat: number[]) => void 32 | /** The content to be displayed in the marker's associated popup */ 33 | children?: any 34 | } 35 | 36 | export const Marker: Component = (props: Props) => { 37 | if (!props.lngLat) throw new Error('Marker - lngLat is required') 38 | const [update, create_popup, create_marker] = splitProps( 39 | props, 40 | ['lngLat', 'children', 'showPopup', 'draggable'], 41 | ['popup', 'onOpen', 'onClose'] 42 | ) 43 | 44 | const [ctx] = useMapContext() 45 | let marker: MarkerType = null 46 | let popup: PopupType = null 47 | const LNGLAT = update.lngLat 48 | 49 | // Add or Update Popup 50 | createEffect(() => { 51 | if (!ctx.map) return 52 | popup?.remove() 53 | popup = new window.MapLib.Popup({ 54 | closeOnClick: false, 55 | focusAfterOpen: false, 56 | ...create_popup.popup, 57 | }) 58 | .on('open', () => create_popup.onOpen?.()) 59 | .on('close', () => create_popup.onClose?.()) 60 | 61 | // Update Popup Content 62 | createEffect(() => { 63 | if (update.children === undefined) return 64 | typeof update.children === 'string' 65 | ? popup?.setHTML(update.children) 66 | : popup?.setDOMContent(update.children) 67 | marker?.setPopup(popup) 68 | }) 69 | 70 | // Toggle Popup 71 | createEffect( 72 | () => update.showPopup !== popup?.isOpen() && marker?.togglePopup() 73 | ) 74 | }) 75 | 76 | // Add or Update Marker 77 | createEffect(() => { 78 | if (!ctx.map) return 79 | marker?.remove() 80 | marker = new window.MapLib.Marker(create_marker.options) 81 | .on('dragstart', () => create_marker.onDragStart?.()) 82 | .on('dragend', () => create_marker.onDragEnd?.()) 83 | .on('drag', () => create_marker.onDrag?.(marker?.getLngLat().toArray())) 84 | .setPopup(popup) 85 | .setLngLat(LNGLAT) 86 | .addTo(ctx.map) 87 | 88 | // Toggle Popup 89 | createEffect( 90 | () => update.showPopup !== popup?.isOpen() && marker?.togglePopup() 91 | ) 92 | }) 93 | 94 | // Update Position 95 | createEffect(() => marker?.setLngLat(update.lngLat)) 96 | 97 | // Update Draggable 98 | createEffect(() => marker?.setDraggable(update.draggable)) 99 | 100 | // Remove Marker 101 | onCleanup(() => marker?.remove()) 102 | 103 | return null 104 | } 105 | -------------------------------------------------------------------------------- /src/components/Popup/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Popup Component 3 | --- 4 | 5 | # Popup 6 | 7 | ## Props 8 | 9 | | Name | Type | Description | 10 | | ------------ | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | 11 | | options | object | [Popup Parameters](https://docs.mapbox.com/mapbox-gl-js/api/markers/#popup-parameters) | 12 | | lngLat\* | [LngLatLike](https://docs.mapbox.com/mapbox-gl-js/api/geography/#lnglatlike) | Popup Location (required if trackPointer is false) | 13 | | trackPointer | boolean | Track Popup to mouse cursor | 14 | | children | [HTML Element \| String](https://developer.mozilla.org/en-US/docs/Web/API/Element) | Popup Content | 15 | | onOpen | function | Called when popup opens | 16 | | onClose | function | Called when popup closes | 17 | 18 | _\*required_ 19 | 20 | ## Example 21 | 22 | ```jsx 23 | import { Component, createSignal } from "solid-js"; 24 | import MapGL, { Viewport, Popup } from "solid-map-gl"; 25 | import 'mapbox-gl/dist/mapbox-gl.css'; 26 | 27 | const App: Component = () => { 28 | const [viewport, setViewport] = createSignal({ 29 | center: [0, 52], 30 | zoom: 6, 31 | } as Viewport); 32 | 33 | return ( 34 | setViewport(evt)} 38 | > 39 | 40 | Hi there! 👋 41 | 42 | 43 | ); 44 | }; 45 | ``` 46 | -------------------------------------------------------------------------------- /src/components/Popup/index.tsx: -------------------------------------------------------------------------------- 1 | import { onCleanup, createEffect, Component, splitProps } from "solid-js"; 2 | import { useMapContext } from "../MapProvider"; 3 | import type { Popup as PopupType, PopupOptions, LngLatLike } from "mapbox-gl"; 4 | 5 | type Props = { 6 | /** Options for configuring the popup */ 7 | options?: PopupOptions; 8 | /** Flag for tracking the mouse pointer */ 9 | trackPointer?: boolean; 10 | /** Longitude and latitude for the popup */ 11 | lngLat?: LngLatLike; 12 | /** Callback for when the popup is opened */ 13 | onOpen?: () => void; 14 | /** Callback for when the popup is closed */ 15 | onClose?: () => void; 16 | /** Children to display within the popup */ 17 | children?: any; 18 | }; 19 | 20 | export const Popup: Component = (props: Props) => { 21 | if (!props.trackPointer && !props.lngLat) 22 | throw new Error("Popup - lngLat or trackPointer is required"); 23 | 24 | const [update, create] = splitProps(props, [ 25 | "lngLat", 26 | "children", 27 | "trackPointer", 28 | ]); 29 | const [ctx] = useMapContext(); 30 | let popup: PopupType | undefined; 31 | 32 | // Update Popup 33 | createEffect(() => { 34 | if (!ctx.map) return; 35 | popup?.remove(); 36 | popup = new window.MapLib.Popup({ 37 | closeOnClick: false, 38 | focusAfterOpen: false, 39 | ...create.options, 40 | }) 41 | .on("open", () => create.onOpen?.()) 42 | .on("close", () => create.onClose?.()) 43 | .addTo(ctx.map); 44 | 45 | // Update Position 46 | createEffect(() => 47 | update.trackPointer 48 | ? popup.trackPointer() 49 | : popup.setLngLat(update.lngLat), 50 | ); 51 | 52 | // Update Content 53 | createEffect(() => 54 | typeof update.children === "string" 55 | ? popup.setHTML(update.children) 56 | : popup.setDOMContent(update.children), 57 | ); 58 | }); 59 | 60 | // Remove Popup 61 | onCleanup(() => popup?.remove()); 62 | 63 | return null; 64 | }; 65 | -------------------------------------------------------------------------------- /src/components/Source/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Source Component 3 | --- 4 | 5 | # Source 6 | 7 | ## Props 8 | 9 | | Name | Type | Description | 10 | | -------- | ------ | ----------------------------------------------------------------------------- | 11 | | id | string | required if Layer components are not nested | 12 | | source\* | object | [Source style specs](https://docs.mapbox.com/mapbox-gl-js/style-spec/sources) | 13 | 14 | _\*required_\ 15 | _^default_ 16 | 17 | ## Examples 18 | 19 | ### GeoJSON URL Source 20 | 21 | ```jsx 22 | import { Component, createSignal } from "solid-js"; 23 | import MapGL, { Viewport, Source, Layer } from "solid-map-gl"; 24 | import 'mapbox-gl/dist/mapbox-gl.css'; 25 | 26 | const App: Component = () => { 27 | const [viewport, setViewport] = createSignal({ 28 | center: [-122.45, 37.78], 29 | zoom: 8, 30 | } as Viewport); 31 | 32 | return ( 33 | setViewport(evt)} 37 | > 38 | 44 | 54 | 55 | 56 | ); 57 | }; 58 | ``` 59 | 60 | ### GeoJSON Source 61 | 62 | ```jsx 63 | import { Component, createSignal } from "solid-js"; 64 | import MapGL, { Viewport, Source, Layer } from "solid-map-gl"; 65 | import 'mapbox-gl/dist/mapbox-gl.css'; 66 | 67 | const data = { 68 | type: 'Feature', 69 | geometry: { 70 | type: 'LineString', 71 | coordinates: [ 72 | [-122.48369693756104, 37.83381888486939], 73 | [-122.48348236083984, 37.83317489144141], 74 | [-122.48339653015138, 37.83270036637107], 75 | [-122.48356819152832, 37.832056363179625], 76 | [-122.48404026031496, 37.83114119107971], 77 | [-122.48404026031496, 37.83049717427869], 78 | [-122.48348236083984, 37.829920943955045], 79 | [-122.48356819152832, 37.82954808664175], 80 | [-122.48507022857666, 37.82944639795659], 81 | [-122.48610019683838, 37.82880236636284], 82 | [-122.48695850372314, 37.82931081282506], 83 | [-122.48700141906738, 37.83080223556934], 84 | [-122.48751640319824, 37.83168351665737], 85 | [-122.48803138732912, 37.832158048267786], 86 | [-122.48888969421387, 37.83297152392784], 87 | [-122.48987674713133, 37.83263257682617], 88 | [-122.49043464660643, 37.832937629287755], 89 | [-122.49125003814696, 37.832429207817725], 90 | [-122.49163627624512, 37.832564787218985], 91 | [-122.49223709106445, 37.83337825839438], 92 | [-122.49378204345702, 37.83368330777276], 93 | ], 94 | }, 95 | }; 96 | 97 | const App: Component = () => { 98 | const [viewport, setViewport] = createSignal({ 99 | center: [-122.486052, 37.830348], 100 | zoom: 15, 101 | } as Viewport); 102 | 103 | return ( 104 | setViewport(evt)} 108 | > 109 | 115 | 128 | 129 | 130 | ); 131 | }; 132 | ``` 133 | 134 | ### Vector Source 135 | 136 | ```jsx 137 | import { Component, createSignal } from "solid-js"; 138 | import MapGL, { Viewport, Source, Layer } from "solid-map-gl"; 139 | import 'mapbox-gl/dist/mapbox-gl.css'; 140 | 141 | const App: Component = () => { 142 | const [viewport, setViewport] = createSignal({ 143 | center: [-122.447303, 37.753574], 144 | zoom: 13, 145 | } as Viewport); 146 | 147 | return ( 148 | setViewport(evt)} 152 | > 153 | 159 | 169 | 170 | 171 | ); 172 | }; 173 | ``` 174 | -------------------------------------------------------------------------------- /src/components/Source/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | onCleanup, 3 | createEffect, 4 | Component, 5 | createContext, 6 | useContext, 7 | createUniqueId, 8 | } from 'solid-js' 9 | import { useMapContext } from '../MapProvider' 10 | import type { SourceSpecification } from 'mapbox-gl/src/style-spec/types.js' 11 | import { rasterStyleList } from '../../mapStyles' 12 | 13 | const SourceContext = createContext() 14 | export const useSourceId = (): string => useContext(SourceContext) 15 | 16 | type Props = { 17 | /** @optional Source ID for referencing non nexted Layers */ 18 | id?: string 19 | /** @see https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/ */ 20 | source: SourceSpecification 21 | /** The map layers associated with this source */ 22 | children?: any 23 | } 24 | 25 | export const Source: Component = props => { 26 | const [ctx] = useMapContext() 27 | props.id ??= createUniqueId() 28 | 29 | const debug = (text, value?) => { 30 | ctx.map.debug && 31 | console.debug('%c[MapGL]', 'color: #ec4899', text, value || '') 32 | } 33 | 34 | const lookup = url => { 35 | const s = url?.split(':').reduce((p, c) => p && p[c], rasterStyleList) 36 | const source = s 37 | ? { 38 | ...props.source, 39 | url: '', 40 | tiles: [ 41 | s 42 | .replace( 43 | '{apikey}', //@ts-ignore 44 | props.source.apikey || import.meta.env.VITE_RASTER_API_KEY 45 | ) 46 | .replace('{r}', window.devicePixelRatio > 1 ? '@2x' : ''), 47 | ], 48 | attribution: rasterStyleList[url.split(':')[0]]._copy, 49 | } 50 | : props.source 51 | 52 | source.tiles && 53 | (source.tiles = ['a', 'b', 'c'].map(i => 54 | source.tiles[0].replace('{s}', i) 55 | )) 56 | 57 | return source 58 | } 59 | 60 | // Add Source 61 | ctx.map.addSource(props.id, lookup(props.source.url)) 62 | ctx.map.sourceIdList.push(props.id) 63 | debug('Add Source:', props.id) 64 | 65 | // Update Data 66 | const source = ctx.map.getSource(props.id) 67 | switch (props.source.type) { 68 | case 'geojson': 69 | createEffect(() => { 70 | const data = props.source.data 71 | if (!ctx.map.isSourceLoaded(props.id)) return 72 | source.setData(data || {}) 73 | debug('Update GeoJSON Data:', props.id) 74 | }) 75 | break 76 | case 'image': 77 | createEffect(() => { 78 | const url = props.source.url 79 | const coords = props.source.coordinates 80 | if (!ctx.map.isSourceLoaded(props.id)) return 81 | source.updateImage(url, coords) 82 | debug('Update Image Data:', props.id) 83 | }) 84 | break 85 | case 'vector': 86 | createEffect(() => { 87 | const url = props.source.url 88 | const tiles = props.source.tiles 89 | if (!ctx.map.isSourceLoaded(props.id)) return 90 | url ? source.setUrl(url) : source.setTiles(tiles) 91 | debug('Update Vector Data:', props.id) 92 | }) 93 | break 94 | case 'raster': 95 | createEffect(() => { 96 | const src = lookup(props.source.url) 97 | if (!ctx.map.isSourceLoaded(props.id)) return 98 | src.url ? source.setUrl(src.url) : source.setTiles(src.tiles) 99 | debug('Update Raster Data:', props.id) 100 | }) 101 | break 102 | } 103 | 104 | // Remove Source 105 | onCleanup(() => { 106 | ctx.map 107 | ?.getStyle() 108 | .layers.forEach( 109 | layer => layer.source === props.id && ctx.map.removeLayer(layer.id) 110 | ) 111 | ctx.map?.getSource(props.id) && ctx.map?.removeSource(props.id) 112 | debug('Remove Source:', props.id) 113 | }) 114 | 115 | return ( 116 | 117 | {props.children} 118 | 119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /src/components/Terrain/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Terrain Layer Component 3 | --- 4 | 5 | # Terrain 6 | 7 | ## Props 8 | 9 | | Name | Type | Description | 10 | | ------------ | ------ | ------------------------------------------------------------------------------- | 11 | | exaggeration | number | [Terrain style specs](https://docs.mapbox.com/mapbox-gl-js/style-spec/terrain/) | 12 | 13 | _\*required_ 14 | 15 | ## Example 16 | 17 | If no source is defined then the default Mapbox / MapLibre DEM will be used. Both examples give the same result. 18 | 19 | ```jsx 20 | import { Component, createSignal } from "solid-js"; 21 | import MapGL, { Viewport, Source, Terrain } from "solid-map-gl"; 22 | import 'mapbox-gl/dist/mapbox-gl.css'; 23 | 24 | const App: Component = () => { 25 | const [viewport, setViewport] = createSignal({ 26 | center: [138.74, 35.3], 27 | zoom: 11, 28 | pitch: 70, 29 | } as Viewport); 30 | 31 | return ( 32 | setViewport(evt)} 36 | > 37 | 38 | 39 | ); 40 | }; 41 | ``` 42 | 43 | ### OR 44 | 45 | ```jsx 46 | import { Component, createSignal } from "solid-js"; 47 | import MapGL, { Viewport, Source, Terrain } from "solid-map-gl"; 48 | import 'mapbox-gl/dist/mapbox-gl.css'; 49 | 50 | const App: Component = () => { 51 | const [viewport, setViewport] = createSignal({ 52 | center: [138.74, 35.3], 53 | zoom: 11, 54 | pitch: 70, 55 | } as Viewport); 56 | 57 | return ( 58 | setViewport(evt)} 62 | > 63 | 71 | 72 | 73 | 74 | ); 75 | }; 76 | ``` 77 | -------------------------------------------------------------------------------- /src/components/Terrain/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'vitest' 2 | import { render } from '@solidjs/testing-library' 3 | import MapGL, { Terrain } from '../..' 4 | 5 | describe('Terrain', () => { 6 | it('renders', () => 7 | render(() => ( 8 | // 9 | 10 | // 11 | ))) 12 | }) 13 | -------------------------------------------------------------------------------- /src/components/Terrain/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createEffect, 3 | VoidComponent, 4 | createUniqueId, 5 | onCleanup, 6 | } from 'solid-js' 7 | import { useMapContext } from '../MapProvider' 8 | import { useSourceId } from '../Source' 9 | 10 | interface TerrainSpecification { 11 | source?: string 12 | exaggeration?: number | undefined 13 | } 14 | 15 | export const Terrain: VoidComponent = ( 16 | props?: TerrainSpecification 17 | ) => { 18 | const [ctx] = useMapContext() 19 | let sourceId: string = useSourceId() 20 | 21 | // Add Terrain Source if not already defined 22 | if (!sourceId && !props?.source) { 23 | sourceId = createUniqueId() 24 | ctx.map.addSource(sourceId, { 25 | type: 'raster-dem', 26 | url: ctx.map.isMapLibre 27 | ? 'https://demotiles.maplibre.org/terrain-tiles/tiles.json' 28 | : 'mapbox://mapbox.terrain-rgb', 29 | tileSize: ctx.map.isMapLibre ? 256 : 512, 30 | maxzoom: ctx.map.isMapLibre ? undefined : 14, 31 | }) 32 | ctx.map.sourceIdList.push(sourceId) 33 | } 34 | 35 | // Add or Update Terrain Layer 36 | createEffect(() => { 37 | ctx.map.setTerrain({ 38 | exaggeration: props.exaggeration || 1, 39 | source: props?.source || sourceId, 40 | }) 41 | }) 42 | 43 | //Remove Terrain 44 | onCleanup(() => ctx.map?.setTerrain(null)) 45 | 46 | return null 47 | } 48 | -------------------------------------------------------------------------------- /src/events.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | MapMouseEvent, 3 | MapTouchEvent, 4 | MapWheelEvent, 5 | MapDataEvent, 6 | MapBoxZoomEvent, 7 | } from 'mapbox-gl' 8 | 9 | type mapEventTypes = { 10 | onMouseDown?: (event: MapMouseEvent) => void 11 | onMouseUp?: (event: MapMouseEvent) => void 12 | onMouseOver?: (event: MapMouseEvent) => void 13 | onMouseOut?: (event: MapMouseEvent) => void 14 | onMouseMove?: (event: MapMouseEvent) => void 15 | onMouseEnter?: (event: MapMouseEvent) => void 16 | onMouseLeave?: (event: MapMouseEvent) => void 17 | onPreClick?: (event: MapMouseEvent) => void 18 | onClick?: (event: MapMouseEvent) => void 19 | onDblClick?: (event: MapMouseEvent) => void 20 | onContextMenu?: (event: MapMouseEvent) => void 21 | onTouchStart?: (event: MapTouchEvent) => void 22 | onTouchEnd?: (event: MapTouchEvent) => void 23 | onTouchCancel?: (event: MapTouchEvent) => void 24 | onTouchMove?: (event: MapTouchEvent) => void 25 | onWheel?: (event: MapWheelEvent) => void 26 | onResize?: () => void 27 | onRemove?: () => void 28 | onMoveStart?: (event: DragEvent) => void 29 | onMove?: (event: MapMouseEvent | MapTouchEvent) => void 30 | onMoveEnd?: (event: DragEvent) => void 31 | onDragStart?: (event: DragEvent) => void 32 | onDrag?: (event: MapMouseEvent | MapTouchEvent) => void 33 | onDragEnd?: (event: DragEvent) => void 34 | onZoomStart?: (event: MapMouseEvent | MapTouchEvent) => void 35 | onZoom?: (event: MapMouseEvent | MapTouchEvent) => void 36 | onZoomEnd?: (event: MapMouseEvent | MapTouchEvent) => void 37 | onRotateStart?: (event: MapMouseEvent | MapTouchEvent) => void 38 | onRotate?: (event: MapMouseEvent | MapTouchEvent) => void 39 | onRotatEnd?: (event: MapMouseEvent | MapTouchEvent) => void 40 | onPitchStart?: (event: MapDataEvent) => void 41 | onPitch?: (event: MapDataEvent) => void 42 | onPitchEnd?: (event: MapDataEvent) => void 43 | onBoxZoomStart?: (event: MapBoxZoomEvent) => void 44 | onBoxZoomEnd?: (event: MapBoxZoomEvent) => void 45 | onBoxZoomCancel?: (event: MapBoxZoomEvent) => void 46 | onWebglContextLost?: () => void 47 | onWebglContextRestored?: () => void 48 | onLoad?: (event: any) => void 49 | onRender?: () => void 50 | onIdle?: () => void 51 | onError?: (error: string) => void 52 | onData?: (event: MapDataEvent) => void 53 | onStyleData?: (event: MapDataEvent) => void 54 | onSourceData?: (event: MapDataEvent) => void 55 | onDataLoading?: (event: MapDataEvent) => void 56 | onStyleDataLoading?: (event: MapDataEvent) => void 57 | onSourceDataLoading?: (event: MapDataEvent) => void 58 | onStyleImageMissing?: (event: MapDataEvent) => void 59 | } 60 | 61 | const mapEvents: string[] = [ 62 | 'onMouseDown', 63 | 'onMouseUp', 64 | 'onMouseOver', 65 | 'onMouseOut', 66 | 'onMouseMove', 67 | 'onMouseEnter', 68 | 'onMouseLeave', 69 | 'onPreClick', 70 | 'onClick', 71 | 'onDblClick', 72 | 'onContextMenu', 73 | 'onTouchStart', 74 | 'onTouchEnd', 75 | 'onTouchCancel', 76 | 'onWheel', 77 | 'onResize', 78 | 'onRemove', 79 | 'onTouchMove', 80 | 'onMoveStart', 81 | 'onMove', 82 | 'onMoveEnd', 83 | 'onDragStart', 84 | 'onDrag', 85 | 'onDragEnd', 86 | 'onZoomStart', 87 | 'onZoom', 88 | 'onZoomEnd', 89 | 'onRotateStart', 90 | 'onRotate', 91 | 'onRotatEnd', 92 | 'onPitchStart', 93 | 'onPitch', 94 | 'onPitchEnd', 95 | 'onBoxZoomStart', 96 | 'onBoxZoomEnd', 97 | 'onBoxZoomCancel', 98 | 'onWebglContextLost', 99 | 'onWebglContextRestored', 100 | 'onLoad', 101 | 'onRender', 102 | 'onIdle', 103 | 'onError', 104 | 'onData', 105 | 'onStyleData', 106 | 'onSourceData', 107 | 'onDataLoading', 108 | 'onStyleDataLoading', 109 | 'onSourceDataLoading', 110 | 'onStyleImageMissing', 111 | ] 112 | 113 | type layerEventTypes = { 114 | onMouseDown?: (event: MapMouseEvent) => void 115 | /** called when the mouse button is pressed down on the layer */ 116 | onMouseUp?: (event: MapMouseEvent) => void 117 | /** called when the mouse button is released after a mouse down event on the layer */ 118 | onMouseOver?: (event: MapMouseEvent) => void 119 | /** called when the mouse enters the layer */ 120 | onMouseOut?: (event: MapMouseEvent) => void 121 | /** called when the mouse leaves the layer */ 122 | onMouseMove?: (event: MapMouseEvent) => void 123 | /** called when the mouse is moved over the layer */ 124 | onMouseEnter?: (event: MapMouseEvent) => void 125 | /** called when the mouse enters the layer */ 126 | onMouseLeave?: (event: MapMouseEvent) => void 127 | /** called when the mouse leaves the layer */ 128 | onClick?: (event: MapMouseEvent) => void 129 | /** called when the layer is clicked */ 130 | onDblClick?: (event: MapMouseEvent) => void 131 | /** called when the layer is double-clicked */ 132 | onContextMenu?: (event: MapMouseEvent) => void 133 | /** called when the context menu is triggered on the layer */ 134 | onTouchStart?: (event: MapTouchEvent) => void 135 | /** called when a touch event is started on the layer */ 136 | onTouchEnd?: (event: MapTouchEvent) => void 137 | /** called when a touch event is ended on the layer */ 138 | onTouchCancel?: (event: MapTouchEvent) => void 139 | /** called when a touch event is cancelled on the layer */ 140 | } 141 | 142 | const layerEvents: any[] = [ 143 | 'onMouseDown', 144 | 'onMouseUp', 145 | 'onMouseOver', 146 | 'onMouseOut', 147 | 'onMouseMove', 148 | 'onMouseEnter', 149 | 'onMouseLeave', 150 | 'onClick', 151 | 'onDblClick', 152 | 'onContextMenu', 153 | 'onTouchStart', 154 | 'onTouchEnd', 155 | 'onTouchCancel', 156 | ] 157 | 158 | const drawEvents: any[] = [ 159 | 'onCreate', 160 | 'onDelete', 161 | 'onCombine', 162 | 'onUncombine', 163 | 'onUpdate', 164 | 'onSelectionchange', 165 | 'onModechange', 166 | 'onRender', 167 | 'onActionable', 168 | ] 169 | 170 | type drawEventTypes = { 171 | onCreate?: (event: Object) => void 172 | /** called when a feature is created */ 173 | onDelete?: (event: Object) => void 174 | /** called when a feature is deleted */ 175 | onCombine?: (event: Object) => void 176 | /** called when features are combined */ 177 | onUncombine?: (event: Object) => void 178 | /** called when features are uncombined */ 179 | onUpdate?: (event: Object) => void 180 | /** called when a feature is updated */ 181 | onSelectionchange?: (event: Object) => void 182 | /** called when the selected features change */ 183 | onModechange?: (event: Object) => void 184 | /** called when the draw mode changes */ 185 | onRender?: (event: Object) => void 186 | /** called when the draw canvas is re-rendered */ 187 | onActionable?: (event: Object) => void 188 | /** called when the state of the draw controls changes */ 189 | } 190 | 191 | export { mapEvents, layerEvents, drawEvents } 192 | export type { mapEventTypes, layerEventTypes, drawEventTypes } 193 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { MapGL as default } from "./components/MapGL"; 2 | export { MapProvider, useMapContext } from "./components/MapProvider"; 3 | export { Source, useSourceId } from "./components/Source"; 4 | export { Layer } from "./components/Layer"; 5 | export { Layer3D, useScene } from "./components/Layer3D"; 6 | export { Atmosphere } from "./components/Atmosphere"; 7 | export { Terrain } from "./components/Terrain"; 8 | export { Control } from "./components/Control"; 9 | export { Marker } from "./components/Marker"; 10 | export { Popup } from "./components/Popup"; 11 | export { MGL_Image as Image, patternList } from "./components/Image"; 12 | export { Camera } from "./components/Camera"; 13 | export { Light } from "./components/Light"; 14 | export { Draw } from "./components/Draw"; 15 | export type { Viewport } from "./components/MapGL"; 16 | export type { Color } from "./components/Image"; 17 | -------------------------------------------------------------------------------- /src/mapStyles.ts: -------------------------------------------------------------------------------- 1 | const mapBase = 'mapbox://styles/mapbox' 2 | const gitBase = 'https://raw.githubusercontent.com/' 3 | const gishub = 'https://raw.githubusercontent.com/GIShub4/map-styles/main/' 4 | const hereBase = 'https://assets.vector.hereapi.com/styles/' 5 | 6 | export const vectorStyleList: object = { 7 | mb: { 8 | standard: `${mapBase}/standard`, 9 | light: `${mapBase}/light-v11`, 10 | dark: `${mapBase}/dark-v11`, 11 | street: `${mapBase}/streets-v12`, 12 | outdoor: `${mapBase}/outdoors-v12`, 13 | sat: `${mapBase}/satellite-v9`, 14 | sat_street: `${mapBase}/satellite-streets-v12`, 15 | nav_base: `${mapBase}/navigation-guidance-day-v4`, 16 | nav_base_night: `${mapBase}/navigation-guidance-night-v4`, 17 | nav: `${mapBase}/navigation-day-v1`, 18 | nav_night: `${mapBase}/navigation-night-v1`, 19 | basic: `${mapBase}/cjf4m44iw0uza2spb3q0a7s41`, 20 | monochrome: `${mapBase}/cjv6rzz4j3m4b1fqcchuxclhb`, 21 | leshine: `${mapBase}/cjcunv5ae262f2sm9tfwg8i0w`, 22 | icecream: `${mapBase}/cj7t3i5yj0unt2rmt3y4b5e32`, 23 | cali: `${mapBase}/cjerxnqt3cgvp2rmyuxbeqme7`, 24 | northstar: `${mapBase}/cj44mfrt20f082snokim4ungi`, 25 | mineral: `${mapBase}/cjtep62gq54l21frr1whf27ak`, 26 | moonlight: `${mapBase}/cj3kbeqzo00022smj7akz3o1e`, 27 | frank: `${mapBase}-map-design/ckshxkppe0gge18nz20i0nrwq`, 28 | minimo: `${mapBase}-map-design/cksjc2nsq1bg117pnekb655h1`, 29 | decimal: `${mapBase}-map-design/ck4014y110wt61ctt07egsel6`, 30 | stand: `${mapBase}-map-design/ckr0svm3922ki18qntevm857n`, 31 | blueprint: `${mapBase}-map-design/cks97e1e37nsd17nzg7p0308g`, 32 | bubble: `${mapBase}-map-design/cksysy2nl62zp17quosctdtcc`, 33 | pencil: `${mapBase}-map-design/cks9iema71es417mlrft4go2k`, 34 | swiss_ski: `${gitBase}mapbox/mapbox-gl-swiss-ski-style/master/cij1zoclj002y8rkkdjl69psd.json`, 35 | vintage: `${gitBase}mapbox/mapbox-gl-vintage-style/master/cif5p01n202nisaktvljx9mv3.json`, 36 | whaam: `${gitBase}mapbox/mapbox-gl-whaam-style/master/cii8323c8004w0nlvtss3dbm2.json`, 37 | neon: `${gitBase}NatEvatt/awesome-mapbox-gl-styles/master/styles/Neon/style.json`, 38 | camoflauge: `${gitBase}jingsam/mapbox-gl-styles/master/Camouflage.json`, 39 | emerald: `${gitBase}jingsam/mapbox-gl-styles/master/Emerald.json`, 40 | runner: `${gitBase}jingsam/mapbox-gl-styles/master/Runner.json`, 41 | x_ray: `${gitBase}jingsam/mapbox-gl-styles/master/X-ray.json`, 42 | }, 43 | here: { 44 | base: `${hereBase}berlin/base/mapbox/tilezen?apikey={apikey}`, 45 | day: `${hereBase}berlin/day/mapbox/tilezen?apikey={apikey}`, 46 | night: `${hereBase}berlin/night/mapbox/tilezen?apikey={apikey}`, 47 | }, 48 | esri: { 49 | blueprint: `${gishub}esri:blueprint.json`, 50 | charted_territory: `${gishub}esri:charted-territory.json`, 51 | colored_pencil: `${gishub}esri:colored-pencil.json`, 52 | community: `${gishub}esri:community.json`, 53 | mid_century: `${gishub}esri:mid-century.json`, 54 | modern_antique: `${gishub}esri:modern-antique.json`, 55 | nat_geo: `${gishub}esri:national-geographic.json`, 56 | newspaper: `${gishub}esri:newspaper.json`, 57 | open_street_map: `${gishub}esri:open-street-map.json`, 58 | light_gray_canvas: `${gishub}esri:light-gray-canvas.json`, 59 | dark_gray_canvas: `${gishub}esri:dark-gray-canvas.json`, 60 | human_geo_light: `${gishub}esri:human-geography-light.json`, 61 | human_geo_dark: `${gishub}esri:human-geography-dark.json`, 62 | world_navigation: `${gishub}esri:world-navigation.json`, 63 | world_street: `${gishub}esri:world-street.json`, 64 | world_street_night: `${gishub}esri:world-street-night.json`, 65 | world_terrain: `${gishub}esri:world-terrain.json`, 66 | world_terrain_hybrid: `${gishub}esri:world-terrain-hybrid.json`, 67 | world_topographic: `${gishub}esri:world-topographic.json`, 68 | chromium: `${gishub}esri:chromium.json`, 69 | dreamcatcher: `${gishub}esri:dreamcatcher.json`, 70 | seahaven: `${gishub}esri:seahaven.json`, 71 | sangria: `${gishub}esri:sangria.json`, 72 | mercurial: `${gishub}esri:mercurial.json`, 73 | imagery: `${gishub}esri:imagery.json`, 74 | imagery_hybrid: `${gishub}esri:imagery-hybrid.json`, 75 | firefly: `${gishub}esri:firefly.json`, 76 | firefly_hybrid: `${gishub}esri:firefly-hybrid.json`, 77 | oceans: `${gishub}esri:oceans.json`, 78 | }, 79 | } 80 | const carto = 'https://{s}.basemaps.cartocdn.com/rastertiles/' 81 | const stamen = 'https://stamen-tiles-{s}.a.ssl.fastly.net/' 82 | const tf = 'https://{s}.tile.thunderforest.com/' 83 | 84 | export const rasterStyleList: object = { 85 | osm: { 86 | org: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 87 | human: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', 88 | cycle: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', 89 | topo: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', 90 | _copy: 91 | '© OpenStreetMap', 92 | }, 93 | carto: { 94 | voyager: `${carto}voyager_labels_under/{z}/{x}/{y}{r}.png`, 95 | positron: `${carto}light_all/{z}/{x}/{y}{r}.png`, 96 | dark: `${carto}dark_all/{z}/{x}/{y}{r}.png`, 97 | _copy: 98 | '© Carto', 99 | }, 100 | stamen: { 101 | toner: `${stamen}toner/{z}/{x}/{y}{r}.png`, 102 | toner_lite: `${stamen}toner-lite/{z}/{x}/{y}{r}.png`, 103 | watercolor: `${stamen}watercolor/{z}/{x}/{y}.png`, 104 | terrain: `${stamen}terrain/{z}/{x}/{y}{r}.png`, 105 | _copy: 106 | '© Stamen Design', 107 | }, 108 | tf: { 109 | cycle: `${tf}cycle/{z}/{x}/{y}{r}.png?apikey={apikey}`, 110 | trans: `${tf}transport/{z}/{x}/{y}{r}.png?apikey={apikey}`, 111 | trans_dark: `${tf}transport-dark/{z}/{x}/{y}{r}.png?apikey={apikey}`, 112 | landscape: `${tf}landscape/{z}/{x}/{y}{r}.png?apikey={apikey}`, 113 | outdoors: `${tf}outdoors/{z}/{x}/{y}{r}.png?apikey={apikey}`, 114 | neighbourhood: `${tf}neighbourhood/{z}/{x}/{y}{r}.png?apikey={apikey}`, 115 | spinal: `${tf}spinal-map/{z}/{x}/{y}{r}.png?apikey={apikey}`, 116 | pioneer: `${tf}pioneer/{z}/{x}/{y}{r}.png?apikey={apikey}`, 117 | atlas: `${tf}atlas/{z}/{x}/{y}{r}.png?apikey={apikey}`, 118 | mobile: `${tf}mobile-atlas/{z}/{x}/{y}{r}.png?apikey={apikey}`, 119 | _copy: 120 | '© Thunderforest', 121 | }, 122 | } 123 | -------------------------------------------------------------------------------- /src/styles.ts: -------------------------------------------------------------------------------- 1 | export const baseStyle = [ 2 | 'id', 3 | 'type', 4 | 'filter', 5 | 'source', 6 | 'source-layer', 7 | 'minzoom', 8 | 'metadata', 9 | 'maxzoom', 10 | 'paint', 11 | 'layout' 12 | ] 13 | 14 | export const layoutStyles = [ 15 | 'visibility', 16 | 'line-join', 17 | 'line-cap', 18 | 'line-miter-limit', 19 | 'line-round-limit', 20 | 'line-sort-key', 21 | 'circle-sort-key', 22 | 'symbol-avoid-edges', 23 | 'symbol-placement', 24 | 'symbol-sort-key', 25 | 'symbol-spacing', 26 | 'symbol-z-order', 27 | 'icon-allow-overlap', 28 | 'icon-anchor', 29 | 'icon-ignore-placement', 30 | 'icon-image', 31 | 'icon-keep-upright', 32 | 'icon-offset', 33 | 'icon-optional', 34 | 'icon-padding', 35 | 'icon-pitch-alignment', 36 | 'icon-rotate', 37 | 'icon-rotation-alignment', 38 | 'icon-size', 39 | 'icon-text-fit', 40 | 'icon-text-fit-padding', 41 | 'text-allow-overlap', 42 | 'text-anchor', 43 | 'text-field', 44 | 'text-font', 45 | 'text-ignore-placement', 46 | 'text-justify', 47 | 'text-keep-upright', 48 | 'text-letter-spacing', 49 | 'text-line-height', 50 | 'text-max-angle', 51 | 'text-max-width', 52 | 'text-offset', 53 | 'text-optional', 54 | 'text-padding', 55 | 'text-pitch-alignment', 56 | 'text-radial-offset', 57 | 'text-rotate', 58 | 'text-rotation-alignment', 59 | 'text-size', 60 | 'text-transform', 61 | 'text-variable-anchor', 62 | 'text-writing-mode' 63 | ] -------------------------------------------------------------------------------- /src/vitest.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import 'jsdom-worker' 3 | 4 | // @ts-ignore 5 | window.matchMedia = 6 | window.matchMedia || 7 | (() => ({ 8 | matches: false, 9 | addListener: () => {}, 10 | removeListener: () => {}, 11 | })) 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "declaration": true, 9 | "declarationDir": "./types", 10 | "emitDeclarationOnly": true, 11 | "jsx": "preserve", 12 | "jsxImportSource": "solid-js", 13 | "resolveJsonModule": true 14 | }, 15 | "include": ["src"], 16 | "exclude": ["node_modules", "**/__tests__/*"] 17 | } 18 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import { defineConfig } from 'vite' 5 | import solidPlugin from 'vite-plugin-solid' 6 | 7 | export default defineConfig({ 8 | plugins: [solidPlugin()], 9 | 10 | test: { 11 | environment: "jsdom", 12 | globals: true, 13 | deps: { 14 | registerNodeLoader: true, 15 | }, 16 | setupFiles: "./src/vitest.ts", 17 | coverage: { 18 | all: true, 19 | include: ["src/"], 20 | reporter: ["text", "html-spa"], 21 | }, 22 | }, 23 | 24 | resolve: { 25 | conditions: ['development', 'browser'], 26 | }, 27 | 28 | optimizeDeps: { 29 | include: ['mapbox-gl'], 30 | }, 31 | }) 32 | --------------------------------------------------------------------------------