Skip to content

Commit

Permalink
v0.6.39
Browse files Browse the repository at this point in the history
  • Loading branch information
mbloch committed Jul 28, 2023
1 parent a83f8ce commit 21de7ad
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 32 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
v0.6.39
* Added `format_dms()` and `parse_dms()` functions to `-each` expressions, for working with DMS-formatted coordinates.

v0.6.38
* Added `-symbols opacity=` option.

Expand Down
14 changes: 13 additions & 1 deletion REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,19 @@ Apply a JavaScript expression to each feature in a layer. Data properties are av

`target=` Layer to target.


**Utility functions**
Several utility functions are available within expressions.

- `format_dms(coord [, fmt])` Format a latitude or longitude coordinate as DMS. The optional second argument is for custom formats. Examples: `[-]DDDMM.MMMMM` `DdMmSs [EW]` `DD° MM′ SS.SSSSS″ [NS]`
- `parse_dms(string [, fmt])` Parse a DMS string to a numerical coordinate. The optional second argument gives the format to use for parsing.
- `round(number [, decimals])` Optional second argument gives the number of decimals to use.


**Properties of `this`**

The `this` object, available within expressions, has properties relating to the geometry and attribute data of a feature.

*Properties are read-only unless otherwise indicated.*

All layer types
Expand Down Expand Up @@ -594,7 +606,7 @@ Polygon, polyline and point layers

**Note:** Centroids are calculated for the largest ring of multi-part polygons, and do not account for holes.

**Note:** Most geometric properties are calculated using planar geometry. Exceptions are the areas of unprojected polygons and the lengths of unprojected polylines. These calculations use spherical geometry.
**Note:** Most geometric properties are calculated using planar geometry. Exceptions are the areas of unprojected polygons and the lengths of unprojected polylines. These calculations use spherical, not ellipsoidal geometry, so are not as accurate as the equivalent calculations in a GIS application.

**Examples**

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mapshaper",
"version": "0.6.38",
"version": "0.6.39",
"description": "A tool for editing vector datasets for mapping and GIS.",
"keywords": [
"shapefile",
Expand Down
6 changes: 4 additions & 2 deletions src/expressions/mapshaper-expression-utils.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import utils from '../utils/mapshaper-utils';
import { blend } from '../color/blending';
import { roundToDigits2 } from '../geom/mapshaper-rounding';

import { formatDMS, parseDMS } from '../geom/mapshaper-dms';

export function cleanExpression(exp) {
// workaround for problem in GNU Make v4: end-of-line backslashes inside
Expand All @@ -14,7 +14,9 @@ export function addFeatureExpressionUtils(env) {
round: roundToDigits2,
int_median: interpolated_median,
sprintf: utils.format,
blend: blend
blend: blend,
format_dms: formatDMS,
parse_dms: parseDMS
});
}

Expand Down
92 changes: 69 additions & 23 deletions src/geom/mapshaper-dms.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,99 @@ import { stop } from '../utils/mapshaper-logging';

