├── .eslintrc ├── .gitignore ├── .prettierrc ├── .stylelintrc ├── LICENSE ├── README.md ├── config └── typography.js ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── package-lock.json ├── package.json └── src ├── components ├── Findelevation │ └── Findelevation.jsx ├── Grid │ ├── Columns.jsx │ ├── Container.jsx │ └── index.jsx ├── Layercontrol │ ├── Layercontrol.jsx │ └── Layercontrolitem.jsx ├── Layout │ ├── Header.jsx │ ├── UnsupportedBrowser.jsx │ └── index.jsx ├── Link │ ├── OutboundLink.jsx │ └── index.jsx ├── Map │ ├── StyleSelector.jsx │ ├── index.jsx │ └── util.js ├── SEO │ └── index.jsx ├── Scrollflyto │ ├── Scrollflyto.jsx │ └── SingleScrollflyto.jsx ├── Sidebar │ └── index.jsx └── Swipemap │ ├── StyleSelector.jsx │ ├── index.jsx │ └── util.js ├── constants ├── directions.js ├── layercontrol.js ├── scrollflyto.js └── styles.js ├── pages ├── 404.jsx ├── index.jsx ├── map-direction.jsx ├── map-full-plus-find-elevation.jsx ├── map-full.jsx ├── map-geojson-simple.jsx ├── map-scale-control.jsx ├── map-show-and-hide-layers.jsx ├── map-style-switcher.jsx ├── map-swipe.jsx ├── map.jsx ├── mapwithsidebar.jsx └── scrollflyto.jsx ├── style ├── index.jsx └── theme.js └── util └── dom.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier", "prettier/react"], 3 | "plugins": ["prettier"], 4 | "parser": "babel-eslint", 5 | "parserOptions": { 6 | "ecmaVersion": 2017, 7 | "ecmaFeatures": { 8 | "experimentalObjectRestSpread": true, 9 | "impliedStrict": true, 10 | "classes": true 11 | } 12 | }, 13 | "env": { 14 | "browser": true, 15 | "node": true 16 | }, 17 | "settings": { 18 | "import/core-modules": [], 19 | "import/resolver": { 20 | "node": { 21 | "extensions": [".js", ".jsx"], 22 | "moduleDirectory": ["src", "node_modules"] 23 | } 24 | } 25 | }, 26 | "rules": { 27 | "no-use-before-define": 0, 28 | "import/no-extraneous-dependencies": 0, 29 | "import/prefer-default-export": 0, 30 | "react/no-danger": 0, 31 | "react/forbid-prop-types": 0, 32 | "react/display-name": 1, 33 | "react/react-in-jsx-scope": 0, 34 | "react/jsx-filename-extension": [ 35 | 1, 36 | { 37 | "extensions": [".js", ".jsx"] 38 | } 39 | ], 40 | "quotes": [ 41 | 2, 42 | "single", 43 | { 44 | "avoidEscape": true, 45 | "allowTemplateLiterals": true 46 | } 47 | ], 48 | "jsx-a11y/accessible-emoji": 0, 49 | "jsx-a11y/href-no-hash": "off", 50 | "jsx-a11y/anchor-is-valid": [ 51 | "warn", 52 | { 53 | "aspects": ["invalidHref"] 54 | } 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variables file 55 | .env 56 | .env.* 57 | 58 | # gatsby files 59 | .cache/ 60 | public 61 | 62 | # Mac files 63 | .DS_Store 64 | 65 | # Yarn 66 | yarn-error.log 67 | .pnp/ 68 | .pnp.js 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": [ 3 | [ 4 | "stylelint-processor-styled-components", 5 | { 6 | "moduleName": "style" 7 | } 8 | ] 9 | ], 10 | "extends": [ 11 | "stylelint-config-recommended", 12 | "stylelint-config-standard", 13 | "stylelint-config-styled-components" 14 | ], 15 | "rules": { 16 | "property-no-vendor-prefix": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Brendan C. Ward 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gatsby starter with Mapbox GL 2 | 3 | __This starter__ is build on the starter https://github.com/brendan-ward/gatsby-starter-mapbox. It gets you going quickly with Mapbox GL in Gatsby. It uses React hooks to wrap the Mapbox GL JS object and I build my examples on this repo. 4 | 5 | [Demo](https://astridx.github.io/gatsbystarter/gatsby-starter-mapbox-examples/) 6 | 7 | The menu items in the navigation can be changed in the file [Header.jsx](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/da0f115b8bc8c52d0b7063ede429d1ce5fb99b92/src/components/Layout/Header.jsx#L63)! 8 | 9 | ## Installation 10 | 11 | You will need to know Gatsby to use this starter. If you are interested in learning more about building your first Gatsby site, check out the [Gatsby.js Tutorials](https://www.gatsbyjs.com/tutorial/). 12 | 13 | 1. Initialize the Site 14 | 15 | To create a site in a `mymapboxglsite` directory with this Gatsby Starter: 16 | 17 | ``` 18 | gatsby new mymapboxglsite https://github.com/astridx/gatsby-starter-mapbox-examples 19 | ``` 20 | 21 | 2. Get Mapbox API token 22 | 23 | You must set the [environment variable](https://www.gatsbyjs.com/docs/environment-variables/) `GATSBY_MAPBOX_API_TOKEN` to your [Mapbox API token](https://docs.mapbox.com/help/how-mapbox-works/access-tokens/). Define an environment config file, `.env.development` and/or `.env.production`, in your root folder. Depending on your active environment, the correct one will be found and its values embedded as environment variables in the browser JavaScript. Add the line 24 | 25 | ``` 26 | GATSBY_MAPBOX_API_TOKEN='YOUR TOKEN' 27 | ``` 28 | 29 | 3. Start the Site 30 | 31 | Start the development mode: 32 | 33 | ``` 34 | gatsby develop 35 | ``` 36 | 37 | Open up a new tab in your browser and navigate to http://localhost:8000/ 38 | 39 | Perfect! This is the beginning of a Gatsby MapBox JS GL site! 40 | 41 | 42 | ## Features 43 | 44 | There is a menu item for each example. 45 | 46 | ### Full Screen Map 47 | 48 | [![Full Screen Map Gatsby Mapbox GL Starter](https://user-images.githubusercontent.com/9974686/97810139-0f17ce80-1c72-11eb-987f-aea7edadfd6f.png)](https://astridx.github.io/gatsbystarter/gatsby-starter-mapbox-examples/map-full) 49 | 50 | #### Adapt to your wishes 51 | 52 | I provide basic map configuration such as initial `zoom` level in the file [src/components/Map](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/da0f115b8bc8c52d0b7063ede429d1ce5fb99b92/src/components/Map/index.jsx#L191): 53 | 54 | ``` 55 | width: 'auto', 56 | height: '100%', 57 | center: [7.221275, 50.326111], 58 | zoom: 9.5, 59 | bounds: null, 60 | minZoom: 0, 61 | maxZoom: 24, 62 | styles: ['streets-v11'], 63 | padding: 0.1, 64 | sources: {}, 65 | layers: [], 66 | directions: [], 67 | ``` 68 | 69 | You can provide optional configuration (for example `sources` and `layers`) according to the Mapbox GL style specification. 70 | 71 | ``` 72 | 77 | ``` 78 | 79 | ### Map with Sidebar 80 | 81 | [![Map with Sidebar Gatsby Mapbox GL Starter](https://user-images.githubusercontent.com/9974686/97810141-117a2880-1c72-11eb-92b1-6410facee11d.png)](https://astridx.github.io/gatsbystarter/gatsby-starter-mapbox-examples/map) 82 | 83 | #### Adapt to your wishes 84 | 85 | This [example](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/da0f115b8bc8c52d0b7063ede429d1ce5fb99b92/src/pages/mapwithsidebar.jsx#L19) shows you how to add a sidebar. You can see a concrete implementation in the _Scoll Fly to_ example below. 86 | 87 | ### Map with GeoJson Layer (simple) 88 | 89 | [![Full Screen Map with GeoJSON Gatsby Mapbox GL Starter](https://user-images.githubusercontent.com/9974686/97810142-12ab5580-1c72-11eb-95a9-ae56e83cc71b.png)](https://astridx.github.io/gatsbystarter/gatsby-starter-mapbox-examples/map-geojson-simple) 90 | 91 | #### Adapt to your wishes 92 | 93 | You add a few geodata directly in the code via JSON souce and layer. The [example](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/da0f115b8bc8c52d0b7063ede429d1ce5fb99b92/src/pages/map-geojson-simple.jsx#L6) is worth a thousand words. 94 | 95 | ### Scroll Fly To 96 | 97 | [![Map with Scroll Fly To in Gatsby Mapbox GL Starter](https://user-images.githubusercontent.com/9974686/97810143-13dc8280-1c72-11eb-9c7a-ba29536eeedd.png)](https://astridx.github.io/gatsbystarter/gatsby-starter-mapbox-examples/scrollflyto) 98 | 99 | This feature allows you to tell a story using the map. 100 | 101 | [Fly to a location](https://astridx.github.io/mapboxexamples/examples/scroll-fly-to.html) based on scroll position in the sidebar. Scroll down through the _Points of interest_ and the map will fly to the location. 102 | 103 | See another [example](https://docs.mapbox.com/mapbox-gl-js/example/scroll-fly-to/). 104 | 105 | #### Adapt to your wishes 106 | 107 | You change the content in the file [src/constants/scrollflyto.js](https://github.com/astridx/gatsby-starter-mapbox/blob/astridx/src/constants/scrollflyto.js) 108 | 109 | Option | Description 110 | --- | --- 111 | bearing | The initial bearing (rotation) of the map, measured in degrees counter-clockwise from north. If bearing is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to 0. 112 | pitch | The initial pitch (tilt) of the map, measured in degrees away from the plane of the screen (0-60). If pitch is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to 0. 113 | speed | The average speed of the animation. 114 | --- | Please visit [MapBox GL Documentation](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#flyto) for all opitons. 115 | 116 | ### Find elevations with the Tilequery API 117 | 118 | [![Full Screen Map Gatsby Mapbox GL Starter with Query for getting elevation](https://user-images.githubusercontent.com/9974686/97810145-14751900-1c72-11eb-8730-a898c8068eb4.png)](https://astridx.github.io/gatsbystarter/gatsby-starter-mapbox-examples/map-full-plus-find-elevation) 119 | 120 | Sometimes it is helpful to get information about a location at the click of a mouse. The [Mapbox Tilequery API](https://docs.mapbox.com/api/maps/#tilequery) allows you to retrieve features from vector tilesets based on a given longitude and latitude. The menu item for elevations information offers an example for getting the elevation of a location an show this in a text field in the lef upper corner of the map. 121 | 122 | #### Adapt to your wishes 123 | 124 | In the starter you can see an [example](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/da0f115b8bc8c52d0b7063ede429d1ce5fb99b92/src/pages/map-full-plus-find-elevation.jsx#L14) for querying the height above sea level. You have the option to query and display other geodata. Test and learn about the Tilequery API in the [Playground](https://docs.mapbox.com/playground/tilequery/). 125 | 126 | ### Layer Switcher 127 | 128 | [![Full Screen Map with GeoJSON and Layer Control Layer Switcher Gatsby Mapbox GL Starter](https://user-images.githubusercontent.com/9974686/97810146-15a64600-1c72-11eb-8043-2ddf5c0e81f6.png)](https://astridx.github.io/gatsbystarter/gatsby-starter-mapbox-examples/map-show-and-hide-layers) 129 | 130 | Do you want to display a lot of geospatial data? Then it makes sense to add them in a separate file. In addition, you might want to show and hide them as needed? So you can create a custom layer switcher to display different datasets. 131 | 132 | #### Adapt to your wishes 133 | 134 | The page [show-and-hide-layers](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/da0f115b8bc8c52d0b7063ede429d1ce5fb99b92/src/pages/map-show-and-hide-layers.jsx#L3) shows you how to integrate data and components. If you want to use other data in another map of the website, simply create a new file similar to [this](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/master/src/constants/layercontrol.js) one in the constants folder and import it. 135 | 136 | ### Swipe between maps 137 | 138 | [![Full Screen Map Gatsby Mapbox GL Starter with Swipe to compare](https://user-images.githubusercontent.com/9974686/97810147-16d77300-1c72-11eb-8573-d464b249af22.png)](https://astridx.github.io/gatsbystarter/gatsby-starter-mapbox-examples/map-swipe) 139 | 140 | Different information can be highlighted with different maps. This function offers a comparison between different maps. 141 | 142 | See this [example](https://astridx.github.io/mapboxexamples/plugins/mapbox-gl-compare-swipe-between-maps.html) 143 | 144 | #### Adapt to your wishes 145 | 146 | Add the component `` like in in this [page](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/da0f115b8bc8c52d0b7063ede429d1ce5fb99b92/src/pages/map-swipe.jsx#L9): ``. You have to use two styles as parameters to be able to compare them. If you add more styles, they will be added to the left map in a layer switcher. So you can show all styles and compare them with the map on the right. 147 | 148 | ### Directions 149 | 150 | [![Full Screen Map with Route / Directions Gatsby Mapbox GL Starter](https://user-images.githubusercontent.com/9974686/97810148-1808a000-1c72-11eb-86cd-77aa3f72a7b8.png)](https://astridx.github.io/gatsbystarter/gatsby-starter-mapbox-examples/map-direction) 151 | 152 | What is the best way to get from A to B. Or: I want to show you how I got from A to B. The second was my requirement. I show special points along this route with markers. 153 | 154 | #### Adapt to your wishes 155 | 156 | You can see in the [example page](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/master/src/pages/map-direction.jsx#L11) how you can add the directions plugin. You enter the data for the plugin in an [extra file](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/master/src/constants/directions.js). You can use different routes. To do this, create a file similar to [this](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/master/src/constants/directions.js) one in the constants directory and import it into the page in which the route should be displayed. 157 | 158 | For all options of the plugin directions see the [Documentation](https://docs.mapbox.com/api/navigation/#directions). 159 | 160 | Points of interest you can add via the parameter `pois`. Here you need to add the longitude, the latitude and a text for a popup. 161 | 162 | ### Style Switcher 163 | 164 | [![Full Screen Map with Style Switcher Gatsby Mapbox GL Starter](https://user-images.githubusercontent.com/9974686/97810149-18a13680-1c72-11eb-9efa-7fbfe67efd11.png)](https://astridx.github.io/gatsbystarter/gatsby-starter-mapbox-examples/map-style-switcher) 165 | 166 | In previous examples you may have already seen that you can use the parameter `styles` with more than one entry (for example like this ``) for showing a style control in the form of an image at the bottom left. In addition it is possible to show a control with text links. 167 | 168 | #### Adapt to your wishes 169 | 170 | The example on [this page](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/bb3fefdbda2e6b2d3e9f2bc62e573cf4a0fc0b9a/src/pages/map-style-switcher.jsx#L13) shows how to do this. You need to add (styleSwitcherData](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/master/src/constants/styles.js). styleSwitcherData are the title you like to display in the control for activation of this style and the [style uri](https://docs.mapbox.com/help/glossary/style-url/). 171 | 172 | 173 | ### Scale Control 174 | 175 | [![Full Screen Map with Scale Control Gatsby Mapbox GL Starter](https://user-images.githubusercontent.com/9974686/97810150-18a13680-1c72-11eb-8843-2e16801738e9.png)](https://astridx.github.io/gatsbystarter/gatsby-starter-mapbox-examples/map-scale-control) 176 | 177 | #### Adapt to your wishes 178 | 179 | The example on [this page](https://github.com/astridx/gatsby-starter-mapbox-examples/blob/bb3fefdbda2e6b2d3e9f2bc62e573cf4a0fc0b9a/src/pages/map-scale-control.jsx#L11) shows how to do this. -------------------------------------------------------------------------------- /config/typography.js: -------------------------------------------------------------------------------- 1 | import Typography from 'typography' 2 | import typeographyTheme from 'typography-theme-noriega' 3 | 4 | import { theme } from 'style' 5 | 6 | typeographyTheme.overrideThemeStyles = () => ({ 7 | html: { 8 | // overflowY: 'scroll', 9 | height: '100%', 10 | }, 11 | body: { 12 | height: '100%', 13 | width: '100%', 14 | }, 15 | // Set height on containing notes to 100% so that full screen map layouts work 16 | '#___gatsby': { 17 | height: '100%', 18 | }, 19 | '#___gatsby > *': { 20 | height: '100%', 21 | }, 22 | button: { 23 | outline: 'none', 24 | cursor: 'pointer', 25 | }, 26 | 'a, a:visited, a:active': { 27 | color: theme.colors.link, 28 | }, 29 | }) 30 | 31 | const typography = new Typography(typeographyTheme) 32 | 33 | export default typography 34 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import GoogleAnalytics from 'react-ga' 2 | import { siteMetadata } from './gatsby-config' 3 | 4 | /** 5 | * Initialize Google Analytics 6 | */ 7 | export const onClientEntry = () => { 8 | if (process.env.NODE_ENV === 'production') { 9 | GoogleAnalytics.initialize(siteMetadata.googleAnalyticsId) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | const googleAnalyticsId = `UA-XXXXX` 2 | 3 | module.exports = { 4 | pathPrefix: `/gatsbystarter/gatsby-starter-mapbox-examples`, 5 | siteMetadata: { 6 | siteUrl: `https://astridx.github.io/gatsbystarter/gatsby-starter-mapbox-examples/`, 7 | title: `Gatsby Mapbox GL Starter`, 8 | description: `Get up and running quickly with a Gatsby starter that includes a basic setup for Mapbox GL`, 9 | author: `Brendan C. Ward`, 10 | googleAnalyticsId, 11 | mapboxToken: process.env.GATSBY_MAPBOX_API_TOKEN, 12 | }, 13 | plugins: [ 14 | `gatsby-plugin-react-helmet`, 15 | `gatsby-transformer-sharp`, 16 | `gatsby-plugin-sharp`, 17 | { 18 | resolve: `gatsby-plugin-styled-components`, 19 | options: { 20 | displayName: process.env.NODE_ENV !== `production`, 21 | fileName: false, 22 | }, 23 | }, 24 | { 25 | resolve: `gatsby-plugin-typography`, 26 | options: { 27 | pathToConfigModule: `./config/typography.js`, 28 | }, 29 | }, 30 | `gatsby-plugin-catch-links`, 31 | // `gatsby-plugin-sitemap`, 32 | // { 33 | // resolve: `gatsby-plugin-google-analytics`, 34 | // options: { 35 | // trackingId: googleAnalyticsId, 36 | // anonymize: true, 37 | // }, 38 | //}, 39 | // { 40 | // resolve: `gatsby-plugin-manifest`, 41 | // options: { 42 | // name: `gatsby-starter-default`, 43 | // short_name: `starter`, 44 | // start_url: `/`, 45 | // background_color: `#663399`, 46 | // theme_color: `#663399`, 47 | // display: `minimal-ui`, 48 | // // icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site. 49 | // }, 50 | // }, 51 | // this (optional) plugin enables Progressive Web App + Offline functionality 52 | // To learn more, visit: https://gatsby.dev/offline 53 | // `gatsby-plugin-offline`, 54 | ], 55 | } 56 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | exports.onCreateWebpackConfig = ({ actions, stage, loaders }) => { 4 | const config = { 5 | resolve: { 6 | modules: [path.resolve(__dirname, "src"), "node_modules"], 7 | }, 8 | } 9 | 10 | // when building HTML, window is not defined, so Leaflet causes the build to blow up 11 | if (stage === "build-html") { 12 | config.module = { 13 | rules: [ 14 | { 15 | test: /mapbox-gl/, 16 | use: loaders.null(), 17 | }, 18 | ], 19 | } 20 | } 21 | 22 | actions.setWebpackConfig(config) 23 | } 24 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/ssr-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-starter-mapbox-examples", 3 | "private": true, 4 | "description": "A Gatsby starter with Mapbox GL and Examples", 5 | "version": "0.1.0", 6 | "author": "Brendan C. Ward extended by Astrid Günther", 7 | "dependencies": { 8 | "@mapbox/geo-viewport": "^0.4.0", 9 | "@mapbox/mapbox-gl-directions": "^4.1.0", 10 | "@rebass/grid": "^6.0.0", 11 | "axios": "^0.20.0", 12 | "babel-plugin-styled-components": "^1.10.0", 13 | "gatsby": "^2.24.73", 14 | "gatsby-image": "^2.0.41", 15 | "gatsby-plugin-catch-links": "^2.0.13", 16 | "gatsby-plugin-google-analytics": "^2.0.19", 17 | "gatsby-plugin-manifest": "^2.1.1", 18 | "gatsby-plugin-offline": "^2.1.0", 19 | "gatsby-plugin-react-helmet": "^3.0.12", 20 | "gatsby-plugin-sharp": "^2.0.36", 21 | "gatsby-plugin-sitemap": "^2.1.0", 22 | "gatsby-plugin-styled-components": "^3.0.7", 23 | "gatsby-plugin-typography": "^2.2.13", 24 | "gatsby-remark-images": "^3.0.11", 25 | "gatsby-source-filesystem": "^2.3.34", 26 | "gatsby-transformer-sharp": "^2.1.19", 27 | "immutable": "^4.0.0-rc.12", 28 | "mapbox-gl": "^1.12.0", 29 | "mapbox-gl-compare": "^0.4.0", 30 | "mapbox-gl-directions": "^3.0.3", 31 | "mapbox-gl-style-switcher": "^1.0.9", 32 | "prop-types": "^15.7.2", 33 | "react": "^16.8.6", 34 | "react-dom": "^16.8.6", 35 | "react-ga": "^2.5.7", 36 | "react-helmet": "^5.2.1", 37 | "react-icons": "^3.11.0", 38 | "react-typography": "^0.16.19", 39 | "rebass": "^3.1.0", 40 | "styled-components": "^4.2.0", 41 | "styled-system": "^4.1.0", 42 | "typography": "^0.16.19", 43 | "typography-theme-noriega": "^0.16.19" 44 | }, 45 | "devDependencies": { 46 | "babel-eslint": "^10.0.1", 47 | "eslint": "^5.16.0", 48 | "eslint-config-airbnb": "^17.1.0", 49 | "eslint-config-prettier": "^4.2.0", 50 | "eslint-import-resolver-alias": "^1.1.2", 51 | "eslint-plugin-import": "^2.17.2", 52 | "eslint-plugin-jsx-a11y": "^6.2.1", 53 | "eslint-plugin-prettier": "^3.0.1", 54 | "eslint-plugin-react": "^7.13.0", 55 | "prettier": "^1.17.0", 56 | "stylelint-config-recommended": "^2.2.0", 57 | "stylelint-config-standard": "^18.3.0", 58 | "stylelint-config-styled-components": "^0.1.1", 59 | "stylelint-processor-styled-components": "^1.6.0" 60 | }, 61 | "keywords": [ 62 | "gatsby" 63 | ], 64 | "license": "MIT", 65 | "scripts": { 66 | "build": "gatsby build", 67 | "develop": "gatsby develop", 68 | "format": "prettier --write src/**/*.{js,jsx}", 69 | "start": "npm run develop", 70 | "serve": "gatsby serve" 71 | }, 72 | "repository": { 73 | "type": "git", 74 | "url": "https://github.com/astridx/gatsby-starter-mapbox-examples.git/" 75 | }, 76 | "bugs": { 77 | "url": "https://github.com/astridx/gatsby-starter-mapbox-examples/issues" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/components/Findelevation/Findelevation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'style' 3 | 4 | const Findelevation = props => { 5 | return ( 6 | 7 |
8 | Elevation:{' '} 9 | Bitte auf die Karte klicken. 10 |
11 |
12 | ) 13 | } 14 | 15 | const FindelevationContainer = styled.section` 16 | position: absolute; 17 | background-color: #fff; 18 | z-index: 1; 19 | ` 20 | 21 | export default Findelevation 22 | -------------------------------------------------------------------------------- /src/components/Grid/Columns.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Flex, Box } from '@rebass/grid' 3 | 4 | export const Columns = props => ( 5 | 12 | ) 13 | 14 | export const Column = props => 15 | -------------------------------------------------------------------------------- /src/components/Grid/Container.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import { Box } from '@rebass/grid' 3 | import styled from 'style' 4 | 5 | const Container = styled(Box)` 6 | max-width: ${props => props.maxWidth}; 7 | ` 8 | 9 | Container.propTypes = { 10 | maxWidth: PropTypes.oneOfType([ 11 | PropTypes.number, 12 | PropTypes.string, 13 | PropTypes.array, 14 | ]), 15 | } 16 | 17 | Container.defaultProps = { 18 | mx: 'auto', 19 | maxWidth: '700px', 20 | } 21 | 22 | export default Container 23 | -------------------------------------------------------------------------------- /src/components/Grid/index.jsx: -------------------------------------------------------------------------------- 1 | import { Flex, Box } from '@rebass/grid' 2 | import Container from './Container' 3 | import { Columns, Column } from './Columns' 4 | 5 | export { Flex, Box, Container, Columns, Column } 6 | -------------------------------------------------------------------------------- /src/components/Layercontrol/Layercontrol.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Layercontrolitem from './Layercontrolitem' 3 | import styled from 'style' 4 | 5 | const Layercontrol = props => { 6 | return ( 7 |
8 | {props.layercontrol.map((item, index) => ( 9 | 10 | {item.layer.map((l, index) => ( 11 | 12 | ))} 13 | 14 | ))} 15 |
16 | ) 17 | } 18 | 19 | const LayercontrolContainer = styled.nav` 20 | background: #fff; 21 | position: absolute; 22 | z-index: 1; 23 | bottom: 10px; 24 | right: 10px; 25 | border-radius: 3px; 26 | width: 120px; 27 | border: 1px solid rgba(0, 0, 0, 0.4); 28 | font-family: 'Open Sans', sans-serif; 29 | 30 | a { 31 | font-size: 13px; 32 | color: #404040; 33 | display: block; 34 | margin: 0; 35 | padding: 0; 36 | padding: 10px; 37 | text-decoration: none; 38 | border-bottom: 1px solid rgba(0, 0, 0, 0.25); 39 | text-align: center; 40 | } 41 | 42 | a:last-child { 43 | border: none; 44 | } 45 | 46 | a:hover { 47 | background-color: #f8f8f8; 48 | color: #404040; 49 | } 50 | 51 | a.active { 52 | background-color: #3887be; 53 | color: #ffffff; 54 | } 55 | 56 | a.active:hover { 57 | background: #3074a4; 58 | } 59 | ` 60 | 61 | export default Layercontrol 62 | -------------------------------------------------------------------------------- /src/components/Layercontrol/Layercontrolitem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled, { themeGet } from 'style' 3 | 4 | const Layercontrolitem = ({ title }) => { 5 | return ( 6 | 7 | 8 | {title} 9 | 10 | 11 | ) 12 | } 13 | 14 | const LayercontrolitemWrapper = styled.p` 15 | text-align: center; 16 | margin-bottom: 0.05rem; 17 | ` 18 | 19 | export default Layercontrolitem 20 | -------------------------------------------------------------------------------- /src/components/Layout/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Text } from 'rebass' 3 | import { SiMapbox as SiteLogo } from 'react-icons/si' 4 | 5 | import { Link } from 'components/Link' 6 | 7 | import { Box, Flex } from 'components/Grid' 8 | import styled, { themeGet } from 'style' 9 | import { siteMetadata } from '../../../gatsby-config' 10 | 11 | const Wrapper = styled(Flex).attrs({ 12 | alignItems: 'center', 13 | justifyContent: 'space-between', 14 | })` 15 | padding: 0.75rem 0.5rem; 16 | flex: 0 0 auto; 17 | border-bottom: 1px solid ${themeGet('colors.grey.900')}; 18 | ` 19 | 20 | const Title = styled(Text).attrs({ 21 | as: 'h1', 22 | })` 23 | margin: 0; 24 | flex-grow: 1; 25 | line-height: 1; 26 | 27 | & * { 28 | text-decoration: none; 29 | } 30 | ` 31 | 32 | const NavBar = styled(Flex).attrs({ 33 | alignItems: 'center', 34 | })` 35 | font-size: 1.25rem; 36 | 37 | .nav-active { 38 | color: ${themeGet('colors.highlight.500')}; 39 | } 40 | ` 41 | 42 | const NavLink = styled(Link)` 43 | text-decoration: none; 44 | padding: 0 0.5rem; 45 | 46 | &:hover { 47 | text-decoration: underline; 48 | } 49 | ` 50 | 51 | const Header = () => ( 52 | 53 | 54 | <Link to="/"> 55 | <Flex alignItems="center" flexWrap="wrap"> 56 | <Box mr="0.5rem"> 57 | <SiteLogo /> 58 | </Box> 59 | {siteMetadata.title} 60 | </Flex> 61 | </Link> 62 | 63 | 64 | 65 | Full Screen Map 66 | 67 | 68 | Map with Sidebar 69 | 70 | 71 | Map with GeoJson Layer (simple) 72 | 73 | 74 | Scroll Fly To 75 | 76 | 77 | Find Elevation 78 | 79 | 80 | Map with Layer Control 81 | 82 | 83 | Compare 84 | 85 | 86 | Map with Direction 87 | 88 | 89 | Map with Style Control (Switcher) 90 | 91 | 92 | Map with Scale Control 93 | 94 | 95 | 96 | ) 97 | 98 | export default Header 99 | -------------------------------------------------------------------------------- /src/components/Layout/UnsupportedBrowser.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Container, Box } from 'components/Grid' 3 | import { FaExclamationTriangle } from 'react-icons/fa' 4 | 5 | import styled, { themeGet } from 'style' 6 | 7 | const IconHeader = styled.h1` 8 | text-align: center; 9 | ` 10 | 11 | const StyledIcon = styled(FaExclamationTriangle)` 12 | height: 10rem; 13 | width: 10rem; 14 | margin-right: 1rem; 15 | color: #fff; 16 | ` 17 | 18 | const WarningBox = styled(Box)` 19 | margin-top: 2rem; 20 | padding: 2rem; 21 | background-color: ${themeGet('colors.primary.900')}; 22 | 23 | h1 { 24 | color: #fff; 25 | } 26 | ` 27 | 28 | const UnsupportedBrowser = () => ( 29 | 30 | 31 | 32 | 33 | 34 |

35 | Unfortunately, you are using an unsupported version of Internet 36 | Explorer. 37 |
38 |
39 | Please use a modern browser such as Google Chrome, Firefox, or Microsoft 40 | Edge. 41 |

42 |
43 |
44 | ) 45 | 46 | export default UnsupportedBrowser 47 | -------------------------------------------------------------------------------- /src/components/Layout/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import SEO from 'components/SEO' 5 | import { Flex } from 'components/Grid' 6 | import styled, { ThemeProvider, theme } from 'style' 7 | import { isUnsupported } from 'util/dom' 8 | import UnsupportedBrowser from './UnsupportedBrowser' 9 | import Header from './Header' 10 | 11 | const Wrapper = styled(Flex).attrs({ flexDirection: 'column' })` 12 | height: 100%; 13 | ` 14 | 15 | const Content = styled.div` 16 | flex: 1 1 auto; 17 | ` 18 | 19 | const Layout = ({ children, title }) => { 20 | return ( 21 | 22 | {isUnsupported ? ( 23 | 24 | ) : ( 25 | 26 | 27 |
28 | {children} 29 | 30 | )} 31 | 32 | ) 33 | } 34 | 35 | Layout.propTypes = { 36 | children: PropTypes.node.isRequired, 37 | title: PropTypes.string, 38 | } 39 | 40 | Layout.defaultProps = { 41 | title: '', 42 | } 43 | 44 | export default Layout 45 | -------------------------------------------------------------------------------- /src/components/Link/OutboundLink.jsx: -------------------------------------------------------------------------------- 1 | import { OutboundLink as Link } from 'gatsby-plugin-google-analytics' 2 | import PropTypes from 'prop-types' 3 | 4 | const OutboundLink = ({ to, target, children, ...props }) => ( 5 | 6 | {children} 7 | 8 | ) 9 | 10 | OutboundLink.propTypes = { 11 | to: PropTypes.string.isRequired, 12 | target: PropTypes.string, 13 | children: PropTypes.any.isRequired, 14 | } 15 | 16 | OutboundLink.defaultProps = { 17 | target: '_blank', 18 | } 19 | 20 | export default OutboundLink 21 | -------------------------------------------------------------------------------- /src/components/Link/index.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'gatsby' 2 | import OutboundLink from './OutboundLink' 3 | 4 | export { Link, OutboundLink } 5 | -------------------------------------------------------------------------------- /src/components/Map/StyleSelector.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react' 2 | import { render } from 'react-dom' 3 | import PropTypes from 'prop-types' 4 | import { fromJS } from 'immutable' 5 | 6 | import styled, { css, themeGet } from 'style' 7 | 8 | const Wrapper = styled.div` 9 | /* cursor: pointer; 10 | position: absolute; 11 | left: 10px; 12 | bottom: 24px; 13 | z-index: 999; */ 14 | ` 15 | 16 | const Basemap = styled.img` 17 | box-sizing: border-box; 18 | border: 2px solid 19 | ${({ isActive }) => (isActive ? themeGet('colors.highlight.500') : '#fff')}; 20 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); 21 | margin: 0; 22 | 23 | ${({ size }) => css` 24 | width: ${size}; 25 | height: ${size}; 26 | border-radius: ${size}; 27 | `} 28 | 29 | &:not(:first-child) { 30 | margin-left: 0.25rem; 31 | } 32 | ` 33 | 34 | const getSrc = ({ styleID, z, x, y, token }) => 35 | `https://api.mapbox.com/styles/v1/mapbox/${styleID}/tiles/256/${z}/${x}/${y}?access_token=${token}` 36 | 37 | const StyleSelector = ({ map, token, styles, tile, size, onChange }) => { 38 | console.log('render StyleSelector') 39 | 40 | map.on('style.load', () => { 41 | console.log('style load') 42 | }) 43 | 44 | const [basemap, setBasemap] = useState(styles[0]) 45 | const [isOpen, setIsOpen] = useState(false) 46 | const baseStyleRef = useRef(null) 47 | 48 | map.once('style.load', () => { 49 | baseStyleRef.current = fromJS(map.getStyle()) 50 | }) 51 | 52 | const handleBasemapClick = newBasemap => { 53 | console.log('handle click', newBasemap) 54 | setIsOpen(false) 55 | 56 | if (newBasemap === basemap) return 57 | 58 | setBasemap(newBasemap) 59 | 60 | const { current: baseStyle } = baseStyleRef 61 | 62 | const snapshot = fromJS(map.getStyle()) 63 | const baseSources = baseStyle.get('sources') 64 | const baseLayers = baseStyle.get('layers') 65 | 66 | // diff the sources and layers to find those added by the user 67 | const userSources = snapshot 68 | .get('sources') 69 | .filter((_, key) => !baseSources.has(key)) 70 | const userLayers = snapshot 71 | .get('layers') 72 | .filter(layer => !baseLayers.includes(layer)) 73 | 74 | map.setStyle(`mapbox://styles/mapbox/${newBasemap}`) 75 | 76 | map.once('style.load', () => { 77 | console.log('on style update') 78 | // after new style has loaded 79 | // save it so that we can diff with it on next change 80 | // and re-add the sources / layers back on it 81 | 82 | // save base for new style 83 | baseStyleRef.current = fromJS(map.getStyle()) 84 | 85 | userSources.forEach((source, id) => { 86 | map.addSource(id, source.toJS()) 87 | }) 88 | 89 | userLayers.forEach(layer => { 90 | map.addLayer(layer.toJS()) 91 | }) 92 | 93 | onChange(newBasemap) 94 | }) 95 | } 96 | 97 | const toggleOpen = () => { 98 | setIsOpen(true) 99 | } 100 | 101 | const toggleClosed = () => { 102 | setIsOpen(false) 103 | } 104 | 105 | // if there are only 2 options, render as a toggle 106 | if (styles.length === 2) { 107 | const nextBasemap = basemap === styles[0] ? styles[1] : styles[0] 108 | 109 | return ( 110 | 111 | handleBasemapClick(nextBasemap)} 115 | /> 116 | 117 | ) 118 | } 119 | 120 | const nextBasemap = styles.filter(style => style !== basemap)[0] 121 | 122 | return ( 123 | 124 | {isOpen ? ( 125 | <> 126 | handleBasemapClick(nextBasemap)} 130 | /> 131 | {styles 132 | .filter(style => style !== nextBasemap) 133 | .map(styleID => ( 134 | handleBasemapClick(styleID)} 140 | /> 141 | ))} 142 | 143 | ) : ( 144 | 149 | )} 150 | 151 | ) 152 | } 153 | 154 | StyleSelector.propTypes = { 155 | map: PropTypes.object, 156 | token: PropTypes.string.isRequired, 157 | // list of mapbox style IDs 158 | styles: PropTypes.arrayOf(PropTypes.string).isRequired, 159 | tile: PropTypes.shape({ 160 | x: PropTypes.number.isRequired, 161 | y: PropTypes.number.isRequired, 162 | z: PropTypes.number.isRequired, 163 | }), 164 | size: PropTypes.string, 165 | onChange: PropTypes.func, 166 | } 167 | 168 | StyleSelector.defaultProps = { 169 | map: null, 170 | tile: { 171 | x: 0, 172 | y: 0, 173 | z: 0, 174 | }, 175 | size: '64px', 176 | onChange: () => {}, 177 | } 178 | 179 | // Wrap in a Mapbox GL plugin so that we can construct the above React element on map init 180 | // TODO: pass props through 181 | class Plugin { 182 | constructor({ styles, token, position }) { 183 | this.styles = styles 184 | this.token = token 185 | this.position = position 186 | } 187 | 188 | onAdd(map) { 189 | console.log('onAdd') 190 | this.map = map 191 | 192 | const { styles, token } = this 193 | 194 | this.container = document.createElement('div') 195 | this.container.classList.add('mapboxgl-ctrl') 196 | this.container.classList.add('mapboxgl-ctrl-style-selector') 197 | this.container.style.float = 'none !important' 198 | this.container.style.cursor = 'pointer' 199 | 200 | render( 201 | , 202 | this.container 203 | ) 204 | 205 | return this.container 206 | } 207 | 208 | init() { 209 | console.log('init') 210 | const { map, styles, token, container } = this 211 | render(, container) 212 | } 213 | 214 | onRemove() { 215 | this.map = null 216 | this.container.parentNode.removeChild(this.container) 217 | } 218 | } 219 | 220 | export default Plugin 221 | -------------------------------------------------------------------------------- /src/components/Map/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len, no-underscore-dangle */ 2 | import React, { useEffect, useRef, useState } from 'react' 3 | import PropTypes from 'prop-types' 4 | import mapboxgl from 'mapbox-gl' 5 | import 'mapbox-gl/dist/mapbox-gl.css' 6 | import styled from 'style' 7 | import { hasWindow } from 'util/dom' 8 | import { getCenterAndZoom } from './util' 9 | import StyleSelector from './StyleSelector' 10 | import MapboxDirections from '@mapbox/mapbox-gl-directions/dist/mapbox-gl-directions' 11 | import { MapboxStyleSwitcherControl } from 'mapbox-gl-style-switcher' 12 | import 'mapbox-gl-style-switcher/styles.css' 13 | import { siteMetadata } from '../../../gatsby-config' 14 | 15 | // This wrapper must be positioned relative for the map to be able to lay itself out properly 16 | const Wrapper = styled.div` 17 | width: ${({ width }) => width}; 18 | height: ${({ height }) => height}; 19 | position: relative; 20 | flex: 1 0 auto; 21 | ` 22 | 23 | const Map = ({ 24 | width, 25 | height, 26 | zoom, 27 | center, 28 | bounds, 29 | padding, 30 | styles, 31 | sources, 32 | layers, 33 | minZoom, 34 | maxZoom, 35 | directions, 36 | scale, 37 | styleSwitcher, 38 | layerSwitcher, 39 | }) => { 40 | const { mapboxToken } = siteMetadata 41 | 42 | if (!mapboxToken) { 43 | console.error( 44 | 'ERROR: Mapbox token is required in gatsby-config.js siteMetadata' 45 | ) 46 | } 47 | 48 | // if there is no window, we cannot render this component 49 | if (!hasWindow) { 50 | return null 51 | } 52 | 53 | // this ref holds the map DOM node so that we can pass it into Mapbox GL 54 | const mapNode = useRef(null) 55 | 56 | // this ref holds the map object once we have instantiated it, so that we 57 | // can use it in other hooks 58 | const mapRef = useRef(null) 59 | // construct the map within an effect that has no dependencies 60 | // this allows us to construct it only once at the time the 61 | // component is constructed. 62 | useEffect(() => { 63 | let mapCenter = center 64 | let mapZoom = zoom 65 | 66 | // If bounds are available, use these to establish center and zoom when map first loads 67 | if (bounds && bounds.length === 4) { 68 | const { center: boundsCenter, zoom: boundsZoom } = getCenterAndZoom( 69 | mapNode.current, 70 | bounds, 71 | padding 72 | ) 73 | mapCenter = boundsCenter 74 | mapZoom = boundsZoom 75 | } 76 | 77 | // Token must be set before constructing map 78 | mapboxgl.accessToken = mapboxToken 79 | 80 | const map = new mapboxgl.Map({ 81 | container: mapNode.current, 82 | style: `mapbox://styles/mapbox/${styles[0]}`, 83 | center: mapCenter, 84 | zoom: mapZoom, 85 | minZoom, 86 | maxZoom, 87 | }) 88 | mapRef.current = map 89 | window.map = map // for easier debugging and querying via console 90 | 91 | map.addControl(new mapboxgl.NavigationControl(), 'top-right') 92 | 93 | if (styles.length > 1) { 94 | map.addControl( 95 | new StyleSelector({ 96 | styles, 97 | token: mapboxToken, 98 | }), 99 | 'bottom-left' 100 | ) 101 | } 102 | 103 | map.on('load', () => { 104 | console.log('map onload') 105 | // add sources 106 | 107 | Object.entries(sources).forEach(([id, source]) => { 108 | map.addSource(id, source) 109 | }) 110 | 111 | // add layers 112 | layers.forEach(layer => { 113 | map.addLayer(layer) 114 | }) 115 | 116 | var direction 117 | directions.forEach(directionData => { 118 | directionData.pois.forEach(poi => { 119 | new mapboxgl.Marker() 120 | .setLngLat([poi[0], poi[1]]) 121 | .setPopup(new mapboxgl.Popup({ offset: 25 }).setText(poi[2])) 122 | .addTo(map) 123 | }) 124 | direction = new MapboxDirections(directionData) 125 | direction.setOrigin(directionData.origin) 126 | direction.setDestination(directionData.destination) 127 | map.addControl(direction, 'top-right') 128 | }) 129 | 130 | scale.forEach(s => { 131 | map.addControl( 132 | new mapboxgl.ScaleControl({ 133 | maxWidth: s.maxWidth, 134 | unit: s.unit, 135 | }) 136 | ) 137 | }) 138 | 139 | styleSwitcher.forEach(ss => { 140 | console.log(ss.styles + ss.position) 141 | map.addControl(new MapboxStyleSwitcherControl(ss.styles, ss.position)) 142 | /* map.addControl( 143 | 144 | );*/ 145 | }) 146 | 147 | layerSwitcher.forEach(ls => { 148 | console.log(ls) 149 | }) 150 | }) 151 | 152 | // hook up map events here, such as click, mouseenter, mouseleave 153 | // e.g., map.on('click', (e) => {}) 154 | 155 | // when this component is destroyed, remove the map 156 | return () => { 157 | map.remove() 158 | } 159 | }, []) 160 | 161 | // You can use other `useEffect` hooks to update the state of the map 162 | // based on incoming props. Just beware that you might need to add additional 163 | // refs to share objects or state between hooks. 164 | 165 | return ( 166 | 167 |
168 | {/* */} 169 | 170 | ) 171 | } 172 | 173 | Map.propTypes = { 174 | width: PropTypes.string, 175 | height: PropTypes.string, 176 | center: PropTypes.arrayOf(PropTypes.number), 177 | zoom: PropTypes.number, 178 | bounds: PropTypes.arrayOf(PropTypes.number), 179 | minZoom: PropTypes.number, 180 | maxZoom: PropTypes.number, 181 | styles: PropTypes.arrayOf(PropTypes.string), 182 | padding: PropTypes.number, 183 | sources: PropTypes.object, 184 | layers: PropTypes.arrayOf(PropTypes.object), 185 | directions: PropTypes.arrayOf(PropTypes.object), 186 | scale: PropTypes.arrayOf(PropTypes.object), 187 | styleSwitcher: PropTypes.arrayOf(PropTypes.object), 188 | layerSwitcher: PropTypes.arrayOf(PropTypes.object), 189 | } 190 | 191 | Map.defaultProps = { 192 | width: 'auto', 193 | height: '100%', 194 | center: [7.221275, 50.326111], 195 | zoom: 9.5, 196 | bounds: null, 197 | minZoom: 0, 198 | maxZoom: 24, 199 | styles: ['streets-v11'], 200 | padding: 0.1, // padding around bounds as a proportion 201 | sources: {}, 202 | layers: [], 203 | directions: [], 204 | scale: [], 205 | styleSwitcher: [], 206 | layerSwitcher: [], 207 | } 208 | 209 | export default Map 210 | -------------------------------------------------------------------------------- /src/components/Map/util.js: -------------------------------------------------------------------------------- 1 | import geoViewport from '@mapbox/geo-viewport' 2 | 3 | /** 4 | * Calculate the appropriate center and zoom to fit the bounds, given padding. 5 | * @param {Element} - map DOM node used to calculate height and width in screen pixels of map 6 | * @param {Array(number)} bounds - [xmin, ymin, xmax, ymax] 7 | * @param {float} padding - proportion of calculated zoom level to zoom out by, to pad the bounds 8 | */ 9 | export const getCenterAndZoom = (mapNode, bounds, padding = 0) => { 10 | const { width, height } = mapNode 11 | const viewport = geoViewport.viewport( 12 | bounds, 13 | [width, height], 14 | undefined, 15 | undefined, 16 | undefined, 17 | true 18 | ) 19 | 20 | // Zoom out slightly to pad around bounds 21 | 22 | const zoom = Math.max(viewport.zoom - 1, 0) * (1 - padding) 23 | 24 | return { center: viewport.center, zoom } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/SEO/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Helmet from 'react-helmet' 4 | import { useStaticQuery, graphql } from 'gatsby' 5 | 6 | function SEO({ description, lang, meta, keywords, title }) { 7 | const { site } = useStaticQuery( 8 | graphql` 9 | query { 10 | site { 11 | siteMetadata { 12 | title 13 | description 14 | author 15 | } 16 | } 17 | } 18 | ` 19 | ) 20 | 21 | const metaDescription = description || site.siteMetadata.description 22 | 23 | return ( 24 | 0 66 | ? { 67 | name: `keywords`, 68 | content: keywords.join(`, `), 69 | } 70 | : [] 71 | ) 72 | .concat(meta)} 73 | /> 74 | ) 75 | } 76 | 77 | SEO.defaultProps = { 78 | lang: `en`, 79 | meta: [], 80 | keywords: [], 81 | description: ``, 82 | } 83 | 84 | SEO.propTypes = { 85 | description: PropTypes.string, 86 | lang: PropTypes.string, 87 | meta: PropTypes.arrayOf(PropTypes.object), 88 | keywords: PropTypes.arrayOf(PropTypes.string), 89 | title: PropTypes.string.isRequired, 90 | } 91 | 92 | export default SEO 93 | -------------------------------------------------------------------------------- /src/components/Scrollflyto/Scrollflyto.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SingleScrollflyto from './SingleScrollflyto' 3 | import styled from 'style' 4 | 5 | const Scrollflyto = props => { 6 | return ( 7 | 8 | 9 | {props.scrollflyto.map((item, index) => ( 10 | 11 | ))} 12 | 13 | 14 | ) 15 | } 16 | 17 | const ScrollflytoContainer = styled.section` 18 | max-height: 400px; 19 | ` 20 | 21 | const ScrollflytoWrapper = styled.div` 22 | font-family: sans-serif; 23 | background-color: #fafafa; 24 | ` 25 | 26 | export default Scrollflyto 27 | -------------------------------------------------------------------------------- /src/components/Scrollflyto/SingleScrollflyto.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled, { themeGet } from 'style' 3 | 4 | const SingleScollflyto = ({ title, text }) => { 5 | return ( 6 | 7 |
8 |

{title}

9 |

{text}

10 |
11 |
12 | ) 13 | } 14 | 15 | const SingleScollflytoWrapper = styled.section` 16 | margin: 200px 0; 17 | padding: 25px 50px; 18 | line-height: 25px; 19 | border-bottom: 1px solid #ddd; 20 | color: gray; 21 | font-size: 13px; 22 | } 23 | section.active { 24 | color: black; 25 | } 26 | section:last-child { 27 | border-bottom: none; 28 | margin-bottom: 400px; 29 | } 30 | ` 31 | 32 | export default SingleScollflyto 33 | -------------------------------------------------------------------------------- /src/components/Sidebar/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { Box, Flex } from 'components/Grid' 5 | import styled, { themeGet } from 'style' 6 | 7 | // This sidebar is responsive: it shrinks a bit in smaller viewports, then eventually expands to fill the full width 8 | const Wrapper = styled(Box).attrs({ 9 | width: ['100%', '350px', '470px'], 10 | flex: '0 0 auto', 11 | })` 12 | border-right: 1px solid ${themeGet('colors.grey.800')}; 13 | height: 100%; 14 | ` 15 | 16 | // The inner wrapper provides the scroll container for the sidebar 17 | // which allows vertical scrolling by default 18 | const InnerWrapper = styled(Flex).attrs({ 19 | flexDirection: 'column', 20 | flex: '1 1 auto', 21 | })` 22 | overflow-x: hidden; 23 | overflow-y: ${({ allowScroll }) => (allowScroll ? 'auto' : 'hidden')}; 24 | height: 100%; 25 | ` 26 | 27 | const Sidebar = ({ children, allowScroll }) => ( 28 | 29 | {children} 30 | 31 | ) 32 | 33 | Sidebar.propTypes = { 34 | children: PropTypes.node.isRequired, 35 | allowScroll: PropTypes.bool, 36 | } 37 | 38 | Sidebar.defaultProps = { 39 | allowScroll: true, 40 | } 41 | 42 | export default Sidebar 43 | -------------------------------------------------------------------------------- /src/components/Swipemap/StyleSelector.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react' 2 | import { render } from 'react-dom' 3 | import PropTypes from 'prop-types' 4 | import { fromJS } from 'immutable' 5 | 6 | import styled, { css, themeGet } from 'style' 7 | 8 | const Wrapper = styled.div` 9 | /* cursor: pointer; 10 | position: absolute; 11 | left: 10px; 12 | bottom: 24px; 13 | z-index: 999; */ 14 | ` 15 | 16 | const Basemap = styled.img` 17 | box-sizing: border-box; 18 | border: 2px solid 19 | ${({ isActive }) => (isActive ? themeGet('colors.highlight.500') : '#fff')}; 20 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); 21 | margin: 0; 22 | 23 | ${({ size }) => css` 24 | width: ${size}; 25 | height: ${size}; 26 | border-radius: ${size}; 27 | `} 28 | 29 | &:not(:first-child) { 30 | margin-left: 0.25rem; 31 | } 32 | ` 33 | 34 | const getSrc = ({ styleID, z, x, y, token }) => 35 | `https://api.mapbox.com/styles/v1/mapbox/${styleID}/tiles/256/${z}/${x}/${y}?access_token=${token}` 36 | 37 | const StyleSelector = ({ map, token, styles, tile, size, onChange }) => { 38 | console.log('render StyleSelector') 39 | 40 | map.on('style.load', () => { 41 | console.log('style load') 42 | }) 43 | 44 | const [basemap, setBasemap] = useState(styles[0]) 45 | const [isOpen, setIsOpen] = useState(false) 46 | const baseStyleRef = useRef(null) 47 | 48 | map.once('style.load', () => { 49 | baseStyleRef.current = fromJS(map.getStyle()) 50 | }) 51 | 52 | const handleBasemapClick = newBasemap => { 53 | console.log('handle click', newBasemap) 54 | setIsOpen(false) 55 | 56 | if (newBasemap === basemap) return 57 | 58 | setBasemap(newBasemap) 59 | 60 | const { current: baseStyle } = baseStyleRef 61 | 62 | const snapshot = fromJS(map.getStyle()) 63 | const baseSources = baseStyle.get('sources') 64 | const baseLayers = baseStyle.get('layers') 65 | 66 | // diff the sources and layers to find those added by the user 67 | const userSources = snapshot 68 | .get('sources') 69 | .filter((_, key) => !baseSources.has(key)) 70 | const userLayers = snapshot 71 | .get('layers') 72 | .filter(layer => !baseLayers.includes(layer)) 73 | 74 | map.setStyle(`mapbox://styles/mapbox/${newBasemap}`) 75 | 76 | map.once('style.load', () => { 77 | console.log('on style update') 78 | // after new style has loaded 79 | // save it so that we can diff with it on next change 80 | // and re-add the sources / layers back on it 81 | 82 | // save base for new style 83 | baseStyleRef.current = fromJS(map.getStyle()) 84 | 85 | userSources.forEach((source, id) => { 86 | map.addSource(id, source.toJS()) 87 | }) 88 | 89 | userLayers.forEach(layer => { 90 | map.addLayer(layer.toJS()) 91 | }) 92 | 93 | onChange(newBasemap) 94 | }) 95 | } 96 | 97 | const toggleOpen = () => { 98 | setIsOpen(true) 99 | } 100 | 101 | const toggleClosed = () => { 102 | setIsOpen(false) 103 | } 104 | 105 | // if there are only 2 options, render as a toggle 106 | if (styles.length === 2) { 107 | const nextBasemap = basemap === styles[0] ? styles[1] : styles[0] 108 | 109 | return ( 110 | 111 | handleBasemapClick(nextBasemap)} 115 | /> 116 | 117 | ) 118 | } 119 | 120 | const nextBasemap = styles.filter(style => style !== basemap)[0] 121 | 122 | return ( 123 | 124 | {isOpen ? ( 125 | <> 126 | handleBasemapClick(nextBasemap)} 130 | /> 131 | {styles 132 | .filter(style => style !== nextBasemap) 133 | .map(styleID => ( 134 | handleBasemapClick(styleID)} 140 | /> 141 | ))} 142 | 143 | ) : ( 144 | 149 | )} 150 | 151 | ) 152 | } 153 | 154 | StyleSelector.propTypes = { 155 | map: PropTypes.object, 156 | token: PropTypes.string.isRequired, 157 | // list of mapbox style IDs 158 | styles: PropTypes.arrayOf(PropTypes.string).isRequired, 159 | tile: PropTypes.shape({ 160 | x: PropTypes.number.isRequired, 161 | y: PropTypes.number.isRequired, 162 | z: PropTypes.number.isRequired, 163 | }), 164 | size: PropTypes.string, 165 | onChange: PropTypes.func, 166 | } 167 | 168 | StyleSelector.defaultProps = { 169 | map: null, 170 | tile: { 171 | x: 0, 172 | y: 0, 173 | z: 0, 174 | }, 175 | size: '64px', 176 | onChange: () => {}, 177 | } 178 | 179 | // Wrap in a Mapbox GL plugin so that we can construct the above React element on map init 180 | // TODO: pass props through 181 | class Plugin { 182 | constructor({ styles, token, position }) { 183 | this.styles = styles 184 | this.token = token 185 | this.position = position 186 | } 187 | 188 | onAdd(map) { 189 | console.log('onAdd') 190 | this.map = map 191 | 192 | const { styles, token } = this 193 | 194 | this.container = document.createElement('div') 195 | this.container.classList.add('mapboxgl-ctrl') 196 | this.container.classList.add('mapboxgl-ctrl-style-selector') 197 | this.container.style.float = 'none !important' 198 | this.container.style.cursor = 'pointer' 199 | 200 | render( 201 | , 202 | this.container 203 | ) 204 | 205 | return this.container 206 | } 207 | 208 | init() { 209 | console.log('init') 210 | const { map, styles, token, container } = this 211 | render(, container) 212 | } 213 | 214 | onRemove() { 215 | this.map = null 216 | this.container.parentNode.removeChild(this.container) 217 | } 218 | } 219 | 220 | export default Plugin 221 | -------------------------------------------------------------------------------- /src/components/Swipemap/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len, no-underscore-dangle */ 2 | import React, { useEffect, useRef, useState } from 'react' 3 | import PropTypes from 'prop-types' 4 | import mapboxgl from 'mapbox-gl' 5 | import 'mapbox-gl/dist/mapbox-gl.css' 6 | import styled from 'style' 7 | import { hasWindow } from 'util/dom' 8 | import { getCenterAndZoom } from './util' 9 | import StyleSelector from './StyleSelector' 10 | import MapboxDirections from '@mapbox/mapbox-gl-directions/dist/mapbox-gl-directions' 11 | import Compare from 'mapbox-gl-compare' 12 | import 'mapbox-gl-compare/dist/mapbox-gl-compare.css' 13 | 14 | import { siteMetadata } from '../../../gatsby-config' 15 | 16 | const Wrapper = styled.div` 17 | width: ${({ width }) => width}; 18 | height: ${({ height }) => height}; 19 | position: relative; 20 | flex: 1 0 auto; 21 | ` 22 | 23 | const Swipemap = ({ 24 | width, 25 | height, 26 | zoom, 27 | center, 28 | bounds, 29 | padding, 30 | styles, 31 | sources, 32 | layers, 33 | minZoom, 34 | maxZoom, 35 | directions, 36 | }) => { 37 | const { mapboxToken } = siteMetadata 38 | 39 | if (!mapboxToken) { 40 | console.error( 41 | 'ERROR: Mapbox token is required in gatsby-config.js siteMetadata' 42 | ) 43 | } 44 | 45 | if (!hasWindow) { 46 | return null 47 | } 48 | 49 | const mapNode = useRef(null) 50 | const mapNode2 = useRef(null) 51 | 52 | const mapRef = useRef(null) 53 | const mapRef2 = useRef(null) 54 | 55 | useEffect(() => { 56 | let mapCenter = center 57 | let mapZoom = zoom 58 | 59 | if (bounds && bounds.length === 4) { 60 | const { center: boundsCenter, zoom: boundsZoom } = getCenterAndZoom( 61 | mapNode.current, 62 | mapNode2.current, 63 | bounds, 64 | padding 65 | ) 66 | mapCenter = boundsCenter 67 | mapZoom = boundsZoom 68 | } 69 | 70 | mapboxgl.accessToken = mapboxToken 71 | 72 | const map = new mapboxgl.Map({ 73 | container: 'eins', 74 | style: `mapbox://styles/mapbox/${styles[0]}`, 75 | center: mapCenter, 76 | zoom: mapZoom, 77 | minZoom, 78 | maxZoom, 79 | }) 80 | mapRef.current = map 81 | window.map = map 82 | 83 | const map2 = new mapboxgl.Map({ 84 | container: 'zwei', 85 | style: `mapbox://styles/mapbox/${styles[1]}`, 86 | center: mapCenter, 87 | zoom: mapZoom, 88 | minZoom, 89 | maxZoom, 90 | }) 91 | mapRef2.current = map2 92 | window.map2 = map2 // for easier debugging and querying via console 93 | 94 | var compare = new Compare(map, map2, '#swipewrapper', {}) 95 | 96 | map2.addControl(new mapboxgl.NavigationControl(), 'top-right') 97 | 98 | if (styles.length > 1) { 99 | map.addControl( 100 | new StyleSelector({ 101 | styles, 102 | token: mapboxToken, 103 | }), 104 | 'bottom-left' 105 | ) 106 | } 107 | 108 | map.on('load', () => { 109 | console.log('map onload') 110 | // add sources 111 | Object.entries(sources).forEach(([id, source]) => { 112 | map.addSource(id, source) 113 | }) 114 | 115 | // add layers 116 | layers.forEach(layer => { 117 | map.addLayer(layer) 118 | }) 119 | 120 | var direction 121 | directions.forEach(directionData => { 122 | directionData.pois.forEach(poi => { 123 | new mapboxgl.Marker() 124 | .setLngLat([poi[0], poi[1]]) 125 | .setPopup(new mapboxgl.Popup({ offset: 25 }).setText(poi[2])) 126 | .addTo(map) 127 | }) 128 | direction = new MapboxDirections(directionData) 129 | direction.setOrigin(directionData.origin) 130 | direction.setDestination(directionData.destination) 131 | map.addControl(direction, 'top-right') 132 | }) 133 | }) 134 | 135 | return () => { 136 | map.remove() 137 | } 138 | }, []) 139 | 140 | return ( 141 | 142 |
146 |
150 | {/* */} 151 | 152 | ) 153 | } 154 | 155 | Swipemap.propTypes = { 156 | width: PropTypes.string, 157 | height: PropTypes.string, 158 | center: PropTypes.arrayOf(PropTypes.number), 159 | zoom: PropTypes.number, 160 | bounds: PropTypes.arrayOf(PropTypes.number), 161 | minZoom: PropTypes.number, 162 | maxZoom: PropTypes.number, 163 | styles: PropTypes.arrayOf(PropTypes.string), 164 | padding: PropTypes.number, 165 | sources: PropTypes.object, 166 | layers: PropTypes.arrayOf(PropTypes.object), 167 | directions: PropTypes.arrayOf(PropTypes.object), 168 | } 169 | 170 | Swipemap.defaultProps = { 171 | width: 'auto', 172 | height: '100%', 173 | center: [7.221275, 50.326111], 174 | zoom: 9.5, 175 | bounds: null, 176 | minZoom: 0, 177 | maxZoom: 24, 178 | styles: ['streets-v11', 'light-v9', 'dark-v9', 'satellite-v9'], 179 | padding: 0.1, 180 | sources: {}, 181 | layers: [], 182 | directions: [], 183 | } 184 | 185 | export default Swipemap 186 | -------------------------------------------------------------------------------- /src/components/Swipemap/util.js: -------------------------------------------------------------------------------- 1 | import geoViewport from '@mapbox/geo-viewport' 2 | 3 | /** 4 | * Calculate the appropriate center and zoom to fit the bounds, given padding. 5 | * @param {Element} - map DOM node used to calculate height and width in screen pixels of map 6 | * @param {Array(number)} bounds - [xmin, ymin, xmax, ymax] 7 | * @param {float} padding - proportion of calculated zoom level to zoom out by, to pad the bounds 8 | */ 9 | export const getCenterAndZoom = (mapNode, bounds, padding = 0) => { 10 | const { width, height } = mapNode 11 | const viewport = geoViewport.viewport( 12 | bounds, 13 | [width, height], 14 | undefined, 15 | undefined, 16 | undefined, 17 | true 18 | ) 19 | 20 | // Zoom out slightly to pad around bounds 21 | 22 | const zoom = Math.max(viewport.zoom - 1, 0) * (1 - padding) 23 | 24 | return { center: viewport.center, zoom } 25 | } 26 | -------------------------------------------------------------------------------- /src/constants/directions.js: -------------------------------------------------------------------------------- 1 | import { siteMetadata } from '../../gatsby-config' 2 | const { mapboxToken } = siteMetadata 3 | export default [ 4 | { 5 | //https://github.com/mapbox/mapbox-gl-directions/blob/master/src/directions.js 6 | language: 'de-DE', 7 | accessToken: mapboxToken, 8 | steps: true, 9 | exclude: 'toll', 10 | unit: 'metric', 11 | alternatives: 'true', 12 | interactive: false, 13 | controls: { 14 | inputs: true, 15 | instructions: true, 16 | profileSwitcher: true, 17 | }, 18 | profile: 'mapbox/driving', 19 | flyTo: false, 20 | // custom 21 | origin: [7.227778, 50.282222], 22 | destination: [-4.488582, 36.86133], 23 | pois: [ 24 | [1.81108, 47.069302, 'Koordinate: 1.811080, 47.069302'], 25 | [-0.610723, 43.185493, 'Koordinate: -0.610723, 43.185493'], 26 | [-3.479979, 39.819714, 'Koordinate: -3.479979, 39.819714'], 27 | ], 28 | }, 29 | ] 30 | -------------------------------------------------------------------------------- /src/constants/layercontrol.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | uniqueid: 'menu1', 4 | layer: [ 5 | { 6 | id: 'Dreieck', 7 | source: 'dreieck', 8 | sources: { 9 | dreieck: { 10 | type: 'geojson', 11 | data: { 12 | type: 'Polygon', 13 | coordinates: [[[7.03, 50.19], [7.58, 50.39], [7.18, 50.29]]], 14 | }, 15 | }, 16 | }, 17 | type: 'fill', 18 | paint: { 19 | 'fill-color': 'red', 20 | 'fill-opacity': 0.5, 21 | }, 22 | }, 23 | { 24 | id: 'Polygon', 25 | source: 'dreieck2', 26 | sources: { 27 | dreieck2: { 28 | type: 'geojson', 29 | data: { 30 | type: 'Polygon', 31 | coordinates: [ 32 | [[7.4, 50.1], [7.4, 50.3], [7.5, 50.3], [7.5, 50.1]], 33 | ], 34 | }, 35 | }, 36 | }, 37 | type: 'fill', 38 | paint: { 39 | 'fill-color': 'red', 40 | 'fill-opacity': 0.5, 41 | }, 42 | }, 43 | ], 44 | }, 45 | ] 46 | -------------------------------------------------------------------------------- /src/constants/scrollflyto.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | title: 'Bitte scrollen Sie nach unten,', 4 | text: 'um weitere Sehenswürdigkeiten zu sehen.', 5 | bearing: 27, 6 | center: [7.2212, 50.3261], 7 | zoom: 16.5, 8 | pitch: 20, 9 | }, 10 | { 11 | title: 'Genovevaburg', 12 | text: 13 | 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.', 14 | bearing: 27, 15 | center: [7.221275, 50.326111], 16 | zoom: 17.5, 17 | pitch: 20, 18 | }, 19 | { 20 | title: 'Pfarrkirche Herz-Jesu', 21 | text: 22 | 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.', 23 | duration: 50, 24 | center: [7.222546, 50.327054], 25 | bearing: 150, 26 | zoom: 18, 27 | speed: 1.1, 28 | pitch: 0, 29 | }, 30 | { 31 | title: 'Obertor', 32 | text: 33 | 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.', 34 | bearing: 90, 35 | center: [7.21869, 50.328], 36 | zoom: 17, 37 | pitch: 40, 38 | }, 39 | { 40 | title: 'Pfarrkirche St. Clemens', 41 | text: 42 | 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.', 43 | bearing: 90, 44 | center: [7.223178, 50.329886], 45 | zoom: 18.3, 46 | }, 47 | { 48 | title: 'Grubenfelder', 49 | text: 50 | 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.', 51 | bearing: 90, 52 | center: [7.24091, 50.33454], 53 | zoom: 15.2, 54 | pitch: 40, 55 | }, 56 | { 57 | title: 'Katzenberg', 58 | text: 59 | 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.', 60 | bearing: 90, 61 | center: [7.243889, 50.32], 62 | zoom: 14.5, 63 | pitch: 20, 64 | }, 65 | { 66 | title: 'Schloss Bürressheim', 67 | text: 68 | 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.', 69 | bearing: 90, 70 | center: [7.179708, 50.352792], 71 | zoom: 12.3, 72 | pitch: 20, 73 | }, 74 | ] 75 | -------------------------------------------------------------------------------- /src/constants/styles.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | title: 'Dark', 4 | uri: 'mapbox://styles/mapbox/dark-v9', 5 | }, 6 | { 7 | title: 'Light', 8 | uri: 'mapbox://styles/mapbox/light-v9', 9 | }, 10 | { 11 | title: 'Streets', 12 | uri: 'mapbox://styles/mapbox/streets-v11', 13 | }, 14 | { 15 | title: 'Satellite', 16 | uri: 'mapbox://styles/mapbox/satellite-v9', 17 | }, 18 | ] 19 | -------------------------------------------------------------------------------- /src/pages/404.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Layout from 'components/Layout' 4 | import { Container } from 'components/Grid' 5 | 6 | const NotFoundPage = () => ( 7 | 8 | 9 |

PAGE NOT FOUND

10 |
11 |
12 | ) 13 | 14 | export default NotFoundPage 15 | -------------------------------------------------------------------------------- /src/pages/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Layout from 'components/Layout' 4 | import { Container } from 'components/Grid' 5 | import Map from 'components/Map' 6 | import styled from 'style' 7 | import { siteMetadata } from '../../gatsby-config' 8 | 9 | const Section = styled.section` 10 | h3 { 11 | margin-bottom: 0.25rem; 12 | } 13 | 14 | &:not(:first-child) { 15 | margin-top: 3rem; 16 | } 17 | ` 18 | 19 | const IndexPage = () => ( 20 | 21 | 22 |

{siteMetadata.title}

23 | 24 |

25 | The menu items in the navigation can be changed in the file 26 | `gatsby-starter-mapbox-examples/src/components/Layout/Header.jsx`. 27 |

28 | 29 |
30 |

Example: a fixed size map:

31 | 32 |
33 | 34 |
35 |

36 | Example: a fluid map that fills the container and different styles: 37 |

38 | 42 |
43 |
44 |

Please see all other examples using the navigation.

45 |
46 |
47 | ) 48 | 49 | export default IndexPage 50 | -------------------------------------------------------------------------------- /src/pages/map-direction.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Layout from 'components/Layout' 4 | import Map from 'components/Map' 5 | import '@mapbox/mapbox-gl-directions/dist/mapbox-gl-directions.css' 6 | import directionData from '../constants/directions' 7 | 8 | const MapPage = () => { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | export default MapPage 17 | -------------------------------------------------------------------------------- /src/pages/map-full-plus-find-elevation.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import Findelevation from '../components/Findelevation/Findelevation' 3 | import axios from 'axios' 4 | import Layout from 'components/Layout' 5 | import Map from 'components/Map' 6 | import { siteMetadata } from '../../gatsby-config' 7 | 8 | const { mapboxToken } = siteMetadata 9 | 10 | const MapPage = () => { 11 | const findElevation = () => { 12 | map.on('click', function(e) { 13 | var query = 14 | 'https://api.mapbox.com/v4/mapbox.mapbox-terrain-v2/tilequery/' + 15 | e.lngLat.lng + 16 | ',' + 17 | e.lngLat.lat + 18 | '.json?layers=contour&limit=50&access_token=' + 19 | mapboxToken 20 | 21 | // https://stackoverflow.com/questions/50949594/axios-having-cors-issue 22 | axios.defaults.headers.post['Content-Type'] = 23 | 'application/x-www-form-urlencoded' 24 | 25 | axios 26 | .get(query) 27 | .then(function(response) { 28 | var elevations = [] 29 | response.data.features.forEach(function(feature) { 30 | elevations.push(feature.properties.ele) 31 | }) 32 | document.getElementById('findelevationfield').textContent = Math.max( 33 | ...elevations 34 | ) 35 | }) 36 | .catch(function(error) { 37 | console.log(error) 38 | }) 39 | .then(function() { 40 | // immer 41 | }) 42 | }) 43 | } 44 | 45 | useEffect(() => { 46 | window.addEventListener('click', findElevation) 47 | 48 | return () => { 49 | window.removeEventListener('click', findElevation) 50 | } 51 | }, []) 52 | 53 | return ( 54 | 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | export default MapPage 62 | -------------------------------------------------------------------------------- /src/pages/map-full.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Layout from 'components/Layout' 4 | import Map from 'components/Map' 5 | 6 | const MapPage = () => { 7 | return ( 8 | 9 | 26 | 27 | ) 28 | } 29 | 30 | export default MapPage 31 | -------------------------------------------------------------------------------- /src/pages/map-geojson-simple.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Layout from 'components/Layout' 4 | import Map from 'components/Map' 5 | 6 | const sources = { 7 | dreieck: { 8 | type: 'geojson', 9 | data: { 10 | type: 'Polygon', 11 | coordinates: [[[7.03, 50.19], [7.58, 50.39], [7.18, 50.29]]], 12 | }, 13 | }, 14 | } 15 | 16 | const layers = [ 17 | { 18 | id: '1', 19 | source: 'dreieck', 20 | type: 'fill', 21 | paint: { 22 | 'fill-color': 'red', 23 | 'fill-opacity': 0.5, 24 | }, 25 | }, 26 | ] 27 | 28 | const MapPage = () => { 29 | return ( 30 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default MapPage 37 | -------------------------------------------------------------------------------- /src/pages/map-scale-control.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Layout from 'components/Layout' 4 | import Map from 'components/Map' 5 | import '@mapbox/mapbox-gl-directions/dist/mapbox-gl-directions.css' 6 | 7 | /* https://docs.mapbox.com/mapbox-gl-js/api/markers/#scalecontrol */ 8 | const MapPage = () => { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | export default MapPage 17 | -------------------------------------------------------------------------------- /src/pages/map-show-and-hide-layers.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import Layercontrol from '../components/Layercontrol/Layercontrol' 3 | import layercontrol from '../constants/layercontrol' 4 | 5 | import Layout from 'components/Layout' 6 | import Map from 'components/Map' 7 | 8 | const MapPage = () => { 9 | const addControl = () => { 10 | layercontrol.forEach(item => { 11 | item.layer.forEach(l => { 12 | map.on('load', () => { 13 | console.log('map onload') 14 | Object.entries(l.sources).forEach(([id, source]) => { 15 | map.addSource(id, source) 16 | }) 17 | 18 | map.addLayer(l) 19 | const link = document.getElementById(l.id) 20 | 21 | link.onclick = function(e) { 22 | var clickedLayer = this.textContent 23 | e.preventDefault() 24 | e.stopPropagation() 25 | 26 | var visibility = map.getLayoutProperty(clickedLayer, 'visibility') 27 | console.log(visibility) 28 | if (visibility === 'visible' || visibility === undefined) { 29 | map.setLayoutProperty(clickedLayer, 'visibility', 'none') 30 | this.className = 'visk' 31 | } else { 32 | this.className = 'active' 33 | map.setLayoutProperty(clickedLayer, 'visibility', 'visible') 34 | } 35 | } 36 | }) 37 | }) 38 | }) 39 | } 40 | 41 | useEffect(() => { 42 | if (document.readyState === 'complete') { 43 | addControl() 44 | } else { 45 | window.addEventListener('load', addControl) 46 | 47 | return () => { 48 | window.removeEventListener('load', addControl) 49 | } 50 | } 51 | }, []) 52 | 53 | return ( 54 | 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | export default MapPage 62 | -------------------------------------------------------------------------------- /src/pages/map-style-switcher.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Layout from 'components/Layout' 4 | import Map from 'components/Map' 5 | import '@mapbox/mapbox-gl-directions/dist/mapbox-gl-directions.css' 6 | import styleSwitcherData from '../constants/styles' 7 | 8 | // https://www.npmjs.com/package/mapbox-gl-style-switcher 9 | const MapPage = () => { 10 | return ( 11 | 12 | 16 | 17 | ) 18 | } 19 | 20 | export default MapPage 21 | -------------------------------------------------------------------------------- /src/pages/map-swipe.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import 'mapbox-gl/dist/mapbox-gl.css' 3 | import Layout from 'components/Layout' 4 | import Swipemap from 'components/Swipemap' 5 | 6 | const MapPage = () => { 7 | return ( 8 | 9 | 13 | 14 | ) 15 | } 16 | 17 | export default MapPage 18 | -------------------------------------------------------------------------------- /src/pages/map.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Layout from 'components/Layout' 4 | import Map from 'components/Map' 5 | import Sidebar from 'components/Sidebar' 6 | import { Box, Flex } from 'components/Grid' 7 | 8 | import styled from 'style' 9 | 10 | // this wrapper needs to be 100% to force map and sidebar to fill the full space 11 | const Wrapper = styled(Flex)` 12 | height: 100%; 13 | ` 14 | 15 | const MapPage = () => { 16 | return ( 17 | 18 | 19 | 20 | Example sidebar content goes here 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | 28 | export default MapPage 29 | -------------------------------------------------------------------------------- /src/pages/mapwithsidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Layout from 'components/Layout' 4 | import Map from 'components/Map' 5 | import Sidebar from 'components/Sidebar' 6 | import { Box, Flex } from 'components/Grid' 7 | 8 | import styled from 'style' 9 | 10 | // this wrapper needs to be 100% to force map and sidebar to fill the full space 11 | const Wrapper = styled(Flex)` 12 | height: 100%; 13 | ` 14 | 15 | const MapPage = () => { 16 | return ( 17 | 18 | 19 | 20 | Example sidebar content goes here 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | 28 | export default MapPage 29 | -------------------------------------------------------------------------------- /src/pages/scrollflyto.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import Layout from 'components/Layout' 3 | import Map from 'components/Map' 4 | import Scrollflyto from '../components/Scrollflyto/Scrollflyto' 5 | import scrollflyto from '../constants/scrollflyto' 6 | import styled from 'style' 7 | import Sidebar from 'components/Sidebar' 8 | import { Box, Flex } from 'components/Grid' 9 | 10 | const MapPage = () => { 11 | const [scrollPosition, setSrollPosition] = useState(0) 12 | const handleScroll = () => { 13 | var activeName = scrollflyto[0].title 14 | for (var i = 0; i < scrollflyto.length; i++) { 15 | if (isElementOnScreen(scrollflyto[i].title)) { 16 | setActiveChapter(scrollflyto[i].title, i) 17 | break 18 | } 19 | } 20 | 21 | function setActiveChapter(chapterName, i) { 22 | if (chapterName === activeName) return 23 | 24 | map.flyTo(scrollflyto[i]) 25 | 26 | document.getElementById(chapterName).setAttribute('class', 'active') 27 | document.getElementById(activeName).setAttribute('class', '') 28 | 29 | activeName = chapterName 30 | } 31 | 32 | function isElementOnScreen(id) { 33 | var element = document.getElementById(id) 34 | var bounds = element.getBoundingClientRect() 35 | return bounds.top < window.innerHeight && bounds.bottom > 0 36 | } 37 | const position = window.pageYOffset 38 | setSrollPosition(position) 39 | } 40 | 41 | useEffect(() => { 42 | window.addEventListener('scroll', handleScroll, true) 43 | 44 | return () => { 45 | window.removeEventListener('scroll', handleScroll) 46 | } 47 | }, []) 48 | 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ) 61 | } 62 | 63 | // this wrapper needs to be 100% to force map and sidebar to fill the full space 64 | const Wrapper = styled(Flex)` 65 | height: 100%; 66 | ` 67 | 68 | export default MapPage 69 | -------------------------------------------------------------------------------- /src/style/index.jsx: -------------------------------------------------------------------------------- 1 | import styled, { css, ThemeProvider } from 'styled-components' 2 | import { themeGet } from 'styled-system' 3 | import theme from './theme' 4 | 5 | export { css, theme, themeGet, ThemeProvider } 6 | export default styled 7 | -------------------------------------------------------------------------------- /src/style/theme.js: -------------------------------------------------------------------------------- 1 | const breakpoints = ['40em', '52em', '64em'] 2 | 3 | // from: https://palx.jxnblk.com/ 4 | const colors = { 5 | white: '#FFF', 6 | black: '#000', 7 | link: '#07c', 8 | primary: { 9 | 0: '#e4f0f9', 10 | 100: '#c6e1f3', 11 | 200: '#a5cfed', 12 | 300: '#7db9e5', 13 | 400: '#4a9eda', 14 | 500: '#0077cc', 15 | 600: '#006bb7', 16 | 700: '#005da0', 17 | 800: '#004d84', 18 | 900: '#00365d', 19 | }, 20 | secondary: { 21 | 0: '#e3f9f7', 22 | 100: '#c4f3ef', 23 | 200: '#a0ece5', 24 | 300: '#77e3da', 25 | 400: '#44d9cd', 26 | 500: '#00ccbb', 27 | 600: '#00b8a9', 28 | 700: '#00a294', 29 | 800: '#00867b', 30 | 900: '#006159', 31 | }, 32 | highlight: { 33 | 0: '#faeaeb', 34 | 100: '#f6d2d5', 35 | 200: '#f0b7bc', 36 | 300: '#ea969d', 37 | 400: '#e16973', 38 | 500: '#cc0011', 39 | 600: '#b8000f', 40 | 700: '#a2000d', 41 | 800: '#86000b', 42 | 900: '#610008', 43 | }, 44 | grey: { 45 | 0: '#f8f9f9', 46 | 100: '#ebedee', 47 | 200: '#dee1e3', 48 | 300: '#cfd3d6', 49 | 400: '#bec4c8', 50 | 500: '#acb4b9', 51 | 600: '#97a1a7', 52 | 700: '#7f8a93', 53 | 800: '#5f6e78', 54 | 900: '#374047', 55 | }, 56 | } 57 | 58 | const theme = { 59 | breakpoints, 60 | colors, 61 | } 62 | 63 | export default theme 64 | -------------------------------------------------------------------------------- /src/util/dom.js: -------------------------------------------------------------------------------- 1 | export const hasWindow = typeof window !== 'undefined' && window 2 | 3 | export const isUnsupported = 4 | hasWindow && 5 | navigator !== undefined && 6 | (/MSIE 9/i.test(navigator.userAgent) || 7 | /MSIE 10/i.test(navigator.userAgent) || 8 | /Trident/i.test(navigator.userAgent)) 9 | 10 | export const isDebug = hasWindow && process.env.NODE_ENV === 'development' 11 | --------------------------------------------------------------------------------