UNPKG

aframe-globe-component

Version:
779 lines (764 loc) 22.2 kB
import ThreeGlobe from 'three-globe'; /* global AFRAME */ if (typeof AFRAME === 'undefined') { throw new Error('Component attempted to register before AFRAME was available.'); } var parseJson = function parseJson(prop) { return typeof prop === 'string' ? JSON.parse(prop) : prop; // already parsed }; var parseFn = function parseFn(prop) { if (typeof prop === 'function') return prop; // already a function var geval = eval; // Avoid using eval directly https://github.com/rollup/rollup/wiki/Troubleshooting#avoiding-eval try { var evalled = geval('(' + prop + ')'); return evalled; } catch (e) {} // Can't eval, not a function return null; }; var parseAccessor = function parseAccessor(prop) { if (!isNaN(parseFloat(prop))) { return parseFloat(prop); } // parse numbers if (parseFn(prop)) { return parseFn(prop); } // parse functions return prop; // strings }; /** * 3D Globe component for A-Frame. */ if (!AFRAME.components.hasOwnProperty('globe')) { AFRAME.registerComponent('globe', { schema: { onHover: { parse: parseFn, "default": null }, onClick: { parse: parseFn, "default": null }, globeImageUrl: { type: 'string', "default": '' }, bumpImageUrl: { type: 'string', "default": '' }, showGlobe: { type: 'boolean', "default": true }, showGraticules: { type: 'boolean', "default": false }, showAtmosphere: { type: 'boolean', "default": true }, atmosphereColor: { type: 'string', "default": 'lightskyblue' }, atmosphereAltitude: { type: 'number', "default": 0.15 }, onGlobeReady: { parse: parseFn, "default": null }, pointsData: { parse: parseJson, "default": [] }, pointLat: { parse: parseAccessor, "default": 'lat' }, pointLng: { parse: parseAccessor, "default": 'lng' }, pointColor: { parse: parseAccessor, "default": function _default() { return '#ffffaa'; } }, pointAltitude: { parse: parseAccessor, "default": 0.1 }, pointRadius: { parse: parseAccessor, "default": 0.25 }, pointResolution: { type: 'number', "default": 12 }, pointsMerge: { type: 'boolean', "default": false }, pointsTransitionDuration: { type: 'number', "default": 1000 }, arcsData: { parse: parseJson, "default": [] }, arcStartLat: { parse: parseAccessor, "default": 'startLat' }, arcStartLng: { parse: parseAccessor, "default": 'startLng' }, arcEndLat: { parse: parseAccessor, "default": 'endLat' }, arcEndLng: { parse: parseAccessor, "default": 'endLng' }, arcColor: { parse: parseAccessor, "default": function _default() { return '#ffffaa'; } }, arcAltitude: { parse: parseAccessor, "default": null }, arcAltitudeAutoScale: { parse: parseAccessor, "default": 0.5 }, arcStroke: { parse: parseAccessor, "default": null }, arcCurveResolution: { type: 'number', "default": 64 }, arcCircularResolution: { type: 'number', "default": 6 }, arcDashLength: { parse: parseAccessor, "default": 1 }, arcDashGap: { parse: parseAccessor, "default": 0 }, arcDashInitialGap: { parse: parseAccessor, "default": 0 }, arcDashAnimateTime: { parse: parseAccessor, "default": 0 }, arcsTransitionDuration: { type: 'number', "default": 1000 }, polygonsData: { parse: parseJson, "default": [] }, polygonGeoJsonGeometry: { parse: parseAccessor, "default": 'geometry' }, polygonCapColor: { parse: parseAccessor, "default": function _default() { return '#ffffaa'; } }, polygonCapMaterial: { parse: parseAccessor, "default": null }, polygonSideColor: { parse: parseAccessor, "default": function _default() { return '#ffffaa'; } }, polygonSideMaterial: { parse: parseAccessor, "default": null }, polygonStrokeColor: { parse: parseAccessor, "default": null }, polygonAltitude: { parse: parseAccessor, "default": 0.01 }, polygonCapCurvatureResolution: { parse: parseAccessor, "default": 5 }, polygonsTransitionDuration: { type: 'number', "default": 1000 }, pathsData: { parse: parseJson, "default": [] }, pathPoints: { parse: parseAccessor, "default": function _default(pnts) { return pnts; } }, pathPointLat: { parse: parseAccessor, "default": function _default(arr) { return arr[0]; } }, pathPointLng: { parse: parseAccessor, "default": function _default(arr) { return arr[1]; } }, pathPointAlt: { parse: parseAccessor, "default": 1e-3 }, pathResolution: { type: 'number', "default": 2 }, pathColor: { parse: parseAccessor, "default": function _default() { return '#ffffaa'; } }, pathStroke: { parse: parseAccessor, "default": null }, pathDashLength: { parse: parseAccessor, "default": 1 }, pathDashGap: { parse: parseAccessor, "default": 0 }, pathDashInitialGap: { parse: parseAccessor, "default": 0 }, pathDashAnimateTime: { parse: parseAccessor, "default": 0 }, pathTransitionDuration: { type: 'number', "default": 1000 }, heatmapsData: { parse: parseJson, "default": [] }, heatmapPoints: { parse: parseAccessor, "default": function _default(pnts) { return pnts; } }, heatmapPointLat: { parse: parseAccessor, "default": function _default(arr) { return arr[0]; } }, heatmapPointLng: { parse: parseAccessor, "default": function _default(arr) { return arr[1]; } }, heatmapPointWeight: { parse: parseAccessor, "default": 1 }, heatmapBandwidth: { parse: parseAccessor, "default": 2.5 }, heatmapColorFn: { parse: parseAccessor, "default": undefined }, heatmapColorSaturation: { parse: parseAccessor, "default": 1.5 }, heatmapBaseAltitude: { parse: parseAccessor, "default": 0.01 }, heatmapTopAltitude: { parse: parseAccessor, "default": null }, heatmapTransitionDuration: { type: 'number', "default": 0 }, hexBinPointsData: { parse: parseJson, "default": [] }, hexBinPointLat: { parse: parseAccessor, "default": 'lat' }, hexBinPointLng: { parse: parseAccessor, "default": 'lng' }, hexBinPointWeight: { parse: parseAccessor, "default": 1 }, hexBinResolution: { type: 'number', "default": 4 }, hexMargin: { parse: parseAccessor, "default": 0.2 }, hexTopCurvatureResolution: { type: 'number', "default": 5 }, hexTopColor: { parse: parseAccessor, "default": function _default() { return '#ffffaa'; } }, hexSideColor: { parse: parseAccessor, "default": function _default() { return '#ffffaa'; } }, hexAltitude: { parse: parseAccessor, "default": function _default(d) { return d.sumWeight * 0.01; } }, hexBinMerge: { type: 'boolean', "default": false }, hexTransitionDuration: { type: 'number', "default": 1000 }, hexPolygonsData: { parse: parseJson, "default": [] }, hexPolygonGeoJsonGeometry: { parse: parseAccessor, "default": 'geometry' }, hexPolygonColor: { parse: parseAccessor, "default": function _default() { return '#ffffaa'; } }, hexPolygonAltitude: { parse: parseAccessor, "default": 0.001 }, hexPolygonResolution: { parse: parseAccessor, "default": 3 }, hexPolygonMargin: { parse: parseAccessor, "default": 0.2 }, hexPolygonUseDots: { parse: parseAccessor, "default": false }, hexPolygonCurvatureResolution: { parse: parseAccessor, "default": 5 }, hexPolygonDotResolution: { parse: parseAccessor, "default": 12 }, hexPolygonsTransitionDuration: { type: 'number', "default": 0 }, tilesData: { parse: parseJson, "default": [] }, tileLat: { parse: parseAccessor, "default": 'lat' }, tileLng: { parse: parseAccessor, "default": 'lng' }, tileAltitude: { parse: parseAccessor, "default": 0.01 }, tileWidth: { parse: parseAccessor, "default": 1 }, tileHeight: { parse: parseAccessor, "default": 1 }, tileUseGlobeProjection: { parse: parseAccessor, "default": true }, tileMaterial: { parse: parseAccessor, "default": undefined }, tileCurvatureResolution: { parse: parseAccessor, "default": 5 }, tilesTransitionDuration: { type: 'number', "default": 1000 }, particlesData: { parse: parseJson, "default": [] }, particlesList: { parse: parseAccessor, "default": function _default(d) { return d; } }, particleLat: { parse: parseAccessor, "default": 'lat' }, particleLng: { parse: parseAccessor, "default": 'lng' }, particleAltitude: { parse: parseAccessor, "default": 0.01 }, particlesSize: { parse: parseAccessor, "default": 0.05 }, particlesSizeAttenuation: { parse: parseAccessor, "default": true }, particlesColor: { parse: parseAccessor, "default": function _default() { return 'white'; } }, particlesTexture: { parse: parseAccessor, "default": undefined }, ringsData: { parse: parseJson, "default": [] }, ringLat: { parse: parseAccessor, "default": 'lat' }, ringLng: { parse: parseAccessor, "default": 'lng' }, ringAltitude: { parse: parseAccessor, "default": 1.5e-3 }, ringColor: { parse: parseAccessor, "default": function _default() { return '#ffffaa'; } }, ringResolution: { type: 'number', "default": 64 }, ringMaxRadius: { parse: parseAccessor, "default": 2 }, ringPropagationSpeed: { parse: parseAccessor, "default": 1 }, ringRepeatPeriod: { parse: parseAccessor, "default": 700 }, labelsData: { parse: parseJson, "default": [] }, labelLat: { parse: parseAccessor, "default": 'lat' }, labelLng: { parse: parseAccessor, "default": 'lng' }, labelAltitude: { parse: parseAccessor, "default": 0 }, labelRotation: { parse: parseAccessor, "default": 0 }, labelText: { parse: parseAccessor, "default": 'text' }, labelSize: { parse: parseAccessor, "default": 0.5 }, labelTypeFace: { parse: parseJson, "default": undefined }, labelColor: { parse: parseAccessor, "default": function _default() { return 'lightgrey'; } }, labelResolution: { type: 'number', "default": 3 }, labelIncludeDot: { parse: parseAccessor, "default": true }, labelDotRadius: { parse: parseAccessor, "default": 0.1 }, labelDotOrientation: { parse: parseAccessor, "default": function _default() { return 'bottom'; } }, labelsTransitionDuration: { type: 'number', "default": 1000 }, objectsData: { parse: parseJson, "default": [] }, objectLat: { parse: parseAccessor, "default": 'lat' }, objectLng: { parse: parseAccessor, "default": 'lng' }, objectAltitude: { parse: parseAccessor, "default": 0.01 }, objectRotation: { parse: parseAccessor, "default": null }, objectFacesSurface: { parse: parseAccessor, "default": true }, objectThreeObject: { parse: parseAccessor, "default": undefined }, customLayerData: { parse: parseJson, "default": [] }, customThreeObject: { parse: parseAccessor, "default": null }, customThreeObjectUpdate: { parse: parseAccessor, "default": null } }, // Bind component methods globeMaterial: function globeMaterial() { if (!this.globe) { // Got here before component init -> initialize globe this.globe = new ThreeGlobe(); } var globe = this.globe; var returnVal = globe.globeMaterial.apply(globe, arguments); return returnVal === globe ? this // return self, not the inner globe component : returnVal; }, getGlobeRadius: function getGlobeRadius() { if (!this.globe) { // Got here before component init -> initialize globe this.globe = new ThreeGlobe(); } var globe = this.globe; return globe.getGlobeRadius.apply(globe, arguments); }, getCoords: function getCoords() { if (!this.globe) { // Got here before component init -> initialize globe this.globe = new ThreeGlobe(); } var globe = this.globe; var returnVal = globe.getCoords.apply(globe, arguments); return returnVal === globe ? this // return self, not the inner globe component : returnVal; }, toGeoCoords: function toGeoCoords() { if (!this.globe) { // Got here before component init -> initialize globe this.globe = new ThreeGlobe(); } var globe = this.globe; var returnVal = globe.toGeoCoords.apply(globe, arguments); return returnVal === globe ? this // return self, not the inner globe component : returnVal; }, init: function init() { var _this = this; var state = this.state = {}; // Internal state // Get camera dom element var cameraEl = document.querySelector('a-entity[camera], a-camera'); // Keep reference to Three camera object state.cameraObj = cameraEl.object3D.children.filter(function (child) { return child.type === 'PerspectiveCamera'; })[0]; // On camera switch this.el.sceneEl.addEventListener('camera-set-active', function (evt) { // Switch camera reference state.cameraObj = evt.detail.cameraEl.components.camera.camera; }); // setup Globe object if (!this.globe) this.globe = new ThreeGlobe(); // initialize globe if it doesn't exist yet // interaction events // prefer raycaster events over mouseenter/mouseleave because they expose immediately available intersection data via detail.getIntersection() this.el.addEventListener('raycaster-intersected', function (ev) { return state.hoverEvent = ev; }); this.el.addEventListener('raycaster-intersected-cleared', function (ev) { return state.hoverEvent = ev; }); this.el.addEventListener('click', function () { return state.hoverObj && _this.data.onClick && _this.data.onClick(formatObjForInteraction(state.hoverObj), state.hoverEvent); }); }, remove: function remove() { // Clean-up elems this.el.removeObject3D('globeGroup'); }, update: function update(oldData) { var _this2 = this; var comp = this; var elData = this.data; var diff = AFRAME.utils.diff(elData, oldData); var globeProps = ['globeImageUrl', 'bumpImageUrl', 'showGlobe', 'showGraticules', 'showAtmosphere', 'atmosphereColor', 'atmosphereAltitude', 'onGlobeReady', 'pointsData', 'pointLat', 'pointLng', 'pointColor', 'pointAltitude', 'pointRadius', 'pointResolution', 'pointsMerge', 'pointsTransitionDuration', 'arcsData', 'arcStartLat', 'arcStartLng', 'arcEndLat', 'arcEndLng', 'arcColor', 'arcAltitude', 'arcAltitudeAutoScale', 'arcStroke', 'arcCurveResolution', 'arcCircularResolution', 'arcDashLength', 'arcDashGap', 'arcDashInitialGap', 'arcDashAnimateTime', 'arcsTransitionDuration', 'polygonsData', 'polygonGeoJsonGeometry', 'polygonCapColor', 'polygonCapMaterial', 'polygonSideColor', 'polygonSideMaterial', 'polygonStrokeColor', 'polygonAltitude', 'polygonCapCurvatureResolution', 'polygonsTransitionDuration', 'pathsData', 'pathPoints', 'pathPointLat', 'pathPointLng', 'pathPointAlt', 'pathResolution', 'pathColor', 'pathStroke', 'pathDashLength', 'pathDashGap', 'pathDashInitialGap', 'pathDashAnimateTime', 'pathTransitionDuration', 'heatmapsData', 'heatmapPoints', 'heatmapPointLat', 'heatmapPointLng', 'heatmapPointWeight', 'heatmapBandwidth', 'heatmapColorFn', 'heatmapColorSaturation', 'heatmapBaseAltitude', 'heatmapTopAltitude', 'heatmapsTransitionDuration', 'hexBinPointsData', 'hexBinPointLat', 'hexBinPointLng', 'hexBinPointWeight', 'hexBinResolution', 'hexMargin', 'hexTopCurvatureResolution', 'hexTopColor', 'hexSideColor', 'hexAltitude', 'hexBinMerge', 'hexTransitionDuration', 'hexPolygonsData', 'hexPolygonGeoJsonGeometry', 'hexPolygonColor', 'hexPolygonAltitude', 'hexPolygonResolution', 'hexPolygonMargin', 'hexPolygonUseDots', 'hexPolygonCurvatureResolution', 'hexPolygonDotResolution', 'hexPolygonsTransitionDuration', 'tilesData', 'tileLat', 'tileLng', 'tileAltitude', 'tileWidth', 'tileHeight', 'tileUseGlobeProjection', 'tileMaterial', 'tileCurvatureResolution', 'tilesTransitionDuration', 'particlesData', 'particlesList', 'particleLat', 'particleLng', 'particleAltitude', 'particlesSize', 'particlesSizeAttenuation', 'particlesColor', 'particlesTexture', 'ringsData', 'ringLat', 'ringLng', 'ringAltitude', 'ringColor', 'ringResolution', 'ringMaxRadius', 'ringPropagationSpeed', 'ringRepeatPeriod', 'labelsData', 'labelLat', 'labelLng', 'labelAltitude', 'labelRotation', 'labelText', 'labelSize', 'labelTypeFace', 'labelColor', 'labelResolution', 'labelIncludeDot', 'labelDotRadius', 'labelDotOrientation', 'labelsTransitionDuration', 'objectsData', 'objectLat', 'objectLng', 'objectAltitude', 'objectRotation', 'objectFacesSurface', 'objectThreeObject', 'customLayerData', 'customThreeObject', 'customThreeObjectUpdate']; globeProps.filter(function (p) { return p in diff && elData[p] !== undefined; }).forEach(function (p) { comp.globe[p](elData[p] !== '' ? elData[p] : null); }); // Convert blank values into nulls setTimeout(function () { return _this2.el.setObject3D('globeGroup', _this2.globe); }); // Re-bind globe to elem }, tick: function tick(t, td) { var state = this.state; var props = this.data; var hoverDetail = state.hoverEvent && state.hoverEvent.detail; // Update hover (intersected) object var intersection = hoverDetail ? hoverDetail.getIntersection ? hoverDetail.getIntersection(this.el) // available in raycaster-intersected events : hoverDetail.intersection || undefined // available in mouseenter/mouseleave events (with delayed update) : undefined; // Note: // Unfortunately we only have access to the intersected object closer to the camera (1st element in the raycaster intersectObjects result), // there is no ".getIntersections()" method available in the event details. Therefore, we can't prioritize hover certain globe objects over others. var topObject = null; if (props.onHover || props.onClick) { // recurse up until globe object is found topObject = intersection ? intersection.object : undefined; while (topObject && !topObject.hasOwnProperty('__globeObjType')) topObject = topObject.parent; // ignore certain layers topObject && ['globe', 'atmosphere'].includes(topObject.__globeObjType) && (topObject = null); } if (topObject !== state.hoverObj) { props.onHover && props.onHover(formatObjForInteraction(topObject), formatObjForInteraction(state.hoverObj)); state.hoverObj = topObject; } } }); } // function formatObjForInteraction(obj) { return !obj ? obj : { type: obj.__globeObjType, data: obj.__globeObjType === 'polygon' ? obj.__data.data : obj.__data }; }