// TODO: Consider adopting GeoJSON CSS // http://wiki.openstreetmap.org/wiki/Geojson_CSS // TODO: Allow interaction to be defined per-layer to save on resources // // For example, only allow polygons to be interactive via a polygonInteractive // option import LayerGroup from './LayerGroup'; import extend from 'lodash.assign'; import reqwest from 'reqwest'; import GeoJSON from '../util/GeoJSON'; import Buffer from '../util/Buffer'; import PickingMaterial from '../engine/PickingMaterial'; import PolygonLayer from './geometry/PolygonLayer'; import PolylineLayer from './geometry/PolylineLayer'; import PointLayer from './geometry/PointLayer'; class GeoJSONLayer extends LayerGroup { constructor(geojson, options) { var defaults = { output: false, interactive: false, topojson: false, filter: null, onEachFeature: null, polygonMaterial: null, onPolygonMesh: null, onPolygonBufferAttributes: null, polylineMaterial: null, onPolylineMesh: null, onPolylineBufferAttributes: null, pointGeometry: null, pointMaterial: null, onPointMesh: null, style: GeoJSON.defaultStyle, keepFeatures: true }; var _options = extend({}, defaults, options); if (typeof options.style === 'function') { _options.style = options.style; } else { _options.style = extend({}, defaults.style, options.style); } super(_options); this._geojson = geojson; } _onAdd(world) { // Only add to picking mesh if this layer is controlling output // // Otherwise, assume another component will eventually add a mesh to // the picking scene if (this.isOutput()) { this._pickingMesh = new THREE.Object3D(); this.addToPicking(this._pickingMesh); } // Request data from URL if needed if (typeof this._geojson === 'string') { return this._requestData(this._geojson); } else { // Process and add GeoJSON to layer return this._processData(this._geojson); } } _requestData(url) { return new Promise((resolve, reject) => { this._request = reqwest({ url: url, type: 'json', crossOrigin: true }).then(res => { // Clear request reference this._request = null; this._processData(res).then(() => { resolve(this); }); }).catch(err => { console.error(err); // Clear request reference this._request = null; reject(err); }); }); } // TODO: Wrap into a helper method so this isn't duplicated in the tiled // GeoJSON output layer // // Need to be careful as to not make it impossible to fork this off into a // worker script at a later stage _processData(data) { return new Promise((resolve) => { // Collects features into a single FeatureCollection // // Also converts TopoJSON to GeoJSON if instructed this._geojson = GeoJSON.collectFeatures(data, this._options.topojson); // TODO: Check that GeoJSON is valid / usable var features = this._geojson.features; // Run filter, if provided if (this._options.filter) { features = this._geojson.features.filter(this._options.filter); } var defaults = {}; // Assume that a style won't be set per feature var style = this._options.style; var layerPromises = []; var options; features.forEach(feature => { // Get per-feature style object, if provided if (typeof this._options.style === 'function') { style = extend({}, GeoJSON.defaultStyle, this._options.style(feature)); } options = extend({}, defaults, { // If merging feature layers, stop them outputting themselves // If not, let feature layers output themselves to the world output: !this.isOutput(), interactive: this._options.interactive, style: style }); var layer = this._featureToLayer(feature, options); if (!layer) { return; } // Sometimes you don't want to store a reference to the feature // // For example, to save memory when being used by tile layers if (this._options.keepFeatures) { layer.feature = feature; } // If defined, call a function for each feature // // This is commonly used for adding event listeners from the user script if (this._options.onEachFeature) { this._options.onEachFeature(feature, layer); } // TODO: Make this a promise array and only continue on completion layerPromises.push(this.addLayer(layer)); }); Promise.all(layerPromises).then((results) => { // If merging layers do that now, otherwise skip as the geometry layers // should have already outputted themselves if (!this.isOutput()) { resolve(); return; } // From here on we can assume that we want to merge the layers var polygonAttributes = []; var polygonOutlineAttributes = []; var polygonAttributeLengths = { positions: 3, normals: 3, colors: 3 }; var polygonFlat = true; var polylineAttributes = []; var polylineAttributeLengths = { positions: 3, colors: 3 }; var polylineFlat = true; var pointAttributes = []; var pointAttributeLengths = { positions: 3, normals: 3, colors: 3 }; var pointFlat = true; this._layers.forEach(layer => { if (layer instanceof PolygonLayer) { polygonAttributes.push(layer.getBufferAttributes()); var outlineBufferAttributes = layer.getOutlineBufferAttributes(); if (outlineBufferAttributes) { polygonOutlineAttributes.push(outlineBufferAttributes); } if (polygonFlat && !layer.isFlat()) { polygonFlat = false; } if (this._options.interactive) { polygonAttributeLengths.pickingIds = 1; } } else if (layer instanceof PolylineLayer) { polylineAttributes.push(layer.getBufferAttributes()); if (polylineFlat && !layer.isFlat()) { polylineFlat = false; } if (this._options.interactive) { polylineAttributeLengths.pickingIds = 1; } } else if (layer instanceof PointLayer) { pointAttributes.push(layer.getBufferAttributes()); if (pointFlat && !layer.isFlat()) { pointFlat = false; } if (this._options.interactive) { pointAttributeLengths.pickingIds = 1; } } }); var style; if (polygonAttributes.length > 0) { var mergedPolygonAttributes = Buffer.mergeAttributes(polygonAttributes); var mergedPolygonOutlineAttributes; if (polygonOutlineAttributes.length > 0) { mergedPolygonOutlineAttributes = Buffer.mergeAttributes(polygonOutlineAttributes); } style = (typeof this._options.style === 'function') ? this._options.style(this._geojson.features[0]) : this._options.style; style = extend({}, GeoJSON.defaultStyle, style); this._setPolygonMesh(mergedPolygonAttributes, polygonAttributeLengths, style, polygonFlat).then((result) => { this._polygonMesh = result.mesh; this.add(this._polygonMesh); if (mergedPolygonOutlineAttributes) { style = (typeof this._options.style === 'function') ? this._options.style(this._geojson.features[0]) : this._options.style; style = extend({}, GeoJSON.defaultStyle, style); if (style.outlineRenderOrder !== undefined) { style.lineRenderOrder = style.outlineRenderOrder; } else { style.lineRenderOrder = (style.renderOrder) ? style.renderOrder + 1 : 4; } if (style.outlineWidth) { style.lineWidth = style.outlineWidth; } this._setPolylineMesh(mergedPolygonOutlineAttributes, polylineAttributeLengths, style, true).then((result) => { this.add(result.mesh); }); } if (result.pickingMesh) { this._pickingMesh.add(result.pickingMesh); } }); } if (polylineAttributes.length > 0) { var mergedPolylineAttributes = Buffer.mergeAttributes(polylineAttributes); style = (typeof this._options.style === 'function') ? this._options.style(this._geojson.features[0]) : this._options.style; style = extend({}, GeoJSON.defaultStyle, style); this._setPolylineMesh(mergedPolylineAttributes, polylineAttributeLengths, style, polylineFlat).then((result) => { this._polylineMesh = result.mesh; this.add(this._polylineMesh); if (result.pickingMesh) { this._pickingMesh.add(result.pickingMesh); } }); } if (pointAttributes.length > 0) { var mergedPointAttributes = Buffer.mergeAttributes(pointAttributes); style = (typeof this._options.style === 'function') ? this._options.style(this._geojson.features[0]) : this._options.style; style = extend({}, GeoJSON.defaultStyle, style); this._setPointMesh(mergedPointAttributes, pointAttributeLengths, style, pointFlat).then((result) => { this._pointMesh = result.mesh; this.add(this._pointMesh); if (result.pickingMesh) { this._pickingMesh.add(result.pickingMesh); } }); } // Clean up layers // // TODO: Are there ever situations where the unmerged buffer attributes // and coordinates would still be required? this._layers.forEach(layer => { layer.clearBufferAttributes(); layer.clearCoordinates(); }); resolve(); }); }); } // Create and store mesh from buffer attributes // // TODO: Probably remove this and call static method directly as it's just a proxy _setPolygonMesh(attributes, attributeLengths, style, flat) { return PolygonLayer.SetMesh(attributes, attributeLengths, flat, style, this._options, this._world._environment._skybox); } _setPolylineMesh(attributes, attributeLengths, style, flat) { return PolylineLayer.SetMesh(attributes, attributeLengths, flat, style, this._options); } _setPointMesh(attributes, attributeLengths, style, flat) { return PointLayer.SetMesh(attributes, attributeLengths, flat, style, this._options); } // TODO: Support all GeoJSON geometry types _featureToLayer(feature, options) { var geometry = feature.geometry; var coordinates = (geometry.coordinates) ? geometry.coordinates : null; if (!coordinates || !geometry) { return; } if (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') { // Get material instance to use for polygon, if provided if (typeof this._options.polygonMaterial === 'function') { options.polygonMaterial = this._options.polygonMaterial(feature); } if (typeof this._options.onPolygonMesh === 'function') { options.onPolygonMesh = this._options.onPolygonMesh; } // Pass onBufferAttributes callback, if defined if (typeof this._options.onPolygonBufferAttributes === 'function') { options.onBufferAttributes = this._options.onPolygonBufferAttributes; } return new PolygonLayer(coordinates, options); } if (geometry.type === 'LineString' || geometry.type === 'MultiLineString') { // Get material instance to use for line, if provided if (typeof this._options.lineMaterial === 'function') { options.lineMaterial = this._options.lineMaterial(feature); } if (typeof this._options.onPolylineMesh === 'function') { options.onPolylineMesh = this._options.onPolylineMesh; } // Pass onBufferAttributes callback, if defined if (typeof this._options.onPolylineBufferAttributes === 'function') { options.onBufferAttributes = this._options.onPolylineBufferAttributes; } return new PolylineLayer(coordinates, options); } if (geometry.type === 'Point' || geometry.type === 'MultiPoint') { // Get geometry object to use for point, if provided if (typeof this._options.pointGeometry === 'function') { options.pointGeometry = this._options.pointGeometry(feature); } // Get material instance to use for point, if provided if (typeof this._options.pointMaterial === 'function') { options.pointMaterial = this._options.pointMaterial(feature); } if (typeof this._options.onPointMesh === 'function') { options.onPointMesh = this._options.onPointMesh; } return new PointLayer(coordinates, options); } } _abortRequest() { if (!this._request) { return; } this._request.abort(); } // Destroy the layers and remove them from the scene and memory destroy() { // Cancel any pending requests this._abortRequest(); // Clear request reference this._request = null; this._geojson = null; if (this._pickingMesh) { // TODO: Properly dispose of picking mesh this._pickingMesh = null; } if (this._polygonMesh) { this._polygonMesh = null; } if (this._polylineMesh) { this._polylineMesh = null; } if (this._pointMesh) { this._pointMesh = null; } // Run common destruction logic from parent super.destroy(); } } export default GeoJSONLayer; var noNew = function(geojson, options) { return new GeoJSONLayer(geojson, options); }; export {noNew as geoJSONLayer};