├── .gitignore ├── LICENSE ├── README.md ├── dist ├── aframe-openlayers-component.js ├── aframe-openlayers-component.min.js └── main.js ├── docs ├── 1.gif ├── 2.gif ├── 3.gif ├── select.png └── simple.png ├── examples ├── build.js ├── curvedPlane.html ├── dev.js ├── inverted.html ├── invertedTwoMaps.html ├── main.js ├── select.html └── simple.html ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # compiled output 12 | /tmp 13 | /out-tsc 14 | 15 | # dependencies 16 | /node_modules 17 | /jspm_packages 18 | 19 | # Optional npm cache directory 20 | .npm 21 | 22 | # dotenv environment variables file 23 | .env 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # Output of 'npm pack' 29 | *.tgz 30 | 31 | # IDEs and editors 32 | /.idea 33 | .project 34 | .classpath 35 | .c9/ 36 | *.launch 37 | .settings/ 38 | *.sublime-workspace 39 | 40 | # IDE - VSCode 41 | .vscode/* 42 | !.vscode/settings.json 43 | !.vscode/tasks.json 44 | !.vscode/launch.json 45 | !.vscode/extensions.json 46 | 47 | # misc 48 | /.sass-cache 49 | /connect.lock 50 | /coverage 51 | /libpeerconnection.log 52 | npm-debug.log 53 | testem.log 54 | /typings 55 | 56 | # e2e 57 | /e2e/*.js 58 | /e2e/*.map 59 | 60 | # System Files 61 | .DS_Store 62 | Thumbs.db 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Luis Calisto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aframe-openlayers-component 2 | 3 | An A-Frame component that allows the use of OpenLayers maps inside A-Frame. 4 | 5 | This component allows the use of any OpenLayers map into any A-Frame object. It uses OpenLayers `postcompose` map event to export the map into an image (using canvas) and then the image is added into the A-Frame material. Also, it tries to pass OpenLayers interactions into A-frame to allow selection, pan, move etc. inside the VR environment. 6 | 7 | **[Simple example:](https://lcalisto.github.io/aframe-openlayers-component/examples/simple.html)** 8 | 9 | [![Example](docs/simple.png)](https://lcalisto.github.io/aframe-openlayers-component/examples/simple.html) 10 | 11 | 12 | **[Inverted globe example with country selection and minimap.](https://lcalisto.github.io/aframe-openlayers-component/examples/invertedTwoMaps.html)** 13 | 14 | [![Example](docs/1.gif)](https://lcalisto.github.io/aframe-openlayers-component/examples/invertedTwoMaps.html) 15 | 16 | [More examples ...](#examples) 17 | 18 | ### Schema 19 | 20 | | Property | Description | Default Value | Required | 21 | | -------- | ----------- | ------------- | ---- | 22 | | **map** | Javascript variable name of the OpenLayers map to use. | map | true| 23 | | **pixToVRRatio** | The multiplication factor between meters in A-Frame and the pixels of the map. ie; when set to 100, will display 100 pixels per 1 meter in VR space. Please check [Info about map](#info-about-map) | 100 | true | 24 | | **OlEvent** | OpenLayers event used in the interaction. This event will be passed into aframe. ie; when select is active probably you want to pass the click event. [More info about OpenLayers interaction events.](https://openlayers.org/en/latest/apidoc/ol.interaction.html) | click | false | 25 | | **aframeEvent** | A-Frame event that will be the destiny for the OLEvent. ie; When select interaction is active probably you want to pass also the click event. [More info about A-Frame events.](https://aframe.io/docs/0.8.0/introduction/interactions-and-controllers.html#sidebar) | click | false | 26 | | **width** | Normally map width is computed based on component width. But in some cases you might not give width in the component, ie; when using a radius instead of width and height. In this cases, if width is not provided in the component, you need to provide a width for the map in VR units (meters). Please check [Info about map](#info-about-map) | | false | 27 | | **height** | Normally map height is computed based on component height. But in some cases you might not give height in the component, ie; when using a radius instead of width and height. In this cases, if height is not provided in the component, you need to provide a height for the map in VR units (meters). Please check [Info about map](#info-about-map) | | false | 28 | 29 | ### Info about map 30 | 31 | Map size is computed using the folowing formula: MapWidth = (pixToVRRatio * component width) and MapHeight = (pixToVRRatio * component height) 32 | 33 | The higher `pixToVRRatio`, the more map area will be displayed per VR unit. The canvas has to be translated into a plane in VR space. This is combined with the width and height in VR space (from geometry.width and geometry.height on the entity or in case they don't exist then from the provided width and height in the OL object) to set up the map plane for rendering in 3D. 34 | 35 | The map is rendered as a texture on a 3D plane. For best performance, texture sizes should be kept to powers of 2, because of that, the component automatically resizes geometry.width and/or geometry.height to the closest power of 2 using the provided pixToVRRatio. If you don't want the component to automatically resize your objects you should make sure `width * pixToVRRatio` and `height * pixToVRRatio` are powers of 2. 36 | 37 | ### Installation options: 38 | 39 | 40 | #### 1. npm 41 | 42 | Via npm: 43 | 44 | ```bash 45 | npm install aframe 46 | npm install aframe-openlayers-component 47 | ``` 48 | 49 | Then 50 | 51 | ```js 52 | require('aframe'); 53 | require('aframe-openlayers-component'); 54 | ``` 55 | 56 | #### 2. Directly including the [minified file](dist) 57 | 58 | ```html 59 | 60 | 61 | A-Frame Openlayers Component - Simple example 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 102 | 103 | 104 | 105 | 106 |
107 |
108 | 116 |
117 | 118 | 119 | ``` 120 | 121 | ### Examples 122 | 123 | All examples are in [examples/](examples/) folder. 124 | 125 | 1. [Simple plane and a globe using the same map.](https://lcalisto.github.io/aframe-openlayers-component/examples/simple.html) 126 | 127 | 2. [Curved plane.](https://lcalisto.github.io/aframe-openlayers-component/examples/curvedPlane.html) 128 | 129 | 3. [Two map panels. A big one allowing selection and a small with the selected country.](https://lcalisto.github.io/aframe-openlayers-component/examples/select.html) 130 | 131 | 4. [Inverted globe with country selection.](https://lcalisto.github.io/aframe-openlayers-component/examples/inverted.html) 132 | 133 | 5. [Inverted globe with country selection and a centered mini map.](https://lcalisto.github.io/aframe-openlayers-component/examples/invertedTwoMaps.html) 134 | 135 | 136 | 137 | 138 | ### Notes 139 | 140 | * Component based on the idea of Allan Walker from [https://blog.mapbox.com/mapbox-gl-js-ar-js-a-frame-vr-mapbox-ar-vr-93c09be08742](https://blog.mapbox.com/mapbox-gl-js-ar-js-a-frame-vr-mapbox-ar-vr-93c09be08742) 141 | 142 | * API had some influences from the [A-Frame Tangram-Component](https://github.com/mattrei/aframe-tangram-component) 143 | -------------------------------------------------------------------------------- /dist/aframe-openlayers-component.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { 40 | /******/ configurable: false, 41 | /******/ enumerable: true, 42 | /******/ get: getter 43 | /******/ }); 44 | /******/ } 45 | /******/ }; 46 | /******/ 47 | /******/ // getDefaultExport function for compatibility with non-harmony modules 48 | /******/ __webpack_require__.n = function(module) { 49 | /******/ var getter = module && module.__esModule ? 50 | /******/ function getDefault() { return module['default']; } : 51 | /******/ function getModuleExports() { return module; }; 52 | /******/ __webpack_require__.d(getter, 'a', getter); 53 | /******/ return getter; 54 | /******/ }; 55 | /******/ 56 | /******/ // Object.prototype.hasOwnProperty.call 57 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 58 | /******/ 59 | /******/ // __webpack_public_path__ 60 | /******/ __webpack_require__.p = ""; 61 | /******/ 62 | /******/ // Load entry module and return exports 63 | /******/ return __webpack_require__(__webpack_require__.s = 0); 64 | /******/ }) 65 | /************************************************************************/ 66 | /******/ ([ 67 | /* 0 */ 68 | /***/ (function(module, exports) { 69 | 70 | if (typeof AFRAME === 'undefined') { 71 | throw new Error('Component attempted to register before AFRAME was available.'); 72 | } 73 | 74 | AFRAME.registerComponent('ol', { 75 | schema: { 76 | map: { 77 | type: 'string', 78 | default: 'map' 79 | }, 80 | pixToVRRatio: { 81 | default: 100 82 | }, 83 | aframeEvent: { 84 | type: 'string', 85 | default: 'click' 86 | }, 87 | OlEvent: { 88 | type: 'string', 89 | default: 'click' 90 | } 91 | }, 92 | init: function () { 93 | }, 94 | update: function (oldData) { 95 | var data = this.data; // Component property values. 96 | var el = this.el; // Reference to the component's entity. 97 | // Nothing changed 98 | if (AFRAME.utils.deepEqual(oldData, data)) { 99 | return; 100 | } 101 | //Check for the required schema elements 102 | if(data.map==''){ 103 | console.warn('OpenLayers component: OpenLayers map variable not specified. Aborting!'); 104 | return; 105 | } 106 | if(data.pixToVRRatio==0){ 107 | console.warn('OpenLayers component: pixToVRRatio not specified. Aborting!'); 108 | return; 109 | } 110 | if(data.aframeEvent==''){ 111 | console.warn('OpenLayers component: AFRAME event not specified. Aborting!'); 112 | return; 113 | } 114 | if(data.OlEvent==''){ 115 | console.warn('OpenLayers component: OpenLayers interaction event not specified. Aborting!'); 116 | return; 117 | } 118 | var olMap=eval(data.map) 119 | if(typeof(olMap)!='object'){ 120 | console.warn('OpenLayers component: Map object/variable cannot be found. Aborting!'); 121 | return; 122 | } 123 | //Check if element has height and width. Otherwise is not possible to plot the map on it. 124 | if(el.getAttribute('height')==null){ 125 | console.warn('OpenLayers component: Element Height is not specified. Aborting!'); 126 | return; 127 | } 128 | if(el.getAttribute('width')==null){ 129 | console.warn('OpenLayers component: Element Width is not specified. Aborting!'); 130 | return; 131 | } 132 | 133 | // Set pixToVRRatio 134 | // from https://github.com/jesstelford/aframe-map -----> https://github.com/jesstelford/aframe-map/blob/master/src/component.js 135 | 136 | var pixToVRRatio=data.pixToVRRatio; 137 | var compWidth=el.getAttribute('width'); 138 | var compHeight=el.getAttribute('height'); 139 | var mapWidth = compWidth*pixToVRRatio; 140 | var mapHeight = compHeight*pixToVRRatio; 141 | 142 | /////////////////////////// 143 | // Check if mapWidth is a power of 2. 144 | // 145 | // If not a new component size will be calculated and set using the provided ratio. 146 | // 147 | // More info here: https://gamedev.stackexchange.com/questions/26187/why-are-textures-always-square-powers-of-two-what-if-they-arent 148 | /////////////////////////// 149 | 150 | if (!(Math.log(mapWidth)/Math.log(2)) % 1 === 0) { 151 | //throw new Error("Map height is not power of 2! -> "+ mapHeight); 152 | var newMapWidth=this.nearestPow2(mapWidth); 153 | var newWidth=this.calculateNewSize(newMapWidth,pixToVRRatio); 154 | console.warn('Map width ',mapWidth,' (Ratio * component width - ',pixToVRRatio,'*',compWidth,') is not a power of 2. Setting new map width to ',newMapWidth,' and adjusting component width to ',newWidth); 155 | el.setAttribute('width',newWidth); 156 | mapWidth=newMapWidth; 157 | } 158 | if (!(Math.log(mapHeight)/Math.log(2)) % 1 === 0) { 159 | //throw new Error("Map height is not power of 2! -> "+mapHeight); 160 | var newMapHeight=this.nearestPow2(mapHeight) 161 | var newHeight=this.calculateNewSize(newMapHeight,pixToVRRatio); 162 | console.warn('Map height ',mapHeight,' (Ratio * component height - ',pixToVRRatio,'*',compHeight,') is not a power of 2. Setting new map height to ',newMapHeight,' and adjusting component height to ',newHeight); 163 | el.setAttribute('height',newHeight); 164 | mapHeight=newMapHeight; 165 | 166 | } 167 | 168 | ////Set Map Size 169 | 170 | olMap.setSize([mapWidth,mapHeight]); 171 | 172 | //// Set A-frame events 173 | 174 | el.addEventListener(data.aframeEvent, function(obj) { 175 | var objX=obj.detail.intersection.uv.x; 176 | var objY=obj.detail.intersection.uv.y; 177 | 178 | var mapSizeX=olMap.getSize()[0]; 179 | var mapSizeY=olMap.getSize()[1]; 180 | 181 | var objMapX=objX*mapSizeX; 182 | var objMapY=(1-objY)*mapSizeY; // in Y needs to be reversed because map origin is top left 183 | this.simulateEvent(data.OlEvent,objMapX,objMapY,false,olMap); 184 | }); 185 | 186 | // Postcompose event. This were we convert canvas to image and the assign this image to the aframe object 187 | 188 | olMap.on('postcompose', function(event) { 189 | var canvas = event.context.canvas; 190 | var img = canvas.toDataURL("image/png") 191 | el.setAttribute("src", img); 192 | }); 193 | olMap.renderSync(); 194 | }, 195 | 196 | //Simulated event function based on https://github.com/openlayers/openlayers/blob/83f87a1f1ee4d9a2e1e3954c908188c8a73cfb75/test/spec/ol/interaction/draw.test.js#L68 197 | simulateEvent:function (type,x,y,shiftKey,olMap){ 198 | var event = new ol.pointer.PointerEvent(type, { 199 | clientX: x, 200 | clientY: y, 201 | shiftKey: shiftKey 202 | }); 203 | olMap.handleMapBrowserEvent(new ol.MapBrowserPointerEvent(type, olMap, event)); //new ol.MapBrowserPointerEvent is available in openlayers debug 204 | }, 205 | nearestPow2: function( aSize ){ 206 | return Math.pow( 2, Math.round( Math.log( aSize ) / Math.log( 2 ) ) ); 207 | }, 208 | 209 | calculateNewSize: function (newMapSize, ratio ){ 210 | return newMapSize/ratio; 211 | } 212 | }); 213 | 214 | /***/ }) 215 | /******/ ]); -------------------------------------------------------------------------------- /dist/aframe-openlayers-component.min.js: -------------------------------------------------------------------------------- 1 | !function(t){function e(i){if(n[i])return n[i].exports;var o=n[i]={i:i,l:!1,exports:{}};return t[i].call(o.exports,o,o.exports,e),o.l=!0,o.exports}var n={};e.m=t,e.c=n,e.d=function(t,n,i){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:i})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=0)}([function(module,exports){if("undefined"==typeof AFRAME)throw new Error("Component attempted to register before AFRAME was available.");AFRAME.registerComponent("ol",{schema:{map:{type:"string",default:"map"},pixToVRRatio:{default:100},aframeEvent:{type:"string",default:"click"},OlEvent:{type:"string",default:"click"}},init:function(){},update:function(oldData){var data=this.data,el=this.el;if(!AFRAME.utils.deepEqual(oldData,data)){if(""==data.map)return void console.warn("OpenLayers component: OpenLayers map variable not specified. Aborting!");if(0==data.pixToVRRatio)return void console.warn("OpenLayers component: pixToVRRatio not specified. Aborting!");if(""==data.aframeEvent)return void console.warn("OpenLayers component: AFRAME event not specified. Aborting!");if(""==data.OlEvent)return void console.warn("OpenLayers component: OpenLayers interaction event not specified. Aborting!");var olMap=eval(data.map);if("object"!=typeof olMap)return void console.warn("OpenLayers component: Map object/variable cannot be found. Aborting!");if(null==el.getAttribute("height"))return void console.warn("OpenLayers component: Element Height is not specified. Aborting!");if(null==el.getAttribute("width"))return void console.warn("OpenLayers component: Element Width is not specified. Aborting!");var pixToVRRatio=data.pixToVRRatio,compWidth=el.getAttribute("width"),compHeight=el.getAttribute("height"),mapWidth=compWidth*pixToVRRatio,mapHeight=compHeight*pixToVRRatio;if(!(Math.log(mapWidth)/Math.log(2))%1==0){var newMapWidth=this.nearestPow2(mapWidth),newWidth=this.calculateNewSize(newMapWidth,pixToVRRatio);console.warn("Map width ",mapWidth," (Ratio * component width - ",pixToVRRatio,"*",compWidth,") is not a power of 2. Setting new map width to ",newMapWidth," and adjusting component width to ",newWidth),el.setAttribute("width",newWidth),mapWidth=newMapWidth}if(!(Math.log(mapHeight)/Math.log(2))%1==0){var newMapHeight=this.nearestPow2(mapHeight),newHeight=this.calculateNewSize(newMapHeight,pixToVRRatio);console.warn("Map height ",mapHeight," (Ratio * component height - ",pixToVRRatio,"*",compHeight,") is not a power of 2. Setting new map height to ",newMapHeight," and adjusting component height to ",newHeight),el.setAttribute("height",newHeight),mapHeight=newMapHeight}olMap.setSize([mapWidth,mapHeight]),el.addEventListener(data.aframeEvent,function(t){var e=t.detail.intersection.uv.x,n=t.detail.intersection.uv.y,i=olMap.getSize()[0],o=olMap.getSize()[1],a=e*i,r=(1-n)*o;this.simulateEvent(data.OlEvent,a,r,!1,olMap)}),olMap.on("postcompose",function(t){var e=t.context.canvas,n=e.toDataURL("image/png");el.setAttribute("src",n)}),olMap.renderSync()}},simulateEvent:function(t,e,n,i,o){var a=new ol.pointer.PointerEvent(t,{clientX:e,clientY:n,shiftKey:i});o.handleMapBrowserEvent(new ol.MapBrowserPointerEvent(t,o,a))},nearestPow2:function(t){return Math.pow(2,Math.round(Math.log(t)/Math.log(2)))},calculateNewSize:function(t,e){return t/e}})}]); -------------------------------------------------------------------------------- /dist/main.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function n(i){if(e[i])return e[i].exports;var o=e[i]={i:i,l:!1,exports:{}};return t[i].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,i){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(i,o,function(e){return t[e]}.bind(null,o));return i},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=2)}([function(module,exports){!function(t){function e(i){if(n[i])return n[i].exports;var o=n[i]={i:i,l:!1,exports:{}};return t[i].call(o.exports,o,o.exports,e),o.l=!0,o.exports}var n={};e.m=t,e.c=n,e.d=function(t,n,i){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:i})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=0)}([function(module,exports){if("undefined"==typeof AFRAME)throw new Error("Component attempted to register before AFRAME was available.");AFRAME.registerComponent("ol",{schema:{map:{type:"string",default:"map"},pixToVRRatio:{default:100},aframeEvent:{type:"string",default:"click"},OlEvent:{type:"string",default:"click"}},init:function(){},update:function(oldData){var data=this.data,el=this.el;if(!AFRAME.utils.deepEqual(oldData,data)){if(""==data.map)return void console.warn("OpenLayers component: OpenLayers map variable not specified. Aborting!");if(0==data.pixToVRRatio)return void console.warn("OpenLayers component: pixToVRRatio not specified. Aborting!");if(""==data.aframeEvent)return void console.warn("OpenLayers component: AFRAME event not specified. Aborting!");if(""==data.OlEvent)return void console.warn("OpenLayers component: OpenLayers interaction event not specified. Aborting!");var olMap=eval(data.map);if("object"!=typeof olMap)return void console.warn("OpenLayers component: Map object/variable cannot be found. Aborting!");if(null==el.getAttribute("height"))return void console.warn("OpenLayers component: Element Height is not specified. Aborting!");if(null==el.getAttribute("width"))return void console.warn("OpenLayers component: Element Width is not specified. Aborting!");var pixToVRRatio=data.pixToVRRatio,compWidth=el.getAttribute("width"),compHeight=el.getAttribute("height"),mapWidth=compWidth*pixToVRRatio,mapHeight=compHeight*pixToVRRatio;if(!(Math.log(mapWidth)/Math.log(2))%1==0){var newMapWidth=this.nearestPow2(mapWidth),newWidth=this.calculateNewSize(newMapWidth,pixToVRRatio);console.warn("Map width ",mapWidth," (Ratio * component width - ",pixToVRRatio,"*",compWidth,") is not a power of 2. Setting new map width to ",newMapWidth," and adjusting component width to ",newWidth),el.setAttribute("width",newWidth),mapWidth=newMapWidth}if(!(Math.log(mapHeight)/Math.log(2))%1==0){var newMapHeight=this.nearestPow2(mapHeight),newHeight=this.calculateNewSize(newMapHeight,pixToVRRatio);console.warn("Map height ",mapHeight," (Ratio * component height - ",pixToVRRatio,"*",compHeight,") is not a power of 2. Setting new map height to ",newMapHeight," and adjusting component height to ",newHeight),el.setAttribute("height",newHeight),mapHeight=newMapHeight}olMap.setSize([mapWidth,mapHeight]),el.addEventListener(data.aframeEvent,function(t){var e=t.detail.intersection.uv.x,n=t.detail.intersection.uv.y,i=e*olMap.getSize()[0],o=(1-n)*olMap.getSize()[1];this.simulateEvent(data.OlEvent,i,o,!1,olMap)}),olMap.on("postcompose",function(t){var e=t.context.canvas.toDataURL("image/png");el.setAttribute("src",e)}),olMap.renderSync()}},simulateEvent:function(t,e,n,i,o){var a=new ol.pointer.PointerEvent(t,{clientX:e,clientY:n,shiftKey:i});o.handleMapBrowserEvent(new ol.MapBrowserPointerEvent(t,o,a))},nearestPow2:function(t){return Math.pow(2,Math.round(Math.log(t)/Math.log(2)))},calculateNewSize:function(t,e){return t/e}})}])},function(module,exports){if("undefined"==typeof AFRAME)throw new Error("Component attempted to register before AFRAME was available.");AFRAME.registerComponent("ol",{schema:{map:{type:"string",default:"map"},height:{type:"number"},width:{type:"number"},pixToVRRatio:{default:100},aframeEvent:{type:"string",default:"click"},OlEvent:{type:"string",default:"click"}},init:function(){},update:function(oldData){var data=this.data,el=this.el,comp=this;if(!AFRAME.utils.deepEqual(oldData,data))if(""!=data.map)if(0!=data.pixToVRRatio)if(""!=data.aframeEvent)if(""!=data.OlEvent){var olMap=eval(data.map);if("object"==typeof olMap)if(null!=el.getAttribute("height")||null!=data.height&&0!=data.height)if(null!=el.getAttribute("width")||null!=data.width&&0!=data.width){var pixToVRRatio=data.pixToVRRatio,compHeight=null==el.getAttribute("height")?data.height:el.getAttribute("height"),compWidth=null==el.getAttribute("width")?data.width:el.getAttribute("width"),mapWidth=compWidth*pixToVRRatio,mapHeight=compHeight*pixToVRRatio;if(!(Math.log(mapWidth)/Math.log(2))%1==0){var newMapWidth=this.nearestPow2(mapWidth),newWidth=this.calculateNewSize(newMapWidth,pixToVRRatio);console.warn("Map width ",mapWidth," (Ratio * component width - ",pixToVRRatio,"*",compWidth,") is not a power of 2. Setting new map width to ",newMapWidth," and adjusting component width to ",newWidth),null==el.getAttribute("width")?data.width=newWidth:el.setAttribute("width",newWidth),mapWidth=newMapWidth}if(!(Math.log(mapHeight)/Math.log(2))%1==0){var newMapHeight=this.nearestPow2(mapHeight),newHeight=this.calculateNewSize(newMapHeight,pixToVRRatio);console.warn("Map height ",mapHeight," (Ratio * component height - ",pixToVRRatio,"*",compHeight,") is not a power of 2. Setting new map height to ",newMapHeight," and adjusting component height to ",newHeight),null==el.getAttribute("height")?data.height=newHeight:el.setAttribute("height",newHeight),mapHeight=newMapHeight}olMap.setSize([mapWidth,mapHeight]),el.addEventListener(data.aframeEvent,function(t){var e=t.detail.intersection.uv.x,n=t.detail.intersection.uv.y,i=e*olMap.getSize()[0],o=(1-n)*olMap.getSize()[1];comp.simulateEvent(data.OlEvent,i,o,!1,olMap)}),olMap.on("postcompose",function(t){var e=t.context.canvas.toDataURL("image/png");el.setAttribute("material","src",e)}),olMap.renderSync()}else console.warn("OpenLayers component: Element Width is not specified. Aborting!");else console.warn("OpenLayers component: Element Height is not specified. Aborting!");else console.warn("OpenLayers component: Map object/variable cannot be found. Aborting!")}else console.warn("OpenLayers component: OpenLayers interaction event not specified. Aborting!");else console.warn("OpenLayers component: AFRAME event not specified. Aborting!");else console.warn("OpenLayers component: pixToVRRatio not specified. Aborting!");else console.warn("OpenLayers component: OpenLayers map variable not specified. Aborting!")},simulateEvent:function(t,e,n,i,o){var a=new ol.pointer.PointerEvent(t,{clientX:e,clientY:n,shiftKey:i});o.handleMapBrowserEvent(new ol.MapBrowserPointerEvent(t,o,a))},nearestPow2:function(t){return Math.pow(2,Math.round(Math.log(t)/Math.log(2)))},calculateNewSize:function(t,e){return t/e}})},function(t,e,n){n(1),t.exports=n(0)}]); -------------------------------------------------------------------------------- /docs/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcalisto/aframe-openlayers-component/c45dcf471c61046e144f618ea29b24403a576c9d/docs/1.gif -------------------------------------------------------------------------------- /docs/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcalisto/aframe-openlayers-component/c45dcf471c61046e144f618ea29b24403a576c9d/docs/2.gif -------------------------------------------------------------------------------- /docs/3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcalisto/aframe-openlayers-component/c45dcf471c61046e144f618ea29b24403a576c9d/docs/3.gif -------------------------------------------------------------------------------- /docs/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcalisto/aframe-openlayers-component/c45dcf471c61046e144f618ea29b24403a576c9d/docs/select.png -------------------------------------------------------------------------------- /docs/simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcalisto/aframe-openlayers-component/c45dcf471c61046e144f618ea29b24403a576c9d/docs/simple.png -------------------------------------------------------------------------------- /examples/build.js: -------------------------------------------------------------------------------- 1 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i https://github.com/jesstelford/aframe-map/blob/master/src/component.js 76 | 77 | var pixToVRRatio=data.pixToVRRatio; 78 | 79 | //var compHeight=el.getAttribute('height'); 80 | var compHeight = el.getAttribute('height')==null ? data.height : el.getAttribute('height'); 81 | 82 | //var compWidth=el.getAttribute('width'); 83 | var compWidth = el.getAttribute('width')==null ? data.width : el.getAttribute('width'); 84 | 85 | var mapWidth = compWidth*pixToVRRatio; 86 | var mapHeight = compHeight*pixToVRRatio; 87 | 88 | /////////////////////////// 89 | // Check if mapWidth is a power of 2. 90 | // 91 | // If not a new component size will be calculated and set using the provided ratio. 92 | // 93 | // More info here: https://gamedev.stackexchange.com/questions/26187/why-are-textures-always-square-powers-of-two-what-if-they-arent 94 | /////////////////////////// 95 | 96 | if (!(Math.log(mapWidth)/Math.log(2)) % 1 === 0) { 97 | //throw new Error("Map height is not power of 2! -> "+ mapHeight); 98 | var newMapWidth=this.nearestPow2(mapWidth); 99 | var newWidth=this.calculateNewSize(newMapWidth,pixToVRRatio); 100 | console.warn('Map width ',mapWidth,' (Ratio * component width - ',pixToVRRatio,'*',compWidth,') is not a power of 2. Setting new map width to ',newMapWidth,' and adjusting component width to ',newWidth); 101 | if (el.getAttribute('width')==null ){ 102 | data.width=newWidth; 103 | }else{ 104 | el.setAttribute('width',newWidth); 105 | } 106 | mapWidth=newMapWidth; 107 | } 108 | if (!(Math.log(mapHeight)/Math.log(2)) % 1 === 0) { 109 | //throw new Error("Map height is not power of 2! -> "+mapHeight); 110 | var newMapHeight=this.nearestPow2(mapHeight) 111 | var newHeight=this.calculateNewSize(newMapHeight,pixToVRRatio); 112 | console.warn('Map height ',mapHeight,' (Ratio * component height - ',pixToVRRatio,'*',compHeight,') is not a power of 2. Setting new map height to ',newMapHeight,' and adjusting component height to ',newHeight); 113 | if (el.getAttribute('height')==null ){ 114 | data.height=newHeight; 115 | }else{ 116 | el.setAttribute('height',newHeight); 117 | } 118 | mapHeight=newMapHeight; 119 | 120 | } 121 | 122 | ////Set Map Size 123 | 124 | olMap.setSize([mapWidth,mapHeight]); 125 | 126 | //// Set A-frame events 127 | 128 | el.addEventListener(data.aframeEvent, function(obj) { 129 | var objX=obj.detail.intersection.uv.x; 130 | var objY=obj.detail.intersection.uv.y; 131 | 132 | var mapSizeX=olMap.getSize()[0]; 133 | var mapSizeY=olMap.getSize()[1]; 134 | 135 | var objMapX=objX*mapSizeX; 136 | var objMapY=(1-objY)*mapSizeY; // in Y needs to be reversed because map origin is top left 137 | comp.simulateEvent(data.OlEvent,objMapX,objMapY,false,olMap); 138 | }); 139 | 140 | // Postcompose event. This were we convert canvas to image and the assign this image to the aframe object 141 | 142 | olMap.on('postcompose', function(event) { 143 | var canvas = event.context.canvas; 144 | var img = canvas.toDataURL("image/png") 145 | //el.setAttribute("src", img); 146 | el.setAttribute("material","src", img); 147 | }); 148 | olMap.renderSync(); 149 | }, 150 | 151 | //Simulated event function based on https://github.com/openlayers/openlayers/blob/83f87a1f1ee4d9a2e1e3954c908188c8a73cfb75/test/spec/ol/interaction/draw.test.js#L68 152 | simulateEvent:function (type,x,y,shiftKey,olMap){ 153 | var event = new ol.pointer.PointerEvent(type, { 154 | clientX: x, 155 | clientY: y, 156 | shiftKey: shiftKey 157 | }); 158 | olMap.handleMapBrowserEvent(new ol.MapBrowserPointerEvent(type, olMap, event)); //new ol.MapBrowserPointerEvent is available in openlayers debug 159 | }, 160 | nearestPow2: function( aSize ){ 161 | return Math.pow( 2, Math.round( Math.log( aSize ) / Math.log( 2 ) ) ); 162 | }, 163 | 164 | calculateNewSize: function (newMapSize, ratio ){ 165 | return newMapSize/ratio; 166 | } 167 | }); 168 | },{}]},{},[1]); 169 | -------------------------------------------------------------------------------- /examples/curvedPlane.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OL VR 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 47 | 48 | 49 | 50 | 51 | 52 |
53 |
54 | 55 | 56 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /examples/dev.js: -------------------------------------------------------------------------------- 1 | // This file is only required if you use npm for dev purposes. Check dev script in ../package.json for more details 2 | var serveIndex = require('serve-index') 3 | 4 | require('budo').cli(process.argv.slice(2), { 5 | middleware: serveIndex(__dirname) 6 | }) 7 | -------------------------------------------------------------------------------- /examples/inverted.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OL VR 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 74 | 75 | 76 | 77 | 78 | 79 |
80 |
81 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /examples/invertedTwoMaps.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OL VR 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 101 | 102 | 103 | 104 | 105 | 106 |
107 |
108 |
109 | 110 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 141 | 142 | 143 |
144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | require('../index.js'); -------------------------------------------------------------------------------- /examples/select.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OL VR 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 85 | 86 | 87 | 88 | 89 | 90 |
91 |
92 |
93 | 103 | 104 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 |
125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /examples/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OL VR 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 | 65 | 66 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | if (typeof AFRAME === 'undefined') { 2 | throw new Error('Component attempted to register before AFRAME was available.'); 3 | } 4 | 5 | AFRAME.registerComponent('ol', { 6 | schema: { 7 | map: { 8 | type: 'string', 9 | default: 'map' 10 | }, 11 | height: { 12 | type: 'number' 13 | }, 14 | width: { 15 | type: 'number' 16 | }, 17 | pixToVRRatio: { 18 | default: 100 19 | }, 20 | aframeEvent: { 21 | type: 'string', 22 | default: 'click' 23 | }, 24 | OlEvent: { 25 | type: 'string', 26 | default: 'click' 27 | } 28 | }, 29 | init: function () { 30 | }, 31 | update: function (oldData) { 32 | var data = this.data; // Component property values. 33 | var el = this.el; // Reference to the component's entity. 34 | var comp=this; 35 | // Nothing changed 36 | if (AFRAME.utils.deepEqual(oldData, data)) { 37 | return; 38 | } 39 | //Check for the required schema elements 40 | if(data.map==''){ 41 | console.warn('OpenLayers component: OpenLayers map variable not specified. Aborting!'); 42 | return; 43 | } 44 | if(data.pixToVRRatio==0){ 45 | console.warn('OpenLayers component: pixToVRRatio not specified. Aborting!'); 46 | return; 47 | } 48 | if(data.aframeEvent==''){ 49 | console.warn('OpenLayers component: AFRAME event not specified. Aborting!'); 50 | return; 51 | } 52 | if(data.OlEvent==''){ 53 | console.warn('OpenLayers component: OpenLayers interaction event not specified. Aborting!'); 54 | return; 55 | } 56 | var olMap=eval(data.map) 57 | if(typeof(olMap)!='object'){ 58 | console.warn('OpenLayers component: Map object/variable cannot be found. Aborting!'); 59 | return; 60 | } 61 | //Check if element has height and width. Otherwise is not possible to plot the map on it. 62 | if(el.getAttribute('height')==null && (data.height==null || data.height==0)){ 63 | console.warn('OpenLayers component: Element Height is not specified. Aborting!'); 64 | return; 65 | } 66 | if(el.getAttribute('width')==null && (data.width==null || data.width==0)){ 67 | console.warn('OpenLayers component: Element Width is not specified. Aborting!'); 68 | return; 69 | } 70 | 71 | // Set pixToVRRatio 72 | // from https://github.com/jesstelford/aframe-map -----> https://github.com/jesstelford/aframe-map/blob/master/src/component.js 73 | 74 | var pixToVRRatio=data.pixToVRRatio; 75 | 76 | //var compHeight=el.getAttribute('height'); 77 | var compHeight = el.getAttribute('height')==null ? data.height : el.getAttribute('height'); 78 | 79 | //var compWidth=el.getAttribute('width'); 80 | var compWidth = el.getAttribute('width')==null ? data.width : el.getAttribute('width'); 81 | 82 | var mapWidth = compWidth*pixToVRRatio; 83 | var mapHeight = compHeight*pixToVRRatio; 84 | 85 | /////////////////////////// 86 | // Check if mapWidth is a power of 2. 87 | // 88 | // If not a new component size will be calculated and set using the provided ratio. 89 | // 90 | // More info here: https://gamedev.stackexchange.com/questions/26187/why-are-textures-always-square-powers-of-two-what-if-they-arent 91 | /////////////////////////// 92 | 93 | if (!(Math.log(mapWidth)/Math.log(2)) % 1 === 0) { 94 | //throw new Error("Map height is not power of 2! -> "+ mapHeight); 95 | var newMapWidth=this.nearestPow2(mapWidth); 96 | var newWidth=this.calculateNewSize(newMapWidth,pixToVRRatio); 97 | console.warn('Map width ',mapWidth,' (Ratio * component width - ',pixToVRRatio,'*',compWidth,') is not a power of 2. Setting new map width to ',newMapWidth,' and adjusting component width to ',newWidth); 98 | if (el.getAttribute('width')==null ){ 99 | data.width=newWidth; 100 | }else{ 101 | el.setAttribute('width',newWidth); 102 | } 103 | mapWidth=newMapWidth; 104 | } 105 | if (!(Math.log(mapHeight)/Math.log(2)) % 1 === 0) { 106 | //throw new Error("Map height is not power of 2! -> "+mapHeight); 107 | var newMapHeight=this.nearestPow2(mapHeight) 108 | var newHeight=this.calculateNewSize(newMapHeight,pixToVRRatio); 109 | console.warn('Map height ',mapHeight,' (Ratio * component height - ',pixToVRRatio,'*',compHeight,') is not a power of 2. Setting new map height to ',newMapHeight,' and adjusting component height to ',newHeight); 110 | if (el.getAttribute('height')==null ){ 111 | data.height=newHeight; 112 | }else{ 113 | el.setAttribute('height',newHeight); 114 | } 115 | mapHeight=newMapHeight; 116 | 117 | } 118 | 119 | ////Set Map Size 120 | 121 | olMap.setSize([mapWidth,mapHeight]); 122 | 123 | //// Set A-frame events 124 | 125 | el.addEventListener(data.aframeEvent, function(obj) { 126 | var objX=obj.detail.intersection.uv.x; 127 | var objY=obj.detail.intersection.uv.y; 128 | 129 | var mapSizeX=olMap.getSize()[0]; 130 | var mapSizeY=olMap.getSize()[1]; 131 | 132 | var objMapX=objX*mapSizeX; 133 | var objMapY=(1-objY)*mapSizeY; // in Y needs to be reversed because map origin is top left 134 | comp.simulateEvent(data.OlEvent,objMapX,objMapY,false,olMap); 135 | }); 136 | 137 | // Postcompose event. This were we convert canvas to image and the assign this image to the aframe object 138 | 139 | olMap.on('postcompose', function(event) { 140 | var canvas = event.context.canvas; 141 | var img = canvas.toDataURL("image/png") 142 | //el.setAttribute("src", img); 143 | el.setAttribute("material","src", img); 144 | }); 145 | olMap.renderSync(); 146 | }, 147 | 148 | //Simulated event function based on https://github.com/openlayers/openlayers/blob/83f87a1f1ee4d9a2e1e3954c908188c8a73cfb75/test/spec/ol/interaction/draw.test.js#L68 149 | simulateEvent:function (type,x,y,shiftKey,olMap){ 150 | var event = new ol.pointer.PointerEvent(type, { 151 | clientX: x, 152 | clientY: y, 153 | shiftKey: shiftKey 154 | }); 155 | olMap.handleMapBrowserEvent(new ol.MapBrowserPointerEvent(type, olMap, event)); //new ol.MapBrowserPointerEvent is available in openlayers debug 156 | }, 157 | nearestPow2: function( aSize ){ 158 | return Math.pow( 2, Math.round( Math.log( aSize ) / Math.log( 2 ) ) ); 159 | }, 160 | 161 | calculateNewSize: function (newMapSize, ratio ){ 162 | return newMapSize/ratio; 163 | } 164 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-openlayers-component", 3 | "version": "1.0.2", 4 | "description": "Use OpenLayers maps inside A-Frame", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "browserify examples/main.js -o examples/build.js", 8 | "dev": "node examples/dev.js examples/main.js:build.js --dir examples --port 8000 --live -t babelify", 9 | "dist": "webpack index.js dist/aframe-openlayers-component.js && webpack -p index.js dist/aframe-openlayers-component.min.js", 10 | "start": "npm run dev" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/lcalisto/aframe-openlayers-component.git" 15 | }, 16 | "keywords": [ 17 | "aframe", 18 | "aframe-component", 19 | "aframe-vr", 20 | "vr", 21 | "mozvr", 22 | "webvr", 23 | "openlayers", 24 | "gis" 25 | ], 26 | "author": "Luis Calisto", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/lcalisto/aframe-openlayers-component/issues" 30 | }, 31 | "homepage": "https://github.com/lcalisto/aframe-openlayers-component#readme", 32 | "devDependencies": { 33 | "browserify": "^16.2.2", 34 | "serve-index": "^1.9.1", 35 | "webpack": "^4.10.2", 36 | "webpack-cli": "^2.1.4" 37 | } 38 | } 39 | --------------------------------------------------------------------------------