'use strict'; var meta = require('@turf/meta'); var invariant = require('@turf/invariant'); var helpers = require('@turf/helpers'); /** * https://github.com/rook2pawn/node-intersection * * Author @rook2pawn */ /** * AB * * @private * @param {Array>} segment - 2 vertex line segment * @returns {Array} coordinates [x, y] */ function ab(segment) { var start = segment[0]; var end = segment[1]; return [end[0] - start[0], end[1] - start[1]]; } /** * Cross Product * * @private * @param {Array} v1 coordinates [x, y] * @param {Array} v2 coordinates [x, y] * @returns {Array} Cross Product */ function crossProduct(v1, v2) { return v1[0] * v2[1] - v2[0] * v1[1]; } /** * Add * * @private * @param {Array} v1 coordinates [x, y] * @param {Array} v2 coordinates [x, y] * @returns {Array} Add */ function add(v1, v2) { return [v1[0] + v2[0], v1[1] + v2[1]]; } /** * Sub * * @private * @param {Array} v1 coordinates [x, y] * @param {Array} v2 coordinates [x, y] * @returns {Array} Sub */ function sub(v1, v2) { return [v1[0] - v2[0], v1[1] - v2[1]]; } /** * scalarMult * * @private * @param {number} s scalar * @param {Array} v coordinates [x, y] * @returns {Array} scalarMult */ function scalarMult(s, v) { return [s * v[0], s * v[1]]; } /** * Intersect Segments * * @private * @param {Array} a coordinates [x, y] * @param {Array} b coordinates [x, y] * @returns {Array} intersection */ function intersectSegments(a, b) { var p = a[0]; var r = ab(a); var q = b[0]; var s = ab(b); var cross = crossProduct(r, s); var qmp = sub(q, p); var numerator = crossProduct(qmp, s); var t = numerator / cross; var intersection = add(p, scalarMult(t, r)); return intersection; } /** * Is Parallel * * @private * @param {Array} a coordinates [x, y] * @param {Array} b coordinates [x, y] * @returns {boolean} true if a and b are parallel (or co-linear) */ function isParallel(a, b) { var r = ab(a); var s = ab(b); return crossProduct(r, s) === 0; } /** * Intersection * * @private * @param {Array} a coordinates [x, y] * @param {Array} b coordinates [x, y] * @returns {Array|boolean} true if a and b are parallel (or co-linear) */ function intersection(a, b) { if (isParallel(a, b)) return false; return intersectSegments(a, b); } /** * Takes a {@link LineString|line} and returns a {@link LineString|line} at offset by the specified distance. * * @name lineOffset * @param {Geometry|Feature} geojson input GeoJSON * @param {number} distance distance to offset the line (can be of negative value) * @param {Object} [options={}] Optional parameters * @param {string} [options.units='kilometers'] can be degrees, radians, miles, kilometers, inches, yards, meters * @returns {Feature} Line offset from the input line * @example * var line = turf.lineString([[-83, 30], [-84, 36], [-78, 41]], { "stroke": "#F00" }); * * var offsetLine = turf.lineOffset(line, 2, {units: 'miles'}); * * //addToMap * var addToMap = [offsetLine, line] * offsetLine.properties.stroke = "#00F" */ function lineOffset(geojson, distance, options) { // Optional parameters options = options || {}; if (!helpers.isObject(options)) throw new Error("options is invalid"); var units = options.units; // Valdiation if (!geojson) throw new Error("geojson is required"); if (distance === undefined || distance === null || isNaN(distance)) throw new Error("distance is required"); var type = invariant.getType(geojson); var properties = geojson.properties; switch (type) { case "LineString": return lineOffsetFeature(geojson, distance, units); case "MultiLineString": var coords = []; meta.flattenEach(geojson, function (feature) { coords.push( lineOffsetFeature(feature, distance, units).geometry.coordinates ); }); return helpers.multiLineString(coords, properties); default: throw new Error("geometry " + type + " is not supported"); } } /** * Line Offset * * @private * @param {Geometry|Feature} line input line * @param {number} distance distance to offset the line (can be of negative value) * @param {string} [units=kilometers] units * @returns {Feature} Line offset from the input line */ function lineOffsetFeature(line, distance, units) { var segments = []; var offsetDegrees = helpers.lengthToDegrees(distance, units); var coords = invariant.getCoords(line); var finalCoords = []; coords.forEach(function (currentCoords, index) { if (index !== coords.length - 1) { var segment = processSegment( currentCoords, coords[index + 1], offsetDegrees ); segments.push(segment); if (index > 0) { var seg2Coords = segments[index - 1]; var intersects = intersection(segment, seg2Coords); // Handling for line segments that aren't straight if (intersects !== false) { seg2Coords[1] = intersects; segment[0] = intersects; } finalCoords.push(seg2Coords[0]); if (index === coords.length - 2) { finalCoords.push(segment[0]); finalCoords.push(segment[1]); } } // Handling for lines that only have 1 segment if (coords.length === 2) { finalCoords.push(segment[0]); finalCoords.push(segment[1]); } } }); return helpers.lineString(finalCoords, line.properties); } /** * Process Segment * Inspiration taken from http://stackoverflow.com/questions/2825412/draw-a-parallel-line * * @private * @param {Array} point1 Point coordinates * @param {Array} point2 Point coordinates * @param {number} offset Offset * @returns {Array>} offset points */ function processSegment(point1, point2, offset) { var L = Math.sqrt( (point1[0] - point2[0]) * (point1[0] - point2[0]) + (point1[1] - point2[1]) * (point1[1] - point2[1]) ); var out1x = point1[0] + (offset * (point2[1] - point1[1])) / L; var out2x = point2[0] + (offset * (point2[1] - point1[1])) / L; var out1y = point1[1] + (offset * (point1[0] - point2[0])) / L; var out2y = point2[1] + (offset * (point1[0] - point2[0])) / L; return [ [out1x, out1y], [out2x, out2y], ]; } module.exports = lineOffset; module.exports.default = lineOffset;