Skip to content

Commit 36d27fb

Browse files
committed
Improve DMS matching code and fix seconds rounding
re: #1389 re: openstreetmap/iD#10066
1 parent 9c696d1 commit 36d27fb

File tree

3 files changed

+132
-8
lines changed

3 files changed

+132
-8
lines changed

modules/core/LocalizationSystem.js

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -730,11 +730,46 @@ export class LocalizationSystem extends AbstractSystem {
730730
}
731731

732732

733+
/**
734+
* Return some parsed values in DMS formats that @mapbox/sexagesimal can't parse, see iD#10066
735+
* Note that `@mapbox/sexagesimal` returns [lat,lon], so this code does too.
736+
* @param {string} q - string to attempt to parse
737+
* @return {Array<Number>?} The location formatted as `[lat,lon]`, or `null` it can't be parsed
738+
*/
739+
dmsMatcher(q) {
740+
let match;
741+
742+
// DD MM SS , DD MM SS ex: 35 11 10.1 , 136 49 53.8
743+
const DMS_DMS = /^\s*(-?)\s*(\d+)\s+(\d+)\s+(\d+\.?\d*)\s*\,\s*(-?)\s*(\d+)\s+(\d+)\s+(\d+\.?\d*)\s*$/;
744+
match = q.match(DMS_DMS);
745+
if (match) {
746+
let lat = (+match[2]) + (+match[3]) / 60 + (+match[4]) / 3600;
747+
let lon = (+match[6]) + (+match[7]) / 60 + (+match[8]) / 3600;
748+
if (match[1] === '-') lat *= -1;
749+
if (match[5] === '-') lon *= -1;
750+
return [lat, lon];
751+
}
752+
753+
// DD MM , DD MM ex: 35 11 10.1 , 136 49 53.8
754+
const DM_DM = /^\s*(-?)\s*(\d+)\s+(\d+\.?\d*)\s*\,\s*(-?)\s*(\d+)\s+(\d+\.?\d*)\s*$/;
755+
match = q.match(DM_DM);
756+
if (match) {
757+
let lat = +match[2] + (+match[3]) / 60;
758+
let lon = +match[5] + (+match[6]) / 60;
759+
if (match[1] === '-') lat *= -1;
760+
if (match[4] === '-') lon *= -1;
761+
return [lat, lon];
762+
}
763+
764+
return null;
765+
}
766+
767+
733768
/**
734769
* Returns the given coordinate pair in decimal format.
735770
* note: unlocalized to avoid comma ambiguity - see iD#4765
736-
* @param {Array<Number>} coord longitude and latitude
737-
* @return {string} Text to display
771+
* @param {Array<Number>} coord - longitude and latitude
772+
* @return {string} Text to display
738773
*/
739774
decimalCoordinatePair(coord) {
740775
const OSM_PRECISION = 7;
@@ -745,26 +780,38 @@ export class LocalizationSystem extends AbstractSystem {
745780
}
746781

747782

783+
/**
784+
* Format a degree coordinate as DMS (degree minute second)for display
785+
* @param {number} degrees - degrees to convert to DMS
786+
* @param {string} pos - string to use for positive values (either 'north' or 'east')
787+
* @param (string) neg - string to use for negative values (either 'south' or 'west')
788+
* @return {string} Text to display
789+
*/
748790
_displayCoordinate(deg, pos, neg) {
791+
const EPSILON = 0.01;
749792
const locale = this._currLocaleCode;
750793
const min = (Math.abs(deg) - Math.floor(Math.abs(deg))) * 60;
751-
const sec = (min - Math.floor(min)) * 60;
794+
let sec = (min - Math.floor(min)) * 60;
795+
796+
// If you input 45°,90°0'0.5" , sec should be 0.5 instead 0.499999…
797+
// To mitigate precision errors after calculating, round again, see iD#10066
798+
sec = +sec.toFixed(8); // 0.499999… => 0.5
799+
752800
const displayDegrees = this.t('units.arcdegrees', {
753801
quantity: Math.floor(Math.abs(deg)).toLocaleString(locale)
754802
});
755803

756804
let displayCoordinate;
757805

758-
if (Math.floor(sec) > 0) {
806+
if (Math.abs(sec) > EPSILON) {
759807
displayCoordinate = displayDegrees +
760808
this.t('units.arcminutes', { quantity: Math.floor(min).toLocaleString(locale) }) +
761809
this.t('units.arcseconds', { quantity: Math.round(sec).toLocaleString(locale) });
762-
} else if (Math.floor(min) > 0) {
810+
} else if (Math.abs(min) > EPSILON) {
763811
displayCoordinate = displayDegrees +
764812
this.t('units.arcminutes', { quantity: Math.round(min).toLocaleString(locale) });
765813
} else {
766-
displayCoordinate =
767-
this.t('units.arcdegrees', { quantity: Math.round(Math.abs(deg)).toLocaleString(locale) });
814+
displayCoordinate = displayDegrees;
768815
}
769816

770817
if (deg === 0) {

modules/ui/feature_list.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export function uiFeatureList(context) {
112112
if (!q) return result;
113113

114114
// User typed something that looks like a coordinate pair
115-
const locationMatch = sexagesimal.pair(q.toUpperCase()) || q.match(/^(-?\d+\.?\d*)\s+(-?\d+\.?\d*)$/);
115+
const locationMatch = sexagesimal.pair(q.toUpperCase()) || l10n.dmsMatcher(q);
116116
if (locationMatch) {
117117
const loc = [ parseFloat(locationMatch[0]), parseFloat(locationMatch[1]) ];
118118
result.push({

test/browser/core/LocalizationSystem.test.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,29 @@ describe('LocalizationSystem', () => {
3333
network_ref_from_to: '{network} {ref} from {from} to {to}',
3434
network_ref_from_to_via: '{network} {ref} from {from} to {to} via {via}'
3535
}
36+
},
37+
units: {
38+
feet: '{quantity} ft',
39+
miles: '{quantity} mi',
40+
square_feet: '{quantity} sq ft',
41+
square_miles: '{quantity} sq mi',
42+
acres: '{quantity} ac',
43+
meters: '{quantity} m',
44+
kilometers: '{quantity} km',
45+
square_meters: '{quantity} m²',
46+
square_kilometers: '{quantity} km²',
47+
hectares: '{quantity} ha',
48+
area_pair: '{area1} ({area2})',
49+
arcdegrees: '{quantity}°',
50+
arcminutes: '{quantity}′',
51+
arcseconds: '{quantity}″',
52+
north: 'N',
53+
south: 'S',
54+
east: 'E',
55+
west: 'W',
56+
coordinate: '{coordinate}{direction}',
57+
coordinate_pair: '{latitude}, {longitude}',
58+
year_month_day: 'YYYY-MM-DD'
3659
}
3760
}
3861
}
@@ -106,4 +129,58 @@ describe('LocalizationSystem', () => {
106129
expect(_l10n.displayName(tags5)).to.eql('BART Yellow from Antioch to Millbrae via Pittsburg/Bay Point;San Francisco International Airport');
107130
});
108131
});
132+
133+
describe('dmsMatcher', () => {
134+
it('parses D M SS format', () => {
135+
const result = _l10n.dmsMatcher('35 11 10.1 , 136 49 53.8');
136+
expect(result[0]).to.be.closeTo( 35.18614, 0.00001);
137+
expect(result[1]).to.be.closeTo(136.83161, 0.00001);
138+
});
139+
it('parses D M SS format, with negative value', () => {
140+
const result = _l10n.dmsMatcher('-35 11 10.1 , -136 49 53.8');
141+
expect(result[0]).to.be.closeTo( -35.18614, 0.00001);
142+
expect(result[1]).to.be.closeTo(-136.83161, 0.00001);
143+
});
144+
145+
it('parses D MM format', () => {
146+
const result = _l10n.dmsMatcher('35 11.1683 , 136 49.8966');
147+
expect(result[0]).to.be.closeTo( 35.18614, 0.00001);
148+
expect(result[1]).to.be.closeTo(136.83161, 0.00001);
149+
});
150+
it('parses D MM format, with negative value', () => {
151+
const result = _l10n.dmsMatcher('-35 11.1683 , -136 49.8966');
152+
expect(result[0]).to.be.closeTo( -35.18614, 0.00001);
153+
expect(result[1]).to.be.closeTo(-136.83161, 0.00001);
154+
});
155+
156+
it('handles invalid input', () => {
157+
const result = _l10n.dmsMatcher('!@#$');
158+
expect(result).to.be.null;
159+
});
160+
});
161+
162+
describe('dmsCoordinatePair', () => {
163+
it('formats coordinate pair', () => {
164+
const result = _l10n.dmsCoordinatePair([90 + 0.5/3600, 45]);
165+
expect(result).to.be.eql('45°N, 90°0′1″E');
166+
});
167+
it('formats 0°', () => {
168+
const result = _l10n.dmsCoordinatePair([0, 0]);
169+
expect(result).to.be.eql('0°, 0°');
170+
});
171+
it('formats negative value', () => {
172+
const result = _l10n.dmsCoordinatePair([-179, -90]);
173+
expect(result).to.be.eql('90°S, 179°W');
174+
});
175+
it('formats 180° lng, should be E or W', () => {
176+
// The longitude at this line can be given as either east or west.
177+
const result = _l10n.dmsCoordinatePair([180, 0]);
178+
expect(result).to.be.oneOf(['0°, 180°W', '0°, 180E°']);
179+
});
180+
it('formats value over 90°lat or 180°lng', () => {
181+
const result = _l10n.dmsCoordinatePair([181, 91]);
182+
expect(result).to.be.oneOf(['90°N, 179°W']);
183+
});
184+
});
185+
109186
});

0 commit comments

Comments
 (0)