/** * @module ol/coordinate */ import {getWidth} from './extent.js'; import {modulo, toFixed} from './math.js'; import {padNumber} from './string.js'; /** * An array of numbers representing an `xy`, `xyz` or `xyzm` coordinate. * Example: `[16, 48]`. * @typedef {Array} Coordinate * @api */ /** * A function that takes a {@link module:ol/coordinate~Coordinate} and * transforms it into a `{string}`. * * @typedef {function((Coordinate|undefined)): string} CoordinateFormat * @api */ /** * Add `delta` to `coordinate`. `coordinate` is modified in place and returned * by the function. * * Example: * * import {add} from 'ol/coordinate.js'; * * const coord = [7.85, 47.983333]; * add(coord, [-2, 4]); * // coord is now [5.85, 51.983333] * * @param {Coordinate} coordinate Coordinate. * @param {Coordinate} delta Delta. * @return {Coordinate} The input coordinate adjusted by * the given delta. * @api */ export function add(coordinate, delta) { coordinate[0] += +delta[0]; coordinate[1] += +delta[1]; return coordinate; } /** * Calculates the point closest to the passed coordinate on the passed circle. * * @param {Coordinate} coordinate The coordinate. * @param {import("./geom/Circle.js").default} circle The circle. * @return {Coordinate} Closest point on the circumference. */ export function closestOnCircle(coordinate, circle) { const r = circle.getRadius(); const center = circle.getCenter(); const x0 = center[0]; const y0 = center[1]; const x1 = coordinate[0]; const y1 = coordinate[1]; let dx = x1 - x0; const dy = y1 - y0; if (dx === 0 && dy === 0) { dx = 1; } const d = Math.sqrt(dx * dx + dy * dy); const x = x0 + (r * dx) / d; const y = y0 + (r * dy) / d; return [x, y]; } /** * Calculates the point closest to the passed coordinate on the passed segment. * This is the foot of the perpendicular of the coordinate to the segment when * the foot is on the segment, or the closest segment coordinate when the foot * is outside the segment. * * @param {Coordinate} coordinate The coordinate. * @param {Array} segment The two coordinates * of the segment. * @return {Coordinate} The foot of the perpendicular of * the coordinate to the segment. */ export function closestOnSegment(coordinate, segment) { const x0 = coordinate[0]; const y0 = coordinate[1]; const start = segment[0]; const end = segment[1]; const x1 = start[0]; const y1 = start[1]; const x2 = end[0]; const y2 = end[1]; const dx = x2 - x1; const dy = y2 - y1; const along = dx === 0 && dy === 0 ? 0 : (dx * (x0 - x1) + dy * (y0 - y1)) / (dx * dx + dy * dy || 0); let x, y; if (along <= 0) { x = x1; y = y1; } else if (along >= 1) { x = x2; y = y2; } else { x = x1 + along * dx; y = y1 + along * dy; } return [x, y]; } /** * Returns a {@link module:ol/coordinate~CoordinateFormat} function that can be * used to format * a {Coordinate} to a string. * * Example without specifying the fractional digits: * * import {createStringXY} from 'ol/coordinate.js'; * * const coord = [7.85, 47.983333]; * const stringifyFunc = createStringXY(); * const out = stringifyFunc(coord); * // out is now '8, 48' * * Example with explicitly specifying 2 fractional digits: * * import {createStringXY} from 'ol/coordinate.js'; * * const coord = [7.85, 47.983333]; * const stringifyFunc = createStringXY(2); * const out = stringifyFunc(coord); * // out is now '7.85, 47.98' * * @param {number} [fractionDigits] The number of digits to include * after the decimal point. Default is `0`. * @return {CoordinateFormat} Coordinate format. * @api */ export function createStringXY(fractionDigits) { return ( /** * @param {Coordinate} coordinate Coordinate. * @return {string} String XY. */ function (coordinate) { return toStringXY(coordinate, fractionDigits); } ); } /** * @param {string} hemispheres Hemispheres. * @param {number} degrees Degrees. * @param {number} [fractionDigits] The number of digits to include * after the decimal point. Default is `0`. * @return {string} String. */ export function degreesToStringHDMS(hemispheres, degrees, fractionDigits) { const normalizedDegrees = modulo(degrees + 180, 360) - 180; const x = Math.abs(3600 * normalizedDegrees); const decimals = fractionDigits || 0; let deg = Math.floor(x / 3600); let min = Math.floor((x - deg * 3600) / 60); let sec = toFixed(x - deg * 3600 - min * 60, decimals); if (sec >= 60) { sec = 0; min += 1; } if (min >= 60) { min = 0; deg += 1; } let hdms = deg + '\u00b0'; if (min !== 0 || sec !== 0) { hdms += ' ' + padNumber(min, 2) + '\u2032'; } if (sec !== 0) { hdms += ' ' + padNumber(sec, 2, decimals) + '\u2033'; } if (normalizedDegrees !== 0) { hdms += ' ' + hemispheres.charAt(normalizedDegrees < 0 ? 1 : 0); } return hdms; } /** * Transforms the given {@link module:ol/coordinate~Coordinate} to a string * using the given string template. The strings `{x}` and `{y}` in the template * will be replaced with the first and second coordinate values respectively. * * Example without specifying the fractional digits: * * import {format} from 'ol/coordinate.js'; * * const coord = [7.85, 47.983333]; * const template = 'Coordinate is ({x}|{y}).'; * const out = format(coord, template); * // out is now 'Coordinate is (8|48).' * * Example explicitly specifying the fractional digits: * * import {format} from 'ol/coordinate.js'; * * const coord = [7.85, 47.983333]; * const template = 'Coordinate is ({x}|{y}).'; * const out = format(coord, template, 2); * // out is now 'Coordinate is (7.85|47.98).' * * @param {Coordinate} coordinate Coordinate. * @param {string} template A template string with `{x}` and `{y}` placeholders * that will be replaced by first and second coordinate values. * @param {number} [fractionDigits] The number of digits to include * after the decimal point. Default is `0`. * @return {string} Formatted coordinate. * @api */ export function format(coordinate, template, fractionDigits) { if (coordinate) { return template .replace('{x}', coordinate[0].toFixed(fractionDigits)) .replace('{y}', coordinate[1].toFixed(fractionDigits)); } return ''; } /** * @param {Coordinate} coordinate1 First coordinate. * @param {Coordinate} coordinate2 Second coordinate. * @return {boolean} The two coordinates are equal. */ export function equals(coordinate1, coordinate2) { let equals = true; for (let i = coordinate1.length - 1; i >= 0; --i) { if (coordinate1[i] != coordinate2[i]) { equals = false; break; } } return equals; } /** * Rotate `coordinate` by `angle`. `coordinate` is modified in place and * returned by the function. * * Example: * * import {rotate} from 'ol/coordinate.js'; * * const coord = [7.85, 47.983333]; * const rotateRadians = Math.PI / 2; // 90 degrees * rotate(coord, rotateRadians); * // coord is now [-47.983333, 7.85] * * @param {Coordinate} coordinate Coordinate. * @param {number} angle Angle in radian. * @return {Coordinate} Coordinate. * @api */ export function rotate(coordinate, angle) { const cosAngle = Math.cos(angle); const sinAngle = Math.sin(angle); const x = coordinate[0] * cosAngle - coordinate[1] * sinAngle; const y = coordinate[1] * cosAngle + coordinate[0] * sinAngle; coordinate[0] = x; coordinate[1] = y; return coordinate; } /** * Scale `coordinate` by `scale`. `coordinate` is modified in place and returned * by the function. * * Example: * * import {scale as scaleCoordinate} from 'ol/coordinate.js'; * * const coord = [7.85, 47.983333]; * const scale = 1.2; * scaleCoordinate(coord, scale); * // coord is now [9.42, 57.5799996] * * @param {Coordinate} coordinate Coordinate. * @param {number} scale Scale factor. * @return {Coordinate} Coordinate. */ export function scale(coordinate, scale) { coordinate[0] *= scale; coordinate[1] *= scale; return coordinate; } /** * @param {Coordinate} coord1 First coordinate. * @param {Coordinate} coord2 Second coordinate. * @return {number} Squared distance between coord1 and coord2. */ export function squaredDistance(coord1, coord2) { const dx = coord1[0] - coord2[0]; const dy = coord1[1] - coord2[1]; return dx * dx + dy * dy; } /** * @param {Coordinate} coord1 First coordinate. * @param {Coordinate} coord2 Second coordinate. * @return {number} Distance between coord1 and coord2. */ export function distance(coord1, coord2) { return Math.sqrt(squaredDistance(coord1, coord2)); } /** * Calculate the squared distance from a coordinate to a line segment. * * @param {Coordinate} coordinate Coordinate of the point. * @param {Array} segment Line segment (2 * coordinates). * @return {number} Squared distance from the point to the line segment. */ export function squaredDistanceToSegment(coordinate, segment) { return squaredDistance(coordinate, closestOnSegment(coordinate, segment)); } /** * Format a geographic coordinate with the hemisphere, degrees, minutes, and * seconds. * * Example without specifying fractional digits: * * import {toStringHDMS} from 'ol/coordinate.js'; * * const coord = [7.85, 47.983333]; * const out = toStringHDMS(coord); * // out is now '47° 58′ 60″ N 7° 50′ 60″ E' * * Example explicitly specifying 1 fractional digit: * * import {toStringHDMS} from 'ol/coordinate.js'; * * const coord = [7.85, 47.983333]; * const out = toStringHDMS(coord, 1); * // out is now '47° 58′ 60.0″ N 7° 50′ 60.0″ E' * * @param {Coordinate} coordinate Coordinate. * @param {number} [fractionDigits] The number of digits to include * after the decimal point. Default is `0`. * @return {string} Hemisphere, degrees, minutes and seconds. * @api */ export function toStringHDMS(coordinate, fractionDigits) { if (coordinate) { return ( degreesToStringHDMS('NS', coordinate[1], fractionDigits) + ' ' + degreesToStringHDMS('EW', coordinate[0], fractionDigits) ); } return ''; } /** * Format a coordinate as a comma delimited string. * * Example without specifying fractional digits: * * import {toStringXY} from 'ol/coordinate.js'; * * const coord = [7.85, 47.983333]; * const out = toStringXY(coord); * // out is now '8, 48' * * Example explicitly specifying 1 fractional digit: * * import {toStringXY} from 'ol/coordinate.js'; * * const coord = [7.85, 47.983333]; * const out = toStringXY(coord, 1); * // out is now '7.8, 48.0' * * @param {Coordinate} coordinate Coordinate. * @param {number} [fractionDigits] The number of digits to include * after the decimal point. Default is `0`. * @return {string} XY. * @api */ export function toStringXY(coordinate, fractionDigits) { return format(coordinate, '{x}, {y}', fractionDigits); } /** * Modifies the provided coordinate in-place to be within the real world * extent. The lower projection extent boundary is inclusive, the upper one * exclusive. * * @param {Coordinate} coordinate Coordinate. * @param {import("./proj/Projection.js").default} projection Projection. * @return {Coordinate} The coordinate within the real world extent. */ export function wrapX(coordinate, projection) { if (projection.canWrapX()) { const worldWidth = getWidth(projection.getExtent()); const worldsAway = getWorldsAway(coordinate, projection, worldWidth); if (worldsAway) { coordinate[0] -= worldsAway * worldWidth; } } return coordinate; } /** * @param {Coordinate} coordinate Coordinate. * @param {import("./proj/Projection.js").default} projection Projection. * @param {number} [sourceExtentWidth] Width of the source extent. * @return {number} Offset in world widths. */ export function getWorldsAway(coordinate, projection, sourceExtentWidth) { const projectionExtent = projection.getExtent(); let worldsAway = 0; if ( projection.canWrapX() && (coordinate[0] < projectionExtent[0] || coordinate[0] > projectionExtent[2]) ) { sourceExtentWidth = sourceExtentWidth || getWidth(projectionExtent); worldsAway = Math.floor( (coordinate[0] - projectionExtent[0]) / sourceExtentWidth ); } return worldsAway; }