├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── basic │ └── index.html ├── custom-style │ └── index.html ├── gps │ └── index.html ├── poi │ └── index.html └── url-style │ └── index.html ├── index.html ├── index.js ├── package-lock.json ├── package.json └── src ├── component.js ├── entity.js └── map-style.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.json 2 | /lib/ 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "extends": "airbnb", 6 | "rules": { 7 | "react/prefer-stateless-function": "off", 8 | "react/sort-comp": "off", 9 | "no-param-reassign": "off", 10 | "padded-blocks": "off", 11 | 12 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }], 13 | "react/prefer-es6-class": ["error", "never"], 14 | "object-curly-spacing": ["error", "never"], 15 | "no-underscore-dangle": ["error", { "allowAfterThis": true }], 16 | "no-unused-vars": ["error", { "argsIgnorePattern": "_" }], 17 | "new-cap": ["error", { "capIsNewExceptions": ["React.Children"] }], 18 | "no-use-before-define": ["error", { "functions": false }] 19 | }, 20 | "parserOptions": { 21 | "ecmaFeatures": { 22 | "experimentalObjectRestSpread": true, 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | cert.pem 4 | key.pem 5 | 6 | .sw[ponm] 7 | examples/build.js 8 | examples/node_modules/ 9 | gh-pages 10 | node_modules/ 11 | npm-debug.log 12 | 13 | # Created by https://www.gitignore.io/api/node 14 | 15 | ### Node ### 16 | # Logs 17 | logs 18 | *.log 19 | npm-debug.log* 20 | 21 | # Runtime data 22 | pids 23 | *.pid 24 | *.seed 25 | *.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (http://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules 47 | jspm_packages 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | Session.vim 59 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | .sw[ponm] 3 | examples/build.js 4 | examples/node_modules/ 5 | gh-pages 6 | node_modules/ 7 | npm-debug.log 8 | 9 | # Created by https://www.gitignore.io/api/node 10 | 11 | ### Node ### 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (http://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules 43 | jspm_packages 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased][] 9 | 10 | ## [2.1.1][] - 2016-11-24 11 | 12 | ### Fixed 13 | 14 | - Only set the center value when it's actually changed 15 | 16 | ## [2.1.0][] - 2016-11-24 17 | 18 | ### Added 19 | 20 | - Fire `map-moveend` event when zoom, center, bearing, or pitch are changed 21 | _after_ the initial render 22 | 23 | ### Fixed 24 | 25 | - Latest mapbox-gl-js dependency which comes with performance enhancements and 26 | bug fixes. 27 | 28 | ## [2.0.6][] - 2016-11-23 29 | 30 | ### Fixed 31 | 32 | - Correct projection between world and pixel coordinates 33 | 34 | ## [2.0.5][] - 2016-11-22 35 | 36 | ### Fixed 37 | 38 | - Changing styles works correctly. 39 | 40 | ## [2.0.4][] - 2016-11-21 41 | 42 | ### Fixed 43 | 44 | - Pixel to world ratio is now correctly calculated 45 | 46 | ## [2.0.3][] - 2016-11-9 47 | 48 | ### Fixed 49 | 50 | - Included missing default `map-style.json` 51 | 52 | ## [2.0.2][] - 2016-10-23 53 | 54 | - Fix changelog enforcement script 55 | 56 | ## [2.0.1][] - 2016-10-23 57 | 58 | - Use changelog enforcement tooling 59 | 60 | ## [2.0.0][] 61 | ### Added 62 | 63 | - Added a Points Of Interest example powered by Foursquare's venues API. 64 | 65 | ### Fixed 66 | 67 | - Fixed a bug where the center would be randomly innacurate 68 | when setting zoom after setting center, 69 | depending on the width & height of the map. 70 | 71 | ## [1.0.0] - 2016-10-18 72 | 73 | Initial release 🎉 74 | 75 | A real-time street map component for 76 | [AframeVR](http://aframe.io) 77 | powered by [MapBox GL](https://github.com/mapbox/mapbox-gl-js) 78 | and [osm2vectortiles](osm2vectortiles.org). 79 | 80 | [Unreleased]: https://github.com/jesstelford/aframe-map/compare/v2.1.1...HEAD 81 | [2.1.1]: https://github.com/jesstelford/aframe-map/compare/v2.1.0...v2.1.1 82 | [2.1.0]: https://github.com/jesstelford/aframe-map/compare/v2.0.6...v2.1.0 83 | [2.0.6]: https://github.com/jesstelford/aframe-map/compare/v2.0.5...v2.0.6 84 | [2.0.5]: https://github.com/jesstelford/aframe-map/compare/v2.0.4...v2.0.5 85 | [2.0.4]: https://github.com/jesstelford/aframe-map/compare/v2.0.3...v2.0.4 86 | [2.0.3]: https://github.com/jesstelford/aframe-map/compare/v2.0.2...v2.0.3 87 | [2.0.2]: https://github.com/jesstelford/aframe-map/compare/v2.0.1...v2.0.2 88 | [2.0.1]: https://github.com/jesstelford/aframe-map/compare/v2.0.0...v2.0.1 89 | [2.0.0]: https://github.com/jesstelford/aframe-map/compare/v1.0.0...v2.0.0 90 | [1.0.0]: https://github.com/jesstelford/aframe-map/tree/v1.0.0 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kevin Ngo 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 | # Looking for maintainers 2 | 3 | I have recently [started a new business](https://mobile.twitter.com/ceteio), which means I no longer have time to maintain this project. 4 | 5 | Please [reach out](https://github.com/jesstelford) if you are interested in updating this component to the latest versions of aframe, merging the pending PRs, and resolving the list of issues ❤️ 6 | 7 | # aframe-map 8 | 9 | A 3D street map entity & component for [A-Frame](https://aframe.io). 10 | 11 | The `` entity displays a plane textured with a rendered OpenStreetMap 12 | map. 13 | 14 | ### Installation 15 | 16 | #### Browser 17 | 18 | Use directly from the unpkg CDN: 19 | 20 | ```html 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ``` 33 | 34 | #### npm 35 | 36 | Install via npm: 37 | 38 | ```bash 39 | npm install aframe-map 40 | ``` 41 | 42 | Then register and use. 43 | 44 | ```javascript 45 | import 'aframe'; 46 | import 'aframe-map'; 47 | ``` 48 | 49 | ### `map` component 50 | 51 | _Note: The `` entity automatically includes the 52 | `map` component._ 53 | 54 | #### Schema 55 | 56 | | attribute | type | default | description | 57 | |---|---|---|---| 58 | | pxToWorldRatio | number | 100 | The number of pixels per world unit to render the map on the plane. ie; when set to 100, will display 100 pixels per 1 meter in world space. (see [a note on fidelity](#a-note-on-fidelity)) | 59 | | accessToken | string | | An optional access token if using Mapbox's style. Not needed if you use your own styling | 60 | | style | string | '' | Either a Mapbox URL (like `mapbox://styles/mapbox/...`) or a `JSON.stringify`'d [MapBox style](https://mapbox.com/mapbox-gl-style-spec/). If none is provided, a default style will be loaded. (see [creating a style](#creating-a-style)) | 61 | | ... | | | All other options are passed directly to Mapbox GL | 62 | 63 | ##### A note on fidelity 64 | 65 | The higher `pxToWorldRatio`, the more map area will be displayed per world 66 | unit. That canvas has to be translated into a plane in world space. This is 67 | combined with the width and height in world space (from geometry.width and 68 | geometry.height on the entity) to set up the plane for rendering in 3D. 69 | 70 | The map is rendered as a texture on a 3D plane. For best performance, texture 71 | sizes should be kept to powers of 2. Keeping this in mind, you should work to 72 | ensure `width * pxToWorldRatio` and `height * pxToWorldRatio` are powers of 2. 73 | 74 | ##### Creating a style 75 | 76 | [MapBox styles](https://mapbox.com/mapbox-gl-style-spec/) are a JSON 77 | specification for how to render different layers of the map. 78 | 79 | Creating new styles can be done by hand, or with an editor such as 80 | [Maputnik](https://github.com/maputnik/editor). 81 | Take special note of the _tile server_ to be used; 82 | Mapbox charges for access to theirs, 83 | however there is a [free CDN](http://osm2vectortiles.tileserver.com/v2.json) 84 | powered by [osm2vectortiles.org/](http://osm2vectortiles.org) (this is the 85 | server used by the default style). 86 | 87 | _Note: The osm2vectortiles free CDN defaults to insecure http URLs which will be 88 | rejected by browsers on security grounds when the main site is served over 89 | secure https._ 90 | 91 | You can also easily [host your own tile 92 | server](http://osm2vectortiles.org/docs/getting-started/)! 93 | 94 | #### Events 95 | 96 | | event name | data | description | 97 | |---|---|---| 98 | | `map-loaded` | (none) | Fired on the first render of the map component | 99 | | `map-moveend` | (none) | Fired when zoom, center, bearing, or pitch are changed _after_ the initial render | 100 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A-Frame Map - Basic 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/custom-style/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A-Frame Map - Basic 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 908 | 909 | 910 | -------------------------------------------------------------------------------- /examples/gps/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A-Frame Map - GPS 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/poi/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A-Frame Map - GPS 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /examples/url-style/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A-Frame Map - Basic 4 | 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A-Frame Map 4 | 23 | 24 | 25 |

A-Frame Map

26 | Basic Demo 27 |

Basic Map on a plane. Maps! ON A PLANE!

28 | 29 |
30 | 31 | Custom Style 32 |

Change the map display style (see the README.md for more info)

33 | 34 |
35 | 36 | Mapbox's public Style 37 |

Set the style to a Mapbox URL. Needs an access token though.

38 | 39 |
40 | GPS location 41 |

Showing the user's location on a map

42 | 43 |
44 | 45 | Points Of Interest near you 46 |

Using Foresquare data to show points of interest near you on the map

47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./src/component'); 2 | require('./src/entity'); 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-map", 3 | "version": "2.1.2", 4 | "description": "Map entity & component for A-Frame.", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "budo index.js:dist/aframe-map.min.js --port 7000 --live", 8 | "dist": "npm run rollup && uglifyjs dist/aframe-map.js > dist/aframe-map.min.js", 9 | "lint": "semistandard -v --fix | snazzy", 10 | "prepublish": "npm run dist", 11 | "ghpages": "ghpages", 12 | "rollup": "rollup index.js --output.format umd --name 'aframe-map' --output.file dist/aframe-map.js", 13 | "start": "npm run dev", 14 | "test": "karma start ./tests/karma.conf.js", 15 | "test:firefox": "karma start ./tests/karma.conf.js --browsers Firefox", 16 | "test:chrome": "karma start ./tests/karma.conf.js --browsers Chrome" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/jesstelford/aframe-map.git" 21 | }, 22 | "keywords": [ 23 | "aframe", 24 | "aframe-component", 25 | "aframe-entity", 26 | "aframe-vr", 27 | "vr", 28 | "mozvr", 29 | "webvr" 30 | ], 31 | "author": "Jess Telford ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/jesstelford/aframe-map/issues" 35 | }, 36 | "homepage": "https://github.com/jesstelford/aframe-map#readme", 37 | "devDependencies": { 38 | "aframe": "*", 39 | "browserify": "^13.0.0", 40 | "budo": "^8.2.2", 41 | "chai": "^3.4.1", 42 | "chai-shallow-deep-equal": "^1.3.0", 43 | "ghpages": "^0.0.8", 44 | "karma": "^0.13.15", 45 | "karma-browserify": "^4.4.2", 46 | "karma-chai-shallow-deep-equal": "0.0.4", 47 | "karma-chrome-launcher": "2.0.0", 48 | "karma-env-preprocessor": "^0.1.1", 49 | "karma-firefox-launcher": "^0.1.7", 50 | "karma-mocha": "^0.2.1", 51 | "karma-mocha-reporter": "^1.1.3", 52 | "karma-sinon-chai": "^1.1.0", 53 | "mocha": "^2.3.4", 54 | "randomcolor": "^0.4.4", 55 | "semistandard": "^8.0.0", 56 | "shelljs": "^0.7.0", 57 | "sinon": "^1.17.5", 58 | "sinon-chai": "^2.8.0", 59 | "shx": "^0.1.1", 60 | "snazzy": "^4.0.0", 61 | "uglify-es": "github:mishoo/UglifyJS2#harmony" 62 | }, 63 | "dependencies": { 64 | "cuid": "^1.3.8", 65 | "mapbox-gl": "^0.48.0" 66 | }, 67 | "semistandard": { 68 | "globals": [ 69 | "AFRAME", 70 | "THREE" 71 | ], 72 | "ignore": [ 73 | "examples/build.js", 74 | "dist/**" 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/component.js: -------------------------------------------------------------------------------- 1 | const cuid = require('cuid'); 2 | 3 | const mapboxgl = require('mapbox-gl'); 4 | 5 | const defaultMapStyle = require('./map-style.json'); 6 | 7 | const MAP_LOADED_EVENT = 'map-loaded'; 8 | const MAP_MOVE_END_EVENT = 'map-moveend'; 9 | 10 | function parseSpacedFloats (value, count, attributeName) { 11 | if (!value) { 12 | return undefined; 13 | } 14 | 15 | let values = value; 16 | 17 | if (Object.prototype.toString.call(value) === '[object String]') { 18 | values = value.split(','); 19 | } 20 | 21 | if (values.length !== count) { 22 | if (process.env.NODE_ENV !== 'production') { 23 | // eslint-disable-next-line no-console 24 | console.warn( 25 | `Unable to parse value of ${attributeName}: ${value}.` 26 | + ` Expected exactly ${count} space separated floats.` 27 | ); 28 | } 29 | return undefined; 30 | } 31 | 32 | if (values.some(num => isNaN(parseFloat(num)))) { 33 | if (process.env.NODE_ENV !== 'production') { 34 | // eslint-disable-next-line no-console 35 | console.warn( 36 | `Unable to parse value of ${attributeName}: ${value}. ` 37 | + 'Expected values to be floats.' 38 | ); 39 | } 40 | return undefined; 41 | } 42 | 43 | return values; 44 | } 45 | 46 | function setDimensions (id, el, width, height) { 47 | const element = document.querySelector(`#${id}`); 48 | element.style.width = `${width}px`; 49 | element.style.height = `${height}px`; 50 | 51 | AFRAME.utils.entity.setComponentProperty(el, 'material.width', width); 52 | AFRAME.utils.entity.setComponentProperty(el, 'material.height', height); 53 | } 54 | 55 | function getCanvasContainerAssetElement (id, width, height) { 56 | let element = document.querySelector(`#${id}`); 57 | 58 | if (!element) { 59 | element = document.createElement('div'); 60 | } 61 | 62 | element.setAttribute('id', id); 63 | element.style.width = `${width}px`; 64 | element.style.height = `${height}px`; 65 | 66 | // This is necessary because mapbox-gl uses the offsetWidth/Height of the 67 | // container element to calculate the canvas size. But those values are 0 if 68 | // the element (or its parent) are hidden. `position: fixed` means it can be 69 | // calculated correctly. 70 | element.style.position = 'fixed'; 71 | element.style.left = '99999px'; 72 | element.style.top = '0'; 73 | 74 | if (!document.body.contains(element)) { 75 | document.body.appendChild(element); 76 | } 77 | 78 | return element; 79 | } 80 | 81 | function processMapboxCanvasElement (mapboxInstance, canvasContainer) { 82 | const canvas = canvasContainer.querySelector('canvas'); 83 | canvas.setAttribute('id', cuid()); 84 | canvas.setAttribute('crossorigin', 'anonymous'); 85 | } 86 | 87 | function createMap (canvasId, options, done) { 88 | const canvasContainer = getCanvasContainerAssetElement(canvasId, options.width, options.height); 89 | 90 | // eslint-disable-next-line no-new 91 | const mapboxInstance = new mapboxgl.Map(Object.assign({ 92 | container: canvasContainer 93 | }, options)); 94 | 95 | if (!mapboxInstance.loaded()) { 96 | mapboxInstance.once('load', _ => { 97 | mapboxInstance.resize(); 98 | processMapboxCanvasElement(mapboxInstance, canvasContainer); 99 | done(mapboxInstance); 100 | }); 101 | } else { 102 | mapboxInstance.resize(); 103 | processMapboxCanvasElement(mapboxInstance, canvasContainer); 104 | done(mapboxInstance); 105 | } 106 | } 107 | 108 | function processStyle (style) { 109 | if (!style) { 110 | return defaultMapStyle; 111 | } 112 | 113 | try { 114 | return JSON.parse(style); 115 | } catch (e) { 116 | return style; 117 | } 118 | } 119 | 120 | /** 121 | * Map component for A-Frame. 122 | */ 123 | AFRAME.registerComponent('map', { 124 | 125 | dependencies: [ 126 | 'geometry', 127 | 'material' 128 | ], 129 | 130 | schema: { 131 | /** 132 | * @param {number} [pxToWorldRatio=100] - The number of pixels per world 133 | * unit to render the map on the plane. ie; when set to 100, will display 134 | * 100 pixels per 1 meter in world space. 135 | */ 136 | pxToWorldRatio: {default: 100}, 137 | 138 | /** 139 | * @param {string} [style=''] - A URL-encoded JSON object of a [MapBox 140 | * style](https://mapbox.com/mapbox-gl-style-spec/). If none is provided, 141 | * a default style will be loaded. 142 | */ 143 | style: {default: ''}, 144 | 145 | /** 146 | * @param {string} [accessToken=''] - Optional access token for styles 147 | * from Mapbox. 148 | */ 149 | accessToken: {default: ''}, 150 | 151 | /** 152 | * @param {int} [minZoom=0] - The minimum zoom level of the map (0-20). (0 153 | * is furthest out) 154 | */ 155 | minZoom: {default: 0}, 156 | 157 | /** 158 | * @param {int} [maxZoom=20] - The maximum zoom level of the map (0-20). (0 159 | * is furthest out) 160 | */ 161 | maxZoom: {default: 20}, 162 | 163 | /** 164 | * @param {int} [bearinSnap=7] - The threshold, measured in degrees, that 165 | * determines when the map's bearing (rotation) will snap to north. For 166 | * example, with a bearingSnap of 7, if the user rotates the map within 7 167 | * degrees of north, the map will automatically snap to exact north. 168 | */ 169 | bearingSnap: {default: 7}, 170 | 171 | /** 172 | * @param {array} [maxBounds=undefined] - If set, the map will be 173 | * constrained to the given bounds. Bounds are specified as 4 space 174 | * delimited floats. The first pair represent the south-west long/lat, the 175 | * second pair represent the north-east long/lat. 176 | */ 177 | maxBounds: { 178 | default: undefined, 179 | type: 'array', 180 | parse: value => { 181 | const values = parseSpacedFloats(value, 4, 'maxBounds'); 182 | 183 | if (!values) { 184 | return undefined; 185 | } 186 | 187 | return [[values[0], values[1]], [values[2], values[3]]]; 188 | } 189 | }, 190 | 191 | /** 192 | * @param {array} [center=[0, 0]] - The inital geographical centerpoint of 193 | * the map in long/lat order. If center is not specified in the 194 | * constructor options, Mapbox GL JS will look for it in the map's style 195 | * object. If it is not specified in the style, either, it will default to 196 | * [0, 0]. Represented as 2 space separated floats. 197 | */ 198 | center: { 199 | default: [0, 0], 200 | type: 'array', 201 | parse: value => { 202 | const values = parseSpacedFloats(value, 2, 'center'); 203 | 204 | if (!values) { 205 | return [0, 0]; 206 | } 207 | 208 | return values; 209 | } 210 | }, 211 | 212 | /** 213 | * @param {int} [zoom=0] - The initial zoom level of the map. If zoom 214 | * is not specified in the constructor options, Mapbox GL JS will look for 215 | * it in the map's style object. If it is not specified in the style, 216 | * either, it will default to 0 . 217 | */ 218 | zoom: {default: 0}, 219 | 220 | /** 221 | * @param {float} [bearing=0] - The initial bearing (rotation) of the map, 222 | * measured in degrees counter-clockwise from north. If bearing is not 223 | * specified in the constructor options, Mapbox GL JS will look for it in 224 | * the map's style object. If it is not specified in the style, either, it 225 | * will default to 0. 226 | */ 227 | bearing: {default: 0.0}, 228 | 229 | /** 230 | * @param {float} [pitch=0] - The initial pitch (tilt) of the map, 231 | * measured in degrees away from the plane of the screen (0-60). If pitch 232 | * is not specified in the constructor options, Mapbox GL JS will look for 233 | * it in the map's style object. If it is not specified in the style, 234 | * either, it will default to 0 . 235 | */ 236 | pitch: {default: 0.0} 237 | 238 | /** 239 | * All other MapBox GL options are disabled 240 | */ 241 | }, 242 | init: function () { 243 | const geomComponent = this.el.components.geometry; 244 | 245 | if (this.data.accessToken) { 246 | mapboxgl.accessToken = 'pk.eyJ1IjoibWF0dHJlIiwiYSI6IjRpa3ItcWcifQ.s0AGgKi0ai23K5OJvkEFnA' 247 | } 248 | 249 | const style = processStyle(this.data.style); 250 | 251 | const options = Object.assign( 252 | {}, 253 | this.data, 254 | { 255 | style, 256 | width: geomComponent.data.width * this.data.pxToWorldRatio, 257 | height: geomComponent.data.height * this.data.pxToWorldRatio, 258 | // Required to ensure the canvas can be used as a texture 259 | preserveDrawingBuffer: true, 260 | hash: false, 261 | interactive: false, 262 | attributionControl: false, 263 | scrollZoom: false, 264 | boxZoom: false, 265 | dragRotate: false, 266 | dragPan: false, 267 | keyboard: false, 268 | doubleClickZoom: false, 269 | touchZoomRotate: false, 270 | trackResize: false 271 | } 272 | ); 273 | 274 | this._canvasContainerId = cuid(); 275 | 276 | AFRAME.utils.entity.setComponentProperty(this.el, 'material.width', options.width); 277 | AFRAME.utils.entity.setComponentProperty(this.el, 'material.height', options.height); 278 | 279 | createMap(this._canvasContainerId, options, mapInstance => { 280 | this._mapInstance = mapInstance; 281 | 282 | const canvasId = document.querySelector(`#${this._canvasContainerId} canvas`).id; 283 | 284 | // Pointing this aframe entity to that canvas as its source 285 | this.el.setAttribute('material', 'src', `#${canvasId}`); 286 | 287 | this.el.emit(MAP_LOADED_EVENT); 288 | }); 289 | }, 290 | 291 | /** 292 | * Called when component is attached and when component data changes. 293 | * Generally modifies the entity based on the data. 294 | */ 295 | update: function (oldData) { 296 | // Nothing changed 297 | if (AFRAME.utils.deepEqual(oldData, this.data)) { 298 | return; 299 | } 300 | 301 | if (oldData.pxToWorldRatio !== this.data.pxToWorldRatio) { 302 | const geomComponent = this.el.components.geometry; 303 | const width = geomComponent.data.width * this.data.pxToWorldRatio; 304 | const height = geomComponent.data.height * this.data.pxToWorldRatio; 305 | setDimensions(this._canvasContainerId, this.el, width, height); 306 | } 307 | 308 | // Everything after this requires a map instance 309 | if (!this._mapInstance) { 310 | return; 311 | } 312 | 313 | if (oldData.style !== this.data.style) { 314 | const style = processStyle(this.data.style); 315 | this._mapInstance.setStyle(style); 316 | } 317 | 318 | if (oldData.minZoom !== this.data.minZoom) { 319 | this._mapInstance.setMinZoom(this.data.minZoom); 320 | } 321 | 322 | if (oldData.maxZoom !== this.data.maxZoom) { 323 | this._mapInstance.setmaxZoom(this.data.maxZoom); 324 | } 325 | 326 | if (oldData.maxBounds !== this.data.maxBounds) { 327 | this._mapInstance.setmaxBounds(this.data.maxBounds); 328 | } 329 | 330 | const jumpOptions = {}; 331 | 332 | if (oldData.zoom !== this.data.zoom) { 333 | jumpOptions.zoom = this.data.zoom; 334 | // we also need to set the center otherwise the mapbox library goes crazy 335 | jumpOptions.center = this.data.center || oldData.center; 336 | } 337 | 338 | if (!AFRAME.utils.deepEqual(oldData.center, this.data.center)) { 339 | jumpOptions.center = this.data.center; 340 | } 341 | 342 | if (oldData.bearing !== this.data.bearing) { 343 | jumpOptions.bearing = this.data.bearing; 344 | } 345 | 346 | if (oldData.pitch !== this.data.pitch) { 347 | jumpOptions.pitch = this.data.pitch; 348 | } 349 | 350 | if (Object.keys(jumpOptions).length > 0) { 351 | // A way to signal when these async actions have completed 352 | this._mapInstance.once('moveend', _ => { 353 | this.el.emit(MAP_MOVE_END_EVENT); 354 | }); 355 | this._mapInstance.jumpTo(jumpOptions); // moveend 356 | } 357 | }, 358 | 359 | /** 360 | * Called when a component is removed (e.g., via removeAttribute). 361 | * Generally undoes all modifications to the entity. 362 | */ 363 | remove () { 364 | // TODO: Kill the map 365 | }, 366 | 367 | /** 368 | * Called when entity pauses. 369 | * Use to stop or remove any dynamic or background behavior such as events. 370 | */ 371 | pause () { 372 | }, 373 | 374 | /** 375 | * Called when entity resumes. 376 | * Use to continue or add any dynamic or background behavior such as events. 377 | */ 378 | play () { 379 | }, 380 | 381 | /** 382 | * Returns {x, y} representing a position relative to the entity's center, 383 | * that correspond to the specified geographical location. 384 | * 385 | * @param {float} long 386 | * @param {float} lat 387 | */ 388 | project: function (long, lat) { 389 | // The position (origin at top-left corner) in pixel space 390 | const {x: pxX, y: pxY} = this._mapInstance.project([long, lat]); 391 | 392 | // The 3D world size of the entity 393 | const {width: elWidth, height: elHeight} = this.el.components.geometry.data; 394 | 395 | return { 396 | x: (pxX / this.data.pxToWorldRatio) - (elWidth / 2), 397 | // y-coord is inverted (positive up in world space, positive down in 398 | // pixel space) 399 | y: -(pxY / this.data.pxToWorldRatio) + (elHeight / 2), 400 | z: 0 401 | }; 402 | }, 403 | 404 | unproject: function (x, y) { 405 | // The 3D world size of the entity 406 | const {width: elWidth, height: elHeight} = this.el.components.geometry.data; 407 | 408 | // Converting back to pixel space 409 | const pxX = (x + (elWidth / 2)) * this.data.pxToWorldRatio; 410 | // y-coord is inverted (positive up in world space, positive down in 411 | // pixel space) 412 | const pxY = ((elHeight / 2) - y) * this.data.pxToWorldRatio; 413 | 414 | // Return the long / lat of that pixel on the map 415 | return this._mapInstance.unproject([pxX, pxY]).toArray(); 416 | } 417 | }); 418 | -------------------------------------------------------------------------------- /src/entity.js: -------------------------------------------------------------------------------- 1 | const extendDeep = AFRAME.utils.extendDeep; 2 | const meshMixin = AFRAME.primitives.getMeshMixin(); 3 | 4 | AFRAME.registerPrimitive('a-map', extendDeep({}, meshMixin, { 5 | defaultComponents: { 6 | geometry: { 7 | primitive: 'plane' 8 | }, 9 | material: { 10 | color: '#ffffff', 11 | shader: 'flat', 12 | side: 'both', 13 | transparent: true 14 | }, 15 | ['map']: {} 16 | }, 17 | 18 | mappings: { 19 | height: 'geometry.height', 20 | width: 'geometry.width' 21 | } 22 | })); 23 | 24 | -------------------------------------------------------------------------------- /src/map-style.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers": [ 3 | { 4 | "id": "background", 5 | "type": "background", 6 | "paint": { 7 | "background-color": "#dedede" 8 | }, 9 | "interactive": true 10 | }, 11 | { 12 | "id": "landuse_overlay_national_park", 13 | "type": "fill", 14 | "source": "mapbox", 15 | "source-layer": "landuse_overlay", 16 | "filter": [ 17 | "==", 18 | "class", 19 | "national_park" 20 | ], 21 | "paint": { 22 | "fill-color": "#d2edae", 23 | "fill-opacity": 0.75 24 | }, 25 | "interactive": true 26 | }, 27 | { 28 | "id": "landuse_park", 29 | "type": "fill", 30 | "source": "mapbox", 31 | "source-layer": "landuse", 32 | "filter": [ 33 | "==", 34 | "class", 35 | "park" 36 | ], 37 | "paint": { 38 | "fill-color": "#d2edae" 39 | }, 40 | "interactive": true 41 | }, 42 | { 43 | "id": "waterway", 44 | "type": "line", 45 | "source": "mapbox", 46 | "source-layer": "waterway", 47 | "filter": [ 48 | "all", 49 | [ 50 | "==", 51 | "$type", 52 | "LineString" 53 | ], 54 | [ 55 | "in", 56 | "class", 57 | "river", 58 | "canal" 59 | ] 60 | ], 61 | "paint": { 62 | "line-color": "#a0cfdf", 63 | "line-width": { 64 | "base": 1.4, 65 | "stops": [ 66 | [ 67 | 8, 68 | 0.5 69 | ], 70 | [ 71 | 20, 72 | 15 73 | ] 74 | ] 75 | } 76 | }, 77 | "interactive": true 78 | }, 79 | { 80 | "id": "water", 81 | "type": "fill", 82 | "source": "mapbox", 83 | "source-layer": "water", 84 | "paint": { 85 | "fill-color": "#a0cfdf" 86 | }, 87 | "interactive": true 88 | }, 89 | { 90 | "id": "building", 91 | "type": "fill", 92 | "source": "mapbox", 93 | "source-layer": "building", 94 | "paint": { 95 | "fill-color": "#d6d6d6" 96 | }, 97 | "interactive": true 98 | }, 99 | { 100 | "interactive": true, 101 | "layout": { 102 | "line-cap": "butt", 103 | "line-join": "miter" 104 | }, 105 | "filter": [ 106 | "all", 107 | [ 108 | "==", 109 | "$type", 110 | "LineString" 111 | ], 112 | [ 113 | "all", 114 | [ 115 | "in", 116 | "class", 117 | "motorway_link", 118 | "street", 119 | "street_limited", 120 | "service", 121 | "track", 122 | "pedestrian", 123 | "path", 124 | "link" 125 | ], 126 | [ 127 | "==", 128 | "structure", 129 | "tunnel" 130 | ] 131 | ] 132 | ], 133 | "type": "line", 134 | "source": "mapbox", 135 | "id": "tunnel_minor", 136 | "paint": { 137 | "line-color": "#efefef", 138 | "line-width": { 139 | "base": 1.55, 140 | "stops": [ 141 | [ 142 | 4, 143 | 0.25 144 | ], 145 | [ 146 | 20, 147 | 30 148 | ] 149 | ] 150 | }, 151 | "line-dasharray": [ 152 | 0.36, 153 | 0.18 154 | ] 155 | }, 156 | "source-layer": "road" 157 | }, 158 | { 159 | "interactive": true, 160 | "layout": { 161 | "line-cap": "butt", 162 | "line-join": "miter" 163 | }, 164 | "filter": [ 165 | "all", 166 | [ 167 | "==", 168 | "$type", 169 | "LineString" 170 | ], 171 | [ 172 | "all", 173 | [ 174 | "in", 175 | "class", 176 | "motorway", 177 | "primary", 178 | "secondary", 179 | "tertiary", 180 | "trunk" 181 | ], 182 | [ 183 | "==", 184 | "structure", 185 | "tunnel" 186 | ] 187 | ] 188 | ], 189 | "type": "line", 190 | "source": "mapbox", 191 | "id": "tunnel_major", 192 | "paint": { 193 | "line-color": "#fff", 194 | "line-width": { 195 | "base": 1.4, 196 | "stops": [ 197 | [ 198 | 6, 199 | 0.5 200 | ], 201 | [ 202 | 20, 203 | 30 204 | ] 205 | ] 206 | }, 207 | "line-dasharray": [ 208 | 0.28, 209 | 0.14 210 | ] 211 | }, 212 | "source-layer": "road" 213 | }, 214 | { 215 | "interactive": true, 216 | "layout": { 217 | "line-cap": "round", 218 | "line-join": "round" 219 | }, 220 | "filter": [ 221 | "all", 222 | [ 223 | "==", 224 | "$type", 225 | "LineString" 226 | ], 227 | [ 228 | "all", 229 | [ 230 | "in", 231 | "class", 232 | "motorway_link", 233 | "street", 234 | "street_limited", 235 | "service", 236 | "track", 237 | "pedestrian", 238 | "path", 239 | "link" 240 | ], 241 | [ 242 | "in", 243 | "structure", 244 | "none", 245 | "ford" 246 | ] 247 | ] 248 | ], 249 | "type": "line", 250 | "source": "mapbox", 251 | "id": "road_minor", 252 | "paint": { 253 | "line-color": "#efefef", 254 | "line-width": { 255 | "base": 1.55, 256 | "stops": [ 257 | [ 258 | 4, 259 | 0.25 260 | ], 261 | [ 262 | 20, 263 | 30 264 | ] 265 | ] 266 | } 267 | }, 268 | "source-layer": "road" 269 | }, 270 | { 271 | "interactive": true, 272 | "layout": { 273 | "line-cap": "round", 274 | "line-join": "round" 275 | }, 276 | "filter": [ 277 | "all", 278 | [ 279 | "==", 280 | "$type", 281 | "LineString" 282 | ], 283 | [ 284 | "all", 285 | [ 286 | "in", 287 | "class", 288 | "motorway", 289 | "primary", 290 | "secondary", 291 | "tertiary", 292 | "trunk" 293 | ], 294 | [ 295 | "in", 296 | "structure", 297 | "none", 298 | "ford" 299 | ] 300 | ] 301 | ], 302 | "type": "line", 303 | "source": "mapbox", 304 | "id": "road_major", 305 | "paint": { 306 | "line-color": "#fff", 307 | "line-width": { 308 | "base": 1.4, 309 | "stops": [ 310 | [ 311 | 6, 312 | 0.5 313 | ], 314 | [ 315 | 20, 316 | 30 317 | ] 318 | ] 319 | } 320 | }, 321 | "source-layer": "road" 322 | }, 323 | { 324 | "interactive": true, 325 | "layout": { 326 | "line-cap": "butt", 327 | "line-join": "miter" 328 | }, 329 | "filter": [ 330 | "all", 331 | [ 332 | "==", 333 | "$type", 334 | "LineString" 335 | ], 336 | [ 337 | "all", 338 | [ 339 | "in", 340 | "class", 341 | "motorway_link", 342 | "street", 343 | "street_limited", 344 | "service", 345 | "track", 346 | "pedestrian", 347 | "path", 348 | "link" 349 | ], 350 | [ 351 | "==", 352 | "structure", 353 | "bridge" 354 | ] 355 | ] 356 | ], 357 | "type": "line", 358 | "source": "mapbox", 359 | "id": "bridge_minor case", 360 | "paint": { 361 | "line-color": "#dedede", 362 | "line-width": { 363 | "base": 1.6, 364 | "stops": [ 365 | [ 366 | 12, 367 | 0.5 368 | ], 369 | [ 370 | 20, 371 | 10 372 | ] 373 | ] 374 | }, 375 | "line-gap-width": { 376 | "base": 1.55, 377 | "stops": [ 378 | [ 379 | 4, 380 | 0.25 381 | ], 382 | [ 383 | 20, 384 | 30 385 | ] 386 | ] 387 | } 388 | }, 389 | "source-layer": "road" 390 | }, 391 | { 392 | "interactive": true, 393 | "layout": { 394 | "line-cap": "butt", 395 | "line-join": "miter" 396 | }, 397 | "filter": [ 398 | "all", 399 | [ 400 | "==", 401 | "$type", 402 | "LineString" 403 | ], 404 | [ 405 | "all", 406 | [ 407 | "in", 408 | "class", 409 | "motorway", 410 | "primary", 411 | "secondary", 412 | "tertiary", 413 | "trunk" 414 | ], 415 | [ 416 | "==", 417 | "structure", 418 | "bridge" 419 | ] 420 | ] 421 | ], 422 | "type": "line", 423 | "source": "mapbox", 424 | "id": "bridge_major case", 425 | "paint": { 426 | "line-color": "#dedede", 427 | "line-width": { 428 | "base": 1.6, 429 | "stops": [ 430 | [ 431 | 12, 432 | 0.5 433 | ], 434 | [ 435 | 20, 436 | 10 437 | ] 438 | ] 439 | }, 440 | "line-gap-width": { 441 | "base": 1.55, 442 | "stops": [ 443 | [ 444 | 4, 445 | 0.25 446 | ], 447 | [ 448 | 20, 449 | 30 450 | ] 451 | ] 452 | } 453 | }, 454 | "source-layer": "road" 455 | }, 456 | { 457 | "interactive": true, 458 | "layout": { 459 | "line-cap": "round", 460 | "line-join": "round" 461 | }, 462 | "filter": [ 463 | "all", 464 | [ 465 | "==", 466 | "$type", 467 | "LineString" 468 | ], 469 | [ 470 | "all", 471 | [ 472 | "in", 473 | "class", 474 | "motorway_link", 475 | "street", 476 | "street_limited", 477 | "service", 478 | "track", 479 | "pedestrian", 480 | "path", 481 | "link" 482 | ], 483 | [ 484 | "==", 485 | "structure", 486 | "bridge" 487 | ] 488 | ] 489 | ], 490 | "type": "line", 491 | "source": "mapbox", 492 | "id": "bridge_minor", 493 | "paint": { 494 | "line-color": "#efefef", 495 | "line-width": { 496 | "base": 1.55, 497 | "stops": [ 498 | [ 499 | 4, 500 | 0.25 501 | ], 502 | [ 503 | 20, 504 | 30 505 | ] 506 | ] 507 | } 508 | }, 509 | "source-layer": "road" 510 | }, 511 | { 512 | "interactive": true, 513 | "layout": { 514 | "line-cap": "round", 515 | "line-join": "round" 516 | }, 517 | "filter": [ 518 | "all", 519 | [ 520 | "==", 521 | "$type", 522 | "LineString" 523 | ], 524 | [ 525 | "all", 526 | [ 527 | "in", 528 | "class", 529 | "motorway", 530 | "primary", 531 | "secondary", 532 | "tertiary", 533 | "trunk" 534 | ], 535 | [ 536 | "==", 537 | "structure", 538 | "bridge" 539 | ] 540 | ] 541 | ], 542 | "type": "line", 543 | "source": "mapbox", 544 | "id": "bridge_major", 545 | "paint": { 546 | "line-color": "#fff", 547 | "line-width": { 548 | "base": 1.4, 549 | "stops": [ 550 | [ 551 | 6, 552 | 0.5 553 | ], 554 | [ 555 | 20, 556 | 30 557 | ] 558 | ] 559 | } 560 | }, 561 | "source-layer": "road" 562 | }, 563 | { 564 | "interactive": true, 565 | "layout": { 566 | "line-cap": "round", 567 | "line-join": "round" 568 | }, 569 | "filter": [ 570 | "all", 571 | [ 572 | "==", 573 | "$type", 574 | "LineString" 575 | ], 576 | [ 577 | "all", 578 | [ 579 | "<=", 580 | "admin_level", 581 | 2 582 | ], 583 | [ 584 | "==", 585 | "maritime", 586 | 0 587 | ] 588 | ] 589 | ], 590 | "type": "line", 591 | "source": "mapbox", 592 | "id": "admin_country", 593 | "paint": { 594 | "line-color": "#8b8a8a", 595 | "line-width": { 596 | "base": 1.3, 597 | "stops": [ 598 | [ 599 | 3, 600 | 0.5 601 | ], 602 | [ 603 | 22, 604 | 15 605 | ] 606 | ] 607 | } 608 | }, 609 | "source-layer": "admin" 610 | }, 611 | { 612 | "interactive": true, 613 | "minzoom": 5, 614 | "layout": { 615 | "icon-image": "{maki}-11", 616 | "text-offset": [ 617 | 0, 618 | 0.5 619 | ], 620 | "text-field": "{name_en}", 621 | "text-font": [ 622 | "Open Sans Semibold" 623 | ], 624 | "text-max-width": 8, 625 | "text-anchor": "top", 626 | "text-size": 11, 627 | "icon-size": 1 628 | }, 629 | "filter": [ 630 | "all", 631 | [ 632 | "==", 633 | "$type", 634 | "Point" 635 | ], 636 | [ 637 | "all", 638 | [ 639 | "==", 640 | "scalerank", 641 | 1 642 | ], 643 | [ 644 | "==", 645 | "localrank", 646 | 1 647 | ] 648 | ] 649 | ], 650 | "type": "symbol", 651 | "source": "mapbox", 652 | "id": "poi_label", 653 | "paint": { 654 | "text-color": "#666", 655 | "text-halo-width": 1, 656 | "text-halo-color": "rgba(255,255,255,0.75)", 657 | "text-halo-blur": 1 658 | }, 659 | "source-layer": "poi_label" 660 | }, 661 | { 662 | "interactive": true, 663 | "layout": { 664 | "symbol-placement": "line", 665 | "text-field": "{name_en}", 666 | "text-font": [ 667 | "Open Sans Semibold" 668 | ], 669 | "text-transform": "uppercase", 670 | "text-letter-spacing": 0.1, 671 | "text-size": { 672 | "base": 1.4, 673 | "stops": [ 674 | [ 675 | 10, 676 | 8 677 | ], 678 | [ 679 | 20, 680 | 14 681 | ] 682 | ] 683 | } 684 | }, 685 | "filter": [ 686 | "all", 687 | [ 688 | "==", 689 | "$type", 690 | "LineString" 691 | ], 692 | [ 693 | "in", 694 | "class", 695 | "motorway", 696 | "primary", 697 | "secondary", 698 | "tertiary", 699 | "trunk" 700 | ] 701 | ], 702 | "type": "symbol", 703 | "source": "mapbox", 704 | "id": "road_major_label", 705 | "paint": { 706 | "text-color": "#666", 707 | "text-halo-color": "rgba(255,255,255,0.75)", 708 | "text-halo-width": 2 709 | }, 710 | "source-layer": "road_label" 711 | }, 712 | { 713 | "interactive": true, 714 | "minzoom": 8, 715 | "layout": { 716 | "text-field": "{name_en}", 717 | "text-font": [ 718 | "Open Sans Semibold" 719 | ], 720 | "text-max-width": 6, 721 | "text-size": { 722 | "stops": [ 723 | [ 724 | 6, 725 | 12 726 | ], 727 | [ 728 | 12, 729 | 16 730 | ] 731 | ] 732 | } 733 | }, 734 | "filter": [ 735 | "all", 736 | [ 737 | "==", 738 | "$type", 739 | "Point" 740 | ], 741 | [ 742 | "in", 743 | "type", 744 | "town", 745 | "village", 746 | "hamlet", 747 | "suburb", 748 | "neighbourhood", 749 | "island" 750 | ] 751 | ], 752 | "type": "symbol", 753 | "source": "mapbox", 754 | "id": "place_label_other", 755 | "paint": { 756 | "text-color": "#666", 757 | "text-halo-color": "rgba(255,255,255,0.75)", 758 | "text-halo-width": 1, 759 | "text-halo-blur": 1 760 | }, 761 | "source-layer": "place_label" 762 | }, 763 | { 764 | "interactive": true, 765 | "layout": { 766 | "text-field": "{name_en}", 767 | "text-font": [ 768 | "Open Sans Bold" 769 | ], 770 | "text-max-width": 10, 771 | "text-size": { 772 | "stops": [ 773 | [ 774 | 3, 775 | 12 776 | ], 777 | [ 778 | 8, 779 | 16 780 | ] 781 | ] 782 | } 783 | }, 784 | "maxzoom": 16, 785 | "filter": [ 786 | "all", 787 | [ 788 | "==", 789 | "$type", 790 | "Point" 791 | ], 792 | [ 793 | "==", 794 | "type", 795 | "city" 796 | ] 797 | ], 798 | "type": "symbol", 799 | "source": "mapbox", 800 | "id": "place_label_city", 801 | "paint": { 802 | "text-color": "#666", 803 | "text-halo-color": "rgba(255,255,255,0.75)", 804 | "text-halo-width": 1, 805 | "text-halo-blur": 1 806 | }, 807 | "source-layer": "place_label" 808 | }, 809 | { 810 | "interactive": true, 811 | "layout": { 812 | "text-field": "{name_en}", 813 | "text-font": [ 814 | "Open Sans Regular" 815 | ], 816 | "text-max-width": 10, 817 | "text-size": { 818 | "stops": [ 819 | [ 820 | 3, 821 | 14 822 | ], 823 | [ 824 | 8, 825 | 22 826 | ] 827 | ] 828 | } 829 | }, 830 | "maxzoom": 12, 831 | "filter": [ 832 | "==", 833 | "$type", 834 | "Point" 835 | ], 836 | "type": "symbol", 837 | "source": "mapbox", 838 | "id": "country_label", 839 | "paint": { 840 | "text-color": "#666", 841 | "text-halo-color": "rgba(255,255,255,0.75)", 842 | "text-halo-width": 1, 843 | "text-halo-blur": 1 844 | }, 845 | "source-layer": "country_label" 846 | } 847 | ], 848 | "sprite": "https://raw.githubusercontent.com/osm2vectortiles/mapbox-gl-styles/master/sprites/basic-v9", 849 | "glyphs": "https://raw.githubusercontent.com/osm2vectortiles/mapbox-gl-styles/master/glyphs/{fontstack}/{range}.pbf", 850 | "created": "2016-10-16T04:40:12.678Z", 851 | "name": "Basic", 852 | "metadata": { 853 | "mapbox:autocomposite": true, 854 | "mapbox:type": "template" 855 | }, 856 | "version": 8, 857 | "sources": { 858 | "mapbox": { 859 | "url": "https://osm2vectortiles.tileserver.com/v2.json", 860 | "type": "vector" 861 | } 862 | }, 863 | "id": "giqd6hug7" 864 | } 865 | --------------------------------------------------------------------------------