// Parse a formatted value in DMS DM or D to a numeric value. Returns NaN if unparsable.
// Delimiters: degrees: D|d|°; minutes: '; seconds: "
export function parseDMS(str) {
var rxp = /^([nsew+-]?)([0-9.]+)[d°]? ?([0-9.]*)[']? ?([0-9.]*)["]? ?([nsew]?)$/i;
export function parseDMS(str, fmt) {
var defaultRxp = /^(?<prefix>[nsew+-]?)(?<d>[0-9.]+)[d°]? ?(?<m>[0-9.]*)[']? ?(?<s>[0-9.]*)["]? ?(?<suffix>[nsew]?)$/i;
var rxp = fmt ? getParseRxp(fmt) : defaultRxp;
var match = rxp.exec(str.trim());
var d = NaN;
var deg, min, sec, inv;
if (match) {
deg = match[2] || '0';
min = match[3] || '0';
sec = match[4] || '0';
deg = match.groups.d || '0';
min = match.groups.m || '0';
sec = match.groups.s || '0';
d = (+deg) + (+min) / 60 + (+sec) / 3600;
if (/[sw-]/i.test(match[1]) || /[sw]/i.test(match[5])) {
if (/[sw-]/i.test(match.groups.prefix || '') || /[sw]/i.test(match.groups.suffix || '')) {
d = -d;
}
}
return d;
}

var cache = {};

function getParseRxp(fmt) {
if (fmt in cache) return cache[fmt];
var rxp = fmt;
rxp = rxp.replace('[-]', '(?<prefix>-)?'); // optional -
rxp = rxp.replace(/\[[NSEW, +-]{2,}\]/, '(?<prefix>$&)');
rxp = rxp.replace(/(MM?)(\.M+)?/, (m, g1, g2) => {
var s = g1.length == 1 ? '[0-9]+' : '[0-9][0-9]';
if (g2) s += `\\.[0-9]+`;
return `(?<m>${s})`;
});
rxp = rxp.replace(/(SS?)(\.S+)?/, (m, g1, g2) => {
var s = g1.length == 1 ? '[0-9]+' : '[0-9][0-9]';
if (g2) s += `\\.[0-9]+`;
return `(?<s>${s})`;
});
rxp = rxp.replace(/D+/, '(?<d>[0-9]+)');
rxp = '^' + rxp + '$';
try {
// TODO: make sure all DMS codes have been matched
cache[fmt] = new RegExp(rxp);
} catch(e) {
stop('Invalid DMS format string:', fmt);
}
return cache[fmt];
}

function formatNumber(val, integers, decimals) {
var str = val.toFixed(decimals);
var parts = str.split('.');
if (parts.length > 0) {
parts[0] = parts[0].padStart(integers, '0');
str = parts.join('.');
}
return str;
}

function getDecimals(fmt) {
var match = /S\.(S+)/.exec(fmt) || /M\.(M+)/.exec(fmt) || null;
return match ? match[1].length : 0;
}

// TODO: support DD.DDDDD
export function formatDMS(coord, fmt) {
if (!fmt) fmt = '[-]D°M\'S.SSS';
var str = fmt;
var dms = /(D+)[^M]*(M+)[^S]*(S+)/.exec(fmt);
if (!dms) {
var dstr, mstr, sstr;
var match = /(D+)[^M]*(M+)[^S[\]]*(S+)?/.exec(fmt);
if (!match) {
stop('Invalid DMS format string:', fmt);
}
var decimalsMatch = /S\.(S+)/.exec(fmt);
var decimals = decimalsMatch ? decimalsMatch[1].length : 0;
var gotSeconds = !!match[3];
var decimals = getDecimals(fmt);
var integers = gotSeconds ? match[3].length : match[2].length;
var RES = Math.pow(10, decimals);
var CONV = 3600 * RES;
var CONV = gotSeconds ? 3600 * RES : 60 * RES;
var r = Math.floor(Math.abs(coord) * CONV + 0.5);
var sstr = ((r / RES) % 60).toFixed(decimals);
var sparts = sstr.split('.');
if (sparts.length > 0) {
sparts[0] = sparts[0].padStart(dms[3].length, '0');
sstr = sparts.join('.');
var lastPart = formatNumber((r / RES) % 60, integers, decimals);
if (gotSeconds) {
r = Math.floor(r / (RES * 60));
sstr = lastPart;
mstr = String(r % 60).padStart(match[2].length, '0');
} else {
r = Math.floor(r / RES);
mstr = lastPart;
sstr = '';
}
r = Math.floor(r / (RES * 60));
var dstr = String(Math.floor(r / 60)).padStart(dms[1].length, '0');
var mstr = String(r % 60).padStart(dms[2].length, '0');
// console.log(deg, dstr, min, mstr, sec, sstr, dms[1].length, dms[2].length, dms[3].length)

dstr = String(Math.floor(r / 60)).padStart(match[1].length, '0');
str = str.replace(/\[-\]/, s => coord < 0 ? '-' : '');
str = str.replace(/\[[+-]+\]/, s => coord < 0 ? '-' : '+');
str = str.replace(/\[[NS, ]+\]/, s => coord < 0 ? 'S' : 'N');
str = str.replace(/\[[EW, ]+\]/, s => coord < 0 ? 'W' : 'E');
str = str.replace(/D+/, dstr);
str = str.replace(/M+/, mstr);
str = str.replace(/S+(\.S+)?/, sstr);
str = str.replace(/M+(\.M+)?/, mstr);
if (gotSeconds) str = str.replace(/S+(\.S+)?/, sstr);
return str;
}
10 changes: 10 additions & 0 deletions src/join/mapshaper-point-point-join.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,13 @@ function getPointToPointFunction(targetLyr, srcLyr, crs, opts) {
return matches.length > 0 ? matches : null;
};
}

function getNearestPointFunction(targetLyr, srcLyr, crs, opts) {

}

function getInverseNearestPointFunction(targetLyr, srcLyr, crs, opts) {


}

3 changes: 2 additions & 1 deletion src/utils/mapshaper-logging.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export function error() {

// Handle an error caused by invalid input or misuse of API
export function stop() {
_stop.apply(null, utils.toArray(arguments));
// _stop.apply(null, utils.toArray(arguments));
_stop.apply(null, messageArgs(arguments));
}

export function interrupt() {
Expand Down
74 changes: 72 additions & 2 deletions test/dms-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,58 @@ var formatDMS = api.internal.formatDMS;

describe('mapshaper-dms.js', function () {

describe('roundtrip tests', function() {
function tests(fmt, isLon) {
function test(coord) {
var dms = formatDMS(coord, fmt);
var coord2 = parseDMS(dms, fmt);
var dms2 = formatDMS(coord2, fmt);
// console.log(dms, dms2)
assert.equal(dms, dms2);
}

it(fmt, function() {
test(0);
test(89.99452);
test(-89.99452);
test(-3.03539843345);
test(-37.49999999999);
test(10.00000001);
test(10.0000001);
test(10.000001);
test(10.00001);
test(10.0001);
test(10.001);
test(10.005);
test(10.049);
if (isLon) {
test(180);
test(-180);
test(-179.999);
test(-100.45140975209458);
}
var n = 0; // 100000;
while (n--) {
test((Math.random() - 0.5) * 180);
}
})
}

tests('[-]DDMMSS');
tests('DD° MM′ SS.SSSSS″ [N S]');
tests('[+-]DDMM.MMMMM');
tests('DDD° MM\' SS.S"');
});


describe('format_dms() expression function', function() {
it ('test1', async function() {
var data = 'lat,lon\n'


})
})

describe('formatDMS()', function() {
// verified by
var fmt1 = 'DDMMSS[WE]';
Expand All @@ -18,18 +70,31 @@ describe('mapshaper-dms.js', function () {
assert.equal(formatDMS(-179.99999, fmt1), '1800000W')
});

var fmt2 = 'DDMMSS.SSS';
var fmt2 = '[-]DDMMSS.SSS';
it(fmt2, function() {
assert.equal(formatDMS(32.0451, fmt2), '320242.360')
assert.equal(formatDMS(-32.0451, fmt2), '-320242.360')
})

var fmt3 = '[+-]DdMmSs';
it(fmt3, function() {
assert.equal(formatDMS(32.0451, fmt3), '+32d2m42s')
assert.equal(formatDMS(-32.0451, fmt3), '-32d2m42s')
})

var fmt4 = 'DD° MM′ SS.SSSSS″ [N S]';
it(fmt4, function() {
assert.equal(formatDMS(149.128684, fmt4), "149° 07′ 43.26240″ N")
assert.equal(formatDMS(-35.282000, fmt4), "35° 16′ 55.20000″ S")
})

var fmt5 = 'DDD° MM.MMM\' [SN]';
it(fmt5, function() {
assert.equal(formatDMS(32.30642, fmt5), "032° 18.385' N")
assert.equal(formatDMS(-122.61458, fmt5), "122° 36.875' S")
})
})


describe('parseDMS()', function () {
// references for format variations
// https://www.maptools.com/tutorials/lat_lon/formats
Expand Down Expand Up @@ -76,6 +141,11 @@ describe('mapshaper-dms.js', function () {
assert.equal(parseDMS('+122.61458°'), 122.61458)
});

var fmt1 = '[-]DDDMMSS.SSS';
it(fmt1, function() {
assert.equal(parseDMS('0d0m0sE', 'DdMmSs[EW]'), 0);
})

it('invalid DMS values', function () {
assert(isNaN(parseDMS('0x')));
assert(isNaN(parseDMS('')));
Expand Down

0 comments on commit 21de7ad

Please sign in to comment.