├── .prettierignore ├── .npmrc ├── .gitignore ├── .eslintignore ├── release.config.js ├── src ├── toDeg.ts ├── toRad.ts ├── isSexagesimal.ts ├── toDeg.test.js ├── toRad.test.js ├── robustAcos.ts ├── isDecimal.ts ├── findNearest.ts ├── convertSpeed.test.js ├── isDecimal.test.js ├── isPointWithinRadius.ts ├── getRhumbLineBearing.test.js ├── isPointNearLine.ts ├── convertArea.ts ├── isPointNearLine.test.js ├── convertDistance.ts ├── isPointInLine.ts ├── convertDistance.test.js ├── isSexagesimal.test.js ├── robustAcos.test.js ├── getRoughCompassDirection.ts ├── getSpeed.ts ├── getLatitude.ts ├── getLongitude.ts ├── convertSpeed.ts ├── isValidLatitude.ts ├── isValidLongitude.ts ├── wktToPolygon.ts ├── findNearest.test.js ├── isPointInLine.test.js ├── getGreatCircleBearing.test.js ├── orderByDistance.ts ├── isValidCoordinate.test.js ├── wktToPolygon.test.js ├── sexagesimalToDecimal.ts ├── getCoordinateKey.ts ├── getLatitude.test.js ├── getLongitude.test.js ├── convertArea.test.js ├── getPathLength.ts ├── getSpeed.test.js ├── getCoordinateKey.test.js ├── getCenterOfBounds.ts ├── decimalToSexagesimal.ts ├── getBounds.ts ├── isPointWithinRadius.test.js ├── getGreatCircleBearing.ts ├── getCoordinateKeys.ts ├── getDistance.ts ├── isPointInPolygon.ts ├── getCenter.ts ├── isValidCoordinate.ts ├── getAreaOfPolygon.ts ├── getPathLength.test.js ├── sexagesimalToDecimal.test.js ├── getCompassDirection.test.js ├── constants.ts ├── getDistance.test.js ├── getRoughCompassDirection.test.js ├── getRhumbLineBearing.ts ├── computeDestinationPoint.ts ├── getCenter.test.js ├── types.ts ├── getPreciseDistance.test.js ├── getCoordinateKeys.test.js ├── toDecimal.ts ├── getCompassDirection.ts ├── decimalToSexagesimal.test.js ├── isValidLatitude.test.js ├── isValidLongitude.test.js ├── toDecimal.test.js ├── getDistanceFromLine.ts ├── getBoundsOfDistance.test.js ├── isPointInPolygon.test.js ├── computeDestinationPoint.test.js ├── orderByDistance.test.js ├── getBoundsOfDistance.ts ├── getCenterOfBounds.test.js ├── getBounds.test.js ├── index.ts ├── getAreaOfPolygon.test.js ├── getPreciseDistance.ts └── getDistanceFromLine.test.js ├── .huskyrc ├── .github └── FUNDING.yml ├── .eslintrc.js ├── .lintstagedrc ├── .babelrc.js ├── .prettierrc ├── .editorconfig ├── tsconfig.json ├── webpack.config.js └── index.js ├── LICENSE ├── .circleci └── config.yml ├── package.json ├── CHANGELOG.md ├── jest.config.js └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | lib/* 2 | es/* 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken = ${NPM_TOKEN} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | /es 4 | /lib 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/public 3 | **/build 4 | *.d.ts 5 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@werkzeugkiste/release-config/package.config'); 2 | -------------------------------------------------------------------------------- /src/toDeg.ts: -------------------------------------------------------------------------------- 1 | const toDeg = (value: number) => (value * 180) / Math.PI; 2 | 3 | export default toDeg; 4 | -------------------------------------------------------------------------------- /src/toRad.ts: -------------------------------------------------------------------------------- 1 | const toRad = (value: number) => (value * Math.PI) / 180; 2 | 3 | export default toRad; 4 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "post-merge": "install-deps-postmerge", 4 | "pre-commit": "lint-staged" 5 | } 6 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: [manuelbieh] 4 | open_collective: geolib 5 | issuehunt: manuelbieh/geolib 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@werkzeugkiste', '@werkzeugkiste/eslint-config/typescript'], 3 | rules: { 4 | 'security/detect-object-injection': 0, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/isSexagesimal.ts: -------------------------------------------------------------------------------- 1 | import { sexagesimalPattern } from './constants'; 2 | 3 | const isSexagesimal = (value: any) => 4 | sexagesimalPattern.test(value.toString().trim()); 5 | 6 | export default isSexagesimal; 7 | -------------------------------------------------------------------------------- /src/toDeg.test.js: -------------------------------------------------------------------------------- 1 | import toDeg from './toDeg'; 2 | 3 | describe('toDeg', () => { 4 | it('converts a value to a degree', () => { 5 | expect(toDeg(35.238965)).toEqual(2019.0439689092252); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/toRad.test.js: -------------------------------------------------------------------------------- 1 | import toRad from './toRad'; 2 | 3 | describe('toRad', () => { 4 | it('converts a value to a radian', () => { 5 | expect(toRad(2019.0439689092252)).toEqual(35.238965); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "src/*.{js,ts}": [ 3 | "prettier --write", 4 | "yarn lint --fix", 5 | "yarn test --passWithNoTests", 6 | "git add" 7 | ], 8 | "src/*.ts": [ 9 | "yarn typecheck" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/env', 5 | { 6 | targets: '> 0.25%, not dead', 7 | }, 8 | ], 9 | '@babel/typescript', 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /src/robustAcos.ts: -------------------------------------------------------------------------------- 1 | const robustAcos = (value: number): number => { 2 | if (value > 1) { 3 | return 1; 4 | } 5 | if (value < -1) { 6 | return -1; 7 | } 8 | 9 | return value; 10 | }; 11 | 12 | export default robustAcos; 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 80 2 | singleQuote: true 3 | tabWidth: 4 4 | trailingComma: es5 5 | semi: true 6 | arrowParens: always 7 | quoteProps: consistent 8 | 9 | overrides: 10 | - files: "*.json" 11 | options: 12 | tabWidth: 2 13 | - files: "*.yml" 14 | options: 15 | tabWidth: 2 16 | -------------------------------------------------------------------------------- /src/isDecimal.ts: -------------------------------------------------------------------------------- 1 | // Checks if a value is in decimal format 2 | const isDecimal = (value: any) => { 3 | const checkedValue = value.toString().trim(); 4 | 5 | if (isNaN(parseFloat(checkedValue))) { 6 | return false; 7 | } 8 | 9 | return parseFloat(checkedValue) === Number(checkedValue); 10 | }; 11 | 12 | export default isDecimal; 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | max_line_length = 80 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.json] 17 | indent_size = 2 18 | 19 | [*.yml] 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /src/findNearest.ts: -------------------------------------------------------------------------------- 1 | import orderByDistance from './orderByDistance'; 2 | import { GeolibInputCoordinates } from './types'; 3 | 4 | // Finds the nearest coordinate to a reference coordinate 5 | const findNearest = ( 6 | point: GeolibInputCoordinates, 7 | coords: GeolibInputCoordinates[] 8 | ) => orderByDistance(point, coords)[0]; 9 | 10 | export default findNearest; 11 | -------------------------------------------------------------------------------- /src/convertSpeed.test.js: -------------------------------------------------------------------------------- 1 | import convertSpeed from './convertSpeed'; 2 | 3 | describe('convertSpeed', () => { 4 | it('converts the result of getSpeed() into different units (kmh, mph)', () => { 5 | expect(convertSpeed(29.86777777777778, 'kmh')).toEqual(107.524); 6 | expect(convertSpeed(29.86777777777778, 'mph')).toEqual( 7 | 66.8123160741271 8 | ); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/isDecimal.test.js: -------------------------------------------------------------------------------- 1 | import isDecimal from './isDecimal'; 2 | 3 | describe('isDecimal', () => { 4 | it('checks if a value is a decimal', () => { 5 | expect(isDecimal(2)).toBe(true); 6 | expect(isDecimal('xyz')).toBe(false); 7 | expect(isDecimal('2.0')).toBe(true); 8 | expect(isDecimal(' 2.0 ')).toBe(true); 9 | expect(isDecimal(' 1..0 ')).toBe(false); 10 | expect(isDecimal(Infinity)).toBe(true); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/isPointWithinRadius.ts: -------------------------------------------------------------------------------- 1 | import getDistance from './getDistance'; 2 | import { GeolibInputCoordinates } from './types'; 3 | 4 | // Checks if a point is inside of given radius 5 | const isPointWithinRadius = ( 6 | point: GeolibInputCoordinates, 7 | center: GeolibInputCoordinates, 8 | radius: number 9 | ) => { 10 | const accuracy = 0.01; 11 | return getDistance(point, center, accuracy) < radius; 12 | }; 13 | 14 | export default isPointWithinRadius; 15 | -------------------------------------------------------------------------------- /src/getRhumbLineBearing.test.js: -------------------------------------------------------------------------------- 1 | import getRhumbLineBearing from './getRhumbLineBearing'; 2 | 3 | describe('getRhumbLineBearing', () => { 4 | it('should return a bearing between two points', () => { 5 | expect( 6 | getRhumbLineBearing( 7 | { latitude: 39.778889, longitude: -104.9825 }, 8 | { latitude: 43.778889, longitude: -102.9825 } 9 | ) 10 | ).toEqual(20.438617005368314); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/isPointNearLine.ts: -------------------------------------------------------------------------------- 1 | import getDistanceFromLine from './getDistanceFromLine'; 2 | import { GeolibInputCoordinates } from './types'; 3 | 4 | // Check if a point lies within a given distance from a line created by two 5 | // other points 6 | const isPointNearLine = ( 7 | point: GeolibInputCoordinates, 8 | start: GeolibInputCoordinates, 9 | end: GeolibInputCoordinates, 10 | distance: number 11 | ) => getDistanceFromLine(point, start, end) < distance; 12 | 13 | export default isPointNearLine; 14 | -------------------------------------------------------------------------------- /src/convertArea.ts: -------------------------------------------------------------------------------- 1 | import { areaConversion } from './constants'; 2 | 3 | // This is a convenience function to easily convert distances in square meters to 4 | // any other common square measure (km2, sqft, ha, ...) 5 | const convertArea = (squareMeters: number, targetUnit: string = 'm') => { 6 | const factor = areaConversion[targetUnit]; 7 | if (factor) { 8 | return squareMeters * factor; 9 | } 10 | throw new Error('Invalid unit used for area conversion.'); 11 | }; 12 | 13 | export default convertArea; 14 | -------------------------------------------------------------------------------- /src/isPointNearLine.test.js: -------------------------------------------------------------------------------- 1 | import isPointNearLine from './isPointNearLine'; 2 | 3 | describe('isPointNearLine', () => { 4 | it('should get the shortest distance from a point to a line of two points', () => { 5 | expect( 6 | isPointNearLine( 7 | { latitude: 51.516, longitude: 7.456 }, 8 | { latitude: 51.512, longitude: 7.456 }, 9 | { latitude: 51.516, longitude: 7.459 }, 10 | 200 11 | ) 12 | ).toBe(true); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/convertDistance.ts: -------------------------------------------------------------------------------- 1 | import { distanceConversion } from './constants'; 2 | 3 | // This is a convenience function to easily convert distances in meters to 4 | // any other common distance unit (cm, m, km, mi, ft, ...) 5 | const convertDistance = (meters: number, targetUnit: string = 'm') => { 6 | const factor = distanceConversion[targetUnit]; 7 | if (factor) { 8 | return meters * factor; 9 | } 10 | throw new Error('Invalid unit used for distance conversion.'); 11 | }; 12 | 13 | export default convertDistance; 14 | -------------------------------------------------------------------------------- /src/isPointInLine.ts: -------------------------------------------------------------------------------- 1 | import getDistance from './getDistance'; 2 | import { GeolibInputCoordinates } from './types'; 3 | 4 | // Check if a point lies in line created by two other points 5 | const isPointInLine = ( 6 | point: GeolibInputCoordinates, 7 | lineStart: GeolibInputCoordinates, 8 | lineEnd: GeolibInputCoordinates 9 | ) => { 10 | return ( 11 | getDistance(lineStart, point) + getDistance(point, lineEnd) === 12 | getDistance(lineStart, lineEnd) 13 | ); 14 | }; 15 | 16 | export default isPointInLine; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es2018", "dom"], 5 | "allowJs": false, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": false, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "noEmit": true, 15 | "strictNullChecks": true 16 | }, 17 | "include": ["src"], 18 | "exclude": ["node_modules", "**/*.test.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /src/convertDistance.test.js: -------------------------------------------------------------------------------- 1 | import convertDistance from './convertDistance'; 2 | 3 | describe('convertDistance', () => { 4 | it('converts a numeric value into different distance units (cm, m, km, mi)', () => { 5 | expect(convertDistance(1000, 'km')).toEqual(1); 6 | expect(convertDistance(10, 'cm')).toEqual(1000); 7 | expect(convertDistance(1609.344, 'mi')).toEqual(1); 8 | }); 9 | 10 | it('should throw if an invalid unit was used', () => { 11 | expect(() => convertDistance(150, 'invalid')).toThrow(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/isSexagesimal.test.js: -------------------------------------------------------------------------------- 1 | import isSexagesimal from './isSexagesimal'; 2 | 3 | describe('isSexagesimal', () => { 4 | it('checks if a value is in sexagesimal representation', () => { 5 | expect(isSexagesimal("51° 31.52'")).toBe(true); 6 | expect(isSexagesimal('7° 28\' 01"')).toBe(true); 7 | expect(isSexagesimal("7° 1'")).toBe(true); 8 | expect(isSexagesimal("51° 31.52' N")).toBe(true); 9 | expect(isSexagesimal('N')).toBe(false); 10 | expect(isSexagesimal('12')).toBe(false); 11 | expect(isSexagesimal('51.32')).toBe(false); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/robustAcos.test.js: -------------------------------------------------------------------------------- 1 | import robustAcos from './robustAcos'; 2 | 3 | describe('robustAcos', () => { 4 | it('should return positive 1 if the value is greater than 1', () => { 5 | expect(robustAcos(1.00000000000000002)).toEqual(1); 6 | }); 7 | 8 | it('should return negative 1 if the value is greater than 1', () => { 9 | expect(robustAcos(-1.00000000000000002)).toEqual(-1); 10 | }); 11 | 12 | it('should return the actual value if it is within positive and negative 1', () => { 13 | expect(robustAcos(Math.acos(0.75))).toEqual(Math.acos(0.75)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/getRoughCompassDirection.ts: -------------------------------------------------------------------------------- 1 | // Receives an exact compass direction (like WNW) and spits out a very rough 2 | // and overly simplified direction (N|E|S|W). Use with caution! 3 | const getRoughCompassDirection = (exact: string) => { 4 | if (/^(NNE|NE|NNW|N)$/.test(exact)) { 5 | return 'N'; 6 | } 7 | 8 | if (/^(ENE|E|ESE|SE)$/.test(exact)) { 9 | return 'E'; 10 | } 11 | 12 | if (/^(SSE|S|SSW|SW)$/.test(exact)) { 13 | return 'S'; 14 | } 15 | 16 | if (/^(WSW|W|WNW|NW)$/.test(exact)) { 17 | return 'W'; 18 | } 19 | }; 20 | 21 | export default getRoughCompassDirection; 22 | -------------------------------------------------------------------------------- /src/getSpeed.ts: -------------------------------------------------------------------------------- 1 | import getDistance from './getDistance'; 2 | import { GeolibInputCoordinatesWithTime, GeolibDistanceFn } from './types'; 3 | 4 | // Calculates the speed between two points within a given time span. 5 | const getSpeed = ( 6 | start: GeolibInputCoordinatesWithTime, 7 | end: GeolibInputCoordinatesWithTime, 8 | distanceFn: GeolibDistanceFn = getDistance 9 | ) => { 10 | const distance = distanceFn(start, end); 11 | const time = Number(end.time) - Number(start.time); 12 | const metersPerSecond = (distance / time) * 1000; 13 | return metersPerSecond; 14 | }; 15 | 16 | export default getSpeed; 17 | -------------------------------------------------------------------------------- /src/getLatitude.ts: -------------------------------------------------------------------------------- 1 | import { GeolibInputCoordinates, LatitudeKeys } from './types'; 2 | import { latitudeKeys } from './constants'; 3 | import getCoordinateKey from './getCoordinateKey'; 4 | import toDecimal from './toDecimal'; 5 | 6 | const getLatitude = (point: GeolibInputCoordinates, raw?: boolean) => { 7 | const latKey = getCoordinateKey(point, latitudeKeys); 8 | 9 | if (typeof latKey === 'undefined' || latKey === null) { 10 | return; 11 | } 12 | 13 | const value = point[latKey as keyof LatitudeKeys]; 14 | 15 | return raw === true ? value : toDecimal(value); 16 | }; 17 | 18 | export default getLatitude; 19 | -------------------------------------------------------------------------------- /src/getLongitude.ts: -------------------------------------------------------------------------------- 1 | import { GeolibInputCoordinates, LongitudeKeys } from './types'; 2 | import { longitudeKeys } from './constants'; 3 | import getCoordinateKey from './getCoordinateKey'; 4 | import toDecimal from './toDecimal'; 5 | 6 | const getLongitude = (point: GeolibInputCoordinates, raw?: boolean) => { 7 | const latKey = getCoordinateKey(point, longitudeKeys); 8 | 9 | if (typeof latKey === 'undefined' || latKey === null) { 10 | return; 11 | } 12 | 13 | const value = point[latKey as keyof LongitudeKeys]; 14 | 15 | return raw === true ? value : toDecimal(value); 16 | }; 17 | 18 | export default getLongitude; 19 | -------------------------------------------------------------------------------- /src/convertSpeed.ts: -------------------------------------------------------------------------------- 1 | import { distanceConversion, timeConversion } from './constants'; 2 | 3 | // This is a convenience function to easily convert the result of getSpeed() 4 | // into miles per hour or kilometers per hour. 5 | const convertSpeed = (metersPerSecond: number, targetUnit = 'kmh') => { 6 | switch (targetUnit) { 7 | case 'kmh': 8 | return metersPerSecond * timeConversion.h * distanceConversion.km; 9 | case 'mph': 10 | return metersPerSecond * timeConversion.h * distanceConversion.mi; 11 | default: 12 | return metersPerSecond; 13 | } 14 | }; 15 | 16 | export default convertSpeed; 17 | -------------------------------------------------------------------------------- /src/isValidLatitude.ts: -------------------------------------------------------------------------------- 1 | import isDecimal from './isDecimal'; 2 | import isSexagesimal from './isSexagesimal'; 3 | import sexagesimalToDecimal from './sexagesimalToDecimal'; 4 | import { MAXLAT, MINLAT } from './constants'; 5 | 6 | const isValidLatitude = (value: any): boolean => { 7 | if (isDecimal(value)) { 8 | if (parseFloat(value) > MAXLAT || value < MINLAT) { 9 | return false; 10 | } 11 | 12 | return true; 13 | } 14 | 15 | if (isSexagesimal(value)) { 16 | return isValidLatitude(sexagesimalToDecimal(value)); 17 | } 18 | 19 | return false; 20 | }; 21 | 22 | export default isValidLatitude; 23 | -------------------------------------------------------------------------------- /src/isValidLongitude.ts: -------------------------------------------------------------------------------- 1 | import isDecimal from './isDecimal'; 2 | import isSexagesimal from './isSexagesimal'; 3 | import sexagesimalToDecimal from './sexagesimalToDecimal'; 4 | import { MAXLON, MINLON } from './constants'; 5 | 6 | const isValidLongitude = (value: any): boolean => { 7 | if (isDecimal(value)) { 8 | if (parseFloat(value) > MAXLON || value < MINLON) { 9 | return false; 10 | } 11 | 12 | return true; 13 | } 14 | 15 | if (isSexagesimal(value)) { 16 | return isValidLongitude(sexagesimalToDecimal(value)); 17 | } 18 | 19 | return false; 20 | }; 21 | 22 | export default isValidLongitude; 23 | -------------------------------------------------------------------------------- /src/wktToPolygon.ts: -------------------------------------------------------------------------------- 1 | // Converts a wkt text to polygon 2 | const wktToPolygon = (wkt: string) => { 3 | if (!wkt.startsWith('POLYGON')) { 4 | throw new Error('Invalid wkt.'); 5 | } 6 | const coordsText = wkt 7 | .slice(wkt.indexOf('(') + 2, wkt.indexOf(')')) 8 | .split(', '); 9 | 10 | const polygon = coordsText.map((coordText) => { 11 | const [longitude, latitude] = coordText.split(' '); 12 | return { 13 | longitude: parseFloat(longitude), 14 | latitude: parseFloat(latitude), 15 | }; 16 | }); 17 | 18 | return polygon; 19 | }; 20 | 21 | export default wktToPolygon; 22 | -------------------------------------------------------------------------------- /src/findNearest.test.js: -------------------------------------------------------------------------------- 1 | import findNearest from './findNearest'; 2 | 3 | describe('findNearest', () => { 4 | it('returns first result from orderByDistance', () => { 5 | const point = { latitude: 51.516241842, longitude: 7.456494328 }; 6 | const nearest = { latitude: 51.515400598, longitude: 7.45518541 }; 7 | const coords = [ 8 | { latitude: 51.513357512, longitude: 7.45574331 }, 9 | nearest, 10 | { latitude: 51.516722545, longitude: 7.459863183 }, 11 | { latitude: 51.517443592, longitude: 7.463232037 }, 12 | ]; 13 | 14 | expect(findNearest(point, coords)).toEqual(nearest); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/isPointInLine.test.js: -------------------------------------------------------------------------------- 1 | import isPointInLine from './isPointInLine'; 2 | 3 | describe('isPointInLine', () => { 4 | it('should return true if a point is in a given line', () => { 5 | const point1 = { latitude: 0.5, longitude: 0 }; 6 | const point2 = { latitude: 0, longitude: 10 }; 7 | const point3 = { latitude: 0, longitude: 15.5 }; 8 | const start = { latitude: 0, longitude: 0 }; 9 | const end = { latitude: 0, longitude: 15 }; 10 | 11 | expect(isPointInLine(point1, start, end)).toBe(false); 12 | expect(isPointInLine(point2, start, end)).toBe(true); 13 | expect(isPointInLine(point3, start, end)).toBe(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/getGreatCircleBearing.test.js: -------------------------------------------------------------------------------- 1 | import getGreatCircleBearing from './getGreatCircleBearing'; 2 | 3 | describe('getGreatCircleBearing', () => { 4 | it('should return a bearing between two points', () => { 5 | expect( 6 | getGreatCircleBearing( 7 | { latitude: 39.778889, longitude: -104.9825 }, 8 | { latitude: 43.778889, longitude: -102.9825 } 9 | ) 10 | ).toEqual(19.787524850709417); 11 | expect( 12 | getGreatCircleBearing( 13 | { latitude: 51.5104, longitude: 7.3256 }, 14 | { latitude: 43.778889, longitude: 7.491 } 15 | ) 16 | ).toEqual(179.11237166124715); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/orderByDistance.ts: -------------------------------------------------------------------------------- 1 | import getDistance from './getDistance'; 2 | import { GeolibInputCoordinates } from './types'; 3 | 4 | type DistanceFn = ( 5 | point: GeolibInputCoordinates, 6 | dest: GeolibInputCoordinates 7 | ) => number; 8 | 9 | // Sorts an array of coords by distance from a reference coordinate 10 | const orderByDistance = ( 11 | point: GeolibInputCoordinates, 12 | coords: GeolibInputCoordinates[], 13 | distanceFn: DistanceFn = getDistance 14 | ) => { 15 | distanceFn = typeof distanceFn === 'function' ? distanceFn : getDistance; 16 | 17 | return coords 18 | .slice() 19 | .sort((a, b) => distanceFn(point, a) - distanceFn(point, b)); 20 | }; 21 | 22 | export default orderByDistance; 23 | -------------------------------------------------------------------------------- /src/isValidCoordinate.test.js: -------------------------------------------------------------------------------- 1 | import isValidCoordinate from './isValidCoordinate'; 2 | 3 | describe('isValidCoordinate', () => { 4 | it('checks if a coordinate has at least a latitude and a longitude', () => { 5 | expect(isValidCoordinate({ lat: 1, lng: 1 })).toBe(true); 6 | expect(isValidCoordinate({ lat: 1, lon: 1 })).toBe(true); 7 | expect(isValidCoordinate({ lat: 1 })).toBe(false); 8 | expect(isValidCoordinate({ lat: 1 })).toBe(false); 9 | }); 10 | it('checks if values for latitude and longitude are valid', () => { 11 | expect( 12 | isValidCoordinate({ lat: "51° 31.52'", lng: '7° 28\' 01"' }) 13 | ).toBe(true); 14 | expect(isValidCoordinate({ lat: 'invalid', lng: 1 })).toBe(false); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/wktToPolygon.test.js: -------------------------------------------------------------------------------- 1 | import wktToPolygon from './wktToPolygon'; 2 | 3 | describe('wktToPolygon', () => { 4 | it('converts a WKT to Polygon', () => { 5 | const wkt = 'POLYGON ((30 10.54321, 40 40, 20 40, 10 20, 30 10))'; 6 | expect(wktToPolygon(wkt)).toEqual([ 7 | { latitude: 10.54321, longitude: 30 }, 8 | { latitude: 40, longitude: 40 }, 9 | { latitude: 40, longitude: 20 }, 10 | { latitude: 20, longitude: 10 }, 11 | { latitude: 10, longitude: 30 }, 12 | ]); 13 | }); 14 | 15 | it('throw error when is not a POLYGON', () => { 16 | const wkt = 'MULTIPOLYGON (((3 2, 45 4, 3 2)), ((15 5, 4 1, 15 5)))'; 17 | expect(() => wktToPolygon(wkt)).toThrowError('Invalid wkt.'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/sexagesimalToDecimal.ts: -------------------------------------------------------------------------------- 1 | import { sexagesimalPattern } from './constants'; 2 | 3 | // Converts a sexagesimal coordinate to decimal format 4 | const sexagesimalToDecimal = (sexagesimal: any) => { 5 | const data = new RegExp(sexagesimalPattern).exec( 6 | sexagesimal.toString().trim() 7 | ); 8 | 9 | if (typeof data === 'undefined' || data === null) { 10 | throw new Error('Given value is not in sexagesimal format'); 11 | } 12 | 13 | const min = Number(data[2]) / 60 || 0; 14 | const sec = Number(data[4]) / 3600 || 0; 15 | 16 | const decimal = parseFloat(data[1]) + min + sec; 17 | 18 | // Southern and western coordinates must be negative decimals 19 | return ['S', 'W'].includes(data[7]) ? -decimal : decimal; 20 | }; 21 | 22 | export default sexagesimalToDecimal; 23 | -------------------------------------------------------------------------------- /src/getCoordinateKey.ts: -------------------------------------------------------------------------------- 1 | import { GeolibInputCoordinates } from './types'; 2 | 3 | const getCoordinateKey = ( 4 | point: GeolibInputCoordinates, 5 | keysToLookup: Keys[] 6 | ) => { 7 | return keysToLookup.reduce((foundKey: Keys | undefined, key: any): 8 | | Keys 9 | | undefined => { 10 | if (typeof point === 'undefined' || point === null) { 11 | throw new Error(`'${point}' is no valid coordinate.`); 12 | } 13 | if ( 14 | Object.prototype.hasOwnProperty.call(point, key) && 15 | typeof key !== 'undefined' && 16 | typeof foundKey === 'undefined' 17 | ) { 18 | foundKey = key; 19 | return key; 20 | } 21 | 22 | return foundKey; 23 | }, undefined); 24 | }; 25 | 26 | export default getCoordinateKey; 27 | -------------------------------------------------------------------------------- /webpack.config.js/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'production', // this will trigger some webpack default stuffs for dev 3 | entry: `${__dirname}/../src/index.ts`, 4 | output: { 5 | filename: 'index.js', 6 | path: `${__dirname}/../lib`, 7 | libraryTarget: 'umd', 8 | library: 'geolib', 9 | // this is a weird hack to make the umd build work in node 10 | // https://github.com/webpack/webpack/issues/6525#issuecomment-417580843 11 | globalObject: 'typeof self !== "undefined" ? self : this', 12 | }, 13 | resolve: { 14 | extensions: ['.js', '.ts'], 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.(ts|js)$/, 20 | loader: require.resolve('babel-loader'), 21 | }, 22 | ], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/getLatitude.test.js: -------------------------------------------------------------------------------- 1 | import getLatitude from './getLatitude'; 2 | 3 | describe('getLatitude', () => { 4 | it('gets the latitude for a point', () => { 5 | expect(getLatitude({ lat: 1 })).toEqual(1); 6 | }); 7 | 8 | it('converts the latitude for a point to decimal', () => { 9 | expect(getLatitude({ lat: "71° 0'" })).toEqual(71); 10 | }); 11 | 12 | it('gets the latitude from a GeoJSON array', () => { 13 | expect(getLatitude([1, 2])).toEqual(2); 14 | }); 15 | 16 | it('does not convert to decimal if second parameter is set to true', () => { 17 | expect(getLatitude({ lat: "71° 0'" }, true)).toEqual("71° 0'"); 18 | }); 19 | 20 | it('gets the latitude from a GeoJSON array without conversion', () => { 21 | expect(getLatitude(["71° 0'", "71° 0'"], true)).toEqual("71° 0'"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/getLongitude.test.js: -------------------------------------------------------------------------------- 1 | import getLongitude from './getLongitude'; 2 | 3 | describe('getLongitude', () => { 4 | it('gets the longitude for a point', () => { 5 | expect(getLongitude({ lng: 1 })).toEqual(1); 6 | }); 7 | 8 | it('converts the longitude for a point to decimal', () => { 9 | expect(getLongitude({ lng: "71° 0'" })).toEqual(71); 10 | }); 11 | 12 | it('gets the longitude from a GeoJSON array', () => { 13 | expect(getLongitude([1, 2])).toEqual(1); 14 | }); 15 | 16 | it('does not convert to decimal if second parameter is set to true', () => { 17 | expect(getLongitude({ lng: "71° 0'" }, true)).toEqual("71° 0'"); 18 | }); 19 | 20 | it('gets the longitude from a GeoJSON array without conversion', () => { 21 | expect(getLongitude(["71° 0'", "71° 0'"], true)).toEqual("71° 0'"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/convertArea.test.js: -------------------------------------------------------------------------------- 1 | import convertArea from './convertArea'; 2 | 3 | describe('convertArea', () => { 4 | it('converts a numeric value into different area units (km2, ha, a, ...)', () => { 5 | expect(convertArea(50000, 'km2')).toEqual(0.049999999999999996); 6 | expect(convertArea(1000, 'ha')).toEqual(0.1); 7 | expect(convertArea(1000, 'a')).toEqual(10); 8 | expect(convertArea(1000, 'ft2')).toEqual(10763.911); 9 | expect(convertArea(1000, 'yd2')).toEqual(1195.99); 10 | expect(convertArea(1000, 'in2')).toEqual(1550003.0999999999); 11 | }); 12 | 13 | it('should work with aliased units', () => { 14 | expect(convertArea(1000, 'sqft')).toEqual(convertArea(1000, 'ft2')); 15 | }); 16 | 17 | it('should throw if an invalid unit was used', () => { 18 | expect(() => convertArea(150, 'invalid')).toThrow(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/getPathLength.ts: -------------------------------------------------------------------------------- 1 | import getDistance from './getDistance'; 2 | import { GeolibInputCoordinates } from './types'; 3 | 4 | type DistanceFn = ( 5 | point: GeolibInputCoordinates, 6 | dest: GeolibInputCoordinates 7 | ) => number; 8 | 9 | type Accumulated = { 10 | last: GeolibInputCoordinates | null; 11 | distance: number; 12 | }; 13 | 14 | // Calculates the length of a given path 15 | const getPathLength = ( 16 | points: GeolibInputCoordinates[], 17 | distanceFn: DistanceFn = getDistance 18 | ) => { 19 | return points.reduce( 20 | (acc: Accumulated, point: GeolibInputCoordinates) => { 21 | if (typeof acc === 'object' && acc.last !== null) { 22 | acc.distance += distanceFn(point, acc.last); 23 | } 24 | acc.last = point; 25 | return acc; 26 | }, 27 | { last: null, distance: 0 } 28 | ).distance; 29 | }; 30 | 31 | export default getPathLength; 32 | -------------------------------------------------------------------------------- /src/getSpeed.test.js: -------------------------------------------------------------------------------- 1 | import getSpeed from './getSpeed'; 2 | 3 | describe('getSpeed', () => { 4 | it('gets the average speed between two given points in meters per second', () => { 5 | expect( 6 | getSpeed( 7 | { lat: 51.567294, lng: 7.38896, time: 1360231200880 }, 8 | { lat: 52.54944, lng: 13.468509, time: 1360245600880 } 9 | ) 10 | ).toEqual(29.86777777777778); 11 | }); 12 | 13 | it('uses an alternative getDistance function if one is passed', () => { 14 | const from = { lat: 51.567294, lng: 7.38896, time: 1360231200880 }; 15 | const to = { lat: 52.54944, lng: 13.468509, time: 1360245600880 }; 16 | const getDistance = jest.fn(() => { 17 | return 100000; 18 | }); 19 | 20 | expect(getSpeed(from, to, getDistance)).toEqual(6.944444444444444); 21 | expect(getDistance).toHaveBeenCalledWith(from, to); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/getCoordinateKey.test.js: -------------------------------------------------------------------------------- 1 | import getCoordinateKey from './getCoordinateKey'; 2 | 3 | describe('getCoordinateKey', () => { 4 | it('should get the correct key out of a list of keys', () => { 5 | expect( 6 | getCoordinateKey({ lat: 1, lng: 1 }, ['latitude', 'lat']) 7 | ).toEqual('lat'); 8 | }); 9 | 10 | it('should return the first match from the lookup array only', () => { 11 | expect( 12 | getCoordinateKey({ lat: 1, latitude: 1 }, ['latitude', 'lat']) 13 | ).toEqual('latitude'); 14 | }); 15 | 16 | it('should return an index of a GeoJSON array', () => { 17 | expect(getCoordinateKey([1, 2], ['latitude', 'lat', 0])).toEqual(0); 18 | }); 19 | 20 | it('should throw when an invalid coordinate is passed', () => { 21 | expect(() => getCoordinateKey(null, ['latitude', 'lat', 0])).toThrow(); 22 | expect(() => 23 | getCoordinateKey(undefined, ['latitude', 'lat', 0]) 24 | ).toThrow(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/getCenterOfBounds.ts: -------------------------------------------------------------------------------- 1 | import getBounds from './getBounds'; 2 | import { GeolibInputCoordinates } from './types'; 3 | 4 | /* 5 | * Calculates the center of the bounds of geo coordinates. 6 | * 7 | * On polygons like political borders (eg. states) this may gives a closer 8 | * result to human expectation, than `getCenter`, because that function can be 9 | * disturbed by uneven distribution of point in different sides. Imagine the 10 | * US state Oklahoma: `getCenter` on that gives a southern point, because the 11 | * southern border contains a lot more nodes, than the others. 12 | */ 13 | const getCenterOfBounds = (coords: GeolibInputCoordinates[]) => { 14 | const bounds = getBounds(coords); 15 | const latitude = bounds.minLat + (bounds.maxLat - bounds.minLat) / 2; 16 | const longitude = bounds.minLng + (bounds.maxLng - bounds.minLng) / 2; 17 | return { 18 | latitude: parseFloat(latitude.toFixed(6)), 19 | longitude: parseFloat(longitude.toFixed(6)), 20 | }; 21 | }; 22 | 23 | export default getCenterOfBounds; 24 | -------------------------------------------------------------------------------- /src/decimalToSexagesimal.ts: -------------------------------------------------------------------------------- 1 | // trying to sanitize floating point fuckups here to a certain extent 2 | const imprecise = (number: number, decimals: number = 4) => { 3 | const factor = Math.pow(10, decimals); 4 | return Math.round(number * factor) / factor; 5 | }; 6 | 7 | // Converts a decimal coordinate value to sexagesimal format 8 | const decimal2sexagesimalNext = (decimal: number) => { 9 | const [pre, post] = decimal.toString().split('.'); 10 | 11 | const deg = Math.abs(Number(pre)); 12 | const min0 = Number('0.' + (post || 0)) * 60; 13 | const sec0 = min0.toString().split('.'); 14 | 15 | const min = Math.floor(min0); 16 | const sec = imprecise(Number('0.' + (sec0[1] || 0)) * 60).toString(); 17 | 18 | const [secPreDec, secDec = '0'] = sec.split('.'); 19 | 20 | return ( 21 | deg + 22 | '° ' + 23 | min.toString().padStart(2, '0') + 24 | "' " + 25 | secPreDec.padStart(2, '0') + 26 | '.' + 27 | secDec.padEnd(1, '0') + 28 | '"' 29 | ); 30 | }; 31 | 32 | export default decimal2sexagesimalNext; 33 | -------------------------------------------------------------------------------- /src/getBounds.ts: -------------------------------------------------------------------------------- 1 | import getLatitude from './getLatitude'; 2 | import getLongitude from './getLongitude'; 3 | import { GeolibBounds, GeolibInputCoordinates } from './types'; 4 | 5 | // Gets the max and min, latitude and longitude 6 | const getBounds = (points: GeolibInputCoordinates[]): GeolibBounds => { 7 | if (Array.isArray(points) === false || points.length === 0) { 8 | throw new Error('No points were given.'); 9 | } 10 | 11 | return points.reduce( 12 | (stats, point) => { 13 | const latitude = getLatitude(point); 14 | const longitude = getLongitude(point); 15 | return { 16 | maxLat: Math.max(latitude, stats.maxLat), 17 | minLat: Math.min(latitude, stats.minLat), 18 | maxLng: Math.max(longitude, stats.maxLng), 19 | minLng: Math.min(longitude, stats.minLng), 20 | }; 21 | }, 22 | { 23 | maxLat: -Infinity, 24 | minLat: Infinity, 25 | maxLng: -Infinity, 26 | minLng: Infinity, 27 | } 28 | ); 29 | }; 30 | 31 | export default getBounds; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Manuel Bieh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/isPointWithinRadius.test.js: -------------------------------------------------------------------------------- 1 | import isPointWithinRadius from './isPointWithinRadius'; 2 | 3 | describe('isPointWithinRadius', () => { 4 | it('should return true if a given point is within a certain radius', () => { 5 | expect( 6 | isPointWithinRadius( 7 | { latitude: 51.567, longitude: 7.456 }, 8 | { latitude: 51.789, longitude: 7.678 }, 9 | 30000 10 | ) 11 | ).toBe(true); 12 | }); 13 | 14 | it('should return false if a given point is not within a certain radius', () => { 15 | expect( 16 | isPointWithinRadius( 17 | { latitude: 51.567, longitude: 7.456 }, 18 | { latitude: 51.789, longitude: 7.678 }, 19 | 20000 20 | ) 21 | ).toBe(false); 22 | }); 23 | 24 | it('should return true if a given point is within a certain radius with high accuracy', () => { 25 | expect( 26 | isPointWithinRadius( 27 | { latitude: 42.53098, longitude: -71.28029 }, 28 | { latitude: 42.53101, longitude: -71.2803986 }, 29 | 10 30 | ) 31 | ).toBe(true); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/getGreatCircleBearing.ts: -------------------------------------------------------------------------------- 1 | import getLatitude from './getLatitude'; 2 | import getLongitude from './getLongitude'; 3 | import toRad from './toRad'; 4 | import toDeg from './toDeg'; 5 | import { GeolibInputCoordinates } from './types'; 6 | 7 | /** 8 | * Gets great circle bearing of two points. See description of getRhumbLineBearing for more information 9 | */ 10 | const getGreatCircleBearing = ( 11 | origin: GeolibInputCoordinates, 12 | dest: GeolibInputCoordinates 13 | ) => { 14 | const destLat = getLatitude(dest); 15 | const detLon = getLongitude(dest); 16 | const originLat = getLatitude(origin); 17 | const originLon = getLongitude(origin); 18 | 19 | const bearing = 20 | (toDeg( 21 | Math.atan2( 22 | Math.sin(toRad(detLon) - toRad(originLon)) * 23 | Math.cos(toRad(destLat)), 24 | Math.cos(toRad(originLat)) * Math.sin(toRad(destLat)) - 25 | Math.sin(toRad(originLat)) * 26 | Math.cos(toRad(destLat)) * 27 | Math.cos(toRad(detLon) - toRad(originLon)) 28 | ) 29 | ) + 30 | 360) % 31 | 360; 32 | 33 | return bearing; 34 | }; 35 | 36 | export default getGreatCircleBearing; 37 | -------------------------------------------------------------------------------- /src/getCoordinateKeys.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GeolibInputCoordinates, 3 | LatitudeKeys, 4 | LongitudeKeys, 5 | AltitudeKeys, 6 | } from './types'; 7 | 8 | import { longitudeKeys, latitudeKeys, altitudeKeys } from './constants'; 9 | import getCoordinateKey from './getCoordinateKey'; 10 | 11 | // TODO: add second parameter that can be passed as keysToLookup to getCoordinateKey 12 | // e.g. { latitude: ['lat', 'latitude'], longitude: ['lon', 'longitude']} 13 | const getCoordinateKeys = ( 14 | point: GeolibInputCoordinates, 15 | keysToLookup = { 16 | longitude: longitudeKeys, 17 | latitude: latitudeKeys, 18 | altitude: altitudeKeys, 19 | } 20 | ) => { 21 | const longitude: LongitudeKeys | undefined = getCoordinateKey( 22 | point, 23 | keysToLookup.longitude 24 | ); 25 | 26 | const latitude: LatitudeKeys | undefined = getCoordinateKey( 27 | point, 28 | keysToLookup.latitude 29 | ); 30 | 31 | const altitude: AltitudeKeys | undefined = getCoordinateKey( 32 | point, 33 | keysToLookup.altitude 34 | ); 35 | 36 | return { 37 | latitude, 38 | longitude, 39 | ...(altitude ? { altitude } : {}), 40 | }; 41 | }; 42 | 43 | export default getCoordinateKeys; 44 | -------------------------------------------------------------------------------- /src/getDistance.ts: -------------------------------------------------------------------------------- 1 | import getLatitude from './getLatitude'; 2 | import getLongitude from './getLongitude'; 3 | import toRad from './toRad'; 4 | import robustAcos from './robustAcos'; 5 | import { earthRadius } from './constants'; 6 | import { GeolibInputCoordinates } from './types'; 7 | 8 | // Calculates the distance between two points. 9 | // This method is simple but also more inaccurate 10 | const getDistance = ( 11 | from: GeolibInputCoordinates, 12 | to: GeolibInputCoordinates, 13 | accuracy: number = 1 14 | ) => { 15 | accuracy = 16 | typeof accuracy !== 'undefined' && !isNaN(accuracy) ? accuracy : 1; 17 | 18 | const fromLat = getLatitude(from); 19 | const fromLon = getLongitude(from); 20 | const toLat = getLatitude(to); 21 | const toLon = getLongitude(to); 22 | 23 | const distance = 24 | Math.acos( 25 | robustAcos( 26 | Math.sin(toRad(toLat)) * Math.sin(toRad(fromLat)) + 27 | Math.cos(toRad(toLat)) * 28 | Math.cos(toRad(fromLat)) * 29 | Math.cos(toRad(fromLon) - toRad(toLon)) 30 | ) 31 | ) * earthRadius; 32 | 33 | return Math.round(distance / accuracy) * accuracy; 34 | }; 35 | 36 | export default getDistance; 37 | -------------------------------------------------------------------------------- /src/isPointInPolygon.ts: -------------------------------------------------------------------------------- 1 | import getLatitude from './getLatitude'; 2 | import getLongitude from './getLongitude'; 3 | import { GeolibInputCoordinates } from './types'; 4 | 5 | // Checks whether a point is inside of a polygon or not. 6 | // Polygon must be in correct order! 7 | const isPointInPolygon = ( 8 | point: GeolibInputCoordinates, 9 | polygon: GeolibInputCoordinates[] 10 | ) => { 11 | let isInside = false; 12 | const totalPolys = polygon.length; 13 | for (let i = -1, j = totalPolys - 1; ++i < totalPolys; j = i) { 14 | if ( 15 | ((getLongitude(polygon[i]) <= getLongitude(point) && 16 | getLongitude(point) < getLongitude(polygon[j])) || 17 | (getLongitude(polygon[j]) <= getLongitude(point) && 18 | getLongitude(point) < getLongitude(polygon[i]))) && 19 | getLatitude(point) < 20 | ((getLatitude(polygon[j]) - getLatitude(polygon[i])) * 21 | (getLongitude(point) - getLongitude(polygon[i]))) / 22 | (getLongitude(polygon[j]) - getLongitude(polygon[i])) + 23 | getLatitude(polygon[i]) 24 | ) { 25 | isInside = !isInside; 26 | } 27 | } 28 | 29 | return isInside; 30 | }; 31 | 32 | export default isPointInPolygon; 33 | -------------------------------------------------------------------------------- /src/getCenter.ts: -------------------------------------------------------------------------------- 1 | import getLatitude from './getLatitude'; 2 | import getLongitude from './getLongitude'; 3 | import toRad from './toRad'; 4 | import toDeg from './toDeg'; 5 | import { GeolibInputCoordinates } from './types'; 6 | 7 | // Calculates the center of a collection of points 8 | const getCenter = (points: GeolibInputCoordinates[]) => { 9 | if (Array.isArray(points) === false || points.length === 0) { 10 | return false; 11 | } 12 | 13 | const numberOfPoints = points.length; 14 | 15 | const sum = points.reduce( 16 | (acc, point) => { 17 | const pointLat = toRad(getLatitude(point)); 18 | const pointLon = toRad(getLongitude(point)); 19 | 20 | return { 21 | X: acc.X + Math.cos(pointLat) * Math.cos(pointLon), 22 | Y: acc.Y + Math.cos(pointLat) * Math.sin(pointLon), 23 | Z: acc.Z + Math.sin(pointLat), 24 | }; 25 | }, 26 | { X: 0, Y: 0, Z: 0 } 27 | ); 28 | 29 | const X = sum.X / numberOfPoints; 30 | const Y = sum.Y / numberOfPoints; 31 | const Z = sum.Z / numberOfPoints; 32 | 33 | return { 34 | longitude: toDeg(Math.atan2(Y, X)), 35 | latitude: toDeg(Math.atan2(Z, Math.sqrt(X * X + Y * Y))), 36 | }; 37 | }; 38 | 39 | export default getCenter; 40 | -------------------------------------------------------------------------------- /src/isValidCoordinate.ts: -------------------------------------------------------------------------------- 1 | import { GeolibInputCoordinates, LongitudeKeys, LatitudeKeys } from './types'; 2 | import getCoordinateKeys from './getCoordinateKeys'; 3 | import isValidLatitude from './isValidLatitude'; 4 | import isValidLongitude from './isValidLongitude'; 5 | 6 | // Checks if a value contains a valid lat/lon object. 7 | // A coordinate is considered valid if it contains at least a latitude 8 | // and a longitude and both are either in decimals or sexagesimal format 9 | const isValidCoordinate = (point: GeolibInputCoordinates) => { 10 | const { latitude, longitude } = getCoordinateKeys(point); 11 | 12 | if (Array.isArray(point) && point.length >= 2) { 13 | return isValidLongitude(point[0]) && isValidLatitude(point[1]); 14 | } 15 | 16 | if (typeof latitude === 'undefined' || typeof longitude === 'undefined') { 17 | return false; 18 | } 19 | 20 | const lon: any = point[longitude as keyof LongitudeKeys]; 21 | const lat: any = point[latitude as keyof LatitudeKeys]; 22 | 23 | if (typeof lat === 'undefined' || typeof lon === 'undefined') { 24 | return false; 25 | } 26 | 27 | if (isValidLatitude(lat) === false || isValidLongitude(lon) === false) { 28 | return false; 29 | } 30 | 31 | return true; 32 | }; 33 | 34 | export default isValidCoordinate; 35 | -------------------------------------------------------------------------------- /src/getAreaOfPolygon.ts: -------------------------------------------------------------------------------- 1 | import toRad from './toRad'; 2 | import getLatitude from './getLatitude'; 3 | import getLongitude from './getLongitude'; 4 | import { earthRadius } from './constants'; 5 | import { GeolibInputCoordinates } from './types'; 6 | 7 | // Calculates the surface area of a polygon. 8 | const getAreaOfPolygon = (points: GeolibInputCoordinates[]) => { 9 | let area = 0; 10 | 11 | if (points.length > 2) { 12 | let lowerIndex; 13 | let middleIndex; 14 | let upperIndex; 15 | 16 | for (let i = 0; i < points.length; i++) { 17 | if (i === points.length - 2) { 18 | lowerIndex = points.length - 2; 19 | middleIndex = points.length - 1; 20 | upperIndex = 0; 21 | } else if (i === points.length - 1) { 22 | lowerIndex = points.length - 1; 23 | middleIndex = 0; 24 | upperIndex = 1; 25 | } else { 26 | lowerIndex = i; 27 | middleIndex = i + 1; 28 | upperIndex = i + 2; 29 | } 30 | 31 | const p1lon = getLongitude(points[lowerIndex]); 32 | const p2lat = getLatitude(points[middleIndex]); 33 | const p3lon = getLongitude(points[upperIndex]); 34 | 35 | area += (toRad(p3lon) - toRad(p1lon)) * Math.sin(toRad(p2lat)); 36 | } 37 | 38 | area = (area * earthRadius * earthRadius) / 2; 39 | } 40 | 41 | return Math.abs(area); 42 | }; 43 | 44 | export default getAreaOfPolygon; 45 | -------------------------------------------------------------------------------- /src/getPathLength.test.js: -------------------------------------------------------------------------------- 1 | import getPathLength from './getPathLength'; 2 | 3 | const polygon = [ 4 | { latitude: 51.513357512, longitude: 7.45574331 }, 5 | { latitude: 51.515400598, longitude: 7.45518541 }, 6 | { latitude: 51.516241842, longitude: 7.456494328 }, 7 | { latitude: 51.516722545, longitude: 7.459863183 }, 8 | { latitude: 51.517443592, longitude: 7.463232037 }, 9 | { latitude: 51.5177507, longitude: 7.464755532 }, 10 | { latitude: 51.517657233, longitude: 7.466622349 }, 11 | { latitude: 51.51722995, longitude: 7.468317505 }, 12 | { latitude: 51.516816015, longitude: 7.47011995 }, 13 | { latitude: 51.516308606, longitude: 7.471793648 }, 14 | { latitude: 51.515974782, longitude: 7.472437378 }, 15 | { latitude: 51.515413951, longitude: 7.472845074 }, 16 | { latitude: 51.514559338, longitude: 7.472909447 }, 17 | { latitude: 51.512195717, longitude: 7.472651955 }, 18 | { latitude: 51.511127373, longitude: 7.47140741 }, 19 | { latitude: 51.51029939, longitude: 7.469948288 }, 20 | { latitude: 51.509831973, longitude: 7.468446251 }, 21 | { latitude: 51.509978876, longitude: 7.462481019 }, 22 | { latitude: 51.510913701, longitude: 7.460678574 }, 23 | { latitude: 51.511594777, longitude: 7.459434029 }, 24 | { latitude: 51.512396029, longitude: 7.457695958 }, 25 | { latitude: 51.513317451, longitude: 7.45574331 }, 26 | ]; 27 | 28 | describe('getLongitude', () => { 29 | it('gets the longitude for a point', () => { 30 | expect(getPathLength(polygon)).toEqual(3375); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/sexagesimalToDecimal.test.js: -------------------------------------------------------------------------------- 1 | import sexagesimalToDecimal from './sexagesimalToDecimal'; 2 | 3 | describe('sexagesimalToDecimal', () => { 4 | test('returns a decimal for a sexagesimal value', () => { 5 | expect(sexagesimalToDecimal('51° 31\' 10.11" N')).toEqual(51.519475); 6 | 7 | expect(sexagesimalToDecimal('7° 28\' 01" E')).toEqual( 8 | 7.466944444444445 9 | ); 10 | 11 | expect(sexagesimalToDecimal('19° 22\' 32" S')).toEqual( 12 | -19.375555555555557 13 | ); 14 | 15 | expect(sexagesimalToDecimal('71° 3\' 34" W')).toEqual( 16 | -71.05944444444444 17 | ); 18 | 19 | expect(sexagesimalToDecimal("71°3'W")).toEqual(-71.05); 20 | 21 | expect(() => sexagesimalToDecimal('51.519470')).toThrow(); 22 | 23 | expect(() => sexagesimalToDecimal('-122.418079')).toThrow(); 24 | 25 | expect(sexagesimalToDecimal('51° 31.52\' 10.11" N')).toEqual( 26 | 51.52814166666667 27 | ); 28 | 29 | expect(sexagesimalToDecimal('121°26′31″W')).toEqual( 30 | -121.44194444444445 31 | ); 32 | 33 | expect(sexagesimalToDecimal('51°15′13"N')).toEqual(51.25361111111111); 34 | }); 35 | 36 | it('trims whitespace', () => { 37 | expect(sexagesimalToDecimal('19° 22\' 32" S ')).toEqual( 38 | -19.375555555555557 39 | ); 40 | 41 | expect(sexagesimalToDecimal(' 19° 22\' 32" S')).toEqual( 42 | -19.375555555555557 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/getCompassDirection.test.js: -------------------------------------------------------------------------------- 1 | import getCompassDirection from './getCompassDirection'; 2 | 3 | describe('getCompassDirection', () => { 4 | it('should return the exact compass direction between two points', () => { 5 | expect( 6 | getCompassDirection( 7 | { latitude: 52.518611, longitude: 13.408056 }, 8 | { latitude: 51.519475, longitude: 7.46694444 } 9 | ) 10 | ).toEqual('WSW'); 11 | 12 | expect( 13 | getCompassDirection( 14 | { latitude: 51.519475, longitude: 7.46694444 }, 15 | { latitude: 52.518611, longitude: 13.408056 } 16 | ) 17 | ).toEqual('ENE'); 18 | }); 19 | 20 | it('should call an optional bearing function', () => { 21 | const alwaysNorthEast = jest.fn(() => 45); 22 | const origin = { latitude: 52.518611, longitude: 13.408056 }; 23 | const dest = { latitude: 51.519475, longitude: 7.46694444 }; 24 | 25 | expect(getCompassDirection(origin, dest, alwaysNorthEast)).toEqual( 26 | 'NE' 27 | ); 28 | expect(alwaysNorthEast).toHaveBeenCalledWith(origin, dest); 29 | }); 30 | 31 | it('should throw if the bearing function does not return a number', () => { 32 | const returnString = () => NaN; 33 | 34 | const origin = { latitude: 52.518611, longitude: 13.408056 }; 35 | const dest = { latitude: 51.519475, longitude: 7.46694444 }; 36 | 37 | expect(() => getCompassDirection(origin, dest, returnString)).toThrow(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { LongitudeKeys, LatitudeKeys, AltitudeKeys } from './types'; 2 | 3 | export const sexagesimalPattern = /^([0-9]{1,3})°\s*([0-9]{1,3}(?:\.(?:[0-9]{1,}))?)['′]\s*(([0-9]{1,3}(\.([0-9]{1,}))?)["″]\s*)?([NEOSW]?)$/; 4 | export const earthRadius = 6378137; 5 | export const MINLAT = -90; 6 | export const MAXLAT = 90; 7 | export const MINLON = -180; 8 | export const MAXLON = 180; 9 | 10 | export const longitudeKeys: LongitudeKeys[] = ['lng', 'lon', 'longitude', 0]; 11 | export const latitudeKeys: LatitudeKeys[] = ['lat', 'latitude', 1]; 12 | export const altitudeKeys: AltitudeKeys[] = [ 13 | 'alt', 14 | 'altitude', 15 | 'elevation', 16 | 'elev', 17 | 2, 18 | ]; 19 | 20 | type unitObject = { 21 | [key: string]: number; 22 | }; 23 | 24 | export const distanceConversion: unitObject = { 25 | m: 1, 26 | km: 0.001, 27 | cm: 100, 28 | mm: 1000, 29 | mi: 1 / 1609.344, 30 | sm: 1 / 1852.216, 31 | ft: 100 / 30.48, 32 | in: 100 / 2.54, 33 | yd: 1 / 0.9144, 34 | }; 35 | 36 | export const timeConversion: unitObject = { 37 | m: 60, 38 | h: 3600, 39 | d: 86400, 40 | }; 41 | 42 | export const areaConversion: unitObject = { 43 | m2: 1, 44 | km2: 0.000001, 45 | ha: 0.0001, 46 | a: 0.01, 47 | ft2: 10.763911, 48 | yd2: 1.19599, 49 | in2: 1550.0031, 50 | }; 51 | 52 | // Aliases 53 | areaConversion.sqm = areaConversion.m2; 54 | areaConversion.sqkm = areaConversion.km2; 55 | areaConversion.sqft = areaConversion.ft2; 56 | areaConversion.sqyd = areaConversion.yd2; 57 | areaConversion.sqin = areaConversion.in2; 58 | -------------------------------------------------------------------------------- /src/getDistance.test.js: -------------------------------------------------------------------------------- 1 | import getDistance from './getDistance'; 2 | 3 | describe('getDistance', () => { 4 | it('should calculate the distance between any two points', () => { 5 | expect( 6 | getDistance( 7 | { latitude: 52.518611, longitude: 13.408056 }, 8 | { latitude: 51.519475, longitude: 7.46694444 } 9 | ) 10 | ).toEqual(421786); 11 | 12 | expect( 13 | getDistance( 14 | { latitude: 52.518611, longitude: 13.408056 }, 15 | { latitude: 51.519475, longitude: 7.46694444 }, 16 | 100 17 | ) 18 | ).toEqual(421800); 19 | 20 | expect( 21 | getDistance( 22 | { latitude: 37.774514, longitude: -122.418079 }, 23 | { latitude: 51.519475, longitude: 7.46694444 } 24 | ) 25 | ).toEqual(8967172); 26 | 27 | expect( 28 | getDistance([-122.418079, 37.774514], [7.46694444, 51.519475]) 29 | ).toEqual(8967172); 30 | }); 31 | 32 | it('should return 0 if two identical points are given', () => { 33 | expect( 34 | getDistance( 35 | { latitude: 51.516241843, longitude: 7.456494328 }, 36 | { latitude: 51.516241843, longitude: 7.456494328 } 37 | ) 38 | ).toBe(0); 39 | 40 | expect( 41 | getDistance( 42 | { latitude: 51.516241842, longitude: 7.456494328 }, 43 | { latitude: 51.516241842, longitude: 7.456494328 } 44 | ) 45 | ).toBe(0); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/getRoughCompassDirection.test.js: -------------------------------------------------------------------------------- 1 | import getRoughCompassDirection from './getRoughCompassDirection'; 2 | 3 | describe('getRoughCompassDirection', () => { 4 | describe('when exact compass direction is Northern', () => { 5 | it('returns N', () => { 6 | ['NNE', 'NE', 'NNW', 'N'].forEach((exactCompassDirection) => { 7 | expect(getRoughCompassDirection(exactCompassDirection)).toEqual( 8 | 'N' 9 | ); 10 | }); 11 | }); 12 | }); 13 | describe('when exact compass direction is Eastern', () => { 14 | it('returns E', () => { 15 | ['ENE', 'E', 'ESE', 'SE'].forEach((exactCompassDirection) => { 16 | expect(getRoughCompassDirection(exactCompassDirection)).toEqual( 17 | 'E' 18 | ); 19 | }); 20 | }); 21 | }); 22 | describe('when exact compass direction is Southern', () => { 23 | it('returns S', () => { 24 | ['SSE', 'S', 'SSW', 'SW'].forEach((exactCompassDirection) => { 25 | expect(getRoughCompassDirection(exactCompassDirection)).toEqual( 26 | 'S' 27 | ); 28 | }); 29 | }); 30 | }); 31 | describe('when exact compass direction is Western', () => { 32 | it('returns W', () => { 33 | ['WSW', 'W', 'WNW', 'NW'].forEach((exactCompassDirection) => { 34 | expect(getRoughCompassDirection(exactCompassDirection)).toEqual( 35 | 'W' 36 | ); 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/getRhumbLineBearing.ts: -------------------------------------------------------------------------------- 1 | import getLatitude from './getLatitude'; 2 | import getLongitude from './getLongitude'; 3 | import toRad from './toRad'; 4 | import toDeg from './toDeg'; 5 | import { GeolibInputCoordinates } from './types'; 6 | 7 | /** 8 | * Gets rhumb line bearing of two points. Find out about the difference between rhumb line and 9 | * great circle bearing on Wikipedia. It's quite complicated. Rhumb line should be fine in most cases: 10 | * 11 | * http://en.wikipedia.org/wiki/Rhumb_line#General_and_mathematical_description 12 | * 13 | * Function heavily based on Doug Vanderweide's great PHP version (licensed under GPL 3.0) 14 | * http://www.dougv.com/2009/07/13/calculating-the-bearing-and-compass-rose-direction-between-two-latitude-longitude-coordinates-in-php/ 15 | */ 16 | const getRhumbLineBearing = ( 17 | origin: GeolibInputCoordinates, 18 | dest: GeolibInputCoordinates 19 | ) => { 20 | // difference of longitude coords 21 | let diffLon = toRad(getLongitude(dest)) - toRad(getLongitude(origin)); 22 | 23 | // difference latitude coords phi 24 | const diffPhi = Math.log( 25 | Math.tan(toRad(getLatitude(dest)) / 2 + Math.PI / 4) / 26 | Math.tan(toRad(getLatitude(origin)) / 2 + Math.PI / 4) 27 | ); 28 | 29 | // recalculate diffLon if it is greater than pi 30 | if (Math.abs(diffLon) > Math.PI) { 31 | if (diffLon > 0) { 32 | diffLon = (Math.PI * 2 - diffLon) * -1; 33 | } else { 34 | diffLon = Math.PI * 2 + diffLon; 35 | } 36 | } 37 | 38 | //return the angle, normalized 39 | return (toDeg(Math.atan2(diffLon, diffPhi)) + 360) % 360; 40 | }; 41 | 42 | export default getRhumbLineBearing; 43 | -------------------------------------------------------------------------------- /src/computeDestinationPoint.ts: -------------------------------------------------------------------------------- 1 | import getLatitude from './getLatitude'; 2 | import getLongitude from './getLongitude'; 3 | import toRad from './toRad'; 4 | import toDeg from './toDeg'; 5 | import { MAXLON, MINLON } from './constants'; 6 | import { GeolibInputCoordinates } from './types'; 7 | 8 | // Computes the destination point given an initial point, a distance and a bearing 9 | // See http://www.movable-type.co.uk/scripts/latlong.html for the original code 10 | const computeDestinationPoint = ( 11 | start: GeolibInputCoordinates, 12 | distance: number, 13 | bearing: number, 14 | radius: number = 6371000 15 | ) => { 16 | const lat = getLatitude(start); 17 | const lng = getLongitude(start); 18 | 19 | const delta = distance / radius; 20 | const theta = toRad(bearing); 21 | 22 | const phi1 = toRad(lat); 23 | const lambda1 = toRad(lng); 24 | 25 | const phi2 = Math.asin( 26 | Math.sin(phi1) * Math.cos(delta) + 27 | Math.cos(phi1) * Math.sin(delta) * Math.cos(theta) 28 | ); 29 | 30 | let lambda2 = 31 | lambda1 + 32 | Math.atan2( 33 | Math.sin(theta) * Math.sin(delta) * Math.cos(phi1), 34 | Math.cos(delta) - Math.sin(phi1) * Math.sin(phi2) 35 | ); 36 | 37 | let longitude = toDeg(lambda2); 38 | if (longitude < MINLON || longitude > MAXLON) { 39 | // normalise to >=-180 and <=180° if value is >MAXLON or { 40 | it('gets the center of two points', () => { 41 | expect(getCenter([cities.Berlin, cities.Moscow])).toEqual({ 42 | longitude: 25.0332388360222, 43 | latitude: 54.74368339960522, 44 | }); 45 | expect(getCenter([cities.Sydney, cities.SanFrancisco])).toEqual({ 46 | longitude: -166.9272249630353, 47 | latitude: 2.6764932317022576, 48 | }); 49 | }); 50 | 51 | it('gets the center of multiple points', () => { 52 | const values = Object.values(cities); 53 | expect(getCenter(values)).toEqual({ 54 | latitude: 65.41916196002177, 55 | longitude: -28.01313266917171, 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type GeolibLongitudeInputValue = number | string; 2 | export type GeolibLatitudeInputValue = number | string; 3 | export type GeolibAltitudeInputValue = number; 4 | 5 | export type GeolibGeoJSONPoint = [ 6 | GeolibLongitudeInputValue, 7 | GeolibLatitudeInputValue, 8 | GeolibAltitudeInputValue? 9 | ]; 10 | 11 | export type LongitudeKeys = 'lng' | 'lon' | 'longitude' | 0; 12 | export type LatitudeKeys = 'lat' | 'latitude' | 1; 13 | export type AltitudeKeys = 'alt' | 'altitude' | 'elevation' | 'elev' | 2; 14 | 15 | export type GeolibInputLongitude = 16 | | { lng: GeolibLongitudeInputValue } 17 | | { lon: GeolibLongitudeInputValue } 18 | | { longitude: GeolibLongitudeInputValue }; 19 | 20 | export type GeolibInputLatitude = 21 | | { lat: GeolibLatitudeInputValue } 22 | | { latitude: GeolibLatitudeInputValue }; 23 | 24 | export type GeolibInputAltitude = 25 | | { alt?: GeolibAltitudeInputValue } 26 | | { altitude?: GeolibAltitudeInputValue } 27 | | { elevation?: GeolibAltitudeInputValue } 28 | | { elev?: GeolibAltitudeInputValue }; 29 | 30 | export type UserInputCoordinates = GeolibInputLongitude & 31 | GeolibInputLatitude & 32 | GeolibInputAltitude; 33 | 34 | export type GeolibInputCoordinates = UserInputCoordinates | GeolibGeoJSONPoint; 35 | 36 | export type GeolibDistanceFn = ( 37 | point: GeolibInputCoordinates, 38 | dest: GeolibInputCoordinates 39 | ) => number; 40 | 41 | export type Timestamp = number; 42 | 43 | export type GeolibInputCoordinatesWithTime = GeolibInputCoordinates & { 44 | time: Timestamp; 45 | }; 46 | 47 | export type GeolibBounds = { 48 | maxLat: number; 49 | minLat: number; 50 | maxLng: number; 51 | minLng: number; 52 | }; 53 | -------------------------------------------------------------------------------- /src/getPreciseDistance.test.js: -------------------------------------------------------------------------------- 1 | import getPreciseDistance from './getPreciseDistance'; 2 | 3 | describe('getPreciseDistance', () => { 4 | it('should calculate the precise distance between any two points', () => { 5 | expect( 6 | getPreciseDistance( 7 | { latitude: 52.518611, longitude: 13.408056 }, 8 | { latitude: 51.519475, longitude: 7.46694444 } 9 | ) 10 | ).toEqual(422592); 11 | 12 | expect( 13 | getPreciseDistance( 14 | { latitude: 52.518611, longitude: 13.408056 }, 15 | { latitude: 51.519475, longitude: 7.46694444 }, 16 | 100 17 | ) 18 | ).toEqual(422600); 19 | 20 | expect( 21 | getPreciseDistance( 22 | { latitude: 37.774514, longitude: -122.418079 }, 23 | { latitude: 51.519475, longitude: 7.46694444 } 24 | ) 25 | ).toEqual(8980260); 26 | 27 | expect( 28 | getPreciseDistance( 29 | [-122.418079, 37.774514], 30 | [7.46694444, 51.519475] 31 | ) 32 | ).toEqual(8980260); 33 | }); 34 | 35 | it('should return 0 if two identical points are given', () => { 36 | expect( 37 | getPreciseDistance( 38 | { latitude: 51.516241843, longitude: 7.456494328 }, 39 | { latitude: 51.516241843, longitude: 7.456494328 } 40 | ) 41 | ).toBe(0); 42 | 43 | expect( 44 | getPreciseDistance( 45 | { latitude: 51.516241842, longitude: 7.456494328 }, 46 | { latitude: 51.516241842, longitude: 7.456494328 } 47 | ) 48 | ).toBe(0); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/getCoordinateKeys.test.js: -------------------------------------------------------------------------------- 1 | import getCoordinateKeys from './getCoordinateKeys'; 2 | 3 | describe('getCoordinateKeys', () => { 4 | it('gets lat/lng[/alt]', () => { 5 | expect(getCoordinateKeys({ lat: 1, lng: 1 })).toEqual({ 6 | longitude: 'lng', 7 | latitude: 'lat', 8 | }); 9 | expect(getCoordinateKeys({ lat: 1, lng: 1, alt: 1 })).toEqual({ 10 | longitude: 'lng', 11 | latitude: 'lat', 12 | altitude: 'alt', 13 | }); 14 | }); 15 | 16 | it('gets lat/lon[/alt]', () => { 17 | expect(getCoordinateKeys({ lat: 1, lon: 1 })).toEqual({ 18 | longitude: 'lon', 19 | latitude: 'lat', 20 | }); 21 | expect(getCoordinateKeys({ lat: 1, lon: 1, alt: 1 })).toEqual({ 22 | longitude: 'lon', 23 | latitude: 'lat', 24 | altitude: 'alt', 25 | }); 26 | }); 27 | 28 | it('gets latitude/longitude[/altitude]', () => { 29 | expect(getCoordinateKeys({ latitude: 1, longitude: 1 })).toEqual({ 30 | longitude: 'longitude', 31 | latitude: 'latitude', 32 | }); 33 | expect( 34 | getCoordinateKeys({ latitude: 1, longitude: 1, altitude: 1 }) 35 | ).toEqual({ 36 | longitude: 'longitude', 37 | latitude: 'latitude', 38 | altitude: 'altitude', 39 | }); 40 | }); 41 | 42 | it('gets GeoJSON array', () => { 43 | expect(getCoordinateKeys([1, 2])).toEqual({ 44 | longitude: 0, 45 | latitude: 1, 46 | }); 47 | expect(getCoordinateKeys([1, 2, 3])).toEqual({ 48 | longitude: 0, 49 | latitude: 1, 50 | altitude: 2, 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/toDecimal.ts: -------------------------------------------------------------------------------- 1 | import isDecimal from './isDecimal'; 2 | import isSexagesimal from './isSexagesimal'; 3 | import sexagesimalToDecimal from './sexagesimalToDecimal'; 4 | import isValidCoordinate from './isValidCoordinate'; 5 | import getCoordinateKeys from './getCoordinateKeys'; 6 | 7 | const toDecimal = (value: any): any => { 8 | if (isDecimal(value)) { 9 | return Number(value); 10 | } 11 | 12 | if (isSexagesimal(value)) { 13 | return sexagesimalToDecimal(value); 14 | } 15 | 16 | // value is a valid coordinate with latitude and longitude. 17 | // Either object literal with latitude and longitue, or GeoJSON array 18 | if (isValidCoordinate(value)) { 19 | const keys = getCoordinateKeys(value); 20 | 21 | // value seems to be a GeoJSON array 22 | if (Array.isArray(value)) { 23 | return value.map((v, index) => 24 | [0, 1].includes(index) ? toDecimal(v) : v 25 | ); 26 | } 27 | 28 | // value is an object with latitude and longitude property 29 | return { 30 | ...value, 31 | ...(keys.latitude && { 32 | [keys.latitude]: toDecimal(value[keys.latitude]), 33 | }), 34 | ...(keys.longitude && { 35 | [keys.longitude]: toDecimal(value[keys.longitude]), 36 | }), 37 | }; 38 | } 39 | 40 | // if it is an array, convert every geojson, latlng object 41 | // and sexagesimal values to decimal 42 | if (Array.isArray(value)) { 43 | return value.map((point) => 44 | isValidCoordinate(point) ? toDecimal(point) : point 45 | ); 46 | } 47 | 48 | // Unrecognized format. Return the value itself. 49 | return value; 50 | }; 51 | 52 | export default toDecimal; 53 | -------------------------------------------------------------------------------- /src/getCompassDirection.ts: -------------------------------------------------------------------------------- 1 | import { GeolibInputCoordinates } from './types'; 2 | import getRhumbLineBearing from './getRhumbLineBearing'; 3 | 4 | type BearingFunction = ( 5 | origin: GeolibInputCoordinates, 6 | dest: GeolibInputCoordinates 7 | ) => number; 8 | 9 | // Gets the compass direction from an origin coordinate to a 10 | // destination coordinate. 11 | const getCompassDirection = ( 12 | origin: GeolibInputCoordinates, 13 | dest: GeolibInputCoordinates, 14 | bearingFn: BearingFunction = getRhumbLineBearing 15 | ) => { 16 | const bearing = 17 | typeof bearingFn === 'function' 18 | ? bearingFn(origin, dest) 19 | : getRhumbLineBearing(origin, dest); 20 | 21 | if (isNaN(bearing)) { 22 | throw new Error( 23 | 'Could not calculate bearing for given points. Check your bearing function' 24 | ); 25 | } 26 | 27 | switch (Math.round(bearing / 22.5)) { 28 | case 1: 29 | return 'NNE'; 30 | case 2: 31 | return 'NE'; 32 | case 3: 33 | return 'ENE'; 34 | case 4: 35 | return 'E'; 36 | case 5: 37 | return 'ESE'; 38 | case 6: 39 | return 'SE'; 40 | case 7: 41 | return 'SSE'; 42 | case 8: 43 | return 'S'; 44 | case 9: 45 | return 'SSW'; 46 | case 10: 47 | return 'SW'; 48 | case 11: 49 | return 'WSW'; 50 | case 12: 51 | return 'W'; 52 | case 13: 53 | return 'WNW'; 54 | case 14: 55 | return 'NW'; 56 | case 15: 57 | return 'NNW'; 58 | default: 59 | return 'N'; 60 | } 61 | }; 62 | 63 | export default getCompassDirection; 64 | -------------------------------------------------------------------------------- /src/decimalToSexagesimal.test.js: -------------------------------------------------------------------------------- 1 | import decimalToSexagesimal from './decimalToSexagesimal'; 2 | 3 | describe('decimalToSexagesimal', () => { 4 | it('should return minutes and seconds with a leading 0 if < 10', () => { 5 | expect(decimalToSexagesimal(121.135)).toEqual('121° 08\' 06.0"'); 6 | }); 7 | 8 | it('should still return 00 and 00.0 if there are no minutes or seconds', () => { 9 | expect(decimalToSexagesimal(50)).toEqual('50° 00\' 00.0"'); 10 | }); 11 | 12 | it('should always return a positive value', () => { 13 | expect(decimalToSexagesimal(-19.37555556)).toEqual('19° 22\' 32.0"'); 14 | }); 15 | 16 | it('should return seconds with decimal places if needed', () => { 17 | expect(decimalToSexagesimal(51.519475)).toEqual('51° 31\' 10.11"'); 18 | expect(decimalToSexagesimal(51.516975)).toEqual('51° 31\' 01.11"'); 19 | }); 20 | 21 | it('should return precise values', () => { 22 | expect(decimalToSexagesimal(31.011306)).toEqual('31° 00\' 40.7016"'); 23 | }); 24 | 25 | it('should handle precision correctly', () => { 26 | expect(decimalToSexagesimal(90.99999996)).toEqual('90° 59\' 59.9999"'); 27 | expect(decimalToSexagesimal(90.9999999)).toEqual('90° 59\' 59.9996"'); 28 | expect(decimalToSexagesimal(90.999999)).toEqual('90° 59\' 59.9964"'); 29 | expect(decimalToSexagesimal(90.99999)).toEqual('90° 59\' 59.964"'); 30 | expect(decimalToSexagesimal(90.9999)).toEqual('90° 59\' 59.64"'); 31 | expect(decimalToSexagesimal(90.999)).toEqual('90° 59\' 56.4"'); 32 | expect(decimalToSexagesimal(90.99)).toEqual('90° 59\' 24.0"'); 33 | expect(decimalToSexagesimal(90.9)).toEqual('90° 54\' 00.0"'); 34 | expect(decimalToSexagesimal(90.1)).toEqual('90° 06\' 00.0"'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/isValidLatitude.test.js: -------------------------------------------------------------------------------- 1 | import isValidLatitude from './isValidLatitude'; 2 | import { MAXLAT, MINLAT } from './constants'; 3 | 4 | describe('isValidLatitude', () => { 5 | describe('when value is a decimal', () => { 6 | describe('when value is between MINLAT and MAXLAT', () => { 7 | it('returns true', () => { 8 | let value = MAXLAT - 1; 9 | expect(isValidLatitude(value)).toEqual(true); 10 | value = MINLAT + 1; 11 | expect(isValidLatitude(value)).toEqual(true); 12 | }); 13 | }); 14 | describe('when value is not between MINLAT and MAXLAT', () => { 15 | it('returns false', () => { 16 | let value = MAXLAT + 1; 17 | expect(isValidLatitude(value)).toEqual(false); 18 | value = MINLAT - 1; 19 | expect(isValidLatitude(value)).toEqual(false); 20 | }); 21 | }); 22 | }); 23 | describe('when value is a sexagesimal', () => { 24 | describe('when value is between MINLAT and MAXLAT', () => { 25 | it('returns true', () => { 26 | const value = '51° 31\' 10.11" N'; 27 | expect(isValidLatitude(value)).toEqual(true); 28 | }); 29 | }); 30 | describe('when value is not between MINLAT and MAXLAT', () => { 31 | it('returns false', () => { 32 | const value = '121°26′31″W'; 33 | expect(isValidLatitude(value)).toEqual(false); 34 | }); 35 | }); 36 | }); 37 | describe('when value is not a decimal or sexagesimal', () => { 38 | it('returns false', () => { 39 | const value = 'foo'; 40 | expect(isValidLatitude(value)).toEqual(false); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/isValidLongitude.test.js: -------------------------------------------------------------------------------- 1 | import isValidLongitude from './isValidLongitude'; 2 | import { MAXLON, MINLON } from './constants'; 3 | 4 | describe('isValidLongitude', () => { 5 | describe('when value is a decimal', () => { 6 | describe('when value is between MINLON and MAXLON', () => { 7 | it('returns true', () => { 8 | let value = MAXLON - 1; 9 | expect(isValidLongitude(value)).toEqual(true); 10 | value = MINLON + 1; 11 | expect(isValidLongitude(value)).toEqual(true); 12 | }); 13 | }); 14 | describe('when value is not between MINLON and MAXLON', () => { 15 | it('returns false', () => { 16 | let value = MAXLON + 1; 17 | expect(isValidLongitude(value)).toEqual(false); 18 | value = MINLON - 1; 19 | expect(isValidLongitude(value)).toEqual(false); 20 | }); 21 | }); 22 | }); 23 | describe('when value is a sexagesimal', () => { 24 | describe('when value is between MINLON and MAXLON', () => { 25 | it('returns true', () => { 26 | const value = '51° 31\' 10.11" N'; 27 | expect(isValidLongitude(value)).toEqual(true); 28 | }); 29 | }); 30 | describe('when value is not between MINLON and MAXLON', () => { 31 | it('returns false', () => { 32 | const value = '221°26′31″W'; 33 | expect(isValidLongitude(value)).toEqual(false); 34 | }); 35 | }); 36 | }); 37 | describe('when value is not a decimal or sexagesimal', () => { 38 | it('returns false', () => { 39 | const value = 'foo'; 40 | expect(isValidLongitude(value)).toEqual(false); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/toDecimal.test.js: -------------------------------------------------------------------------------- 1 | import toDecimal from './toDecimal'; 2 | 3 | describe('toDecimal', () => { 4 | it('converts a sexagesimal value into decimal representation', () => { 5 | expect(toDecimal('51° 31\' 10.11" N')).toEqual(51.519475); 6 | }); 7 | 8 | it('returns a decimal value as number if it is already in decimal format', () => { 9 | expect(toDecimal(51.519475)).toEqual(51.519475); 10 | expect(toDecimal('51.519475')).toEqual(51.519475); 11 | }); 12 | 13 | it('converts a valid coordinate of any type into decimal representation', () => { 14 | expect(toDecimal({ lat: 1, lng: 1 })).toEqual({ lat: 1, lng: 1 }); 15 | expect(toDecimal({ lat: '51° 31\' 10.11" N', lng: 1 })).toEqual({ 16 | lat: 51.519475, 17 | lng: 1, 18 | }); 19 | expect( 20 | toDecimal({ 21 | latitude: '51° 31\' 10.11" N', 22 | longitude: '51° 31\' 10.11" N', 23 | }) 24 | ).toEqual({ 25 | latitude: 51.519475, 26 | longitude: 51.519475, 27 | }); 28 | expect(toDecimal([1, 2])).toEqual([1, 2]); 29 | expect(toDecimal(["71° 0'", 2])).toEqual([71, 2]); 30 | }); 31 | 32 | it('converts an array of arbitrary coordinates to an array of decimal coordinates', () => { 33 | expect(toDecimal([{ lat: "71° 0'", lng: 1 }])).toEqual([ 34 | { 35 | lat: 71, 36 | lng: 1, 37 | }, 38 | ]); 39 | expect( 40 | toDecimal([ 41 | { latitude: "71° 0'", longitude: 1 }, 42 | ["71° 0'", "71° 0'"], 43 | ]) 44 | ).toEqual([ 45 | { 46 | latitude: 71, 47 | longitude: 1, 48 | }, 49 | [71, 71], 50 | ]); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/getDistanceFromLine.ts: -------------------------------------------------------------------------------- 1 | import getDistance from './getDistance'; 2 | import robustAcos from './robustAcos'; 3 | import { GeolibInputCoordinates } from './types'; 4 | 5 | // Returns the minimum distance from a point to a line 6 | const getDistanceFromLine = ( 7 | point: GeolibInputCoordinates, 8 | lineStart: GeolibInputCoordinates, 9 | lineEnd: GeolibInputCoordinates, 10 | accuracy: number = 1 11 | ) => { 12 | const d1 = getDistance(lineStart, point, accuracy); 13 | const d2 = getDistance(point, lineEnd, accuracy); 14 | const d3 = getDistance(lineStart, lineEnd, accuracy); 15 | 16 | // alpha is the angle between the line from start to point, and from start to end 17 | const alpha = Math.acos( 18 | robustAcos((d1 * d1 + d3 * d3 - d2 * d2) / (2 * d1 * d3)) 19 | ); 20 | 21 | // beta is the angle between the line from end to point and from end to start // 22 | const beta = Math.acos( 23 | robustAcos((d2 * d2 + d3 * d3 - d1 * d1) / (2 * d2 * d3)) 24 | ); 25 | 26 | const pointAtLineStart = d1 === 0; 27 | const pointAtLineEnd = d2 === 0; 28 | if (pointAtLineStart || pointAtLineEnd) { 29 | return 0; 30 | } 31 | 32 | const lineLengthZero = d3 === 0; 33 | if (lineLengthZero) { 34 | return d1; 35 | } 36 | 37 | // if the angle is greater than 90 degrees, then the minimum distance is the 38 | // line from the start to the point 39 | if (alpha > Math.PI / 2) { 40 | return d1; 41 | } 42 | 43 | if (beta > Math.PI / 2) { 44 | // same for the beta 45 | return d2; 46 | } 47 | 48 | // console.log(Math.sin(alpha), Math.sin(alpha) * d1); 49 | 50 | // otherwise the minimum distance is achieved through a line perpendicular 51 | // to the start-end line, which goes from the start-end line to the point 52 | return Math.sin(alpha) * d1; 53 | }; 54 | 55 | export default getDistanceFromLine; 56 | -------------------------------------------------------------------------------- /src/getBoundsOfDistance.test.js: -------------------------------------------------------------------------------- 1 | import getDistance from './getDistance'; 2 | import getBoundsOfDistance from './getBoundsOfDistance'; 3 | 4 | describe('getBoundsOfDistance', () => { 5 | it('should return the top most north, east, south and west points for a given distance', () => { 6 | const point = { latitude: 34.090166, longitude: -118.276736555556 }; 7 | const bounds1000meters = getBoundsOfDistance(point, 1000); 8 | expect(bounds1000meters).toEqual([ 9 | { latitude: 34.08118284715881, longitude: -118.28758372313425 }, 10 | { latitude: 34.0991491528412, longitude: -118.26588938797775 }, 11 | ]); 12 | }); 13 | 14 | it('should correctly calculate the given distance for the returned bounds', () => { 15 | const point = { latitude: 34.090166, longitude: -118.276736555556 }; 16 | const bounds1000meters = getBoundsOfDistance(point, 1000); 17 | expect(bounds1000meters).toEqual([ 18 | { latitude: 34.08118284715881, longitude: -118.28758372313425 }, 19 | { latitude: 34.0991491528412, longitude: -118.26588938797775 }, 20 | ]); 21 | 22 | const north = { 23 | latitude: bounds1000meters[1].latitude, 24 | longitude: point.longitude, 25 | }; 26 | const east = { 27 | latitude: point.latitude, 28 | longitude: bounds1000meters[1].longitude, 29 | }; 30 | const south = { 31 | latitude: bounds1000meters[0].latitude, 32 | longitude: point.longitude, 33 | }; 34 | const west = { 35 | latitude: point.latitude, 36 | longitude: bounds1000meters[0].longitude, 37 | }; 38 | 39 | expect(getDistance(point, north)).toBe(1000); 40 | expect(getDistance(point, east)).toBe(1000); 41 | expect(getDistance(point, south)).toBe(1000); 42 | expect(getDistance(point, west)).toBe(1000); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/isPointInPolygon.test.js: -------------------------------------------------------------------------------- 1 | import isPointInPolygon from './isPointInPolygon'; 2 | 3 | const polygon = [ 4 | { latitude: 51.513357512, longitude: 7.45574331 }, 5 | { latitude: 51.515400598, longitude: 7.45518541 }, 6 | { latitude: 51.516241842, longitude: 7.456494328 }, 7 | { latitude: 51.516722545, longitude: 7.459863183 }, 8 | { latitude: 51.517443592, longitude: 7.463232037 }, 9 | { latitude: 51.5177507, longitude: 7.464755532 }, 10 | { latitude: 51.517657233, longitude: 7.466622349 }, 11 | { latitude: 51.51722995, longitude: 7.468317505 }, 12 | { latitude: 51.516816015, longitude: 7.47011995 }, 13 | { latitude: 51.516308606, longitude: 7.471793648 }, 14 | { latitude: 51.515974782, longitude: 7.472437378 }, 15 | { latitude: 51.515413951, longitude: 7.472845074 }, 16 | { latitude: 51.514559338, longitude: 7.472909447 }, 17 | { latitude: 51.512195717, longitude: 7.472651955 }, 18 | { latitude: 51.511127373, longitude: 7.47140741 }, 19 | { latitude: 51.51029939, longitude: 7.469948288 }, 20 | { latitude: 51.509831973, longitude: 7.468446251 }, 21 | { latitude: 51.509978876, longitude: 7.462481019 }, 22 | { latitude: 51.510913701, longitude: 7.460678574 }, 23 | { latitude: 51.511594777, longitude: 7.459434029 }, 24 | { latitude: 51.512396029, longitude: 7.457695958 }, 25 | { latitude: 51.513317451, longitude: 7.45574331 }, 26 | ]; 27 | 28 | describe('isPointInPolygon', () => { 29 | it('should return true if a given point is inside of a polygon', () => { 30 | const pointIsInside = isPointInPolygon( 31 | { latitude: 51.514252208, longitude: 7.464905736 }, 32 | polygon 33 | ); 34 | expect(pointIsInside).toBe(true); 35 | }); 36 | 37 | it('should return false if a given point is not inside of a polygon', () => { 38 | const pointIsNotInside = isPointInPolygon( 39 | { latitude: 51.510539773, longitude: 7.454691884 }, 40 | polygon 41 | ); 42 | expect(pointIsNotInside).toBe(false); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/computeDestinationPoint.test.js: -------------------------------------------------------------------------------- 1 | import computeDestinationPoint from './computeDestinationPoint'; 2 | 3 | describe('computeDestinationPoint', () => { 4 | it('should get the destination point to a given point, distance and bearing', () => { 5 | expect( 6 | computeDestinationPoint( 7 | { latitude: 52.518611, longitude: 13.408056 }, 8 | 15000, 9 | 180 10 | ) 11 | ).toEqual({ 12 | latitude: 52.383712759112186, 13 | longitude: 13.408056, 14 | }); 15 | 16 | expect( 17 | computeDestinationPoint( 18 | { latitude: 52.518611, longitude: 13.408056 }, 19 | 15000, 20 | 135 21 | ) 22 | ).toEqual({ 23 | latitude: 52.42312025947117, 24 | longitude: 13.56447370636139, 25 | }); 26 | }); 27 | 28 | it('should not exceed maxLon or fall below minLon', () => { 29 | expect( 30 | computeDestinationPoint( 31 | { latitude: 18.5075232, longitude: 73.8047121 }, 32 | 50000000, 33 | 0 34 | ) 35 | ).toEqual({ 36 | latitude: 71.83167384063478, 37 | longitude: -106.19528790000001, 38 | }); 39 | }); 40 | 41 | it('should leave longitude untouched if bearing is 0 or 180', () => { 42 | expect( 43 | computeDestinationPoint( 44 | { latitude: 18.5075232, longitude: 73.8047121 }, 45 | 500, 46 | 0 47 | ) 48 | ).toEqual({ 49 | latitude: 18.512019808029596, 50 | longitude: 73.8047121, 51 | }); 52 | 53 | expect( 54 | computeDestinationPoint( 55 | { latitude: 18.5075232, longitude: 73.8047121 }, 56 | 500, 57 | 180 58 | ) 59 | ).toEqual({ 60 | latitude: 18.50302659197041, 61 | longitude: 73.8047121, 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/orderByDistance.test.js: -------------------------------------------------------------------------------- 1 | import orderByDistance from './orderByDistance'; 2 | 3 | describe('orderByDistance', () => { 4 | it('should sort an array of coords based on distance to an origin point', () => { 5 | expect( 6 | orderByDistance( 7 | { latitude: 51.516241842, longitude: 7.456494328 }, 8 | [ 9 | { latitude: 51.513357512, longitude: 7.45574331 }, 10 | { latitude: 51.515400598, longitude: 7.45518541 }, 11 | { latitude: 51.516722545, longitude: 7.459863183 }, 12 | { latitude: 51.517443592, longitude: 7.463232037 }, 13 | ] 14 | ) 15 | ).toEqual([ 16 | { latitude: 51.515400598, longitude: 7.45518541 }, 17 | { latitude: 51.516722545, longitude: 7.459863183 }, 18 | { latitude: 51.513357512, longitude: 7.45574331 }, 19 | { latitude: 51.517443592, longitude: 7.463232037 }, 20 | ]); 21 | 22 | expect( 23 | orderByDistance({ latitude: 1, longitude: 1 }, [ 24 | [1, 74], 25 | [1, 15], 26 | [1, 12], 27 | [1, 2], 28 | [1, 37], 29 | [1, 4], 30 | ]) 31 | ).toEqual([ 32 | [1, 2], 33 | [1, 4], 34 | [1, 12], 35 | [1, 15], 36 | [1, 37], 37 | [1, 74], 38 | ]); 39 | }); 40 | 41 | it('should use an optional getDistance function', () => { 42 | const origin = [1, 1]; 43 | const dest = [ 44 | [30, 28], 45 | [5, 0], 46 | [20, 17], 47 | [40, 39], 48 | [10, 6], 49 | ]; 50 | const subtractLatFromLon = (origin, dest) => dest[0] - dest[1]; 51 | expect(orderByDistance(origin, dest, subtractLatFromLon)).toEqual([ 52 | [40, 39], 53 | [30, 28], 54 | [20, 17], 55 | [10, 6], 56 | [5, 0], 57 | ]); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/getBoundsOfDistance.ts: -------------------------------------------------------------------------------- 1 | import getLatitude from './getLatitude'; 2 | import getLongitude from './getLongitude'; 3 | import toRad from './toRad'; 4 | import toDeg from './toDeg'; 5 | import { earthRadius, MAXLAT, MINLAT, MAXLON, MINLON } from './constants'; 6 | import { GeolibInputCoordinates } from './types'; 7 | 8 | // Computes the bounding coordinates of all points on the surface of the earth 9 | // less than or equal to the specified great circle distance. 10 | const getBoundsOfDistance = ( 11 | point: GeolibInputCoordinates, 12 | distance: number 13 | ) => { 14 | const latitude = getLatitude(point); 15 | const longitude = getLongitude(point); 16 | 17 | const radLat = toRad(latitude); 18 | const radLon = toRad(longitude); 19 | 20 | const radDist = distance / earthRadius; 21 | let minLat = radLat - radDist; 22 | let maxLat = radLat + radDist; 23 | 24 | const MAX_LAT_RAD = toRad(MAXLAT); 25 | const MIN_LAT_RAD = toRad(MINLAT); 26 | const MAX_LON_RAD = toRad(MAXLON); 27 | const MIN_LON_RAD = toRad(MINLON); 28 | 29 | let minLon; 30 | let maxLon; 31 | 32 | if (minLat > MIN_LAT_RAD && maxLat < MAX_LAT_RAD) { 33 | const deltaLon = Math.asin(Math.sin(radDist) / Math.cos(radLat)); 34 | minLon = radLon - deltaLon; 35 | 36 | if (minLon < MIN_LON_RAD) { 37 | minLon += Math.PI * 2; 38 | } 39 | 40 | maxLon = radLon + deltaLon; 41 | 42 | if (maxLon > MAX_LON_RAD) { 43 | maxLon -= Math.PI * 2; 44 | } 45 | } else { 46 | // A pole is within the distance. 47 | minLat = Math.max(minLat, MIN_LAT_RAD); 48 | maxLat = Math.min(maxLat, MAX_LAT_RAD); 49 | minLon = MIN_LON_RAD; 50 | maxLon = MAX_LON_RAD; 51 | } 52 | 53 | return [ 54 | // Southwest 55 | { 56 | latitude: toDeg(minLat), 57 | longitude: toDeg(minLon), 58 | }, 59 | // Northeast 60 | { 61 | latitude: toDeg(maxLat), 62 | longitude: toDeg(maxLon), 63 | }, 64 | ]; 65 | }; 66 | 67 | export default getBoundsOfDistance; 68 | -------------------------------------------------------------------------------- /src/getCenterOfBounds.test.js: -------------------------------------------------------------------------------- 1 | import getCenterOfBounds from './getCenterOfBounds'; 2 | 3 | const polygon = [ 4 | { latitude: 51.513357512, longitude: 7.45574331 }, 5 | { latitude: 51.515400598, longitude: 7.45518541 }, 6 | { latitude: 51.516241842, longitude: 7.456494328 }, 7 | { latitude: 51.516722545, longitude: 7.459863183 }, 8 | { latitude: 51.517443592, longitude: 7.463232037 }, 9 | { lat: 51.5177507, lon: 7.464755532 }, 10 | { latitude: 51.517657233, longitude: 7.466622349 }, 11 | { latitude: 51.51722995, longitude: 7.468317505 }, 12 | { latitude: 51.516816015, longitude: 7.47011995 }, 13 | { latitude: 51.516308606, longitude: 7.471793648 }, 14 | { latitude: 51.515974782, longitude: 7.472437378 }, 15 | { latitude: 51.515413951, longitude: 7.472845074 }, 16 | { latitude: 51.514559338, longitude: 7.472909447 }, 17 | { latitude: 51.512195717, longitude: 7.472651955 }, 18 | { latitude: 51.511127373, longitude: 7.47140741 }, 19 | { latitude: 51.51029939, longitude: 7.469948288 }, 20 | { latitude: 51.509831973, longitude: 7.468446251 }, 21 | { latitude: 51.509978876, longitude: 7.462481019 }, 22 | [7.460678574, 51.510913701], 23 | { latitude: 51.511594777, longitude: 7.459434029 }, 24 | { latitude: 51.512396029, longitude: 7.457695958 }, 25 | { latitude: 51.513317451, longitude: 7.45574331 }, 26 | ]; 27 | 28 | const polygon2 = [ 29 | { latitude: 51.513357512, longitude: 7.45574331 }, 30 | { latitude: 51.515400598, longitude: 7.45518541 }, 31 | { latitude: 51.516241842, longitude: 7.456494328 }, 32 | { latitude: 51.516722545, longitude: 7.459863183 }, 33 | { latitude: 51.517443592, longitude: 7.463232037 }, 34 | ]; 35 | 36 | describe('getCenterOfBounds', () => { 37 | it('should get the center of bounds for a polygon 🤷', () => { 38 | expect(getCenterOfBounds(polygon)).toEqual({ 39 | latitude: 51.513791, 40 | longitude: 7.464047, 41 | }); 42 | 43 | expect(getCenterOfBounds(polygon2)).toEqual({ 44 | latitude: 51.515401, 45 | longitude: 7.459209, 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/getBounds.test.js: -------------------------------------------------------------------------------- 1 | import getBounds from './getBounds'; 2 | 3 | const polygon = [ 4 | { latitude: 51.513357512, longitude: 7.45574331 }, 5 | { latitude: 51.515400598, longitude: 7.45518541 }, 6 | { latitude: 51.516241842, longitude: 7.456494328 }, 7 | { latitude: 51.516722545, longitude: 7.459863183 }, 8 | { latitude: 51.517443592, longitude: 7.463232037 }, 9 | { lat: 51.5177507, lon: 7.464755532 }, 10 | { latitude: 51.517657233, longitude: 7.466622349 }, 11 | { latitude: 51.51722995, longitude: 7.468317505 }, 12 | { latitude: 51.516816015, longitude: 7.47011995 }, 13 | { latitude: 51.516308606, longitude: 7.471793648 }, 14 | { latitude: 51.515974782, longitude: 7.472437378 }, 15 | { latitude: 51.515413951, longitude: 7.472845074 }, 16 | { latitude: 51.514559338, longitude: 7.472909447 }, 17 | { latitude: 51.512195717, longitude: 7.472651955 }, 18 | { latitude: 51.511127373, longitude: 7.47140741 }, 19 | { latitude: 51.51029939, longitude: 7.469948288 }, 20 | { latitude: 51.509831973, longitude: 7.468446251 }, 21 | { latitude: 51.509978876, longitude: 7.462481019 }, 22 | [7.460678574, 51.510913701], 23 | { latitude: 51.511594777, longitude: 7.459434029 }, 24 | { latitude: 51.512396029, longitude: 7.457695958 }, 25 | { latitude: 51.513317451, longitude: 7.45574331 }, 26 | ]; 27 | 28 | // TODO: elevation is not used yet. Re-add support for elevation later. 29 | const polygon2 = [ 30 | { latitude: 51.513357512, longitude: 7.45574331, elevation: 523.92 }, 31 | { latitude: 51.515400598, longitude: 7.45518541, elevation: 524.54 }, 32 | { latitude: 51.516241842, longitude: 7.456494328, elevation: 523.12 }, 33 | { latitude: 51.516722545, longitude: 7.459863183, elevation: 522.77 }, 34 | { latitude: 51.517443592, longitude: 7.463232037, elevation: 521.12 }, 35 | ]; 36 | 37 | describe('getBounds', () => { 38 | it('gets the bounds for a polygon', () => { 39 | expect(getBounds(polygon)).toEqual({ 40 | maxLat: 51.5177507, 41 | minLat: 51.509831973, 42 | maxLng: 7.472909447, 43 | minLng: 7.45518541, 44 | }); 45 | 46 | expect(getBounds(polygon2)).toEqual({ 47 | maxLat: 51.517443592, 48 | minLat: 51.513357512, 49 | maxLng: 7.463232037, 50 | minLng: 7.45518541, 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | setup: 4 | docker: 5 | - image: circleci/node:14.3 6 | steps: 7 | - add_ssh_keys: 8 | fingerprints: 9 | - '6a:78:0f:2d:57:a2:d1:83:1a:81:4e:af:76:d1:ff:31' 10 | - checkout 11 | - restore_cache: 12 | keys: 13 | - v1-dependencies-{{ checksum "package.json" }} 14 | - v1-dependencies- 15 | - run: 16 | name: Install dependencies 17 | command: yarn install 18 | - save_cache: 19 | paths: 20 | - node_modules 21 | key: v1-dependencies-{{ checksum "package.json" }} 22 | - persist_to_workspace: 23 | root: . 24 | paths: 25 | - '*' 26 | 27 | lint: 28 | docker: 29 | - image: circleci/node:14.3 30 | working_directory: ~/repo 31 | steps: 32 | - checkout 33 | - attach_workspace: 34 | at: ~/repo 35 | - run: 36 | name: Lint files 37 | command: yarn lint 38 | 39 | test: 40 | docker: 41 | - image: circleci/node:14.3 42 | working_directory: ~/repo 43 | steps: 44 | - checkout 45 | - attach_workspace: 46 | at: ~/repo 47 | - run: 48 | name: Run tests 49 | command: yarn test 50 | 51 | typecheck: 52 | docker: 53 | - image: circleci/node:14.3 54 | working_directory: ~/repo 55 | 56 | steps: 57 | - checkout 58 | - attach_workspace: 59 | at: ~/repo 60 | - run: 61 | name: Check types 62 | command: yarn tsc 63 | 64 | publish: 65 | docker: 66 | - image: circleci/node:14.3 67 | working_directory: ~/repo 68 | steps: 69 | - checkout 70 | - attach_workspace: 71 | at: ~/repo 72 | - run: 73 | name: Create builds 74 | command: yarn build 75 | 76 | - run: 77 | name: Publish new version 78 | command: yarn release 79 | 80 | workflows: 81 | version: 2 82 | check-and-publish: 83 | jobs: 84 | - setup 85 | - lint: 86 | requires: 87 | - setup 88 | - test: 89 | requires: 90 | - setup 91 | - typecheck: 92 | requires: 93 | - setup 94 | - publish: 95 | requires: 96 | - setup 97 | - lint 98 | - test 99 | - typecheck 100 | filters: 101 | branches: 102 | only: master 103 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as computeDestinationPoint } from './computeDestinationPoint'; 2 | export { default as convertArea } from './convertArea'; 3 | export { default as convertDistance } from './convertDistance'; 4 | export { default as convertSpeed } from './convertSpeed'; 5 | export { default as decimalToSexagesimal } from './decimalToSexagesimal'; 6 | export { default as findNearest } from './findNearest'; 7 | export { default as getAreaOfPolygon } from './getAreaOfPolygon'; 8 | export { default as getBounds } from './getBounds'; 9 | export { default as getBoundsOfDistance } from './getBoundsOfDistance'; 10 | export { default as getCenter } from './getCenter'; 11 | export { default as getCenterOfBounds } from './getCenterOfBounds'; 12 | export { default as getCompassDirection } from './getCompassDirection'; 13 | export { default as getCoordinateKey } from './getCoordinateKey'; 14 | export { default as getCoordinateKeys } from './getCoordinateKeys'; 15 | export { default as getDistance } from './getDistance'; 16 | export { default as getDistanceFromLine } from './getDistanceFromLine'; 17 | export { default as getGreatCircleBearing } from './getGreatCircleBearing'; 18 | export { default as getLatitude } from './getLatitude'; 19 | export { default as getLongitude } from './getLongitude'; 20 | export { default as getPathLength } from './getPathLength'; 21 | export { default as getPreciseDistance } from './getPreciseDistance'; 22 | export { default as getRhumbLineBearing } from './getRhumbLineBearing'; 23 | export { default as getRoughCompassDirection } from './getRoughCompassDirection'; 24 | export { default as getSpeed } from './getSpeed'; 25 | export { default as isDecimal } from './isDecimal'; 26 | export { default as isPointInLine } from './isPointInLine'; 27 | export { default as isPointInPolygon } from './isPointInPolygon'; 28 | export { default as isPointNearLine } from './isPointNearLine'; 29 | export { default as isPointWithinRadius } from './isPointWithinRadius'; 30 | export { default as isSexagesimal } from './isSexagesimal'; 31 | export { default as isValidCoordinate } from './isValidCoordinate'; 32 | export { default as isValidLatitude } from './isValidLatitude'; 33 | export { default as isValidLongitude } from './isValidLongitude'; 34 | export { default as orderByDistance } from './orderByDistance'; 35 | export { default as sexagesimalToDecimal } from './sexagesimalToDecimal'; 36 | export { default as toDecimal } from './toDecimal'; 37 | export { default as toRad } from './toRad'; 38 | export { default as toDeg } from './toDeg'; 39 | export { default as wktToPolygon } from './wktToPolygon'; 40 | export * from './constants'; 41 | -------------------------------------------------------------------------------- /src/getAreaOfPolygon.test.js: -------------------------------------------------------------------------------- 1 | import getAreaOfPolygon from './getAreaOfPolygon'; 2 | 3 | const wyoming = [ 4 | [-104.053615199998, 41.698218257724], 5 | [-104.053513414154, 41.9998153422964], 6 | [-104.056219380476, 42.6146696865973], 7 | [-104.056198856311, 43.0030623563908], 8 | [-104.059157507468, 43.4791339417582], 9 | [-104.057913943497, 43.5037122621461], 10 | [-104.059479420181, 43.8529065675403], 11 | [-104.059731381692, 44.1458254687842], 12 | [-104.061036140765, 44.1818252843501], 13 | [-104.059465130268, 44.5743526100096], 14 | [-104.059842395291, 44.9973362616199], 15 | [-105.041795987521, 45.0010758746085], 16 | [-105.08500310735, 44.9998170469188], 17 | [-106.021150701601, 44.9972137020636], 18 | [-106.259231717931, 44.9961625110408], 19 | [-107.894374357914, 44.9997736986363], 20 | [-108.259238500746, 45.000115150176], 21 | [-108.625256221974, 44.9975931654829], 22 | [-109.799385375449, 44.9995227676354], 23 | [-109.99552921526, 45.0027929256921], 24 | [-110.392759905743, 44.9986252880153], 25 | [-110.429649489646, 44.9922851168859], 26 | [-111.053428630452, 44.9956954937749], 27 | [-111.051615814026, 44.6644904630696], 28 | [-111.051560651262, 44.4733232643312], 29 | [-111.050405173289, 43.9825533508377], 30 | [-111.046771181184, 43.5155282322774], 31 | [-111.047498202203, 43.2847346290475], 32 | [-111.04921566545, 43.0198830902658], 33 | [-111.046780328328, 42.503251870505], 34 | [-111.04869741386, 41.9962033494069], 35 | [-111.051068773655, 41.578592411864], 36 | [-111.051651122482, 41.2584254005779], 37 | [-111.05102250907, 40.9965835985974], 38 | [-110.06318573561, 40.9978919528284], 39 | [-110.002165480573, 40.9975995171866], 40 | [-109.048314704754, 40.9984333935171], 41 | [-107.918671336725, 41.0033751160193], 42 | [-107.304051053295, 41.0001333468858], 43 | [-106.865438763821, 40.9984573861084], 44 | [-106.329125682765, 41.001288969127], 45 | [-106.203471481278, 41.0000850018961], 46 | [-105.278797604523, 40.9963491628159], 47 | [-104.934492922627, 40.9942891435778], 48 | [-104.05170553525, 41.00321132686], 49 | [-104.054012364451, 41.3880858190034], 50 | [-104.055500519791, 41.5642223678205], 51 | [-104.053615199998, 41.698218257724], 52 | ]; 53 | 54 | const roteErde = [ 55 | [7.453635617650258, 51.49320556213869], 56 | [7.454583481047989, 51.49328893754685], 57 | [7.454778172179346, 51.49240881084831], 58 | [7.453832678225655, 51.49231619246726], 59 | [7.453635617650258, 51.49320556213869], 60 | ]; 61 | 62 | describe('getAreaOfPolygon', () => { 63 | it('should correctly calculate the size of the state of wyoming', () => { 64 | expect(getAreaOfPolygon(wyoming)).toEqual(253416473809.58646); 65 | }); 66 | 67 | it('should correctly calculate the area of a soccer pitch', () => { 68 | expect(getAreaOfPolygon(roteErde)).toEqual(6595.2230501515005); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geolib", 3 | "version": "3.3.4", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "typings": "es/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "require": { 11 | "types": "./es/index.d.ts", 12 | "default": "./lib/index.js" 13 | }, 14 | "default": "./es/index.js" 15 | }, 16 | "./es/*": "./es/*.js", 17 | "./lib/*": "./lib/*.js", 18 | "./package.json": "./package.json" 19 | }, 20 | "files": [ 21 | "lib", 22 | "es" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/manuelbieh/geolib.git" 27 | }, 28 | "author": "Manuel Bieh", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/manuelbieh/geolib/issues" 32 | }, 33 | "homepage": "https://github.com/manuelbieh/geolib#readme", 34 | "scripts": { 35 | "babel": "babel", 36 | "build": "yarn clean && yarn build:es && yarn build:types && yarn build:umd", 37 | "build:es": "babel --minified --ignore **/*.test.js,**/*.test.ts --out-dir es --extensions .ts,.js --no-comments src", 38 | "build:types": "tsc --outDir es --emitDeclarationOnly --noEmit false --declaration", 39 | "build:umd": "webpack-cli --config webpack.config.js", 40 | "clean": "rimraf lib es", 41 | "lint": "eslint src/**/*.{js,ts}", 42 | "release": "semantic-release", 43 | "test": "jest", 44 | "typecheck": "tsc --noEmit" 45 | }, 46 | "devDependencies": { 47 | "@babel/cli": "^7.8.4", 48 | "@babel/core": "^7.9.6", 49 | "@babel/preset-env": "^7.9.6", 50 | "@babel/preset-typescript": "^7.9.0", 51 | "@semantic-release/changelog": "^5.0.1", 52 | "@semantic-release/git": "^9.0.0", 53 | "@semantic-release/npm": "^7.0.5", 54 | "@types/jest": "^25.2.3", 55 | "@types/node": "^14.0.5", 56 | "@typescript-eslint/eslint-plugin": "^3.0.0", 57 | "@typescript-eslint/parser": "^3.0.0", 58 | "@werkzeugkiste/eslint-config": "^2.0.0", 59 | "@werkzeugkiste/release-config": "^1.1.0", 60 | "babel-eslint": "^10.1.0", 61 | "babel-jest": "^26.0.1", 62 | "babel-loader": "^8.1.0", 63 | "confusing-browser-globals": "1.0.9", 64 | "eslint": "^7.1.0", 65 | "eslint-config-prettier": "^6.11.0", 66 | "eslint-import-resolver-typescript": "2.0.0", 67 | "eslint-plugin-babel": "5.3.0", 68 | "eslint-plugin-import": "^2.20.2", 69 | "eslint-plugin-prettier": "^3.1.3", 70 | "eslint-plugin-react": "^7.20.0", 71 | "eslint-plugin-react-hooks": "^4.0.2", 72 | "eslint-plugin-security": "1.4.0", 73 | "eslint-plugin-unicorn": "^20.0.0", 74 | "husky": "^4.2.5", 75 | "install-deps-postmerge": "^1.0.5", 76 | "jest": "^26.0.1", 77 | "lint-staged": "^10.2.6", 78 | "prettier": "^2.0.5", 79 | "rimraf": "^3.0.2", 80 | "semantic-release": "^17.0.7", 81 | "semantic-release-conventional-commits": "^2.0.1", 82 | "typescript": "^3.9.3", 83 | "webpack": "^4.43.0", 84 | "webpack-cli": "^3.3.11" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### [3.3.4](https://github.com/manuelbieh/geolib/compare/v3.3.3...v3.3.4) (2023-06-01) 2 | 3 | 4 | ### 🔧 Fixes 5 | 6 | * getRoughCompassDirection regex used "contains" logic rather than exact matching resulting incorrect results ([955937b](https://github.com/manuelbieh/geolib/commit/955937b6a0ec53a9ced3667c923ab413a31eb8c6)) 7 | * isPointWithinRadius false even if true ([249d047](https://github.com/manuelbieh/geolib/commit/249d047e238df95d8a62189d5c3245120bc4421d)) 8 | 9 | ### [3.3.3](https://github.com/manuelbieh/geolib/compare/v3.3.2...v3.3.3) (2021-10-11) 10 | 11 | 12 | ### 🔧 Fixes 13 | 14 | * ignore whitespaces in sexagesimal patterns. fixes [#254](https://github.com/manuelbieh/geolib/issues/254) ([47850ea](https://github.com/manuelbieh/geolib/commit/47850eaa1b8dcbd379b70d7af142ccd74760f0cc)) 15 | * increase accuracy ([2a7b443](https://github.com/manuelbieh/geolib/commit/2a7b443207fd508cb35e6f57c2c8c2899d012922)) 16 | * Merge pull request [#278](https://github.com/manuelbieh/geolib/issues/278) from PaulCapron/patch-1 ([3827a8f](https://github.com/manuelbieh/geolib/commit/3827a8f9acc1bf766c26b19ae0b96a9d3420c4c7)) 17 | 18 | ### [3.3.2](https://github.com/manuelbieh/geolib/compare/v3.3.1...v3.3.2) (2021-10-11) 19 | 20 | 21 | ### 🔧 Fixes 22 | 23 | * make native ESM importing from Node.js work ([7a850b7](https://github.com/manuelbieh/geolib/commit/7a850b784df3c342a10289e2c8da564d1297fbf1)) 24 | 25 | ### [3.3.1](https://github.com/manuelbieh/geolib/compare/v3.3.0...v3.3.1) (2020-05-24) 26 | 27 | 28 | ### 🔧 Fixes 29 | 30 | * add missing wktToPolygon export to UMD build. fixes [#221](https://github.com/manuelbieh/geolib/issues/221) ([e76848b](https://github.com/manuelbieh/geolib/commit/e76848b1f61bcb85d77ccd31b9cbaa176ffbc5b7)) 31 | 32 | ## [3.3.0](https://github.com/manuelbieh/geolib/compare/v3.2.2...v3.3.0) (2020-05-24) 33 | 34 | 35 | ### 🧩 Features 36 | 37 | * re-export constants so they can be used by library consumers ([1a5e214](https://github.com/manuelbieh/geolib/commit/1a5e214b78f15ef9783d0fda5c22c97c39c71a13)) 38 | 39 | 40 | ### 💉 Improvements 41 | 42 | * update all deps and make release workflow work with external config ([2cf5513](https://github.com/manuelbieh/geolib/commit/2cf5513992ba431414212596d6858cf6765cf8c5)) 43 | * update node image during ci ([17c821f](https://github.com/manuelbieh/geolib/commit/17c821f0104f75af1e37d90bd92e7eee2065fb71)) 44 | * use external release-config to publish new releases with automated CHANGELOG.md ([81b4bce](https://github.com/manuelbieh/geolib/commit/81b4bce833abea83fecd538126c348f27eee1810)) 45 | 46 | ## v2.0.24 47 | 48 | - Dropped support for IE6, IE7, IE8 49 | - Added new methods `geolib.latitude()`, `geolib.longitude()`, `geolib.elevation()` to get latitude, longitude or elevation of points. Will be converted to decimal format automatically 50 | - Added new method `geolib.extend()` to extend geolib object 51 | - Added support for GeoJSON format (`[lon, lat, elev]`) 52 | - Added property `geolib.version` to query the currently used version 53 | - Moved `geolib.elevation` to an optional module (`geolib.elevation.js`) 54 | - Using `Object.create(Geolib.prototype)` instead of object literal `{}` 55 | - New folder structure: compiled `geolib.js` can now be found in `dist/` instead of root dir 56 | - Improved Grunt build task 57 | -------------------------------------------------------------------------------- /src/getPreciseDistance.ts: -------------------------------------------------------------------------------- 1 | import getLatitude from './getLatitude'; 2 | import getLongitude from './getLongitude'; 3 | import toRad from './toRad'; 4 | import { earthRadius } from './constants'; 5 | import { GeolibInputCoordinates } from './types'; 6 | 7 | // Calculates geodetic distance between two points specified by latitude/longitude using 8 | // Vincenty inverse formula for ellipsoids. Taken from: 9 | // https://www.movable-type.co.uk/scripts/latlong-vincenty.html 10 | const getDistance = ( 11 | start: GeolibInputCoordinates, 12 | end: GeolibInputCoordinates, 13 | accuracy: number = 1 14 | ) => { 15 | accuracy = 16 | typeof accuracy !== 'undefined' && !isNaN(accuracy) ? accuracy : 1; 17 | 18 | const startLat = getLatitude(start); 19 | const startLon = getLongitude(start); 20 | const endLat = getLatitude(end); 21 | const endLon = getLongitude(end); 22 | 23 | const b = 6356752.314245; 24 | const ellipsoidParams = 1 / 298.257223563; // WGS-84 ellipsoid params 25 | const L = toRad(endLon - startLon); 26 | 27 | let cosSigma; 28 | let sigma; 29 | let sinAlpha; 30 | let cosSqAlpha; 31 | let cos2SigmaM; 32 | let sinSigma; 33 | 34 | const U1 = Math.atan( 35 | (1 - ellipsoidParams) * Math.tan(toRad(parseFloat(startLat))) 36 | ); 37 | const U2 = Math.atan( 38 | (1 - ellipsoidParams) * Math.tan(toRad(parseFloat(endLat))) 39 | ); 40 | const sinU1 = Math.sin(U1); 41 | const cosU1 = Math.cos(U1); 42 | const sinU2 = Math.sin(U2); 43 | const cosU2 = Math.cos(U2); 44 | 45 | let lambda = L; 46 | let lambdaP; 47 | let iterLimit = 100; 48 | do { 49 | const sinLambda = Math.sin(lambda); 50 | const cosLambda = Math.cos(lambda); 51 | sinSigma = Math.sqrt( 52 | cosU2 * sinLambda * (cosU2 * sinLambda) + 53 | (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) * 54 | (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) 55 | ); 56 | 57 | if (sinSigma === 0) { 58 | // co-incident points 59 | return 0; 60 | } 61 | 62 | cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda; 63 | sigma = Math.atan2(sinSigma, cosSigma); 64 | sinAlpha = (cosU1 * cosU2 * sinLambda) / sinSigma; 65 | cosSqAlpha = 1 - sinAlpha * sinAlpha; 66 | cos2SigmaM = cosSigma - (2 * sinU1 * sinU2) / cosSqAlpha; 67 | 68 | if (isNaN(cos2SigmaM)) { 69 | // equatorial line: cosSqAlpha=0 (§6) 70 | cos2SigmaM = 0; 71 | } 72 | const C = 73 | (ellipsoidParams / 16) * 74 | cosSqAlpha * 75 | (4 + ellipsoidParams * (4 - 3 * cosSqAlpha)); 76 | lambdaP = lambda; 77 | lambda = 78 | L + 79 | (1 - C) * 80 | ellipsoidParams * 81 | sinAlpha * 82 | (sigma + 83 | C * 84 | sinSigma * 85 | (cos2SigmaM + 86 | C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM))); 87 | } while (Math.abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0); 88 | 89 | if (iterLimit === 0) { 90 | // formula failed to converge 91 | return NaN; 92 | } 93 | 94 | const uSq = (cosSqAlpha * (earthRadius * earthRadius - b * b)) / (b * b); 95 | 96 | const A = 97 | 1 + (uSq / 16384) * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))); 98 | 99 | const B = (uSq / 1024) * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))); 100 | 101 | const deltaSigma = 102 | B * 103 | sinSigma * 104 | (cos2SigmaM + 105 | (B / 4) * 106 | (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - 107 | (B / 6) * 108 | cos2SigmaM * 109 | (-3 + 4 * sinSigma * sinSigma) * 110 | (-3 + 4 * cos2SigmaM * cos2SigmaM))); 111 | 112 | const distance = b * A * (sigma - deltaSigma); 113 | 114 | return Math.round(distance / accuracy) * accuracy; 115 | }; 116 | 117 | export default getDistance; 118 | -------------------------------------------------------------------------------- /src/getDistanceFromLine.test.js: -------------------------------------------------------------------------------- 1 | import getDistanceFromLine from './getDistanceFromLine'; 2 | 3 | describe('getDistanceFromLine', () => { 4 | it('should get the shortest distance from a point to a line of two points', () => { 5 | expect( 6 | getDistanceFromLine( 7 | { latitude: 51.516, longitude: 7.456 }, 8 | { latitude: 51.512, longitude: 7.456 }, 9 | { latitude: 51.516, longitude: 7.459 } 10 | ) 11 | ).toEqual(188.5131192933101); 12 | }); 13 | 14 | it('should not break if line start and line end are too close', () => { 15 | const point = { 16 | longitude: -75.63287336843746, 17 | latitude: 6.278381350919607, 18 | }; 19 | 20 | const lineStart = { 21 | longitude: -75.6220658304469, 22 | latitude: 6.285304104233529, 23 | }; 24 | 25 | const lineEnd = { 26 | longitude: -75.62216373107594, 27 | latitude: 6.285232119894652, 28 | }; 29 | 30 | expect(getDistanceFromLine(point, lineStart, lineEnd)).toEqual(1409); 31 | }); 32 | 33 | it('https://github.com/manuelbieh/geolib/issues/129', () => { 34 | expect( 35 | getDistanceFromLine( 36 | { 37 | latitude: 53.0281161107639, 38 | longitude: 5.64420448614743, 39 | }, 40 | { 41 | latitude: 53.028118, 42 | longitude: 5.644203, 43 | }, 44 | { 45 | latitude: 53.029021, 46 | longitude: 5.646562, 47 | }, 48 | 0.1 49 | ) 50 | ).not.toBeNaN(); 51 | 52 | expect( 53 | getDistanceFromLine( 54 | { 55 | latitude: 53.0515182362456, 56 | longitude: 5.67842625473533, 57 | }, 58 | { 59 | latitude: 53.051521, 60 | longitude: 5.678421, 61 | }, 62 | { 63 | latitude: 53.051652, 64 | longitude: 5.67852, 65 | }, 66 | 0.1 67 | ) 68 | ).not.toBeNaN(); 69 | 70 | expect( 71 | getDistanceFromLine( 72 | { 73 | latitude: 53.0933224175307, 74 | longitude: 5.61011575344944, 75 | }, 76 | { 77 | latitude: 53.093321, 78 | longitude: 5.610115, 79 | }, 80 | { 81 | latitude: 53.093236, 82 | longitude: 5.610037, 83 | }, 84 | 0.1 85 | ) 86 | ).not.toBeNaN(); 87 | 88 | expect( 89 | getDistanceFromLine( 90 | { 91 | latitude: 53.0867058030163, 92 | longitude: 5.59876618900706, 93 | }, 94 | { 95 | latitude: 53.086705, 96 | longitude: 5.598759, 97 | }, 98 | { 99 | latitude: 53.085538, 100 | longitude: 5.597901, 101 | }, 102 | 0.1 103 | ) 104 | ).not.toBeNaN(); 105 | 106 | expect( 107 | getDistanceFromLine( 108 | { 109 | latitude: 53.0657207151762, 110 | longitude: 5.60056383087291, 111 | }, 112 | { 113 | latitude: 53.065721, 114 | longitude: 5.600568, 115 | }, 116 | { 117 | latitude: 53.062609, 118 | longitude: 5.600793, 119 | }, 120 | 0.1 121 | ) 122 | ).not.toBeNaN(); 123 | }); 124 | 125 | it('should not return NaN if point is on line', () => { 126 | expect( 127 | getDistanceFromLine( 128 | { 129 | latitude: 53, 130 | longitude: 5, 131 | }, 132 | { 133 | latitude: 53, 134 | longitude: 5, 135 | }, 136 | { 137 | latitude: 54, 138 | longitude: 6, 139 | }, 140 | 1 141 | ) 142 | ).not.toBeNaN(); 143 | }); 144 | 145 | it('should not return NaN if lineStart and lineEnd are (effectively) the same', () => { 146 | expect( 147 | getDistanceFromLine( 148 | { 149 | latitude: 51.5588, 150 | longitude: 7.06044, 151 | }, 152 | { 153 | latitude: 51.42829895019531, 154 | longitude: 7.05250883102417, 155 | }, 156 | { 157 | latitude: 51.42829895019531, 158 | longitude: 7.0525078773498535, 159 | }, 160 | 1 161 | ) 162 | ).not.toBeNaN(); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "C:\\cygwin64\\tmp\\jest", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: null, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "\\\\node_modules\\\\" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: [ 65 | // "node_modules" 66 | // ], 67 | 68 | // An array of file extensions your modules use 69 | moduleFileExtensions: ['js', 'json', 'ts'], 70 | 71 | // A map from regular expressions to module names that allow to stub out resources with a single module 72 | // moduleNameMapper: {}, 73 | 74 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 75 | // modulePathIgnorePatterns: [], 76 | 77 | // Activates notifications for test results 78 | // notify: false, 79 | 80 | // An enum that specifies notification mode. Requires { notify: true } 81 | // notifyMode: "failure-change", 82 | 83 | // A preset that is used as a base for Jest's configuration 84 | // preset: null, 85 | 86 | // Run tests from one or more projects 87 | // projects: null, 88 | 89 | // Use this configuration option to add custom reporters to Jest 90 | // reporters: undefined, 91 | 92 | // Automatically reset mock state between every test 93 | // resetMocks: false, 94 | 95 | // Reset the module registry before running each individual test 96 | // resetModules: false, 97 | 98 | // A path to a custom resolver 99 | // resolver: null, 100 | 101 | // Automatically restore mock state between every test 102 | // restoreMocks: false, 103 | 104 | // The root directory that Jest should scan for tests and modules within 105 | // rootDir: null, 106 | 107 | // A list of paths to directories that Jest should use to search for files in 108 | // roots: [ 109 | // "" 110 | // ], 111 | 112 | // Allows you to use a custom runner instead of Jest's default test runner 113 | // runner: "jest-runner", 114 | 115 | // The paths to modules that run some code to configure or set up the testing environment before each test 116 | // setupFiles: [], 117 | 118 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 119 | // setupFilesAfterEnv: [], 120 | 121 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 122 | // snapshotSerializers: [], 123 | 124 | // The test environment that will be used for testing 125 | testEnvironment: 'node', 126 | 127 | // Options that will be passed to the testEnvironment 128 | // testEnvironmentOptions: {}, 129 | 130 | // Adds a location field to test results 131 | // testLocationInResults: false, 132 | 133 | // The glob patterns Jest uses to detect test files 134 | // testMatch: [ 135 | // "**/__tests__/**/*.[jt]s?(x)", 136 | // "**/?(*.)+(spec|test).[tj]s?(x)" 137 | // ], 138 | 139 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 140 | // testPathIgnorePatterns: [ 141 | // "\\\\node_modules\\\\" 142 | // ], 143 | 144 | // The regexp pattern or array of patterns that Jest uses to detect test files 145 | // testRegex: [], 146 | 147 | // This option allows the use of a custom results processor 148 | // testResultsProcessor: null, 149 | 150 | // This option allows use of a custom test runner 151 | // testRunner: "jasmine2", 152 | 153 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 154 | // testURL: "http://localhost", 155 | 156 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 157 | // timers: "real", 158 | 159 | // A map from regular expressions to paths to transformers 160 | // transform: null, 161 | 162 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 163 | // transformIgnorePatterns: [ 164 | // "\\\\node_modules\\\\" 165 | // ], 166 | 167 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 168 | // unmockedModulePathPatterns: undefined, 169 | 170 | // Indicates whether each individual test should be reported during the run 171 | verbose: true, 172 | 173 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 174 | // watchPathIgnorePatterns: [], 175 | 176 | // Whether to use watchman for file crawling 177 | // watchman: true, 178 | }; 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Geolib 2 | 3 | Library to provide basic geospatial operations like distance calculation, conversion of decimal coordinates to sexagesimal and vice versa, etc. This library is currently **2D**, meaning that altitude/elevation is not yet supported by any of its functions! 4 | 5 | [![CircleCI](https://circleci.com/gh/manuelbieh/geolib/tree/master.svg?style=svg)](https://circleci.com/gh/manuelbieh/geolib/tree/master) 6 | ![](https://badgen.net/bundlephobia/minzip/geolib) 7 | ![](https://badgen.net/npm/dm/geolib) 8 | ![](https://badgen.net/github/license/manuelbieh/geolib) 9 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 10 | 11 | ## Changelog 12 | 13 | A detailed changelog can be found in [CHANGELOG.md](./CHANGELOG.md) 14 | 15 | ## Install 16 | 17 | ```sh 18 | npm install geolib 19 | ``` 20 | 21 | ```sh 22 | yarn add geolib 23 | ``` 24 | 25 | ## Usage 26 | 27 | There is a **UMD** build and an **ES Module** build. You can either use the UMD build in Node like any other library: 28 | 29 | ```js 30 | const geolib = require('geolib'); 31 | ``` 32 | 33 | or in the browser by using a simple script element: 34 | 35 | ```html 36 | 37 | ``` 38 | 39 | If you load it in the browser, you can access all the functions via `window.geolib`. 40 | 41 | If you're working with a bundler (like Webpack or Parcel) or have an environment that supports ES Modules natively, you can either import certain functions from the package directly: 42 | 43 | ```js 44 | import { getDistance } from 'geolib'; 45 | ``` 46 | 47 | or load the whole library: 48 | 49 | ```js 50 | import * as geolib from 'geolib'; 51 | ``` 52 | 53 | or you can import single functions directly to potentially make use of treeshaking (recommended): 54 | 55 | ```js 56 | import getDistance from 'geolib/es/getDistance'; 57 | ``` 58 | 59 | ## General 60 | 61 | This library is written in TypeScript. You don't have to know TypeScript to use Geolib but the [type definitions](./src/types.ts) give you valuable information about the general usage, input parameters etc. 62 | 63 | ### Supported values and formats 64 | 65 | All methods that are working with coordinates accept either an object with a `lat`/`latitude` **and** a `lon`/`lng`/`longitude` property, **or** a GeoJSON coordinates array, like: `[lon, lat]`. All values can be either in decimal (`53.471`) or sexagesimal (`53° 21' 16"`) format. 66 | 67 | Distance values are **always** floats and represent the distance in **meters**. 68 | 69 | ## Functions 70 | 71 | ### `getDistance(start, end, accuracy = 1)` 72 | 73 | Calculates the distance between two geo coordinates. 74 | 75 | This function takes up to 3 arguments. First 2 arguments must be valid `GeolibInputCoordinates` (e.g. `{latitude: 52.518611, longitude: 13.408056}`). Coordinates can be in sexagesimal or decimal format. The third argument is accuracy (in meters). By default the accuracy is 1 meter. If you need a more accurate result, you can set it to a lower value, e.g. to `0.01` for centimeter accuracy. You can set it higher to have the result rounded to the next value that is divisible by your chosen accuracy (e.g. `25428` with an accuracy of `100` becomes `25400`). 76 | 77 | ```js 78 | getDistance( 79 | { latitude: 51.5103, longitude: 7.49347 }, 80 | { latitude: "51° 31' N", longitude: "7° 28' E" } 81 | ); 82 | ``` 83 | 84 | ```js 85 | // Working with W3C Geolocation API 86 | navigator.geolocation.getCurrentPosition( 87 | (position) => { 88 | console.log( 89 | 'You are ', 90 | geolib.getDistance(position.coords, { 91 | latitude: 51.525, 92 | longitude: 7.4575, 93 | }), 94 | 'meters away from 51.525, 7.4575' 95 | ); 96 | }, 97 | () => { 98 | alert('Position could not be determined.'); 99 | } 100 | ); 101 | ``` 102 | 103 | Returns the distance in meters as a numeric value. 104 | 105 | ### `getPreciseDistance(start, end, accuracy = 1)` 106 | 107 | Calculates the distance between two geo coordinates. This method is more accurate then `getDistance`, especially for long distances but it is also slower. It is using the Vincenty inverse formula for ellipsoids. 108 | 109 | It takes the same (up to 3) arguments as `getDistance`. 110 | 111 | ```js 112 | geolib.getPreciseDistance( 113 | { latitude: 51.5103, longitude: 7.49347 }, 114 | { latitude: "51° 31' N", longitude: "7° 28' E" } 115 | ); 116 | ``` 117 | 118 | ### `getCenter(coords)` 119 | 120 | Calculates the geographical center of all points in a collection of geo coordinates. Takes an array of coordinates and calculates the center of it. 121 | 122 | ```js 123 | geolib.getCenter([ 124 | { latitude: 52.516272, longitude: 13.377722 }, 125 | { latitude: 51.515, longitude: 7.453619 }, 126 | { latitude: 51.503333, longitude: -0.119722 }, 127 | ]); 128 | ``` 129 | 130 | Returns an object: 131 | 132 | ```js 133 | { 134 | "latitude": centerLat, 135 | "longitude": centerLon 136 | } 137 | ``` 138 | 139 | ### `getCenterOfBounds(coords)` 140 | 141 | Calculates the center of the bounds of geo coordinates. 142 | 143 | Takes an array of coordinates, calculate the border of those, and gives back the center of that rectangle. 144 | 145 | On polygons like political borders (eg. states), this may gives a closer result to human expectation, than `getCenter`, because that function can be disturbed by uneven distribution of point in different sides. 146 | 147 | Imagine the US state Oklahoma: `getCenter` on that gives a southern point, because the southern border contains a lot more nodes, than the others. 148 | 149 | ```js 150 | geolib.getCenterOfBounds([ 151 | { latitude: 51.513357512, longitude: 7.45574331 }, 152 | { latitude: 51.515400598, longitude: 7.45518541 }, 153 | { latitude: 51.516241842, longitude: 7.456494328 }, 154 | { latitude: 51.516722545, longitude: 7.459863183 }, 155 | { latitude: 51.517443592, longitude: 7.463232037 }, 156 | ]); 157 | ``` 158 | 159 | Returns an object: 160 | 161 | ```js 162 | { 163 | "latitude": centerLat, 164 | "longitude": centerLng 165 | } 166 | ``` 167 | 168 | ### `getBounds(points)` 169 | 170 | Calculates the bounds of geo coordinates. 171 | 172 | ```js 173 | geolib.getBounds([ 174 | { latitude: 52.516272, longitude: 13.377722 }, 175 | { latitude: 51.515, longitude: 7.453619 }, 176 | { latitude: 51.503333, longitude: -0.119722 }, 177 | ]); 178 | ``` 179 | 180 | It returns minimum and maximum latitude and minimum and maximum longitude as an object: 181 | 182 | ```js 183 | { 184 | "minLat": minimumLatitude, 185 | "maxLat": maximumLatitude, 186 | "minLng": minimumLongitude, 187 | "maxLng": maximumLongitude, 188 | } 189 | ``` 190 | 191 | ### `isPointInPolygon(point, polygon)` 192 | 193 | Checks whether a point is inside of a polygon or not. 194 | 195 | ```js 196 | geolib.isPointInPolygon({ latitude: 51.5125, longitude: 7.485 }, [ 197 | { latitude: 51.5, longitude: 7.4 }, 198 | { latitude: 51.555, longitude: 7.4 }, 199 | { latitude: 51.555, longitude: 7.625 }, 200 | { latitude: 51.5125, longitude: 7.625 }, 201 | ]); 202 | ``` 203 | 204 | Returns `true` or `false` 205 | 206 | ### `isPointWithinRadius(point, centerPoint, radius)` 207 | 208 | Checks whether a point is inside of a circle or not. 209 | 210 | ```js 211 | // checks if 51.525/7.4575 is within a radius of 5 km from 51.5175/7.4678 212 | geolib.isPointWithinRadius( 213 | { latitude: 51.525, longitude: 7.4575 }, 214 | { latitude: 51.5175, longitude: 7.4678 }, 215 | 5000 216 | ); 217 | ``` 218 | 219 | Returns `true` or `false` 220 | 221 | ### `getRhumbLineBearing(origin, destination)` 222 | 223 | Gets rhumb line bearing of two points. Find out about the difference between rhumb line and great circle bearing on Wikipedia. Rhumb line should be fine in most cases: 224 | 225 | http://en.wikipedia.org/wiki/Rhumb_line#General_and_mathematical_description 226 | 227 | Function is heavily based on Doug Vanderweide's great PHP version (licensed under GPL 3.0) 228 | http://www.dougv.com/2009/07/13/calculating-the-bearing-and-compass-rose-direction-between-two-latitude-longitude-coordinates-in-php/ 229 | 230 | ```js 231 | geolib.getRhumbLineBearing( 232 | { latitude: 52.518611, longitude: 13.408056 }, 233 | { latitude: 51.519475, longitude: 7.46694444 } 234 | ); 235 | ``` 236 | 237 | Returns calculated bearing as number. 238 | 239 | ### `getGreatCircleBearing(origin, destination)` 240 | 241 | Gets great circle bearing of two points. This is more accurate than rhumb line bearing but also slower. 242 | 243 | ```js 244 | geolib.getGreatCircleBearing( 245 | { latitude: 52.518611, longitude: 13.408056 }, 246 | { latitude: 51.519475, longitude: 7.46694444 } 247 | ); 248 | ``` 249 | 250 | Returns calculated bearing as number. 251 | 252 | ### `getCompassDirection(origin, destination, bearingFunction = getRhumbLineBearing)` 253 | 254 | Gets the compass direction from an origin coordinate to a destination coordinate. Optionally a function to determine the bearing can be passed as third parameter. Default is `getRhumbLineBearing`. 255 | 256 | ```js 257 | geolib.getCompassDirection( 258 | { latitude: 52.518611, longitude: 13.408056 }, 259 | { latitude: 51.519475, longitude: 7.46694444 } 260 | ); 261 | ``` 262 | 263 | Returns the direction (e.g. `NNE`, `SW`, `E`, …) as string. 264 | 265 | ### `orderByDistance(point, arrayOfPoints)` 266 | 267 | Sorts an array of coords by distance to a reference coordinate. 268 | 269 | ```js 270 | geolib.orderByDistance({ latitude: 51.515, longitude: 7.453619 }, [ 271 | { latitude: 52.516272, longitude: 13.377722 }, 272 | { latitude: 51.518, longitude: 7.45425 }, 273 | { latitude: 51.503333, longitude: -0.119722 }, 274 | ]); 275 | ``` 276 | 277 | Returns an array of points ordered by their distance to the reference point. 278 | 279 | ### `findNearest(point, arrayOfPoints)` 280 | 281 | Finds the single one nearest point to a reference coordinate. It's actually just a convenience method that uses `orderByDistance` under the hood and returns the first result. 282 | 283 | ```js 284 | geolib.findNearest({ latitude: 52.456221, longitude: 12.63128 }, [ 285 | { latitude: 52.516272, longitude: 13.377722 }, 286 | { latitude: 51.515, longitude: 7.453619 }, 287 | { latitude: 51.503333, longitude: -0.119722 }, 288 | { latitude: 55.751667, longitude: 37.617778 }, 289 | { latitude: 48.8583, longitude: 2.2945 }, 290 | { latitude: 59.3275, longitude: 18.0675 }, 291 | { latitude: 59.916911, longitude: 10.727567 }, 292 | ]); 293 | ``` 294 | 295 | Returns the point nearest to the reference point. 296 | 297 | ### `getPathLength(points, distanceFunction = getDistance)` 298 | 299 | Calculates the length of a collection of coordinates. Expects an array of points as first argument and optionally a function to determine the distance as second argument. Default is `getDistance`. 300 | 301 | ```js 302 | geolib.getPathLength([ 303 | { latitude: 52.516272, longitude: 13.377722 }, 304 | { latitude: 51.515, longitude: 7.453619 }, 305 | { latitude: 51.503333, longitude: -0.119722 }, 306 | ]); 307 | ``` 308 | 309 | Returns the length of the path in meters as number. 310 | 311 | ### `getDistanceFromLine(point, lineStart, lineEnd, accuracy = 1)` 312 | 313 | Gets the minimum distance from a point to a line of two points. 314 | 315 | ```js 316 | geolib.getDistanceFromLine( 317 | { latitude: 51.516, longitude: 7.456 }, 318 | { latitude: 51.512, longitude: 7.456 }, 319 | { latitude: 51.516, longitude: 7.459 } 320 | ); 321 | ``` 322 | 323 | Returns the shortest distance to the given line as number. 324 | 325 | **Note:** if all points are too close together the function might return NaN. In this case it usually helps to slightly increase the accuracy (e.g. `0.01`). 326 | 327 | ### `getBoundsOfDistance(point, distance)` 328 | 329 | Computes the bounding coordinates of all points on the surface of the earth less than or equal to the specified great circle distance. 330 | 331 | ```js 332 | geolib.getBoundsOfDistance( 333 | { latitude: 34.090166, longitude: -118.276736555556 }, 334 | 1000 335 | ); 336 | ``` 337 | 338 | Returns an array with the southwestern and northeastern coordinates. 339 | 340 | ### `isPointInLine(point, lineStart, lineEnd)` 341 | 342 | Calculates if given point lies in a line formed by start and end. 343 | 344 | ```js 345 | geolib.isPointInLine( 346 | { latitude: 0, longitude: 10 }, 347 | { latitude: 0, longitude: 0 }, 348 | { latitude: 0, longitude: 15 } 349 | ); 350 | ``` 351 | 352 | ### `sexagesimalToDecimal(value)` 353 | 354 | Converts a sexagesimal coordinate into decimal format 355 | 356 | ```js 357 | geolib.sexagesimalToDecimal(`51° 29' 46" N`); 358 | ``` 359 | 360 | Returns the new value as decimal number. 361 | 362 | ### `decimalToSexagesimal(value)` 363 | 364 | Converts a decimal coordinate to sexagesimal format 365 | 366 | ```js 367 | geolib.decimalToSexagesimal(51.49611111); // -> 51° 29' 46` 368 | ``` 369 | 370 | Returns the new value as sexagesimal string. 371 | 372 | ### `geolib.getLatitude(point, raw = false)` 373 | 374 | ### `geolib.getLongitude(point, raw = false)` 375 | 376 | Returns the latitude/longitude for a given point **and** converts it to decimal. If the second argument is set to true it does **not** convert the value to decimal. 377 | 378 | ```js 379 | geolib.getLatitude({ lat: 51.49611, lng: 7.38896 }); // -> 51.49611 380 | geolib.getLongitude({ lat: 51.49611, lng: 7.38896 }); // -> 7.38896 381 | ``` 382 | 383 | Returns the value as decimal or in its original format if the second argument was set to true. 384 | 385 | ### `toDecimal(point)` 386 | 387 | Checks if a coordinate is already in decimal format and, if not, converts it to. Works with single values (e.g. `51° 32' 17"`) and complete coordinates (e.g. `{lat: 1, lon: 1}`) as long as it in a [supported format](#supported-values-and-formats). 388 | 389 | ```js 390 | geolib.toDecimal(`51° 29' 46" N`); // -> 51.59611111 391 | geolib.toDecimal(51.59611111); // -> 51.59611111 392 | ``` 393 | 394 | Returns a decimal value for the given input value. 395 | 396 | ### `computeDestinationPoint(point, distance, bearing, radius = earthRadius)` 397 | 398 | Computes the destination point given an initial point, a distance (in meters) and a bearing (in degrees). If no radius is given it defaults to the mean earth radius of 6,371,000 meters. 399 | 400 | Attention: this formula is not _100%_ accurate (but very close though). 401 | 402 | ```js 403 | geolib.computeDestinationPoint( 404 | { latitude: 52.518611, longitude: 13.408056 }, 405 | 15000, 406 | 180 407 | ); 408 | ``` 409 | 410 | ```js 411 | geolib.computeDestinationPoint( 412 | [13.408056, 52.518611] 413 | 15000, 414 | 180 415 | ); 416 | ``` 417 | 418 | Returns the destination in the same format as the input coordinates. So if you pass a GeoJSON point, you will get a GeoJSON point. 419 | 420 | ### `getAreaOfPolygon(points)` 421 | 422 | Calculates the surface area of a polygon. 423 | 424 | ```js 425 | geolib.getAreaOfPolygon([ 426 | [7.453635617650258, 51.49320556213869], 427 | [7.454583481047989, 51.49328893754685], 428 | [7.454778172179346, 51.49240881084831], 429 | [7.453832678225655, 51.49231619246726], 430 | [7.453635617650258, 51.49320556213869], 431 | ]); 432 | ``` 433 | 434 | Returns the result as number in square meters. 435 | 436 | ### `getCoordinateKeys(point)` 437 | 438 | Gets the property names of that are used in the point in a normalized form: 439 | 440 | ```js 441 | geolib.getCoordinateKeys({ lat: 1, lon: 1 }); 442 | // -> { latitude: 'lat', longitude: 'lon' } 443 | ``` 444 | 445 | Returns an object with a `latitude` and a `longitude` property. Their values are the property names for latitude and longitude that are used in the passed point. Should probably only be used internally. 446 | 447 | ### `getCoordinateKey(point, keysToLookup)` 448 | 449 | Is used by `getCoordinateKeys` under the hood and returns the property name out of a list of possible names. 450 | 451 | ```js 452 | geolib.getCoordinateKey({ latitude: 1, longitude: 2 }, ['lat', 'latitude']); 453 | // -> latitude 454 | ``` 455 | 456 | Returns the name of the property as string or `undefined` if no there was no match. 457 | 458 | ### `isValidCoordinate(point)` 459 | 460 | Checks if a given point has at least a **latitude** and a **longitude** and is in a supported format. 461 | 462 | ```js 463 | // true: 464 | geolib.isValidCoordinate({ latitude: 1, longitude: 2 }); 465 | 466 | // false, longitude is missing: 467 | geolib.isValidCoordinate({ latitude: 1 }); 468 | 469 | // true, GeoJSON format: 470 | geolib.isValidCoordinate([2, 1]); 471 | ``` 472 | 473 | Returns `true` or `false`. 474 | 475 | ### `getSpeed(startPointWithTime, endPointWithTime)` 476 | 477 | Calculates the speed between two points within a given time span. 478 | 479 | ```js 480 | geolib.getSpeed( 481 | { latitude: 51.567294, longitude: 7.38896, time: 1360231200880 }, 482 | { latitude: 52.54944, longitude: 13.468509, time: 1360245600880 } 483 | ); 484 | ``` 485 | 486 | Return the speed in meters per second as number. 487 | 488 | ### `convertSpeed(value, unit)` 489 | 490 | Converts the result from `getSpeed` into a more human friendly format. Currently available units are `mph` and `kmh`. 491 | 492 | #### Units 493 | 494 | `unit` can be one of: 495 | 496 | - kmh (kilometers per hour) 497 | - mph (miles per hour) 498 | 499 | ```js 500 | geolib.convertSpeed(29.8678, 'kmh'); 501 | ``` 502 | 503 | Returns the converted value as number. 504 | 505 | ### `convertDistance(value, unit)` 506 | 507 | Converts a given distance (in meters) into another unit. 508 | 509 | #### Units 510 | 511 | `unit` can be one of: 512 | 513 | - m (meter) 514 | - km (kilometers) 515 | - cm (centimeters) 516 | - mm (millimeters) 517 | - mi (miles) 518 | - sm (seamiles) 519 | - ft (feet) 520 | - in (inches) 521 | - yd (yards) 522 | 523 | ```js 524 | geolib.convertDistance(14200, 'km'); // 14.2 525 | geolib.convertDistance(500, 'km'); // 0.5 526 | ``` 527 | 528 | Returns the converted distance as number. 529 | 530 | ### `convertArea(value, unit)` 531 | 532 | Converts the result from `getAreaForPolygon` into a different unit. 533 | 534 | #### Units 535 | 536 | `unit` can be one of: 537 | 538 | - m2, sqm (square meters) 539 | - km2, sqkm (square kilometers) 540 | - ha (hectares) 541 | - a (ares) 542 | - ft2, sqft (square feet) 543 | - yd2, sqyd (square yards) 544 | - in2, sqin (square inches) 545 | 546 | ```js 547 | geolib.convertArea(298678, 'km2')); 548 | ``` 549 | 550 | Returns the converted area as number. 551 | 552 | ### `wktToPolygon(wkt)` 553 | 554 | Converts the Well-known text (a.k.a WKT) to polygon that Geolib understands. 555 | [https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Geometric_Objects](WKT) 556 | 557 | ```js 558 | geolib.wktToPolygon('POLYGON ((30 10.54321, 40 40, 20 40, 10 20, 30 10))'); 559 | // [ 560 | // { latitude: 10.54321, longitude: 30 }, 561 | // { latitude: 40, longitude: 40 }, 562 | // { latitude: 40, longitude: 20 }, 563 | // { latitude: 20, longitude: 10 }, 564 | // { latitude: 10, longitude: 30 },} 565 | // ] 566 | ``` 567 | 568 | Returns the array of coordinates. 569 | 570 | ## Breaking Changes in 3.0.0 and migration from 2.x.x 571 | 572 | In version 3.0.0 I'm trying to get a little bit _back to the roots_. **Geolib** was once started because I needed a handful of methods to perform very specific geo related tasks like getting the distance or the direction between two points. Since it was one of the very first libraries on npm back then to do these kind of things in a very simple way it became very popular (with more than 300k downloads per month as of April 2019!) and as a consequence got a lot of contributions over the years. Many of which I just merged as long as they had accompanying tests, without looking at consistency, conventions, complexity, coding style or even the overall quality of the functions that I sometimes didn't even fully understand. 573 | 574 | I have now cleaned up the codebase completely, rebuilt the entire library "from scratch", unified all the function arguments, removed a few functions where I wasn't sure if they should be in here (feel free to add them back of you're using them!) or if they were even used (did a few searches on GitHub for the function names, turned out there are zero results). 575 | 576 | Elevation support was dropped, as well as a few functions that unnecessarily made the library really large in size (e.g. `isPointInsideRobust` alone was over 700[!] lines of code and was basically taken from a [different library](https://github.com/mikolalysenko/robust-point-in-polygon)). 577 | 578 | I removed Grunt from the build process, added "modern" tools like ESLint and Prettier. I switched from Travis CI to Circle CI and I am in the process of further automating the release process of new versions using `semantic-release` and `conventional-commits`. I also switched from pure JavaScript to TypeScript because I think it does have some benefits. 579 | 580 | - All functions are pure functions now. No input data is mutated anymore. You give the same input, you get the same output. No side effects or whatsoever. 581 | - I changed the default `getDistance` function from being the slow, accurate one to being the fast, slightly inaccurate one. The old `getDistance` function is now named `getPreciseDistance` while the old `getDistanceSimple` function is now the default `getDistance` function. You can, however, pass `getPreciseDistance` as argument to any function that uses distance calculation internally. 582 | - Artificial limitation to 8 decimal places in decimal coordinates was removed 583 | - `getBoundsOfDistance()` now returns the _exact_ coordinates due to the removal of the artificial 8 decimal place limitation 584 | - `getCompassDirection()` does no longer return an object with an _exact_ and a _rough_ direction but only the exact direction as string 585 | - third argument to `getCompassDirection()` is no longer a string ("circle", "line") but a function to determine the bearing (you can pass `getRhumbLineBearing` or `getGreatCircleBearing`). The function receives the origin and the destination as first and second argument. If no 3rd argument was given, `getRhumbLineBearing(origin, dest)` is used by default. 586 | - There is now a new helper function `roughCompassDirection(exact)` if you _really_ only need a very rough (and potentially inaccurate or inappropriate) direction. Better don't use it. 587 | - `orderByDistance()` does no longer modify its input so does not add a `distance` and `key` property to the returned coordinates. 588 | - The result of `getSpeed()` is now always returned as **meters per second**. It can be converted using the new convenience function `convertSpeed(mps, targetUnit)` 589 | - Relevant value (usually point or distance) is now consistently the **first** argument for each function (it wasn't before, how confusing is that?) 590 | - `findNearest()` does no longer take `offset` and `limit` arguments. It's only a convenience method to get the single one nearest point from a set of coordinates. If you need more than one, have a look at the implementation and implement your own logic using `orderByDistance` 591 | - Whereever distances are involved, they are returned as meters or meters per second. No more inconsistent defaults like kilometers or kilometers per hour. 592 | - The method how sexagesimal is formatted differs a little bit. It may now potentially return ugly float point units like `52° 46' 21.0004"` in rare cases but it is also more accurate than it was before. 593 | - Dropped support for Meteor (feel free to add it back if you like) 594 | 595 | ### ✅ Functions with the same name 596 | 597 | - `computeDestinationPoint` 598 | - `getBounds` 599 | - `getBoundsOfDistance` 600 | - `getCenter` 601 | - `getCenterOfBounds` 602 | - `getCompassDirection` 603 | - `getDistanceFromLine` 604 | - `getPathLength` 605 | - `getRhumbLineBearing` 606 | - `getSpeed` 607 | - `isDecimal` 608 | - `isPointInLine` 609 | - `isPointNearLine` 610 | - `isSexagesimal` 611 | - `orderByDistance` 612 | 613 | ### ❗ Renamed functions 614 | 615 | - `getKeys` renamed to `getCoordinateKeys` 616 | - `validate` renamed to `isValidCoordinate` 617 | - `getLat` renamed to `getLatitude` 618 | - `getLon` renamed to `getLongitude` 619 | - `latitude` -> renamed to `getLatitude` 620 | - `longitude` -> renamed to `getLongitude` 621 | - `convertUnit` -> remamed to convertDistance, because name was too ambiguous 622 | - `useDecimal` renamed to `toDecimal` 623 | - `decimal2sexagesimal` renamed to `decimalToSexagesimal` 624 | - `sexagesimal2decimal` renamed to `sexagesimalToDecimal` 625 | - `getDistance` renamed to `getPreciseDistance` 626 | - `getDistanceSimple` renamed to `getDistance` 627 | - `isPointInside` renamed to `isPointInPolygon` 628 | - `isPointInCircle` renamed to `isPointWithinRadius` 629 | - `getBearing` renamed to `getGreatCircleBearing` to be more explicit 630 | 631 | ### 🗑 Removed functions 632 | 633 | - `getElev` -> removed 634 | - `elevation` -> removed 635 | - `coords` -> removed (might be re-added as getCoordinate or getNormalizedCoordinate) 636 | - `ll` -> removed (because wtf?) 637 | - `preparePolygonForIsPointInsideOptimized` -> removed due to missing documentation and missing tests 638 | - `isPointInsideWithPreparedPolygon` -> removed due to missing documentation 639 | - `isInside` alias -> removed (too ambiguous) - use `isPointInPolygon` or `isPointWithinRadius` 640 | - `withinRadius` -> removed, use `isPointWithinRadius` 641 | - `getDirection` alias -> removed (unnecessary clutter) - use `getCompassDirection` 642 | 643 | ### 🆕 Added functions 644 | 645 | - `getAreaOfPolygon` to calculate the area of a polygon 646 | - `getCoordinateKey` to get a property name (e.g. `lat` or `lng` of an object based on an array of possible names) 647 | --------------------------------------------------------------------------------