UNPKG

terriajs

Version:

Geospatial data visualization platform.

172 lines (154 loc) 6.71 kB
'use strict'; /*global require*/ var computeRingWindingOrder = require('../Map/computeRingWindingOrder'); var defined = require('terriajs-cesium/Source/Core/defined'); var pointInPolygon = require('point-in-polygon'); var Point = require('@mapbox/point-geometry'); var WindingOrder = require('terriajs-cesium/Source/Core/WindingOrder'); /** * Converts feature data, such as from a WMS GetFeatureInfo or an Esri Identify, to * GeoJSON. The set of feature data formats supported by this function can be extended * by adding to {@link featureDataToGeoJson#supportedFormats}. * * @param {Object} featureData The feature data to convert to GeoJSON. * @return {Object} The GeoJSON representation of this feature data, or undefined if it cannot be converted to GeoJSON. */ function featureDataToGeoJson(featureData) { if (!defined(featureData)) { return undefined; } for (var i = 0; i < featureDataToGeoJson.supportedFormats.length; ++i) { var converted = featureDataToGeoJson.supportedFormats[i].converter(featureData); if (defined(converted)) { return converted; } } return undefined; } featureDataToGeoJson.supportedFormats = [ { name: 'GeoJSON', converter: convertGeoJson }, { name: 'Esri', converter: convertEsri } ]; function convertGeoJson(featureData) { if ((featureData.type === 'Feature' && defined(featureData.geometry)) || (featureData.type === 'FeatureCollection' && defined(featureData.features))) { return featureData; } } function convertEsri(featureData) { return getEsriGeometry(featureData, featureData.geometryType, featureData.geometry && featureData.geometry.spatialReference); } // spatialReference is optional. function getEsriGeometry(featureData, geometryType, spatialReference) { if (defined(featureData.features)) { // This is a FeatureCollection. return { type: 'FeatureCollection', crs: esriSpatialReferenceToCrs(featureData.spatialReference), features: featureData.features.map(function(subFeatureData) { return getEsriGeometry(subFeatureData, geometryType); }) }; } var geoJsonFeature = { type: 'Feature', geometry: undefined, properties: featureData.attributes, }; if (defined(spatialReference)) { geoJsonFeature.crs = esriSpatialReferenceToCrs(spatialReference); } if (geometryType === 'esriGeometryPolygon') { // There are a bunch of differences between Esri polygons and GeoJSON polygons. // For GeoJSON, see https://tools.ietf.org/html/rfc7946#section-3.1.6. // For Esri, see http://resources.arcgis.com/en/help/arcgis-rest-api/#/Geometry_objects/02r3000000n1000000/ // In particular: // 1. Esri polygons can actually be multiple polygons by using multiple outer rings. GeoJSON polygons // can only have one outer ring and we need to use a MultiPolygon to represent multiple outer rings. // 2. In Esri which rings are outer rings and which are holes is determined by the winding order of the // rings. In GeoJSON, the first ring is the outer ring and subsequent rings are holes. // 3. In Esri polygons, clockwise rings are exterior, counter-clockwise are interior. In GeoJSON, the first // (exterior) ring is expected to be counter-clockwise, though lots of implementations probably don't // enforce this. The spec says, "For backwards compatibility, parsers SHOULD NOT reject // Polygons that do not follow the right-hand rule." // Group rings into outer rings and holes/ const outerRings = []; const holes = []; featureData.geometry.rings.forEach(function(ring) { if (computeRingWindingOrder(ring.map(p => new Point(...p))) === WindingOrder.CLOCKWISE) { outerRings.push(ring); } else { holes.push(ring); } // Reverse the coordinate order along the way due to #3 above. ring.reverse(); }); if (outerRings.length === 0 && holes.length > 0) { // Well, this is pretty weird. We have holes but not outer ring? // Most likely scenario is that someone messed up the winding order. // So let's treat all the holes as outer rings instead. holes.forEach(hole => { hole.reverse(); }); outerRings.push(...holes); holes.length = 0; } // If there's only one outer ring, we can use a `Polygon` and things are simple. if (outerRings.length === 1) { geoJsonFeature.geometry = { type: 'Polygon', coordinates: [outerRings[0], ...holes] }; } else { // Multiple (or zero!) outer rings, so we need to use a multipolygon, and we need // to figure out which outer ring contains each hole. geoJsonFeature.geometry = { type: 'MultiPolygon', coordinates: outerRings.map(ring => [ring, ...findHolesInRing(ring, holes)]) }; } } else if (geometryType === 'esriGeometryPoint') { geoJsonFeature.geometry = { type: 'Point', coordinates: [featureData.geometry.x, featureData.geometry.y] }; } else if (geometryType === 'esriGeometryPolyline') { geoJsonFeature.geometry = { type: 'MultiLineString', coordinates: featureData.geometry.paths }; } else { return undefined; } return geoJsonFeature; } function findHolesInRing(ring, holes) { // Return all holes where every vertex in the hole ring is inside the outer ring. return holes.filter(hole => hole.every(coordinates => pointInPolygon(coordinates, ring))); } function esriSpatialReferenceToCrs(spatialReference) { if (!defined(spatialReference)) { return undefined; } if (spatialReference.wkid === 102100) { return { type: 'name', properties: { name: 'urn:ogc:def:crs:EPSG::3857' } }; } else if (defined(spatialReference.wkid)) { return { type: 'name', properties: { name: 'urn:ogc:def:crs:EPSG::' + spatialReference.wkid } }; } return undefined; } module.exports = featureDataToGeoJson;