Skip to content

Commit

Permalink
feat: add DmsBlockSuffixedHemisphereFormat for parsing DMS block suff…
Browse files Browse the repository at this point in the history
…ixed hemisphere coordinates and include related tests
  • Loading branch information
reskume committed Dec 4, 2024
1 parent 09a9852 commit 01041a5
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 5 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# OpenAIP Coordinate Parser

Easily extendable and configurable parser for NodeJS and the browser that extracts coordinates from a variety of formatted lat/lon strings. The main focus is parsing various decimal or DMS format variations. Contributions that
Easily extendable and configurable parser for NodeJS and the browser that extracts coordinates from a variety
of formatted lat/lon strings. The main focus is parsing various decimal or DMS format variations. Contributions that
add other format parsers are always welcome!

### Usage
Expand Down Expand Up @@ -133,5 +134,6 @@ Currently the out-of-the-box format parsers supports the following various forma
- `N044506 E1030342`
- `N044506E1030342`
- `N044506.123 E1030342.123`
- ``
- ``
- `044506N 1030342E`
- `044506N1030342E`
- `044506.123N 1030342.123E`
2 changes: 1 addition & 1 deletion src/formats/dms-block-prefixed-hemisphere-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Coordinate } from '../types.js';
import { validateSchema } from '../validate-schema.js';
import { BaseFormat } from './base-format.js';

const REGEX = /^([NS])(\d{2})(\d{2})(\d{2}(\.\d+)?),?\s*([EW])(\d{3})(\d{2})(\d{2}(\.\d+)?)$/;
const REGEX = /^([NS])(\d{2})(\d{2})(\d{2}(\.\d+)?)\s*,?\s*([EW])(\d{3})(\d{2})(\d{2}(\.\d+)?)$/;

/**
* Parses coordinates strings in DMS block prefixed hemisphere format. Coordinate ordering is
Expand Down
80 changes: 80 additions & 0 deletions src/formats/dms-block-suffixed-hemisphere-format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { z } from 'zod';
import type { Coordinate } from '../types.js';
import { validateSchema } from '../validate-schema.js';
import { BaseFormat } from './base-format.js';

const REGEX = /^(\d{2})(\d{2})(\d{2}(\.\d+)?)\s*([NS])\s*,?\s*(\d{3})(\d{2})(\d{2}(\.\d+)?)\s*([EW])$/;

/**
* Parses coordinates strings in DMS block prefixed hemisphere format. Coordinate ordering is
* always latitude, longitude.
*
* Supported formats:
*
* 044506N 1030342E
* 044506N1030342E
* 044506.123N 1030342.123E
*/
export class DmsBlockSuffixedHemisphereFormat extends BaseFormat {
parse(coordinateString: string): Coordinate {
validateSchema(coordinateString, z.string(), { assert: true, name: 'coordinateString' });

if (DmsBlockSuffixedHemisphereFormat.canParse(coordinateString) === false) {
throw new Error('Invalid coordinate string');
}
// use the regex to parse the latitude and longitude
const match = coordinateString.match(REGEX);
if (match == null) {
throw new Error('Invalid coordinate string');
}
const matchLatDegree = match[1];
const matchLatMinutes = match[2];
const matchLatSeconds = match[3];
const matchLatDirection = match[5];
const matchLonDegree = match[6];
const matchLonMinutes = match[7];
const matchLonSeconds = match[8];
const matchLonDirection = match[10];

this.enforceValidLatitudeDegrees(matchLatDegree);
this.enforceValidMinutes(matchLatMinutes);
this.enforceValidSeconds(matchLatSeconds);
this.enforceValidLongitudeDegrees(matchLonDegree);
this.enforceValidMinutes(matchLonMinutes);
this.enforceValidSeconds(matchLonSeconds);

const latDegree = Number.parseInt(matchLatDegree);
const latMinutes = Number.parseInt(matchLatMinutes);
const latSeconds = Number.parseFloat(matchLatSeconds);
const lonDegree = Number.parseInt(matchLonDegree);
const lonMinutes = Number.parseInt(matchLonMinutes);
const lonSeconds = Number.parseFloat(matchLonSeconds);

const decimalLat = this.dmsToDecimal({
degrees: Math.abs(latDegree),
minutes: latMinutes,
seconds: latSeconds,
direction: matchLatDirection,
});
const decimalLon = this.dmsToDecimal({
degrees: Math.abs(lonDegree),
minutes: lonMinutes,
seconds: lonSeconds,
direction: matchLonDirection,
});

const lat = decimalLat.toFixed(this.precision);
const lon = decimalLon.toFixed(this.precision);

return {
latitude: parseFloat(lat),
longitude: parseFloat(lon),
};
}

static canParse(coordinateString: string): boolean {
validateSchema(coordinateString, z.string(), { assert: true, name: 'coordinateString' });

return REGEX.test(coordinateString);
}
}
2 changes: 1 addition & 1 deletion src/formats/dms-signed-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Coordinate } from '../types.js';
import { validateSchema } from '../validate-schema.js';
import { BaseFormat } from './base-format.js';

const REGEX = /^(-?\d+)°\s*(\d+)'\s*(\d+(?:\.\d+)?)\",?\s*(-?\d+)°\s*(\d+)'\s*(\d+(?:\.\d+)?)\"$/;
const REGEX = /^(-?\d+)°\s*(\d+)'\s*(\d+(?:\.\d+)?)\"\s*,?\s*(-?\d+)°\s*(\d+)'\s*(\d+(?:\.\d+)?)\"$/;

