@esri/arcgis-to-geojson-utils
Version:
Tools to convert ArcGIS JSON geometries to GeoJSON geometries and vica-versa.
404 lines (359 loc) • 12.1 kB
JavaScript
/*
* Copyright 2017 Esri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// checks if 2 x,y points are equal
function pointsEqual (a, b) {
for (var i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
// checks if the first and last points of a ring are equal and closes the ring
function closeRing (coordinates) {
if (!pointsEqual(coordinates[0], coordinates[coordinates.length - 1])) {
coordinates.push(coordinates[0]);
}
return coordinates;
}
// determine if polygon ring coordinates are clockwise. clockwise signifies outer ring, counter-clockwise an inner ring
// or hole. this logic was found at http://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-
// points-are-in-clockwise-order
function ringIsClockwise (ringToTest) {
var total = 0;
var i = 0;
var rLength = ringToTest.length;
var pt1 = ringToTest[i];
var pt2;
for (i; i < rLength - 1; i++) {
pt2 = ringToTest[i + 1];
total += (pt2[0] - pt1[0]) * (pt2[1] + pt1[1]);
pt1 = pt2;
}
return (total >= 0);
}
// ported from terraformer.js https://github.com/Esri/Terraformer/blob/master/terraformer.js#L504-L519
function vertexIntersectsVertex (a1, a2, b1, b2) {
var uaT = ((b2[0] - b1[0]) * (a1[1] - b1[1])) - ((b2[1] - b1[1]) * (a1[0] - b1[0]));
var ubT = ((a2[0] - a1[0]) * (a1[1] - b1[1])) - ((a2[1] - a1[1]) * (a1[0] - b1[0]));
var uB = ((b2[1] - b1[1]) * (a2[0] - a1[0])) - ((b2[0] - b1[0]) * (a2[1] - a1[1]));
if (uB !== 0) {
var ua = uaT / uB;
var ub = ubT / uB;
if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
return true;
}
}
return false;
}
// ported from terraformer.js https://github.com/Esri/Terraformer/blob/master/terraformer.js#L521-L531
function arrayIntersectsArray (a, b) {
for (var i = 0; i < a.length - 1; i++) {
for (var j = 0; j < b.length - 1; j++) {
if (vertexIntersectsVertex(a[i], a[i + 1], b[j], b[j + 1])) {
return true;
}
}
}
return false;
}
// ported from terraformer.js https://github.com/Esri/Terraformer/blob/master/terraformer.js#L470-L480
function coordinatesContainPoint (coordinates, point) {
var contains = false;
for (var i = -1, l = coordinates.length, j = l - 1; ++i < l; j = i) {
if (((coordinates[i][1] <= point[1] && point[1] < coordinates[j][1]) ||
(coordinates[j][1] <= point[1] && point[1] < coordinates[i][1])) &&
(point[0] < (((coordinates[j][0] - coordinates[i][0]) * (point[1] - coordinates[i][1])) / (coordinates[j][1] - coordinates[i][1])) + coordinates[i][0])) {
contains = !contains;
}
}
return contains;
}
// ported from terraformer-arcgis-parser.js https://github.com/Esri/terraformer-arcgis-parser/blob/master/terraformer-arcgis-parser.js#L106-L113
function coordinatesContainCoordinates (outer, inner) {
var intersects = arrayIntersectsArray(outer, inner);
var contains = coordinatesContainPoint(outer, inner[0]);
if (!intersects && contains) {
return true;
}
return false;
}
// do any polygons in this array contain any other polygons in this array?
// used for checking for holes in arcgis rings
// ported from terraformer-arcgis-parser.js https://github.com/Esri/terraformer-arcgis-parser/blob/master/terraformer-arcgis-parser.js#L117-L172
function convertRingsToGeoJSON (rings) {
var outerRings = [];
var holes = [];
var x; // iterator
var outerRing; // current outer ring being evaluated
var hole; // current hole being evaluated
// for each ring
for (var r = 0; r < rings.length; r++) {
var ring = closeRing(rings[r].slice(0));
if (ring.length < 4) {
continue;
}
// is this ring an outer ring? is it clockwise?
if (ringIsClockwise(ring)) {
var polygon = [ ring.slice().reverse() ]; // wind outer rings counterclockwise for RFC 7946 compliance
outerRings.push(polygon); // push to outer rings
} else {
holes.push(ring.slice().reverse()); // wind inner rings clockwise for RFC 7946 compliance
}
}
var uncontainedHoles = [];
// while there are holes left...
while (holes.length) {
// pop a hole off out stack
hole = holes.pop();
// loop over all outer rings and see if they contain our hole.
var contained = false;
for (x = outerRings.length - 1; x >= 0; x--) {
outerRing = outerRings[x][0];
if (coordinatesContainCoordinates(outerRing, hole)) {
// the hole is contained push it into our polygon
outerRings[x].push(hole);
contained = true;
break;
}
}
// ring is not contained in any outer ring
// sometimes this happens https://github.com/Esri/esri-leaflet/issues/320
if (!contained) {
uncontainedHoles.push(hole);
}
}
// if we couldn't match any holes using contains we can try intersects...
while (uncontainedHoles.length) {
// pop a hole off out stack
hole = uncontainedHoles.pop();
// loop over all outer rings and see if any intersect our hole.
var intersects = false;
for (x = outerRings.length - 1; x >= 0; x--) {
outerRing = outerRings[x][0];
if (arrayIntersectsArray(outerRing, hole)) {
// the hole is contained push it into our polygon
outerRings[x].push(hole);
intersects = true;
break;
}
}
if (!intersects) {
outerRings.push([hole.reverse()]);
}
}
if (outerRings.length === 1) {
return {
type: 'Polygon',
coordinates: outerRings[0]
};
} else {
return {
type: 'MultiPolygon',
coordinates: outerRings
};
}
}
// This function ensures that rings are oriented in the right directions
// outer rings are clockwise, holes are counterclockwise
// used for converting GeoJSON Polygons to ArcGIS Polygons
function orientRings (poly) {
var output = [];
var polygon = poly.slice(0);
var outerRing = closeRing(polygon.shift().slice(0));
if (outerRing.length >= 4) {
if (!ringIsClockwise(outerRing)) {
outerRing.reverse();
}
output.push(outerRing);
for (var i = 0; i < polygon.length; i++) {
var hole = closeRing(polygon[i].slice(0));
if (hole.length >= 4) {
if (ringIsClockwise(hole)) {
hole.reverse();
}
output.push(hole);
}
}
}
return output;
}
// This function flattens holes in multipolygons to one array of polygons
// used for converting GeoJSON Polygons to ArcGIS Polygons
function flattenMultiPolygonRings (rings) {
var output = [];
for (var i = 0; i < rings.length; i++) {
var polygon = orientRings(rings[i]);
for (var x = polygon.length - 1; x >= 0; x--) {
var ring = polygon[x].slice(0);
output.push(ring);
}
}
return output;
}
// shallow object clone for feature properties and attributes
// from http://jsperf.com/cloning-an-object/2
function shallowClone (obj) {
var target = {};
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
target[i] = obj[i];
}
}
return target;
}
function getId (attributes, idAttribute) {
var keys = idAttribute ? [idAttribute, 'OBJECTID', 'FID'] : ['OBJECTID', 'FID'];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (
key in attributes &&
(typeof attributes[key] === 'string' ||
typeof attributes[key] === 'number')
) {
return attributes[key];
}
}
throw Error('No valid id attribute found');
}
export function arcgisToGeoJSON (arcgis, idAttribute) {
var geojson = {};
if (arcgis.features) {
geojson.type = 'FeatureCollection';
geojson.features = [];
for (var i = 0; i < arcgis.features.length; i++) {
geojson.features.push(arcgisToGeoJSON(arcgis.features[i], idAttribute));
}
}
if (typeof arcgis.x === 'number' && typeof arcgis.y === 'number') {
geojson.type = 'Point';
geojson.coordinates = [arcgis.x, arcgis.y];
if (typeof arcgis.z === 'number') {
geojson.coordinates.push(arcgis.z);
}
}
if (arcgis.points) {
geojson.type = 'MultiPoint';
geojson.coordinates = arcgis.points.slice(0);
}
if (arcgis.paths) {
if (arcgis.paths.length === 1) {
geojson.type = 'LineString';
geojson.coordinates = arcgis.paths[0].slice(0);
} else {
geojson.type = 'MultiLineString';
geojson.coordinates = arcgis.paths.slice(0);
}
}
if (arcgis.rings) {
geojson = convertRingsToGeoJSON(arcgis.rings.slice(0));
}
if (
typeof arcgis.xmin === 'number' &&
typeof arcgis.ymin === 'number' &&
typeof arcgis.xmax === 'number' &&
typeof arcgis.ymax === 'number'
) {
geojson.type = 'Polygon';
geojson.coordinates = [[
[arcgis.xmax, arcgis.ymax],
[arcgis.xmin, arcgis.ymax],
[arcgis.xmin, arcgis.ymin],
[arcgis.xmax, arcgis.ymin],
[arcgis.xmax, arcgis.ymax]
]];
}
if (arcgis.geometry || arcgis.attributes) {
geojson.type = 'Feature';
geojson.geometry = (arcgis.geometry) ? arcgisToGeoJSON(arcgis.geometry) : null;
geojson.properties = (arcgis.attributes) ? shallowClone(arcgis.attributes) : null;
if (arcgis.attributes) {
try {
geojson.id = getId(arcgis.attributes, idAttribute);
} catch (err) {
// don't set an id
}
}
}
// if no valid geometry was encountered
if (JSON.stringify(geojson.geometry) === JSON.stringify({})) {
geojson.geometry = null;
}
if (
arcgis.spatialReference &&
arcgis.spatialReference.wkid &&
arcgis.spatialReference.wkid !== 4326
) {
console.warn('Object converted in non-standard crs - ' + JSON.stringify(arcgis.spatialReference));
}
return geojson;
}
export function geojsonToArcGIS (geojson, idAttribute) {
idAttribute = idAttribute || 'OBJECTID';
var spatialReference = { wkid: 4326 };
var result = {};
var i;
switch (geojson.type) {
case 'Point':
result.x = geojson.coordinates[0];
result.y = geojson.coordinates[1];
result.spatialReference = spatialReference;
break;
case 'MultiPoint':
result.points = geojson.coordinates.slice(0);
result.spatialReference = spatialReference;
break;
case 'LineString':
result.paths = [geojson.coordinates.slice(0)];
result.spatialReference = spatialReference;
break;
case 'MultiLineString':
result.paths = geojson.coordinates.slice(0);
result.spatialReference = spatialReference;
break;
case 'Polygon':
result.rings = orientRings(geojson.coordinates.slice(0));
result.spatialReference = spatialReference;
break;
case 'MultiPolygon':
result.rings = flattenMultiPolygonRings(geojson.coordinates.slice(0));
result.spatialReference = spatialReference;
break;
case 'Feature':
if (geojson.geometry) {
result.geometry = geojsonToArcGIS(geojson.geometry, idAttribute);
}
result.attributes = (geojson.properties) ? shallowClone(geojson.properties) : {};
if (geojson.id) {
result.attributes[idAttribute] = geojson.id;
}
break;
case 'FeatureCollection':
result = [];
for (i = 0; i < geojson.features.length; i++) {
result.push(geojsonToArcGIS(geojson.features[i], idAttribute));
}
break;
case 'GeometryCollection':
result = [];
for (i = 0; i < geojson.geometries.length; i++) {
result.push(geojsonToArcGIS(geojson.geometries[i], idAttribute));
}
break;
}
return result;
}
export default { arcgisToGeoJSON: arcgisToGeoJSON, geojsonToArcGIS: geojsonToArcGIS };