UNPKG

@spatial/turf

Version:

a JavaScript library for performing geospatial operations with GeoJSON

1,240 lines (1,153 loc) 4.31 MB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = global || self, factory(global.turf = {})); }(this, function (exports) { 'use strict'; /** * Earth Radius used with the Harvesine formula and approximates using a spherical (non-ellipsoid) Earth. */ var earthRadius = 6371008.8; /** * Unit of measurement factors using a spherical (non-ellipsoid) earth radius. */ var factors = { meters: earthRadius, metres: earthRadius, millimeters: earthRadius * 1000, millimetres: earthRadius * 1000, centimeters: earthRadius * 100, centimetres: earthRadius * 100, kilometers: earthRadius / 1000, kilometres: earthRadius / 1000, miles: earthRadius / 1609.344, nauticalmiles: earthRadius / 1852, inches: earthRadius * 39.370, yards: earthRadius / 1.0936, feet: earthRadius * 3.28084, radians: 1, degrees: earthRadius / 111325, }; /** * Units of measurement factors based on 1 meter. */ var unitsFactors = { meters: 1, metres: 1, millimeters: 1000, millimetres: 1000, centimeters: 100, centimetres: 100, kilometers: 1 / 1000, kilometres: 1 / 1000, miles: 1 / 1609.344, nauticalmiles: 1 / 1852, inches: 39.370, yards: 1 / 1.0936, feet: 3.28084, radians: 1 / earthRadius, degrees: 1 / 111325, }; /** * Area of measurement factors based on 1 square meter. */ var areaFactors = { meters: 1, metres: 1, millimeters: 1000000, millimetres: 1000000, centimeters: 10000, centimetres: 10000, kilometers: 0.000001, kilometres: 0.000001, acres: 0.000247105, miles: 3.86e-7, yards: 1.195990046, feet: 10.763910417, inches: 1550.003100006 }; /** * Wraps a GeoJSON {@link Geometry} in a GeoJSON {@link Feature}. * * @name feature * @param {Geometry} geometry input geometry * @param {Object} [properties={}] an Object of key-value pairs to add as properties * @param {Object} [options={}] Optional Parameters * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature * @param {string|number} [options.id] Identifier associated with the Feature * @returns {Feature} a GeoJSON Feature * @example * var geometry = { * "type": "Point", * "coordinates": [110, 50] * }; * * var feature = turf.feature(geometry); * * //=feature */ function feature(geometry, properties, options) { // Optional Parameters options = options || {}; if (!isObject(options)) throw new Error('options is invalid'); const bbox = options.bbox; const id = options.id; // Validation if (geometry === undefined) throw new Error('geometry is required'); if (properties && properties.constructor !== Object) throw new Error('properties must be an Object'); if (bbox) validateBBox(bbox); if (id) validateId(id); // Main const feat = {type: 'Feature'}; if (id) feat.id = id; if (bbox) feat.bbox = bbox; feat.properties = properties || {}; feat.geometry = geometry; return feat; } /** * Creates a GeoJSON {@link Geometry} from a Geometry string type & coordinates. * For GeometryCollection type use `helpers.geometryCollection` * * @name geometry * @param {string} type Geometry Type * @param {Array<number>} coordinates Coordinates * @param {Object} [options={}] Optional Parameters * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Geometry * @returns {Geometry} a GeoJSON Geometry * @example * var type = 'Point'; * var coordinates = [110, 50]; * * var geometry = turf.geometry(type, coordinates); * * //=geometry */ function geometry(type, coordinates, options) { // Optional Parameters options = options || {}; if (!isObject(options)) throw new Error('options is invalid'); const bbox = options.bbox; // Validation if (!type) throw new Error('type is required'); if (!coordinates) throw new Error('coordinates is required'); if (!Array.isArray(coordinates)) throw new Error('coordinates must be an Array'); if (bbox) validateBBox(bbox); // Main let geom; switch (type) { case 'Point': geom = point(coordinates).geometry; break; case 'LineString': geom = lineString(coordinates).geometry; break; case 'Polygon': geom = polygon(coordinates).geometry; break; case 'MultiPoint': geom = multiPoint(coordinates).geometry; break; case 'MultiLineString': geom = multiLineString(coordinates).geometry; break; case 'MultiPolygon': geom = multiPolygon(coordinates).geometry; break; default: throw new Error(`${type } is invalid`); } if (bbox) geom.bbox = bbox; return geom; } /** * Creates a {@link Point} {@link Feature} from a Position. * * @name point * @param {Array<number>} coordinates longitude, latitude position (each in decimal degrees) * @param {Object} [properties={}] an Object of key-value pairs to add as properties * @param {Object} [options={}] Optional Parameters * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature * @param {string|number} [options.id] Identifier associated with the Feature * @returns {Feature<Point>} a Point feature * @example * var point = turf.point([-75.343, 39.984]); * * //=point */ function point(coordinates, properties, options) { if (!coordinates) throw new Error('coordinates is required'); if (!Array.isArray(coordinates)) throw new Error('coordinates must be an Array'); if (coordinates.length < 2) throw new Error('coordinates must be at least 2 numbers long'); if (!isNumber(coordinates[0]) || !isNumber(coordinates[1])) throw new Error('coordinates must contain numbers'); return feature({ type: 'Point', coordinates }, properties, options); } /** * Creates a {@link Point} {@link FeatureCollection} from an Array of Point coordinates. * * @name points * @param {Array<Array<number>>} coordinates an array of Points * @param {Object} [properties={}] Translate these properties to each Feature * @param {Object} [options={}] Optional Parameters * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the FeatureCollection * @param {string|number} [options.id] Identifier associated with the FeatureCollection * @returns {FeatureCollection<Point>} Point Feature * @example * var points = turf.points([ * [-75, 39], * [-80, 45], * [-78, 50] * ]); * * //=points */ function points(coordinates, properties, options) { if (!coordinates) throw new Error('coordinates is required'); if (!Array.isArray(coordinates)) throw new Error('coordinates must be an Array'); return featureCollection(coordinates.map(coords => point(coords, properties)), options); } /** * Creates a {@link Polygon} {@link Feature} from an Array of LinearRings. * * @name polygon * @param {Array<Array<Array<number>>>} coordinates an array of LinearRings * @param {Object} [properties={}] an Object of key-value pairs to add as properties * @param {Object} [options={}] Optional Parameters * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature * @param {string|number} [options.id] Identifier associated with the Feature * @returns {Feature<Polygon>} Polygon Feature * @example * var polygon = turf.polygon([[[-5, 52], [-4, 56], [-2, 51], [-7, 54], [-5, 52]]], { name: 'poly1' }); * * //=polygon */ function polygon(coordinates, properties, options) { if (!coordinates) throw new Error('coordinates is required'); for (let i = 0; i < coordinates.length; i++) { const ring = coordinates[i]; if (ring.length < 4) { throw new Error('Each LinearRing of a Polygon must have 4 or more Positions.'); } for (let j = 0; j < ring[ring.length - 1].length; j++) { // Check if first point of Polygon contains two numbers if (i === 0 && j === 0 && !isNumber(ring[0][0]) || !isNumber(ring[0][1])) throw new Error('coordinates must contain numbers'); if (ring[ring.length - 1][j] !== ring[0][j]) { throw new Error('First and last Position are not equivalent.'); } } } return feature({ type: 'Polygon', coordinates }, properties, options); } /** * Creates a {@link Polygon} {@link FeatureCollection} from an Array of Polygon coordinates. * * @name polygons * @param {Array<Array<Array<Array<number>>>>} coordinates an array of Polygon coordinates * @param {Object} [properties={}] an Object of key-value pairs to add as properties * @param {Object} [options={}] Optional Parameters * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature * @param {string|number} [options.id] Identifier associated with the FeatureCollection * @returns {FeatureCollection<Polygon>} Polygon FeatureCollection * @example * var polygons = turf.polygons([ * [[[-5, 52], [-4, 56], [-2, 51], [-7, 54], [-5, 52]]], * [[[-15, 42], [-14, 46], [-12, 41], [-17, 44], [-15, 42]]], * ]); * * //=polygons */ function polygons(coordinates, properties, options) { if (!coordinates) throw new Error('coordinates is required'); if (!Array.isArray(coordinates)) throw new Error('coordinates must be an Array'); return featureCollection(coordinates.map(coords => polygon(coords, properties)), options); } /** * Creates a {@link LineString} {@link Feature} from an Array of Positions. * * @name lineString * @param {Array<Array<number>>} coordinates an array of Positions * @param {Object} [properties={}] an Object of key-value pairs to add as properties * @param {Object} [options={}] Optional Parameters * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature * @param {string|number} [options.id] Identifier associated with the Feature * @returns {Feature<LineString>} LineString Feature * @example * var linestring1 = turf.lineString([[-24, 63], [-23, 60], [-25, 65], [-20, 69]], {name: 'line 1'}); * var linestring2 = turf.lineString([[-14, 43], [-13, 40], [-15, 45], [-10, 49]], {name: 'line 2'}); * * //=linestring1 * //=linestring2 */ function lineString(coordinates, properties, options) { if (!coordinates) throw new Error('coordinates is required'); if (coordinates.length < 2) throw new Error('coordinates must be an array of two or more positions'); // Check if first point of LineString contains two numbers if (!isNumber(coordinates[0][1]) || !isNumber(coordinates[0][1])) throw new Error('coordinates must contain numbers'); return feature({ type: 'LineString', coordinates }, properties, options); } /** * Creates a {@link LineString} {@link FeatureCollection} from an Array of LineString coordinates. * * @name lineStrings * @param {Array<Array<number>>} coordinates an array of LinearRings * @param {Object} [properties={}] an Object of key-value pairs to add as properties * @param {Object} [options={}] Optional Parameters * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the FeatureCollection * @param {string|number} [options.id] Identifier associated with the FeatureCollection * @returns {FeatureCollection<LineString>} LineString FeatureCollection * @example * var linestrings = turf.lineStrings([ * [[-24, 63], [-23, 60], [-25, 65], [-20, 69]], * [[-14, 43], [-13, 40], [-15, 45], [-10, 49]] * ]); * * //=linestrings */ function lineStrings(coordinates, properties, options) { if (!coordinates) throw new Error('coordinates is required'); if (!Array.isArray(coordinates)) throw new Error('coordinates must be an Array'); return featureCollection(coordinates.map(coords => lineString(coords, properties)), options); } /** * Takes one or more {@link Feature|Features} and creates a {@link FeatureCollection}. * * @name featureCollection * @param {Feature[]} features input features * @param {Object} [options={}] Optional Parameters * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature * @param {string|number} [options.id] Identifier associated with the Feature * @returns {FeatureCollection} FeatureCollection of Features * @example * var locationA = turf.point([-75.343, 39.984], {name: 'Location A'}); * var locationB = turf.point([-75.833, 39.284], {name: 'Location B'}); * var locationC = turf.point([-75.534, 39.123], {name: 'Location C'}); * * var collection = turf.featureCollection([ * locationA, * locationB, * locationC * ]); * * //=collection */ function featureCollection(features, options) { // Optional Parameters options = options || {}; if (!isObject(options)) throw new Error('options is invalid'); const bbox = options.bbox; const id = options.id; // Validation if (!features) throw new Error('No features passed'); if (!Array.isArray(features)) throw new Error('features must be an Array'); if (bbox) validateBBox(bbox); if (id) validateId(id); // Main const fc = {type: 'FeatureCollection'}; if (id) fc.id = id; if (bbox) fc.bbox = bbox; fc.features = features; return fc; } /** * Creates a {@link Feature<MultiLineString>} based on a * coordinate array. Properties can be added optionally. * * @name multiLineString * @param {Array<Array<Array<number>>>} coordinates an array of LineStrings * @param {Object} [properties={}] an Object of key-value pairs to add as properties * @param {Object} [options={}] Optional Parameters * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature * @param {string|number} [options.id] Identifier associated with the Feature * @returns {Feature<MultiLineString>} a MultiLineString feature * @throws {Error} if no coordinates are passed * @example * var multiLine = turf.multiLineString([[[0,0],[10,10]]]); * * //=multiLine */ function multiLineString(coordinates, properties, options) { if (!coordinates) throw new Error('coordinates is required'); return feature({ type: 'MultiLineString', coordinates }, properties, options); } /** * Creates a {@link Feature<MultiPoint>} based on a * coordinate array. Properties can be added optionally. * * @name multiPoint * @param {Array<Array<number>>} coordinates an array of Positions * @param {Object} [properties={}] an Object of key-value pairs to add as properties * @param {Object} [options={}] Optional Parameters * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature * @param {string|number} [options.id] Identifier associated with the Feature * @returns {Feature<MultiPoint>} a MultiPoint feature * @throws {Error} if no coordinates are passed * @example * var multiPt = turf.multiPoint([[0,0],[10,10]]); * * //=multiPt */ function multiPoint(coordinates, properties, options) { if (!coordinates) throw new Error('coordinates is required'); return feature({ type: 'MultiPoint', coordinates }, properties, options); } /** * Creates a {@link Feature<MultiPolygon>} based on a * coordinate array. Properties can be added optionally. * * @name multiPolygon * @param {Array<Array<Array<Array<number>>>>} coordinates an array of Polygons * @param {Object} [properties={}] an Object of key-value pairs to add as properties * @param {Object} [options={}] Optional Parameters * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature * @param {string|number} [options.id] Identifier associated with the Feature * @returns {Feature<MultiPolygon>} a multipolygon feature * @throws {Error} if no coordinates are passed * @example * var multiPoly = turf.multiPolygon([[[[0,0],[0,10],[10,10],[10,0],[0,0]]]]); * * //=multiPoly * */ function multiPolygon(coordinates, properties, options) { if (!coordinates) throw new Error('coordinates is required'); return feature({ type: 'MultiPolygon', coordinates }, properties, options); } /** * Creates a {@link Feature<GeometryCollection>} based on a * coordinate array. Properties can be added optionally. * * @name geometryCollection * @param {Array<Geometry>} geometries an array of GeoJSON Geometries * @param {Object} [properties={}] an Object of key-value pairs to add as properties * @param {Object} [options={}] Optional Parameters * @param {Array<number>} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature * @param {string|number} [options.id] Identifier associated with the Feature * @returns {Feature<GeometryCollection>} a GeoJSON GeometryCollection Feature * @example * var pt = { * "type": "Point", * "coordinates": [100, 0] * }; * var line = { * "type": "LineString", * "coordinates": [ [101, 0], [102, 1] ] * }; * var collection = turf.geometryCollection([pt, line]); * * //=collection */ function geometryCollection(geometries, properties, options) { if (!geometries) throw new Error('geometries is required'); if (!Array.isArray(geometries)) throw new Error('geometries must be an Array'); return feature({ type: 'GeometryCollection', geometries }, properties, options); } /** * Round number to precision * * @param {number} num Number * @param {number} [precision=0] Precision * @returns {number} rounded number * @example * turf.round(120.4321) * //=120 * * turf.round(120.4321, 2) * //=120.43 */ function round(num, precision) { if (num === undefined || num === null || isNaN(num)) throw new Error('num is required'); if (precision && !(precision >= 0)) throw new Error('precision must be a positive number'); const multiplier = Math.pow(10, precision || 0); return Math.round(num * multiplier) / multiplier; } /** * Convert a distance measurement (assuming a spherical Earth) from radians to a more friendly unit. * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet * * @name radiansToLength * @param {number} radians in radians across the sphere * @param {string} [units='kilometers'] can be degrees, radians, miles, or kilometers inches, yards, metres, meters, kilometres, kilometers. * @returns {number} distance */ function radiansToLength(radians, units) { if (radians === undefined || radians === null) throw new Error('radians is required'); if (units && typeof units !== 'string') throw new Error('units must be a string'); const factor = factors[units || 'kilometers']; if (!factor) throw new Error(`${units } units is invalid`); return radians * factor; } /** * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into radians * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet * * @name lengthToRadians * @param {number} distance in real units * @param {string} [units='kilometers'] can be degrees, radians, miles, or kilometers inches, yards, metres, meters, kilometres, kilometers. * @returns {number} radians */ function lengthToRadians(distance, units) { if (distance === undefined || distance === null) throw new Error('distance is required'); if (units && typeof units !== 'string') throw new Error('units must be a string'); const factor = factors[units || 'kilometers']; if (!factor) throw new Error(`${units } units is invalid`); return distance / factor; } /** * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into degrees * Valid units: miles, nauticalmiles, inches, yards, meters, metres, centimeters, kilometres, feet * * @name lengthToDegrees * @param {number} distance in real units * @param {string} [units='kilometers'] can be degrees, radians, miles, or kilometers inches, yards, metres, meters, kilometres, kilometers. * @returns {number} degrees */ function lengthToDegrees(distance, units) { return radiansToDegrees(lengthToRadians(distance, units)); } /** * Converts any bearing angle from the north line direction (positive clockwise) * and returns an angle between 0-360 degrees (positive clockwise), 0 being the north line * * @name bearingToAzimuth * @param {number} bearing angle, between -180 and +180 degrees * @returns {number} angle between 0 and 360 degrees */ function bearingToAzimuth(bearing) { if (bearing === null || bearing === undefined) throw new Error('bearing is required'); let angle = bearing % 360; if (angle < 0) angle += 360; return angle; } /** * Converts an angle in radians to degrees * * @name radiansToDegrees * @param {number} radians angle in radians * @returns {number} degrees between 0 and 360 degrees */ function radiansToDegrees(radians) { if (radians === null || radians === undefined) throw new Error('radians is required'); const degrees = radians % (2 * Math.PI); return degrees * 180 / Math.PI; } /** * Converts an angle in degrees to radians * * @name degreesToRadians * @param {number} degrees angle between 0 and 360 degrees * @returns {number} angle in radians */ function degreesToRadians(degrees) { if (degrees === null || degrees === undefined) throw new Error('degrees is required'); const radians = degrees % 360; return radians * Math.PI / 180; } /** * Converts a length to the requested unit. * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet * * @param {number} length to be converted * @param {string} originalUnit of the length * @param {string} [finalUnit='kilometers'] returned unit * @returns {number} the converted length */ function convertLength(length, originalUnit, finalUnit) { if (length === null || length === undefined) throw new Error('length is required'); if (!(length >= 0)) throw new Error('length must be a positive number'); return radiansToLength(lengthToRadians(length, originalUnit), finalUnit || 'kilometers'); } /** * Converts a area to the requested unit. * Valid units: kilometers, kilometres, meters, metres, centimetres, millimeters, acres, miles, yards, feet, inches * @param {number} area to be converted * @param {string} [originalUnit='meters'] of the distance * @param {string} [finalUnit='kilometers'] returned unit * @returns {number} the converted distance */ function convertArea(area, originalUnit, finalUnit) { if (area === null || area === undefined) throw new Error('area is required'); if (!(area >= 0)) throw new Error('area must be a positive number'); const startFactor = areaFactors[originalUnit || 'meters']; if (!startFactor) throw new Error('invalid original units'); const finalFactor = areaFactors[finalUnit || 'kilometers']; if (!finalFactor) throw new Error('invalid final units'); return (area / startFactor) * finalFactor; } /** * isNumber * * @param {*} num Number to validate * @returns {boolean} true/false * @example * turf.isNumber(123) * //=true * turf.isNumber('foo') * //=false */ function isNumber(num) { return !isNaN(num) && num !== null && !Array.isArray(num); } /** * isObject * * @param {*} input variable to validate * @returns {boolean} true/false * @example * turf.isObject({elevation: 10}) * //=true * turf.isObject('foo') * //=false */ function isObject(input) { return (!!input) && (input.constructor === Object); } /** * Validate BBox * * @private * @param {Array<number>} bbox BBox to validate * @returns {void} * @throws Error if BBox is not valid * @example * validateBBox([-180, -40, 110, 50]) * //=OK * validateBBox([-180, -40]) * //=Error * validateBBox('Foo') * //=Error * validateBBox(5) * //=Error * validateBBox(null) * //=Error * validateBBox(undefined) * //=Error */ function validateBBox(bbox) { if (!bbox) throw new Error('bbox is required'); if (!Array.isArray(bbox)) throw new Error('bbox must be an Array'); if (bbox.length !== 4 && bbox.length !== 6) throw new Error('bbox must be an Array of 4 or 6 numbers'); bbox.forEach((num) => { if (!isNumber(num)) throw new Error('bbox must only contain numbers'); }); } /** * Validate Id * * @private * @param {string|number} id Id to validate * @returns {void} * @throws Error if Id is not valid * @example * validateId([-180, -40, 110, 50]) * //=Error * validateId([-180, -40]) * //=Error * validateId('Foo') * //=OK * validateId(5) * //=OK * validateId(null) * //=Error * validateId(undefined) * //=Error */ function validateId(id) { if (!id) throw new Error('id is required'); if (['string', 'number'].indexOf(typeof id) === -1) throw new Error('id must be a number or a string'); } // Deprecated methods function radians2degrees() { throw new Error('method has been renamed to `radiansToDegrees`'); } function degrees2radians() { throw new Error('method has been renamed to `degreesToRadians`'); } function distanceToDegrees() { throw new Error('method has been renamed to `lengthToDegrees`'); } function distanceToRadians() { throw new Error('method has been renamed to `lengthToRadians`'); } function radiansToDistance() { throw new Error('method has been renamed to `radiansToLength`'); } function bearingToAngle() { throw new Error('method has been renamed to `bearingToAzimuth`'); } function convertDistance() { throw new Error('method has been renamed to `convertLength`'); } var main_es = /*#__PURE__*/Object.freeze({ areaFactors: areaFactors, bearingToAngle: bearingToAngle, bearingToAzimuth: bearingToAzimuth, convertArea: convertArea, convertDistance: convertDistance, convertLength: convertLength, degrees2radians: degrees2radians, degreesToRadians: degreesToRadians, distanceToDegrees: distanceToDegrees, distanceToRadians: distanceToRadians, earthRadius: earthRadius, factors: factors, feature: feature, featureCollection: featureCollection, geometry: geometry, geometryCollection: geometryCollection, isNumber: isNumber, isObject: isObject, lengthToDegrees: lengthToDegrees, lengthToRadians: lengthToRadians, lineString: lineString, lineStrings: lineStrings, multiLineString: multiLineString, multiPoint: multiPoint, multiPolygon: multiPolygon, point: point, points: points, polygon: polygon, polygons: polygons, radians2degrees: radians2degrees, radiansToDegrees: radiansToDegrees, radiansToDistance: radiansToDistance, radiansToLength: radiansToLength, round: round, unitsFactors: unitsFactors, validateBBox: validateBBox, validateId: validateId }); /** * Callback for coordEach * * @callback coordEachCallback * @param {Array<number>} currentCoord The current coordinate being processed. * @param {number} coordIndex The current index of the coordinate being processed. * @param {number} featureIndex The current index of the Feature being processed. * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed. * @param {number} geometryIndex The current index of the Geometry being processed. */ /** * Iterate over coordinates in any GeoJSON object, similar to Array.forEach() * * @name coordEach * @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON object * @param {Function} callback a method that takes (currentCoord, coordIndex, featureIndex, multiFeatureIndex) * @param {boolean} [excludeWrapCoord=false] whether or not to include the final coordinate of LinearRings that wraps the ring in its iteration. * @example * var features = turf.featureCollection([ * turf.point([26, 37], {"foo": "bar"}), * turf.point([36, 53], {"hello": "world"}) * ]); * * turf.coordEach(features, function (currentCoord, coordIndex, featureIndex, multiFeatureIndex, geometryIndex) { * //=currentCoord * //=coordIndex * //=featureIndex * //=multiFeatureIndex * //=geometryIndex * }); */ function coordEach(geojson, callback, excludeWrapCoord) { // Handles null Geometry -- Skips this GeoJSON if (geojson === null) return; let j, k, l, geometry, stopG, coords, geometryMaybeCollection, wrapShrink = 0, coordIndex = 0, isGeometryCollection, type = geojson.type, isFeatureCollection = type === 'FeatureCollection', isFeature = type === 'Feature', stop = isFeatureCollection ? geojson.features.length : 1; // This logic may look a little weird. The reason why it is that way // is because it's trying to be fast. GeoJSON supports multiple kinds // of objects at its root: FeatureCollection, Features, Geometries. // This function has the responsibility of handling all of them, and that // means that some of the `for` loops you see below actually just don't apply // to certain inputs. For instance, if you give this just a // Point geometry, then both loops are short-circuited and all we do // is gradually rename the input until it's called 'geometry'. // // This also aims to allocate as few resources as possible: just a // few numbers and booleans, rather than any temporary arrays as would // be required with the normalization approach. for (let featureIndex = 0; featureIndex < stop; featureIndex++) { geometryMaybeCollection = (isFeatureCollection ? geojson.features[featureIndex].geometry : (isFeature ? geojson.geometry : geojson)); isGeometryCollection = (geometryMaybeCollection) ? geometryMaybeCollection.type === 'GeometryCollection' : false; stopG = isGeometryCollection ? geometryMaybeCollection.geometries.length : 1; for (let geomIndex = 0; geomIndex < stopG; geomIndex++) { let multiFeatureIndex = 0; let geometryIndex = 0; geometry = isGeometryCollection ? geometryMaybeCollection.geometries[geomIndex] : geometryMaybeCollection; // Handles null Geometry -- Skips this geometry if (geometry === null) continue; coords = geometry.coordinates; const geomType = geometry.type; wrapShrink = (excludeWrapCoord && (geomType === 'Polygon' || geomType === 'MultiPolygon')) ? 1 : 0; switch (geomType) { case null: break; case 'Point': callback(coords, coordIndex, featureIndex, multiFeatureIndex, geometryIndex); coordIndex++; multiFeatureIndex++; break; case 'LineString': case 'MultiPoint': for (j = 0; j < coords.length; j++) { callback(coords[j], coordIndex, featureIndex, multiFeatureIndex, geometryIndex); coordIndex++; if (geomType === 'MultiPoint') multiFeatureIndex++; } if (geomType === 'LineString') multiFeatureIndex++; break; case 'Polygon': case 'MultiLineString': for (j = 0; j < coords.length; j++) { for (k = 0; k < coords[j].length - wrapShrink; k++) { callback(coords[j][k], coordIndex, featureIndex, multiFeatureIndex, geometryIndex); coordIndex++; } if (geomType === 'MultiLineString') multiFeatureIndex++; if (geomType === 'Polygon') geometryIndex++; } if (geomType === 'Polygon') multiFeatureIndex++; break; case 'MultiPolygon': for (j = 0; j < coords.length; j++) { if (geomType === 'MultiPolygon') geometryIndex = 0; for (k = 0; k < coords[j].length; k++) { for (l = 0; l < coords[j][k].length - wrapShrink; l++) { callback(coords[j][k][l], coordIndex, featureIndex, multiFeatureIndex, geometryIndex); coordIndex++; } geometryIndex++; } multiFeatureIndex++; } break; case 'GeometryCollection': for (j = 0; j < geometry.geometries.length; j++) coordEach(geometry.geometries[j], callback, excludeWrapCoord); break; default: throw new Error('Unknown Geometry Type'); } } } } /** * Callback for coordReduce * * The first time the callback function is called, the values provided as arguments depend * on whether the reduce method has an initialValue argument. * * If an initialValue is provided to the reduce method: * - The previousValue argument is initialValue. * - The currentValue argument is the value of the first element present in the array. * * If an initialValue is not provided: * - The previousValue argument is the value of the first element present in the array. * - The currentValue argument is the value of the second element present in the array. * * @callback coordReduceCallback * @param {*} previousValue The accumulated value previously returned in the last invocation * of the callback, or initialValue, if supplied. * @param {Array<number>} currentCoord The current coordinate being processed. * @param {number} coordIndex The current index of the coordinate being processed. * Starts at index 0, if an initialValue is provided, and at index 1 otherwise. * @param {number} featureIndex The current index of the Feature being processed. * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed. * @param {number} geometryIndex The current index of the Geometry being processed. */ /** * Reduce coordinates in any GeoJSON object, similar to Array.reduce() * * @name coordReduce * @param {FeatureCollection|Geometry|Feature} geojson any GeoJSON object * @param {Function} callback a method that takes (previousValue, currentCoord, coordIndex) * @param {*} [initialValue] Value to use as the first argument to the first call of the callback. * @param {boolean} [excludeWrapCoord=false] whether or not to include the final coordinate of LinearRings that wraps the ring in its iteration. * @returns {*} The value that results from the reduction. * @example * var features = turf.featureCollection([ * turf.point([26, 37], {"foo": "bar"}), * turf.point([36, 53], {"hello": "world"}) * ]); * * turf.coordReduce(features, function (previousValue, currentCoord, coordIndex, featureIndex, multiFeatureIndex, geometryIndex) { * //=previousValue * //=currentCoord * //=coordIndex * //=featureIndex * //=multiFeatureIndex * //=geometryIndex * return currentCoord; * }); */ function coordReduce(geojson, callback, initialValue, excludeWrapCoord) { let previousValue = initialValue; coordEach(geojson, (currentCoord, coordIndex, featureIndex, multiFeatureIndex, geometryIndex) => { if (coordIndex === 0 && initialValue === undefined) previousValue = currentCoord; else previousValue = callback(previousValue, currentCoord, coordIndex, featureIndex, multiFeatureIndex, geometryIndex); }, excludeWrapCoord); return previousValue; } /** * Callback for propEach * * @callback propEachCallback * @param {Object} currentProperties The current Properties being processed. * @param {number} featureIndex The current index of the Feature being processed. */ /** * Iterate over properties in any GeoJSON object, similar to Array.forEach() * * @name propEach * @param {FeatureCollection|Feature} geojson any GeoJSON object * @param {Function} callback a method that takes (currentProperties, featureIndex) * @example * var features = turf.featureCollection([ * turf.point([26, 37], {foo: 'bar'}), * turf.point([36, 53], {hello: 'world'}) * ]); * * turf.propEach(features, function (currentProperties, featureIndex) { * //=currentProperties * //=featureIndex * }); */ function propEach(geojson, callback) { let i; switch (geojson.type) { case 'FeatureCollection': for (i = 0; i < geojson.features.length; i++) { callback(geojson.features[i].properties, i); } break; case 'Feature': callback(geojson.properties, 0); break; } } /** * Callback for propReduce * * The first time the callback function is called, the values provided as arguments depend * on whether the reduce method has an initialValue argument. * * If an initialValue is provided to the reduce method: * - The previousValue argument is initialValue. * - The currentValue argument is the value of the first element present in the array. * * If an initialValue is not provided: * - The previousValue argument is the value of the first element present in the array. * - The currentValue argument is the value of the second element present in the array. * * @callback propReduceCallback * @param {*} previousValue The accumulated value previously returned in the last invocation * of the callback, or initialValue, if supplied. * @param {*} currentProperties The current Properties being processed. * @param {number} featureIndex The current index of the Feature being processed. */ /** * Reduce properties in any GeoJSON object into a single value, * similar to how Array.reduce works. However, in this case we lazily run * the reduction, so an array of all properties is unnecessary. * * @name propReduce * @param {FeatureCollection|Feature} geojson any GeoJSON object * @param {Function} callback a method that takes (previousValue, currentProperties, featureIndex) * @param {*} [initialValue] Value to use as the first argument to the first call of the callback. * @returns {*} The value that results from the reduction. * @example * var features = turf.featureCollection([ * turf.point([26, 37], {foo: 'bar'}), * turf.point([36, 53], {hello: 'world'}) * ]); * * turf.propReduce(features, function (previousValue, currentProperties, featureIndex) { * //=previousValue * //=currentProperties * //=featureIndex * return currentProperties * }); */ function propReduce(geojson, callback, initialValue) { let previousValue = initialValue; propEach(geojson, (currentProperties, featureIndex) => { if (featureIndex === 0 && initialValue === undefined) previousValue = currentProperties; else previousValue = callback(previousValue, currentProperties, featureIndex); }); return previousValue; } /** * Callback for featureEach * * @callback featureEachCallback * @param {Feature<any>} currentFeature The current Feature being processed. * @param {number} featureIndex The current index of the Feature being processed. */ /** * Iterate over features in any GeoJSON object, similar to * Array.forEach. * * @name featureEach * @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON object * @param {Function} callback a method that takes (currentFeature, featureIndex) * @example * var features = turf.featureCollection([ * turf.point([26, 37], {foo: 'bar'}), * turf.point([36, 53], {hello: 'world'}) * ]); * * turf.featureEach(features, function (currentFeature, featureIndex) { * //=currentFeature * //=featureIndex * }); */ function featureEach(geojson, callback) { if (geojson.type === 'Feature') { callback(geojson, 0); } else if (geojson.type === 'FeatureCollection') { for (let i = 0; i < geojson.features.length; i++) { callback(geojson.features[i], i); } } } /** * Callback for featureReduce * * The first time the callback function is called, the values provided as arguments depend * on whether the reduce method has an initialValue argument. * * If an initialValue is provided to the reduce method: * - The previousValue argument is initialValue. * - The currentValue argument is the value of the first element present in the array. * * If an initialValue is not provided: * - The previousValue argument is the value of the first element present in the array. * - The currentValue argument is the value of the second element present in the array. * * @callback featureReduceCallback * @param {*} previousValue The accumulated value previously returned in the last invocation * of the callback, or initialValue, if supplied. * @param {Feature} currentFeature The current Feature being processed. * @param {number} featureIndex The current index of the Feature being processed. */ /** * Reduce features in any GeoJSON object, similar to Array.reduce(). * * @name featureReduce * @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON object * @param {Function} callback a method that takes (previousValue, currentFeature, featureIndex) * @param {*} [initialValue] Value to use as the first argument to the first call of the callback. * @returns {*} The value that results from the reduction. * @example * var features = turf.featureCollection([ * turf.point([26, 37], {"foo": "bar"}), * turf.point([36, 53], {"hello": "world"}) * ]); * * turf.featureReduce(features, function (previousValue, currentFeature, featureIndex) { * //=previousValue * //=currentFeature * //=featureIndex * return currentFeature * }); */ function featureReduce(geojson, callback, initialValue) { let previousValue = initialValue; featureEach(geojson, (currentFeature, featureIndex) => { if (featureIndex === 0 && initialValue === undefined) previousValue = currentFeature; else previousValue = callback(previousValue, currentFeature, featureIndex); }); return previousValue; } /** * Get all coordinates from any GeoJSON object. * * @name coordAll * @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON object * @returns {Array<Array<number>>} coordinate position array * @example * var features = turf.featureCollection([ * turf.point([26, 37], {foo: 'bar'}), * turf.point([36, 53], {hello: 'world'}) * ]); * * var coords = turf.coordAll(features); * //= [[26, 37], [36, 53]] */ function coordAll(geojson) { const coords = []; coordEach(geojson, (coord) => { coords.push(coord); }); return coords; } /** * Callback for geomEach * * @callback geomEachCallback * @param {Geometry} currentGeometry The current Geometry being processed. * @param {number} featureIndex The current index of the Feature being processed. * @param {Object} featureProperties The current Feature Properties being processed. * @param {Array<number>} featureBBox The current Feature BBox being processed. * @param {number|string} featureId The current Feature Id being processed. */ /** * Iterate over each geometry in any GeoJSON object, similar to Array.forEach() * * @name geomEach * @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON object * @param {Function} callback a method that takes (currentGeometry, featureIndex, featureProperties, featureBBox, featureId) * @example * var features = turf.featureCollection([ * turf.point([26, 37], {foo: 'bar'}), * turf.point([36, 53], {hello: 'world'}) * ]); * * turf.geomEach(features, function (currentGeometry, featureIndex, featureProperties, featureBBox, featureId) { * //=currentGeometry * //=featureIndex * //=featureProperties * //=featureBBox * //=featureId * }); */ function geomEach(geojson, callback) { let i, j, g, geometry, stopG, geometryMaybeCollection, isGeometryCollection, featureProperties, featureBBox, featureId, featureIndex = 0, isFeatureCollection = geojson.type === 'FeatureCollection', isFeature = geojson.type === 'Feature', stop = isFeatureCollection ? geojson.features.length : 1; // This logic may look a little weird. The reason why it is that way // is because it's trying to be fast. GeoJSON supports multiple kinds // of objects at its root: FeatureCollection, Features, Geometries. // This function has the responsibility of handling all of them, and that // means that some of the `for` loops you see below actually just don't apply // to certain inputs. For instance, if you give this just a // Point geometry, then both loops are short-circuited and all we do // is gradually rename the input until it's called 'geometry'. // // This also aims to allocate as few resources as possible: just a // few numbers and booleans, rather than any temporary arrays as would // be required with the normalization approach. for (i = 0; i < stop; i++) { geometryMaybeCollection = (isFeatureCollection ? geojson.features[i].geometry : (isFeature ? geojso