/**
* Parses coordinates strings in DMS signed format. Coordinate ordering is
Expand Down
2 changes: 2 additions & 0 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DecimalUnsignedSuffixedHemisphereFormat } from './formats/decimal-unsig
import { DmUnsignedPrefixedHemisphereFormat } from './formats/dm-unsigned-prefixed-hemisphere-format.js';
import { DmUnsignedSuffixedHemisphereFormat } from './formats/dm-unsigned-suffixed-hemisphere-format.js';
import { DmsBlockPrefixedHemisphereFormat } from './formats/dms-block-prefixed-hemisphere-format.js';
import { DmsBlockSuffixedHemisphereFormat } from './formats/dms-block-suffixed-hemisphere-format.js';
import { DmsSignedFormat } from './formats/dms-signed-format.js';
import { DmsSignedPrefixedHemisphereFormat } from './formats/dms-signed-prefixed-hemisphere-format.js';
import { DmsSignedSuffixedHemisphereFormat } from './formats/dms-signed-suffixed-hemisphere-format.js';
Expand Down Expand Up @@ -61,6 +62,7 @@ export class Parser {
new DmUnsignedPrefixedHemisphereFormat({ precision: precision }),
new DmUnsignedSuffixedHemisphereFormat({ precision: precision }),
new DmsBlockPrefixedHemisphereFormat({ precision: precision }),
new DmsBlockSuffixedHemisphereFormat({ precision: precision }),
new DmsSignedFormat({ precision: precision }),
new DmsSignedPrefixedHemisphereFormat({ precision: precision }),
new DmsSignedSuffixedHemisphereFormat({ precision: precision }),
Expand Down
40 changes: 40 additions & 0 deletions tests/dms-block-suffixed-hemisphere-format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest';
import { DmsBlockSuffixedHemisphereFormat } from '../src/formats/dms-block-suffixed-hemisphere-format.js';

describe('canParse', () => {
it('returns true for known formats', () => {
expect(DmsBlockSuffixedHemisphereFormat.canParse(`400723N 0740723W`)).toBe(true);
expect(DmsBlockSuffixedHemisphereFormat.canParse(`400723N0740723W`)).toBe(true);
expect(DmsBlockSuffixedHemisphereFormat.canParse(`400723N, 0740723W`)).toBe(true);
expect(DmsBlockSuffixedHemisphereFormat.canParse(`400723.999N, 0740723.999W`)).toBe(true);
});
});
describe('parse', () => {
it(`returns the correct latitude and longitude for 400723N 0740723W`, () => {
const formatParser = new DmsBlockSuffixedHemisphereFormat();
const result = formatParser.parse(`400723N 0740723W`);
expect(result.latitude).toBe(40.123);
expect(result.longitude).toBe(-74.123);
});

it(`returns the correct latitude and longitude for 400723N0740723W`, () => {
const formatParser = new DmsBlockSuffixedHemisphereFormat();
const result = formatParser.parse(`400723N0740723W`);
expect(result.latitude).toBe(40.123);
expect(result.longitude).toBe(-74.123);
});

it(`returns the correct latitude and longitude for 400723N, 0740723W`, () => {
const formatParser = new DmsBlockSuffixedHemisphereFormat();
const result = formatParser.parse(`400723N, 0740723W`);
expect(result.latitude).toBe(40.123);
expect(result.longitude).toBe(-74.123);
});

it(`returns the correct latitude and longitude for 400723.999N, 0740723.999W`, () => {
const formatParser = new DmsBlockSuffixedHemisphereFormat({ precision: 5 });
const result = formatParser.parse(`400723.999N, 0740723.999W`);
expect(result.latitude).toBe(40.12333);
expect(result.longitude).toBe(-74.12333);
});
});
29 changes: 29 additions & 0 deletions tests/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,35 @@ describe('Test that all configured format parsers do not interfere', () => {
expect(result.longitude).toBe(-74.12333);
});
});
describe('test dms block suffixed hemisphere format', () => {
it(`returns the correct latitude and longitude for 400723N 0740723W`, () => {
const parser = new Parser();
const result = parser.parse(`400723N 0740723W`);
expect(result.latitude).toBe(40.123);
expect(result.longitude).toBe(-74.123);
});

it(`returns the correct latitude and longitude for 400723N0740723W`, () => {
const parser = new Parser();
const result = parser.parse(`400723N0740723W`);
expect(result.latitude).toBe(40.123);
expect(result.longitude).toBe(-74.123);
});

it(`returns the correct latitude and longitude for 400723N, 0740723W`, () => {
const parser = new Parser();
const result = parser.parse(`400723N, 0740723W`);
expect(result.latitude).toBe(40.123);
expect(result.longitude).toBe(-74.123);
});

it(`returns the correct latitude and longitude for 400723.999N, 0740723.999W`, () => {
const parser = new Parser({ precision: 5 });
const result = parser.parse(`400723.999N, 0740723.999W`);
expect(result.latitude).toBe(40.12333);
expect(result.longitude).toBe(-74.12333);
});
});
describe('test dsm signed format', () => {
it(`returns the correct latitude and longitude for 40°7'23" -74°7'23"`, () => {
const parser = new Parser();
Expand Down

0 comments on commit 01041a5

Please sign in to comment.