├── .eslintignore
├── webpack.config.js
├── .eslintrc
├── .npmignore
├── .gitignore
├── LICENSE
├── examples
├── basic
│ └── index.html
├── gps
│ └── index.html
└── custom-style
│ └── index.html
├── index.html
├── CHANGELOG.md
├── package.json
├── README.md
└── index.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/*.json
2 | /lib/
3 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: './index.js',
3 | module: {
4 | loaders: [{
5 | test: /\.css$/,
6 | loader: "style-loader!css-loader"
7 | },
8 | ],
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib/
2 | cert.pem
3 | key.pem
4 |
5 | .sw[ponm]
6 | examples/build.js
7 | examples/node_modules/
8 | gh-pages
9 | node_modules/
10 | npm-debug.log
11 |
12 | # Created by https://www.gitignore.io/api/node
13 |
14 | ### Node ###
15 | # Logs
16 | logs
17 | *.log
18 | npm-debug.log*
19 |
20 | # Runtime data
21 | pids
22 | *.pid
23 | *.seed
24 | *.pid.lock
25 |
26 | # Directory for instrumented libs generated by jscoverage/JSCover
27 | lib-cov
28 |
29 | # Coverage directory used by tools like istanbul
30 | coverage
31 |
32 | # nyc test coverage
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
36 | .grunt
37 |
38 | # node-waf configuration
39 | .lock-wscript
40 |
41 | # Compiled binary addons (http://nodejs.org/api/addons.html)
42 | build/Release
43 |
44 | # Dependency directories
45 | node_modules
46 | jspm_packages
47 |
48 | # Optional npm cache directory
49 | .npm
50 |
51 | # Optional eslint cache
52 | .eslintcache
53 |
54 | # Optional REPL history
55 | .node_repl_history
56 |
57 | Session.vim
58 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | A-Frame Map - Basic
4 |
5 |
6 |
7 |
8 |
9 |
10 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/examples/gps/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | A-Frame Map - GPS
4 |
5 |
6 |
7 |
8 |
9 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/examples/custom-style/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | A-Frame Map - Custom style
4 |
5 |
6 |
7 |
8 |
9 |
10 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | A-Frame Map
4 |
23 |
24 |
25 | A-Frame Map
26 | Basic Demo
27 | Basic Satellite Map on a plane.
28 |
29 |
30 |
31 | Custom Style
32 | Setting a custom style and updating the map position dynamically every 5 secs
33 |
34 |
35 |
36 | GPS location
37 | Showing the user's location on a map
38 |
39 |
40 |
41 |
44 |
45 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aframe-mapbox-component",
3 | "version": "4.0.1",
4 | "description": "A map component using Mapbox for A-Frame.",
5 | "main": "index.js",
6 | "unpkg": "dist/aframe-mapbox-component.min.js",
7 | "scripts": {
8 | "build": "webpack index.js dist/aframe-mapbox-component.js",
9 | "dev": "budo index.js:dist/aframe-mapbox-component.min.js --port 7000 --live -- -g browserify-css",
10 | "dist": "npm run build && uglifyjs dist/aframe-mapbox-component.js > dist/aframe-mapbox-component.min.js",
11 | "lint": "semistandard -v | snazzy",
12 | "prepublish": "npm run dist",
13 | "ghpages": "ghpages",
14 | "start": "npm run dev",
15 | "test": "karma start ./tests/karma.conf.js",
16 | "test:firefox": "karma start ./tests/karma.conf.js --browsers Firefox",
17 | "test:chrome": "karma start ./tests/karma.conf.js --browsers Chrome"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/mattrei/aframe-mapbox-component.git"
22 | },
23 | "keywords": [
24 | "mapbox",
25 | "aframe",
26 | "aframe-component",
27 | "aframe-entity",
28 | "aframe-vr",
29 | "vr",
30 | "mozvr",
31 | "webvr"
32 | ],
33 | "author": "Matthias Treitler ",
34 | "license": "MIT",
35 | "bugs": {
36 | "url": "https://github.com/mattrei/aframe-mapbox-component/issues"
37 | },
38 | "homepage": "https://github.com/mattrei/aframe-mapbox-component#readme",
39 | "devDependencies": {
40 | "aframe": "*",
41 | "browserify": "^13.0.0",
42 | "browserify-css": "^0.15.0",
43 | "budo": "^8.2.2",
44 | "chai": "^3.4.1",
45 | "chai-shallow-deep-equal": "^1.3.0",
46 | "css-loader": "^2.1.1",
47 | "ghpages": "^0.0.8",
48 | "karma": "^0.13.15",
49 | "karma-browserify": "^4.4.2",
50 | "karma-chai-shallow-deep-equal": "0.0.4",
51 | "karma-chrome-launcher": "2.0.0",
52 | "karma-env-preprocessor": "^0.1.1",
53 | "karma-firefox-launcher": "^0.1.7",
54 | "karma-mocha": "^0.2.1",
55 | "karma-mocha-reporter": "^1.1.3",
56 | "karma-sinon-chai": "^1.1.0",
57 | "mocha": "^2.3.4",
58 | "semistandard": "^8.0.0",
59 | "shelljs": "^0.7.0",
60 | "shx": "^0.1.1",
61 | "sinon": "^1.17.5",
62 | "sinon-chai": "^2.8.0",
63 | "snazzy": "^4.0.0",
64 | "style-loader": "^0.23.1",
65 | "uglify-es": "github:mishoo/UglifyJS2#harmony",
66 | "webpack": "^2.7.0"
67 | },
68 | "dependencies": {
69 | "cuid": "^2.1.8",
70 | "mapbox-gl": "^1.13.0"
71 | },
72 | "semistandard": {
73 | "globals": [
74 | "AFRAME",
75 | "THREE"
76 | ],
77 | "ignore": [
78 | "examples/build.js",
79 | "dist/**"
80 | ]
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # aframe-mapbox-component
2 |
3 | A 3D street map entity & component for [A-Frame](https://aframe.io). Uses [Mapbox version 1.13.0](https://github.com/mapbox/mapbox-gl-js/blob/main/CHANGELOG.md) 3-Clause BSD.
4 |
5 | The component and idea was originally thankfully developed by [jesstelford](https://github.com/jesstelford). I forked and renamed the project accordingly and will continue to develop this component.
6 |
7 | ### Installation
8 |
9 | #### Browser
10 |
11 | Use directly from the unpkg CDN:
12 |
13 | ```html
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | ```
26 |
27 | #### npm
28 |
29 | Install via npm:
30 |
31 | ```bash
32 | npm install aframe-mapbox-component
33 | ```
34 |
35 | Then register and use.
36 |
37 | ```javascript
38 | import 'aframe';
39 | import 'aframe-mapbox-component';
40 | ```
41 |
42 | ### `mapbox` component
43 |
44 | #### Schema
45 |
46 | | attribute | type | default | description |
47 | |---|---|---|---|
48 | | 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)). However the resulting ratio is calculated from the widht and height of the material, which always needs to be in power of two. |
49 | | accesstoken | string | | An optional access token if using Mapbox's style. Not needed if you use your own styling |
50 | | style | string | '' | Either a standard Mapbox URL (like `mapbox://styles/mapbox/satellite-v8`) or a your custom style url created with [Mapbox Studio](https://www.mapbox.com/mapbox-studio/) |
51 | | ... | | | All other options are passed directly to Mapbox GL |
52 |
53 | ##### A note on fidelity
54 |
55 | The higher `pxToWorldRatio`, the more map area will be displayed per world
56 | unit. That canvas has to be translated into a plane in world space. This is
57 | combined with the width and height in world space (from geometry.width and
58 | geometry.height on the entity) to set up the plane for rendering in 3D.
59 |
60 | The map is rendered as a texture on a 3D plane. For best performance, texture
61 | sizes should be kept to powers of 2. Keeping this in mind, you should work to
62 | ensure `width * pxToWorldRatio` and `height * pxToWorldRatio` are powers of 2.
63 |
64 | #### Events
65 |
66 | | event name | data | description |
67 | |---|---|---|
68 | | `mapbox-load` | (none) | Fired before the first render of the map component |
69 | | `mapbox-loaded` | (none) | Fired on the first render of the map component |
70 | | `mapbox-moveend` | (none) | Fired when zoom, center, bearing, or pitch are changed _after_ the initial render |
71 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require('mapbox-gl/dist/mapbox-gl.css')
2 |
3 | const extendDeep = AFRAME.utils.extendDeep;
4 | const meshMixin = AFRAME.primitives.getMeshMixin();
5 |
6 | const cuid = require('cuid');
7 |
8 | const mapboxgl = require('mapbox-gl');
9 |
10 | const MAP_LOAD_EVENT = 'mapbox-load';
11 | const MAP_LOADED_EVENT = 'mapbox-loaded';
12 | const MAP_MOVE_END_EVENT = 'mapbox-moveend';
13 |
14 | function parseSpacedFloats (value, count, attributeName) {
15 | if (!value) {
16 | return undefined;
17 | }
18 |
19 | let values = value;
20 |
21 | if (Object.prototype.toString.call(value) === '[object String]') {
22 | values = value.split(',');
23 | }
24 |
25 | if (values.length !== count) {
26 | if (process.env.NODE_ENV !== 'production') {
27 | // eslint-disable-next-line no-console
28 | console.warn(
29 | `Unable to parse value of ${attributeName}: ${value}.`
30 | + ` Expected exactly ${count} space separated floats.`
31 | );
32 | }
33 | return undefined;
34 | }
35 |
36 | if (values.some(num => isNaN(parseFloat(num)))) {
37 | if (process.env.NODE_ENV !== 'production') {
38 | // eslint-disable-next-line no-console
39 | console.warn(
40 | `Unable to parse value of ${attributeName}: ${value}. `
41 | + 'Expected values to be floats.'
42 | );
43 | }
44 | return undefined;
45 | }
46 |
47 | return values;
48 | }
49 |
50 | function setDimensions (id, el, width, height) {
51 | const element = document.querySelector(`#${id}`);
52 | element.style.width = `${width}px`;
53 | element.style.height = `${height}px`;
54 |
55 | AFRAME.utils.entity.setComponentProperty(el, 'material.width', width);
56 | AFRAME.utils.entity.setComponentProperty(el, 'material.height', height);
57 | }
58 |
59 | function getCanvasContainerAssetElement (id, width, height) {
60 | let element = document.querySelector(`#${id}`);
61 |
62 | if (!element) {
63 | element = document.createElement('div');
64 | }
65 |
66 | element.setAttribute('id', id);
67 | element.style.width = `${width}px`;
68 | element.style.height = `${height}px`;
69 |
70 | // This is necessary because mapbox-gl uses the offsetWidth/Height of the
71 | // container element to calculate the canvas size. But those values are 0 if
72 | // the element (or its parent) are hidden. `position: fixed` means it can be
73 | // calculated correctly.
74 | element.style.position = 'fixed';
75 | element.style.left = '99999px';
76 | element.style.top = '0';
77 |
78 | if (!document.body.contains(element)) {
79 | document.body.appendChild(element);
80 | }
81 |
82 | return element;
83 | }
84 |
85 | function processMapboxCanvasElement (mapboxInstance, canvasContainer) {
86 | const canvas = mapboxInstance.getCanvas();
87 | canvas.setAttribute('id', cuid());
88 | canvas.setAttribute('crossorigin', 'anonymous');
89 | }
90 |
91 | function createMap (canvasId, options) {
92 | return new Promise((resolve, reject) => {
93 | const canvasContainer = getCanvasContainerAssetElement(canvasId, options.width, options.height);
94 |
95 | // eslint-disable-next-line no-new
96 | const mapboxInstance = new mapboxgl.Map(Object.assign({
97 | container: canvasContainer
98 | }, options));
99 |
100 | mapboxInstance.on('load', _ => {
101 | mapboxInstance.resize();
102 | processMapboxCanvasElement(mapboxInstance, canvasContainer);
103 | resolve(mapboxInstance);
104 | });
105 | });
106 | }
107 |
108 | /**
109 | * Map component for A-Frame.
110 | */
111 | AFRAME.registerComponent('mapbox', {
112 |
113 | dependencies: [
114 | 'geometry',
115 | 'material'
116 | ],
117 |
118 | schema: {
119 | /**
120 | * @param {number} [pxToWorldRatio=100] - The number of pixels per world
121 | * unit to render the map on the plane. ie; when set to 100, will display
122 | * 100 pixels per 1 meter in world space.
123 | */
124 | pxToWorldRatio: {default: 100},
125 |
126 | /**
127 | * @param {string} [style=''] - A URL-encoded JSON object of a [MapBox
128 | * style](https://mapbox.com/mapbox-gl-style-spec/). If none is provided,
129 | * a default style will be loaded.
130 | */
131 | style: {default: ''},
132 |
133 | /**
134 | * @param {string} [accessToken=''] - Optional access token for styles
135 | * from Mapbox.
136 | */
137 | accessToken: {default: ''},
138 |
139 | /**
140 | * @param {int} [minZoom=0] - The minimum zoom level of the map (0-20). (0
141 | * is furthest out)
142 | */
143 | minZoom: {default: 0},
144 |
145 | /**
146 | * @param {int} [maxZoom=20] - The maximum zoom level of the map (0-20). (0
147 | * is furthest out)
148 | */
149 | maxZoom: {default: 20},
150 |
151 | /**
152 | * @param {int} [bearinSnap=7] - The threshold, measured in degrees, that
153 | * determines when the map's bearing (rotation) will snap to north. For
154 | * example, with a bearingSnap of 7, if the user rotates the map within 7
155 | * degrees of north, the map will automatically snap to exact north.
156 | */
157 | bearingSnap: {default: 7},
158 |
159 | /**
160 | * @param {array} [maxBounds=undefined] - If set, the map will be
161 | * constrained to the given bounds. Bounds are specified as 4 space
162 | * delimited floats. The first pair represent the south-west long/lat, the
163 | * second pair represent the north-east long/lat.
164 | */
165 | maxBounds: {
166 | default: undefined,
167 | type: 'array',
168 | parse: value => {
169 | const values = parseSpacedFloats(value, 4, 'maxBounds');
170 |
171 | if (!values) {
172 | return undefined;
173 | }
174 |
175 | return [[values[0], values[1]], [values[2], values[3]]];
176 | }
177 | },
178 |
179 | /**
180 | * @param {array} [center=[0, 0]] - The inital geographical centerpoint of
181 | * the map in long/lat order. If center is not specified in the
182 | * constructor options, Mapbox GL JS will look for it in the map's style
183 | * object. If it is not specified in the style, either, it will default to
184 | * [0, 0]. Represented as 2 space separated floats.
185 | */
186 | center: {
187 | default: [0, 0],
188 | type: 'array',
189 | parse: value => {
190 | const values = parseSpacedFloats(value, 2, 'center');
191 |
192 | if (!values) {
193 | return [0, 0];
194 | }
195 |
196 | return values;
197 | }
198 | },
199 |
200 | /**
201 | * @param {int} [zoom=0] - The initial zoom level of the map. If zoom
202 | * is not specified in the constructor options, Mapbox GL JS will look for
203 | * it in the map's style object. If it is not specified in the style,
204 | * either, it will default to 0 .
205 | */
206 | zoom: {default: 0},
207 |
208 | /**
209 | * @param {float} [bearing=0] - The initial bearing (rotation) of the map,
210 | * measured in degrees counter-clockwise from north. If bearing is not
211 | * specified in the constructor options, Mapbox GL JS will look for it in
212 | * the map's style object. If it is not specified in the style, either, it
213 | * will default to 0.
214 | */
215 | bearing: {default: 0.0},
216 |
217 | /**
218 | * @param {float} [pitch=0] - The initial pitch (tilt) of the map,
219 | * measured in degrees away from the plane of the screen (0-60). If pitch
220 | * is not specified in the constructor options, Mapbox GL JS will look for
221 | * it in the map's style object. If it is not specified in the style,
222 | * either, it will default to 0 .
223 | */
224 | pitch: {default: 0.0},
225 |
226 | /**
227 | * All other MapBox GL options are disabled
228 | */
229 | canvas: {type: 'selector'}
230 | },
231 | init: function () {
232 | const el = this.el;
233 | const data = this.data;
234 | const geomData = el.components.geometry.data;
235 |
236 | if (data.accessToken) {
237 | mapboxgl.accessToken = data.accessToken;
238 | }
239 |
240 | const style = data.style;
241 | const width = THREE.Math.floorPowerOfTwo(geomData.width * data.pxToWorldRatio);
242 | const height = THREE.Math.floorPowerOfTwo(geomData.height * data.pxToWorldRatio);
243 | this.xPxToWorldRatio = width / geomData.width;
244 | this.yPxToWorldRatio = height / geomData.height;
245 |
246 | const options = Object.assign(
247 | {},
248 | this.data,
249 | {
250 | style,
251 | width: width,
252 | height: height,
253 | // Required to ensure the canvas can be used as a texture
254 | preserveDrawingBuffer: true,
255 | hash: false,
256 | interactive: false,
257 | attributionControl: false,
258 | scrollZoom: false,
259 | boxZoom: false,
260 | dragRotate: false,
261 | dragPan: false,
262 | keyboard: false,
263 | doubleClickZoom: false,
264 | touchZoomRotate: false,
265 | trackResize: false
266 | }
267 | );
268 |
269 | this._canvasContainerId = cuid();
270 |
271 | AFRAME.utils.entity.setComponentProperty(el, 'material.width', width);
272 | AFRAME.utils.entity.setComponentProperty(el, 'material.height', height);
273 | //setDimensions(this._canvasContainerId, el, width, height);
274 |
275 | this.created = false;
276 |
277 | const canvasContainer = getCanvasContainerAssetElement(this._canvasContainerId, options.width, options.height);
278 |
279 | // eslint-disable-next-line no-new
280 | this.mapInstance = new mapboxgl.Map(Object.assign({
281 | container: canvasContainer
282 | }, options));
283 | this.el.emit(MAP_LOAD_EVENT);
284 |
285 | this.mapInstance.once('load', _ => {
286 | this.mapInstance.resize();
287 | processMapboxCanvasElement(this.mapInstance, canvasContainer);
288 | const canvasId = document.querySelector(`#${this._canvasContainerId} canvas`).id;
289 |
290 | this.el.setAttribute('material', 'src', `#${canvasId}`);
291 | this.el.emit(MAP_LOADED_EVENT);
292 | });
293 | },
294 |
295 | /**
296 | * Called when component is attached and when component data changes.
297 | * Generally modifies the entity based on the data.
298 | */
299 | update: function (oldData) {
300 | const data = this.data;
301 | // Everything after this requires a map instance
302 | if (!this.mapInstance) {
303 | return;
304 | }
305 | if (!this.created) {
306 | oldData = {}
307 | this.created = true
308 | }
309 |
310 | // Nothing changed
311 | if (AFRAME.utils.deepEqual(oldData, data)) {
312 | return;
313 | }
314 |
315 | if (oldData.pxToWorldRatio !== data.pxToWorldRatio) {
316 | const geomData = this.el.components.geometry.data;
317 | const width = THREE.Math.floorPowerOfTwo(geomData.width * data.pxToWorldRatio);
318 | const height = THREE.Math.floorPowerOfTwo(geomData.height * data.pxToWorldRatio);
319 | this.xPxToWorldRatio = width / geomData.width;
320 | this.yPxToWorldRatio = height / geomData.height;
321 | setDimensions(this._canvasContainerId, this.el, width, height);
322 | }
323 |
324 | if (oldData.style !== this.data.style) {
325 | const style = this.data.style;
326 | this.mapInstance.setStyle(style);
327 | }
328 |
329 | if (oldData.minZoom !== this.data.minZoom) {
330 | this.mapInstance.setMinZoom(this.data.minZoom);
331 | }
332 |
333 | if (oldData.maxZoom !== this.data.maxZoom) {
334 | this.mapInstance.setMaxZoom(this.data.maxZoom);
335 | }
336 |
337 | if (oldData.maxBounds !== this.data.maxBounds) {
338 | this.mapInstance.setmaxBounds(this.data.maxBounds);
339 | }
340 |
341 | const jumpOptions = {};
342 |
343 | if (oldData.zoom !== this.data.zoom) {
344 | jumpOptions.zoom = this.data.zoom;
345 | }
346 |
347 | if (!AFRAME.utils.deepEqual(oldData.center, this.data.center)) {
348 | jumpOptions.center = this.data.center;
349 | }
350 |
351 | if (oldData.bearing !== this.data.bearing) {
352 | jumpOptions.bearing = this.data.bearing;
353 | }
354 |
355 | if (oldData.pitch !== this.data.pitch) {
356 | jumpOptions.pitch = this.data.pitch;
357 | }
358 |
359 | if (Object.keys(jumpOptions).length > 0) {
360 | // A way to signal when these async actions have completed
361 | this.mapInstance.once('moveend', _ => {
362 | this.el.emit(MAP_MOVE_END_EVENT);
363 | });
364 | this.mapInstance.once('idle', _ => {
365 | const material = this.el.getObject3D('mesh').material;
366 | if (material.map) {
367 | material.map.needsUpdate = true;
368 | }
369 | });
370 | this.mapInstance.jumpTo(jumpOptions); // moveend
371 | }
372 | },
373 |
374 | /**
375 | * Called when a component is removed (e.g., via removeAttribute).
376 | * Generally undoes all modifications to the entity.
377 | */
378 | remove () {
379 | // TODO: Kill the map
380 | },
381 |
382 | /**
383 | * Returns {x, y} representing a position relative to the entity's center,
384 | * that correspond to the specified geographical location.
385 | *
386 | * @param {float} long
387 | * @param {float} lat
388 | */
389 | project: function (long, lat) {
390 | // The position (origin at top-left corner) in pixel space
391 | const {x: pxX, y: pxY} = this.mapInstance.project([long, lat]);
392 |
393 | // The 3D world size of the entity
394 | const {width: elWidth, height: elHeight} = this.el.components.geometry.data;
395 |
396 | return {
397 | x: (pxX / this.xPxToWorldRatio) - (elWidth / 2),
398 | // y-coord is inverted (positive up in world space, positive down in
399 | // pixel space)
400 | y: -(pxY / this.yPxToWorldRatio) + (elHeight / 2),
401 | z: 0
402 | };
403 | },
404 |
405 | unproject: function (x, y) {
406 | // The 3D world size of the entity
407 | const {width: elWidth, height: elHeight} = this.el.components.geometry.data;
408 |
409 | // Converting back to pixel space
410 | const pxX = (x + (elWidth / 2)) * this.xPxToWorldRatio;
411 | // y-coord is inverted (positive up in world space, positive down in
412 | // pixel space)
413 | const pxY = ((elHeight / 2) - y) * this.yPxToWorldRatio;
414 |
415 | // Return the long / lat of that pixel on the map
416 | return this.mapInstance.unproject([pxX, pxY]).toArray();
417 | },
418 |
419 | getMap: function() {
420 | return this.mapInstance;
421 | }
422 | });
423 |
424 |
--------------------------------------------------------------------------------