From 32e2573e73df3c1b0afc7e319310d91c96eb53fc Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 11 Apr 2021 21:47:28 -0400 Subject: [PATCH 001/891] v0.5.42 (-dots command) --- CHANGELOG.md | 3 + package-lock.json | 2 +- package.json | 2 +- src/cli/mapshaper-options.js | 28 ++- src/commands/mapshaper-dots.js | 135 ++++++++++++--- src/dataset/mapshaper-layer-utils.js | 5 + src/datatable/mapshaper-data-utils.js | 9 +- src/points/mapshaper-dot-density.js | 237 ++++++++++++++++++++++++++ 8 files changed, 395 insertions(+), 26 deletions(-) create mode 100644 src/points/mapshaper-dot-density.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ea08d9d8a..c4ef86568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.42 +* Added -dots command for making dot density maps. + v0.5.41 * Fixed error parsing .prj files for southern UTM zones. diff --git a/package-lock.json b/package-lock.json index b38dbf8c5..09f406fc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.41", + "version": "0.5.42", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d2b9a1688..2b48b5360 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.41", + "version": "0.5.42", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index d1d18807d..d984a5d99 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -674,11 +674,31 @@ export function getOptionParser() { // .option('no-replace', noReplaceOpt); parser.command('dots') - .describe('') - .option('field', { - describe: 'field containing number of dots' + .describe('fill polygons with dots of one or more colors') + .option('fields', { + DEFAULT: true, + describe: 'one or more fields containing numbers of dots', + type: 'strings' }) - .option('target', targetOpt); + .option('colors', { + describe: 'one or more colors', + type: 'strings' + }) + .option('r', { + describe: 'radius of each dot in pixels', + type: 'number' + }) + .option('spacing', { + describe: 'how evenly dots should be spaced (0-1, default is 1)', + type: 'number' + }) + .option('random', { + describe: 'place dots randomly instead of spreading them out', + type: 'flag' + }) + .option('target', targetOpt) + .option('name', nameOpt) + .option('no-replace', noReplaceOpt); parser.command('drop') .describe('delete layer(s) or elements within the target layer(s)') diff --git a/src/commands/mapshaper-dots.js b/src/commands/mapshaper-dots.js index b1da8a758..b43200eef 100644 --- a/src/commands/mapshaper-dots.js +++ b/src/commands/mapshaper-dots.js @@ -1,38 +1,135 @@ import { requireDataField } from '../dataset/mapshaper-layer-utils'; -import { requirePolygonLayer } from '../dataset/mapshaper-layer-utils'; +import { requirePolygonLayer, layerHasNonNullData } from '../dataset/mapshaper-layer-utils'; +import { parseColor } from '../color/color-utils'; import cmd from '../mapshaper-cmd'; import geom from '../geom/mapshaper-geom'; +import { stop } from '../utils/mapshaper-logging'; +import { DataTable } from '../datatable/mapshaper-data-table'; +import utils from '../utils/mapshaper-utils'; +import { explodePolygon } from '../commands/mapshaper-explode'; +import { placeDotsEvenly, placeDotsRandomly } from '../points/mapshaper-dot-density'; cmd.dots = function(lyr, arcs, opts) { requirePolygonLayer(lyr); - requireDataField(lyr, opts.field); + if (!Array.isArray(opts.fields)) { + stop("Missing required fields parameter"); + } + if (!Array.isArray(opts.colors)) { + stop("Missing required colors parameter"); + } + if (layerHasNonNullData(lyr)) { + opts.fields.forEach(function(f, i) { + requireDataField(lyr, f); + }); + } + opts.colors.forEach(parseColor); // validate colors + var records = lyr.data ? lyr.data.getRecords() : []; - var shapes = []; + var shapes2 = []; + var records2 = []; lyr.shapes.forEach(function(shp, i) { var d = records[i]; - var n = d ? +d[opts.field] : 0; - var coords = null; - if (n > 0) { - coords = createInnerPoints(shp, arcs, n); - } - shapes.push(coords); + if (!d) return; + var data = makeDotsForShape(shp, arcs, d, opts); + shapes2.push.apply(shapes2, data.shapes); + records2.push.apply(records2, data.attributes); }); - return { - type: 'point', - shapes: shapes + + var lyr2 = { + name: opts.no_replace ? null : lyr.name, + geometry_type: 'point', + shapes: shapes2, + data: new DataTable(records2) }; + return [lyr2]; }; -function createInnerPoints(shp, arcs, n) { - if (!shp || shp.length != 1) { - return null; // TODO: support polygons with holes and multipart polygons +function makeDotsForShape(shp, arcs, d, opts) { + var retn = { + shapes: [], + attributes:[] + }; + if (!shp) return retn; + var counts = opts.fields.map(function(f) { + return d[f] || 0; + }); + var indexes = expandCounts(counts); + var dots = placeDots(shp, arcs, indexes.length, opts); + + // randomize dot sequence so dots of the same color do not always overlap dots of + // other colors in dense areas. + // TODO: instead of random shuffling, interleave dot classes more regularly? + shuffle(indexes); + var idx, prevIdx = -1; + var coords; + for (var i=0; i 1 ? explodePolygon(shp, arcs) : [shp]; + var counts = apportionDotsByArea(polys, arcs, n); + var dots = []; + for (var i=0; i 0) arr.push(i); + }); + return arr; } +function shuffle(arr) { + var tmp, i, j; + for (i = arr.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } +} -function fillPolygonWithDots(shp, arcs, n) { - var area = geom.getPlanarShapeArea(shp, arcs); - var bounds = arcs.getMultiShapeBounds(shp); +function getDataRecord(i, opts) { + var o = { + fill: opts.colors[i], + r: opts.r || 2 + }; + if (opts.opacity < 1) { + o.opacity = opts.opacity; + } + return o; } diff --git a/src/dataset/mapshaper-layer-utils.js b/src/dataset/mapshaper-layer-utils.js index 98ec9beb6..273d0035d 100644 --- a/src/dataset/mapshaper-layer-utils.js +++ b/src/dataset/mapshaper-layer-utils.js @@ -5,6 +5,7 @@ import { getPathBounds, countArcsInShapes } from '../paths/mapshaper-path-utils' import { cloneShapes, editShapes } from '../paths/mapshaper-shape-utils'; import { stop, formatStringsAsGrid } from '../utils/mapshaper-logging'; import { DataTable } from '../datatable/mapshaper-data-table'; +import { getFirstNonEmptyRecord } from '../datatable/mapshaper-data-utils'; import utils from '../utils/mapshaper-utils'; import { absArcId } from '../paths/mapshaper-arc-utils'; @@ -31,6 +32,10 @@ export function getLayerDataTable(lyr) { return data; } +export function layerHasNonNullData(lyr) { + return lyr.data && getFirstNonEmptyRecord(lyr.data.getRecords()) ? true : false; +} + export function layerHasGeometry(lyr) { return layerHasPaths(lyr) || layerHasPoints(lyr); } diff --git a/src/datatable/mapshaper-data-utils.js b/src/datatable/mapshaper-data-utils.js index 947c4ea66..86e32a638 100644 --- a/src/datatable/mapshaper-data-utils.js +++ b/src/datatable/mapshaper-data-utils.js @@ -175,8 +175,15 @@ export function applyFieldOrder(arr, option) { return arr; } +export function getFirstNonEmptyRecord(records) { + for (var i=0, n=records ? records.length : 0; i 0 === false) return []; + var bounds = arcs.getMultiShapeBounds(shp); + var approxCells = Math.round(n * bounds.area() / shpArea); + var evenness = opts.spacing >= 0 ? Math.min(opts.spacing, 1) : 1; + var grid = new DotGrid(bounds, approxCells, evenness); + var coords = []; + for (var i=0; i 0) { + id = queuedCells.pop(); + p = getRandomPointInCell(id); + // console.log('retry?', pointIsUsable(p)) + if (pointIsUsable(p)) return p; + // if (Math.random() > 0.3) queuedCells.push(id); + } + + // random dart-throwing + while (probes++ < maxProbes) { + p = getRandomPoint(); + if (pointIsUsable(p)) { + probedPoints++; + return p; + } + if (probes % probesBeforeRelaxation === 0) { + // relax min dist after a number of failed probes + minDist *= 0.9; + } + } + return null; + }; + + api.rejectPoint = function(xy) { + addPointToCell(xy); + }; + + api.usePoint = function(xy) { + addPointToCell(xy); + }; + + return api; + + function getRandomPointInCell(i) { + var r = getRow(i); + var c = getCol(i); + var x = (Math.random() + r) / rows * w + x0; + var y = (Math.random() + c) / cols * h + y0; + return [x, y]; + } + + function getJitteredCoord(i, n, range, offs) { + return (Math.random() + i) / n * range + offs; + } + + function getRandomPoint() { + return getRandomPointInCell(getRandomCell()); + } + + function getRandomCell() { + return Math.floor(Math.random() * cells); + } + + + function addPointToCell(xy) { + var i = getCellIdx(xy); + var points = grid[i]; + if (!points) { + points= grid[i] = []; + } + points.push(xy); + } + + function getCellIdx(xy) { + var c = getPointCol(xy); + var r = getPointRow(xy); + return r * cols + c; + } + + function pointIsUsable(xy) { + var c = getPointCol(xy), + r = getPointRow(xy); + var collision = testCollision(xy, c, r) || + testCollision(xy, c+1, r) || + testCollision(xy, c, r+1) || + testCollision(xy, c-1, r) || + testCollision(xy, c, r-1) || + testCollision(xy, c+1, r+1) || + testCollision(xy, c-1, r+1) || + testCollision(xy, c-1, r-1) || + testCollision(xy, c+1, r-1); + return !collision; + } + + function testCollision(xy, c, r) { + if (c < 0 || r < 0 || c >= cols || r >= cols) return false; + var i = r * cols + c; + var points = grid[i]; + if (!points || !testPointCollision(xy, points, minDist)) return false; + return true; + } + + function testPointCollision(xy, points, dist) { + for (var i=0; i Date: Tue, 13 Apr 2021 13:42:20 -0400 Subject: [PATCH 002/891] v0.5.43 --- CHANGELOG.md | 4 + package-lock.json | 2 +- package.json | 2 +- src/cli/mapshaper-options.js | 8 +- src/commands/mapshaper-dots.js | 8 +- src/points/mapshaper-dot-density.js | 283 +++++++++++++++++++--------- 6 files changed, 201 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4ef86568..24be73f4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.43 +* Improved evenness of dot placement. +* Replaced "random" and "spacing=" options with a single "evenness=" option, which varies from 0 (random placement) to 1 (very even). + v0.5.42 * Added -dots command for making dot density maps. diff --git a/package-lock.json b/package-lock.json index 09f406fc8..13b47b36c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.42", + "version": "0.5.43", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 2b48b5360..9eda66e57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.42", + "version": "0.5.43", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index d984a5d99..d9616fbc5 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -688,14 +688,10 @@ export function getOptionParser() { describe: 'radius of each dot in pixels', type: 'number' }) - .option('spacing', { - describe: 'how evenly dots should be spaced (0-1, default is 1)', + .option('evenness', { + describe: '(0-1) 0 is random placement, 1 is even placement; default is 1', type: 'number' }) - .option('random', { - describe: 'place dots randomly instead of spreading them out', - type: 'flag' - }) .option('target', targetOpt) .option('name', nameOpt) .option('no-replace', noReplaceOpt); diff --git a/src/commands/mapshaper-dots.js b/src/commands/mapshaper-dots.js index b43200eef..b22ca4b31 100644 --- a/src/commands/mapshaper-dots.js +++ b/src/commands/mapshaper-dots.js @@ -8,7 +8,7 @@ import { stop } from '../utils/mapshaper-logging'; import { DataTable } from '../datatable/mapshaper-data-table'; import utils from '../utils/mapshaper-utils'; import { explodePolygon } from '../commands/mapshaper-explode'; -import { placeDotsEvenly, placeDotsRandomly } from '../points/mapshaper-dot-density'; +import { placeDotsInPolygon } from '../points/mapshaper-dot-density'; cmd.dots = function(lyr, arcs, opts) { requirePolygonLayer(lyr); @@ -62,12 +62,13 @@ function makeDotsForShape(shp, arcs, d, opts) { // TODO: instead of random shuffling, interleave dot classes more regularly? shuffle(indexes); var idx, prevIdx = -1; + var grouped = false; var coords; for (var i=0; i 1 ? explodePolygon(shp, arcs) : [shp]; var counts = apportionDotsByArea(polys, arcs, n); var dots = []; for (var i=0; i= 0 ? Math.min(opts.evenness, 1) : 1; + // TODO: also skip tiny sliver polygons? + if (n === 0) return []; + if (evenness === 0) return placeDotsRandomly(shp, arcs, n); + // TODO: if n == 1, consider using the 'inner' point + return placeDotsEvenly(shp, arcs, n, evenness); +} + +function placeDotsRandomly(shp, arcs, n) { var bounds = arcs.getMultiShapeBounds(shp); var coords = []; for (var i=0; i 0 === false) return []; var bounds = arcs.getMultiShapeBounds(shp); - var approxCells = Math.round(n * bounds.area() / shpArea); - var evenness = opts.spacing >= 0 ? Math.min(opts.spacing, 1) : 1; - var grid = new DotGrid(bounds, approxCells, evenness); + var approxQueries = Math.round(n * bounds.area() / shpArea); + var grid = new DotGrid(bounds, approxQueries, evenness); var coords = []; for (var i=0; i 0) { - id = queuedCells.pop(); - p = getRandomPointInCell(id); - // console.log('retry?', pointIsUsable(p)) - if (pointIsUsable(p)) return p; - // if (Math.random() > 0.3) queuedCells.push(id); + if (pointIsUsable(p)) { + return usePoint(p); + } } + // assumes that the point grid has already been seeded + return useBestPoint(); + } - // random dart-throwing + function getSpacedPoint() { + // use dart-throwing, reject points that are within the minimum distance + var probes = 0; + var p; while (probes++ < maxProbes) { p = getRandomPoint(); if (pointIsUsable(p)) { - probedPoints++; - return p; + return usePoint(p); } if (probes % probesBeforeRelaxation === 0) { // relax min dist after a number of failed probes @@ -126,57 +130,140 @@ function DotGrid(bounds, approxCells, evenness) { } } return null; - }; + } - api.rejectPoint = function(xy) { - addPointToCell(xy); - }; + // Add point to grid of used points + function usePoint(p) { + var i = pointToIdx(p); + var points = grid[i] || (grid[i] = []); + points.push(p); + return p; + } - api.usePoint = function(xy) { - addPointToCell(xy); - }; + function useBestPoint() { + var maxDist = 0; + var bestId, p; + if (!bestPoints) { + bestPoints = initBestPoints(); + } + for (var i=0; i maxDist) { + maxDist = p[2]; + bestId = i; + } + } + p = bestPoints[bestId].slice(0, 2); // remove distance member + usePoint(p); // add to grid of used points + updateBestPoints(p, bestId); // update best point of this cell and neighbors + return p; + } - return api; + function initBestPoints() { + var bestPoints = []; + for (var i=0; i bestPt[2]) return; + bestPoints[i] = findBestPointInCell(i); } - function getRandomPoint() { - return getRandomPointInCell(getRandomCell()); + function findBestPointInCell(i) { + var probes = 30; + var maxDist = 0; + var best, p, dist; + while (probes-- > 0) { + p = getRandomPointInCell(i); + dist = findDistanceFromNeighbors(p, i); + if (dist > maxDist) { + maxDist = dist; + best = p; + } + } + best.push(maxDist); // add distance as third element in the point + return best; } - function getRandomCell() { - return Math.floor(Math.random() * cells); + function findDistanceFromNeighbors(xy, i) { + var dist = Infinity; + var r = idxToRow(i); + var c = idxToCol(i); + dist = reduceDistance(dist, xy, c, r); + dist = reduceDistance(dist, xy, c+1, r); + dist = reduceDistance(dist, xy, c, r+1); + dist = reduceDistance(dist, xy, c-1, r); + dist = reduceDistance(dist, xy, c, r-1); + dist = reduceDistance(dist, xy, c+1, r+1); + dist = reduceDistance(dist, xy, c+1, r-1); + dist = reduceDistance(dist, xy, c-1, r+1); + dist = reduceDistance(dist, xy, c-1, r-1); + return dist; } + function reduceDistance(memo, xy, c, r) { + var i = colRowToIdx(c, r); + if (i == -1) return memo; // off the edge + var distSq = pointToPointsDistSq(xy, grid[i] || []); + return Math.min(memo, distSq); + } - function addPointToCell(xy) { - var i = getCellIdx(xy); - var points = grid[i]; - if (!points) { - points= grid[i] = []; + function pointToPointsDistSq(xy, points) { + var dist = Infinity; + for (var i=0; i= cols || r >= cols) return false; - var i = r * cols + c; + var i = colRowToIdx(c, r); + if (i == -1) return false; var points = grid[i]; if (!points || !testPointCollision(xy, points, minDist)) return false; return true; } function testPointCollision(xy, points, dist) { + var d2 = dist * dist; for (var i=0; i= cols) c = cols-1; + return c; + } + + function pointToRow(xy) { + var dy = xy[1] - y0; + var r = Math.floor(dy / h * rows); + if (r < 0) r = 0; + if (r >= rows) r = rows-1; + return r; } - function getPointCol(xy) { - var x = xy[0]; - var dx = x - x0; - var c = Math.floor(dx / w); - return c; // todo: validate or clamp + function colRowToIdx(c, r) { + if (c < 0 || r < 0 || c >= cols || r >= rows) return -1; + return r * cols + c; } - function getPointRow(xy) { - var y = xy[1]; - var dy = y - y0; - var r = Math.floor(dy / h); - return r; // todo: validate or clamp + function pointToIdx(xy) { + var c = pointToCol(xy); + var r = pointToRow(xy); + var idx = r * cols + c; + return idx; } - function getCol(i) { + function idxToCol(i) { return Math.floor(i % cols); } - function getRow(i) { + function idxToRow(i) { return Math.floor(i / cols); } } - From 5b1673f95d86328b17c3691714f8ee1086f3ec34 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 14 Apr 2021 20:05:13 -0400 Subject: [PATCH 003/891] v0.5.44 --- CHANGELOG.md | 5 + package-lock.json | 2 +- package.json | 2 +- src/cli/mapshaper-options.js | 14 +- src/commands/mapshaper-dots.js | 53 +++++--- src/points/mapshaper-dot-density.js | 193 ++++++++++++++++++---------- src/simplify/mapshaper-heap.js | 40 +++++- 7 files changed, 213 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24be73f4e..fd479b3ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +v0.5.44 +* Added "per-dot=" option to the -dots command, for setting the value-to-dot ratio. To represent 100 people per dot on a population map, you would use per-dot=100. +* Added "copy-fields=" option to -dots, to copy one or more data fields from the original polygon layer to the generated dot features. +* Added "multipart" option to -dots, which combines groups of same-color dots into multi-part features. + v0.5.43 * Improved evenness of dot placement. * Replaced "random" and "spacing=" options with a single "evenness=" option, which varies from 0 (random placement) to 1 (very even). diff --git a/package-lock.json b/package-lock.json index 13b47b36c..39a6f451a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.43", + "version": "0.5.44", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9eda66e57..7ff676acb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.43", + "version": "0.5.44", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index d9616fbc5..5d0679556 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -689,9 +689,21 @@ export function getOptionParser() { type: 'number' }) .option('evenness', { - describe: '(0-1) 0 is random placement, 1 is even placement; default is 1', + describe: '(0-1) dot spacing, from random to even (default is 1)', type: 'number' }) + .option('per-dot', { + describe: 'number for scaling data values (e.g. 10 per dot)', + type: 'number' + }) + .option('copy-fields', { + describe: 'list of fields to copy from polygons to dots', + type: 'strings' + }) + .option('multipart', { + describe: 'combine groups of same-color dots into multi-part features', + type: 'flag' + }) .option('target', targetOpt) .option('name', nameOpt) .option('no-replace', noReplaceOpt); diff --git a/src/commands/mapshaper-dots.js b/src/commands/mapshaper-dots.js index b22ca4b31..01f55333b 100644 --- a/src/commands/mapshaper-dots.js +++ b/src/commands/mapshaper-dots.js @@ -15,15 +15,20 @@ cmd.dots = function(lyr, arcs, opts) { if (!Array.isArray(opts.fields)) { stop("Missing required fields parameter"); } - if (!Array.isArray(opts.colors)) { - stop("Missing required colors parameter"); - } if (layerHasNonNullData(lyr)) { opts.fields.forEach(function(f, i) { requireDataField(lyr, f); }); + (opts.copy_fields || []).forEach(function(f) { + requireDataField(lyr, f); + }); + } + // if (!Array.isArray(opts.colors)) { + // stop("Missing required colors parameter"); + // } + if (Array.isArray(opts.colors)) { + opts.colors.forEach(parseColor); // validate colors } - opts.colors.forEach(parseColor); // validate colors var records = lyr.data ? lyr.data.getRecords() : []; var shapes2 = []; @@ -45,14 +50,18 @@ cmd.dots = function(lyr, arcs, opts) { return [lyr2]; }; -function makeDotsForShape(shp, arcs, d, opts) { +function makeDotsForShape(shp, arcs, rec, opts) { var retn = { shapes: [], attributes:[] }; if (!shp) return retn; var counts = opts.fields.map(function(f) { - return d[f] || 0; + var val = rec[f] || 0; + if (opts.per_dot > 0) { + val = Math.round(val / opts.per_dot); + } + return val; }); var indexes = expandCounts(counts); var dots = placeDots(shp, arcs, indexes.length, opts); @@ -62,16 +71,16 @@ function makeDotsForShape(shp, arcs, d, opts) { // TODO: instead of random shuffling, interleave dot classes more regularly? shuffle(indexes); var idx, prevIdx = -1; - var grouped = false; - var coords; + var multipart = !!opts.multipart; + var coords, p; for (var i=0; i maxDist) { - maxDist = p[2]; - bestId = i; - } - } - p = bestPoints[bestId].slice(0, 2); // remove distance member + var bestId = bestHeap.peek(); + var p = bestPoints[bestId]; usePoint(p); // add to grid of used points - updateBestPoints(p, bestId); // update best point of this cell and neighbors + updateNeighbors(p, bestId); // update best point of this cell and neighbors + lastDistSq = bestHeap.peekValue(); + bestCount++; return p; } function initBestPoints() { - var bestPoints = []; + var values = []; + bestPoints = []; + var distSq; for (var i=0; i bestPt[2]) return; - bestPoints[i] = findBestPointInCell(i); + // to have an effect. + // (about 80% of updates are skipped, typically) + if (distSq(addedPt, bestPt) < bestHeap.getValue(i)) { + updateBestPointInCell(i); + } } - function findBestPointInCell(i) { - var probes = 30; + function updateBestPointInCell(i) { + var distSq = findBestPointInCell(i); + bestHeap.updateValue(i, distSq); + } + + function findBestPointInCell_v1(i) { + // randomly probe for the best point + var probes = 20; var maxDist = 0; var best, p, dist; while (probes-- > 0) { p = getRandomPointInCell(i); - dist = findDistanceFromNeighbors(p, i); + dist = findDistanceFromNeighbors(maxDist, p, i); if (dist > maxDist) { maxDist = dist; best = p; } } - best.push(maxDist); // add distance as third element in the point - return best; + bestPoints[i] = best; + return maxDist; } - function findDistanceFromNeighbors(xy, i) { + function findBestPointInCell(idx) { + // use a grid pattern to find the best point + var perSide = 5; + var maxDist = 0; + var best, p, dist; + for (var i=0, n=perSide*perSide; i maxDist) { + maxDist = dist; + best = p; + } + } + bestPoints[idx] = best; + return maxDist; + } + + function getGridPointInCell(cellIdx, i, n) { + var r = idxToRow(cellIdx); + var c = idxToCol(cellIdx); + var dx = (i % n + 0.5) / n; + var dy = (Math.floor(i / n) + 0.5) / n; + var x = (dx + c) / cols * w + x0; + var y = (dy + r) / rows * h + y0; + return [x, y]; + } + + function findDistanceFromNeighbors(memo, xy, i) { var dist = Infinity; var r = idxToRow(i); var c = idxToCol(i); dist = reduceDistance(dist, xy, c, r); + if (dist < memo) return 0; // 10-15% speedup dist = reduceDistance(dist, xy, c+1, r); dist = reduceDistance(dist, xy, c, r+1); dist = reduceDistance(dist, xy, c-1, r); @@ -226,19 +279,20 @@ function DotGrid(bounds, approxQueries, evenness) { function reduceDistance(memo, xy, c, r) { var i = colRowToIdx(c, r); if (i == -1) return memo; // off the edge - var distSq = pointToPointsDistSq(xy, grid[i] || []); - return Math.min(memo, distSq); + var distSq = pointToPointsDistSq(xy, grid[i]); + return distSq < memo ? distSq : memo; } function pointToPointsDistSq(xy, points) { - var dist = Infinity; + var minDist = Infinity, dist; for (var i=0; i 0) { parentIdx = (idx - 1) >> 1; - if (greaterThan(idx, parentIdx)) { + if (heavierThan(idx, parentIdx)) { break; } swapItems(idx, parentIdx); @@ -93,6 +115,7 @@ export function Heap() { heapArr[heapIdx] = valId; } + // comparator for Visvalingam min heap // @a, @b: Indexes in @heapArr function greaterThan(a, b) { var idx1 = heapArr[a], @@ -107,14 +130,21 @@ export function Heap() { return (val1 > val2 || val1 === val2 && idx1 > idx2); } + // comparator for max heap + function lessThan(a, b) { + var idx1 = heapArr[a], + idx2 = heapArr[b]; + return dataArr[idx1] < dataArr[idx2]; + } + function compareDown(idx) { var a = 2 * idx + 1, b = a + 1, n = itemsInHeap; - if (a < n && greaterThan(idx, a)) { + if (a < n && heavierThan(idx, a)) { idx = a; } - if (b < n && greaterThan(idx, b)) { + if (b < n && heavierThan(idx, b)) { idx = b; } return idx; From 389d09fd7c37ac128143af272c0349c01f71f98f Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 14 Apr 2021 20:05:58 -0400 Subject: [PATCH 004/891] Add test --- test/dots-test.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 test/dots-test.js diff --git a/test/dots-test.js b/test/dots-test.js new file mode 100644 index 000000000..a32ad61a8 --- /dev/null +++ b/test/dots-test.js @@ -0,0 +1,22 @@ +import { + getDataRecord +} from '../src/commands/mapshaper-dots'; + +var api = require('../'), + assert = require('assert'); + +describe('mapshaper-dots.js', function () { + describe('getDataRecord()', function () { + it('copy_fields option', function () { + var out = getDataRecord(0, {foo: 2, bar: 'a'}, {copy_fields: ['foo', 'bar']}); + assert.deepEqual(out, {foo: 2, bar: 'a'}); + }) + + it('colors option', function () { + var out = getDataRecord(1, null, {colors: ['red', 'green']}); + assert.deepEqual(out, {fill: 'green', r: 1.5}) + }) + + }) + +}) From de8a6b637b72bfa62e2a53c2e5fa1972f5ea4d25 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 18 Apr 2021 19:50:08 -0400 Subject: [PATCH 005/891] [gui] allow setting layer names in export dialog --- src/dataset/mapshaper-target-utils.js | 2 +- src/gui/gui-export-control.js | 170 +++++++++++------- src/gui/gui-layer-control.js | 18 +- ...ui-layer-sorting.js => gui-layer-utils.js} | 10 ++ src/gui/gui-map.js | 2 +- www/index.html | 2 +- www/page.css | 6 +- 7 files changed, 134 insertions(+), 76 deletions(-) rename src/gui/{gui-layer-sorting.js => gui-layer-utils.js} (72%) diff --git a/src/dataset/mapshaper-target-utils.js b/src/dataset/mapshaper-target-utils.js index c11fcbf94..90b7c1902 100644 --- a/src/dataset/mapshaper-target-utils.js +++ b/src/dataset/mapshaper-target-utils.js @@ -26,7 +26,7 @@ export function findCommandTargets(layers, pattern, type) { } // arr: array of {layer: <>, dataset: <>} objects -function groupLayersByDataset(arr) { +export function groupLayersByDataset(arr) { var datasets = []; var targets = []; arr.forEach(function(o) { diff --git a/src/gui/gui-export-control.js b/src/gui/gui-export-control.js index 44b9caf39..4feca7951 100644 --- a/src/gui/gui-export-control.js +++ b/src/gui/gui-export-control.js @@ -1,15 +1,17 @@ import { internal, utils, error } from './gui-core'; import { SimpleButton } from './gui-elements'; -import { sortLayersForMenuDisplay } from './gui-layer-sorting'; +import { sortLayersForMenuDisplay, cleanLayerName, formatLayerNameForDisplay } from './gui-layer-utils'; import { El } from './gui-el'; import { GUI } from './gui-lib'; +import { ClickText2 } from './gui-elements'; +import { groupLayersByDataset } from '../dataset/mapshaper-target-utils'; // Export buttons and their behavior export var ExportControl = function(gui) { var model = gui.model; var unsupportedMsg = "Exporting is not supported in this browser"; var menu = gui.container.findChild('.export-options').on('click', GUI.handleDirectEvent(gui.clearMode)); - var checkboxes = []; // array of layer checkboxes + var layersArr = []; var toggleBtn = null; // checkbox for toggling layer selection var exportBtn = gui.container.findChild('.export-btn'); new SimpleButton(menu.findChild('.cancel-btn')).on('click', gui.clearMode); @@ -28,18 +30,40 @@ export var ExportControl = function(gui) { gui.keyboard.onMenuSubmit(menu, onExportClick); } + function turnOn() { + layersArr = initLayerMenu(); + initFormatMenu(); + menu.show(); + } + + function turnOff() { + layersArr = []; + menu.hide(); + } + + function getSelectedLayers() { + var targets = layersArr.reduce(function(memo, o) { + return o.checkbox.checked ? memo.concat(o.target) : memo; + }, []); + return groupLayersByDataset(targets); + } + function onExportClick() { - gui.showProgressMessage('Exporting'); + var layers = getSelectedLayers(); + if (layers.length === 0) { + return gui.alert('No layers were selected'); + } gui.clearMode(); + gui.showProgressMessage('Exporting'); setTimeout(function() { - exportMenuSelection(function(err) { + exportMenuSelection(layers, function(err) { if (err) { if (utils.isString(err)) { gui.alert(err); } else { // stack seems to change if Error is logged directly console.error(err.stack); - gui.alert("Export failed for an unknown reason"); + gui.alert('Export failed for an unknown reason'); } } gui.clearProgressMessage(); @@ -59,16 +83,12 @@ export var ExportControl = function(gui) { return freeform.trim(); } - // @done function(string|Error|null) - function exportMenuSelection(done) { - var opts, files, layers; + // done: function(string|Error|null) + function exportMenuSelection(layers, done) { + var opts, files; try { opts = getExportOpts(); - // ignoring command line "target" option - layers = getTargetLayers(); - if (layers.length === 0) { - return done('No layers were selected'); - } + // note: command line "target" option gets ignored files = internal.exportTargetLayers(layers, opts); gui.session.layersExported(getTargetLayerIds(), getExportOptsAsString()); } catch(e) { @@ -77,63 +97,105 @@ export var ExportControl = function(gui) { internal.writeFiles(files, opts, done); } + function initLayerItem(o, i) { + var template = ' %s'; + var target = { + dataset: o.dataset, + // shallow-copy layer, so it can be renamed in the export dialog + // without changing its name elsewhere + layer: Object.assign({}, o.layer) + }; + var html = utils.format(template, i + 1, target.layer.name || '[unnamed layer]'); + // return {layer: o.layer, html: html}; + var el = El('div').html(html).addClass('layer-item'); + var box = el.findChild('input').node(); + box.addEventListener('click', updateToggleBtn); + + new ClickText2(el.findChild('.layer-name')) + .on('change', function(e) { + var str = cleanLayerName(this.value()); + this.value(formatLayerNameForDisplay(str)); + target.layer.name = str; + // gui.session.layerRenamed(target.layer, str); + }); + + + return { + target: target, + el: el, + checkbox: box + }; + } + + function initSelectAll() { + var toggleHtml = ''; + var el = El('div').html(toggleHtml); + var btn = el.findChild('input').node(); + toggleBtn = btn; + + btn.addEventListener('click', function() { + var state = getSelectionState(); + if (state == 'all') { + setLayerSelection(false); + } else { + setLayerSelection(true); + } + updateToggleBtn(); + }); + return el; + } + function initLayerMenu() { var list = menu.findChild('.export-layer-list').empty(); - var template = ''; - var objects = model.getLayers().map(function(o, i) { - var html = utils.format(template, i + 1, o.layer.name || '[unnamed layer]'); - return {layer: o.layer, html: html}; - }); - var toggleHtml = utils.format(template, 'toggle', 'Select All'); - - // only add a 'select all' button for three or more layers - if (objects.length > 2) { - toggleBtn = El('div').html(toggleHtml).appendTo(list).findChild('input').node(); - toggleBtn.addEventListener('click', function() { - var state = getSelectionState(); - if (state == 'all') { - setLayerSelection(false); - } else { - setLayerSelection(true); - } - updateToggleBtn(); - }); + var layers = model.getLayers(); + sortLayersForMenuDisplay(layers); + + if (layers.length > 2) { + // add select all toggle + list.appendChild(initSelectAll()); } - sortLayersForMenuDisplay(objects); - checkboxes = objects.map(function(o) { - var box = El('div').html(o.html).appendTo(list).findChild('input').node(); - box.addEventListener('click', updateToggleBtn); - return box; + // add layers to menu + var objects = layers.map(function(target, i) { + var o = initLayerItem(target, i); + list.appendChild(o.el); + return o; }); - menu.findChild('.export-layers').css('display', checkboxes.length < 2 ? 'none' : 'block'); + + // hide checkbox if only one layer + if (layers.length < 2) { + menu.findChild('.export-layers input').css('display', 'none'); + } + + // update menu title + gui.container.findChild('.export-layers .menu-title').html(layers.length == 1 ? 'Layer name' : 'Layers'); + + return objects; } function setLayerSelection(checked) { - checkboxes.forEach(function(box) { - box.checked = !!checked; + layersArr.forEach(function(o) { + o.checkbox.checked = !!checked; }); } function updateToggleBtn() { if (!toggleBtn) return; var state = getSelectionState(); - // style of intermediate state doesn't look right in Chrome -- removing + // style of intermediate checkbox state doesn't look right in Chrome -- + // removing intermediate state, only using checked and unchecked states if (state == 'all') { toggleBtn.checked = true; - //toggleBtn.indeterminate = false; } else if (state == 'some') { toggleBtn.checked = false; - //toggleBtn.indeterminate = true; } else { toggleBtn.checked = false; - //toggleBtn.indeterminate = false; } } function getSelectionState() { var count = getTargetLayerIds().length; - if (count == checkboxes.length) return 'all'; + if (count == layersArr.length) return 'all'; if (count === 0) return 'none'; return 'some'; } @@ -162,29 +224,15 @@ export var ExportControl = function(gui) { menu.findChild('.export-formats input[value="' + getDefaultExportFormat() + '"]').node().checked = true; } - function turnOn() { - initLayerMenu(); - initFormatMenu(); - menu.show(); - } - - function turnOff() { - menu.hide(); - } - function getSelectedFormat() { return menu.findChild('.export-formats input:checked').node().value; } function getTargetLayerIds() { - return checkboxes.reduce(function(memo, box, i) { - if (box.checked) memo.push(box.value); + return layersArr.reduce(function(memo, o, i) { + if (o.checkbox.checked) memo.push(o.checkbox.value); return memo; }, []); } - function getTargetLayers() { - var ids = getTargetLayerIds().join(','); - return ids ? model.findCommandTargets(ids) : []; - } }; diff --git a/src/gui/gui-layer-control.js b/src/gui/gui-layer-control.js index 9bbce34a7..80ee874ba 100644 --- a/src/gui/gui-layer-control.js +++ b/src/gui/gui-layer-control.js @@ -1,5 +1,8 @@ import { DomCache } from './gui-dom-cache'; -import { sortLayersForMenuDisplay } from './gui-layer-sorting'; +import { + sortLayersForMenuDisplay, + formatLayerNameForDisplay, + cleanLayerName } from './gui-layer-utils'; import { utils, internal } from './gui-core'; import { El } from './gui-el'; import { ClickText2 } from './gui-elements'; @@ -171,7 +174,7 @@ export function LayerControl(gui) { if (lyr.pinned) classes += ' pinned'; html = '
'; - html += rowHTML('name', '' + getDisplayName(lyr.name) + '', 'row1'); + html += rowHTML('name', '' + formatLayerNameForDisplay(lyr.name) + '', 'row1'); if (opts.show_source) { html += rowHTML('source file', describeSrc(lyr, dataset) || 'n/a'); } @@ -235,7 +238,6 @@ export function LayerControl(gui) { } function initMouseEvents2(entry, id, pinnable) { - initLayerDragging(entry, id); // init delete button @@ -272,7 +274,7 @@ export function LayerControl(gui) { .on('change', function(e) { var target = findLayerById(id); var str = cleanLayerName(this.value()); - this.value(getDisplayName(str)); + this.value(formatLayerNameForDisplay(str)); target.layer.name = str; gui.session.layerRenamed(target.layer, str); updateMenuBtn(); @@ -329,18 +331,12 @@ export function LayerControl(gui) { return internal.getLayerSourceFile(lyr, dataset); } - function getDisplayName(name) { - return name || '[unnamed]'; - } function isPinnable(lyr) { return internal.layerHasGeometry(lyr) || internal.layerHasFurniture(lyr); } - function cleanLayerName(raw) { - return raw.replace(/[\n\t/\\]/g, '') - .replace(/^[\.\s]+/, '').replace(/[\.\s]+$/, ''); - } + function rowHTML(c1, c2, cname) { return utils.format('
%s
' + diff --git a/src/gui/gui-layer-sorting.js b/src/gui/gui-layer-utils.js similarity index 72% rename from src/gui/gui-layer-sorting.js rename to src/gui/gui-layer-utils.js index bcb39a86a..adcdb20d1 100644 --- a/src/gui/gui-layer-sorting.js +++ b/src/gui/gui-layer-utils.js @@ -1,4 +1,14 @@ + +export function formatLayerNameForDisplay(name) { + return name || '[unnamed]'; +} + +export function cleanLayerName(raw) { + return raw.replace(/[\n\t/\\]/g, '') + .replace(/^[\.\s]+/, '').replace(/[\.\s]+$/, ''); +} + export function updateLayerStackOrder(layers) { // 1. assign ascending ids to unassigned layers above the range of other layers layers.forEach(function(o, i) { diff --git a/src/gui/gui-map.js b/src/gui/gui-map.js index 646fb6f2e..255f00fa2 100644 --- a/src/gui/gui-map.js +++ b/src/gui/gui-map.js @@ -3,7 +3,7 @@ import { CoordinatesDisplay } from './gui-coordinates-display'; import { MapNav } from './gui-map-nav'; import { SelectionTool } from './gui-selection-tool'; import { InspectionControl2 } from './gui-inspection-control2'; -import { updateLayerStackOrder } from './gui-layer-sorting'; +import { updateLayerStackOrder } from './gui-layer-utils'; import { SymbolDragging2 } from './gui-symbol-dragging2'; import * as MapStyle from './gui-map-style'; import { MapExtent } from './gui-map-extent'; diff --git a/www/index.html b/www/index.html index f19e1d5cd..f3c841a30 100644 --- a/www/index.html +++ b/www/index.html @@ -133,7 +133,7 @@

Layers

Export menu

-

Layers

+

File format

diff --git a/www/page.css b/www/page.css index 6ad0796c7..d199d8eaf 100644 --- a/www/page.css +++ b/www/page.css @@ -75,6 +75,10 @@ body { border-bottom: 1px dotted #0774a5; } +.dot-underline-black { + border-bottom: 1px dotted black; +} + .nav-btn * { fill: #1385b7; } @@ -803,7 +807,7 @@ img.close-btn:hover, text-decoration: none; } -.layer-item:hover:not(.active):not(.dragging) { +.layer-menu .layer-item:hover:not(.active):not(.dragging) { background-color: #f7f7f7; } From 432380a9c82a9c0f3e13a01a6d411a7cdbcd66a6 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 18 Apr 2021 19:51:17 -0400 Subject: [PATCH 006/891] Tuning of -dots command --- src/cli/mapshaper-options.js | 1 + src/commands/mapshaper-dots.js | 5 +- src/points/mapshaper-dot-density.js | 219 ++++++++++++++++++---------- test/dots-test.js | 2 +- 4 files changed, 148 insertions(+), 79 deletions(-) diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 5d0679556..9bafeab75 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -704,6 +704,7 @@ export function getOptionParser() { describe: 'combine groups of same-color dots into multi-part features', type: 'flag' }) + .option('debug', {type: 'flag'}) .option('target', targetOpt) .option('name', nameOpt) .option('no-replace', noReplaceOpt); diff --git a/src/commands/mapshaper-dots.js b/src/commands/mapshaper-dots.js index 01f55333b..4d1f7493d 100644 --- a/src/commands/mapshaper-dots.js +++ b/src/commands/mapshaper-dots.js @@ -77,6 +77,9 @@ function makeDotsForShape(shp, arcs, rec, opts) { p = dots[i]; if (!p) continue; idx = indexes[i]; + if (p.length === 3 && opts.debug) { + idx = p.pop(); // way to debug dot placement visually + } if (!multipart || idx != prevIdx) { prevIdx = idx; retn.shapes.push(coords = []); @@ -139,7 +142,7 @@ export function getDataRecord(i, d, opts) { var o = {}; if (opts.colors) { o.fill = opts.colors[i]; - o.r = opts.r || 1.5; + o.r = opts.r || 1.3; } else if (opts.r) { o.r = opts.r; } diff --git a/src/points/mapshaper-dot-density.js b/src/points/mapshaper-dot-density.js index 49edb0aed..9f6874b49 100644 --- a/src/points/mapshaper-dot-density.js +++ b/src/points/mapshaper-dot-density.js @@ -8,10 +8,10 @@ import utils from '../utils/mapshaper-utils'; export function placeDotsInPolygon(shp, arcs, n, opts) { var evenness = opts.evenness >= 0 ? Math.min(opts.evenness, 1) : 1; - // TODO: also skip tiny sliver polygons? + // TODO: skip tiny sliver polygons? if (n === 0) return []; if (evenness === 0) return placeDotsRandomly(shp, arcs, n); - // TODO: if n == 1, consider using the 'inner' point + // TODO: if n == 1, consider using the 'inner' point of a polygon return placeDotsEvenly(shp, arcs, n, evenness); } @@ -66,41 +66,50 @@ function placeDot(shp, arcs, grid, bounds) { return null; } +// A method for placing dots in a 2D rectangular space +// evenness: varies from 0-1 +// 0 is purely random +// 1 uses a hybrid approach, first creating a sparse structure of random +// dots, then progressively filling in the spaces between dots +// (0-1) first creates an evenish structure of dots, then places additional +// dots using "dart-throwing" -- picking random points until a point +// is found that exceeds a (variable) distance from any other point +// function DotGrid(bounds, approxQueries, evenness) { var x0 = bounds.xmin; var y0 = bounds.ymin; var w = bounds.width(); var h = bounds.height(); - var approxCells = approxQueries * 0.8; + var k = 0.5 * (evenness - 1) + 1; // k varies from 0.5 to 1 + var approxCells = approxQueries * 0.9 * k; var cols = Math.round(Math.sqrt(approxCells * w / h)) || 1; var rows = Math.ceil(cols * h / w); // overshoots bbox height var gridWidth = w; var gridHeight = w * rows / cols; var cells = cols * rows; + var cellSize = gridWidth / cols; var cellId = -1; var grid = initGrid(cells); // data used by optimal method var bestPoints; var bestHeap; - // Estimate the initial distance threshold between dots (based on a square grid) - // When evenness == 1, the initial value should be larger than the final - // spacing between dots, after all the dots are added. + // Set the initial distance threshold between dots (based on a square grid) // When evenness < 1 (dart-throwing mode) the distance threshold is reduced in // proportion to the value of evenness. - var k = Math.pow(evenness, 0.85); // applying a curve seems create a better scale - // From trial and error, 0.75 seems to give a good result. - var minDist = gridWidth / cols * 0.75 * k; + // From trial and error, a 0.7 constant seems to give good results. + var initialDotSpacing = gridWidth / cols * 0.7 * evenness; + var dotSpacing = initialDotSpacing; // metrics var queries = 0; - var lastDistSq = 0; var bestCount = 0; - return {done: done, getPoint: getPoint}; + this.done = done; + this.getPoint = getPoint; function done() { - // console.log( 'queries:', queries,'bestPct:', pct(bestCount, queries), "minDist:", Math.round(minDist), "lastDist:", Math.round(Math.sqrt(lastDistSq))); + // console.log( 'queries:', queries,'bestPct:', pct(bestCount, queries), "dotSpacing:", Math.round(dotSpacing)); function pct(a, b) { return Math.round(a / b * 100) + '%'; } @@ -120,20 +129,26 @@ function DotGrid(bounds, approxQueries, evenness) { } function getOptimalPoint() { + var p = getFirstFillPoint(); + if (p) return usePoint(p); + + // fill in the gaps of the initial placement, starting with the largest gap + if (!bestPoints) { + initBestPoints(); + } + return useBestPoint(); + } + + // try to place a random but spaced point in each grid cell + // (to create an initial sparse structure that gets filled in later) + function getFirstFillPoint() { var p; - // first, try to place a random but well-spaced point in each grid cell - // (to create an initial sparse structure that gets filled in later) while (++cellId < cells) { p = getRandomPointInCell(cellId); if (pointIsUsable(p)) { - return usePoint(p); + return p; } } - // fill in the gaps of the initial placement, starting with the largest gap - if (!bestPoints) { - initBestPoints(); - } - return useBestPoint(); } function getSpacedPoint() { @@ -141,7 +156,9 @@ function DotGrid(bounds, approxQueries, evenness) { var probesBeforeRelaxation = Math.ceil(Math.pow(cells, 0.8)); var maxProbes = cells * 10; var probes = 0; - var p; + var p = getFirstFillPoint(); + if (p) return usePoint(p); + while (probes++ < maxProbes) { p = getRandomPoint(); if (pointIsUsable(p)) { @@ -149,7 +166,7 @@ function DotGrid(bounds, approxQueries, evenness) { } if (probes % probesBeforeRelaxation === 0) { // relax min dist after a number of failed probes - minDist *= 0.9; + dotSpacing *= 0.9; } } return null; @@ -167,7 +184,7 @@ function DotGrid(bounds, approxQueries, evenness) { var p = bestPoints[bestId]; usePoint(p); // add to grid of used points updateNeighbors(p, bestId); // update best point of this cell and neighbors - lastDistSq = bestHeap.peekValue(); + dotSpacing = bestHeap.peekValue(); bestCount++; return p; } @@ -175,10 +192,8 @@ function DotGrid(bounds, approxQueries, evenness) { function initBestPoints() { var values = []; bestPoints = []; - var distSq; for (var i=0; i 0) { - p = getRandomPointInCell(i); - dist = findDistanceFromNeighbors(maxDist, p, i); - if (dist > maxDist) { - maxDist = dist; - best = p; + var dist, p, bestPoint; + for (var i=0; i maxDist) { + maxDist = dist; + bestPoint = p; + } } } - bestPoints[i] = best; + bestPoints[idx] = bestPoint; return maxDist; } function findBestPointInCell(idx) { - // use a grid pattern to find the best point - var perSide = 5; + // Find a point by finding the best-placed center point in a grid of sub-cells, + // then recursively dividing the winning sub-cell + var r = idxToRow(idx); + var c = idxToCol(idx); + var p = findBestPointInSubCell(c, r, 0, 0, 1); + bestPoints[idx] = p; + return p.pop(); + } + + // c, r: location of parent cell in the grid + // c1, r1: index of sub-cell at the given z-value + // z: depth of recursive subdivision + function findBestPointInSubCell(c, r, c1, r1, z) { + // using a 3x3 grid instead of 2x2 ... testing showed that 2x2 was more + // likely to misidentify the sub-cell with the optimal point + var q = 3; + var perSide = Math.pow(q, z); // number of cell divisions per axis at this z var maxDist = 0; - var best, p, dist; - for (var i=0, n=perSide*perSide; i maxDist) { - maxDist = dist; - best = p; + var c2, r2, p, best, dist; + for (var i=0; i maxDist) { + maxDist = dist; + best = p; + c2 = i; + r2 = j; + } } } - bestPoints[idx] = best; - return maxDist; + if (z == 2) { // stop subdividing the cell at this level + best.push(maxDist); // return distance as third element + return best; + } else { + return findBestPointInSubCell(c, r, (c1 + c2)*q, (r1 + r2)*q, z + 1); + } } - function getGridPointInCell(cellIdx, i, n) { - var r = idxToRow(cellIdx); - var c = idxToCol(cellIdx); - var dx = (i % n + 0.5) / n; - var dy = (Math.floor(i / n) + 0.5) / n; + function getGridPointInCell(c, r, c2, r2, n) { + var dx = (c2 + 0.5) / n; + var dy = (r2 + 0.5) / n; var x = (dx + c) / cols * w + x0; var y = (dy + r) / rows * h + y0; return [x, y]; } - function findDistanceFromNeighbors(memo, xy, i) { - var dist = Infinity; - var r = idxToRow(i); - var c = idxToCol(i); - dist = reduceDistance(dist, xy, c, r); - if (dist < memo) return 0; // 10-15% speedup - dist = reduceDistance(dist, xy, c+1, r); - dist = reduceDistance(dist, xy, c, r+1); - dist = reduceDistance(dist, xy, c-1, r); - dist = reduceDistance(dist, xy, c, r-1); - dist = reduceDistance(dist, xy, c+1, r+1); - dist = reduceDistance(dist, xy, c+1, r-1); - dist = reduceDistance(dist, xy, c-1, r+1); - dist = reduceDistance(dist, xy, c-1, r-1); + // col, row offsets of a cell and its 8 neighbors + // (ordered to reject unsuitable points faster) + var nabes = [ + [0, 0], [0, -1], [-1, 0], [1, 0], [0, 1], + [-1, 1], [1, -1], [-1, -1], [1, 1] + ]; + + function findDistanceFromNearbyFeatures(memo, xy, c, r) { + var minDistSq = Infinity; + var offs, c2, r2, distSq, dist; + for (var i=0; i<9; i++) { + offs = nabes[i]; + c2 = offs[0]; + r2 = offs[1]; + distSq = distSqFromPointsInCell(xy, c + c2, r + r2); + if (distSq < memo * memo) { + // short-circuit rejection of this point (optimization) + // -- it is closer than a previously tested point + return 0; + } + if (distSq < minDistSq) { + minDistSq = distSq; + } + } + dist = Math.sqrt(minDistSq); + // maintain distance from grid edge + // (this prevents two sets of dots from appearing right along the edges of + // rectangular polygons). + dist = Math.min(dist, spaceFromEdge(xy, c, r)); return dist; } - function reduceDistance(memo, xy, c, r) { - var i = colRowToIdx(c, r); - if (i == -1) return memo; // off the edge - var distSq = pointToPointsDistSq(xy, grid[i]); - return distSq < memo ? distSq : memo; + function spaceFromEdge(xy, c, r) { + // ignore edges if cell is internal to the grid + if (c > 0 && r > 0 && c < cols-1 && r < rows-1) return Infinity; + var x = xy[0], y = xy[1]; + // exaggerating the true distance to prevent a visible gutter from appearing + // along the borders of shapes with rectangular edges. + return Math.min(x - x0, x0 + w - x, y - y0, y0 + h - y) * 3; } - function pointToPointsDistSq(xy, points) { + function distSqFromPointsInCell(xy, c, r) { var minDist = Infinity, dist; + var idx = colRowToIdx(c, r); + var points = idx > -1 ? grid[idx] : []; // off the edge for (var i=0; i Date: Sun, 18 Apr 2021 19:54:02 -0400 Subject: [PATCH 007/891] v0.5.45 --- CHANGELOG.md | 4 ++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd479b3ca..2be149863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.45 +* [web ui] Made layer names in the export dialog settable. +* Tuned parameters in the -dots command. + v0.5.44 * Added "per-dot=" option to the -dots command, for setting the value-to-dot ratio. To represent 100 people per dot on a population map, you would use per-dot=100. * Added "copy-fields=" option to -dots, to copy one or more data fields from the original polygon layer to the generated dot features. diff --git a/package-lock.json b/package-lock.json index 39a6f451a..5da00289a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.44", + "version": "0.5.45", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 7ff676acb..434f90cf9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.44", + "version": "0.5.45", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 1a2f3b0b9a7fdf578b3b250cd580a24dd2f6074c Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 18 Apr 2021 23:38:01 -0400 Subject: [PATCH 008/891] Bug fixes --- src/commands/mapshaper-dots.js | 6 ++++-- src/commands/mapshaper-rectangle.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/commands/mapshaper-dots.js b/src/commands/mapshaper-dots.js index 4d1f7493d..85bee5b16 100644 --- a/src/commands/mapshaper-dots.js +++ b/src/commands/mapshaper-dots.js @@ -37,8 +37,10 @@ cmd.dots = function(lyr, arcs, opts) { var d = records[i]; if (!d) return; var data = makeDotsForShape(shp, arcs, d, opts); - shapes2.push.apply(shapes2, data.shapes); - records2.push.apply(records2, data.attributes); + for (var j=0, n=data.shapes.length; j Date: Mon, 19 Apr 2021 22:23:18 -0400 Subject: [PATCH 009/891] Fix for issue #476 --- src/io/mapshaper-import.js | 11 +--- src/points/mapshaper-dot-density.js | 2 +- src/utils/mapshaper-filename-utils.js | 47 +++++++-------- ...h-utils-test.js => filename-utils-test.js} | 57 ++++++++++++++++--- 4 files changed, 76 insertions(+), 41 deletions(-) rename test/{path-utils-test.js => filename-utils-test.js} (64%) diff --git a/src/io/mapshaper-import.js b/src/io/mapshaper-import.js index 032727920..075560f20 100644 --- a/src/io/mapshaper-import.js +++ b/src/io/mapshaper-import.js @@ -59,7 +59,9 @@ export function importContent(obj, opts) { // Use file basename for layer name, except TopoJSON, which uses object names if (fileFmt != 'topojson') { dataset.layers.forEach(function(lyr) { - setLayerName(lyr, filenameToLayerName(data.filename || '')); + if (!lyr.name) { + lyr.name = filenameToLayerName(data.filename || ''); + } }); } @@ -122,10 +124,3 @@ function filenameToLayerName(path) { } return name; } - -// initialize layer name using filename -function setLayerName(lyr, path) { - if (!lyr.name) { - lyr.name = getFileBase(path); - } -} diff --git a/src/points/mapshaper-dot-density.js b/src/points/mapshaper-dot-density.js index 9f6874b49..e95eadc89 100644 --- a/src/points/mapshaper-dot-density.js +++ b/src/points/mapshaper-dot-density.js @@ -52,7 +52,7 @@ function placeDotsEvenly(shp, arcs, n, evenness) { return coords; } -function placeDot(shp, arcs, grid, bounds) { +function placeDot(shp, arcs, grid) { var i = 0; var limit = 100; var p; diff --git a/src/utils/mapshaper-filename-utils.js b/src/utils/mapshaper-filename-utils.js index db01db7e1..cac1f3a82 100644 --- a/src/utils/mapshaper-filename-utils.js +++ b/src/utils/mapshaper-filename-utils.js @@ -1,40 +1,35 @@ import utils from '../utils/mapshaper-utils'; -export function replaceFileExtension(path, ext) { - var info = parseLocalPath(path); - return info.pathbase + '.' + ext; -} - function getPathSep(path) { // TODO: improve return path.indexOf('/') == -1 && path.indexOf('\\') != -1 ? '\\' : '/'; } // Parse the path to a file without using Node -// Assumes: not a directory path +// Guess if the path is a directory or file export function parseLocalPath(path) { - var obj = {}, + var obj = { + filename: '', + directory: '', + basename: '', + extension: '' + }, sep = getPathSep(path), parts = path.split(sep), - i; + lastPart = parts.pop(), + // try to match typical extensions but reject directory names with dots + extRxp = /\.([a-z][a-z0-9]*)$/i; - if (parts.length == 1) { - obj.filename = parts[0]; - obj.directory = ""; - } else { - obj.filename = parts.pop(); + if (extRxp.test(lastPart)) { + obj.filename = lastPart; + obj.extension = extRxp.exec(lastPart)[1]; + obj.basename = lastPart.slice(0, lastPart.length - obj.extension.length - 1); + obj.directory = parts.join(sep); + } else if (!lastPart) { // path ends with separator obj.directory = parts.join(sep); - } - i = obj.filename.lastIndexOf('.'); - if (i > -1) { - obj.extension = obj.filename.substr(i + 1); - obj.basename = obj.filename.substr(0, i); - obj.pathbase = path.substr(0, path.lastIndexOf('.')); } else { - obj.extension = ""; - obj.basename = obj.filename; - obj.pathbase = path; + obj.directory = path; } return obj; } @@ -48,7 +43,13 @@ export function getFileExtension(path) { } export function getPathBase(path) { - return parseLocalPath(path).pathbase; + var info = parseLocalPath(path); + if (!info.extension) return path; + return path.slice(0, path.length - info.extension.length - 1); +} + +export function replaceFileExtension(path, ext) { + return getPathBase(path) + '.' + ext; } export function getCommonFileBase(names) { diff --git a/test/path-utils-test.js b/test/filename-utils-test.js similarity index 64% rename from test/path-utils-test.js rename to test/filename-utils-test.js index 055b1579e..0eb7de7c8 100644 --- a/test/path-utils-test.js +++ b/test/filename-utils-test.js @@ -1,4 +1,5 @@ var api = require('../'), + internal = api.internal, assert = require('assert'); describe('mapshaper-filename-utils.js', function () { @@ -24,12 +25,35 @@ describe('mapshaper-filename-utils.js', function () { }) }) + describe('getPathBase()', function () { + it('test1', function () { + var base = internal.getPathBase('out/file.json') + assert.equal(base, 'out/file'); + }) + + it('test2', function () { + var base = internal.getPathBase('file.json') + assert.equal(base, 'file'); + }) + }) + + describe('replaceFileExtension()', function () { + it('test1', function () { + var base = internal.replaceFileExtension('out/file.json', 'geojson') + assert.equal(base, 'out/file.geojson'); + }) + + it('test2', function () { + var base = internal.replaceFileExtension('file.json', 'txt') + assert.equal(base, 'file.txt'); + }) + }) + describe('parseLocalPath()', function () { var path1 = "shapefiles/usa.shp"; it(path1, function () { assert.deepEqual(api.internal.parseLocalPath(path1), { extension: "shp", - pathbase: "shapefiles/usa", basename: "usa", filename: "usa.shp", directory: "shapefiles" @@ -39,7 +63,6 @@ describe('mapshaper-filename-utils.js', function () { it("handle wildcard", function () { assert.deepEqual(api.internal.parseLocalPath("shapefiles/*.shp"), { extension: "shp", - pathbase: "shapefiles/*", basename: "*", filename: "*.shp", directory: "shapefiles" @@ -49,7 +72,6 @@ describe('mapshaper-filename-utils.js', function () { it("handle Windows paths", function () { assert.deepEqual(api.internal.parseLocalPath("shapefiles\\*.shp"), { extension: "shp", - pathbase: "shapefiles\\*", basename: "*", filename: "*.shp", directory: "shapefiles" @@ -60,7 +82,6 @@ describe('mapshaper-filename-utils.js', function () { it(path2, function () { assert.deepEqual(api.internal.parseLocalPath(path2), { extension: "shp", - pathbase: "usa", basename: "usa", filename: "usa.shp", directory: "" @@ -71,7 +92,6 @@ describe('mapshaper-filename-utils.js', function () { it(path3, function () { assert.deepEqual(api.internal.parseLocalPath(path3), { extension: "shp", - pathbase: "../usa", basename: "usa", filename: "usa.shp", directory: ".." @@ -82,10 +102,29 @@ describe('mapshaper-filename-utils.js', function () { it(path4, function () { assert.deepEqual(api.internal.parseLocalPath(path4), { extension: "", - pathbase: "shapefiles/usa", - basename: "usa", - filename: "usa", - directory: "shapefiles" + basename: "", + filename: "", + directory: "shapefiles/usa" + }) + }) + + var path5 = "shapefiles/usa.json/"; + it(path5, function () { + assert.deepEqual(api.internal.parseLocalPath(path5), { + extension: "", + basename: "", + filename: "", + directory: "shapefiles/usa.json" + }) + }) + + var path6 = "shapefiles/04.02"; + it(path6, function () { + assert.deepEqual(api.internal.parseLocalPath(path6), { + extension: "", + basename: "", + filename: "", + directory: "shapefiles/04.02" }) }) From 6bf4eed7cef1a0fd89834f3b8299610db2e72022 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 19 Apr 2021 22:41:08 -0400 Subject: [PATCH 010/891] v0.5.46 --- CHANGELOG.md | 4 ++++ package-lock.json | 26 +++++++++++++------------- package.json | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2be149863..20536f95b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.46 +* Fix for issue #476. + + v0.5.45 * [web ui] Made layer names in the export dialog settable. * Tuned parameters in the -dots command. diff --git a/package-lock.json b/package-lock.json index 5da00289a..f5fcb374d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.45", + "version": "0.5.46", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -859,24 +859,24 @@ } }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } diff --git a/package.json b/package.json index 434f90cf9..b93001e04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.45", + "version": "0.5.46", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From d42a5218751e89dc07b4efc332daea8c9ae450dc Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 30 Apr 2021 18:09:24 -0400 Subject: [PATCH 011/891] Bug fix, style tweak --- CHANGELOG.md | 1 - src/gui/gui-box-tool.js | 52 ++++++++++++++++++++++------------------ test/path-import-test.js | 4 +--- www/page.css | 14 ++++++----- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20536f95b..402973217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,6 @@ v0.5.46 * Fix for issue #476. - v0.5.45 * [web ui] Made layer names in the export dialog settable. * Tuned parameters in the -dots command. diff --git a/src/gui/gui-box-tool.js b/src/gui/gui-box-tool.js index 3dd0713c0..27e67c05c 100644 --- a/src/gui/gui-box-tool.js +++ b/src/gui/gui-box-tool.js @@ -5,13 +5,15 @@ import { internal } from './gui-core'; import { El } from './gui-el'; import { GUI } from './gui-lib'; +// Controls both the shift-drag zoom-to-extent tool and the shift-drag editing tool + export function BoxTool(gui, ext, mouse, nav) { var self = new EventDispatcher(); var box = new HighlightBox('body'); var popup = gui.container.findChild('.box-tool-options'); var coords = popup.findChild('.box-coords'); var _on = false; - var bbox, bboxPixels; + var bboxCoords, bboxPixels; var infoBtn = new SimpleButton(popup.findChild('.info-btn')).on('click', function() { if (coords.visible()) hideCoords(); else showCoords(); @@ -27,13 +29,6 @@ export function BoxTool(gui, ext, mouse, nav) { // reset(); // }); - new SimpleButton(popup.findChild('.select-btn')).on('click', function() { - gui.enterMode('selection_tool'); - gui.interaction.setMode('selection'); - // kludge to pass bbox to the selection tool - gui.dispatchEvent('box_drag_end', {map_bbox: bboxPixels}); - }); - // Removing button for creating a layer containing a single rectangle. // You can get the bbox with the Info button and create a rectangle in the console // using -rectangle bbox= @@ -41,8 +36,15 @@ export function BoxTool(gui, ext, mouse, nav) { // runCommand('-rectangle bbox=' + bbox.join(',')); // }); + new SimpleButton(popup.findChild('.select-btn')).on('click', function() { + gui.enterMode('selection_tool'); + gui.interaction.setMode('selection'); + // kludge to pass bbox to the selection tool + gui.dispatchEvent('box_drag_end', {map_bbox: bboxPixels}); + }); + new SimpleButton(popup.findChild('.clip-btn')).on('click', function() { - runCommand('-clip bbox2=' + bbox.join(',')); + runCommand('-clip bbox2=' + bboxCoords.join(',')); }); gui.addMode('box_tool', turnOn, turnOff); @@ -55,9 +57,11 @@ export function BoxTool(gui, ext, mouse, nav) { } }); + // Update the visible rectangle when the map view changes + // (e.g. during zooming or panning) ext.on('change', function() { - if (!_on || !box.visible()) return; - var b = bboxToPixels(bbox); + if (!_on || !box.visible() || !bboxCoords) return; + var b = coordsToPix(bboxCoords); var pos = ext.position(); var dx = pos.pageX, dy = pos.pageY; @@ -65,32 +69,31 @@ export function BoxTool(gui, ext, mouse, nav) { }); gui.on('box_drag_start', function() { - box.classed('zooming', zoomDragging()); + box.classed('zooming', inZoomMode()); hideCoords(); }); gui.on('box_drag', function(e) { var b = e.page_bbox; - if (_on || zoomDragging()) { + bboxPixels = e.map_bbox; + bboxCoords = pixToCoords(bboxPixels); + if (_on || inZoomMode()) { box.show(b[0], b[1], b[2], b[3]); } }); gui.on('box_drag_end', function(e) { bboxPixels = e.map_bbox; - if (zoomDragging()) { + bboxCoords = pixToCoords(bboxPixels); + if (inZoomMode()) { box.hide(); nav.zoomToBbox(bboxPixels); } else if (_on) { - bbox = bboxToCoords(bboxPixels); - // round coords, for nicer 'info' display - // (rounded precision should be sub-pixel) - bbox = internal.getRoundedCoords(bbox, internal.getBoundsPrecisionForDisplay(bbox)); popup.show(); } }); - function zoomDragging() { + function inZoomMode() { return !_on && gui.getMode() != 'selection_tool'; } @@ -105,7 +108,7 @@ export function BoxTool(gui, ext, mouse, nav) { function showCoords() { El(infoBtn.node()).addClass('selected-btn'); - coords.text(bbox.join(',')); + coords.text(bboxCoords.join(',')); coords.show(); GUI.selectElement(coords.node()); } @@ -134,13 +137,16 @@ export function BoxTool(gui, ext, mouse, nav) { hideCoords(); } - function bboxToCoords(bbox) { + function pixToCoords(bbox) { var a = ext.translatePixelCoords(bbox[0], bbox[1]); var b = ext.translatePixelCoords(bbox[2], bbox[3]); - return [a[0], b[1], b[0], a[1]]; + var bbox2 = [a[0], b[1], b[0], a[1]]; + // round coords, for nicer 'info' display + // (rounded precision should be sub-pixel) + return internal.getRoundedCoords(bbox2, internal.getBoundsPrecisionForDisplay(bbox2)); } - function bboxToPixels(bbox) { + function coordsToPix(bbox) { var a = ext.translateCoords(bbox[0], bbox[1]); var b = ext.translateCoords(bbox[2], bbox[3]); return [a[0], b[1], b[0], a[1]]; diff --git a/test/path-import-test.js b/test/path-import-test.js index f0ba885c6..53a58b7f8 100644 --- a/test/path-import-test.js +++ b/test/path-import-test.js @@ -39,8 +39,6 @@ describe('mapshaper-path-import.js', function () { }); }) - }) - -}) \ No newline at end of file +}) diff --git a/www/page.css b/www/page.css index d199d8eaf..ffc8f6d74 100644 --- a/www/page.css +++ b/www/page.css @@ -486,16 +486,21 @@ body.dragover #import-options-drop-area .drop-area { text-align: center; } +.info-box,.popup { + border-radius: 9px; + border: 1px solid #ddd; + box-shadow: 0 4px 6px rgba(0,0,0,0.35); + background-color: #fff; +} + .info-box { word-wrap: break-word; text-align: left; margin-top: 12px; - background-color: #fff; padding: 11px 15px 9px 15px; vertical-align: top; display: inline-block; - border: 1px solid #aaa; - border-radius: 9px; + /* border: 1px solid #aaa; */ } .info-box h3 { @@ -890,9 +895,6 @@ img.close-btn:hover, z-index: 40; top: 12px; left: 12px; - background-color: white; - border: 1px solid #999; - border-radius: 9px; padding: 5px 0; } From 1519c51401ef4e1b42d62d8b637347ce292112c0 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 3 May 2021 18:09:56 -0400 Subject: [PATCH 012/891] v0.5.47 --- CHANGELOG.md | 3 +++ package-lock.json | 8 ++++---- package.json | 4 ++-- www/modules.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 402973217..01922ea58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.47 +* Updated projections library to include the "Cupola" projection. + v0.5.46 * Fix for issue #476. diff --git a/package-lock.json b/package-lock.json index f5fcb374d..e0014deb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.46", + "version": "0.5.47", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1475,9 +1475,9 @@ } }, "mproj": { - "version": "0.0.27", - "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.27.tgz", - "integrity": "sha512-MJ04mobvsbkmqWDq0ETQMzoSUDzMtO/4J6CYNS7f1blsdkwDpsywQPXr5uvS70o1ucpdGfvwD8KIut5B/IKMqQ==", + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.29.tgz", + "integrity": "sha512-FWsNIka7NXfKVEVu6m5CbnSsRNShb+3/LTHAvEMqvyX0LGPeErGbE6SzgxhKBCaCbzi0F0dT6wPXSjxWafaK2w==", "requires": { "geographiclib": "1.48.0", "rw": "~1.3.2" diff --git a/package.json b/package.json index b93001e04..514039c15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.46", + "version": "0.5.47", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", @@ -43,7 +43,7 @@ "d3-scale-chromatic": "^2.0.0", "flatbush": "^3.2.1", "iconv-lite": "0.4.24", - "mproj": "0.0.27", + "mproj": "0.0.29", "opn": "^5.3.0", "rw": "~1.3.3", "sync-request": "5.0.0" diff --git a/www/modules.js b/www/modules.js index d04b5abec..2f41219f6 100644 --- a/www/modules.js +++ b/www/modules.js @@ -16027,6 +16027,48 @@ function pj_crast(P) { } +pj_add(pj_cupola, 'cupola', 'Cupola', '\n\tMisc., Sph., NoInv.'); + +// Source: https://github.com/OSGeo/PROJ/issues/2706 +// See also: http://www.at-a-lanta.nl/weia/cupola.html + +function pj_cupola(P) { + // parameters for Cupola + var c1 = 0.5253; // part of the equator on intermediate sphere, default = 1 + var c2 = 0.7264; // sin of angle of polarline; default = 1 + var c3 = 0.4188; // height of the equator, can be negative, default = 0 + var c4 = 22.00; // phi of centre projection in degrees, default = 0 + var c5 = 0.9701; // stretch in plane, default = 1 + var c6 = 11.023; // central meridian 11.023 degrees, also defines border of the map + var c7 = 180.00; // degrees to the right of c6, default = 180 + var r2 = 1.61885660611815; + var w123 = 0.258701109423297; + var c4r = 0.383972435438752; + var pc = 0.559562341761853; + var spc = sin(pc); + var cpc = cos(pc); + var c6r = DEG_TO_RAD * (c6-c7+180); // c6 in [rad], v of center of projection + var qc = c1*c6r; + + P.es = 0; + P.fwd = s_fwd; + + function s_fwd(lp, xy) { + // p, q: radians on intermediate sphere + var p = asin(c2 * sin(lp.phi) + w123); + var sp = sin(p); + var cp = cos(p); + var q = c1 * lp.lam; + var sqqc = sin(q-qc); + var cqqc = cos(q-qc); + // k is Snyder's constant, taken as product with r2: + var r2k = r2 * sqrt(2/(1+spc*sp+cpc*cp*cqqc)); + xy.x = r2k*cp*sqqc*c5; + xy.y = r2k*(cpc*sp-spc*cp*cqqc)/c5; + } +} + + pj_add(pj_denoy, 'denoy', 'Denoyer Semi-Elliptical', '\n\tPCyl, Sph., no inv.'); function pj_denoy(P) { From 1f362f0d3ee4a21efe54c152170fd91e5ede1e46 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 5 May 2021 10:55:21 -0400 Subject: [PATCH 013/891] v0.5.48 --- package-lock.json | 6 ++--- package.json | 2 +- www/modules.js | 57 +++++++++++++++++++++++++++-------------------- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index e0014deb4..baae91c05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1475,9 +1475,9 @@ } }, "mproj": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.29.tgz", - "integrity": "sha512-FWsNIka7NXfKVEVu6m5CbnSsRNShb+3/LTHAvEMqvyX0LGPeErGbE6SzgxhKBCaCbzi0F0dT6wPXSjxWafaK2w==", + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.30.tgz", + "integrity": "sha512-l/aR8hyHB3ZIWgmsX41Dss+ItkV7JAsMdRe+uObt7icP5AptO1GQB9Pu/idog9XSHllGSurqUv8rbFq1cl6LuQ==", "requires": { "geographiclib": "1.48.0", "rw": "~1.3.2" diff --git a/package.json b/package.json index 514039c15..4588e8da7 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "d3-scale-chromatic": "^2.0.0", "flatbush": "^3.2.1", "iconv-lite": "0.4.24", - "mproj": "0.0.29", + "mproj": "0.0.30", "opn": "^5.3.0", "rw": "~1.3.3", "sync-request": "5.0.0" diff --git a/www/modules.js b/www/modules.js index 2f41219f6..44ddf3c4b 100644 --- a/www/modules.js +++ b/www/modules.js @@ -16029,42 +16029,51 @@ function pj_crast(P) { pj_add(pj_cupola, 'cupola', 'Cupola', '\n\tMisc., Sph., NoInv.'); -// Source: https://github.com/OSGeo/PROJ/issues/2706 +// Source: https://www.tandfonline.com/eprint/EE7Y8RK4GXA4ITWUTQPY/full?target=10.1080/23729333.2020.1862962 // See also: http://www.at-a-lanta.nl/weia/cupola.html function pj_cupola(P) { - // parameters for Cupola - var c1 = 0.5253; // part of the equator on intermediate sphere, default = 1 - var c2 = 0.7264; // sin of angle of polarline; default = 1 - var c3 = 0.4188; // height of the equator, can be negative, default = 0 - var c4 = 22.00; // phi of centre projection in degrees, default = 0 - var c5 = 0.9701; // stretch in plane, default = 1 - var c6 = 11.023; // central meridian 11.023 degrees, also defines border of the map - var c7 = 180.00; // degrees to the right of c6, default = 180 - var r2 = 1.61885660611815; - var w123 = 0.258701109423297; - var c4r = 0.383972435438752; - var pc = 0.559562341761853; + var de = 0.5253; // part of the equator on intermediate sphere, default = 1 + var dp = 0.7264; // sin of angle of polar line; default = 1 + var ri = 1 / Math.sqrt(de * dp); + // height of the equator, can be negative, default = 0 + var he = 0.4188; + // phi of projection center + var phi0 = 22 * DEG_TO_RAD; + + // NOTE: Proj applies the +lon_0 (central meridian) parameter before passing + // coordinates to the projection function. + // TODO: Use 11.023 as default central meridian + // // var lam0 = 11.023 * DEG_TO_RAD; + + var se = 0.9701; // stretch in plane, default = 1 + // center of projection on intermediate sphere + var pc = calcP(phi0); + var qc = calcQ(0); var spc = sin(pc); var cpc = cos(pc); - var c6r = DEG_TO_RAD * (c6-c7+180); // c6 in [rad], v of center of projection - var qc = c1*c6r; P.es = 0; P.fwd = s_fwd; + function calcP(phi) { + return asin(dp * sin(phi) + he * sqrt(de * dp)); + } + + function calcQ(lam) { + return de * lam; + } + function s_fwd(lp, xy) { - // p, q: radians on intermediate sphere - var p = asin(c2 * sin(lp.phi) + w123); + var p = calcP(lp.phi); + var q = calcQ(lp.lam); var sp = sin(p); var cp = cos(p); - var q = c1 * lp.lam; - var sqqc = sin(q-qc); - var cqqc = cos(q-qc); - // k is Snyder's constant, taken as product with r2: - var r2k = r2 * sqrt(2/(1+spc*sp+cpc*cp*cqqc)); - xy.x = r2k*cp*sqqc*c5; - xy.y = r2k*(cpc*sp-spc*cp*cqqc)/c5; + var sqqc = sin(q - qc); + var cqqc = cos(q - qc); + var K = sqrt(2 / (1 + sin(pc) * sp + cpc * cp * cqqc)); + xy.x = ri * K * cp * sqqc * se; + xy.y = ri * K * (cpc * sp - spc * cp * cqqc) / se; } } From ceab9f23cbce1611cdad5e6399952182bf712dff Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 5 May 2021 10:55:44 -0400 Subject: [PATCH 014/891] v0.5.48 --- CHANGELOG.md | 3 +++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01922ea58..fba5fe844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.48 +* Update to Cupola projection. + v0.5.47 * Updated projections library to include the "Cupola" projection. diff --git a/package-lock.json b/package-lock.json index baae91c05..19f8fb207 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.47", + "version": "0.5.48", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4588e8da7..d2fc6b998 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.47", + "version": "0.5.48", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 7910805e81552647bf5977112857d458d143156a Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 5 May 2021 20:13:26 -0400 Subject: [PATCH 015/891] Add src/crs/ subdirectory --- package.json | 3 ++- src/buffer/mapshaper-buffer-common.js | 2 +- src/cli/mapshaper-run-command.js | 2 +- src/cli/mapshaper-run-commands.js | 2 +- src/commands/mapshaper-affine.js | 2 +- src/commands/mapshaper-frame.js | 3 +-- src/commands/mapshaper-graticule.js | 2 +- src/commands/mapshaper-info.js | 2 +- src/commands/mapshaper-point-grid.js | 2 +- src/commands/mapshaper-points.js | 2 +- src/commands/mapshaper-polygon-grid.js | 3 +-- src/commands/mapshaper-proj.js | 4 ++-- src/commands/mapshaper-rectangle.js | 2 +- src/commands/mapshaper-simplify.js | 2 +- src/commands/mapshaper-snap.js | 2 +- src/{geom => crs}/mapshaper-custom-projections.js | 2 +- src/{geom => crs}/mapshaper-mixed-projection.js | 0 src/{geom => crs}/mapshaper-projection-params.js | 2 +- src/{geom => crs}/mapshaper-projections.js | 3 +-- src/dataset/mapshaper-merging.js | 2 +- src/dataset/mapshaper-metadata.js | 2 +- src/geojson/geojson-export.js | 2 +- src/geom/mapshaper-geodesic.js | 2 +- src/mapshaper-internal.js | 4 ++-- src/paths/mapshaper-intersection-cuts.js | 2 +- src/paths/mapshaper-path-import.js | 2 +- src/polygons/mapshaper-slivers.js | 2 +- src/shapefile/shp-export.js | 2 +- src/svg/mapshaper-svg.js | 2 +- src/topology/mapshaper-undershoots.js | 2 +- test/projection-params-test.js | 2 +- 31 files changed, 33 insertions(+), 35 deletions(-) rename src/{geom => crs}/mapshaper-custom-projections.js (97%) rename src/{geom => crs}/mapshaper-mixed-projection.js (100%) rename src/{geom => crs}/mapshaper-projection-params.js (96%) rename src/{geom => crs}/mapshaper-projections.js (98%) diff --git a/package.json b/package.json index d2fc6b998..31a4e5fc5 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "prepublishOnly": "npm test", "postpublish": "./release", "browserify": "browserify -r sync-request -r mproj -r buffer -r iconv-lite -r fs -r flatbush -r rw -r path -r d3-scale-chromatic -r d3-color -r d3-interpolate -o www/modules.js", - "watch": "rollup --config --watch" + "watch": "rollup --config --watch", + "dev": "rollup --config --watch" }, "main": "./mapshaper.js", "files": [ diff --git a/src/buffer/mapshaper-buffer-common.js b/src/buffer/mapshaper-buffer-common.js index edf2957c2..261b9e19e 100644 --- a/src/buffer/mapshaper-buffer-common.js +++ b/src/buffer/mapshaper-buffer-common.js @@ -1,6 +1,6 @@ import { compileValueExpression } from '../expressions/mapshaper-expressions'; -import { getDatasetCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS } from '../crs/mapshaper-projections'; import { convertDistanceParam } from '../geom/mapshaper-units'; import { parseMeasure2 } from '../geom/mapshaper-units'; import { reversePath } from '../paths/mapshaper-path-utils'; diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index 93d50ed6a..c327d8d86 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -1,6 +1,6 @@ import { cleanupArcs, replaceLayers, splitApartLayers } from '../dataset/mapshaper-dataset-utils'; import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; -import { initProjLibrary } from '../geom/mapshaper-projections'; +import { initProjLibrary } from '../crs/mapshaper-projections'; import { writeFiles } from '../io/mapshaper-file-export'; import { exportTargetLayers } from '../io/mapshaper-export'; import { expandCommandTargets } from '../dataset/mapshaper-target-utils'; diff --git a/src/cli/mapshaper-run-commands.js b/src/cli/mapshaper-run-commands.js index b72441a76..4c041a610 100644 --- a/src/cli/mapshaper-run-commands.js +++ b/src/cli/mapshaper-run-commands.js @@ -1,5 +1,5 @@ import { runCommand } from '../cli/mapshaper-run-command'; -import { printProjections } from '../geom/mapshaper-projections'; +import { printProjections } from '../crs/mapshaper-projections'; import { printEncodings } from '../text/mapshaper-encodings'; import { printColorSchemeNames } from '../color/color-schemes'; import { parseCommands } from '../cli/mapshaper-parse-commands'; diff --git a/src/commands/mapshaper-affine.js b/src/commands/mapshaper-affine.js index 653ef73ed..00f001df7 100644 --- a/src/commands/mapshaper-affine.js +++ b/src/commands/mapshaper-affine.js @@ -5,7 +5,7 @@ import { forEachPoint } from '../points/mapshaper-point-utils'; import { countArcsInShapes } from '../paths/mapshaper-path-utils'; import { compileValueExpression } from '../expressions/mapshaper-expressions'; import { layerHasGeometry } from '../dataset/mapshaper-layer-utils'; -import { getDatasetCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS } from '../crs/mapshaper-projections'; import { convertIntervalPair } from '../geom/mapshaper-units'; import { ArcCollection } from '../paths/mapshaper-arcs'; import utils from '../utils/mapshaper-utils'; diff --git a/src/commands/mapshaper-frame.js b/src/commands/mapshaper-frame.js index e81107428..187bc3fab 100644 --- a/src/commands/mapshaper-frame.js +++ b/src/commands/mapshaper-frame.js @@ -5,9 +5,8 @@ import { DataTable } from '../datatable/mapshaper-data-table'; import { message, stop } from '../utils/mapshaper-logging'; import { probablyDecimalDegreeBounds } from '../geom/mapshaper-latlon'; import { Bounds } from '../geom/mapshaper-bounds'; -import { getDatasetCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS, getScaleFactorAtXY} from '../crs/mapshaper-projections'; import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; -import { getScaleFactorAtXY } from '../geom/mapshaper-projections'; import { importPolygon } from '../svg/geojson-to-svg'; import cmd from '../mapshaper-cmd'; import utils from '../utils/mapshaper-utils'; diff --git a/src/commands/mapshaper-graticule.js b/src/commands/mapshaper-graticule.js index 8f60bc62d..7bcd550ef 100644 --- a/src/commands/mapshaper-graticule.js +++ b/src/commands/mapshaper-graticule.js @@ -1,6 +1,6 @@ import { importGeoJSON } from '../geojson/geojson-import'; import { projectDataset } from '../commands/mapshaper-proj'; -import { getDatasetCRS, getCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS, getCRS } from '../crs/mapshaper-projections'; import { stop } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import cmd from '../mapshaper-cmd'; diff --git a/src/commands/mapshaper-info.js b/src/commands/mapshaper-info.js index 74289ea54..de80f2028 100644 --- a/src/commands/mapshaper-info.js +++ b/src/commands/mapshaper-info.js @@ -1,6 +1,6 @@ import { applyFieldOrder } from '../datatable/mapshaper-data-utils'; import { editShapes } from '../paths/mapshaper-shape-utils'; -import { getProjInfo } from '../geom/mapshaper-projections'; +import { getProjInfo } from '../crs/mapshaper-projections'; import { getLayerBounds, getFeatureCount, getLayerSourceFile } from '../dataset/mapshaper-layer-utils'; import utils from '../utils/mapshaper-utils'; import geom from '../geom/mapshaper-geom'; diff --git a/src/commands/mapshaper-point-grid.js b/src/commands/mapshaper-point-grid.js index 4d9cc2b7a..a806f7cd8 100644 --- a/src/commands/mapshaper-point-grid.js +++ b/src/commands/mapshaper-point-grid.js @@ -1,6 +1,6 @@ import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; import { convertIntervalParam } from '../geom/mapshaper-units'; -import { getDatasetCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS } from '../crs/mapshaper-projections'; import { stop } from '../utils/mapshaper-logging'; import cmd from '../mapshaper-cmd'; diff --git a/src/commands/mapshaper-points.js b/src/commands/mapshaper-points.js index 781df1728..b8c702d97 100644 --- a/src/commands/mapshaper-points.js +++ b/src/commands/mapshaper-points.js @@ -2,7 +2,7 @@ import { requirePolylineLayer } from '../dataset/mapshaper-layer-utils'; import { parseDMS } from '../geom/mapshaper-dms'; import { findAnchorPoint } from '../points/mapshaper-anchor-points'; import { polylineToPoint, polylineToMidpoints } from '../paths/mapshaper-polyline-to-point'; -import { getDatasetCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS } from '../crs/mapshaper-projections'; import { convertIntervalParam } from '../geom/mapshaper-units'; import { calcSegmentIntersectionStripeCount } from '../paths/mapshaper-segment-intersection'; import { calcSegmentIntersectionStripeCount2 } from '../paths/mapshaper-segment-intersection'; diff --git a/src/commands/mapshaper-polygon-grid.js b/src/commands/mapshaper-polygon-grid.js index 77698ef42..de0f706d5 100644 --- a/src/commands/mapshaper-polygon-grid.js +++ b/src/commands/mapshaper-polygon-grid.js @@ -1,12 +1,11 @@ import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; import { convertIntervalParam } from '../geom/mapshaper-units'; -import { getDatasetCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS, requireProjectedDataset } from '../crs/mapshaper-projections'; import { importGeoJSON } from '../geojson/geojson-import'; import cmd from '../mapshaper-cmd'; import { stop } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; -import { requireProjectedDataset } from '../geom/mapshaper-projections'; import { buildTopology } from '../topology/mapshaper-topology'; cmd.polygonGrid = function(targetLayers, targetDataset, opts) { requireProjectedDataset(targetDataset); diff --git a/src/commands/mapshaper-proj.js b/src/commands/mapshaper-proj.js index bee1540e2..0f37d4ca2 100644 --- a/src/commands/mapshaper-proj.js +++ b/src/commands/mapshaper-proj.js @@ -8,8 +8,8 @@ import { crsAreEqual, getDatasetCRS, setDatasetCRS -} from '../geom/mapshaper-projections'; -import { expandProjDefn } from '../geom/mapshaper-projection-params'; +} from '../crs/mapshaper-projections'; +import { expandProjDefn } from '../crs/mapshaper-projection-params'; import { layerHasPoints } from '../dataset/mapshaper-layer-utils'; import { datasetHasGeometry } from '../dataset/mapshaper-dataset-utils'; import { runningInBrowser } from '../mapshaper-state'; diff --git a/src/commands/mapshaper-rectangle.js b/src/commands/mapshaper-rectangle.js index 1dd5c688f..b84edd7d3 100644 --- a/src/commands/mapshaper-rectangle.js +++ b/src/commands/mapshaper-rectangle.js @@ -1,6 +1,6 @@ import cmd from '../mapshaper-cmd'; import { convertFourSides } from '../geom/mapshaper-units'; -import { setDatasetCRS, getDatasetCRS, getCRS } from '../geom/mapshaper-projections'; +import { setDatasetCRS, getDatasetCRS, getCRS } from '../crs/mapshaper-projections'; import { getLayerBounds, layerHasGeometry } from '../dataset/mapshaper-layer-utils'; import { mergeDatasetsIntoDataset } from '../dataset/mapshaper-merging'; import { importGeoJSON } from '../geojson/geojson-import'; diff --git a/src/commands/mapshaper-simplify.js b/src/commands/mapshaper-simplify.js index 9db770983..91f28ae91 100644 --- a/src/commands/mapshaper-simplify.js +++ b/src/commands/mapshaper-simplify.js @@ -1,5 +1,5 @@ import { convertIntervalParam, convertDistanceParam } from '../geom/mapshaper-units'; -import { getDatasetCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS } from '../crs/mapshaper-projections'; import { printSimplifyInfo } from '../simplify/mapshaper-simplify-info'; import { postSimplifyRepair } from '../simplify/mapshaper-post-simplify-repair'; import cmd from '../mapshaper-cmd'; diff --git a/src/commands/mapshaper-snap.js b/src/commands/mapshaper-snap.js index 459715768..5c7b549e9 100644 --- a/src/commands/mapshaper-snap.js +++ b/src/commands/mapshaper-snap.js @@ -1,5 +1,5 @@ import { getHighPrecisionSnapInterval, snapCoordsByInterval } from '../paths/mapshaper-snapping'; -import { getDatasetCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS } from '../crs/mapshaper-projections'; import { convertIntervalParam } from '../geom/mapshaper-units'; import { setCoordinatePrecision } from '../geom/mapshaper-rounding'; import { buildTopology } from '../topology/mapshaper-topology'; diff --git a/src/geom/mapshaper-custom-projections.js b/src/crs/mapshaper-custom-projections.js similarity index 97% rename from src/geom/mapshaper-custom-projections.js rename to src/crs/mapshaper-custom-projections.js index e6480c229..990b979d7 100644 --- a/src/geom/mapshaper-custom-projections.js +++ b/src/crs/mapshaper-custom-projections.js @@ -1,4 +1,4 @@ -import {MixedProjection} from '../geom/mapshaper-mixed-projection'; +import {MixedProjection} from '../crs/mapshaper-mixed-projection'; import utils from '../utils/mapshaper-utils'; // str: a custom projection string, e.g.: "albersusa +PR" diff --git a/src/geom/mapshaper-mixed-projection.js b/src/crs/mapshaper-mixed-projection.js similarity index 100% rename from src/geom/mapshaper-mixed-projection.js rename to src/crs/mapshaper-mixed-projection.js diff --git a/src/geom/mapshaper-projection-params.js b/src/crs/mapshaper-projection-params.js similarity index 96% rename from src/geom/mapshaper-projection-params.js rename to src/crs/mapshaper-projection-params.js index 4f8126050..2a3ddf5c1 100644 --- a/src/geom/mapshaper-projection-params.js +++ b/src/crs/mapshaper-projection-params.js @@ -1,5 +1,5 @@ import { stop, message } from '../utils/mapshaper-logging'; -import { isLatLngCRS , getDatasetCRS } from '../geom/mapshaper-projections'; +import { isLatLngCRS , getDatasetCRS } from '../crs/mapshaper-projections'; import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; import { getBoundsPrecisionForDisplay } from '../geom/mapshaper-rounding'; diff --git a/src/geom/mapshaper-projections.js b/src/crs/mapshaper-projections.js similarity index 98% rename from src/geom/mapshaper-projections.js rename to src/crs/mapshaper-projections.js index 5da3c1cf1..40a9a1196 100644 --- a/src/geom/mapshaper-projections.js +++ b/src/crs/mapshaper-projections.js @@ -1,9 +1,8 @@ -import { AlbersUSA, parseCustomProjection } from '../geom/mapshaper-custom-projections'; +import { AlbersUSA, parseCustomProjection } from '../crs/mapshaper-custom-projections'; import { stop, print } from '../utils/mapshaper-logging'; import { probablyDecimalDegreeBounds } from '../geom/mapshaper-latlon'; import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; import { getStateVar } from '../mapshaper-state'; - import utils from '../utils/mapshaper-utils'; import geom from '../geom/mapshaper-geom'; diff --git a/src/dataset/mapshaper-merging.js b/src/dataset/mapshaper-merging.js index d7441f1c0..dde4b3676 100644 --- a/src/dataset/mapshaper-merging.js +++ b/src/dataset/mapshaper-merging.js @@ -1,4 +1,4 @@ -import { isLatLngCRS, getDatasetCRS } from '../geom/mapshaper-projections'; +import { isLatLngCRS, getDatasetCRS } from '../crs/mapshaper-projections'; import { forEachArcId } from '../paths/mapshaper-path-utils'; import { copyLayerShapes } from '../dataset/mapshaper-layer-utils'; import utils from '../utils/mapshaper-utils'; diff --git a/src/dataset/mapshaper-metadata.js b/src/dataset/mapshaper-metadata.js index 1b695da47..8b108da63 100644 --- a/src/dataset/mapshaper-metadata.js +++ b/src/dataset/mapshaper-metadata.js @@ -1,5 +1,5 @@ -import { getCRS, crsToProj4, getDatasetCRS } from '../geom/mapshaper-projections'; +import { getCRS, crsToProj4, getDatasetCRS } from '../crs/mapshaper-projections'; export function importMetadata(dataset, obj) { if (obj.proj4) { diff --git a/src/geojson/geojson-export.js b/src/geojson/geojson-export.js index e5e9c3445..67bbd299c 100644 --- a/src/geojson/geojson-export.js +++ b/src/geojson/geojson-export.js @@ -3,7 +3,7 @@ import { groupPolygonRings } from '../polygons/mapshaper-ring-nesting'; import { exportPathData } from '../paths/mapshaper-path-export'; import { forEachPoint } from '../points/mapshaper-point-utils'; import { layerHasPoints, layerHasPaths } from '../dataset/mapshaper-layer-utils'; -import { isLatLngCRS, getDatasetCRS } from '../geom/mapshaper-projections'; +import { isLatLngCRS, getDatasetCRS } from '../crs/mapshaper-projections'; import { getFormattedStringify, stringifyAsNDJSON } from '../geojson/mapshaper-stringify'; import { mergeLayerNames } from '../commands/mapshaper-merge-layers'; import { setCoordinatePrecision } from '../geom/mapshaper-rounding'; diff --git a/src/geom/mapshaper-geodesic.js b/src/geom/mapshaper-geodesic.js index 73c445779..d0e519ac9 100644 --- a/src/geom/mapshaper-geodesic.js +++ b/src/geom/mapshaper-geodesic.js @@ -1,4 +1,4 @@ -import { isLatLngCRS, getDatasetCRS } from '../geom/mapshaper-projections'; +import { isLatLngCRS, getDatasetCRS } from '../crs/mapshaper-projections'; import { error } from '../utils/mapshaper-logging'; import geom from '../geom/mapshaper-geom'; diff --git a/src/mapshaper-internal.js b/src/mapshaper-internal.js index 21dfd3645..a3a4c65f0 100644 --- a/src/mapshaper-internal.js +++ b/src/mapshaper-internal.js @@ -76,7 +76,7 @@ import * as Catalog from './dataset/mapshaper-catalog'; import * as ClipErase from './commands/mapshaper-clip-erase'; import * as ClipPoints from './clipping/mapshaper-point-clipping'; import * as Colorizer from './commands/mapshaper-colorizer'; -import * as CustomProjections from './geom/mapshaper-custom-projections'; +import * as CustomProjections from './crs/mapshaper-custom-projections'; import * as DataAggregation from './dissolve/mapshaper-data-aggregation'; import * as DatasetUtils from './dataset/mapshaper-dataset-utils'; import * as DataUtils from './datatable/mapshaper-data-utils'; @@ -142,7 +142,7 @@ import * as PolygonTiler from './polygons/mapshaper-polygon-tiler'; import * as PolylineClipping from './clipping/mapshaper-polyline-clipping'; import * as PostSimplifyRepair from './simplify/mapshaper-post-simplify-repair'; import * as Proj from './commands/mapshaper-proj'; -import * as Projections from './geom/mapshaper-projections'; +import * as Projections from './crs/mapshaper-projections'; import * as Rectangle from './commands/mapshaper-rectangle'; import * as Rounding from './geom/mapshaper-rounding'; import * as RunCommands from './cli/mapshaper-run-commands'; diff --git a/src/paths/mapshaper-intersection-cuts.js b/src/paths/mapshaper-intersection-cuts.js index 8425c7243..cb2c2640e 100644 --- a/src/paths/mapshaper-intersection-cuts.js +++ b/src/paths/mapshaper-intersection-cuts.js @@ -3,7 +3,7 @@ import { getHighPrecisionSnapInterval, snapCoordsByInterval } from '../paths/map import { convertIntervalParam } from '../geom/mapshaper-units'; import { debug, error } from '../utils/mapshaper-logging'; import { NodeCollection } from '../topology/mapshaper-nodes'; -import { getDatasetCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS } from '../crs/mapshaper-projections'; import { layerHasPaths, getArcPresenceTest2 } from '../dataset/mapshaper-layer-utils'; import { cleanShapes } from '../paths/mapshaper-path-repair-utils'; import { buildTopology } from '../topology/mapshaper-topology'; diff --git a/src/paths/mapshaper-path-import.js b/src/paths/mapshaper-path-import.js index 881946512..536c435d0 100644 --- a/src/paths/mapshaper-path-import.js +++ b/src/paths/mapshaper-path-import.js @@ -1,5 +1,5 @@ -import { getDatasetCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS } from '../crs/mapshaper-projections'; import { convertIntervalParam } from '../geom/mapshaper-units'; import { snapCoords } from '../paths/mapshaper-snapping'; import { layerHasPaths, divideFeaturesByType } from '../dataset/mapshaper-layer-utils'; diff --git a/src/polygons/mapshaper-slivers.js b/src/polygons/mapshaper-slivers.js index ebb9fb617..f23062f87 100644 --- a/src/polygons/mapshaper-slivers.js +++ b/src/polygons/mapshaper-slivers.js @@ -1,6 +1,6 @@ import { roundToSignificantDigits } from '../geom/mapshaper-rounding'; -import { getDatasetCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS } from '../crs/mapshaper-projections'; import { convertAreaParam, getAreaLabel } from '../geom/mapshaper-units'; import geom from '../geom/mapshaper-geom'; import { forEachSegmentInPath } from '../paths/mapshaper-path-utils'; diff --git a/src/shapefile/shp-export.js b/src/shapefile/shp-export.js index da21106c8..bc48c1321 100644 --- a/src/shapefile/shp-export.js +++ b/src/shapefile/shp-export.js @@ -1,7 +1,7 @@ import { exportPathData } from '../paths/mapshaper-path-export'; import { getFeatureCount } from '../dataset/mapshaper-layer-utils'; import { findMaxPartCount } from '../paths/mapshaper-shape-utils'; -import { getDatasetCRS, crsToPrj } from '../geom/mapshaper-projections'; +import { getDatasetCRS, crsToPrj } from '../crs/mapshaper-projections'; import { exportDbfFile } from '../shapefile/dbf-export'; import { message, error } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; diff --git a/src/svg/mapshaper-svg.js b/src/svg/mapshaper-svg.js index ae782a8fd..5f32a2262 100644 --- a/src/svg/mapshaper-svg.js +++ b/src/svg/mapshaper-svg.js @@ -1,5 +1,5 @@ import { exportDatasetAsGeoJSON } from '../geojson/geojson-export'; -import { getDatasetCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS } from '../crs/mapshaper-projections'; import { getFurnitureLayerData, layerHasFurniture, importFurniture } from '../furniture/mapshaper-furniture'; import { findFrameLayerInDataset } from '../commands/mapshaper-frame'; import { setCoordinatePrecision } from '../geom/mapshaper-rounding'; diff --git a/src/topology/mapshaper-undershoots.js b/src/topology/mapshaper-undershoots.js index d17912364..339a7fbb0 100644 --- a/src/topology/mapshaper-undershoots.js +++ b/src/topology/mapshaper-undershoots.js @@ -1,7 +1,7 @@ import { addIntersectionCuts } from '../paths/mapshaper-intersection-cuts'; import { getArcPresenceTest } from '../paths/mapshaper-path-utils'; -import { getDatasetCRS } from '../geom/mapshaper-projections'; +import { getDatasetCRS } from '../crs/mapshaper-projections'; import { convertIntervalParam } from '../geom/mapshaper-units'; import geom from '../geom/mapshaper-geom'; import { PathIndex } from '../paths/mapshaper-path-index'; diff --git a/test/projection-params-test.js b/test/projection-params-test.js index 87fb58185..10a00b8ae 100644 --- a/test/projection-params-test.js +++ b/test/projection-params-test.js @@ -1,4 +1,4 @@ -import { getConicParams, getCenterParams, expandProjDefn } from '../src/geom/mapshaper-projection-params' +import { getConicParams, getCenterParams, expandProjDefn } from '../src/crs/mapshaper-projection-params' import assert from 'assert'; describe('mapshaper-projection-params.js', function () { From 4c01df068b0209229a57c6f48768fee2b777932c Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 6 May 2021 12:01:44 -0400 Subject: [PATCH 016/891] Split shapes when applying a rotated world projection --- CHANGELOG.md | 3 + package.json | 4 +- src/commands/mapshaper-clean.js | 6 +- src/commands/mapshaper-clip-erase.js | 2 +- src/commands/mapshaper-graticule.js | 52 +++- src/commands/mapshaper-proj.js | 92 ++---- src/crs/mapshaper-densify.js | 65 +++++ src/crs/mapshaper-proj-info.js | 32 +++ src/crs/mapshaper-spherical-cutting.js | 38 +++ src/dissolve/mapshaper-polygon-dissolve2.js | 2 +- src/geom/mapshaper-latlon.js | 6 + www/modules.js | 302 ++++++++++---------- 12 files changed, 366 insertions(+), 238 deletions(-) create mode 100644 src/crs/mapshaper-densify.js create mode 100644 src/crs/mapshaper-proj-info.js create mode 100644 src/crs/mapshaper-spherical-cutting.js diff --git a/CHANGELOG.md b/CHANGELOG.md index fba5fe844..dccb9ee91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.49 +* Split polylines and lines that cross the rotated antimeridan when applying a world projection with a non-zero central meridian. + v0.5.48 * Update to Cupola projection. diff --git a/package.json b/package.json index 31a4e5fc5..4e08db706 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.48", + "version": "0.5.49", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", @@ -28,7 +28,7 @@ "postpublish": "./release", "browserify": "browserify -r sync-request -r mproj -r buffer -r iconv-lite -r fs -r flatbush -r rw -r path -r d3-scale-chromatic -r d3-color -r d3-interpolate -o www/modules.js", "watch": "rollup --config --watch", - "dev": "rollup --config --watch" + "dev": "rollup --config --watch" }, "main": "./mapshaper.js", "files": [ diff --git a/src/commands/mapshaper-clean.js b/src/commands/mapshaper-clean.js index 0d354c9cc..abb9aec5b 100644 --- a/src/commands/mapshaper-clean.js +++ b/src/commands/mapshaper-clean.js @@ -8,7 +8,9 @@ import { buildTopology } from '../topology/mapshaper-topology'; import utils from '../utils/mapshaper-utils'; import cmd from '../mapshaper-cmd'; -cmd.cleanLayers = function(layers, dataset, optsArg) { +cmd.cleanLayers = cleanLayers; + +export function cleanLayers(layers, dataset, optsArg) { var opts = optsArg || {}; var deepClean = !opts.only_arcs; var pathClean = utils.some(layers, layerHasPaths); @@ -43,7 +45,7 @@ cmd.cleanLayers = function(layers, dataset, optsArg) { // remove leftover endpoints within contiguous lines dissolveArcs(dataset); } -}; +} function cleanPolygonLayerGeometry(lyr, dataset, opts) { var groups = lyr.shapes.map(function(shp, i) { diff --git a/src/commands/mapshaper-clip-erase.js b/src/commands/mapshaper-clip-erase.js index 3cbb05934..8c2d8892a 100644 --- a/src/commands/mapshaper-clip-erase.js +++ b/src/commands/mapshaper-clip-erase.js @@ -39,7 +39,7 @@ cmd.sliceLayer = function(targetLyr, src, dataset, opts) { // @clipSrc: layer in @dataset or filename // @type: 'clip' or 'erase' -function clipLayers(targetLayers, clipSrc, targetDataset, type, opts) { +export function clipLayers(targetLayers, clipSrc, targetDataset, type, opts) { var usingPathClip = utils.some(targetLayers, layerHasPaths); var mergedDataset, clipLyr, nodes; opts = opts || {no_cleanup: true}; // TODO: update testing functions diff --git a/src/commands/mapshaper-graticule.js b/src/commands/mapshaper-graticule.js index 7bcd550ef..eb51cf219 100644 --- a/src/commands/mapshaper-graticule.js +++ b/src/commands/mapshaper-graticule.js @@ -1,41 +1,67 @@ import { importGeoJSON } from '../geojson/geojson-import'; import { projectDataset } from '../commands/mapshaper-proj'; import { getDatasetCRS, getCRS } from '../crs/mapshaper-projections'; +import { isRotatedWorldProjection } from '../crs/mapshaper-proj-info'; +import { getAntimeridian } from '../geom/mapshaper-latlon'; import { stop } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import cmd from '../mapshaper-cmd'; cmd.graticule = function(dataset, opts) { - var graticule = createGraticule(opts); - var dest, src; + var graticule, dest, src; if (dataset) { // project graticule to match dataset dest = getDatasetCRS(dataset); src = getCRS('wgs84'); if (!dest) stop("Coordinate system is unknown, unable to create a graticule"); + graticule = createGraticuleForProjection(dest, opts); projectDataset(graticule, src, dest, {}); // TODO: densify? + } else { + graticule = createGraticule(0, opts); } return graticule; }; +function createGraticuleForProjection(P, opts) { + var lon0 = 0; + // see mapshaper-spherical-cutting.js + if (isRotatedWorldProjection(P)) { + lon0 = P.lam0 * 180 / Math.PI; + } + return createGraticule(lon0, opts); +} + // create graticule as a dataset -function createGraticule(opts) { +function createGraticule(lon0, opts) { var precision = 1; // degrees between each vertex - var step = 10; - var majorStep = 90; - var xn = Math.round(360 / step) + 1; - var yn = Math.round(180 / step) + 1; - var xx = utils.range(xn, -180, step); - var yy = utils.range(yn, -90, step); + var xstep = 10; + var ystep = 10; + var xstepMajor = 90; + var antimeridian = getAntimeridian(lon0); + var isRotated = lon0 != 0; + var e = 2e-8; + var xn = Math.round(360 / xstep) + (isRotated ? 0 : 1); + var yn = Math.round(180 / ystep) + 1; + var xx = utils.range(xn, -180, xstep); + var yy = utils.range(yn, -90, ystep); var meridians = xx.map(function(x) { var ymin = -90, ymax = 90; - if (x % majorStep !== 0) { - ymin += step; - ymax -= step; + if (isRotated && Math.abs(x - antimeridian) < xstep / 5) { + // skip meridians that are close to the enclosure of a rotated graticule + return null; + } + if (x % xstepMajor !== 0) { + ymin += ystep; + ymax -= ystep; } return createMeridian(x, ymin, ymax, precision); - }); + }).filter(o => !!o); + if (isRotated) { + // this kludge adds + meridians.push(createMeridian(antimeridian - e, -90, 90, precision)); + meridians.push(createMeridian(antimeridian + e, -90, 90, precision)); + } var parallels = yy.map(function(y) { return createParallel(y, -180, 180, precision); }); diff --git a/src/commands/mapshaper-proj.js b/src/commands/mapshaper-proj.js index 0f37d4ca2..fda013041 100644 --- a/src/commands/mapshaper-proj.js +++ b/src/commands/mapshaper-proj.js @@ -1,4 +1,3 @@ -import { getAvgSegment2 } from '../paths/mapshaper-path-utils'; import { editArcs } from '../paths/mapshaper-arc-editor'; import { editShapes, cloneShapes } from '../paths/mapshaper-shape-utils'; import { @@ -9,8 +8,12 @@ import { getDatasetCRS, setDatasetCRS } from '../crs/mapshaper-projections'; +import { insertPreProjectionCuts } from '../crs/mapshaper-spherical-cutting'; +import { cleanLayers } from '../commands/mapshaper-clean'; +import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; +import { projectAndDensifyArcs } from '../crs/mapshaper-densify'; import { expandProjDefn } from '../crs/mapshaper-projection-params'; -import { layerHasPoints } from '../dataset/mapshaper-layer-utils'; +import { layerHasPoints, copyLayerShapes } from '../dataset/mapshaper-layer-utils'; import { datasetHasGeometry } from '../dataset/mapshaper-dataset-utils'; import { runningInBrowser } from '../mapshaper-state'; import { stop, message } from '../utils/mapshaper-logging'; @@ -72,11 +75,11 @@ function projCmd(dataset, destInfo, opts) { target.arcs = modifyCopy ? dataset.arcs.getCopy() : dataset.arcs; } - target.layers = dataset.layers.filter(layerHasPoints).map(function(lyr) { + // target.layers = dataset.layers.filter(layerHasPoints).map(function(lyr) { + target.layers = dataset.layers.map(function(lyr) { if (modifyCopy) { originals.push(lyr); - lyr = utils.extend({}, lyr); - lyr.shapes = cloneShapes(lyr.shapes); + lyr = copyLayerShapes(lyr); } return lyr; }); @@ -126,13 +129,15 @@ export function getCrsInfo(name, catalog) { export function projectDataset(dataset, src, dest, opts) { var proj = getProjTransform2(src, dest); // v2 returns null points instead of throwing an error - var errors; + var errors, cuts; dataset.layers.forEach(function(lyr) { if (layerHasPoints(lyr)) { projectPointLayer(lyr, proj); // v2 compatible (invalid points are removed) } }); if (dataset.arcs) { + cuts = insertPreProjectionCuts(dataset, src, dest); + if (opts.densify) { errors = projectAndDensifyArcs(dataset.arcs, proj); } else { @@ -142,9 +147,23 @@ export function projectDataset(dataset, src, dest, opts) { // TODO: implement this (null arcs have zero length) // internal.removeShapesWithNullArcs(dataset); } + + if (cuts) { + cleanProjectedLayers(dataset); + } } } +function cleanProjectedLayers(dataset) { + // heal cuts in previously split-apart polygons + // TODO: only clean affected polygons (cleaning all polygons can be slow) + var polygonLayers = dataset.layers.filter(lyr => lyr.geometry_type == 'polygon'); + cleanLayers(polygonLayers, dataset, {no_arc_dissolve: true, quiet: true}); + // remove unused arcs from polygon and polyline layers + // TODO: fix bug that leaves uncut arcs in the arc table + // (e.g. when projecting a graticule) + dissolveArcs(dataset); +} // proj: function to project [x, y] point; should return null if projection fails // TODO: fatal error if no points project? @@ -184,64 +203,3 @@ function projectArcs2(arcs, proj) { } } -function projectAndDensifyArcs(arcs, proj) { - var interval = getDefaultDensifyInterval(arcs, proj); - var p = [0, 0]; - return editArcs(arcs, onPoint); - - function onPoint(append, lng, lat, prevLng, prevLat, i) { - var prevX = p[0], - prevY = p[1]; - p = proj(lng, lat); - if (!p) return false; // signal that current arc contains an error - - // Don't try to densify shorter segments (optimization) - if (i > 0 && geom.distanceSq(p[0], p[1], prevX, prevY) > interval * interval * 25) { - densifySegment(prevLng, prevLat, prevX, prevY, lng, lat, p[0], p[1], proj, interval) - .forEach(append); - } - append(p); - } -} - -function getDefaultDensifyInterval(arcs, proj) { - var xy = getAvgSegment2(arcs), - bb = arcs.getBounds(), - a = proj(bb.centerX(), bb.centerY()), - b = proj(bb.centerX() + xy[0], bb.centerY() + xy[1]), - c = proj(bb.centerX(), bb.ymin), // right center - d = proj(bb.xmax, bb.centerY()), // bottom center - // interval A: based on average segment length - intervalA = geom.distance2D(a[0], a[1], b[0], b[1]), - // interval B: a fraction of avg bbox side length - // (added this for bbox densification) - intervalB = (geom.distance2D(a[0], a[1], c[0], c[1]) + - geom.distance2D(a[0], a[1], d[0], d[1])) / 5000; - return Math.min(intervalA, intervalB); -} - -// Interpolate points into a projected line segment if needed to prevent large -// deviations from path of original unprojected segment. -// @points (optional) array of accumulated points -function densifySegment(lng0, lat0, x0, y0, lng2, lat2, x2, y2, proj, interval, points) { - // Find midpoint between two endpoints and project it (assumes longitude does - // not wrap). TODO Consider bisecting along great circle path -- although this - // would not be good for boundaries that follow line of constant latitude. - var lng1 = (lng0 + lng2) / 2, - lat1 = (lat0 + lat2) / 2, - p = proj(lng1, lat1), - distSq; - if (!p) return; // TODO: consider if this is adequate for handling proj. errors - distSq = geom.pointSegDistSq2(p[0], p[1], x0, y0, x2, y2); // sq displacement - points = points || []; - // Bisect current segment if the projected midpoint deviates from original - // segment by more than the @interval parameter. - // ... but don't bisect very small segments to prevent infinite recursion - // (e.g. if projection function is discontinuous) - if (distSq > interval * interval * 0.25 && geom.distance2D(lng0, lat0, lng2, lat2) > 0.01) { - densifySegment(lng0, lat0, x0, y0, lng1, lat1, p[0], p[1], proj, interval, points); - points.push(p); - densifySegment(lng1, lat1, p[0], p[1], lng2, lat2, x2, y2, proj, interval, points); - } - return points; -} diff --git a/src/crs/mapshaper-densify.js b/src/crs/mapshaper-densify.js new file mode 100644 index 000000000..d372595ce --- /dev/null +++ b/src/crs/mapshaper-densify.js @@ -0,0 +1,65 @@ +import geom from '../geom/mapshaper-geom'; +import { editArcs } from '../paths/mapshaper-arc-editor'; +import { getAvgSegment2 } from '../paths/mapshaper-path-utils'; + +export function projectAndDensifyArcs(arcs, proj) { + var interval = getDefaultDensifyInterval(arcs, proj); + var p = [0, 0]; + return editArcs(arcs, onPoint); + + function onPoint(append, lng, lat, prevLng, prevLat, i) { + var prevX = p[0], + prevY = p[1]; + p = proj(lng, lat); + if (!p) return false; // signal that current arc contains an error + + // Don't try to densify shorter segments (optimization) + if (i > 0 && geom.distanceSq(p[0], p[1], prevX, prevY) > interval * interval * 25) { + densifySegment(prevLng, prevLat, prevX, prevY, lng, lat, p[0], p[1], proj, interval) + .forEach(append); + } + append(p); + } +} + +function getDefaultDensifyInterval(arcs, proj) { + var xy = getAvgSegment2(arcs), + bb = arcs.getBounds(), + a = proj(bb.centerX(), bb.centerY()), + b = proj(bb.centerX() + xy[0], bb.centerY() + xy[1]), + c = proj(bb.centerX(), bb.ymin), // right center + d = proj(bb.xmax, bb.centerY()), // bottom center + // interval A: based on average segment length + intervalA = geom.distance2D(a[0], a[1], b[0], b[1]), + // interval B: a fraction of avg bbox side length + // (added this for bbox densification) + intervalB = (geom.distance2D(a[0], a[1], c[0], c[1]) + + geom.distance2D(a[0], a[1], d[0], d[1])) / 5000; + return Math.min(intervalA, intervalB); +} + +// Interpolate points into a projected line segment if needed to prevent large +// deviations from path of original unprojected segment. +// @points (optional) array of accumulated points +function densifySegment(lng0, lat0, x0, y0, lng2, lat2, x2, y2, proj, interval, points) { + // Find midpoint between two endpoints and project it (assumes longitude does + // not wrap). TODO Consider bisecting along great circle path -- although this + // would not be good for boundaries that follow line of constant latitude. + var lng1 = (lng0 + lng2) / 2, + lat1 = (lat0 + lat2) / 2, + p = proj(lng1, lat1), + distSq; + if (!p) return; // TODO: consider if this is adequate for handling proj. errors + distSq = geom.pointSegDistSq2(p[0], p[1], x0, y0, x2, y2); // sq displacement + points = points || []; + // Bisect current segment if the projected midpoint deviates from original + // segment by more than the @interval parameter. + // ... but don't bisect very small segments to prevent infinite recursion + // (e.g. if projection function is discontinuous) + if (distSq > interval * interval * 0.25 && geom.distance2D(lng0, lat0, lng2, lat2) > 0.01) { + densifySegment(lng0, lat0, x0, y0, lng1, lat1, p[0], p[1], proj, interval, points); + points.push(p); + densifySegment(lng1, lat1, p[0], p[1], lng2, lat2, x2, y2, proj, interval, points); + } + return points; +} diff --git a/src/crs/mapshaper-proj-info.js b/src/crs/mapshaper-proj-info.js new file mode 100644 index 000000000..74065aa4b --- /dev/null +++ b/src/crs/mapshaper-proj-info.js @@ -0,0 +1,32 @@ + +export function getCrsSlug(P) { + return P.params.proj.param; // kludge +} + +export function isWorldProjection(P) { + return getWorldProjections().includes(getCrsSlug(P)); +} + +export function isRotatedWorldProjection(P) { + return isWorldProjection(P) && P.lam0 !== 0; +} + +// TODO: rename this function +// These are projections that cover the entire world and can be rotated horizontally +// +// not included +// bertin1953 (doesn't rotate) +// euler (world? seems to need more params) +// murd1,murd2,murd3 (missing param) +// +// not implemented +// bonne,cc,collg,comill,fahey,igh,larr,lask +// +function getWorldProjections() { + return 'robin,cupola,wintri,aitoff,apian,august,bacon,boggs,cea,crast,' + + 'denoy,eck1,eck2,eck3,eck4,eck5,eck6,eqc,eqearth,fouc,gall,gilbert,gins8,goode,' + + 'hammer,hatano,igh,kav5,kav7,loxim,mbt_fpp,mbt_fpq,mbt_fps,mbt_s,mbtfps,mill,' + + 'moll,natearth,natearth2,nell,nell_h,ortel,patterson,putp1,putp2,putp3,putp3p,' + + 'putp4p,putp5,putp5p,putp6,putp6p,qua_aut,times,vandg,vandg2,vandg3,vandg4,' + + 'wag1,wag2,wag3,wag4,wag5,wag6,wag7,weren,wink1,wink2'.split(); +} diff --git a/src/crs/mapshaper-spherical-cutting.js b/src/crs/mapshaper-spherical-cutting.js new file mode 100644 index 000000000..ddc1b3b04 --- /dev/null +++ b/src/crs/mapshaper-spherical-cutting.js @@ -0,0 +1,38 @@ +import { isLatLngCRS } from '../crs/mapshaper-projections'; +import { isRotatedWorldProjection } from '../crs/mapshaper-proj-info'; +import { importGeoJSON } from '../geojson/geojson-import'; +import { clipLayers } from '../commands/mapshaper-clip-erase'; +import { layerHasPaths } from '../dataset/mapshaper-layer-utils'; +import { getAntimeridian } from '../geom/mapshaper-latlon'; + +export function insertPreProjectionCuts(dataset, src, dest) { + if (isLatLngCRS(src) && isRotatedWorldProjection(dest)) { + insertVerticalCut(dataset, getAntimeridian(dest.lam0 * 180 / Math.PI)); + return true; + } + return false; +} + +function insertVerticalCut(dataset, lon) { + var pathLayers = dataset.layers.filter(layerHasPaths); + if (pathLayers.length === 0) return; + var e =1e-8; + var coords = [[lon+e, 90], [lon+e, -90], [lon-e, -90], [lon-e, 90], [lon+e, 90]]; + var geojson = { + type: 'Polygon', + coordinates: [coords] + }; + var clipDataset = importGeoJSON(geojson, {}); + var clip = { + layer: clipDataset.layers[0], + dataset: clipDataset + }; + var outputLayers = clipLayers(pathLayers, clip, dataset, 'erase', {}); + pathLayers.forEach(function(lyr, i) { + var lyr2 = outputLayers[i]; + lyr.shapes = lyr2.shapes; + lyr.data = lyr2.data; + }); +} + + diff --git a/src/dissolve/mapshaper-polygon-dissolve2.js b/src/dissolve/mapshaper-polygon-dissolve2.js index 24b261fae..888b27b8b 100644 --- a/src/dissolve/mapshaper-polygon-dissolve2.js +++ b/src/dissolve/mapshaper-polygon-dissolve2.js @@ -90,7 +90,7 @@ export function dissolvePolygonGroups2(groups, lyr, dataset, opts) { // Simple Features compliance dissolvedShapes = fixTangentHoles(dissolvedShapes, pathfind); var gapMessage = getGapRemovalMessage(cleanupData.removed, cleanupData.remaining, filterData.label); - if (gapMessage) message(gapMessage); + if (gapMessage && !opts.quiet) message(gapMessage); return dissolvedShapes; } diff --git a/src/geom/mapshaper-latlon.js b/src/geom/mapshaper-latlon.js index b2c44f0ac..516b2c75e 100644 --- a/src/geom/mapshaper-latlon.js +++ b/src/geom/mapshaper-latlon.js @@ -18,3 +18,9 @@ export function clampToWorldBounds(b) { return new Bounds().setBounds(Math.max(bbox[0], -180), Math.max(bbox[1], -90), Math.min(bbox[2], 180), Math.min(bbox[3], 90)); } + +export function getAntimeridian(lon0) { + var anti = lon0 - 180; + while (anti <= -180) anti += 360; + return anti; +} diff --git a/www/modules.js b/www/modules.js index 44ddf3c4b..e8ac85bfc 100644 --- a/www/modules.js +++ b/www/modules.js @@ -9695,6 +9695,7 @@ function pj_param(params, code) { obj = params[name], isset = obj !== void 0, val, param; + if (type == 't') { val = isset; } else if (isset) { @@ -14935,8 +14936,8 @@ function pj_msfn(sinphi, cosphi, es) { } -pj_add(pj_aea, 'aea', 'Albers Equal Area', '\n\tConic Sph&Ell\n\tlat_1= lat_2='); -pj_add(pj_leac, 'leac', 'Lambert Equal Area Conic', '\n\tConic, Sph&Ell\n\tlat_1= south'); +pj_add(pj_aea, 'aea', 'Albers Equal Area', 'Conic Sph&Ell\nlat_1= lat_2='); +pj_add(pj_leac, 'leac', 'Lambert Equal Area Conic', 'Conic, Sph&Ell\nlat_1= south'); function pj_aea(P) { var phi1 = pj_param(P.params, "rlat_1"); @@ -15129,7 +15130,7 @@ function aatan2(n, d) { } -pj_add(pj_aeqd, 'aeqd', 'Azimuthal Equidistant', '\n\tAzi, Sph&Ell\n\tlat_0 guam'); +pj_add(pj_aeqd, 'aeqd', 'Azimuthal Equidistant', 'Azi, Sph&Ell\nlat_0 guam'); function pj_aeqd(P) { var EPS10 = 1.e-10, @@ -15340,7 +15341,7 @@ function pj_aeqd(P) { } -pj_add(pj_airy, 'airy', 'Airy', '\n\tMisc Sph, no inv.\n\tno_cut lat_b='); +pj_add(pj_airy, 'airy', 'Airy', 'Misc Sph, no inv.\nno_cut lat_b='); function pj_airy(P) { var EPS = 1e-10, @@ -15425,8 +15426,8 @@ function pj_airy(P) { } -pj_add(pj_wintri, 'wintri', 'Winkel Tripel', '\n\tMisc Sph\n\tlat_1'); -pj_add(pj_aitoff, 'aitoff', 'Aitoff', '\n\tMisc Sph'); +pj_add(pj_wintri, 'wintri', 'Winkel Tripel', 'Misc Sph\nlat_1'); +pj_add(pj_aitoff, 'aitoff', 'Aitoff', 'Misc Sph'); function pj_wintri(P) { var Q = P.opaque = {mode: 1}; @@ -15529,7 +15530,7 @@ function pj_aitoff(P) { } -pj_add(pj_august, 'august', 'August Epicycloidal', '\n\tMisc Sph, no inv.'); +pj_add(pj_august, 'august', 'August Epicycloidal', 'Misc Sph, no inv.'); function pj_august(P) { P.fwd = s_fwd; @@ -15550,9 +15551,9 @@ function pj_august(P) { } -pj_add(pj_apian, 'apian', 'Apian Globular I', '\n\tMisc Sph, no inv.'); -pj_add(pj_ortel, 'ortel', 'Ortelius Oval', '\n\tMisc Sph, no inv.'); -pj_add(pj_bacon, 'bacon', 'Bacon Globular', '\n\tMisc Sph, no inv.'); +pj_add(pj_apian, 'apian', 'Apian Globular I', 'Misc Sph, no inv.'); +pj_add(pj_ortel, 'ortel', 'Ortelius Oval', 'Misc Sph, no inv.'); +pj_add(pj_bacon, 'bacon', 'Bacon Globular', 'Misc Sph, no inv.'); function pj_bacon(P) { pj_bacon_init(P, true, false); @@ -15600,7 +15601,7 @@ function pj_bacon_init(P, bacn, ortl) { Port to PROJ by Philippe Rivière, 21 September 2018 Port to JavaScript by Matthew Bloch October 2018 */ -pj_add(pj_bertin1953, 'bertin1953', 'Bertin 1953', "\n\tMisc Sph no inv."); +pj_add(pj_bertin1953, 'bertin1953', 'Bertin 1953', 'Misc Sph no inv.'); function pj_bertin1953(P) { var cos_delta_phi, sin_delta_phi, cos_delta_gamma, sin_delta_gamma; @@ -15658,7 +15659,7 @@ function pj_bertin1953(P) { } -pj_add(pj_boggs, 'boggs', 'Boggs Eumorphic', '\n\tPCyl., no inv., Sph.'); +pj_add(pj_boggs, 'boggs', 'Boggs Eumorphic', 'PCyl., no inv., Sph.'); function pj_boggs(P) { var NITER = 20, @@ -15691,7 +15692,7 @@ function pj_boggs(P) { } -pj_add(pj_bonne, 'bonne', 'Bonne (Werner lat_1=90)', '\n\tConic Sph&Ell\n\tlat_1='); +pj_add(pj_bonne, 'bonne', 'Bonne (Werner lat_1=90)', 'Conic Sph&Ell\nlat_1='); function pj_bonne(P) { var EPS10 = 1e-10; @@ -15757,7 +15758,7 @@ function pj_bonne(P) { } -pj_add(pj_cass, 'cass', 'Cassini', '\n\tCyl, Sph&Ell'); +pj_add(pj_cass, 'cass', 'Cassini', 'Cyl, Sph&Ell'); function pj_cass(P) { var C1 = 0.16666666666666666666, @@ -15846,7 +15847,7 @@ function pj_authlat(beta, APA) { } -pj_add(pj_cea, 'cea', 'Equal Area Cylindrical', '\n\tCyl, Sph&Ell\n\tlat_ts='); +pj_add(pj_cea, 'cea', 'Equal Area Cylindrical', 'Cyl, Sph&Ell\nlat_ts='); function pj_cea(P) { var t = 0, qp, apa; @@ -15898,7 +15899,7 @@ function pj_cea(P) { } -pj_add(pj_chamb, 'chamb', 'Chamberlin Trimetric', '\n\tMisc Sph, no inv.\n\tlat_1= lon_1= lat_2= lon_2= lat_3= lon_3='); +pj_add(pj_chamb, 'chamb', 'Chamberlin Trimetric', 'Misc Sph, no inv.\nlat_1= lon_1= lat_2= lon_2= lat_3= lon_3='); function pj_chamb(P) { var THIRD = 1/3, @@ -16002,7 +16003,7 @@ function pj_chamb(P) { } -pj_add(pj_crast, 'crast', 'Craster Parabolic (Putnins P4)', '\n\tPCyl., Sph.'); +pj_add(pj_crast, 'crast', 'Craster Parabolic (Putnins P4)', 'PCyl., Sph.'); function pj_crast(P) { var XM = 0.97720502380583984317; @@ -16027,32 +16028,29 @@ function pj_crast(P) { } -pj_add(pj_cupola, 'cupola', 'Cupola', '\n\tMisc., Sph., NoInv.'); +pj_add(pj_cupola, 'cupola', 'Cupola', 'Misc., Sph., NoInv.'); // Source: https://www.tandfonline.com/eprint/EE7Y8RK4GXA4ITWUTQPY/full?target=10.1080/23729333.2020.1862962 // See also: http://www.at-a-lanta.nl/weia/cupola.html function pj_cupola(P) { var de = 0.5253; // part of the equator on intermediate sphere, default = 1 - var dp = 0.7264; // sin of angle of polar line; default = 1 + var dp = 0.7264; // sin of angle of polar line, default = 1 var ri = 1 / Math.sqrt(de * dp); - // height of the equator, can be negative, default = 0 - var he = 0.4188; - // phi of projection center - var phi0 = 22 * DEG_TO_RAD; - - // NOTE: Proj applies the +lon_0 (central meridian) parameter before passing - // coordinates to the projection function. - // TODO: Use 11.023 as default central meridian - // // var lam0 = 11.023 * DEG_TO_RAD; - + var he = 0.4188; // height of equator (can be negative, default = 0) var se = 0.9701; // stretch in plane, default = 1 + var phi0 = 22 * DEG_TO_RAD; // phi of projection center // center of projection on intermediate sphere var pc = calcP(phi0); var qc = calcQ(0); var spc = sin(pc); var cpc = cos(pc); + // apply default central meridian + if (!pj_param(P.params, 'tlon_0')) { + P.lam0 = 11.023 * DEG_TO_RAD; + } + P.es = 0; P.fwd = s_fwd; @@ -16078,7 +16076,7 @@ function pj_cupola(P) { } -pj_add(pj_denoy, 'denoy', 'Denoyer Semi-Elliptical', '\n\tPCyl, Sph., no inv.'); +pj_add(pj_denoy, 'denoy', 'Denoyer Semi-Elliptical', 'PCyl, Sph., no inv.'); function pj_denoy(P) { P.fwd = s_fwd; @@ -16100,14 +16098,14 @@ function pj_denoy(P) { } -pj_add(pj_eck1, 'eck1', 'Eckert I', '\n\tPCyl Sph'); -pj_add(pj_eck2, 'eck2', 'Eckert II', '\n\tPCyl Sph'); -pj_add(pj_eck3, 'eck3', 'Eckert III', '\n\tPCyl Sph'); -pj_add(pj_wag6, 'wag6', 'Wagner VI', '\n\tPCyl Sph'); -pj_add(pj_kav7, 'kav7', 'Kavraisky VII', '\n\tPCyl Sph'); -pj_add(pj_putp1, 'putp1', 'Putnins P1', '\n\tPCyl Sph'); -pj_add(pj_eck4, 'eck4', 'Eckert IV', '\n\tPCyl Sph'); -pj_add(pj_eck5, 'eck5', 'Eckert V', '\n\tPCyl Sph'); +pj_add(pj_eck1, 'eck1', 'Eckert I', 'PCyl Sph'); +pj_add(pj_eck2, 'eck2', 'Eckert II', 'PCyl Sph'); +pj_add(pj_eck3, 'eck3', 'Eckert III', 'PCyl Sph'); +pj_add(pj_wag6, 'wag6', 'Wagner VI', 'PCyl Sph'); +pj_add(pj_kav7, 'kav7', 'Kavraisky VII', 'PCyl Sph'); +pj_add(pj_putp1, 'putp1', 'Putnins P1', 'PCyl Sph'); +pj_add(pj_eck4, 'eck4', 'Eckert IV', 'PCyl Sph'); +pj_add(pj_eck5, 'eck5', 'Eckert V', 'PCyl Sph'); function pj_eck1(P) { var FC = 0.92131773192356127802, @@ -16276,7 +16274,7 @@ function pj_eck5(P) { } -pj_add(pj_eqc, 'eqc', 'Equidistant Cylindrical (Plate Caree)', '\n\tCyl, Sph\n\tlat_ts=[, lat_0=0]'); +pj_add(pj_eqc, 'eqc', 'Equidistant Cylindrical (Plate Caree)', 'Cyl, Sph\nlat_ts=[, lat_0=0]'); function pj_eqc(P) { var rc = cos(pj_param(P.params, "rlat_ts")); @@ -16297,7 +16295,7 @@ function pj_eqc(P) { } -pj_add(pj_eqdc, 'eqdc', 'Equidistant Conic', '\n\tConic, Sph&Ell\n\tlat_1= lat_2='); +pj_add(pj_eqdc, 'eqdc', 'Equidistant Conic', 'Conic, Sph&Ell\nlat_1= lat_2='); function pj_eqdc(P) { var phi1, phi2, n, rho, rho0, c, en, ellips, cosphi, sinphi, secant; @@ -16385,7 +16383,7 @@ function pj_eqdc(P) { * Code released August 2018 * Ported to JavaScript and adapted for mapshaper-proj by Matthew Bloch August 2018 */ -pj_add(pj_eqearth, 'eqearth', 'Equal Earth', "\n\tPCyl., Sph."); +pj_add(pj_eqearth, 'eqearth', 'Equal Earth', 'PCyl., Sph.'); function pj_eqearth(P) { var A1 = 1.340264, @@ -16432,8 +16430,8 @@ function pj_eqearth(P) { } -pj_add(pj_etmerc, 'etmerc', 'Extended Transverse Mercator', '\n\tCyl, Sph\n\tlat_ts=(0)\nlat_0=(0)'); -pj_add(pj_utm, 'utm', 'Universal Transverse Mercator (UTM)', '\n\tCyl, Sph\n\tzone= south'); +pj_add(pj_etmerc, 'etmerc', 'Extended Transverse Mercator', 'Cyl, Sph\nlat_ts=(0)\nlat_0=(0)'); +pj_add(pj_utm, 'utm', 'Universal Transverse Mercator (UTM)', 'Cyl, Sph\nzone= south'); function pj_utm_zone(P) { @@ -16654,7 +16652,7 @@ function pj_etmerc(P) { } -pj_add(pj_gall, 'gall', 'Gall (Gall Stereographic)', '\n\tCyl, Sph'); +pj_add(pj_gall, 'gall', 'Gall (Gall Stereographic)', 'Cyl, Sph'); function pj_gall(P) { var YF = 1.70710678118654752440, @@ -16678,7 +16676,7 @@ function pj_gall(P) { } -pj_add(pj_geocent, 'geocent', 'Geocentric', '\n\t'); +pj_add(pj_geocent, 'geocent', 'Geocentric', ''); function pj_geocent(P) { P.is_geocent = true; @@ -16699,7 +16697,7 @@ function pj_geocent(P) { // from -pj_add(pj_gilbert, 'gilbert', 'Gilbert Two World Perspective', '\n\tPCyl., Sph., NoInv.\n\tlat_1='); +pj_add(pj_gilbert, 'gilbert', 'Gilbert Two World Perspective', 'PCyl., Sph., NoInv.\nlat_1='); function pj_gilbert(P) { var lat1 = pj_param(P.params, 'tlat_1') ? pj_param(P.params, 'rlat_1') : 0, @@ -16729,7 +16727,7 @@ function pj_gilbert(P) { } -pj_add(pj_gins8, 'gins8', 'Ginsburg VIII (TsNIIGAiK)', '\n\tPCyl, Sph., no inv.'); +pj_add(pj_gins8, 'gins8', 'Ginsburg VIII (TsNIIGAiK)', 'PCyl, Sph., no inv.'); function pj_gins8(P) { P.fwd = s_fwd; @@ -16748,10 +16746,10 @@ function pj_gins8(P) { } -pj_add(pj_gn_sinu, 'gn_sinu', 'General Sinusoidal Series', '\n\tPCyl, Sph.\n\tm= n='); -pj_add(pj_sinu, 'sinu', 'Sinusoidal (Sanson-Flamsteed)', '\n\tPCyl, Sph&Ell'); -pj_add(pj_eck6, 'eck6', 'Eckert VI', '\n\tPCyl, Sph.\n\tm= n='); -pj_add(pj_mbtfps, 'mbtfps', 'McBryde-Thomas Flat-Polar Sinusoidal', '\n\tPCyl, Sph.'); +pj_add(pj_gn_sinu, 'gn_sinu', 'General Sinusoidal Series', 'PCyl, Sph.\nm= n='); +pj_add(pj_sinu, 'sinu', 'Sinusoidal (Sanson-Flamsteed)', 'PCyl, Sph&Ell'); +pj_add(pj_eck6, 'eck6', 'Eckert VI', 'PCyl, Sph.\nm= n='); +pj_add(pj_mbtfps, 'mbtfps', 'McBryde-Thomas Flat-Polar Sinusoidal', 'PCyl, Sph.'); function pj_gn_sinu(P) { if (pj_param(P.params, 'tn'), pj_param(P.params, 'tm')) { @@ -16836,7 +16834,7 @@ function pj_sinu_init(P, m, n) { -pj_add(pj_gnom, 'gnom', 'Gnomonic', '\n\tAzi, Sph.'); +pj_add(pj_gnom, 'gnom', 'Gnomonic', 'Azi, Sph.'); function pj_gnom(P) { var EPS10 = 1.e-10, @@ -16943,9 +16941,9 @@ function pj_gnom(P) { } -pj_add(pj_moll, 'moll', 'Mollweide', '\n\tPCyl Sph'); -pj_add(pj_wag4, 'wag4', 'Wagner IV', '\n\tPCyl Sph'); -pj_add(pj_wag5, 'wag5', 'Wagner V', '\n\tPCyl Sph'); +pj_add(pj_moll, 'moll', 'Mollweide', 'PCyl Sph'); +pj_add(pj_wag4, 'wag4', 'Wagner IV', 'PCyl Sph'); +pj_add(pj_wag5, 'wag5', 'Wagner V', 'PCyl Sph'); function pj_moll(P) { pj_moll_init(P, pj_moll_init_Q(P, M_HALFPI)); @@ -17013,7 +17011,7 @@ function pj_moll_init(P, Q) { } -pj_add(pj_goode, 'goode', "Goode Homolosine", "\n\tPCyl, Sph."); +pj_add(pj_goode, 'goode', 'Goode Homolosine', 'PCyl, Sph.'); function pj_goode(P) { var Y_COR = 0.05280, @@ -17045,7 +17043,7 @@ function pj_goode(P) { } -pj_add(pj_hammer, 'hammer', 'Hammer & Eckert-Greifendorff', '\n\tMisc Sph, \n\tW= M='); +pj_add(pj_hammer, 'hammer', 'Hammer & Eckert-Greifendorff', 'Misc Sph,\nW= M='); function pj_hammer(P) { var w, m, rm; @@ -17086,7 +17084,7 @@ function pj_hammer(P) { } -pj_add(pj_hatano, 'hatano', 'Hatano Asymmetrical Equal Area', '\n\tPCyl., Sph.'); +pj_add(pj_hatano, 'hatano', 'Hatano Asymmetrical Equal Area', 'PCyl., Sph.'); function pj_hatano(P) { var NITER = 20; @@ -17147,8 +17145,8 @@ function pj_hatano(P) { } -pj_add(pj_healpix, 'healpix', 'HEALPix', '\n\tSph., Ellps.'); -pj_add(pj_rhealpix, 'rhealpix', 'rHEALPix', '\n\tSph., Ellps.\n\tnorth_square= south_square='); +pj_add(pj_healpix, 'healpix', 'HEALPix', 'Sph., Ellps.'); +pj_add(pj_rhealpix, 'rhealpix', 'rHEALPix', 'Sph., Ellps.\nnorth_square= south_square='); function pj_rhealpix(P) { pj_healpix(P, true); @@ -17619,7 +17617,7 @@ function pj_healpix(P, rhealpix) { } -pj_add(pj_krovak, 'krovak', 'Krovak', '\n\tPCyl., Ellps.'); +pj_add(pj_krovak, 'krovak', 'Krovak', 'PCyl., Ellps.'); function pj_krovak(P) { var u0, n0, g; @@ -17712,7 +17710,7 @@ function pj_krovak(P) { } -pj_add(pj_laea, 'laea', 'Lambert Azimuthal Equal Area', '\n\tAzi, Sph&Ell'); +pj_add(pj_laea, 'laea', 'Lambert Azimuthal Equal Area', 'Azi, Sph&Ell'); function pj_laea(P) { var EPS10 = 1e-10, @@ -17936,10 +17934,10 @@ function pj_laea(P) { } -pj_add(pj_lonlat, 'lonlat', 'Lat/long (Geodetic)', '\n\t'); -pj_add(pj_lonlat, 'longlat', 'Lat/long (Geodetic alias)', '\n\t'); -pj_add(pj_lonlat, 'latlon', 'Lat/long (Geodetic alias)', '\n\t'); -pj_add(pj_lonlat, 'latlong', 'Lat/long (Geodetic alias)', '\n\t'); +pj_add(pj_lonlat, 'lonlat', 'Lat/long (Geodetic)', ''); +pj_add(pj_lonlat, 'longlat', 'Lat/long (Geodetic alias)', ''); +pj_add(pj_lonlat, 'latlon', 'Lat/long (Geodetic alias)', ''); +pj_add(pj_lonlat, 'latlong', 'Lat/long (Geodetic alias)', ''); function pj_lonlat(P) { P.x0 = 0; @@ -17967,7 +17965,7 @@ function pj_tsfn(phi, sinphi, e) { } -pj_add(pj_lcc, 'lcc', 'Lambert Conformal Conic', '\n\tConic, Sph&Ell\n\tlat_1= and lat_2= or lat_0='); +pj_add(pj_lcc, 'lcc', 'Lambert Conformal Conic', 'Conic, Sph&Ell\nlat_1= and lat_2= or lat_0='); function pj_lcc(P) { var EPS10 = 1e-10; @@ -18055,7 +18053,7 @@ function pj_lcc(P) { } -pj_add(pj_loxim, 'loxim', 'Loximuthal', '\n\tPCyl Sph'); +pj_add(pj_loxim, 'loxim', 'Loximuthal', 'PCyl Sph'); function pj_loxim(P) { var EPS = 1e-8; @@ -18096,7 +18094,7 @@ function pj_loxim(P) { } -pj_add(pj_mbt_fpp, 'mbt_fpp', 'McBride-Thomas Flat-Polar Parabolic', '\n\tCyl., Sph.'); +pj_add(pj_mbt_fpp, 'mbt_fpp', 'McBride-Thomas Flat-Polar Parabolic', 'Cyl., Sph.'); function pj_mbt_fpp(P) { var CS = 0.95257934441568037152, @@ -18138,7 +18136,7 @@ function pj_mbt_fpp(P) { } -pj_add(pj_mbt_fpq, 'mbt_fpq', 'McBryde-Thomas Flat-Polar Quartic', '\n\tCyl., Sph.'); +pj_add(pj_mbt_fpq, 'mbt_fpq', 'McBryde-Thomas Flat-Polar Quartic', 'Cyl., Sph.'); function pj_mbt_fpq(P) { var NITER = 20, @@ -18193,7 +18191,7 @@ function pj_mbt_fpq(P) { } -pj_add(pj_mbt_fps, 'mbt_fps', 'McBryde-Thomas Flat-Pole Sine (No. 2)', '\n\tCyl., Sph.'); +pj_add(pj_mbt_fps, 'mbt_fps', 'McBryde-Thomas Flat-Pole Sine (No. 2)', 'Cyl., Sph.'); function pj_mbt_fps(P) { var MAX_ITER = 10, @@ -18254,15 +18252,15 @@ function pj_phi2(ts, e) { } -pj_add(pj_merc, "merc", "Mercator", "\n\tCyl, Sph&Ell\n\tlat_ts="); +pj_add(pj_merc, 'merc', 'Mercator', 'Cyl, Sph&Ell\nlat_ts='); function pj_merc(P) { var EPS10 = 1e-10; var phits = 0; - var is_phits = pj_param(P.params, "tlat_ts"); + var is_phits = pj_param(P.params, 'tlat_ts'); if (is_phits) { - phits = pj_param(P.params, "rlat_ts"); + phits = pj_param(P.params, 'rlat_ts'); if (phits >= M_HALFPI) { e_error(-24); } @@ -18310,7 +18308,7 @@ function pj_merc(P) { } -pj_add(pj_mill, 'mill', 'Miller Cylindrical', '\n\tCyl, Sph'); +pj_add(pj_mill, 'mill', 'Miller Cylindrical', 'Cyl, Sph'); function pj_mill(P) { @@ -18381,11 +18379,11 @@ function pj_zpolyd1(z, C, der) { } -pj_add(pj_mil_os, 'mil_os', 'Miller Oblated Stereographic', '\n\tAzi(mod)'); -pj_add(pj_lee_os, 'lee_os', 'Lee Oblated Stereographic', '\n\tAzi(mod)'); -pj_add(pj_gs48, 'gs48', 'Mod Stereographic of 48 U.S.', '\n\tAzi(mod)'); -pj_add(pj_alsk, 'alsk', 'Mod Stereographic of Alaska', '\n\tAzi(mod)'); -pj_add(pj_gs50, 'gs50', 'Mod Stereographic of 50 U.S.', '\n\tAzi(mod)'); +pj_add(pj_mil_os, 'mil_os', 'Miller Oblated Stereographic', 'Azi(mod)'); +pj_add(pj_lee_os, 'lee_os', 'Lee Oblated Stereographic', 'Azi(mod)'); +pj_add(pj_gs48, 'gs48', 'Mod Stereographic of 48 U.S.', 'Azi(mod)'); +pj_add(pj_alsk, 'alsk', 'Mod Stereographic of Alaska', 'Azi(mod)'); +pj_add(pj_gs50, 'gs50', 'Mod Stereographic of 50 U.S.', 'Azi(mod)'); function pj_mil_os(P) { var AB = [ @@ -18584,8 +18582,8 @@ function pj_mod_ster(P, zcoeff) { } -pj_add(pj_natearth, 'natearth', 'Natural Earth', '\n\tPCyl., Sph.'); -pj_add(pj_natearth2, 'natearth2', 'Natural Earth 2', '\n\tPCyl., Sph.'); +pj_add(pj_natearth, 'natearth', 'Natural Earth', 'PCyl., Sph.'); +pj_add(pj_natearth2, 'natearth2', 'Natural Earth 2', 'PCyl., Sph.'); function pj_natearth(P) { var A0 = 0.8707, @@ -18703,7 +18701,7 @@ function pj_natearth2(P) { } -pj_add(pj_nell, 'nell', 'Nell', '\n\tPCyl., Sph.'); +pj_add(pj_nell, 'nell', 'Nell', 'PCyl., Sph.'); function pj_nell(P) { var MAX_ITER = 10; @@ -18734,7 +18732,7 @@ function pj_nell(P) { } -pj_add(pj_nell_h, 'nell_h', 'Nell-Hammer', '\n\tPCyl., Sph.'); +pj_add(pj_nell_h, 'nell_h', 'Nell-Hammer', 'PCyl., Sph.'); function pj_nell_h(P) { var NITER = 9, @@ -18766,8 +18764,8 @@ var NITER = 9, } -pj_add(pj_nsper, 'nsper', 'Near-sided perspective', '\n\tAzi, Sph\n\th='); -pj_add(pj_tpers, 'tpers', 'Tilted perspective', '\n\tAzi, Sph\n\ttilt= azi= h='); +pj_add(pj_nsper, 'nsper', 'Near-sided perspective', 'Azi, Sph\nh='); +pj_add(pj_tpers, 'tpers', 'Tilted perspective', 'Azi, Sph\ntilt= azi= h='); function pj_nsper(P) { pj_tpers_init(P, pj_param(P.params, "dh")); @@ -18903,7 +18901,7 @@ function pj_tpers_init(P, height, tiltAngle, azimuth) { } -pj_add(pj_nzmg, 'nzmg', 'New Zealand Map Grid', '\n\tfixed Earth'); +pj_add(pj_nzmg, 'nzmg', 'New Zealand Map Grid', 'fixed Earth'); function pj_nzmg(P) { var EPSLN = 1e-10; @@ -18972,11 +18970,11 @@ function pj_nzmg(P) { } -pj_add(pj_ob_tran, 'ob_tran', 'General Oblique Transformation', "\n\tMisc Sph" + - "\n\to_proj= plus parameters for projection" + - "\n\to_lat_p= o_lon_p= (new pole) or" + - "\n\to_alpha= o_lon_c= o_lat_c= or" + - "\n\to_lon_1= o_lat_1= o_lon_2= o_lat_2="); +pj_add(pj_ob_tran, 'ob_tran', 'General Oblique Transformation', 'Misc Sph\n' + + 'o_proj= plus parameters for projection\n' + + 'o_lat_p= o_lon_p= (new pole) or\n' + + 'o_alpha= o_lon_c= o_lat_c= or\n' + + 'o_lon_1= o_lat_1= o_lon_2= o_lat_2='); function pj_ob_tran(P) { var name, defn, P2; @@ -19005,25 +19003,25 @@ function pj_ob_tran(P) { P.fr_meter = RAD_TO_DEG; } - if (pj_param(P.params, "to_alpha")) { - lamc = pj_param(P.params, "ro_lon_c"); - phic = pj_param(P.params, "ro_lat_c"); - alpha = pj_param(P.params, "ro_alpha"); + if (pj_param(P.params, 'to_alpha')) { + lamc = pj_param(P.params, 'ro_lon_c'); + phic = pj_param(P.params, 'ro_lat_c'); + alpha = pj_param(P.params, 'ro_alpha'); if (fabs(fabs(phic) - M_HALFPI) <= TOL) e_error(-32); lamp = lamc + aatan2(-cos(alpha), -sin(alpha) * sin(phic)); phip = aasin(cos(phic) * sin(alpha)); - } else if (pj_param(P.params, "to_lat_p")) { /* specified new pole */ - lamp = pj_param(P.params, "ro_lon_p"); - phip = pj_param(P.params, "ro_lat_p"); + } else if (pj_param(P.params, 'to_lat_p')) { /* specified new pole */ + lamp = pj_param(P.params, 'ro_lon_p'); + phip = pj_param(P.params, 'ro_lat_p'); - } else { /* specified new "equator" points */ + } else { /* specified new 'equator' points */ - lam1 = pj_param(P.params, "ro_lon_1"); - phi1 = pj_param(P.params, "ro_lat_1"); - lam2 = pj_param(P.params, "ro_lon_2"); - phi2 = pj_param(P.params, "ro_lat_2"); + lam1 = pj_param(P.params, 'ro_lon_1'); + phi1 = pj_param(P.params, 'ro_lat_1'); + lam2 = pj_param(P.params, 'ro_lon_2'); + phi2 = pj_param(P.params, 'ro_lat_2'); if (fabs(phi1 - phi2) <= TOL || (con = fabs(phi1)) <= TOL || fabs(con - M_HALFPI) <= TOL || @@ -19090,7 +19088,7 @@ function pj_ob_tran(P) { } -pj_add(pj_ocea, 'ocea', 'Oblique Cylindrical Equal Area', '\n\tCyl, Sph lonc= alpha= or\n\tlat_1= lat_2= lon_1= lon_2='); +pj_add(pj_ocea, 'ocea', 'Oblique Cylindrical Equal Area', 'Cyl, Sph lonc= alpha= or\nlat_1= lat_2= lon_1= lon_2='); function pj_ocea(P) { var phi_0 = 0, @@ -19159,8 +19157,8 @@ function pj_ocea(P) { } -pj_add(pj_omerc, 'omerc', 'Oblique Mercator', '\n\tCyl, Sph&Ell no_rot' + - '\n\talpha= [gamma=] [no_off] lonc= or\n\t lon_1= lat_1= lon_2= lat_2='); +pj_add(pj_omerc, 'omerc', 'Oblique Mercator', 'Cyl, Sph&Ell no_rot\n' + + 'alpha= [gamma=] [no_off] lonc= or\nlon_1= lat_1= lon_2= lat_2='); function pj_omerc(P) { var TOL = 1e-7; @@ -19321,7 +19319,7 @@ function pj_omerc(P) { } -pj_add(pj_ortho, 'ortho', 'Orthographic', '\n\tAzi, Sph.'); +pj_add(pj_ortho, 'ortho', 'Orthographic', 'Azi, Sph.'); function pj_ortho(P) { var EPS10 = 1.e-10, @@ -19413,7 +19411,7 @@ function pj_ortho(P) { } -pj_add(pj_patterson, 'patterson', 'Patterson Cylindrical', '\n\tCyl., Sph.'); +pj_add(pj_patterson, 'patterson', 'Patterson Cylindrical', 'Cyl., Sph.'); function pj_patterson(P) { var K1 = 1.0148, @@ -19470,7 +19468,7 @@ function pj_patterson(P) { } -pj_add(pj_poly, 'poly', 'Polyconic (American)', '\n\tConic, Sph&Ell'); +pj_add(pj_poly, 'poly', 'Polyconic (American)', 'Conic, Sph&Ell'); function pj_poly(P) { var TOL = 1e-10, @@ -19572,7 +19570,7 @@ function pj_poly(P) { } -pj_add(pj_putp2, 'putp2', 'Putnins P2', '\n\tPCyl., Sph.'); +pj_add(pj_putp2, 'putp2', 'Putnins P2', 'PCyl., Sph.'); function pj_putp2(P) { var C_x = 1.89490, @@ -19613,8 +19611,8 @@ function pj_putp2(P) { } -pj_add(pj_putp3, 'putp3', 'Putnins P3', '\n\tPCyl., Sph.'); -pj_add(pj_putp3p, 'putp3p', 'Putnins P3\'', '\n\tPCyl., Sph.'); +pj_add(pj_putp3, 'putp3', 'Putnins P3', 'PCyl., Sph.'); +pj_add(pj_putp3p, 'putp3p', 'Putnins P3\'', 'PCyl., Sph.'); function pj_putp3p(P) { pj_putp3(P, true); @@ -19640,8 +19638,8 @@ function pj_putp3(P, prime) { } -pj_add(pj_putp4p, 'putp4p', 'Putnins P4\'', '\n\tPCyl., Sph.'); -pj_add(pj_weren, 'weren', 'Werenskiold I', '\n\tPCyl., Sph.'); +pj_add(pj_putp4p, 'putp4p', 'Putnins P4\'', 'PCyl., Sph.'); +pj_add(pj_weren, 'weren', 'Werenskiold I', 'PCyl., Sph.'); function pj_putp4p(P) { pj_putp4p_init(P, 0.874038744, 3.883251825); @@ -19673,8 +19671,8 @@ function pj_putp4p_init(P, C_x, C_y) { } -pj_add(pj_putp5, 'putp5', 'Putnins P5', '\n\tPCyl., Sph.'); -pj_add(pj_putp5p, 'putp5p', 'Putnins P5\'', '\n\tPCyl., Sph.'); +pj_add(pj_putp5, 'putp5', 'Putnins P5', 'PCyl., Sph.'); +pj_add(pj_putp5p, 'putp5p', 'Putnins P5\'', 'PCyl., Sph.'); function pj_putp5p(P) { pj_putp5(P, true); @@ -19702,8 +19700,8 @@ function pj_putp5(P, prime) { } -pj_add(pj_putp6, 'putp6', 'Putnins P6', '\n\tPCyl., Sph.'); -pj_add(pj_putp6p, 'putp6p', 'Putnins P6\'', '\n\tPCyl., Sph.'); +pj_add(pj_putp6, 'putp6', 'Putnins P6', 'PCyl., Sph.'); +pj_add(pj_putp6p, 'putp6p', 'Putnins P6\'', 'PCyl., Sph.'); function pj_putp6p(P) { pj_putp6(P, true); @@ -19760,7 +19758,7 @@ function pj_putp6(P, prime) { } -pj_add(pj_qsc, 'qsc', 'Quadrilateralized Spherical Cube', '\n\tAzi, Sph.'); +pj_add(pj_qsc, 'qsc', 'Quadrilateralized Spherical Cube', 'Azi, Sph.'); function pj_qsc(P) { var EPS10 = 1.e-10; @@ -20104,7 +20102,7 @@ function pj_qsc(P) { } -pj_add(pj_robin, 'robin', 'Robinson', "\n\tPCyl., Sph."); +pj_add(pj_robin, 'robin', 'Robinson', 'PCyl., Sph.'); function pj_robin(P) { var X = to_float([ @@ -20228,13 +20226,13 @@ function pj_robin(P) { } -pj_add(pj_get_sconic('EULER'), 'euler', 'Euler', '\n\tConic, Sph\n\tlat_1= and lat_2='); -pj_add(pj_get_sconic('MURD1'), 'murd1', 'Murdoch I', '\n\tConic, Sph\n\tlat_1= and lat_2='); -pj_add(pj_get_sconic('MURD2'), 'murd2', 'Murdoch II', '\n\tConic, Sph\n\tlat_1= and lat_2='); -pj_add(pj_get_sconic('MURD3'), 'murd3', 'Murdoch III', '\n\tConic, Sph\n\tlat_1= and lat_2='); -pj_add(pj_get_sconic('PCONIC'), 'pconic', 'Perspective Conic', '\n\tConic, Sph\n\tlat_1= and lat_2='); -pj_add(pj_get_sconic('TISSOT'), 'tissot', 'Tissot', '\n\tConic, Sph\n\tlat_1= and lat_2='); -pj_add(pj_get_sconic('VITK1'), 'vitk1', 'Vitkovsky I', '\n\tConic, Sph\n\tlat_1= and lat_2='); +pj_add(pj_get_sconic('EULER'), 'euler', 'Euler', 'Conic, Sph\nlat_1= and lat_2='); +pj_add(pj_get_sconic('MURD1'), 'murd1', 'Murdoch I', 'Conic, Sph\nlat_1= and lat_2='); +pj_add(pj_get_sconic('MURD2'), 'murd2', 'Murdoch II', 'Conic, Sph\nlat_1= and lat_2='); +pj_add(pj_get_sconic('MURD3'), 'murd3', 'Murdoch III', 'Conic, Sph\nlat_1= and lat_2='); +pj_add(pj_get_sconic('PCONIC'), 'pconic', 'Perspective Conic', 'Conic, Sph\nlat_1= and lat_2='); +pj_add(pj_get_sconic('TISSOT'), 'tissot', 'Tissot', 'Conic, Sph\nlat_1= and lat_2='); +pj_add(pj_get_sconic('VITK1'), 'vitk1', 'Vitkovsky I', 'Conic, Sph\nlat_1= and lat_2='); function pj_get_sconic(type) { return function(P) { @@ -20361,7 +20359,7 @@ function pj_sconic(P, type) { } -pj_add(pj_somerc, 'somerc', 'Swiss. Obl. Mercator', '\n\tCyl, Ell\n\tFor CH1903'); +pj_add(pj_somerc, 'somerc', 'Swiss. Obl. Mercator', 'Cyl, Ell\nFor CH1903'); function pj_somerc(P) { var K, c, hlf_e, kR, cosp0, sinp0; @@ -20421,8 +20419,8 @@ function pj_somerc(P) { } -pj_add(pj_stere, 'stere', 'Stereographic', '\n\tAzi, Sph&Ell\n\tlat_ts='); -pj_add(pj_ups, 'ups', 'Universal Polar Stereographic', '\n\tAzi, Sph&Ell\n\tsouth'); +pj_add(pj_stere, 'stere', 'Stereographic', 'Azi, Sph&Ell\nlat_ts='); +pj_add(pj_ups, 'ups', 'Universal Polar Stereographic', 'Azi, Sph&Ell\nsouth'); function pj_ups(P) { P.phi0 = pj_param(P.params, "bsouth") ? -M_HALFPI : M_HALFPI; @@ -20703,7 +20701,7 @@ function pj_inv_gauss(lp, en) { } -pj_add(pj_sterea, 'sterea', 'Oblique Stereographic Alternative', '\n\tAzimuthal, Sph&Ell'); +pj_add(pj_sterea, 'sterea', 'Oblique Stereographic Alternative', 'Azimuthal, Sph&Ell'); function pj_sterea(P) { var en = pj_gauss_ini(P.e, P.phi0), @@ -20746,10 +20744,10 @@ function pj_sterea(P) { } -pj_add(pj_kav5, 'kav5', 'Kavraisky V', '\n\tPCyl., Sph.'); -pj_add(pj_qua_aut, 'qua_aut', 'Quartic Authalic', '\n\tPCyl., Sph.'); -pj_add(pj_fouc, 'fouc', 'Foucaut', '\n\tPCyl., Sph.'); -pj_add(pj_mbt_s, 'mbt_s', 'McBryde-Thomas Flat-Polar Sine (No. 1)', '\n\tPCyl., Sph.'); +pj_add(pj_kav5, 'kav5', 'Kavraisky V', 'PCyl., Sph.'); +pj_add(pj_qua_aut, 'qua_aut', 'Quartic Authalic', 'PCyl., Sph.'); +pj_add(pj_fouc, 'fouc', 'Foucaut', 'PCyl., Sph.'); +pj_add(pj_mbt_s, 'mbt_s', 'McBryde-Thomas Flat-Polar Sine (No. 1)', 'PCyl., Sph.'); function pj_kav5(P) { pj_sts(P, 1.50488, 1.35439, false); @@ -20804,7 +20802,7 @@ function pj_sts(P, p, q, tan_mode) { } -pj_add(pj_tcea, 'tcea', 'Transverse Cylindrical Equal Area', '\n\tCyl, Sph'); +pj_add(pj_tcea, 'tcea', 'Transverse Cylindrical Equal Area', 'Cyl, Sph'); function pj_tcea(P) { P.es = 0; @@ -20827,7 +20825,7 @@ function pj_tcea(P) { } -pj_add(pj_times, 'times', 'Times', "\n\tCyl, Sph"); +pj_add(pj_times, 'times', 'Times', 'Cyl, Sph'); function pj_times(P) { P.es = 0; @@ -20846,7 +20844,7 @@ function pj_times(P) { } -pj_add(pj_tmerc, 'tmerc', "Transverse Mercator", "\n\tCyl, Sph&Ell"); +pj_add(pj_tmerc, 'tmerc', 'Transverse Mercator', 'Cyl, Sph&Ell'); function pj_tmerc(P) { var EPS10 = 1e-10, @@ -20976,7 +20974,7 @@ function pj_tmerc(P) { } -pj_add(pj_tpeqd, 'tpeqd', 'Two Point Equidistant', '\n\tMisc Sph\n\tlat_1= lon_1= lat_2= lon_2='); +pj_add(pj_tpeqd, 'tpeqd', 'Two Point Equidistant', 'Misc Sph\nlat_1= lon_1= lat_2= lon_2='); function pj_tpeqd(P) { var cp1, sp1, cp2, sp2, ccs, cs, sc, r2z0, z02, dlam2; @@ -21052,7 +21050,7 @@ function pj_tpeqd(P) { } -pj_add(pj_urm5, 'urm5', 'Urmaev V', '\n\tPCyl., Sph., no inv.\n\tn= q= alpha='); +pj_add(pj_urm5, 'urm5', 'Urmaev V', 'PCyl., Sph., no inv.\nn= q= alpha='); function pj_urm5(P) { var m, rmn, q3, n; @@ -21079,8 +21077,8 @@ function pj_urm5(P) { } -pj_add(pj_urmfps, 'urmfps', 'Urmaev Flat-Polar Sinusoidal', '\n\tPCyl, Sph.\n\tn='); -pj_add(pj_wag1, 'wag1', 'Wagner I (Kavraisky VI)', '\n\tPCyl, Sph.'); +pj_add(pj_urmfps, 'urmfps', 'Urmaev Flat-Polar Sinusoidal', 'PCyl, Sph.\nn='); +pj_add(pj_wag1, 'wag1', 'Wagner I (Kavraisky VI)', 'PCyl, Sph.'); function pj_wag1(P) { @@ -21115,10 +21113,10 @@ function pj_urmfps_init(P, n) { } -pj_add(pj_vandg, 'vandg', 'van der Grinten (I)', '\n\tMisc Sph'); -pj_add(pj_vandg2, 'vandg2', 'van der Grinten II', '\n\tMisc Sph, no inv.'); -pj_add(pj_vandg3, 'vandg3', 'van der Grinten III', '\n\tMisc Sph, no inv.'); -pj_add(pj_vandg4, 'vandg4', 'van der Grinten IV', '\n\tMisc Sph, no inv.'); +pj_add(pj_vandg, 'vandg', 'van der Grinten (I)', 'Misc Sph'); +pj_add(pj_vandg2, 'vandg2', 'van der Grinten II', 'Misc Sph, no inv.'); +pj_add(pj_vandg3, 'vandg3', 'van der Grinten III', 'Misc Sph, no inv.'); +pj_add(pj_vandg4, 'vandg4', 'van der Grinten IV', 'Misc Sph, no inv.'); function pj_vandg(P) { var TOL = 1.e-10, @@ -21277,9 +21275,9 @@ function pj_vandg4(P) { } -pj_add(pj_wag2, 'wag2', 'Wagner II', '\n\tPCyl., Sph.'); -pj_add(pj_wag3, 'wag3', 'Wagner III', '\n\tPCyl., Sph.\n\tlat_ts='); -pj_add(pj_wag7, 'wag7', 'Wagner VII', '\n\tMisc Sph, no inv.'); +pj_add(pj_wag2, 'wag2', 'Wagner II', 'PCyl., Sph.'); +pj_add(pj_wag3, 'wag3', 'Wagner III', 'PCyl., Sph.\nlat_ts='); +pj_add(pj_wag7, 'wag7', 'Wagner VII', 'Misc Sph, no inv.'); function pj_wag2(P) { var C_x = 0.92483, @@ -21336,8 +21334,8 @@ function pj_wag7(P) { -pj_add(pj_wink1, 'wink1', 'Winkel I', '\n\tPCyl., Sph.\n\tlat_ts='); -pj_add(pj_wink2, 'wink2', 'Winkel II', '\n\tPCyl., Sph., no inv.\n\tlat_1='); +pj_add(pj_wink1, 'wink1', 'Winkel I', 'PCyl., Sph.\nlat_ts='); +pj_add(pj_wink2, 'wink2', 'Winkel II', 'PCyl., Sph., no inv.\nlat_1='); function pj_wink1(P) { var cosphi1 = cos(pj_param(P.params, "rlat_ts")); From 4fcef6d997318a193bba2e1387cf913109962077 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 6 May 2021 12:38:12 -0400 Subject: [PATCH 017/891] Remove a console message --- package-lock.json | 2 +- src/commands/mapshaper-clean.js | 2 +- src/commands/mapshaper-proj.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19f8fb207..02ca31c5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.48", + "version": "0.5.49", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/commands/mapshaper-clean.js b/src/commands/mapshaper-clean.js index abb9aec5b..7a8f3e5b7 100644 --- a/src/commands/mapshaper-clean.js +++ b/src/commands/mapshaper-clean.js @@ -37,7 +37,7 @@ export function cleanLayers(layers, dataset, optsArg) { } } if (!opts.allow_empty) { - cmd.filterFeatures(lyr, dataset.arcs, {remove_empty: true}); + cmd.filterFeatures(lyr, dataset.arcs, {remove_empty: true, verbose: opts.verbose}); } }); diff --git a/src/commands/mapshaper-proj.js b/src/commands/mapshaper-proj.js index fda013041..ff6375e28 100644 --- a/src/commands/mapshaper-proj.js +++ b/src/commands/mapshaper-proj.js @@ -158,7 +158,7 @@ function cleanProjectedLayers(dataset) { // heal cuts in previously split-apart polygons // TODO: only clean affected polygons (cleaning all polygons can be slow) var polygonLayers = dataset.layers.filter(lyr => lyr.geometry_type == 'polygon'); - cleanLayers(polygonLayers, dataset, {no_arc_dissolve: true, quiet: true}); + cleanLayers(polygonLayers, dataset, {no_arc_dissolve: true, verbose: false}); // remove unused arcs from polygon and polyline layers // TODO: fix bug that leaves uncut arcs in the arc table // (e.g. when projecting a graticule) From f9b09aa139fdcaf83030ff0760dfce8baf3045ca Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 6 May 2021 12:49:03 -0400 Subject: [PATCH 018/891] Fix error sending output to stdout --- src/cli/mapshaper-cli-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/mapshaper-cli-utils.js b/src/cli/mapshaper-cli-utils.js index 0413bfe26..f0851b4bb 100644 --- a/src/cli/mapshaper-cli-utils.js +++ b/src/cli/mapshaper-cli-utils.js @@ -49,7 +49,7 @@ cli.readFile = function(fname, encoding, cache) { cli.createDirIfNeeded = function(fname) { var odir = parseLocalPath(fname).directory; - if (!odir || cli.isDirectory(odir)) return; + if (!odir || cli.isDirectory(odir) || fname == '/dev/stdout') return; try { require('fs').mkdirSync(odir, {recursive: true}); message('Created output directory:', odir); From 074f81189e67edf681a02b2269294f5652eb8aae Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 6 May 2021 12:53:25 -0400 Subject: [PATCH 019/891] v0.5.49 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dccb9ee91..856049f7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ v0.5.49 * Split polylines and lines that cross the rotated antimeridan when applying a world projection with a non-zero central meridian. +* Fixed error when sending output to /dev/stdout. v0.5.48 * Update to Cupola projection. From b582161582de0ee019caf6196d7900fc5edbc976 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 6 May 2021 13:19:57 -0400 Subject: [PATCH 020/891] comment --- src/commands/mapshaper-graticule.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/mapshaper-graticule.js b/src/commands/mapshaper-graticule.js index eb51cf219..ad3db87b1 100644 --- a/src/commands/mapshaper-graticule.js +++ b/src/commands/mapshaper-graticule.js @@ -58,7 +58,8 @@ function createGraticule(lon0, opts) { return createMeridian(x, ymin, ymax, precision); }).filter(o => !!o); if (isRotated) { - // this kludge adds + // add meridian lines that will appear on the left and right sides of the + // projected graticule meridians.push(createMeridian(antimeridian - e, -90, 90, precision)); meridians.push(createMeridian(antimeridian + e, -90, 90, precision)); } From 4ec639ff4fbad51f704435035218a36fa67e29a6 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 6 May 2021 20:08:58 -0400 Subject: [PATCH 021/891] Support lists of quoted strings in assigment options --- src/cli/mapshaper-command-parser.js | 19 +++++++++---------- src/cli/mapshaper-option-parsing-utils.js | 13 ++++++++++++- src/crs/mapshaper-spherical-cutting.js | 2 ++ src/utils/mapshaper-utils.js | 3 ++- test/classify-test.js | 12 +++++++++++- test/utils-test.js | 7 +++++++ 6 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/cli/mapshaper-command-parser.js b/src/cli/mapshaper-command-parser.js index f289e478d..57a709373 100644 --- a/src/cli/mapshaper-command-parser.js +++ b/src/cli/mapshaper-command-parser.js @@ -1,4 +1,11 @@ -import { parseStringList, parseColorList, cleanArgv } from '../cli/mapshaper-option-parsing-utils'; +import { + parseStringList, + parseColorList, + cleanArgv, + trimQuotes, + isAssignment, + splitAssignment +} from '../cli/mapshaper-option-parsing-utils'; import utils from '../utils/mapshaper-utils'; import { stop, print, error } from '../utils/mapshaper-logging'; import { runningInBrowser } from '../mapshaper-state'; @@ -6,7 +13,6 @@ import { runningInBrowser } from '../mapshaper-state'; export function CommandParser() { var commandRxp = /^--?([a-z][\w-]*)$/i, invalidCommandRxp = /^--?[a-z][\w-]*[=]/i, // e.g. -target=A // could be more general - assignmentRxp = /^([a-z0-9_+-]+)=(?!\=)(.*)$/i, // exclude == _usage = "", _examples = [], _commands = [], @@ -114,7 +120,7 @@ export function CommandParser() { var token = argv.shift(), optName, optDef, parts; - if (assignmentRxp.test(token)) { + if (isAssignment(token)) { // token looks like name=value style option parts = splitAssignment(token); optDef = findOptionDefn(parts[0], cmdDef); @@ -157,13 +163,6 @@ export function CommandParser() { } } - function splitAssignment(token) { - var match = assignmentRxp.exec(token), - name = match[1], - val = utils.trimQuotes(match[2]); - return [name, val]; - } - // Read an option value for @optDef from @argv function readOptionValue(argv, optDef) { if (argv.length === 0 || tokenLooksLikeCommand(argv[0])) { diff --git a/src/cli/mapshaper-option-parsing-utils.js b/src/cli/mapshaper-option-parsing-utils.js index 5df264ba5..9ebb5139a 100644 --- a/src/cli/mapshaper-option-parsing-utils.js +++ b/src/cli/mapshaper-option-parsing-utils.js @@ -1,4 +1,5 @@ import utils from '../utils/mapshaper-utils'; +var assignmentRxp = /^([a-z0-9_+-]+)=(?!\=)(.*)$/i; // exclude == export function splitShellTokens(str) { var BAREWORD = '([^\'"\\s])+'; @@ -13,7 +14,6 @@ export function splitShellTokens(str) { return chunks; } - // Split comma-delimited list, trim quotes from entire list and // individual members export function parseStringList(token) { @@ -75,3 +75,14 @@ export function formatOptionValue(val) { } return val; } + +export function isAssignment(token) { + return assignmentRxp.test(token); +} + +export function splitAssignment(token) { + var match = assignmentRxp.exec(token), + name = match[1], + val = utils.trimQuotes(match[2]); + return [name, val]; +} diff --git a/src/crs/mapshaper-spherical-cutting.js b/src/crs/mapshaper-spherical-cutting.js index ddc1b3b04..8baea826c 100644 --- a/src/crs/mapshaper-spherical-cutting.js +++ b/src/crs/mapshaper-spherical-cutting.js @@ -6,6 +6,8 @@ import { layerHasPaths } from '../dataset/mapshaper-layer-utils'; import { getAntimeridian } from '../geom/mapshaper-latlon'; export function insertPreProjectionCuts(dataset, src, dest) { + // currently only supports adding a single vertical cut to (most) world map projections + // centered on a non-zero longitude. if (isLatLngCRS(src) && isRotatedWorldProjection(dest)) { insertVerticalCut(dataset, getAntimeridian(dest.lam0 * 180 / Math.PI)); return true; diff --git a/src/utils/mapshaper-utils.js b/src/utils/mapshaper-utils.js index c504559c3..02b6597d3 100644 --- a/src/utils/mapshaper-utils.js +++ b/src/utils/mapshaper-utils.js @@ -955,7 +955,8 @@ export function trimQuotes(raw) { if (len >= 2) { first = raw.charAt(0); last = raw.charAt(len-1); - if (first == '"' && last == '"' || first == "'" && last == "'") { + if (first == '"' && last == '"' && !raw.includes('","') || + first == "'" && last == "'" && !raw.includes("','")) { return raw.substr(1, len-2); } } diff --git a/test/classify-test.js b/test/classify-test.js index be12901d3..7a63cc1f0 100644 --- a/test/classify-test.js +++ b/test/classify-test.js @@ -3,10 +3,20 @@ var api = require('../'), describe('mapshaper-classify.js', function () { + describe('categorical colors', function () { + it('options use lists of quoted strings', function (done) { + var data='plu\nAsian Indian\n"Chinese, except Taiwanese"\nFilipino'; + var cmd = "-i data.csv -classify plu categories='Asian Indian','Chinese, except Taiwanese' colors='#e6194b','#3cb44b' -o"; + api.applyCommands(cmd, {'data.csv': data}, function(err, out) { + var target='plu,fill\nAsian Indian,#e6194b\n"Chinese, except Taiwanese",#3cb44b\nFilipino,#eee'; + assert.equal(out['data.csv'], target); + done(); + }); + }) + }) it('error on unknown color scheme', function(done) { var data='value\n1\n2\n3\n4'; api.applyCommands('-i data.csv -classify value colors=blues -o', {'data.csv': data}, function(err, out) { - assert(err.message.includes('Unsupported color')); done(); }); }); diff --git a/test/utils-test.js b/test/utils-test.js index bc33dc786..99b74051b 100644 --- a/test/utils-test.js +++ b/test/utils-test.js @@ -4,6 +4,13 @@ var api = require('../'), assert = require('assert'); describe('mapshaper-utils.js', function () { + describe('trimQuotes()', function () { + it('lists of quoted strings', function () { + assert.equal(utils.trimQuotes("'blue','red'"), "'blue','red'"); + assert.equal(utils.trimQuotes("'reddish blue','bluish red'"), "'reddish blue','bluish red'"); + }) + }) + describe('formatDateISO()', function () { it('rounds to minutes', function () { assert.equal(utils.formatDateISO(new Date('2020-10-01T02:59:00.000Z')), '2020-10-01T02:59Z') From fd99341c0eb205e58956d2d302bacc5057e7717b Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 6 May 2021 20:27:12 -0400 Subject: [PATCH 022/891] v0.5.50 --- CHANGELOG.md | 3 +++ package-lock.json | 8 ++++---- package.json | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 856049f7e..0c7f14993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.50 +* Bug fix for an argument parsing error. + v0.5.49 * Split polylines and lines that cross the rotated antimeridan when applying a world projection with a non-zero central meridian. * Fixed error when sending output to /dev/stdout. diff --git a/package-lock.json b/package-lock.json index 02ca31c5c..9c26382d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.49", + "version": "0.5.50", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2093,9 +2093,9 @@ } }, "underscore": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz", - "integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", + "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", "dev": true }, "url": { diff --git a/package.json b/package.json index 4e08db706..c6e1fd2b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.49", + "version": "0.5.50", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", @@ -57,7 +57,7 @@ "mocha": "^8.3.0", "rollup": "^2.28.2", "shell-quote": "^1.6.1", - "underscore": "^1.9.0" + "underscore": "^1.13.1" }, "bin": { "mapshaper": "./bin/mapshaper", From e5e20f9b7673910d4dd682380bfcee59b791c1fd Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 7 May 2021 01:08:46 -0400 Subject: [PATCH 023/891] Improve graticule; apply shape splitting to more projections --- src/cli/mapshaper-command-parser.js | 1 - src/commands/mapshaper-graticule.js | 77 ++++++++++++++------------ src/crs/mapshaper-proj-info.js | 41 +++++++------- src/crs/mapshaper-spherical-cutting.js | 24 ++++++-- 4 files changed, 81 insertions(+), 62 deletions(-) diff --git a/src/cli/mapshaper-command-parser.js b/src/cli/mapshaper-command-parser.js index 57a709373..c3608ff82 100644 --- a/src/cli/mapshaper-command-parser.js +++ b/src/cli/mapshaper-command-parser.js @@ -2,7 +2,6 @@ import { parseStringList, parseColorList, cleanArgv, - trimQuotes, isAssignment, splitAssignment } from '../cli/mapshaper-option-parsing-utils'; diff --git a/src/commands/mapshaper-graticule.js b/src/commands/mapshaper-graticule.js index ad3db87b1..c806dc504 100644 --- a/src/commands/mapshaper-graticule.js +++ b/src/commands/mapshaper-graticule.js @@ -1,12 +1,13 @@ import { importGeoJSON } from '../geojson/geojson-import'; import { projectDataset } from '../commands/mapshaper-proj'; import { getDatasetCRS, getCRS } from '../crs/mapshaper-projections'; -import { isRotatedWorldProjection } from '../crs/mapshaper-proj-info'; +import { isRotatedNormalProjection } from '../crs/mapshaper-proj-info'; import { getAntimeridian } from '../geom/mapshaper-latlon'; import { stop } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import cmd from '../mapshaper-cmd'; + cmd.graticule = function(dataset, opts) { var graticule, dest, src; if (dataset) { @@ -25,7 +26,7 @@ cmd.graticule = function(dataset, opts) { function createGraticuleForProjection(P, opts) { var lon0 = 0; // see mapshaper-spherical-cutting.js - if (isRotatedWorldProjection(P)) { + if (isRotatedNormalProjection(P)) { lon0 = P.lam0 * 180 / Math.PI; } return createGraticule(lon0, opts); @@ -39,32 +40,28 @@ function createGraticule(lon0, opts) { var xstepMajor = 90; var antimeridian = getAntimeridian(lon0); var isRotated = lon0 != 0; - var e = 2e-8; var xn = Math.round(360 / xstep) + (isRotated ? 0 : 1); var yn = Math.round(180 / ystep) + 1; var xx = utils.range(xn, -180, xstep); var yy = utils.range(yn, -90, ystep); - var meridians = xx.map(function(x) { - var ymin = -90, - ymax = 90; + var meridians = []; + var parallels = []; + xx.forEach(function(x) { if (isRotated && Math.abs(x - antimeridian) < xstep / 5) { // skip meridians that are close to the enclosure of a rotated graticule return null; } - if (x % xstepMajor !== 0) { - ymin += ystep; - ymax -= ystep; - } - return createMeridian(x, ymin, ymax, precision); - }).filter(o => !!o); + createMeridian(x, x % xstepMajor === 0); + }); if (isRotated) { // add meridian lines that will appear on the left and right sides of the // projected graticule - meridians.push(createMeridian(antimeridian - e, -90, 90, precision)); - meridians.push(createMeridian(antimeridian + e, -90, 90, precision)); + // offset the lines by a larger amount than the width of any cuts + createMeridian(antimeridian - 2e-8, true); + createMeridian(antimeridian + 2e-8, true); } - var parallels = yy.map(function(y) { - return createParallel(y, -180, 180, precision); + yy.forEach(function(y) { + createParallel(y); }); var geojson = { type: 'FeatureCollection', @@ -73,6 +70,36 @@ function createGraticule(lon0, opts) { var graticule = importGeoJSON(geojson, {}); graticule.layers[0].name = 'graticule'; return graticule; + + function createMeridian(x, extended) { + createMeridianPart(x, -80, 80); + if (extended) { + // adding extensions as separate parts, so if the polar coordinates + // fail to project, at least the rest of the meridian line will remain + createMeridianPart(x, -90, -80); + createMeridianPart(x, 80, 90); + } + } + + function createMeridianPart(x, ymin, ymax) { + var coords = []; + for (var y = ymin; y < ymax; y += precision) { + coords.push([x, y]); + } + coords.push([x, ymax]); + meridians.push(graticuleFeature(coords, {type: 'meridian', value: x})); + } + + function createParallel(y) { + var coords = []; + var xmin = -180; + var xmax = 180; + for (var x = xmin; x < xmax; x += precision) { + coords.push([x, y]); + } + coords.push([xmax, y]); + parallels.push(graticuleFeature(coords, {type: 'parallel', value: y})); + } } function graticuleFeature(coords, o) { @@ -85,21 +112,3 @@ function graticuleFeature(coords, o) { } }; } - -function createMeridian(x, ymin, ymax, precision) { - var coords = []; - for (var y = ymin; y < ymax; y += precision) { - coords.push([x, y]); - } - coords.push([x, ymax]); - return graticuleFeature(coords, {type: 'meridian', value: x}); -} - -function createParallel(y, xmin, xmax, precision) { - var coords = []; - for (var x = xmin; x < xmax; x += precision) { - coords.push([x, y]); - } - coords.push([xmax, y]); - return graticuleFeature(coords, {type: 'parallel', value: y}); -} diff --git a/src/crs/mapshaper-proj-info.js b/src/crs/mapshaper-proj-info.js index 74065aa4b..66d072098 100644 --- a/src/crs/mapshaper-proj-info.js +++ b/src/crs/mapshaper-proj-info.js @@ -3,30 +3,27 @@ export function getCrsSlug(P) { return P.params.proj.param; // kludge } -export function isWorldProjection(P) { - return getWorldProjections().includes(getCrsSlug(P)); +// 'normal' = the projection is aligned to the Earth's axis +// (i.e. it has a normal aspect) +export function isRotatedNormalProjection(P) { + return isAxisAligned(P) && P.lam0 !== 0; } -export function isRotatedWorldProjection(P) { - return isWorldProjection(P) && P.lam0 !== 0; +export function isAxisAligned(P) { + return !isNonNormal(P); } -// TODO: rename this function -// These are projections that cover the entire world and can be rotated horizontally -// -// not included -// bertin1953 (doesn't rotate) -// euler (world? seems to need more params) -// murd1,murd2,murd3 (missing param) -// -// not implemented -// bonne,cc,collg,comill,fahey,igh,larr,lask -// -function getWorldProjections() { - return 'robin,cupola,wintri,aitoff,apian,august,bacon,boggs,cea,crast,' + - 'denoy,eck1,eck2,eck3,eck4,eck5,eck6,eqc,eqearth,fouc,gall,gilbert,gins8,goode,' + - 'hammer,hatano,igh,kav5,kav7,loxim,mbt_fpp,mbt_fpq,mbt_fps,mbt_s,mbtfps,mill,' + - 'moll,natearth,natearth2,nell,nell_h,ortel,patterson,putp1,putp2,putp3,putp3p,' + - 'putp4p,putp5,putp5p,putp6,putp6p,qua_aut,times,vandg,vandg2,vandg3,vandg4,' + - 'wag1,wag2,wag3,wag4,wag5,wag6,wag7,weren,wink1,wink2'.split(); +function isNonNormal(P) { + var others = 'cassini,gnom,bertin1953,chamb,ob_tran,tpeqd,healpix,rhealpix,' + + 'ob_tran,ocea,omerc,tmerc,etmerc'; + return isAzimuthal(P) || inList(P, others); +} + +function isAzimuthal(P) { + return inList(P, + 'aeqd,gnom,laea,mil_os,lee_os,gs48,alsk,gs50,nsper,tpers,ortho,qsc,stere,ups,sterea'); +} + +function inList(P, str) { + return str.split(',').includes(getCrsSlug(P)); } diff --git a/src/crs/mapshaper-spherical-cutting.js b/src/crs/mapshaper-spherical-cutting.js index 8baea826c..981c1f5da 100644 --- a/src/crs/mapshaper-spherical-cutting.js +++ b/src/crs/mapshaper-spherical-cutting.js @@ -1,20 +1,34 @@ import { isLatLngCRS } from '../crs/mapshaper-projections'; -import { isRotatedWorldProjection } from '../crs/mapshaper-proj-info'; +import { isRotatedNormalProjection } from '../crs/mapshaper-proj-info'; import { importGeoJSON } from '../geojson/geojson-import'; import { clipLayers } from '../commands/mapshaper-clip-erase'; import { layerHasPaths } from '../dataset/mapshaper-layer-utils'; import { getAntimeridian } from '../geom/mapshaper-latlon'; export function insertPreProjectionCuts(dataset, src, dest) { - // currently only supports adding a single vertical cut to (most) world map projections - // centered on a non-zero longitude. - if (isLatLngCRS(src) && isRotatedWorldProjection(dest)) { - insertVerticalCut(dataset, getAntimeridian(dest.lam0 * 180 / Math.PI)); + var antimeridian = getAntimeridian(dest.lam0 * 180 / Math.PI); + // currently only supports adding a single vertical cut to earth axis-aligned + // map projections centered on a non-zero longitude. + // TODO: need a more sophisticated kind of cutting to handle other cases + if (isLatLngCRS(src) && + isRotatedNormalProjection(dest) && + datasetCrossesLon(dataset, antimeridian)) { + insertVerticalCut(dataset, antimeridian); return true; } return false; } +function datasetCrossesLon(dataset, lon) { + var crosses = 0; + dataset.arcs.forEachSegment(function(i, j, xx, yy) { + var ax = xx[i], + bx = xx[j]; + if (ax <= lon && bx >= lon || ax >= lon && bx <= lon) crosses++; + }); + return crosses > 0; +} + function insertVerticalCut(dataset, lon) { var pathLayers = dataset.layers.filter(layerHasPaths); if (pathLayers.length === 0) return; From 9c4121f5f2fe57296804c026d3119ab39a21cf05 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 7 May 2021 01:51:49 -0400 Subject: [PATCH 024/891] Better handling of unprojectable arcs --- src/commands/mapshaper-proj.js | 3 +-- src/crs/mapshaper-densify.js | 30 +++++++++++++++++------------- src/paths/mapshaper-arc-editor.js | 1 + 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/commands/mapshaper-proj.js b/src/commands/mapshaper-proj.js index ff6375e28..81f02253a 100644 --- a/src/commands/mapshaper-proj.js +++ b/src/commands/mapshaper-proj.js @@ -144,8 +144,7 @@ export function projectDataset(dataset, src, dest, opts) { errors = projectArcs2(dataset.arcs, proj); } if (errors > 0) { - // TODO: implement this (null arcs have zero length) - // internal.removeShapesWithNullArcs(dataset); + message(`Removed ${errors} ${errors == 1 ? 'path' : 'paths'} containing unprojectable vertices.`); } if (cuts) { diff --git a/src/crs/mapshaper-densify.js b/src/crs/mapshaper-densify.js index d372595ce..0568c0386 100644 --- a/src/crs/mapshaper-densify.js +++ b/src/crs/mapshaper-densify.js @@ -1,21 +1,21 @@ import geom from '../geom/mapshaper-geom'; import { editArcs } from '../paths/mapshaper-arc-editor'; import { getAvgSegment2 } from '../paths/mapshaper-path-utils'; +import { stop } from '../utils/mapshaper-logging'; export function projectAndDensifyArcs(arcs, proj) { var interval = getDefaultDensifyInterval(arcs, proj); - var p = [0, 0]; + var p; return editArcs(arcs, onPoint); function onPoint(append, lng, lat, prevLng, prevLat, i) { - var prevX = p[0], - prevY = p[1]; + var pp = p; p = proj(lng, lat); if (!p) return false; // signal that current arc contains an error // Don't try to densify shorter segments (optimization) - if (i > 0 && geom.distanceSq(p[0], p[1], prevX, prevY) > interval * interval * 25) { - densifySegment(prevLng, prevLat, prevX, prevY, lng, lat, p[0], p[1], proj, interval) + if (i > 0 && geom.distanceSq(p[0], p[1], pp[0], pp[1]) > interval * interval * 25) { + densifySegment(prevLng, prevLat, pp[0], pp[1], lng, lat, p[0], p[1], proj, interval) .forEach(append); } append(p); @@ -28,14 +28,18 @@ function getDefaultDensifyInterval(arcs, proj) { a = proj(bb.centerX(), bb.centerY()), b = proj(bb.centerX() + xy[0], bb.centerY() + xy[1]), c = proj(bb.centerX(), bb.ymin), // right center - d = proj(bb.xmax, bb.centerY()), // bottom center - // interval A: based on average segment length - intervalA = geom.distance2D(a[0], a[1], b[0], b[1]), - // interval B: a fraction of avg bbox side length - // (added this for bbox densification) - intervalB = (geom.distance2D(a[0], a[1], c[0], c[1]) + - geom.distance2D(a[0], a[1], d[0], d[1])) / 5000; - return Math.min(intervalA, intervalB); + d = proj(bb.xmax, bb.centerY()); // bottom center + // interval A: based on average segment length + var intervalA = a && b ? geom.distance2D(a[0], a[1], b[0], b[1]) : Infinity; + // interval B: a fraction of avg bbox side length + // (added this for bbox densification) + var intervalB = c && d ? (geom.distance2D(a[0], a[1], c[0], c[1]) + + geom.distance2D(a[0], a[1], d[0], d[1])) / 5000 : Infinity; + var interval = Math.min(intervalA, intervalB); + if (interval == Infinity) { + stop('Projection failure'); + } + return interval; } // Interpolate points into a projected line segment if needed to prevent large diff --git a/src/paths/mapshaper-arc-editor.js b/src/paths/mapshaper-arc-editor.js index 35ec3d883..a3fe3b75b 100644 --- a/src/paths/mapshaper-arc-editor.js +++ b/src/paths/mapshaper-arc-editor.js @@ -1,5 +1,6 @@ import { message } from '../utils/mapshaper-logging'; +// Returns number of arcs that were removed export function editArcs(arcs, onPoint) { var nn2 = [], xx2 = [], From f3fe6bec71e57e6b1eb07ac6927ed7dfecfdcec8 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 7 May 2021 01:59:11 -0400 Subject: [PATCH 025/891] Show a message when point features fail to project --- src/commands/mapshaper-proj.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/commands/mapshaper-proj.js b/src/commands/mapshaper-proj.js index 81f02253a..d1af46542 100644 --- a/src/commands/mapshaper-proj.js +++ b/src/commands/mapshaper-proj.js @@ -129,28 +129,33 @@ export function getCrsInfo(name, catalog) { export function projectDataset(dataset, src, dest, opts) { var proj = getProjTransform2(src, dest); // v2 returns null points instead of throwing an error - var errors, cuts; + var badArcs = 0; + var badPoints = 0; + var cuts; dataset.layers.forEach(function(lyr) { if (layerHasPoints(lyr)) { - projectPointLayer(lyr, proj); // v2 compatible (invalid points are removed) + badPoints += projectPointLayer(lyr, proj); // v2 compatible (invalid points are removed) } }); if (dataset.arcs) { cuts = insertPreProjectionCuts(dataset, src, dest); if (opts.densify) { - errors = projectAndDensifyArcs(dataset.arcs, proj); + badArcs = projectAndDensifyArcs(dataset.arcs, proj); } else { - errors = projectArcs2(dataset.arcs, proj); - } - if (errors > 0) { - message(`Removed ${errors} ${errors == 1 ? 'path' : 'paths'} containing unprojectable vertices.`); + badArcs = projectArcs2(dataset.arcs, proj); } if (cuts) { cleanProjectedLayers(dataset); } } + if (badArcs > 0) { + message(`Removed ${badArcs} ${badArcs == 1 ? 'path' : 'paths'} containing unprojectable vertices.`); + } + if (badPoints > 0) { + message(`Removed ${badPoints} unprojectable ${badPoints == 1 ? 'point' : 'points'}.`); + } } function cleanProjectedLayers(dataset) { @@ -167,9 +172,13 @@ function cleanProjectedLayers(dataset) { // proj: function to project [x, y] point; should return null if projection fails // TODO: fatal error if no points project? function projectPointLayer(lyr, proj) { + var errors = 0; editShapes(lyr.shapes, function(p) { - return proj(p[0], p[1]); // removes points that fail to project + var p2 = proj(p[0], p[1]); + if (!p2) errors++; + return p2; // removes points that fail to project }); + return errors; } function projectArcs(arcs, proj) { From 9cc0af018a45fa4bda7f6eef2d1159d2f38d847c Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 7 May 2021 18:45:40 -0400 Subject: [PATCH 026/891] v0.5.51 --- CHANGELOG.md | 5 ++ package-lock.json | 2 +- package.json | 2 +- src/color/color-schemes.js | 80 +++++++++++++++++++++++------- src/commands/mapshaper-classify.js | 8 ++- test/classify-test.js | 12 +++++ 6 files changed, 87 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c7f14993..390f295ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +v0.5.51 +* Apply antimeridian cutting to more projections. +* Assign categorical colors automatically using -classify categories=\*. +* Add several 20-color categorical color schemes. + v0.5.50 * Bug fix for an argument parsing error. diff --git a/package-lock.json b/package-lock.json index 9c26382d6..9e5f79fc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.50", + "version": "0.5.51", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c6e1fd2b5..900672f16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.50", + "version": "0.5.51", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/color/color-schemes.js b/src/color/color-schemes.js index 2d942fa2e..44e9a780d 100644 --- a/src/color/color-schemes.js +++ b/src/color/color-schemes.js @@ -3,19 +3,57 @@ import { print, stop, error, message } from '../utils/mapshaper-logging'; import { getStoppedValues } from '../classification/mapshaper-interpolation'; import utils from '../utils/mapshaper-utils'; -var categorical = 'Category10,Accent,Dark2,Paired,Pastel1,Pastel2,Set1,Set2,Set3,Tableau10'.split(','); -var sequential = 'Blues,Greens,Greys,Purples,Reds,Oranges,BuGn,BuPu,GnBu,OrRd,PuBuGn,PuBu,PuRd,RdPu,YlGnBu,YlGn,YlOrBr,YlOrRd'.split(','); -var rainbow = 'Cividis,CubehelixDefault,Rainbow,Warm,Cool,Sinebow,Turbo,Viridis,Magma,Inferno,Plasma'.split(','); -var diverging = 'BrBG,PRGn,PRGn,PiYG,PuOr,RdBu,RdGy,RdYlBu,RdYlGn,Spectral'.split(','); +var index = { + categorical: [], + sequential: [], + rainbow: [], + diverging: [] +}; +var ramps; + +function initSchemes() { + if (ramps) return; + ramps = {}; + addSchemesFromD3('categorical', 'Category10,Accent,Dark2,Paired,Pastel1,Pastel2,Set1,Set2,Set3,Tableau10'); + addSchemesFromD3('sequential', 'Blues,Greens,Greys,Purples,Reds,Oranges,BuGn,BuPu,GnBu,OrRd,PuBuGn,PuBu,PuRd,RdPu,YlGnBu,YlGn,YlOrBr,YlOrRd'); + addSchemesFromD3('rainbow', 'Cividis,CubehelixDefault,Rainbow,Warm,Cool,Sinebow,Turbo,Viridis,Magma,Inferno,Plasma'); + addSchemesFromD3('diverging', 'BrBG,PRGn,PRGn,PiYG,PuOr,RdBu,RdGy,RdYlBu,RdYlGn,Spectral'); + testLib(); // make sure these schemes are all available + addCategoricalScheme('Category20', + '1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5'); + addCategoricalScheme('Category20b', + '393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6'); + addCategoricalScheme('Category20c', + '3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9'); + addCategoricalScheme('Tableau20', + '4c78a89ecae9f58518ffbf7954a24b88d27ab79a20f2cf5b43989483bcb6e45756ff9d9879706ebab0acd67195fcbfd2b279a2d6a5c99e765fd8b5a5'); +} + +function addSchemesFromD3(type, names) { + index[type] = index[type].concat(names.split(',')); +} + +function addCategoricalScheme(name, str) { + index.categorical.push(name); + ramps[name] = unpackRamp(str); +} + +function unpackRamp(str) { + var colors = []; + for (var i=0, n=str.length; i colors.length) { stop(name, 'does not contain', n, 'colors'); } @@ -55,10 +94,13 @@ export function getCategoricalColorScheme(name, n) { } export function isColorSchemeName(name) { - return categorical.concat(sequential).concat(rainbow).concat(diverging).includes(name); + initSchemes(); + return index.categorical.includes(name) || index.sequential.includes(name) || + index.diverging.includes(name) || index.rainbow.includes(name); } export function getColorRamp(name, n, stops) { + initSchemes(); var lib = require('d3-scale-chromatic'); var ramps = lib['scheme' + name]; var interpolate = lib['interpolate' + name]; @@ -66,7 +108,7 @@ export function getColorRamp(name, n, stops) { if (!ramps && !interpolate) { stop('Unknown color scheme name:', name); } - if (categorical.includes(name)) { + if (index.categorical.includes(name)) { stop(name, ' is a categorical color scheme (expected a sequential color scheme)'); } if (ramps && ramps[n]) { diff --git a/src/commands/mapshaper-classify.js b/src/commands/mapshaper-classify.js index e99073ee3..d33c25d50 100644 --- a/src/commands/mapshaper-classify.js +++ b/src/commands/mapshaper-classify.js @@ -18,6 +18,7 @@ import { interpolateValuesToClasses } from '../classification/mapshaper-interpolation'; import cmd from '../mapshaper-cmd'; +import { getUniqFieldValues } from '../datatable/mapshaper-data-utils'; cmd.classify = function(lyr, optsArg) { var opts = optsArg || {}; @@ -37,6 +38,7 @@ cmd.classify = function(lyr, optsArg) { numBuckets = opts.classes; } + // TODO: better validation of breaks values if (opts.breaks) { numBuckets = opts.breaks.length + 1; @@ -53,6 +55,11 @@ cmd.classify = function(lyr, optsArg) { stop('Missing a data field to classify'); } + // expand categories if value is '*' + if (dataField && opts.categories && opts.categories.includes('*')) { + opts.categories = getUniqFieldValues(records, dataField); + } + requireDataField(lyr.data, dataField); if (numBuckets) { @@ -76,7 +83,6 @@ cmd.classify = function(lyr, optsArg) { if (opts.categories) { classValues = getCategoricalColorScheme(colorScheme, opts.categories.length); - message('Colors:', formatValuesForLogging(classValues)); numBuckets = numValues = classValues.length; } else { if (!numBuckets) { diff --git a/test/classify-test.js b/test/classify-test.js index 7a63cc1f0..7550190ba 100644 --- a/test/classify-test.js +++ b/test/classify-test.js @@ -13,7 +13,19 @@ describe('mapshaper-classify.js', function () { done(); }); }) + + it('assign a color to each value when categories=* is used', function(done) { + var data = 'name\ncar\ntruck\ntrain\nbike'; + var cmd = '-i data.csv -classify name categories=* colors=Tableau20 -o'; + api.applyCommands(cmd, {'data.csv': data}, function(err, out) { + var target = 'name,fill\ncar,#4c78a8\ntruck,#9ecae9\ntrain,#f58518\nbike,#ffbf79'; + assert.equal(out['data.csv'], target); + done(); + }); + }) + }) + it('error on unknown color scheme', function(done) { var data='value\n1\n2\n3\n4'; api.applyCommands('-i data.csv -classify value colors=blues -o', {'data.csv': data}, function(err, out) { From e3280646885e4ff7bc150d403f3a55d7849f2b5c Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 11 May 2021 12:35:10 -0400 Subject: [PATCH 027/891] Improvements to -proj and -graticule --- CHANGELOG.md | 8 + src/buffer/mapshaper-point-buffer.js | 60 +++++-- src/buffer/mapshaper-polyline-buffer.js | 5 +- src/cli/mapshaper-options.js | 29 +++- src/cli/mapshaper-run-command.js | 2 +- src/clipping/mapshaper-clip-utils.js | 19 +++ src/commands/mapshaper-graticule.js | 59 ++++--- src/commands/mapshaper-proj.js | 24 ++- src/commands/mapshaper-rectangle.js | 2 +- src/commands/mapshaper-shape.js | 47 ++++-- src/crs/mapshaper-proj-info.js | 47 +++++- src/crs/mapshaper-proj-utils.js | 5 + src/crs/mapshaper-spherical-clipping.js | 79 +++++++++ src/crs/mapshaper-spherical-cutting.js | 15 +- src/geom/mapshaper-antimeridian.js | 155 ++++++++++++++++++ src/geom/mapshaper-geodesic.js | 22 +-- src/geom/mapshaper-polygon-geom.js | 8 +- src/paths/mapshaper-intersection-cuts.js | 12 +- src/polylines/mapshaper-polyline-clean.js | 2 +- .../issues/proj_issues/split_island_geo.json | 4 + test/proj-test.js | 18 +- 21 files changed, 525 insertions(+), 97 deletions(-) create mode 100644 src/clipping/mapshaper-clip-utils.js create mode 100644 src/crs/mapshaper-proj-utils.js create mode 100644 src/crs/mapshaper-spherical-clipping.js create mode 100644 src/geom/mapshaper-antimeridian.js create mode 100644 test/data/issues/proj_issues/split_island_geo.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 390f295ce..122cde84d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +v0.5.53 +* Automatically clip content to an appropriate circle when applying most of the azimuthal projections (including stere,ortho,gnom,laea,nsper). +* Added the -proj clip-angle= option to override the default clipping circle. +* Automatically clip away the poles when projecting to Mercator. +* Added the -proj clip-bbox= to clip content to a lat-long bounding box before projecting. +* Added the -graticule interval= option, for customizing the spacing of graticule lines. +* Added a circular outline to the graticule when creating a graticule for an azimuthal projection. + v0.5.51 * Apply antimeridian cutting to more projections. * Assign categorical colors automatically using -classify categories=\*. diff --git a/src/buffer/mapshaper-point-buffer.js b/src/buffer/mapshaper-point-buffer.js index 684a9a4e5..63ab5f6ee 100644 --- a/src/buffer/mapshaper-point-buffer.js +++ b/src/buffer/mapshaper-point-buffer.js @@ -1,20 +1,36 @@ -import { getGeodeticSegmentFunction } from '../geom/mapshaper-geodesic'; +import { getPreciseGeodeticSegmentFunction, getFastGeodeticSegmentFunction } from '../geom/mapshaper-geodesic'; import { getBufferDistanceFunction } from '../buffer/mapshaper-buffer-common'; import { importGeoJSON } from '../geojson/geojson-import'; +import { getDatasetCRS } from '../crs/mapshaper-projections'; +import { removeAntimeridianCrosses } from '../geom/mapshaper-antimeridian'; +import { getCRS } from '../crs/mapshaper-projections'; export function makePointBuffer(lyr, dataset, opts) { var geojson = makePointBufferGeoJSON(lyr, dataset, opts); return importGeoJSON(geojson, {}); } +// Make a single geodetic circle +export function getCircleGeoJSON(center, radius, vertices, opts) { + var n = vertices || 360; + var geod = getPreciseGeodeticSegmentFunction(getCRS('wgs84')); // ? + if (opts.inset) { + radius -= opts.inset; + } + return opts.geometry_type == 'polyline' ? + getPointBufferLineString([center], radius, n, geod) : + getPointBufferPolygon([center], radius, n, geod); +} + +// Convert a point layer to circles function makePointBufferGeoJSON(lyr, dataset, opts) { var vertices = opts.vertices || 72; var distanceFn = getBufferDistanceFunction(lyr, dataset, opts); - var geod = getGeodeticSegmentFunction(dataset); + var geod = getPreciseGeodeticSegmentFunction(getDatasetCRS(dataset)); var geometries = lyr.shapes.map(function(shape, i) { var dist = distanceFn(i); if (!dist || !shape) return null; - return getPointBufferGeometry(shape, dist, vertices, geod); + return getPointBufferPolygon(shape, dist, vertices, geod); }); // TODO: make sure that importer supports null geometries (nonstandard GeoJSON); return { @@ -23,27 +39,47 @@ function makePointBufferGeoJSON(lyr, dataset, opts) { }; } -function getPointBufferGeometry(points, distance, vertices, geod) { - var coordinates = []; +export function getPointBufferPolygon(points, distance, vertices, geod) { + var rings = [], coords; if (!points || !points.length) return null; for (var i=0; i 0) rings.push(coords.pop()); } - return coordinates.length == 1 ? { + return rings.length == 1 ? { type: 'Polygon', - coordinates: coordinates[0] + coordinates: rings[0] } : { type: 'MultiPolygon', - coordinates: coordinates + coordinates: rings + }; +} + +export function getPointBufferLineString(points, distance, vertices, geod) { + var rings = [], coords; + if (!points || !points.length) return null; + for (var i=0; i 0) rings.push(coords.pop()); + } + return rings.length == 1 ? { + type: 'LineString', + coordinates: rings[0] + } : { + type: 'MultiLineString', + coordinates: rings }; } -function getPointBufferPolygonCoordinates(p, meterDist, vertices, geod) { +// Returns GeoJSON MultiPolygon coordinates +function getPointBufferCoordinates(center, meterDist, vertices, geod) { var coords = [], angle = 360 / vertices; for (var i=0; i 0) { // adding extensions as separate parts, so if the polar coordinates // fail to project, at least the rest of the meridian line will remain - createMeridianPart(x, -90, -80); - createMeridianPart(x, 80, 90); + createMeridianPart(x, -90, -90 + y0); + createMeridianPart(x, 90 - y0, 90); } } diff --git a/src/commands/mapshaper-proj.js b/src/commands/mapshaper-proj.js index d1af46542..531eb6dbc 100644 --- a/src/commands/mapshaper-proj.js +++ b/src/commands/mapshaper-proj.js @@ -9,6 +9,7 @@ import { setDatasetCRS } from '../crs/mapshaper-projections'; import { insertPreProjectionCuts } from '../crs/mapshaper-spherical-cutting'; +import { preProjectionClip } from '../crs/mapshaper-spherical-clipping'; import { cleanLayers } from '../commands/mapshaper-clean'; import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; import { projectAndDensifyArcs } from '../crs/mapshaper-densify'; @@ -18,6 +19,7 @@ import { datasetHasGeometry } from '../dataset/mapshaper-dataset-utils'; import { runningInBrowser } from '../mapshaper-state'; import { stop, message } from '../utils/mapshaper-logging'; import { importFile } from '../io/mapshaper-file-import'; +import { buildTopology } from '../topology/mapshaper-topology'; import cmd from '../mapshaper-cmd'; import utils from '../utils/mapshaper-utils'; import geom from '../geom/mapshaper-geom'; @@ -132,6 +134,8 @@ export function projectDataset(dataset, src, dest, opts) { var badArcs = 0; var badPoints = 0; var cuts; + var clipped = preProjectionClip(dataset, src, dest, opts); + dataset.layers.forEach(function(lyr) { if (layerHasPoints(lyr)) { badPoints += projectPointLayer(lyr, proj); // v2 compatible (invalid points are removed) @@ -145,11 +149,14 @@ export function projectDataset(dataset, src, dest, opts) { } else { badArcs = projectArcs2(dataset.arcs, proj); } + } - if (cuts) { - cleanProjectedLayers(dataset); - } + if (cuts || clipped) { + // TODO: could more selective in cleaning clipped layers + // (probably only needed when clipped area crosses the antimeridian or includes a pole) + cleanProjectedLayers(dataset); } + if (badArcs > 0) { message(`Removed ${badArcs} ${badArcs == 1 ? 'path' : 'paths'} containing unprojectable vertices.`); } @@ -158,11 +165,18 @@ export function projectDataset(dataset, src, dest, opts) { } } +// * Heals cuts in previously split-apart polygons +// * Removes line intersections +// * TODO: what if a layer contains polygons with desired overlaps? should +// we ignore overlaps between different features? function cleanProjectedLayers(dataset) { - // heal cuts in previously split-apart polygons // TODO: only clean affected polygons (cleaning all polygons can be slow) var polygonLayers = dataset.layers.filter(lyr => lyr.geometry_type == 'polygon'); - cleanLayers(polygonLayers, dataset, {no_arc_dissolve: true, verbose: false}); + // clean options: force a topology update (by default, this only happens when + // vertices change during cleaning, but reprojection can require a topology update + // even if clean does not change vertices) + var cleanOpts = {rebuild_topology: true, no_arc_dissolve: true, verbose: false}; + cleanLayers(polygonLayers, dataset, cleanOpts); // remove unused arcs from polygon and polyline layers // TODO: fix bug that leaves uncut arcs in the arc table // (e.g. when projecting a graticule) diff --git a/src/commands/mapshaper-rectangle.js b/src/commands/mapshaper-rectangle.js index b84edd7d3..00f211f03 100644 --- a/src/commands/mapshaper-rectangle.js +++ b/src/commands/mapshaper-rectangle.js @@ -126,7 +126,7 @@ function applyBoundsOffset(offsetOpt, bounds, crs) { return bounds; } -function convertBboxToGeoJSON(bbox, opts) { +export function convertBboxToGeoJSON(bbox, opts) { var coords = [[bbox[0], bbox[1]], [bbox[0], bbox[3]], [bbox[2], bbox[3]], [bbox[2], bbox[1]], [bbox[0], bbox[1]]]; return { diff --git a/src/commands/mapshaper-shape.js b/src/commands/mapshaper-shape.js index b356ef6e7..288c7e368 100644 --- a/src/commands/mapshaper-shape.js +++ b/src/commands/mapshaper-shape.js @@ -2,15 +2,40 @@ import { importGeoJSON } from '../geojson/geojson-import'; import GeoJSON from '../geojson/geojson-common'; import { stop } from '../utils/mapshaper-logging'; import cmd from '../mapshaper-cmd'; +import { getDatasetCRS } from '../crs/mapshaper-projections'; +import { projectDataset } from '../commands/mapshaper-proj'; +import { getCircleGeoJSON } from '../buffer/mapshaper-point-buffer'; -cmd.shape = function(opts) { - var coords = opts.coordinates; - var offsets = opts.offsets || []; - var coordinates = []; - var geojson, dataset, type, i, x, y; +cmd.shape = function(targetDataset, opts) { + var geojson, dataset; + if (opts.coordinates) { + geojson = makeShapeFromCoords(opts); + } else if (opts.type == 'circle') { + geojson = makeCircle(opts); + } else { + stop('Missing coordinates parameter'); + } + // TODO: project shape if targetDataset is projected + dataset = importGeoJSON(geojson, {}); + dataset.layers[0].name = opts.name || 'shape'; + return dataset; +}; - if (!coords || coords.length >= 2 === false) { - stop('Missing list of coordinates'); +function makeCircle(opts) { + if (opts.radius > 0 === false) { + stop('Missing required radius parameter.'); + } + var cp = opts.center || [0, 0]; + return getCircleGeoJSON(cp, opts.radius, null, {geometry_type : opts.geometry || 'polygon'}); +} + +function makeShapeFromCoords(opts) { + var coordinates = []; + var offsets = opts.offsets || []; + var coords = opts.coordinates; + var type, i, x, y; + if (coords.length >= 2 === false) { + stop('Invalid coordinates parameter.'); } for (i=0; i0 && Math.abs(pp[0] - p[0]) > 180) { + parts.push(part = []); + } + part.push(p); + pp = p; + } + if (type == 'polyline') { + return dividePolylines(parts); + } + if (parts.length == 1) { + // TODO: this test should not be needed when processing small circles + // (could affect performance when buffering many points) + if (ringArea(ring) < 0) { + // negative area: CCW ring, indicating a circle of >180 degrees + // that fully encloses both poles and the antimeridian. + // need to add an enclosure around the entire sphere + parts = [[[180, 90], [180, -90], [-180, -90], [-180, 90], [180, 90]], parts[0]]; + } + return [parts]; + } + if (parts.length == 2) { + return removeOneCross(parts); + } + if (parts.length == 3) { + return removeTwoCrosses(parts, ringArea(ring)); + } + stop('Unexpected geometry of an antimeridan-crossing polygon ring.'); +} + +function ringArea(ring) { + var iter = new PointIter(ring); + return getSphericalPathArea2(iter); +} + +function dividePolylines(parts) { + var a, b, x1, x2, y; + for (var i=1; i 1) { + parts[0] = parts.pop().concat(parts[0]); + } + return parts; +} + +// Ring crosses twice... +// Returns one ring containing both poles or two rings split across +// the antimeridian and including neither pole. +function removeTwoCrosses(parts, ringArea) { + var a = lastEl(parts[0]), + b = firstEl(parts[1]), + c = lastEl(parts[1]), + d = firstEl(parts[2]), + y1 = planarIntercept(a, b), + y2 = planarIntercept(c, d), + x1 = a[0] < 0 ? -180 : 180, + x2 = x1 < 0 ? 180 : -180, + ring1, ring2, pole1, pole2; + if (ringArea < 1) { + ring1 = parts[0].concat([[x1, y1], [x1, y2]], parts[2]); + ring2 = parts[1].concat([[x2, y2], [x2, y1], b]); + return [[dedup(ring1)], [dedup(ring2)]]; + } + if (y1 > y2) { + pole1 = 90; + pole2 = -90; + } else { + pole1 = -90; + pole2 = 90; + } + ring1 = parts[0].concat( + [[x1, y1], [x1, pole1], [x2, pole1], [x2, y1]], + parts[1], + [[x2, y2], [x2, pole2], [x1, pole2], [x1, y2]], + parts[2]); + return [[dedup(ring1)]]; +} + +// duplicate points occur if a vertex is on the antimeridan +function dedup(ring) { + return ring.reduce(function(memo, p, i) { + var pp = memo.length > 0 ? memo[memo.length-1] : null; + if (!pp || pp[0] != p[0] || pp[1] != p[1]) memo.push(p); + return memo; + }, []); +} + +// Ring contains a pole. +// Returns one ring including n or s pole line. +function removeOneCross(parts) { + var p1 = lastEl(parts[0]); + var p2 = firstEl(parts[1]); + var dx = p2[0] - p1[0]; // pos: crosses antimeridian w->e, neg: e->w + var y = planarIntercept(p1, p2); + // if ring crosses w->e, go through n pole + // (this assumes that ring has CW winding / is not a hole) + var ypole = dx > 0 ? 90 : -90; + var coords = dx > 0 ? + [[-180, y], [-180, ypole], [180, ypole], [180, y]] : + [[180, y], [180, ypole], [-180, ypole], [-180, y]]; + var ring = parts[0].concat(coords, parts[1]); + return [[dedup(ring)]]; // multipolygon format +} + +function lastEl(arr) { + return arr[arr.length - 1]; +} + +function firstEl(arr) { + return arr[0]; +} + +// p1, p2: two vertices on different sides of the antimeridian +// Returns y-intercept of the segment connecting p1, p2 +// TODO: consider using the great-circle intersection, instead of +// the planar intersection. +// (Planar should be fine if p1 and p2 are close to lon. 180) +function planarIntercept(p1, p2) { + var dx = p2[0] - p1[0]; // pos: crosses antimeridian w->e, neg: e->w + var dx1, dx2; + if (dx > 0) { + dx1 = p1[0] + 180; + dx2 = 180 - p2[0]; + } else { + dx1 = 180 - p1[0]; + dx2 = p2[0] + 180; + } + return (dx2 * p1[1] + dx1 * p2[1]) / (dx1 + dx2); +} diff --git a/src/geom/mapshaper-geodesic.js b/src/geom/mapshaper-geodesic.js index d0e519ac9..7532a2081 100644 --- a/src/geom/mapshaper-geodesic.js +++ b/src/geom/mapshaper-geodesic.js @@ -2,8 +2,7 @@ import { isLatLngCRS, getDatasetCRS } from '../crs/mapshaper-projections'; import { error } from '../utils/mapshaper-logging'; import geom from '../geom/mapshaper-geom'; -function getGeodesic(dataset) { - var P = getDatasetCRS(dataset); +function getGeodesic(P) { if (!isLatLngCRS(P)) error('Expected an unprojected CRS'); var f = P.es / (1 + Math.sqrt(P.one_es)); var GeographicLib = require('mproj').internal.GeographicLib; @@ -33,17 +32,11 @@ function fastGeodeticSegmentFunction(lng, lat, bearing, meterDist) { return [lng2, lat2]; } -export function getGeodeticSegmentFunction(dataset, highPrecision) { - var P = getDatasetCRS(dataset); +export function getPreciseGeodeticSegmentFunction(P) { if (!isLatLngCRS(P)) { return getPlanarSegmentEndpoint; } - if (!highPrecision) { - // CAREFUL: this function has higher error at very large distances and at the poles - // also, it wouldn't work for other planets than Earth - return fastGeodeticSegmentFunction; - } - var g = getGeodesic(dataset); + var g = getGeodesic(P); return function(lng, lat, bearing, meterDist) { var o = g.Direct(lat, lng, bearing, meterDist); var p = [o.lon2, o.lat2]; @@ -51,11 +44,10 @@ export function getGeodeticSegmentFunction(dataset, highPrecision) { }; } -function getGeodeticDistanceFunction(dataset, highPrecision) { - var P = getDatasetCRS(dataset); - if (!isLatLngCRS(P)) { - return getPlanarSegmentEndpoint; - } +export function getFastGeodeticSegmentFunction(P) { + // CAREFUL: this function has higher error at very large distances and at the poles + // also, it wouldn't work for other planets than Earth + return isLatLngCRS(P) ? fastGeodeticSegmentFunction : getPlanarSegmentEndpoint; } // Useful for determining if a segment that intersects another segment is diff --git a/src/geom/mapshaper-polygon-geom.js b/src/geom/mapshaper-polygon-geom.js index 920d8a1ab..aeba727bb 100644 --- a/src/geom/mapshaper-polygon-geom.js +++ b/src/geom/mapshaper-polygon-geom.js @@ -158,8 +158,12 @@ export function getPathArea(ids, arcs) { } export function getSphericalPathArea(ids, arcs) { - var iter = arcs.getShapeIter(ids), - sum = 0, + var iter = arcs.getShapeIter(ids); + return getSphericalPathArea2(iter); +} + +export function getSphericalPathArea2(iter) { + var sum = 0, started = false, deg2rad = Math.PI / 180, x, y, xp, yp; diff --git a/src/paths/mapshaper-intersection-cuts.js b/src/paths/mapshaper-intersection-cuts.js index cb2c2640e..ce1454dbc 100644 --- a/src/paths/mapshaper-intersection-cuts.js +++ b/src/paths/mapshaper-intersection-cuts.js @@ -45,7 +45,11 @@ export function addIntersectionCuts(dataset, _opts) { // used to reset simplification) arcs.flatten(); - snapAndCut(dataset, snapDist); + var changed = snapAndCut(dataset, snapDist); + // Detect topology again if coordinates have changed + if (changed || opts.rebuild_topology) { + buildTopology(dataset); + } // Clean shapes by removing collapsed arc references, etc. // TODO: consider alternative -- avoid creating degenerate arcs @@ -58,7 +62,6 @@ export function addIntersectionCuts(dataset, _opts) { // Further clean-up -- remove duplicate and missing arcs nodes = cleanArcReferences(dataset); - return nodes; } @@ -94,10 +97,7 @@ function snapAndCut(dataset, snapDist) { debug('Second-pass vertices added:', cutCount, 'consider third pass?'); } } - // Detect topology again if coordinates have changed - if (coordsHaveChanged) { - buildTopology(dataset); - } + return coordsHaveChanged; } diff --git a/src/polylines/mapshaper-polyline-clean.js b/src/polylines/mapshaper-polyline-clean.js index e46e62109..4111f996b 100644 --- a/src/polylines/mapshaper-polyline-clean.js +++ b/src/polylines/mapshaper-polyline-clean.js @@ -83,7 +83,7 @@ function divideShapeAtNodes(shp, nodes) { } function combineContiguousParts(parts, nodes, endpointIndex) { - if (parts.length < 2) return parts; + if (!parts || parts.length < 2) return parts; // Index the terminal arcs of this group of polyline parts parts.forEach(function(ids, i) { diff --git a/test/data/issues/proj_issues/split_island_geo.json b/test/data/issues/proj_issues/split_island_geo.json new file mode 100644 index 000000000..e973c0bec --- /dev/null +++ b/test/data/issues/proj_issues/split_island_geo.json @@ -0,0 +1,4 @@ +{"type":"GeometryCollection", "geometries": [ +{"type":"Polygon","coordinates":[[[180,70.87606],[180,71.4019],[179.82007,71.33236],[179.81779,71.28767],[179.51854,71.27421],[179.50786,71.23475],[179.27319,71.18453],[179.14645,71.11702],[178.92603,71.07658],[178.76402,70.99132],[178.588,70.93881],[178.66408,70.8531],[178.80505,70.81367],[178.76418,70.76479],[178.76588,70.6843],[179.10182,70.76752],[179.37158,70.78583],[179.52663,70.75187],[179.65904,70.77711],[179.65802,70.80994],[179.75522,70.85212],[179.86269,70.84535],[180,70.87606]]]}, +{"type":"Polygon","coordinates":[[[-180,71.4019],[-180,70.87606],[-179.76345,70.85404],[-179.49509,70.84942],[-179.43017,70.89558],[-179.11071,70.82946],[-178.8549,70.84238],[-178.63662,70.86782],[-178.49356,70.86534],[-178.28395,70.88663],[-178.23445,70.86864],[-178.07102,70.91376],[-177.77279,70.92802],[-177.59065,70.97463],[-177.47715,70.98501],[-177.29708,71.04282],[-177.19214,71.09789],[-177.61698,71.19674],[-177.76107,71.24799],[-177.91268,71.27183],[-177.98176,71.3041],[-178.12452,71.30808],[-178.24032,71.35107],[-178.48054,71.35124],[-178.6173,71.38392],[-178.9774,71.42085],[-179.1489,71.37719],[-179.48491,71.38027],[-179.62857,71.40648],[-179.68711,71.37683],[-179.87876,71.36724],[-180,71.4019]]]} +]} \ No newline at end of file diff --git a/test/proj-test.js b/test/proj-test.js index cb93999ba..dd1b62097 100644 --- a/test/proj-test.js +++ b/test/proj-test.js @@ -4,6 +4,22 @@ var api = require(".."); var helpers = require('./helpers'); describe('mapshaper-proj.js', function() { + + describe('antimeridian issues', function () { + it('issue: split-apart island does not dissolve', function (done) { + var file = 'test/data/issues/proj_issues/split_island_geo.json'; + var proj = '-proj +proj=laea clip-angle=160 +lon_0=170 +lat_0=20'; + var cmd = `-i ${file} -dissolve ${proj} -o out.json`; + api.applyCommands(cmd, function(err, out) { + var json = JSON.parse(out['out.json']); + // one polygon is created + assert.equal(json.geometries.length, 1); + assert.equal(json.geometries[0].type, 'Polygon'); + done(); + }) + }) + }) + describe('dynamic projection definition using -calc', function () { it('set tmerc origin', function (done) { var csv = 'id,x,y\na,1,2\nb,3,4'; @@ -44,8 +60,6 @@ describe('mapshaper-proj.js', function() { }) - - describe('-proj ', function() { it('webmercator alias', function(done) { api.applyCommands('-i test/data/three_points.shp -proj webmercator -o', From d9d5fc10d7912e6d07984e8ff7faa56edb362a81 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 11 May 2021 12:36:09 -0400 Subject: [PATCH 028/891] v0.5.52 --- CHANGELOG.md | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 122cde84d..64bdbc894 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -v0.5.53 +v0.5.52 * Automatically clip content to an appropriate circle when applying most of the azimuthal projections (including stere,ortho,gnom,laea,nsper). * Added the -proj clip-angle= option to override the default clipping circle. * Automatically clip away the poles when projecting to Mercator. diff --git a/package-lock.json b/package-lock.json index 9e5f79fc0..e0a19c020 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.51", + "version": "0.5.52", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 900672f16..13a847dbf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.51", + "version": "0.5.52", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From b31627224344ac0919eb0d6db9df7ed400b0c958 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 11 May 2021 13:40:07 -0400 Subject: [PATCH 029/891] Attach CRS data to projected graticules --- src/commands/mapshaper-proj.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/mapshaper-proj.js b/src/commands/mapshaper-proj.js index 531eb6dbc..1d5747408 100644 --- a/src/commands/mapshaper-proj.js +++ b/src/commands/mapshaper-proj.js @@ -47,7 +47,7 @@ function projCmd(dataset, destInfo, opts) { // are preserved if an error occurs var modifyCopy = runningInBrowser(), originals = [], - target = {}, + target = {info: dataset.info || {}}, src, dest; dest = destInfo.crs; @@ -94,7 +94,6 @@ function projCmd(dataset, destInfo, opts) { e.point ? ' at ' + e.point.join(' ') : '', e.message)); } - dataset.info.crs = dest; dataset.info.prj = destInfo.prj; // may be undefined dataset.arcs = target.arcs; originals.forEach(function(lyr, i) { @@ -163,6 +162,7 @@ export function projectDataset(dataset, src, dest, opts) { if (badPoints > 0) { message(`Removed ${badPoints} unprojectable ${badPoints == 1 ? 'point' : 'points'}.`); } + dataset.info.crs = dest; } // * Heals cuts in previously split-apart polygons From 759a44d2313d765ed9ca3003ce438a84197e0f2c Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 11 May 2021 21:24:07 -0400 Subject: [PATCH 030/891] Fix default clipping circle for nsper --- src/crs/mapshaper-proj-info.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/crs/mapshaper-proj-info.js b/src/crs/mapshaper-proj-info.js index 221bfd2e7..046e68a92 100644 --- a/src/crs/mapshaper-proj-info.js +++ b/src/crs/mapshaper-proj-info.js @@ -26,18 +26,17 @@ export function getDefaultClipBBox(P) { }[getCrsSlug(P)] || null; } -// TODO: add nsper, tpers export function isClippedAzimuthalProjection(P) { return inList(P, 'stere,sterea,ups,ortho,gnom,laea,nsper,tpers'); } function getPerspectiveClipAngle(P) { var h = parseFloat(P.params.h.param); - if (!h || h < P.a) { - + if (!h || h < 0) { return 0; } - var theta = Math.acos(P.a / h) * 180 / Math.PI; + var theta = Math.acos(P.a / (P.a + h)) * 180 / Math.PI; + theta *= 0.995; // reducing a bit to avoid out-of-range errors return theta; } @@ -53,7 +52,7 @@ export function getDefaultClipAngle(P) { laea: 179, ortho: 90, stere: 142, - stereea: 142, + sterea: 142, ups: 10.5 // TODO: should be 6.5 deg at north pole }[slug] || 0; } From 84f22c5961376f2fec703512886dc1364c31a51d Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 11 May 2021 21:24:34 -0400 Subject: [PATCH 031/891] Add verbose and debug as logging options for each command --- src/cli/mapshaper-command-parser.js | 3 +++ src/cli/mapshaper-options.js | 8 -------- src/cli/mapshaper-run-command.js | 4 ++-- src/cli/mapshaper-run-commands.js | 4 ++++ src/crs/mapshaper-spherical-clipping.js | 3 ++- src/geojson/geojson-reader.js | 4 ++-- src/polygons/mapshaper-polygon-mosaic.js | 4 ++-- src/utils/mapshaper-logging.js | 5 +++-- src/utils/mapshaper-timing.js | 15 +++------------ 9 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/cli/mapshaper-command-parser.js b/src/cli/mapshaper-command-parser.js index c3608ff82..9f063b4fa 100644 --- a/src/cli/mapshaper-command-parser.js +++ b/src/cli/mapshaper-command-parser.js @@ -42,6 +42,9 @@ export function CommandParser() { this.command = function(name) { var opts = new CommandOptions(name); + // support 'verbose' and 'debug' flags for each command, without help entries + opts.option('verbose', {type: 'flag'}); + opts.option('debug', {type: 'flag'}); _commands.push(opts); return opts; }; diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index a6785083a..4349363f6 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -537,9 +537,6 @@ export function getOptionParser() { describe: 'delete unused arcs but don\'t remove gaps and overlaps', type: 'flag' }) - .option('debug', { - type: 'flag' - }) .option('no-arc-dissolve', { type: 'flag' // no description }) @@ -704,7 +701,6 @@ export function getOptionParser() { describe: 'combine groups of same-color dots into multi-part features', type: 'flag' }) - .option('debug', {type: 'flag'}) .option('target', targetOpt) .option('name', nameOpt) .option('no-replace', noReplaceOpt); @@ -883,9 +879,6 @@ export function getOptionParser() { // type: 'bbox', // describe: 'xmin,ymin,xmax,ymax (default is bbox of data)' // }) - .option('debug', { - type: 'flag' - }) .option('name', nameOpt) .option('target', targetOpt) .option('no-replace', noReplaceOpt); @@ -1011,7 +1004,6 @@ export function getOptionParser() { parser.command('mosaic') .describe('convert a polygon layer with overlaps into a flat mosaic') .option('calc', calcOpt) - .option('debug', {type: 'flag'}) .option('name', nameOpt) .option('target', targetOpt) .option('no-replace', noReplaceOpt); diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index 60a17046a..707db59ea 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -9,7 +9,7 @@ import { convertSourceName, findCommandSource } from '../dataset/mapshaper-sourc import { Catalog, getFormattedLayerList } from '../dataset/mapshaper-catalog'; import { mergeCommandTargets } from '../dataset/mapshaper-merging'; import { T } from '../utils/mapshaper-timing'; -import { stop, error, UserError } from '../utils/mapshaper-logging'; +import { stop, error, UserError, verbose } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import cmd from '../mapshaper-cmd'; @@ -448,7 +448,7 @@ export function runCommand(command, catalog, cb) { done(null); function done(err) { - T.stop('-'); + verbose('-', T.stop()); cb(err, err ? null : catalog); } } diff --git a/src/cli/mapshaper-run-commands.js b/src/cli/mapshaper-run-commands.js index 4c041a610..276bcb664 100644 --- a/src/cli/mapshaper-run-commands.js +++ b/src/cli/mapshaper-run-commands.js @@ -228,6 +228,8 @@ export function runParsedCommands(commands, catalog, cb) { function nextCommand(catalog, cmd, next) { setStateVar('current_command', cmd.name); // for log msgs + setStateVar('verbose', !!cmd.options.verbose); + setStateVar('debug', !!cmd.options.debug); runCommand(cmd, catalog, next); } @@ -235,6 +237,8 @@ export function runParsedCommands(commands, catalog, cb) { if (err) printError(err); cb(err, catalog); setStateVar('current_command', null); + setStateVar('verbose', false); + setStateVar('debug', false); } } diff --git a/src/crs/mapshaper-spherical-clipping.js b/src/crs/mapshaper-spherical-clipping.js index 3ab48ae59..96f8fa3bc 100644 --- a/src/crs/mapshaper-spherical-clipping.js +++ b/src/crs/mapshaper-spherical-clipping.js @@ -13,7 +13,7 @@ import { getPreciseGeodeticSegmentFunction } from '../geom/mapshaper-geodesic'; import { clipLayersByGeoJSON } from '../clipping/mapshaper-clip-utils'; import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; import { convertBboxToGeoJSON } from '../commands/mapshaper-rectangle'; -import { error } from '../utils/mapshaper-logging'; +import { error, verbose } from '../utils/mapshaper-logging'; export function preProjectionClip(dataset, src, dest, opts) { var clipped = false; @@ -40,6 +40,7 @@ export function getProjectionOutline(src, dest, opts) { function getClipShapeGeoJSON(src, dest, opts) { var angle = opts.clip_angle || dest.clip_angle || getDefaultClipAngle(dest); if (!angle) return null; + verbose('Using clip angle of', angle, 'degrees'); var dist = getClippingRadius(src, angle); var cp = getProjCenter(dest); // kludge: attach the clipping angle to the CRS, so subsequent commands diff --git a/src/geojson/geojson-reader.js b/src/geojson/geojson-reader.js index b7dae1704..f09e7c4e1 100644 --- a/src/geojson/geojson-reader.js +++ b/src/geojson/geojson-reader.js @@ -1,5 +1,5 @@ import { bufferToString } from '../text/mapshaper-encodings'; -import { stop } from '../utils/mapshaper-logging'; +import { stop, debug } from '../utils/mapshaper-logging'; import { parseObjects } from '../geojson/json-parser'; import { T } from '../utils/mapshaper-timing'; @@ -21,7 +21,7 @@ export function GeoJSONReader(reader) { T.start(); parseObjects(reader, offset, onObject); // parseObjects_native(reader, offset, onObject); - T.stop('Parse GeoJSON'); + debug('Parse GeoJSON', T.stop()); }; } diff --git a/src/polygons/mapshaper-polygon-mosaic.js b/src/polygons/mapshaper-polygon-mosaic.js index f7f702f70..525ca87d0 100644 --- a/src/polygons/mapshaper-polygon-mosaic.js +++ b/src/polygons/mapshaper-polygon-mosaic.js @@ -77,7 +77,7 @@ export function buildPolygonMosaic(nodes) { // Process CW rings: these are indivisible space-enclosing boundaries of mosaic tiles var mosaic = data.cw.map(function(ring) {return [ring];}); - T.stop('Find mosaic rings'); + debug('Find mosaic rings', T.stop()); T.start(); // Process CCW rings: these are either holes or enclosure @@ -95,7 +95,7 @@ export function buildPolygonMosaic(nodes) { enclosures.push([ring]); } }); - T.stop(utils.format("Detect holes (holes: %d, enclosures: %d)", data.ccw.length - enclosures.length, enclosures.length)); + debug(utils.format("Detect holes (holes: %d, enclosures: %d)", data.ccw.length - enclosures.length, enclosures.length), T.stop()); return {mosaic: mosaic, enclosures: enclosures, lostArcs: data.lostArcs}; } diff --git a/src/utils/mapshaper-logging.js b/src/utils/mapshaper-logging.js index 42382cc2b..1ae75db40 100644 --- a/src/utils/mapshaper-logging.js +++ b/src/utils/mapshaper-logging.js @@ -58,13 +58,14 @@ export function print() { } export function verbose() { - if (getStateVar('VERBOSE')) { + // verbose can be set globally with the -verbose command or separately for each command + if (getStateVar('VERBOSE') || getStateVar('verbose')) { message.apply(null, arguments); } } export function debug() { - if (getStateVar('DEBUG')) { + if (getStateVar('DEBUG') || getStateVar('debug')) { logArgs(arguments); } } diff --git a/src/utils/mapshaper-timing.js b/src/utils/mapshaper-timing.js index 8b64202b3..0ed35f9be 100644 --- a/src/utils/mapshaper-timing.js +++ b/src/utils/mapshaper-timing.js @@ -1,19 +1,10 @@ -import { verbose } from '../utils/mapshaper-logging'; - -// Support for timing using T.start() and T.stop("message") +// Support for timing using T.start() and T.stop() export var T = { stack: [], start: function() { T.stack.push(+new Date()); }, - stop: function(note) { - var elapsed = (+new Date() - T.stack.pop()); - var msg = elapsed + 'ms'; - if (note) { - msg = note + " " + msg; - } - verbose(msg); - return elapsed; + stop: function() { + return (+new Date() - T.stack.pop()) + 'ms'; } }; - From c34f5db1b6f49460cbace8578d5a6f64250cf87a Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 12 May 2021 07:23:21 -0400 Subject: [PATCH 032/891] graticule and clipping tweaks --- src/commands/mapshaper-graticule.js | 2 +- src/crs/mapshaper-spherical-clipping.js | 10 ++++++---- src/geom/mapshaper-bounds.js | 5 +++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/commands/mapshaper-graticule.js b/src/commands/mapshaper-graticule.js index 85ea287e6..a5f545336 100644 --- a/src/commands/mapshaper-graticule.js +++ b/src/commands/mapshaper-graticule.js @@ -46,7 +46,7 @@ function createGraticule(P, opts) { var interval = opts.interval || 10; if (![5,10,15,30,45].includes(interval)) stop('Invalid interval:', interval); var lon0 = P.lam0 * 180 / Math.PI; - var precision = 1; // degrees between each vertex + var precision = interval > 10 ? 1 : 0.5; // degrees between each vertex var xstep = interval; var ystep = interval; var xstepMajor = 90; diff --git a/src/crs/mapshaper-spherical-clipping.js b/src/crs/mapshaper-spherical-clipping.js index 96f8fa3bc..801fcf203 100644 --- a/src/crs/mapshaper-spherical-clipping.js +++ b/src/crs/mapshaper-spherical-clipping.js @@ -11,9 +11,11 @@ import { isLatLngCRS, getDatasetCRS } from '../crs/mapshaper-projections'; import { getCircleGeoJSON } from '../buffer/mapshaper-point-buffer'; import { getPreciseGeodeticSegmentFunction } from '../geom/mapshaper-geodesic'; import { clipLayersByGeoJSON } from '../clipping/mapshaper-clip-utils'; +import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; import { convertBboxToGeoJSON } from '../commands/mapshaper-rectangle'; import { error, verbose } from '../utils/mapshaper-logging'; +import { Bounds } from '../geom/mapshaper-bounds'; export function preProjectionClip(dataset, src, dest, opts) { var clipped = false; @@ -22,8 +24,7 @@ export function preProjectionClip(dataset, src, dest, opts) { clipped = clipToCircle(dataset, src, dest, opts); } if (isClippedCylindricalProjection(dest) || opts.clip_bbox) { - clipToRectangle(dataset, dest, opts); - clipped = true; + clipped = clipped || clipToRectangle(dataset, dest, opts); } if (clipped) { // remove arcs outside the clip area, so they don't get projected @@ -40,7 +41,7 @@ export function getProjectionOutline(src, dest, opts) { function getClipShapeGeoJSON(src, dest, opts) { var angle = opts.clip_angle || dest.clip_angle || getDefaultClipAngle(dest); if (!angle) return null; - verbose('Using clip angle of', angle, 'degrees'); + verbose(`Using clip angle of ${ +angle.toFixed(2) } degrees`); var dist = getClippingRadius(src, angle); var cp = getProjCenter(dest); // kludge: attach the clipping angle to the CRS, so subsequent commands @@ -52,7 +53,8 @@ function getClipShapeGeoJSON(src, dest, opts) { function clipToRectangle(dataset, dest, opts) { var bbox = opts.clip_bbox || getDefaultClipBBox(dest); if (!bbox) error('Missing expected clip bbox.'); - // TODO: don't clip if dataset fits within the bbox + // don't clip if dataset fits within the bbox + if (Bounds.from(bbox).contains(getDatasetBounds(dataset))) return false; var geojson = convertBboxToGeoJSON(bbox); clipLayersByGeoJSON(dataset.layers, dataset, geojson, 'clip'); return true; diff --git a/src/geom/mapshaper-bounds.js b/src/geom/mapshaper-bounds.js index ba64d74ad..df9f0157f 100644 --- a/src/geom/mapshaper-bounds.js +++ b/src/geom/mapshaper-bounds.js @@ -8,6 +8,11 @@ export function Bounds() { } } +Bounds.from = function() { + var b = new Bounds(); + return b.setBounds.apply(b, arguments); +}; + Bounds.prototype.toString = function() { return JSON.stringify({ xmin: this.xmin, From 51ad3e4e11aa1f64d6fdb3cf19a65c86814ab56f Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 12 May 2021 07:24:34 -0400 Subject: [PATCH 033/891] GUI refactor, fix --- src/gui/gui-display-layer.js | 2 +- src/gui/gui-layer-utils.js | 11 +++++++ src/gui/gui-map-utils.js | 51 +++++++++++++++++---------------- src/gui/gui-map.js | 35 ++-------------------- src/gui/gui-symbol-dragging2.js | 21 +++++++++++++- 5 files changed, 62 insertions(+), 58 deletions(-) diff --git a/src/gui/gui-display-layer.js b/src/gui/gui-display-layer.js index 48bc72b97..aa83afd31 100644 --- a/src/gui/gui-display-layer.js +++ b/src/gui/gui-display-layer.js @@ -1,7 +1,7 @@ import { MultiScaleArcCollection } from './gui-shapes'; import { getDisplayLayerForTable } from './gui-table'; import { needReprojectionForDisplay, projectArcsForDisplay, projectPointsForDisplay } from './gui-dynamic-crs'; -import { filterLayerByIds } from './gui-map-utils'; +import { filterLayerByIds } from './gui-layer-utils'; import { internal, Bounds, utils } from './gui-core'; // displayCRS: CRS to use for display, or null (which clears any current display CRS) diff --git a/src/gui/gui-layer-utils.js b/src/gui/gui-layer-utils.js index adcdb20d1..5dbe4b50a 100644 --- a/src/gui/gui-layer-utils.js +++ b/src/gui/gui-layer-utils.js @@ -1,4 +1,15 @@ +import { utils } from './gui-core'; +export function filterLayerByIds(lyr, ids) { + var shapes; + if (lyr.shapes) { + shapes = ids.map(function(id) { + return lyr.shapes[id]; + }); + return utils.defaults({shapes: shapes, data: null}, lyr); + } + return lyr; +} export function formatLayerNameForDisplay(name) { return name || '[unnamed]'; diff --git a/src/gui/gui-map-utils.js b/src/gui/gui-map-utils.js index 0b0418c58..3c5c382b9 100644 --- a/src/gui/gui-map-utils.js +++ b/src/gui/gui-map-utils.js @@ -1,31 +1,34 @@ -import { utils } from './gui-core'; +import { utils, Bounds } from './gui-core'; -export function filterLayerByIds(lyr, ids) { - var shapes; - if (lyr.shapes) { - shapes = ids.map(function(id) { - return lyr.shapes[id]; - }); - return utils.defaults({shapes: shapes, data: null}, lyr); - } - return lyr; +// Test if map should be re-framed to show updated layer +export function mapNeedsReset(newBounds, prevBounds, viewportBounds, flags) { + var viewportPct = getIntersectionPct(newBounds, viewportBounds); + var contentPct = getIntersectionPct(viewportBounds, newBounds); + var boundsChanged = !prevBounds.equals(newBounds); + var inView = newBounds.intersects(viewportBounds); + var areaChg = newBounds.area() / prevBounds.area(); + var chgThreshold = flags.proj ? 1e3 : 1e8; + // don't reset if layer extent hasn't changed + if (!boundsChanged) return false; + // reset if layer is out-of-view + if (!inView) return true; + // reset if content is mostly offscreen + if (viewportPct < 0.3 && contentPct < 0.9) return true; + // reset if content bounds have changed a lot (e.g. after projection) + if (areaChg > chgThreshold || areaChg < 1/chgThreshold) return true; + return false; } -export function getDisplayCoordsById(id, layer, ext) { - var coords = getPointCoordsById(id, layer); - return ext.translateCoords(coords[0], coords[1]); +// Returns proportion of bb2 occupied by bb1 +function getIntersectionPct(bb1, bb2) { + return getBoundsIntersection(bb1, bb2).area() / bb2.area() || 0; } -export function getPointCoordsById(id, layer) { - var coords = layer && layer.geometry_type == 'point' && layer.shapes[id]; - if (!coords || coords.length != 1) { - return null; +function getBoundsIntersection(a, b) { + var c = new Bounds(); + if (a.intersects(b)) { + c.setBounds(Math.max(a.xmin, b.xmin), Math.max(a.ymin, b.ymin), + Math.min(a.xmax, b.xmax), Math.min(a.ymax, b.ymax)); } - return coords[0]; -} - -export function translateDeltaDisplayCoords(dx, dy, ext) { - var a = ext.translatePixelCoords(0, 0); - var b = ext.translatePixelCoords(dx, dy); - return [b[0] - a[0], b[1] - a[1]]; + return c; } diff --git a/src/gui/gui-map.js b/src/gui/gui-map.js index 255f00fa2..1e2af6794 100644 --- a/src/gui/gui-map.js +++ b/src/gui/gui-map.js @@ -3,7 +3,8 @@ import { CoordinatesDisplay } from './gui-coordinates-display'; import { MapNav } from './gui-map-nav'; import { SelectionTool } from './gui-selection-tool'; import { InspectionControl2 } from './gui-inspection-control2'; -import { updateLayerStackOrder } from './gui-layer-utils'; +import { updateLayerStackOrder, filterLayerByIds } from './gui-layer-utils'; +import { mapNeedsReset } from './gui-map-utils'; import { SymbolDragging2 } from './gui-symbol-dragging2'; import * as MapStyle from './gui-map-style'; import { MapExtent } from './gui-map-extent'; @@ -11,7 +12,6 @@ import { LayerStack } from './gui-layer-stack'; import { BoxTool } from './gui-box-tool'; import { projectMapExtent } from './gui-dynamic-crs'; import { getDisplayLayer, projectDisplayLayer } from './gui-display-layer'; -import { filterLayerByIds } from './gui-map-utils'; import { utils, internal, Bounds } from './gui-core'; import { EventDispatcher } from './gui-events'; import { ElementPosition } from './gui-element-position'; @@ -177,7 +177,7 @@ export function MshpMap(gui) { if (!prevLyr || !_fullBounds || prevLyr.tabular || _activeLyr.tabular || isFrameView()) { needReset = true; } else { - needReset = GUI.mapNeedsReset(fullBounds, _fullBounds, _ext.getBounds()); + needReset = mapNeedsReset(fullBounds, _fullBounds, _ext.getBounds(), e.flags); } if (isFrameView()) { @@ -459,32 +459,3 @@ function getDisplayLayerOverlay(obj, e) { style: style }, obj); } - -// Test if map should be re-framed to show updated layer -GUI.mapNeedsReset = function(newBounds, prevBounds, mapBounds) { - var viewportPct = GUI.getIntersectionPct(newBounds, mapBounds); - var contentPct = GUI.getIntersectionPct(mapBounds, newBounds); - var boundsChanged = !prevBounds.equals(newBounds); - var inView = newBounds.intersects(mapBounds); - var areaChg = newBounds.area() / prevBounds.area(); - if (!boundsChanged) return false; // don't reset if layer extent hasn't changed - if (!inView) return true; // reset if layer is out-of-view - if (viewportPct < 0.3 && contentPct < 0.9) return true; // reset if content is mostly offscreen - if (areaChg > 1e8 || areaChg < 1e-8) return true; // large area chg, e.g. after projection - return false; -}; - -// TODO: move to utilities file -GUI.getBoundsIntersection = function(a, b) { - var c = new Bounds(); - if (a.intersects(b)) { - c.setBounds(Math.max(a.xmin, b.xmin), Math.max(a.ymin, b.ymin), - Math.min(a.xmax, b.xmax), Math.min(a.ymax, b.ymax)); - } - return c; -}; - -// Returns proportion of bb2 occupied by bb1 -GUI.getIntersectionPct = function(bb1, bb2) { - return GUI.getBoundsIntersection(bb1, bb2).area() / bb2.area() || 0; -}; diff --git a/src/gui/gui-symbol-dragging2.js b/src/gui/gui-symbol-dragging2.js index 79229525b..fc823027e 100644 --- a/src/gui/gui-symbol-dragging2.js +++ b/src/gui/gui-symbol-dragging2.js @@ -1,10 +1,29 @@ import { getSvgSymbolTransform } from './gui-svg-symbols'; import { isMultilineLabel, toggleTextAlign, setMultilineAttribute, autoUpdateTextAnchor, applyDelta } from './gui-svg-labels'; import { error, internal } from './gui-core'; -import { translateDeltaDisplayCoords, getPointCoordsById, getDisplayCoordsById } from './gui-map-utils'; import { EventDispatcher } from './gui-events'; import { findNearestVertex, findVertexIds, getVertexCoords, setVertexCoords, vertexIsArcStart, vertexIsArcEnd } from '../paths/mapshaper-vertex-utils'; +function getDisplayCoordsById(id, layer, ext) { + var coords = getPointCoordsById(id, layer); + return ext.translateCoords(coords[0], coords[1]); +} + +function getPointCoordsById(id, layer) { + var coords = layer && layer.geometry_type == 'point' && layer.shapes[id]; + if (!coords || coords.length != 1) { + return null; + } + return coords[0]; +} + +function translateDeltaDisplayCoords(dx, dy, ext) { + var a = ext.translatePixelCoords(0, 0); + var b = ext.translatePixelCoords(dx, dy); + return [b[0] - a[0], b[1] - a[1]]; +} + + export function SymbolDragging2(gui, ext, hit) { // var targetTextNode; // text node currently being dragged var dragging = false; From d66291428230be42fd0eb2b7f8b52edf29fa78f9 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 13 May 2021 09:53:53 -0400 Subject: [PATCH 034/891] v0.5.53 --- CHANGELOG.md | 3 ++ package-lock.json | 8 ++-- package.json | 4 +- src/crs/mapshaper-spherical-rotation.js | 55 +++++++++++++++++++++++++ www/modules.js | 2 +- 5 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 src/crs/mapshaper-spherical-rotation.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 64bdbc894..e1186f16a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.53 +* Fixed clipping area of nsper (Near Side Perspective). + v0.5.52 * Automatically clip content to an appropriate circle when applying most of the azimuthal projections (including stere,ortho,gnom,laea,nsper). * Added the -proj clip-angle= option to override the default clipping circle. diff --git a/package-lock.json b/package-lock.json index e0a19c020..5f54d1508 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.52", + "version": "0.5.53", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1475,9 +1475,9 @@ } }, "mproj": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.30.tgz", - "integrity": "sha512-l/aR8hyHB3ZIWgmsX41Dss+ItkV7JAsMdRe+uObt7icP5AptO1GQB9Pu/idog9XSHllGSurqUv8rbFq1cl6LuQ==", + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.32.tgz", + "integrity": "sha512-zYQ48xsihf84QPH6snBS4KYa5Y5bx5XwuZccgyoQVWfIdN53NAGQyfyvdz9XYvDADjI0ra8DKb3MjqMJnCxIIg==", "requires": { "geographiclib": "1.48.0", "rw": "~1.3.2" diff --git a/package.json b/package.json index 13a847dbf..991950305 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.52", + "version": "0.5.53", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", @@ -44,7 +44,7 @@ "d3-scale-chromatic": "^2.0.0", "flatbush": "^3.2.1", "iconv-lite": "0.4.24", - "mproj": "0.0.30", + "mproj": "0.0.32", "opn": "^5.3.0", "rw": "~1.3.3", "sync-request": "5.0.0" diff --git a/src/crs/mapshaper-spherical-rotation.js b/src/crs/mapshaper-spherical-rotation.js new file mode 100644 index 000000000..eda24513f --- /dev/null +++ b/src/crs/mapshaper-spherical-rotation.js @@ -0,0 +1,55 @@ +import { projectPointLayer, projectArcs } from '../commands/mapshaper-proj'; +import { layerHasPoints } from '../dataset/mapshaper-layer-utils'; +import { R2D, D2R } from '../geom/mapshaper-basic-geom'; + +export function rotateDatasetCoords(dataset, rotation) { + var proj = getRotationFunction(rotation); + dataset.layers.filter(layerHasPoints).forEach(function(lyr) { + projectPointLayer(lyr, proj); + }); + if (dataset.arcs) { + projectArcs(dataset.arcs, proj); + } +} + +function getRotationFunction(rotation) { + var a = (rotation[0] || 0) * D2R, + b = (rotation[1] || 0) * D2R, + c = (rotation[2] || 0) * D2R; + return function(lng, lat) { + return rotatePoint([lng, lat], a, b, c); + }; +} + +function rotatePoint(p, deltaLambda, deltaPhi, deltaGamma) { + p[0] *= D2R; + p[1] *= D2R; + if (deltaLambda != 0) rotateLambda(p, deltaLambda); + if (deltaPhi !== 0 || deltaGamma !== 0) { + rotatePhiGamma(p, deltaPhi, deltaGamma); + } + p[0] *= R2D; + p[1] *= R2D; + return p; +} + +function rotateLambda(p, deltaLambda) { + var lam = p[0] + deltaLambda; + if (lam > Math.PI) lam -= 2 * Math.PI; + else if (lam < -Math.PI) lam += 2 * Math.PI; + p[0] = lam; +} + +function rotatePhiGamma(p, deltaPhi, deltaGamma) { + var cosDeltaPhi = Math.cos(deltaPhi), + sinDeltaPhi = Math.sin(deltaPhi), + cosDeltaGamma = Math.cos(deltaGamma), + sinDeltaGamma = Math.sin(deltaGamma), + cosPhi = Math.cos(p[1]), + x = Math.cos(p[0]) * cosPhi, + y = Math.sin(p[0]) * cosPhi, + z = Math.sin(p[1]), + k = z * cosDeltaPhi + x * sinDeltaPhi; + p[0] = Math.atan2(y * cosDeltaGamma - k * sinDeltaGamma, x * cosDeltaPhi - z * sinDeltaPhi); + p[1] = Math.asin(k * cosDeltaGamma + y * sinDeltaGamma); +} diff --git a/www/modules.js b/www/modules.js index e8ac85bfc..658ff0d49 100644 --- a/www/modules.js +++ b/www/modules.js @@ -15651,7 +15651,7 @@ function pj_bertin1953(P) { xy.x *= 1 + d; } if (xy.y > 0) { - xy.x *= 1 + d / 1.5 * xy.x * xy.x; + xy.y *= 1 + d / 1.5 * xy.x * xy.x; } return xy; From d1400384ab0e6bd87c2c1684d8ff4e0614ee9d79 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 14 May 2021 11:56:02 -0400 Subject: [PATCH 035/891] Refactor --- src/cli/mapshaper-options.js | 12 ++ src/commands/mapshaper-proj.js | 6 +- src/commands/mapshaper-shape.js | 47 +++++++- src/crs/mapshaper-proj-info.js | 2 +- src/crs/mapshaper-proj-utils.js | 7 ++ src/crs/mapshaper-spherical-clipping.js | 14 +-- src/geom/mapshaper-antimeridian.js | 149 +++++++++++++++--------- src/geom/mapshaper-basic-geom.js | 1 + test/antimeridian-test.js | 65 +++++++++++ 9 files changed, 229 insertions(+), 74 deletions(-) create mode 100644 test/antimeridian-test.js diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 4349363f6..1d397e7e6 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1644,9 +1644,21 @@ export function getOptionParser() { //describe: 'radius of the circle in meters', type: 'number' }) + .option('radius-angle', { + //describe: 'radius of the circle in degrees', + type: 'number' + }) + .option('bbox', { + // describe: 'rectangle bounding box', + type: 'numbers' + }) .option('geometry', { //describe: 'polygon or polyline' }) + .option('rotation', { + // describe: 'two or three angles of rotation', + type: 'numbers' + }) .option('name', nameOpt); parser.command('subdivide') diff --git a/src/commands/mapshaper-proj.js b/src/commands/mapshaper-proj.js index 1d5747408..2262f2d2c 100644 --- a/src/commands/mapshaper-proj.js +++ b/src/commands/mapshaper-proj.js @@ -185,7 +185,7 @@ function cleanProjectedLayers(dataset) { // proj: function to project [x, y] point; should return null if projection fails // TODO: fatal error if no points project? -function projectPointLayer(lyr, proj) { +export function projectPointLayer(lyr, proj) { var errors = 0; editShapes(lyr.shapes, function(p) { var p2 = proj(p[0], p[1]); @@ -195,7 +195,7 @@ function projectPointLayer(lyr, proj) { return errors; } -function projectArcs(arcs, proj) { +export function projectArcs(arcs, proj) { var data = arcs.getVertexData(), xx = data.xx, yy = data.yy, @@ -212,7 +212,7 @@ function projectArcs(arcs, proj) { arcs.updateVertexData(data.nn, xx, yy, zz); } -function projectArcs2(arcs, proj) { +export function projectArcs2(arcs, proj) { return editArcs(arcs, onPoint); function onPoint(append, x, y, prevX, prevY, i) { var p = proj(x, y); diff --git a/src/commands/mapshaper-shape.js b/src/commands/mapshaper-shape.js index 288c7e368..203e995aa 100644 --- a/src/commands/mapshaper-shape.js +++ b/src/commands/mapshaper-shape.js @@ -2,9 +2,11 @@ import { importGeoJSON } from '../geojson/geojson-import'; import GeoJSON from '../geojson/geojson-common'; import { stop } from '../utils/mapshaper-logging'; import cmd from '../mapshaper-cmd'; -import { getDatasetCRS } from '../crs/mapshaper-projections'; +import { getDatasetCRS, getCRS } from '../crs/mapshaper-projections'; import { projectDataset } from '../commands/mapshaper-proj'; import { getCircleGeoJSON } from '../buffer/mapshaper-point-buffer'; +import { getCircleRadiusFromAngle } from '../crs/mapshaper-proj-utils'; +import { rotateDatasetCoords } from '../crs/mapshaper-spherical-rotation'; cmd.shape = function(targetDataset, opts) { var geojson, dataset; @@ -12,21 +14,58 @@ cmd.shape = function(targetDataset, opts) { geojson = makeShapeFromCoords(opts); } else if (opts.type == 'circle') { geojson = makeCircle(opts); + } else if (opts.type == 'rectangle' && opts.bbox) { + geojson = getRectangleGeoJSON(opts); } else { stop('Missing coordinates parameter'); } // TODO: project shape if targetDataset is projected dataset = importGeoJSON(geojson, {}); - dataset.layers[0].name = opts.name || 'shape'; + if (opts.rotation) { + rotateDatasetCoords(dataset, opts.rotation); + } + dataset.layers[0].name = opts.name || opts.type || 'shape'; return dataset; }; +function getRectangleGeoJSON(opts) { + var bbox = opts.bbox, + xmin = bbox[0], + ymin = bbox[1], + xmax = bbox[2], + ymax = bbox[3], + interval = 0.5, + coords = [], + type = opts.geometry == 'polyline' ? 'LineString' : 'Polygon'; + addSide(xmin, ymin, xmin, ymax); + addSide(xmin, ymax, xmax, ymax); + addSide(xmax, ymax, xmax, ymin); + addSide(xmax, ymin, xmin, ymin); + coords.push([xmin, ymin]); + return { + type: type, + coordinates: type == 'Polygon' ? [coords] : coords + }; + + function addSide(x1, y1, x2, y2) { + var dx = x2 - x1, + dy = y2 - y1, + n = Math.ceil(Math.max(Math.abs(dx) / interval, Math.abs(dy) / interval)), + xint = dx / n, + yint = dy / n; + for (var i=0; i 0 === false) { + if (opts.radius > 0 === false && opts.radius_angle > 0 === false) { stop('Missing required radius parameter.'); } var cp = opts.center || [0, 0]; - return getCircleGeoJSON(cp, opts.radius, null, {geometry_type : opts.geometry || 'polygon'}); + var radius = opts.radius || getCircleRadiusFromAngle(getCRS('wgs84'), opts.radius_angle); + return getCircleGeoJSON(cp, radius, null, {geometry_type : opts.geometry || 'polygon'}); } function makeShapeFromCoords(opts) { diff --git a/src/crs/mapshaper-proj-info.js b/src/crs/mapshaper-proj-info.js index 046e68a92..e8613bf85 100644 --- a/src/crs/mapshaper-proj-info.js +++ b/src/crs/mapshaper-proj-info.js @@ -26,7 +26,7 @@ export function getDefaultClipBBox(P) { }[getCrsSlug(P)] || null; } -export function isClippedAzimuthalProjection(P) { +export function isCircleClippedProjection(P) { return inList(P, 'stere,sterea,ups,ortho,gnom,laea,nsper,tpers'); } diff --git a/src/crs/mapshaper-proj-utils.js b/src/crs/mapshaper-proj-utils.js index 2c8f10a9d..c13f36fa2 100644 --- a/src/crs/mapshaper-proj-utils.js +++ b/src/crs/mapshaper-proj-utils.js @@ -3,3 +3,10 @@ export function getSemiMinorAxis(P) { return P.a * Math.sqrt(1 - (P.es || 0)); } +export function getCircleRadiusFromAngle(P, angle) { + // Using semi-minor axis radius, to prevent overflowing projection bounds + // when clipping up to the edge of the projectable area + // TODO: improve (this just gives a safe minimum distance, not the best distance) + // TODO: modify point buffer function to use angle + ellipsoidal geometry + return angle * Math.PI / 180 * getSemiMinorAxis(P); +} diff --git a/src/crs/mapshaper-spherical-clipping.js b/src/crs/mapshaper-spherical-clipping.js index 801fcf203..966578111 100644 --- a/src/crs/mapshaper-spherical-clipping.js +++ b/src/crs/mapshaper-spherical-clipping.js @@ -1,11 +1,11 @@ import { - isClippedAzimuthalProjection, + isCircleClippedProjection, getDefaultClipAngle, isClippedCylindricalProjection, getDefaultClipBBox, } from '../crs/mapshaper-proj-info'; import { - getSemiMinorAxis + getSemiMinorAxis, getCircleRadiusFromAngle } from '../crs/mapshaper-proj-utils'; import { isLatLngCRS, getDatasetCRS } from '../crs/mapshaper-projections'; import { getCircleGeoJSON } from '../buffer/mapshaper-point-buffer'; @@ -20,7 +20,7 @@ import { Bounds } from '../geom/mapshaper-bounds'; export function preProjectionClip(dataset, src, dest, opts) { var clipped = false; if (!isLatLngCRS(src) || opts.no_clip) return false; - if (isClippedAzimuthalProjection(dest) || opts.clip_angle) { + if (isCircleClippedProjection(dest) || opts.clip_angle) { clipped = clipToCircle(dataset, src, dest, opts); } if (isClippedCylindricalProjection(dest) || opts.clip_bbox) { @@ -34,7 +34,7 @@ export function preProjectionClip(dataset, src, dest, opts) { } export function getProjectionOutline(src, dest, opts) { - if (!isClippedAzimuthalProjection(dest)) return null; + if (!isCircleClippedProjection(dest)) return null; return getClipShapeGeoJSON(src, dest, opts); } @@ -74,9 +74,5 @@ function getProjCenter(P) { // Convert a clip angle to a distance in meters function getClippingRadius(P, angle) { - // Using semi-minor axis radius, to prevent overflowing projection bounds - // when clipping up to the edge of the projectable area - // TODO: improve (this just gives a safe minimum distance, not the best distance) - // TODO: modify point buffer function to use angle + ellipsoidal geometry - return angle * Math.PI / 180 * getSemiMinorAxis(P); + return getCircleRadiusFromAngle(P, angle); } diff --git a/src/geom/mapshaper-antimeridian.js b/src/geom/mapshaper-antimeridian.js index b8436544a..91c70dcc6 100644 --- a/src/geom/mapshaper-antimeridian.js +++ b/src/geom/mapshaper-antimeridian.js @@ -1,6 +1,7 @@ import { stop } from '../utils/mapshaper-logging'; import { getSphericalPathArea2 } from '../geom/mapshaper-polygon-geom'; import { PointIter } from '../paths/mapshaper-shape-iter'; +import utils from '../utils/mapshaper-utils'; // Removes one or two antimeridian crossings from a circular ring // TODO: handle more complicated rings, with any number of crosses. @@ -8,26 +9,19 @@ import { PointIter } from '../paths/mapshaper-shape-iter'; // TODO: handle edge case: a single vertex touches the antimeridian without crossing // TODO: handle edge case: path coordinates exceed the standard lat-long range // -// ring: a closed path of [x,y] points. Assumes a space-enclosing ring with CW winding. +// ring: a path of [x,y] points. // type: 'polygon' or 'polyline' +// 'polygon' Assumes a space-enclosing ring with CW winding. // Returns MultiPolygon or MultiLineString coordinates array export function removeAntimeridianCrosses(ring, type) { - var part = []; - var parts = [part]; - var p, pp; + var parts = splitPathAtAntimeridian(ring); - for (var i=0, n=ring.length; i0 && Math.abs(pp[0] - p[0]) > 180) { - parts.push(part = []); - } - part.push(p); - pp = p; - } if (type == 'polyline') { - return dividePolylines(parts); + return parts; // MultiLineString coords } - if (parts.length == 1) { + + // case: polygon does not intersect the antimeridian + if (parts.length == 1 && !isAntimeridanPoint(parts[0][0])) { // TODO: this test should not be needed when processing small circles // (could affect performance when buffering many points) if (ringArea(ring) < 0) { @@ -38,37 +32,84 @@ export function removeAntimeridianCrosses(ring, type) { } return [parts]; } - if (parts.length == 2) { + + // Now we can assume that the first and last point of the split-apart path is 180 or -180 + if (parts.length == 1) { return removeOneCross(parts); } - if (parts.length == 3) { + if (parts.length == 2) { return removeTwoCrosses(parts, ringArea(ring)); } stop('Unexpected geometry of an antimeridan-crossing polygon ring.'); } -function ringArea(ring) { - var iter = new PointIter(ring); - return getSphericalPathArea2(iter); +function addSubPath(paths, path) { + if (path.length > 1) paths.push(path); } -function dividePolylines(parts) { - var a, b, x1, x2, y; - for (var i=1; i0 && Math.abs(pp[0] - p[0]) > 180) { + y = planarIntercept(pp, p); + addIntersectionPoint(part, pp, y); + addSubPath(parts, part); + part = []; + addIntersectionPoint(part, p, y); + } + part.push(p); + pp = p; } - if (parts.length > 1) { - parts[0] = parts.pop().concat(parts[0]); + addSubPath(parts, part); + + // join first and last parts of a split-apart ring, so that the first part + // originates at the antimeridian + if (closed && parts.length > 1 && !isAntimeridanPoint(firstPoint)) { + part = parts.pop(); + part.pop(); // remove duplicate point + parts[0] = part.concat(parts[0]); } return parts; } +export function getSortedIntersections(parts) { + var values = parts.map(function(p) { + return p[0][1]; + }); + return genericSort(values, true); +} + +function samePoint(a, b) { + return a[0] === b[0] && a[1] === b[1]; +} + +function isAntimeridanPoint(p) { + return p[0] == 180 || p[0] == -180; +} + +function addIntersectionPoint(part, p, yint) { + var xint = p[0] < 0 ? -180 : 180; + if (!isAntimeridanPoint(p)) { // don't a point if p is already on the antimeridian + part.push([xint, yint]); + } +} + +function ringArea(ring) { + var iter = new PointIter(ring); + return getSphericalPathArea2(iter); +} + // Ring crosses twice... // Returns one ring containing both poles or two rings split across // the antimeridian and including neither pole. @@ -76,16 +117,16 @@ function removeTwoCrosses(parts, ringArea) { var a = lastEl(parts[0]), b = firstEl(parts[1]), c = lastEl(parts[1]), - d = firstEl(parts[2]), - y1 = planarIntercept(a, b), - y2 = planarIntercept(c, d), - x1 = a[0] < 0 ? -180 : 180, - x2 = x1 < 0 ? 180 : -180, - ring1, ring2, pole1, pole2; - if (ringArea < 1) { - ring1 = parts[0].concat([[x1, y1], [x1, y2]], parts[2]); - ring2 = parts[1].concat([[x2, y2], [x2, y1], b]); - return [[dedup(ring1)], [dedup(ring2)]]; + d = firstEl(parts[0]), + y1 = a[1], + y2 = c[1], + x1 = a[0], + x2 = b[0], + ring, pole1, pole2; + if (ringArea < 0) { + parts[0].push(d); + parts[1].push(b); + return [[parts[0]], [parts[1]]]; } if (y1 > y2) { pole1 = 90; @@ -94,12 +135,9 @@ function removeTwoCrosses(parts, ringArea) { pole1 = -90; pole2 = 90; } - ring1 = parts[0].concat( - [[x1, y1], [x1, pole1], [x2, pole1], [x2, y1]], - parts[1], - [[x2, y2], [x2, pole2], [x1, pole2], [x1, y2]], - parts[2]); - return [[dedup(ring1)]]; + ring = parts[0].concat( + [[x1, pole1], [x2, pole1]], parts[1], [[x2, pole2], [x1, pole2], d]); + return [[dedup(ring)]]; } // duplicate points occur if a vertex is on the antimeridan @@ -114,18 +152,15 @@ function dedup(ring) { // Ring contains a pole. // Returns one ring including n or s pole line. function removeOneCross(parts) { - var p1 = lastEl(parts[0]); - var p2 = firstEl(parts[1]); - var dx = p2[0] - p1[0]; // pos: crosses antimeridian w->e, neg: e->w - var y = planarIntercept(p1, p2); + var ring = parts[0]; + var lastX = lastEl(ring)[0]; + var firstX = firstEl(ring)[0]; + // lastX === -180: crosses antimeridian w->e, 180: e->w // if ring crosses w->e, go through n pole // (this assumes that ring has CW winding / is not a hole) - var ypole = dx > 0 ? 90 : -90; - var coords = dx > 0 ? - [[-180, y], [-180, ypole], [180, ypole], [180, y]] : - [[180, y], [180, ypole], [-180, ypole], [-180, y]]; - var ring = parts[0].concat(coords, parts[1]); - return [[dedup(ring)]]; // multipolygon format + var poleY = lastX === -180 ? 90 : -90; + ring.push([lastX, poleY], [firstX, poleY], ring[0]); + return [[ring]]; // multipolygon format } function lastEl(arr) { diff --git a/src/geom/mapshaper-basic-geom.js b/src/geom/mapshaper-basic-geom.js index 3058d3a88..a2614213e 100644 --- a/src/geom/mapshaper-basic-geom.js +++ b/src/geom/mapshaper-basic-geom.js @@ -2,6 +2,7 @@ // also consider using ellipsoidal formulas when appropriate export var R = 6378137; export var D2R = Math.PI / 180; +export var R2D = 180 / Math.PI; // Equirectangular projection export function degreesToMeters(deg) { diff --git a/test/antimeridian-test.js b/test/antimeridian-test.js new file mode 100644 index 000000000..c220862a7 --- /dev/null +++ b/test/antimeridian-test.js @@ -0,0 +1,65 @@ + +import { splitPathAtAntimeridian } from '../src/geom/mapshaper-antimeridian'; +var assert = require('assert'); + +describe('mapshaper-antimeridian.js', function () { + + describe('splitPathAtAntimeridian()', function () { + + it('ring is split, starts away from antimeridian', function () { + var input = [[-179, 1], [-179, -1], [179, -1], [179, 1], [-179, 1]]; + var expect = [ + [[-180, 1], [-179, 1], [-179, -1], [-180, -1]], + [[180, -1], [179, -1], [179, 1], [180, 1]]]; + assert.deepEqual(splitPathAtAntimeridian(input), expect); + }) + + it('ring is split, starts on antimeridian 1', function () { + var input = [[-180, 1], [-179, 0], [-180, -1], [179, 0], [-180, 1]]; + var expect = [ + [[-180, 1], [-179, 0], [-180, -1]], + [[180, -1], [179, 0], [180, 1]]]; + assert.deepEqual(splitPathAtAntimeridian(input), expect); + }) + + it('ring is split, starts on antimeridian 2', function() { + var input = [[180, 1], [-179, 0], [-180, -1], [179, 0], [180, 1]]; + var expect = [ + [[-180, 1], [-179, 0], [-180, -1]], + [[180, -1], [179, 0], [180, 1]]]; + assert.deepEqual(splitPathAtAntimeridian(input), expect); + }) + + it('ring touches antimeridian once at first vertex', function() { + var input = [[180, 0], [179, -1], [179, 1], [180, 0]]; + var expect = [[[180, 0], [179, -1], [179, 1], [180, 0]]]; + assert.deepEqual(splitPathAtAntimeridian(input), expect); + }) + + it ('ring touches antimeridian once at first vertex 2', function() { + var input = [[-180, 0], [179, -1], [179, 1], [-180, 0]]; + var expect = [[[180, 0], [179, -1], [179, 1], [180, 0]]]; + assert.deepEqual(splitPathAtAntimeridian(input), expect); + }) + + it ('ring touches antimeridian once in middle', function() { + var input = [[179, 1], [180, 0], [179, -1], [179, 1]]; + var expect = [[[179, 1], [180, 0], [179, -1], [179, 1]]]; + assert.deepEqual(splitPathAtAntimeridian(input), expect); + }) + + it ('ring touches antimeridian once in middle 2', function() { + var input = [[179, 1], [-180, 0], [179, -1], [179, 1]]; + // var expect = [[[179, 1], [180, 0], [179, -1], [179, 1]]]; + var expect = [[[180, 0], [179, -1], [179, 1], [180, 0]]]; + assert.deepEqual(splitPathAtAntimeridian(input), expect); + }) + + it ('ring contains a segment that parallels the antimeridian', function() { + // var input = [[180, 1], + }) + + }) + + +}) \ No newline at end of file From 808c681dcee044169ed91130b0757aefe2073b99 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 14 May 2021 11:56:28 -0400 Subject: [PATCH 036/891] Fix --- src/geom/mapshaper-antimeridian.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/geom/mapshaper-antimeridian.js b/src/geom/mapshaper-antimeridian.js index 91c70dcc6..8ad0522f6 100644 --- a/src/geom/mapshaper-antimeridian.js +++ b/src/geom/mapshaper-antimeridian.js @@ -87,7 +87,7 @@ export function getSortedIntersections(parts) { var values = parts.map(function(p) { return p[0][1]; }); - return genericSort(values, true); + return utils.genericSort(values, true); } function samePoint(a, b) { From c4b87e20526d5dd99fbd5ca747392eeada975cf4 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 14 May 2021 14:12:20 -0400 Subject: [PATCH 037/891] Fix for issue #485 --- src/utils/mapshaper-filename-utils.js | 9 +++++---- test/data/issues/485_wildcard/three_points.geojson | 3 +++ test/filename-utils-test.js | 11 ++++++++++- test/issue-485-test.js | 14 ++++++++++++++ three_points.json | 5 +++++ 5 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 test/data/issues/485_wildcard/three_points.geojson create mode 100644 test/issue-485-test.js create mode 100644 three_points.json diff --git a/src/utils/mapshaper-filename-utils.js b/src/utils/mapshaper-filename-utils.js index cac1f3a82..6e22c381f 100644 --- a/src/utils/mapshaper-filename-utils.js +++ b/src/utils/mapshaper-filename-utils.js @@ -19,12 +19,13 @@ export function parseLocalPath(path) { parts = path.split(sep), lastPart = parts.pop(), // try to match typical extensions but reject directory names with dots - extRxp = /\.([a-z][a-z0-9]*)$/i; + extRxp = /\.([a-z][a-z0-9]*)$/i, + extMatch = extRxp.test(lastPart) ? extRxp.exec(lastPart)[0] : ''; - if (extRxp.test(lastPart)) { + if (extMatch || lastPart.includes('*')) { obj.filename = lastPart; - obj.extension = extRxp.exec(lastPart)[1]; - obj.basename = lastPart.slice(0, lastPart.length - obj.extension.length - 1); + obj.extension = extMatch ? extMatch.slice(1) : ''; + obj.basename = lastPart.slice(0, lastPart.length - extMatch.length); obj.directory = parts.join(sep); } else if (!lastPart) { // path ends with separator obj.directory = parts.join(sep); diff --git a/test/data/issues/485_wildcard/three_points.geojson b/test/data/issues/485_wildcard/three_points.geojson new file mode 100644 index 000000000..2077aff2b --- /dev/null +++ b/test/data/issues/485_wildcard/three_points.geojson @@ -0,0 +1,3 @@ +{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"scalerank":2,"featurecla":"waterfall","name":"Niagara Falls","comment":"","name_alt":"","lat_y":43.087653,"long_x":-79.044073,"region":"North America","subregion":""},"geometry":{"type":"Point","coordinates":[-79.04411780507252,43.08771393436908]}}, +{"type":"Feature","properties":{"scalerank":2,"featurecla":"waterfall","name":"Salto Angel","comment":"","name_alt":"Angel Falls","lat_y":5.686836,"long_x":-62.061848,"region":"South America","subregion":""},"geometry":{"type":"Point","coordinates":[-62.06181800038502,5.686896063275327]}}, +{"type":"Feature","properties":{"scalerank":2,"featurecla":"waterfall","name":"Iguazu Falls","comment":"","name_alt":"","lat_y":-25.568265,"long_x":-54.582842,"region":"South America","subregion":""},"geometry":{"type":"Point","coordinates":[-54.58299719960377,-25.568291925005923]}}]} \ No newline at end of file diff --git a/test/filename-utils-test.js b/test/filename-utils-test.js index 0eb7de7c8..bf28ea2ce 100644 --- a/test/filename-utils-test.js +++ b/test/filename-utils-test.js @@ -60,7 +60,7 @@ describe('mapshaper-filename-utils.js', function () { }) }) - it("handle wildcard", function () { + it("handle wildcard + extension", function () { assert.deepEqual(api.internal.parseLocalPath("shapefiles/*.shp"), { extension: "shp", basename: "*", @@ -69,6 +69,15 @@ describe('mapshaper-filename-utils.js', function () { }) }) + it("handle wildcard w/o extension", function () { + assert.deepEqual(api.internal.parseLocalPath("shapefiles/*"), { + extension: "", + basename: "*", + filename: "*", + directory: "shapefiles" + }) + }) + it("handle Windows paths", function () { assert.deepEqual(api.internal.parseLocalPath("shapefiles\\*.shp"), { extension: "shp", diff --git a/test/issue-485-test.js b/test/issue-485-test.js new file mode 100644 index 000000000..45b460c24 --- /dev/null +++ b/test/issue-485-test.js @@ -0,0 +1,14 @@ +var api = require('..'), + assert = require('assert'); + +describe('Issue #485 (Error importing files with * wildcard)', function () { + + it('match files with *', function(done) { + var cmd = '-i test/data/issues/485_wildcard/* -o'; + api.applyCommands(cmd, {}, function(err, out) { + var json = JSON.parse(out['three_points.json']); + assert.equal(json.features.length, 3); + done(); + }) + }); +}); diff --git a/three_points.json b/three_points.json new file mode 100644 index 000000000..4f5f82d78 --- /dev/null +++ b/three_points.json @@ -0,0 +1,5 @@ +{"type":"FeatureCollection", "features": [ +{"type":"Feature","geometry":{"type":"Point","coordinates":[-79.04411780507252,43.08771393436908]},"properties":{"scalerank":2,"featurecla":"waterfall","name":"Niagara Falls","comment":"","name_alt":"","lat_y":43.087653,"long_x":-79.044073,"region":"North America","subregion":""}}, +{"type":"Feature","geometry":{"type":"Point","coordinates":[-62.06181800038502,5.686896063275327]},"properties":{"scalerank":2,"featurecla":"waterfall","name":"Salto Angel","comment":"","name_alt":"Angel Falls","lat_y":5.686836,"long_x":-62.061848,"region":"South America","subregion":""}}, +{"type":"Feature","geometry":{"type":"Point","coordinates":[-54.58299719960377,-25.568291925005923]},"properties":{"scalerank":2,"featurecla":"waterfall","name":"Iguazu Falls","comment":"","name_alt":"","lat_y":-25.568265,"long_x":-54.582842,"region":"South America","subregion":""}} +]} \ No newline at end of file From 2cb6dc62c02044ff352f27da4758a76468c858bc Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 14 May 2021 14:14:56 -0400 Subject: [PATCH 038/891] v0.5.54 --- CHANGELOG.md | 3 +++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1186f16a..c06d303d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.54 +* Fix for issue #485 (error when using * wildcard to match all files in a directory) + v0.5.53 * Fixed clipping area of nsper (Near Side Perspective). diff --git a/package-lock.json b/package-lock.json index 5f54d1508..1a4a89ed4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.53", + "version": "0.5.54", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 991950305..7db0a2061 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.53", + "version": "0.5.54", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From bdabf38d46b69af45349bbe7941729826e2d6312 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 20 May 2021 11:47:07 -0400 Subject: [PATCH 039/891] Add support for spherical rotation; improve -proj and -graticule --- src/cli/mapshaper-options.js | 11 ++ src/cli/mapshaper-run-command.js | 8 +- src/clipping/mapshaper-clip-utils.js | 19 --- src/clipping/mapshaper-overlay-utils.js | 9 +- src/commands/mapshaper-clip-erase.js | 9 ++ src/commands/mapshaper-graticule.js | 124 ++++++++++-------- src/commands/mapshaper-proj.js | 8 +- src/commands/mapshaper-rectangle.js | 12 +- src/commands/mapshaper-rotate.js | 49 ++++++++ src/crs/mapshaper-densify.js | 59 ++++++++- src/crs/mapshaper-proj-extents.js | 119 ++++++++++++++++++ src/crs/mapshaper-proj-info.js | 70 +++++------ src/crs/mapshaper-projections.js | 4 + src/crs/mapshaper-spherical-clipping.js | 79 ++---------- src/crs/mapshaper-spherical-cutting.js | 13 +- src/crs/mapshaper-spherical-rotation.js | 70 +++++++---- src/dataset/mapshaper-dataset-editor.js | 61 +++++++++ src/geom/mapshaper-antimeridian.js | 160 ++++++++++++++---------- test/antimeridian-test.js | 1 + 19 files changed, 605 insertions(+), 280 deletions(-) create mode 100644 src/commands/mapshaper-rotate.js create mode 100644 src/crs/mapshaper-proj-extents.js create mode 100644 src/dataset/mapshaper-dataset-editor.js diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 1d397e7e6..a14ca17a0 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1596,6 +1596,17 @@ export function getOptionParser() { describe: 'JS expression to run after the module loads' }); + parser.command('rotate') + // .describe('apply d3-style 3-axis rotation to a lat-long dataset') + .option('rotation', { + // describe: 'two or three angles of rotation', + DEFAULT: true, + type: 'numbers' + }) + .option('invert', { + type: 'flag' + }); + parser.command('run') .describe('create commands on-the-fly and run them') .option('include', { diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index 707db59ea..d99221a0f 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -56,6 +56,7 @@ import '../commands/mapshaper-proj'; import '../commands/mapshaper-rectangle'; import '../commands/mapshaper-rename-layers'; import '../commands/mapshaper-require'; +import '../commands/mapshaper-rotate'; import '../commands/mapshaper-run'; import '../commands/mapshaper-scalebar'; import '../commands/mapshaper-simplify'; @@ -104,7 +105,7 @@ export function runCommand(command, catalog, cb) { // TODO: check that combine_layers is only used w/ GeoJSON output targets = catalog.findCommandTargets(opts.target || opts.combine_layers && '*'); - } else if (name == 'info' || name == 'proj' || name == 'drop' || name == 'target') { + } else if (name == 'rotate' || name == 'info' || name == 'proj' || name == 'drop' || name == 'target') { // these commands accept multiple target datasets targets = catalog.findCommandTargets(opts.target); @@ -326,6 +327,11 @@ export function runCommand(command, catalog, cb) { } else if (name == 'require') { cmd.require(targets, opts); + } else if (name == 'rotate') { + targets.forEach(function(targ) { + cmd.rotate(targ.dataset, opts); + }); + } else if (name == 'run') { cmd.run(targets, catalog, opts, done); return; diff --git a/src/clipping/mapshaper-clip-utils.js b/src/clipping/mapshaper-clip-utils.js index 466bf4a42..e69de29bb 100644 --- a/src/clipping/mapshaper-clip-utils.js +++ b/src/clipping/mapshaper-clip-utils.js @@ -1,19 +0,0 @@ -import { clipLayers } from '../commands/mapshaper-clip-erase'; -import { importGeoJSON } from '../geojson/geojson-import'; - -// type: 'clip' or 'erase' -export function clipLayersByGeoJSON(layers, dataset, geojson, typeArg) { - var clipDataset = importGeoJSON(geojson, {}); - var type = typeArg || 'clip'; - var clip = { - layer: clipDataset.layers[0], - dataset: clipDataset - }; - var outputLayers = clipLayers(layers, clip, dataset, type, {}); - layers.forEach(function(lyr, i) { - var lyr2 = outputLayers[i]; - lyr.shapes = lyr2.shapes; - lyr.data = lyr2.data; - }); - -} \ No newline at end of file diff --git a/src/clipping/mapshaper-overlay-utils.js b/src/clipping/mapshaper-overlay-utils.js index fcf50e0ca..ac4c0d84e 100644 --- a/src/clipping/mapshaper-overlay-utils.js +++ b/src/clipping/mapshaper-overlay-utils.js @@ -21,11 +21,14 @@ export function mergeLayersForOverlay(targetLayers, targetDataset, clipSrc, opts if (bbox) { clipDataset = convertClipBounds(bbox); clipLyr = clipDataset.layers[0]; - } else if (clipSrc) { + } else if (!clipSrc) { + stop("Command requires a source file, layer id or bbox"); + } else if (clipSrc.layer && clipSrc.dataset) { clipLyr = clipSrc.layer; clipDataset = utils.defaults({layers: [clipLyr]}, clipSrc.dataset); - } else { - stop("Command requires a source file, layer id or bbox"); + } else if (clipSrc.layers && clipSrc.layers.length == 1) { + clipLyr = clipSrc.layers[0]; + clipDataset = clipSrc; } if (targetDataset.arcs != clipDataset.arcs) { // using external dataset -- need to merge arcs diff --git a/src/commands/mapshaper-clip-erase.js b/src/commands/mapshaper-clip-erase.js index 8c2d8892a..c7a32c36b 100644 --- a/src/commands/mapshaper-clip-erase.js +++ b/src/commands/mapshaper-clip-erase.js @@ -37,6 +37,15 @@ cmd.sliceLayer = function(targetLyr, src, dataset, opts) { return cmd.sliceLayers([targetLyr], src, dataset, opts); }; +export function clipLayersInPlace(layers, clipSrc, dataset, type, opts) { + var outputLayers = clipLayers(layers, clipSrc, dataset, type, opts); + layers.forEach(function(lyr, i) { + var lyr2 = outputLayers[i]; + lyr.shapes = lyr2.shapes; + lyr.data = lyr2.data; + }); +} + // @clipSrc: layer in @dataset or filename // @type: 'clip' or 'erase' export function clipLayers(targetLayers, clipSrc, targetDataset, type, opts) { diff --git a/src/commands/mapshaper-graticule.js b/src/commands/mapshaper-graticule.js index a5f545336..b7cfb3b05 100644 --- a/src/commands/mapshaper-graticule.js +++ b/src/commands/mapshaper-graticule.js @@ -1,48 +1,68 @@ import { importGeoJSON } from '../geojson/geojson-import'; import { projectDataset } from '../commands/mapshaper-proj'; -import { getDatasetCRS, getCRS } from '../crs/mapshaper-projections'; -import { isRotatedNormalProjection, isAzimuthal } from '../crs/mapshaper-proj-info'; +import { getDatasetCRS, getCRS, isLatLngDataset } from '../crs/mapshaper-projections'; +import { isMeridianBounded, getBoundingMeridian } from '../crs/mapshaper-proj-info'; import { getAntimeridian } from '../geom/mapshaper-latlon'; -import { getProjectionOutline } from '../crs/mapshaper-spherical-clipping'; +import { getOutlineDataset } from '../crs/mapshaper-proj-extents'; +import { densifyPathByInterval } from '../crs/mapshaper-densify'; import { stop } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import cmd from '../mapshaper-cmd'; +import { mergeDatasets } from '../dataset/mapshaper-merging'; +import { buildTopology } from '../topology/mapshaper-topology'; +import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; +import { convertBboxToGeoJSON } from '../commands/mapshaper-rectangle'; +import { cleanLayers } from '../commands/mapshaper-clean'; +import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; cmd.graticule = function(dataset, opts) { - var graticule, dest, src; - if (dataset) { + var graticule, dest; + if (dataset && !isLatLngDataset(dataset)) { // project graticule to match dataset dest = getDatasetCRS(dataset); - src = getCRS('wgs84'); if (!dest) stop("Coordinate system is unknown, unable to create a graticule"); - graticule = importGeoJSON(createGraticuleForProjection(src, dest, opts)); - projectDataset(graticule, src, dest, {no_clip: false}); // TODO: densify? + graticule = createProjectedGraticule(dest, opts); } else { - graticule = importGeoJSON(createGraticule(getCRS('wgs84'), opts)); + graticule = createUnprojectedGraticule(opts); } graticule.layers[0].name = 'graticule'; return graticule; }; -function createGraticuleForProjection(src, dest, opts) { - var geojson = createGraticule(dest, opts); - // inset the outline by 1 meter, so it doesn't get clipped - var outline = getProjectionOutline(src, dest, {inset: 1, geometry_type: 'polyline'}); +function createUnprojectedGraticule(opts) { + var src = getCRS('wgs84'); + var graticule = importGeoJSON(createGraticule(src, false, opts)); + return graticule; +} + +function createProjectedGraticule(dest, opts) { + var src = getCRS('wgs84'); + var outline = getOutlineDataset(src, dest, {inset: 0, geometry_type: 'polyline'}); + var graticule = importGeoJSON(createGraticule(dest, !!outline, opts)); + projectDataset(graticule, src, dest, {no_clip: false}); // TODO: densify? if (outline) { - geojson.features.push({ - type: 'Feature', - geometry: outline, - properties: { - type: 'outline', - value: null - } - }); + projectDataset(outline, src, dest, {no_clip: true}); + graticule = addOutlineToGraticule(graticule, outline); } - return geojson; + buildTopology(graticule); // needed for cleaning to work + cleanLayers(graticule.layers, graticule, {verbose: false}); + return graticule; } -// create graticule as a dataset -function createGraticule(P, opts) { +function addOutlineToGraticule(graticule, outline) { + var merged = mergeDatasets([graticule, outline]); + var lyr = merged.layers[0]; + var records = lyr.data.getRecords(); + merged.layers[1].shapes.forEach(function(shp) { + lyr.shapes.push(shp); + records.push({type: 'outline', value: null}); + }); + return merged; +} + +// Create graticule as a polyline dataset +// +function createGraticule(P, outlined, opts) { var interval = opts.interval || 10; if (![5,10,15,30,45].includes(interval)) stop('Invalid interval:', interval); var lon0 = P.lam0 * 180 / Math.PI; @@ -50,39 +70,41 @@ function createGraticule(P, opts) { var xstep = interval; var ystep = interval; var xstepMajor = 90; - var antimeridian = getAntimeridian(lon0); - var boundedByAntimeridian = !isAzimuthal(P); // TODO: improve - var isRotated = lon0 != 0; - var xn = Math.round(360 / xstep) + (isRotated ? 0 : 1); + var xn = Math.round(360 / xstep); var yn = Math.round(180 / ystep) + 1; - var xx = utils.range(xn, -180, xstep); + var xx = utils.range(xn, -180 + xstep, xstep); var yy = utils.range(yn, -90, ystep); var meridians = []; var parallels = []; - + var edgeMeridians = isMeridianBounded(P) ? getEdgeMeridians(P) : null; xx.forEach(function(x) { - if (boundedByAntimeridian && isRotated && Math.abs(x - antimeridian) < xstep / 5) { - // skip meridians that are close to the enclosure of a rotated graticule - return null; + if (edgeMeridians && (tooClose(x, edgeMeridians[0]) || tooClose(x, edgeMeridians[1]))) { + return; } createMeridian(x, x % xstepMajor === 0); }); - if (isRotated && boundedByAntimeridian) { + + if (edgeMeridians && !outlined) { // add meridian lines that will appear on the left and right sides of the // projected graticule - // offset the lines by a larger amount than the width of any cuts - createMeridian(antimeridian - 2e-8, true); - createMeridian(antimeridian + 2e-8, true); + createMeridian(edgeMeridians[0], true); + createMeridian(edgeMeridians[1], true); } + yy.forEach(function(y) { createParallel(y); }); + var geojson = { type: 'FeatureCollection', features: meridians.concat(parallels) }; return geojson; + function tooClose(a, b) { + return Math.abs(a - b) < interval / 5; + } + function createMeridian(x, extended) { var y0 = ystep <= 15 ? ystep : 0; createMeridianPart(x, -90 + y0, 90 - y0); @@ -95,26 +117,28 @@ function createGraticule(P, opts) { } function createMeridianPart(x, ymin, ymax) { - var coords = []; - for (var y = ymin; y < ymax; y += precision) { - coords.push([x, y]); - } - coords.push([x, ymax]); - meridians.push(graticuleFeature(coords, {type: 'meridian', value: x})); + var coords = densifyPathByInterval([[x, ymin], [x, ymax]], precision); + meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord(x)})); } function createParallel(y) { - var coords = []; - var xmin = -180; - var xmax = 180; - for (var x = xmin; x < xmax; x += precision) { - coords.push([x, y]); - } - coords.push([xmax, y]); + var coords = densifyPathByInterval([[-180, y], [180, y]], precision); parallels.push(graticuleFeature(coords, {type: 'parallel', value: y})); } } +// remove tiny offsets +function roundCoord(x) { + return +x.toFixed(3) || 0; +} + +function getEdgeMeridians(P) { + var lon = getBoundingMeridian(P); + // offs must be larger than gutter width in mapshaper-spherical-cutting.js + var offs = 2e-8; + return lon == 180 ? [-180, 180] : [lon - offs, lon + offs]; +} + function graticuleFeature(coords, o) { return { type: 'Feature', diff --git a/src/commands/mapshaper-proj.js b/src/commands/mapshaper-proj.js index 2262f2d2c..9fccac542 100644 --- a/src/commands/mapshaper-proj.js +++ b/src/commands/mapshaper-proj.js @@ -175,9 +175,13 @@ function cleanProjectedLayers(dataset) { // clean options: force a topology update (by default, this only happens when // vertices change during cleaning, but reprojection can require a topology update // even if clean does not change vertices) - var cleanOpts = {rebuild_topology: true, no_arc_dissolve: true, verbose: false}; + var cleanOpts = { + rebuild_topology: true, + no_arc_dissolve: true, + quiet: true, + verbose: false}; cleanLayers(polygonLayers, dataset, cleanOpts); - // remove unused arcs from polygon and polyline layers + // remove unused arcs from polygon and polyline layers // TODO: fix bug that leaves uncut arcs in the arc table // (e.g. when projecting a graticule) dissolveArcs(dataset); diff --git a/src/commands/mapshaper-rectangle.js b/src/commands/mapshaper-rectangle.js index 00f211f03..ff03149df 100644 --- a/src/commands/mapshaper-rectangle.js +++ b/src/commands/mapshaper-rectangle.js @@ -9,6 +9,7 @@ import utils from '../utils/mapshaper-utils'; import { stop } from '../utils/mapshaper-logging'; import { probablyDecimalDegreeBounds, clampToWorldBounds } from '../geom/mapshaper-latlon'; import { Bounds } from '../geom/mapshaper-bounds'; +import { densifyPathByInterval } from '../crs/mapshaper-densify'; // Create rectangles around each feature in a layer cmd.rectangles = function(targetLyr, targetDataset, opts) { @@ -126,10 +127,17 @@ function applyBoundsOffset(offsetOpt, bounds, crs) { return bounds; } -export function convertBboxToGeoJSON(bbox, opts) { +export function convertBboxToGeoJSON(bbox, optsArg) { + var opts = optsArg || {}; var coords = [[bbox[0], bbox[1]], [bbox[0], bbox[3]], [bbox[2], bbox[3]], [bbox[2], bbox[1]], [bbox[0], bbox[1]]]; - return { + if (opts.interval > 0) { + coords = densifyPathByInterval(coords, opts.interval); + } + return opts.geometry_type == 'polyline' ? { + type: 'LineString', + coordinates: coords + } : { type: 'Polygon', coordinates: [coords] }; diff --git a/src/commands/mapshaper-rotate.js b/src/commands/mapshaper-rotate.js new file mode 100644 index 000000000..362feae60 --- /dev/null +++ b/src/commands/mapshaper-rotate.js @@ -0,0 +1,49 @@ + +import cmd from '../mapshaper-cmd'; +import { rotateDatasetCoords, getRotationFunction2 } from '../crs/mapshaper-spherical-rotation'; +import { removeAntimeridianCrosses } from '../geom/mapshaper-antimeridian'; +import { DatasetEditor } from '../dataset/mapshaper-dataset-editor'; +import { getDatasetCRS, isLatLngCRS } from '../crs/mapshaper-projections'; +import { getPlanarPathArea, getSphericalPathArea } from '../geom/mapshaper-polygon-geom'; +import { densifyDataset, densifyPathByInterval } from '../crs/mapshaper-densify'; +import { stop } from '../utils/mapshaper-logging'; + +cmd.rotate = rotateDataset; + +export function rotateDataset(dataset, opts) { + if (!isLatLngCRS(getDatasetCRS(dataset))) { + stop('Command requires a lat-long dataset.'); + } + if (!Array.isArray(opts.rotation) || !opts.rotation.length) { + stop('Invalid rotation parameter.'); + } + var rotatePoint = getRotationFunction2(opts.rotation, opts.invert); + var editor = new DatasetEditor(dataset); + var originalArcs; + if (dataset.arcs) { + dataset.arcs.flatten(); + // make a copy so we can calculate original path winding after rotation + originalArcs = dataset.arcs.getCopy(); + } + + dataset.layers.forEach(function(lyr) { + var type = lyr.geometry_type; + editor.editLayer(lyr, function(coords, i, shape) { + if (type == 'point') { + coords.forEach(rotatePoint); + return coords; + } + coords = densifyPathByInterval(coords, 0.5); + coords.forEach(rotatePoint); + if (type == 'polyline') { + return removeAntimeridianCrosses(coords, type); + } + var isHole = type == 'polygon' && getPlanarPathArea(shape[i], originalArcs) < 0; + var coords2 = removeAntimeridianCrosses(coords, type, isHole); + return coords2.reduce(function(memo, polygonCoords) { + return memo.concat(polygonCoords); + }, []); + }); + }); + editor.done(); +} diff --git a/src/crs/mapshaper-densify.js b/src/crs/mapshaper-densify.js index 0568c0386..0670abaaf 100644 --- a/src/crs/mapshaper-densify.js +++ b/src/crs/mapshaper-densify.js @@ -1,7 +1,64 @@ import geom from '../geom/mapshaper-geom'; import { editArcs } from '../paths/mapshaper-arc-editor'; import { getAvgSegment2 } from '../paths/mapshaper-path-utils'; -import { stop } from '../utils/mapshaper-logging'; +import { stop, error } from '../utils/mapshaper-logging'; +import { DatasetEditor } from '../dataset/mapshaper-dataset-editor'; + +export function densifyDataset(dataset, opts) { + var interval = opts.interval; + if (interval > 0 === false) { + error('Expected a valid interval parameter'); + } + var editor = new DatasetEditor(dataset); + dataset.layers.forEach(function(lyr) { + var type = lyr.geometry_type; + editor.editLayer(lyr, function(coords, i, shape) { + if (type == 'point') return coords; + return [densifyPathByInterval(coords, interval)]; + }); + }); + editor.done(); +} + + +// Planar densification by an interval +export function densifyPathByInterval(coords, interval) { + if (findMaxPathInterval(coords) < interval) return coords; + var coords2 = [coords[0]], a, b, dist; + for (var i=1, n=coords.length; i interval) { + pushInterpolatedPoints(coords2, a, b, Math.round(dist / interval) - 1); + } + coords2.push(b); + } + return coords2; +} + +function pushInterpolatedPoints(coords2, a, b, n) { + var dx = (b[0] - a[0]) / (n + 1), + dy = (b[1] - a[1]) / (n + 1); + for (var i=1; i<=n; i++) { + coords2.push([a[0] + dx * i, a[1] + dy * i]); + } +} + +function findMaxPathInterval(coords) { + var maxSq = 0, intSq, a, b; + for (var i=1, n=coords.length; i maxSq) maxSq = intSq; + } + return Math.sqrt(maxSq); +} + +export function densifyUnprojectedPathByDistance(coords, meters) { + // stub +} export function projectAndDensifyArcs(arcs, proj) { var interval = getDefaultDensifyInterval(arcs, proj); diff --git a/src/crs/mapshaper-proj-extents.js b/src/crs/mapshaper-proj-extents.js new file mode 100644 index 000000000..c773884c3 --- /dev/null +++ b/src/crs/mapshaper-proj-extents.js @@ -0,0 +1,119 @@ +import { convertBboxToGeoJSON } from '../commands/mapshaper-rectangle'; +import { getPreciseGeodeticSegmentFunction } from '../geom/mapshaper-geodesic'; +import { inList, getCrsSlug, isAxisAligned, isMeridianBounded } from '../crs/mapshaper-proj-info'; +import { + getSemiMinorAxis, getCircleRadiusFromAngle +} from '../crs/mapshaper-proj-utils'; +import { Bounds } from '../geom/mapshaper-bounds'; +import { getCircleGeoJSON } from '../buffer/mapshaper-point-buffer'; +import { importGeoJSON } from '../geojson/geojson-import'; +import { verbose, error, message } from '../utils/mapshaper-logging'; +import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; +import { rotateDataset } from '../commands/mapshaper-rotate'; + + +export function getClippingDataset(src, dest, opts) { + var dataset, bbox; + if (isCircleClippedProjection(dest) || opts.clip_angle) { + dataset = getClipCircle(src, dest, opts); + } else if (isClippedCylindricalProjection(dest) || opts.clip_bbox) { + dataset = getClipRectangle(dest, opts); + } + return dataset || null; +} + +export function getOutlineDataset(src, dest, opts) { + opts = Object.assign({geometry_type: 'polyline'}, opts); + var dataset = getClippingDataset(src, dest, opts); + return dataset; +} + +function getClipRectangle(dest, opts) { + var bbox = opts.clip_bbox || getDefaultClipBBox(dest); + var rotation = getRotationParams(dest); + if (!bbox) error('Missing expected clip bbox.'); + opts = Object.assign({interval: 0.5}, opts); // make sure edges can curve + var geojson = convertBboxToGeoJSON(bbox, opts); + var dataset = importGeoJSON(geojson); + if (rotation) { + rotateDataset(dataset, {rotation: rotation, invert: true}); + } + return dataset; +} + +function getClipCircle(src, dest, opts) { + var angle = opts.clip_angle || dest.clip_angle || getDefaultClipAngle(dest); + if (!angle) return null; + verbose(`Using clip angle of ${ +angle.toFixed(2) } degrees`); + var dist = getClippingRadius(src, angle); + var cp = getProjCenter(dest); + // kludge: attach the clipping angle to the CRS, so subsequent commands + // (e.g. -graticule) can create an outline + dest.clip_angle = angle; + var geojson = getCircleGeoJSON(cp, dist, null, opts); + return importGeoJSON(geojson); +} + +export function isClippedCylindricalProjection(P) { + // TODO: add tmerc, etmerc, ... + return inList(P, 'merc,bertin1953'); +} + +export function getDefaultClipBBox(P) { + var e = 1e-3; + var bbox = { + merc: [-180, -87, 180, 87], + bertin1953: [-180 + e, -90 + e, 180 - e, 90 - e] + }[getCrsSlug(P)]; + return bbox; +} + +export function isCircleClippedProjection(P) { + return inList(P, 'stere,sterea,ups,ortho,gnom,laea,nsper,tpers'); +} + +function getPerspectiveClipAngle(P) { + var h = parseFloat(P.params.h.param); + if (!h || h < 0) { + return 0; + } + var theta = Math.acos(P.a / (P.a + h)) * 180 / Math.PI; + theta *= 0.995; // reducing a bit to avoid out-of-range errors + return theta; +} + +export function getDefaultClipAngle(P) { + var slug = getCrsSlug(P); + if (slug == 'nsper') return getPerspectiveClipAngle(P); + if (slug == 'tpers') { + message('Automatic clipping is not supported for the Tilted Perspective projection'); + return 0; + } + return { + gnom: 60, + laea: 179, + ortho: 89.9, // TODO: investigate projection errors closer to 90 + stere: 142, + sterea: 142, + ups: 10.5 // TODO: should be 6.5 deg at north pole + }[slug] || 0; +} + +export function getRotationParams(P) { + var slug = getCrsSlug(P); + if (slug == 'bertin1953') return [-16.5,-42]; + return null; +} + + + +function getProjCenter(P) { + var rtod = 180 / Math.PI; + return [P.lam0 * rtod, P.phi0 * rtod]; +} + +// Convert a clip angle to a distance in meters +function getClippingRadius(P, angle) { + return getCircleRadiusFromAngle(P, angle); +} + diff --git a/src/crs/mapshaper-proj-info.js b/src/crs/mapshaper-proj-info.js index e8613bf85..6e6f9c367 100644 --- a/src/crs/mapshaper-proj-info.js +++ b/src/crs/mapshaper-proj-info.js @@ -1,4 +1,5 @@ import { getSemiMinorAxis } from '../crs/mapshaper-proj-utils'; +import { getAntimeridian } from '../geom/mapshaper-latlon'; import { message } from '../utils/mapshaper-logging'; export function getCrsSlug(P) { @@ -11,56 +12,49 @@ export function isRotatedNormalProjection(P) { return isAxisAligned(P) && P.lam0 !== 0; } +// Projection is vertically aligned to earth's axis export function isAxisAligned(P) { - return !isNonNormal(P); + // TODO: consider projections that may or may not be aligned, + // depending on parameters + if (inList(P, 'cassini,gnom,bertin1953,chamb,ob_tran,tpeqd,healpix,rhealpix,' + + 'ocea,omerc,tmerc,etmerc')) { + return false; + } + if (isAzimuthal(P)) { + return false; + } + return true; } -export function isClippedCylindricalProjection(P) { - // TODO: add tmerc, etmerc, ... - return inList(P, 'merc'); +export function getBoundingMeridian(P) { + if (P.lam0 === 0) return 180; + return getAntimeridian(P.lam0 * 180 / Math.PI); } -export function getDefaultClipBBox(P) { - return { - merc: [-180, -87, 180, 87] - }[getCrsSlug(P)] || null; +// Are the projection's bounds meridians? +export function isMeridianBounded(P) { + // TODO: add azimuthal projection with lat0 == 0 + // if (inList(P, 'ortho') && P.lam0 === 0) return true; + return isAxisAligned(P); // TODO: look for exceptions to this } -export function isCircleClippedProjection(P) { - return inList(P, 'stere,sterea,ups,ortho,gnom,laea,nsper,tpers'); +// Is the projection bounded by parallels or polar lines? +export function isParallelBounded(P) { + // TODO: add polar azimuthal projections + // TODO: reject world projections that do not have polar lines + return isAxisAligned(P); } -function getPerspectiveClipAngle(P) { - var h = parseFloat(P.params.h.param); - if (!h || h < 0) { - return 0; - } - var theta = Math.acos(P.a / (P.a + h)) * 180 / Math.PI; - theta *= 0.995; // reducing a bit to avoid out-of-range errors - return theta; +export function getBoundingMeridians(P) { + } -export function getDefaultClipAngle(P) { - var slug = getCrsSlug(P); - if (slug == 'nsper') return getPerspectiveClipAngle(P); - if (slug == 'tpers') { - message('Automatic clipping is not supported for the Tilted Perspective projection'); - return 0; - } - return { - gnom: 60, - laea: 179, - ortho: 90, - stere: 142, - sterea: 142, - ups: 10.5 // TODO: should be 6.5 deg at north pole - }[slug] || 0; +export function getBoundingParallels(P) { + } -function isNonNormal(P) { - var others = 'cassini,gnom,bertin1953,chamb,ob_tran,tpeqd,healpix,rhealpix,' + - 'ob_tran,ocea,omerc,tmerc,etmerc'; - return isAzimuthal(P) || inList(P, others); +export function isConic(P) { + return inList(P, 'aea,bonne,eqdc,lcc,poly,euler,murd1,murd2,murd3,pconic,tissot,vitk1'); } export function isAzimuthal(P) { @@ -68,6 +62,6 @@ export function isAzimuthal(P) { 'aeqd,gnom,laea,mil_os,lee_os,gs48,alsk,gs50,nsper,tpers,ortho,qsc,stere,ups,sterea'); } -function inList(P, str) { +export function inList(P, str) { return str.split(',').includes(getCrsSlug(P)); } diff --git a/src/crs/mapshaper-projections.js b/src/crs/mapshaper-projections.js index 40a9a1196..7e77f3c0f 100644 --- a/src/crs/mapshaper-projections.js +++ b/src/crs/mapshaper-projections.js @@ -217,6 +217,10 @@ export function isLatLngCRS(P) { return P && P.is_latlong || false; } +export function isLatLngDataset(dataset) { + return isLatLngCRS(getDatasetCRS(dataset)); +} + export function printProjections() { var index = require('mproj').internal.pj_list; var msg = 'Proj4 projections\n'; diff --git a/src/crs/mapshaper-spherical-clipping.js b/src/crs/mapshaper-spherical-clipping.js index 966578111..ace00368f 100644 --- a/src/crs/mapshaper-spherical-clipping.js +++ b/src/crs/mapshaper-spherical-clipping.js @@ -1,78 +1,17 @@ -import { - isCircleClippedProjection, - getDefaultClipAngle, - isClippedCylindricalProjection, - getDefaultClipBBox, -} from '../crs/mapshaper-proj-info'; -import { - getSemiMinorAxis, getCircleRadiusFromAngle -} from '../crs/mapshaper-proj-utils'; import { isLatLngCRS, getDatasetCRS } from '../crs/mapshaper-projections'; -import { getCircleGeoJSON } from '../buffer/mapshaper-point-buffer'; -import { getPreciseGeodeticSegmentFunction } from '../geom/mapshaper-geodesic'; -import { clipLayersByGeoJSON } from '../clipping/mapshaper-clip-utils'; -import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; +import { clipLayersInPlace } from '../commands/mapshaper-clip-erase'; +import { getClippingDataset } from '../crs/mapshaper-proj-extents'; import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; -import { convertBboxToGeoJSON } from '../commands/mapshaper-rectangle'; -import { error, verbose } from '../utils/mapshaper-logging'; -import { Bounds } from '../geom/mapshaper-bounds'; export function preProjectionClip(dataset, src, dest, opts) { - var clipped = false; if (!isLatLngCRS(src) || opts.no_clip) return false; - if (isCircleClippedProjection(dest) || opts.clip_angle) { - clipped = clipToCircle(dataset, src, dest, opts); - } - if (isClippedCylindricalProjection(dest) || opts.clip_bbox) { - clipped = clipped || clipToRectangle(dataset, dest, opts); - } - if (clipped) { - // remove arcs outside the clip area, so they don't get projected + var clipData = getClippingDataset(src, dest, opts); + if (clipData) { + // TODO: don't bother to clip content that is fully within + // the clipping shape. But how to tell? + clipLayersInPlace(dataset.layers, clipData, dataset, 'clip'); + // remove arcs outside the clip area, so they don't get projected dissolveArcs(dataset); } - return clipped; -} - -export function getProjectionOutline(src, dest, opts) { - if (!isCircleClippedProjection(dest)) return null; - return getClipShapeGeoJSON(src, dest, opts); -} - -function getClipShapeGeoJSON(src, dest, opts) { - var angle = opts.clip_angle || dest.clip_angle || getDefaultClipAngle(dest); - if (!angle) return null; - verbose(`Using clip angle of ${ +angle.toFixed(2) } degrees`); - var dist = getClippingRadius(src, angle); - var cp = getProjCenter(dest); - // kludge: attach the clipping angle to the CRS, so subsequent commands - // (e.g. -graticule) can create an outline - dest.clip_angle = angle; - return getCircleGeoJSON(cp, dist, null, opts); -} - -function clipToRectangle(dataset, dest, opts) { - var bbox = opts.clip_bbox || getDefaultClipBBox(dest); - if (!bbox) error('Missing expected clip bbox.'); - // don't clip if dataset fits within the bbox - if (Bounds.from(bbox).contains(getDatasetBounds(dataset))) return false; - var geojson = convertBboxToGeoJSON(bbox); - clipLayersByGeoJSON(dataset.layers, dataset, geojson, 'clip'); - return true; -} - -function clipToCircle(dataset, src, dest, opts) { - var geojson = getClipShapeGeoJSON(src, dest, opts); - if (!geojson) return false; - clipLayersByGeoJSON(dataset.layers, dataset, geojson, 'clip'); - return true; -} - -function getProjCenter(P) { - var rtod = 180 / Math.PI; - return [P.lam0 * rtod, P.phi0 * rtod]; -} - -// Convert a clip angle to a distance in meters -function getClippingRadius(P, angle) { - return getCircleRadiusFromAngle(P, angle); + return !!clipData; } diff --git a/src/crs/mapshaper-spherical-cutting.js b/src/crs/mapshaper-spherical-cutting.js index 34fe0cab5..ff98c6fd5 100644 --- a/src/crs/mapshaper-spherical-cutting.js +++ b/src/crs/mapshaper-spherical-cutting.js @@ -2,7 +2,8 @@ import { isLatLngCRS } from '../crs/mapshaper-projections'; import { isRotatedNormalProjection } from '../crs/mapshaper-proj-info'; import { layerHasPaths } from '../dataset/mapshaper-layer-utils'; import { getAntimeridian } from '../geom/mapshaper-latlon'; -import { clipLayersByGeoJSON } from '../clipping/mapshaper-clip-utils'; +import { clipLayersInPlace } from '../commands/mapshaper-clip-erase'; +import { importGeoJSON } from '../geojson/geojson-import'; export function insertPreProjectionCuts(dataset, src, dest) { var antimeridian = getAntimeridian(dest.lam0 * 180 / Math.PI); @@ -31,13 +32,11 @@ function datasetCrossesLon(dataset, lon) { function insertVerticalCut(dataset, lon) { var pathLayers = dataset.layers.filter(layerHasPaths); if (pathLayers.length === 0) return; - var e =1e-8; + var e = 1e-8; var coords = [[lon+e, 90], [lon+e, -90], [lon-e, -90], [lon-e, 90], [lon+e, 90]]; - var geojson = { + var clip = importGeoJSON({ type: 'Polygon', coordinates: [coords] - }; - clipLayersByGeoJSON(pathLayers, dataset, geojson, 'erase'); + }); + clipLayersInPlace(pathLayers, clip, dataset, 'erase'); } - - diff --git a/src/crs/mapshaper-spherical-rotation.js b/src/crs/mapshaper-spherical-rotation.js index eda24513f..9ad70ddae 100644 --- a/src/crs/mapshaper-spherical-rotation.js +++ b/src/crs/mapshaper-spherical-rotation.js @@ -2,8 +2,12 @@ import { projectPointLayer, projectArcs } from '../commands/mapshaper-proj'; import { layerHasPoints } from '../dataset/mapshaper-layer-utils'; import { R2D, D2R } from '../geom/mapshaper-basic-geom'; -export function rotateDatasetCoords(dataset, rotation) { - var proj = getRotationFunction(rotation); +// based on d3 implementation of Euler-angle rotation +// https://github.com/d3/d3-geo/blob/master/src/rotation.js +// license: https://github.com/d3/d3-geo/blob/master/LICENSE + +export function rotateDatasetCoords(dataset, rotation, inv) { + var proj = getRotationFunction(rotation, inv); dataset.layers.filter(layerHasPoints).forEach(function(lyr) { projectPointLayer(lyr, proj); }); @@ -12,44 +16,68 @@ export function rotateDatasetCoords(dataset, rotation) { } } -function getRotationFunction(rotation) { +export function getRotationFunction(rotation, inv) { + var f = getRotationFunction2(rotation, inv); + return function(lng, lat) { + return f([lng, lat]); + }; +} + +export function getRotationFunction2(rotation, inv) { var a = (rotation[0] || 0) * D2R, b = (rotation[1] || 0) * D2R, c = (rotation[2] || 0) * D2R; - return function(lng, lat) { - return rotatePoint([lng, lat], a, b, c); + return function(p) { + p[0] *= D2R; + p[1] *= D2R; + var rotate = inv ? rotatePointInv : rotatePoint; + rotate(p, a, b, c); + p[0] *= R2D; + p[1] *= R2D; + return p; }; } -function rotatePoint(p, deltaLambda, deltaPhi, deltaGamma) { - p[0] *= D2R; - p[1] *= D2R; - if (deltaLambda != 0) rotateLambda(p, deltaLambda); - if (deltaPhi !== 0 || deltaGamma !== 0) { - rotatePhiGamma(p, deltaPhi, deltaGamma); +function rotatePoint(p, deltaLam, deltaPhi, deltaGam) { + if (deltaLam != 0) rotateLambda(p, deltaLam); + if (deltaPhi !== 0 || deltaGam !== 0) { + rotatePhiGamma(p, deltaPhi, deltaGam, false); } - p[0] *= R2D; - p[1] *= R2D; return p; } -function rotateLambda(p, deltaLambda) { - var lam = p[0] + deltaLambda; +function rotatePointInv(p, deltaLam, deltaPhi, deltaGam) { + if (deltaPhi !== 0 || deltaGam !== 0) { + rotatePhiGamma(p, deltaPhi, deltaGam, true); + } + if (deltaLam != 0) rotateLambda(p, -deltaLam); + return p; +} + +function rotateLambda(p, deltaLam) { + var lam = p[0] + deltaLam; if (lam > Math.PI) lam -= 2 * Math.PI; else if (lam < -Math.PI) lam += 2 * Math.PI; p[0] = lam; } -function rotatePhiGamma(p, deltaPhi, deltaGamma) { +function rotatePhiGamma(p, deltaPhi, deltaGam, inv) { var cosDeltaPhi = Math.cos(deltaPhi), sinDeltaPhi = Math.sin(deltaPhi), - cosDeltaGamma = Math.cos(deltaGamma), - sinDeltaGamma = Math.sin(deltaGamma), + cosDeltaGam = Math.cos(deltaGam), + sinDeltaGam = Math.sin(deltaGam), cosPhi = Math.cos(p[1]), x = Math.cos(p[0]) * cosPhi, y = Math.sin(p[0]) * cosPhi, z = Math.sin(p[1]), - k = z * cosDeltaPhi + x * sinDeltaPhi; - p[0] = Math.atan2(y * cosDeltaGamma - k * sinDeltaGamma, x * cosDeltaPhi - z * sinDeltaPhi); - p[1] = Math.asin(k * cosDeltaGamma + y * sinDeltaGamma); + k; + if (inv) { + k = z * cosDeltaGam - y * sinDeltaGam; + p[0] = Math.atan2(y * cosDeltaGam + z * sinDeltaGam, x * cosDeltaPhi + k * sinDeltaPhi); + p[1] = Math.asin(k * cosDeltaPhi - x * sinDeltaPhi); + } else { + k = z * cosDeltaPhi + x * sinDeltaPhi; + p[0] = Math.atan2(y * cosDeltaGam - k * sinDeltaGam, x * cosDeltaPhi - z * sinDeltaPhi); + p[1] = Math.asin(k * cosDeltaGam + y * sinDeltaGam); + } } diff --git a/src/dataset/mapshaper-dataset-editor.js b/src/dataset/mapshaper-dataset-editor.js new file mode 100644 index 000000000..3f77623f2 --- /dev/null +++ b/src/dataset/mapshaper-dataset-editor.js @@ -0,0 +1,61 @@ +import { buildTopology } from '../topology/mapshaper-topology'; +import { ArcCollection } from '../paths/mapshaper-arcs'; +import { stop, error } from '../utils/mapshaper-logging'; + +export function DatasetEditor(dataset) { + var layers = []; + var arcs = []; + + this.done = function() { + dataset.layers = layers; + if (arcs.length) { + dataset.arcs = new ArcCollection(arcs); + buildTopology(dataset); + } + }; + + this.editLayer = function(lyr, cb) { + var type = lyr.geometry_type; + if (dataset.layers.indexOf(lyr) != layers.length) { + error('Layer was edited out-of-order'); + } + if (!type) { + layers.push(lyr); + return; + } + var shapes = lyr.shapes.map(function(shape, shpId) { + var shape2 = [], retn, input; + for (var i=0, n=shape ? shape.length : 0; i 0 ? shape2 : null; + }); + layers.push(Object.assign(lyr, {shapes: shapes})); + }; + + function extendPathShape(shape, parts) { + for (var i=0; i180 degrees // that fully encloses both poles and the antimeridian. // need to add an enclosure around the entire sphere - parts = [[[180, 90], [180, -90], [-180, -90], [-180, 90], [180, 90]], parts[0]]; + parts = [[[180, 90], [180, -90], [0, -90], [-180, -90], [-180, 90], [0, 90], [180, 90]], parts[0]]; } return [parts]; } // Now we can assume that the first and last point of the split-apart path is 180 or -180 - if (parts.length == 1) { - return removeOneCross(parts); - } - if (parts.length == 2) { - return removeTwoCrosses(parts, ringArea(ring)); - } - stop('Unexpected geometry of an antimeridan-crossing polygon ring.'); + return reconnectSplitParts(parts); } function addSubPath(paths, path) { @@ -49,13 +42,96 @@ function addSubPath(paths, path) { export function reconnectSplitParts(parts) { var yy = getSortedIntersections(parts); + var rings = []; + var usedParts = []; + var errors = 0; + parts.forEach(function(part, i) { + if (usedParts[i]) return; + var ring = addPartToRing(part, []); + if (ring) { + rings.push([ring]); // multipolygon coords + } else { + errors++; + } + }); + + return rings; + + function addPartToRing(part, ring) { + var lastPoint = lastEl(part); + var i = parts.indexOf(part); + if (usedParts[i]) { + debug('Tried to use a previously used path'); + return null; + } + usedParts[i] = true; + ring = ring.concat(part); + var nextPoint = findNextPoint(parts, lastPoint, yy); + if (!nextPoint) { + return null; + } + if (lastPoint[0] != nextPoint[0]) { + // add polar line to switch from east to west or west to east + // coming from east -> turn south + // coming from west -> turn north + var poleY = lastPoint[0] == 180 ? -90 : 90; + // need a center point (lines longer than 90 degrees cause confusion when rotating) + ring.push([lastPoint[0], poleY], [0, poleY], [nextPoint[0], poleY]); + } + var nextPart = findPartStartingAt(parts, nextPoint); + if (!nextPart) { + return null; + } + if (samePoint(ring[0], nextPart[0])) { + // done! + ring.push(ring[0]); // close the ring + return ring; + } + return addPartToRing(nextPart, ring); + } +} + +// p: last point of previous part +function findNextPoint(parts, p, yy) { + var x = p[0]; + var y = p[1]; + var i = yy.indexOf(y); + var xOpp = x == -180 ? 180 : -180; + var turnSouth = x == 180; // intersecting from the east -> turn south + var iNext = turnSouth ? i - 1 : i + 1; + var nextPoint; + if (x != 180 && x != -180) { + debug('Unexpected error'); + return null; + } + if (i == -1) { + debug('Point missing from intersection table:', p); + return null; + } + if (iNext < 0 || iNext >= yy.length) { + // no path to traverse to along the antimeridian -- + // assume the path surrounds one of the poles + // enclose south pole + nextPoint = [xOpp, y]; + } else { + nextPoint = [x, yy[iNext]]; + } + return nextPoint; +} +function findPartStartingAt(parts, firstPoint) { + for (var i=0; i y2) { - pole1 = 90; - pole2 = -90; - } else { - pole1 = -90; - pole2 = 90; - } - ring = parts[0].concat( - [[x1, pole1], [x2, pole1]], parts[1], [[x2, pole2], [x1, pole2], d]); - return [[dedup(ring)]]; -} - // duplicate points occur if a vertex is on the antimeridan function dedup(ring) { return ring.reduce(function(memo, p, i) { @@ -149,28 +195,10 @@ function dedup(ring) { }, []); } -// Ring contains a pole. -// Returns one ring including n or s pole line. -function removeOneCross(parts) { - var ring = parts[0]; - var lastX = lastEl(ring)[0]; - var firstX = firstEl(ring)[0]; - // lastX === -180: crosses antimeridian w->e, 180: e->w - // if ring crosses w->e, go through n pole - // (this assumes that ring has CW winding / is not a hole) - var poleY = lastX === -180 ? 90 : -90; - ring.push([lastX, poleY], [firstX, poleY], ring[0]); - return [[ring]]; // multipolygon format -} - function lastEl(arr) { return arr[arr.length - 1]; } -function firstEl(arr) { - return arr[0]; -} - // p1, p2: two vertices on different sides of the antimeridian // Returns y-intercept of the segment connecting p1, p2 // TODO: consider using the great-circle intersection, instead of diff --git a/test/antimeridian-test.js b/test/antimeridian-test.js index c220862a7..49e7bcc9f 100644 --- a/test/antimeridian-test.js +++ b/test/antimeridian-test.js @@ -55,6 +55,7 @@ describe('mapshaper-antimeridian.js', function () { assert.deepEqual(splitPathAtAntimeridian(input), expect); }) + // TODO it ('ring contains a segment that parallels the antimeridian', function() { // var input = [[180, 1], }) From 45dfde3107382e76645f99cf97e5f3d07aea1f72 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 20 May 2021 11:53:13 -0400 Subject: [PATCH 040/891] Graticule fix --- src/commands/mapshaper-graticule.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/commands/mapshaper-graticule.js b/src/commands/mapshaper-graticule.js index b7cfb3b05..092d902f4 100644 --- a/src/commands/mapshaper-graticule.js +++ b/src/commands/mapshaper-graticule.js @@ -51,10 +51,11 @@ function createProjectedGraticule(dest, opts) { function addOutlineToGraticule(graticule, outline) { var merged = mergeDatasets([graticule, outline]); - var lyr = merged.layers[0]; - var records = lyr.data.getRecords(); - merged.layers[1].shapes.forEach(function(shp) { - lyr.shapes.push(shp); + var src = merged.layers.pop(); + var dest = merged.layers[0]; + var records = dest.data.getRecords(); + src.shapes.forEach(function(shp) { + dest.shapes.push(shp); records.push({type: 'outline', value: null}); }); return merged; From 5faa224cc4ecd0e9c71a2ad7ba40a5281a86d42a Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 20 May 2021 12:23:38 -0400 Subject: [PATCH 041/891] Add graticule tests --- test/data/world_land.json | 3 +++ test/graticule-test.js | 43 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 test/data/world_land.json diff --git a/test/data/world_land.json b/test/data/world_land.json new file mode 100644 index 000000000..bb80ae861 --- /dev/null +++ b/test/data/world_land.json @@ -0,0 +1,3 @@ +{"type":"GeometryCollection", "geometries": [ +{"type":"MultiPolygon","coordinates":[[[[30.86,69.78],[28.19,71.08],[21.78,69.88],[18.15,69.46],[13.11,66.54],[12.47,65.13],[9.7,63.64],[4.98,61.74],[6.57,58.11],[8.13,58.09],[10.52,59.31],[13.01,55.39],[16.38,56.66],[16.78,58.57],[18.96,59.86],[17.08,61.55],[18.64,63.17],[20.78,63.87],[23.14,65.74],[25.36,65.48],[21.12,62.69],[21.34,60.88],[24.5,59.99],[28.43,60.66],[27.86,59.4],[23.48,59.21],[24.48,58.24],[21.05,56.84],[21.09,55.53],[19.39,54.27],[17.92,54.83],[13.96,53.77],[9.7,54.81],[8.49,53.65],[4.87,52.9],[4.06,51.72],[1.54,50.28],[-1.11,49.37],[-2.85,47.63],[-1.07,45.91],[-1.78,43.36],[-7.24,43.56],[-8.85,42.66],[-8.65,41],[-9.48,38.71],[-8.62,37.13],[-7.05,37.21],[-5.62,36],[-4.43,36.7],[-2.12,36.73],[0.2,38.8],[-0.33,39.49],[0.97,41.03],[3.23,41.95],[3.96,43.54],[6.17,43.1],[8.75,44.42],[13.05,41.22],[15.68,40.03],[16.93,40.45],[14.14,42.53],[12.37,44.24],[12.37,45.42],[14.84,45.09],[15.15,44.18],[19.6,41.81],[19.31,40.65],[22.48,36.6],[23.25,37.97],[22.72,38.81],[24.39,40.93],[27.24,40.64],[28.63,44.29],[31.02,46.6],[34.46,44.72],[34.81,46.16],[37.58,47.07],[38.49,46.06],[37.48,44.7],[41.45,42.73],[41.42,41.39],[38.36,40.91],[35.92,41.71],[33.37,42.02],[31.02,41.06],[29.17,41.23],[28.65,40.37],[26.76,40.4],[26.16,39.44],[27.59,36.97],[29.71,36.15],[30.68,36.87],[32.81,36.03],[34.72,36.8],[35.98,36.01],[35.99,34.57],[34,31.2],[31.01,31.58],[29.17,30.81],[23.24,32.21],[21.6,32.9],[20.56,32.53],[20.15,31],[19.24,30.26],[15.68,31.44],[15.19,32.38],[11.22,33.19],[10.15,37.22],[1.35,36.54],[-1.94,35.07],[-4.35,35.14],[-5.88,35.79],[-6.93,33.93],[-9.27,32.54],[-9.82,29.81],[-11.55,28.28],[-12.91,27.94],[-14.4,26.25],[-15,24.55],[-16.96,21.82],[-16.21,20.25],[-16.53,19.38],[-16.06,17.56],[-17.26,14.73],[-16.8,12.44],[-13.32,9.29],[-12.49,7.38],[-7.71,4.37],[-5.85,5.04],[-3.97,5.24],[-2.11,4.72],[2.07,6.31],[3.84,6.43],[4.94,5.96],[5.96,4.31],[8.89,4.58],[9.99,3.09],[9.37,1.14],[9.32,-1.97],[11.83,-4.61],[13.41,-8.42],[13,-9.11],[13.87,-10.97],[13.65,-12.24],[12.55,-13.43],[11.81,-16.03],[11.76,-17.76],[14.42,-22.31],[14.46,-24.07],[15.31,-27.36],[16.82,-29.11],[18.27,-31.88],[17.89,-33],[18.8,-34.31],[20.04,-34.84],[22.58,-33.99],[24.83,-34.21],[27.2,-33.47],[29.96,-31.33],[32.36,-28.61],[32.9,-25.54],[35.11,-24.62],[35.61,-22.95],[34.92,-19.86],[37.96,-17.35],[39.78,-16.51],[40.84,-14.86],[40.35,-11.35],[39.25,-8.27],[39.5,-6.85],[38.84,-5.81],[40.17,-2.8],[41.57,-1.69],[43.81,0.95],[45.99,2.42],[47.45,3.86],[48.88,5.86],[50.63,9.06],[51.08,11.25],[50.02,11.48],[47.4,11.16],[44.31,10.4],[43.35,12.4],[41.19,14.61],[39.72,15.23],[38.58,17.97],[37.46,18.82],[36.89,22.05],[35.69,22.92],[33.57,27.85],[35.26,28],[37.29,24.68],[38.46,23.8],[39.18,21.14],[40.75,19.79],[42.75,16.67],[42.7,15.7],[43.48,12.81],[44.86,12.73],[49.1,14.53],[52.18,15.61],[52.48,16.41],[55.06,17.02],[56.35,17.91],[58.52,20.42],[59.83,22.29],[58.77,23.49],[57.12,23.96],[55.58,25.53],[53.94,24.06],[51.8,24],[51.6,25.72],[50.17,25.65],[49.95,26.85],[48.8,27.71],[48.01,29.86],[50.18,29.94],[51.46,27.91],[54.85,26.51],[56.79,27.11],[57.35,25.78],[61.14,25.19],[66.1,25.42],[68.63,23.14],[69.09,22.07],[70.85,20.68],[72.57,21.35],[73.48,16.06],[74.82,12.83],[75.68,11.39],[76.52,8.94],[77.59,8.11],[79.39,10.34],[80.36,13.35],[80.08,15.31],[84.35,18.53],[85.28,19.81],[86.37,19.94],[87.17,21.53],[91.53,22.7],[92.45,20.64],[93.94,19.44],[94.58,17.38],[94.43,16.07],[97.61,16.55],[98.09,13.71],[98.71,12.79],[98.62,8.39],[100.35,6],[100.74,3.86],[101.45,2.68],[103.34,1.54],[103.49,4.33],[103.15,5.33],[100.52,7.26],[99.16,10.36],[100.99,13.43],[102.8,11.98],[103.12,10.86],[104.9,9.79],[107.24,10.37],[109.01,11.35],[109.44,12.92],[108.92,15.14],[107.18,16.91],[105.64,18.97],[106.01,20],[107.91,21.54],[110.78,21.41],[115.89,22.78],[118.67,24.56],[121.14,28.19],[121.9,29.75],[121.95,31.71],[119.61,35.59],[122.05,36.95],[120.87,37.79],[119.76,37.09],[118,39.21],[121.19,40.91],[121.66,38.99],[123.65,39.87],[125.53,39.4],[124.87,38.31],[126.57,37.78],[126.39,35.32],[127.39,34.78],[129.26,35.17],[129.44,37.06],[127.52,39.77],[129.77,40.93],[130.03,42.04],[131.37,42.75],[133.11,42.69],[135.15,43.5],[137.67,45.8],[140.19,48.47],[140.41,50.55],[141.39,53.21],[139.82,54.18],[136.72,53.86],[135.18,54.81],[143.07,59.35],[147.34,59.32],[149.61,59.75],[151.78,59.13],[154.26,59.84],[157.45,61.81],[160.45,61.79],[160.81,60.84],[159.13,58.45],[155.98,56.63],[155.58,55.13],[156.8,50.95],[157.94,51.64],[159.94,54.03],[162.14,54.75],[162.03,57.95],[163.77,60.04],[169.33,60.53],[177.72,62.6],[180,65.04],[180,68.96],[172.98,69.83],[171.15,69.08],[164.2,69.76],[161.09,69.55],[158.69,70.8],[153.01,70.66],[148.86,72.38],[142.85,72.69],[139.75,71.45],[132.36,71.73],[113.54,73.5],[110.21,73.99],[113.66,75.25],[111.17,76.75],[103,77.65],[97.14,75.98],[89.31,75.48],[85.19,73.65],[80.55,73.52],[80.95,72.4],[77.58,71.83],[69.03,72.73],[67.02,69.73],[61.38,69.77],[57.35,68.54],[52.07,68.5],[48.01,67.63],[44.18,68.51],[42.22,66.52],[39.7,65.4],[34.65,65.21],[35.56,66.38],[40.47,66.4],[41.06,67.64],[35.97,69.16],[30.86,69.78]],[[49.03,46.37],[52.96,46.94],[52.88,45.28],[51.4,45.33],[50.88,44.15],[52.43,42.1],[53.99,38.92],[54.03,36.8],[52.02,36.57],[49.06,37.67],[49.55,40.64],[47.47,43.02],[46.94,44.44],[49.03,46.37]]],[[[-68.59,-52.68],[-70.4,-53.01],[-69.89,-54.27],[-66.52,-55.06],[-68.59,-52.68]]],[[[-141,69.64],[-152.24,70.55],[-156.82,71.3],[-166.29,68.29],[-161.9,67.05],[-165.64,66.36],[-166.12,64.57],[-164.14,63.26],[-166.18,61.62],[-164.19,60.02],[-162.24,60.06],[-160.73,58.92],[-157.47,58.5],[-158.51,56.03],[-153.33,58.89],[-148.65,59.95],[-147.09,61.01],[-143.97,60.01],[-140.24,59.71],[-137.11,58.39],[-135.05,58.34],[-130.89,55.71],[-130.11,53.94],[-127.8,52.26],[-127.78,51.16],[-124.59,50.24],[-122.54,48.78],[-124.73,48.17],[-123.91,46.45],[-124.56,42.84],[-124.36,40.26],[-121.94,36.49],[-120.47,34.45],[-117.47,33.3],[-115.7,29.75],[-114.04,28.47],[-114.48,27.22],[-112.19,25.99],[-112.18,24.84],[-110.32,23.57],[-111.56,26.7],[-114.66,30.2],[-114.81,31.82],[-113.03,31.16],[-111.69,28.45],[-109.41,26.66],[-109.37,25.66],[-108.03,25.07],[-105.65,22.47],[-105.68,20.37],[-103.52,18.34],[-99.61,16.68],[-96.25,15.69],[-94.85,16.42],[-93.88,16.09],[-91.49,14.02],[-87.94,13.18],[-85.77,11.11],[-85.86,10.25],[-80.45,7.25],[-80.46,8.07],[-79.15,9.06],[-77.29,6.52],[-77.24,3.47],[-78.43,2.54],[-78.89,1.31],[-80.07,0.76],[-80.77,-2.42],[-79.84,-3],[-81.24,-4.27],[-81.11,-6.05],[-79.71,-7.09],[-78.24,-9.93],[-75.1,-15.4],[-71.45,-17.3],[-70.31,-18.41],[-70.04,-21.44],[-70.44,-25.38],[-71.71,-30.69],[-71.4,-32.39],[-72.78,-36.35],[-73.7,-37.64],[-73.24,-39.54],[-73.93,-40.99],[-72.85,-41.94],[-73.22,-44.19],[-74.62,-47.75],[-73.54,-52.05],[-70.8,-52.73],[-68.76,-51.96],[-69.06,-50.47],[-65.97,-48.11],[-65.75,-47.21],[-67.58,-46.42],[-65.62,-45.04],[-65.32,-43.66],[-63.79,-41.16],[-62.36,-40.9],[-62.06,-39.45],[-57.55,-38.11],[-57.26,-36.16],[-56.27,-34.92],[-53.78,-34.41],[-52.15,-32.21],[-51.07,-31.49],[-49.7,-29.27],[-48.8,-28.52],[-48.59,-25.87],[-46.94,-24.3],[-44.66,-23.07],[-42.18,-22.93],[-39.79,-19.61],[-38.86,-15.86],[-39.05,-13.36],[-37.96,-12.58],[-34.95,-8.49],[-34.79,-7.14],[-35.29,-5.37],[-37.16,-4.93],[-39.8,-2.94],[-43.91,-2.53],[-45.1,-1.45],[-48.44,-0.23],[-51.16,-0.09],[-49.88,1.15],[-51.39,4.36],[-53.08,5.51],[-54.99,6],[-56.97,6],[-59.74,8.39],[-61.93,8.49],[-62.49,10.1],[-63.53,10.59],[-65.02,10.03],[-66.25,10.65],[-67.81,10.45],[-69.72,11.57],[-71.57,10.79],[-71.92,11.56],[-74.85,11.11],[-77.01,8.24],[-78.03,9.22],[-79.61,9.62],[-81.19,8.76],[-82.24,9],[-83.83,11.12],[-83.41,15.26],[-84.3,15.83],[-88.53,15.93],[-88.29,17.57],[-86.8,21.01],[-88.11,21.6],[-90.35,20.97],[-91.32,18.93],[-94.4,18.15],[-95.85,18.71],[-97.76,22.06],[-97.72,24.15],[-97.18,25.7],[-97.02,27.89],[-94.62,29.46],[-91.62,29.73],[-90.06,29.18],[-89,30.39],[-86.17,30.46],[-84.88,29.73],[-83.64,29.88],[-82.64,28.88],[-82.83,27.82],[-81.09,25.12],[-80.05,26.89],[-81.52,30.55],[-80.64,32.27],[-78.79,33.76],[-76.51,34.72],[-75.72,35.69],[-76.4,37.21],[-75.42,37.99],[-73.59,41.03],[-70.54,41.81],[-70.02,43.86],[-67.17,44.66],[-65.73,43.49],[-64.22,44.38],[-63.3,45.72],[-64.92,46.83],[-64.5,49.05],[-66.39,50.2],[-60.14,50.2],[-58.48,51.29],[-55.7,52.08],[-56.23,53.58],[-57.94,54.87],[-60.19,55.26],[-61.92,56.43],[-62.08,57.9],[-64.81,60.37],[-66.09,58.8],[-69.24,58.9],[-69.92,60.8],[-73.8,62.47],[-77.49,62.57],[-78.61,58.85],[-76.7,57.44],[-77.12,55.65],[-79.53,54.62],[-78.52,52.34],[-79.77,51.19],[-82.28,52.96],[-82.44,55.12],[-85.1,55.27],[-88.88,56.86],[-92.46,57.44],[-94.78,58.88],[-94.25,61.36],[-90.96,63.57],[-88.09,64.15],[-85.94,66.04],[-82.11,66.71],[-81.33,69.18],[-84.48,69.86],[-85.91,68.04],[-91.48,69.66],[-94.58,72],[-96.53,70.13],[-93.65,68.53],[-95.69,67.7],[-102.25,67.73],[-105.29,68.34],[-108.59,67.61],[-115,67.79],[-115.02,68.87],[-121.42,69.76],[-125.99,69.42],[-130.58,70.13],[-136.75,68.88],[-141,69.64]],[[-82.42,43],[-83.49,45.35],[-84.79,45.79],[-87.91,43.24],[-86.64,46.41],[-90.76,47.61],[-87.95,48.93],[-84.89,47.96],[-84.05,46.32],[-80.73,45.91],[-82.42,43]]],[[[145.27,-42.07],[146.86,-43.57],[148.33,-41.91],[146.15,-41.15],[145.27,-42.07]]],[[[129.66,-14.05],[128.21,-14.72],[126.2,-13.96],[122.26,-17.11],[122.37,-18.16],[121.08,-19.59],[117.78,-20.7],[116.52,-20.78],[114.65,-21.86],[113.41,-24.47],[114.27,-25.88],[113.61,-26.71],[114.89,-29.15],[115.73,-31.76],[115.07,-34.29],[117.6,-35.14],[120.04,-33.91],[123.54,-33.96],[124.08,-33.2],[126.16,-32.24],[131.17,-31.46],[134.25,-32.54],[135.92,-34.53],[137.54,-34],[139.25,-35.33],[140.41,-37.94],[143.49,-38.86],[144.92,-37.83],[145.89,-38.82],[147.97,-37.88],[149.98,-37.5],[150.1,-36.05],[151.43,-33.37],[152.73,-31.86],[153.64,-28.73],[152.9,-25.31],[149.77,-22.45],[148.54,-20.09],[146.6,-19.18],[145.4,-16.43],[145.22,-14.85],[143.79,-14.45],[143.45,-12.6],[142.11,-10.99],[141.48,-13.97],[141.7,-15.15],[140.9,-17.34],[140.06,-17.73],[135.5,-15],[135.96,-13.24],[135.29,-12.29],[132.65,-11.64],[131.12,-12.16],[129.66,-14.05]]],[[[109.37,-0.64],[110.09,-1.41],[110.25,-2.9],[111.78,-2.93],[114.62,-3.67],[116.02,-3.59],[116.56,-1.46],[117.51,-0.8],[117.96,1.79],[117.47,4.02],[119.25,5.12],[116.65,6.86],[115.86,5.56],[114.31,4.62],[112.91,3.11],[111.43,2.71],[110.95,1.51],[109.04,1.51],[109.37,-0.64]]],[[[-56.4,50.41],[-59.42,47.89],[-53.84,47.41],[-53.28,48.38],[-55.26,49.25],[-56.4,50.41]]],[[[-85.47,63.09],[-82.1,64.69],[-86.02,65.66],[-85.47,63.09]]],[[[-113.62,68.85],[-112.67,68.47],[-105.77,69.17],[-103.43,68.78],[-100.87,69.79],[-104.57,71.06],[-106.86,73.31],[-110.74,72.57],[-114.56,73.38],[-119.09,71.91],[-116.55,69.41],[-113.62,68.85]]],[[[-81.26,73.56],[-86.72,73.84],[-90.11,71.91],[-88.69,70.46],[-84.33,69.98],[-78.67,69.96],[-73.2,68.27],[-72.27,67.28],[-77.39,65.47],[-74.71,64.37],[-69.2,62.44],[-64.54,63.74],[-62.47,66.19],[-64.07,67.6],[-68.37,68.63],[-68.31,70.56],[-75.23,72.5],[-79.18,72.36],[-81.26,73.56]]],[[[-101.09,73.33],[-99.22,71.34],[-96.57,71.82],[-97.22,73.86],[-101.09,73.33]]],[[[-116.17,73.29],[-121.52,74.55],[-125.68,72.22],[-123.13,71.08],[-116.17,73.29]]],[[[-110,76.48],[-115.5,76.45],[-111.68,74.49],[-105.85,75.19],[-110,76.48]]],[[[-90,76.36],[-91.54,74.65],[-81.78,74.46],[-81.1,75.75],[-88.2,75.53],[-90,76.36]]],[[[-94.34,81.11],[-96.59,79.85],[-92.92,78.42],[-88.78,78.17],[-84.93,79.26],[-94.34,81.11]]],[[[-85.38,81.86],[-91.73,81.73],[-86.51,80.3],[-84.33,78.97],[-87.48,78.45],[-85.19,76.28],[-78.37,76.49],[-75.02,79.37],[-71.14,79.78],[-61.16,82.24],[-69.67,83.11],[-79.37,82.97],[-85.38,81.86]]],[[[109.75,18.27],[110.96,19.97],[109.34,19.96],[108.68,18.51],[109.75,18.27]]],[[[-75.11,19.98],[-75.43,20.75],[-78.71,22.39],[-80.76,23.1],[-81.82,22.18],[-78.62,21.5],[-77.74,19.86],[-75.11,19.98]]],[[[-71.68,18],[-69.22,18.46],[-69.96,19.68],[-72.8,19.96],[-72.69,18.44],[-71.68,18]]],[[[-38.67,83.2],[-47.34,81.99],[-55.24,82.34],[-67.46,80.35],[-64.62,79.57],[-72.38,78.53],[-71.36,77.01],[-66.78,75.96],[-60.9,76.17],[-56.78,74.82],[-55.24,71.93],[-51.41,71.11],[-51.53,68.52],[-53.73,67.79],[-53.66,66.1],[-51.5,63.57],[-48.54,61.19],[-42.89,60.57],[-40.6,63.74],[-39.89,65.57],[-36.08,65.92],[-32.01,68.13],[-25.75,68.87],[-22.79,70.09],[-27.68,70.92],[-22.55,71.92],[-25.39,73.45],[-20.54,73.45],[-19.36,75.3],[-21.3,78.25],[-19.07,79.74],[-11.61,81.3],[-21.32,81.45],[-21.07,82.82],[-27.19,83.55],[-38.67,83.2]]],[[[-21.24,65.44],[-21.27,63.89],[-18.71,63.39],[-14.54,64.4],[-14.96,66.29],[-20.42,66.09],[-21.24,65.44]]],[[[106.91,-6.11],[106.39,-7.39],[110.66,-8.17],[114.04,-8.65],[112.56,-6.89],[110.71,-6.47],[108.69,-6.82],[106.91,-6.11]]],[[[134.92,-3.27],[134.17,-2.36],[133.9,-0.73],[132.73,-0.34],[131.23,-0.84],[132.92,-3.57],[134.13,-3.76],[137.89,-5.38],[139.17,-8.11],[141.14,-9.25],[142.58,-9.34],[144.44,-7.58],[145.99,-8.05],[147.63,-10],[148.58,-9.08],[147.21,-7.47],[147.6,-6.08],[145.74,-5.43],[144.55,-3.83],[139.79,-2.36],[137.92,-1.47],[134.92,-3.27]]],[[[123.53,0.89],[121.53,1.08],[119.89,0.46],[118.9,-3.56],[119.37,-5.41],[120.35,-5.52],[120.42,-3.27],[121.6,-4.06],[122.26,-3.03],[120.06,-0.66],[120.32,0.41],[123.53,0.89]]],[[[95.55,4.69],[96.88,3.69],[97.67,2.4],[98.81,1.67],[99.14,0.24],[100.35,-0.87],[100.88,-2.31],[102.3,-4],[104.58,-5.88],[105.9,-4.94],[105.61,-2.45],[103.65,-0.97],[103.45,0.51],[100.55,2.15],[97.51,5.24],[95.55,4.69]]],[[[15.63,38.24],[12.47,37.7],[15.14,36.68],[15.63,38.24]]],[[[129.85,33.36],[130.18,31.78],[131.33,31.38],[131.74,33.58],[129.85,33.36]]],[[[134.25,34.7],[135.76,33.48],[137.32,34.8],[140.88,35.73],[140.95,38.16],[142.06,39.47],[140.98,41.5],[139.27,38.01],[136.76,36.84],[135.81,35.53],[134.25,34.7]]],[[[142.37,42.33],[144.2,42.99],[144.79,43.93],[143.02,44.55],[141.43,43.33],[142.37,42.33]]],[[[48.02,-14.06],[47.04,-15.25],[44.92,-16.19],[43.93,-17.49],[44.47,-19.98],[43.23,-22.04],[44.07,-25.03],[45.5,-25.58],[47.19,-24.81],[47.93,-22.37],[49.34,-18.39],[50.47,-15.27],[49.94,-13.06],[48.02,-14.06]]],[[[166.84,-45.57],[169.37,-46.64],[171.35,-44.28],[174.27,-41.74],[172.68,-40.71],[171.1,-42.6],[168.37,-44],[166.84,-45.57]]],[[[174.83,-36.37],[174.84,-37.8],[173.93,-39.53],[176.62,-40.49],[176.89,-39.38],[178.34,-38.41],[175.93,-37.56],[174.83,-36.37]]],[[[123.77,8.54],[124.31,6.11],[125.42,5.57],[126.58,7.22],[125.89,9.52],[124.72,8.47],[123.77,8.54]]],[[[121.43,18.32],[120.34,17.53],[120.1,14.75],[120.62,13.86],[122.47,17.01],[121.43,18.32]]],[[[-180,68.96],[-180,65.04],[-179.27,65.63],[-173.11,64.25],[-171.64,66.85],[-180,68.96]]],[[[67.52,76.95],[58.07,75.66],[53.8,73.71],[56.74,73.24],[62.03,75.43],[68.28,76.22],[67.52,76.95]]],[[[80,6.41],[81.81,6.7],[81.37,8.48],[80.3,9.71],[79.78,7.99],[80,6.41]]],[[[-7.25,55.07],[-10.08,54.26],[-9.3,51.48],[-6.34,52.43],[-6.24,53.86],[-7.25,55.07]]],[[[-3.51,56],[-1.77,57.46],[-5.01,58.62],[-5.97,56.78],[-2.9,53.73],[-4.83,52.02],[-3.65,50.22],[0.97,50.91],[1.77,52.48],[0.34,53.1],[-1.64,55.58],[-3.51,56]]],[[[15.89,79.5],[10.68,79.54],[13.91,77.53],[19.06,78.38],[15.89,79.5]]],[[[44.26,-67.97],[29.48,-70.39],[26.6,-71.02],[14.01,-70.19],[5.08,-70.63],[-0.66,-71.39],[-9.26,-71.11],[-11.17,-72.4],[-15.4,-73.08],[-18.44,-75.43],[-25.82,-75.94],[-33.57,-77.32],[-35.9,-78.88],[-29.28,-80.11],[-40.8,-81.16],[-44.01,-82.44],[-53.75,-82.23],[-59.05,-83.38],[-68.17,-81.17],[-75.61,-80.68],[-77.24,-78.61],[-69.93,-76.45],[-63.86,-75.69],[-60.4,-73.02],[-62.27,-69.56],[-65.09,-68.69],[-65.45,-67.44],[-62.05,-65.32],[-63.87,-65.1],[-67.43,-67.12],[-66.68,-69.1],[-68.24,-69.74],[-66.54,-72.1],[-67.66,-72.92],[-73.56,-73.72],[-79.33,-73.08],[-81.89,-73.92],[-86.01,-73.17],[-95.85,-73.33],[-100.15,-73.75],[-103.41,-75.07],[-110.97,-75.2],[-114.82,-74.47],[-127.55,-74.7],[-135.66,-74.74],[-148.24,-76.1],[-148.71,-77.66],[-156.59,-77.05],[-159.09,-77.9],[-150.91,-79.6],[-153.31,-82.57],[-168.81,-84.65],[-179.78,-84.36],[-180,-84.36],[-180,-90],[-135,-90],[-90,-90],[-45,-90],[0,-90],[45,-90],[90,-90],[135,-90],[180,-90],[180,-84.36],[161.18,-81.52],[160.45,-79.17],[164.23,-78.58],[162.8,-74.69],[169.54,-73.08],[170.87,-71.82],[166.27,-70.52],[161.4,-70.65],[158.23,-69.16],[149.13,-68.39],[143.35,-66.85],[133.11,-66.07],[129.49,-67.01],[125.91,-66.34],[120.99,-67.15],[116.61,-67.09],[113.26,-65.84],[107.81,-66.57],[102.72,-65.86],[93.85,-66.68],[84.63,-66.99],[79.18,-68.14],[75.43,-69.81],[73.29,-69.78],[69.73,-71.99],[68.67,-69.97],[69.75,-67.71],[58.61,-67.31],[55.35,-65.91],[52.28,-65.94],[50.93,-67],[44.26,-67.97]]],[[[-75.07,-71.74],[-72.21,-72.7],[-68.41,-72.3],[-70.71,-70.86],[-75.07,-71.74]]],[[[-47.16,-77.8],[-53.22,-80.24],[-43.29,-80.16],[-44.54,-78],[-47.16,-77.8]]]]} +]} \ No newline at end of file diff --git a/test/graticule-test.js b/test/graticule-test.js index 3014d36ad..774d2c8fc 100644 --- a/test/graticule-test.js +++ b/test/graticule-test.js @@ -1,7 +1,48 @@ var api = require('../'), assert = require('assert'); +function hasOutline(json) { + return json.features.some(feat => { + return feat.properties.type == 'outline'; + }); +} + +function hasMeridians(arr) { + return function(json) { + arr.every(lon => { + return json.features.some(feat => { + return feat.properties.type == 'meridian' && feat.properties.value == lon; + }); + }); + } +} + +function projTest(str, test) { + var path = 'test/data/world_land.json'; + it(str, function(done) { + var cmd = `-i ${path} -proj ${str} densify -graticule -o graticule.json`; + api.applyCommands(cmd, {}, function(err, out) { + var json = JSON.parse(out['graticule.json']); + test(json); + done(); + }); + }); +} + describe('mapshaper-graticule.js', function () { + // Test of graticule outlines and edge meridians + // ... also should catch some projection failures + describe('proj tests', function () { + projTest('+proj=merc', hasMeridians([-180, 180])); + projTest('wgs84', hasMeridians([-180, 180])); + projTest('aea', hasMeridians([-180, 180])); + projTest('+proj=aea +lat_1=30 +lat_2=55', hasMeridians([-180, 180])); + projTest('+proj=bertin1953', hasOutline); + projTest('+proj=cupola', hasMeridians([168.977, 180])); + projTest('+proj=ortho', hasOutline); + projTest('+proj=ortho +lat_0=60 +lon_0=-120', hasOutline); + projTest('+proj=nsper +lat_0=30 +lon_0=80 +h=1e8', hasOutline); + }) it('create latlong graticule if no data has been loaded', function(done) { @@ -22,5 +63,5 @@ describe('mapshaper-graticule.js', function () { done(); }); }) - }); + From 8ce4ab6b8e556120086cf3db47de29b405f67369 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 20 May 2021 12:28:31 -0400 Subject: [PATCH 042/891] v0.5.55 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c06d303d3..4dedff263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.55 +* Improved support for projected graticules. +* Fixed shape clipping for the bertin1953 projection. + v0.5.54 * Fix for issue #485 (error when using * wildcard to match all files in a directory) diff --git a/package.json b/package.json index 7db0a2061..c06296c5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.54", + "version": "0.5.55", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 75317d30e7f95332d2857d4a616f83956f361bc1 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 20 May 2021 12:29:31 -0400 Subject: [PATCH 043/891] Check in package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 1a4a89ed4..3e6f140e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.54", + "version": "0.5.55", "lockfileVersion": 1, "requires": true, "dependencies": { From 2689962e567dca05772fc0f0ce6a0224ecd0f2a2 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 20 May 2021 14:34:00 -0400 Subject: [PATCH 044/891] Comment --- src/geom/mapshaper-antimeridian.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/geom/mapshaper-antimeridian.js b/src/geom/mapshaper-antimeridian.js index 8905995aa..71b1f0283 100644 --- a/src/geom/mapshaper-antimeridian.js +++ b/src/geom/mapshaper-antimeridian.js @@ -4,7 +4,7 @@ import { PointIter } from '../paths/mapshaper-shape-iter'; import utils from '../utils/mapshaper-utils'; import { getStateVar } from '../mapshaper-state'; -// Removes one or two antimeridian crossings from a circular ring +// Removes antimeridian crossings from polygon and polyline paths // TODO: handle edge case: segment is collinear with antimeridian // TODO: handle edge case: path coordinates exceed the standard lat-long range // From ca964214a053039907efbf1b02d2a1a467c6c05e Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sat, 22 May 2021 09:59:05 -0400 Subject: [PATCH 045/891] Spruce up the UI --- src/gui/gui-layer-control.js | 3 +- www/elements.css | 14 +++++---- www/page.css | 58 ++++++++++++++++++++++++------------ 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/gui/gui-layer-control.js b/src/gui/gui-layer-control.js index 80ee874ba..21ed23e9c 100644 --- a/src/gui/gui-layer-control.js +++ b/src/gui/gui-layer-control.js @@ -13,7 +13,6 @@ export function LayerControl(gui) { var model = gui.model; var el = gui.container.findChild(".layer-control").on('click', GUI.handleDirectEvent(gui.clearMode)); var btn = gui.container.findChild('.layer-control-btn'); - var buttonLabel = btn.findChild('.layer-name'); var isOpen = false; var cache = new DomCache(); var pinAll = el.findChild('.pin-all'); // button for toggling layer visibility @@ -115,7 +114,7 @@ export function LayerControl(gui) { function updateMenuBtn() { var name = model.getActiveLayer().layer.name || "[unnamed layer]"; - buttonLabel.html(name + "  ▼"); + btn.classed('active', 'true').findChild('.layer-name').html(name + "  ▼"); } function render() { diff --git a/www/elements.css b/www/elements.css index 440226641..7506b96b0 100644 --- a/www/elements.css +++ b/www/elements.css @@ -113,12 +113,16 @@ div.tip { /* -------- BUTTONS ---------- */ -.header-btn { +.btn.header-btn { color: #fff; border: none; - padding: 3px 7px 4px 7px; - border-radius: 3px; - margin-top: 5px; + margin-top: 0px; + height: 33px; + box-sizing: border-box; + border-radius: 1px; + font-size: 14px; + font-weight: 700; + padding: 9px 8px 0px 8px; } .page-header .header-btn.disabled, @@ -126,7 +130,7 @@ div.tip { background-color: transparent; } -.page-header .header-btn.active { +.page-header .btn.header-btn.active { background-color: black; } diff --git a/www/page.css b/www/page.css index ffc8f6d74..7662f1e7a 100644 --- a/www/page.css +++ b/www/page.css @@ -44,7 +44,7 @@ html, body { body { overflow: hidden; background-color: #fff; - font: 15px/1.4 'Source Sans Pro', Helvetica, sans-serif; + font: 14px/1.4 'Source Sans Pro', Helvetica, sans-serif; color: #444; user-select: none; -webkit-user-select: none; @@ -64,15 +64,21 @@ body { .page-header, .dialog-btn { - background-color: #1385b7; + background-color: #1385B7; } -.colored-text { - color: #0774a5; +.colored-text, +.nav-menu-item { + color: #10699b; +} + +.dot-underline, +.zoom-box.zooming { + border-color: #10699b; } .dot-underline { - border-bottom: 1px dotted #0774a5; + border-bottom: 1px dotted; } .dot-underline-black { @@ -80,14 +86,14 @@ body { } .nav-btn * { - fill: #1385b7; + fill: #1385B7; } .dialog-btn:hover, -.header-btn:hover, +.btn.header-btn:hover, .dialog-btn.default-btn, .dialog-btn.selected-btn { - background-color: #0F5A84; + background-color: #1A6A96; } .colored-text::selection { @@ -167,7 +173,7 @@ body { left: 0; z-index: 40; width: 100%; - height: 34px; + height: 33px; } .coordinate-info { @@ -182,8 +188,8 @@ body { .mapshaper-logo { font-weight: bold; - font-size: 18px; - margin: 3px 0 0 12px; + font-size: 17px; + margin: 4px 0 0 11px; } .mapshaper-logo .logo-highlight { @@ -207,6 +213,7 @@ body { border-left: 1px solid white; height: 10px; margin: 0 2px 0 2px; + display: none; } /* --- Selection tool dialog --------- */ @@ -266,7 +273,7 @@ body { right: 0; bottom: 0; left: 0; - margin: 34px 0 0 0; + margin: 33px 0 0 0; } @@ -524,6 +531,8 @@ body.dragover #import-options-drop-area .drop-area { } .option-menu input { + position: relative; + top: 2px; width: 12px; height: 12px; } @@ -676,6 +685,21 @@ body.console-open .map-area { text-align: center; z-index: 10; width: 100%; + display: none; +} + +.layer-control-btn.active { + display: block; +} + +.layer-control-btn > .btn.header-btn { + background-color: #1A6A96; + padding-left: 12px; + padding-right: 12px; +} + +.layer-control-btn > .btn.header-btn:hover { + background-color: #094B70; } body.simplify .layer-control-btn { @@ -1082,7 +1106,6 @@ img.close-btn:hover, .nav-menu-item { background-color: #fff; - color: #0774a5; /* .colored-text color */ float: right; clear: right; white-space: nowrap; @@ -1139,24 +1162,21 @@ img.close-btn:hover, pointer-events: none; } -.zoom-box.zooming { - border-color: #0774a5; -} /* Simplification control ------------ */ @media (max-width: 765px) { body.simplify .simplify-control-wrapper { - top: 22px; + top: 26px; } body.simplify .page-header { - height: 52px; + height: 57px; } body.simplify .main-area { - margin-top: 52px; + margin-top: 57px; } } From 2d8c9e3b9db5932334afbf53e651a14524d14809 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 24 May 2021 13:16:55 -0400 Subject: [PATCH 046/891] CSS styles --- www/elements.css | 4 ++-- www/page.css | 42 ++++++++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/www/elements.css b/www/elements.css index 7506b96b0..ef7f86ff0 100644 --- a/www/elements.css +++ b/www/elements.css @@ -117,12 +117,12 @@ div.tip { color: #fff; border: none; margin-top: 0px; - height: 33px; + height: 31px; box-sizing: border-box; border-radius: 1px; font-size: 14px; font-weight: 700; - padding: 9px 8px 0px 8px; + padding: 8px 8px 0px 8px; } .page-header .header-btn.disabled, diff --git a/www/page.css b/www/page.css index 7662f1e7a..f1c63f5ac 100644 --- a/www/page.css +++ b/www/page.css @@ -173,7 +173,7 @@ body { left: 0; z-index: 40; width: 100%; - height: 33px; + height: 31px; } .coordinate-info { @@ -189,7 +189,7 @@ body { .mapshaper-logo { font-weight: bold; font-size: 17px; - margin: 4px 0 0 11px; + margin: 3px 0 0 11px; } .mapshaper-logo .logo-highlight { @@ -206,7 +206,7 @@ body { top: 0px; right: 0px; display: none; - margin: 0 8px 3px 0; + margin: 0 6px 3px 0; } .separator { @@ -273,7 +273,7 @@ body { right: 0; bottom: 0; left: 0; - margin: 33px 0 0 0; + margin: 31px 0 0 0; } @@ -681,11 +681,12 @@ body.console-open .map-area { .layer-control-btn { top: 0; + width: 100%; position: absolute; text-align: center; - z-index: 10; - width: 100%; + z-index: 30; display: none; + pointer-events: none; } .layer-control-btn.active { @@ -693,9 +694,13 @@ body.console-open .map-area { } .layer-control-btn > .btn.header-btn { + white-space: nowrap; + position: relative; background-color: #1A6A96; padding-left: 12px; padding-right: 12px; + pointer-events: auto; + margin-right: 40px; } .layer-control-btn > .btn.header-btn:hover { @@ -804,7 +809,7 @@ img.close-btn:hover, } .layer-item .row { - line-height: 18px; + line-height: 17px; margin-bottom: 1px; white-space: nowrap; } @@ -1062,8 +1067,8 @@ img.close-btn:hover, .nav-buttons { z-index: 20; position: absolute; - top: 6px; - right: 9px; + top: 7px; + right: 7px; padding: 2px; background-color: rgba(255, 255, 255, 0.85); } @@ -1111,7 +1116,7 @@ img.close-btn:hover, white-space: nowrap; display: inline-block; padding: 2px 7px 4px 7px; - line-height: 13px; + line-height: 11px; cursor: pointer; } @@ -1133,20 +1138,25 @@ img.close-btn:hover, .nav-btn { position: relative; cursor:pointer; - padding: 3px; - margin-bottom: 1px; + padding: 3px 3px 5px 3px; line-height: 1; } +.nav-btn.menu-btn { + padding: 2px 3px; +} + .nav-btn.selected { background-color: black; - border-radius: 4px; + border-radius: 3px; } -.nav-btn.hover:not(.disabled) svg * { - fill: black; + +.nav-btn:hover:not(.disabled) svg *, +.nav-btn.hover.menu-btn:not(.disabled) svg * { + fill: #1A6A96; } -.nav-btn.selected:not(.disabled) svg * { +.nav-btn.menu-btn.selected:not(.disabled) svg * { fill: white; } From 56aa568124a310f820edc7e75f152f3b3e7914bc Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 24 May 2021 13:18:26 -0400 Subject: [PATCH 047/891] allow bare proj names in proj definitions with options --- src/crs/mapshaper-projections.js | 9 +++++++-- test/projections-test.js | 13 +++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/crs/mapshaper-projections.js b/src/crs/mapshaper-projections.js index 7e77f3c0f..288102c9e 100644 --- a/src/crs/mapshaper-projections.js +++ b/src/crs/mapshaper-projections.js @@ -124,10 +124,15 @@ export function crsAreEqual(a, b) { export function getProjDefn(str) { var mproj = require('mproj'); var defn; + // prepend '+proj=' to bare proj names + str = str.replace(/(^| )([\w]+)($| )/, function(a, b, c, d) { + if (c in mproj.internal.pj_list) { + return b + '+proj=' + c + d; + } + return a; + }); if (looksLikeProj4String(str)) { defn = str; - } else if (str in mproj.internal.pj_list) { - defn = '+proj=' + str; } else if (str in projectionAliases) { defn = projectionAliases[str]; // defn is a function } else if (looksLikeInitString(str)) { diff --git a/test/projections-test.js b/test/projections-test.js index 04f1099d0..4d05a8ad2 100644 --- a/test/projections-test.js +++ b/test/projections-test.js @@ -33,6 +33,19 @@ describe('mapshaper-projections.js', function() { invalid('-proj merc +ellps=sphere'); }) + describe('getProjDefn()', function () { + test('merc', '+proj=merc'); + test('merc +lon_0=60', '+proj=merc +lon_0=60'); + test('wgs84', '+proj=longlat +datum=WGS84'); + + function test(src, target) { + var getProjDefn = api.internal.getProjDefn; + it(src + ' -> ' + target, function() { + assert.equal(getProjDefn(src), target); + }) + } + }) + describe('findProjLibs()', function() { it('tests', function() { From 38e38bab13969a37435616125401176cc242e71b Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 24 May 2021 13:21:47 -0400 Subject: [PATCH 048/891] Projection improvements; added -graticule polygon options --- src/buffer/mapshaper-point-buffer.js | 36 +++++-- src/cli/mapshaper-options.js | 4 + src/commands/mapshaper-clip-erase.js | 3 + src/commands/mapshaper-graticule.js | 27 +++-- src/commands/mapshaper-proj.js | 6 +- src/commands/mapshaper-rotate.js | 122 ++++++++++++++++++---- src/crs/mapshaper-densify.js | 4 +- src/crs/mapshaper-proj-extents.js | 28 ++++- src/crs/mapshaper-spherical-clipping.js | 2 +- src/crs/mapshaper-spherical-cutting.js | 12 ++- src/dataset/mapshaper-dataset-editor.js | 3 - src/geom/mapshaper-antimeridian.js | 130 +++++++++++++----------- src/gui/gui-interaction-mode-control.js | 2 +- src/paths/mapshaper-coordinate-utils.js | 72 +++++++++++++ src/polygons/mapshaper-slivers.js | 4 +- test/graticule-test.js | 22 ++++ 16 files changed, 362 insertions(+), 115 deletions(-) create mode 100644 src/paths/mapshaper-coordinate-utils.js diff --git a/src/buffer/mapshaper-point-buffer.js b/src/buffer/mapshaper-point-buffer.js index 63ab5f6ee..37ae55c99 100644 --- a/src/buffer/mapshaper-point-buffer.js +++ b/src/buffer/mapshaper-point-buffer.js @@ -2,8 +2,16 @@ import { getPreciseGeodeticSegmentFunction, getFastGeodeticSegmentFunction } fro import { getBufferDistanceFunction } from '../buffer/mapshaper-buffer-common'; import { importGeoJSON } from '../geojson/geojson-import'; import { getDatasetCRS } from '../crs/mapshaper-projections'; -import { removeAntimeridianCrosses } from '../geom/mapshaper-antimeridian'; +import { removePolylineCrosses, removePolygonCrosses, countCrosses } + from '../geom/mapshaper-antimeridian'; import { getCRS } from '../crs/mapshaper-projections'; +import { getSphericalPathArea2 } from '../geom/mapshaper-polygon-geom'; +import { PointIter } from '../paths/mapshaper-shape-iter'; + +function ringArea(ring) { + var iter = new PointIter(ring); + return getSphericalPathArea2(iter); +} export function makePointBuffer(lyr, dataset, opts) { var geojson = makePointBufferGeoJSON(lyr, dataset, opts); @@ -40,17 +48,27 @@ function makePointBufferGeoJSON(lyr, dataset, opts) { } export function getPointBufferPolygon(points, distance, vertices, geod) { - var rings = [], coords; + var rings = [], coords, coords2; if (!points || !points.length) return null; for (var i=0; i 0) rings.push(coords.pop()); + if (countCrosses(coords) > 0) { + coords2 = removePolygonCrosses([coords]); + while (coords2.length > 0) rings.push([coords2.pop()]); // geojson polygon coords, no hole + } else if (ringArea(coords) < 0) { + // negative spherical area: CCW ring, indicating a circle of >180 degrees + // that fully encloses both poles and the antimeridian. + // need to add an enclosure around the entire sphere + // TODO: compare to distance param as a sanity check + rings.push([ + [[180, 90], [180, -90], [0, -90], [-180, -90], [-180, 90], [0, 90], [180, 90]], + coords + ]); + } else { + rings.push([coords]); + } } - return rings.length == 1 ? { - type: 'Polygon', - coordinates: rings[0] - } : { + return { type: 'MultiPolygon', coordinates: rings }; @@ -61,7 +79,7 @@ export function getPointBufferLineString(points, distance, vertices, geod) { if (!points || !points.length) return null; for (var i=0; i 0) rings.push(coords.pop()); } return rings.length == 1 ? { diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index a14ca17a0..72562b577 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -858,6 +858,10 @@ export function getOptionParser() { .option('interval', { describe: 'size of grid cells in degrees (options: 5 10 15 30 45, default is 10)', type: 'number' + }) + .option('polygon', { + describe: 'create a polygon to match the outline of the graticule', + type: 'flag' }); parser.command('grid') diff --git a/src/commands/mapshaper-clip-erase.js b/src/commands/mapshaper-clip-erase.js index c7a32c36b..9fd5f4f47 100644 --- a/src/commands/mapshaper-clip-erase.js +++ b/src/commands/mapshaper-clip-erase.js @@ -12,6 +12,7 @@ import { stop, message } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import { ArcCollection } from '../paths/mapshaper-arcs'; import { NodeCollection } from '../topology/mapshaper-nodes'; +import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; cmd.clipLayers = function(target, src, dataset, opts) { return clipLayers(target, src, dataset, "clip", opts); @@ -39,11 +40,13 @@ cmd.sliceLayer = function(targetLyr, src, dataset, opts) { export function clipLayersInPlace(layers, clipSrc, dataset, type, opts) { var outputLayers = clipLayers(layers, clipSrc, dataset, type, opts); + // remove arcs from the clipping dataset, if they are not used by any layer layers.forEach(function(lyr, i) { var lyr2 = outputLayers[i]; lyr.shapes = lyr2.shapes; lyr.data = lyr2.data; }); + dissolveArcs(dataset); } // @clipSrc: layer in @dataset or filename diff --git a/src/commands/mapshaper-graticule.js b/src/commands/mapshaper-graticule.js index 092d902f4..216fff03e 100644 --- a/src/commands/mapshaper-graticule.js +++ b/src/commands/mapshaper-graticule.js @@ -3,7 +3,7 @@ import { projectDataset } from '../commands/mapshaper-proj'; import { getDatasetCRS, getCRS, isLatLngDataset } from '../crs/mapshaper-projections'; import { isMeridianBounded, getBoundingMeridian } from '../crs/mapshaper-proj-info'; import { getAntimeridian } from '../geom/mapshaper-latlon'; -import { getOutlineDataset } from '../crs/mapshaper-proj-extents'; +import { getOutlineDataset, getPolygonDataset } from '../crs/mapshaper-proj-extents'; import { densifyPathByInterval } from '../crs/mapshaper-densify'; import { stop } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; @@ -16,19 +16,34 @@ import { cleanLayers } from '../commands/mapshaper-clean'; import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; cmd.graticule = function(dataset, opts) { + var name = opts.polygon ? 'polygon' : 'graticule'; var graticule, dest; if (dataset && !isLatLngDataset(dataset)) { // project graticule to match dataset dest = getDatasetCRS(dataset); if (!dest) stop("Coordinate system is unknown, unable to create a graticule"); - graticule = createProjectedGraticule(dest, opts); + graticule = opts.polygon ? + createProjectedPolygon(dest, opts) : + createProjectedGraticule(dest, opts); } else { - graticule = createUnprojectedGraticule(opts); + graticule = opts.polygon ? + createUnprojectedPolygon(opts) : + createUnprojectedGraticule(opts); } - graticule.layers[0].name = 'graticule'; + graticule.layers[0].name = name; return graticule; }; +function createUnprojectedPolygon(opts) { + var crs = getCRS('wgs84'); + return getPolygonDataset(crs, crs, opts); +} + +function createProjectedPolygon(dest, opts) { + var src = getCRS('wgs84'); + return getPolygonDataset(src, dest, opts); +} + function createUnprojectedGraticule(opts) { var src = getCRS('wgs84'); var graticule = importGeoJSON(createGraticule(src, false, opts)); @@ -37,11 +52,11 @@ function createUnprojectedGraticule(opts) { function createProjectedGraticule(dest, opts) { var src = getCRS('wgs84'); - var outline = getOutlineDataset(src, dest, {inset: 0, geometry_type: 'polyline'}); + // var outline = getOutlineDataset(src, dest, {inset: 0, geometry_type: 'polyline'}); + var outline = getOutlineDataset(src, dest, {}); var graticule = importGeoJSON(createGraticule(dest, !!outline, opts)); projectDataset(graticule, src, dest, {no_clip: false}); // TODO: densify? if (outline) { - projectDataset(outline, src, dest, {no_clip: true}); graticule = addOutlineToGraticule(graticule, outline); } buildTopology(graticule); // needed for cleaning to work diff --git a/src/commands/mapshaper-proj.js b/src/commands/mapshaper-proj.js index 9fccac542..f545a378a 100644 --- a/src/commands/mapshaper-proj.js +++ b/src/commands/mapshaper-proj.js @@ -156,10 +156,10 @@ export function projectDataset(dataset, src, dest, opts) { cleanProjectedLayers(dataset); } - if (badArcs > 0) { + if (badArcs > 0 && !opts.quiet) { message(`Removed ${badArcs} ${badArcs == 1 ? 'path' : 'paths'} containing unprojectable vertices.`); } - if (badPoints > 0) { + if (badPoints > 0 && !opts.quiet) { message(`Removed ${badPoints} unprojectable ${badPoints == 1 ? 'point' : 'points'}.`); } dataset.info.crs = dest; @@ -169,7 +169,7 @@ export function projectDataset(dataset, src, dest, opts) { // * Removes line intersections // * TODO: what if a layer contains polygons with desired overlaps? should // we ignore overlaps between different features? -function cleanProjectedLayers(dataset) { +export function cleanProjectedLayers(dataset) { // TODO: only clean affected polygons (cleaning all polygons can be slow) var polygonLayers = dataset.layers.filter(lyr => lyr.geometry_type == 'polygon'); // clean options: force a topology update (by default, this only happens when diff --git a/src/commands/mapshaper-rotate.js b/src/commands/mapshaper-rotate.js index 362feae60..871b55bec 100644 --- a/src/commands/mapshaper-rotate.js +++ b/src/commands/mapshaper-rotate.js @@ -1,12 +1,25 @@ import cmd from '../mapshaper-cmd'; import { rotateDatasetCoords, getRotationFunction2 } from '../crs/mapshaper-spherical-rotation'; -import { removeAntimeridianCrosses } from '../geom/mapshaper-antimeridian'; +import { removePolygonCrosses, removePolylineCrosses } from '../geom/mapshaper-antimeridian'; import { DatasetEditor } from '../dataset/mapshaper-dataset-editor'; import { getDatasetCRS, isLatLngCRS } from '../crs/mapshaper-projections'; import { getPlanarPathArea, getSphericalPathArea } from '../geom/mapshaper-polygon-geom'; import { densifyDataset, densifyPathByInterval } from '../crs/mapshaper-densify'; -import { stop } from '../utils/mapshaper-logging'; +import { cleanProjectedLayers } from '../commands/mapshaper-proj'; +import { stop, error, debug } from '../utils/mapshaper-logging'; +import { buildTopology } from '../topology/mapshaper-topology'; +import { + samePoint, + snapToEdge, + isEdgeSegment, + isEdgePoint, + isWholeWorld, + touchesEdge, + onPole, + isClosedPath, + lastEl +} from '../paths/mapshaper-coordinate-utils'; cmd.rotate = rotateDataset; @@ -15,35 +28,102 @@ export function rotateDataset(dataset, opts) { stop('Command requires a lat-long dataset.'); } if (!Array.isArray(opts.rotation) || !opts.rotation.length) { - stop('Invalid rotation parameter.'); + stop('Invalid rotation parameter'); } var rotatePoint = getRotationFunction2(opts.rotation, opts.invert); var editor = new DatasetEditor(dataset); - var originalArcs; if (dataset.arcs) { dataset.arcs.flatten(); - // make a copy so we can calculate original path winding after rotation - originalArcs = dataset.arcs.getCopy(); } dataset.layers.forEach(function(lyr) { var type = lyr.geometry_type; - editor.editLayer(lyr, function(coords, i, shape) { - if (type == 'point') { - coords.forEach(rotatePoint); - return coords; - } + editor.editLayer(lyr, getGeometryRotator(type, rotatePoint)); + }); + editor.done(); + buildTopology(dataset); + cleanProjectedLayers(dataset); +} + +function getGeometryRotator(layerType, rotatePoint) { + var rings; + if (layerType == 'point') { + return function(coords) { + coords.forEach(rotatePoint); + return coords; + }; + } + if (layerType == 'polyline') { + return function(coords) { coords = densifyPathByInterval(coords, 0.5); coords.forEach(rotatePoint); - if (type == 'polyline') { - return removeAntimeridianCrosses(coords, type); + return removePolylineCrosses(coords); + }; + } + if (layerType == 'polygon') { + return function(coords, i, shape) { + if (isWholeWorld(coords)) { + coords = densifyPathByInterval(coords, 0.5); + } else { + coords.forEach(snapToEdge); + coords = densifyPathByInterval(coords, 0.5, densifySegment); + coords = removeCutSegments(coords); + coords.forEach(rotatePoint); + coords.forEach(snapToEdge); } - var isHole = type == 'polygon' && getPlanarPathArea(shape[i], originalArcs) < 0; - var coords2 = removeAntimeridianCrosses(coords, type, isHole); - return coords2.reduce(function(memo, polygonCoords) { - return memo.concat(polygonCoords); - }, []); - }); - }); - editor.done(); + if (i === 0) { // first part + rings = []; + } + if (coords.length < 4) { + debug('Short ring', coords); + return; + } + if (!samePoint(coords[0], lastEl(coords))) { + error('Open polygon ring'); + } + rings.push(coords); // accumulate rings + if (i == shape.length - 1) { // last part + return removePolygonCrosses(rings); + } + }; + } + return null; // assume layer has no geometry -- callback should not be called +} + +function densifySegment(a, b) { + return !isEdgeSegment(a, b); +} + +// Remove segments that belong solely to cut points +// TODO: verify that antimeridian crosses have matching y coords +// TODO: stitch together split-apart polygons +// +function removeCutSegments(coords) { + if (!touchesEdge(coords)) return coords; + var coords2 = []; + var a, b, c, x, y; + var skipped = false; + coords.pop(); // remove duplicate point + a = coords[coords.length-1]; + b = coords[0]; + for (var ci=1, n=coords.length; ci <= n; ci++) { + c = ci == n ? coords2[0] : coords[ci]; + if (isEdgePoint(a) && isEdgeSegment(b, c)) { + // skip b + // debug(' interval) { + if (dist > interval && (!filter || filter(a, b))) { pushInterpolatedPoints(coords2, a, b, Math.round(dist / interval) - 1); } coords2.push(b); diff --git a/src/crs/mapshaper-proj-extents.js b/src/crs/mapshaper-proj-extents.js index c773884c3..39911f5ad 100644 --- a/src/crs/mapshaper-proj-extents.js +++ b/src/crs/mapshaper-proj-extents.js @@ -10,10 +10,12 @@ import { importGeoJSON } from '../geojson/geojson-import'; import { verbose, error, message } from '../utils/mapshaper-logging'; import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; import { rotateDataset } from '../commands/mapshaper-rotate'; - +import { projectDataset } from '../commands/mapshaper-proj'; +import { polygonsToLines } from '../commands/mapshaper-lines'; +import { insertPreProjectionCuts } from '../crs/mapshaper-spherical-cutting'; export function getClippingDataset(src, dest, opts) { - var dataset, bbox; + var dataset; if (isCircleClippedProjection(dest) || opts.clip_angle) { dataset = getClipCircle(src, dest, opts); } else if (isClippedCylindricalProjection(dest) || opts.clip_bbox) { @@ -22,12 +24,29 @@ export function getClippingDataset(src, dest, opts) { return dataset || null; } -export function getOutlineDataset(src, dest, opts) { - opts = Object.assign({geometry_type: 'polyline'}, opts); +// Return projected polygon extent of both clipped and unclipped projections +export function getPolygonDataset(src, dest, opts) { + // use clipping area if projection is clipped var dataset = getClippingDataset(src, dest, opts); + if (!dataset) { + // use entire world if projection is not clipped + dataset = getClipRectangle(dest, {clip_bbox: [-180,-90,180,90]}); + } + projectDataset(dataset, src, dest, {no_clip: false, quiet: true}); return dataset; } +// Return projected outline of clipped projections +export function getOutlineDataset(src, dest, opts) { + var dataset = getClippingDataset(src, dest, opts); + if (dataset) { + // project, with cutting & cleanup + projectDataset(dataset, src, dest, {no_clip: false, quiet: true}); + dataset.layers[0].geometry_type = 'polyline'; + } + return dataset || null; +} + function getClipRectangle(dest, opts) { var bbox = opts.clip_bbox || getDefaultClipBBox(dest); var rotation = getRotationParams(dest); @@ -62,6 +81,7 @@ export function isClippedCylindricalProjection(P) { export function getDefaultClipBBox(P) { var e = 1e-3; var bbox = { + // longlat: [-180, -90, 180, 90], merc: [-180, -87, 180, 87], bertin1953: [-180 + e, -90 + e, 180 - e, 90 - e] }[getCrsSlug(P)]; diff --git a/src/crs/mapshaper-spherical-clipping.js b/src/crs/mapshaper-spherical-clipping.js index ace00368f..d206e2322 100644 --- a/src/crs/mapshaper-spherical-clipping.js +++ b/src/crs/mapshaper-spherical-clipping.js @@ -11,7 +11,7 @@ export function preProjectionClip(dataset, src, dest, opts) { // the clipping shape. But how to tell? clipLayersInPlace(dataset.layers, clipData, dataset, 'clip'); // remove arcs outside the clip area, so they don't get projected - dissolveArcs(dataset); + //dissolveArcs(dataset); } return !!clipData; } diff --git a/src/crs/mapshaper-spherical-cutting.js b/src/crs/mapshaper-spherical-cutting.js index ff98c6fd5..f39a8ec88 100644 --- a/src/crs/mapshaper-spherical-cutting.js +++ b/src/crs/mapshaper-spherical-cutting.js @@ -4,6 +4,8 @@ import { layerHasPaths } from '../dataset/mapshaper-layer-utils'; import { getAntimeridian } from '../geom/mapshaper-latlon'; import { clipLayersInPlace } from '../commands/mapshaper-clip-erase'; import { importGeoJSON } from '../geojson/geojson-import'; +import { convertBboxToGeoJSON } from '../commands/mapshaper-rectangle'; +import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; export function insertPreProjectionCuts(dataset, src, dest) { var antimeridian = getAntimeridian(dest.lam0 * 180 / Math.PI); @@ -14,6 +16,7 @@ export function insertPreProjectionCuts(dataset, src, dest) { isRotatedNormalProjection(dest) && datasetCrossesLon(dataset, antimeridian)) { insertVerticalCut(dataset, antimeridian); + dissolveArcs(dataset); return true; } return false; @@ -33,10 +36,9 @@ function insertVerticalCut(dataset, lon) { var pathLayers = dataset.layers.filter(layerHasPaths); if (pathLayers.length === 0) return; var e = 1e-8; - var coords = [[lon+e, 90], [lon+e, -90], [lon-e, -90], [lon-e, 90], [lon+e, 90]]; - var clip = importGeoJSON({ - type: 'Polygon', - coordinates: [coords] - }); + var bbox = [lon-e, -91, lon+e, 91]; + // densify (so cut line can curve, e.g. Cupola projection) + var geojson = convertBboxToGeoJSON(bbox, {interval: 0.5}); + var clip = importGeoJSON(geojson); clipLayersInPlace(pathLayers, clip, dataset, 'erase'); } diff --git a/src/dataset/mapshaper-dataset-editor.js b/src/dataset/mapshaper-dataset-editor.js index 3f77623f2..cf703b1ae 100644 --- a/src/dataset/mapshaper-dataset-editor.js +++ b/src/dataset/mapshaper-dataset-editor.js @@ -27,10 +27,7 @@ export function DatasetEditor(dataset) { var shape2 = [], retn, input; for (var i=0, n=shape ? shape.length : 0; i= R; + return p[0] == -180 || p[0] == 180; +} -// Removes antimeridian crossings from polygon and polyline paths +// Removes antimeridian crossings from an array of polygon rings // TODO: handle edge case: segment is collinear with antimeridian // TODO: handle edge case: path coordinates exceed the standard lat-long range // -// path: a path of [x,y] points. -// type: 'polygon' or 'polyline' -// 'polygon' Assumes a closed ring with CCW winding for holes -// Returns MultiPolygon or MultiLineString coordinates array -export function removeAntimeridianCrosses(path, type, isHole) { - var parts = splitPathAtAntimeridian(path); - - if (type == 'polyline') { - return parts; // MultiLineString coords - } - - // case: polygon does not intersect the antimeridian - if (parts.length == 1 && !isAntimeridanPoint(parts[0][0])) { - // TODO: the area test should not be needed when processing small circles - // (could affect performance when buffering many points) - if (ringArea(path) < 0 && !isHole) { - // negative area: CCW ring, indicating a circle of >180 degrees - // that fully encloses both poles and the antimeridian. - // need to add an enclosure around the entire sphere - parts = [[[180, 90], [180, -90], [0, -90], [-180, -90], [-180, 90], [0, 90], [180, 90]], parts[0]]; +// rings: array of rings of [x,y] points. +// Returns array of split-apart rings +export function removePolygonCrosses(rings, isHole) { + var rings2 = []; + var splitRings = []; + var ring; + for (var i=0; i 1) paths.push(path); + if (splitRings.length > 0) { + rings2 = rings2.concat(reconnectSplitParts(splitRings)); + } + return rings2; } +// Stitch an array of split-apart paths into coordinate rings +// Assumes that the first and last point of each split-apart path is 180 or -180 +// parts: array of paths that have been split at the antimeridian export function reconnectSplitParts(parts) { var yy = getSortedIntersections(parts); var rings = []; @@ -47,9 +53,15 @@ export function reconnectSplitParts(parts) { var errors = 0; parts.forEach(function(part, i) { if (usedParts[i]) return; + if (!isValidSplitPart(part)) { + error('Geometry error'); + } var ring = addPartToRing(part, []); if (ring) { - rings.push([ring]); // multipolygon coords + if (!isClosedPath(ring)) { + error('Generated an open ring'); + } + rings.push(ring); } else { errors++; } @@ -91,6 +103,16 @@ export function reconnectSplitParts(parts) { } } +function addSubPath(paths, path) { + if (path.length > 1) paths.push(path); +} + +function isValidSplitPart(part) { + var lastX = lastEl(part)[0]; + var firstX = part[0][0]; + return (lastX == 180 || lastX == -180) && (firstX == 180 || firstX == -180); +} + // p: last point of previous part function findNextPoint(parts, p, yy) { var x = p[0]; @@ -128,6 +150,18 @@ function findPartStartingAt(parts, firstPoint) { return null; } +export function countCrosses(path) { + var c = 0, pp, p; + for (var i=0, n=path.length; i0 && Math.abs(pp[0] - p[0]) > 180) { + c++; + } + pp = p; + } + return c; +} + export function splitPathAtAntimeridian(path) { var parts = []; var part = []; @@ -151,7 +185,7 @@ export function splitPathAtAntimeridian(path) { // join first and last parts of a split-apart ring, so that the first part // originates at the antimeridian - if (closed && parts.length > 1 && !isAntimeridanPoint(firstPoint)) { + if (closed && parts.length > 1 && !isAntimeridianPoint(firstPoint)) { part = parts.pop(); part.pop(); // remove duplicate point parts[0] = part.concat(parts[0]); @@ -166,38 +200,15 @@ export function getSortedIntersections(parts) { return utils.genericSort(values, true); } -function samePoint(a, b) { - return a[0] === b[0] && a[1] === b[1]; -} -function isAntimeridanPoint(p) { - return p[0] == 180 || p[0] == -180; -} function addIntersectionPoint(part, p, yint) { var xint = p[0] < 0 ? -180 : 180; - if (!isAntimeridanPoint(p)) { // don't a point if p is already on the antimeridian + if (!isAntimeridianPoint(p)) { // don't a point if p is already on the antimeridian part.push([xint, yint]); } } -function ringArea(ring) { - var iter = new PointIter(ring); - return getSphericalPathArea2(iter); -} - -// duplicate points occur if a vertex is on the antimeridan -function dedup(ring) { - return ring.reduce(function(memo, p, i) { - var pp = memo.length > 0 ? memo[memo.length-1] : null; - if (!pp || pp[0] != p[0] || pp[1] != p[1]) memo.push(p); - return memo; - }, []); -} - -function lastEl(arr) { - return arr[arr.length - 1]; -} // p1, p2: two vertices on different sides of the antimeridian // Returns y-intercept of the segment connecting p1, p2 @@ -214,5 +225,8 @@ function planarIntercept(p1, p2) { dx1 = 180 - p1[0]; dx2 = p2[0] + 180; } + // avoid fp rounding error if a point is on antimeridian + if (dx1 === 0) return p1[1]; + if (dx2 === 0) return p2[1]; return (dx2 * p1[1] + dx1 * p2[1]) / (dx1 + dx2); } diff --git a/src/gui/gui-interaction-mode-control.js b/src/gui/gui-interaction-mode-control.js index 209202c57..fb74c581c 100644 --- a/src/gui/gui-interaction-mode-control.js +++ b/src/gui/gui-interaction-mode-control.js @@ -37,7 +37,7 @@ export function InteractionMode(gui) { // Only render edit mode button/menu if this option is present if (gui.options.inspectorControl) { - btn = gui.buttons.addButton('#pointer-icon'); + btn = gui.buttons.addButton('#pointer-icon').addClass('menu-btn'); menu = El('div').addClass('nav-sub-menu').appendTo(btn.node()); btn.on('mouseleave', function() { diff --git a/src/paths/mapshaper-coordinate-utils.js b/src/paths/mapshaper-coordinate-utils.js new file mode 100644 index 000000000..b88cde820 --- /dev/null +++ b/src/paths/mapshaper-coordinate-utils.js @@ -0,0 +1,72 @@ + +// Utility functions for GeoJSON-style lat-long [x,y] coordinates and arrays of coords + +var e = 1e-10; +var T = 90 - e; +var L = -180 + e; +var B = -90 + e; +var R = 180 - e; + +export function lastEl(arr) { + return arr[arr.length - 1]; +} + +export function samePoint(a, b) { + return a[0] === b[0] && a[1] === b[1]; +} + +export function isClosedPath(arr) { + return samePoint(arr[0], lastEl(arr)); +} + +// duplicate points occur if a vertex is on the antimeridan +export function dedup(ring) { + return ring.reduce(function(memo, p, i) { + var pp = memo.length > 0 ? memo[memo.length-1] : null; + if (!pp || pp[0] != p[0] || pp[1] != p[1]) memo.push(p); + return memo; + }, []); +} + + +// remove likely rounding errors +export function snapToEdge(p) { + if (p[0] <= L) p[0] = -180; + if (p[0] >= R) p[0] = 180; + if (p[1] <= B) p[1] = -90; + if (p[1] >= T) p[1] = 90; +} + + +export function onPole(p) { + return p[1] >= T || p[1] <= B; +} + +export function isWholeWorld(coords) { + // TODO: check that l,r,t,b are all reached + for (var i=0, n=coords.length; i= R && b[0] >= R; +} + +export function isEdgePoint(p) { + return p[1] <= B || p[1] >= T || p[0] <= L || p[0] >= R; +} + + + diff --git a/src/polygons/mapshaper-slivers.js b/src/polygons/mapshaper-slivers.js index f23062f87..3b2477a06 100644 --- a/src/polygons/mapshaper-slivers.js +++ b/src/polygons/mapshaper-slivers.js @@ -53,7 +53,7 @@ export function getSliverTest(arcs, threshold, strength) { if (strength >= 0 === false) { strength = 1; // default is 1 (full-strength) } - if (strength > 1 || threshold > 0 === false) { + if (strength > 1 || threshold >= 0 === false) { error('Invalid parameter'); } var calcEffectiveArea = getSliverAreaFunction(arcs, strength); @@ -92,7 +92,7 @@ export function getDefaultSliverThreshold(lyr, arcs) { ringCount++; forEachSegmentInPath(path, arcs, onSeg); }); - var segPerRing = segCount / ringCount; + var segPerRing = segCount / ringCount || 0; var complexityFactor = Math.pow(segPerRing, 0.75); // use seg/ring as a proxy for complexity var threshold = avgSegLen * avgSegLen / 50 * complexityFactor; threshold = roundToSignificantDigits(threshold, 2); // round for display diff --git a/test/graticule-test.js b/test/graticule-test.js index 774d2c8fc..f903ceb6d 100644 --- a/test/graticule-test.js +++ b/test/graticule-test.js @@ -7,6 +7,11 @@ function hasOutline(json) { }); } +function hasOnePolygon(json) { + return json.geometries && json.geometries.length == 1 && + json.geometries[0] && json.geometries[0].type == 'Polygon'; +} + function hasMeridians(arr) { return function(json) { arr.every(lon => { @@ -17,6 +22,18 @@ function hasMeridians(arr) { } } +function polygonTest(str) { + var path = 'test/data/world_land.json'; + it(str, function(done) { + var cmd = `-i ${path} -proj ${str} densify -graticule polygon -o graticule.json`; + api.applyCommands(cmd, {}, function(err, out) { + var json = JSON.parse(out['graticule.json']); + assert(hasOnePolygon(json)); + done(); + }); + }); +} + function projTest(str, test) { var path = 'test/data/world_land.json'; it(str, function(done) { @@ -42,6 +59,11 @@ describe('mapshaper-graticule.js', function () { projTest('+proj=ortho', hasOutline); projTest('+proj=ortho +lat_0=60 +lon_0=-120', hasOutline); projTest('+proj=nsper +lat_0=30 +lon_0=80 +h=1e8', hasOutline); + }); + + describe('-graticule polygon tests', function() { + + }) it('create latlong graticule if no data has been loaded', function(done) { From 41b1227513163948711d4be68e60b085dc915fcf Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 24 May 2021 13:26:15 -0400 Subject: [PATCH 049/891] v0.5.56 --- CHANGELOG.md | 5 +++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dedff263..441f71ac3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +v0.5.56 +* Added "-graticule polygon" option, which creates a polygon matching the outline of the projected graticule. +* Allow bare PROJ projection names in CRS definitions (e.g. "robin +lon_0=120"). +* Web UI style updates. + v0.5.55 * Improved support for projected graticules. * Fixed shape clipping for the bertin1953 projection. diff --git a/package-lock.json b/package-lock.json index 3e6f140e5..369beaa5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.55", + "version": "0.5.56", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c06296c5e..4d38906a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.55", + "version": "0.5.56", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 96c79076a165ce9bf778402ffa8c6f307e267afc Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 27 May 2021 08:54:42 -0400 Subject: [PATCH 050/891] CSS style --- www/elements.css | 22 ----------- www/page.css | 101 +++++++++++++---------------------------------- 2 files changed, 27 insertions(+), 96 deletions(-) diff --git a/www/elements.css b/www/elements.css index ef7f86ff0..7725ac0e1 100644 --- a/www/elements.css +++ b/www/elements.css @@ -113,27 +113,6 @@ div.tip { /* -------- BUTTONS ---------- */ -.btn.header-btn { - color: #fff; - border: none; - margin-top: 0px; - height: 31px; - box-sizing: border-box; - border-radius: 1px; - font-size: 14px; - font-weight: 700; - padding: 8px 8px 0px 8px; -} - -.page-header .header-btn.disabled, -.page-header .header-btn.disabled:hover { - background-color: transparent; -} - -.page-header .btn.header-btn.active { - background-color: black; -} - .btn.active { cursor: pointer; } @@ -187,4 +166,3 @@ div.tip { color: #999; } - diff --git a/www/page.css b/www/page.css index f1c63f5ac..99141479a 100644 --- a/www/page.css +++ b/www/page.css @@ -16,18 +16,6 @@ src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), url('assets/SourceSansPro-Semibold.woff') format('woff'); } -/* -@font-face { - font-family: 'Iosevka'; - src: url('assets/iosevka-regular.woff2') format('woff2'), url('assets/iosevka-regular.woff') format('woff'); -} - -@font-face { - font-family: 'Iosevka'; - src: url('assets/iosevka-slab-light.woff2') format('woff2'), url('assets/iosevka-slab-light.woff') format('woff'); -} -*/ - @font-face { font-family: 'Iosevka'; src: url('assets/iosevka-light.woff2') format('woff2'), url('assets/iosevka-light.woff') format('woff'); @@ -108,62 +96,6 @@ body { background-color: #e6f7ff; } -/* THEME 2 */ - -.theme2 .page-header, -.theme2 .dialog-btn { - background-color: #678691; -} - -.theme2 .colored-text { - color: #678691; -} - -.theme2 .dot-underline { - border-bottom: 1px dotted #678691; -} - -.theme2 .nav-btn * { - fill: #678691; -} - -.theme2 .dialog-btn:hover, -.theme2 .header-btn:hover { - background-color: #365A68; -} - -.theme2 .colored-text::selection { - background-color: #DEF1F9; -} - -.theme2 .colored-text::-moz-selection { - background-color: #DEF1F9; -} - -.theme2 .layer-item.active { - background-color: #DEF1F9; -} - -/* THEME 3 -- GREY */ - -.theme3 .page-header, -.theme3 .dialog-btn { - background-color: #376E7F; -} - -.theme3 .nav-btn * { - fill: #376E7F; -} - -.theme3 .nav-btn.selected { - background-color: black; -} - -.theme3 .dialog-btn:hover, -.theme3 .header-btn:hover { - background-color: #1D5760; -} - /* --- Page header --------------- */ .page-header { @@ -173,7 +105,7 @@ body { left: 0; z-index: 40; width: 100%; - height: 31px; + height: 29px; } .coordinate-info { @@ -189,7 +121,7 @@ body { .mapshaper-logo { font-weight: bold; font-size: 17px; - margin: 3px 0 0 11px; + margin: 2px 0 0 11px; } .mapshaper-logo .logo-highlight { @@ -209,6 +141,26 @@ body { margin: 0 6px 3px 0; } +.btn.header-btn { + color: #fff; + border: none; + margin-top: 0px; + height: 29px; + box-sizing: border-box; + border-radius: 1px; + font-size: 14px; + font-weight: 700; + padding: 7px 8px 0px 8px; +} + +.page-header .header-btn.disabled, +.page-header .header-btn.disabled:hover { + background-color: transparent; +} + +.page-header .btn.header-btn.active { + background-color: black; +} .separator { border-left: 1px solid white; height: 10px; @@ -273,7 +225,7 @@ body { right: 0; bottom: 0; left: 0; - margin: 31px 0 0 0; + margin: 29px 0 0 0; } @@ -504,6 +456,7 @@ body.dragover #import-options-drop-area .drop-area { word-wrap: break-word; text-align: left; margin-top: 12px; + margin-right: 20px; padding: 11px 15px 9px 15px; vertical-align: top; display: inline-block; @@ -700,7 +653,7 @@ body.console-open .map-area { padding-left: 12px; padding-right: 12px; pointer-events: auto; - margin-right: 40px; + margin-right: 20px; } .layer-control-btn > .btn.header-btn:hover { @@ -1182,11 +1135,11 @@ img.close-btn:hover, } body.simplify .page-header { - height: 57px; + height: 55px; } body.simplify .main-area { - margin-top: 57px; + margin-top: 55px; } } From 19abb88ab16de27022250de1287e91b75bdcbe51 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 27 May 2021 09:00:06 -0400 Subject: [PATCH 051/891] Support pasting multiline command list into web console --- src/cli/mapshaper-command-parser.js | 2 ++ src/cli/mapshaper-parse-commands.js | 13 +++++++++---- src/gui/gui-console.js | 3 ++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/cli/mapshaper-command-parser.js b/src/cli/mapshaper-command-parser.js index 9f063b4fa..7462c73eb 100644 --- a/src/cli/mapshaper-command-parser.js +++ b/src/cli/mapshaper-command-parser.js @@ -53,6 +53,8 @@ export function CommandParser() { return this.command("").title(name); }; + this.isCommandName = tokenIsCommandName; + this.parseArgv = function(raw) { var commandDefs = getCommands(), commands = [], cmd, diff --git a/src/cli/mapshaper-parse-commands.js b/src/cli/mapshaper-parse-commands.js index aecfc40df..0f3b097c3 100644 --- a/src/cli/mapshaper-parse-commands.js +++ b/src/cli/mapshaper-parse-commands.js @@ -20,10 +20,15 @@ export function parseCommands(tokens) { export function standardizeConsoleCommands(raw) { var str = raw.replace(/^mapshaper\b/, '').trim(); - if (/^[a-z]/.test(str)) { - // add hyphen prefix to bare command - str = '-' + str; - } + var parser = getOptionParser(); + // support multiline string of commands pasted into console + str = str.split(/\n+/g).map(function(str) { + var match = /^[a-z][\w-]*/.exec(str = str.trim()); + if (match && parser.isCommandName(match[0])) { + str = '-' + str; // add hyphen prefix to bare command + } + return str; + }).join(' '); return str; } diff --git a/src/gui/gui-console.js b/src/gui/gui-console.js index 87044c07b..3584880f5 100644 --- a/src/gui/gui-console.js +++ b/src/gui/gui-console.js @@ -345,7 +345,8 @@ export function Console(gui) { function submit(str) { // remove newlines // TODO: remove other whitespace at beginning + end of lines - var cmd = str.replace(/\\?\n/g, '').trim(); + // var cmd = str.replace(/\\?\n/g, ' ').trim(); + var cmd = str.trim(); toLog(CURSOR + str); toCommandLine(''); if (cmd) { From e76ca196c2e574261ff6497457c373ef5209bfbb Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 27 May 2021 09:06:01 -0400 Subject: [PATCH 052/891] Add allow-overlaps option --- src/cli/mapshaper-options.js | 9 ++++++- src/commands/mapshaper-clean.js | 1 + src/dissolve/mapshaper-polygon-dissolve2.js | 17 +++++++----- src/paths/mapshaper-pathfinder.js | 14 +++++----- .../features/dissolve2/ex3_two_polygons.json | 26 +++++++++++++++++++ test/dissolve2-test.js | 14 ++++++++++ 6 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 test/data/features/dissolve2/ex3_two_polygons.json diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 72562b577..b27322a0a 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -511,11 +511,14 @@ export function getOptionParser() { parser.command('clean') .describe('fixes geometry issues, such as polygon overlaps and gaps') - .option('gap-fill-area', minGapAreaOpt) .option('sliver-control', sliverControlOpt) .option('snap-interval', snapIntervalOpt) .option('no-snap', noSnapOpt) + .option('allow-overlaps', { + describe: 'allow polygons to overlap (disables gap fill)', + type: 'flag' + }) .option('allow-empty', { describe: 'keep null geometries (removed by default)', type: 'flag' @@ -645,6 +648,10 @@ export function getOptionParser() { .option('copy-fields', copyFieldsOpt) .option('gap-fill-area', minGapAreaOpt) .option('sliver-control', sliverControlOpt) + .option('allow-overlaps', { + describe: 'allow dissolved polygons to overlap (disables gap fill)', + type: 'flag' + }) .option('name', nameOpt) .option('no-snap', noSnapOpt) .option('target', targetOpt) diff --git a/src/commands/mapshaper-clean.js b/src/commands/mapshaper-clean.js index 7a8f3e5b7..389cb0c08 100644 --- a/src/commands/mapshaper-clean.js +++ b/src/commands/mapshaper-clean.js @@ -48,6 +48,7 @@ export function cleanLayers(layers, dataset, optsArg) { } function cleanPolygonLayerGeometry(lyr, dataset, opts) { + // clean polygons by apply the 'dissolve2' function to each feature var groups = lyr.shapes.map(function(shp, i) { return [i]; }); diff --git a/src/dissolve/mapshaper-polygon-dissolve2.js b/src/dissolve/mapshaper-polygon-dissolve2.js index 888b27b8b..c95a64665 100644 --- a/src/dissolve/mapshaper-polygon-dissolve2.js +++ b/src/dissolve/mapshaper-polygon-dissolve2.js @@ -69,13 +69,17 @@ export function dissolvePolygonGroups2(groups, lyr, dataset, opts) { var arcFilter = getArcPresenceTest(lyr.shapes, dataset.arcs); var nodes = new NodeCollection(dataset.arcs, arcFilter); var mosaicOpts = { - flat: true, + flat: !opts.allow_overlaps, simple: groups.length == 1 }; var mosaicIndex = new MosaicIndex(lyr, nodes, mosaicOpts); - var sliverOpts = utils.extend({sliver_control: 1}, opts); - var filterData = getSliverFilter(lyr, dataset, sliverOpts); - var cleanupData = mosaicIndex.removeGaps(filterData.filter); + var fillGaps = !opts.allow_overlaps; // gap fill doesn't work yet with overlapping shapes + var cleanupData, filterData; + if (fillGaps) { + var sliverOpts = utils.extend({sliver_control: 1}, opts); + filterData = getSliverFilter(lyr, dataset, sliverOpts); + cleanupData = mosaicIndex.removeGaps(filterData.filter); + } var pathfind = getRingIntersector(mosaicIndex.nodes); var dissolvedShapes = groups.map(function(shapeIds) { var tiles = mosaicIndex.getTilesByShapeIds(shapeIds); @@ -89,8 +93,9 @@ export function dissolvePolygonGroups2(groups, lyr, dataset, opts) { // convert self-intersecting rings to outer/inner rings, for OGC // Simple Features compliance dissolvedShapes = fixTangentHoles(dissolvedShapes, pathfind); - var gapMessage = getGapRemovalMessage(cleanupData.removed, cleanupData.remaining, filterData.label); - if (gapMessage && !opts.quiet) message(gapMessage); + if (fillGaps && !opts.quiet) { + message(getGapRemovalMessage(cleanupData.removed, cleanupData.remaining, filterData.label)); + } return dissolvedShapes; } diff --git a/src/paths/mapshaper-pathfinder.js b/src/paths/mapshaper-pathfinder.js index 376b63904..0f8c70593 100644 --- a/src/paths/mapshaper-pathfinder.js +++ b/src/paths/mapshaper-pathfinder.js @@ -167,10 +167,10 @@ export function getPathFinder(nodes, useRoute, routeIsUsable) { // Returns a function for flattening or dissolving a collection of rings // Assumes rings are oriented in CW direction // -export function getRingIntersector(nodes, flags) { +export function getRingIntersector(nodes, flagsArr) { var arcs = nodes.arcs; var findPath = getPathFinder(nodes, useRoute, routeIsActive); - flags = flags || new Uint8Array(arcs.size()); + flagsArr = flagsArr || new Uint8Array(arcs.size()); // types: "dissolve" "flatten" return function(rings, type) { @@ -181,7 +181,7 @@ export function getRingIntersector(nodes, flags) { // even single rings get transformed (e.g. to remove spikes) if (rings.length > 0) { output = []; - openArcRoutes(rings, arcs, flags, openFwd, openRev, dissolve); + openArcRoutes(rings, arcs, flagsArr, openFwd, openRev, dissolve); forEachShapePart(rings, function(ids) { var path; for (var i=0, n=ids.length; i Date: Thu, 27 May 2021 09:09:46 -0400 Subject: [PATCH 053/891] proj command improvements --- src/commands/mapshaper-proj.js | 26 ++++----- src/crs/mapshaper-densify.js | 3 +- src/crs/mapshaper-proj-extents.js | 49 ++++++++++++----- src/crs/mapshaper-spherical-clipping.js | 70 +++++++++++++++++++++++-- src/crs/mapshaper-spherical-cutting.js | 44 ---------------- src/paths/mapshaper-coordinate-utils.js | 2 - 6 files changed, 117 insertions(+), 77 deletions(-) delete mode 100644 src/crs/mapshaper-spherical-cutting.js diff --git a/src/commands/mapshaper-proj.js b/src/commands/mapshaper-proj.js index f545a378a..f2ceebdc9 100644 --- a/src/commands/mapshaper-proj.js +++ b/src/commands/mapshaper-proj.js @@ -8,7 +8,6 @@ import { getDatasetCRS, setDatasetCRS } from '../crs/mapshaper-projections'; -import { insertPreProjectionCuts } from '../crs/mapshaper-spherical-cutting'; import { preProjectionClip } from '../crs/mapshaper-spherical-clipping'; import { cleanLayers } from '../commands/mapshaper-clean'; import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; @@ -77,7 +76,6 @@ function projCmd(dataset, destInfo, opts) { target.arcs = modifyCopy ? dataset.arcs.getCopy() : dataset.arcs; } - // target.layers = dataset.layers.filter(layerHasPoints).map(function(lyr) { target.layers = dataset.layers.map(function(lyr) { if (modifyCopy) { originals.push(lyr); @@ -86,13 +84,7 @@ function projCmd(dataset, destInfo, opts) { return lyr; }); - try { - projectDataset(target, src, dest, opts || {}); - } catch(e) { - console.error(e); - stop(utils.format("Projection failure%s (%s)", - e.point ? ' at ' + e.point.join(' ') : '', e.message)); - } + projectDataset(target, src, dest, opts || {}); dataset.info.prj = destInfo.prj; // may be undefined dataset.arcs = target.arcs; @@ -129,10 +121,19 @@ export function getCrsInfo(name, catalog) { } export function projectDataset(dataset, src, dest, opts) { + try { + _projectDataset(dataset, src, dest, opts); + } catch(e) { + // console.error(e); + stop(utils.format("Projection failure%s (%s)", + e.point ? ' at ' + e.point.join(' ') : '', e.message)); + } +} + +function _projectDataset(dataset, src, dest, opts) { var proj = getProjTransform2(src, dest); // v2 returns null points instead of throwing an error var badArcs = 0; var badPoints = 0; - var cuts; var clipped = preProjectionClip(dataset, src, dest, opts); dataset.layers.forEach(function(lyr) { @@ -141,8 +142,6 @@ export function projectDataset(dataset, src, dest, opts) { } }); if (dataset.arcs) { - cuts = insertPreProjectionCuts(dataset, src, dest); - if (opts.densify) { badArcs = projectAndDensifyArcs(dataset.arcs, proj); } else { @@ -150,7 +149,7 @@ export function projectDataset(dataset, src, dest, opts) { } } - if (cuts || clipped) { + if (clipped) { // TODO: could more selective in cleaning clipped layers // (probably only needed when clipped area crosses the antimeridian or includes a pole) cleanProjectedLayers(dataset); @@ -176,6 +175,7 @@ export function cleanProjectedLayers(dataset) { // vertices change during cleaning, but reprojection can require a topology update // even if clean does not change vertices) var cleanOpts = { + allow_overlaps: true, rebuild_topology: true, no_arc_dissolve: true, quiet: true, diff --git a/src/crs/mapshaper-densify.js b/src/crs/mapshaper-densify.js index d42728e02..c4a19a7ee 100644 --- a/src/crs/mapshaper-densify.js +++ b/src/crs/mapshaper-densify.js @@ -62,6 +62,7 @@ export function densifyUnprojectedPathByDistance(coords, meters) { export function projectAndDensifyArcs(arcs, proj) { var interval = getDefaultDensifyInterval(arcs, proj); + var minIntervalSq = interval * interval * 25; var p; return editArcs(arcs, onPoint); @@ -71,7 +72,7 @@ export function projectAndDensifyArcs(arcs, proj) { if (!p) return false; // signal that current arc contains an error // Don't try to densify shorter segments (optimization) - if (i > 0 && geom.distanceSq(p[0], p[1], pp[0], pp[1]) > interval * interval * 25) { + if (i > 0 && geom.distanceSq(p[0], p[1], pp[0], pp[1]) > minIntervalSq) { densifySegment(prevLng, prevLat, pp[0], pp[1], lng, lat, p[0], p[1], proj, interval) .forEach(append); } diff --git a/src/crs/mapshaper-proj-extents.js b/src/crs/mapshaper-proj-extents.js index 39911f5ad..350cf3429 100644 --- a/src/crs/mapshaper-proj-extents.js +++ b/src/crs/mapshaper-proj-extents.js @@ -12,25 +12,40 @@ import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; import { rotateDataset } from '../commands/mapshaper-rotate'; import { projectDataset } from '../commands/mapshaper-proj'; import { polygonsToLines } from '../commands/mapshaper-lines'; -import { insertPreProjectionCuts } from '../crs/mapshaper-spherical-cutting'; export function getClippingDataset(src, dest, opts) { + return getUnprojectedBoundingPolygon(src, dest, opts); +} + +export function getUnprojectedBoundingPolygon(src, dest, opts) { var dataset; - if (isCircleClippedProjection(dest) || opts.clip_angle) { - dataset = getClipCircle(src, dest, opts); - } else if (isClippedCylindricalProjection(dest) || opts.clip_bbox) { - dataset = getClipRectangle(dest, opts); + if (isCircleClippedProjection(dest) || opts.clip_angle || dest.clip_angle) { + dataset = getBoundingCircle(src, dest, opts); + } else if (isRectangleClippedProjection(dest) || opts.clip_bbox) { + dataset = getBoundingRectangle(dest, opts); } return dataset || null; } +// If possible, return a lat-long bbox that can be used to +// test whether data exceeds the projection bounds ands needs to be clipped +// export function getInnerBoundingBBox(P, opts) { +// var bbox = null; +// if (opts.clip_bbox) { +// bbox = opts.clip_bbox; +// } else if (isRectangleClippedProjection(dest)) { +// bbox +// } +// return bbox; +// } + // Return projected polygon extent of both clipped and unclipped projections export function getPolygonDataset(src, dest, opts) { // use clipping area if projection is clipped - var dataset = getClippingDataset(src, dest, opts); + var dataset = getUnprojectedBoundingPolygon(src, dest, opts); if (!dataset) { // use entire world if projection is not clipped - dataset = getClipRectangle(dest, {clip_bbox: [-180,-90,180,90]}); + dataset = getBoundingRectangle(dest, {clip_bbox: [-180,-90,180,90]}); } projectDataset(dataset, src, dest, {no_clip: false, quiet: true}); return dataset; @@ -38,7 +53,7 @@ export function getPolygonDataset(src, dest, opts) { // Return projected outline of clipped projections export function getOutlineDataset(src, dest, opts) { - var dataset = getClippingDataset(src, dest, opts); + var dataset = getUnprojectedBoundingPolygon(src, dest, opts); if (dataset) { // project, with cutting & cleanup projectDataset(dataset, src, dest, {no_clip: false, quiet: true}); @@ -47,7 +62,7 @@ export function getOutlineDataset(src, dest, opts) { return dataset || null; } -function getClipRectangle(dest, opts) { +function getBoundingRectangle(dest, opts) { var bbox = opts.clip_bbox || getDefaultClipBBox(dest); var rotation = getRotationParams(dest); if (!bbox) error('Missing expected clip bbox.'); @@ -60,7 +75,7 @@ function getClipRectangle(dest, opts) { return dataset; } -function getClipCircle(src, dest, opts) { +function getBoundingCircle(src, dest, opts) { var angle = opts.clip_angle || dest.clip_angle || getDefaultClipAngle(dest); if (!angle) return null; verbose(`Using clip angle of ${ +angle.toFixed(2) } degrees`); @@ -73,7 +88,7 @@ function getClipCircle(src, dest, opts) { return importGeoJSON(geojson); } -export function isClippedCylindricalProjection(P) { +export function isRectangleClippedProjection(P) { // TODO: add tmerc, etmerc, ... return inList(P, 'merc,bertin1953'); } @@ -82,12 +97,22 @@ export function getDefaultClipBBox(P) { var e = 1e-3; var bbox = { // longlat: [-180, -90, 180, 90], - merc: [-180, -87, 180, 87], + merc: [-180, -89.5, 180, 89.5], bertin1953: [-180 + e, -90 + e, 180 - e, 90 - e] }[getCrsSlug(P)]; return bbox; } +export function getClampBBox(P) { + var bbox; + if (inList(P, 'merc')) { + bbox = getDefaultClipBBox(P); + } + return bbox; +} + + + export function isCircleClippedProjection(P) { return inList(P, 'stere,sterea,ups,ortho,gnom,laea,nsper,tpers'); } diff --git a/src/crs/mapshaper-spherical-clipping.js b/src/crs/mapshaper-spherical-clipping.js index d206e2322..67b47590d 100644 --- a/src/crs/mapshaper-spherical-clipping.js +++ b/src/crs/mapshaper-spherical-clipping.js @@ -1,17 +1,77 @@ import { isLatLngCRS, getDatasetCRS } from '../crs/mapshaper-projections'; import { clipLayersInPlace } from '../commands/mapshaper-clip-erase'; -import { getClippingDataset } from '../crs/mapshaper-proj-extents'; +import { getClippingDataset, getClampBBox } from '../crs/mapshaper-proj-extents'; +import { isRotatedNormalProjection } from '../crs/mapshaper-proj-info'; +import { layerHasPaths } from '../dataset/mapshaper-layer-utils'; +import { getAntimeridian } from '../geom/mapshaper-latlon'; +import { importGeoJSON } from '../geojson/geojson-import'; +import { convertBboxToGeoJSON } from '../commands/mapshaper-rectangle'; import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; +import { transformPoints } from '../dataset/mapshaper-dataset-utils'; +import utils from '../utils/mapshaper-utils'; export function preProjectionClip(dataset, src, dest, opts) { if (!isLatLngCRS(src) || opts.no_clip) return false; - var clipData = getClippingDataset(src, dest, opts); + // rotated normal-aspect projections can generally have a thin slice removed + // from the rotated antimeridian, instead of clipping them + var cut = insertPreProjectionCuts(dataset, src, dest); + var clipped = false; + var clipData; + // experimental -- we can probably get away with just clamping some CRSs that + // have a slightly restricted coord range (e.g. Mercator), instead of doing + // a clip (more expensive) + var clampBox = getClampBBox(dest); + if (clampBox) { + clampDataset(dataset, clampBox); + } else { + clipData = getClippingDataset(src, dest, opts); + } if (clipData) { // TODO: don't bother to clip content that is fully within // the clipping shape. But how to tell? clipLayersInPlace(dataset.layers, clipData, dataset, 'clip'); - // remove arcs outside the clip area, so they don't get projected - //dissolveArcs(dataset); + clipped = true; + } + return cut || clipped; +} + + +export function insertPreProjectionCuts(dataset, src, dest) { + var antimeridian = getAntimeridian(dest.lam0 * 180 / Math.PI); + // currently only supports adding a single vertical cut to earth axis-aligned + // map projections centered on a non-zero longitude. + // TODO: need a more sophisticated kind of cutting to handle other cases + if (dataset.arcs && isRotatedNormalProjection(dest) && datasetCrossesLon(dataset, antimeridian)) { + insertVerticalCut(dataset, antimeridian); + dissolveArcs(dataset); + return true; } - return !!clipData; + return false; +} + +function clampDataset(dataset, bbox) { + transformPoints(dataset, function(x, y) { + return [utils.clamp(x, bbox[0], bbox[2]), utils.clamp(y, bbox[1], bbox[3])]; + }); +} + +function datasetCrossesLon(dataset, lon) { + var crosses = 0; + dataset.arcs.forEachSegment(function(i, j, xx, yy) { + var ax = xx[i], + bx = xx[j]; + if (ax <= lon && bx >= lon || ax >= lon && bx <= lon) crosses++; + }); + return crosses > 0; +} + +function insertVerticalCut(dataset, lon) { + var pathLayers = dataset.layers.filter(layerHasPaths); + if (pathLayers.length === 0) return; + var e = 1e-8; + var bbox = [lon-e, -91, lon+e, 91]; + // densify (so cut line can curve, e.g. Cupola projection) + var geojson = convertBboxToGeoJSON(bbox, {interval: 0.5}); + var clip = importGeoJSON(geojson); + clipLayersInPlace(pathLayers, clip, dataset, 'erase'); } diff --git a/src/crs/mapshaper-spherical-cutting.js b/src/crs/mapshaper-spherical-cutting.js deleted file mode 100644 index f39a8ec88..000000000 --- a/src/crs/mapshaper-spherical-cutting.js +++ /dev/null @@ -1,44 +0,0 @@ -import { isLatLngCRS } from '../crs/mapshaper-projections'; -import { isRotatedNormalProjection } from '../crs/mapshaper-proj-info'; -import { layerHasPaths } from '../dataset/mapshaper-layer-utils'; -import { getAntimeridian } from '../geom/mapshaper-latlon'; -import { clipLayersInPlace } from '../commands/mapshaper-clip-erase'; -import { importGeoJSON } from '../geojson/geojson-import'; -import { convertBboxToGeoJSON } from '../commands/mapshaper-rectangle'; -import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; - -export function insertPreProjectionCuts(dataset, src, dest) { - var antimeridian = getAntimeridian(dest.lam0 * 180 / Math.PI); - // currently only supports adding a single vertical cut to earth axis-aligned - // map projections centered on a non-zero longitude. - // TODO: need a more sophisticated kind of cutting to handle other cases - if (isLatLngCRS(src) && - isRotatedNormalProjection(dest) && - datasetCrossesLon(dataset, antimeridian)) { - insertVerticalCut(dataset, antimeridian); - dissolveArcs(dataset); - return true; - } - return false; -} - -function datasetCrossesLon(dataset, lon) { - var crosses = 0; - dataset.arcs.forEachSegment(function(i, j, xx, yy) { - var ax = xx[i], - bx = xx[j]; - if (ax <= lon && bx >= lon || ax >= lon && bx <= lon) crosses++; - }); - return crosses > 0; -} - -function insertVerticalCut(dataset, lon) { - var pathLayers = dataset.layers.filter(layerHasPaths); - if (pathLayers.length === 0) return; - var e = 1e-8; - var bbox = [lon-e, -91, lon+e, 91]; - // densify (so cut line can curve, e.g. Cupola projection) - var geojson = convertBboxToGeoJSON(bbox, {interval: 0.5}); - var clip = importGeoJSON(geojson); - clipLayersInPlace(pathLayers, clip, dataset, 'erase'); -} diff --git a/src/paths/mapshaper-coordinate-utils.js b/src/paths/mapshaper-coordinate-utils.js index b88cde820..ad55f9ee5 100644 --- a/src/paths/mapshaper-coordinate-utils.js +++ b/src/paths/mapshaper-coordinate-utils.js @@ -28,7 +28,6 @@ export function dedup(ring) { }, []); } - // remove likely rounding errors export function snapToEdge(p) { if (p[0] <= L) p[0] = -180; @@ -37,7 +36,6 @@ export function snapToEdge(p) { if (p[1] >= T) p[1] = 90; } - export function onPole(p) { return p[1] >= T || p[1] <= B; } From 2a3875c3efb3a9fc2f900f24910f42e81bc9bbf8 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 27 May 2021 09:11:40 -0400 Subject: [PATCH 054/891] v0.5.57 --- CHANGELOG.md | 5 ++++ package-lock.json | 8 +++--- package.json | 4 +-- www/modules.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 441f71ac3..64f949069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +v0.5.57 +* Added "allow-overlaps" option to -dissolve2 and -clean, which allows polygon features to overlap. +* Added "Hill Eucyclic" projection (+proj=hill). +* Fixed bug that removed overlapping polygons when projecting polygon layers. + v0.5.56 * Added "-graticule polygon" option, which creates a polygon matching the outline of the projected graticule. * Allow bare PROJ projection names in CRS definitions (e.g. "robin +lon_0=120"). diff --git a/package-lock.json b/package-lock.json index 369beaa5c..eed86145c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.56", + "version": "0.5.57", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1475,9 +1475,9 @@ } }, "mproj": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.32.tgz", - "integrity": "sha512-zYQ48xsihf84QPH6snBS4KYa5Y5bx5XwuZccgyoQVWfIdN53NAGQyfyvdz9XYvDADjI0ra8DKb3MjqMJnCxIIg==", + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.33.tgz", + "integrity": "sha512-A+ACJ0qMiXECymQ7A53zw4gXUKUQiZyInYKA7vrDxm49Mkk2AXESWrbcTT+lTQOUxRwOMZE91LHkHUeQPQD8Wg==", "requires": { "geographiclib": "1.48.0", "rw": "~1.3.2" diff --git a/package.json b/package.json index 4d38906a6..3eb847f96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.56", + "version": "0.5.57", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", @@ -44,7 +44,7 @@ "d3-scale-chromatic": "^2.0.0", "flatbush": "^3.2.1", "iconv-lite": "0.4.24", - "mproj": "0.0.32", + "mproj": "0.0.33", "opn": "^5.3.0", "rw": "~1.3.3", "sync-request": "5.0.0" diff --git a/www/modules.js b/www/modules.js index 658ff0d49..960cd89cb 100644 --- a/www/modules.js +++ b/www/modules.js @@ -15601,7 +15601,7 @@ function pj_bacon_init(P, bacn, ortl) { Port to PROJ by Philippe Rivière, 21 September 2018 Port to JavaScript by Matthew Bloch October 2018 */ -pj_add(pj_bertin1953, 'bertin1953', 'Bertin 1953', 'Misc Sph no inv.'); +pj_add(pj_bertin1953, 'bertin1953', 'Bertin 1953', 'Misc., Sph., NoInv.'); function pj_bertin1953(P) { var cos_delta_phi, sin_delta_phi, cos_delta_gamma, sin_delta_gamma; @@ -17617,6 +17617,66 @@ function pj_healpix(P, rhealpix) { } +pj_add(pj_hill, 'hill', 'Hill Eucyclic', 'Misc., Sph., NoInv.'); + +// Adapted from: https://github.com/d3/d3-geo-projection/blob/master/src/hill.js +// License: https://github.com/d3/d3-geo-projection/blob/master/LICENSE + +function pj_hill(P) { + var K = 1, // TODO: expose as parameter + L = 1 + K, + sinBt = sin(1 / L), + Bt = asin(sinBt), + A = 2 * sqrt(M_PI / (B = M_PI + 4 * Bt * L)), + B, + rho0 = 0.5 * A * (L + sqrt(K * (2 + K))), + K2 = K * K, + L2 = L * L, + EPS = 1e-12; + + P.es = 0; + P.fwd = s_fwd; + P.inv = s_inv; + + function s_fwd(lp, xy) { + var t = 1 - sin(lp.phi), + rho, omega; + if (t && t < 2) { + var theta = M_HALFPI - lp.phi, + i = 25, + delta, sinTheta, cosTheta, C, Bt_Bt1; + do { + sinTheta = sin(theta); + cosTheta = cos(theta); + Bt_Bt1 = Bt + atan2(sinTheta, L - cosTheta); + C = 1 + L2 - 2 * L * cosTheta; + theta -= delta = (theta - K2 * Bt - L * sinTheta + C * Bt_Bt1 -0.5 * t * B) / (2 * L * sinTheta * Bt_Bt1); + } while (fabs(delta) > EPS && --i > 0); + rho = A * sqrt(C); + omega = lp.lam * Bt_Bt1 / M_PI; + } else { + rho = A * (K + t); + omega = lp.lam * Bt / M_PI; + } + + xy.x = rho * sin(omega); + xy.y = rho0 - rho * cos(omega); + } + + function s_inv(xy, lp) { + var x = xy.x, + y = xy.y, + rho2 = x * x + (y -= rho0) * y, + cosTheta = (1 + L2 - rho2 / (A * A)) / (2 * L), + theta = acos(cosTheta), + sinTheta = sin(theta), + Bt_Bt1 = Bt + atan2(sinTheta, L - cosTheta); + lp.lam = asin(x / sqrt(rho2)) * M_PI / Bt_Bt1, + lp.phi = asin(1 - 2 * (theta - K2 * Bt - L * sinTheta + (1 + L2 - 2 * L * cosTheta) * Bt_Bt1) / B); + } +} + + pj_add(pj_krovak, 'krovak', 'Krovak', 'PCyl., Ellps.'); function pj_krovak(P) { From 4d5db439d85f9d7c585238b15afa08b63c164ba8 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 30 May 2021 12:17:20 -0400 Subject: [PATCH 055/891] Update mproj --- package.json | 2 +- www/modules.js | 93 +++++++++++++++++++++++++++++++++----------------- 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 3eb847f96..dff63a8ee 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "d3-scale-chromatic": "^2.0.0", "flatbush": "^3.2.1", "iconv-lite": "0.4.24", - "mproj": "0.0.33", + "mproj": "0.0.34", "opn": "^5.3.0", "rw": "~1.3.3", "sync-request": "5.0.0" diff --git a/www/modules.js b/www/modules.js index 960cd89cb..464057d05 100644 --- a/www/modules.js +++ b/www/modules.js @@ -16028,7 +16028,7 @@ function pj_crast(P) { } -pj_add(pj_cupola, 'cupola', 'Cupola', 'Misc., Sph., NoInv.'); +pj_add(pj_cupola, 'cupola', 'Cupola', 'PCyl., Sph., NoInv.'); // Source: https://www.tandfonline.com/eprint/EE7Y8RK4GXA4ITWUTQPY/full?target=10.1080/23729333.2020.1862962 // See also: http://www.at-a-lanta.nl/weia/cupola.html @@ -16431,35 +16431,6 @@ function pj_eqearth(P) { pj_add(pj_etmerc, 'etmerc', 'Extended Transverse Mercator', 'Cyl, Sph\nlat_ts=(0)\nlat_0=(0)'); -pj_add(pj_utm, 'utm', 'Universal Transverse Mercator (UTM)', 'Cyl, Sph\nzone= south'); - - -function pj_utm_zone(P) { - -} - -function pj_utm(P) { - var zone; - if (!P.es) e_error(-34); - P.y0 = pj_param(P.params, "bsouth") ? 10000000 : 0; - P.x0 = 500000; - if (pj_param(P.params, "tzone")) { - if ((zone = pj_param(P.params, "izone")) > 0 && zone <= 60) - --zone; - else - e_error(-35); - } else { /* nearest central meridian input */ - zone = floor((adjlon(P.lam0) + M_PI) * 30 / M_PI); - if (zone < 0) - zone = 0; - else if (zone >= 60) - zone = 59; - } - P.lam0 = (zone + 0.5) * M_PI / 30 - M_PI; - P.k0 = 0.9996; - P.phi0 = 0; - pj_etmerc(P); -} function pj_etmerc(P) { var cgb = [], @@ -17617,7 +17588,7 @@ function pj_healpix(P, rhealpix) { } -pj_add(pj_hill, 'hill', 'Hill Eucyclic', 'Misc., Sph., NoInv.'); +pj_add(pj_hill, 'hill', 'Hill Eucyclic', 'PCyl., Sph.'); // Adapted from: https://github.com/d3/d3-geo-projection/blob/master/src/hill.js // License: https://github.com/d3/d3-geo-projection/blob/master/LICENSE @@ -20905,8 +20876,68 @@ function pj_times(P) { pj_add(pj_tmerc, 'tmerc', 'Transverse Mercator', 'Cyl, Sph&Ell'); +pj_add(pj_utm, 'utm', 'Universal Transverse Mercator (UTM)', 'Cyl, Sph\nzone= south'); + +function pj_utm_zone(P) { + +} + +function pj_utm(P) { + var zone; + if (!P.es) e_error(-34); + P.y0 = pj_param(P.params, "bsouth") ? 10000000 : 0; + P.x0 = 500000; + if (pj_param(P.params, "tzone")) { + if ((zone = pj_param(P.params, "izone")) > 0 && zone <= 60) + --zone; + else + e_error(-35); + } else { /* nearest central meridian input */ + zone = floor((adjlon(P.lam0) + M_PI) * 30 / M_PI); + if (zone < 0) + zone = 0; + else if (zone >= 60) + zone = 59; + } + P.lam0 = (zone + 0.5) * M_PI / 30 - M_PI; + P.k0 = 0.9996; + P.phi0 = 0; + pj_etmerc(P); +} function pj_tmerc(P) { + // TODO: support +algo option + if (pj_param(P.params, "bapprox")) { + pj_tmerc_approx(P); + } else { + pj_tmerc_auto(P); + } +} + +function pj_tmerc_auto(P) { + if (P.es === 0) { + return pj_tmerc_approx(P); + } + pj_etmerc(P); + var etfwd = P.fwd; + var etinv = P.inv; + pj_tmerc_approx(P); + var fwd = P.fwd; + var inv = P.inv; + + P.fwd = function(lp, xy) { + if (fabs(lp.lam) > 3 * DEG_TO_RAD) etfwd(lp, xy); + else fwd(lp, xy); + }; + + P.inv = function(xy, lp) { + // See https://github.com/OSGeo/PROJ/blob/master/src/projections/tmerc.cpp + if (fabs(xy.x) > 0.053 - 0.022 * xy.y * xy.y) etinv(xy, lp); + else inv(xy, lp); + }; +} + +function pj_tmerc_approx(P) { var EPS10 = 1e-10, FC1 = 1, FC2 = 0.5, From c1e971641a9138d17670a2813413f01af3553451 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 30 May 2021 12:17:51 -0400 Subject: [PATCH 056/891] Rotation WIP --- package-lock.json | 6 +- src/buffer/mapshaper-point-buffer.js | 2 +- src/commands/mapshaper-proj.js | 12 +-- src/commands/mapshaper-rotate.js | 78 ++++++++----------- src/crs/mapshaper-densify.js | 62 ++++++++++++--- src/crs/mapshaper-proj-extents.js | 19 +++-- src/crs/mapshaper-proj-info.js | 7 -- ...dian.js => mapshaper-antimeridian-cuts.js} | 72 ++++++++++++++++- ...dian-test.js => antimeridian-cuts-test.js} | 33 +++++++- test/densify-test.js | 16 ++++ 10 files changed, 216 insertions(+), 91 deletions(-) rename src/geom/{mapshaper-antimeridian.js => mapshaper-antimeridian-cuts.js} (76%) rename test/{antimeridian-test.js => antimeridian-cuts-test.js} (67%) create mode 100644 test/densify-test.js diff --git a/package-lock.json b/package-lock.json index eed86145c..2e20fa178 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1475,9 +1475,9 @@ } }, "mproj": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.33.tgz", - "integrity": "sha512-A+ACJ0qMiXECymQ7A53zw4gXUKUQiZyInYKA7vrDxm49Mkk2AXESWrbcTT+lTQOUxRwOMZE91LHkHUeQPQD8Wg==", + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.34.tgz", + "integrity": "sha512-WUImA718tT8Ik9bGy81C0Nbwoa0SkqvsWkKqem9EH6m25MvuSFGWw+EK4vkFrng5nJvimPxUIEH+De5HWAjF2w==", "requires": { "geographiclib": "1.48.0", "rw": "~1.3.2" diff --git a/src/buffer/mapshaper-point-buffer.js b/src/buffer/mapshaper-point-buffer.js index 37ae55c99..61d444e25 100644 --- a/src/buffer/mapshaper-point-buffer.js +++ b/src/buffer/mapshaper-point-buffer.js @@ -3,7 +3,7 @@ import { getBufferDistanceFunction } from '../buffer/mapshaper-buffer-common'; import { importGeoJSON } from '../geojson/geojson-import'; import { getDatasetCRS } from '../crs/mapshaper-projections'; import { removePolylineCrosses, removePolygonCrosses, countCrosses } - from '../geom/mapshaper-antimeridian'; + from '../geom/mapshaper-antimeridian-cuts'; import { getCRS } from '../crs/mapshaper-projections'; import { getSphericalPathArea2 } from '../geom/mapshaper-polygon-geom'; import { PointIter } from '../paths/mapshaper-shape-iter'; diff --git a/src/commands/mapshaper-proj.js b/src/commands/mapshaper-proj.js index f2ceebdc9..a85b83f50 100644 --- a/src/commands/mapshaper-proj.js +++ b/src/commands/mapshaper-proj.js @@ -16,7 +16,7 @@ import { expandProjDefn } from '../crs/mapshaper-projection-params'; import { layerHasPoints, copyLayerShapes } from '../dataset/mapshaper-layer-utils'; import { datasetHasGeometry } from '../dataset/mapshaper-dataset-utils'; import { runningInBrowser } from '../mapshaper-state'; -import { stop, message } from '../utils/mapshaper-logging'; +import { stop, message, error } from '../utils/mapshaper-logging'; import { importFile } from '../io/mapshaper-file-import'; import { buildTopology } from '../topology/mapshaper-topology'; import cmd from '../mapshaper-cmd'; @@ -121,16 +121,6 @@ export function getCrsInfo(name, catalog) { } export function projectDataset(dataset, src, dest, opts) { - try { - _projectDataset(dataset, src, dest, opts); - } catch(e) { - // console.error(e); - stop(utils.format("Projection failure%s (%s)", - e.point ? ' at ' + e.point.join(' ') : '', e.message)); - } -} - -function _projectDataset(dataset, src, dest, opts) { var proj = getProjTransform2(src, dest); // v2 returns null points instead of throwing an error var badArcs = 0; var badPoints = 0; diff --git a/src/commands/mapshaper-rotate.js b/src/commands/mapshaper-rotate.js index 871b55bec..071b8bd1f 100644 --- a/src/commands/mapshaper-rotate.js +++ b/src/commands/mapshaper-rotate.js @@ -1,11 +1,19 @@ import cmd from '../mapshaper-cmd'; import { rotateDatasetCoords, getRotationFunction2 } from '../crs/mapshaper-spherical-rotation'; -import { removePolygonCrosses, removePolylineCrosses } from '../geom/mapshaper-antimeridian'; +import { + removePolygonCrosses, + removePolylineCrosses, + segmentCrossesAntimeridian, + removeCutSegments } from '../geom/mapshaper-antimeridian-cuts'; import { DatasetEditor } from '../dataset/mapshaper-dataset-editor'; import { getDatasetCRS, isLatLngCRS } from '../crs/mapshaper-projections'; import { getPlanarPathArea, getSphericalPathArea } from '../geom/mapshaper-polygon-geom'; -import { densifyDataset, densifyPathByInterval } from '../crs/mapshaper-densify'; +import { + densifyDataset, + densifyPathByInterval, + densifyAntimeridianSegment, + getIntervalInterpolator } from '../crs/mapshaper-densify'; import { cleanProjectedLayers } from '../commands/mapshaper-proj'; import { stop, error, debug } from '../utils/mapshaper-logging'; import { buildTopology } from '../topology/mapshaper-topology'; @@ -13,9 +21,7 @@ import { samePoint, snapToEdge, isEdgeSegment, - isEdgePoint, isWholeWorld, - touchesEdge, onPole, isClosedPath, lastEl @@ -38,14 +44,16 @@ export function rotateDataset(dataset, opts) { dataset.layers.forEach(function(lyr) { var type = lyr.geometry_type; - editor.editLayer(lyr, getGeometryRotator(type, rotatePoint)); + editor.editLayer(lyr, getGeometryRotator(type, rotatePoint, opts)); }); editor.done(); - buildTopology(dataset); - cleanProjectedLayers(dataset); + if (!opts.debug) { + buildTopology(dataset); + cleanProjectedLayers(dataset); + } } -function getGeometryRotator(layerType, rotatePoint) { +function getGeometryRotator(layerType, rotatePoint, opts) { var rings; if (layerType == 'point') { return function(coords) { @@ -66,10 +74,10 @@ function getGeometryRotator(layerType, rotatePoint) { coords = densifyPathByInterval(coords, 0.5); } else { coords.forEach(snapToEdge); - coords = densifyPathByInterval(coords, 0.5, densifySegment); coords = removeCutSegments(coords); + coords = densifyPathByInterval(coords, 0.5, getInterpolator(0.5)); coords.forEach(rotatePoint); - coords.forEach(snapToEdge); + // coords.forEach(snapToEdge); } if (i === 0) { // first part rings = []; @@ -83,47 +91,27 @@ function getGeometryRotator(layerType, rotatePoint) { } rings.push(coords); // accumulate rings if (i == shape.length - 1) { // last part - return removePolygonCrosses(rings); + return opts.debug ? rings : removePolygonCrosses(rings); } }; } return null; // assume layer has no geometry -- callback should not be called } -function densifySegment(a, b) { - return !isEdgeSegment(a, b); -} - -// Remove segments that belong solely to cut points -// TODO: verify that antimeridian crosses have matching y coords -// TODO: stitch together split-apart polygons -// -function removeCutSegments(coords) { - if (!touchesEdge(coords)) return coords; - var coords2 = []; - var a, b, c, x, y; - var skipped = false; - coords.pop(); // remove duplicate point - a = coords[coords.length-1]; - b = coords[0]; - for (var ci=1, n=coords.length; ci <= n; ci++) { - c = ci == n ? coords2[0] : coords[ci]; - if (isEdgePoint(a) && isEdgeSegment(b, c)) { - // skip b - // debug(' interval && (!filter || filter(a, b))) { - pushInterpolatedPoints(coords2, a, b, Math.round(dist / interval) - 1); + if (geom.distance2D(a[0], a[1], b[0], b[1]) > interval + 1e-4) { + appendArr(coords2, interpolate(a, b)); } coords2.push(b); } return coords2; } -function pushInterpolatedPoints(coords2, a, b, n) { - var dx = (b[0] - a[0]) / (n + 1), - dy = (b[1] - a[1]) / (n + 1); - for (var i=1; i<=n; i++) { - coords2.push([a[0] + dx * i, a[1] + dy * i]); +export function getIntervalInterpolator(interval) { + return function(a, b) { + var points = []; + // var rev = a[0] == b[0] ? a[1] > b[1] : a[0] > b[0]; + var dist = geom.distance2D(a[0], a[1], b[0], b[1]); + var n = Math.round(dist / interval) - 1; + var dx = (b[0] - a[0]) / (n + 1), + dy = (b[1] - a[1]) / (n + 1); + for (var i=1; i<=n; i++) { + points.push([a[0] + dx * i, a[1] + dy * i]); + } + return points; + }; +} + + +// Interpolate the same points regardless of segment direction +export function densifyAntimeridianSegment(a, b, interval) { + var y1, y2; + var coords = []; + var ascending = a[1] < b[1]; + if (a[0] != b[0]) error('Expected an edge segment'); + if (interval > 0 === false) error('Expected a positive interval'); + if (ascending) { + y1 = a[1]; + y2 = b[1]; + } else { + y1 = b[1]; + y2 = a[1]; } + var y = Math.floor(y1 / interval) * interval + interval; + while (y < y2) { + coords.push([a[0], y]); + y += interval; + } + if (!ascending) coords.reverse(); + return coords; +} + + +function appendArr(dest, src) { + for (var i=0; i 0) { + coords2.push(coords2[0].concat()); // close the path + } + // TODO: handle runs that are split at the array boundary + return coords2; +} + + export function removePolylineCrosses(path) { return splitPathAtAntimeridian(path); } @@ -168,15 +208,17 @@ export function splitPathAtAntimeridian(path) { var firstPoint = path[0]; var lastPoint = lastEl(path); var closed = samePoint(firstPoint, lastPoint); - var p, pp, y; + var p, pp, y, y2; for (var i=0, n=path.length; i0 && Math.abs(pp[0] - p[0]) > 180) { + if (i>0 && segmentCrossesAntimeridian(pp, p)) { + // y = sphericalIntercept(pp, p); y = planarIntercept(pp, p); addIntersectionPoint(part, pp, y); addSubPath(parts, part); part = []; addIntersectionPoint(part, p, y); + // console.log(y, y2) } part.push(p); pp = p; @@ -193,6 +235,10 @@ export function splitPathAtAntimeridian(path) { return parts; } +export function segmentCrossesAntimeridian(a, b) { + return Math.abs(a[0] - b[0]) > 180; +} + export function getSortedIntersections(parts) { var values = parts.map(function(p) { return p[0][1]; @@ -201,7 +247,6 @@ export function getSortedIntersections(parts) { } - function addIntersectionPoint(part, p, yint) { var xint = p[0] < 0 ? -180 : 180; if (!isAntimeridianPoint(p)) { // don't a point if p is already on the antimeridian @@ -230,3 +275,22 @@ function planarIntercept(p1, p2) { if (dx2 === 0) return p2[1]; return (dx2 * p1[1] + dx1 * p2[1]) / (dx1 + dx2); } + +// From: https://github.com/d3/d3-geo/blob/master/src/clip/antimeridian.js +function sphericalIntercept(p1, p2) { + var lam1 = p1[0] * D2R, + phi1 = p1[1] * D2R, + lam2 = p2[0] * D2R, + phi2 = p2[1] * D2R, + sinLam1Lam2 = Math.sin(lam1 - lam2), + cosPhi2 = Math.cos(phi2), + cosPhi1 = Math.cos(phi1), + phi; + if (Math.abs(sinLam1Lam2) > 1e-6) { + phi = Math.atan((Math.sin(phi1) * cosPhi2 * Math.sin(lam2) - + Math.sin(phi2) * cosPhi1) * Math.sin(lam1)) / (cosPhi1 * cosPhi2 * sinLam1Lam2); + } else { + phi = (phi1 + phi2) / 2; + } + return phi * R2D; +} diff --git a/test/antimeridian-test.js b/test/antimeridian-cuts-test.js similarity index 67% rename from test/antimeridian-test.js rename to test/antimeridian-cuts-test.js index 49e7bcc9f..86ede8e0e 100644 --- a/test/antimeridian-test.js +++ b/test/antimeridian-cuts-test.js @@ -1,8 +1,37 @@ -import { splitPathAtAntimeridian } from '../src/geom/mapshaper-antimeridian'; +import { + splitPathAtAntimeridian, + removeCutSegments } from '../src/geom/mapshaper-antimeridian-cuts'; var assert = require('assert'); -describe('mapshaper-antimeridian.js', function () { +describe('mapshaper-antimeridian-cuts.js', function () { + + describe('removeCutSegments()', function() { + + it('remove polar line', function() { + var coords = [[-180,80], [-180,90], [0,90], [180,90], [180, 80], [-180, 80]]; + var output = removeCutSegments(coords); + var target = [[-180, 80], [180, 80], [-180, 80]]; + assert.deepEqual(output, target); + }); + + it('remove vertices along edge', function() { + var coords = [[-180,80], [-180,70], [-180,60], [180,60], [180, 70], + [180,80], [-180,80]]; + var output = removeCutSegments(coords); + var target = [[-180,80], [-180,60], [180,60], [180,80], [-180,80]]; + assert.deepEqual(output, target); + }) + + // it('remove vertices across array boundary', function() { + // var coords = [[0,90], [90, 90], [180,90], [180, 80], [-180, 80],[-180,90], [-90, 90], [0,90]]; + // var output = removeCutSegments(coords); + // var target = [[-180, 80], [180, 80], [-180, 80]]; + // assert.deepEqual(output, target); + // }) + + + }) describe('splitPathAtAntimeridian()', function () { diff --git a/test/densify-test.js b/test/densify-test.js new file mode 100644 index 000000000..904b10152 --- /dev/null +++ b/test/densify-test.js @@ -0,0 +1,16 @@ +import { + densifyAntimeridianSegment +} from '../src/crs/mapshaper-densify'; +var assert = require('assert'); + +describe('mapshaper-densify.js', function () { + describe('densifyAntimeridianSegment()', function () { + it('n and s direction yield same points', function () { + var s = densifyAntimeridianSegment([180, 1.1], [180, -1], 0.5); + var n = densifyAntimeridianSegment([180, -1], [180, 1.1], 0.5); + assert.deepEqual(s, [[180, 1], [180, 0.5], [180, 0], [180, -0.5]]) + assert.deepEqual(n, [[180, -0.5], [180, 0], [180, 0.5], [180, 1]]) + }); + + }) +}) \ No newline at end of file From b4983ba71b68b86e82171870f6a2c07be4d3b37a Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 4 Jun 2021 11:48:44 -0400 Subject: [PATCH 057/891] Better parsing of lists of CSS colors --- src/cli/mapshaper-option-parsing-utils.js | 3 +- test/command-parser-test.js | 64 --------------- test/option-parsing-utils-test.js | 95 +++++++++++++++++++++-- 3 files changed, 92 insertions(+), 70 deletions(-) diff --git a/src/cli/mapshaper-option-parsing-utils.js b/src/cli/mapshaper-option-parsing-utils.js index 9ebb5139a..d08b1deae 100644 --- a/src/cli/mapshaper-option-parsing-utils.js +++ b/src/cli/mapshaper-option-parsing-utils.js @@ -28,7 +28,8 @@ export function parseStringList(token) { // Accept spaces and/or commas as delimiters export function parseColorList(token) { var delim = ', '; - var token2 = token.replace(/, *(?=[^(]*\))/g, '~~~'); // kludge: protect rgba() functions from being split apart + // accept rgb(0 0 0) rgb(0,0,0) rgb(0, 0, 0) + var token2 = token.replace(/[ ,] *(?=[^(]*\))/g, '~~~'); // kludge: protect rgba() functions from being split apart var list = splitOptionList(token2, delim); if (list.length == 1) { list = splitOptionList(list[0], delim); diff --git a/test/command-parser-test.js b/test/command-parser-test.js index a5e502ed5..d932df814 100644 --- a/test/command-parser-test.js +++ b/test/command-parser-test.js @@ -4,69 +4,5 @@ var api = require('../'), describe('mapshaper-command-parser.js', function () { - describe('parseStringList()', function () { - var list1 = '"County FIPS,State FIPS"', - list2 = '"County FIPS","State FIPS"'; - function test(str, target) { - assert.deepEqual(api.internal.parseStringList(str), target); - } - - it(list1, function() { - test(list1, ["County FIPS", "State FIPS"]); - }) - - it(list2, function() { - test(list2, ["County FIPS", "State FIPS"]); - }) - - it('Internal quotes are ignored', function() { - var str = "Clinton '16,Hubbell '18"; - test(str, ["Clinton '16", "Hubbell '18"]); - }) - - it('splits list containing quoted strings', function () { - test('foo,"foo bar",baz', ['foo', 'foo bar', 'baz']); - }) - - it('splits list containing apostrophes', function () { - test('Clinton \'16,Hubbell \'18,16+\'18 Votes', ['Clinton \'16', 'Hubbell \'18', '16+\'18 Votes']); - }) - - it('splits list containing apostrophes 2', function () { - test('"Clinton \'16","Hubbell \'18","16+\'18 Votes"', ['Clinton \'16', 'Hubbell \'18', '16+\'18 Votes']); - }) - - it('ignores empty strings', function () { - test('mapshaper,', ['mapshaper']); - test('foo,,,bar', ['foo', 'bar']); - }) - }) - - describe('parseColorList()', function () { - var list1 = '"white black"', - list2 = '"white","black"', - list3 = '"white, black"', - list4 = '"white", "black"', - expected = ['white', 'black']; - - var list5 = 'rgba(0, 0, 0, 0), rgb(22,32,0),aliceblue', - expected5 = ['rgba(0,0,0,0)', 'rgb(22,32,0)', 'aliceblue']; - - it(list1, function() { - assert.deepEqual(internal.parseColorList(list1), expected); - }) - it(list2, function() { - assert.deepEqual(internal.parseColorList(list2), expected); - }) - it(list3, function() { - assert.deepEqual(internal.parseColorList(list3), expected); - }) - it(list4, function() { - assert.deepEqual(internal.parseColorList(list4), expected); - }) - it(list5, function() { - assert.deepEqual(internal.parseColorList(list5), expected5); - }) - }) }) diff --git a/test/option-parsing-utils-test.js b/test/option-parsing-utils-test.js index 41378b8cf..f1e83f13f 100644 --- a/test/option-parsing-utils-test.js +++ b/test/option-parsing-utils-test.js @@ -1,15 +1,100 @@ var assert = require('assert'), api = require("../"), - split = api.internal.splitShellTokens; + internal = api.internal; + -function test(src, dest) { - // assert.deepEqual(require('shell-quote').parse(src), split(src)); - assert.deepEqual(split(src), dest); -} describe('mapshaper-option-parsing-utils.js', function () { + describe('parseStringList()', function () { + var list1 = '"County FIPS,State FIPS"', + list2 = '"County FIPS","State FIPS"'; + + function test(str, target) { + assert.deepEqual(api.internal.parseStringList(str), target); + } + + it('quoted strings with commas are accepted', function() { + var list = '"rgb(0,0,0)","rgb(3,3,3)"'; + test(list, ['rgb(0,0,0)','rgb(3,3,3)']) + }) + + it(list1, function() { + test(list1, ["County FIPS", "State FIPS"]); + }) + + it(list2, function() { + test(list2, ["County FIPS", "State FIPS"]); + }) + + it('Internal quotes are ignored', function() { + var str = "Clinton '16,Hubbell '18"; + test(str, ["Clinton '16", "Hubbell '18"]); + }) + + it('splits list containing quoted strings', function () { + test('foo,"foo bar",baz', ['foo', 'foo bar', 'baz']); + }) + + it('splits list containing apostrophes', function () { + test('Clinton \'16,Hubbell \'18,16+\'18 Votes', ['Clinton \'16', 'Hubbell \'18', '16+\'18 Votes']); + }) + + it('splits list containing apostrophes 2', function () { + test('"Clinton \'16","Hubbell \'18","16+\'18 Votes"', ['Clinton \'16', 'Hubbell \'18', '16+\'18 Votes']); + }) + + it('ignores empty strings', function () { + test('mapshaper,', ['mapshaper']); + test('foo,,,bar', ['foo', 'bar']); + }) + }) + + describe('parseColorList()', function () { + var list1 = '"white black"', + list2 = '"white","black"', + list3 = '"white, black"', + list4 = '"white", "black"', + expected = ['white', 'black']; + + var list5 = 'rgba(0, 0, 0, 0), rgb(22,32,0),aliceblue', + expected5 = ['rgba(0,0,0,0)', 'rgb(22,32,0)', 'aliceblue']; + + it(list1, function() { + assert.deepEqual(internal.parseColorList(list1), expected); + }) + it(list2, function() { + assert.deepEqual(internal.parseColorList(list2), expected); + }) + it(list3, function() { + assert.deepEqual(internal.parseColorList(list3), expected); + }) + it(list4, function() { + assert.deepEqual(internal.parseColorList(list4), expected); + }) + it(list5, function() { + assert.deepEqual(internal.parseColorList(list5), expected5); + }) + it('cmyk() colors are accepted', function() { + var list = 'cmyk(0,0,0,0),cmyk(100 100 100 100), cmyk(0, 0, 0, 0)'; + var expect = ['cmyk(0,0,0,0)', 'cmyk(100,100,100,100)', 'cmyk(0,0,0,0)'] + assert.deepEqual(internal.parseColorList(list), expect); + }) + it('color scheme names are accepted', function() { + var list = 'Category20b'; + var expect = ['Category20b'] + assert.deepEqual(internal.parseColorList(list), expect); + }) + + }) + describe('splitShellTokens()', function () { + var split = api.internal.splitShellTokens; + function test(src, dest) { + // assert.deepEqual(require('shell-quote').parse(src), split(src)); + assert.deepEqual(split(src), dest); + } + it('mapshaper', function () { test('mapshaper', ['mapshaper']); }) From 949ca762f59fd127abe648d94cf7cdea982e2c9f Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 4 Jun 2021 11:50:41 -0400 Subject: [PATCH 058/891] Add undocumented alpha-shapes command --- package-lock.json | 658 ++++++++++++++++++------- package.json | 4 +- rollup.config.js | 3 +- src/cli/mapshaper-options.js | 20 +- src/cli/mapshaper-run-command.js | 5 + src/commands/mapshaper-alpha-shapes.js | 74 +++ src/commands/mapshaper-polygon-grid.js | 36 +- 7 files changed, 612 insertions(+), 188 deletions(-) create mode 100644 src/commands/mapshaper-alpha-shapes.js diff --git a/package-lock.json b/package-lock.json index 2e20fa178..60d3492a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,31 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@rollup/plugin-node-resolve": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.0.0.tgz", + "integrity": "sha512-41X411HJ3oikIDivT5OKe9EZ6ud6DXudtfNrGbC4nniaxx2esiWjkLOzgnZsWq1IM8YIeL2rzRGLZLBjlhnZtQ==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + } + }, "@types/concat-stream": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz", @@ -12,6 +37,12 @@ "@types/node": "*" } }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, "@types/form-data": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", @@ -30,6 +61,15 @@ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz", "integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==" }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -47,9 +87,9 @@ } }, "acorn": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", - "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true }, "acorn-node": { @@ -64,9 +104,9 @@ } }, "acorn-walk": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz", - "integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", "dev": true }, "ansi-colors": { @@ -112,20 +152,21 @@ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", "dev": true, "requires": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" }, "dependencies": { "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -162,6 +203,12 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, + "available-typed-arrays": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.4.tgz", + "integrity": "sha512-SA5mXJWrId1TaQjfxUYghbqQ/hYioKmLJvPJyDuYRtXXenFNMjj4hSSt1Cf1xsuXSXrtxrVC5Ot4eU6cOtBDdA==", + "dev": true + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -169,9 +216,9 @@ "dev": true }, "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, "binary-extensions": { @@ -181,9 +228,9 @@ "dev": true }, "bn.js": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz", - "integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", "dev": true }, "brace-expansion": { @@ -226,20 +273,12 @@ } }, "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", "dev": true, "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } + "resolve": "^1.17.0" } }, "browser-stdout": { @@ -249,15 +288,15 @@ "dev": true }, "browserify": { - "version": "16.5.1", - "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.5.1.tgz", - "integrity": "sha512-EQX0h59Pp+0GtSRb5rL6OTfrttlzv+uyaUVlK6GX3w11SQ0jKPKyjC/54RhPR2ib2KmfcELM06e8FxcI5XNU2A==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-17.0.0.tgz", + "integrity": "sha512-SaHqzhku9v/j6XsQMRxPyBrSP3gnwmE27gLJYZgMT2GeK3J0+0toN+MnuNYDfHwVGQfLiMZ7KSNSIXHemy905w==", "dev": true, "requires": { "JSONStream": "^1.0.3", "assert": "^1.4.0", "browser-pack": "^6.0.1", - "browser-resolve": "^1.11.0", + "browser-resolve": "^2.0.0", "browserify-zlib": "~0.2.0", "buffer": "~5.2.1", "cached-path-relative": "^1.0.0", @@ -266,31 +305,31 @@ "constants-browserify": "~1.0.0", "crypto-browserify": "^3.0.0", "defined": "^1.0.0", - "deps-sort": "^2.0.0", + "deps-sort": "^2.0.1", "domain-browser": "^1.2.0", "duplexer2": "~0.1.2", - "events": "^2.0.0", + "events": "^3.0.0", "glob": "^7.1.0", "has": "^1.0.0", "htmlescape": "^1.1.0", "https-browserify": "^1.0.0", "inherits": "~2.0.1", - "insert-module-globals": "^7.0.0", + "insert-module-globals": "^7.2.1", "labeled-stream-splicer": "^2.0.0", "mkdirp-classic": "^0.5.2", - "module-deps": "^6.0.0", + "module-deps": "^6.2.3", "os-browserify": "~0.3.0", "parents": "^1.0.1", - "path-browserify": "~0.0.0", + "path-browserify": "^1.0.0", "process": "~0.11.0", "punycode": "^1.3.2", "querystring-es3": "~0.2.0", "read-only-stream": "^2.0.0", "readable-stream": "^2.0.2", "resolve": "^1.1.4", - "shasum": "^1.0.0", + "shasum-object": "^1.0.0", "shell-quote": "^1.6.1", - "stream-browserify": "^2.0.0", + "stream-browserify": "^3.0.0", "stream-http": "^3.0.0", "string_decoder": "^1.1.1", "subarg": "^1.0.0", @@ -299,7 +338,7 @@ "timers-browserify": "^1.0.1", "tty-browserify": "0.0.1", "url": "~0.11.0", - "util": "~0.10.1", + "util": "~0.12.0", "vm-browserify": "^1.0.0", "xtend": "^4.0.0" } @@ -342,37 +381,30 @@ } }, "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", "dev": true, "requires": { - "bn.js": "^4.1.0", + "bn.js": "^5.0.0", "randombytes": "^2.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true - } } }, "browserify-sign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.1.0.tgz", - "integrity": "sha512-VYxo7cDCeYUoBZ0ZCy4UyEUCP3smyBd4DRQM5nrFS1jJjPJjX7rP3oLRpPoWfkhQfyJ0I9ZbHbKafrFD/SGlrg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", "dev": true, "requires": { "bn.js": "^5.1.1", "browserify-rsa": "^4.0.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.2", + "elliptic": "^6.5.3", "inherits": "^2.0.4", "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0" + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" }, "dependencies": { "readable-stream": { @@ -385,6 +417,12 @@ "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true } } }, @@ -418,6 +456,12 @@ "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, + "builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true + }, "builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -430,6 +474,16 @@ "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", "dev": true }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "camelcase": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", @@ -632,19 +686,19 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", "dev": true, "requires": { "bn.js": "^4.1.0", - "elliptic": "^6.0.0" + "elliptic": "^6.5.3" }, "dependencies": { "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -761,12 +815,35 @@ "type-detect": "^4.0.0" } }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, "defined": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", "dev": true }, + "delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "requires": { + "robust-predicates": "^3.0.0" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -808,14 +885,6 @@ "acorn-node": "^1.6.1", "defined": "^1.0.0", "minimist": "^1.1.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - } } }, "diff": { @@ -836,9 +905,9 @@ }, "dependencies": { "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -887,6 +956,41 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "es-abstract": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", + "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.10.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -905,10 +1009,16 @@ "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", "dev": true }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, "events": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", - "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true }, "evp_bytestokey": { @@ -965,6 +1075,12 @@ "resolved": "https://registry.npmjs.org/flatqueue/-/flatqueue-1.2.0.tgz", "integrity": "sha512-Z/nhmRwSywE3xnHXHqbLzJiUZ9akOHZlB1IIqCzRRldWrxqp6EzqGVxTl9Fl5cSoUzC5ge7xq3WIPct8ADYdhw==" }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, "form-data": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", @@ -1011,6 +1127,17 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "get-port": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", @@ -1054,12 +1181,24 @@ "function-bind": "^1.1.1" } }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, "hash-base": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", @@ -1166,9 +1305,9 @@ } }, "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true }, "inflight": { @@ -1196,9 +1335,9 @@ } }, "insert-module-globals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.0.tgz", - "integrity": "sha512-VE6NlW+WGn2/AeOMd496AHFYmE7eLKkUY6Ty31k4og5vmA3Fjuwe9v6ifH6Xx/Hz27QvdoMoviw1/pqWRB09Sw==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.1.tgz", + "integrity": "sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==", "dev": true, "requires": { "JSONStream": "^1.0.3", @@ -1213,6 +1352,21 @@ "xtend": "^4.0.0" } }, + "is-arguments": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", + "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0" + } + }, + "is-bigint": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", + "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", + "dev": true + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1222,12 +1376,42 @@ "binary-extensions": "^2.0.0" } }, + "is-boolean-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", + "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "dev": true + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", + "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", + "dev": true + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1240,6 +1424,12 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, + "is-generator-function": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.9.tgz", + "integrity": "sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A==", + "dev": true + }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", @@ -1249,18 +1439,74 @@ "is-extglob": "^2.1.1" } }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-number-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", + "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", + "dev": true + }, "is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, + "is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", + "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", + "dev": true + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.5.tgz", + "integrity": "sha512-S+GRDgJlR3PyEbsX/Fobd9cqpZBuvUS+8asRqYDMLCb2qMzt1oz5m5oxQCxOgUDxiWsOVNi4yaF+/uvdlHlYug==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.2", + "call-bind": "^1.0.2", + "es-abstract": "^1.18.0-next.2", + "foreach": "^2.0.5", + "has-symbols": "^1.0.1" + } + }, "is-wsl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", @@ -1286,21 +1532,6 @@ "argparse": "^2.0.1" } }, - "json-stable-stringify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", - "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -1371,9 +1602,9 @@ }, "dependencies": { "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -1412,6 +1643,12 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, "mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -1452,13 +1689,13 @@ } }, "module-deps": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.2.tgz", - "integrity": "sha512-a9y6yDv5u5I4A+IPHTnqFxcaKr4p50/zxTjcQJaX2ws9tN/W6J6YXnEKhqRyPhl494dkcxx951onSKVezmI+3w==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.3.tgz", + "integrity": "sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==", "dev": true, "requires": { "JSONStream": "^1.0.3", - "browser-resolve": "^1.7.0", + "browser-resolve": "^2.0.0", "cached-path-relative": "^1.0.2", "concat-stream": "~1.6.0", "defined": "^1.0.0", @@ -1507,6 +1744,30 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, + "object-inspect": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", + "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1564,14 +1825,13 @@ } }, "parse-asn1": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", - "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", "dev": true, "requires": { - "asn1.js": "^4.0.0", + "asn1.js": "^5.2.0", "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", "pbkdf2": "^3.0.3", "safe-buffer": "^5.1.1" @@ -1583,9 +1843,9 @@ "integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104=" }, "path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "dev": true }, "path-exists": { @@ -1601,9 +1861,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-platform": { @@ -1613,9 +1873,9 @@ "dev": true }, "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", "dev": true, "requires": { "create-hash": "^1.1.2", @@ -1665,9 +1925,9 @@ }, "dependencies": { "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -1753,11 +2013,12 @@ "dev": true }, "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", "dev": true, "requires": { + "is-core-module": "^2.2.0", "path-parse": "^1.0.6" } }, @@ -1771,6 +2032,11 @@ "inherits": "^2.0.1" } }, + "robust-predicates": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz", + "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" + }, "rollup": { "version": "2.28.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.28.2.tgz", @@ -1814,16 +2080,6 @@ "safe-buffer": "^5.0.1" } }, - "shasum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", - "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", - "dev": true, - "requires": { - "json-stable-stringify": "~0.0.0", - "sha.js": "~2.4.4" - } - }, "shasum-object": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", @@ -1840,9 +2096,9 @@ "dev": true }, "simple-concat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", - "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "dev": true }, "source-map": { @@ -1852,13 +2108,26 @@ "dev": true }, "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", "dev": true, "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "stream-combiner2": { @@ -1872,9 +2141,9 @@ } }, "stream-http": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.1.1.tgz", - "integrity": "sha512-S7OqaYu0EkFpgeGFb/NPOoPLxFko7TPqtEeFg5DXPB4v/KETHG0Ln6fRFrNezoelpaDKmycEmmZ81cC9DAwgYg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", "dev": true, "requires": { "builtin-status-codes": "^3.0.0", @@ -1916,6 +2185,26 @@ "strip-ansi": "^4.0.0" } }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -1946,14 +2235,6 @@ "dev": true, "requires": { "minimist": "^1.1.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - } } }, "supports-color": { @@ -2079,6 +2360,18 @@ "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", "dev": true }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, "undeclared-identifiers": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", @@ -2117,20 +2410,17 @@ } }, "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", + "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", "dev": true, "requires": { - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "safe-buffer": "^5.1.2", + "which-typed-array": "^1.1.2" } }, "util-deprecate": { @@ -2153,6 +2443,34 @@ "isexe": "^2.0.0" } }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-typed-array": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.4.tgz", + "integrity": "sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.2", + "call-bind": "^1.0.0", + "es-abstract": "^1.18.0-next.1", + "foreach": "^2.0.5", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.1", + "is-typed-array": "^1.1.3" + } + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", diff --git a/package.json b/package.json index dff63a8ee..5841cf92f 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "commander": "^5.1.0", "cookies": "^0.8.0", "d3-scale-chromatic": "^2.0.0", + "delaunator": "^5.0.0", "flatbush": "^3.2.1", "iconv-lite": "0.4.24", "mproj": "0.0.34", @@ -50,7 +51,8 @@ "sync-request": "5.0.0" }, "devDependencies": { - "browserify": "^16.5.0", + "@rollup/plugin-node-resolve": "^13.0.0", + "browserify": "^17.0.0", "csv-spectrum": "^1.0.0", "deep-eql": ">=0.1.3", "esm": "^3.2.25", diff --git a/rollup.config.js b/rollup.config.js index db8cedc0c..c3ed1b51e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,3 +1,4 @@ +import { nodeResolve } from '@rollup/plugin-node-resolve'; const onBundle = { name: 'onbundle', @@ -28,5 +29,5 @@ export default [{ file: 'mapshaper.js', intro: 'var VERSION = "' + require('./package.json').version + '";\n' }], - plugins: [onBundle] + plugins: [onBundle, nodeResolve()] }]; diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index b27322a0a..a7f72c704 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -364,6 +364,13 @@ export function getOptionParser() { .option('where', whereOpt) .option('target', targetOpt); + parser.command('alpha-shapes') + .option('interval', { + type: 'number' + }) + .option('target', targetOpt) + .option('no-replace', noReplaceOpt); + parser.command('buffer') // .describe('') .option('radius', { @@ -412,7 +419,7 @@ export function getOptionParser() { }) .option('colors', { describe: 'list of CSS colors or color scheme name (see -colors)', - type: 'strings' + type: 'colors' }) .option('values', { describe: 'values to assign to classes (alternative to colors=)', @@ -1019,6 +1026,17 @@ export function getOptionParser() { .option('target', targetOpt) .option('no-replace', noReplaceOpt); + // parser.command('point-to-grid') + // .option('interval', { + // type: 'number' + // }) + // .option('radius', { + // // describe: 'radius of ' + // type: 'number' + // }) + // .option('target', targetOpt) + // .option('no-replace', noReplaceOpt); + parser.command('point-grid') .describe('create a rectangular grid of points') .validate(V.validateGridOpts) diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index d99221a0f..e16c4bd8c 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -14,6 +14,7 @@ import utils from '../utils/mapshaper-utils'; import cmd from '../mapshaper-cmd'; import '../commands/mapshaper-affine'; +import '../commands/mapshaper-alpha-shapes'; import '../commands/mapshaper-buffer'; import '../commands/mapshaper-calc'; import '../commands/mapshaper-classify'; @@ -149,6 +150,10 @@ export function runCommand(command, catalog, cb) { if (name == 'affine') { cmd.affine(targetLayers, targetDataset, opts); + } else if (name == 'alpha-shapes') { + outputLayers = applyCommandToEachLayer(cmd.alphaShapes, targetLayers, targetDataset, opts); + // outputLayers = null; + } else if (name == 'buffer') { // applyCommandToEachLayer(cmd.buffer, targetLayers, targetDataset, opts); outputLayers = cmd.buffer(targetLayers, targetDataset, opts); diff --git a/src/commands/mapshaper-alpha-shapes.js b/src/commands/mapshaper-alpha-shapes.js new file mode 100644 index 000000000..704992f18 --- /dev/null +++ b/src/commands/mapshaper-alpha-shapes.js @@ -0,0 +1,74 @@ +import { importGeoJSON } from '../geojson/geojson-import'; +import cmd from '../mapshaper-cmd'; +import { stop } from '../utils/mapshaper-logging'; +import { isLatLngDataset } from '../crs/mapshaper-projections'; +import { requirePointLayer } from '../dataset/mapshaper-layer-utils'; +import Delaunator from 'delaunator'; +import { forEachPoint } from '../points/mapshaper-point-utils'; +import { mergeDatasets } from '../dataset/mapshaper-merging'; +import { greatCircleDistance, distance2D } from '../geom/mapshaper-basic-geom'; +import { buildTopology } from '../topology/mapshaper-topology'; +import { cleanLayers } from '../commands/mapshaper-clean'; + +cmd.alphaShapes = function(pointLyr, targetDataset, opts) { + requirePointLayer(pointLyr); + if (opts.interval > 0 === false) { + stop('Expected a non-negative interval parameter'); + } + var filter = isLatLngDataset(targetDataset) ? getSphericalFilter(opts.interval) : getPlanarFilter(opts.interval); + var dataset = getPolygonDataset(pointLyr, filter, opts); + var merged = mergeDatasets([targetDataset, dataset]); + targetDataset.arcs = merged.arcs; + return merged.layers.pop(); +}; + +function getPlanarFilter(interval) { + return function(a, b) { + return distance2D(a[0], a[1], b[0], b[1]) <= interval; + }; +} + +// TODO: switch to real distance metric (don't assume meters, use CRS data) +function getSphericalFilter(interval) { + return function(a, b) { + return greatCircleDistance(a[0], a[1], b[0], b[1]) <= interval; + }; +} + +function getTriangleDataset(lyr, filter) { + var points = getPointArr(lyr); + var del = Delaunator.from(points); + var triangles = del.triangles; + var geojson = { + type: 'MultiPolygon', + coordinates: [] + }; + var a, b, c; + for (var i=0, n=triangles.length; i Date: Fri, 4 Jun 2021 18:28:30 -0400 Subject: [PATCH 059/891] Fix point buffer bug --- src/buffer/mapshaper-point-buffer.js | 17 ++++++++++------- src/dissolve/mapshaper-polygon-dissolve2.js | 3 ++- src/geom/mapshaper-antimeridian-cuts.js | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/buffer/mapshaper-point-buffer.js b/src/buffer/mapshaper-point-buffer.js index 61d444e25..0b3f19d41 100644 --- a/src/buffer/mapshaper-point-buffer.js +++ b/src/buffer/mapshaper-point-buffer.js @@ -1,10 +1,9 @@ import { getPreciseGeodeticSegmentFunction, getFastGeodeticSegmentFunction } from '../geom/mapshaper-geodesic'; import { getBufferDistanceFunction } from '../buffer/mapshaper-buffer-common'; import { importGeoJSON } from '../geojson/geojson-import'; -import { getDatasetCRS } from '../crs/mapshaper-projections'; +import { getDatasetCRS, getCRS, isLatLngCRS } from '../crs/mapshaper-projections'; import { removePolylineCrosses, removePolygonCrosses, countCrosses } from '../geom/mapshaper-antimeridian-cuts'; -import { getCRS } from '../crs/mapshaper-projections'; import { getSphericalPathArea2 } from '../geom/mapshaper-polygon-geom'; import { PointIter } from '../paths/mapshaper-shape-iter'; @@ -27,18 +26,20 @@ export function getCircleGeoJSON(center, radius, vertices, opts) { } return opts.geometry_type == 'polyline' ? getPointBufferLineString([center], radius, n, geod) : - getPointBufferPolygon([center], radius, n, geod); + getPointBufferPolygon([center], radius, n, geod, true); } // Convert a point layer to circles function makePointBufferGeoJSON(lyr, dataset, opts) { var vertices = opts.vertices || 72; var distanceFn = getBufferDistanceFunction(lyr, dataset, opts); - var geod = getPreciseGeodeticSegmentFunction(getDatasetCRS(dataset)); + var crs = getDatasetCRS(dataset); + var spherical = isLatLngCRS(crs); + var geod = getPreciseGeodeticSegmentFunction(crs); var geometries = lyr.shapes.map(function(shape, i) { var dist = distanceFn(i); if (!dist || !shape) return null; - return getPointBufferPolygon(shape, dist, vertices, geod); + return getPointBufferPolygon(shape, dist, vertices, geod, spherical); }); // TODO: make sure that importer supports null geometries (nonstandard GeoJSON); return { @@ -47,12 +48,14 @@ function makePointBufferGeoJSON(lyr, dataset, opts) { }; } -export function getPointBufferPolygon(points, distance, vertices, geod) { +export function getPointBufferPolygon(points, distance, vertices, geod, spherical) { var rings = [], coords, coords2; if (!points || !points.length) return null; for (var i=0; i 0) { + if (!spherical) { + rings.push([coords]); + } else if (countCrosses(coords) > 0) { coords2 = removePolygonCrosses([coords]); while (coords2.length > 0) rings.push([coords2.pop()]); // geojson polygon coords, no hole } else if (ringArea(coords) < 0) { diff --git a/src/dissolve/mapshaper-polygon-dissolve2.js b/src/dissolve/mapshaper-polygon-dissolve2.js index c95a64665..968a3791d 100644 --- a/src/dissolve/mapshaper-polygon-dissolve2.js +++ b/src/dissolve/mapshaper-polygon-dissolve2.js @@ -94,7 +94,8 @@ export function dissolvePolygonGroups2(groups, lyr, dataset, opts) { // Simple Features compliance dissolvedShapes = fixTangentHoles(dissolvedShapes, pathfind); if (fillGaps && !opts.quiet) { - message(getGapRemovalMessage(cleanupData.removed, cleanupData.remaining, filterData.label)); + var msg = getGapRemovalMessage(cleanupData.removed, cleanupData.remaining, filterData.label); + if (msg) message(msg); } return dissolvedShapes; } diff --git a/src/geom/mapshaper-antimeridian-cuts.js b/src/geom/mapshaper-antimeridian-cuts.js index 0398c647b..8f306c82d 100644 --- a/src/geom/mapshaper-antimeridian-cuts.js +++ b/src/geom/mapshaper-antimeridian-cuts.js @@ -62,7 +62,7 @@ function isAntimeridianPoint(p) { // // rings: array of rings of [x,y] points. // Returns array of split-apart rings -export function removePolygonCrosses(rings, isHole) { +export function removePolygonCrosses(rings) { var rings2 = []; var splitRings = []; var ring; From 7545a3b619bcd9ae9be3552a5c5d9d25ca57ad9f Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 4 Jun 2021 18:30:27 -0400 Subject: [PATCH 060/891] v0.5.58 --- CHANGELOG.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64f949069..bc2242ad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.58 +* Bug fixes + v0.5.57 * Added "allow-overlaps" option to -dissolve2 and -clean, which allows polygon features to overlap. * Added "Hill Eucyclic" projection (+proj=hill). diff --git a/package.json b/package.json index 5841cf92f..5307aa00f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.57", + "version": "0.5.58", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 34f93f582ab0533d205a4372b31d525a577793e0 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 4 Jun 2021 18:30:50 -0400 Subject: [PATCH 061/891] v0.5.58 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 60d3492a7..d6356d0f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.57", + "version": "0.5.58", "lockfileVersion": 1, "requires": true, "dependencies": { From 7902c7d398be9f8932330dd0ec401a3807515d67 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 11 Jun 2021 18:21:40 -0400 Subject: [PATCH 062/891] Refactor --- src/commands/mapshaper-dots.js | 4 ++-- src/commands/mapshaper-innerlines.js | 4 ++-- src/commands/mapshaper-lines.js | 15 +++++++-------- src/commands/mapshaper-point-grid.js | 9 +++++---- src/commands/mapshaper-rectangle.js | 7 +++---- src/dataset/mapshaper-layer-utils.js | 11 +++++++++++ test/point-grid-test.js | 5 +++++ 7 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/commands/mapshaper-dots.js b/src/commands/mapshaper-dots.js index 85bee5b16..33ad200f1 100644 --- a/src/commands/mapshaper-dots.js +++ b/src/commands/mapshaper-dots.js @@ -1,6 +1,6 @@ import { requireDataField } from '../dataset/mapshaper-layer-utils'; -import { requirePolygonLayer, layerHasNonNullData } from '../dataset/mapshaper-layer-utils'; +import { requirePolygonLayer, layerHasNonNullData, setOutputLayerName } from '../dataset/mapshaper-layer-utils'; import { parseColor } from '../color/color-utils'; import cmd from '../mapshaper-cmd'; import geom from '../geom/mapshaper-geom'; @@ -44,11 +44,11 @@ cmd.dots = function(lyr, arcs, opts) { }); var lyr2 = { - name: opts.no_replace ? null : lyr.name, geometry_type: 'point', shapes: shapes2, data: new DataTable(records2) }; + setOutputLayerName(lyr2, lyr, null, opts); return [lyr2]; }; diff --git a/src/commands/mapshaper-innerlines.js b/src/commands/mapshaper-innerlines.js index 7132e319f..aa53f6c30 100644 --- a/src/commands/mapshaper-innerlines.js +++ b/src/commands/mapshaper-innerlines.js @@ -2,7 +2,7 @@ import { createLineLayer } from '../commands/mapshaper-lines'; import { extractInnerLines } from '../commands/mapshaper-lines'; import { getArcClassifier } from '../topology/mapshaper-arc-classifier'; import { compileFeaturePairFilterExpression } from '../expressions/mapshaper-expressions'; -import { requirePolygonLayer } from '../dataset/mapshaper-layer-utils'; +import { requirePolygonLayer, setOutputLayerName } from '../dataset/mapshaper-layer-utils'; import cmd from '../mapshaper-cmd'; import { message } from '../utils/mapshaper-logging'; @@ -17,6 +17,6 @@ cmd.innerlines = function(lyr, arcs, opts) { if (lines.length === 0) { message("No shared boundaries were found"); } - outputLyr.name = opts.no_replace ? null : lyr.name; + setOutputLayerName(outputLyr, lyr, null, opts); return outputLyr; }; diff --git a/src/commands/mapshaper-lines.js b/src/commands/mapshaper-lines.js index 91fe936b4..bf6282d08 100644 --- a/src/commands/mapshaper-lines.js +++ b/src/commands/mapshaper-lines.js @@ -1,6 +1,6 @@ import { traversePaths, getArcPresenceTest } from '../paths/mapshaper-path-utils'; import { compileFeaturePairExpression, compileFeaturePairFilterExpression } from '../expressions/mapshaper-expressions'; -import { requireDataField, requirePolygonLayer, requirePointLayer, getLayerBounds } from '../dataset/mapshaper-layer-utils'; +import { requireDataField, requirePolygonLayer, requirePointLayer, getLayerBounds, setOutputLayerName } from '../dataset/mapshaper-layer-utils'; import { getArcClassifier } from '../topology/mapshaper-arc-classifier'; import { forEachPoint } from '../points/mapshaper-point-utils'; import { aggregateDataRecords, getCategoryClassifier } from '../dissolve/mapshaper-data-aggregation'; @@ -81,9 +81,10 @@ function pointsToLines(lyr, dataset, opts) { pointShapesToLineGeometry(lyr.shapes); // no grouping: return single line with no attributes var dataset2 = importGeoJSON(geojson); var outputLayers = mergeDatasetsIntoDataset(dataset, [dataset2]); - if (!opts.no_replace) { - outputLayers[0].name = lyr.name || outputLayers[0].name; - } + // if (!opts.no_replace) { + // outputLayers[0].name = lyr.name || outputLayers[0].name; + // } + setOutputLayerName(outputLayers[0], lyr, null, opts); return outputLayers; } @@ -108,9 +109,7 @@ function pointsToCallouts(lyr, dataset, opts) { }; var dataset2 = importGeoJSON(geojson); var outputLayers = mergeDatasetsIntoDataset(dataset, [dataset2]); - if (!opts.no_replace) { - outputLayers[0].name = lyr.name || outputLayers[0].name; - } + setOutputLayerName(outputLayers[0], lyr.name, null, opts); return outputLayers; } @@ -183,7 +182,7 @@ export function polygonsToLines(lyr, arcs, opts) { addLines(extractInnerLines(lyr.shapes, classifier), 'inner'); outputLyr = createLineLayer(shapes, records); - outputLyr.name = opts.no_replace ? null : lyr.name; + setOutputLayerName(outputLyr, lyr, null, opts); return outputLyr; function addLines(lines, typeName) { diff --git a/src/commands/mapshaper-point-grid.js b/src/commands/mapshaper-point-grid.js index a806f7cd8..002b4a899 100644 --- a/src/commands/mapshaper-point-grid.js +++ b/src/commands/mapshaper-point-grid.js @@ -1,4 +1,5 @@ import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; +import { setOutputLayerName } from '../dataset/mapshaper-layer-utils'; import { convertIntervalParam } from '../geom/mapshaper-units'; import { getDatasetCRS } from '../crs/mapshaper-projections'; import { stop } from '../utils/mapshaper-logging'; @@ -6,7 +7,9 @@ import cmd from '../mapshaper-cmd'; cmd.pointGrid = function(dataset, opts) { var gridOpts = getPointGridParams(dataset, opts); - return createPointGridLayer(createPointGrid(gridOpts), opts); + var lyr = createPointGridLayer(createPointGrid(gridOpts), opts); + setOutputLayerName(lyr, null, 'grid', opts); + return lyr; }; function getPointGridParams(dataset, opts) { @@ -37,12 +40,10 @@ function createPointGridLayer(rows, opts) { points.push([row[i]]); } }); - lyr = { + return { geometry_type: 'point', shapes: points }; - if (opts.name) lyr.name = opts.name; - return lyr; } diff --git a/src/commands/mapshaper-rectangle.js b/src/commands/mapshaper-rectangle.js index ff03149df..185cf3db3 100644 --- a/src/commands/mapshaper-rectangle.js +++ b/src/commands/mapshaper-rectangle.js @@ -1,7 +1,7 @@ import cmd from '../mapshaper-cmd'; import { convertFourSides } from '../geom/mapshaper-units'; import { setDatasetCRS, getDatasetCRS, getCRS } from '../crs/mapshaper-projections'; -import { getLayerBounds, layerHasGeometry } from '../dataset/mapshaper-layer-utils'; +import { getLayerBounds, layerHasGeometry, setOutputLayerName } from '../dataset/mapshaper-layer-utils'; import { mergeDatasetsIntoDataset } from '../dataset/mapshaper-merging'; import { importGeoJSON } from '../geojson/geojson-import'; import { getPointFeatureBounds } from '../points/mapshaper-point-utils'; @@ -41,9 +41,7 @@ cmd.rectangles = function(targetLyr, targetDataset, opts) { }; var dataset = importGeoJSON(geojson, {}); var outputLayers = mergeDatasetsIntoDataset(targetDataset, [dataset]); - if (!opts.no_replace) { - outputLayers[0].name = targetLyr.name || outputLayers[0].name; - } + setOutputLayerName(outputLayers[0], targetLyr, null, opts); return outputLayers; }; @@ -52,6 +50,7 @@ cmd.rectangles = function(targetLyr, targetDataset, opts) { cmd.rectangle2 = function(target, opts) { var datasets = target.layers.map(function(lyr) { var dataset = cmd.rectangle({layer: lyr, dataset: target.dataset}, opts); + setOutputLayerName(dataset.layers[0], lyr, null, opts); if (!opts.no_replace) { dataset.layers[0].name = lyr.name || dataset.layers[0].name; } diff --git a/src/dataset/mapshaper-layer-utils.js b/src/dataset/mapshaper-layer-utils.js index 273d0035d..78c27e470 100644 --- a/src/dataset/mapshaper-layer-utils.js +++ b/src/dataset/mapshaper-layer-utils.js @@ -178,6 +178,17 @@ export function getOutputLayer(src, opts) { return opts && opts.no_replace ? {geometry_type: src.geometry_type} : src; } +export function setOutputLayerName(dest, src, defName, opts) { + opts = opts || {}; + if (opts.name) { + dest.name = opts.name; + } else if (opts.no_replace) { + dest.name = defName || undefined; + } else { + dest.name = src && src.name || defName || undefined; + } +} + // Make a deep copy of a layer export function copyLayer(lyr) { var copy = copyLayerShapes(lyr); diff --git a/test/point-grid-test.js b/test/point-grid-test.js index ef269e380..021b83be4 100644 --- a/test/point-grid-test.js +++ b/test/point-grid-test.js @@ -64,6 +64,7 @@ describe('mapshaper-point-grid.js', function () { }; var lyr = api.pointGrid(null, opts); assert.deepEqual(lyr, { + name: 'grid', geometry_type: 'point', shapes: [[[1, 1]], [[1, 3]]] }); @@ -76,6 +77,7 @@ describe('mapshaper-point-grid.js', function () { }; var lyr = api.pointGrid(null, opts); assert.deepEqual(lyr, { + name: 'grid', geometry_type: 'point', shapes: [[[1, 1]], [[1, 3]]] }); @@ -84,6 +86,7 @@ describe('mapshaper-point-grid.js', function () { it('uses bbox of dataset, if no bbox option present', function() { var dataset = { layers: [{ + name: 'grid', geometry_type: 'point', shapes: [[[0, 4], [2, 0]]] }] @@ -93,6 +96,7 @@ describe('mapshaper-point-grid.js', function () { }; var lyr = api.pointGrid(dataset, opts); assert.deepEqual(lyr, { + name: 'grid', geometry_type: 'point', shapes: [[[1, 1]], [[1, 3]]] }); @@ -104,6 +108,7 @@ describe('mapshaper-point-grid.js', function () { }; var lyr = api.pointGrid(null, opts); assert.deepEqual(lyr, { + name: 'grid', geometry_type: 'point', shapes: [[[-90, 0]], [[90, 0]]] }); From 28ebd6118a3b9f99dd9213a2e7c8fd4594e7c5ed Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 11 Jun 2021 18:22:46 -0400 Subject: [PATCH 063/891] Add -point-to-grid command and -merge-layers flatten option --- src/buffer/mapshaper-point-buffer.js | 12 +- src/cli/mapshaper-options.js | 65 ++++-- src/cli/mapshaper-run-command.js | 11 +- src/commands/mapshaper-alpha-shapes.js | 6 +- src/commands/mapshaper-buffer.js | 13 +- src/commands/mapshaper-merge-layers.js | 20 +- src/commands/mapshaper-point-to-grid.js | 226 ++++++++++++++++++++ src/commands/mapshaper-polygon-grid.js | 3 +- src/dissolve/mapshaper-polygon-dissolve2.js | 3 +- src/geom/mapshaper-geodesic.js | 2 +- src/polygons/mapshaper-mosaic-index.js | 27 ++- src/polygons/mapshaper-tile-shape-index.js | 1 - test/data/features/merge_layers/ex1_a.json | 8 + test/data/features/merge_layers/ex1_b.json | 8 + test/merge-layers-test.js | 39 ++++ test/option-parsing-utils-test.js | 2 - test/point-to-grid-test.js | 23 ++ 17 files changed, 424 insertions(+), 45 deletions(-) create mode 100644 src/commands/mapshaper-point-to-grid.js create mode 100644 test/data/features/merge_layers/ex1_a.json create mode 100644 test/data/features/merge_layers/ex1_b.json create mode 100644 test/point-to-grid-test.js diff --git a/src/buffer/mapshaper-point-buffer.js b/src/buffer/mapshaper-point-buffer.js index 0b3f19d41..cffa95c62 100644 --- a/src/buffer/mapshaper-point-buffer.js +++ b/src/buffer/mapshaper-point-buffer.js @@ -94,12 +94,16 @@ export function getPointBufferLineString(points, distance, vertices, geod) { }; } -// Returns GeoJSON MultiPolygon coordinates -function getPointBufferCoordinates(center, meterDist, vertices, geod) { +// Returns array of [x, y] coordinates in a closed ring +export function getPointBufferCoordinates(center, meterDist, vertices, geod) { var coords = [], - angle = 360 / vertices; + angle = 360 / vertices, + theta; for (var i=0; i 0 === false) { + stop('Expected a non-negative interval parameter'); + } + if (opts.radius > 0 === false) { + stop('Expected a non-negative radius parameter'); + } + // var bbox = getLayerBounds(pointLyr).toArray(); + // Use target dataset, so grids are aligned between layers + // TODO: align grids between datasets + var bbox = getDatasetBounds(targetDataset).toArray(); + + var datasets = [targetDataset]; + var outputLayers = targetLayers.map(function(pointLyr) { + if (countMultiPartFeatures(pointLyr) > 0) { + stop('This command requires single points'); + } + var dataset = getPolygonDataset(pointLyr, bbox, opts); + var gridLyr = dataset.layers[0]; + datasets.push(dataset); + setOutputLayerName(gridLyr, pointLyr, 'grid', opts); + return gridLyr; + }); + + var merged = mergeDatasets(datasets); + // build topology for the entire dataset, in case the command is used on + // multiple target layers. + buildTopology(merged); + targetDataset.arcs = merged.arcs; + return outputLayers; +}; + +function getPolygonDataset(pointLyr, gridBBox, opts) { + var interval = opts.interval; + var points = getPointArr(pointLyr); + var lookup = getPointIndex(points, opts.radius); + var grid = getGridData(gridBBox, interval); + var size = grid.size(); + var n = size[0] * size[1]; + var geojson = { + type: 'FeatureCollection', + features: [] + }; + var cands, bbox, center, weight; + for (var i=0; i 0.05 === false) continue; + geojson.features.push({ + type: 'Feature', + properties: { + id: i, + weight: weight + }, + geometry: makeCellPolygon(i, grid, opts) + }); + } + var dataset = importGeoJSON(geojson, {}); + return dataset; +} + +function calcCellWeight(center, ids, points, opts) { + // radius of circle with same area as the cell + var radius = opts.interval * Math.sqrt(1 / Math.PI); + var circleArea = Math.PI * opts.radius * opts.radius; + var totArea = 0; + for (var i=0; i= r1 + r2) return 0; + var r1sq = r1 * r1, + r2sq = r2 * r2, + d1 = (r1sq - r2sq + d * d) / (2 * d), + d2 = d - d1; + if (d <= Math.abs(r1 - r2)) { + return Math.PI * Math.min(r1sq, r2sq); + } + return r1sq * Math.acos(d1/r1) - d1 * Math.sqrt(r1sq - d1 * d1) + + r2sq * Math.acos(d2/r2) - d2 * Math.sqrt(r2sq - d2 * d2); +} + +function makeCellPolygon(idx, grid, opts) { + var coords = opts.circles ? + makeCircleCoords(grid.idxToPoint(idx), opts) : + makeCellCoords(grid.idxToBBox(idx), opts); + return { + type: 'Polygon', + coordinates: [coords] + }; +} + +function makeCellCoords(bbox, opts) { + var margin = opts.interval * (opts.cell_margin || 0); + var a = bbox[0] + margin, + b = bbox[1] + margin, + c = bbox[2] - margin, + d = bbox[3] - margin; + return [[a, b],[a, d],[c, d],[c, b],[a, b]]; +} + +function makeCircleCoords(center, opts) { + var margin = opts.cell_margin > 0 ? opts.cell_margin : 1e-6; + var radius = opts.interval / 2 * (1 - margin); + return getPointBufferCoordinates(center, radius, 20, getPlanarSegmentEndpoint); +} + +function getPointIndex(points, radius) { + var Flatbush = require('flatbush'); + var index = new Flatbush(points.length); + points.forEach(function(p) { + index.add.apply(index, getPointBounds(p, radius)); + }); + index.finish(); + return function(bbox) { + return index.search.apply(index, bbox); + }; +} + +// TODO: support spherical coords +function getPointBounds(p, radius) { + return [p[0] - radius, p[1] - radius, p[0] + radius, p[1] + radius]; +} + +// TODO: remove duplication with alpha-shapes.js +function getPointArr(lyr) { + var coords = []; + forEachPoint(lyr.shapes, function(p) { + coords.push(p); + }); + return coords; +} + +function interpolateToGrid(pointLyr, bbox, opts) { + var records = pointLyr.data ? pointLyr.data.getRecords() : []; + forEachPoint(pointLyr.shapes, function(p, id) { + + }); +} + +function getGridInterpolator(bbox, interval) { + var sparseArr = []; + var grid = getGridData(bbox, interval); + +} + +// TODO: put this in a separate file, use it for other grid-based commands +// like -dots +function getGridData(bbox, interval) { + var xmin = bbox[0] - interval; + var ymin = bbox[1] - interval; + var xmax = bbox[2] + interval; + var ymax = bbox[3] + interval; + var w = xmax - xmin; + var h = ymax - ymin; + var cols = Math.ceil(w / interval); + var rows = Math.ceil(h / interval); + function size() { + return [cols, rows]; + } + function pointToCol(xy) { + var dx = xy[0] - xmin; + return Math.floor(dx / w * cols); + } + function pointToRow(xy) { + var dy = xy[1] - ymin; + return Math.floor(dy / h * rows); + } + function colRowToIdx(c, r) { + if (c < 0 || r < 0 || c >= cols || r >= rows) return -1; + return r * cols + c; + } + function pointToIdx(xy) { + var c = pointToCol(xy); + var r = pointToRow(xy); + return colRowToIdx(c, r); + } + function idxToCol(i) { + return i % cols; + } + function idxToRow(i) { + return Math.floor(i / cols); + } + function idxToPoint(idx) { + var x = xmin + (idxToCol(idx) + 0.5) * interval; + var y = ymin + (idxToRow(idx) + 0.5) * interval; + return [x, y]; + } + function idxToBBox(idx) { + var c = idxToCol(idx); + var r = idxToRow(idx); + return [ + xmin + c * interval, ymin + r * interval, + xmin + (c + 1) * interval, ymin + (r + 1) * interval + ]; + } + return { + size, pointToCol, pointToRow, colRowToIdx, pointToIdx, + idxToCol, idxToRow, idxToBBox, idxToPoint + }; +} diff --git a/src/commands/mapshaper-polygon-grid.js b/src/commands/mapshaper-polygon-grid.js index 2b7aba695..7b0293f52 100644 --- a/src/commands/mapshaper-polygon-grid.js +++ b/src/commands/mapshaper-polygon-grid.js @@ -1,5 +1,6 @@ import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; +import { setOutputLayerName } from '../dataset/mapshaper-layer-utils'; import { convertIntervalParam } from '../geom/mapshaper-units'; import { getDatasetCRS, requireProjectedDataset } from '../crs/mapshaper-projections'; import { importGeoJSON } from '../geojson/geojson-import'; @@ -13,7 +14,7 @@ cmd.polygonGrid = function(targetLayers, targetDataset, opts) { var gridDataset = makeGridDataset(params, opts); gridDataset.info = targetDataset.info; // copy CRS to grid dataset // TODO: improve - gridDataset.layers[0].name = opts.name || 'grid'; + setOutputLayerName(gridDataset.layers[0], null, 'grid', opts); if (opts.debug) gridDataset.layers.push(cmd.pointGrid2(targetLayers, targetDataset, opts)); return gridDataset; }; diff --git a/src/dissolve/mapshaper-polygon-dissolve2.js b/src/dissolve/mapshaper-polygon-dissolve2.js index 968a3791d..597ec57be 100644 --- a/src/dissolve/mapshaper-polygon-dissolve2.js +++ b/src/dissolve/mapshaper-polygon-dissolve2.js @@ -70,7 +70,8 @@ export function dissolvePolygonGroups2(groups, lyr, dataset, opts) { var nodes = new NodeCollection(dataset.arcs, arcFilter); var mosaicOpts = { flat: !opts.allow_overlaps, - simple: groups.length == 1 + simple: groups.length == 1, + overlap_rule: opts.overlap_rule }; var mosaicIndex = new MosaicIndex(lyr, nodes, mosaicOpts); var fillGaps = !opts.allow_overlaps; // gap fill doesn't work yet with overlapping shapes diff --git a/src/geom/mapshaper-geodesic.js b/src/geom/mapshaper-geodesic.js index 7532a2081..de9b67cd6 100644 --- a/src/geom/mapshaper-geodesic.js +++ b/src/geom/mapshaper-geodesic.js @@ -9,7 +9,7 @@ function getGeodesic(P) { return new GeographicLib.Geodesic.Geodesic(P.a, f); } -function getPlanarSegmentEndpoint(x, y, bearing, meterDist) { +export function getPlanarSegmentEndpoint(x, y, bearing, meterDist) { var rad = bearing / 180 * Math.PI; var dx = Math.sin(rad) * meterDist; var dy = Math.cos(rad) * meterDist; diff --git a/src/polygons/mapshaper-mosaic-index.js b/src/polygons/mapshaper-mosaic-index.js index 31834a850..a8c0bbd45 100644 --- a/src/polygons/mapshaper-mosaic-index.js +++ b/src/polygons/mapshaper-mosaic-index.js @@ -5,7 +5,7 @@ import { buildPolygonMosaic } from '../polygons/mapshaper-polygon-mosaic'; import { IdTestIndex } from '../indexing/mapshaper-id-test-index'; import { IdLookupIndex } from '../indexing/mapshaper-id-lookup-index'; import { PolygonTiler } from '../polygons/mapshaper-polygon-tiler'; -import { error } from '../utils/mapshaper-logging'; +import { error, stop } from '../utils/mapshaper-logging'; import geom from '../geom/mapshaper-geom'; import { T } from '../utils/mapshaper-timing'; @@ -28,7 +28,7 @@ export function MosaicIndex(lyr, nodes, optsArg) { // using -dissolve2. In this situation, we don't need a weight function. // Otherwise, if polygons are being dissolved into multiple groups, // we use a function to assign tiles in overlapping areas to a single shape. - weightFunction = getAreaWeightFunction(lyr.shapes, nodes.arcs); + weightFunction = getOverlapPriorityFunction(lyr.shapes, nodes.arcs, opts.overlap_rule); } this.mosaic = mosaic; this.nodes = nodes; // kludge @@ -72,14 +72,33 @@ export function MosaicIndex(lyr, nodes, optsArg) { return getTileIdsByShapeIds(shapeIds).map(tileIdToTile); }; - function getAreaWeightFunction(shapes, arcs) { + function getOverlapPriorityFunction(shapes, arcs, rule) { + var f; + if (!rule || rule == 'max-area') { + f = getAreaWeightFunction(shapes, arcs, false); + } else if (rule == 'min-area') { + f = getAreaWeightFunction(shapes, arcs, true); + } else if (rule == 'max-id') { + f = function(shapeId) { + return shapeId; }; + } else if (rule == 'min-id') { + f = function(shapeId) { return -shapeId; }; + } else { + stop('Unknown overlap rule:', rule); + } + return f; + } + + function getAreaWeightFunction(shapes, arcs, invert) { var index = []; + var sign = invert ? -1 : 1; return function(shpId) { var weight; if (shpId in index) { weight = index[shpId]; } else { - weight = index[shpId] = Math.abs(geom.getShapeArea(shapes[shpId], arcs)); + weight = sign * Math.abs(geom.getShapeArea(shapes[shpId], arcs)); + index[shpId] = weight; } return weight; }; diff --git a/src/polygons/mapshaper-tile-shape-index.js b/src/polygons/mapshaper-tile-shape-index.js index 9ceee6f2c..d3bc1df82 100644 --- a/src/polygons/mapshaper-tile-shape-index.js +++ b/src/polygons/mapshaper-tile-shape-index.js @@ -40,7 +40,6 @@ export function TileShapeIndex(mosaic, opts) { }; this.indexTileIdsByShapeId = function(shapeId, tileIds, weightFunction) { - // shapeIndex[shapeId] = tileIds; shapeIndex[shapeId] = []; for (var i=0; i Date: Thu, 17 Jun 2021 12:46:57 -0400 Subject: [PATCH 064/891] Use 1/6 and 5/6 for automatic conic SP values --- src/crs/mapshaper-projection-params.js | 6 ++++-- test/projection-params-test.js | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/crs/mapshaper-projection-params.js b/src/crs/mapshaper-projection-params.js index 2a3ddf5c1..b4cf846d8 100644 --- a/src/crs/mapshaper-projection-params.js +++ b/src/crs/mapshaper-projection-params.js @@ -36,11 +36,13 @@ function getBBox(dataset) { return getDatasetBounds(dataset).toArray(); } +// See: Savric & Jenny, "Automating the selection of standard parallels for conic map projections" +// Using one-sixth rule, not the more complicated formula proposed by the authors export function getConicParams(bbox, decimals) { var cx = (bbox[0] + bbox[2]) / 2; var h = bbox[3] - bbox[1]; - var sp1 = bbox[1] + 0.25 * h; - var sp2 = bbox[1] + 0.75 * h; + var sp1 = bbox[1] + 1/6 * h; + var sp2 = bbox[1] + 5/6 * h; return `+lon_0=${ cx.toFixed(decimals) } +lat_1=${ sp1.toFixed(decimals) } +lat_2=${ sp2.toFixed(decimals) }`; } diff --git a/test/projection-params-test.js b/test/projection-params-test.js index 10a00b8ae..2d1856c77 100644 --- a/test/projection-params-test.js +++ b/test/projection-params-test.js @@ -20,9 +20,9 @@ describe('mapshaper-projection-params.js', function () { }) it('getConicParams()', function() { - var bbox=[0, 10, 30, 50]; + var bbox=[0, 10, 30, 70]; var str = getConicParams(bbox, 2); - assert.equal(str, '+lon_0=15.00 +lat_1=20.00 +lat_2=40.00'); + assert.equal(str, '+lon_0=15.00 +lat_1=20.00 +lat_2=60.00'); }) it('getCenterParams()', function() { From 48a813299bdec4d7a013b0b1e4a45eb318220086 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 17 Jun 2021 22:45:38 -0400 Subject: [PATCH 065/891] Don't drop small paths so early in gui --- src/gui/gui-canvas.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/gui/gui-canvas.js b/src/gui/gui-canvas.js index 7cadfaa6a..e6e396e9e 100644 --- a/src/gui/gui-canvas.js +++ b/src/gui/gui-canvas.js @@ -2,6 +2,9 @@ import { internal, utils, Bounds } from './gui-core'; import { El } from './gui-el'; import { GUI } from './gui-lib'; +var MIN_ARC_LEN = 0.1; +var MIN_PATH_LEN = 0.1; + // TODO: consider moving this upstream function getArcsForRendering(obj, ext) { var dataset = obj.source.dataset; @@ -61,7 +64,7 @@ export function drawStyledLayerToCanvas(obj, canv, ext) { // Return a function for testing if an arc should be drawn in the current view function getArcFilter(arcs, ext, usedFlag, arcCounts) { - var minPathLen = 0.5 * ext.getPixelSize(), + var minPathLen = ext.getPixelSize() * MIN_PATH_LEN, // * 0.5 geoBounds = ext.getBounds(), geoBBox = geoBounds.toArray(), allIn = geoBounds.contains(arcs.getBounds()), @@ -307,7 +310,7 @@ export function DisplayCanvas() { startPath(ctx, style); } iter = protectIterForDrawing(arcs.getArcIter(i), _ext); - drawPath(iter, t, ctx, 0.6); + drawPath(iter, t, ctx, MIN_ARC_LEN); } endPath(ctx, style); }; From fe9ad8f609f1e9fed569e6abffb71572741ce262 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 17 Jun 2021 22:47:23 -0400 Subject: [PATCH 066/891] Add -filter-points command --- CHANGELOG.md | 5 ++ src/buffer/mapshaper-buffer-common.js | 1 - src/buffer/mapshaper-point-buffer.js | 6 +- src/cli/mapshaper-options.js | 15 +++++ src/cli/mapshaper-run-command.js | 4 ++ src/commands/mapshaper-alpha-shapes.js | 62 ++++++++++++++------- src/commands/mapshaper-filter-points.js | 45 +++++++++++++++ src/commands/mapshaper-filter.js | 21 +++++++ src/commands/mapshaper-join.js | 3 +- src/commands/mapshaper-point-to-grid.js | 71 ++++++++++++++---------- src/commands/mapshaper-polygon-grid.js | 1 - src/crs/mapshaper-proj-extents.js | 2 +- src/geom/mapshaper-geodesic.js | 2 +- src/join/mapshaper-point-point-join.js | 20 +++++++ src/join/mapshaper-point-polygon-join.js | 16 ------ src/points/mapshaper-point-utils.js | 10 ++++ 16 files changed, 211 insertions(+), 73 deletions(-) create mode 100644 src/commands/mapshaper-filter-points.js create mode 100644 src/join/mapshaper-point-point-join.js diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2242ad7..70834d22f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +v0.5.59 +* Added -merge-layers flatten option, for removing overlaps in polygon layers. +* Added -clean overlap-rule=[min-id|max-id|min-area|max-area] +* Added undocumented -filter-points command + v0.5.58 * Bug fixes diff --git a/src/buffer/mapshaper-buffer-common.js b/src/buffer/mapshaper-buffer-common.js index 261b9e19e..7c339f66b 100644 --- a/src/buffer/mapshaper-buffer-common.js +++ b/src/buffer/mapshaper-buffer-common.js @@ -1,4 +1,3 @@ - import { compileValueExpression } from '../expressions/mapshaper-expressions'; import { getDatasetCRS } from '../crs/mapshaper-projections'; import { convertDistanceParam } from '../geom/mapshaper-units'; diff --git a/src/buffer/mapshaper-point-buffer.js b/src/buffer/mapshaper-point-buffer.js index cffa95c62..db0a5b0e5 100644 --- a/src/buffer/mapshaper-point-buffer.js +++ b/src/buffer/mapshaper-point-buffer.js @@ -1,4 +1,4 @@ -import { getPreciseGeodeticSegmentFunction, getFastGeodeticSegmentFunction } from '../geom/mapshaper-geodesic'; +import { getGeodeticSegmentFunction } from '../geom/mapshaper-geodesic'; import { getBufferDistanceFunction } from '../buffer/mapshaper-buffer-common'; import { importGeoJSON } from '../geojson/geojson-import'; import { getDatasetCRS, getCRS, isLatLngCRS } from '../crs/mapshaper-projections'; @@ -20,7 +20,7 @@ export function makePointBuffer(lyr, dataset, opts) { // Make a single geodetic circle export function getCircleGeoJSON(center, radius, vertices, opts) { var n = vertices || 360; - var geod = getPreciseGeodeticSegmentFunction(getCRS('wgs84')); // ? + var geod = getGeodeticSegmentFunction(getCRS('wgs84')); // ? if (opts.inset) { radius -= opts.inset; } @@ -35,7 +35,7 @@ function makePointBufferGeoJSON(lyr, dataset, opts) { var distanceFn = getBufferDistanceFunction(lyr, dataset, opts); var crs = getDatasetCRS(dataset); var spherical = isLatLngCRS(crs); - var geod = getPreciseGeodeticSegmentFunction(crs); + var geod = getGeodeticSegmentFunction(crs); var geometries = lyr.shapes.map(function(shape, i) { var dist = distanceFn(i); if (!dist || !shape) return null; diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 48d965e1f..9477a075e 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1509,6 +1509,10 @@ export function getOptionParser() { describe: 'alpha parameter', type: 'number' }) + .option('keep-points', { + // describe: 'replace single points with tiny triangles', + type: 'flag' + }) .option('name', nameOpt) .option('target', targetOpt) .option('no-replace', noReplaceOpt); @@ -1563,6 +1567,17 @@ export function getOptionParser() { describe: 'name of Node module containing the command' }); + parser.command('filter-points') + // .describe('remove points that are not part of a group') + // .option('min-group-size', { + // // describe: 'drop points with fewer points in the vicinity', + // type: 'number' + // }) + .option('group-interval', { + // describe: max interval separating a point from other points + type: 'number' + }); + parser.command('frame') // .describe('create a map frame at a given size') .option('bbox', { diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index d45e5fba4..ffef9f877 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -38,6 +38,7 @@ import '../commands/mapshaper-filter-geom'; import '../commands/mapshaper-filter-islands'; import '../commands/mapshaper-filter-islands2'; import '../commands/mapshaper-filter-rename-fields'; +import '../commands/mapshaper-filter-points'; import '../commands/mapshaper-filter-slivers'; import '../commands/mapshaper-fuzzy-join'; import '../commands/mapshaper-graticule'; @@ -223,6 +224,9 @@ export function runCommand(command, catalog, cb) { } else if (name == 'filter-islands2') { applyCommandToEachLayer(cmd.filterIslands2, targetLayers, targetDataset, opts); + } else if (name == 'filter-points') { + applyCommandToEachLayer(cmd.filterPoints, targetLayers, targetDataset, opts); + } else if (name == 'filter-slivers') { applyCommandToEachLayer(cmd.filterSlivers, targetLayers, targetDataset, opts); diff --git a/src/commands/mapshaper-alpha-shapes.js b/src/commands/mapshaper-alpha-shapes.js index 86e6cf0e2..56e2832ec 100644 --- a/src/commands/mapshaper-alpha-shapes.js +++ b/src/commands/mapshaper-alpha-shapes.js @@ -4,7 +4,7 @@ import { stop } from '../utils/mapshaper-logging'; import { isLatLngDataset } from '../crs/mapshaper-projections'; import { requirePointLayer, setOutputLayerName } from '../dataset/mapshaper-layer-utils'; import Delaunator from 'delaunator'; -import { forEachPoint } from '../points/mapshaper-point-utils'; +import { getPointsInLayer } from '../points/mapshaper-point-utils'; import { mergeDatasets } from '../dataset/mapshaper-merging'; import { greatCircleDistance, distance2D } from '../geom/mapshaper-basic-geom'; import { buildTopology } from '../topology/mapshaper-topology'; @@ -15,7 +15,7 @@ cmd.alphaShapes = function(pointLyr, targetDataset, opts) { if (opts.interval > 0 === false) { stop('Expected a non-negative interval parameter'); } - var filter = isLatLngDataset(targetDataset) ? getSphericalFilter(opts.interval) : getPlanarFilter(opts.interval); + var filter = getAlphaDistanceFilter(targetDataset, opts.interval); var dataset = getPolygonDataset(pointLyr, filter, opts); var merged = mergeDatasets([targetDataset, dataset]); var lyr = merged.layers.pop(); @@ -24,6 +24,10 @@ cmd.alphaShapes = function(pointLyr, targetDataset, opts) { return lyr; }; +export function getAlphaDistanceFilter(dataset, interval) { + return isLatLngDataset(dataset) ? getSphericalFilter(interval) : getPlanarFilter(interval); +} + function getPlanarFilter(interval) { return function(a, b) { return distance2D(a[0], a[1], b[0], b[1]) <= interval; @@ -37,40 +41,60 @@ function getSphericalFilter(interval) { }; } -function getTriangleDataset(lyr, filter) { - var points = getPointArr(lyr); + +function getTriangleDataset(lyr, filter, opts) { + var points = getPointsInLayer(lyr); var del = Delaunator.from(points); + var index = opts.keep_points ? new Uint8Array(points.length) : null; var triangles = del.triangles; var geojson = { type: 'MultiPolygon', coordinates: [] }; - var a, b, c; + var a, b, c, ai, bi, ci; for (var i=0, n=triangles.length; i 0) { + stop('This command requires single points'); + } + if (opts.group_interval > 0 === false) { + stop('Expected a positive group_interval parameter'); + } + + // TODO: remove duplication with mapshaper-alpha-shapes.js + var alphaFilter = getAlphaDistanceFilter(dataset, opts.group_interval); + var points = getPointsInLayer(lyr); + var del = Delaunator.from(points); + var triangles = del.triangles; + var index = new Uint8Array(points.length); + var a, b, c, ai, bi, ci; + for (var i=0, n=triangles.length; i -1) index.setId(i); } -function interpolateToGrid(pointLyr, bbox, opts) { - var records = pointLyr.data ? pointLyr.data.getRecords() : []; - forEachPoint(pointLyr.shapes, function(p, id) { - - }); +// TODO: support spherical coords +function getPointBounds(p, radius) { + return [p[0] - radius, p[1] - radius, p[0] + radius, p[1] + radius]; } function getGridInterpolator(bbox, interval) { @@ -183,6 +191,9 @@ function getGridData(bbox, interval) { function size() { return [cols, rows]; } + function cells() { + return cols * rows; + } function pointToCol(xy) { var dx = xy[0] - xmin; return Math.floor(dx / w * cols); @@ -220,7 +231,7 @@ function getGridData(bbox, interval) { ]; } return { - size, pointToCol, pointToRow, colRowToIdx, pointToIdx, + size, cells, pointToCol, pointToRow, colRowToIdx, pointToIdx, idxToCol, idxToRow, idxToBBox, idxToPoint }; } diff --git a/src/commands/mapshaper-polygon-grid.js b/src/commands/mapshaper-polygon-grid.js index 7b0293f52..48ecbe6ee 100644 --- a/src/commands/mapshaper-polygon-grid.js +++ b/src/commands/mapshaper-polygon-grid.js @@ -1,4 +1,3 @@ - import { getDatasetBounds } from '../dataset/mapshaper-dataset-utils'; import { setOutputLayerName } from '../dataset/mapshaper-layer-utils'; import { convertIntervalParam } from '../geom/mapshaper-units'; diff --git a/src/crs/mapshaper-proj-extents.js b/src/crs/mapshaper-proj-extents.js index 79e25bf4a..154c14c63 100644 --- a/src/crs/mapshaper-proj-extents.js +++ b/src/crs/mapshaper-proj-extents.js @@ -1,5 +1,5 @@ import { convertBboxToGeoJSON } from '../commands/mapshaper-rectangle'; -import { getPreciseGeodeticSegmentFunction } from '../geom/mapshaper-geodesic'; +import { getGeodeticSegmentFunction } from '../geom/mapshaper-geodesic'; import { inList, getCrsSlug, isAxisAligned, isMeridianBounded } from '../crs/mapshaper-proj-info'; import { getSemiMinorAxis, getCircleRadiusFromAngle diff --git a/src/geom/mapshaper-geodesic.js b/src/geom/mapshaper-geodesic.js index de9b67cd6..89551428f 100644 --- a/src/geom/mapshaper-geodesic.js +++ b/src/geom/mapshaper-geodesic.js @@ -32,7 +32,7 @@ function fastGeodeticSegmentFunction(lng, lat, bearing, meterDist) { return [lng2, lat2]; } -export function getPreciseGeodeticSegmentFunction(P) { +export function getGeodeticSegmentFunction(P) { if (!isLatLngCRS(P)) { return getPlanarSegmentEndpoint; } diff --git a/src/join/mapshaper-point-point-join.js b/src/join/mapshaper-point-point-join.js new file mode 100644 index 000000000..2927d459c --- /dev/null +++ b/src/join/mapshaper-point-point-join.js @@ -0,0 +1,20 @@ +import { PointIndex } from '../points/mapshaper-point-index'; +import { stop } from '../utils/mapshaper-logging'; +import { prepJoinLayers } from './mapshaper-point-polygon-join'; +import { joinTables } from '../join/mapshaper-join-tables'; + +export function joinPointsToPoints(targetLyr, srcLyr, opts) { + var joinFunction = getPointToPointFunction(targetLyr, srcLyr, opts); + prepJoinLayers(targetLyr, srcLyr); + return joinTables(targetLyr.data, srcLyr.data, joinFunction, opts); +} + +function getPointToPointFunction(targetLyr, srcLyr, opts) { + var shapes = targetLyr.shapes; + var index = new PointIndex(srcLyr.shapes, {}); + return function(targId) { + var srcId = index.findNearestPointFeature(shapes[targId]); + // TODO: accept multiple hits + return srcId > -1 ? [srcId] : null; + }; +} diff --git a/src/join/mapshaper-point-polygon-join.js b/src/join/mapshaper-point-polygon-join.js index a63de189d..a4824de8e 100644 --- a/src/join/mapshaper-point-polygon-join.js +++ b/src/join/mapshaper-point-polygon-join.js @@ -1,7 +1,6 @@ import { joinTables } from '../join/mapshaper-join-tables'; import { stop } from '../utils/mapshaper-logging'; import { PathIndex } from '../paths/mapshaper-path-index'; -import { PointIndex } from '../points/mapshaper-point-index'; import { DataTable } from '../datatable/mapshaper-data-table'; export function joinPointsToPolygons(targetLyr, arcs, pointLyr, opts) { @@ -17,11 +16,6 @@ export function joinPolygonsToPoints(targetLyr, polygonLyr, arcs, opts) { return joinTables(targetLyr.data, polygonLyr.data, joinFunction, opts); } -export function joinPointsToPoints(targetLyr, srcLyr, opts) { - var joinFunction = getPointToPointFunction(targetLyr, srcLyr, opts); - prepJoinLayers(targetLyr, srcLyr); - return joinTables(targetLyr.data, srcLyr.data, joinFunction, opts); -} export function prepJoinLayers(targetLyr, srcLyr) { if (!targetLyr.data) { @@ -33,16 +27,6 @@ export function prepJoinLayers(targetLyr, srcLyr) { } } -function getPointToPointFunction(targetLyr, srcLyr, opts) { - var shapes = targetLyr.shapes; - var index = new PointIndex(srcLyr.shapes, {}); - return function(targId) { - var srcId = index.findNearestPointFeature(shapes[targId]); - // TODO: accept multiple hits - return srcId > -1 ? [srcId] : null; - }; -} - export function getPolygonToPointsFunction(polygonLyr, arcs, pointLyr, opts) { // Build a reverse lookup table for mapping polygon ids to point ids. var joinFunction = getPointToPolygonsFunction(pointLyr, polygonLyr, arcs, opts); diff --git a/src/points/mapshaper-point-utils.js b/src/points/mapshaper-point-utils.js index afb4fe7ce..9a0205c86 100644 --- a/src/points/mapshaper-point-utils.js +++ b/src/points/mapshaper-point-utils.js @@ -1,5 +1,6 @@ import { layerHasPoints } from '../dataset/mapshaper-layer-utils'; import { Bounds } from '../geom/mapshaper-bounds'; +import { stop } from '../utils/mapshaper-logging'; export function countPointsInLayer(lyr) { var count = 0; @@ -28,6 +29,15 @@ export function getPointFeatureBounds(shape, bounds) { return bounds; } +// NOTE: layers can have multipoint features and null features +export function getPointsInLayer(lyr) { + var coords = []; + forEachPoint(lyr.shapes, function(p) { + coords.push(p); + }); + return coords; +} + // Iterate over each [x,y] point in a layer // shapes: one layer's "shapes" array export function forEachPoint(shapes, cb) { From df44100d79cd7c1bb2b9d57677186ebad720023c Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sat, 26 Jun 2021 17:11:31 -0400 Subject: [PATCH 067/891] Fix false multi-hit detections in multipoint layers --- src/gui/gui-shape-hit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/gui-shape-hit.js b/src/gui/gui-shape-hit.js index 7fae3a96e..d821f9e61 100644 --- a/src/gui/gui-shape-hit.js +++ b/src/gui/gui-shape-hit.js @@ -105,7 +105,7 @@ export function getShapeHitTest(displayLayer, ext) { } }); // console.log(hitThreshold, bullseye); - return hits; + return utils.uniq(hits); // multipoint features can register multiple hits } function getRadiusFunction(style) { From 0d482ba358338320f8b728780af41df89c9e1357 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sat, 26 Jun 2021 17:18:45 -0400 Subject: [PATCH 068/891] Spatial join updates --- CHANGELOG.md | 9 +- package-lock.json | 27 +- package.json | 10 +- src/cli/mapshaper-options.js | 12 + src/commands/mapshaper-filter-points.js | 7 +- src/commands/mapshaper-join.js | 14 +- src/crs/mapshaper-projections.js | 12 + src/dataset/mapshaper-layer-utils.js | 7 + src/dataset/mapshaper-merging.js | 14 +- src/indexing/mapshaper-id-test-index.js | 20 +- .../mapshaper-join-polygons-via-mosaic.js | 92 +- src/join/mapshaper-join-tables.js | 4 + src/join/mapshaper-point-point-join.js | 14 +- src/paths/mapshaper-path-repair-utils.js | 4 +- src/points/mapshaper-point-index.js | 57 +- src/polygons/mapshaper-mosaic-index.js | 4 +- src/polygons/mapshaper-polygon-tiler.js | 12 +- src/thirdparty/geokdbush.js | 190 ++++ src/utils/mapshaper-utils.js | 7 + test/data/features/join/ex1_polyA.json | 4 + test/data/features/join/ex1_polyB.json | 25 + test/data/features/join/ex2_pointA.json | 4 + test/data/features/join/ex2_pointB.json | 25 + test/data/features/join/ex3_pointA.json | 4 + test/data/features/join/ex3_pointB.json | 25 + test/join-points-to-points-test.js | 48 + test/join-points-to-polygons-test.js | 7 +- test/join-polygons-to-polygons-test.js | 4 +- test/join-test.js | 12 +- www/modules.js | 934 +++++++++++++----- 30 files changed, 1252 insertions(+), 356 deletions(-) create mode 100644 src/thirdparty/geokdbush.js create mode 100644 test/data/features/join/ex1_polyA.json create mode 100644 test/data/features/join/ex1_polyB.json create mode 100644 test/data/features/join/ex2_pointA.json create mode 100644 test/data/features/join/ex2_pointB.json create mode 100644 test/data/features/join/ex3_pointA.json create mode 100644 test/data/features/join/ex3_pointB.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 70834d22f..a6a51dec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ v0.5.59 -* Added -merge-layers flatten option, for removing overlaps in polygon layers. -* Added -clean overlap-rule=[min-id|max-id|min-area|max-area] -* Added undocumented -filter-points command +* Added -merge-layers flatten option, for removing overlaps when merging multiple polygon layers. +* Added -clean overlap-rule=[min-id|max-id|min-area|max-area] option. +* Added -join max-distance= option for point-to-point spatial joins. +* Added support for many-to-one point-to-point spatial joins. +* Added -join largest-overlap option for polygon-to-polygon spatial joins, to select a single polygon to join when multiple source polygons overlap a target polygon, based on area of overlap. +* Added undocumented -filter-points command. v0.5.58 * Bug fixes diff --git a/package-lock.json b/package-lock.json index d6356d0f2..5c050d13f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.58", + "version": "0.5.59", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1115,6 +1115,21 @@ "resolved": "https://registry.npmjs.org/geographiclib/-/geographiclib-1.48.0.tgz", "integrity": "sha1-j/KuGFrTgPZ122okOTX63RR974I=" }, + "geokdbush": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/geokdbush/-/geokdbush-1.1.0.tgz", + "integrity": "sha1-ql6OeVOmWUtAqF+9thBZrw9RRo8=", + "requires": { + "tinyqueue": "^1.2.2" + }, + "dependencies": { + "tinyqueue": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz", + "integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==" + } + } + }, "get-assigned-identifiers": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", @@ -1538,6 +1553,11 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, + "kdbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", + "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" + }, "keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -2323,6 +2343,11 @@ "process": "~0.11.0" } }, + "tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index 5307aa00f..557e1bfc3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.58", + "version": "0.5.59", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", @@ -26,7 +26,8 @@ "build": "rollup --config", "prepublishOnly": "npm test", "postpublish": "./release", - "browserify": "browserify -r sync-request -r mproj -r buffer -r iconv-lite -r fs -r flatbush -r rw -r path -r d3-scale-chromatic -r d3-color -r d3-interpolate -o www/modules.js", + "browserify_old": "browserify -r sync-request -r mproj -r buffer -r iconv-lite -r fs -r flatbush -r rw -r path -r d3-scale-chromatic -r d3-color -r d3-interpolate -o www/modules.js", + "browserify": "browserify -r sync-request -r mproj -r buffer -r iconv-lite -r fs -r flatbush -r rw -r path -r d3-scale-chromatic -r d3-color -r d3-interpolate -r kdbush -o www/modules.js", "watch": "rollup --config --watch", "dev": "rollup --config --watch" }, @@ -44,11 +45,14 @@ "d3-scale-chromatic": "^2.0.0", "delaunator": "^5.0.0", "flatbush": "^3.2.1", + "geokdbush": "^1.1.0", "iconv-lite": "0.4.24", + "kdbush": "^3.0.0", "mproj": "0.0.34", "opn": "^5.3.0", "rw": "~1.3.3", - "sync-request": "5.0.0" + "sync-request": "5.0.0", + "tinyqueue": "^2.0.3" }, "devDependencies": { "@rollup/plugin-node-resolve": "^13.0.0", diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 9477a075e..727dc73a8 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -956,6 +956,18 @@ export function getOptionParser() { describe: '(polygon-polygon join) join polygons via inner points', type: 'flag' }) + .option('largest-overlap', { + describe: '(polygon-polygon join) use max overlap to join one polygon', + type: 'flag' + }) + // .option('nearest-point', { + // describe: '(point-point join)', + // type: 'flag' + // }) + .option('max-distance', { + describe: '(point-point join)', + type: 'distance' + }) .option('planar', { // describe: 'use planar geometry when interpolating by area' // useful for testing type: 'flag' diff --git a/src/commands/mapshaper-filter-points.js b/src/commands/mapshaper-filter-points.js index b5c5abf41..9e0c94cf1 100644 --- a/src/commands/mapshaper-filter-points.js +++ b/src/commands/mapshaper-filter-points.js @@ -1,7 +1,7 @@ import { message } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import cmd from '../mapshaper-cmd'; -import { requirePointLayer, countMultiPartFeatures } from '../dataset/mapshaper-layer-utils'; +import { requireSinglePointLayer } from '../dataset/mapshaper-layer-utils'; import { isLatLngDataset, getDatasetCRS, isLatLngCRS } from '../crs/mapshaper-projections'; import { stop } from '../utils/mapshaper-logging'; import { filterLayerInPlace } from '../commands/mapshaper-filter'; @@ -10,10 +10,7 @@ import { getAlphaDistanceFilter } from '../commands/mapshaper-alpha-shapes'; import Delaunator from 'delaunator'; cmd.filterPoints = function(lyr, dataset, opts) { - requirePointLayer(lyr); - if (countMultiPartFeatures(lyr) > 0) { - stop('This command requires single points'); - } + requireSinglePointLayer(lyr); if (opts.group_interval > 0 === false) { stop('Expected a positive group_interval parameter'); } diff --git a/src/commands/mapshaper-join.js b/src/commands/mapshaper-join.js index 0244cfd27..64dd35899 100644 --- a/src/commands/mapshaper-join.js +++ b/src/commands/mapshaper-join.js @@ -7,8 +7,9 @@ import cmd from '../mapshaper-cmd'; import { joinTables, validateFieldNames } from '../join/mapshaper-join-tables'; import { joinPointsToPolygons, joinPolygonsToPoints } from '../join/mapshaper-point-polygon-join'; import { joinPointsToPoints } from '../join/mapshaper-point-point-join'; +import { requireDatasetsHaveCompatibleCRS, getDatasetCRS } from '../crs/mapshaper-projections'; -cmd.join = function(targetLyr, dataset, src, opts) { +cmd.join = function(targetLyr, targetDataset, src, opts) { var srcType, targetType, retn; if (!src || !src.layer.data || !src.dataset) { stop("Missing a joinable data source"); @@ -21,16 +22,17 @@ cmd.join = function(targetLyr, dataset, src, opts) { retn = joinAttributesToFeatures(targetLyr, src.layer.data, opts); } else { // spatial join + requireDatasetsHaveCompatibleCRS([targetDataset, src.dataset]); srcType = src.layer.geometry_type; targetType = targetLyr.geometry_type; if (srcType == 'point' && targetType == 'polygon') { - retn = joinPointsToPolygons(targetLyr, dataset.arcs, src.layer, opts); + retn = joinPointsToPolygons(targetLyr, targetDataset.arcs, src.layer, opts); } else if (srcType == 'polygon' && targetType == 'point') { retn = joinPolygonsToPoints(targetLyr, src.layer, src.dataset.arcs, opts); } else if (srcType == 'point' && targetType == 'point') { - retn = joinPointsToPoints(targetLyr, src.layer, opts); + retn = joinPointsToPoints(targetLyr, src.layer, getDatasetCRS(targetDataset), opts); } else if (srcType == 'polygon' && targetType == 'polygon') { - retn = joinPolygonsToPolygons(targetLyr, dataset, src, opts); + retn = joinPolygonsToPolygons(targetLyr, targetDataset, src, opts); } else { stop(utils.format("Unable to join %s geometry to %s geometry", srcType || 'null', targetType || 'null')); @@ -38,10 +40,10 @@ cmd.join = function(targetLyr, dataset, src, opts) { } if (retn.unmatched) { - dataset.layers.push(retn.unmatched); + targetDataset.layers.push(retn.unmatched); } if (retn.unjoined) { - dataset.layers.push(retn.unjoined); + targetDataset.layers.push(retn.unjoined); } }; diff --git a/src/crs/mapshaper-projections.js b/src/crs/mapshaper-projections.js index 288102c9e..cb4ba46ce 100644 --- a/src/crs/mapshaper-projections.js +++ b/src/crs/mapshaper-projections.js @@ -201,6 +201,18 @@ export function getDatasetCRS(dataset) { return P; } +export function requireDatasetsHaveCompatibleCRS(arr) { + arr.reduce(function(memo, dataset) { + var P = getDatasetCRS(dataset); + if (memo && P) { + if (isLatLngCRS(memo) != isLatLngCRS(P)) { + stop("Unable to combine projected and unprojected datasets"); + } + } + return P || memo; + }, null); +} + // Assumes conformal projections; consider returning average of vertical and // horizontal scale factors. // x, y: a point location in projected coordinates diff --git a/src/dataset/mapshaper-layer-utils.js b/src/dataset/mapshaper-layer-utils.js index 78c27e470..f3e6b8aee 100644 --- a/src/dataset/mapshaper-layer-utils.js +++ b/src/dataset/mapshaper-layer-utils.js @@ -127,6 +127,13 @@ export function requirePointLayer(lyr, msg) { stop(layerTypeMessage(lyr, "Expected a point layer", msg)); } +export function requireSinglePointLayer(lyr, msg) { + requirePointLayer(lyr); + if (countMultiPartFeatures(lyr) > 0) { + stop(msg || 'This command requires single points'); + } +} + export function requirePolylineLayer(lyr, msg) { if (!lyr || lyr.geometry_type !== 'polyline') stop(layerTypeMessage(lyr, "Expected a polyline layer", msg)); diff --git a/src/dataset/mapshaper-merging.js b/src/dataset/mapshaper-merging.js index dde4b3676..1d7b74e11 100644 --- a/src/dataset/mapshaper-merging.js +++ b/src/dataset/mapshaper-merging.js @@ -1,4 +1,4 @@ -import { isLatLngCRS, getDatasetCRS } from '../crs/mapshaper-projections'; +import { requireDatasetsHaveCompatibleCRS } from '../crs/mapshaper-projections'; import { forEachArcId } from '../paths/mapshaper-path-utils'; import { copyLayerShapes } from '../dataset/mapshaper-layer-utils'; import utils from '../utils/mapshaper-utils'; @@ -97,18 +97,6 @@ export function mergeDatasets(arr) { }; } -function requireDatasetsHaveCompatibleCRS(arr) { - arr.reduce(function(memo, dataset) { - var P = getDatasetCRS(dataset); - if (memo && P) { - if (isLatLngCRS(memo) != isLatLngCRS(P)) { - stop("Unable to combine projected and unprojected datasets"); - } - } - return P || memo; - }, null); -} - function mergeDatasetInfo(merged, dataset) { var info = dataset.info || {}; merged.input_files = utils.uniq((merged.input_files || []).concat(info.input_files || [])); diff --git a/src/indexing/mapshaper-id-test-index.js b/src/indexing/mapshaper-id-test-index.js index 63413fb58..3329c3f60 100644 --- a/src/indexing/mapshaper-id-test-index.js +++ b/src/indexing/mapshaper-id-test-index.js @@ -1,9 +1,14 @@ // Keep track of whether positive or negative integer ids are 'used' or not. +import { error } from '../utils/mapshaper-logging'; export function IdTestIndex(n) { var index = new Uint8Array(n); + var setList = []; this.setId = function(id) { + if (!this.hasId(id)) { + setList.push(id); + } if (id < 0) { index[~id] |= 2; } else { @@ -11,6 +16,14 @@ export function IdTestIndex(n) { } }; + this.clear = function() { + var index = this; + setList.forEach(function(id) { + index.clearId(id); + }); + setList = []; + }; + this.hasId = function(id) { return id < 0 ? (index[~id] & 2) == 2 : (index[id] & 1) == 1; }; @@ -24,11 +37,8 @@ export function IdTestIndex(n) { } }; - // clear pos. and neg. ids in ids array - this.clearIds = function(ids) { - for (var i=0; i= maxArea) { + maxId = srcId; + maxArea = area; + } + }); + if (maxId == -1) error('Geometry error'); + return [maxId]; + }; +} + + // Returned function converts a target layer feature id to multiple source feature ids -function getPolygonToPolygonFunction(targetLyr, srcLyr, mosaicIndex) { +// TODO: option to join the source polygon with the greatest overlapping area +// TODO: option to ignore source polygon with small overlaps +// (as a percentage of the area of one or the other polygon?) +function getPolygonToPolygonFunction(targetLyr, srcLyr, mosaicIndex, opts) { var mergedToSourceIds = getIdConversionFunction(targetLyr.shapes.length, srcLyr.shapes.length); + var selectMaxOverlap; + if (opts.largest_overlap) { + selectMaxOverlap = getMaxOverlapFunction(targetLyr, srcLyr, mosaicIndex); + } + return function(targId) { var tileIds = mosaicIndex.getTileIdsByShapeId(targId); - var sourceIds = [], tmp; + var sourceIds = [], overlappingTiles = [], tmp; for (var i=0; i 0 ? sourceIds.concat(tmp) : tmp; } sourceIds = utils.uniq(sourceIds); + if (sourceIds.length > 1 && opts.largest_overlap) { + sourceIds = selectMaxOverlap(targId, sourceIds); + } return sourceIds; }; } diff --git a/src/join/mapshaper-join-tables.js b/src/join/mapshaper-join-tables.js index ee087a747..2905015b0 100644 --- a/src/join/mapshaper-join-tables.js +++ b/src/join/mapshaper-join-tables.js @@ -199,6 +199,10 @@ export function getFieldsToJoin(destFields, srcFields, opts) { joinFields = opts.fields; validateFieldNames(joinFields); } + } else if (opts.calc) { + // presence of calc= option suggests a many-to-one or many-to-many join; + // it usually doesn't make sense to join all fields by default + joinFields = []; } else { // If a list of fields to join is not given, try to join all of the // source fields diff --git a/src/join/mapshaper-point-point-join.js b/src/join/mapshaper-point-point-join.js index 2927d459c..b55458a48 100644 --- a/src/join/mapshaper-point-point-join.js +++ b/src/join/mapshaper-point-point-join.js @@ -2,19 +2,19 @@ import { PointIndex } from '../points/mapshaper-point-index'; import { stop } from '../utils/mapshaper-logging'; import { prepJoinLayers } from './mapshaper-point-polygon-join'; import { joinTables } from '../join/mapshaper-join-tables'; +import { isLatLngCRS } from '../crs/mapshaper-projections'; -export function joinPointsToPoints(targetLyr, srcLyr, opts) { - var joinFunction = getPointToPointFunction(targetLyr, srcLyr, opts); +export function joinPointsToPoints(targetLyr, srcLyr, crs, opts) { + var joinFunction = getPointToPointFunction(targetLyr, srcLyr, crs, opts); prepJoinLayers(targetLyr, srcLyr); return joinTables(targetLyr.data, srcLyr.data, joinFunction, opts); } -function getPointToPointFunction(targetLyr, srcLyr, opts) { +function getPointToPointFunction(targetLyr, srcLyr, crs, opts) { var shapes = targetLyr.shapes; - var index = new PointIndex(srcLyr.shapes, {}); + var index = new PointIndex(srcLyr, crs, opts); return function(targId) { - var srcId = index.findNearestPointFeature(shapes[targId]); - // TODO: accept multiple hits - return srcId > -1 ? [srcId] : null; + var matches = index.lookupByMultiPoint(shapes[targId]); + return matches.length > 0 ? matches : null; }; } diff --git a/src/paths/mapshaper-path-repair-utils.js b/src/paths/mapshaper-path-repair-utils.js index b7a2f64c9..888d4f84e 100644 --- a/src/paths/mapshaper-path-repair-utils.js +++ b/src/paths/mapshaper-path-repair-utils.js @@ -75,14 +75,14 @@ export function removeSpikesInPath(ids) { // split into two rings that touch each other where the original ring crossed itself. // export function getSelfIntersectionSplitter(nodes) { - var pathIndex = new IdTestIndex(nodes.arcs.size()); + var pathIndex = new IdTestIndex(nodes.arcs.size(), true); var filter = function(arcId) { return pathIndex.hasId(~arcId); }; return function(path) { pathIndex.setIds(path); var paths = dividePath(path); - pathIndex.clearIds(path); + pathIndex.clear(); return paths; }; diff --git a/src/points/mapshaper-point-index.js b/src/points/mapshaper-point-index.js index e5eb64582..27f23cda3 100644 --- a/src/points/mapshaper-point-index.js +++ b/src/points/mapshaper-point-index.js @@ -1,27 +1,46 @@ import geom from '../geom/mapshaper-geom'; import utils from '../utils/mapshaper-utils'; -import { forEachPoint } from '../points/mapshaper-point-utils'; +import { getPointsInLayer } from '../points/mapshaper-point-utils'; +import { requireSinglePointLayer } from '../dataset/mapshaper-layer-utils'; +import { isLatLngCRS } from '../crs/mapshaper-projections'; +import { convertDistanceParam } from '../geom/mapshaper-units'; +import { IdTestIndex } from '../indexing/mapshaper-id-test-index'; +import * as geokdbush from '../thirdparty/geokdbush'; -// TODO: use an actual index instead of linear search -export function PointIndex(shapes, opts) { - var buf = utils.isNonNegNumber(opts.buffer) ? opts.buffer : 1e-3; - var minDistSq, minId, target; - this.findNearestPointFeature = function(shape) { - minDistSq = Infinity; - minId = -1; - target = shape || []; - forEachPoint(shapes, testPoint); - return minId; - }; +export function PointIndex(srcLyr, crs, opts) { + requireSinglePointLayer(srcLyr); + var points = getPointsInLayer(srcLyr); + var maxDist = opts.max_distance ? convertDistanceParam(opts.max_distance, crs) : 1e-3; + var kdbush = require('kdbush'); + var index = new kdbush(points); + var lookup = getLookupFunction(index, crs, maxDist); + var uniqIndex = new IdTestIndex(points.length); - function testPoint(p, id) { - var distSq; - for (var i=0; i shape ids var tileShapeIndex = new TileShapeIndex(mosaic, opts); // assign tiles to shapes @@ -145,7 +145,7 @@ export function MosaicIndex(lyr, nodes, optsArg) { } // clearing this index allows duplicate tile ids between calls to this function // (should not happen in a typical dissolve) - fetchedTileIndex.clearIds(uniqIds); + fetchedTileIndex.clear(); return uniqIds; } } diff --git a/src/polygons/mapshaper-polygon-tiler.js b/src/polygons/mapshaper-polygon-tiler.js index 0b7138ccf..a18e5562a 100644 --- a/src/polygons/mapshaper-polygon-tiler.js +++ b/src/polygons/mapshaper-polygon-tiler.js @@ -8,15 +8,15 @@ import { getHoleDivider } from '../polygons/mapshaper-polygon-holes'; // export function PolygonTiler(mosaic, arcTileIndex, nodes, opts) { var arcs = nodes.arcs; - var visitedTileIndex = new IdTestIndex(mosaic.length); + var visitedTileIndex = new IdTestIndex(mosaic.length, true); var divide = getHoleDivider(nodes); // temp vars var currHoles; // arc ids of all holes in shape var currShapeId; var currRingBbox; var tilesInShape; // accumulator for tile ids of tiles in current shape - var ringIndex = new IdTestIndex(arcs.size()); - var holeIndex = new IdTestIndex(arcs.size()); + var ringIndex = new IdTestIndex(arcs.size(), true); + var holeIndex = new IdTestIndex(arcs.size(), true); // return ids of tiles in shape this.getTilesInShape = function(shp, shapeId) { @@ -40,7 +40,7 @@ export function PolygonTiler(mosaic, arcTileIndex, nodes, opts) { retn = tilesInShape; // reset tmp vars, etc tilesInShape = null; - holeIndex.clearIds(currHoles); + holeIndex.clear(); currHoles = null; return retn; }; @@ -53,9 +53,9 @@ export function PolygonTiler(mosaic, arcTileIndex, nodes, opts) { currRingBbox = arcs.getSimpleShapeBounds2(path); ringIndex.setIds(path); procArcIds(path); - ringIndex.clearIds(path); + ringIndex.clear(); // allow overlapping rings to visit the same tiles - visitedTileIndex.clearIds(tilesInShape); + visitedTileIndex.clear(); } // optimized version: traversal without recursion (to avoid call stack oflo, excessive gc, etc) diff --git a/src/thirdparty/geokdbush.js b/src/thirdparty/geokdbush.js new file mode 100644 index 000000000..b0a81c03c --- /dev/null +++ b/src/thirdparty/geokdbush.js @@ -0,0 +1,190 @@ +/* +ISC License + +Copyright (c) 2017, Vladimir Agafonkin + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +*/ + +import TinyQueue from 'tinyqueue'; + +var earthRadius = 6371; +var rad = Math.PI / 180; + +export function around(index, lng, lat, maxResults, maxDistance, predicate) { + var maxHaverSinDist = 1, result = []; + + if (maxResults === undefined) maxResults = Infinity; + if (maxDistance !== undefined) maxHaverSinDist = haverSin(maxDistance / earthRadius); + + // a distance-sorted priority queue that will contain both points and kd-tree nodes + var q = new TinyQueue([], compareDist); + + // an object that represents the top kd-tree node (the whole Earth) + var node = { + left: 0, // left index in the kd-tree array + right: index.ids.length - 1, // right index + axis: 0, // 0 for longitude axis and 1 for latitude axis + dist: 0, // will hold the lower bound of children's distances to the query point + minLng: -180, // bounding box of the node + minLat: -90, + maxLng: 180, + maxLat: 90 + }; + + var cosLat = Math.cos(lat * rad); + var right, left, item; + + while (node) { + right = node.right; + left = node.left; + + if (right - left <= index.nodeSize) { // leaf node + + // add all points of the leaf node to the queue + for (var i = left; i <= right; i++) { + item = index.points[index.ids[i]]; + if (!predicate || predicate(item)) { + q.push({ + i: index.ids[i], + item: item, + dist: haverSinDist(lng, lat, index.coords[2 * i], index.coords[2 * i + 1], cosLat) + }); + } + } + + } else { // not a leaf node (has child nodes) + + var m = (left + right) >> 1; // middle index + var midLng = index.coords[2 * m]; + var midLat = index.coords[2 * m + 1]; + + // add middle point to the queue + item = index.points[index.ids[m]]; + if (!predicate || predicate(item)) { + q.push({ + i: index.ids[m], + item: item, + dist: haverSinDist(lng, lat, midLng, midLat, cosLat) + }); + } + + var nextAxis = (node.axis + 1) % 2; + + // first half of the node + var leftNode = { + left: left, + right: m - 1, + axis: nextAxis, + minLng: node.minLng, + minLat: node.minLat, + maxLng: node.axis === 0 ? midLng : node.maxLng, + maxLat: node.axis === 1 ? midLat : node.maxLat, + dist: 0 + }; + // second half of the node + var rightNode = { + left: m + 1, + right: right, + axis: nextAxis, + minLng: node.axis === 0 ? midLng : node.minLng, + minLat: node.axis === 1 ? midLat : node.minLat, + maxLng: node.maxLng, + maxLat: node.maxLat, + dist: 0 + }; + + leftNode.dist = boxDist(lng, lat, cosLat, leftNode); + rightNode.dist = boxDist(lng, lat, cosLat, rightNode); + + // add child nodes to the queue + q.push(leftNode); + q.push(rightNode); + } + + // fetch closest points from the queue; they're guaranteed to be closer + // than all remaining points (both individual and those in kd-tree nodes), + // since each node's distance is a lower bound of distances to its children + while (q.length && q.peek().item) { + var candidate = q.pop(); + if (candidate.dist > maxHaverSinDist) return result; + // result.push(candidate.item); + result.push(candidate.i); + if (result.length === maxResults) return result; + } + + // the next closest kd-tree node + node = q.pop(); + } + + return result; +} + +// lower bound for distance from a location to points inside a bounding box +function boxDist(lng, lat, cosLat, node) { + var minLng = node.minLng; + var maxLng = node.maxLng; + var minLat = node.minLat; + var maxLat = node.maxLat; + + // query point is between minimum and maximum longitudes + if (lng >= minLng && lng <= maxLng) { + if (lat < minLat) return haverSin((lat - minLat) * rad); + if (lat > maxLat) return haverSin((lat - maxLat) * rad); + return 0; + } + + // query point is west or east of the bounding box; + // calculate the extremum for great circle distance from query point to the closest longitude; + var haverSinDLng = Math.min(haverSin((lng - minLng) * rad), haverSin((lng - maxLng) * rad)); + var extremumLat = vertexLat(lat, haverSinDLng); + + // if extremum is inside the box, return the distance to it + if (extremumLat > minLat && extremumLat < maxLat) { + return haverSinDistPartial(haverSinDLng, cosLat, lat, extremumLat); + } + // otherwise return the distan e to one of the bbox corners (whichever is closest) + return Math.min( + haverSinDistPartial(haverSinDLng, cosLat, lat, minLat), + haverSinDistPartial(haverSinDLng, cosLat, lat, maxLat) + ); +} + +function compareDist(a, b) { + return a.dist - b.dist; +} + +function haverSin(theta) { + var s = Math.sin(theta / 2); + return s * s; +} + +function haverSinDistPartial(haverSinDLng, cosLat1, lat1, lat2) { + return cosLat1 * Math.cos(lat2 * rad) * haverSinDLng + haverSin((lat1 - lat2) * rad); +} + +function haverSinDist(lng1, lat1, lng2, lat2, cosLat1) { + var haverSinDLng = haverSin((lng1 - lng2) * rad); + return haverSinDistPartial(haverSinDLng, cosLat1, lat1, lat2); +} + +export function distance(lng1, lat1, lng2, lat2) { + var h = haverSinDist(lng1, lat1, lng2, lat2, Math.cos(lat1 * rad)); + return 2 * earthRadius * Math.asin(Math.sqrt(h)); +} + +function vertexLat(lat, haverSinDLng) { + var cosDLng = 1 - 2 * haverSinDLng; + if (cosDLng <= 0) return lat > 0 ? 90 : -90; + return Math.atan(Math.tan(lat * rad) / cosDLng) / rad; +} diff --git a/src/utils/mapshaper-utils.js b/src/utils/mapshaper-utils.js index 02b6597d3..cf503ecc8 100644 --- a/src/utils/mapshaper-utils.js +++ b/src/utils/mapshaper-utils.js @@ -231,6 +231,13 @@ export function difference(arr, other) { }); } +// Return the intersection of two arrays +export function intersection(a, b) { + return a.filter(function(el) { + return b.includes(el); + }); +} + export function indexOf(arr, item) { var nan = item !== item; for (var i = 0, len = arr.length || 0; i < len; i++) { diff --git a/test/data/features/join/ex1_polyA.json b/test/data/features/join/ex1_polyA.json new file mode 100644 index 000000000..1ce7ef767 --- /dev/null +++ b/test/data/features/join/ex1_polyA.json @@ -0,0 +1,4 @@ +{ + "type": "Polygon", + "coordinates": [[[1, 0], [1, 3], [4, 3], [4, 0], [1, 0]]] +} \ No newline at end of file diff --git a/test/data/features/join/ex1_polyB.json b/test/data/features/join/ex1_polyB.json new file mode 100644 index 000000000..1f5b1d6dc --- /dev/null +++ b/test/data/features/join/ex1_polyB.json @@ -0,0 +1,25 @@ +{ + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": {"id": "A"}, + "geometry": { + "type": "Polygon", + "coordinates": [[[0, 0], [0, 1], [2, 1], [2, 0], [0, 0]]] + } + }, { + "type": "Feature", + "properties": {"id": "B"}, + "geometry": { + "type": "Polygon", + "coordinates": [[[2, 0], [2, 1], [5, 1], [5, 0], [2, 0]]] + } + }, { + "type": "Feature", + "properties": {"id": "C"}, + "geometry": { + "type": "Polygon", + "coordinates": [[[0, 1], [0, 1.5], [5, 1.5], [5, 1], [0, 1]]] + } + }] +} \ No newline at end of file diff --git a/test/data/features/join/ex2_pointA.json b/test/data/features/join/ex2_pointA.json new file mode 100644 index 000000000..cd8c7c1a7 --- /dev/null +++ b/test/data/features/join/ex2_pointA.json @@ -0,0 +1,4 @@ +{ + "type": "Point", + "coordinates": [0,100] +} \ No newline at end of file diff --git a/test/data/features/join/ex2_pointB.json b/test/data/features/join/ex2_pointB.json new file mode 100644 index 000000000..9b2a4eee5 --- /dev/null +++ b/test/data/features/join/ex2_pointB.json @@ -0,0 +1,25 @@ +{ + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": {"id": "A"}, + "geometry": { + "type": "Point", + "coordinates": [0, 100] + } + }, { + "type": "Feature", + "properties": {"id": "B"}, + "geometry": { + "type": "Point", + "coordinates": [10, 100] + } + }, { + "type": "Feature", + "properties": {"id": "C"}, + "geometry": { + "type": "Point", + "coordinates": [100, 100] + } + }] +} \ No newline at end of file diff --git a/test/data/features/join/ex3_pointA.json b/test/data/features/join/ex3_pointA.json new file mode 100644 index 000000000..313550c7f --- /dev/null +++ b/test/data/features/join/ex3_pointA.json @@ -0,0 +1,4 @@ +{ + "type": "MultiPoint", + "coordinates": [[179, 1], [0,0]] +} \ No newline at end of file diff --git a/test/data/features/join/ex3_pointB.json b/test/data/features/join/ex3_pointB.json new file mode 100644 index 000000000..a13cc8fd9 --- /dev/null +++ b/test/data/features/join/ex3_pointB.json @@ -0,0 +1,25 @@ +{ + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": {"id": "A"}, + "geometry": { + "type": "Point", + "coordinates": [-179, 1] + } + }, { + "type": "Feature", + "properties": {"id": "B"}, + "geometry": { + "type": "Point", + "coordinates": [0, 2] + } + }, { + "type": "Feature", + "properties": {"id": "C"}, + "geometry": { + "type": "Point", + "coordinates": [90, 90] + } + }] +} \ No newline at end of file diff --git a/test/join-points-to-points-test.js b/test/join-points-to-points-test.js index a64d48ea9..3ce443d87 100644 --- a/test/join-points-to-points-test.js +++ b/test/join-points-to-points-test.js @@ -2,6 +2,54 @@ var api = require('../'), assert = require('assert'); describe('Points to points spatial join', function () { + + describe('lat-long coords', function () { + var a = 'test/data/features/join/ex3_pointA.json'; + var b = 'test/data/features/join/ex3_pointB.json'; + + it('antimeridian cross, etc', function (done) { + var cmd = `-i ${a} -join ${b} max-distance=500km calc='ids = collect(id)' -o out.json`; + api.applyCommands(cmd, {}, function(err, out) { + var json = JSON.parse(out['out.json']); + assert.deepEqual(json.features[0].properties, {ids: ['A', 'B']}) + done(); + }); + }) + + it('joining projected + unprojected datasets causes error', function (done) { + var cmd = `-i ${a} -proj init=merc -join ${b} max-distance=500km calc='ids = collect(id)' -o out.json`; + api.applyCommands(cmd, {}, function(err, out) { + assert.equal(err.name, 'UserError'); + done(); + }); + }) + + }) + + describe('projected coords, max-distance= option', function () { + var a = 'test/data/features/join/ex2_pointA.json'; + var b = 'test/data/features/join/ex2_pointB.json'; + it('test1', function(done) { + var cmd = `-i ${a} -join ${b} max-distance=12 calc='ids = collect(id)' -o out.json`; + api.applyCommands(cmd, {}, function(err, out) { + var json = JSON.parse(out['out.json']); + assert.deepEqual(json.features[0].properties, {ids: ['A', 'B']}) + done(); + }); + }) + + it('test2', function(done) { + var cmd = `-i ${a} -join ${b} max-distance=1 calc='ids = collect(id)' -o out.json`; + api.applyCommands(cmd, {}, function(err, out) { + var json = JSON.parse(out['out.json']); + assert.deepEqual(json.features[0].properties, {ids: ['A']}) + done(); + }); + }) + }) + + + it('simple one-point join', function(done) { var a = { type: 'Point', diff --git a/test/join-points-to-polygons-test.js b/test/join-points-to-polygons-test.js index 4a959378d..5db86060f 100644 --- a/test/join-points-to-polygons-test.js +++ b/test/join-points-to-polygons-test.js @@ -108,7 +108,12 @@ describe('Points to polygons and polygons to points spatial join', function () { }; var opts = {calc: "joins = _.count(), total=sum(count)"}; api.internal.joinPointsToPolygons(target.layers[0], target.arcs, src, opts); - assert.deepEqual(target.layers[0].data.getRecords(), [{total: 4, joins: 2, foo: 'a', count: 1}] ) + assert.deepEqual(target.layers[0].data.getRecords(), [{ + total: 4, + joins: 2, + // foo: 'a', + // count: 1 + }] ) }) it('simple polygon to point join', function () { diff --git a/test/join-polygons-to-polygons-test.js b/test/join-polygons-to-polygons-test.js index babfc7a59..532c0b0d2 100644 --- a/test/join-polygons-to-polygons-test.js +++ b/test/join-polygons-to-polygons-test.js @@ -10,9 +10,9 @@ describe('Polygons to polygons spatial joins', function () { var json = JSON.parse(output['out.json']); var rec = json.features[0].properties; assert.deepEqual(rec, { - name: 'A', // first joined value gets assigned + // name: 'A', // first joined value gets assigned names: ['A', 'B'], // collected names - group: 'foo', + // group: 'foo', groups: ['foo', 'foo'], // collected groups n: 2 }); diff --git a/test/join-test.js b/test/join-test.js index 0cab05c9f..8f7a50be7 100644 --- a/test/join-test.js +++ b/test/join-test.js @@ -399,14 +399,12 @@ describe('mapshaper-join.js', function () { assert.deepEqual(fields, ['st', 'co']); }) - // Changed in v0.74. - // it('Do not join all fields by default if calc= option is present', function () { - // var fields = api.internal.getFieldsToJoin([], ['st', 'co'], {calc: 'n=count()'}) - // assert.deepEqual(fields, []); - // }) - it('Join all fields by default even if calc= option is present', function () { + // Original behavior: copy all fields even if calc= is present (for consistency) + // v0.5.59: don't copy fields by default when calc= is present + // + it('Do not copy all fields by default if calc= option is present', function () { var fields = api.internal.getFieldsToJoin([], ['st', 'co'], {calc: 'n=count()'}) - assert.deepEqual(fields, ['st', 'co']); + assert.deepEqual(fields, []); }) it('Error if type hints are present', function() { diff --git a/www/modules.js b/www/modules.js index 464057d05..abcba67b3 100644 --- a/www/modules.js +++ b/www/modules.js @@ -126,9 +126,7 @@ function fromByteArray (uint8) { // go through the array every three bytes, we'll deal with trailing stuff later for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { - parts.push(encodeChunk( - uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength) - )) + parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength))) } // pad the end with zeros, but make sure to not forget the extra bytes @@ -3607,6 +3605,7 @@ StripBOMWrapper.prototype.end = function() { },{}],22:[function(require,module,exports){ +/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ exports.read = function (buffer, offset, isLE, mLen, nBytes) { var e, m var eLen = (nBytes * 8) - mLen - 1 @@ -4725,7 +4724,7 @@ exports.writeFile = dashify(require("./write-file"), "/dev/stdout"); exports.writeFileSync = dashify(require("./write-file-sync"), "/dev/stdout"); },{"./read-file":33,"./read-file-sync":32,"./write-file":35,"./write-file-sync":34}],30:[function(require,module,exports){ -(function (Buffer){ +(function (Buffer){(function (){ module.exports = function(options) { if (options) { if (typeof options === "string") return encoding(options); @@ -4750,9 +4749,9 @@ function encoding(encoding) { }; } -}).call(this,require("buffer").Buffer) +}).call(this)}).call(this,require("buffer").Buffer) },{"buffer":"buffer"}],31:[function(require,module,exports){ -(function (Buffer){ +(function (Buffer){(function (){ module.exports = function(data, options) { return typeof data === "string" ? new Buffer(data, typeof options === "string" ? options @@ -4761,9 +4760,9 @@ module.exports = function(data, options) { : data; }; -}).call(this,require("buffer").Buffer) +}).call(this)}).call(this,require("buffer").Buffer) },{"buffer":"buffer"}],32:[function(require,module,exports){ -(function (Buffer){ +(function (Buffer){(function (){ var fs = require("fs"), decode = require("./decode"); @@ -4794,9 +4793,9 @@ module.exports = function(filename, options) { var bufferSize = 1 << 16; -}).call(this,require("buffer").Buffer) +}).call(this)}).call(this,require("buffer").Buffer) },{"./decode":30,"buffer":"buffer","fs":"fs"}],33:[function(require,module,exports){ -(function (process){ +(function (process){(function (){ var fs = require("fs"), decode = require("./decode"); @@ -4821,7 +4820,7 @@ function readStream(stream, options, callback) { stream.on("end", function() { callback(null, decoder.value()); }); } -}).call(this,require('_process')) +}).call(this)}).call(this,require('_process')) },{"./decode":30,"_process":23,"fs":"fs"}],34:[function(require,module,exports){ var fs = require("fs"), encode = require("./encode"); @@ -4857,7 +4856,7 @@ module.exports = function(filename, data, options) { }; },{"./encode":31,"fs":"fs"}],35:[function(require,module,exports){ -(function (process){ +(function (process){(function (){ var fs = require("fs"), encode = require("./encode"); @@ -4881,7 +4880,7 @@ function writeStream(stream, send, data, options, callback) { stream[send](encode(data, options), function(error) { callback(error && error.code === "EPIPE" ? null : error); }); } -}).call(this,require('_process')) +}).call(this)}).call(this,require('_process')) },{"./encode":31,"_process":23,"fs":"fs"}],36:[function(require,module,exports){ /* eslint-disable node/no-deprecated-api */ var buffer = require('buffer') @@ -4947,7 +4946,7 @@ SafeBuffer.allocUnsafeSlow = function (size) { } },{"buffer":"buffer"}],37:[function(require,module,exports){ -(function (process){ +(function (process){(function (){ /* eslint-disable node/no-deprecated-api */ 'use strict' @@ -5026,7 +5025,7 @@ if (!safer.constants) { module.exports = safer -}).call(this,require('_process')) +}).call(this)}).call(this,require('_process')) },{"_process":23,"buffer":"buffer"}],38:[function(require,module,exports){ // Copyright Joyent, Inc. and other Node contributors. // @@ -5345,7 +5344,7 @@ function handleQs(url, query) { exports["default"] = handleQs; },{"qs":25}],"buffer":[function(require,module,exports){ -(function (Buffer){ +(function (Buffer){(function (){ /*! * The buffer module from node.js, for the browser. * @@ -7124,7 +7123,7 @@ function numberIsNaN (obj) { return obj !== obj // eslint-disable-line no-self-compare } -}).call(this,require("buffer").Buffer) +}).call(this)}).call(this,require("buffer").Buffer) },{"base64-js":1,"buffer":"buffer","ieee754":22}],"d3-color":[function(require,module,exports){ // https://d3js.org/d3-color/ v2.0.0 Copyright 2020 Mike Bostock (function (global, factory) { @@ -9285,7 +9284,7 @@ return Flatbush; },{}],"fs":[function(require,module,exports){ arguments[4][2][0].apply(exports,arguments) },{"dup":2}],"iconv-lite":[function(require,module,exports){ -(function (process){ +(function (process){(function (){ "use strict"; // Some environments don't have global Buffer (e.g. React Native). @@ -9440,9 +9439,207 @@ if ("Ā" != "\u0100") { console.error("iconv-lite warning: javascript files use encoding different from utf-8. See https://github.com/ashtuchkin/iconv-lite/wiki/Javascript-source-file-encodings for more info."); } -}).call(this,require('_process')) -},{"../encodings":6,"./bom-handling":21,"./extend-node":2,"./streams":2,"_process":23,"safer-buffer":37}],"mproj":[function(require,module,exports){ -(function (__filename){ +}).call(this)}).call(this,require('_process')) +},{"../encodings":6,"./bom-handling":21,"./extend-node":2,"./streams":2,"_process":23,"safer-buffer":37}],"kdbush":[function(require,module,exports){ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : +typeof define === 'function' && define.amd ? define(factory) : +(global.KDBush = factory()); +}(this, (function () { 'use strict'; + +function sortKD(ids, coords, nodeSize, left, right, depth) { + if (right - left <= nodeSize) { return; } + + var m = (left + right) >> 1; + + select(ids, coords, m, left, right, depth % 2); + + sortKD(ids, coords, nodeSize, left, m - 1, depth + 1); + sortKD(ids, coords, nodeSize, m + 1, right, depth + 1); +} + +function select(ids, coords, k, left, right, inc) { + + while (right > left) { + if (right - left > 600) { + var n = right - left + 1; + var m = k - left + 1; + var z = Math.log(n); + var s = 0.5 * Math.exp(2 * z / 3); + var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1); + var newLeft = Math.max(left, Math.floor(k - m * s / n + sd)); + var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd)); + select(ids, coords, k, newLeft, newRight, inc); + } + + var t = coords[2 * k + inc]; + var i = left; + var j = right; + + swapItem(ids, coords, left, k); + if (coords[2 * right + inc] > t) { swapItem(ids, coords, left, right); } + + while (i < j) { + swapItem(ids, coords, i, j); + i++; + j--; + while (coords[2 * i + inc] < t) { i++; } + while (coords[2 * j + inc] > t) { j--; } + } + + if (coords[2 * left + inc] === t) { swapItem(ids, coords, left, j); } + else { + j++; + swapItem(ids, coords, j, right); + } + + if (j <= k) { left = j + 1; } + if (k <= j) { right = j - 1; } + } +} + +function swapItem(ids, coords, i, j) { + swap(ids, i, j); + swap(coords, 2 * i, 2 * j); + swap(coords, 2 * i + 1, 2 * j + 1); +} + +function swap(arr, i, j) { + var tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; +} + +function range(ids, coords, minX, minY, maxX, maxY, nodeSize) { + var stack = [0, ids.length - 1, 0]; + var result = []; + var x, y; + + while (stack.length) { + var axis = stack.pop(); + var right = stack.pop(); + var left = stack.pop(); + + if (right - left <= nodeSize) { + for (var i = left; i <= right; i++) { + x = coords[2 * i]; + y = coords[2 * i + 1]; + if (x >= minX && x <= maxX && y >= minY && y <= maxY) { result.push(ids[i]); } + } + continue; + } + + var m = Math.floor((left + right) / 2); + + x = coords[2 * m]; + y = coords[2 * m + 1]; + + if (x >= minX && x <= maxX && y >= minY && y <= maxY) { result.push(ids[m]); } + + var nextAxis = (axis + 1) % 2; + + if (axis === 0 ? minX <= x : minY <= y) { + stack.push(left); + stack.push(m - 1); + stack.push(nextAxis); + } + if (axis === 0 ? maxX >= x : maxY >= y) { + stack.push(m + 1); + stack.push(right); + stack.push(nextAxis); + } + } + + return result; +} + +function within(ids, coords, qx, qy, r, nodeSize) { + var stack = [0, ids.length - 1, 0]; + var result = []; + var r2 = r * r; + + while (stack.length) { + var axis = stack.pop(); + var right = stack.pop(); + var left = stack.pop(); + + if (right - left <= nodeSize) { + for (var i = left; i <= right; i++) { + if (sqDist(coords[2 * i], coords[2 * i + 1], qx, qy) <= r2) { result.push(ids[i]); } + } + continue; + } + + var m = Math.floor((left + right) / 2); + + var x = coords[2 * m]; + var y = coords[2 * m + 1]; + + if (sqDist(x, y, qx, qy) <= r2) { result.push(ids[m]); } + + var nextAxis = (axis + 1) % 2; + + if (axis === 0 ? qx - r <= x : qy - r <= y) { + stack.push(left); + stack.push(m - 1); + stack.push(nextAxis); + } + if (axis === 0 ? qx + r >= x : qy + r >= y) { + stack.push(m + 1); + stack.push(right); + stack.push(nextAxis); + } + } + + return result; +} + +function sqDist(ax, ay, bx, by) { + var dx = ax - bx; + var dy = ay - by; + return dx * dx + dy * dy; +} + +var defaultGetX = function (p) { return p[0]; }; +var defaultGetY = function (p) { return p[1]; }; + +var KDBush = function KDBush(points, getX, getY, nodeSize, ArrayType) { + if ( getX === void 0 ) getX = defaultGetX; + if ( getY === void 0 ) getY = defaultGetY; + if ( nodeSize === void 0 ) nodeSize = 64; + if ( ArrayType === void 0 ) ArrayType = Float64Array; + + this.nodeSize = nodeSize; + this.points = points; + + var IndexArrayType = points.length < 65536 ? Uint16Array : Uint32Array; + + var ids = this.ids = new IndexArrayType(points.length); + var coords = this.coords = new ArrayType(points.length * 2); + + for (var i = 0; i < points.length; i++) { + ids[i] = i; + coords[2 * i] = getX(points[i]); + coords[2 * i + 1] = getY(points[i]); + } + + sortKD(ids, coords, nodeSize, 0, ids.length - 1, 0); +}; + +KDBush.prototype.range = function range$1 (minX, minY, maxX, maxY) { + return range(this.ids, this.coords, minX, minY, maxX, maxY, this.nodeSize); +}; + +KDBush.prototype.within = function within$1 (x, y, r) { + return within(this.ids, this.coords, x, y, r, this.nodeSize); +}; + +return KDBush; + +}))); + +},{}],"mproj":[function(require,module,exports){ +(function (__filename){(function (){ (function(){ // add math.h functions to library scope @@ -21535,11 +21732,11 @@ function pj_latlong_from_proj(P) { }()); -}).call(this,"/node_modules/mproj/dist/mproj.js") +}).call(this)}).call(this,"/node_modules/mproj/dist/mproj.js") },{"fs":"fs","path":"path"}],"path":[function(require,module,exports){ -(function (process){ -// .dirname, .basename, and .extname methods are extracted from Node.js v8.11.1, -// backported and transplited with Babel, with backwards-compat fixes +(function (process){(function (){ +// 'path' module extracted from Node.js v8.11.1 (only the posix part) +// transplited with Babel // Copyright Joyent, Inc. and other Node contributors. // @@ -21562,286 +21759,513 @@ function pj_latlong_from_proj(P) { // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -// resolves . and .. elements in a path array with directory names there -// must be no slashes, empty elements, or device names (c:\) in the array -// (so also no leading and trailing slashes - it does not distinguish -// relative and absolute paths) -function normalizeArray(parts, allowAboveRoot) { - // if the path tries to go above the root, `up` ends up > 0 - var up = 0; - for (var i = parts.length - 1; i >= 0; i--) { - var last = parts[i]; - if (last === '.') { - parts.splice(i, 1); - } else if (last === '..') { - parts.splice(i, 1); - up++; - } else if (up) { - parts.splice(i, 1); - up--; - } +'use strict'; + +function assertPath(path) { + if (typeof path !== 'string') { + throw new TypeError('Path must be a string. Received ' + JSON.stringify(path)); } +} - // if the path is allowed to go above the root, restore leading ..s - if (allowAboveRoot) { - for (; up--; up) { - parts.unshift('..'); +// Resolves . and .. elements in a path with directory names +function normalizeStringPosix(path, allowAboveRoot) { + var res = ''; + var lastSegmentLength = 0; + var lastSlash = -1; + var dots = 0; + var code; + for (var i = 0; i <= path.length; ++i) { + if (i < path.length) + code = path.charCodeAt(i); + else if (code === 47 /*/*/) + break; + else + code = 47 /*/*/; + if (code === 47 /*/*/) { + if (lastSlash === i - 1 || dots === 1) { + // NOOP + } else if (lastSlash !== i - 1 && dots === 2) { + if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 /*.*/ || res.charCodeAt(res.length - 2) !== 46 /*.*/) { + if (res.length > 2) { + var lastSlashIndex = res.lastIndexOf('/'); + if (lastSlashIndex !== res.length - 1) { + if (lastSlashIndex === -1) { + res = ''; + lastSegmentLength = 0; + } else { + res = res.slice(0, lastSlashIndex); + lastSegmentLength = res.length - 1 - res.lastIndexOf('/'); + } + lastSlash = i; + dots = 0; + continue; + } + } else if (res.length === 2 || res.length === 1) { + res = ''; + lastSegmentLength = 0; + lastSlash = i; + dots = 0; + continue; + } + } + if (allowAboveRoot) { + if (res.length > 0) + res += '/..'; + else + res = '..'; + lastSegmentLength = 2; + } + } else { + if (res.length > 0) + res += '/' + path.slice(lastSlash + 1, i); + else + res = path.slice(lastSlash + 1, i); + lastSegmentLength = i - lastSlash - 1; + } + lastSlash = i; + dots = 0; + } else if (code === 46 /*.*/ && dots !== -1) { + ++dots; + } else { + dots = -1; } } + return res; +} - return parts; +function _format(sep, pathObject) { + var dir = pathObject.dir || pathObject.root; + var base = pathObject.base || (pathObject.name || '') + (pathObject.ext || ''); + if (!dir) { + return base; + } + if (dir === pathObject.root) { + return dir + base; + } + return dir + sep + base; } -// path.resolve([from ...], to) -// posix version -exports.resolve = function() { - var resolvedPath = '', - resolvedAbsolute = false; +var posix = { + // path.resolve([from ...], to) + resolve: function resolve() { + var resolvedPath = ''; + var resolvedAbsolute = false; + var cwd; - for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { - var path = (i >= 0) ? arguments[i] : process.cwd(); + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path; + if (i >= 0) + path = arguments[i]; + else { + if (cwd === undefined) + cwd = process.cwd(); + path = cwd; + } + + assertPath(path); - // Skip empty and invalid entries - if (typeof path !== 'string') { - throw new TypeError('Arguments to path.resolve must be strings'); - } else if (!path) { - continue; + // Skip empty entries + if (path.length === 0) { + continue; + } + + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charCodeAt(0) === 47 /*/*/; } - resolvedPath = path + '/' + resolvedPath; - resolvedAbsolute = path.charAt(0) === '/'; - } + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) - // At this point the path should be resolved to a full absolute path, but - // handle relative paths to be safe (might happen when process.cwd() fails) + // Normalize the path + resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute); + + if (resolvedAbsolute) { + if (resolvedPath.length > 0) + return '/' + resolvedPath; + else + return '/'; + } else if (resolvedPath.length > 0) { + return resolvedPath; + } else { + return '.'; + } + }, - // Normalize the path - resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) { - return !!p; - }), !resolvedAbsolute).join('/'); + normalize: function normalize(path) { + assertPath(path); - return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; -}; + if (path.length === 0) return '.'; -// path.normalize(path) -// posix version -exports.normalize = function(path) { - var isAbsolute = exports.isAbsolute(path), - trailingSlash = substr(path, -1) === '/'; + var isAbsolute = path.charCodeAt(0) === 47 /*/*/; + var trailingSeparator = path.charCodeAt(path.length - 1) === 47 /*/*/; - // Normalize the path - path = normalizeArray(filter(path.split('/'), function(p) { - return !!p; - }), !isAbsolute).join('/'); + // Normalize the path + path = normalizeStringPosix(path, !isAbsolute); - if (!path && !isAbsolute) { - path = '.'; - } - if (path && trailingSlash) { - path += '/'; - } + if (path.length === 0 && !isAbsolute) path = '.'; + if (path.length > 0 && trailingSeparator) path += '/'; - return (isAbsolute ? '/' : '') + path; -}; + if (isAbsolute) return '/' + path; + return path; + }, -// posix version -exports.isAbsolute = function(path) { - return path.charAt(0) === '/'; -}; + isAbsolute: function isAbsolute(path) { + assertPath(path); + return path.length > 0 && path.charCodeAt(0) === 47 /*/*/; + }, -// posix version -exports.join = function() { - var paths = Array.prototype.slice.call(arguments, 0); - return exports.normalize(filter(paths, function(p, index) { - if (typeof p !== 'string') { - throw new TypeError('Arguments to path.join must be strings'); + join: function join() { + if (arguments.length === 0) + return '.'; + var joined; + for (var i = 0; i < arguments.length; ++i) { + var arg = arguments[i]; + assertPath(arg); + if (arg.length > 0) { + if (joined === undefined) + joined = arg; + else + joined += '/' + arg; + } } - return p; - }).join('/')); -}; + if (joined === undefined) + return '.'; + return posix.normalize(joined); + }, + relative: function relative(from, to) { + assertPath(from); + assertPath(to); -// path.relative(from, to) -// posix version -exports.relative = function(from, to) { - from = exports.resolve(from).substr(1); - to = exports.resolve(to).substr(1); + if (from === to) return ''; - function trim(arr) { - var start = 0; - for (; start < arr.length; start++) { - if (arr[start] !== '') break; + from = posix.resolve(from); + to = posix.resolve(to); + + if (from === to) return ''; + + // Trim any leading backslashes + var fromStart = 1; + for (; fromStart < from.length; ++fromStart) { + if (from.charCodeAt(fromStart) !== 47 /*/*/) + break; } + var fromEnd = from.length; + var fromLen = fromEnd - fromStart; - var end = arr.length - 1; - for (; end >= 0; end--) { - if (arr[end] !== '') break; + // Trim any leading backslashes + var toStart = 1; + for (; toStart < to.length; ++toStart) { + if (to.charCodeAt(toStart) !== 47 /*/*/) + break; } + var toEnd = to.length; + var toLen = toEnd - toStart; - if (start > end) return []; - return arr.slice(start, end - start + 1); - } + // Compare paths to find the longest common path from root + var length = fromLen < toLen ? fromLen : toLen; + var lastCommonSep = -1; + var i = 0; + for (; i <= length; ++i) { + if (i === length) { + if (toLen > length) { + if (to.charCodeAt(toStart + i) === 47 /*/*/) { + // We get here if `from` is the exact base path for `to`. + // For example: from='/foo/bar'; to='/foo/bar/baz' + return to.slice(toStart + i + 1); + } else if (i === 0) { + // We get here if `from` is the root + // For example: from='/'; to='/foo' + return to.slice(toStart + i); + } + } else if (fromLen > length) { + if (from.charCodeAt(fromStart + i) === 47 /*/*/) { + // We get here if `to` is the exact base path for `from`. + // For example: from='/foo/bar/baz'; to='/foo/bar' + lastCommonSep = i; + } else if (i === 0) { + // We get here if `to` is the root. + // For example: from='/foo'; to='/' + lastCommonSep = 0; + } + } + break; + } + var fromCode = from.charCodeAt(fromStart + i); + var toCode = to.charCodeAt(toStart + i); + if (fromCode !== toCode) + break; + else if (fromCode === 47 /*/*/) + lastCommonSep = i; + } - var fromParts = trim(from.split('/')); - var toParts = trim(to.split('/')); + var out = ''; + // Generate the relative path based on the path difference between `to` + // and `from` + for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { + if (i === fromEnd || from.charCodeAt(i) === 47 /*/*/) { + if (out.length === 0) + out += '..'; + else + out += '/..'; + } + } - var length = Math.min(fromParts.length, toParts.length); - var samePartsLength = length; - for (var i = 0; i < length; i++) { - if (fromParts[i] !== toParts[i]) { - samePartsLength = i; - break; + // Lastly, append the rest of the destination (`to`) path that comes after + // the common path parts + if (out.length > 0) + return out + to.slice(toStart + lastCommonSep); + else { + toStart += lastCommonSep; + if (to.charCodeAt(toStart) === 47 /*/*/) + ++toStart; + return to.slice(toStart); } - } + }, - var outputParts = []; - for (var i = samePartsLength; i < fromParts.length; i++) { - outputParts.push('..'); - } + _makeLong: function _makeLong(path) { + return path; + }, - outputParts = outputParts.concat(toParts.slice(samePartsLength)); + dirname: function dirname(path) { + assertPath(path); + if (path.length === 0) return '.'; + var code = path.charCodeAt(0); + var hasRoot = code === 47 /*/*/; + var end = -1; + var matchedSlash = true; + for (var i = path.length - 1; i >= 1; --i) { + code = path.charCodeAt(i); + if (code === 47 /*/*/) { + if (!matchedSlash) { + end = i; + break; + } + } else { + // We saw the first non-path separator + matchedSlash = false; + } + } - return outputParts.join('/'); -}; + if (end === -1) return hasRoot ? '/' : '.'; + if (hasRoot && end === 1) return '//'; + return path.slice(0, end); + }, -exports.sep = '/'; -exports.delimiter = ':'; - -exports.dirname = function (path) { - if (typeof path !== 'string') path = path + ''; - if (path.length === 0) return '.'; - var code = path.charCodeAt(0); - var hasRoot = code === 47 /*/*/; - var end = -1; - var matchedSlash = true; - for (var i = path.length - 1; i >= 1; --i) { - code = path.charCodeAt(i); - if (code === 47 /*/*/) { - if (!matchedSlash) { - end = i; - break; - } - } else { - // We saw the first non-path separator - matchedSlash = false; - } - } + basename: function basename(path, ext) { + if (ext !== undefined && typeof ext !== 'string') throw new TypeError('"ext" argument must be a string'); + assertPath(path); - if (end === -1) return hasRoot ? '/' : '.'; - if (hasRoot && end === 1) { - // return '//'; - // Backwards-compat fix: - return '/'; - } - return path.slice(0, end); -}; + var start = 0; + var end = -1; + var matchedSlash = true; + var i; -function basename(path) { - if (typeof path !== 'string') path = path + ''; + if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { + if (ext.length === path.length && ext === path) return ''; + var extIdx = ext.length - 1; + var firstNonSlashEnd = -1; + for (i = path.length - 1; i >= 0; --i) { + var code = path.charCodeAt(i); + if (code === 47 /*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else { + if (firstNonSlashEnd === -1) { + // We saw the first non-path separator, remember this index in case + // we need it if the extension ends up not matching + matchedSlash = false; + firstNonSlashEnd = i + 1; + } + if (extIdx >= 0) { + // Try to match the explicit extension + if (code === ext.charCodeAt(extIdx)) { + if (--extIdx === -1) { + // We matched the extension, so mark this as the end of our path + // component + end = i; + } + } else { + // Extension does not match, so our result is the entire path + // component + extIdx = -1; + end = firstNonSlashEnd; + } + } + } + } - var start = 0; - var end = -1; - var matchedSlash = true; - var i; + if (start === end) end = firstNonSlashEnd;else if (end === -1) end = path.length; + return path.slice(start, end); + } else { + for (i = path.length - 1; i >= 0; --i) { + if (path.charCodeAt(i) === 47 /*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // path component + matchedSlash = false; + end = i + 1; + } + } - for (i = path.length - 1; i >= 0; --i) { - if (path.charCodeAt(i) === 47 /*/*/) { - // If we reached a path separator that was not part of a set of path - // separators at the end of the string, stop now - if (!matchedSlash) { - start = i + 1; - break; + if (end === -1) return ''; + return path.slice(start, end); + } + }, + + extname: function extname(path) { + assertPath(path); + var startDot = -1; + var startPart = 0; + var end = -1; + var matchedSlash = true; + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + var preDotState = 0; + for (var i = path.length - 1; i >= 0; --i) { + var code = path.charCodeAt(i); + if (code === 47 /*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; } - } else if (end === -1) { - // We saw the first non-path separator, mark this as the end of our - // path component - matchedSlash = false; - end = i + 1; + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === 46 /*.*/) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) + startDot = i; + else if (preDotState !== 1) + preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } } - } - if (end === -1) return ''; - return path.slice(start, end); -} + if (startDot === -1 || end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) { + return ''; + } + return path.slice(startDot, end); + }, -// Uses a mixed approach for backwards-compatibility, as ext behavior changed -// in new Node.js versions, so only basename() above is backported here -exports.basename = function (path, ext) { - var f = basename(path); - if (ext && f.substr(-1 * ext.length) === ext) { - f = f.substr(0, f.length - ext.length); - } - return f; -}; + format: function format(pathObject) { + if (pathObject === null || typeof pathObject !== 'object') { + throw new TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject); + } + return _format('/', pathObject); + }, -exports.extname = function (path) { - if (typeof path !== 'string') path = path + ''; - var startDot = -1; - var startPart = 0; - var end = -1; - var matchedSlash = true; - // Track the state of characters (if any) we see before our first dot and - // after any path separator we find - var preDotState = 0; - for (var i = path.length - 1; i >= 0; --i) { - var code = path.charCodeAt(i); - if (code === 47 /*/*/) { - // If we reached a path separator that was not part of a set of path - // separators at the end of the string, stop now - if (!matchedSlash) { - startPart = i + 1; - break; + parse: function parse(path) { + assertPath(path); + + var ret = { root: '', dir: '', base: '', ext: '', name: '' }; + if (path.length === 0) return ret; + var code = path.charCodeAt(0); + var isAbsolute = code === 47 /*/*/; + var start; + if (isAbsolute) { + ret.root = '/'; + start = 1; + } else { + start = 0; + } + var startDot = -1; + var startPart = 0; + var end = -1; + var matchedSlash = true; + var i = path.length - 1; + + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + var preDotState = 0; + + // Get non-dir info + for (; i >= start; --i) { + code = path.charCodeAt(i); + if (code === 47 /*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; } - continue; + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === 46 /*.*/) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) startDot = i;else if (preDotState !== 1) preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; } - if (end === -1) { - // We saw the first non-path separator, mark this as the end of our - // extension - matchedSlash = false; - end = i + 1; - } - if (code === 46 /*.*/) { - // If this is our first dot, mark it as the start of our extension - if (startDot === -1) - startDot = i; - else if (preDotState !== 1) - preDotState = 1; - } else if (startDot !== -1) { - // We saw a non-dot and non-path separator before our dot, so we should - // have a good chance at having a non-empty extension - preDotState = -1; - } - } - - if (startDot === -1 || end === -1 || - // We saw a non-dot character immediately before the dot - preDotState === 0 || - // The (right-most) trimmed path component is exactly '..' - preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) { - return ''; - } - return path.slice(startDot, end); -}; - -function filter (xs, f) { - if (xs.filter) return xs.filter(f); - var res = []; - for (var i = 0; i < xs.length; i++) { - if (f(xs[i], i, xs)) res.push(xs[i]); } - return res; -} -// String.prototype.substr - negative index don't work in IE8 -var substr = 'ab'.substr(-1) === 'b' - ? function (str, start, len) { return str.substr(start, len) } - : function (str, start, len) { - if (start < 0) start = str.length + start; - return str.substr(start, len); + if (startDot === -1 || end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) { + if (end !== -1) { + if (startPart === 0 && isAbsolute) ret.base = ret.name = path.slice(1, end);else ret.base = ret.name = path.slice(startPart, end); + } + } else { + if (startPart === 0 && isAbsolute) { + ret.name = path.slice(1, startDot); + ret.base = path.slice(1, end); + } else { + ret.name = path.slice(startPart, startDot); + ret.base = path.slice(startPart, end); + } + ret.ext = path.slice(startDot, end); } -; -}).call(this,require('_process')) + if (startPart > 0) ret.dir = path.slice(0, startPart - 1);else if (isAbsolute) ret.dir = '/'; + + return ret; + }, + + sep: '/', + delimiter: ':', + win32: null, + posix: null +}; + +posix.posix = posix; + +module.exports = posix; + +}).call(this)}).call(this,require('_process')) },{"_process":23}],"rw":[function(require,module,exports){ exports.dash = require("./lib/rw/dash"); exports.readFile = require("./lib/rw/read-file"); From 0e5ad8ed169250fa4a54190ed129b978d06b4aa6 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sat, 26 Jun 2021 20:12:30 -0400 Subject: [PATCH 069/891] Add save=as= and values= options to -dots command --- src/cli/mapshaper-options.js | 9 ++++++++- src/commands/mapshaper-dots.js | 6 ++++-- test/dots-test.js | 11 +++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 727dc73a8..144afa935 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -695,6 +695,13 @@ export function getOptionParser() { describe: 'one or more colors', type: 'strings' }) + .option('values', { + describe: 'values to assign to dot classes (alternative to colors=)', + type: 'strings' + }) + .option('save-as', { + describe: 'name of output field (default is fill)' + }) .option('r', { describe: 'radius of each dot in pixels', type: 'number' @@ -965,7 +972,7 @@ export function getOptionParser() { // type: 'flag' // }) .option('max-distance', { - describe: '(point-point join)', + describe: '(point-point join) join source points within this radius', type: 'distance' }) .option('planar', { diff --git a/src/commands/mapshaper-dots.js b/src/commands/mapshaper-dots.js index 33ad200f1..399119008 100644 --- a/src/commands/mapshaper-dots.js +++ b/src/commands/mapshaper-dots.js @@ -142,8 +142,10 @@ function shuffle(arr) { // opts: dots command options export function getDataRecord(i, d, opts) { var o = {}; - if (opts.colors) { - o.fill = opts.colors[i]; + var key = opts.save_as || 'fill'; + var values = opts.colors || opts.values; + if (values) { + o[key] = values[i]; o.r = opts.r || 1.3; } else if (opts.r) { o.r = opts.r; diff --git a/test/dots-test.js b/test/dots-test.js index 174256058..2d83f0771 100644 --- a/test/dots-test.js +++ b/test/dots-test.js @@ -17,6 +17,17 @@ describe('mapshaper-dots.js', function () { assert.deepEqual(out, {fill: 'green', r: 1.3}) }) + it('values option', function () { + var out = getDataRecord(1, null, {values: ['a', 'b']}); + assert.deepEqual(out, {fill: 'b', r: 1.3}) + }) + + it('save-as option', function () { + var out = getDataRecord(0, null, {values: ['a', 'b'], save_as: 'name'}); + assert.deepEqual(out, {name: 'a', r: 1.3}) + }) + + }) }) From 1034ec076ce909d66113a974a1555258876df57e Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sat, 26 Jun 2021 20:14:07 -0400 Subject: [PATCH 070/891] Update dependency --- package-lock.json | 48 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5c050d13f..ac85c8230 100644 --- a/package-lock.json +++ b/package-lock.json @@ -131,9 +131,9 @@ } }, "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dev": true, "requires": { "normalize-path": "^3.0.0", @@ -496,9 +496,9 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -575,9 +575,9 @@ "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -1173,9 +1173,9 @@ } }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { "is-glob": "^4.0.1" @@ -1676,9 +1676,9 @@ "dev": true }, "mocha": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.3.0.tgz", - "integrity": "sha512-TQqyC89V1J/Vxx0DhJIXlq9gbbL9XFNdeLQ1+JsnZsVaSOV1z3tWfw0qZmQJGQRIfkvZcs7snQnZnOCKoldq1Q==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", + "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", @@ -2535,9 +2535,9 @@ "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -2569,9 +2569,9 @@ "dev": true }, "y18n": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", - "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yargs": { @@ -2602,9 +2602,9 @@ "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { "emoji-regex": "^8.0.0", diff --git a/package.json b/package.json index 557e1bfc3..5c78207de 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "csv-spectrum": "^1.0.0", "deep-eql": ">=0.1.3", "esm": "^3.2.25", - "mocha": "^8.3.0", + "mocha": "^8.4.0", "rollup": "^2.28.2", "shell-quote": "^1.6.1", "underscore": "^1.13.1" From edd60ebe6c0375fb04b189ecbba2def0eae85bb2 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 28 Jun 2021 10:35:24 -0400 Subject: [PATCH 071/891] Add save-as= and values= options to -dots --- CHANGELOG.md | 3 +++ package-lock.json | 2 +- package.json | 2 +- src/cli/mapshaper-options.js | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6a51dec3..d78432898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.60 +* Added save-as= and values= options to -dots command (like -classify). + v0.5.59 * Added -merge-layers flatten option, for removing overlaps when merging multiple polygon layers. * Added -clean overlap-rule=[min-id|max-id|min-area|max-area] option. diff --git a/package-lock.json b/package-lock.json index ac85c8230..1136fbb50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.59", + "version": "0.5.60", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5c78207de..0ed213616 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.59", + "version": "0.5.60", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 144afa935..0124a78bf 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -700,7 +700,7 @@ export function getOptionParser() { type: 'strings' }) .option('save-as', { - describe: 'name of output field (default is fill)' + describe: 'name of color/value output field (default is fill)' }) .option('r', { describe: 'radius of each dot in pixels', From cecb860cb5ce2f6988b70d3db1c795aba2b41fa4 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 12 Jul 2021 08:06:51 -0400 Subject: [PATCH 072/891] wip and fixes --- src/cli/mapshaper-options.js | 4 ++++ src/color/blending.js | 27 ++++++++++++++++--------- src/commands/mapshaper-classify.js | 8 +++++++- src/commands/mapshaper-dots.js | 17 ++++------------ src/commands/mapshaper-point-to-grid.js | 6 ++++-- src/points/mapshaper-dot-density.js | 19 ++++++++++++----- src/utils/mapshaper-utils.js | 9 +++++++++ 7 files changed, 59 insertions(+), 31 deletions(-) diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 0124a78bf..13877e828 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -702,6 +702,10 @@ export function getOptionParser() { .option('save-as', { describe: 'name of color/value output field (default is fill)' }) + .option('progressive', { + // describe: 'fill in points progressively', + type: 'flag' + }) .option('r', { describe: 'radius of each dot in pixels', type: 'number' diff --git a/src/color/blending.js b/src/color/blending.js index c3684ea97..daf6d815b 100644 --- a/src/color/blending.js +++ b/src/color/blending.js @@ -3,19 +3,25 @@ import { stop } from '../utils/mapshaper-logging'; import { parseColor } from '../color/color-utils'; import { formatColor } from '../color/color-utils'; -export function blend() { - var args = Array.from(arguments); - var colors = [], - weights = [], - col, weight, i; - for (i=0; i 0 === false) { diff --git a/src/commands/mapshaper-classify.js b/src/commands/mapshaper-classify.js index d33c25d50..d99fefde9 100644 --- a/src/commands/mapshaper-classify.js +++ b/src/commands/mapshaper-classify.js @@ -46,7 +46,13 @@ cmd.classify = function(lyr, optsArg) { if (opts.index_field) { dataField = opts.index_field; - numBuckets = validateClassIndexField(records, opts.index_field); + if (numBuckets > 0 === false) { + stop('The index-field= option requires the classes= option to be set'); + } + // You can't infer the number of classes by looking at index values; + // this can cause unwanted interpolation if one or more values are + // not present in the index field + // numBuckets = validateClassIndexField(records, opts.index_field); } else if (opts.field) { dataField = opts.field; diff --git a/src/commands/mapshaper-dots.js b/src/commands/mapshaper-dots.js index 399119008..9bc8a851a 100644 --- a/src/commands/mapshaper-dots.js +++ b/src/commands/mapshaper-dots.js @@ -71,10 +71,10 @@ function makeDotsForShape(shp, arcs, rec, opts) { // randomize dot sequence so dots of the same color do not always overlap dots of // other colors in dense areas. // TODO: instead of random shuffling, interleave dot classes more regularly? - shuffle(indexes); + utils.shuffle(indexes); var idx, prevIdx = -1; var multipart = !!opts.multipart; - var coords, p; + var coords, p, d; for (var i=0; i 0; i--) { - j = Math.floor(Math.random() * (i + 1)); - tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; - } -} - // i: dot class index // d: properties of original polygon // opts: dots command options diff --git a/src/commands/mapshaper-point-to-grid.js b/src/commands/mapshaper-point-to-grid.js index cdfe706bc..9c1621069 100644 --- a/src/commands/mapshaper-point-to-grid.js +++ b/src/commands/mapshaper-point-to-grid.js @@ -80,13 +80,15 @@ function getPolygonDataset(pointLyr, gridBBox, opts) { function calcCellWeight(center, ids, points, opts) { // radius of circle with same area as the cell - var radius = opts.interval * Math.sqrt(1 / Math.PI); + var interval = opts.interval; + var radius = interval * Math.sqrt(1 / Math.PI); var circleArea = Math.PI * opts.radius * opts.radius; + var cellArea = interval * interval; var totArea = 0; for (var i=0; i= 0 ? Math.min(opts.evenness, 1) : 1; // TODO: skip tiny sliver polygons? if (n === 0) return []; - if (evenness === 0) return placeDotsRandomly(shp, arcs, n); + if (opts.evenness === 0) return placeDotsRandomly(shp, arcs, n); // TODO: if n == 1, consider using the 'inner' point of a polygon - return placeDotsEvenly(shp, arcs, n, evenness); + return placeDotsEvenly(shp, arcs, n, opts); } function placeDotsRandomly(shp, arcs, n) { @@ -38,11 +37,16 @@ function placeRandomDot(shp, arcs, bounds) { return null; } -function placeDotsEvenly(shp, arcs, n, evenness) { +function placeDotsEvenly(shp, arcs, n, opts) { + var evenness = opts.evenness >= 0 ? Math.min(opts.evenness, 1) : 1; var shpArea = geom.getPlanarShapeArea(shp, arcs); if (shpArea > 0 === false) return []; var bounds = arcs.getMultiShapeBounds(shp); var approxQueries = Math.round(n * bounds.area() / shpArea); + if (opts.progressive) { + // TODO: implement this properly + approxQueries = Math.ceil(approxQueries / 6); + } var grid = new DotGrid(bounds, approxQueries, evenness); var coords = []; for (var i=0; i 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } +} // Sort an array of objects based on one or more properties. // Usage: sortOn(array, key1, asc?[, key2, asc? ...]) From 979ff6784746f0bb67261a1f40fdd4fc8b093427 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 12 Jul 2021 08:09:12 -0400 Subject: [PATCH 073/891] v0.5.61 --- CHANGELOG.md | 3 +++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d78432898..37740e776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.61 +* Bug fixes + v0.5.60 * Added save-as= and values= options to -dots command (like -classify). diff --git a/package-lock.json b/package-lock.json index 1136fbb50..35c44b21b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.60", + "version": "0.5.61", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0ed213616..73db31ead 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.60", + "version": "0.5.61", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From f6bd2db5171a673f5444c9cfe1784a8a6e55070a Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 25 Jul 2021 22:34:31 -0400 Subject: [PATCH 074/891] Add decimal-comma option for importing CSV numbers with decimal commas --- src/cli/mapshaper-options.js | 5 +++++ src/text/mapshaper-delim-import.js | 15 ++++++++------- src/utils/mapshaper-utils.js | 25 +++++++++++++++++++------ test/delim-import-test.js | 29 ++++++++++++++++++++++++++++- 4 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 13877e828..212d57a4f 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -178,6 +178,10 @@ export function getOptionParser() { type: 'strings', describe: '[CSV] comma-sep. list of fields to import' }) + .option('decimal-comma', { + type: 'flag', + describe: '[CSV] import numbers formatted like 1.000,01 or 1 000,01' + }) .option('json-path', { old_alias: 'json-subtree', describe: '[JSON] path to JSON input data; separator is /' @@ -1671,6 +1675,7 @@ export function getOptionParser() { // describe: (0-1) inset grid shapes by a percentage type: 'number' }) + .option('calc', calcOpt) .option('name', nameOpt) .option('target', targetOpt) .option('no-replace', noReplaceOpt); diff --git a/src/text/mapshaper-delim-import.js b/src/text/mapshaper-delim-import.js index 98b2abc6b..1a78103aa 100644 --- a/src/text/mapshaper-delim-import.js +++ b/src/text/mapshaper-delim-import.js @@ -15,7 +15,6 @@ export function importDelim(str, opts) { // Convert a string, buffer or file containing delimited text into a dataset obj. export function importDelim2(data, opts) { - // TODO: remove duplication with importJSON() var readFromFile = !data.content && data.content !== '', content = data.content, @@ -127,24 +126,26 @@ export function getFieldTypeHints(opts) { // Detect and convert data types of data from csv files. // TODO: decide how to handle records with inconstent properties. Mapshaper // currently assumes tabular data -export function adjustRecordTypes(records, opts) { - var typeIndex = getFieldTypeHints(opts), +export function adjustRecordTypes(records, optsArg) { + var opts = optsArg || {}, + typeIndex = getFieldTypeHints(opts), singleType = typeIndex['*'], // support for setting all fields to a single type fields = Object.keys(records[0] || []), detectedNumFields = [], + parseNumber = opts.decimal_comma ? utils.parseIntlNumber : utils.parseNumber, replacements = {}; fields.forEach(function(key) { var typeHint = typeIndex[key]; var values = null; if (typeHint == 'number' || singleType == 'number') { - values = convertDataField(key, records, utils.parseNumber); + values = convertDataField(key, records, parseNumber); } else if (typeHint == 'string' || singleType == 'string') { // We should be able to assume that imported CSV fields are strings, // so parsing + replacement is not required // values = internal.convertDataField(key, records, utils.parseString); values = null; } else { - values = tryNumericField(key, records); + values = tryNumericField(key, records, parseNumber); if (values) detectedNumFields.push(key); } if (values) replacements[key] = values; @@ -172,13 +173,13 @@ function updateFieldsInRecords(fields, records, replacements) { }); } -function tryNumericField(key, records) { +function tryNumericField(key, records, parseNumber) { var arr = [], count = 0, raw, str, num; for (var i=0, n=records.length; i 0 && str != 'NA' && str != 'NaN') { // ignore NA values ("NA" seen in R output) diff --git a/src/utils/mapshaper-utils.js b/src/utils/mapshaper-utils.js index 150fc7b31..4e08ea9bf 100644 --- a/src/utils/mapshaper-utils.js +++ b/src/utils/mapshaper-utils.js @@ -945,11 +945,6 @@ export function uniqifyNames(names, formatter) { return names2; } -// Remove comma separators from strings -// TODO: accept European-style numbers? -export function cleanNumericString(str) { - return (str.indexOf(',') > 0) ? str.replace(/,([0-9]{3})/g, '$1') : str; -} // Assume: @raw is string, undefined or null export function parseString(raw) { @@ -961,11 +956,29 @@ export function parseString(raw) { // (in part because if NaN is used, empty strings get converted to "NaN" // when re-exported). export function parseNumber(raw) { + return parseToNum(raw, cleanNumericString); +} + +export function parseIntlNumber(raw) { + return parseToNum(raw, convertIntlNumString); +} + +function parseToNum(raw, clean) { var str = String(raw).trim(); - var parsed = str ? Number(cleanNumericString(str)) : NaN; + var parsed = str ? Number(clean(str)) : NaN; return isNaN(parsed) ? null : parsed; } +// Remove comma separators from strings +export function cleanNumericString(str) { + return (str.indexOf(',') > 0) ? str.replace(/,([0-9]{3})/g, '$1') : str; +} + +function convertIntlNumString(str) { + str = str.replace(/[ .]([0-9]{3})/g, '$1'); + return str.replace(',', '.'); +} + export function trimQuotes(raw) { var len = raw.length, first, last; if (len >= 2) { diff --git a/test/delim-import-test.js b/test/delim-import-test.js index d7498b082..73c364405 100644 --- a/test/delim-import-test.js +++ b/test/delim-import-test.js @@ -383,10 +383,37 @@ LOS ANGELES,,`; it('rejects dates', function() { assert.strictEqual(utils.parseNumber('2013-12-03'), null); }) + }) + + describe('parseIntlNumber()', function() { + it('csv-decimal-comma option', function(done) { + var csv = 'num\n"20,1"\n"-5,0"'; + var cmd = '-i data.csv decimal-comma -o format=json'; + api.applyCommands(cmd, {'data.csv': csv}, function(err, out) { + assert.deepEqual(JSON.parse(out['data.json']), [{num: 20.1}, {num: -5}]); + done(); + }); + }) + + + it('comma decimal', function() { + assert.equal(utils.parseIntlNumber('123,10'), 123.10); + }) + + it('point separator', function() { + assert.equal(utils.parseIntlNumber('1.000.000'), 1e6); + }) - // TODO: European decimals? + it('point and comma', function() { + assert.equal(utils.parseIntlNumber('1.000.000,5'), 1000000.5); + }) + + it('space and comma', function() { + assert.equal(utils.parseIntlNumber('1 000 000,5'), 1000000.5); + }) }) + describe('guessDelimiter()', function () { it('guesses CSV', function () { assert.equal(api.internal.guessDelimiter("a,b\n1,2"), ','); From 43601c52e04cadc8a5097e0395f81f836d365f56 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 25 Jul 2021 22:38:06 -0400 Subject: [PATCH 075/891] v0.5.62 --- CHANGELOG.md | 3 +++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37740e776..b67969d25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.62 +* Add -i decimal-comma option, for importing numbers from CSV files with numbers formatted like 1.000,23 or 1 000,23 + v0.5.61 * Bug fixes diff --git a/package-lock.json b/package-lock.json index 35c44b21b..bd6323434 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.61", + "version": "0.5.62", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 73db31ead..214facd8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.61", + "version": "0.5.62", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 21286f65b2b755aa944c88f8e870f17cdafb80ce Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sat, 31 Jul 2021 18:03:07 -0400 Subject: [PATCH 076/891] Add -o decimal-comma flag for formatting CSV numbers --- src/cli/mapshaper-options.js | 62 ++++++++++++++++-------------- src/text/mapshaper-delim-export.js | 43 ++++++++++++++------- test/delim-export-test.js | 6 +++ 3 files changed, 69 insertions(+), 42 deletions(-) diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 212d57a4f..02ad68ce5 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -227,44 +227,44 @@ export function getOptionParser() { type: 'flag' }) .option('encoding', { - describe: '(Shapefile/CSV) text encoding (default is utf8)' + describe: '[Shapefile/CSV] text encoding (default is utf8)' }) .option('field-order', { - describe: '(Shapefile/CSV) field-order=ascending sorts columns A-Z' + describe: '[Shapefile/CSV] field-order=ascending sorts columns A-Z' }) .option('id-field', { - describe: '(Topo/GeoJSON/SVG) field to use for id property', + describe: '[Topo/GeoJSON/SVG] field to use for id property', type: 'strings' }) .option('bbox', { type: 'flag', - describe: '(Topo/GeoJSON) add bbox property' + describe: '[Topo/GeoJSON] add bbox property' }) .option('extension', { - describe: '(Topo/GeoJSON) set file extension (default is ".json")' + describe: '[Topo/GeoJSON] set file extension (default is ".json")' }) .option('prettify', { type: 'flag', - describe: '(Topo/GeoJSON/JSON) format output for readability' + describe: '[Topo/GeoJSON/JSON] format output for readability' }) .option('singles', { - describe: '(TopoJSON) save each target layer as a separate file', + describe: '[TopoJSON] save each target layer as a separate file', type: 'flag' }) .option('quantization', { - describe: '(TopoJSON) specify quantization (auto-set by default)', + describe: '[TopoJSON] specify quantization (auto-set by default)', type: 'integer' }) .option('no-quantization', { - describe: '(TopoJSON) export coordinates without quantization', + describe: '[TopoJSON] export coordinates without quantization', type: 'flag' }) .option('no-point-quantization', { - // describe: '(TopoJSON) export point coordinates without quantization', + // describe: '[TopoJSON] export point coordinates without quantization', type: 'flag' }) .option('presimplify', { - describe: '(TopoJSON) add per-vertex data for dynamic simplification', + describe: '[TopoJSON] add per-vertex data for dynamic simplification', type: 'flag' }) .option('topojson-precision', { @@ -275,72 +275,76 @@ export function getOptionParser() { // obsolete -- rfc 7946 compatible outptu is now the default. // This option also rounds coordinates to 7 decimals. I'm retaining the // option for backwards compatibility. - // describe: '(GeoJSON) follow RFC 7946 (CCW outer ring order, etc.)', + // describe: '[GeoJSON] follow RFC 7946 (CCW outer ring order, etc.)', type: 'flag' }) // .option('winding', { - // describe: '(GeoJSON) set polygon winding order (use CW with d3-geo)' + // describe: '[GeoJSON] set polygon winding order (use CW with d3-geo)' // }) .option('gj2008', { - describe: '(GeoJSON) use original GeoJSON spec (not RFC 7946)', + describe: '[GeoJSON] use original GeoJSON spec (not RFC 7946)', type: 'flag' }) .option('combine-layers', { - describe: '(GeoJSON) output layers as a single file', + describe: '[GeoJSON] output layers as a single file', type: 'flag' }) .option('geojson-type', { - describe: '(GeoJSON) FeatureCollection, GeometryCollection or Feature' + describe: '[GeoJSON] FeatureCollection, GeometryCollection or Feature' }) .option('ndjson', { - describe: '(GeoJSON/JSON) output newline-delimited features or records', + describe: '[GeoJSON/JSON] output newline-delimited features or records', type: 'flag' }) .option('width', { - describe: '(SVG/TopoJSON) pixel width of output (SVG default is 800)', + describe: '[SVG/TopoJSON] pixel width of output (SVG default is 800)', type: 'number' }) .option('height', { - describe: '(SVG/TopoJSON) pixel height of output (optional)', + describe: '[SVG/TopoJSON] pixel height of output (optional)', type: 'number' }) .option('max-height', { - describe: '(SVG/TopoJSON) max pixel height of output (optional)', + describe: '[SVG/TopoJSON] max pixel height of output (optional)', type: 'number' }) .option('margin', { - describe: '(SVG/TopoJSON) space betw. data and viewport (default is 1)' + describe: '[SVG/TopoJSON] space betw. data and viewport (default is 1)' }) .option('pixels', { - describe: '(SVG/TopoJSON) output area in pix. (alternative to width=)', + describe: '[SVG/TopoJSON] output area in pix. (alternative to width=)', type: 'number' }) .option('fit-bbox', { type: 'bbox', - describe: '(TopoJSON) scale and shift coordinates to fit a bbox' + describe: '[TopoJSON] scale and shift coordinates to fit a bbox' }) .option('svg-scale', { - describe: '(SVG) source units per pixel (alternative to width= option)', + describe: '[SVG] source units per pixel (alternative to width= option)', type: 'number' }) .option('point-symbol', { - describe: '(SVG) circle or square (default is circle)' + describe: '[SVG] circle or square (default is circle)' }) .option('svg-data', { type: 'strings', - describe: '(SVG) fields to export as data-* attributes' + describe: '[SVG] fields to export as data-* attributes' }) .option('id-prefix', { - describe: '(SVG) prefix for namespacing layer and feature ids' + describe: '[SVG] prefix for namespacing layer and feature ids' }) .option('delimiter', { - describe: '(CSV) field delimiter' + describe: '[CSV] field delimiter' + }) + .option('decimal-comma', { + type: 'flag', + describe: '[CSV] export numbers with decimal commas not points' }) .option('final', { type: 'flag' // for testing }) .option('metadata', { - // describe: '(TopoJSON) add a metadata object', + // describe: '[TopoJSON] add a metadata object', type: 'flag' }); diff --git a/src/text/mapshaper-delim-export.js b/src/text/mapshaper-delim-export.js index 7f2797979..10f0adf1b 100644 --- a/src/text/mapshaper-delim-export.js +++ b/src/text/mapshaper-delim-export.js @@ -25,26 +25,25 @@ export function exportLayerAsDSV(lyr, delim, optsArg) { var encoding = opts.encoding || 'utf8'; var records = lyr.data.getRecords(); var fields = findFieldNames(records, opts.field_order); + var formatRow = getDelimRowFormatter(fields, delim, opts); // exporting utf8 and ascii text as string by default (for now) var exportAsString = encodingIsUtf8(encoding) && !opts.to_buffer && (records.length < 10000 || opts.to_string); if (exportAsString) { - return exportRecordsAsString(fields, records, delim); + return exportRecordsAsString(fields, records, formatRow); } else { - return exportRecordsAsBuffer(fields, records, delim, encoding); + return exportRecordsAsBuffer(fields, records, formatRow, encoding); } } -function exportRecordsAsString(fields, records, delim) { - var formatRow = getDelimRowFormatter(fields, delim); - var header = formatDelimHeader(fields, delim); +function exportRecordsAsString(fields, records, formatRow) { + var header = formatHeader(fields, formatRow); if (!records.length) return header; return header + '\n' + records.map(formatRow).join('\n'); } -function exportRecordsAsBuffer(fields, records, delim, encoding) { - var formatRow = getDelimRowFormatter(fields, delim); - var str = formatDelimHeader(fields, delim); +function exportRecordsAsBuffer(fields, records, formatRow, encoding) { + var str = formatHeader(fields, formatRow); var buffers = [encodeString(str, encoding)]; var tmp = []; var n = records.length; @@ -61,13 +60,21 @@ function exportRecordsAsBuffer(fields, records, delim, encoding) { return Buffer.concat(buffers); } +function formatHeader(fields, formatRow) { + var rec = fields.reduce(function(memo, f) { + memo[f] = f; + return memo; + }, {}); + return formatRow(rec); +} + function formatDelimHeader(fields, delim) { var formatValue = getDelimValueFormatter(delim); return fields.map(formatValue).join(delim); } -function getDelimRowFormatter(fields, delim) { - var formatValue = getDelimValueFormatter(delim); +function getDelimRowFormatter(fields, delim, opts) { + var formatValue = getDelimValueFormatter(delim, opts); return function(rec) { return fields.map(function(f) { return formatValue(rec[f]); @@ -75,8 +82,18 @@ function getDelimRowFormatter(fields, delim) { }; } -export function getDelimValueFormatter(delim) { - var dquoteRxp = new RegExp('["\n\r' + delim + ']'); +export function formatNumber(val) { + return val + ''; +} + +export function formatIntlNumber(val) { + var str = formatNumber(val); + return '"' + str.replace('.', ',') + '"'; // need to quote if comma-delimited +} + +export function getDelimValueFormatter(delim, opts) { + var dquoteRxp = new RegExp('["\n\r' + delim + ']'); + var decimalComma = opts && opts.decimal_comma || false; function formatString(s) { if (dquoteRxp.test(s)) { s = '"' + s.replace(/"/g, '""') + '"'; @@ -90,7 +107,7 @@ export function getDelimValueFormatter(delim) { } else if (utils.isString(val)) { s = formatString(val); } else if (utils.isNumber(val)) { - s = val + ''; + s = decimalComma ? formatIntlNumber(val) : formatNumber(val); } else if (utils.isObject(val)) { s = formatString(JSON.stringify(val)); } else { diff --git a/test/delim-export-test.js b/test/delim-export-test.js index 754fd4c9d..3c85cf8ae 100644 --- a/test/delim-export-test.js +++ b/test/delim-export-test.js @@ -24,6 +24,12 @@ describe('mapshaper-delim-export.js', function() { assert.equal(csv(-45), '-45'); assert.equal(csv(5.6), '5.6'); }) + it('Decimal comma', function() { + var csv = api.internal.getDelimValueFormatter(',', {decimal_comma: true}); + assert.equal(csv(0), '"0"'); + assert.equal(csv(5.6), '"5,6"'); + assert.equal(csv(-0.66), '"-0,66"'); + }) }) From 4bf982706af465612228a9381e6220f3fed2a6ea Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sat, 31 Jul 2021 18:06:15 -0400 Subject: [PATCH 077/891] Optimize interactive selection operations --- src/commands/mapshaper-each.js | 4 ++- src/expressions/mapshaper-expressions.js | 34 +++++++++++++----------- src/gui/gui-console.js | 1 - src/gui/gui-selection-tool.js | 20 +++++++------- src/svg/svg-properties.js | 3 ++- 5 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/commands/mapshaper-each.js b/src/commands/mapshaper-each.js index ebfad97d6..e7e82db4c 100644 --- a/src/commands/mapshaper-each.js +++ b/src/commands/mapshaper-each.js @@ -15,7 +15,9 @@ cmd.evaluateEachFeature = function(lyr, arcs, exp, opts) { if (opts && opts.where) { filter = compileValueExpression(opts.where, lyr, arcs); } - compiled = compileFeatureExpression(exp, lyr, arcs, {context: getStateVar('defs')}); + // 'defs' are now added to the context of all expressions + // compiled = compileFeatureExpression(exp, lyr, arcs, {context: getStateVar('defs')}); + compiled = compileFeatureExpression(exp, lyr, arcs); // call compiled expression with id of each record for (var i=0; i -1'); - } - function runCommand(cmd) { - if (gui.console) gui.console.runMapshaperCommands(cmd, function(err) {}); + var defs = internal.getStateVar('defs'); + defs.$$selection = utils.arrayToIndex(hit.getSelectionIds()); + if (gui.console) gui.console.runMapshaperCommands(cmd, function(err) { + delete defs.$$selection; + }); reset(); } } diff --git a/src/svg/svg-properties.js b/src/svg/svg-properties.js index 5cfa9bac8..e763c0073 100644 --- a/src/svg/svg-properties.js +++ b/src/svg/svg-properties.js @@ -131,7 +131,8 @@ export function getSymbolPropertyAccessor(strVal, svgName, lyr) { function parseStyleExpression(strVal, lyr) { var func; try { - func = compileValueExpression(strVal, lyr, null, {context: getStateVar('defs'), no_warn: true}); + // func = compileValueExpression(strVal, lyr, null, {context: getStateVar('defs'), no_warn: true}); + func = compileValueExpression(strVal, lyr, null, {no_warn: true}); func(0); // check for runtime errors (e.g. undefined variables) } catch(e) { func = null; From 185624ecee57cf53eb8e9351171fab27e6904444 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sat, 31 Jul 2021 18:06:55 -0400 Subject: [PATCH 078/891] Add -point-to-grid calc= option (WIP) --- src/commands/mapshaper-point-to-grid.js | 49 +++++++++++++++++++------ 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/src/commands/mapshaper-point-to-grid.js b/src/commands/mapshaper-point-to-grid.js index 9c1621069..f61351b82 100644 --- a/src/commands/mapshaper-point-to-grid.js +++ b/src/commands/mapshaper-point-to-grid.js @@ -14,6 +14,7 @@ import { cleanLayers } from '../commands/mapshaper-clean'; import { getPlanarSegmentEndpoint } from '../geom/mapshaper-geodesic'; import { getPointBufferCoordinates } from '../buffer/mapshaper-point-buffer'; import { IdTestIndex } from '../indexing/mapshaper-id-test-index'; +import { getJoinCalc } from '../join/mapshaper-join-calc'; cmd.pointToGrid = function(targetLayers, targetDataset, opts) { targetLayers.forEach(requirePointLayer); @@ -58,19 +59,23 @@ function getPolygonDataset(pointLyr, gridBBox, opts) { type: 'FeatureCollection', features: [] }; - var cands, center, weight; + var calc = null; + var cands, center, weight, d; + if (opts.calc) { + calc = getJoinCalc(pointLyr.data, opts.calc); + } + for (var i=0; i 0.05 === false) continue; + d = calcCellProperties(center, cands, points, calc, opts); + // weight = calcCellWeight(center, cands, points, opts); + if (d.weight > 0.05 === false) continue; + d.id = i; geojson.features.push({ type: 'Feature', - properties: { - id: i, - weight: weight - }, + properties: d, geometry: makeCellPolygon(i, grid, opts) }); } @@ -78,19 +83,41 @@ function getPolygonDataset(pointLyr, gridBBox, opts) { return dataset; } -function calcCellWeight(center, ids, points, opts) { +function calcCellProperties(center, cands, points, calc, opts) { // radius of circle with same area as the cell var interval = opts.interval; var radius = interval * Math.sqrt(1 / Math.PI); var circleArea = Math.PI * opts.radius * opts.radius; var cellArea = interval * interval; + var ids = []; var totArea = 0; - for (var i=0; i 0 === false) continue; + totArea += intersection; + ids.push(cands[i]); } - return totArea / cellArea; + var d = {weight: totArea / cellArea}; + if (calc) { + calc(ids, d); + } + return d; } +// function calcCellWeight(center, ids, points, opts) { +// // radius of circle with same area as the cell +// var interval = opts.interval; +// var radius = interval * Math.sqrt(1 / Math.PI); +// var circleArea = Math.PI * opts.radius * opts.radius; +// var cellArea = interval * interval; +// var totArea = 0; +// for (var i=0; i Date: Sun, 1 Aug 2021 01:29:33 -0400 Subject: [PATCH 079/891] Fix for issue #497 --- CHANGELOG.md | 6 +++++- package.json | 2 +- src/clipping/mapshaper-polygon-clipping.js | 10 ++++------ src/commands/mapshaper-clip-erase.js | 8 +++++++- src/dissolve/mapshaper-polygon-dissolve2.js | 1 + test/clip-erase-test.js | 4 ++-- test/data/features/clip/ex3_inner.json | 7 +++++++ test/data/features/clip/ex3_outer.json | 4 ++++ 8 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 test/data/features/clip/ex3_inner.json create mode 100644 test/data/features/clip/ex3_outer.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b67969d25..d2050753a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ +v0.5.63 +* Added -o decimal-comma function, for exporting CSV numbers with decimal commas. +* Fix for issue #497 (error erasing with overlapping polygons). + v0.5.62 -* Add -i decimal-comma option, for importing numbers from CSV files with numbers formatted like 1.000,23 or 1 000,23 +* Added -i decimal-comma option, for importing numbers from CSV files with numbers formatted like 1.000,23 or 1 000,23 v0.5.61 * Bug fixes diff --git a/package.json b/package.json index 214facd8e..53bcc7a9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.62", + "version": "0.5.63", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/clipping/mapshaper-polygon-clipping.js b/src/clipping/mapshaper-polygon-clipping.js index b2f09d446..6bf7932b4 100644 --- a/src/clipping/mapshaper-polygon-clipping.js +++ b/src/clipping/mapshaper-polygon-clipping.js @@ -30,12 +30,10 @@ export function clipPolygons(targetShapes, clipShapes, nodes, type, optsArg) { targetShapes = targetShapes.map(dissolvePolygon); } - // NOTE: commenting-out dissolve of clipping shapes, because the dissolve function - // does not tolerate overlapping shapes and some other topology errors. - // Dissolving was an optimization intended to improve performance when using a - // mosaic (e.g. counties, states) to clip or erase another layer. The user - // can optimize this case by dissolving as a separate step. - // // merge rings of clip/erase polygons and dissolve them all + // Originally, clip shapes were dissolved here as an optimization, using + // an unreliable dissolve function. + // Now, clip shapes are dissolved using a more reliable (but slower) + // function in mapshaper-clip-erase.js // clipShapes = [dissolvePolygon(internal.concatShapes(clipShapes))]; // Open pathways in the clip/erase layer diff --git a/src/commands/mapshaper-clip-erase.js b/src/commands/mapshaper-clip-erase.js index 9fd5f4f47..a339f940d 100644 --- a/src/commands/mapshaper-clip-erase.js +++ b/src/commands/mapshaper-clip-erase.js @@ -13,6 +13,7 @@ import utils from '../utils/mapshaper-utils'; import { ArcCollection } from '../paths/mapshaper-arcs'; import { NodeCollection } from '../topology/mapshaper-nodes'; import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; +import { dissolvePolygonLayer2 } from '../dissolve/mapshaper-polygon-dissolve2'; cmd.clipLayers = function(target, src, dataset, opts) { return clipLayers(target, src, dataset, "clip", opts); @@ -59,15 +60,20 @@ export function clipLayers(targetLayers, clipSrc, targetDataset, type, opts) { return clipLayersByBBox(targetLayers, targetDataset, opts); } mergedDataset = mergeLayersForOverlay(targetLayers, targetDataset, clipSrc, opts); + clipLyr = mergedDataset.layers[mergedDataset.layers.length-1]; if (usingPathClip) { // add vertices at all line intersections // (generally slower than actual clipping) nodes = addIntersectionCuts(mergedDataset, opts); targetDataset.arcs = mergedDataset.arcs; + // dissolve clip layer shapes (to remove overlaps and other topological issues + // that might confuse the clipping function) + clipLyr = dissolvePolygonLayer2(clipLyr, mergedDataset, {quiet: true, silent: true}); + } else { nodes = new NodeCollection(mergedDataset.arcs); } - clipLyr = mergedDataset.layers.pop(); + // clipLyr = mergedDataset.layers.pop(); return clipLayersByLayer(targetLayers, clipLyr, nodes, type, opts); } diff --git a/src/dissolve/mapshaper-polygon-dissolve2.js b/src/dissolve/mapshaper-polygon-dissolve2.js index 597ec57be..ae0c4cc99 100644 --- a/src/dissolve/mapshaper-polygon-dissolve2.js +++ b/src/dissolve/mapshaper-polygon-dissolve2.js @@ -94,6 +94,7 @@ export function dissolvePolygonGroups2(groups, lyr, dataset, opts) { // convert self-intersecting rings to outer/inner rings, for OGC // Simple Features compliance dissolvedShapes = fixTangentHoles(dissolvedShapes, pathfind); + if (fillGaps && !opts.quiet) { var msg = getGapRemovalMessage(cleanupData.removed, cleanupData.remaining, filterData.label); if (msg) message(msg); diff --git a/test/clip-erase-test.js b/test/clip-erase-test.js index 70f535e30..921672674 100644 --- a/test/clip-erase-test.js +++ b/test/clip-erase-test.js @@ -671,8 +671,8 @@ describe('mapshaper-clip-erase.js', function () { }; var erasedLyr = api.eraseLayer(lyr1, lyr2, dataset); - var target = [[[0], [~2, ~3]]]; - //var target = [[[0], [~3, ~2]]]; + //var target = [[[0], [~2, ~3]]]; + var target = [[[0], [~3, ~2]]]; assert.deepEqual(erasedLyr.shapes, target); }) }) diff --git a/test/data/features/clip/ex3_inner.json b/test/data/features/clip/ex3_inner.json new file mode 100644 index 000000000..579ec4226 --- /dev/null +++ b/test/data/features/clip/ex3_inner.json @@ -0,0 +1,7 @@ +{ + "type": "MultiPolygon", + "coordinates": [ + [[[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]], + [[[2, 1], [2, 2], [3, 2], [3, 1], [2, 1]]] + ] +} \ No newline at end of file diff --git a/test/data/features/clip/ex3_outer.json b/test/data/features/clip/ex3_outer.json new file mode 100644 index 000000000..82a1b3ce2 --- /dev/null +++ b/test/data/features/clip/ex3_outer.json @@ -0,0 +1,4 @@ +{ + "type": "Polygon", + "coordinates": [[[0, 0], [0, 3], [4, 3], [4, 0], [0, 0]]] +} \ No newline at end of file From b89b77ff47c211510cd6a2bfe59d514e74743209 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 1 Aug 2021 01:30:52 -0400 Subject: [PATCH 080/891] package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index bd6323434..3ed6f1278 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.62", + "version": "0.5.63", "lockfileVersion": 1, "requires": true, "dependencies": { From 5723324fdc1db43bfcc4f2be0eca39dfd207616b Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 3 Aug 2021 08:42:51 -0400 Subject: [PATCH 081/891] v0.5.64 --- CHANGELOG.md | 3 +++ package.json | 2 +- src/gui/gui-canvas.js | 25 ++++++++++++++++--------- src/gui/gui-selection-tool.js | 7 +++++-- src/utils/mapshaper-utils.js | 28 +++++++++++++++++++++------- 5 files changed, 46 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2050753a..5afce92eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.64 +* Bug fixes + v0.5.63 * Added -o decimal-comma function, for exporting CSV numbers with decimal commas. * Fix for issue #497 (error erasing with overlapping polygons). diff --git a/package.json b/package.json index 53bcc7a9c..880f6b455 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.63", + "version": "0.5.64", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/gui/gui-canvas.js b/src/gui/gui-canvas.js index e6e396e9e..fdae76b1c 100644 --- a/src/gui/gui-canvas.js +++ b/src/gui/gui-canvas.js @@ -155,6 +155,11 @@ export function DisplayCanvas() { shp = shapes[i]; if (!shp || filter && !filter(shp)) continue; if (styler) styler(style, i); + if (style.overlay || style.opacity < 1 || style.fillOpacity < 1 || style.strokeOpacity < 1) { + // don't batch shapes with opacity, in case they overlap + drawPaths([shp], startPath, draw, style); + continue; + } key = getStyleKey(style); if (key in styleIndex === false) { styleIndex[key] = { @@ -164,9 +169,7 @@ export function DisplayCanvas() { } item = styleIndex[key]; item.shapes.push(shp); - // overlays should not be batched, so transparency of overlapping shapes - // is drawn correctly - if (item.shapes.length >= batchSize || style.overlay) { + if (item.shapes.length >= batchSize) { drawPaths(item.shapes, startPath, draw, item.style); item.shapes = []; } @@ -209,7 +212,10 @@ export function DisplayCanvas() { if (styler !== null) { // e.g. selected points styler(style, i); size = style.dotSize * scaleRatio; - _ctx.fillStyle = style.dotColor; + if (style.dotColor != color) { + color = style.dotColor; + _ctx.fillStyle = color; + } } shp = shapes[i]; for (j=0, m=shp ? shp.length : 0; j 0 ? style.strokeColor + '~' + style.strokeWidth + - '~' + (style.lineDash ? style.lineDash + '~' : '') + - (style.strokeOpacity >= 0 ? style.strokeOpacity + '~' : '') : '') + + '~' + (style.lineDash ? style.lineDash + '~' : '') : '') + (style.fillColor || '') + - (style.fillOpacity ? '~' + style.fillOpacity : '') + - (style.fillPattern ? '~' + style.fillPattern : '') + - (style.opacity < 1 ? '~' + style.opacity : ''); + // styles with <1 opacity are no longer batch-rendered + // (style.strokeOpacity >= 0 ? style.strokeOpacity + '~' : '') : '') + + // (style.fillOpacity ? '~' + style.fillOpacity : '') + + // (style.opacity < 1 ? '~' + style.opacity : '') + + (style.fillPattern ? '~' + style.fillPattern : ''); } return _self; diff --git a/src/gui/gui-selection-tool.js b/src/gui/gui-selection-tool.js index fe3b9628c..45c619746 100644 --- a/src/gui/gui-selection-tool.js +++ b/src/gui/gui-selection-tool.js @@ -26,13 +26,16 @@ export function SelectionTool(gui, ext, hit) { gui.on('box_drag_end', function(e) { if (!_on) return; box.hide(); - var bboxPixels = e.map_bbox; + updateSelection(e.map_bbox); + }); + + function updateSelection(bboxPixels) { var bbox = bboxToCoords(bboxPixels); var active = gui.model.getActiveLayer(); var ids = internal.findShapesIntersectingBBox(bbox, active.layer, active.dataset.arcs); if (!ids.length) return; hit.addSelectionIds(ids); - }); + } function turnOn() { _on = true; diff --git a/src/utils/mapshaper-utils.js b/src/utils/mapshaper-utils.js index 4e08ea9bf..d4a4b074c 100644 --- a/src/utils/mapshaper-utils.js +++ b/src/utils/mapshaper-utils.js @@ -338,15 +338,29 @@ export function getArrayBounds(arr) { }; } +// export function uniq(src) { +// var index = {}; +// return src.reduce(function(memo, el) { +// if (el in index === false) { +// index[el] = true; +// memo.push(el); +// } +// return memo; +// }, []); +// } + export function uniq(src) { - var index = {}; - return src.reduce(function(memo, el) { - if (el in index === false) { - index[el] = true; - memo.push(el); + var index = new Set(); + var arr = []; + var item; + for (var i=0, n=src.length; i Date: Tue, 3 Aug 2021 08:43:23 -0400 Subject: [PATCH 082/891] v0.5.64 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 3ed6f1278..17440c0d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.63", + "version": "0.5.64", "lockfileVersion": 1, "requires": true, "dependencies": { From 7bd30ead3b3afd104a290e4b8e7a4df8a24d19b8 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 4 Aug 2021 22:35:18 -0400 Subject: [PATCH 083/891] Add variability to zoom buttons --- src/gui/gui-map-nav.js | 31 ++++++++++++++++++++++--------- src/gui/gui-mouse-utils.js | 19 +++++++++++++++++++ src/gui/gui-selection-tool.js | 1 + 3 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 src/gui/gui-mouse-utils.js diff --git a/src/gui/gui-map-nav.js b/src/gui/gui-map-nav.js index fcc63523b..676fe002f 100644 --- a/src/gui/gui-map-nav.js +++ b/src/gui/gui-map-nav.js @@ -1,12 +1,12 @@ import { MouseWheel } from './gui-mouse'; import { Tween } from './gui-tween'; -import { Bounds, internal } from './gui-core'; +import { Bounds, internal, utils } from './gui-core'; +import { initVariableClick } from './gui-mouse-utils'; export function MapNav(gui, ext, mouse) { var wheel = new MouseWheel(mouse), zoomTween = new Tween(Tween.sineInOut), boxDrag = false, - zoomScale = 1.5, zoomScaleMultiplier = 1, inBtn, outBtn, dragStartEvt, @@ -26,8 +26,10 @@ export function MapNav(gui, ext, mouse) { } if (gui.options.zoomControl) { - inBtn = gui.buttons.addButton("#zoom-in-icon").on('click', zoomIn); - outBtn = gui.buttons.addButton("#zoom-out-icon").on('click', zoomOut); + inBtn = gui.buttons.addButton("#zoom-in-icon"); + outBtn = gui.buttons.addButton("#zoom-out-icon"); + initVariableClick(inBtn.node(), zoomIn); + initVariableClick(outBtn.node(), zoomOut); ext.on('change', function() { inBtn.classed('disabled', ext.scale() >= ext.maxScale()); }); @@ -43,7 +45,7 @@ export function MapNav(gui, ext, mouse) { mouse.on('dblclick', function(e) { if (disabled()) return; - zoomByPct(1 + zoomScale * zoomScaleMultiplier, e.x / ext.width(), e.y / ext.height()); + zoomByPct(getZoomInPct(), e.x / ext.width(), e.y / ext.height()); }); mouse.on('dragstart', function(e) { @@ -111,14 +113,25 @@ export function MapNav(gui, ext, mouse) { return !!gui.options.disableNavigation; } - function zoomIn() { + function zoomIn(e) { if (disabled()) return; - zoomByPct(1 + zoomScale * zoomScaleMultiplier, 0.5, 0.5); + zoomByPct(getZoomInPct(e.time), 0.5, 0.5); } - function zoomOut() { + function zoomOut(e) { if (disabled()) return; - zoomByPct(1/(1 + zoomScale * zoomScaleMultiplier), 0.5, 0.5); + zoomByPct(1/getZoomInPct(e.time), 0.5, 0.5); + } + + function getZoomInPct(clickTime) { + var minScale = 0.2, + maxScale = 4, + minTime = 100, + maxTime = 800, + time = utils.clamp(clickTime || 200, minTime, maxTime), + k = (time - minTime) / (maxTime - minTime), + scale = minScale + k * (maxScale - minScale); + return 1 + scale * zoomScaleMultiplier; } // @box Bounds with pixels from t,l corner of map area. diff --git a/src/gui/gui-mouse-utils.js b/src/gui/gui-mouse-utils.js new file mode 100644 index 000000000..d95d1e10e --- /dev/null +++ b/src/gui/gui-mouse-utils.js @@ -0,0 +1,19 @@ +export function initVariableClick(node, cb) { + var downEvent = null; + var downTime = 0; + + node.addEventListener('mousedown', function(e) { + downEvent = e; + downTime = Date.now(); + }); + + node.addEventListener('mouseup', function(upEvent) { + if (!downEvent) return; + var shift = Math.abs(downEvent.pageX - upEvent.pageX) + + Math.abs(downEvent.pageY - upEvent.pageY); + var elapsed = Date.now() - downTime; + if (shift > 5 || elapsed > 1000) return; + downEvent = null; + cb({time: elapsed}); + }); +} diff --git a/src/gui/gui-selection-tool.js b/src/gui/gui-selection-tool.js index 45c619746..7662f9b2e 100644 --- a/src/gui/gui-selection-tool.js +++ b/src/gui/gui-selection-tool.js @@ -2,6 +2,7 @@ import { HighlightBox } from './gui-highlight-box'; import { internal, utils } from './gui-core'; import { SimpleButton } from './gui-elements'; + export function SelectionTool(gui, ext, hit) { var popup = gui.container.findChild('.selection-tool-options'); var box = new HighlightBox('body'); From 70b30261ee5e6322c6454c3ebf560960ce5d2991 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 4 Aug 2021 22:35:58 -0400 Subject: [PATCH 084/891] Bug fix --- src/shapefile/shp-record.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/shapefile/shp-record.js b/src/shapefile/shp-record.js index dd2fcb26b..af757ab10 100644 --- a/src/shapefile/shp-record.js +++ b/src/shapefile/shp-record.js @@ -1,14 +1,15 @@ import ShpType from '../shapefile/shp-type'; import { error } from '../utils/mapshaper-logging'; -var NullRecord = function() { +function getNullRecord(id) { return { + id: id, isNull: true, pointCount: 0, partCount: 0, byteLength: 12 }; -}; +} // Returns a constructor function for a shape record class with // properties and methods for reading coordinate data. @@ -30,19 +31,15 @@ export default function ShpRecordClass(type) { hasM = ShpType.isMType(type), singlePoint = !hasBounds, mzRangeBytes = singlePoint ? 0 : 16, - constructor; - - if (type === 0) { - return NullRecord; - } + constructor, proto; // @bin is a BinArray set to the first data byte of a shape record constructor = function ShapeRecord(bin, bytes) { var pos = bin.position(); this.id = bin.bigEndian().readUint32(); this.type = bin.littleEndian().skipBytes(4).readUint32(); - if (this.type === 0) { - return new NullRecord(); + if (this.type === 0 || type === 0) { + return getNullRecord(this.id); } if (bytes > 0 !== true || (this.type != type && this.type !== 0)) { error("Unable to read a shape -- .shp file may be corrupted"); @@ -63,7 +60,7 @@ export default function ShpRecordClass(type) { // base prototype has methods shared by all Shapefile types except NULL type // (Type-specific methods are mixed in below) - var proto = { + var baseProto = { // return offset of [x, y] point data in the record _xypos: function() { var offs = 12; // skip header & record type @@ -241,10 +238,12 @@ export default function ShpRecordClass(type) { } }; - if (singlePoint) { - Object.assign(proto, singlePointProto); + if (type === 0) { + proto = {}; + } else if (singlePoint) { + proto = Object.assign(baseProto, singlePointProto); } else { - Object.assign(proto, multiCoordProto); + proto = Object.assign(baseProto, multiCoordProto); } if (hasZ) Object.assign(proto, zProto); if (hasM) Object.assign(proto, mProto); From 9de449cb8d479347b9e4b0cd71a801f926117356 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 6 Aug 2021 21:22:37 -0400 Subject: [PATCH 085/891] Better -join error msg --- src/commands/mapshaper-join.js | 6 ++++++ test/join-test.js | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/commands/mapshaper-join.js b/src/commands/mapshaper-join.js index 64dd35899..329d21365 100644 --- a/src/commands/mapshaper-join.js +++ b/src/commands/mapshaper-join.js @@ -60,6 +60,12 @@ export function joinAttributesToFeatures(lyr, srcTable, opts) { // Return a function for translating a target id to an array of source ids based on values // of two key fields. function getJoinByKey(dest, destKey, src, srcKey) { + if (!dest) { + stop('Target layer is missing an attribute table'); + } + if (!src) { + stop('Source layer is missing an attribute table'); + } var destRecords = dest.getRecords(); var srcRecords = src.getRecords(); var index = createTableIndex(srcRecords, srcKey); diff --git a/test/join-test.js b/test/join-test.js index 8f7a50be7..0694ec784 100644 --- a/test/join-test.js +++ b/test/join-test.js @@ -14,6 +14,21 @@ describe('mapshaper-join.js', function () { describe('-join command', function () { + it('add error msg when joining to a layer without attributes', function(done) { + var targ = { + type: 'Point', + coordinates: [0, 0] + }; + var data = [{id: 'foo'}]; + var cmd = '-i point.json -join data.json keys=id,id -o'; + api.applyCommands(cmd, {'point.json': targ, 'data.json': data}, function(err, out) { + assert.equal(err.name, 'UserError'); + assert(err.message.includes('missing an attribute table')); + done(); + }) + + }); + it('includes source key with fields=* option', function(done) { var a = 'id,name\n1,foo'; var b = 'key,score\n1,100'; From 90008047985475d8e29913972a843f918b452464 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 6 Aug 2021 21:27:24 -0400 Subject: [PATCH 086/891] Improve interative selection commands --- src/cli/mapshaper-options.js | 7 +++++++ src/cli/mapshaper-run-command.js | 4 ++++ src/commands/mapshaper-define.js | 13 +++++++++++++ src/expressions/mapshaper-expressions.js | 2 +- src/gui/gui-selection-tool.js | 14 ++++++++------ test/each-test.js | 10 ++++++++++ 6 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 src/commands/mapshaper-define.js diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 02ad68ce5..44224c173 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -617,6 +617,13 @@ export function getOptionParser() { '$ mapshaper data.json -colorizer name=getColor nodata=#eee breaks=20,40 \\\n' + ' colors=#e0f3db,#a8ddb5,#43a2ca -each \'fill = getColor(RATING)\' -o output.json'); + parser.command('define') + // .describe('define expression variables') + .option('expression', { + DEFAULT: true, + describe: 'one or more assignment expressions (comma-sep.)' + }); + parser.command('dissolve') .describe('merge features within a layer') .example('Dissolve all polygons in a feature layer into a single polygon\n' + diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index ffef9f877..e134ac36f 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -23,6 +23,7 @@ import '../commands/mapshaper-clip-erase'; import '../commands/mapshaper-cluster'; import '../commands/mapshaper-colorizer'; import '../commands/mapshaper-data-fill'; +import '../commands/mapshaper-define'; import '../commands/mapshaper-dissolve'; import '../commands/mapshaper-dissolve2'; import '../commands/mapshaper-divide'; @@ -181,6 +182,9 @@ export function runCommand(command, catalog, cb) { } else if (name == 'colorizer') { outputLayers = cmd.colorizer(opts); + } else if (name == 'define') { + cmd.define(opts); + } else if (name == 'dissolve') { outputLayers = applyCommandToEachLayer(cmd.dissolve, targetLayers, arcs, opts); diff --git a/src/commands/mapshaper-define.js b/src/commands/mapshaper-define.js new file mode 100644 index 000000000..92ad6aa54 --- /dev/null +++ b/src/commands/mapshaper-define.js @@ -0,0 +1,13 @@ +import cmd from '../mapshaper-cmd'; +import { getStateVar } from '../mapshaper-state'; +import { message, error, stop } from '../utils/mapshaper-logging'; +import { compileFeatureExpression } from '../expressions/mapshaper-expressions'; + +cmd.define = function(opts) { + if (!opts.expression) { + stop('Missing an assignment expression'); + } + var defs = getStateVar('defs'); + var compiled = compileFeatureExpression(opts.expression, {}, null, {}); + var result = compiled(null, defs); +}; diff --git a/src/expressions/mapshaper-expressions.js b/src/expressions/mapshaper-expressions.js index 9e9881c76..76e29eacb 100644 --- a/src/expressions/mapshaper-expressions.js +++ b/src/expressions/mapshaper-expressions.js @@ -127,7 +127,7 @@ export function compileFeatureExpression(rawExp, lyr, arcs, opts_) { // Return array of variables on the left side of assignment operations // @hasDot (bool) Return property assignments via dot notation export function getAssignedVars(exp, hasDot) { - var rxp = /[a-z_][.a-z0-9_]*(?= *=[^>=])/ig; // ignore arrow functions and comparisons + var rxp = /[a-z_$][.a-z0-9_$]*(?= *=[^>=])/ig; // ignore arrow functions and comparisons var matches = exp.match(rxp) || []; var f = function(s) { var i = s.indexOf('.'); diff --git a/src/gui/gui-selection-tool.js b/src/gui/gui-selection-tool.js index 7662f9b2e..abb0498bc 100644 --- a/src/gui/gui-selection-tool.js +++ b/src/gui/gui-selection-tool.js @@ -76,20 +76,20 @@ export function SelectionTool(gui, ext, hit) { }); new SimpleButton(popup.findChild('.delete-btn')).on('click', function() { - var cmd = '-filter "this.id in $$selection === false"'; + var cmd = '-filter "$$set.has(this.id) === false"'; runCommand(cmd); hit.clearSelection(); }); new SimpleButton(popup.findChild('.filter-btn')).on('click', function() { - var cmd = '-filter "$$selection[this.id] === true"'; + var cmd = '-filter "$$set.has(this.id)"'; runCommand(cmd); hit.clearSelection(); }); new SimpleButton(popup.findChild('.split-btn')).on('click', function() { - var cmd = '-each "split_id = $$selection[this.id] ? \'1\' : \'2\'" -split split_id'; + var cmd = '-each "split_id = $$set.has(this.id) ? \'1\' : \'2\'" -split split_id'; runCommand(cmd); hit.clearSelection(); }); @@ -99,10 +99,12 @@ export function SelectionTool(gui, ext, hit) { }); function runCommand(cmd) { - var defs = internal.getStateVar('defs'); - defs.$$selection = utils.arrayToIndex(hit.getSelectionIds()); + // var defs = internal.getStateVar('defs'); + // defs.$$selection = utils.arrayToIndex(hit.getSelectionIds()); + var ids = JSON.stringify(hit.getSelectionIds()); + cmd = `-define "$$set = new Set(${ids})" ${cmd} -define "delete $$set"`; if (gui.console) gui.console.runMapshaperCommands(cmd, function(err) { - delete defs.$$selection; + // delete defs.$$selection; }); reset(); } diff --git a/test/each-test.js b/test/each-test.js index 83dd4fb35..7e7f91dea 100644 --- a/test/each-test.js +++ b/test/each-test.js @@ -142,6 +142,16 @@ describe('mapshaper-each.js', function () { assert.deepEqual(records, [{bar: 'mice'}, {bar: 'beans'}]); }) + it('create a variable containing "$"', function () { + var records = [{foo:'mice'}, {foo:'beans'}]; + var lyr = { + shapes: [], + data: new api.internal.DataTable(records) + }; + api.evaluateEachFeature(lyr, nullArcs, "$$foo = foo + foo"); + assert.deepEqual(records, [{foo: 'mice', '$$foo': 'micemice'}, {foo: 'beans', '$$foo': 'beansbeans'}]); + }) + it('data record is available as $.properties', function () { var records = [{foo:'mice'}, {foo:'beans'}]; var lyr = { From 684b9cf5f7f44e17350ae44352ac74d9e1fc21d6 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 6 Aug 2021 21:40:44 -0400 Subject: [PATCH 087/891] v0.5.65 --- CHANGELOG.md | 4 ++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5afce92eb..5ddc433da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.65 +* Web UI zoom buttons respond to variable-length clicks. +* Bug fixes + v0.5.64 * Bug fixes diff --git a/package-lock.json b/package-lock.json index 17440c0d3..25493649e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.64", + "version": "0.5.65", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 880f6b455..a5496f633 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.64", + "version": "0.5.65", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 786bfc13c48bd0ab1d84e2b8da3e5a366bb1a264 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 9 Aug 2021 23:27:32 -0400 Subject: [PATCH 088/891] Improved interactive selection style --- src/gui/gui-box-tool.js | 2 +- src/gui/gui-canvas.js | 38 +------ src/gui/gui-highlight-box.js | 4 +- src/gui/gui-interactive-selection.js | 13 +++ src/gui/gui-map-style.js | 159 +++++++++++++++++---------- src/gui/gui-map.js | 28 ++--- src/gui/gui-selection-tool.js | 19 ++-- www/page.css | 9 +- 8 files changed, 147 insertions(+), 125 deletions(-) diff --git a/src/gui/gui-box-tool.js b/src/gui/gui-box-tool.js index 27e67c05c..f190c185e 100644 --- a/src/gui/gui-box-tool.js +++ b/src/gui/gui-box-tool.js @@ -9,7 +9,7 @@ import { GUI } from './gui-lib'; export function BoxTool(gui, ext, mouse, nav) { var self = new EventDispatcher(); - var box = new HighlightBox('body'); + var box = new HighlightBox(); var popup = gui.container.findChild('.box-tool-options'); var coords = popup.findChild('.box-coords'); var _on = false; diff --git a/src/gui/gui-canvas.js b/src/gui/gui-canvas.js index fdae76b1c..c1ed9544d 100644 --- a/src/gui/gui-canvas.js +++ b/src/gui/gui-canvas.js @@ -190,7 +190,7 @@ export function DisplayCanvas() { _self.drawSquareDots = function(shapes, style) { var t = getScaledTransform(_ext), - scaleRatio = getDotScale2(shapes, _ext), + scaleRatio = getDotScale(_ext), size = Math.ceil((style.dotSize >= 0 ? style.dotSize : 3) * scaleRatio), styler = style.styler || null, xmax = _canvas.width + size, @@ -353,42 +353,12 @@ function getLineScale(ext) { return s; } -function getDotScale(ext) { - return Math.pow(getLineScale(ext), 0.7); -} -function countPoints(shapes, test, max) { - var count = 0; - var i, n, j, m, shp; - max = max || Infinity; - for (i=0, n=shapes.length; i= 2) { - test = function(p) { - return bounds.containsPoint(p[0], p[1]); - }; - } - n = countPoints(shapes, test, topTier + 2); // short-circuit point counting above top threshold - k = n >= topTier && 0.25 || n > 10000 && 0.45 || n > 2500 && 0.65 || n > 200 && 0.85 || 1; - j = side < 200 && 0.5 || side < 400 && 0.75 || 1; - return getDotScale(ext) * k * j * pixRatio; + var j = side < 200 && 0.5 || side < 400 && 0.75 || 1; + return Math.pow(getLineScale(ext), 0.7) * j * pixRatio; } function getScaledTransform(ext) { diff --git a/src/gui/gui-highlight-box.js b/src/gui/gui-highlight-box.js index b4c5ac434..398b6b1bc 100644 --- a/src/gui/gui-highlight-box.js +++ b/src/gui/gui-highlight-box.js @@ -1,7 +1,7 @@ import { El } from './gui-el'; -export function HighlightBox(el) { - var box = El('div').addClass('zoom-box').appendTo(el), +export function HighlightBox() { + var box = El('div').addClass('zoom-box').appendTo('body'), show = box.show.bind(box), // original show() function stroke = 2; box.hide(); diff --git a/src/gui/gui-interactive-selection.js b/src/gui/gui-interactive-selection.js index 23a5e6ecd..22f30fa7d 100644 --- a/src/gui/gui-interactive-selection.js +++ b/src/gui/gui-interactive-selection.js @@ -8,6 +8,7 @@ export function InteractiveSelection(gui, ext, mouse) { var self = new EventDispatcher(); var storedData = noHitData(); // may include additional data from SVG symbol hit (e.g. hit node) var selectionIds = []; + var transientIds = []; // e.g. hit ids while dragging a box var active = false; var interactionMode; var targetLayer; @@ -102,6 +103,14 @@ export function InteractiveSelection(gui, ext, mouse) { updateSelectionState({ids: ids}); }; + self.setTransientIds = function(ids) { + // turnOn('selection'); + transientIds = ids || []; + if (active) { + triggerHitEvent('change'); + } + }; + self.clearSelection = function() { updateSelectionState(null); }; @@ -257,6 +266,7 @@ export function InteractiveSelection(gui, ext, mouse) { // evt: (optional) mouse event function updateSelectionState(newData) { var nonEmpty = newData && (newData.ids.length || newData.id > -1); + transientIds = []; if (!newData) { newData = noHitData(); selectionIds = []; @@ -300,6 +310,9 @@ export function InteractiveSelection(gui, ext, mouse) { function triggerHitEvent(type, d) { // Merge stored hit data into the event data var eventData = utils.extend({mode: interactionMode}, d || {}, storedData); + if (transientIds.length) { + eventData.ids = utils.uniq(transientIds.concat(eventData.ids || [])); + } self.dispatchEvent(type, eventData); } diff --git a/src/gui/gui-map-style.js b/src/gui/gui-map-style.js index 8b91f6720..5b4b30e38 100644 --- a/src/gui/gui-map-style.js +++ b/src/gui/gui-map-style.js @@ -37,39 +37,40 @@ var darkStroke = "#334", strokeColor: black, strokeWidth: 1.2 }, point: { - dotColor: black, - dotSize: 8 - }, polyline: { + dotColor: violet, // black, + dotSize: 10 + }, polyline: { strokeColor: black, strokeWidth: 2.5 } }, - unfilledHoverStyles = { + unselectedHoverStyles = { polygon: { fillColor: 'rgba(0,0,0,0)', strokeColor: black, strokeWidth: 1.2 }, point: { - dotColor: grey, + dotColor: black, // grey, dotSize: 8 }, polyline: { - strokeColor: grey, + strokeColor: black, // grey, strokeWidth: 2.5 } }, selectionStyles = { polygon: { - fillColor: selectionFill, - strokeColor: gold, - strokeWidth: 1 + fillColor: hoverFill, + strokeColor: black, + strokeWidth: 1.2 }, point: { - dotColor: gold, + dotColor: violet, // black, dotSize: 6 }, polyline: { - strokeColor: gold, - strokeWidth: 1.5 + strokeColor: violet, // black, + strokeWidth: 2.5 } }, + // not used selectionHoverStyles = { polygon: { fillColor: selectionFill, @@ -89,10 +90,10 @@ var darkStroke = "#334", strokeColor: violet, strokeWidth: 1.8 }, point: { - dotColor: 'violet', - dotSize: 8 + dotColor: violet, + dotSize: 12 }, polyline: { - strokeColor: violet, + strokeColor: black, // violet, strokeWidth: 3 } }; @@ -101,6 +102,35 @@ export function getIntersectionStyle(lyr) { return utils.extend({}, intersectionStyle); } +function getDefaultStyle(lyr, baseStyle) { + var style = utils.extend({}, baseStyle); + // reduce the dot size of large point layers + if (lyr.geometry_type == 'point' && style.dotSize > 0) { + style.dotSize *= getDotScale(lyr); + } + return style; +} + +function getDotScale(lyr) { + var topTier = 50000; + var n = countPoints(lyr.shapes, topTier + 2); // short-circuit point counting above top threshold + var k = n >= topTier && 0.25 || n > 10000 && 0.45 || n > 2500 && 0.65 || n > 200 && 0.85 || 1; + return k; +} + +function countPoints(shapes, max) { + var count = 0; + var i, n, shp; + max = max || Infinity; + for (i=0, n=shapes.length; i -1 ? selectionHoverStyles[type] : hoverStyles[type]; - ids.push(i); - styles.push(style); + var baseStyle = getDefaultStyle(lyr, selectionStyles[geomType]); + var topStyle; + var ids = o.ids.filter(function(i) { + return i != o.id; // move selected id to the end }); - // top layer: feature that was selected by clicking in inspection mode ([i]) - if (topId > -1) { - var isPinned = o.pinned; - var inSelection = o.ids.indexOf(topId) > -1; - var style; - if (isPinned) { - style = pinnedStyles[type]; - } else if (inSelection) { - style = hoverStyles[type]; - } else { - style = unfilledHoverStyles[type]; - } - ids.push(topId); - styles.push(style); + if (o.id > -1) { + topStyle = getSelectedFeatureStyle(lyr, o); + topIdx = ids.length; + ids.push(o.id); // put the pinned/hover feature last in the render order } - + var overlayStyle = { + styler: styler, + ids: ids, + overlay: true + }; if (layerHasCanvasDisplayStyle(lyr)) { - if (type == 'point') { - overlayStyle = wrapOverlayStyle(getCanvasDisplayStyle(lyr), overlayStyle); + if (geomType == 'point') { + overlayStyle.styler = getOverlayPointStyler(getCanvasDisplayStyle(lyr).styler, styler); } overlayStyle.type = 'styled'; } - overlayStyle.ids = ids; - overlayStyle.overlay = true; return ids.length > 0 ? overlayStyle : null; } +function getSelectedFeatureStyle(lyr, o) { + var isPinned = o.pinned; + var inSelection = o.ids.indexOf(o.id) > -1; + var geomType = lyr.geometry_type; + var style; + if (isPinned) { + // a feature is pinned + style = pinnedStyles[geomType]; + } else if (inSelection) { + // normal hover, or hover id is in the selection set + style = hoverStyles[geomType]; + } else { + // features are selected, but hover id is not in the selection set + style = unselectedHoverStyles[geomType]; + } + return getDefaultStyle(lyr, style); +} + // Modify style to use scaled circle instead of dot symbol -function wrapOverlayStyle(style, hoverStyle) { - var styler = function(obj, i) { +function getOverlayPointStyler(baseStyler, overlayStyler) { + return function(obj, i) { var dotColor; var id = obj.ids ? obj.ids[i] : -1; obj.strokeWidth = 0; // kludge to support setting minimum stroke width - style.styler(obj, id); - if (hoverStyle.styler) { - hoverStyle.styler(obj, i); + baseStyler(obj, id); + if (overlayStyler) { + overlayStyler(obj, i); } dotColor = obj.dotColor; if (obj.radius && dotColor) { @@ -195,7 +233,6 @@ function wrapOverlayStyle(style, hoverStyle) { obj.opacity = 1; } }; - return {styler: styler}; } function getCanvasDisplayStyle(lyr) { diff --git a/src/gui/gui-map.js b/src/gui/gui-map.js index 1e2af6794..7579fd08d 100644 --- a/src/gui/gui-map.js +++ b/src/gui/gui-map.js @@ -230,18 +230,26 @@ export function MshpMap(gui) { drawLayers('nav'); }); - _hit.on('change', function(e) { - // draw highlight effect for hover and select - _overlayLyr = getDisplayLayerOverlay(_activeLyr, e); - drawLayers('hover'); - // _stack.drawOverlayLayer(_overlayLyr); - }); + _hit.on('change', updateOverlayLayer); gui.on('resize', function() { position.update(); // kludge to detect new map size after console toggle }); } + function updateOverlayLayer(e) { + var style = MapStyle.getOverlayStyle(_activeLyr.layer, e); + if (style) { + _overlayLyr = utils.defaults({ + layer: filterLayerByIds(_activeLyr.layer, style.ids), + style: style + }, _activeLyr); + } else { + _overlayLyr = null; + } + drawLayers('hover'); + } + function getDisplayOptions() { return { crs: _dynamicCRS @@ -451,11 +459,3 @@ export function MshpMap(gui) { } } -function getDisplayLayerOverlay(obj, e) { - var style = MapStyle.getOverlayStyle(obj.layer, e); - if (!style) return null; - return utils.defaults({ - layer: filterLayerByIds(obj.layer, style.ids), - style: style - }, obj); -} diff --git a/src/gui/gui-selection-tool.js b/src/gui/gui-selection-tool.js index abb0498bc..f59a27103 100644 --- a/src/gui/gui-selection-tool.js +++ b/src/gui/gui-selection-tool.js @@ -5,7 +5,7 @@ import { SimpleButton } from './gui-elements'; export function SelectionTool(gui, ext, hit) { var popup = gui.container.findChild('.selection-tool-options'); - var box = new HighlightBox('body'); + var box = new HighlightBox(); var _on = false; gui.addMode('selection_tool', turnOn, turnOff); @@ -22,6 +22,7 @@ export function SelectionTool(gui, ext, hit) { if (!_on) return; var b = e.page_bbox; box.show(b[0], b[1], b[2], b[3]); + updateSelection(e.map_bbox, true); }); gui.on('box_drag_end', function(e) { @@ -30,12 +31,15 @@ export function SelectionTool(gui, ext, hit) { updateSelection(e.map_bbox); }); - function updateSelection(bboxPixels) { + function updateSelection(bboxPixels, transient) { var bbox = bboxToCoords(bboxPixels); var active = gui.model.getActiveLayer(); var ids = internal.findShapesIntersectingBBox(bbox, active.layer, active.dataset.arcs); - if (!ids.length) return; - hit.addSelectionIds(ids); + if (transient) { + hit.setTransientIds(ids); + } else if (ids.length) { + hit.addSelectionIds(ids); + } } function turnOn() { @@ -78,20 +82,17 @@ export function SelectionTool(gui, ext, hit) { new SimpleButton(popup.findChild('.delete-btn')).on('click', function() { var cmd = '-filter "$$set.has(this.id) === false"'; runCommand(cmd); - hit.clearSelection(); }); new SimpleButton(popup.findChild('.filter-btn')).on('click', function() { var cmd = '-filter "$$set.has(this.id)"'; runCommand(cmd); - hit.clearSelection(); }); new SimpleButton(popup.findChild('.split-btn')).on('click', function() { var cmd = '-each "split_id = $$set.has(this.id) ? \'1\' : \'2\'" -split split_id'; runCommand(cmd); - hit.clearSelection(); }); new SimpleButton(popup.findChild('.cancel-btn')).on('click', function() { @@ -103,9 +104,9 @@ export function SelectionTool(gui, ext, hit) { // defs.$$selection = utils.arrayToIndex(hit.getSelectionIds()); var ids = JSON.stringify(hit.getSelectionIds()); cmd = `-define "$$set = new Set(${ids})" ${cmd} -define "delete $$set"`; + popup.hide(); if (gui.console) gui.console.runMapshaperCommands(cmd, function(err) { - // delete defs.$$selection; + reset(); }); - reset(); } } diff --git a/www/page.css b/www/page.css index 99141479a..4ac997e10 100644 --- a/www/page.css +++ b/www/page.css @@ -60,8 +60,7 @@ body { color: #10699b; } -.dot-underline, -.zoom-box.zooming { +.dot-underline { border-color: #10699b; } @@ -1120,12 +1119,14 @@ img.close-btn:hover, .zoom-box { position: absolute; - border: 1px solid #f74b80; + border: 1px solid #cc6acc; z-index: 15; pointer-events: none; } - +.zoom-box.zooming { + border-color: #1385B7; +} /* Simplification control ------------ */ From eb2b7351161af7461e7d097d6fec5734c1d7035a Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 13 Aug 2021 23:31:46 -0400 Subject: [PATCH 089/891] Add -define command and global expression object --- src/commands/mapshaper-define.js | 2 +- src/expressions/mapshaper-expressions.js | 12 +++++---- test/define-test.js | 34 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 test/define-test.js diff --git a/src/commands/mapshaper-define.js b/src/commands/mapshaper-define.js index 92ad6aa54..4682f46ca 100644 --- a/src/commands/mapshaper-define.js +++ b/src/commands/mapshaper-define.js @@ -8,6 +8,6 @@ cmd.define = function(opts) { stop('Missing an assignment expression'); } var defs = getStateVar('defs'); - var compiled = compileFeatureExpression(opts.expression, {}, null, {}); + var compiled = compileFeatureExpression(opts.expression, {}, null, {no_warn: true}); var result = compiled(null, defs); }; diff --git a/src/expressions/mapshaper-expressions.js b/src/expressions/mapshaper-expressions.js index 76e29eacb..38f19b214 100644 --- a/src/expressions/mapshaper-expressions.js +++ b/src/expressions/mapshaper-expressions.js @@ -133,7 +133,8 @@ export function getAssignedVars(exp, hasDot) { var i = s.indexOf('.'); return hasDot ? i > -1 : i == -1; }; - return utils.uniq(matches.filter(f)); + var vars = utils.uniq(matches.filter(f)); + return vars; } // Return array of objects with properties assigned via dot notation @@ -198,23 +199,24 @@ function nullifyUnsetProperties(vars, obj) { } function getExpressionContext(lyr, mixins, opts) { + var defs = getStateVar('defs'); var env = getBaseContext(); var ctx = {}; var fields = lyr.data ? lyr.data.getFields() : []; opts = opts || {}; addUtils(env); // mix in round(), sprintf(), etc. - if (lyr.data) { + if (fields.length > 0) { // default to null values, so assignments to missing data properties // are applied to the data record, not the global object nullifyUnsetProperties(fields, env); } // Add global 'defs' to the expression context - mixins = utils.defaults(mixins || {}, getStateVar('defs')); + mixins = utils.defaults(mixins || {}, defs); + // also add defs as 'global' object + env.global = defs; Object.keys(mixins).forEach(function(key) { // Catch name collisions between data fields and user-defined functions var d = Object.getOwnPropertyDescriptor(mixins, key); - if (key in env) { - } if (d.get) { // copy accessor function from mixins to context Object.defineProperty(ctx, key, {get: d.get}); // copy getter function to context diff --git a/test/define-test.js b/test/define-test.js new file mode 100644 index 000000000..0ccc4821b --- /dev/null +++ b/test/define-test.js @@ -0,0 +1,34 @@ +var api = require('../'), + internal = api.internal, + assert = require('assert'); + +describe('mapshaper-define.js', function () { + + it('adds a variable to the expression context',function(done) { + var json = [{foo: 'bar'}]; + var cmd = `-i data.json -define 'bar = "foo"' -each 'baz = bar' -o`; + api.applyCommands(cmd, {'data.json': json}, function(err, out) { + assert.deepEqual(JSON.parse(out['data.json']), [{foo: 'bar', baz: 'foo'}]) + done(); + }); + }); + + it('global object',function(done) { + var json = [{foo: 'bar'}]; + var cmd = `-i data.json -define 'global.bar = "foo"' -each 'baz = global.bar' -o`; + api.applyCommands(cmd, {'data.json': json}, function(err, out) { + assert.deepEqual(JSON.parse(out['data.json']), [{foo: 'bar', baz: 'foo'}]) + done(); + }); + }); + + it('global object used as accumulator',function(done) { + var json = [{foo: 3}, {foo: 5}]; + var cmd = `-i data.json -define 'total = 0' -each 'global.total += foo, sum = global.total' -o`; + api.applyCommands(cmd, {'data.json': json}, function(err, out) { + assert.deepEqual(JSON.parse(out['data.json']), [{foo: 3, sum: 3}, {foo: 5, sum: 8}]); + done(); + }); + }); + +}); From 71a54ea55f677b9a9e352aab9fb900f350572825 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 13 Aug 2021 23:34:00 -0400 Subject: [PATCH 090/891] Update gui dot size --- src/gui/gui-canvas.js | 22 +++++++++++++++++----- src/gui/gui-map-style.js | 23 ++++++++++++----------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/gui/gui-canvas.js b/src/gui/gui-canvas.js index c1ed9544d..b18d8e241 100644 --- a/src/gui/gui-canvas.js +++ b/src/gui/gui-canvas.js @@ -191,7 +191,7 @@ export function DisplayCanvas() { _self.drawSquareDots = function(shapes, style) { var t = getScaledTransform(_ext), scaleRatio = getDotScale(_ext), - size = Math.ceil((style.dotSize >= 0 ? style.dotSize : 3) * scaleRatio), + size = Math.round((style.dotSize || 1) * scaleRatio), styler = style.styler || null, xmax = _canvas.width + size, ymax = _canvas.height + size, @@ -355,10 +355,22 @@ function getLineScale(ext) { function getDotScale(ext) { - var pixRatio = GUI.getPixelRatio(); - var side = Math.min(ext.width(), ext.height()); - var j = side < 200 && 0.5 || side < 400 && 0.75 || 1; - return Math.pow(getLineScale(ext), 0.7) * j * pixRatio; + var smallSide = Math.min(ext.width(), ext.height()); + // reduce size on smaller screens + var j = smallSide < 200 && 0.5 || smallSide < 400 && 0.75 || 1; + var k = 1; + var mapScale = ext.scale(); + if (mapScale < 0.5) { + k = Math.pow(mapScale + 0.5, 0.35); + } + if (mapScale > 1) { + // scale faster at first, so small dots in large datasets + // become easily visible and clickable after zooming in a bit + k *= Math.pow(Math.min(mapScale, 10), 0.3); + k *= Math.pow(mapScale, 0.1); + } + + return k * j * GUI.getPixelRatio(); } function getScaledTransform(ext) { diff --git a/src/gui/gui-map-style.js b/src/gui/gui-map-style.js index 5b4b30e38..ab26c5a6a 100644 --- a/src/gui/gui-map-style.js +++ b/src/gui/gui-map-style.js @@ -14,22 +14,22 @@ var darkStroke = "#334", strokeColors: [lightStroke, darkStroke], strokeWidth: 0.7, dotColor: "#223", - dotSize: 4 + dotSize: 1 }, activeStyleForLabels = { dotColor: "rgba(250, 0, 250, 0.45)", // violet dot with transparency - dotSize: 4 + dotSize: 1 }, referenceStyle = { // outline style for reference layers type: 'outline', strokeColors: [null, '#86c927'], strokeWidth: 0.85, dotColor: "#73ba20", - dotSize: 4 + dotSize: 1 }, intersectionStyle = { dotColor: "#F24400", - dotSize: 4 + dotSize: 1 }, hoverStyles = { polygon: { @@ -38,7 +38,7 @@ var darkStroke = "#334", strokeWidth: 1.2 }, point: { dotColor: violet, // black, - dotSize: 10 + dotSize: 2.5 }, polyline: { strokeColor: black, strokeWidth: 2.5 @@ -51,7 +51,7 @@ var darkStroke = "#334", strokeWidth: 1.2 }, point: { dotColor: black, // grey, - dotSize: 8 + dotSize: 2 }, polyline: { strokeColor: black, // grey, strokeWidth: 2.5 @@ -64,7 +64,7 @@ var darkStroke = "#334", strokeWidth: 1.2 }, point: { dotColor: violet, // black, - dotSize: 6 + dotSize: 1.5 }, polyline: { strokeColor: violet, // black, strokeWidth: 2.5 @@ -78,7 +78,7 @@ var darkStroke = "#334", strokeWidth: 1.2 }, point: { dotColor: black, - dotSize: 6 + dotSize: 1.5 }, polyline: { strokeColor: black, strokeWidth: 2 @@ -91,7 +91,7 @@ var darkStroke = "#334", strokeWidth: 1.8 }, point: { dotColor: violet, - dotSize: 12 + dotSize: 3 }, polyline: { strokeColor: black, // violet, strokeWidth: 3 @@ -99,7 +99,7 @@ var darkStroke = "#334", }; export function getIntersectionStyle(lyr) { - return utils.extend({}, intersectionStyle); + return getDefaultStyle(lyr, intersectionStyle); } function getDefaultStyle(lyr, baseStyle) { @@ -114,7 +114,8 @@ function getDefaultStyle(lyr, baseStyle) { function getDotScale(lyr) { var topTier = 50000; var n = countPoints(lyr.shapes, topTier + 2); // short-circuit point counting above top threshold - var k = n >= topTier && 0.25 || n > 10000 && 0.45 || n > 2500 && 0.65 || n > 200 && 0.85 || 1; + var k = n < 200 && 4 || n < 2500 && 3 || n < 10000 && 2 || 1; + // var k = n >= topTier && 0.25 || n > 10000 && 0.45 || n > 2500 && 0.65 || n > 200 && 0.85 || 1; return k; } From b460aa526d6d85cb0570d7ca4654812a0fac4a16 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 13 Aug 2021 23:35:23 -0400 Subject: [PATCH 091/891] Address performance bottleneck when loading large ascii dbf files --- src/shapefile/dbf-reader.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/shapefile/dbf-reader.js b/src/shapefile/dbf-reader.js index 5ce241753..54222b1e8 100644 --- a/src/shapefile/dbf-reader.js +++ b/src/shapefile/dbf-reader.js @@ -349,7 +349,7 @@ export default function DbfReader(src, encodingArg) { function findStringEncoding() { var ldid = header.ldid, codepage = lookupCodePage(ldid), - samples = getNonAsciiSamples(50), + samples = getNonAsciiSamples(), only7bit = samples.length === 0, encoding, msg; @@ -394,21 +394,25 @@ export default function DbfReader(src, encodingArg) { return arr; } - // Return up to @size buffers containing text samples + // Return an array of buffers containing text samples // with at least one byte outside the 7-bit ascii range. - function getNonAsciiSamples(size) { + function getNonAsciiSamples() { var samples = []; var stringFields = header.fields.filter(function(f) { return f.type == 'C'; }); + var cols = stringFields.length; + // don't scan all the rows in large files (slow) + var rows = Math.min(header.recordCount, 10000); + var maxSamples = 50; var buf = utils.createBuffer(256); var index = {}; var f, chars, sample, hash; // include non-ascii field names, if any samples = getNonAsciiHeaders(); - for (var r=0, rows=header.recordCount; r= size) break; + for (var r=0; r= maxSamples) break; f = stringFields[c]; bin.position(getRowOffset(r) + f.columnOffset); chars = readStringBytes(bin, f.size, buf); From e7a5ce0b3deaf1d6b7b3852bb8a88054376db2cd Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 13 Aug 2021 23:35:48 -0400 Subject: [PATCH 092/891] Misc --- src/gui/gui-import-control.js | 14 ++++++++++++++ src/utils/mapshaper-timing.js | 13 +++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/gui/gui-import-control.js b/src/gui/gui-import-control.js index 1fa77fe38..7145a9f12 100644 --- a/src/gui/gui-import-control.js +++ b/src/gui/gui-import-control.js @@ -464,6 +464,19 @@ export function ImportControl(gui, opts) { } function downloadNextFile(memo, item, next) { + var blob, err; + fetch(item.url).then(resp => resp.blob()).then(b => { + blob = b; + blob.name = item.basename; + memo.push(blob); + }).catch(e => { + err = "Error loading " + item.name + ". Possible causes include: wrong URL, no network connection, server not configured for cross-domain sharing (CORS)."; + }).finally(() => { + next(err, memo); + }); + } + + function downloadNextFile_v1(memo, item, next) { var req = new XMLHttpRequest(); var blob; req.responseType = 'blob'; @@ -473,6 +486,7 @@ export function ImportControl(gui, opts) { } }); req.addEventListener('progress', function(e) { + if (!e.lengthComputable) return; var pct = e.loaded / e.total; if (catalog) catalog.progress(pct); }); diff --git a/src/utils/mapshaper-timing.js b/src/utils/mapshaper-timing.js index 0ed35f9be..e7ddb840a 100644 --- a/src/utils/mapshaper-timing.js +++ b/src/utils/mapshaper-timing.js @@ -2,9 +2,18 @@ export var T = { stack: [], start: function() { - T.stack.push(+new Date()); + T.stack.push(Date.now()); }, stop: function() { - return (+new Date() - T.stack.pop()) + 'ms'; + return (Date.now() - T.stack.pop()) + 'ms'; } }; + +export function tick(msg) { + var now = Date.now(); + var elapsed = tickTime ? ' - ' + (now - tickTime) + 'ms' : ''; + tickTime = now; + console.log((msg || '') + elapsed); +} + +var tickTime = 0; From e26a99b24927ff4ae6f363e1276be2665e88dfb0 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 13 Aug 2021 23:37:33 -0400 Subject: [PATCH 093/891] v0.5.66 --- CHANGELOG.md | 4 ++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ddc433da..de436fb58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.66 +* Improve interactive selection. +* Added undocumented -define command. + v0.5.65 * Web UI zoom buttons respond to variable-length clicks. * Bug fixes diff --git a/package-lock.json b/package-lock.json index 25493649e..bb5f61618 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.65", + "version": "0.5.66", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a5496f633..854c33673 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.65", + "version": "0.5.66", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 39f1e26fbd50919833bb2839b1cd8ec17830cbc5 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sat, 21 Aug 2021 23:06:32 -0400 Subject: [PATCH 094/891] v0.5.67 --- CHANGELOG.md | 3 +++ package-lock.json | 2 +- package.json | 2 +- src/gui/gui-map-style.js | 1 - src/text/mapshaper-delim-reader.js | 4 ++-- test/delim-reader-test.js | 19 ++++++++++++++++++- www/elements.css | 2 +- www/page.css | 4 ++-- 8 files changed, 28 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de436fb58..85943daa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.67 +* Bug fix + v0.5.66 * Improve interactive selection. * Added undocumented -define command. diff --git a/package-lock.json b/package-lock.json index bb5f61618..4fe3c28b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.66", + "version": "0.5.67", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 854c33673..568430dbc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.66", + "version": "0.5.67", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/gui/gui-map-style.js b/src/gui/gui-map-style.js index ab26c5a6a..bf03298b0 100644 --- a/src/gui/gui-map-style.js +++ b/src/gui/gui-map-style.js @@ -156,7 +156,6 @@ export function getActiveStyle(lyr) { return style; } - // Returns a display style for the overlay layer. // The overlay layer renders several kinds of feature, each of which is displayed // with a different style. diff --git a/src/text/mapshaper-delim-reader.js b/src/text/mapshaper-delim-reader.js index a401d4f60..88533fc62 100644 --- a/src/text/mapshaper-delim-reader.js +++ b/src/text/mapshaper-delim-reader.js @@ -155,7 +155,7 @@ function skipDelimLines(reader, lines) { reader.advance(retn.bytesRead); } -function readLinesAsString(reader, lines, encoding) { +export function readLinesAsString(reader, lines, encoding) { var buf = reader.readSync(); var retn = readLinesFromBuffer(buf, lines); var str; @@ -185,7 +185,7 @@ function readLinesFromBuffer(buf, linesToRead) { c = buf[i]; if (c == DQUOTE) { inQuotedText = !inQuotedText; - } else if (c == CR || c == LF) { + } else if ((c == CR || c == LF) && !inQuotedText) { if (c == CR && i + 1 < bufLen && buf[i + 1] == LF) { // first half of CRLF pair: advance one byte i++; diff --git a/test/delim-reader-test.js b/test/delim-reader-test.js index 087acf951..926ff334d 100644 --- a/test/delim-reader-test.js +++ b/test/delim-reader-test.js @@ -2,10 +2,27 @@ var assert = require('assert'), api = require('..'), internal = api.internal, csv_spectrum = require('csv-spectrum'), - StringReader = require('./helpers.js').Reader; + StringReader = require('./helpers.js').Reader, + Reader2 = api.internal.Reader2; describe('mapshaper-delim-reader.js', function () { + describe('readLinesAsString()', function () { + it('handles newlines in quoted string', function () { + var str = `"1942 Grand River Avenue + +http://parksandrecdiner.com/ + +",_hours_in_detroit_273384,42.3346355,-82.98547730000001 +foo +`; + var reader = new Reader2(new StringReader(str)); + var line = api.internal.readLinesAsString(reader, 1); + assert(/0001$/.test(line.trim())); + }) + }) + + describe('readDelimRecordsFromString()', function () { var read = api.internal.readDelimRecordsFromString; it('simple test', function () { diff --git a/www/elements.css b/www/elements.css index 7725ac0e1..3134fa68d 100644 --- a/www/elements.css +++ b/www/elements.css @@ -144,7 +144,7 @@ div.tip { .dialog-btn { display: inline-block; - margin-bottom: 4px; + margin-bottom: 1px; margin-top: 1px; font-size: 13px; color: white; diff --git a/www/page.css b/www/page.css index 4ac997e10..610493f8a 100644 --- a/www/page.css +++ b/www/page.css @@ -456,7 +456,7 @@ body.dragover #import-options-drop-area .drop-area { text-align: left; margin-top: 12px; margin-right: 20px; - padding: 11px 15px 9px 15px; + padding: 12px 18px 12px 18px; vertical-align: top; display: inline-block; /* border: 1px solid #aaa; */ @@ -904,7 +904,7 @@ img.close-btn:hover, box-sizing: border-box; overflow: hidden; overflow-y: auto; - padding: 4px 12px; + padding: 4px 12px 3px; line-height: 1.2; } From 8ad20f2c73a33522386429cf4218a9380b904582 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 1 Sep 2021 11:34:03 -0400 Subject: [PATCH 095/891] Fix d3 dependency issue --- package-lock.json | 2 +- package.json | 5 +++-- src/cli/mapshaper-options.js | 3 +++ test/delim-reader-test.js | 7 +++++++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4fe3c28b5..fd803ad60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.67", + "version": "0.5.68", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 568430dbc..5430e3410 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.67", + "version": "0.5.68", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", @@ -42,7 +42,8 @@ "dependencies": { "commander": "^5.1.0", "cookies": "^0.8.0", - "d3-scale-chromatic": "^2.0.0", + "d3-color": "2.0.0", + "d3-scale-chromatic": "2.0.0", "delaunator": "^5.0.0", "flatbush": "^3.2.1", "geokdbush": "^1.1.0", diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 44224c173..1de64b9c4 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -178,6 +178,9 @@ export function getOptionParser() { type: 'strings', describe: '[CSV] comma-sep. list of fields to import' }) + // .option('csv-comment', { + // describe: '[CSV] comment line character(s)' + // }) .option('decimal-comma', { type: 'flag', describe: '[CSV] import numbers formatted like 1.000,01 or 1 000,01' diff --git a/test/delim-reader-test.js b/test/delim-reader-test.js index 926ff334d..533fa287d 100644 --- a/test/delim-reader-test.js +++ b/test/delim-reader-test.js @@ -63,6 +63,13 @@ foo assert.deepEqual(retn, {headers: ['a', 'b', 'c'], import_fields: ['a', 'b', 'c'], remainder: '1,2,3'}) }) + // TODO: fix + if (false) it('skip over line with quoted newline', function() { + var str = '"comment\none","comment\ntwo"\na,b\n1,2'; + var retn = parse(str, ',', {csv_skip_lines: 1}); + assert.deepEqual(retn, {headers:['a', 'b'], import_fields: ['a', 'b', 'c'], remainder: '1,2'}) + }); + it('csv_field_names', function () { var str = 'a,b,c\n1,2,3'; var retn = parse(str, ',', {csv_field_names: ['d', 'e', 'f']}); From 4a095c925d808e18973720611192b0f038b1c1cd Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 1 Sep 2021 11:35:48 -0400 Subject: [PATCH 096/891] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85943daa5..aa9f32e5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.68 +* Bug fix + v0.5.67 * Bug fix From ad78ff852b911a34c7c1d68fdb90b2fea26d3250 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 29 Oct 2021 17:12:37 -0400 Subject: [PATCH 097/891] Convert entities in SVG text (labels) --- src/svg/svg-stringify.js | 11 ++++++++--- test/svg-stringify-test.js | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/svg/svg-stringify.js b/src/svg/svg-stringify.js index 0597ffaca..098f4ba66 100644 --- a/src/svg/svg-stringify.js +++ b/src/svg/svg-stringify.js @@ -13,7 +13,7 @@ export function stringify(obj) { joinStr = obj.tag == 'text' || obj.tag == 'tspan' ? '' : '\n'; svg += '>' + joinStr; if (obj.value) { - svg += obj.value; + svg += stringEscape(obj.value); } if (obj.children) { svg += obj.children.map(stringify).join(joinStr); @@ -36,8 +36,13 @@ var rxp = /[&<>"']/g, "'": ''' }; export function stringEscape(s) { - return String(s).replace(rxp, function(s) { - return map[s]; + return String(s).replace(rxp, function(match, i) { + var entity = map[match]; + // don't replace & with &amp; + if (match == '&' && s.substr(i, entity.length) == entity) { + return '&'; + } + return entity; }); } diff --git a/test/svg-stringify-test.js b/test/svg-stringify-test.js index fde47a375..5b744c0ba 100644 --- a/test/svg-stringify-test.js +++ b/test/svg-stringify-test.js @@ -11,6 +11,24 @@ describe('svg-stringfy.js', function () { assert.equal(SVG.stringify(obj), ''); }) + it('text element', function() { + var obj = {tag: 'text', value: 'TEXAS'}; + var expect = 'TEXAS'; + assert.equal(SVG.stringify(obj), expect); + }) + + it('text element with ampersand', function() { + var obj = {tag: 'text', value: 'WEST BANK & GAZA'}; + var expect = 'WEST BANK & GAZA'; + assert.equal(SVG.stringify(obj), expect); + }) + + it('text element with entities', function() { + var obj = {tag: 'text', value: 'WEST BANK & GAZA'}; + var expect = 'WEST BANK & GAZA'; + assert.equal(SVG.stringify(obj), expect); + }) + it('path element', function() { var obj = {tag: 'path', properties: {d: 'M 0 0 1 1'}}; assert.equal(SVG.stringify(obj), '') From 75a88d6f6303e05e7a21e82f424cefaf7874bae0 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 29 Oct 2021 17:13:27 -0400 Subject: [PATCH 098/891] Add undocumented -ignore command --- src/cli/mapshaper-options.js | 8 ++++ src/cli/mapshaper-run-command.js | 4 ++ src/cli/mapshaper-run-commands.js | 66 +++++++++++++++++++++---------- src/commands/mapshaper-ignore.js | 9 +++++ src/utils/mapshaper-logging.js | 20 +++++++++- test/commands-test.js | 13 ++++++ 6 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 src/commands/mapshaper-ignore.js diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 1de64b9c4..39822b385 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -930,6 +930,14 @@ export function getOptionParser() { .option('target', targetOpt) .option('no-replace', noReplaceOpt); + parser.command('ignore') + // .describe('stop processing if a condition is met') + .option('empty', { + describe: 'ignore empty files', + type: 'flag' + }) + .option('target', targetOpt); + parser.command('inlay') .describe('inscribe a polygon layer inside another polygon layer') .option('source', { diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index e134ac36f..b757522c6 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -43,6 +43,7 @@ import '../commands/mapshaper-filter-points'; import '../commands/mapshaper-filter-slivers'; import '../commands/mapshaper-fuzzy-join'; import '../commands/mapshaper-graticule'; +import '../commands/mapshaper-ignore'; import '../commands/mapshaper-include'; import '../commands/mapshaper-info'; import '../commands/mapshaper-inlay'; @@ -255,6 +256,9 @@ export function runCommand(command, catalog, cb) { outputLayers = targetDataset.layers; // kludge to allow layer naming below } + } else if (name == 'ignore') { + applyCommandToEachLayer(cmd.ignore, targetLayers, targetDataset, opts); + } else if (name == 'include') { cmd.include(opts); diff --git a/src/cli/mapshaper-run-commands.js b/src/cli/mapshaper-run-commands.js index 276bcb664..5dff37638 100644 --- a/src/cli/mapshaper-run-commands.js +++ b/src/cli/mapshaper-run-commands.js @@ -139,6 +139,7 @@ function _runCommands(argv, opts, callback) { cmd.options.output = outputArr; } }); + runParsedCommands(commands, null, callback); } @@ -199,10 +200,6 @@ export function runParsedCommands(commands, catalog, cb) { error("Changed in v0.4: runParsedCommands() takes a Catalog object"); } - if (!utils.isFunction(done)) { - error("Missing a callback function"); - } - if (!utils.isArray(commands)) { error("Expected an array of parsed commands"); } @@ -223,18 +220,25 @@ export function runParsedCommands(commands, catalog, cb) { // that it can modify the output dataset in-place instead of making a copy. commands[commands.length-1].options.final = true; } - commands = divideImportCommand(commands); - utils.reduceAsync(commands, catalog, nextCommand, done); - function nextCommand(catalog, cmd, next) { - setStateVar('current_command', cmd.name); // for log msgs - setStateVar('verbose', !!cmd.options.verbose); - setStateVar('debug', !!cmd.options.debug); - runCommand(cmd, catalog, next); + var groups = divideImportCommand(commands); + if (groups.length == 1) { + // run a simple sequence of commands (input files are not batched) + return runParsedCommands2(commands, catalog, done); + } + + // run duplicated commands (i.e. batch mode) + utils.reduceAsync(groups, catalog, nextGroup, done); + + function nextGroup(catalog, commands, next) { + runParsedCommands2(commands, catalog, function(err, catalog) { + err = filterError(err); + next(err, catalog); + }); } function done(err, catalog) { - if (err) printError(err); + err = filterError(err); cb(err, catalog); setStateVar('current_command', null); setStateVar('verbose', false); @@ -242,11 +246,31 @@ export function runParsedCommands(commands, catalog, cb) { } } +function filterError(err) { + if (err) printError(err); + if (err && err.name == 'NonFatalError') { + return null; + } + return err; +} + +function runParsedCommands2(commands, catalog, cb) { + utils.reduceAsync(commands, catalog, nextCommand, cb); + + function nextCommand(catalog, cmd, next) { + setStateVar('current_command', cmd.name); // for log msgs + setStateVar('verbose', !!cmd.options.verbose); + setStateVar('debug', !!cmd.options.debug); + runCommand(cmd, catalog, next); + } +} + + // If an initial import command indicates that several input files should be // processed separately, then duplicate the sequence of commands to run // once for each input file // @commands Array of parsed commands -// Returns: either original command array or array of duplicated commands. +// Returns: Array of one or more sequences of parsed commands // function divideImportCommand(commands) { var firstCmd = commands[0], @@ -254,23 +278,23 @@ function divideImportCommand(commands) { if (firstCmd.name != 'i' || opts.stdin || opts.merge_files || opts.combine_files || !opts.files || opts.files.length < 2) { - return commands; + return [commands]; } - return (opts.files).reduce(function(memo, file) { - var importCmd = { + return opts.files.map(function(file) { + var group = [{ name: 'i', options: utils.defaults({ files:[file], replace: true // kludge to replace data catalog }, opts) - }; - memo.push(importCmd); - memo.push.apply(memo, commands.slice(1)); - return memo; - }, []); + }]; + group.push.apply(group, commands.slice(1)); + return group; + }); } + function printStartupMessages() { // print heap memory message if running with a custom amount var rxp = /^--max-old-space-size=([0-9]+)$/; diff --git a/src/commands/mapshaper-ignore.js b/src/commands/mapshaper-ignore.js new file mode 100644 index 000000000..eb8e658a8 --- /dev/null +++ b/src/commands/mapshaper-ignore.js @@ -0,0 +1,9 @@ +import cmd from '../mapshaper-cmd'; +import { layerIsEmpty } from '../dataset/mapshaper-layer-utils'; +import { interrupt } from '../utils/mapshaper-logging'; + +cmd.ignore = function(targetLayer, dataset, opts) { + if (opts.empty && layerIsEmpty(targetLayer)) { + interrupt('Layer is empty, stopping processing'); + } +}; \ No newline at end of file diff --git a/src/utils/mapshaper-logging.js b/src/utils/mapshaper-logging.js index 1ae75db40..3c41d95a1 100644 --- a/src/utils/mapshaper-logging.js +++ b/src/utils/mapshaper-logging.js @@ -15,6 +15,10 @@ var _stop = function() { throw new UserError(formatLogArgs(arguments)); }; +var _interrupt = function() { + throw new NonFatalError(formatLogArgs(arguments)); +}; + var _message = function() { logArgs(arguments); }; @@ -33,10 +37,14 @@ export function error() { } // Handle an error caused by invalid input or misuse of API -export function stop () { +export function stop() { _stop.apply(null, utils.toArray(arguments)); } +export function interrupt() { + _interrupt.apply(null, utils.toArray(arguments)); +} + // Print a status message export function message() { _message.apply(null, messageArgs(arguments)); @@ -76,7 +84,9 @@ export function printError(err) { if (utils.isString(err)) { err = new UserError(err); } - if (err.name == 'UserError') { + if (err.name == 'NonFatalError') { + console.error(messageArgs([err.message]).join(' ')); + } else if (err.name == 'UserError') { msg = err.message; if (!/Error/.test(msg)) { msg = "Error: " + msg; @@ -96,6 +106,12 @@ export function UserError(msg) { return err; } +export function NonFatalError(msg) { + var err = new Error(msg); + err.name = 'NonFatalError'; + return err; +} + export function formatColumns(arr, alignments) { var widths = arr.reduce(function(memo, line) { return line.map(function(str, i) { diff --git a/test/commands-test.js b/test/commands-test.js index 6d9116d81..7df623e52 100644 --- a/test/commands-test.js +++ b/test/commands-test.js @@ -29,6 +29,19 @@ function runCmd(cmd, input, done) { describe('mapshaper-run-commands.js', function () { + + it('when two files are processed in sequence and second file triggers error, no output is generated', function(done) { + var input = { + 'data.csv': 'id\n0\n1', + 'data2.json': '{' + }; + api.applyCommands('-i data.csv data2.json -o', input, function(err, output) { + assert.equal(err.name, 'UserError'); + assert.equal(output, null); + done(); + }) + }); + describe('Issue #264 applyCommands()', function() { it ('should throw error if input is a file path, not file content', function(done) { mapshaper.applyCommands('-i input.shp -o out.json', {'input.shp': 'test/data/two_states.shp'}, function( From daacf8cf3a342b14c1ceebe37c8e0f1f91168828 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 29 Oct 2021 17:14:00 -0400 Subject: [PATCH 099/891] v0.5.69 --- CHANGELOG.md | 4 ++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa9f32e5d..091e7459b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.69 +* Added undocumented -ignore command. +* Bug fix + v0.5.68 * Bug fix diff --git a/package-lock.json b/package-lock.json index fd803ad60..94f9f6f6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.68", + "version": "0.5.69", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5430e3410..b4626733f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.68", + "version": "0.5.69", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 239e9c63b3017ece491526a01db837d3cc01aaff Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 14 Nov 2021 01:53:45 -0500 Subject: [PATCH 100/891] v0.5.70 --- CHANGELOG.md | 3 ++ src/cli/mapshaper-options.js | 4 ++ src/commands/mapshaper-join.js | 8 ++-- .../mapshaper-join-polygons-via-mosaic.js | 7 +-- src/join/mapshaper-join-tables.js | 39 ++++++++++++++--- src/join/mapshaper-point-point-join.js | 4 +- src/join/mapshaper-point-polygon-join.js | 6 +-- test/join-test.js | 43 +++++++++++++++++++ 8 files changed, 95 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 091e7459b..40f0b323b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.70 +* Added -join duplication option, which duplicates features in the target layer when many-to-one joins occur. + v0.5.69 * Added undocumented -ignore command. * Bug fix diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 39822b385..49e336333 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1009,6 +1009,10 @@ export function getOptionParser() { // describe: 'use planar geometry when interpolating by area' // useful for testing type: 'flag' }) + .option('duplication', { + describe: 'duplicate target features on many-to-one joins', + type: 'flag' + }) .option('string-fields', stringFieldsOpt) .option('field-types', fieldTypesOpt) .option('sum-fields', { diff --git a/src/commands/mapshaper-join.js b/src/commands/mapshaper-join.js index 329d21365..20e62100f 100644 --- a/src/commands/mapshaper-join.js +++ b/src/commands/mapshaper-join.js @@ -4,7 +4,7 @@ import { joinPolygonsToPolygons } from '../join/mapshaper-polygon-polygon-join'; import { message, stop } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import cmd from '../mapshaper-cmd'; -import { joinTables, validateFieldNames } from '../join/mapshaper-join-tables'; +import { joinTableToLayer, validateFieldNames } from '../join/mapshaper-join-tables'; import { joinPointsToPolygons, joinPolygonsToPoints } from '../join/mapshaper-point-polygon-join'; import { joinPointsToPoints } from '../join/mapshaper-point-point-join'; import { requireDatasetsHaveCompatibleCRS, getDatasetCRS } from '../crs/mapshaper-projections'; @@ -47,14 +47,14 @@ cmd.join = function(targetLyr, targetDataset, src, opts) { } }; -export function joinAttributesToFeatures(lyr, srcTable, opts) { +export function joinAttributesToFeatures(destLyr, srcTable, opts) { var keys = opts.keys, destKey = keys[0], srcKey = keys[1], - destTable = lyr.data, + destTable = destLyr.data, joinFunction = getJoinByKey(destTable, destKey, srcTable, srcKey); validateFieldNames(keys); - return joinTables(destTable, srcTable, joinFunction, opts); + return joinTableToLayer(destLyr, srcTable, joinFunction, opts); } // Return a function for translating a target id to an array of source ids based on values diff --git a/src/join/mapshaper-join-polygons-via-mosaic.js b/src/join/mapshaper-join-polygons-via-mosaic.js index bb8f7b7e7..72c651e42 100644 --- a/src/join/mapshaper-join-polygons-via-mosaic.js +++ b/src/join/mapshaper-join-polygons-via-mosaic.js @@ -1,9 +1,9 @@ -import { joinTables } from '../join/mapshaper-join-tables'; +import { joinTables, joinTableToLayer } from '../join/mapshaper-join-tables'; import { prepJoinLayers } from '../join/mapshaper-point-polygon-join'; import { addIntersectionCuts } from '../paths/mapshaper-intersection-cuts'; import { mergeLayersForOverlay } from '../clipping/mapshaper-overlay-utils'; import { MosaicIndex } from '../polygons/mapshaper-mosaic-index'; -import { error } from '../utils/mapshaper-logging'; +import { error, stop } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import geom from '../geom/mapshaper-geom'; @@ -21,9 +21,10 @@ export function joinPolygonsViaMosaic(targetLyr, targetDataset, source, opts) { var joinOpts = utils.extend({}, opts); var joinFunction = getPolygonToPolygonFunction(targetLyr, sourceLyr, mosaicIndex, opts); - var retn = joinTables(targetLyr.data, sourceLyr.data, joinFunction, joinOpts); + var retn = joinTableToLayer(targetLyr, sourceLyr.data, joinFunction, joinOpts); if (opts.interpolate) { + if (opts.duplication) stop('duplication and interpolate options cannot be used together'); interpolateFieldsByArea(targetLyr, sourceLyr, mosaicIndex, opts); } return retn; diff --git a/src/join/mapshaper-join-tables.js b/src/join/mapshaper-join-tables.js index 2905015b0..700fdad4c 100644 --- a/src/join/mapshaper-join-tables.js +++ b/src/join/mapshaper-join-tables.js @@ -3,14 +3,22 @@ import { getJoinFilter } from '../join/mapshaper-join-filter'; import { message, stop } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import { DataTable } from '../datatable/mapshaper-data-table'; +import { cloneShape } from '../paths/mapshaper-shape-utils'; +import { copyRecord } from '../datatable/mapshaper-data-utils'; + +export function joinTables(dest, src, join, opts) { + return joinTableToLayer({data: dest}, src, join, opts); +} // Join data from @src table to records in @dest table // @join function // Receives index of record in the dest table // Returns array of matching records in src table, or null if no matches // -export function joinTables(dest, src, join, opts) { - var srcRecords = src.getRecords(), +export function joinTableToLayer(destLyr, src, join, opts) { + var dest = destLyr.data, + useDuplication = !!opts.duplication, + srcRecords = src.getRecords(), destRecords = dest.getRecords(), prefix = opts.prefix || '', unmatchedRecords = [], @@ -25,6 +33,14 @@ export function joinTables(dest, src, join, opts) { retn = {}, srcRec, srcId, destRec, joins, count, filter, calc, i, j, n, m; + // support for duplication + var duplicateRecords, destShapes; + if (useDuplication) { + if (opts.calc) stop('duplication and calc options cannot be used together'); + duplicateRecords = dest.clone().getRecords(); + destShapes = destLyr.shapes || []; + } + if (opts.where) { filter = getJoinFilter(src, opts.where); } @@ -34,7 +50,8 @@ export function joinTables(dest, src, join, opts) { } // join source records to target records - for (i=0, n=destRecords.length; i 0 && useDuplication) { + destRec = copyRecord(duplicateRecords[i]); + destRecords.push(destRec); + destShapes.push(cloneShape(destShapes[i])); + } + if (count === 0 || useDuplication) { if (copyFields.length > 0) { // only copying the first match joinByCopy(destRec, srcRec, copyFields, prefix); @@ -77,7 +99,7 @@ export function joinTables(dest, src, join, opts) { } } - printJoinMessage(matchCount, destRecords.length, + printJoinMessage(matchCount, n, countJoins(joinCounts), srcRecords.length, skipCount, collisionCount, collisionFields); if (opts.unjoined) { @@ -105,6 +127,7 @@ export function validateFieldNames(arr) { }); } + function countJoins(counts) { var joinCount = 0; for (var i=0, n=counts.length; i 0 === false) { message("No records could be joined"); return; } message(utils.format("Joined data from %'d source record%s to %'d target record%s", joins, utils.pluralSuffix(joins), matches, utils.pluralSuffix(matches))); - if (matches < n) { - message(utils.format('%d/%d target records received no data', n-matches, n)); + if (unmatched > 0) { + message(utils.format('%d target record%s received no data', unmatched, utils.pluralSuffix(unmatched))); + // message(utils.format('%d target records received no data', n-matches)); } if (joins < m) { message(utils.format("%d/%d source records could not be joined", m-joins, m)); diff --git a/src/join/mapshaper-point-point-join.js b/src/join/mapshaper-point-point-join.js index b55458a48..62bad521a 100644 --- a/src/join/mapshaper-point-point-join.js +++ b/src/join/mapshaper-point-point-join.js @@ -1,13 +1,13 @@ import { PointIndex } from '../points/mapshaper-point-index'; import { stop } from '../utils/mapshaper-logging'; import { prepJoinLayers } from './mapshaper-point-polygon-join'; -import { joinTables } from '../join/mapshaper-join-tables'; +import { joinTables, joinTableToLayer } from '../join/mapshaper-join-tables'; import { isLatLngCRS } from '../crs/mapshaper-projections'; export function joinPointsToPoints(targetLyr, srcLyr, crs, opts) { var joinFunction = getPointToPointFunction(targetLyr, srcLyr, crs, opts); prepJoinLayers(targetLyr, srcLyr); - return joinTables(targetLyr.data, srcLyr.data, joinFunction, opts); + return joinTableToLayer(targetLyr, srcLyr.data, joinFunction, opts); } function getPointToPointFunction(targetLyr, srcLyr, crs, opts) { diff --git a/src/join/mapshaper-point-polygon-join.js b/src/join/mapshaper-point-polygon-join.js index a4824de8e..841d56b49 100644 --- a/src/join/mapshaper-point-polygon-join.js +++ b/src/join/mapshaper-point-polygon-join.js @@ -1,4 +1,4 @@ -import { joinTables } from '../join/mapshaper-join-tables'; +import { joinTables, joinTableToLayer } from '../join/mapshaper-join-tables'; import { stop } from '../utils/mapshaper-logging'; import { PathIndex } from '../paths/mapshaper-path-index'; import { DataTable } from '../datatable/mapshaper-data-table'; @@ -7,13 +7,13 @@ export function joinPointsToPolygons(targetLyr, arcs, pointLyr, opts) { // TODO: option to copy points that can't be joined to a new layer var joinFunction = getPolygonToPointsFunction(targetLyr, arcs, pointLyr, opts); prepJoinLayers(targetLyr, pointLyr); - return joinTables(targetLyr.data, pointLyr.data, joinFunction, opts); + return joinTableToLayer(targetLyr, pointLyr.data, joinFunction, opts); } export function joinPolygonsToPoints(targetLyr, polygonLyr, arcs, opts) { var joinFunction = getPointToPolygonsFunction(targetLyr, polygonLyr, arcs, opts); prepJoinLayers(targetLyr, polygonLyr); - return joinTables(targetLyr.data, polygonLyr.data, joinFunction, opts); + return joinTableToLayer(targetLyr, polygonLyr.data, joinFunction, opts); } diff --git a/test/join-test.js b/test/join-test.js index 0694ec784..255ca0fbc 100644 --- a/test/join-test.js +++ b/test/join-test.js @@ -14,6 +14,49 @@ describe('mapshaper-join.js', function () { describe('-join command', function () { + it('join two tables with duplication flag', function(done) { + var a = 'id,name\n1,foo'; + var b = 'key,score\n1,100\n1,200\n1,300'; + api.applyCommands('a.csv -join b.csv duplication keys=id,key fields=score -o', {'a.csv': a, 'b.csv': b}, function(err, out) { + assert.deepEqual(out['a.csv'], 'id,name,score\n1,foo,100\n1,foo,200\n1,foo,300'); + done(); + }); + }) + + it('join data to points layer with duplication flag', function(done) { + var a = 'id,name,lng,lat\n1,foo,10,10'; + var b = 'key,score\n1,100\n1,200\n1,300'; + api.applyCommands('a.csv -points -join b.csv duplication keys=id,key fields=score -o format=geojson', {'a.csv': a, 'b.csv': b}, function(err, out) { + const target = { + type: 'FeatureCollection', + features: [{ + type: 'Feature', + properties: {id: 1, name: 'foo', lng: 10, lat: 10, score: 100}, + geometry: { + type: 'Point', + coordinates: [10, 10] + } + }, { + type: 'Feature', + properties: {id: 1, name: 'foo', lng: 10, lat: 10, score: 200}, + geometry: { + type: 'Point', + coordinates: [10, 10] + } + }, { + type: 'Feature', + properties: {id: 1, name: 'foo', lng: 10, lat: 10, score: 300}, + geometry: { + type: 'Point', + coordinates: [10, 10] + } + }] + } + assert.deepEqual(JSON.parse(out['a.json']), target); + done(); + }); + }) + it('add error msg when joining to a layer without attributes', function(done) { var targ = { type: 'Point', From 0b1b6804e75eeef70c20118f6e5c662770fea2be Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 14 Nov 2021 01:56:17 -0500 Subject: [PATCH 101/891] Bump --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 94f9f6f6c..bbeb89d68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.69", + "version": "0.5.70", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b4626733f..08cf0aaff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.69", + "version": "0.5.70", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 781951300eda217e93577360c4d0b78b1f7f0e71 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 15 Nov 2021 01:48:36 -0500 Subject: [PATCH 102/891] Add -classify non-adjacent opt and bump version --- package-lock.json | 2 +- package.json | 2 +- src/cli/mapshaper-options.js | 8 ++- src/cli/mapshaper-run-command.js | 2 +- src/color/graph-color.js | 77 +++++++++++++++++++++++++++++ src/commands/mapshaper-classify.js | 50 +++++++++++++------ src/commands/mapshaper-colorizer.js | 1 + src/join/mapshaper-join-tables.js | 6 ++- 8 files changed, 126 insertions(+), 22 deletions(-) create mode 100644 src/color/graph-color.js diff --git a/package-lock.json b/package-lock.json index bbeb89d68..6ef5be819 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.70", + "version": "0.5.71", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 08cf0aaff..ef8628eac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.70", + "version": "0.5.71", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 49e336333..53b839b3b 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -417,7 +417,8 @@ export function getOptionParser() { .option('no-replace', noReplaceOpt); parser.command('classify') - .describe('apply sequential or categorical classification') + // .describe('apply sequential or categorical classification') + .describe('assign colors or values using one of several methods') .option('field', { describe: 'name of field to classify', DEFAULT: true @@ -437,6 +438,10 @@ export function getOptionParser() { // deprecated in favor of colors= // describe: 'name of a predefined color scheme (see -colors command)' }) + .option('non-adjacent', { + describe: 'assign non-adjacent colors to a polygon layer', + assign_to: 'method' + }) .option('stops', { describe: 'a pair of values (0-100) for limiting a color ramp', type: 'numbers' @@ -521,7 +526,6 @@ export function getOptionParser() { .option('key-last-suffix', { describe: 'string to append to last label' }) - .option('target', targetOpt); parser.command('clean') diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index b757522c6..0683d56f1 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -172,7 +172,7 @@ export function runCommand(command, catalog, cb) { applyCommandToEachLayer(cmd.calc, targetLayers, arcs, opts); } else if (name == 'classify') { - applyCommandToEachLayer(cmd.classify, targetLayers, opts); + applyCommandToEachLayer(cmd.classify, targetLayers, targetDataset, opts); } else if (name == 'clean') { cmd.cleanLayers(targetLayers, targetDataset, opts); diff --git a/src/color/graph-color.js b/src/color/graph-color.js new file mode 100644 index 000000000..d83bd275f --- /dev/null +++ b/src/color/graph-color.js @@ -0,0 +1,77 @@ +import { getNeighborLookupFunction } from '../polygons/mapshaper-polygon-neighbors'; +import utils from '../utils/mapshaper-utils'; +import { getFeatureCount, requirePolygonLayer } from '../dataset/mapshaper-layer-utils'; +import { message, stop, error } from '../utils/mapshaper-logging'; + +export function getNonAdjacentClassifier(lyr, dataset, colors) { + requirePolygonLayer(lyr); + var getNeighbors = getNeighborLookupFunction(lyr, dataset.arcs); + var errorCount = 0; + var data = utils.range(getFeatureCount(lyr)).map(function(shpId) { + var nabes = getNeighbors(shpId) || []; + return { + nabes: nabes, + n: nabes.length, + colorId: -1 + }; + }); + var getSortedColorIds = getUpdateFunction(colors.length); + var colorIds = getSortedColorIds(); + // Sort adjacency data by number of neighbors in descending order + var sorted = data.concat(); + utils.sortOn(sorted, 'n', false); + // Assign colors, starting with polygons with the largest number of neighbors + sorted.forEach(function(d) { + var colorId = pickColor(d, data, colorIds); + if (colorId == -1) { + errorCount++; + colorId = colorIds[0]; + } + d.colorId = colorId; + colorIds = getSortedColorIds(colorId); + }); + + if (errorCount > 0) { + message(`Unable to find non-adjacent colors for ${errorCount} ${errorCount == 1 ? 'polygon' : 'polygons'}`); + } + return function(shpId) { + return colors[data[shpId].colorId]; + }; +} + +// Pick the id of a color that is not shared with a neighboring polygon +export function pickColor(d, data, colorIds) { + var candidateId; + for (var i=0; i= 0 && i < n) { + counts[i]++; + utils.sortArrayIndex(ids, counts, true); + } else if (i !== undefined) { + error('Unexpected color index:', i); + } + return ids; + }; +} diff --git a/src/commands/mapshaper-classify.js b/src/commands/mapshaper-classify.js index d99fefde9..d42df0072 100644 --- a/src/commands/mapshaper-classify.js +++ b/src/commands/mapshaper-classify.js @@ -1,6 +1,6 @@ import { stop, message } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; -import { requireDataField } from '../dataset/mapshaper-layer-utils'; +import { requireDataField, initDataTable } from '../dataset/mapshaper-layer-utils'; import { getFieldValues } from '../datatable/mapshaper-data-utils'; import { isColorSchemeName, getColorRamp, getCategoricalColorScheme } from '../color/color-schemes'; import { parseColor } from '../color/color-utils'; @@ -19,26 +19,29 @@ import { } from '../classification/mapshaper-interpolation'; import cmd from '../mapshaper-cmd'; import { getUniqFieldValues } from '../datatable/mapshaper-data-utils'; +import { getNonAdjacentClassifier } from '../color/graph-color'; -cmd.classify = function(lyr, optsArg) { +cmd.classify = function(lyr, dataset, optsArg) { + if (!lyr.data) { + initDataTable(lyr); + } var opts = optsArg || {}; var records = lyr.data && lyr.data.getRecords(); var nullValue = opts.null_value || null; var looksLikeColors = !!opts.colors || !!opts.color_scheme; var colorScheme; - var classValues, classify; + var classValues, classifyByValue, classifyById; var numBuckets, numValues; var dataField, outputField; // validate explicitly set classes if (opts.classes) { if (!utils.isInteger(opts.classes) || opts.classes > 1 === false) { - stop('Invalid classes= value:', opts.classes); + stop('Invalid number of classes:', opts.classes, '(expected a value greater than 1)'); } numBuckets = opts.classes; } - // TODO: better validation of breaks values if (opts.breaks) { numBuckets = opts.breaks.length + 1; @@ -56,9 +59,6 @@ cmd.classify = function(lyr, optsArg) { } else if (opts.field) { dataField = opts.field; - - } else { - stop('Missing a data field to classify'); } // expand categories if value is '*' @@ -66,7 +66,18 @@ cmd.classify = function(lyr, optsArg) { opts.categories = getUniqFieldValues(records, dataField); } - requireDataField(lyr.data, dataField); + if (opts.method == 'non-adjacent') { + if (lyr.geometry_type != 'polygon') { + stop('The non-adjacent option requires a polygon layer'); + } + if (dataField) { + stop('The non-adjacent option does not accept a data field argument'); + } + } else if (!dataField) { + stop('Missing a data field to classify'); + } else { + requireDataField(lyr.data, dataField); + } if (numBuckets) { numValues = opts.continuous ? numBuckets + 1 : numBuckets; @@ -134,15 +145,25 @@ cmd.classify = function(lyr, optsArg) { // get a function to convert input data to class indexes // - if (opts.index_field) { + if (opts.method == 'non-adjacent') { + classifyById = getNonAdjacentClassifier(lyr, dataset, classValues); + } else if (opts.index_field) { // data is pre-classified... just read the index from a field - classify = getIndexedClassifier(classValues, nullValue, opts); + classifyByValue = getIndexedClassifier(classValues, nullValue, opts); } else if (opts.categories) { - classify = getCategoricalClassifier(classValues, nullValue, opts); + classifyByValue = getCategoricalClassifier(classValues, nullValue, opts); } else { - classify = getSequentialClassifier(classValues, nullValue, getFieldValues(records, dataField), opts); + classifyByValue = getSequentialClassifier(classValues, nullValue, getFieldValues(records, dataField), opts); } + if (classifyByValue) { + classifyById = function(id) { + var d = records[id] || {}; + return classifyByValue(d[dataField]); + }; + } + + // get the name of the output field // if (looksLikeColors) { @@ -158,8 +179,7 @@ cmd.classify = function(lyr, optsArg) { } records.forEach(function(d, i) { - d = d || {}; - d[outputField] = classify(d[dataField]); + d[outputField] = classifyById(i); }); }; diff --git a/src/commands/mapshaper-colorizer.js b/src/commands/mapshaper-colorizer.js index a9e45c4d7..7cea0a236 100644 --- a/src/commands/mapshaper-colorizer.js +++ b/src/commands/mapshaper-colorizer.js @@ -6,6 +6,7 @@ import { getCategoricalClassifier } from '../classification/mapshaper-categorica import { getDiscreteValueGetter } from '../classification/mapshaper-classification'; import { getDiscreteClassifier } from '../classification/mapshaper-sequential-classifier'; import { getRoundingFunction } from '../geom/mapshaper-rounding'; +import { getNonAdjacentClassifier } from '../color/graph-color'; cmd.colorizer = function(opts) { if (!opts.name) { diff --git a/src/join/mapshaper-join-tables.js b/src/join/mapshaper-join-tables.js index 700fdad4c..f58060b36 100644 --- a/src/join/mapshaper-join-tables.js +++ b/src/join/mapshaper-join-tables.js @@ -6,11 +6,12 @@ import { DataTable } from '../datatable/mapshaper-data-table'; import { cloneShape } from '../paths/mapshaper-shape-utils'; import { copyRecord } from '../datatable/mapshaper-data-utils'; +// Join data from @src table to records in @dest table export function joinTables(dest, src, join, opts) { return joinTableToLayer({data: dest}, src, join, opts); } -// Join data from @src table to records in @dest table +// Join data from @src table to records in @destLyr layer. // @join function // Receives index of record in the dest table // Returns array of matching records in src table, or null if no matches @@ -33,7 +34,7 @@ export function joinTableToLayer(destLyr, src, join, opts) { retn = {}, srcRec, srcId, destRec, joins, count, filter, calc, i, j, n, m; - // support for duplication + // support for duplication of destination records var duplicateRecords, destShapes; if (useDuplication) { if (opts.calc) stop('duplication and calc options cannot be used together'); @@ -62,6 +63,7 @@ export function joinTableToLayer(destLyr, src, join, opts) { for (j=0, count=0, m=joins ? joins.length : 0; j 0 && useDuplication) { destRec = copyRecord(duplicateRecords[i]); destRecords.push(destRec); From 341bf01593d2836004058241082cbcd7e4d5eda9 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 15 Nov 2021 01:51:17 -0500 Subject: [PATCH 103/891] v0.5.71 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40f0b323b..a09bcfc91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.71 +* Added -classify non-adjacent option, for assigning non-adjacent colors to a polygon layer in a randomish pattern. + v0.5.70 * Added -join duplication option, which duplicates features in the target layer when many-to-one joins occur. From 5950210b5e79219d457d707c2f239e9e0fbbaa9c Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 16 Nov 2021 20:00:52 -0500 Subject: [PATCH 104/891] v0.5.72 --- .gitignore | 3 +- CHANGELOG.md | 5 ++- package-lock.json | 2 +- package.json | 6 ++-- src/color/graph-color.js | 72 ++++++++++++++++++++++++++++++++++++---- 5 files changed, 76 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 96fec66bc..632708dd2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ npm-debug.log /www/mapshaper-gui.js /www/mapshaper.js /www/node_modules.js -/build/*.js +pre-publish +pre-release.js release* nacis diff --git a/CHANGELOG.md b/CHANGELOG.md index a09bcfc91..96833133c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.72 +* Improved performance of non-adjacent polygon coloring using DSATUR algorithm. + v0.5.71 * Added -classify non-adjacent option, for assigning non-adjacent colors to a polygon layer in a randomish pattern. @@ -26,7 +29,7 @@ v0.5.64 * Bug fixes v0.5.63 -* Added -o decimal-comma function, for exporting CSV numbers with decimal commas. +* Added -o decimal-comma option, for exporting CSV numbers with decimal commas. * Fix for issue #497 (error erasing with overlapping polygons). v0.5.62 diff --git a/package-lock.json b/package-lock.json index 6ef5be819..c339f4d95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.71", + "version": "0.5.72", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index ef8628eac..333102b22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.71", + "version": "0.5.72", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", @@ -24,8 +24,8 @@ "scripts": { "test": "mocha -r esm --parallel --jobs 4 --check-leaks -R dot", "build": "rollup --config", - "prepublishOnly": "npm test", - "postpublish": "./release", + "prepublishOnly": "npm test; ./pre-publish", + "postpublish": "./release_web_ui; ./release_github_version", "browserify_old": "browserify -r sync-request -r mproj -r buffer -r iconv-lite -r fs -r flatbush -r rw -r path -r d3-scale-chromatic -r d3-color -r d3-interpolate -o www/modules.js", "browserify": "browserify -r sync-request -r mproj -r buffer -r iconv-lite -r fs -r flatbush -r rw -r path -r d3-scale-chromatic -r d3-color -r d3-interpolate -r kdbush -o www/modules.js", "watch": "rollup --config --watch", diff --git a/src/color/graph-color.js b/src/color/graph-color.js index d83bd275f..7670b589a 100644 --- a/src/color/graph-color.js +++ b/src/color/graph-color.js @@ -3,6 +3,8 @@ import utils from '../utils/mapshaper-utils'; import { getFeatureCount, requirePolygonLayer } from '../dataset/mapshaper-layer-utils'; import { message, stop, error } from '../utils/mapshaper-logging'; +var BALANCE_COLORS = true; + export function getNonAdjacentClassifier(lyr, dataset, colors) { requirePolygonLayer(lyr); var getNeighbors = getNeighborLookupFunction(lyr, dataset.arcs); @@ -10,25 +12,30 @@ export function getNonAdjacentClassifier(lyr, dataset, colors) { var data = utils.range(getFeatureCount(lyr)).map(function(shpId) { var nabes = getNeighbors(shpId) || []; return { + // id: shpId, nabes: nabes, - n: nabes.length, - colorId: -1 + colorId: -1, + nabeColors: [], + uncolored: nabes.length, + saturation: 0, + saturation2: 0 }; }); var getSortedColorIds = getUpdateFunction(colors.length); var colorIds = getSortedColorIds(); // Sort adjacency data by number of neighbors in descending order - var sorted = data.concat(); - utils.sortOn(sorted, 'n', false); + var iter = getNodeIterator(data); // Assign colors, starting with polygons with the largest number of neighbors - sorted.forEach(function(d) { + iter.forEach(function(d) { var colorId = pickColor(d, data, colorIds); if (colorId == -1) { errorCount++; colorId = colorIds[0]; } d.colorId = colorId; - colorIds = getSortedColorIds(colorId); + if (BALANCE_COLORS) { + colorIds = getSortedColorIds(colorId); + } }); if (errorCount > 0) { @@ -39,6 +46,59 @@ export function getNonAdjacentClassifier(lyr, dataset, colors) { }; } +function getNodeIterator(data) { + var sorted = data.concat(); + utils.sortOn(sorted, 'uncolored', true); + function forEach(cb) { + var item; + while(sorted.length > 0) { + item = sorted.pop(); + cb(item); + updateNeighbors(item, sorted, data); + } + } + + return { + forEach: forEach + }; +} + +function updateNeighbors(item, sorted, data) { + var nabe; + var ids = item.nabes; + for (var i=0; i -1) continue; + updateNeighbor(nabe, item.colorId, sorted); + } +} + +function updateNeighbor(a, colorId, sorted) { + var i = sorted.indexOf(a); // could optimize with a binary search + var n = sorted.length; + var b; + if (i == -1) { + error('Indexing error'); + } + a.uncolored--; + a.saturation2++; + if (!a.nabeColors.includes(colorId)) { + a.saturation++; + a.nabeColors.push(colorId); + } + // insertion sort with a stopping condition + while (++i < n) { + b = sorted[i]; + // standard dsatur + if (a.saturation < b.saturation || a.saturation == b.saturation && a.uncolored > b.uncolored) break; + // based on 4-color tests with counties and zipcodes, this condition adds a bit of strength + if (a.saturation == b.saturation && a.uncolored == b.uncolored && a.saturation2 < b.saturation2) break; // 332 + sorted[i-1] = b; + sorted[i] = a; + } +} + + // Pick the id of a color that is not shared with a neighboring polygon export function pickColor(d, data, colorIds) { var candidateId; From 95781cf9ff4230b27ea5c078fb409ec219807588 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 17 Nov 2021 14:06:48 -0500 Subject: [PATCH 105/891] Improve and speed up non-adjacent color finding --- src/color/graph-color.js | 46 +++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/color/graph-color.js b/src/color/graph-color.js index 7670b589a..68b7642e3 100644 --- a/src/color/graph-color.js +++ b/src/color/graph-color.js @@ -11,15 +11,15 @@ export function getNonAdjacentClassifier(lyr, dataset, colors) { var errorCount = 0; var data = utils.range(getFeatureCount(lyr)).map(function(shpId) { var nabes = getNeighbors(shpId) || []; - return { - // id: shpId, + var d = { nabes: nabes, colorId: -1, nabeColors: [], - uncolored: nabes.length, - saturation: 0, - saturation2: 0 + uncolored: nabes.length, // number of uncolored neighbors + saturation: 0, // number of unique colors of neighbors + common: 0 // number of repeated colors in neighbors }; + return d; }); var getSortedColorIds = getUpdateFunction(colors.length); var colorIds = getSortedColorIds(); @@ -74,30 +74,52 @@ function updateNeighbors(item, sorted, data) { } function updateNeighbor(a, colorId, sorted) { - var i = sorted.indexOf(a); // could optimize with a binary search + var i = findItem(a, sorted); var n = sorted.length; var b; if (i == -1) { error('Indexing error'); } a.uncolored--; - a.saturation2++; if (!a.nabeColors.includes(colorId)) { a.saturation++; a.nabeColors.push(colorId); + } else { + a.common++; } - // insertion sort with a stopping condition + while (++i < n) { b = sorted[i]; - // standard dsatur - if (a.saturation < b.saturation || a.saturation == b.saturation && a.uncolored > b.uncolored) break; - // based on 4-color tests with counties and zipcodes, this condition adds a bit of strength - if (a.saturation == b.saturation && a.uncolored == b.uncolored && a.saturation2 < b.saturation2) break; // 332 + if (!betterThan(a, b)) break; sorted[i-1] = b; sorted[i] = a; } } +function findItem(a, sorted) { + // return sorted.indexOf(a); // bottleneck + // binary search in sorted array + var start = 0, end = sorted.length, i; + while (end - start > 50) { + i = Math.floor((start + end) / 2); + if (sorted[i].saturation >= a.saturation) { + end = i; + } else { + start = i; + } + } + return sorted.indexOf(a, start); +} + +function betterThan(a, b) { + if (a.saturation > b.saturation) return true; + if (a.saturation < b.saturation) return false; + if (a.common > b.common) return true; + if (a.common < b.common) return false; + // based on 4-color tests with counties and zipcodes, this condition adds a bit of strength + if (a.uncolored < b.uncolored) return true; + return false; +} // Pick the id of a color that is not shared with a neighboring polygon export function pickColor(d, data, colorIds) { From 4b17796ed919beae5abb5fc4d79b386be5491878 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 19 Nov 2021 00:09:37 -0500 Subject: [PATCH 106/891] Update deps --- package-lock.json | 113 +++++++++++++++++++++------------------------- package.json | 4 +- 2 files changed, 54 insertions(+), 63 deletions(-) diff --git a/package-lock.json b/package-lock.json index c339f4d95..490fa3d8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@rollup/plugin-node-resolve": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.0.0.tgz", - "integrity": "sha512-41X411HJ3oikIDivT5OKe9EZ6ud6DXudtfNrGbC4nniaxx2esiWjkLOzgnZsWq1IM8YIeL2rzRGLZLBjlhnZtQ==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.0.6.tgz", + "integrity": "sha512-sFsPDMPd4gMqnh2gS0uIxELnoRUp5kBl5knxD2EO0778G1oOJv4G1vyT2cpWz75OU2jDVcXhjVUuTAczGyFNKA==", "dev": true, "requires": { "@rollup/pluginutils": "^3.1.0", @@ -485,9 +485,9 @@ } }, "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", "dev": true }, "caseless": { @@ -496,9 +496,9 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -530,15 +530,6 @@ "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.5.0" - }, - "dependencies": { - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - } } }, "cipher-base": { @@ -563,9 +554,9 @@ }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "is-fullwidth-code-point": { @@ -575,23 +566,23 @@ "dev": true }, "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } } } @@ -1098,9 +1089,9 @@ "dev": true }, "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "optional": true }, @@ -1446,9 +1437,9 @@ "dev": true }, "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { "is-extglob": "^2.1.1" @@ -2058,12 +2049,12 @@ "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" }, "rollup": { - "version": "2.28.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.28.2.tgz", - "integrity": "sha512-8txbsFBFLmm9Xdt4ByTOGa9Muonmc8MfNjnGAR8U8scJlF1ZW7AgNZa7aqBXaKtlvnYP/ab++fQIq9dB9NWUbg==", + "version": "2.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.60.0.tgz", + "integrity": "sha512-cHdv9GWd58v58rdseC8e8XIaPUo8a9cgZpnCMMDGZFDZKEODOiPPEQFXLriWr/TjXzhPPmG5bkAztPsOARIcGQ==", "dev": true, "requires": { - "fsevents": "~2.1.2" + "fsevents": "~2.3.2" } }, "rw": { @@ -2523,9 +2514,9 @@ }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "is-fullwidth-code-point": { @@ -2535,23 +2526,23 @@ "dev": true }, "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } } } @@ -2590,9 +2581,9 @@ }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "is-fullwidth-code-point": { @@ -2602,23 +2593,23 @@ "dev": true }, "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } } } diff --git a/package.json b/package.json index 333102b22..3bee66a4b 100644 --- a/package.json +++ b/package.json @@ -56,13 +56,13 @@ "tinyqueue": "^2.0.3" }, "devDependencies": { - "@rollup/plugin-node-resolve": "^13.0.0", + "@rollup/plugin-node-resolve": "^13.0.6", "browserify": "^17.0.0", "csv-spectrum": "^1.0.0", "deep-eql": ">=0.1.3", "esm": "^3.2.25", "mocha": "^8.4.0", - "rollup": "^2.28.2", + "rollup": "^2.60.0", "shell-quote": "^1.6.1", "underscore": "^1.13.1" }, From 8bf4a0aa715f8a78c40c78abdf4387cb1dab0e20 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 19 Nov 2021 00:11:21 -0500 Subject: [PATCH 107/891] Fix -dissolve2 error on empty file --- src/color/graph-color.js | 2 +- src/commands/mapshaper-dissolve2.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/color/graph-color.js b/src/color/graph-color.js index 68b7642e3..e10eabd82 100644 --- a/src/color/graph-color.js +++ b/src/color/graph-color.js @@ -87,7 +87,7 @@ function updateNeighbor(a, colorId, sorted) { } else { a.common++; } - + // bubble sort!!! while (++i < n) { b = sorted[i]; if (!betterThan(a, b)) break; diff --git a/src/commands/mapshaper-dissolve2.js b/src/commands/mapshaper-dissolve2.js index abc70df69..cf4436d96 100644 --- a/src/commands/mapshaper-dissolve2.js +++ b/src/commands/mapshaper-dissolve2.js @@ -1,13 +1,14 @@ import cmd from '../mapshaper-cmd'; import { dissolvePolygonLayer2 } from '../dissolve/mapshaper-polygon-dissolve2'; import { addIntersectionCuts } from '../paths/mapshaper-intersection-cuts'; -import { requirePolygonLayer } from '../dataset/mapshaper-layer-utils'; +import { requirePolygonLayer, layerHasPaths } from '../dataset/mapshaper-layer-utils'; // Removes small gaps and all overlaps cmd.dissolve2 = function(layers, dataset, opts) { layers.forEach(requirePolygonLayer); var nodes = addIntersectionCuts(dataset, opts); return layers.map(function(lyr) { + if (!layerHasPaths(lyr)) return lyr; return dissolvePolygonLayer2(lyr, dataset, opts); }); }; From 761501f035cd056e9f64151365abfc706dbb3933 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 24 Nov 2021 11:37:48 -0500 Subject: [PATCH 108/891] Add this.geojson getter/setter to -each expressions --- src/cli/mapshaper-run-command.js | 4 +- src/commands/mapshaper-each.js | 18 ++- src/dataset/mapshaper-dataset-utils.js | 30 +++++ src/dataset/mapshaper-merging.js | 2 + src/expressions/mapshaper-each-geojson.js | 43 ++++++ src/expressions/mapshaper-expressions.js | 2 +- src/expressions/mapshaper-feature-proxy.js | 16 ++- src/geojson/geojson-common.js | 21 +++ src/geojson/geojson-export.js | 13 +- test/each-test.js | 148 +++++++++++++++++---- 10 files changed, 255 insertions(+), 42 deletions(-) create mode 100644 src/expressions/mapshaper-each-geojson.js diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index 0683d56f1..2e95ad211 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -203,7 +203,7 @@ export function runCommand(command, catalog, cb) { // cmd.drop(catalog, targetLayers, targetDataset, opts); } else if (name == 'each') { - applyCommandToEachLayer(cmd.evaluateEachFeature, targetLayers, arcs, opts.expression, opts); + applyCommandToEachLayer(cmd.evaluateEachFeature, targetLayers, targetDataset, opts.expression, opts); } else if (name == 'erase') { outputLayers = cmd.eraseLayers(targetLayers, source, targetDataset, opts); @@ -462,8 +462,6 @@ export function runCommand(command, catalog, cb) { } - - // delete arcs if no longer needed (e.g. after -points command) // (after output layers have been integrated) if (targetDataset) { diff --git a/src/commands/mapshaper-each.js b/src/commands/mapshaper-each.js index e7e82db4c..1bd3c37c3 100644 --- a/src/commands/mapshaper-each.js +++ b/src/commands/mapshaper-each.js @@ -2,12 +2,21 @@ import { compileFeatureExpression, compileValueExpression } from '../expressions import { getFeatureCount } from '../dataset/mapshaper-layer-utils'; import { getStateVar } from '../mapshaper-state'; import { DataTable } from '../datatable/mapshaper-data-table'; +import { expressionUsesGeoJSON, getFeatureEditor } from '../expressions/mapshaper-each-geojson'; +import { dissolveArcs } from '../paths/mapshaper-arc-dissolve'; +import { replaceLayerContents } from '../dataset/mapshaper-dataset-utils'; + import cmd from '../mapshaper-cmd'; -cmd.evaluateEachFeature = function(lyr, arcs, exp, opts) { +cmd.evaluateEachFeature = function(lyr, dataset, exp, opts) { var n = getFeatureCount(lyr), + arcs = dataset.arcs, compiled, filter; + var exprOpts = { + geojson_editor: expressionUsesGeoJSON(exp) ? getFeatureEditor(lyr, dataset) : null + }; + // TODO: consider not creating a data table -- not needed if expression only references geometry if (n > 0 && !lyr.data) { lyr.data = new DataTable(n); @@ -17,11 +26,16 @@ cmd.evaluateEachFeature = function(lyr, arcs, exp, opts) { } // 'defs' are now added to the context of all expressions // compiled = compileFeatureExpression(exp, lyr, arcs, {context: getStateVar('defs')}); - compiled = compileFeatureExpression(exp, lyr, arcs); + compiled = compileFeatureExpression(exp, lyr, arcs, exprOpts); // call compiled expression with id of each record for (var i=0; i= 4 && first[0] == last[0] && first[1] == last[1]; }; + +GeoJSON.toFeature = function(obj, properties) { + var type = obj ? obj.type : null; + var feat; + if (type == 'Feature') { + feat = obj; + } else if (type in GeoJSON.typeLookup) { + feat = { + type: 'Feature', + geometry: obj, + properties: properties || null + }; + } else { + feat = { + type: 'Feature', + geometry: null, + properties: properties || null + }; + } + return feat; +}; diff --git a/src/geojson/geojson-export.js b/src/geojson/geojson-export.js index 67bbd299c..ca4742546 100644 --- a/src/geojson/geojson-export.js +++ b/src/geojson/geojson-export.js @@ -90,18 +90,17 @@ export function exportLayerAsGeoJSON(lyr, dataset, opts, asFeatures, ofmt) { return (shapes || properties || []).reduce(function(memo, o, i) { var shape = shapes ? shapes[i] : null, exporter = GeoJSON.exporters[lyr.geometry_type], - obj = shape ? exporter(shape, dataset.arcs, opts) : null; + geom = shape ? exporter(shape, dataset.arcs, opts) : null, + obj = null; if (asFeatures) { - obj = { - type: 'Feature', - geometry: obj, - properties: properties ? properties[i] : null - }; + obj = GeoJSON.toFeature(geom, properties ? properties[i] : null); if (ids) { obj.id = ids[i]; } - } else if (!obj) { + } else if (!geom) { return memo; // don't add null objects to GeometryCollection + } else { + obj = geom; } if (ofmt) { // stringify features as soon as they are generated, to reduce the diff --git a/test/each-test.js b/test/each-test.js index 7e7f91dea..1afc9c896 100644 --- a/test/each-test.js +++ b/test/each-test.js @@ -2,10 +2,103 @@ var assert = require('assert'), api = require("../"), ArcCollection = api.internal.ArcCollection; + +// Adapter for older version of the function used by some tests +function evaluateEachFeature(lyr, arcs, expr, opts) { + var dataset = { + layers: [lyr], + arcs: arcs + }; + return api.evaluateEachFeature(lyr, dataset, expr, opts); +} + describe('mapshaper-each.js', function () { describe('-each command', function () { + it('this.geojson getter', function(done) { + var data = { + type: 'Feature', + properties: {name: 'Fred'}, + geometry: { + type: 'Point', + coordinates: [2, 3] + } + }; + var cmd = '-i data.json -each "geojson = this.geojson" -o'; + api.applyCommands(cmd, {'data.json': JSON.stringify(data)}, function(err, out) { + var output = JSON.parse(out['data.json']) + var geojson = output.features[0].properties.geojson; + assert.deepEqual(geojson, data); + done(); + }); + }) + + it('this.geojson setter', function(done) { + var data = { + type: 'Feature', + properties: {geostr: `{ + "type": "LineString", + "coordinates": [[0,0], [1,1]] + }`}, + geometry: { + type: 'Point', + coordinates: [2, 3] + } + }; + var cmd = '-i data.json -each "this.geojson = geostr" -o'; + api.applyCommands(cmd, {'data.json': JSON.stringify(data)}, function(err, out) { + var output = JSON.parse(out['data.json']) + var expect = JSON.parse(data.properties.geostr); + assert.deepEqual(output.geometries[0], expect); + done(); + }); + }) + + + it('this.geojson setter replacing lines with lines', function(done) { + var data = { + type: 'Feature', + properties: {geostr: `{ + "type": "LineString", + "coordinates": [[0,0], [1,1]] + }`}, + geometry: { + type: 'LineString', + coordinates: [[1,1], [0,0]] + } + }; + var cmd = '-i data.json -each "this.geojson = geostr" -o'; + api.applyCommands(cmd, {'data.json': JSON.stringify(data)}, function(err, out) { + var output = JSON.parse(out['data.json']) + var expect = JSON.parse(data.properties.geostr); + assert.deepEqual(output.geometries[0], expect); + done(); + }); + }) + + it('this.geojson getter + setter', function(done) { + var data = { + type: 'Feature', + properties: {name: 'Fred'}, + geometry: { + type: 'Point', + coordinates: [2, 3] + } + }; + var cmd = '-i data.json -each "tmp = this.geojson, tmp.geometry.coordinates[0] = 4, this.geojson = tmp" -o'; + api.applyCommands(cmd, {'data.json': JSON.stringify(data)}, function(err, out) { + var output = JSON.parse(out['data.json']) + var geom = output.features[0].geometry; + var expect = { + type: 'Point', + coordinates: [4, 3] + }; + assert.deepEqual(geom, expect); + done(); + }); + }) + it('layer.name works', function (done) { var csv = 'id\na\nb'; var cmd = '-i data.csv -each "name = `${this.layer.name}_${id}`" -o format=json'; @@ -38,6 +131,7 @@ describe('mapshaper-each.js', function () { }) + describe('evaluateEachFeature()', function () { var nullArcs = new api.internal.ArcCollection([]); it('create new numeric field', function () { @@ -45,7 +139,7 @@ describe('mapshaper-each.js', function () { var lyr = { data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, null, "FOO=0"); + evaluateEachFeature(lyr, null, "FOO=0"); assert.deepEqual(records, [{FOO:0}, {FOO:0}]); }) @@ -54,7 +148,7 @@ describe('mapshaper-each.js', function () { var lyr = { data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, null, "FOO=''"); + evaluateEachFeature(lyr, null, "FOO=''"); assert.deepEqual(records, [{FOO:''}, {FOO:''}]); }) @@ -63,7 +157,7 @@ describe('mapshaper-each.js', function () { var lyr = { data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "delete foo"); + evaluateEachFeature(lyr, nullArcs, "delete foo"); assert.deepEqual(records, [{}, {}]); }) @@ -72,7 +166,7 @@ describe('mapshaper-each.js', function () { var lyr = { data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "foo = foo.map(n => n+1).join('')"); + evaluateEachFeature(lyr, nullArcs, "foo = foo.map(n => n+1).join('')"); assert.deepEqual(records, [{foo: '234'}]); }) @@ -81,7 +175,7 @@ describe('mapshaper-each.js', function () { var lyr = { data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "foo=foo.substr(0, 2)"); + evaluateEachFeature(lyr, nullArcs, "foo=foo.substr(0, 2)"); assert.deepEqual(records, [{foo:'mi'}, {foo:'be'}]); }) @@ -90,7 +184,7 @@ describe('mapshaper-each.js', function () { var lyr = { data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "this.properties['label-text'] = this.properties['label-text'].toUpperCase()"); + evaluateEachFeature(lyr, nullArcs, "this.properties['label-text'] = this.properties['label-text'].toUpperCase()"); assert.deepEqual(records, [{'label-text':'FINLAND'}, {'label-text':'SWEDEN'}]); }) @@ -99,7 +193,7 @@ describe('mapshaper-each.js', function () { var lyr = { data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "d['label-text'] = d['label-text'].toUpperCase()"); + evaluateEachFeature(lyr, nullArcs, "d['label-text'] = d['label-text'].toUpperCase()"); assert.deepEqual(records, [{'label-text':'FINLAND'}, {'label-text':'SWEDEN'}]); }) @@ -110,7 +204,7 @@ describe('mapshaper-each.js', function () { shapes: [[[0, 2], [-2]], null], data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "parts=$.partCount"); + evaluateEachFeature(lyr, nullArcs, "parts=$.partCount"); assert.deepEqual(records, [{parts: 2}, {parts: 0}]); }) @@ -119,7 +213,7 @@ describe('mapshaper-each.js', function () { geometry_type: 'polygon', shapes: [[[0, 2], [-2]], null] }; - api.evaluateEachFeature(lyr, nullArcs, "parts=$.partCount"); + evaluateEachFeature(lyr, nullArcs, "parts=$.partCount"); assert.deepEqual(lyr.data.getRecords(), [{parts: 2}, {parts: 0}]); }) @@ -128,7 +222,7 @@ describe('mapshaper-each.js', function () { shapes: [null, null], data: new api.internal.DataTable([null, {'a': 13}]) }; - api.evaluateEachFeature(lyr, nullArcs, "FID=$.id"); + evaluateEachFeature(lyr, nullArcs, "FID=$.id"); assert.deepEqual(lyr.data.getRecords(), [{FID: 0}, {a: 13, FID: 1}]); }) @@ -138,7 +232,7 @@ describe('mapshaper-each.js', function () { shapes: [], data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "bar = foo, delete foo"); + evaluateEachFeature(lyr, nullArcs, "bar = foo, delete foo"); assert.deepEqual(records, [{bar: 'mice'}, {bar: 'beans'}]); }) @@ -148,7 +242,7 @@ describe('mapshaper-each.js', function () { shapes: [], data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "$$foo = foo + foo"); + evaluateEachFeature(lyr, nullArcs, "$$foo = foo + foo"); assert.deepEqual(records, [{foo: 'mice', '$$foo': 'micemice'}, {foo: 'beans', '$$foo': 'beansbeans'}]); }) @@ -158,7 +252,7 @@ describe('mapshaper-each.js', function () { shapes: [], data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "valid = $.properties.foo === foo"); + evaluateEachFeature(lyr, nullArcs, "valid = $.properties.foo === foo"); assert.deepEqual(records, [{foo: 'mice', valid: true}, {foo: 'beans', valid: true}]); }) @@ -167,7 +261,7 @@ describe('mapshaper-each.js', function () { shapes: [null, null], data: null }; - api.evaluateEachFeature(lyr, nullArcs, "$.properties = {FID: $.id}"); + evaluateEachFeature(lyr, nullArcs, "$.properties = {FID: $.id}"); assert.deepEqual(lyr.data.getRecords(), [{FID: 0}, {FID: 1}]); }) @@ -178,7 +272,7 @@ describe('mapshaper-each.js', function () { shapes: [null, null], data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "$.properties = {menu: foo}"); + evaluateEachFeature(lyr, nullArcs, "$.properties = {menu: foo}"); assert.deepEqual(lyr.data.getRecords(), [{menu: 'mice'}, {menu: 'beans'}]); }) @@ -187,7 +281,7 @@ describe('mapshaper-each.js', function () { var lyr = { data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "bar = foo"); + evaluateEachFeature(lyr, nullArcs, "bar = foo"); assert.deepEqual(lyr.data.getRecords(), [{foo:'mice', bar: 'mice'}, { bar: null }]); @@ -199,7 +293,7 @@ describe('mapshaper-each.js', function () { shapes: [], data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "bar=Math.sqrt(foo); delete foo"); + evaluateEachFeature(lyr, nullArcs, "bar=Math.sqrt(foo); delete foo"); assert.deepEqual(records, [{bar: 2}, {bar: 0}]); }) @@ -209,7 +303,7 @@ describe('mapshaper-each.js', function () { shapes: [], data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "foo = 22", {"where": "bar == 'b'"}); + evaluateEachFeature(lyr, nullArcs, "foo = 22", {"where": "bar == 'b'"}); assert.deepEqual(records, [{foo: 4, bar: 'a'}, {foo: 22, bar: 'b'}]); }) @@ -218,7 +312,7 @@ describe('mapshaper-each.js', function () { var lyr = { data: new api.internal.DataTable(records) }; - api.evaluateEachFeature(lyr, nullArcs, "foo = 'A»s'; bar=''", {"where": "!/;/.test(foo)"}); + evaluateEachFeature(lyr, nullArcs, "foo = 'A»s'; bar=''", {"where": "!/;/.test(foo)"}); assert.deepEqual(records, [{foo: 'A»s', bar: ''}]); }) @@ -228,7 +322,7 @@ describe('mapshaper-each.js', function () { geometry_type: 'point', shapes: [[[0, 1]], [[2, 3], [3, 4]], null] }; - api.evaluateEachFeature(lyr, null, "x=$.x, y=$.y"); + evaluateEachFeature(lyr, null, "x=$.x, y=$.y"); assert.deepEqual(lyr.data.getRecords(), [{x: 0, y: 1}, {x: 2, y: 3}, {x: null, y: null}]); }) @@ -238,7 +332,7 @@ describe('mapshaper-each.js', function () { shapes: [[[0, 1]], [[2, 3], [3, 4]], null] }; // first point of multipoint is set; null shapes are ignored (for now) - api.evaluateEachFeature(lyr, null, "$.x = 0, $.y = 0"); + evaluateEachFeature(lyr, null, "$.x = 0, $.y = 0"); assert.deepEqual(lyr.shapes, [[[0, 0]], [[0, 0], [3, 4]], null]); }) @@ -247,7 +341,7 @@ describe('mapshaper-each.js', function () { geometry_type: 'point', shapes: [[[0, 1]], [[2, 3], [3, 4]]] }; - api.evaluateEachFeature(lyr, null, "x=$.coordinates[0][0], y=$.coordinates[0][1]"); + evaluateEachFeature(lyr, null, "x=$.coordinates[0][0], y=$.coordinates[0][1]"); assert.deepEqual(lyr.data.getRecords(), [{x: 0, y: 1}, {x: 2, y: 3}]); }) }); @@ -336,28 +430,28 @@ describe('mapshaper-each.js', function () { }) it ("$.centroidX and $.centroidY", function() { - api.evaluateEachFeature(lyr, arcs, "x=$.centroidX, y=$.centroidY"); + evaluateEachFeature(lyr, arcs, "x=$.centroidX, y=$.centroidY"); assert.deepEqual(lyr.data.getRecords(), [{x: 1.5, y: 2.5}, {x: 2, y: 1.5}, {x: null, y: null}]) }) it ("$.partCount and $.isNull", function() { - api.evaluateEachFeature(lyr, arcs, "parts=$.partCount, isNull=$.isNull"); + evaluateEachFeature(lyr, arcs, "parts=$.partCount, isNull=$.isNull"); assert.deepEqual(lyr.data.getRecords(), [{parts: 1, isNull: false}, {parts: 2, isNull: false}, {parts: 0, isNull: true}]) }) /* it ("$.area and $.originalArea", function() { - api.evaluateEachFeature(lyr, arcs, "area=$.area, area2=$.originalArea"); + evaluateEachFeature(lyr, arcs, "area=$.area, area2=$.originalArea"); assert.deepEqual(lyr.data.getRecords(), [{area: 1, area2: 1}, {area: 3, area2: 3}, {area: 0, area2: 0}]) }) */ it ("$.height and $.width", function() { - api.evaluateEachFeature(lyr, arcs, "h=$.height, w=$.width"); + evaluateEachFeature(lyr, arcs, "h=$.height, w=$.width"); assert.deepEqual(lyr.data.getRecords(), [{w: 1, h: 1}, {w: 2, h: 2}, {w: 0, h: 0}]) }) it ("$.bounds", function() { - api.evaluateEachFeature(lyr, arcs, "bb=$.bounds"); + evaluateEachFeature(lyr, arcs, "bb=$.bounds"); assert.deepEqual(lyr.data.getRecords(), [{bb: [1, 2, 2, 3]}, {bb: [1, 1, 3, 3]}, {bb: []}]) }) From 171a3a762838b4f457a619abe3fca8d9409be7fb Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 24 Nov 2021 11:40:09 -0500 Subject: [PATCH 109/891] v0.5.73 --- CHANGELOG.md | 3 +++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96833133c..eea7a4ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.73 +* Added this.geojson getter/setter to -each expressions. This can be used in combination with -require to transform the geometry of individual features using an external script. + v0.5.72 * Improved performance of non-adjacent polygon coloring using DSATUR algorithm. diff --git a/package-lock.json b/package-lock.json index 490fa3d8e..30cd182c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.72", + "version": "0.5.73", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3bee66a4b..d147d3599 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.72", + "version": "0.5.73", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 443c03a0b66ec3319b6f4bf4e67597c00f706410 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 24 Nov 2021 16:06:10 -0500 Subject: [PATCH 110/891] [gui] Import JSON and CSV text pasted to browser window --- src/gui/gui-import-control.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/gui/gui-import-control.js b/src/gui/gui-import-control.js index 7145a9f12..3aef07f36 100644 --- a/src/gui/gui-import-control.js +++ b/src/gui/gui-import-control.js @@ -10,7 +10,8 @@ function DropControl(el, cb) { var area = El(el); area.on('dragleave', ondragleave) .on('dragover', ondragover) - .on('drop', ondrop); + .on('drop', ondrop) + .on('paste', onpaste); function ondragleave(e) { block(e); out(); @@ -31,12 +32,39 @@ function DropControl(el, cb) { function out() { area.removeClass('dragover'); } + function onpaste(e) { + var text, file; + block(e); + try { + text = e.clipboardData.getData('text/plain').trim(); + file = text ? pastedTextToFile(text) : null; + if (file) { + cb([file]); + } + } catch(err) { + console.error(err); + } + } function block(e) { e.preventDefault(); e.stopPropagation(); } } +function pastedTextToFile(str) { + var type = internal.guessInputContentType(str); + var name; + if (type == 'text') { + name = 'pasted.txt'; + } else if (type == 'json') { + name = 'pasted.json'; + } else { + return null; + } + var blob = new Blob([str]); + return new File([blob], name); +} + // @el DOM element for select button // @cb function() function FileChooser(el, cb) { From 23b759891959d465b5fa2f411e86e19c51277d74 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 26 Nov 2021 01:11:24 -0500 Subject: [PATCH 111/891] v0.5.74 --- CHANGELOG.md | 4 ++++ package.json | 2 +- src/gui/gui-canvas.js | 47 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eea7a4ee4..30dc0b4c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.74 +* Added support for importing data in the GUI by pasting JSON and delimited text onto the browser window. +* Sped up drawing shapes in the GUI. + v0.5.73 * Added this.geojson getter/setter to -each expressions. This can be used in combination with -require to transform the geometry of individual features using an external script. diff --git a/package.json b/package.json index d147d3599..c6a7a938f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.73", + "version": "0.5.74", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/gui/gui-canvas.js b/src/gui/gui-canvas.js index b18d8e241..39aa92c28 100644 --- a/src/gui/gui-canvas.js +++ b/src/gui/gui-canvas.js @@ -2,9 +2,6 @@ import { internal, utils, Bounds } from './gui-core'; import { El } from './gui-el'; import { GUI } from './gui-lib'; -var MIN_ARC_LEN = 0.1; -var MIN_PATH_LEN = 0.1; - // TODO: consider moving this upstream function getArcsForRendering(obj, ext) { var dataset = obj.source.dataset; @@ -64,6 +61,7 @@ export function drawStyledLayerToCanvas(obj, canv, ext) { // Return a function for testing if an arc should be drawn in the current view function getArcFilter(arcs, ext, usedFlag, arcCounts) { + var MIN_PATH_LEN = 0.1; var minPathLen = ext.getPixelSize() * MIN_PATH_LEN, // * 0.5 geoBounds = ext.getBounds(), geoBBox = geoBounds.toArray(), @@ -316,7 +314,8 @@ export function DisplayCanvas() { startPath(ctx, style); } iter = protectIterForDrawing(arcs.getArcIter(i), _ext); - drawPath(iter, t, ctx, MIN_ARC_LEN); + // drawPath(iter, t, ctx, 0.1); + drawPath2(iter, t, ctx, roundToHalfPix); } endPath(ctx, style); }; @@ -403,6 +402,9 @@ function drawSquare(x, y, size, ctx) { } } +// Draw a path, but skip vertices within a given pixel threshold from the prev. vertex +// This optimization introduces visible gaps between filled polygons unless the +// threshold is much smaller than a pixel, so switching to drawPath2. function drawPath(vec, t, ctx, minLen) { // copy to local variables because of odd performance regression in Chrome 80 var mx = t.mx, @@ -411,7 +413,6 @@ function drawPath(vec, t, ctx, minLen) { by = t.by; var x, y, xp, yp; if (!vec.hasNext()) return; - minLen = utils.isNonNegNumber(minLen) ? minLen : 0.4; x = xp = vec.x * mx + bx; y = yp = vec.y * my + by; ctx.moveTo(x, y); @@ -426,6 +427,39 @@ function drawPath(vec, t, ctx, minLen) { } } + +// Draw a path, optimized by snapping pixel coordinates and skipping +// duplicate coords. +function drawPath2(vec, t, ctx, round) { + // copy to local variables because of odd performance regression in Chrome 80 + var mx = t.mx, + my = t.my, + bx = t.bx, + by = t.by; + var x, y, xp, yp; + if (!vec.hasNext()) return; + x = xp = round(vec.x * mx + bx); + y = yp = round(vec.y * my + by); + ctx.moveTo(x, y); + while (vec.hasNext()) { + x = round(vec.x * mx + bx); + y = round(vec.y * my + by); + if (x != xp || y != yp) { + ctx.lineTo(x, y); + xp = x; + yp = y; + } + } +} + +function roundToPix(x) { + return x + 0.5 | 0; +} + +function roundToHalfPix(x) { + return (x * 2 | 0) / 2; +} + function getShapePencil(arcs, ext) { var t = getScaledTransform(ext); var iter = new internal.ShapeIter(arcs); @@ -433,7 +467,8 @@ function getShapePencil(arcs, ext) { for (var i=0, n=shp ? shp.length : 0; i Date: Fri, 26 Nov 2021 01:11:45 -0500 Subject: [PATCH 112/891] v0.5.74 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 30cd182c2..030199dde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.73", + "version": "0.5.74", "lockfileVersion": 1, "requires": true, "dependencies": { From c9760c7bdb4bcf44ec299965ba6ac815c4b21bb5 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sat, 27 Nov 2021 22:38:54 -0500 Subject: [PATCH 113/891] [gui] support import by pasting files (Chrome + Safari) --- src/gui/gui-import-control.js | 87 ++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/src/gui/gui-import-control.js b/src/gui/gui-import-control.js index 3aef07f36..f016e6b24 100644 --- a/src/gui/gui-import-control.js +++ b/src/gui/gui-import-control.js @@ -6,43 +6,43 @@ import { SimpleButton } from './gui-elements'; import { GUI } from './gui-lib'; // @cb function() -function DropControl(el, cb) { +function DropControl(gui, el, cb) { var area = El(el); - area.on('dragleave', ondragleave) - .on('dragover', ondragover) + // blocking drag events enables drop event + area.on('dragleave', block) + .on('dragover', block) .on('drop', ondrop) .on('paste', onpaste); - function ondragleave(e) { - block(e); - out(); - } - function ondragover(e) { - // blocking drag events enables drop event - block(e); - over(); - } + area.node().addEventListener('paste', onpaste); function ondrop(e) { block(e); - out(); cb(e.dataTransfer.files); } - function over() { - area.addClass('dragover'); - } - function out() { - area.removeClass('dragover'); - } function onpaste(e) { - var text, file; + var types = Array.from(e.clipboardData.types || []).join(','); + var items = Array.from(e.clipboardData.items || []); + var files; block(e); - try { - text = e.clipboardData.getData('text/plain').trim(); - file = text ? pastedTextToFile(text) : null; - if (file) { - cb([file]); - } - } catch(err) { - console.error(err); + // Browser compatibility (tested on MacOS only): + // Chrome and Safari: full support + // FF: supports pasting JSON and CSV from the clipboard but not files. + // Single files of all types are pasted as a string and an image/png + // Multiple files are pasted as a string containing a list of file names + if (types == 'text/plain') { + // text from clipboard (supported by Chrome, FF, Safari) + // TODO: handle FF case of string containing multiple file names. + files = [pastedTextToFile(e.clipboardData.getData('text/plain'))]; + } else { + files = items.map(function(item) { + return item.kind == 'file' && !item.type.includes('image') ? + item.getAsFile() : null; + }); + } + files = files.filter(Boolean); + if (files.length) { + cb(files); + } else { + gui.alert('Pasted content could not be imported.'); } } function block(e) { @@ -96,6 +96,7 @@ export function ImportControl(gui, opts) { var model = gui.model; var importCount = 0; var importTotal = 0; + var overQuickView = false; var useQuickView = opts.quick_view; // may be set by mapshaper-gui var queuedFiles = []; var manifestFiles = opts.files || []; @@ -108,13 +109,12 @@ export function ImportControl(gui, opts) { new SimpleButton('#import-buttons .submit-btn').on('click', onSubmit); new SimpleButton('#import-buttons .cancel-btn').on('click', gui.clearMode); - new DropControl('body', receiveFiles); // default drop area is entire page - new DropControl('#import-drop', receiveFiles); - new DropControl('#import-quick-drop', receiveFilesQuickView); + new DropControl(gui, 'body', receiveFiles); new FileChooser('#file-selection-btn', receiveFiles); new FileChooser('#import-buttons .add-btn', receiveFiles); new FileChooser('#add-file-btn', receiveFiles); - + initDropArea('#import-quick-drop', true); + initDropArea('#import-drop'); gui.keyboard.onMenuSubmit(El('#import-options'), onSubmit); gui.addMode('import', turnOn, turnOff); @@ -127,6 +127,23 @@ export function ImportControl(gui, opts) { } }); + function initDropArea(el, isQuick) { + var area = El(el) + .on('dragleave', onout) + .on('dragover', onover) + .on('mouseover', onover) + .on('mouseout', onout); + + function onover() { + overQuickView = !!isQuick; + area.addClass('dragover'); + } + function onout() { + overQuickView = false; + area.removeClass('dragover'); + } + } + function findMatchingShp(filename) { // use case-insensitive matching var base = internal.getPathBase(filename).toLowerCase(); @@ -231,13 +248,9 @@ export function ImportControl(gui, opts) { }); } - function receiveFilesQuickView(files) { - useQuickView = true; - receiveFiles(files); - } - function receiveFiles(files) { var prevSize = queuedFiles.length; + useQuickView = overQuickView; files = handleZipFiles(utils.toArray(files)); addFilesToQueue(files); if (queuedFiles.length === 0) return; From 9052b692ec9b3797f7219779a16e89ee6fbf07dc Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 28 Nov 2021 00:55:22 -0500 Subject: [PATCH 114/891] Splash screen text --- www/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/index.html b/www/index.html index f3c841a30..edfb6b1af 100644 --- a/www/index.html +++ b/www/index.html @@ -210,7 +210,7 @@

Mapshaper is an editor for map data

-

Drop files here or select from a folder

+

Drop or paste files here or select from a folder

Shapefile, GeoJSON, TopoJSON, DBF and CSV files are supported
Files can be loose or in a zip archive
@@ -218,7 +218,7 @@

Drop files here or

Quick import

-
Drop files here to import with default settings
+
Drop or paste files here to import with default settings
From 05a3bb1396e0f6e6dfd04bc6f0a617a16c472b7b Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 28 Nov 2021 00:56:15 -0500 Subject: [PATCH 115/891] Fix dependencies --- src/gui/gui-export-control.js | 4 ++-- src/gui/gui-symbol-dragging2.js | 27 +++------------------------ src/mapshaper-internal.js | 4 +++- src/paths/mapshaper-vertex-utils.js | 26 ++++++++++++++++++++++++++ 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/gui/gui-export-control.js b/src/gui/gui-export-control.js index 4feca7951..38adce65f 100644 --- a/src/gui/gui-export-control.js +++ b/src/gui/gui-export-control.js @@ -4,7 +4,7 @@ import { sortLayersForMenuDisplay, cleanLayerName, formatLayerNameForDisplay } f import { El } from './gui-el'; import { GUI } from './gui-lib'; import { ClickText2 } from './gui-elements'; -import { groupLayersByDataset } from '../dataset/mapshaper-target-utils'; +// import { groupLayersByDataset } from '../dataset/mapshaper-target-utils'; // Export buttons and their behavior export var ExportControl = function(gui) { @@ -45,7 +45,7 @@ export var ExportControl = function(gui) { var targets = layersArr.reduce(function(memo, o) { return o.checkbox.checked ? memo.concat(o.target) : memo; }, []); - return groupLayersByDataset(targets); + return internal.groupLayersByDataset(targets); } function onExportClick() { diff --git a/src/gui/gui-symbol-dragging2.js b/src/gui/gui-symbol-dragging2.js index fc823027e..4d4f67150 100644 --- a/src/gui/gui-symbol-dragging2.js +++ b/src/gui/gui-symbol-dragging2.js @@ -2,7 +2,6 @@ import { getSvgSymbolTransform } from './gui-svg-symbols'; import { isMultilineLabel, toggleTextAlign, setMultilineAttribute, autoUpdateTextAnchor, applyDelta } from './gui-svg-labels'; import { error, internal } from './gui-core'; import { EventDispatcher } from './gui-events'; -import { findNearestVertex, findVertexIds, getVertexCoords, setVertexCoords, vertexIsArcStart, vertexIsArcEnd } from '../paths/mapshaper-vertex-utils'; function getDisplayCoordsById(id, layer, ext) { var coords = getPointCoordsById(id, layer); @@ -162,38 +161,18 @@ export function SymbolDragging2(gui, ext, hit) { var target = hit.getHitTarget(); var p = ext.translatePixelCoords(e.x, e.y); if (!activeVertexIds) { - var p2 = findNearestVertex(p[0], p[1], target.layer.shapes[e.id], target.arcs); - activeVertexIds = findVertexIds(p2.x, p2.y, target.arcs); + activeVertexIds = internal.findNearestVertices(p, target.layer.shapes[e.id], target.arcs); } if (!activeVertexIds) return; // ignore error condition if (gui.keyboard.shiftIsPressed()) { - snapEndpointCoords(p, target.arcs); + internal.snapPointToArcEndpoint(p, activeVertexIds, target.arcs); } activeVertexIds.forEach(function(idx) { - setVertexCoords(p[0], p[1], idx, target.arcs); + internal.setVertexCoords(p[0], p[1], idx, target.arcs); }); self.dispatchEvent('location_change'); // signal map to redraw } - function snapEndpointCoords(p, arcs) { - var p2, p3, dx, dy; - activeVertexIds.forEach(function(idx) { - if (vertexIsArcStart(idx, arcs)) { - p2 = getVertexCoords(idx + 1, arcs); - } else if (vertexIsArcEnd(idx, arcs)) { - p2 = getVertexCoords(idx - 1, arcs); - } - }); - if (!p2) return; - dx = p2[0] - p[0]; - dy = p2[1] - p[1]; - if (Math.abs(dx) > Math.abs(dy)) { - p[1] = p2[1]; // snap y coord - } else { - p[0] = p2[0]; - } - } - function onLocationDragEnd(e) { triggerGlobalEvent('symbol_dragend', e); } diff --git a/src/mapshaper-internal.js b/src/mapshaper-internal.js index a3a4c65f0..2f7f59e06 100644 --- a/src/mapshaper-internal.js +++ b/src/mapshaper-internal.js @@ -171,6 +171,7 @@ import * as TopojsonImport from './topojson/topojson-import'; import * as Topology from './topology/mapshaper-topology'; import * as Units from './geom/mapshaper-units'; import * as SvgHatch from './svg/svg-hatch'; +import * as VertexUtils from './paths/mapshaper-vertex-utils'; Object.assign(internal, AnchorPoints, @@ -279,5 +280,6 @@ Object.assign(internal, TopojsonImport, Topology, Units, - SvgHatch + SvgHatch, + VertexUtils ); diff --git a/src/paths/mapshaper-vertex-utils.js b/src/paths/mapshaper-vertex-utils.js index f0f5e97ed..366e1028f 100644 --- a/src/paths/mapshaper-vertex-utils.js +++ b/src/paths/mapshaper-vertex-utils.js @@ -1,5 +1,31 @@ import geom from '../geom/mapshaper-geom'; +export function findNearestVertices(p, shp, arcs) { + var p2 = findNearestVertex(p[0], p[1], shp, arcs); + return findVertexIds(p2.x, p2.y, arcs); +} + +// p: point to snap +// ids: ids of nearby vertices, possibly including an arc endpoint +export function snapPointToArcEndpoint(p, ids, arcs) { + var p2, p3, dx, dy; + ids.forEach(function(idx) { + if (vertexIsArcStart(idx, arcs)) { + p2 = getVertexCoords(idx + 1, arcs); + } else if (vertexIsArcEnd(idx, arcs)) { + p2 = getVertexCoords(idx - 1, arcs); + } + }); + if (!p2) return; + dx = p2[0] - p[0]; + dy = p2[1] - p[1]; + if (Math.abs(dx) > Math.abs(dy)) { + p[1] = p2[1]; // snap y coord + } else { + p[0] = p2[0]; + } +} + // Find ids of vertices with identical coordinates to x,y in an ArcCollection // Caveat: does not exclude vertices that are not visible at the // current level of simplification. From 9ae9e9e64cbc35d2f8baa3f981e3ef867b73690e Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 28 Nov 2021 22:06:28 -0500 Subject: [PATCH 116/891] v0.5.75 --- CHANGELOG.md | 3 +++ package-lock.json | 2 +- package.json | 2 +- www/page.css | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30dc0b4c7..550b104cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.75 +* Added support for importing data by copy-pasting files onto the web UI (works in Chrome and Safari but not Firefox). + v0.5.74 * Added support for importing data in the GUI by pasting JSON and delimited text onto the browser window. * Sped up drawing shapes in the GUI. diff --git a/package-lock.json b/package-lock.json index 030199dde..ffad39f89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.74", + "version": "0.5.75", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c6a7a938f..9f437cba4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.74", + "version": "0.5.75", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/www/page.css b/www/page.css index 610493f8a..9d4c76ca9 100644 --- a/www/page.css +++ b/www/page.css @@ -32,7 +32,7 @@ html, body { body { overflow: hidden; background-color: #fff; - font: 14px/1.4 'Source Sans Pro', Helvetica, sans-serif; + font: 14px/1.4 'Source Sans Pro', Arial, sans-serif; color: #444; user-select: none; -webkit-user-select: none; From 0f8ae1e48eb82ee2c92ba3cf585072e07644a680 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 30 Nov 2021 18:34:50 -0500 Subject: [PATCH 117/891] Add undocumented -split-lines command --- src/buffer/mapshaper-path-buffer2.js | 2 +- src/cli/mapshaper-options.js | 17 ++++ src/cli/mapshaper-run-command.js | 4 + src/commands/mapshaper-points.js | 6 +- src/commands/mapshaper-split-lines.js | 116 ++++++++++++++++++++++ src/expressions/mapshaper-each-geojson.js | 1 + src/geom/mapshaper-geodesic.js | 43 ++++---- src/geom/mapshaper-rounding.js | 8 +- src/geom/mapshaper-segment-geom.js | 20 ++++ src/gui/dom-utils.js | 1 + src/gui/gui-import-control.js | 2 +- src/svg/svg-common.js | 4 +- test/geodesic-test.js | 18 ---- test/segment-geom-test.js | 24 ++++- 14 files changed, 215 insertions(+), 51 deletions(-) create mode 100644 src/commands/mapshaper-split-lines.js diff --git a/src/buffer/mapshaper-path-buffer2.js b/src/buffer/mapshaper-path-buffer2.js index ff28545fe..5ee802347 100644 --- a/src/buffer/mapshaper-path-buffer2.js +++ b/src/buffer/mapshaper-path-buffer2.js @@ -1,5 +1,5 @@ import { testSegmentBoundsIntersection } from '../geom/mapshaper-bounds-geom'; -import { segmentTurn } from '../geom/mapshaper-geodesic'; +import { segmentTurn } from '../geom/mapshaper-segment-geom'; import { bufferIntersection } from '../buffer/mapshaper-path-buffer'; import { reversePath } from '../paths/mapshaper-path-utils'; import geom from '../geom/mapshaper-geom'; diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 53b839b3b..9334e668e 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1395,6 +1395,23 @@ export function getOptionParser() { .option('target', targetOpt) .option('no-replace', noReplaceOpt); + parser.command('split-lines') + // .describe('divide lines into sections') + .option('dash-length', { + type: 'distance', + describe: 'length of split-apart lines' + }) + .option('gap-length', { + type: 'distance', + describe: 'length of gap between segments' + }) + .option('planar', { + type: 'flag', + describe: 'use planar geometry' + }) + .option('where', whereOpt) + .option('target', targetOpt); + parser.command('split-on-grid') .describe('split features into separate layers using a grid') .validate(V.validateGridOpts) diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index 2e95ad211..2db473955 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -68,6 +68,7 @@ import '../commands/mapshaper-simplify'; import '../commands/mapshaper-sort'; import '../commands/mapshaper-snap'; import '../commands/mapshaper-split'; +import '../commands/mapshaper-split-lines'; import '../commands/mapshaper-svg-style'; import '../commands/mapshaper-symbols'; import '../commands/mapshaper-target'; @@ -383,6 +384,9 @@ export function runCommand(command, catalog, cb) { } else if (name == 'split') { outputLayers = applyCommandToEachLayer(cmd.splitLayer, targetLayers, opts.expression, opts); + } else if (name == 'split-lines') { + applyCommandToEachLayer(cmd.splitLines, targetLayers, targetDataset, opts); + } else if (name == 'split-on-grid') { outputLayers = applyCommandToEachLayer(cmd.splitLayerOnGrid, targetLayers, arcs, opts); diff --git a/src/commands/mapshaper-points.js b/src/commands/mapshaper-points.js index b8c702d97..d3e86c952 100644 --- a/src/commands/mapshaper-points.js +++ b/src/commands/mapshaper-points.js @@ -12,6 +12,7 @@ import utils from '../utils/mapshaper-utils'; import geom from '../geom/mapshaper-geom'; import { stop, message } from '../utils/mapshaper-logging'; import { findSegmentIntersections } from '../paths/mapshaper-segment-intersection'; +import { interpolatePoint2D } from '../geom/mapshaper-geodesic'; cmd.createPointLayer = function(srcLyr, dataset, opts) { var destLyr = getOutputLayer(srcLyr, opts); @@ -76,11 +77,6 @@ function testIntersections(arcs) { }); } -function interpolatePoint2D(ax, ay, bx, by, k) { - var j = 1 - k; - return [ax * j + bx * k, ay * j + by * k]; -} - function interpolatePointsAlongArc(ids, arcs, interval) { var iter = arcs.getShapeIter(ids); var distance = arcs.isPlanar() ? geom.distance2D : geom.greatCircleDistance; diff --git a/src/commands/mapshaper-split-lines.js b/src/commands/mapshaper-split-lines.js new file mode 100644 index 000000000..76171368f --- /dev/null +++ b/src/commands/mapshaper-split-lines.js @@ -0,0 +1,116 @@ + +import cmd from '../mapshaper-cmd'; +import { stop, error } from '../utils/mapshaper-logging'; +import { convertDistanceParam } from '../geom/mapshaper-units'; +import { isLatLngCRS , getDatasetCRS } from '../crs/mapshaper-projections'; +import { requirePolylineLayer, getFeatureCount } from '../dataset/mapshaper-layer-utils'; +import { getFeatureEditor } from '../expressions/mapshaper-each-geojson'; +import { compileFeatureExpression, compileValueExpression } from '../expressions/mapshaper-expressions'; +import { replaceLayerContents } from '../dataset/mapshaper-dataset-utils'; +import { pointSegDistSq2, greatCircleDistance, distance2D } from '../geom/mapshaper-basic-geom'; +import { getInterpolationFunction } from '../geom/mapshaper-geodesic'; + +cmd.splitLines = function(lyr, dataset, opts) { + var crs = getDatasetCRS(dataset); + requirePolylineLayer(lyr); + var splitFeature = getSplitFeatureFunction(crs, opts); + + // TODO: remove duplication with mapshaper-each.js + var editor = getFeatureEditor(lyr, dataset); + var exprOpts = { + geojson_editor: editor, + context: {splitFeature} + }; + var exp = `this.geojson = splitFeature(this.geojson)`; + + var compiled = compileFeatureExpression(exp, lyr, dataset.arcs, exprOpts); + var n = getFeatureCount(lyr); + var filter; + if (opts && opts.where) { + filter = compileValueExpression(opts.where, lyr, dataset.arcs); + } + for (var i=0; i 0 === false) { + stop('Missing required segment-length parameter'); + } + if (gapLen >= 0 == false) { + stop('Invalid gap-length option'); + } + var splitLine = getSplitLineFunction(crs, dashLen, gapLen, !!opts.planar); + return function(feat) { + var geom = feat.geometry; + if (!geom) return feat; + if (geom.type == 'LineString') { + geom.type = 'MultiLineString'; + geom.coordinates = [geom.coordinates]; + } + if (geom.type != 'MultiLineString') { + error('Unexpected geometry:', geom.type); + } + geom.coordinates = geom.coordinates.reduce(function(memo, coords) { + try { + var parts = splitLine(coords); + memo = memo.concat(parts); + } catch(e) { + console.error(e); + throw e; + } + return memo; + }, []); + + return feat; + }; +} + +function getSplitLineFunction(crs, dashLen, gapLen, planar) { + var interpolate = getInterpolationFunction(planar ? null : crs); + var distance = isLatLngCRS(crs) ? greatCircleDistance : distance2D; + var inDash, parts2, interval; + function addPart(coords) { + if (inDash) parts2.push(coords); + if (gapLen > 0) { + inDash = !inDash; + interval = inDash ? dashLen : gapLen; + } + } + return function splitLineString(coords) { + var elapsedDist = 0; + var p = coords[0]; + var coords2 = [p]; + var segLen, k, prev; + // init this LineString + inDash = true; + parts2 = []; + interval = gapLen; + for (var i=1, n=coords.length; i= interval) { + k = (interval - elapsedDist) / segLen; + prev = interpolate(prev[0], prev[1], p[0], p[1], k); + elapsedDist = 0; + coords2.push(prev); + addPart(coords2); + coords2 = [prev]; + segLen = distance(prev[0], prev[1], p[0], p[1]); + } + coords2.push(p); + elapsedDist += segLen; + } + if (elapsedDist > 0 && coords2.length > 1) { + addPart(coords2); + } + return parts2; + }; +} diff --git a/src/expressions/mapshaper-each-geojson.js b/src/expressions/mapshaper-each-geojson.js index a25b434bf..f4b3126ee 100644 --- a/src/expressions/mapshaper-each-geojson.js +++ b/src/expressions/mapshaper-each-geojson.js @@ -36,6 +36,7 @@ export function getFeatureEditor(lyr, dataset) { type: 'FeatureCollection', features: features }; + // console.log(JSON.stringify(geojson, null, 2)) return importGeoJSON(geojson); }; diff --git a/src/geom/mapshaper-geodesic.js b/src/geom/mapshaper-geodesic.js index 89551428f..be7d769c8 100644 --- a/src/geom/mapshaper-geodesic.js +++ b/src/geom/mapshaper-geodesic.js @@ -2,6 +2,9 @@ import { isLatLngCRS, getDatasetCRS } from '../crs/mapshaper-projections'; import { error } from '../utils/mapshaper-logging'; import geom from '../geom/mapshaper-geom'; +// GeographicLib docs: https://geographiclib.sourceforge.io/html/js/ +// https://geographiclib.sourceforge.io/html/js/module-GeographicLib_Geodesic.Geodesic.html +// https://geographiclib.sourceforge.io/html/js/tutorial-2-interface.html function getGeodesic(P) { if (!isLatLngCRS(P)) error('Expected an unprojected CRS'); var f = P.es / (1 + Math.sqrt(P.one_es)); @@ -9,6 +12,23 @@ function getGeodesic(P) { return new GeographicLib.Geodesic.Geodesic(P.a, f); } +export function interpolatePoint2D(ax, ay, bx, by, k) { + var j = 1 - k; + return [ax * j + bx * k, ay * j + by * k]; +} + +export function getInterpolationFunction(P) { + var spherical = P && isLatLngCRS(P); + if (!spherical) return interpolatePoint2D; + var geod = getGeodesic(P); + return function(lng, lat, lng2, lat2, k) { + var r = geod.Inverse(lat, lng, lat2, lng2); + var dist = r.s12 * k; + var r2 = geod.Direct(lat, lng, r.azi1, dist); + return [r2.lon2, r2.lat2]; + }; +} + export function getPlanarSegmentEndpoint(x, y, bearing, meterDist) { var rad = bearing / 180 * Math.PI; var dx = Math.sin(rad) * meterDist; @@ -50,31 +70,12 @@ export function getFastGeodeticSegmentFunction(P) { return isLatLngCRS(P) ? fastGeodeticSegmentFunction : getPlanarSegmentEndpoint; } -// Useful for determining if a segment that intersects another segment is -// entering or leaving an enclosed buffer area -// returns -1 if angle of p1p2 -> p3p4 is counter-clockwise (left turn) -// returns 1 if angle is clockwise -// return 0 if segments are collinear -export function segmentTurn(p1, p2, p3, p4) { - var ax = p1[0], - ay = p1[1], - bx = p2[0], - by = p2[1], - // shift p3p4 segment to start at p2 - dx = bx - p3[0], - dy = by - p3[1], - cx = p4[0] + dx, - cy = p4[1] + dy, - orientation = geom.orient2D(ax, ay, bx, by, cx, cy); - if (!orientation) return 0; - return orientation < 0 ? 1 : -1; -} -function bearingDegrees(a, b, c, d) { +export function bearingDegrees(a, b, c, d) { return geom.bearing(a, b, c, d) * 180 / Math.PI; } -function bearingDegrees2D(a, b, c, d) { +export function bearingDegrees2D(a, b, c, d) { return geom.bearing2D(a, b, c, d) * 180 / Math.PI; } diff --git a/src/geom/mapshaper-rounding.js b/src/geom/mapshaper-rounding.js index 0143d1433..0aae69a6b 100644 --- a/src/geom/mapshaper-rounding.js +++ b/src/geom/mapshaper-rounding.js @@ -8,10 +8,14 @@ export function roundToSignificantDigits(n, d) { } export function roundToDigits(n, d) { - return +n.toFixed(d); + return +n.toFixed(d); // string conversion makes this slow } -// inc: Rounding incrememnt (e.g. 0.001 rounds to thousandths) +export function roundToTenths(n) { + return (Math.round(n * 10)) / 10; +} + +// inc: Rounding increment (e.g. 0.001 rounds to thousandths) export function getRoundingFunction(inc) { if (!utils.isNumber(inc) || inc === 0) { error("Rounding increment must be a non-zero number."); diff --git a/src/geom/mapshaper-segment-geom.js b/src/geom/mapshaper-segment-geom.js index dcca2c60f..d0d84ade6 100644 --- a/src/geom/mapshaper-segment-geom.js +++ b/src/geom/mapshaper-segment-geom.js @@ -227,3 +227,23 @@ export function segmentHit(ax, ay, bx, by, cx, cy, dx, dy) { orient2D(cx, cy, dx, dy, ax, ay) * orient2D(cx, cy, dx, dy, bx, by) <= 0; } + +// Useful for determining if a segment that intersects another segment is +// entering or leaving an enclosed buffer area +// returns -1 if angle of p1p2 -> p3p4 is counter-clockwise (left turn) +// returns 1 if angle is clockwise +// return 0 if segments are collinear +export function segmentTurn(p1, p2, p3, p4) { + var ax = p1[0], + ay = p1[1], + bx = p2[0], + by = p2[1], + // shift p3p4 segment to start at p2 + dx = bx - p3[0], + dy = by - p3[1], + cx = p4[0] + dx, + cy = p4[1] + dy, + orientation = orient2D(ax, ay, bx, by, cx, cy); + if (!orientation) return 0; + return orientation < 0 ? 1 : -1; +} diff --git a/src/gui/dom-utils.js b/src/gui/dom-utils.js index 6ccb20b73..50dc83c96 100644 --- a/src/gui/dom-utils.js +++ b/src/gui/dom-utils.js @@ -92,6 +92,7 @@ export function mergeCSS(s1, s2) { } export function addCSS(el, css) { + // console.error(css); el.style.cssText = mergeCSS(el.style.cssText, css); } diff --git a/src/gui/gui-import-control.js b/src/gui/gui-import-control.js index f016e6b24..acf2e7923 100644 --- a/src/gui/gui-import-control.js +++ b/src/gui/gui-import-control.js @@ -250,7 +250,7 @@ export function ImportControl(gui, opts) { function receiveFiles(files) { var prevSize = queuedFiles.length; - useQuickView = overQuickView; + useQuickView = useQuickView || overQuickView; files = handleZipFiles(utils.toArray(files)); addFilesToQueue(files); if (queuedFiles.length === 0) return; diff --git a/src/svg/svg-common.js b/src/svg/svg-common.js index 08291c2fb..3df198230 100644 --- a/src/svg/svg-common.js +++ b/src/svg/svg-common.js @@ -1,9 +1,9 @@ - +import { roundToTenths } from '../geom/mapshaper-rounding'; export var symbolBuilders = {}; export var symbolRenderers = {}; export function getTransform(xy, scale) { - var str = 'translate(' + xy[0] + ' ' + xy[1] + ')'; + var str = 'translate(' + roundToTenths(xy[0]) + ' ' + roundToTenths(xy[1]) + ')'; if (scale && scale != 1) { str += ' scale(' + scale + ')'; } diff --git a/test/geodesic-test.js b/test/geodesic-test.js index e3c378a7b..11876e897 100644 --- a/test/geodesic-test.js +++ b/test/geodesic-test.js @@ -3,23 +3,5 @@ var assert = require('assert'), internal = api.internal; describe('mapshaper-geodesic.js', function () { - describe('segmentTurn()', function () { - it('left turn', function () { - var pp = [[0, 0], [0, 1], [2, 2], [1, 2]]; - assert.equal(internal.segmentTurn.apply(null, pp), -1); - }) - it('right turn', function () { - var pp = [[0, 0], [0, 1], [1, 2], [2, 2]]; - assert.equal(internal.segmentTurn.apply(null, pp), 1); - }) - it('collinear', function () { - var pp = [[0, 0], [0, 1], [2, 2], [2, 3]]; - assert.equal(internal.segmentTurn.apply(null, pp), 0); - }) - it('collinear 2', function () { - var pp = [[0, 0], [0, 1], [2, 2], [2, 1]]; - assert.equal(internal.segmentTurn.apply(null, pp), 0); - }) - }) }) diff --git a/test/segment-geom-test.js b/test/segment-geom-test.js index 925900623..9d9297ff6 100644 --- a/test/segment-geom-test.js +++ b/test/segment-geom-test.js @@ -1,10 +1,32 @@ var api = require('../'), + internal = api.internal, assert = require('assert'), geom = api.geom, - ArcCollection = api.internal.ArcCollection; + ArcCollection = internal.ArcCollection; describe('mapshaper-segment-geom.js', function () { + describe('segmentTurn()', function () { + + it('left turn', function () { + var pp = [[0, 0], [0, 1], [2, 2], [1, 2]]; + assert.equal(geom.segmentTurn.apply(null, pp), -1); + }) + it('right turn', function () { + var pp = [[0, 0], [0, 1], [1, 2], [2, 2]]; + assert.equal(geom.segmentTurn.apply(null, pp), 1); + }) + it('collinear', function () { + var pp = [[0, 0], [0, 1], [2, 2], [2, 3]]; + assert.equal(geom.segmentTurn.apply(null, pp), 0); + }) + it('collinear 2', function () { + var pp = [[0, 0], [0, 1], [2, 2], [2, 1]]; + assert.equal(geom.segmentTurn.apply(null, pp), 0); + }) + }) + + describe('Tests based on real data', function () { return; // REMOVING THIS TEST --- no longer detecting both T-intersections and crosses From 9e38b44981b16ed94979155d775a022b61ac62c4 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 30 Nov 2021 18:37:08 -0500 Subject: [PATCH 118/891] v0.5.76 --- CHANGELOG.md | 4 ++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 550b104cd..2b504d091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.76 +* Fixed bug in mapshaper-gui -q. +* Added undocumented -split-lines command. + v0.5.75 * Added support for importing data by copy-pasting files onto the web UI (works in Chrome and Safari but not Firefox). diff --git a/package-lock.json b/package-lock.json index ffad39f89..065b95442 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.75", + "version": "0.5.76", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9f437cba4..d7be4fdce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.75", + "version": "0.5.76", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From e6945c7350e33108a90bb71dec0cb5c8f20d21e4 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 30 Nov 2021 21:50:58 -0500 Subject: [PATCH 119/891] Text updates --- src/cli/mapshaper-options.js | 4 ++-- src/commands/mapshaper-split-lines.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 9334e668e..5ffd7e7e2 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1399,11 +1399,11 @@ export function getOptionParser() { // .describe('divide lines into sections') .option('dash-length', { type: 'distance', - describe: 'length of split-apart lines' + describe: 'length of split-apart lines (e.g. 200km)' }) .option('gap-length', { type: 'distance', - describe: 'length of gap between segments' + describe: 'length of gaps between dashes (default is 0)' }) .option('planar', { type: 'flag', diff --git a/src/commands/mapshaper-split-lines.js b/src/commands/mapshaper-split-lines.js index 76171368f..932afbabf 100644 --- a/src/commands/mapshaper-split-lines.js +++ b/src/commands/mapshaper-split-lines.js @@ -41,7 +41,7 @@ function getSplitFeatureFunction(crs, opts) { var dashLen = opts.dash_length ? convertDistanceParam(opts.dash_length, crs) : 0; var gapLen = opts.gap_length ? convertDistanceParam(opts.gap_length, crs) : 0; if (dashLen > 0 === false) { - stop('Missing required segment-length parameter'); + stop('Missing required dash-length parameter'); } if (gapLen >= 0 == false) { stop('Invalid gap-length option'); From 30938b7e6542aa105f47ae8cf1c8627647093b60 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 2 Dec 2021 19:00:14 -0500 Subject: [PATCH 120/891] Prevent MacOs firewall prompts --- bin/mapshaper-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/mapshaper-gui b/bin/mapshaper-gui index 662ceccf0..299d685de 100755 --- a/bin/mapshaper-gui +++ b/bin/mapshaper-gui @@ -83,7 +83,7 @@ function startServer(port) { } serveFile(getAssetFilePath(uri), response); } - }).listen(port, function() { + }).listen(port, 'localhost', function() { opn("http://localhost:" + port); }); } From 0854c91de39cdd0ac114a1a9b8f6f2dbac048c32 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 2 Dec 2021 21:42:27 -0500 Subject: [PATCH 121/891] Add polygon-to-polyline and polyline-to-polygon joins --- src/commands/mapshaper-join.js | 17 +++++++++- src/join/mapshaper-point-polygon-join.js | 1 - src/join/mapshaper-polyline-polygon-join.js | 35 +++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 src/join/mapshaper-polyline-polygon-join.js diff --git a/src/commands/mapshaper-join.js b/src/commands/mapshaper-join.js index 20e62100f..7e6e3adff 100644 --- a/src/commands/mapshaper-join.js +++ b/src/commands/mapshaper-join.js @@ -1,6 +1,7 @@ import { getColumnType } from '../datatable/mapshaper-data-utils'; import { requireDataField } from '../dataset/mapshaper-layer-utils'; import { joinPolygonsToPolygons } from '../join/mapshaper-polygon-polygon-join'; +import { joinPolylinesToPolygons, joinPolygonsToPolylines } from '../join/mapshaper-polyline-polygon-join'; import { message, stop } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import cmd from '../mapshaper-cmd'; @@ -8,10 +9,11 @@ import { joinTableToLayer, validateFieldNames } from '../join/mapshaper-join-tab import { joinPointsToPolygons, joinPolygonsToPoints } from '../join/mapshaper-point-polygon-join'; import { joinPointsToPoints } from '../join/mapshaper-point-point-join'; import { requireDatasetsHaveCompatibleCRS, getDatasetCRS } from '../crs/mapshaper-projections'; +import { initDataTable } from '../dataset/mapshaper-layer-utils'; cmd.join = function(targetLyr, targetDataset, src, opts) { var srcType, targetType, retn; - if (!src || !src.layer.data || !src.dataset) { + if (!src || !src.dataset) { stop("Missing a joinable data source"); } if (opts.keys) { @@ -19,9 +21,18 @@ cmd.join = function(targetLyr, targetDataset, src, opts) { if (opts.keys.length != 2) { stop("Expected two key fields: a target field and a source field"); } + if (!src.layer.data) { + stop("Source layer is missing attribute data"); + } retn = joinAttributesToFeatures(targetLyr, src.layer.data, opts); } else { // spatial join + if (!src.layer.data) { + // KLUDGE -- users might want to join a layer without attributes + // to test for intersection... the simplest way to support this is + // to add an empty data table to the source layer + initDataTable(src.layer); + } requireDatasetsHaveCompatibleCRS([targetDataset, src.dataset]); srcType = src.layer.geometry_type; targetType = targetLyr.geometry_type; @@ -33,6 +44,10 @@ cmd.join = function(targetLyr, targetDataset, src, opts) { retn = joinPointsToPoints(targetLyr, src.layer, getDatasetCRS(targetDataset), opts); } else if (srcType == 'polygon' && targetType == 'polygon') { retn = joinPolygonsToPolygons(targetLyr, targetDataset, src, opts); + } else if (srcType == 'polyline' && targetType == 'polygon') { + retn = joinPolylinesToPolygons(targetLyr, targetDataset, src, opts); + } else if (srcType == 'polygon' && targetType == 'polyline') { + retn = joinPolygonsToPolylines(targetLyr, targetDataset, src, opts); } else { stop(utils.format("Unable to join %s geometry to %s geometry", srcType || 'null', targetType || 'null')); diff --git a/src/join/mapshaper-point-polygon-join.js b/src/join/mapshaper-point-polygon-join.js index 841d56b49..fafe35c16 100644 --- a/src/join/mapshaper-point-polygon-join.js +++ b/src/join/mapshaper-point-polygon-join.js @@ -16,7 +16,6 @@ export function joinPolygonsToPoints(targetLyr, polygonLyr, arcs, opts) { return joinTableToLayer(targetLyr, polygonLyr.data, joinFunction, opts); } - export function prepJoinLayers(targetLyr, srcLyr) { if (!targetLyr.data) { // create an empty data table if target layer is missing attributes diff --git a/src/join/mapshaper-polyline-polygon-join.js b/src/join/mapshaper-polyline-polygon-join.js new file mode 100644 index 000000000..953956739 --- /dev/null +++ b/src/join/mapshaper-polyline-polygon-join.js @@ -0,0 +1,35 @@ +import { polylineToMidpoints } from '../paths/mapshaper-polyline-to-point'; +import { joinPolygonsToPoints, joinPointsToPolygons } from '../join/mapshaper-point-polygon-join'; +import { stop } from '../utils/mapshaper-logging'; + +function pointsFromPolylinesForJoin(lyr, dataset) { + var shapes = lyr.shapes.map(function(shp) { + return polylineToMidpoints(shp, dataset.arcs); + }); + return { + geometry_type: 'point', + shapes: shapes, + data: lyr.data // TODO copy if needed + }; +} + +function validateOpts(opts) { + if (!opts.point_method) { + stop('The "point-method" flag is required for polyline-polygon joins'); + } +} + +export function joinPolylinesToPolygons(targetLyr, targetDataset, source, opts) { + validateOpts(opts); + var pointLyr = pointsFromPolylinesForJoin(source.layer, source.dataset); + var retn = joinPointsToPolygons(targetLyr, targetDataset.arcs, pointLyr, opts); + return retn; +} + +export function joinPolygonsToPolylines(targetLyr, targetDataset, source, opts) { + validateOpts(opts); + var pointLyr = pointsFromPolylinesForJoin(targetLyr, targetDataset); + var retn = joinPolygonsToPoints(pointLyr, source.layer, source.dataset.arcs, opts); + targetLyr.data = pointLyr.data; + return retn; +} From fb9b3079049a0afd29000af451c9521bdd92519b Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 2 Dec 2021 21:55:46 -0500 Subject: [PATCH 122/891] Add test --- test/join-polygons-to-polylines-test.js | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 test/join-polygons-to-polylines-test.js diff --git a/test/join-polygons-to-polylines-test.js b/test/join-polygons-to-polylines-test.js new file mode 100644 index 000000000..091cb9cf1 --- /dev/null +++ b/test/join-polygons-to-polylines-test.js @@ -0,0 +1,41 @@ +var api = require('../'), + assert = require('assert'); + +describe('Polygons to polylines spatial joins', function () { + + var lines = { + type: 'GeometryCollection', + geometries: [{ + type: 'LineString', + coordinates: [[0, 0], [1, 1], [2, 2]] + }, { + type: 'LineString', + coordinates: [[3, 3], [4, 4]] + }] + } + + var polygon = { + type: 'Polygon', + coordinates: [[[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]] + }; + + it ('polyline to polygon', function(done) { + var cmd = '-i targ.json -join point-method src.json calc="n = count()" -o'; + api.applyCommands(cmd, {'targ.json': polygon, 'src.json': lines}, function(err, out) { + var json = JSON.parse(out['targ.json']); + var expect = {n: 1}; + assert.deepEqual(json.features[0].properties, expect) + done(); + }) + }); + + it ('polygon to polyline', function(done) { + var cmd = '-i targ.json -join point-method src.json calc="n = count()" -o'; + api.applyCommands(cmd, {'targ.json': lines, 'src.json': polygon}, function(err, out) { + var json = JSON.parse(out['targ.json']); + assert.deepEqual(json.features[0].properties, {n: 1}) + assert.deepEqual(json.features[1].properties, {n: 0}) + done(); + }) + }); +}) From bcab3994155f279a75a3f455e56d92e421b39631 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 2 Dec 2021 21:57:38 -0500 Subject: [PATCH 123/891] Add -dashlines command --- src/cli/mapshaper-options.js | 39 ++--- src/cli/mapshaper-run-command.js | 8 +- ...-split-lines.js => mapshaper-dashlines.js} | 102 ++++++++----- src/gui/gui-console.js | 1 + test/dashlines-test.js | 135 ++++++++++++++++++ 5 files changed, 228 insertions(+), 57 deletions(-) rename src/commands/{mapshaper-split-lines.js => mapshaper-dashlines.js} (54%) create mode 100644 test/dashlines-test.js diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 5ffd7e7e2..01730afa3 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -624,6 +624,28 @@ export function getOptionParser() { '$ mapshaper data.json -colorizer name=getColor nodata=#eee breaks=20,40 \\\n' + ' colors=#e0f3db,#a8ddb5,#43a2ca -each \'fill = getColor(RATING)\' -o output.json'); + parser.command('dashlines') + .describe('split lines into sections, with or without a gap') + .oldAlias('split-lines') + .option('dash-length', { + type: 'distance', + describe: 'length of split-apart lines (e.g. 200km)' + }) + .option('gap-length', { + type: 'distance', + describe: 'length of gaps between dashes (default is 0)' + }) + .option('scaled', { + type: 'flag', + describe: 'scale dashes and gaps to prevent partial dashes' + }) + .option('planar', { + type: 'flag', + describe: 'use planar geometry' + }) + .option('where', whereOpt) + .option('target', targetOpt); + parser.command('define') // .describe('define expression variables') .option('expression', { @@ -1395,23 +1417,6 @@ export function getOptionParser() { .option('target', targetOpt) .option('no-replace', noReplaceOpt); - parser.command('split-lines') - // .describe('divide lines into sections') - .option('dash-length', { - type: 'distance', - describe: 'length of split-apart lines (e.g. 200km)' - }) - .option('gap-length', { - type: 'distance', - describe: 'length of gaps between dashes (default is 0)' - }) - .option('planar', { - type: 'flag', - describe: 'use planar geometry' - }) - .option('where', whereOpt) - .option('target', targetOpt); - parser.command('split-on-grid') .describe('split features into separate layers using a grid') .validate(V.validateGridOpts) diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index 2db473955..01a8f935a 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -22,6 +22,7 @@ import '../commands/mapshaper-clean'; import '../commands/mapshaper-clip-erase'; import '../commands/mapshaper-cluster'; import '../commands/mapshaper-colorizer'; +import '../commands/mapshaper-dashlines'; import '../commands/mapshaper-data-fill'; import '../commands/mapshaper-define'; import '../commands/mapshaper-dissolve'; @@ -68,7 +69,6 @@ import '../commands/mapshaper-simplify'; import '../commands/mapshaper-sort'; import '../commands/mapshaper-snap'; import '../commands/mapshaper-split'; -import '../commands/mapshaper-split-lines'; import '../commands/mapshaper-svg-style'; import '../commands/mapshaper-symbols'; import '../commands/mapshaper-target'; @@ -184,6 +184,9 @@ export function runCommand(command, catalog, cb) { } else if (name == 'colorizer') { outputLayers = cmd.colorizer(opts); + } else if (name == 'dashlines') { + applyCommandToEachLayer(cmd.dashlines, targetLayers, targetDataset, opts); + } else if (name == 'define') { cmd.define(opts); @@ -384,9 +387,6 @@ export function runCommand(command, catalog, cb) { } else if (name == 'split') { outputLayers = applyCommandToEachLayer(cmd.splitLayer, targetLayers, opts.expression, opts); - } else if (name == 'split-lines') { - applyCommandToEachLayer(cmd.splitLines, targetLayers, targetDataset, opts); - } else if (name == 'split-on-grid') { outputLayers = applyCommandToEachLayer(cmd.splitLayerOnGrid, targetLayers, arcs, opts); diff --git a/src/commands/mapshaper-split-lines.js b/src/commands/mapshaper-dashlines.js similarity index 54% rename from src/commands/mapshaper-split-lines.js rename to src/commands/mapshaper-dashlines.js index 932afbabf..61b24f187 100644 --- a/src/commands/mapshaper-split-lines.js +++ b/src/commands/mapshaper-dashlines.js @@ -7,34 +7,18 @@ import { requirePolylineLayer, getFeatureCount } from '../dataset/mapshaper-laye import { getFeatureEditor } from '../expressions/mapshaper-each-geojson'; import { compileFeatureExpression, compileValueExpression } from '../expressions/mapshaper-expressions'; import { replaceLayerContents } from '../dataset/mapshaper-dataset-utils'; -import { pointSegDistSq2, greatCircleDistance, distance2D } from '../geom/mapshaper-basic-geom'; +import { greatCircleDistance, distance2D } from '../geom/mapshaper-basic-geom'; import { getInterpolationFunction } from '../geom/mapshaper-geodesic'; +import { getStateVar } from '../mapshaper-state'; -cmd.splitLines = function(lyr, dataset, opts) { +cmd.dashlines = function(lyr, dataset, opts) { var crs = getDatasetCRS(dataset); - requirePolylineLayer(lyr); - var splitFeature = getSplitFeatureFunction(crs, opts); - - // TODO: remove duplication with mapshaper-each.js - var editor = getFeatureEditor(lyr, dataset); - var exprOpts = { - geojson_editor: editor, - context: {splitFeature} - }; + var defs = getStateVar('defs'); var exp = `this.geojson = splitFeature(this.geojson)`; - - var compiled = compileFeatureExpression(exp, lyr, dataset.arcs, exprOpts); - var n = getFeatureCount(lyr); - var filter; - if (opts && opts.where) { - filter = compileValueExpression(opts.where, lyr, dataset.arcs); - } - for (var i=0; i= 0 == false) { stop('Invalid gap-length option'); } - var splitLine = getSplitLineFunction(crs, dashLen, gapLen, !!opts.planar); + var splitLine = getSplitLineFunction(crs, dashLen, gapLen, opts); return function(feat) { var geom = feat.geometry; if (!geom) return feat; @@ -72,38 +56,62 @@ function getSplitFeatureFunction(crs, opts) { }; } -function getSplitLineFunction(crs, dashLen, gapLen, planar) { +function getSplitLineFunction(crs, dashLen, gapLen, opts) { + var planar = !!opts.planar; var interpolate = getInterpolationFunction(planar ? null : crs); var distance = isLatLngCRS(crs) ? greatCircleDistance : distance2D; - var inDash, parts2, interval; + var inDash, parts2, interval, scale; function addPart(coords) { if (inDash) parts2.push(coords); if (gapLen > 0) { inDash = !inDash; - interval = inDash ? dashLen : gapLen; + interval = scale * (inDash ? dashLen : gapLen); } } + return function splitLineString(coords) { var elapsedDist = 0; var p = coords[0]; var coords2 = [p]; - var segLen, k, prev; + var segLen, pct, prev; + if (opts.scaled) { + scale = scaleDashes(dashLen, gapLen, getLineLength(coords, distance)); + } else { + scale = 1; + } // init this LineString - inDash = true; + inDash = gapLen > 0 ? false : true; + interval = scale * (inDash ? dashLen : gapLen); + if (!inDash) { + // start gapped lines with a half-gap + // (a half-gap or a half-dash is probably better for rings and intersecting lines) + interval *= 0.5; + } parts2 = []; - interval = gapLen; for (var i=1, n=coords.length; i= interval) { - k = (interval - elapsedDist) / segLen; - prev = interpolate(prev[0], prev[1], p[0], p[1], k); - elapsedDist = 0; + // this segment contains a break either within it or at the far endpoint + pct = (interval - elapsedDist) / segLen; + if (pct > 0.999 && i == n - 1) { + // snap to endpoint (so fp rounding errors don't result in a tiny + // last segment) + pct = 1; + } + if (pct < 1) { + prev = interpolate(prev[0], prev[1], p[0], p[1], pct); + } else { + prev = p; + } coords2.push(prev); addPart(coords2); - coords2 = [prev]; - segLen = distance(prev[0], prev[1], p[0], p[1]); + // start a new part + coords2 = pct < 1 ? [prev] : []; + elapsedDist = 0; + segLen = (1 - pct) * segLen; } coords2.push(p); elapsedDist += segLen; @@ -114,3 +122,25 @@ function getSplitLineFunction(crs, dashLen, gapLen, planar) { return parts2; }; } + +function getLineLength(coords, distance) { + var len = 0; + for (var i=1, n=coords.length; i1 + var k2 = len / (n2 * (dash + gap)); // scaled-down dashes <1 + var k = k2; + if (k1 < 1/k2 && n1 > 0) { + k = k1; // pick the smaller of the two scales + } + return k; +} diff --git a/src/gui/gui-console.js b/src/gui/gui-console.js index 7d477cb00..42a3606a5 100644 --- a/src/gui/gui-console.js +++ b/src/gui/gui-console.js @@ -106,6 +106,7 @@ export function Console(gui) { function onPaste(e) { // paste plain text (remove any copied HTML tags) e.preventDefault(); + e.stopPropagation(); // don't try to import pasted text as data (see gui-import-control.js) var str = (e.originalEvent || e).clipboardData.getData('text/plain'); document.execCommand("insertHTML", false, str); } diff --git a/test/dashlines-test.js b/test/dashlines-test.js new file mode 100644 index 000000000..9d6913397 --- /dev/null +++ b/test/dashlines-test.js @@ -0,0 +1,135 @@ +var api = require('../'), + internal = api.internal, + assert = require('assert'); + +describe('mapshaper-dashlines.js', function () { + it ('-split-lines alias works', function(done) { + var data = { + type: 'LineString', + coordinates: [[0, 0], [1000, 0]] + }; + var cmd = '-i data.json -split-lines dash-length=500 -o'; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var json = JSON.parse(out['data.json']); + var expect = { + type: 'MultiLineString', + coordinates: [ + [[0, 0], [500, 0]], [[500, 0], [1000, 0]] + ] + } + assert.deepEqual(json.geometries[0], expect); + done(); + }); + }); + + it ('projected, no gap, scaled', function(done) { + var data = { + type: 'LineString', + coordinates: [[0, 0], [0, 1200]] + }; + var cmd = '-i data.json -dashlines dash-length=302 scaled -o'; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var json = JSON.parse(out['data.json']); + var expect = { + type: 'MultiLineString', + coordinates: [[[0, 0], [0, 300]], [[0, 300], [0, 600]], [[0, 600], [0, 900]], [[0, 900], [0, 1200]]] + }; + assert.deepEqual(json.geometries[0], expect); + done(); + }); + }); + + it ('projected, no gap; avoid tiny segments', function(done) { + var data = { + type: 'LineString', + coordinates: [[0, 0], [1000.1, 0]] + }; + var cmd = '-i data.json -dashlines dash-length=500 -o'; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var json = JSON.parse(out['data.json']); + var expect = { + type: 'MultiLineString', + coordinates: [ + [[0, 0], [500, 0]], [[500, 0], [1000.1, 0]] + ] + } + assert.deepEqual(json.geometries[0], expect); + done(); + }); + }); + + it ('projected, no gap', function(done) { + var data = { + type: 'LineString', + coordinates: [[0, 0], [1000, 0], [1100, 0]] + }; + var cmd = '-i data.json -dashlines dash-length=300 -o'; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var json = JSON.parse(out['data.json']); + var expect = { + type: 'MultiLineString', + coordinates: [ + [[0, 0], [300, 0]], [[300, 0], [600, 0]], [[600, 0], [900, 0]], [[900, 0], [1000, 0], [1100, 0]] + ] + } + assert.deepEqual(json.geometries[0], expect); + done(); + }); + }); + + + it ('projected, gap, scaled down', function(done) { + var data = { + type: 'LineString', + coordinates: [[0, 0], [1000, 0]] + }; + var cmd = '-i data.json -dashlines dash-length=600 gap-length=600 scaled -o'; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var json = JSON.parse(out['data.json']); + var expect = { + type: 'LineString', + coordinates: [[250, 0], [750, 0]] + }; + assert.deepEqual(json.geometries[0], expect); + done(); + }); + }); + + it ('projected, gap, scaled way down', function(done) { + var data = { + type: 'LineString', + coordinates: [[0, 0], [1000, 0]] + }; + var cmd = '-i data.json -dashlines dash-length=2000 gap-length=2000 scaled -o'; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var json = JSON.parse(out['data.json']); + var expect = { + type: 'LineString', + coordinates: [[250, 0], [750, 0]] + }; + assert.deepEqual(json.geometries[0], expect); + done(); + }); + }); + + + it ('projected, gap, scaled up', function(done) { + var data = { + type: 'LineString', + coordinates: [[0, 0], [1000, 0]] + }; + var cmd = '-i data.json -dashlines dash-length=400 gap-length=400 scaled -o'; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var json = JSON.parse(out['data.json']); + var expect = { + type: 'LineString', + coordinates: [[250, 0], [750, 0]] + }; + assert.deepEqual(json.geometries[0], expect); + done(); + }); + }); + + + +}) From 0f465e7e941a1d35b283a28cf30260dc0221f80c Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 2 Dec 2021 21:58:43 -0500 Subject: [PATCH 124/891] v0.5.77 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b504d091..2be01f758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.77 +* Added -dashlines command (formerly -split-lines). +* Added support for joining polyline and polyline layers using point-method + v0.5.76 * Fixed bug in mapshaper-gui -q. * Added undocumented -split-lines command. diff --git a/package.json b/package.json index d7be4fdce..22cc087b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.76", + "version": "0.5.77", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 789403220bac05b4416aca8a22053ccf888cf273 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 2 Dec 2021 21:59:08 -0500 Subject: [PATCH 125/891] v0.5.77 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 065b95442..e7e9d6f07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.76", + "version": "0.5.77", "lockfileVersion": 1, "requires": true, "dependencies": { From 131a5101b198ae1f3cf8a3b3d50205b5ee1cf593 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 7 Dec 2021 20:41:10 -0500 Subject: [PATCH 126/891] Improve GUI console history translation --- src/cli/mapshaper-options.js | 9 ++++++++ src/commands/mapshaper-filter.js | 11 ++++++++++ src/commands/mapshaper-split.js | 36 +++++++++++++++++++++++--------- src/gui/gui-selection-tool.js | 15 +++++++------ test/filter-test.js | 11 ++++++++++ 5 files changed, 64 insertions(+), 18 deletions(-) diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 01730afa3..6e05d654a 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -842,6 +842,10 @@ export function getOptionParser() { .option('keep-shapes', { type: 'flag' }) + .option('ids', { + // describe: 'filter on a list of feature ids', + type: 'numbers' + }) .option('cleanup', {type: 'flag'}) // TODO: document .option('name', nameOpt) .option('target', targetOpt) @@ -1410,6 +1414,11 @@ export function getOptionParser() { DEFAULT: true, describe: 'expression or field for grouping features and naming split layers' }) + .option('ids', { + // used by gui history to split on selected features + // describe: 'split on a list of feature ids', + type: 'numbers' + }) .option('apart', { describe: 'save output layers to independent datasets', type: 'flag' diff --git a/src/commands/mapshaper-filter.js b/src/commands/mapshaper-filter.js index b3eb22d04..909ab545d 100644 --- a/src/commands/mapshaper-filter.js +++ b/src/commands/mapshaper-filter.js @@ -21,6 +21,10 @@ cmd.filterFeatures = function(lyr, arcs, opts) { filter = compileValueExpression(opts.expression, lyr, arcs); } + if (opts.ids) { + filter = combineFilters(filter, getIdFilter(opts.ids)); + } + if (opts.remove_empty) { filter = combineFilters(filter, getNullGeometryFilter(lyr, arcs)); } @@ -79,6 +83,13 @@ export function filterLayerInPlace(lyr, filter, invert) { lyr.data = filteredRecords ? new DataTable(filteredRecords) : null; } +function getIdFilter(ids) { + var set = new Set(ids); + return function(i) { + return set.has(i); + }; +} + function getNullGeometryFilter(lyr, arcs) { var shapes = lyr.shapes; if (lyr.geometry_type == 'polygon') { diff --git a/src/commands/mapshaper-split.js b/src/commands/mapshaper-split.js index 7e8129f77..abe8bf7a6 100644 --- a/src/commands/mapshaper-split.js +++ b/src/commands/mapshaper-split.js @@ -6,13 +6,20 @@ import utils from '../utils/mapshaper-utils'; import { DataTable } from '../datatable/mapshaper-data-table'; // @expression: optional field name or expression // -cmd.splitLayer = function(src, expression, opts) { - var lyr0 = opts && opts.no_replace ? copyLayer(src) : src, +cmd.splitLayer = function(src, expression, optsArg) { + var opts = optsArg || {}, + lyr0 = opts.no_replace ? copyLayer(src) : src, properties = lyr0.data ? lyr0.data.getRecords() : null, shapes = lyr0.shapes, index = {}, splitLayers = [], - namer = getSplitNameFunction(lyr0, expression); + namer; + + if (opts.ids) { + namer = getIdSplitFunction(opts.ids); + } else { + namer = getSplitNameFunction(lyr0, expression); + } // if (splitField) { // internal.requireDataField(lyr0, splitField); @@ -44,15 +51,24 @@ cmd.splitLayer = function(src, expression, opts) { return splitLayers; }; +function getIdSplitFunction(ids) { + var set = new Set(ids); + return function(i) { + return set.has(i) ? '1' : '2'; + }; +} + +function getDefaultSplitFunction(lyr) { + // if not splitting on an expression and layer is unnamed, name split-apart layers + // like: split-1, split-2, ... + return function(i) { + return (lyr && lyr.name || 'split') + '-' + (i + 1); + }; +} + export function getSplitNameFunction(lyr, exp) { var compiled; - if (!exp) { - // if not splitting on an expression and layer is unnamed, name split-apart layers - // like: split-1, split-2, ... - return function(i) { - return (lyr && lyr.name || 'split') + '-' + (i + 1); - }; - } + if (!exp) return getDefaultSplitFunction(lyr); lyr = {name: lyr.name, data: lyr.data}; // remove shape info compiled = compileValueExpression(exp, lyr, null); return function(i) { diff --git a/src/gui/gui-selection-tool.js b/src/gui/gui-selection-tool.js index f59a27103..80ffaac3f 100644 --- a/src/gui/gui-selection-tool.js +++ b/src/gui/gui-selection-tool.js @@ -66,6 +66,10 @@ export function SelectionTool(gui, ext, hit) { hit.clearSelection(); } + function getIdsOpt() { + return hit.getSelectionIds().join(','); + } + hit.on('change', function(e) { if (e.mode != 'selection') return; var ids = hit.getSelectionIds(); @@ -80,18 +84,17 @@ export function SelectionTool(gui, ext, hit) { }); new SimpleButton(popup.findChild('.delete-btn')).on('click', function() { - var cmd = '-filter "$$set.has(this.id) === false"'; + var cmd = '-filter invert ids=' + getIdsOpt(); runCommand(cmd); }); new SimpleButton(popup.findChild('.filter-btn')).on('click', function() { - - var cmd = '-filter "$$set.has(this.id)"'; + var cmd = '-filter ids=' + getIdsOpt(); runCommand(cmd); }); new SimpleButton(popup.findChild('.split-btn')).on('click', function() { - var cmd = '-each "split_id = $$set.has(this.id) ? \'1\' : \'2\'" -split split_id'; + var cmd = '-split ids=' + getIdsOpt(); runCommand(cmd); }); @@ -100,10 +103,6 @@ export function SelectionTool(gui, ext, hit) { }); function runCommand(cmd) { - // var defs = internal.getStateVar('defs'); - // defs.$$selection = utils.arrayToIndex(hit.getSelectionIds()); - var ids = JSON.stringify(hit.getSelectionIds()); - cmd = `-define "$$set = new Set(${ids})" ${cmd} -define "delete $$set"`; popup.hide(); if (gui.console) gui.console.runMapshaperCommands(cmd, function(err) { reset(); diff --git a/test/filter-test.js b/test/filter-test.js index 726f866ab..37d73f796 100644 --- a/test/filter-test.js +++ b/test/filter-test.js @@ -73,6 +73,17 @@ describe('mapshaper-filter.js', function () { }); }) + it('-filter ids=', function(done) { + var cmd = '-i data.csv -filter ids=3,5 -o'; + var input = 'name\na\nb\nc\nd\ne\nf\ng\nh'; + api.applyCommands(cmd, {'data.csv': input}, function(err, out) { + var output = out['data.csv']; + assert.equal(output, 'name\nd\nf'); + done(); + }) + + }); + it ('-filter (combined options)', function(done) { api.applyCommands('-filter remove-empty "name != \'a\'"', geojson, function(err, json) { var output = JSON.parse(json); From 72672cca039c271a2eb948f3962ed2ae626ee80c Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 7 Dec 2021 20:42:18 -0500 Subject: [PATCH 127/891] Add support for reading and writing fixed-width text files --- CHANGELOG.md | 5 +- src/cli/mapshaper-option-parsing-utils.js | 16 ++- src/commands/mapshaper-info.js | 8 +- src/dissolve/mapshaper-polyline-dissolve.js | 6 + src/gui/gui-simplify-control.js | 2 +- src/indexing/mapshaper-id-lookup-index.js | 2 +- src/text/mapshaper-delim-export.js | 15 +-- src/text/mapshaper-delim-import.js | 2 +- src/text/mapshaper-delim-reader.js | 5 +- src/text/mapshaper-fixed-width.js | 135 ++++++++++++++++++++ src/utils/mapshaper-utils.js | 18 ++- test/fixed-width-test.js | 50 ++++++++ test/utils-test.js | 6 + 13 files changed, 245 insertions(+), 25 deletions(-) create mode 100644 src/text/mapshaper-fixed-width.js create mode 100644 test/fixed-width-test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2be01f758..fece870d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ +v0.5.78 +* Added support for reading and writing fixed-width text files. + v0.5.77 * Added -dashlines command (formerly -split-lines). -* Added support for joining polyline and polyline layers using point-method +* Added support for joining polyline and polygon layers using point-method v0.5.76 * Fixed bug in mapshaper-gui -q. diff --git a/src/cli/mapshaper-option-parsing-utils.js b/src/cli/mapshaper-option-parsing-utils.js index d08b1deae..5c4c542cd 100644 --- a/src/cli/mapshaper-option-parsing-utils.js +++ b/src/cli/mapshaper-option-parsing-utils.js @@ -41,9 +41,21 @@ export function parseColorList(token) { } export function cleanArgv(argv) { - argv = argv.map(function(s) {return s.trim();}); // trim whitespace + // Note: original trim caused some quoted spaces to be removed + // (e.g. bash shell seems to convert [delimiter=" "] to [delimiter= ], + // which then got trimmed to [delimiter=] below) + //// argv = argv.map(function(s) {return s.trim();}); // trim whitespace + + // Updated: don't trim space from tokens like [delimeter= ] + argv = argv.map(function(s) { + if (!/= $/.test(s)) { + s = s.trimEnd(); + } + s = s.trimStart(); + return s; + }); argv = argv.filter(function(s) {return s !== '';}); // remove empty tokens - // removing trimQuotes() call... now, strings like 'name="Meg"' will no longer + // Note: removing trimQuotes() call... now, strings like 'name="Meg"' will no longer // be parsed the same way as name=Meg and name="Meg" //// argv = argv.map(utils.trimQuotes); // remove one level of single or dbl quotes return argv; diff --git a/src/commands/mapshaper-info.js b/src/commands/mapshaper-info.js index de80f2028..e707e33e7 100644 --- a/src/commands/mapshaper-info.js +++ b/src/commands/mapshaper-info.js @@ -155,10 +155,6 @@ function formatAttributeTable(data, i) { -function formatNumber(val) { - return val + ''; -} - function maxChars(arr) { return arr.reduce(function(memo, str) { var w = stringDisplayWidth(str); @@ -182,14 +178,14 @@ function formatString(str) { } function countIntegralChars(val) { - return utils.isNumber(val) ? (formatNumber(val) + '.').indexOf('.') : 0; + return utils.isNumber(val) ? (utils.formatNumber(val) + '.').indexOf('.') : 0; } export function formatTableValue(val, integralChars) { var str; if (utils.isNumber(val)) { str = utils.lpad("", integralChars - countIntegralChars(val), ' ') + - formatNumber(val); + utils.formatNumber(val); } else if (utils.isString(val)) { str = formatString(val); } else if (utils.isDate(val)) { diff --git a/src/dissolve/mapshaper-polyline-dissolve.js b/src/dissolve/mapshaper-polyline-dissolve.js index 9cdc1f004..269ab6465 100644 --- a/src/dissolve/mapshaper-polyline-dissolve.js +++ b/src/dissolve/mapshaper-polyline-dissolve.js @@ -38,6 +38,12 @@ function getPolylineDissolver(arcs) { }; } +/* + + + +*/ + // TODO: use polygon pathfinder shared code function collectPolylineArcs(ids, nodes, testArc, useArc) { var parts = []; diff --git a/src/gui/gui-simplify-control.js b/src/gui/gui-simplify-control.js index 8b536b612..14b228377 100644 --- a/src/gui/gui-simplify-control.js +++ b/src/gui/gui-simplify-control.js @@ -90,7 +90,7 @@ export var SimplifyControl = function(gui) { else if (pct < 0.01) decimals = 3; else if (pct < 1) decimals = 2; else if (pct < 100) decimals = 1; - return utils.formatNumber(pct, decimals) + "%"; + return utils.formatNumberForDisplay(pct, decimals) + "%"; }); text.parser(function(s) { diff --git a/src/indexing/mapshaper-id-lookup-index.js b/src/indexing/mapshaper-id-lookup-index.js index 1ec68edd7..2e6c19dd4 100644 --- a/src/indexing/mapshaper-id-lookup-index.js +++ b/src/indexing/mapshaper-id-lookup-index.js @@ -1,7 +1,7 @@ -// Map positive or negative integer ids to non-negative integer ids import utils from '../utils/mapshaper-utils'; import { error } from '../utils/mapshaper-logging'; +// Map positive or negative integer ids to non-negative integer ids export function IdLookupIndex(n, clearable) { var fwdIndex = new Int32Array(n); var revIndex = new Int32Array(n); diff --git a/src/text/mapshaper-delim-export.js b/src/text/mapshaper-delim-export.js index 10f0adf1b..ebc02a466 100644 --- a/src/text/mapshaper-delim-export.js +++ b/src/text/mapshaper-delim-export.js @@ -3,6 +3,7 @@ import { findFieldNames } from '../datatable/mapshaper-data-utils'; import utils from '../utils/mapshaper-utils'; import { Buffer } from '../utils/mapshaper-node-buffer'; import { getFileExtension } from '../utils/mapshaper-filename-utils'; +import { exportRecordsAsFixedWidthString } from './mapshaper-fixed-width'; // Generate output content from a dataset object export function exportDelim(dataset, opts) { @@ -25,6 +26,9 @@ export function exportLayerAsDSV(lyr, delim, optsArg) { var encoding = opts.encoding || 'utf8'; var records = lyr.data.getRecords(); var fields = findFieldNames(records, opts.field_order); + if (delim == ' ') { + return exportRecordsAsFixedWidthString(fields, records, opts); + } var formatRow = getDelimRowFormatter(fields, delim, opts); // exporting utf8 and ascii text as string by default (for now) var exportAsString = encodingIsUtf8(encoding) && !opts.to_buffer && @@ -82,15 +86,6 @@ function getDelimRowFormatter(fields, delim, opts) { }; } -export function formatNumber(val) { - return val + ''; -} - -export function formatIntlNumber(val) { - var str = formatNumber(val); - return '"' + str.replace('.', ',') + '"'; // need to quote if comma-delimited -} - export function getDelimValueFormatter(delim, opts) { var dquoteRxp = new RegExp('["\n\r' + delim + ']'); var decimalComma = opts && opts.decimal_comma || false; @@ -107,7 +102,7 @@ export function getDelimValueFormatter(delim, opts) { } else if (utils.isString(val)) { s = formatString(val); } else if (utils.isNumber(val)) { - s = decimalComma ? formatIntlNumber(val) : formatNumber(val); + s = decimalComma ? utils.formatIntlNumber(val) : utils.formatNumber(val); } else if (utils.isObject(val)) { s = formatString(JSON.stringify(val)); } else { diff --git a/src/text/mapshaper-delim-import.js b/src/text/mapshaper-delim-import.js index 1a78103aa..ecf074ee8 100644 --- a/src/text/mapshaper-delim-import.js +++ b/src/text/mapshaper-delim-import.js @@ -72,7 +72,7 @@ export function importDelim2(data, opts) { }; } -var supportedDelimiters = ['|', '\t', ',', ';']; +var supportedDelimiters = ['|', '\t', ',', ';', ' ']; export function isSupportedDelimiter(d) { return utils.contains(supportedDelimiters, d); diff --git a/src/text/mapshaper-delim-reader.js b/src/text/mapshaper-delim-reader.js index 88533fc62..2ad0dbe48 100644 --- a/src/text/mapshaper-delim-reader.js +++ b/src/text/mapshaper-delim-reader.js @@ -3,6 +3,7 @@ import { getBaseContext, compileExpressionToFunction } from '../expressions/maps import utils from '../utils/mapshaper-utils'; import { stop } from '../utils/mapshaper-logging'; import { Reader2 } from '../io/mapshaper-file-reader'; +import { readFixedWidthRecords, readFixedWidthRecordsFromString } from '../text/mapshaper-fixed-width'; // Read and parse a DSV file // This version performs field filtering before fields are extracted (faster) @@ -10,8 +11,9 @@ import { Reader2 } from '../io/mapshaper-file-reader'; // // TODO: confirm compatibility with all supported encodings export function readDelimRecords(reader, delim, optsArg) { + var opts = optsArg || {}; + if (delim == ' ') return readFixedWidthRecords(reader, opts); var reader2 = new Reader2(reader), - opts = optsArg || {}, headerStr = readLinesAsString(reader2, getDelimHeaderLines(opts), opts.encoding), header = parseDelimHeaderSection(headerStr, delim, opts), convertRowArr = getRowConverter(header.import_fields), @@ -34,6 +36,7 @@ export function readDelimRecords(reader, delim, optsArg) { // for delimiter characters and newlines. Input size is limited by the maximum // string size. export function readDelimRecordsFromString(str, delim, opts) { + if (delim == ' ') return readFixedWidthRecordsFromString(str, opts); var header = parseDelimHeaderSection(str, delim, opts); if (header.import_fields.length === 0 || !header.remainder) return []; var convert = getRowConverter(header.import_fields); diff --git a/src/text/mapshaper-fixed-width.js b/src/text/mapshaper-fixed-width.js new file mode 100644 index 000000000..a9750469e --- /dev/null +++ b/src/text/mapshaper-fixed-width.js @@ -0,0 +1,135 @@ +import utils from '../utils/mapshaper-utils'; +import { stop } from '../utils/mapshaper-logging'; + +export function exportRecordsAsFixedWidthString(fields, records, opts) { + var rows = [], col; + for (var i=0; i 2) lines.pop(); // remove possible partial line + var n = getMaxLineLength(lines); + var headerLine = lines[0]; + var colInfo = []; + var colStart = 0; + var inContent = false; + var inHeader = false; + var isContentChar, isHeaderChar, isColStart, colEnd; + for (var i=0; i<=n; i++) { + isHeaderChar = testContentChar(headerLine, i); + isContentChar = !testEmptyCol(lines, i); + isColStart = isHeaderChar && !inHeader; + if (isColStart && inContent) { + // all lines should have a space char in the position right before a header starts + return null; + } + if (i == n || i > 0 && isColStart) { + colEnd = i == n ? undefined : i-1; + colInfo.push({ + name: readValue(headerLine, colStart, colEnd), + end: colEnd, + start: colStart + }); + colStart = i; + } + inContent = isContentChar; + inHeader = isHeaderChar; + } + return colInfo.length > 0 ? colInfo : null; +} + +function getMaxLineLength(lines) { + var max = 0; + for (var i=0; i= 0 ? num.toFixed(decimals) : String(num); } -export function formatNumber(num, decimals, nullStr, showPos) { +export function formatNumber(val) { + return val + ''; +} + +export function formatIntlNumber(val) { + var str = formatNumber(val); + return '"' + str.replace('.', ',') + '"'; // need to quote if comma-delimited +} + +export function formatNumberForDisplay(num, decimals, nullStr, showPos) { var fmt; if (isNaN(num)) { fmt = nullStr || '-'; @@ -770,7 +783,8 @@ function formatValue(val, matches) { str = str.toUpperCase(); } else if (isNumber) { - str = numToStr(val, isInt ? 0 : decimals); + // str = formatNumberForDisplay(val, isInt ? 0 : decimals); + str = numToStr(val, decimals); if (str[0] == '-') { isNeg = true; str = str.substr(1); diff --git a/test/fixed-width-test.js b/test/fixed-width-test.js new file mode 100644 index 000000000..861ba4a0f --- /dev/null +++ b/test/fixed-width-test.js @@ -0,0 +1,50 @@ + +import {parseFixedWidthInfo} from '../src/text/mapshaper-fixed-width'; + +var api = require('..'), + assert = require('assert'); + +describe('mapshaper-fixed-width.js', function () { + describe('-o tests', function() { + it ('simple test', function(done) { + var input = 'color,number\nred,10 \ncrimson,0 '; + var cmd = '-i data.csv -o format=dsv delimiter=" "'; + api.applyCommands(cmd, {'data.csv': input}, function(err, out) { + var expect = 'color number\nred 10 \ncrimson 0 '; + assert.equal(out['data.txt'], expect); + done(); + }); + }); + }); + + describe('parseFixedWidthInfo()', function() { + it('simple test', function() { + var str = 'name color\nfoo red '; + var output = parseFixedWidthInfo(str); + var expect = [{name: 'name', start: 0, end: 4}, {name: 'color', start: 5, end: undefined}]; + assert.deepEqual(output, expect); + }) + + it('simple test 2', function() { + var str = 'name color\nfoo bar red '; + var output = parseFixedWidthInfo(str); + var expect = [{name: 'name', start: 0, end: 7}, {name: 'color', start: 8, end: undefined}]; + assert.deepEqual(output, expect); + }) + + it('uneven last field length', function() { + var str = 'name color\nfoo red red red'; + var output = parseFixedWidthInfo(str); + var expect = [{name: 'name', start: 0, end: 4}, {name: 'color', start: 5, end: undefined}]; + assert.deepEqual(output, expect); + }) + + it('uneven last field length 2', function() { + var str = 'name color\nfoo red'; + var output = parseFixedWidthInfo(str); + var expect = [{name: 'name', start: 0, end: 4}, {name: 'color', start: 5, end: undefined}]; + assert.deepEqual(output, expect); + }) + + }); +}) diff --git a/test/utils-test.js b/test/utils-test.js index 99b74051b..2220d165b 100644 --- a/test/utils-test.js +++ b/test/utils-test.js @@ -11,6 +11,12 @@ describe('mapshaper-utils.js', function () { }) }) + describe('splitLines()', function() { + it('test 1', function() { + assert.deepEqual(utils.splitLines('a\nb'), ['a', 'b']); + }) + }); + describe('formatDateISO()', function () { it('rounds to minutes', function () { assert.equal(utils.formatDateISO(new Date('2020-10-01T02:59:00.000Z')), '2020-10-01T02:59Z') From 6b1a279125d789e4f461257afd29a0678528fa36 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 9 Dec 2021 17:07:56 -0500 Subject: [PATCH 128/891] v0.5.78 --- package-lock.json | 8 ++++---- package.json | 4 ++-- www/modules.js | 23 +++++++++++++++++++++-- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index e7e9d6f07..acca9c914 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.77", + "version": "0.5.78", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1723,9 +1723,9 @@ } }, "mproj": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.34.tgz", - "integrity": "sha512-WUImA718tT8Ik9bGy81C0Nbwoa0SkqvsWkKqem9EH6m25MvuSFGWw+EK4vkFrng5nJvimPxUIEH+De5HWAjF2w==", + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.35.tgz", + "integrity": "sha512-xqO9BXjTezwyPFbAShWRkYZ98DD9wWOyr86WX6miWq3brBNGypsErMmobpUx3G45SrfvyJ5jI997Zw0qr7Ko7A==", "requires": { "geographiclib": "1.48.0", "rw": "~1.3.2" diff --git a/package.json b/package.json index 22cc087b9..f53d2cd77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.77", + "version": "0.5.78", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", @@ -49,7 +49,7 @@ "geokdbush": "^1.1.0", "iconv-lite": "0.4.24", "kdbush": "^3.0.0", - "mproj": "0.0.34", + "mproj": "0.0.35", "opn": "^5.3.0", "rw": "~1.3.3", "sync-request": "5.0.0", diff --git a/www/modules.js b/www/modules.js index abcba67b3..f11635cc6 100644 --- a/www/modules.js +++ b/www/modules.js @@ -12464,8 +12464,9 @@ function wkt_parse(str) { // WKT format: http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#11 function wkt_unpack(str) { var obj; - // Convert WKT escaped quote to JSON escaped quote - str = str.replace(/""/g, '\\"'); + // Convert WKT escaped quotes to JSON escaped quotes + // str = str.replace(/""/g, '\\"'); // BUGGY + str = convert_wkt_quotes(str); // Convert WKT entities to JSON arrays str = str.replace(/([A-Z0-9]+)\[/g, '["$1",'); @@ -12487,6 +12488,23 @@ function wkt_unpack(str) { return obj; } +// Convert WKT escaped quotes to JSON escaped quotes ("" -> \") +function convert_wkt_quotes(str) { + var c = 0; + return str.replace(/"+/g, function(s) { + var even = c % 2 == 0; + c += s.length; + // ordinary, unescaped quotes + if (s == '"' || s == '""' && even) return s; + // WKT-escaped quotes + if (even) { + return '"' + s.substring(1).replace(/""/g, '\\"'); + } else { + return s.replace(/""/g, '\\"'); + } + }); +} + // Rearrange a subarray of a parsed WKT file for easier traversal // E.g. // ["WGS84", ...] to {NAME: "WGS84"} @@ -21706,6 +21724,7 @@ api.internal = { RAD_TO_DEG: RAD_TO_DEG, wkt_parse: wkt_parse, wkt_unpack: wkt_unpack, + convert_wkt_quotes: convert_wkt_quotes, wkt_to_proj4: wkt_to_proj4, wkt_from_proj4: wkt_from_proj4, wkt_make_projcs: wkt_make_projcs, From 492f31c55500103bf03d065a7d4823e6a7e4d8b8 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 10 Dec 2021 19:42:29 -0500 Subject: [PATCH 129/891] v0.5.79 --- CHANGELOG.md | 4 ++ package.json | 2 +- src/cli/mapshaper-options.js | 10 +++ src/commands/mapshaper-shapes.js | 23 ++++++ src/shapefile/shp-reader.js | 116 ++++++++++++++++--------------- src/utils/mapshaper-logging.js | 7 +- test/shp-reader-test.js | 3 +- 7 files changed, 105 insertions(+), 60 deletions(-) create mode 100644 src/commands/mapshaper-shapes.js diff --git a/CHANGELOG.md b/CHANGELOG.md index fece870d5..0b7f186d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ +v0.5.79 +* More permissive importing of some non-standard Shapefiles. + v0.5.78 * Added support for reading and writing fixed-width text files. +* Bug fixes. v0.5.77 * Added -dashlines command (formerly -split-lines). diff --git a/package.json b/package.json index f53d2cd77..1c87ae8c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.78", + "version": "0.5.79", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 6e05d654a..6104cce6f 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1830,6 +1830,16 @@ export function getOptionParser() { }) .option('name', nameOpt); + // parser.command('shapes') + // .describe('convert points to shapes') + // .option('type', { + // }) + // .option('size', { + // }) + // .option('rotation', { + // }) + + parser.command('subdivide') .describe('recursively split a layer using a JS expression') .validate(V.validateExpressionOpt) diff --git a/src/commands/mapshaper-shapes.js b/src/commands/mapshaper-shapes.js new file mode 100644 index 000000000..d57e4311d --- /dev/null +++ b/src/commands/mapshaper-shapes.js @@ -0,0 +1,23 @@ +import { stop } from '../utils/mapshaper-logging'; +import cmd from '../mapshaper-cmd'; +import { getDatasetCRS, getCRS, requireProjectedDataset } from '../crs/mapshaper-projections'; + +cmd.shapes = function(lyr, dataset, opts) { + requireProjectedDataset(dataset); +}; + +function makeShape(type, center, opts) { + +} + +function makeCircle(center, opts) { + +} + +function makeRegularPolygon(center, maxLen, sideCount, opts) { + +} + +function makeStar(center, outerLen, innerLen, opts) { + +} diff --git a/src/shapefile/shp-reader.js b/src/shapefile/shp-reader.js index a420be8ae..d8809809a 100644 --- a/src/shapefile/shp-reader.js +++ b/src/shapefile/shp-reader.js @@ -34,7 +34,7 @@ export function ShpReader(shpSrc, shxSrc) { var header = parseHeader(shpFile.readToBinArray(0, 100)); var shpSize = shpFile.size(); var RecordClass = new ShpRecordClass(header.type); - var shpOffset, recordCount, skippedBytes; + var shpOffset, recordCount; var shxBin, shxFile; if (shxSrc) { @@ -61,61 +61,36 @@ export function ShpReader(shpSrc, shxSrc) { // Iterator interface for reading shape records this.nextShape = function() { - var shape = readNextShape(); - if (!shape) { - if (skippedBytes > 0) { - // Encountered in files from natural earth v2.0.0: - // ne_10m_admin_0_boundary_lines_land.shp - // ne_110m_admin_0_scale_rank.shp - verbose("Skipped over " + skippedBytes + " non-data bytes in the .shp file."); - } + var shape = readNextShape(recordCount); + if (shape) { + recordCount++; + } else { shpFile.close(); reset(); } return shape; }; - function readNextShape() { - var expectedId = recordCount + 1; // Shapefile ids are 1-based + // Returns a shape record or null if no more shapes can be read + // + function readNextShape(i) { + var expectedId = i + 1; // Shapefile ids are 1-based var shape, offset; - if (done()) return null; if (shxBin) { - shxBin.position(100 + recordCount * 8); + if (shxFile.size() <= 100 + i * 8) return null; // done + shxBin.position(100 + i * 8); offset = shxBin.readUint32() * 2; - if (offset > shpOffset) { - skippedBytes += offset - shpOffset; - } + shape = readIndexedShape(shpFile, offset, expectedId); } else { + // Reading without a .shx file (returns null at end-of-file) offset = shpOffset; - } - shape = readShapeAtOffset(offset); - if (!shape) { - // Some in-the-wild .shp files contain junk bytes between records. This - // is a problem if the .shx index file is not present. - // Here, we try to scan past the junk to find the next record. - shape = huntForNextShape(offset, expectedId); - } - if (shape) { - if (shape.id < expectedId) { - message("Found a Shapefile record with the same id as a previous record (" + shape.id + ") -- skipping."); - return readNextShape(); - } else if (shape.id > expectedId) { - stop("Shapefile contains an out-of-sequence record. Possible data corruption -- bailing."); - } - recordCount++; + shape = readNonIndexedShape(shpFile, offset, expectedId); } return shape || null; } - function done() { - if (shxFile && shxFile.size() <= 100 + recordCount * 8) return true; - if (shpOffset + 12 > shpSize) return true; - return false; - } - function reset() { shpOffset = 100; - skippedBytes = 0; recordCount = 0; } @@ -145,7 +120,8 @@ export function ShpReader(shpSrc, shxSrc) { return header; } - function readShapeAtOffset(offset) { + + function readShapeAtOffset(shpFile, offset) { var shape = null, recordSize, recordType, recordId, goodSize, goodType, bin; @@ -160,32 +136,62 @@ export function ShpReader(shpSrc, shxSrc) { if (goodSize && goodType) { bin = shpFile.readToBinArray(offset, recordSize); shape = new RecordClass(bin, recordSize); - shpOffset = offset + shape.byteLength; // advance read position } } return shape; } - // TODO: add tests - // Try to scan past unreadable content to find next record - function huntForNextShape(start, id) { - var offset = start + 4, + function readIndexedShape(shpFile, offset, expectedId) { + var shape = readShapeAtOffset(shpFile, offset); + if (!shape) { + stop('Index of Shapefile record', expectedId, 'in the .shx file is invalid.'); + } + if (shape.id != expectedId) { + // stop("Found a Shapefile record with an out-of-sequence id (" + shape.id + ") -- bailing."); + message(`Warning: A feature has a different record number in .shx (${expectedId}) and .shp (${shape.id}).`); + } + // TODO: consider printing verbose message if a .shp file contains garbage bytes + // example files: + // ne_10m_admin_0_boundary_lines_land.shp + // ne_110m_admin_0_scale_rank.shp + return shape; + } + + // The Shapefile specification does not require records to be densely packed or + // in consecutive sequence in the .shp file. This is a problem when the .shx + // index file is not present. + // + // Here, we try to scan past invalid content to find the next record. + // Records are required to be in sequential order. + // + function readNonIndexedShape(shpFile, start, expectedId) { + var offset = start, + fileSize = shpFile.size(), shape = null, - bin, recordId, recordType, count; - while (offset + 12 <= shpSize) { + bin, recordId, recordType, isValidType; + while (offset + 12 <= fileSize) { bin = shpFile.readToBinArray(offset, 12); recordId = bin.bigEndian().readUint32(); recordType = bin.littleEndian().skipBytes(4).readUint32(); - if (recordId == id && (recordType == header.type || recordType === 0)) { - // we have a likely position, but may still be unparsable - shape = readShapeAtOffset(offset); - break; + isValidType = recordType == header.type || recordType === 0; + if (!isValidType || recordId != expectedId && recordType === 0) { + offset += 4; // keep scanning -- try next integer position + continue; } - offset += 4; // try next integer position + shape = readShapeAtOffset(shpFile, offset); + if (!shape) break; // probably ran into end of file + shpOffset = offset + shape.byteLength; // update + if (recordId == expectedId) break; // found an apparently valid shape + if (recordId < expectedId) { + message("Found a Shapefile record with the same id as a previous record (" + shape.id + ") -- skipping."); + offset += shape.byteLength; + } else { + stop("Shapefile contains an out-of-sequence record. Possible data corruption -- bailing."); + } + } + if (shape && offset > start) { + verbose("Skipped over " + (offset - start) + " non-data bytes in the .shp file."); } - count = shape ? offset - start : shpSize - start; - // debug('Skipped', count, 'bytes', shape ? 'before record ' + id : 'at the end of the file'); - skippedBytes += count; return shape; } } diff --git a/src/utils/mapshaper-logging.js b/src/utils/mapshaper-logging.js index 3c41d95a1..2451408cb 100644 --- a/src/utils/mapshaper-logging.js +++ b/src/utils/mapshaper-logging.js @@ -161,7 +161,8 @@ function messageArgs(args) { } export function logArgs(args) { - if (LOGGING && !getStateVar('QUIET') && utils.isArrayLike(args)) { - (!STDOUT && console.error || console.log).call(console, formatLogArgs(args)); - } + if (!LOGGING || getStateVar('QUIET') || !utils.isArrayLike(args)) return; + var msg = formatLogArgs(args); + if (STDOUT) console.log(msg); + else console.error(msg); } diff --git a/test/shp-reader-test.js b/test/shp-reader-test.js index 79de50455..8d76f27d1 100644 --- a/test/shp-reader-test.js +++ b/test/shp-reader-test.js @@ -99,7 +99,8 @@ function testCounts(file) { }); if (counts.shapeCount != shapes) - assert.ok(false, "Shape counts don't match"); + // assert.ok(false, "Shape counts don't match"); + assert.equal(counts.shapeCount, shapes); if (parts != counts.partCount) assert.ok(false, "Part counts don't match"); From 11bdea0148c49f415b3012f929871258f6b3cc74 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 10 Dec 2021 19:42:55 -0500 Subject: [PATCH 130/891] v0.5.79 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index acca9c914..c7dd1d16c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.78", + "version": "0.5.79", "lockfileVersion": 1, "requires": true, "dependencies": { From 2c96a36fc7980461c90efffa306c1c849fe6d6af Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 14 Dec 2021 00:36:22 -0500 Subject: [PATCH 131/891] Improve shp reading code --- src/shapefile/shp-reader.js | 117 ++++++++---------- .../data_corruption_error.cpg | 1 + .../data_corruption_error.dbf | Bin 0 -> 1810 bytes .../data_corruption_error.prj | 1 + .../data_corruption_error.shp | Bin 0 -> 1604 bytes .../data_corruption_error.shx | Bin 0 -> 164 bytes .../max_callstack_error.dbf | Bin 0 -> 5756 bytes .../max_callstack_error.prj | 1 + .../max_callstack_error.shp | Bin 0 -> 12244 bytes .../max_callstack_error.shx | Bin 0 -> 1204 bytes test/shapefile-test.js | 23 ++++ test/shp-reader-test.js | 18 ++- 12 files changed, 94 insertions(+), 67 deletions(-) create mode 100644 test/data/issues/518_519_shp_reading/data_corruption_error.cpg create mode 100644 test/data/issues/518_519_shp_reading/data_corruption_error.dbf create mode 100644 test/data/issues/518_519_shp_reading/data_corruption_error.prj create mode 100644 test/data/issues/518_519_shp_reading/data_corruption_error.shp create mode 100644 test/data/issues/518_519_shp_reading/data_corruption_error.shx create mode 100644 test/data/issues/518_519_shp_reading/max_callstack_error.dbf create mode 100644 test/data/issues/518_519_shp_reading/max_callstack_error.prj create mode 100644 test/data/issues/518_519_shp_reading/max_callstack_error.shp create mode 100644 test/data/issues/518_519_shp_reading/max_callstack_error.shx diff --git a/src/shapefile/shp-reader.js b/src/shapefile/shp-reader.js index d8809809a..bf9ef415b 100644 --- a/src/shapefile/shp-reader.js +++ b/src/shapefile/shp-reader.js @@ -29,12 +29,13 @@ export function ShpReader(shpSrc, shxSrc) { if (this instanceof ShpReader === false) { return new ShpReader(shpSrc, shxSrc); } - var shpFile = utils.isString(shpSrc) ? new FileReader(shpSrc) : new BufferReader(shpSrc); var header = parseHeader(shpFile.readToBinArray(0, 100)); - var shpSize = shpFile.size(); - var RecordClass = new ShpRecordClass(header.type); - var shpOffset, recordCount; + var shpType = header.type; + var shpOffset = 100; // used when reading .shp without .shx + var recordCount = 0; + var badRecordNumberCount = 0; + var RecordClass = new ShpRecordClass(shpType); var shxBin, shxFile; if (shxSrc) { @@ -42,8 +43,6 @@ export function ShpReader(shpSrc, shxSrc) { shxBin = shxFile.readToBinArray(0, shxFile.size()).bigEndian(); } - reset(); - this.header = function() { return header; }; @@ -61,37 +60,35 @@ export function ShpReader(shpSrc, shxSrc) { // Iterator interface for reading shape records this.nextShape = function() { - var shape = readNextShape(recordCount); - if (shape) { - recordCount++; - } else { - shpFile.close(); - reset(); + var shape; + if (!shpFile) { + error('Tried to read from a used ShpReader'); + // return null; // this reader was already used + } + shape = readNextShape(recordCount); + if (!shape) { + done(); + return null; } + recordCount++; return shape; }; // Returns a shape record or null if no more shapes can be read + // i: Expected 0-based index of the next record // function readNextShape(i) { - var expectedId = i + 1; // Shapefile ids are 1-based - var shape, offset; - if (shxBin) { - if (shxFile.size() <= 100 + i * 8) return null; // done - shxBin.position(100 + i * 8); - offset = shxBin.readUint32() * 2; - shape = readIndexedShape(shpFile, offset, expectedId); - } else { - // Reading without a .shx file (returns null at end-of-file) - offset = shpOffset; - shape = readNonIndexedShape(shpFile, offset, expectedId); - } - return shape || null; + return shxBin ? + readIndexedShape(shpFile, shxBin, i) : + readNonIndexedShape(shpFile, shpOffset, i); } - function reset() { - shpOffset = 100; - recordCount = 0; + function done() { + shpFile.close(); + shpFile = shxFile = shxBin = null; + if (badRecordNumberCount > 0) { + message(`Warning: ${badRecordNumberCount}/${recordCount} features have non-standard record numbers in the .shp file.`); + } } function parseHeader(bin) { @@ -122,33 +119,35 @@ export function ShpReader(shpSrc, shxSrc) { function readShapeAtOffset(shpFile, offset) { - var shape = null, - recordSize, recordType, recordId, goodSize, goodType, bin; - - if (offset + 12 <= shpSize) { - bin = shpFile.readToBinArray(offset, 12); - recordId = bin.bigEndian().readUint32(); - // record size is bytes in content section + 8 header bytes - recordSize = bin.readUint32() * 2 + 8; - recordType = bin.littleEndian().readUint32(); - goodSize = offset + recordSize <= shpSize && recordSize >= 12; - goodType = recordType === 0 || recordType == header.type; - if (goodSize && goodType) { - bin = shpFile.readToBinArray(offset, recordSize); - shape = new RecordClass(bin, recordSize); - } + var fileSize = shpFile.size(); + if (offset + 12 > fileSize) return null; // reached end-of-file + var bin = shpFile.readToBinArray(offset, 12); + var recordId = bin.bigEndian().readUint32(); + // record size is bytes in content section + 8 header bytes + var recordSize = bin.readUint32() * 2 + 8; + var recordType = bin.littleEndian().readUint32(); + var goodSize = offset + recordSize <= fileSize && recordSize >= 12; + var goodType = recordType === 0 || recordType == shpType; + if (!goodSize || !goodType) { + return null; } - return shape; + bin = shpFile.readToBinArray(offset, recordSize); + return new RecordClass(bin, recordSize); } - function readIndexedShape(shpFile, offset, expectedId) { + function readIndexedShape(shpFile, shxBin, i) { + if (shxBin.size() <= 100 + i * 8) return null; // done + shxBin.position(100 + i * 8); + var expectedId = i + 1; + var offset = shxBin.readUint32() * 2; + var recLen = shxBin.readUint32() * 2; // TODO: match this to recLen in .shp var shape = readShapeAtOffset(shpFile, offset); if (!shape) { stop('Index of Shapefile record', expectedId, 'in the .shx file is invalid.'); } if (shape.id != expectedId) { - // stop("Found a Shapefile record with an out-of-sequence id (" + shape.id + ") -- bailing."); - message(`Warning: A feature has a different record number in .shx (${expectedId}) and .shp (${shape.id}).`); + badRecordNumberCount++; + verbose(`Warning: A feature has a different record number in .shx (${expectedId}) and .shp (${shape.id}).`); } // TODO: consider printing verbose message if a .shp file contains garbage bytes // example files: @@ -161,11 +160,12 @@ export function ShpReader(shpSrc, shxSrc) { // in consecutive sequence in the .shp file. This is a problem when the .shx // index file is not present. // - // Here, we try to scan past invalid content to find the next record. + // Here, we try to scan past any invalid content to find the next record. // Records are required to be in sequential order. // - function readNonIndexedShape(shpFile, start, expectedId) { - var offset = start, + function readNonIndexedShape(shpFile, start, i) { + var expectedId = i + 1, // Shapefile ids are 1-based + offset = start, fileSize = shpFile.size(), shape = null, bin, recordId, recordType, isValidType; @@ -173,7 +173,7 @@ export function ShpReader(shpSrc, shxSrc) { bin = shpFile.readToBinArray(offset, 12); recordId = bin.bigEndian().readUint32(); recordType = bin.littleEndian().skipBytes(4).readUint32(); - isValidType = recordType == header.type || recordType === 0; + isValidType = recordType == shpType || recordType === 0; if (!isValidType || recordId != expectedId && recordType === 0) { offset += 4; // keep scanning -- try next integer position continue; @@ -200,18 +200,3 @@ ShpReader.prototype.type = function() { return this.header().type; }; -ShpReader.prototype.getCounts = function() { - var counts = { - nullCount: 0, - partCount: 0, - shapeCount: 0, - pointCount: 0 - }; - this.forEachShape(function(shp) { - if (shp.isNull) counts.nullCount++; - counts.pointCount += shp.pointCount; - counts.partCount += shp.partCount; - counts.shapeCount++; - }); - return counts; -}; diff --git a/test/data/issues/518_519_shp_reading/data_corruption_error.cpg b/test/data/issues/518_519_shp_reading/data_corruption_error.cpg new file mode 100644 index 000000000..3ad133c04 --- /dev/null +++ b/test/data/issues/518_519_shp_reading/data_corruption_error.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/test/data/issues/518_519_shp_reading/data_corruption_error.dbf b/test/data/issues/518_519_shp_reading/data_corruption_error.dbf new file mode 100644 index 0000000000000000000000000000000000000000..8a652c53fe4b16c9b8c1b2508fcd5f50eb8dcb45 GIT binary patch literal 1810 zcmdT^u?oU45Uscr!Cjp011g+trR^*@siSL&Nfd3c#m2!O^ZQJiYDKMrleytI^4`6? z97nomci=jXLpm3gUxO8CF3$y5$Wo)dCyK^*tU7GN>6iZUw5}L!{W+x{^+irIO=#<9 zTyV*mGuNb!P7k5{my*a&?_*_=Zp8MzzA~TEwDLdIvSt*`1%1#FA&${kMUX^s?1$AM zgotP=l{IQ&d@BHFb?HA4aX}@qPIs^)Mz?JbMeQ21@f_K zM-^lRaw9-yT|2ckDXdDF+j7jD!=wz1o9byYJk>)0Zb02wrpNx z=Xapq!k#m(hk$BfX2JAAwNaB|0~+Q5a`&-G*<3x9nvQn5tGhw&Tu{0=2;`6Lit`pc zhPs;xYBNj@rWU3brj`m0WCxl74o_Izs4GLn=u6Gp*2d#kaItF#e9jH6EDJ*chfaWfk984`tFH9{N?m*W9%A4TuYWwN` zs~G4mSRR~y>A!jxkgsrIS^PFMccIIn>qWPO3_dqdPX{PpY|(l=r3~nf`5QJ&*aLKj zk>vdpP}$J5`Qo4BPPL*yk&VuhHWZJsrsBfA+EOAdru47sQqUC3%3hSAfFhg_6MkDxh0kHz>q{;)mOM z?yZSH{;i8AmxBBX%9o%*5d>grqwVfzg5*+vyqQ^pq&DSDOHMzKUw`nOAxIu(*3!(# V`kg@jWSt&!P+EZ51u>O=Bml*APOAU_ literal 0 HcmV?d00001 diff --git a/test/data/issues/518_519_shp_reading/data_corruption_error.shx b/test/data/issues/518_519_shp_reading/data_corruption_error.shx new file mode 100644 index 0000000000000000000000000000000000000000..05bae46ef0e31bd82fb613d17abd0f140c3bbcc6 GIT binary patch literal 164 zcmZQzQ0HR64uW1VGcfQ11cYkx@zsgXveUkw?|jhXgWr7@yCA!^08}2 o5j6txB7nFLh&_P#7KG1e2H`U{LFsc4nn?;scQ7z8Ed$aO09^hTg8%>k literal 0 HcmV?d00001 diff --git a/test/data/issues/518_519_shp_reading/max_callstack_error.dbf b/test/data/issues/518_519_shp_reading/max_callstack_error.dbf new file mode 100644 index 0000000000000000000000000000000000000000..a7ece37eef8d70e510cef12b74223e313c3938c4 GIT binary patch literal 5756 zcmZu#y^bbD4E!Xxkc^1PDaoj9_is*!h)EGT!6kPRI$naOrh22>DsQmTdZ))_yIgk9 zcmIC)@Zan8`s4L8e$&tIuh*|1KmYah?>GBz_xF$AzklYJPyc-U^XsQCpI@)?|3BC_ zKf8_fWNYmkn_13hkZpTg&myDk^K`aFwC9k~XM2Kdy^khu^uo3ISq~1s$2jAeYzt3u zZENa-KBq=~ro!nbo=t9|%7kx?I$}Ofht0do8$Ci?Tf|Y|=g1q~dza*TbFo?aRO9aI zC4fijx`&V=M{EJ^sJ}#=ZEFcp8S&yr(_=g3!9MnsH@X)X6Ln=i8gk2JwP*!X`q9Ha zm*BCJlG{Eq1Y^3C)ms1~i1MD{BQ@dMySheMCBOtm?zrr!QTL&gih`q-FtVG9^=2p` zD@BZ<0YX$y}vwnjKqnk_&xWpk09>`fvJh^yuiRghE^0)Rd$>bkTGZMllQp<3!Xo6z3Y z5?eo0I}UY~2h+QXeY#T569OQ4L?#4dx|YyU1SUl~C?%&&1i(=1rmnZBFotldAbBtJ zD6=4LQz0E$C~n{|_mKtMu<^D6uL_nJKFsjj>(m~A-L*eFrCmMNbj>3&p{^^@ME&JK z!}wt1ZDFxtLlOb7Ii%J$3N?dKQ??6?m*L!SfKrHkAzX1?bHWX}&egI?xeN*C!@i&t z(h9&WWFC>4oa-8n5w~YGa$Z6^hUFe`wTadqq(@&_**6A3Moe&3+uID#2Dsk zVY?0ieqHk$EwNrR@PHTQ=y2?c#64u9%i-BED)_X+Mz|dpq)Nk8g;$%p*#UVXn zG(&nQ;kb|#v)NL;T+EWQD_rE-yy5!Ee9Ylef9$=g6akEs9(zkJC)~hM_Ob#OF=jX} z5P~r`uB7ZHLQ{tJP%XS$vv_f|CgTb0W}F^Kp$HOYxQYTER5r)bRS_$96Fe6q!NCm> zYQ}lIC1l&YbB35=7(QG=cVwK62&S`Z2mshacrf;~GKQ@}g7IqG3y_qPdp~Hf?GjRq zXgLWm7si>80$U2F2bI}^@m7XN;l6Q!WZ4UN;oKS?l?MS_aG#A4#}Nh^j&Wn|g=!&& zCM(<uTs#OpGZ{$qieytjwsY41kcKzrQy2Rm|n^NlHQQRFn1>*_akh$SNGiCT) zuen-w#|KZ+K(*NBm`Iv4e7s_gp6V;tjhb=iLeH@AZmAj!7ER{fl|6QiPj(tLQlWi) k2RolmZ2L@<-O!rU?3RUGm)}V5C0Ft|tG=H74dYM$1DWz@QUCw| literal 0 HcmV?d00001 diff --git a/test/data/issues/518_519_shp_reading/max_callstack_error.prj b/test/data/issues/518_519_shp_reading/max_callstack_error.prj new file mode 100644 index 000000000..3979d146e --- /dev/null +++ b/test/data/issues/518_519_shp_reading/max_callstack_error.prj @@ -0,0 +1 @@ +PROJCS["RT90_25_gon_V",GEOGCS["GCS_RT_1990",DATUM["D_RT_1990",SPHEROID["Bessel_1841",6377397.155,299.1528128]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",1500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",15.8082777777778],PARAMETER["Scale_Factor",1.0],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]] \ No newline at end of file diff --git a/test/data/issues/518_519_shp_reading/max_callstack_error.shp b/test/data/issues/518_519_shp_reading/max_callstack_error.shp new file mode 100644 index 0000000000000000000000000000000000000000..87a8c973d016f32fd53163910c8427d4b83e6c8b GIT binary patch literal 12244 zcmai)d0dV89>=4UEn7^9kSMan&~uIver2u<*=gpq-D+gt$u=<~OCd{kDI}wjd4K&FB4j|333{dY;oM3k$c(7G?fpfA6lP zg$4ae+A>QxpAy&$Zvn+1&cGmqRYLVRrJ!vbUiU}h0EP2n3Y%tEA_TwDxl^(F@lD7i(B6EM- z>$?c@?jMue>IHehaxYk9VA17XWR7#zzi3U~F+~k$2AVVlmV3b>1B)(st+7{T;a)Sk zuMW^ujq|napA_J$?g1EGGwQ>dmc;eI12Yk0q}S_QF%S@0ey^)BP-cqhwn_bAyYT9<ux;cy+qz@cfZ1hx!QxD?=yLCd%`;=J zMrsNf*oi4SlPldZzYfU2q6?P1Rj^lPPC?-*ZC7jRRM^qER)ywXJ(hc==f#tPda((b zI_|tYd&QsTUOkq)Rk7E#d)%JJ%mc4&VgDNsgcljkL>Dagn%YltSiDwKcHJAL?jet4 z`S+X5WS6|Q*n56rszoe$6YP$)3MH>(xmRYg%e_{~Dq9@rW z>GcW^*p$t|C)0!%8CZ0|a_{eJihr)VOjF3f7PfJ;34da~7a3S|!IHNI_6AuFxD>KP zQ#(3m^&3PU$@1$UGuh?dg>wRK4w_f$1sim+pm^rxGVof=98`683@anPL3y))3>hJ*aZ@ zKNB9wa_@@#)T)Yk!0wKJGQ*m#3*UHK|GJ8N3!H~!d{u}J^%g3gWk~RQMN@79}2G?%e~UG_0xfo zo|O&CCDgY#Ry>FFSn@W&Ug^2$(q~iM`UZ6=!<0Ik?ic*)rpI!x^yClDIkCElK~+4J zx$W=^bFUst-iFvKJtYCZIe9iUsD2@;@iXDoW4Tv);;fRKzI8XK^=Dc|zIXjvb@NeI{tF>oBjrMdW%&fTTAojvGhomye`-)v-`ch27htgpf;YJ9lO4w zxmS>Ta|iw$7WGSvvastdcdMfUU%$m^|kY{7+0g3HmIOQI`e|%Ug?ot?rmE0am69EOTAzd zEgUjWh|lejVFS8g$=d>ZHw^7k!N<<19$Rioio9x4lI444CcE5QxqEH5t2Yg5!-Ua4 zbs(=~xmRYgOWu~)t2LgOaQbAa7p!me=AHGfn?K)>VFS8g%km<#Y`wwHrG(y3W>*dS zQhdHHJ(4AFE9{lo%Dm)t*Xir{c=^iL_2@ptuY(@Ty*59tS(o(6pt>##@(y`uQXlfl zOm@lJ8hbOcqJv84{~KPghaVKL9wqi71B)(L?!A}R=1SosgBp3%e(e_8D_MRWWG1`h zZG*isyE>i8Z6hD_MRWWG1`hZHv7b2hwgkFfZ85 zbqD6f(mC9V3>(k|%f0Oy8MpK(Fes0yd9TNkN3z^2Gub6?JM3+FdePLNyQN;RTdx%d zjuU&4VFS8gxwm)qf@i1h(9eOnLmx(vN3wjc%w(6mA7gJs_;&@SEQ1=7+obhT`hA6D zxmRYgTb4IBs7D9SdnV=K?X>m5zS48JS9-vrOWyX_+jC)1__BC|^3Uv2(U;yoNS1qL zCcE6bbo5GV>tzP@d_s8(19>IOy)u(s@^-*pWc?2Z9CaXXmUp!l4cIx{3lQ zS5coi2DSB7;I97kK1#COD>K<8uZF#Iw@0RhMwfcQF8(6-#oKeZ7a2C73zmDkt*-Fg zevm;S0~>h5+t(!iUIQ6ebitC>fW0y^+Dxk5n$8*8yJqkd;nicgw_>wlM;zOhdf@$6 zWW&1Oi}w%6a3;E7$!o;kW#f(2YLOQi*jqpCoPSt&k%2`QEcZ_7dMInHhe06&dwh4p zqFmuc1{PhgRl>9O3~@1w7#&u1QZGdq+kE)ia2I1^p4 zr&`*RN3wjc%w(5)A34`MGn#q9w%oEQzMa^M3>(k|OWv;7`+AdS zMi%}3;ppI=YwM6lvV5=1WS4ur{VSdB-lx=Qq>T&l^$}5B`<@Zcv=Vy)u(s@_J)$v|Z$`PsnTC zMLXV;yprW!naS>lygyox*%?9Sd^6~POEjIsz0xCD^7>$J-e>KHSuqdTO3%~P3gJbD zGtmXhy_(;U#l!1q3K`gimU*rR#ODOaz@iJ5yxp)j+&F3EMMq5`1N*?Q+Qw_biwrEf dV7WIWBWhqmZB31Pez)9i@<^6nUzy47e*qnM5t0A^ literal 0 HcmV?d00001 diff --git a/test/data/issues/518_519_shp_reading/max_callstack_error.shx b/test/data/issues/518_519_shp_reading/max_callstack_error.shx new file mode 100644 index 0000000000000000000000000000000000000000..92c0234a55321fac3d4ffee71cc7748d9aa98945 GIT binary patch literal 1204 zcmZw8F-QUd6vpw(%IYjDD}x3Hhl&OV2_dQA93mVX3>+>H5^`{8aBvW6aBy&NxWS>p zp`oF`py31uhXw}=hXw}+AqNWw1BZeJ$$0-I@bNx=ybqqDY^loFvFIO@iZWW|t@yBi z@a@yycMY@rqHE=|^W&4tJ+1Zpd7rYKYGdF3m)_C2JxUaFXktg4u%jR2XkZn4qQ!{; zyufR$;Xs^pVGvVzgAM!>X*cPoS`Y?uhtYBB1ao`~)uz*k4 z7iTx{2$NXEI(~_BZVX`>Z}Am}qPl|-%wP#yqRon4jA9N=?1=Mr^kWJFLBk4Axz^fzT!|^+rbECu!JqqX+ Date: Tue, 14 Dec 2021 00:36:59 -0500 Subject: [PATCH 132/891] Added -shapes command --- src/buffer/mapshaper-buffer-common.js | 1 - src/cli/mapshaper-options.js | 55 ++++++++++----- src/cli/mapshaper-run-command.js | 6 +- src/commands/mapshaper-buffer.js | 13 ++-- src/commands/mapshaper-shapes.js | 97 ++++++++++++++++++++++++-- src/dataset/mapshaper-dataset-utils.js | 49 ++++++++----- src/dataset/mapshaper-layer-utils.js | 1 + src/dataset/mapshaper-merging.js | 10 ++- 8 files changed, 179 insertions(+), 53 deletions(-) diff --git a/src/buffer/mapshaper-buffer-common.js b/src/buffer/mapshaper-buffer-common.js index 7c339f66b..b7bf8913f 100644 --- a/src/buffer/mapshaper-buffer-common.js +++ b/src/buffer/mapshaper-buffer-common.js @@ -100,7 +100,6 @@ export function getBufferToleranceFunction(dataset, opts) { if (constTol) return constTol; return constTol ? constTol : meterDist * pctOfRadius; }; - } export function getBufferDistanceFunction(lyr, dataset, opts) { diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 6104cce6f..2ea309770 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -968,6 +968,13 @@ export function getOptionParser() { }) .option('target', targetOpt); + parser.command('include') + .describe('import JS data and functions for use in JS expressions') + .option('file', { + DEFAULT: true, + describe: 'file containing a JS object with key:value pairs to import' + }); + parser.command('inlay') .describe('inscribe a polygon layer inside another polygon layer') .option('source', { @@ -1280,6 +1287,37 @@ export function getOptionParser() { }) .option('target', targetOpt); + parser.command('shapes') + .describe('convert points to polygons, circles or stars') + .option('type', { + describe: 'type of shape (e.g. star, polygon, circle)' + }) + .option('radius', { + describe: 'distance from center to farthest point on the shape', + type: 'distance' + }) + .option('units', { + describe: 'geographical units of radius parameter (e.g. km)' + }) + .option('sides', { + describe: 'number of sides; five-pointed stars have 10 sides', + type: 'number' + }) + .option('rotation', { + describe: 'rotation of the shape in degrees', + type: 'number' + }) + .option('orientation', { + describe: 'use orientation=b for a rotated orientation' + }) + .option('star-ratio', { + describe: 'ratio of major to minor radius of stars', + type: 'number' + }) + .option('name', nameOpt) + .option('target', targetOpt) + .option('no-replace', noReplaceOpt); + parser.command('simplify') .validate(V.validateSimplifyOpts) .example('Retain 10% of removable vertices\n$ mapshaper input.shp -simplify 10%') @@ -1688,13 +1726,6 @@ export function getOptionParser() { }) .option('name', nameOpt); - parser.command('include') - .describe('import JS data and functions for use in JS expressions') - .option('file', { - DEFAULT: true, - describe: 'file containing a JS object with key:value pairs to import' - }); - parser.command('fuzzy-join') .describe('join points to polygons, with data fill and fuzzy match') .option('source', { @@ -1830,16 +1861,6 @@ export function getOptionParser() { }) .option('name', nameOpt); - // parser.command('shapes') - // .describe('convert points to shapes') - // .option('type', { - // }) - // .option('size', { - // }) - // .option('rotation', { - // }) - - parser.command('subdivide') .describe('recursively split a layer using a JS expression') .validate(V.validateExpressionOpt) diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index 01a8f935a..a504e82f4 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -65,6 +65,8 @@ import '../commands/mapshaper-require'; import '../commands/mapshaper-rotate'; import '../commands/mapshaper-run'; import '../commands/mapshaper-scalebar'; +import '../commands/mapshaper-shape'; +import '../commands/mapshaper-shapes'; import '../commands/mapshaper-simplify'; import '../commands/mapshaper-sort'; import '../commands/mapshaper-snap'; @@ -76,7 +78,6 @@ import '../commands/mapshaper-union'; import '../commands/mapshaper-uniq'; import '../io/mapshaper-file-import'; import '../commands/mapshaper-merge-layers'; -import '../commands/mapshaper-shape'; import '../simplify/mapshaper-variable-simplify'; import '../commands/mapshaper-split-on-grid'; import '../commands/mapshaper-subdivide'; @@ -368,6 +369,9 @@ export function runCommand(command, catalog, cb) { } else if (name == 'shape') { catalog.addDataset(cmd.shape(targetDataset, opts)); + } else if (name == 'shapes') { + outputLayers = applyCommandToEachLayer(cmd.shapes, targetLayers, targetDataset, opts); + } else if (name == 'simplify') { if (opts.variable) { cmd.variableSimplify(targetLayers, targetDataset, opts); diff --git a/src/commands/mapshaper-buffer.js b/src/commands/mapshaper-buffer.js index 1106cd18a..38cb14871 100644 --- a/src/commands/mapshaper-buffer.js +++ b/src/commands/mapshaper-buffer.js @@ -3,6 +3,7 @@ import { makePolygonBuffer } from '../buffer/mapshaper-polygon-buffer'; import { makePolylineBuffer } from '../buffer/mapshaper-polyline-buffer'; import { makePointBuffer } from '../buffer/mapshaper-point-buffer'; import { setOutputLayerName } from '../dataset/mapshaper-layer-utils'; +import { mergeOutputLayerIntoDataset } from '../dataset/mapshaper-dataset-utils'; import { stop } from '../utils/mapshaper-logging'; import cmd from '../mapshaper-cmd'; @@ -14,7 +15,7 @@ import cmd from '../mapshaper-cmd'; cmd.buffer = makeBufferLayer; function makeBufferLayer(lyr, dataset, opts) { - var dataset2, lyr2; + var dataset2; if (lyr.geometry_type == 'point') { dataset2 = makePointBuffer(lyr, dataset, opts); } else if (lyr.geometry_type == 'polyline') { @@ -24,12 +25,8 @@ function makeBufferLayer(lyr, dataset, opts) { } else { stop("Unsupported geometry type"); } - var outputLayers = mergeDatasetsIntoDataset(dataset, [dataset2]); - lyr2 = outputLayers[0]; - setOutputLayerName(lyr2, lyr, null, opts); - if (lyr.data && !lyr2.data) { - lyr2.data = opts.no_replace ? lyr.data.clone() : lyr.data; - } - return outputLayers; + + var lyr2 = mergeOutputLayerIntoDataset(lyr, dataset, dataset2, opts); + return [lyr2]; } diff --git a/src/commands/mapshaper-shapes.js b/src/commands/mapshaper-shapes.js index d57e4311d..ac3748e30 100644 --- a/src/commands/mapshaper-shapes.js +++ b/src/commands/mapshaper-shapes.js @@ -1,23 +1,106 @@ import { stop } from '../utils/mapshaper-logging'; import cmd from '../mapshaper-cmd'; import { getDatasetCRS, getCRS, requireProjectedDataset } from '../crs/mapshaper-projections'; +import { requirePointLayer } from '../dataset/mapshaper-layer-utils'; +import { importGeoJSON } from '../geojson/geojson-import'; +import { getPointBufferPolygon, getPointBufferCoordinates } from '../buffer/mapshaper-point-buffer'; +import { getBufferDistanceFunction } from '../buffer/mapshaper-buffer-common'; +import { mergeOutputLayerIntoDataset } from '../dataset/mapshaper-dataset-utils'; +import { getGeodeticSegmentFunction } from '../geom/mapshaper-geodesic'; +import { getAffineTransform } from '../commands/mapshaper-affine'; cmd.shapes = function(lyr, dataset, opts) { requireProjectedDataset(dataset); + requirePointLayer(lyr); + var type = opts.type || 'polygon'; + var sides = opts.sides || getDefaultSides(type); + var rotation = +opts.rotation || 0; + var distanceFn = getBufferDistanceFunction(lyr, dataset, opts); + var crs = getDatasetCRS(dataset); + var geod = getGeodeticSegmentFunction(crs); + var geometries = lyr.shapes.map(function(shape, i) { + var dist = distanceFn(i); + if (!dist || !shape) return null; + return getMultiPolygon(shape, geod, dist, sides, rotation, opts); + }); + var geojson = { + type: 'GeometryCollection', + geometries: geometries + }; + var dataset2 = importGeoJSON(geojson); + var lyr2 = mergeOutputLayerIntoDataset(lyr, dataset, dataset2, opts); + return [lyr2]; }; -function makeShape(type, center, opts) { - +function getDefaultSides(type) { + return { + star: 10, + circle: 72, + triangle: 3, + square: 4, + pentagon: 5, + hexagon: 6, + heptagon: 7, + octagon: 8, + nonagon: 9, + decagon: 10 + }[type] || 4; } -function makeCircle(center, opts) { - +function getMultiPolygon(shape, geod, dist, sides, rotation, opts) { + var geom = { + type: 'MultiPolygon', + coordinates: [] + }; + var coords; + for (var i=0; i= 3 === false) { + stop(`Invalid number of sides (${sides})`); + } + var coords = [], + angle = 360 / sides, + b = isStar ? 1 : 0.5, + theta, even, len; + if (opts.orientation == 'b') { + b = 0; + } + for (var i=0; i 0) { + mergedArcs = mergeArcs(arcSources); + if (mergedArcs.size() != arcCount) { + error("[mergeDatasets()] Arc indexing error"); + } } return { @@ -108,6 +110,8 @@ function mergeDatasetInfo(merged, dataset) { } export function mergeArcs(arr) { + // Returning the original causes a test to fail + // if (arr.length < 2) return arr[0]; var dataArr = arr.map(function(arcs) { if (arcs.getRetainedInterval() > 0) { verbose("Baking-in simplification setting."); From 6acdf8b841cc56995f6c67976d8d5f9998b37d03 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 19 Dec 2021 09:22:34 -0500 Subject: [PATCH 133/891] v0.5.80 --- CHANGELOG.md | 3 + package-lock.json | 2 +- package.json | 2 +- src/cli/mapshaper-options.js | 111 ++++++++------ src/cli/mapshaper-run-command.js | 3 +- src/commands/mapshaper-scalebar.js | 2 +- src/commands/mapshaper-shapes.js | 106 ------------- src/commands/mapshaper-symbols.js | 119 +++++++++++---- src/dataset/mapshaper-layer-utils.js | 3 +- src/expressions/mapshaper-feature-proxy.js | 7 + src/geom/mapshaper-basic-geom.js | 8 +- src/geom/mapshaper-geom-constants.js | 12 ++ src/geom/mapshaper-polygon-geom.js | 14 +- src/svg/mapshaper-svg-arrows.js | 108 -------------- ...ic-symbols.js => mapshaper-svg-symbols.js} | 0 src/svg/svg-common.js | 1 - src/svg/svg-path-utils.js | 28 ---- src/svg/svg-properties.js | 11 +- src/symbols/mapshaper-arrow-symbols.js | 139 ++++++++++++++++++ src/symbols/mapshaper-basic-symbols.js | 49 ++++++ src/symbols/mapshaper-symbol-utils.js | 73 +++++++++ test/svg-path-utils-test.js | 13 -- test/symbol-utils-test.js | 13 ++ test/symbols-test.js | 22 --- 24 files changed, 485 insertions(+), 364 deletions(-) delete mode 100644 src/commands/mapshaper-shapes.js create mode 100644 src/geom/mapshaper-geom-constants.js delete mode 100644 src/svg/mapshaper-svg-arrows.js rename src/svg/{mapshaper-basic-symbols.js => mapshaper-svg-symbols.js} (100%) create mode 100644 src/symbols/mapshaper-arrow-symbols.js create mode 100644 src/symbols/mapshaper-basic-symbols.js create mode 100644 src/symbols/mapshaper-symbol-utils.js delete mode 100644 test/svg-path-utils-test.js create mode 100644 test/symbol-utils-test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b7f186d8..9c0f40934 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.80 +* Added arrows, stars and polygons to undocumented -symbols command. + v0.5.79 * More permissive importing of some non-standard Shapefiles. diff --git a/package-lock.json b/package-lock.json index c7dd1d16c..776606a5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.79", + "version": "0.5.80", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 1c87ae8c0..3ba17edfe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.79", + "version": "0.5.80", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 2ea309770..74367bdde 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1287,36 +1287,6 @@ export function getOptionParser() { }) .option('target', targetOpt); - parser.command('shapes') - .describe('convert points to polygons, circles or stars') - .option('type', { - describe: 'type of shape (e.g. star, polygon, circle)' - }) - .option('radius', { - describe: 'distance from center to farthest point on the shape', - type: 'distance' - }) - .option('units', { - describe: 'geographical units of radius parameter (e.g. km)' - }) - .option('sides', { - describe: 'number of sides; five-pointed stars have 10 sides', - type: 'number' - }) - .option('rotation', { - describe: 'rotation of the shape in degrees', - type: 'number' - }) - .option('orientation', { - describe: 'use orientation=b for a rotated orientation' - }) - .option('star-ratio', { - describe: 'ratio of major to minor radius of stars', - type: 'number' - }) - .option('name', nameOpt) - .option('target', targetOpt) - .option('no-replace', noReplaceOpt); parser.command('simplify') .validate(V.validateSimplifyOpts) @@ -1556,26 +1526,81 @@ export function getOptionParser() { .option('target', targetOpt); parser.command('symbols') - // .describe('generate a variety of SVG symbols') + // .describe('symbolize points as polygons, circles, stars or arrows') .option('type', { - describe: 'symbol type' + describe: 'symbol type (e.g. star, polygon, circle, arrow)' + }) + .option('scale', { + describe: 'scale symbols by a factor', + type: 'number' + }) + .option('pixel-scale', { + describe: 'symbol scale in meters-per-pixel (see polygons option)', + type: 'number', + }) + .option('polygons', { + describe: 'generate symbols as polygons instead of SVG objects', + type: 'flag' + }) + .option('radius', { + describe: 'distance from center to farthest point on the symbol', + type: 'distance' + }) + .option('sides', { + describe: 'sides of a polygon or star symbol', + type: 'number' + }) + .option('rotation', { + describe: 'rotation of symbol in degrees' + }) + .option('orientation', { + describe: 'use orientation=b for a rotated or flipped orientation' + }) + .option('length', { + // alias for arrow-length + }) + .option('star-ratio', { + describe: 'ratio of major to minor radius of star', + type: 'number' + }) + .option('arrow-length', { + describe: 'length of arrows in pixels (use with type=arrow)' + }) + .option('arrow-direction', { + describe: 'angle off of vertical (-90 = left-pointing arrow)' + }) + .option('arrow-head-angle', { + describe: 'angle of tip of arrow (default is 40 degrees)' + }) + .option('arrow-head-width', { + describe: 'size of arrow head from side to side' + }) + .option('arrow-stem-width', { + describe: 'width of stem at its widest point' + }) + .option('arrow-stem-taper', { + describe: 'factor for tapering the width of the stem' + }) + .option('arrow-stem-curve', { + describe: 'curvature in degrees (arrows are straight by default)' + }) + .option('arrow-min-stem', { + describe: 'min ratio of stem to total length (for small arrows)', + type: 'number' }) .option('stroke', {}) .option('stroke-width', {}) - .option('fill', {}) - .option('length', {}) - .option('rotation', {}) + .option('fill', { + describe: 'symbol fill color' + }) .option('effect', {}) - .option('arrow-head-angle', {}) - .option('arrow-stem-width', {}) - .option('arrow-head-width', {}) - .option('arrow-stem-curve', {}) - .option('arrow-stem-taper', {}) - .option('arrow-scaling', {}) - .option('where', whereOpt) - .option('target', targetOpt); + // .option('where', whereOpt) + .option('name', nameOpt) + .option('target', targetOpt) + .option('no-replace', noReplaceOpt); // .option('name', nameOpt); + parser.command('target') .describe('set active layer (or layers)') .option('target', { diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index a504e82f4..e99199cf6 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -66,7 +66,6 @@ import '../commands/mapshaper-rotate'; import '../commands/mapshaper-run'; import '../commands/mapshaper-scalebar'; import '../commands/mapshaper-shape'; -import '../commands/mapshaper-shapes'; import '../commands/mapshaper-simplify'; import '../commands/mapshaper-sort'; import '../commands/mapshaper-snap'; @@ -401,7 +400,7 @@ export function runCommand(command, catalog, cb) { applyCommandToEachLayer(cmd.svgStyle, targetLayers, targetDataset, opts); } else if (name == 'symbols') { - applyCommandToEachLayer(cmd.symbols, targetLayers, opts); + outputLayers = applyCommandToEachLayer(cmd.symbols, targetLayers, targetDataset, opts); } else if (name == 'subdivide') { outputLayers = applyCommandToEachLayer(cmd.subdivideLayer, targetLayers, arcs, opts.expression); diff --git a/src/commands/mapshaper-scalebar.js b/src/commands/mapshaper-scalebar.js index fc43bd512..4272ee2cf 100644 --- a/src/commands/mapshaper-scalebar.js +++ b/src/commands/mapshaper-scalebar.js @@ -5,7 +5,7 @@ import utils from '../utils/mapshaper-utils'; import { DataTable } from '../datatable/mapshaper-data-table'; import { stop } from '../utils/mapshaper-logging'; // import { symbolRenderers } from '../svg/svg-common'; -import { symbolRenderers } from '../svg/mapshaper-basic-symbols'; +import { symbolRenderers } from '../svg/mapshaper-svg-symbols'; cmd.scalebar = function(catalog, opts) { var frame = findFrameDataset(catalog); diff --git a/src/commands/mapshaper-shapes.js b/src/commands/mapshaper-shapes.js deleted file mode 100644 index ac3748e30..000000000 --- a/src/commands/mapshaper-shapes.js +++ /dev/null @@ -1,106 +0,0 @@ -import { stop } from '../utils/mapshaper-logging'; -import cmd from '../mapshaper-cmd'; -import { getDatasetCRS, getCRS, requireProjectedDataset } from '../crs/mapshaper-projections'; -import { requirePointLayer } from '../dataset/mapshaper-layer-utils'; -import { importGeoJSON } from '../geojson/geojson-import'; -import { getPointBufferPolygon, getPointBufferCoordinates } from '../buffer/mapshaper-point-buffer'; -import { getBufferDistanceFunction } from '../buffer/mapshaper-buffer-common'; -import { mergeOutputLayerIntoDataset } from '../dataset/mapshaper-dataset-utils'; -import { getGeodeticSegmentFunction } from '../geom/mapshaper-geodesic'; -import { getAffineTransform } from '../commands/mapshaper-affine'; - -cmd.shapes = function(lyr, dataset, opts) { - requireProjectedDataset(dataset); - requirePointLayer(lyr); - var type = opts.type || 'polygon'; - var sides = opts.sides || getDefaultSides(type); - var rotation = +opts.rotation || 0; - var distanceFn = getBufferDistanceFunction(lyr, dataset, opts); - var crs = getDatasetCRS(dataset); - var geod = getGeodeticSegmentFunction(crs); - var geometries = lyr.shapes.map(function(shape, i) { - var dist = distanceFn(i); - if (!dist || !shape) return null; - return getMultiPolygon(shape, geod, dist, sides, rotation, opts); - }); - var geojson = { - type: 'GeometryCollection', - geometries: geometries - }; - var dataset2 = importGeoJSON(geojson); - var lyr2 = mergeOutputLayerIntoDataset(lyr, dataset, dataset2, opts); - return [lyr2]; -}; - -function getDefaultSides(type) { - return { - star: 10, - circle: 72, - triangle: 3, - square: 4, - pentagon: 5, - hexagon: 6, - heptagon: 7, - octagon: 8, - nonagon: 9, - decagon: 10 - }[type] || 4; -} - -function getMultiPolygon(shape, geod, dist, sides, rotation, opts) { - var geom = { - type: 'MultiPolygon', - coordinates: [] - }; - var coords; - for (var i=0; i= 3 === false) { - stop(`Invalid number of sides (${sides})`); - } - var coords = [], - angle = 360 / sides, - b = isStar ? 1 : 0.5, - theta, even, len; - if (opts.orientation == 'b') { - b = 0; - } - for (var i=0; i 0 === false) return null; + coords = constructor(size, d, opts); + rotateCoords(coords, +d.rotation || 0); + if (!polygonMode) { + flipY(coords); + } + if (+opts.scale) { + scaleAndShiftCoords(coords, +opts.scale, [0, 0]); + } + if (polygonMode) { + scaleAndShiftCoords(coords, metersPerPx, shp[0]); + if (d.tfill) rec.fill = d.fill; + return createGeometry(coords); + } else { + rec['svg-symbol'] = makeSvgSymbol(coords, d); } }); + + var outputLyr, dataset2; + if (polygonMode) { + dataset2 = importGeometries(geometries, records); + outputLyr = mergeOutputLayerIntoDataset(inputLyr, dataset, dataset2, opts); + outputLyr.data = lyr.data; + } else { + outputLyr = lyr; + } + return [outputLyr]; }; +function importGeometries(geometries, records) { + var features = geometries.map(function(geom, i) { + var d = records[i]; + return { + type: 'Feature', + properties: records[i] || null, + geometry: geom + }; + }); + var geojson = { + type: 'FeatureCollection', + features: features + }; + return importGeoJSON(geojson); +} + +function createGeometry(coords) { + return { + type: 'Polygon', + coordinates: coords + }; +} + +function getMetersPerPixel(lyr, dataset) { + + // TODO: handle single point, no extent + var bounds = getLayerBounds(lyr); + return bounds.width() / 800; +} + // Returns an svg-symbol data object for one symbol -export function buildSymbol(properties) { - var type = properties.type; - var f = symbolBuilders[type]; - if (!type) { - stop('Missing required "type" parameter'); - } else if (!f) { - stop('Unknown symbol type:', type); - } - return f(properties); +export function makeSvgSymbol(coords, properties) { + roundCoordsForSVG(coords); + return { + type: 'polygon', + coordinates: coords, + fill: properties.fill || 'magenta' + }; } diff --git a/src/dataset/mapshaper-layer-utils.js b/src/dataset/mapshaper-layer-utils.js index 6fc1e6747..0ed56c94d 100644 --- a/src/dataset/mapshaper-layer-utils.js +++ b/src/dataset/mapshaper-layer-utils.js @@ -130,7 +130,7 @@ export function requirePointLayer(lyr, msg) { export function requireSinglePointLayer(lyr, msg) { requirePointLayer(lyr); if (countMultiPartFeatures(lyr) > 0) { - stop(msg || 'This command requires single points'); + stop(msg || 'This command requires single points; layer contains multi-point features.'); } } @@ -262,6 +262,7 @@ export function countArcsInLayers(layers, arcs) { return counts; } +// Returns a Bounds object export function getLayerBounds(lyr, arcs) { var bounds = null; if (lyr.geometry_type == 'point') { diff --git a/src/expressions/mapshaper-feature-proxy.js b/src/expressions/mapshaper-feature-proxy.js index 088320c26..3041c1495 100644 --- a/src/expressions/mapshaper-feature-proxy.js +++ b/src/expressions/mapshaper-feature-proxy.js @@ -5,6 +5,7 @@ import { layerHasPaths, layerHasPoints } from '../dataset/mapshaper-layer-utils' import { addLayerGetters } from '../expressions/mapshaper-layer-proxy'; import { addGetters } from '../expressions/mapshaper-expression-utils'; import { stop } from '../utils/mapshaper-logging'; +import { WGS84 } from '../geom/mapshaper-geom-constants'; import geom from '../geom/mapshaper-geom'; import utils from '../utils/mapshaper-utils'; @@ -89,6 +90,12 @@ export function initFeatureProxy(lyr, arcs, optsArg) { area: function() { return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs); }, + // area2: function() { + // return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs, WGS84.SEMIMINOR_RADIUS); + // }, + // area3: function() { + // return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs, WGS84.AUTHALIC_RADIUS); + // }, perimeter: function() { return geom.getShapePerimeter(_ids, arcs); }, diff --git a/src/geom/mapshaper-basic-geom.js b/src/geom/mapshaper-basic-geom.js index a2614213e..d0e464929 100644 --- a/src/geom/mapshaper-basic-geom.js +++ b/src/geom/mapshaper-basic-geom.js @@ -1,6 +1,8 @@ -// TODO: remove this constant, use actual data from dataset CRS -// also consider using ellipsoidal formulas when appropriate -export var R = 6378137; +import { WGS84 } from './mapshaper-geom-constants'; + +// TODO: remove this constant, use actual data from dataset CRS, +// also consider using ellipsoidal formulas where greater accuracy might be important. +export var R = WGS84.SEMIMAJOR_AXIS; export var D2R = Math.PI / 180; export var R2D = 180 / Math.PI; diff --git a/src/geom/mapshaper-geom-constants.js b/src/geom/mapshaper-geom-constants.js new file mode 100644 index 000000000..3b10637e5 --- /dev/null +++ b/src/geom/mapshaper-geom-constants.js @@ -0,0 +1,12 @@ + +var WGS84 = { + // https://en.wikipedia.org/wiki/Earth_radius + SEMIMAJOR_AXIS: 6378137, + SEMIMINOR_AXIS: 6356752.3142, + AUTHALIC_RADIUS: 6371007.2, + VOLUMETRIC_RADIUS: 6371000.8 +}; + +export { + WGS84 +}; diff --git a/src/geom/mapshaper-polygon-geom.js b/src/geom/mapshaper-polygon-geom.js index aeba727bb..43c600ede 100644 --- a/src/geom/mapshaper-polygon-geom.js +++ b/src/geom/mapshaper-polygon-geom.js @@ -2,6 +2,7 @@ import { error } from '../utils/mapshaper-logging'; import { forEachSegmentInPath } from '../paths/mapshaper-path-utils'; import { calcPathLen } from '../geom/mapshaper-path-geom'; +import { WGS84 } from '../geom/mapshaper-geom-constants'; // A compactness measure designed for testing electoral districts for gerrymandering. // Returns value in [0-1] range. 1 = perfect circle, 0 = collapsed polygon @@ -35,12 +36,12 @@ export function getPlanarShapeArea(shp, arcs) { }, 0); } -export function getSphericalShapeArea(shp, arcs) { +export function getSphericalShapeArea(shp, arcs, R) { if (arcs.isPlanar()) { error("[getSphericalShapeArea()] Function requires decimal degree coordinates"); } return (shp || []).reduce(function(area, ids) { - return area + getSphericalPathArea(ids, arcs); + return area + getSphericalPathArea(ids, arcs, R); }, 0); } @@ -157,16 +158,17 @@ export function getPathArea(ids, arcs) { return (arcs.isPlanar() ? getPlanarPathArea : getSphericalPathArea)(ids, arcs); } -export function getSphericalPathArea(ids, arcs) { +export function getSphericalPathArea(ids, arcs, R) { var iter = arcs.getShapeIter(ids); - return getSphericalPathArea2(iter); + return getSphericalPathArea2(iter, R); } -export function getSphericalPathArea2(iter) { +export function getSphericalPathArea2(iter, R) { var sum = 0, started = false, deg2rad = Math.PI / 180, x, y, xp, yp; + R = R || WGS84.SEMIMAJOR_AXIS; while (iter.hasNext()) { x = iter.x * deg2rad; y = Math.sin(iter.y * deg2rad); @@ -178,7 +180,7 @@ export function getSphericalPathArea2(iter) { xp = x; yp = y; } - return sum / 2 * 6378137 * 6378137; + return sum / 2 * R * R; } // Get path area from an array of [x, y] points diff --git a/src/svg/mapshaper-svg-arrows.js b/src/svg/mapshaper-svg-arrows.js deleted file mode 100644 index 170ba8385..000000000 --- a/src/svg/mapshaper-svg-arrows.js +++ /dev/null @@ -1,108 +0,0 @@ - -import { symbolBuilders } from '../svg/svg-common'; -import { addBezierArcControlPoints } from '../svg/svg-path-utils'; -import { getAffineTransform } from '../commands/mapshaper-affine'; - -symbolBuilders.arrow = function(d) { - var len = 'length' in d ? d.length : 10; - var filled = 'fill' in d; - return filled ? getFilledArrow(d, len) : getStickArrow(d, len); -}; - -function getStickArrow(d, len) { - return { - type: 'polyline', - coordinates: getStickArrowCoords(d, len), - stroke: d.stroke || 'magenta', - 'stroke-width': 'stroke-width' in d ? d['stroke-width'] : 1 - }; -} - -function getFilledArrow(d, totalLen) { - return { - type: 'polygon', - coordinates: getFilledArrowCoords(d, totalLen), - fill: d.fill || 'magenta' - }; -} - -function getScale(totalLen, headLen) { - var maxHeadPct = 0.60; - var headPct = headLen / totalLen; - if (headPct > maxHeadPct) { - return maxHeadPct / headPct; - } - return 1; -} - -function getStickArrowTip(totalLen, curve) { - // curve/2 intersects the arrowhead at 90deg (trigonometry) - var theta = Math.abs(curve/2) / 180 * Math.PI; - var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1); - var dy = totalLen * Math.cos(theta); - return [dx, dy]; -} - -function addPoints(a, b) { - return [a[0] + b[0], a[1] + b[1]]; -} - -function getStickArrowCoords(d, totalLen) { - var headAngle = d['arrow-head-angle'] || 90; - var curve = d['arrow-stem-curve'] || 0; - var unscaledHeadWidth = d['arrow-head-width'] || 9; - var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle); - var scale = getScale(totalLen, unscaledHeadLen); // scale down small arrows - var headWidth = unscaledHeadWidth * scale; - var headLen = unscaledHeadLen * scale; - var tip = getStickArrowTip(totalLen, curve); - var stem = [[0, 0], tip.concat()]; - if (curve) { - addBezierArcControlPoints(stem, curve); - } - if (!headLen) return [stem]; - var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)]; - - rotateSymbolCoords(stem, d.rotation); - rotateSymbolCoords(head, d.rotation); - return [stem, head]; -} - -function getHeadLength(headWidth, headAngle) { - var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio - return headWidth * headRatio; -} - -function getFilledArrowCoords(d, totalLen) { - var headAngle = d['arrow-head-angle'] || 40, - unscaledStemWidth = d['arrow-stem-width'] || 2, - unscaledHeadWidth = d['arrow-head-width'] || unscaledStemWidth * 3, - unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle), - scale = getScale(totalLen, unscaledHeadLen), // scale down small arrows - headWidth = unscaledHeadWidth * scale, - headLen = unscaledHeadLen * scale, - stemWidth = unscaledStemWidth * scale, - stemTaper = d['arrow-stem-taper'] || 0, - stemLen = totalLen - headLen; - - var headDx = headWidth / 2, - stemDx = stemWidth / 2, - baseDx = stemDx * (1 - stemTaper); - - var coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen], - [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]]; - - rotateSymbolCoords(coords, d.rotation); - return [coords]; -} - -export function rotateSymbolCoords(coords, rotation) { - // TODO: consider avoiding re-instantiating function on every call - var f = getAffineTransform(rotation || 0, 1, [0, 0], [0, 0]); - coords.forEach(function(p) { - var p2 = f ? f(p[0], p[1]) : p; - p[0] = p2[0]; - p[1] = -p2[1]; // flip y-axis (to produce display coords) - }); -} - diff --git a/src/svg/mapshaper-basic-symbols.js b/src/svg/mapshaper-svg-symbols.js similarity index 100% rename from src/svg/mapshaper-basic-symbols.js rename to src/svg/mapshaper-svg-symbols.js diff --git a/src/svg/svg-common.js b/src/svg/svg-common.js index 3df198230..7542efba2 100644 --- a/src/svg/svg-common.js +++ b/src/svg/svg-common.js @@ -1,5 +1,4 @@ import { roundToTenths } from '../geom/mapshaper-rounding'; -export var symbolBuilders = {}; export var symbolRenderers = {}; export function getTransform(xy, scale) { diff --git a/src/svg/svg-path-utils.js b/src/svg/svg-path-utils.js index 69476032a..806c9433a 100644 --- a/src/svg/svg-path-utils.js +++ b/src/svg/svg-path-utils.js @@ -44,31 +44,3 @@ function stringifyBezierArc(coords) { stringifyCP(p2) + stringifyVertex(p2); } -export function findArcCenter(p1, p2, degrees) { - var p3 = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2], // midpoint betw. p1, p2 - tan = 1 / Math.tan(degrees / 180 * Math.PI / 2), - cp = getAffineTransform(90, tan, [0, 0], p3)(p2[0], p2[1]); - return cp; -} - -// export function addBezierArcControlPoints(p1, p2, degrees) { -export function addBezierArcControlPoints(points, degrees) { - // source: https://stackoverflow.com/questions/734076/how-to-best-approximate-a-geometrical-arc-with-a-bezier-curve - var p2 = points.pop(), - p1 = points.pop(), - cp = findArcCenter(p1, p2, degrees), - xc = cp[0], - yc = cp[1], - ax = p1[0] - xc, - ay = p1[1] - yc, - bx = p2[0] - xc, - by = p2[1] - yc, - q1 = ax * ax + ay * ay, - q2 = q1 + ax * bx + ay * by, - k2 = 4/3 * (Math.sqrt(2 * q1 * q2) - q2) / (ax * by - ay * bx); - - points.push(p1); - points.push([xc + ax - k2 * ay, yc + ay + k2 * ax, 'C']); - points.push([xc + bx + k2 * by, yc + by - k2 * bx, 'C']); - points.push(p2); -} diff --git a/src/svg/svg-properties.js b/src/svg/svg-properties.js index e763c0073..469258c87 100644 --- a/src/svg/svg-properties.js +++ b/src/svg/svg-properties.js @@ -36,11 +36,15 @@ var symbolPropertyTypes = utils.extend({ type: null, length: 'number', // e.g. arrow length rotation: 'number', + radius: 'number', + 'arrow-length': 'number', + 'arrow-direction': 'number', 'arrow-head-angle': 'number', 'arrow-head-width': 'number', 'arrow-stem-width': 'number', 'arrow-stem-curve': 'number', // degrees of arc 'arrow-stem-taper': 'number', + 'arrow-min-stem': 'number', 'arrow-scaling': 'number', effect: null // e.g. "fade" }, stylePropertyTypes); @@ -82,8 +86,8 @@ export function getSymbolDataAccessor(lyr, opts) { if (!isSupportedSvgSymbolProperty(svgName)) { return; } - var strVal = opts[optName].trim(); - functions[svgName] = getSymbolPropertyAccessor(strVal, svgName, lyr); + var val = opts[optName]; + functions[svgName] = getSymbolPropertyAccessor(val, svgName, lyr); properties.push(svgName); }); @@ -106,7 +110,8 @@ export function mightBeExpression(str, fields) { return /[(){}.+-/*?:&|=\[]/.test(str); } -export function getSymbolPropertyAccessor(strVal, svgName, lyr) { +export function getSymbolPropertyAccessor(val, svgName, lyr) { + var strVal = String(val).trim(); var typeHint = symbolPropertyTypes[svgName]; var fields = lyr.data ? lyr.data.getFields() : []; var literalVal = null; diff --git a/src/symbols/mapshaper-arrow-symbols.js b/src/symbols/mapshaper-arrow-symbols.js new file mode 100644 index 000000000..15081aec3 --- /dev/null +++ b/src/symbols/mapshaper-arrow-symbols.js @@ -0,0 +1,139 @@ + +import { addBezierArcControlPoints, rotateCoords } from './mapshaper-symbol-utils'; + +export function getStickArrowCoords(d, totalLen) { + var minStemRatio = getMinStemRatio(d); + var headAngle = d['arrow-head-angle'] || 90; + var curve = d['arrow-stem-curve'] || 0; + var unscaledHeadWidth = d['arrow-head-width'] || 9; + var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle); + var scale = getScale(totalLen, unscaledHeadLen, minStemRatio); + var headWidth = unscaledHeadWidth * scale; + var headLen = unscaledHeadLen * scale; + var tip = getStickArrowTip(totalLen, curve); + var stem = [[0, 0], tip.concat()]; + if (curve) { + addBezierArcControlPoints(stem, curve); + } + if (!headLen) return [stem]; + var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)]; + + rotateCoords(stem, d.rotation); + rotateCoords(head, d.rotation); + return [stem, head]; +} + +function getMinStemRatio(d) { + return d['arrow-min-stem'] >= 0 ? d['arrow-min-stem'] : 0.4; +} + +export function getFilledArrowCoords(totalLen, d) { + var minStemRatio = getMinStemRatio(d), + headAngle = d['arrow-head-angle'] || 40, + direction = d.rotation || d['arrow-direction'] || 0, + unscaledStemWidth = d['arrow-stem-width'] || 2, + unscaledHeadWidth = d['arrow-head-width'] || unscaledStemWidth * 3, + unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle), + scale = getScale(totalLen, unscaledHeadLen, minStemRatio), + headWidth = unscaledHeadWidth * scale, + headLen = unscaledHeadLen * scale, + stemWidth = unscaledStemWidth * scale, + stemTaper = d['arrow-stem-taper'] || 0, + stemCurve = d['arrow-stem-curve'] || 0, + stemLen = totalLen - headLen; + + var headDx = headWidth / 2, + stemDx = stemWidth / 2, + baseDx = stemDx * (1 - stemTaper); + + var coords; + + if (!stemCurve || Math.abs(stemCurve) > 90) { + coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen], + [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]]; + } else { + if (direction > 0) stemCurve = -stemCurve; + coords = getCurvedArrowCoords(stemLen, headLen, stemCurve, stemDx, headDx, baseDx); + } + + rotateCoords(coords, direction); + return [coords]; +} + + +function getScale(totalLen, headLen, minStemRatio) { + var maxHeadPct = 1 - minStemRatio; + var headPct = headLen / totalLen; + if (headPct > maxHeadPct) { + return maxHeadPct / headPct; + } + return 1; +} + +function getStickArrowTip(totalLen, curve) { + // curve/2 intersects the arrowhead at 90deg (trigonometry) + var theta = Math.abs(curve/2) / 180 * Math.PI; + var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1); + var dy = totalLen * Math.cos(theta); + return [dx, dy]; +} + +function addPoints(a, b) { + return [a[0] + b[0], a[1] + b[1]]; +} + + +function getHeadLength(headWidth, headAngle) { + var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio + return headWidth * headRatio; +} + +function getCurvedArrowCoords(stemLen, headLen, curvature, stemDx, headDx, baseDx) { + // coordinates go counter clockwise, starting from the leftmost head coordinate + var theta = Math.abs(curvature) / 180 * Math.PI; + var sign = curvature > 0 ? 1 : -1; + var dx = stemLen * Math.sin(theta / 2) * sign; + var dy = stemLen * Math.cos(theta / 2); + var head = [[stemDx + dx, dy], [headDx + dx, dy], + [dx, headLen + dy], [-headDx + dx, dy], [-stemDx + dx, dy]]; + var ax = baseDx * Math.cos(theta); // rotate arrow base + var ay = baseDx * Math.sin(theta) * -sign; + var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta); + var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta); + // if (stemTaper == 1) leftStem.pop(); + var stem = leftStem.concat(rightStem.reverse()); + stem.pop(); + return stem.concat(head); +} + +// ax, ay: point on the base +// bx, by: point on the stem +function getCurvedStemCoords(ax, ay, bx, by, theta0) { + var dx = bx - ax, + dy = by - ay, + dy1 = (dy * dy - dx * dx) / (2 * dy), + dy2 = dy - dy1, + dx2 = Math.sqrt(dx * dx + dy * dy) / 2, + theta = Math.PI - Math.asin(dx2 / dy2) * 2, + degrees = theta * 180 / Math.PI, + radius = dy2 / Math.tan(theta / 2), + leftBend = bx > ax, + sign = leftBend ? 1 : -1, + points = Math.round(degrees / 5) + 2, + // points = theta > 2 && 7 || theta > 1 && 6 || 5, + increment = theta / (points + 1); + + var coords = [[bx, by]]; + for (var i=1; i<= points; i++) { + var phi = i * increment / 2; + var sinPhi = Math.sin(phi); + var cosPhi = Math.cos(phi); + var c = sinPhi * radius * 2; + var a = sinPhi * c; + var b = cosPhi * c; + coords.push([bx - a * sign, by - b]); + } + coords.push([ax, ay]); + return coords; +} + diff --git a/src/symbols/mapshaper-basic-symbols.js b/src/symbols/mapshaper-basic-symbols.js new file mode 100644 index 000000000..5ba07c16a --- /dev/null +++ b/src/symbols/mapshaper-basic-symbols.js @@ -0,0 +1,49 @@ +import { getPlanarSegmentEndpoint } from '../geom/mapshaper-geodesic'; +import { stop } from '../utils/mapshaper-logging'; + +// sides: e.g. 5-pointed star has 10 sides +// radius: distance from center to point +// +export function getPolygonCoords(radius, opts) { + var type = opts.type; + var sides = +opts.sides || getDefaultSides(type); + var isStar = type == 'star'; + if (isStar && (sides < 6 || sides % 2 !== 0)) { + stop(`Invalid number of sides for a star (${sides})`); + } else if (sides >= 3 === false) { + stop(`Invalid number of sides (${sides})`); + } + var coords = [], + angle = 360 / sides, + b = isStar ? 1 : 0.5, + theta, even, len; + if (opts.orientation == 'b') { + b = 0; + } + for (var i=0; i Date: Sun, 19 Dec 2021 16:17:26 -0500 Subject: [PATCH 134/891] Handle defective short curved stems --- src/symbols/mapshaper-arrow-symbols.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/symbols/mapshaper-arrow-symbols.js b/src/symbols/mapshaper-arrow-symbols.js index 15081aec3..58cefd1a7 100644 --- a/src/symbols/mapshaper-arrow-symbols.js +++ b/src/symbols/mapshaper-arrow-symbols.js @@ -1,5 +1,6 @@ import { addBezierArcControlPoints, rotateCoords } from './mapshaper-symbol-utils'; +import { stop } from '../utils/mapshaper-logging'; export function getStickArrowCoords(d, totalLen) { var minStemRatio = getMinStemRatio(d); @@ -40,13 +41,16 @@ export function getFilledArrowCoords(totalLen, d) { stemWidth = unscaledStemWidth * scale, stemTaper = d['arrow-stem-taper'] || 0, stemCurve = d['arrow-stem-curve'] || 0, - stemLen = totalLen - headLen; + stemLen = totalLen - headLen, + coords; var headDx = headWidth / 2, stemDx = stemWidth / 2, baseDx = stemDx * (1 - stemTaper); - var coords; + if (unscaledHeadWidth < unscaledStemWidth) { + stop('Arrow head must be at least as wide as the stem.'); + } if (!stemCurve || Math.abs(stemCurve) > 90) { coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen], @@ -100,15 +104,18 @@ function getCurvedArrowCoords(stemLen, headLen, curvature, stemDx, headDx, baseD var ay = baseDx * Math.sin(theta) * -sign; var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta); var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta); - // if (stemTaper == 1) leftStem.pop(); var stem = leftStem.concat(rightStem.reverse()); - stem.pop(); + // stem.pop(); return stem.concat(head); } // ax, ay: point on the base // bx, by: point on the stem function getCurvedStemCoords(ax, ay, bx, by, theta0) { + // case: curved side intrudes into head (because stem is too short) + if (ay > by) { + return [[ax * by / ay, by]]; + } var dx = bx - ax, dy = by - ay, dy1 = (dy * dy - dx * dx) / (2 * dy), @@ -120,10 +127,9 @@ function getCurvedStemCoords(ax, ay, bx, by, theta0) { leftBend = bx > ax, sign = leftBend ? 1 : -1, points = Math.round(degrees / 5) + 2, - // points = theta > 2 && 7 || theta > 1 && 6 || 5, - increment = theta / (points + 1); + increment = theta / (points + 1), + coords = [[bx, by]]; - var coords = [[bx, by]]; for (var i=1; i<= points; i++) { var phi = i * increment / 2; var sinPhi = Math.sin(phi); From f4be5e5647085bc16f823dd1d69a8eee682f5f8e Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Mon, 20 Dec 2021 16:40:22 -0500 Subject: [PATCH 135/891] v0.5.81 --- CHANGELOG.md | 3 +++ package-lock.json | 2 +- package.json | 2 +- src/cli/mapshaper-option-parsing-utils.js | 4 ++-- src/gui/gui-symbol-dragging2.js | 1 - 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c0f40934..fc8620d66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.81 +* Bug fixes + v0.5.80 * Added arrows, stars and polygons to undocumented -symbols command. diff --git a/package-lock.json b/package-lock.json index 776606a5c..58b650dd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.80", + "version": "0.5.81", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3ba17edfe..b9a58a247 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.80", + "version": "0.5.81", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-option-parsing-utils.js b/src/cli/mapshaper-option-parsing-utils.js index 5c4c542cd..80f222372 100644 --- a/src/cli/mapshaper-option-parsing-utils.js +++ b/src/cli/mapshaper-option-parsing-utils.js @@ -49,9 +49,9 @@ export function cleanArgv(argv) { // Updated: don't trim space from tokens like [delimeter= ] argv = argv.map(function(s) { if (!/= $/.test(s)) { - s = s.trimEnd(); + s = utils.rtrim(s); } - s = s.trimStart(); + s = utils.ltrim(s); return s; }); argv = argv.filter(function(s) {return s !== '';}); // remove empty tokens diff --git a/src/gui/gui-symbol-dragging2.js b/src/gui/gui-symbol-dragging2.js index 4d4f67150..b69efd025 100644 --- a/src/gui/gui-symbol-dragging2.js +++ b/src/gui/gui-symbol-dragging2.js @@ -252,7 +252,6 @@ export function SymbolDragging2(gui, ext, hit) { return parent.querySelector(sel); } - function getTextTarget3(e) { if (e.id > -1 === false || !e.container) return null; return getSymbolNodeById(e.id, e.container); From b2cda2a9ce69b0c7b83f3b13648bf4ef7e3a1b57 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 23 Dec 2021 15:59:28 -0500 Subject: [PATCH 136/891] v0.5.82 --- CHANGELOG.md | 3 + package-lock.json | 2 +- package.json | 2 +- src/cli/mapshaper-options.js | 9 +++ src/commands/mapshaper-symbols.js | 11 ++- src/geom/mapshaper-polygon-geom.js | 6 ++ src/svg/svg-properties.js | 2 + src/symbols/mapshaper-arrow-symbols.js | 99 +++++++++++++++----------- src/symbols/mapshaper-basic-symbols.js | 4 +- 9 files changed, 88 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc8620d66..fc92b21e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.82 +* More options for arrow styling. + v0.5.81 * Bug fixes diff --git a/package-lock.json b/package-lock.json index 58b650dd1..a246b267e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.81", + "version": "0.5.82", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b9a58a247..14daf945f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.81", + "version": "0.5.82", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 74367bdde..455504455 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1575,9 +1575,18 @@ export function getOptionParser() { .option('arrow-head-width', { describe: 'size of arrow head from side to side' }) + .option('arrow-head-length', { + describe: 'length of arrow head (alternative to arrow-head-angle)' + }) + .option('arrow-head-shape', { + // describe: 'options: a b c' + }) .option('arrow-stem-width', { describe: 'width of stem at its widest point' }) + .option('arrow-stem-length', { + describe: 'alternative to arrow-length' + }) .option('arrow-stem-taper', { describe: 'factor for tapering the width of the stem' }) diff --git a/src/commands/mapshaper-symbols.js b/src/commands/mapshaper-symbols.js index 62aeef2b1..d2a6ee566 100644 --- a/src/commands/mapshaper-symbols.js +++ b/src/commands/mapshaper-symbols.js @@ -31,16 +31,13 @@ cmd.symbols = function(inputLyr, dataset, opts) { if (!shp) return null; var d = getSymbolData(i); var rec = records[i] || {}; - var coords, size, constructor; + var coords; if (d.type == 'arrow') { - size = d.radius || d.length || d['arrow-length'] || d.r; - constructor = getFilledArrowCoords; + coords = getFilledArrowCoords(d); } else { - size = d.radius || d.length || d.r; - constructor = getPolygonCoords; + coords = getPolygonCoords(d); } - if (size > 0 === false) return null; - coords = constructor(size, d, opts); + if (!coords) return null; rotateCoords(coords, +d.rotation || 0); if (!polygonMode) { flipY(coords); diff --git a/src/geom/mapshaper-polygon-geom.js b/src/geom/mapshaper-polygon-geom.js index 43c600ede..2fa5e53a7 100644 --- a/src/geom/mapshaper-polygon-geom.js +++ b/src/geom/mapshaper-polygon-geom.js @@ -45,6 +45,12 @@ export function getSphericalShapeArea(shp, arcs, R) { }, 0); } +// export function getEllipsoidalShapeArea(shp, arcs, crs) { +// return (shp || []).reduce(function(area, ids) { +// return area + getEllipsoidalPathArea(ids, arcs, crs); +// }, 0); +// } + // Return true if point is inside or on boundary of a shape // export function testPointInPolygon(x, y, shp, arcs) { diff --git a/src/svg/svg-properties.js b/src/svg/svg-properties.js index 469258c87..16cf57f69 100644 --- a/src/svg/svg-properties.js +++ b/src/svg/svg-properties.js @@ -44,6 +44,8 @@ var symbolPropertyTypes = utils.extend({ 'arrow-stem-width': 'number', 'arrow-stem-curve': 'number', // degrees of arc 'arrow-stem-taper': 'number', + 'arrow-stem-length': 'number', + 'arrow-head-length': 'number', 'arrow-min-stem': 'number', 'arrow-scaling': 'number', effect: null // e.g. "fade" diff --git a/src/symbols/mapshaper-arrow-symbols.js b/src/symbols/mapshaper-arrow-symbols.js index 58cefd1a7..2750d226a 100644 --- a/src/symbols/mapshaper-arrow-symbols.js +++ b/src/symbols/mapshaper-arrow-symbols.js @@ -2,47 +2,63 @@ import { addBezierArcControlPoints, rotateCoords } from './mapshaper-symbol-utils'; import { stop } from '../utils/mapshaper-logging'; -export function getStickArrowCoords(d, totalLen) { - var minStemRatio = getMinStemRatio(d); - var headAngle = d['arrow-head-angle'] || 90; - var curve = d['arrow-stem-curve'] || 0; - var unscaledHeadWidth = d['arrow-head-width'] || 9; - var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle); - var scale = getScale(totalLen, unscaledHeadLen, minStemRatio); - var headWidth = unscaledHeadWidth * scale; - var headLen = unscaledHeadLen * scale; - var tip = getStickArrowTip(totalLen, curve); - var stem = [[0, 0], tip.concat()]; - if (curve) { - addBezierArcControlPoints(stem, curve); - } - if (!headLen) return [stem]; - var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)]; - - rotateCoords(stem, d.rotation); - rotateCoords(head, d.rotation); - return [stem, head]; -} +// export function getStickArrowCoords(d, totalLen) { +// var minStemRatio = getMinStemRatio(d); +// var headAngle = d['arrow-head-angle'] || 90; +// var curve = d['arrow-stem-curve'] || 0; +// var unscaledHeadWidth = d['arrow-head-width'] || 9; +// var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle); +// var scale = getScale(totalLen, unscaledHeadLen, minStemRatio); +// var headWidth = unscaledHeadWidth * scale; +// var headLen = unscaledHeadLen * scale; +// var tip = getStickArrowTip(totalLen, curve); +// var stem = [[0, 0], tip.concat()]; +// if (curve) { +// addBezierArcControlPoints(stem, curve); +// } +// if (!headLen) return [stem]; +// var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)]; + +// rotateCoords(stem, d.rotation); +// rotateCoords(head, d.rotation); +// return [stem, head]; +// } function getMinStemRatio(d) { return d['arrow-min-stem'] >= 0 ? d['arrow-min-stem'] : 0.4; } -export function getFilledArrowCoords(totalLen, d) { - var minStemRatio = getMinStemRatio(d), - headAngle = d['arrow-head-angle'] || 40, +export function getFilledArrowCoords(d) { + var totalLen = d['arrow-length'] || d.radius || d.length || d.r || 0, direction = d.rotation || d['arrow-direction'] || 0, unscaledStemWidth = d['arrow-stem-width'] || 2, unscaledHeadWidth = d['arrow-head-width'] || unscaledStemWidth * 3, - unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle), - scale = getScale(totalLen, unscaledHeadLen, minStemRatio), - headWidth = unscaledHeadWidth * scale, - headLen = unscaledHeadLen * scale, - stemWidth = unscaledStemWidth * scale, + unscaledHeadLen = getHeadLength(unscaledHeadWidth, d), stemTaper = d['arrow-stem-taper'] || 0, - stemCurve = d['arrow-stem-curve'] || 0, - stemLen = totalLen - headLen, - coords; + stemCurve = d['arrow-stem-curve'] || 0; + + var headLen, headWidth, stemLen, stemWidth; + + var scale = 1; + + if (totalLen > 0) { + scale = getScale(totalLen, unscaledHeadLen, getMinStemRatio(d)); + headWidth = unscaledHeadWidth * scale; + headLen = unscaledHeadLen * scale; + stemWidth = unscaledStemWidth * scale; + stemLen = totalLen - headLen; + + } else { + headWidth = unscaledHeadWidth; + headLen = unscaledHeadLen; + stemWidth = unscaledStemWidth; + stemLen = d['arrow-stem-length'] || 0; + totalLen = headLen + stemLen; + } + + if (totalLen > 0 === false) return null; + + var coords; var headDx = headWidth / 2, stemDx = stemWidth / 2, @@ -74,20 +90,23 @@ function getScale(totalLen, headLen, minStemRatio) { return 1; } -function getStickArrowTip(totalLen, curve) { - // curve/2 intersects the arrowhead at 90deg (trigonometry) - var theta = Math.abs(curve/2) / 180 * Math.PI; - var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1); - var dy = totalLen * Math.cos(theta); - return [dx, dy]; -} +// function getStickArrowTip(totalLen, curve) { +// // curve/2 intersects the arrowhead at 90deg (trigonometry) +// var theta = Math.abs(curve/2) / 180 * Math.PI; +// var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1); +// var dy = totalLen * Math.cos(theta); +// return [dx, dy]; +// } function addPoints(a, b) { return [a[0] + b[0], a[1] + b[1]]; } -function getHeadLength(headWidth, headAngle) { +function getHeadLength(headWidth, d) { + var headLength = d['arrow-head-length']; + if (headLength > 0) return headLength; + var headAngle = d['arrow-head-angle'] || 40; var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio return headWidth * headRatio; } diff --git a/src/symbols/mapshaper-basic-symbols.js b/src/symbols/mapshaper-basic-symbols.js index 5ba07c16a..f8ec30bc7 100644 --- a/src/symbols/mapshaper-basic-symbols.js +++ b/src/symbols/mapshaper-basic-symbols.js @@ -4,7 +4,9 @@ import { stop } from '../utils/mapshaper-logging'; // sides: e.g. 5-pointed star has 10 sides // radius: distance from center to point // -export function getPolygonCoords(radius, opts) { +export function getPolygonCoords(opts) { + var radius = opts.radius || opts.length || opts.r; + if (radius > 0 === false) return null; var type = opts.type; var sides = +opts.sides || getDefaultSides(type); var isStar = type == 'star'; From d2ad5cf83a6b2b6a68cf4264e4d1c042af9fb31c Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 30 Dec 2021 09:40:58 -0500 Subject: [PATCH 137/891] Support importing nested GeometryCollections --- src/geojson/geojson-import.js | 107 ++++++++++++++++++++--------- src/paths/mapshaper-path-import.js | 2 +- test/geojson-test.js | 41 ++++++++++- 3 files changed, 113 insertions(+), 37 deletions(-) diff --git a/src/geojson/geojson-import.js b/src/geojson/geojson-import.js index c79dc9395..f3fd976ee 100644 --- a/src/geojson/geojson-import.js +++ b/src/geojson/geojson-import.js @@ -2,6 +2,34 @@ import { verbose } from '../utils/mapshaper-logging'; import GeoJSON from '../geojson/geojson-common'; import utils from '../utils/mapshaper-utils'; import { PathImporter } from '../paths/mapshaper-path-import'; +import { copyRecord } from '../datatable/mapshaper-data-utils'; + +export function importGeoJSON(src, optsArg) { + var opts = optsArg || {}; + var supportedGeometries = Object.keys(GeoJSON.pathImporters), + srcObj = utils.isString(src) ? JSON.parse(src) : src, + importer = new GeoJSONParser(opts), + srcCollection, dataset; + + // Convert single feature or geometry into a collection with one member + if (srcObj.type == 'Feature') { + srcCollection = { + type: 'FeatureCollection', + features: [srcObj] + }; + } else if (supportedGeometries.includes(srcObj.type)) { + srcCollection = { + type: 'GeometryCollection', + geometries: [srcObj] + }; + } else { + srcCollection = srcObj; + } + (srcCollection.features || srcCollection.geometries || []).forEach(importer.parseObject); + dataset = importer.done(); + importCRS(dataset, srcObj); // TODO: remove this + return dataset; +} export function GeoJSONParser(opts) { var idField = opts.id_field || GeoJSON.ID_FIELD, @@ -24,8 +52,11 @@ export function GeoJSONParser(opts) { geom = o; } // TODO: improve so geometry_type option skips features instead of creating null geometries - importer.startShape(rec); - if (geom) GeoJSON.importGeometry(geom, importer, opts); + if (geom && geom.type == 'GeometryCollection') { + GeoJSON.importComplexFeature(importer, geom, rec, opts); + } else { + GeoJSON.importSimpleFeature(importer, geom, rec, opts); + } }; this.done = function() { @@ -33,50 +64,58 @@ export function GeoJSONParser(opts) { }; } -export function importGeoJSON(src, optsArg) { - var opts = optsArg || {}; - var supportedGeometries = Object.keys(GeoJSON.pathImporters), - srcObj = utils.isString(src) ? JSON.parse(src) : src, - importer = new GeoJSONParser(opts), - srcCollection, dataset; - - // Convert single feature or geometry into a collection with one member - if (srcObj.type == 'Feature') { - srcCollection = { - type: 'FeatureCollection', - features: [srcObj] - }; - } else if (supportedGeometries.includes(srcObj.type)) { - srcCollection = { - type: 'GeometryCollection', - geometries: [srcObj] - }; - } else { - srcCollection = srcObj; +GeoJSON.importComplexFeature = function(importer, geom, rec, opts) { + var types = divideGeometriesByType(geom.geometries || []); + if (types.length === 0) { + importer.startShape(rec); // import a feature with null geometry + return; } - (srcCollection.features || srcCollection.geometries || []).forEach(importer.parseObject); - dataset = importer.done(); - importCRS(dataset, srcObj); // TODO: remove this - return dataset; + types.forEach(function(geometries, i) { + importer.startShape(copyRecord(rec)); + geometries.forEach(function(geom) { + GeoJSON.importSimpleGeometry(importer, geom, opts); + }); + }); +}; + +function divideGeometriesByType(geometries, index) { + index = index || {}; + geometries.forEach(function(geom) { + if (!geom) return; + var mtype = GeoJSON.translateGeoJSONType(geom.type); + if (mtype) { + if (mtype in index === false) { + index[mtype] = []; + } + index[mtype].push(geom); + } else if (geom.type == 'GeometryCollection') { + divideGeometriesByType(geom.geometries || [], index); + } + }); + return Object.values(index); } -GeoJSON.importGeometry = function(geom, importer, opts) { - var type = geom.type; - if (type in GeoJSON.pathImporters) { +GeoJSON.importSimpleFeature = function(importer, geom, rec, opts) { + importer.startShape(rec); + GeoJSON.importSimpleGeometry(importer, geom, opts); +}; + +GeoJSON.importSimpleGeometry = function(importer, geom, opts) { + var type = geom ? geom.type : null; + if (type === null) { + // no geometry to import + } else if (type in GeoJSON.pathImporters) { if (opts.geometry_type && opts.geometry_type != GeoJSON.translateGeoJSONType(type)) { // kludge to filter out all but one type of geometry return; } GeoJSON.pathImporters[type](geom.coordinates, importer); - } else if (type == 'GeometryCollection') { - geom.geometries.forEach(function(geom) { - GeoJSON.importGeometry(geom, importer, opts); - }); } else { - verbose("GeoJSON.importGeometry() Unsupported geometry type:", geom.type); + verbose("Unsupported geometry type:", geom.type); } }; + // Functions for importing geometry coordinates using a PathImporter // GeoJSON.pathImporters = { diff --git a/src/paths/mapshaper-path-import.js b/src/paths/mapshaper-path-import.js index 536c435d0..6b40ecd99 100644 --- a/src/paths/mapshaper-path-import.js +++ b/src/paths/mapshaper-path-import.js @@ -214,7 +214,7 @@ export function PathImporter(opts) { collectionType = 'mixed'; } } else if (currType != t) { - stop("Unable to import mixed-geometry GeoJSON features"); + stop("Unable to import mixed-geometry features"); } } diff --git a/test/geojson-test.js b/test/geojson-test.js index 3344eaa2a..bf7b0195b 100644 --- a/test/geojson-test.js +++ b/test/geojson-test.js @@ -181,7 +181,9 @@ describe('mapshaper-geojson.js', function () { assert.deepEqual(dataset.layers[0].shapes, [[[0, 1], [2, 3], [4, 5]]]) }) - it('Unable to import Feature containing mixed geometry types', function() { + + + it('Features with GeometryCollection type geometries are supported', function() { var json = { "type": "Feature", "properties": {"name": "A"}, @@ -201,7 +203,42 @@ describe('mapshaper-geojson.js', function () { } }; - assert.throws(function() {api.internal.importGeoJSON(json, {});}, /Unable to import mixed/); + var output = api.internal.importGeoJSON(json, {}); + assert.equal(output.layers.length, 3); + assert.equal(output.layers[0].geometry_type, 'point') + assert.equal(output.layers[1].geometry_type, 'polyline') + assert.equal(output.layers[2].geometry_type, 'polygon') + assert.deepEqual(output.layers[2].data.getRecords(), [{name: 'A'}]) + }); + + + it('Features with nested GeometryCollection type geometries are supported', function() { + var json = { + "type": "Feature", + "properties": {"name": "A"}, + "geometry": { + "type": "GeometryCollection", + "geometries": [{ + "type": "MultiPoint", + "coordinates": [[0, 1], [2, 3]] + }, { + "type": "GeometryCollection", + "geometries": [{ + "type": "Point", + "coordinates": [0, 4] + }, { + "type": "LineString", + "coordinates": [[0, 1], [2, 3], [4, 5]] + } + ] + } + ] + } + }; + + var output = api.internal.importGeoJSON(json, {}); + assert.equal(output.layers.length, 2); + assert.deepEqual(output.layers[0].shapes, [[[0, 1], [2, 3], [0, 4]]]) }); it('Import FeatureCollection with mixed geometry types', function() { From d945708f9d1364b9ffaacbcc2186391bb5edee76 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 30 Dec 2021 09:44:32 -0500 Subject: [PATCH 138/891] Improvements to -symbols command --- src/cli/mapshaper-command-parser.js | 3 +- src/cli/mapshaper-option-parsing-utils.js | 4 + src/cli/mapshaper-options.js | 70 ++++++++++----- src/commands/mapshaper-symbols.js | 28 ++++-- src/svg/geojson-to-svg.js | 3 + src/svg/mapshaper-svg-symbols.js | 2 - src/svg/svg-properties.js | 38 +++++--- src/symbols/mapshaper-arrow-symbols.js | 104 ++++++++++++---------- src/symbols/mapshaper-basic-symbols.js | 12 +-- src/symbols/mapshaper-ring-symbols.js | 35 ++++++++ src/utils/mapshaper-utils.js | 8 ++ test/svg-properties-test.js | 18 ++++ 12 files changed, 227 insertions(+), 98 deletions(-) create mode 100644 src/symbols/mapshaper-ring-symbols.js create mode 100644 test/svg-properties-test.js diff --git a/src/cli/mapshaper-command-parser.js b/src/cli/mapshaper-command-parser.js index 7462c73eb..ce58cece0 100644 --- a/src/cli/mapshaper-command-parser.js +++ b/src/cli/mapshaper-command-parser.js @@ -1,6 +1,7 @@ import { parseStringList, parseColorList, + parseNumberList, cleanArgv, isAssignment, splitAssignment @@ -192,7 +193,7 @@ export function CommandParser() { } else if (type == 'strings') { val = parseStringList(token); } else if (type == 'bbox' || type == 'numbers') { - val = token.split(',').map(parseFloat); + val = parseNumberList(token); } else if (type == 'percent') { // val = utils.parsePercent(token); val = token; // string value is parsed by command function diff --git a/src/cli/mapshaper-option-parsing-utils.js b/src/cli/mapshaper-option-parsing-utils.js index 80f222372..544fcc119 100644 --- a/src/cli/mapshaper-option-parsing-utils.js +++ b/src/cli/mapshaper-option-parsing-utils.js @@ -14,6 +14,10 @@ export function splitShellTokens(str) { return chunks; } +export function parseNumberList(token) { + return token.split(',').map(parseFloat); +} + // Split comma-delimited list, trim quotes from entire list and // individual members export function parseStringList(token) { diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 455504455..70b3d5db1 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1554,47 +1554,69 @@ export function getOptionParser() { describe: 'rotation of symbol in degrees' }) .option('orientation', { - describe: 'use orientation=b for a rotated or flipped orientation' + // describe: 'use orientation=b for a rotated or flipped orientation' + }) + .option('flipped', { + type: 'flag', + describe: 'symbol is vertically flipped' + }) + .option('rotated', { + type: 'flag', + describe: 'symbol is rotated to a different orientation' }) .option('length', { // alias for arrow-length }) - .option('star-ratio', { - describe: 'ratio of major to minor radius of star', + .option('point-ratio', { + old_alias: 'star-ratio', + describe: '(star) ratio of minor to major radius of star', type: 'number' }) - .option('arrow-length', { - describe: 'length of arrows in pixels (use with type=arrow)' + .option('radii', { + describe: '(ring) comma-sep. list of concentric radii, ascending order' + }) + .option('length', { + old_alias: 'arrow-length', + describe: '(arrow) length of arrow in pixels' }) - .option('arrow-direction', { - describe: 'angle off of vertical (-90 = left-pointing arrow)' + .option('direction', { + old_alias: 'arrow-direction', + describe: '(arrow) angle off vertical (-90 = left-pointing)' }) - .option('arrow-head-angle', { - describe: 'angle of tip of arrow (default is 40 degrees)' + .option('head-angle', { + old_alias: 'arrow-head-angle', + describe: '(arrow) angle of tip of arrow (default is 40 degrees)' }) - .option('arrow-head-width', { - describe: 'size of arrow head from side to side' + .option('head-width', { + old_alias: 'arrow-head-width', + describe: '(arrow) width of arrow head from side to side' }) - .option('arrow-head-length', { - describe: 'length of arrow head (alternative to arrow-head-angle)' + .option('head-length', { + old_alias: 'arrow-head-width', + describe: '(arrow) length of head (alternative to head-angle)' }) - .option('arrow-head-shape', { + .option('head-shape', { // describe: 'options: a b c' }) - .option('arrow-stem-width', { - describe: 'width of stem at its widest point' + .option('stem-width', { + old_alias: 'arrow-stem-width', + describe: '(arrow) width of stem at its widest point' }) - .option('arrow-stem-length', { - describe: 'alternative to arrow-length' + .option('stem-length', { + old_alias: 'arrow-stem-length', + describe: '(arrow) alternative to length' }) - .option('arrow-stem-taper', { - describe: 'factor for tapering the width of the stem' + .option('stem-taper', { + old_alias: 'arrow-stem-taper', + describe: '(arrow) factor for tapering the width of the stem (0-1)' }) - .option('arrow-stem-curve', { - describe: 'curvature in degrees (arrows are straight by default)' + .option('stem-curve', { + old_alias: 'arrow-stem-curve', + describe: '(arrow) curvature in degrees (default is 0)' }) - .option('arrow-min-stem', { - describe: 'min ratio of stem to total length (for small arrows)', + .option('min-stem-ratio', { + old_alias: 'arrow-min-stem', + describe: '(arrow) min ratio of stem to total length', type: 'number' }) .option('stroke', {}) diff --git a/src/commands/mapshaper-symbols.js b/src/commands/mapshaper-symbols.js index d2a6ee566..ab8d35da2 100644 --- a/src/commands/mapshaper-symbols.js +++ b/src/commands/mapshaper-symbols.js @@ -3,12 +3,13 @@ import { getLayerDataTable } from '../dataset/mapshaper-layer-utils'; import { compileValueExpression } from '../expressions/mapshaper-expressions'; import { getSymbolDataAccessor } from '../svg/svg-properties'; import { requirePointLayer, requireSinglePointLayer, getLayerBounds, copyLayer } from '../dataset/mapshaper-layer-utils'; -import { stop } from '../utils/mapshaper-logging'; +import { stop, error } from '../utils/mapshaper-logging'; // import { symbolBuilders } from '../svg/svg-common'; // import '../svg/mapshaper-svg-arrows'; import { rotateCoords, scaleAndShiftCoords, flipY, roundCoordsForSVG } from '../symbols/mapshaper-symbol-utils'; import { getFilledArrowCoords } from '../symbols/mapshaper-arrow-symbols'; import { getPolygonCoords } from '../symbols/mapshaper-basic-symbols'; +import { getRingCoords } from '../symbols/mapshaper-ring-symbols'; import { getAffineTransform } from '../commands/mapshaper-affine'; import { mergeOutputLayerIntoDataset } from '../dataset/mapshaper-dataset-utils'; import { importGeoJSON } from '../geojson/geojson-import'; @@ -31,9 +32,13 @@ cmd.symbols = function(inputLyr, dataset, opts) { if (!shp) return null; var d = getSymbolData(i); var rec = records[i] || {}; + var geojsonType = 'Polygon'; var coords; if (d.type == 'arrow') { coords = getFilledArrowCoords(d); + } else if (d.type == 'ring') { + coords = getRingCoords(d); + geojsonType = 'MultiPolygon'; } else { coords = getPolygonCoords(d); } @@ -48,9 +53,9 @@ cmd.symbols = function(inputLyr, dataset, opts) { if (polygonMode) { scaleAndShiftCoords(coords, metersPerPx, shp[0]); if (d.tfill) rec.fill = d.fill; - return createGeometry(coords); + return createGeometry(coords, geojsonType); } else { - rec['svg-symbol'] = makeSvgSymbol(coords, d); + rec['svg-symbol'] = makeSvgPolygonSymbol(coords, d, geojsonType); } }); @@ -81,9 +86,9 @@ function importGeometries(geometries, records) { return importGeoJSON(geojson); } -function createGeometry(coords) { +function createGeometry(coords, type) { return { - type: 'Polygon', + type: type, coordinates: coords }; } @@ -96,7 +101,12 @@ function getMetersPerPixel(lyr, dataset) { } // Returns an svg-symbol data object for one symbol -export function makeSvgSymbol(coords, properties) { +function makeSvgPolygonSymbol(coords, properties, geojsonType) { + if (geojsonType == 'MultiPolygon') { + coords = convertMultiPolygonCoords(coords); + } else if (geojsonType != 'Polygon') { + error('Unsupported type:', geojsonType); + } roundCoordsForSVG(coords); return { type: 'polygon', @@ -104,3 +114,9 @@ export function makeSvgSymbol(coords, properties) { fill: properties.fill || 'magenta' }; } + +function convertMultiPolygonCoords(coords) { + return coords.reduce(function(memo, poly) { + return memo.concat(poly); + }, []); +} diff --git a/src/svg/geojson-to-svg.js b/src/svg/geojson-to-svg.js index 1b22c7369..9d51b6c0a 100644 --- a/src/svg/geojson-to-svg.js +++ b/src/svg/geojson-to-svg.js @@ -203,6 +203,9 @@ export function importPolygon(coords) { o = importLineString(coords[i]); o.properties.d = d + o.properties.d + ' Z'; } + if (coords.length > 1) { + o.properties['fill-rule'] = 'evenodd'; // support polygons with holes + } return o; } diff --git a/src/svg/mapshaper-svg-symbols.js b/src/svg/mapshaper-svg-symbols.js index 820a289d4..39eaf9bcc 100644 --- a/src/svg/mapshaper-svg-symbols.js +++ b/src/svg/mapshaper-svg-symbols.js @@ -69,5 +69,3 @@ symbolRenderers.group = function(d, x, y) { return memo.concat(sym); }, []); }; - - diff --git a/src/svg/svg-properties.js b/src/svg/svg-properties.js index 16cf57f69..d48115f35 100644 --- a/src/svg/svg-properties.js +++ b/src/svg/svg-properties.js @@ -37,16 +37,18 @@ var symbolPropertyTypes = utils.extend({ length: 'number', // e.g. arrow length rotation: 'number', radius: 'number', - 'arrow-length': 'number', - 'arrow-direction': 'number', - 'arrow-head-angle': 'number', - 'arrow-head-width': 'number', - 'arrow-stem-width': 'number', - 'arrow-stem-curve': 'number', // degrees of arc - 'arrow-stem-taper': 'number', - 'arrow-stem-length': 'number', - 'arrow-head-length': 'number', - 'arrow-min-stem': 'number', + radii: null, // string, parsed by function + flipped: 'boolean', + rotated: 'boolean', + direction: 'number', + 'head-angle': 'number', + 'head-width': 'number', + 'head-length': 'number', + 'stem-width': 'number', + 'stem-curve': 'number', // degrees of arc + 'stem-taper': 'number', + 'stem-length': 'number', + 'min-stem': 'number', 'arrow-scaling': 'number', effect: null // e.g. "fade" }, stylePropertyTypes); @@ -82,6 +84,7 @@ export function findPropertiesBySymbolGeom(fields, type) { export function getSymbolDataAccessor(lyr, opts) { var functions = {}; var properties = []; + var fields = lyr.data ? lyr.data.getFields() : []; Object.keys(opts).forEach(function(optName) { var svgName = optName.replace(/_/g, '-'); @@ -93,6 +96,8 @@ export function getSymbolDataAccessor(lyr, opts) { properties.push(svgName); }); + // TODO: consider applying values of existing fields with names of symbol properties + return function(id) { var d = {}, name; for (var i=0; i -1) return true; - return /[(){}.+-/*?:&|=\[]/.test(str); + return /[(){}./*?:&|=\[+-]/.test(str); } export function getSymbolPropertyAccessor(val, svgName, lyr) { @@ -130,6 +136,7 @@ export function getSymbolPropertyAccessor(val, svgName, lyr) { // treating the string as a literal value literalVal = strVal; } + // console.log("literalVal:", mightBeExpression(strVal, fields), strVal, fields) if (accessor) return accessor; if (literalVal !== null) return function(id) {return literalVal;}; stop('Unexpected value for', svgName + ':', strVal); @@ -163,6 +170,8 @@ function parseSvgLiteralValue(strVal, type) { val = isDashArray(strVal) ? strVal : null; } else if (type == 'pattern') { val = isPattern(strVal) ? strVal : null; + } else if (type == 'boolean') { + val = parseBoolean(strVal); } // else { // // unknown type -- assume literal value @@ -183,10 +192,17 @@ export function isSvgClassName(str) { return /^( ?[_a-z][-_a-z0-9]*\b)+$/i.test(str); } + export function isSvgNumber(o) { return utils.isFiniteNumber(o) || utils.isString(o) && /^-?[.0-9]+$/.test(o); } +export function parseBoolean(o) { + if (o === true || o === 'true') return true; + if (o === false || o === 'false') return false; + return null; +} + export function isSvgMeasure(o) { return utils.isFiniteNumber(o) || utils.isString(o) && /^-?[.0-9]+[a-z]*$/.test(o); } diff --git a/src/symbols/mapshaper-arrow-symbols.js b/src/symbols/mapshaper-arrow-symbols.js index 2750d226a..52ce8e903 100644 --- a/src/symbols/mapshaper-arrow-symbols.js +++ b/src/symbols/mapshaper-arrow-symbols.js @@ -1,5 +1,5 @@ -import { addBezierArcControlPoints, rotateCoords } from './mapshaper-symbol-utils'; +import { addBezierArcControlPoints, rotateCoords, flipY } from './mapshaper-symbol-utils'; import { stop } from '../utils/mapshaper-logging'; // export function getStickArrowCoords(d, totalLen) { @@ -24,70 +24,80 @@ import { stop } from '../utils/mapshaper-logging'; // return [stem, head]; // } -function getMinStemRatio(d) { - return d['arrow-min-stem'] >= 0 ? d['arrow-min-stem'] : 0.4; -} export function getFilledArrowCoords(d) { - var totalLen = d['arrow-length'] || d.radius || d.length || d.r || 0, - direction = d.rotation || d['arrow-direction'] || 0, - unscaledStemWidth = d['arrow-stem-width'] || 2, - unscaledHeadWidth = d['arrow-head-width'] || unscaledStemWidth * 3, - unscaledHeadLen = getHeadLength(unscaledHeadWidth, d), - stemTaper = d['arrow-stem-taper'] || 0, - stemCurve = d['arrow-stem-curve'] || 0; - - var headLen, headWidth, stemLen, stemWidth; + var direction = d.rotation || d.direction || 0, + stemTaper = d['stem-taper'] || 0, + stemCurve = d['stem-curve'] || 0, + size = calcArrowSize(d); - var scale = 1; + if (!size) return null; - if (totalLen > 0) { - scale = getScale(totalLen, unscaledHeadLen, getMinStemRatio(d)); - headWidth = unscaledHeadWidth * scale; - headLen = unscaledHeadLen * scale; - stemWidth = unscaledStemWidth * scale; - stemLen = totalLen - headLen; + var headDx = size.headWidth / 2, + stemDx = size.stemWidth / 2, + baseDx = stemDx * (1 - stemTaper), + coords; + if (!stemCurve || Math.abs(stemCurve) > 90) { + coords = calcStraightArrowCoords(size.stemLen, size.headLen, stemDx, headDx, baseDx); } else { - headWidth = unscaledHeadWidth; - headLen = unscaledHeadLen; - stemWidth = unscaledStemWidth; - stemLen = d['arrow-stem-length'] || 0; - totalLen = headLen + stemLen; + if (direction > 0) stemCurve = -stemCurve; + coords = getCurvedArrowCoords(size.stemLen, size.headLen, size.stemCurve, stemDx, headDx, baseDx); } - if (totalLen > 0 === false) return null; + rotateCoords(coords, direction); + if (d.flipped) { + flipY(coords); + } + return [coords]; +} - var coords; +function calcStraightArrowCoords(stemLen, headLen, stemDx, headDx, baseDx) { + return [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen], + [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]]; +} - var headDx = headWidth / 2, - stemDx = stemWidth / 2, - baseDx = stemDx * (1 - stemTaper); +function calcArrowSize(d) { + var totalLen = d.radius || d.length || d.r || 0, + unscaledStemWidth = d['stem-width'] || 2, + unscaledHeadWidth = d['head-width'] || unscaledStemWidth * 3, + unscaledHeadLen = d['head-length'] || calcHeadLength(unscaledHeadWidth, d), + scale = 1, + o = {}; - if (unscaledHeadWidth < unscaledStemWidth) { - stop('Arrow head must be at least as wide as the stem.'); - } + if (totalLen > 0) { + scale = calcScale(totalLen, unscaledHeadLen, d); + o.headWidth = unscaledHeadWidth * scale; + o.headLen = unscaledHeadLen * scale; + o.stemWidth = unscaledStemWidth * scale; + o.stemLen = totalLen - o.headLen; - if (!stemCurve || Math.abs(stemCurve) > 90) { - coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen], - [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]]; } else { - if (direction > 0) stemCurve = -stemCurve; - coords = getCurvedArrowCoords(stemLen, headLen, stemCurve, stemDx, headDx, baseDx); + o.headWidth = unscaledHeadWidth; + o.headLen = unscaledHeadLen; + o.stemWidth = unscaledStemWidth; + o.stemLen = d['stem-length'] || 0; } - rotateCoords(coords, direction); - return [coords]; + if (unscaledHeadWidth < unscaledStemWidth) { + stop('Arrow head must be at least as wide as the stem.'); + } + return o; } - -function getScale(totalLen, headLen, minStemRatio) { +function calcScale(totalLen, headLen, d) { + var minStemRatio = d['min-stem'] >= 0 ? d['min-stem'] : 0; + var stemLen = d['stem-length'] || 0; var maxHeadPct = 1 - minStemRatio; var headPct = headLen / totalLen; + var scale = 1; + if (headPct > maxHeadPct) { - return maxHeadPct / headPct; + scale = maxHeadPct / headPct; + } else if (stemLen + headLen > totalLen) { + scale = totalLen / (stemLen + headLen); } - return 1; + return scale; } // function getStickArrowTip(totalLen, curve) { @@ -103,10 +113,8 @@ function addPoints(a, b) { } -function getHeadLength(headWidth, d) { - var headLength = d['arrow-head-length']; - if (headLength > 0) return headLength; - var headAngle = d['arrow-head-angle'] || 40; +function calcHeadLength(headWidth, d) { + var headAngle = d['head-angle'] || 40; var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio return headWidth * headRatio; } diff --git a/src/symbols/mapshaper-basic-symbols.js b/src/symbols/mapshaper-basic-symbols.js index f8ec30bc7..33dd31d14 100644 --- a/src/symbols/mapshaper-basic-symbols.js +++ b/src/symbols/mapshaper-basic-symbols.js @@ -4,11 +4,11 @@ import { stop } from '../utils/mapshaper-logging'; // sides: e.g. 5-pointed star has 10 sides // radius: distance from center to point // -export function getPolygonCoords(opts) { - var radius = opts.radius || opts.length || opts.r; +export function getPolygonCoords(d) { + var radius = d.radius || d.length || d.r; if (radius > 0 === false) return null; - var type = opts.type; - var sides = +opts.sides || getDefaultSides(type); + var type = d.type; + var sides = +d.sides || getDefaultSides(type); var isStar = type == 'star'; if (isStar && (sides < 6 || sides % 2 !== 0)) { stop(`Invalid number of sides for a star (${sides})`); @@ -19,14 +19,14 @@ export function getPolygonCoords(opts) { angle = 360 / sides, b = isStar ? 1 : 0.5, theta, even, len; - if (opts.orientation == 'b') { + if (d.orientation == 'b' || d.flipped || d.rotated) { b = 0; } for (var i=0; i 0) { + i++; + hole = ring; + ring = getPolygonCoords({ + type: 'circle', + radius: radii[i] + }); + ring.push(hole[0]); + } + coords.push(ring); + } + return coords; +} + +function parseRings(arg) { + var arr = Array.isArray(arg) ? arg : parseNumberList(arg); + utils.genericSort(arr, true); + return utils.uniq(arr); +} diff --git a/src/utils/mapshaper-utils.js b/src/utils/mapshaper-utils.js index 2f50f160f..bbc16a86b 100644 --- a/src/utils/mapshaper-utils.js +++ b/src/utils/mapshaper-utils.js @@ -54,6 +54,14 @@ export function isInteger(obj) { return isNumber(obj) && ((obj | 0) === obj); } +export function isEven(obj) { + return (obj % 2) === 0; +} + +export function isOdd(obj) { + return (obj % 2) === 1; +} + export function isString(obj) { return obj != null && obj.toString === String.prototype.toString; // TODO: replace w/ something better. diff --git a/test/svg-properties-test.js b/test/svg-properties-test.js new file mode 100644 index 000000000..1c75084a7 --- /dev/null +++ b/test/svg-properties-test.js @@ -0,0 +1,18 @@ +import { mightBeExpression } from '../src/svg/svg-properties'; + +var api = require('../'), + assert = require('assert'); + +describe('svg-properties.js', function () { + describe('mightBeExpression()', function () { + it('lists of numbers are not expressions', function() { + assert(!mightBeExpression('1,2,3')); + }); + + it('division', function() { + var str = 'foo / 3'; + assert(mightBeExpression('foo / 3')); + }); + + }) +}) From 5a0ea2c54640c296ec7a1e23b6516ec5e46d19d1 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 30 Dec 2021 09:48:37 -0500 Subject: [PATCH 139/891] v0.5.83 --- CHANGELOG.md | 4 ++++ package-lock.json | 2 +- package.json | 2 +- src/cli/mapshaper-options.js | 6 +++--- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc92b21e6..6957b9054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.83 +* Added support for importing GeoJSON features with GeometryCollection type geometries. +* Added "ring" symbol type. + v0.5.82 * More options for arrow styling. diff --git a/package-lock.json b/package-lock.json index a246b267e..73e7110dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.82", + "version": "0.5.83", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 14daf945f..a8e94e651 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.82", + "version": "0.5.83", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 70b3d5db1..2da90238b 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1550,9 +1550,6 @@ export function getOptionParser() { describe: 'sides of a polygon or star symbol', type: 'number' }) - .option('rotation', { - describe: 'rotation of symbol in degrees' - }) .option('orientation', { // describe: 'use orientation=b for a rotated or flipped orientation' }) @@ -1564,6 +1561,9 @@ export function getOptionParser() { type: 'flag', describe: 'symbol is rotated to a different orientation' }) + .option('rotation', { + describe: 'rotation of symbol in degrees' + }) .option('length', { // alias for arrow-length }) From 93e864cc7d964c9a61a32a2bf586e17aa0763093 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 4 Jan 2022 16:40:00 -0500 Subject: [PATCH 140/891] v0.5.84 --- CHANGELOG.md | 3 ++ package.json | 2 +- src/svg/svg-properties.js | 2 +- src/symbols/mapshaper-arrow-symbols.js | 73 +++++++++++++++----------- 4 files changed, 46 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6957b9054..7a72ff674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.84 +* Bug fixes + v0.5.83 * Added support for importing GeoJSON features with GeometryCollection type geometries. * Added "ring" symbol type. diff --git a/package.json b/package.json index a8e94e651..f84516b21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.83", + "version": "0.5.84", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/svg/svg-properties.js b/src/svg/svg-properties.js index d48115f35..e5c1fad1f 100644 --- a/src/svg/svg-properties.js +++ b/src/svg/svg-properties.js @@ -48,7 +48,7 @@ var symbolPropertyTypes = utils.extend({ 'stem-curve': 'number', // degrees of arc 'stem-taper': 'number', 'stem-length': 'number', - 'min-stem': 'number', + 'min-stem-ratio': 'number', 'arrow-scaling': 'number', effect: null // e.g. "fade" }, stylePropertyTypes); diff --git a/src/symbols/mapshaper-arrow-symbols.js b/src/symbols/mapshaper-arrow-symbols.js index 52ce8e903..4584a4f7e 100644 --- a/src/symbols/mapshaper-arrow-symbols.js +++ b/src/symbols/mapshaper-arrow-symbols.js @@ -24,6 +24,17 @@ import { stop } from '../utils/mapshaper-logging'; // return [stem, head]; // } +// function getStickArrowTip(totalLen, curve) { +// // curve/2 intersects the arrowhead at 90deg (trigonometry) +// var theta = Math.abs(curve/2) / 180 * Math.PI; +// var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1); +// var dy = totalLen * Math.cos(theta); +// return [dx, dy]; +// } + +// function addPoints(a, b) { +// return [a[0] + b[0], a[1] + b[1]]; +// } export function getFilledArrowCoords(d) { var direction = d.rotation || d.direction || 0, @@ -42,7 +53,7 @@ export function getFilledArrowCoords(d) { coords = calcStraightArrowCoords(size.stemLen, size.headLen, stemDx, headDx, baseDx); } else { if (direction > 0) stemCurve = -stemCurve; - coords = getCurvedArrowCoords(size.stemLen, size.headLen, size.stemCurve, stemDx, headDx, baseDx); + coords = getCurvedArrowCoords(size.stemLen, size.headLen, stemCurve, stemDx, headDx, baseDx); } rotateCoords(coords, direction); @@ -59,34 +70,25 @@ function calcStraightArrowCoords(stemLen, headLen, stemDx, headDx, baseDx) { function calcArrowSize(d) { var totalLen = d.radius || d.length || d.r || 0, - unscaledStemWidth = d['stem-width'] || 2, - unscaledHeadWidth = d['head-width'] || unscaledStemWidth * 3, - unscaledHeadLen = d['head-length'] || calcHeadLength(unscaledHeadWidth, d), scale = 1, - o = {}; + o = initArrowSize(d); // calc several parameters if (totalLen > 0) { - scale = calcScale(totalLen, unscaledHeadLen, d); - o.headWidth = unscaledHeadWidth * scale; - o.headLen = unscaledHeadLen * scale; - o.stemWidth = unscaledStemWidth * scale; + scale = calcScale(totalLen, o.headLen, d); + o.stemWidth *= scale; + o.headWidth *= scale; + o.headLen *= scale; o.stemLen = totalLen - o.headLen; - - } else { - o.headWidth = unscaledHeadWidth; - o.headLen = unscaledHeadLen; - o.stemWidth = unscaledStemWidth; - o.stemLen = d['stem-length'] || 0; } - if (unscaledHeadWidth < unscaledStemWidth) { + if (o.headWidth < o.stemWidth) { stop('Arrow head must be at least as wide as the stem.'); } return o; } function calcScale(totalLen, headLen, d) { - var minStemRatio = d['min-stem'] >= 0 ? d['min-stem'] : 0; + var minStemRatio = d['min-stem-ratio'] >= 0 ? d['min-stem-ratio'] : 0; var stemLen = d['stem-length'] || 0; var maxHeadPct = 1 - minStemRatio; var headPct = headLen / totalLen; @@ -100,23 +102,31 @@ function calcScale(totalLen, headLen, d) { return scale; } -// function getStickArrowTip(totalLen, curve) { -// // curve/2 intersects the arrowhead at 90deg (trigonometry) -// var theta = Math.abs(curve/2) / 180 * Math.PI; -// var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1); -// var dy = totalLen * Math.cos(theta); -// return [dx, dy]; -// } - -function addPoints(a, b) { - return [a[0] + b[0], a[1] + b[1]]; +export function initArrowSize(d) { + var sizeRatio = getHeadSizeRatio(d['head-angle'] || 40); // length to width + var o = { + stemWidth: d['stem-width'] || 2, + stemLen: d['stem-length'] || 0, + headWidth: d['head-width'], + headLen: d['head-length'] + }; + if (!o.headWidth) { + if (o.headLen) { + o.headWidth = o.headLen / sizeRatio; + } else { + o.headWidth = o.stemWidth * 3; // assumes stemWidth has been set + } + } + if (!o.headLen) { + o.headLen = o.headWidth * sizeRatio; + } + return o; } -function calcHeadLength(headWidth, d) { - var headAngle = d['head-angle'] || 40; - var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio - return headWidth * headRatio; +// Returns ratio of head length to head width +function getHeadSizeRatio(headAngle) { + return 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; } function getCurvedArrowCoords(stemLen, headLen, curvature, stemDx, headDx, baseDx) { @@ -169,4 +179,3 @@ function getCurvedStemCoords(ax, ay, bx, by, theta0) { coords.push([ax, ay]); return coords; } - From 5b2bdc0789620a87bac409b1fc94287f012ee285 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 4 Jan 2022 16:40:30 -0500 Subject: [PATCH 141/891] v0.5.84 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 73e7110dc..28e6efbab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.83", + "version": "0.5.84", "lockfileVersion": 1, "requires": true, "dependencies": { From 917d62be173e6da140556c94af5b2a21c79752a5 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 6 Jan 2022 22:36:17 -0500 Subject: [PATCH 142/891] v0.5.85 --- CHANGELOG.md | 3 + package.json | 2 +- src/cli/mapshaper-options.js | 10 ++- src/svg/svg-properties.js | 3 + src/symbols/mapshaper-arrow-symbols.js | 89 ++++++++++++++------------ src/symbols/mapshaper-basic-symbols.js | 11 +++- src/symbols/mapshaper-star-symbols.js | 25 ++++++++ 7 files changed, 94 insertions(+), 49 deletions(-) create mode 100644 src/symbols/mapshaper-star-symbols.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a72ff674..b34ebe774 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.85 +* Improved arrow and star symbols. + v0.5.84 * Bug fixes diff --git a/package.json b/package.json index f84516b21..3ac6f841b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.84", + "version": "0.5.85", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 2da90238b..587595f31 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1547,10 +1547,11 @@ export function getOptionParser() { type: 'distance' }) .option('sides', { - describe: 'sides of a polygon or star symbol', + describe: 'number of sides of a polygon symbol', type: 'number' }) .option('orientation', { + // TODO: removed (replaced by flipped and rotated) // describe: 'use orientation=b for a rotated or flipped orientation' }) .option('flipped', { @@ -1564,8 +1565,8 @@ export function getOptionParser() { .option('rotation', { describe: 'rotation of symbol in degrees' }) - .option('length', { - // alias for arrow-length + .option('points', { + describe: '(star) number of points' }) .option('point-ratio', { old_alias: 'star-ratio', @@ -1619,6 +1620,9 @@ export function getOptionParser() { describe: '(arrow) min ratio of stem to total length', type: 'number' }) + .option('anchor', { + describe: '(arrow) takes one of: start, middle, end (default is start)' + }) .option('stroke', {}) .option('stroke-width', {}) .option('fill', { diff --git a/src/svg/svg-properties.js b/src/svg/svg-properties.js index e5c1fad1f..b2f9a0c61 100644 --- a/src/svg/svg-properties.js +++ b/src/svg/svg-properties.js @@ -41,6 +41,9 @@ var symbolPropertyTypes = utils.extend({ flipped: 'boolean', rotated: 'boolean', direction: 'number', + sides: 'number', // polygons and stars + points: 'number', // polygons and stars + anchor: null, // arrows; takes start, middle, end 'head-angle': 'number', 'head-width': 'number', 'head-length': 'number', diff --git a/src/symbols/mapshaper-arrow-symbols.js b/src/symbols/mapshaper-arrow-symbols.js index 4584a4f7e..c5d477486 100644 --- a/src/symbols/mapshaper-arrow-symbols.js +++ b/src/symbols/mapshaper-arrow-symbols.js @@ -1,5 +1,5 @@ -import { addBezierArcControlPoints, rotateCoords, flipY } from './mapshaper-symbol-utils'; +import { addBezierArcControlPoints, rotateCoords, flipY, scaleAndShiftCoords } from './mapshaper-symbol-utils'; import { stop } from '../utils/mapshaper-logging'; // export function getStickArrowCoords(d, totalLen) { @@ -36,33 +36,6 @@ import { stop } from '../utils/mapshaper-logging'; // return [a[0] + b[0], a[1] + b[1]]; // } -export function getFilledArrowCoords(d) { - var direction = d.rotation || d.direction || 0, - stemTaper = d['stem-taper'] || 0, - stemCurve = d['stem-curve'] || 0, - size = calcArrowSize(d); - - if (!size) return null; - - var headDx = size.headWidth / 2, - stemDx = size.stemWidth / 2, - baseDx = stemDx * (1 - stemTaper), - coords; - - if (!stemCurve || Math.abs(stemCurve) > 90) { - coords = calcStraightArrowCoords(size.stemLen, size.headLen, stemDx, headDx, baseDx); - } else { - if (direction > 0) stemCurve = -stemCurve; - coords = getCurvedArrowCoords(size.stemLen, size.headLen, stemCurve, stemDx, headDx, baseDx); - } - - rotateCoords(coords, direction); - if (d.flipped) { - flipY(coords); - } - return [coords]; -} - function calcStraightArrowCoords(stemLen, headLen, stemDx, headDx, baseDx) { return [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen], [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]]; @@ -129,21 +102,53 @@ function getHeadSizeRatio(headAngle) { return 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; } -function getCurvedArrowCoords(stemLen, headLen, curvature, stemDx, headDx, baseDx) { +export function getFilledArrowCoords(d) { + var direction = d.rotation || d.direction || 0, + stemTaper = d['stem-taper'] || 0, + curvature = d['stem-curve'] || 0, + size = calcArrowSize(d); + if (!size) return null; + var stemLen = size.stemLen, + headLen = size.headLen, + headDx = size.headWidth / 2, + stemDx = size.stemWidth / 2, + baseDx = stemDx * (1 - stemTaper), + head, stem, coords, dx, dy; + + if (curvature) { + if (direction > 0) curvature = -curvature; + var theta = Math.abs(curvature) / 180 * Math.PI; + var sign = curvature > 0 ? 1 : -1; + var ax = baseDx * Math.cos(theta); // rotate arrow base + var ay = baseDx * Math.sin(theta) * -sign; + dx = stemLen * Math.sin(theta / 2) * sign; + dy = stemLen * Math.cos(theta / 2); + var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta); + var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta); + stem = leftStem.concat(rightStem.reverse()); + + } else { + dx = 0; + dy = stemLen; + stem = [[-baseDx, 0], [baseDx, 0], [baseDx, 0]]; + } + // coordinates go counter clockwise, starting from the leftmost head coordinate - var theta = Math.abs(curvature) / 180 * Math.PI; - var sign = curvature > 0 ? 1 : -1; - var dx = stemLen * Math.sin(theta / 2) * sign; - var dy = stemLen * Math.cos(theta / 2); - var head = [[stemDx + dx, dy], [headDx + dx, dy], - [dx, headLen + dy], [-headDx + dx, dy], [-stemDx + dx, dy]]; - var ax = baseDx * Math.cos(theta); // rotate arrow base - var ay = baseDx * Math.sin(theta) * -sign; - var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta); - var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta); - var stem = leftStem.concat(rightStem.reverse()); - // stem.pop(); - return stem.concat(head); + head = [[stemDx + dx, dy], [headDx + dx, dy], + [dx, headLen + dy], [-headDx + dx, dy], [-stemDx + dx, dy]]; + + coords = stem.concat(head); + if (d.anchor == 'end') { + scaleAndShiftCoords(coords, 1, [-dx, -dy - headLen]); + } else if (d.anchor == 'middle') { + scaleAndShiftCoords(coords, 1, [-dx/2, (-dy - headLen)/2]); + } + + rotateCoords(coords, direction); + if (d.flipped) { + flipY(coords); + } + return [coords]; } // ax, ay: point on the base diff --git a/src/symbols/mapshaper-basic-symbols.js b/src/symbols/mapshaper-basic-symbols.js index 33dd31d14..785848e0e 100644 --- a/src/symbols/mapshaper-basic-symbols.js +++ b/src/symbols/mapshaper-basic-symbols.js @@ -1,5 +1,6 @@ import { getPlanarSegmentEndpoint } from '../geom/mapshaper-geodesic'; import { stop } from '../utils/mapshaper-logging'; +import { getMinorRadius } from '../symbols/mapshaper-star-symbols'; // sides: e.g. 5-pointed star has 10 sides // radius: distance from center to point @@ -10,8 +11,12 @@ export function getPolygonCoords(d) { var type = d.type; var sides = +d.sides || getDefaultSides(type); var isStar = type == 'star'; - if (isStar && (sides < 6 || sides % 2 !== 0)) { - stop(`Invalid number of sides for a star (${sides})`); + if (isStar && d.points > 0) { + sides = d.points * 2; + } + var starRatio = isStar ? d.star_ratio || getMinorRadius(sides / 2) : 0; + if (isStar && (sides < 10 || sides % 2 !== 0)) { + stop(`Invalid number of points for a star (${sides / 2})`); } else if (sides >= 3 === false) { stop(`Invalid number of sides (${sides})`); } @@ -26,7 +31,7 @@ export function getPolygonCoords(d) { even = i % 2 == 0; len = radius; if (isStar && even) { - len *= (d.star_ratio || 0.5); + len *= starRatio; } theta = (i + b) * angle % 360; coords.push(getPlanarSegmentEndpoint(0, 0, theta, len)); diff --git a/src/symbols/mapshaper-star-symbols.js b/src/symbols/mapshaper-star-symbols.js new file mode 100644 index 000000000..184602f0a --- /dev/null +++ b/src/symbols/mapshaper-star-symbols.js @@ -0,0 +1,25 @@ + + +export function getMinorRadius(points) { + var innerAngle = 360 / points; + var pointAngle = getDefaultPointAngle(points); + var thetaA = Math.PI / 180 * innerAngle / 2; + var thetaB = Math.PI / 180 * pointAngle / 2; + var a = Math.tan(thetaB) / (Math.tan(thetaB) + Math.tan(thetaA)); + var c = a / Math.cos(thetaA); + return c; +} + + +function getPointAngle(points, skip) { + var unitAngle = 360 / points; + var centerAngle = unitAngle * (skip + 1); + return 180 - centerAngle; +} + +function getDefaultPointAngle(points) { + var minSkip = 1; + var maxSkip = Math.ceil(points / 2) - 2; + var skip = Math.floor((maxSkip + minSkip) / 2); + return getPointAngle(points, skip); +} From e980b36260eeeb546293e53a4c7b49acadfbd4b9 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 6 Jan 2022 22:36:42 -0500 Subject: [PATCH 143/891] v0.5.85 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 28e6efbab..4b0d1cc60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.84", + "version": "0.5.85", "lockfileVersion": 1, "requires": true, "dependencies": { From 77340b4422954c85f80b4021d6922637b041dee1 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 7 Jan 2022 10:19:08 -0500 Subject: [PATCH 144/891] Refactoring --- src/commands/mapshaper-symbols.js | 3 ++ src/symbols/mapshaper-basic-symbols.js | 41 ++++++++------------------ src/symbols/mapshaper-star-symbols.js | 37 ++++++++++++++++++----- 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/src/commands/mapshaper-symbols.js b/src/commands/mapshaper-symbols.js index ab8d35da2..93f149f74 100644 --- a/src/commands/mapshaper-symbols.js +++ b/src/commands/mapshaper-symbols.js @@ -9,6 +9,7 @@ import { stop, error } from '../utils/mapshaper-logging'; import { rotateCoords, scaleAndShiftCoords, flipY, roundCoordsForSVG } from '../symbols/mapshaper-symbol-utils'; import { getFilledArrowCoords } from '../symbols/mapshaper-arrow-symbols'; import { getPolygonCoords } from '../symbols/mapshaper-basic-symbols'; +import { getStarCoords } from '../symbols/mapshaper-star-symbols'; import { getRingCoords } from '../symbols/mapshaper-ring-symbols'; import { getAffineTransform } from '../commands/mapshaper-affine'; import { mergeOutputLayerIntoDataset } from '../dataset/mapshaper-dataset-utils'; @@ -39,6 +40,8 @@ cmd.symbols = function(inputLyr, dataset, opts) { } else if (d.type == 'ring') { coords = getRingCoords(d); geojsonType = 'MultiPolygon'; + } else if (d.type == 'star') { + coords = getStarCoords(d); } else { coords = getPolygonCoords(d); } diff --git a/src/symbols/mapshaper-basic-symbols.js b/src/symbols/mapshaper-basic-symbols.js index 785848e0e..9b9a1bff6 100644 --- a/src/symbols/mapshaper-basic-symbols.js +++ b/src/symbols/mapshaper-basic-symbols.js @@ -1,48 +1,31 @@ import { getPlanarSegmentEndpoint } from '../geom/mapshaper-geodesic'; import { stop } from '../utils/mapshaper-logging'; -import { getMinorRadius } from '../symbols/mapshaper-star-symbols'; -// sides: e.g. 5-pointed star has 10 sides -// radius: distance from center to point -// export function getPolygonCoords(d) { - var radius = d.radius || d.length || d.r; + var radius = d.radius || d.length || d.r, + sides = +d.sides || getSidesByType(d.type), + rotated = sides % 2 == 1, + coords = [], + angle, b; + if (radius > 0 === false) return null; - var type = d.type; - var sides = +d.sides || getDefaultSides(type); - var isStar = type == 'star'; - if (isStar && d.points > 0) { - sides = d.points * 2; - } - var starRatio = isStar ? d.star_ratio || getMinorRadius(sides / 2) : 0; - if (isStar && (sides < 10 || sides % 2 !== 0)) { - stop(`Invalid number of points for a star (${sides / 2})`); - } else if (sides >= 3 === false) { + if (sides >= 3 === false) { stop(`Invalid number of sides (${sides})`); } - var coords = [], - angle = 360 / sides, - b = isStar ? 1 : 0.5, - theta, even, len; if (d.orientation == 'b' || d.flipped || d.rotated) { - b = 0; + rotated = !rotated; } + b = rotated ? 0 : 0.5; for (var i=0; i 0 === false) return null; + if (points < 5) { + stop(`Invalid number of points for a star (${points})`); + } + for (var i=0; i Date: Fri, 7 Jan 2022 17:18:55 -0500 Subject: [PATCH 145/891] Remove -symbols flipped option --- src/cli/mapshaper-options.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 587595f31..2c8b19f66 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1554,10 +1554,10 @@ export function getOptionParser() { // TODO: removed (replaced by flipped and rotated) // describe: 'use orientation=b for a rotated or flipped orientation' }) - .option('flipped', { - type: 'flag', - describe: 'symbol is vertically flipped' - }) + // .option('flipped', { + // type: 'flag', + // describe: 'symbol is vertically flipped' + // }) .option('rotated', { type: 'flag', describe: 'symbol is rotated to a different orientation' From 9c2b68f78671f1819304693cde36f52eb301de3f Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 11 Jan 2022 12:00:57 -0500 Subject: [PATCH 146/891] Add undo for point and label dragging --- src/gui/gui-instance.js | 2 + src/gui/gui-map.js | 4 ++ src/gui/gui-symbol-dragging2.js | 51 +++++--------- src/gui/gui-undo.js | 120 ++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 33 deletions(-) create mode 100644 src/gui/gui-undo.js diff --git a/src/gui/gui-instance.js b/src/gui/gui-instance.js index c5a3ce252..86df9629a 100644 --- a/src/gui/gui-instance.js +++ b/src/gui/gui-instance.js @@ -1,5 +1,6 @@ import { WriteFilesProxy, ImportFileProxy } from './gui-proxy'; import { SessionHistory } from './gui-session-history'; +import { Undo } from './gui-undo'; import { SidebarButtons } from './gui-sidebar-buttons'; import { ModeSwitcher } from './gui-modes'; import { KeyboardEvents } from './gui-keyboard'; @@ -31,6 +32,7 @@ export function GuiInstance(container, opts) { gui.map = new MshpMap(gui); gui.interaction = new InteractionMode(gui); gui.session = new SessionHistory(gui); + gui.undo = new Undo(gui); gui.showProgressMessage = function(msg) { if (!gui.progressMessage) { diff --git a/src/gui/gui-map.js b/src/gui/gui-map.js index 7579fd08d..a404497b2 100644 --- a/src/gui/gui-map.js +++ b/src/gui/gui-map.js @@ -56,6 +56,10 @@ export function MshpMap(gui) { _mouse.disable(); }); + gui.on('undo_redo', function() { + drawLayers(); + }); + model.on('update', onUpdate); // Update display of segment intersections diff --git a/src/gui/gui-symbol-dragging2.js b/src/gui/gui-symbol-dragging2.js index b69efd025..8077d17e3 100644 --- a/src/gui/gui-symbol-dragging2.js +++ b/src/gui/gui-symbol-dragging2.js @@ -85,6 +85,7 @@ export function SymbolDragging2(gui, ext, hit) { if (e.mode != 'labels') { stopDragging(); } + gui.undo.clear(); // TODO: put this elsewhere? }); // down event on svg @@ -96,12 +97,16 @@ export function SymbolDragging2(gui, ext, hit) { // 3: on other text -> stop dragging, select new text hit.on('dragstart', function(e) { - if (labelEditingEnabled()) { - onLabelDragStart(e); + if (e.id >= 0 === false) return; + if (labelEditingEnabled() && onLabelDragStart(e)) { + triggerGlobalEvent('label_dragstart', e); + startDragging(); } else if (locationEditingEnabled()) { - onLocationDragStart(e); + triggerGlobalEvent('symbol_dragstart', e); + startDragging(); } else if (vertexEditingEnabled()) { - onVertexDragStart(e); + triggerGlobalEvent('vertex_dragstart', e); + startDragging(); } }); @@ -117,12 +122,14 @@ export function SymbolDragging2(gui, ext, hit) { hit.on('dragend', function(e) { if (locationEditingEnabled()) { - onLocationDragEnd(e); + triggerGlobalEvent('symbol_dragend', e); stopDragging(); } else if (labelEditingEnabled()) { + triggerGlobalEvent('label_dragend', e); stopDragging(); } else if (vertexEditingEnabled()) { onVertexDragEnd(e); + triggerGlobalEvent('vertex_dragend', e); stopDragging(); } }); @@ -133,18 +140,6 @@ export function SymbolDragging2(gui, ext, hit) { } }); - function onLocationDragStart(e) { - if (e.id >= 0) { - dragging = true; - triggerGlobalEvent('symbol_dragstart', e); - } - } - - function onVertexDragStart(e) { - if (e.id >= 0) { - dragging = true; - } - } function onLocationDrag(e) { var lyr = hit.getHitTarget().layer; @@ -173,10 +168,6 @@ export function SymbolDragging2(gui, ext, hit) { self.dispatchEvent('location_change'); // signal map to redraw } - function onLocationDragEnd(e) { - triggerGlobalEvent('symbol_dragend', e); - } - function onVertexDragEnd(e) { // kludge to get dataset to recalculate internal bounding boxes hit.getHitTarget().arcs.transformPoints(function() {}); @@ -219,11 +210,11 @@ export function SymbolDragging2(gui, ext, hit) { function onLabelDragStart(e) { var textNode = getTextTarget3(e); var table = hit.getTargetDataTable(); - if (!textNode || !table) return; + if (!textNode || !table) return false; activeId = e.id; activeRecord = getLabelRecordById(activeId); - dragging = true; downEvt = e; + return true; } function onLabelDrag(e) { @@ -348,6 +339,10 @@ export function SymbolDragging2(gui, ext, hit) { // } } + function startDragging() { + dragging = true; + } + function stopDragging() { dragging = false; activeId = -1; @@ -364,14 +359,4 @@ export function SymbolDragging2(gui, ext, hit) { return dist <= 4 && elapsed < 300; } - - // function deselectText(el) { - // el.removeAttribute('class'); - // } - - // function selectText(el) { - // el.setAttribute('class', 'selected'); - // } - - } diff --git a/src/gui/gui-undo.js b/src/gui/gui-undo.js new file mode 100644 index 000000000..456a0870a --- /dev/null +++ b/src/gui/gui-undo.js @@ -0,0 +1,120 @@ + +import { cloneShape } from '../paths/mapshaper-shape-utils'; +import { copyRecord } from '../datatable/mapshaper-data-utils'; + +export function Undo(gui) { + var history, offset, stashedUndo; + reset(); + + function reset() { + history = []; + stashedUndo = null; + offset = 0; + } + + function refreshMap() { + gui.dispatchEvent('undo_redo'); + } + + function isUndoEvt(e) { + return (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key == 'z'; + } + + function isRedoEvt(e) { + return (e.ctrlKey || e.metaKey) && (e.shiftKey && e.key == 'z' || !e.shiftKey && e.key == 'y'); + } + + gui.keyboard.on('keydown', function(evt) { + var e = evt.originalEvent, + kc = e.keyCode; + if (isUndoEvt(e)) { + this.undo(); + e.stopPropagation(); + e.preventDefault(); + } + if (isRedoEvt(e)) { + this.redo(); + e.stopPropagation(); + e.preventDefault(); + } + + }, this, 10); + + // undo/redo point/symbol dragging + // + gui.on('symbol_dragstart', function(e) { + stashedUndo = this.makePointSetter(e.FID); + }, this); + + gui.on('symbol_dragend', function(e) { + var redo = this.makePointSetter(e.FID); + this.addHistoryState(stashedUndo, redo); + }, this); + + // undo/redo label dragging + // + gui.on('label_dragstart', function(e) { + stashedUndo = this.makeDataSetter(e.FID); + }, this); + + gui.on('label_dragend', function(e) { + var redo = this.makeDataSetter(e.FID); + this.addHistoryState(stashedUndo, redo); + }, this); + + this.clear = function() { + reset(); + }; + + this.makePointSetter = function(i) { + var target = gui.model.getActiveLayer(); + var shp = cloneShape(target.layer.shapes[i]); + return function() { + target.layer.shapes[i] = shp; + }; + }; + + + this.makeDataSetter = function(id) { + var target = gui.model.getActiveLayer(); + var rec = copyRecord(target.layer.data.getRecordAt(id)); + return function() { + target.layer.data.getRecords()[id] = rec; + gui.dispatchEvent('popup-needs-refresh'); + }; + }; + + + this.addHistoryState = function(undo, redo) { + if (offset > 0) { + history.splice(-offset); + offset = 0; + } + history.push({undo, redo}); + }; + + this.undo = function() { + var item = getHistoryItem(); + if (item) { + offset++; + item.undo(); + refreshMap(); + } + }; + + this.redo = function() { + if (offset <= 0) return; + offset--; + var item = getHistoryItem(); + item.redo(); + refreshMap(); + }; + + function getHistoryItem() { + var item = history[history.length - offset - 1]; + return item || null; + } + +} + + From 4a0aaf60d582e42cb7c6fe3a4b90ba3986c036f8 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 11 Jan 2022 13:58:57 -0500 Subject: [PATCH 147/891] v0.5.86 --- CHANGELOG.md | 3 +++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b34ebe774..4a207b7a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.86 +* Added keyboard commands for undo/redo for interactive point and label positioning. + v0.5.85 * Improved arrow and star symbols. diff --git a/package-lock.json b/package-lock.json index 4b0d1cc60..c571eb0bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.85", + "version": "0.5.86", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3ac6f841b..cb81f7140 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.85", + "version": "0.5.86", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From d4033d466b957d7cea327303f00d2f5c9f3aa5b5 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 11 Jan 2022 18:02:52 -0500 Subject: [PATCH 148/891] Add undo for interactive data editing --- src/gui/gui-popup.js | 2 ++ src/gui/gui-undo.js | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/gui/gui-popup.js b/src/gui/gui-popup.js index 82b92ddfa..70f4b9ecd 100644 --- a/src/gui/gui-popup.js +++ b/src/gui/gui-popup.js @@ -154,7 +154,9 @@ export function Popup(gui, toNext, toPrev) { } else { // field content has changed strval = strval2; + gui.dispatchEvent('data_preupdate', {FID: currId}); // for undo/redo rec[key] = val2; + gui.dispatchEvent('data_postupdate', {FID: currId}); input.value(strval); setFieldClass(el, val2, type); self.dispatchEvent('update', {field: key, value: val2, id: currId}); diff --git a/src/gui/gui-undo.js b/src/gui/gui-undo.js index 456a0870a..ec5db5fed 100644 --- a/src/gui/gui-undo.js +++ b/src/gui/gui-undo.js @@ -62,6 +62,19 @@ export function Undo(gui) { this.addHistoryState(stashedUndo, redo); }, this); + + // undo/redo data editing + // TODO: consider setting selected feature to the undo/redo target feature + // + gui.on('data_preupdate', function(e) { + stashedUndo = this.makeDataSetter(e.FID); + }, this); + + gui.on('data_postupdate', function(e) { + var redo = this.makeDataSetter(e.FID); + this.addHistoryState(stashedUndo, redo); + }, this); + this.clear = function() { reset(); }; @@ -74,7 +87,6 @@ export function Undo(gui) { }; }; - this.makeDataSetter = function(id) { var target = gui.model.getActiveLayer(); var rec = copyRecord(target.layer.data.getRecordAt(id)); @@ -84,7 +96,6 @@ export function Undo(gui) { }; }; - this.addHistoryState = function(undo, redo) { if (offset > 0) { history.splice(-offset); From ce306ee6f6338ef43644f4bb1a3cd6112be1935e Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 13 Jan 2022 22:07:31 -0500 Subject: [PATCH 149/891] Handle negative arrow lengths --- src/symbols/mapshaper-arrow-symbols.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/symbols/mapshaper-arrow-symbols.js b/src/symbols/mapshaper-arrow-symbols.js index c5d477486..f5de01afb 100644 --- a/src/symbols/mapshaper-arrow-symbols.js +++ b/src/symbols/mapshaper-arrow-symbols.js @@ -42,7 +42,8 @@ function calcStraightArrowCoords(stemLen, headLen, stemDx, headDx, baseDx) { } function calcArrowSize(d) { - var totalLen = d.radius || d.length || d.r || 0, + // don't display arrows with negative length + var totalLen = Math.max(d.radius || d.length || d.r || 0, 0), scale = 1, o = initArrowSize(d); // calc several parameters From 434d3a71a9cbd97e797a34cdfe43d988c834cfe3 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 13 Jan 2022 22:16:33 -0500 Subject: [PATCH 150/891] Add undo/redo to vertex editing --- CHANGELOG.md | 3 ++ package-lock.json | 2 +- package.json | 2 +- src/gui/gui-canvas.js | 31 +++++++++++-- src/gui/gui-instance.js | 1 + src/gui/gui-interaction-mode-control.js | 4 +- src/gui/gui-map-style.js | 2 +- src/gui/gui-map.js | 4 ++ src/gui/gui-symbol-dragging2.js | 59 ++++++++++++++++--------- src/gui/gui-undo.js | 31 ++++++++++--- src/paths/mapshaper-arcs.js | 4 ++ 11 files changed, 108 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a207b7a7..a6220fe82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.87 +* Added undo/redo to the "drag vertices" interactive editing mode. + v0.5.86 * Added keyboard commands for undo/redo for interactive point and label positioning. diff --git a/package-lock.json b/package-lock.json index c571eb0bf..ed0e1db45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.86", + "version": "0.5.87", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index cb81f7140..035e5461c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.86", + "version": "0.5.87", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/gui/gui-canvas.js b/src/gui/gui-canvas.js index 39aa92c28..16fb21d3e 100644 --- a/src/gui/gui-canvas.js +++ b/src/gui/gui-canvas.js @@ -54,7 +54,10 @@ export function drawStyledLayerToCanvas(obj, canv, ext) { } else { arcs = getArcsForRendering(obj, ext); filter = getShapeFilter(arcs, ext); - canv.drawPathShapes(layer.shapes, arcs, style, filter); + canv.drawStyledPaths(layer.shapes, arcs, style, filter); + if (style.vertices) { + canv.drawVertices(layer.shapes, arcs, style, filter); + } } } @@ -128,7 +131,7 @@ export function DisplayCanvas() { /* // Original function, not optimized - _self.drawPathShapes = function(shapes, arcs, style) { + _self.drawStyledPaths = function(shapes, arcs, style) { var startPath = getPathStart(_ext), drawPath = getShapePencil(arcs, _ext), styler = style.styler || null; @@ -141,8 +144,28 @@ export function DisplayCanvas() { }; */ + _self.drawVertices = function(shapes, arcs, style, filter) { + var iter = new internal.ShapeIter(arcs); + var t = getScaledTransform(_ext); + var radius = (style.strokeWidth * 0.8 || 2.2) * GUI.getPixelRatio() * getScaledLineScale(_ext); + var color = style.strokeColor || 'black'; + var shp; + _ctx.beginPath(); + _ctx.fillStyle = color; + for (var i=0; i= 0) { - // fire event to signal external editor that symbol coords have changed - gui.dispatchEvent(type, {FID: e.id, layer_name: hit.getHitTarget().layer.name}); - } + if (e.id >= 0 === false) return; + var o = { + FID: e.id, + layer_name: hit.getHitTarget().layer.name, + vertex_ids: activeVertexIds + }; + // fire event to signal external editor that symbol coords have changed + gui.dispatchEvent(type, o); } function getLabelRecordById(id) { @@ -347,8 +365,7 @@ export function SymbolDragging2(gui, ext, hit) { dragging = false; activeId = -1; activeRecord = null; - // targetTextNode = null; - // svg.removeAttribute('class'); + activeVertexIds = null; } function isClickEvent(up, down) { diff --git a/src/gui/gui-undo.js b/src/gui/gui-undo.js index ec5db5fed..60d1f0c84 100644 --- a/src/gui/gui-undo.js +++ b/src/gui/gui-undo.js @@ -1,6 +1,9 @@ - -import { cloneShape } from '../paths/mapshaper-shape-utils'; -import { copyRecord } from '../datatable/mapshaper-data-utils'; +import { internal } from './gui-core'; +import { snapVerticesToPoint } from './gui-symbol-dragging2'; +// import { cloneShape } from '../paths/mapshaper-shape-utils'; +// import { copyRecord } from '../datatable/mapshaper-data-utils'; +var cloneShape = internal.cloneShape; +var copyRecord = internal.copyRecord; export function Undo(gui) { var history, offset, stashedUndo; @@ -62,7 +65,6 @@ export function Undo(gui) { this.addHistoryState(stashedUndo, redo); }, this); - // undo/redo data editing // TODO: consider setting selected feature to the undo/redo target feature // @@ -75,6 +77,16 @@ export function Undo(gui) { this.addHistoryState(stashedUndo, redo); }, this); + // undo/redo vertex dragging + gui.on('vertex_dragstart', function(e) { + stashedUndo = this.makeVertexSetter(e.FID, e.vertex_ids); + }, this); + + gui.on('vertex_dragend', function(e) { + var redo = this.makeVertexSetter(e.FID, e.vertex_ids); + this.addHistoryState(stashedUndo, redo); + }, this); + this.clear = function() { reset(); }; @@ -96,6 +108,15 @@ export function Undo(gui) { }; }; + this.makeVertexSetter = function(fid, ids) { + var target = gui.model.getActiveLayer(); + var arcs = target.dataset.arcs; + var p = arcs.getVertex2(ids[0]); + return function() { + snapVerticesToPoint(ids, p, arcs, true); + }; + }; + this.addHistoryState = function(undo, redo) { if (offset > 0) { history.splice(-offset); @@ -127,5 +148,3 @@ export function Undo(gui) { } } - - diff --git a/src/paths/mapshaper-arcs.js b/src/paths/mapshaper-arcs.js index fc92a3af5..a5b10be63 100644 --- a/src/paths/mapshaper-arcs.js +++ b/src/paths/mapshaper-arcs.js @@ -373,6 +373,10 @@ export function ArcCollection() { return i - i2; }; + this.getVertex2 = function(i) { + return [_xx[i], _yy[i]]; + }; + this.getVertex = function(arcId, nth) { var i = this.indexOfVertex(arcId, nth); return { From fa8af49e16e91bde710ab9db665181af03ef06a9 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 25 Jan 2022 16:42:16 -0500 Subject: [PATCH 151/891] fix text paste bug --- src/gui/gui-import-control.js | 4 ++++ src/gui/gui-lib.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/gui/gui-import-control.js b/src/gui/gui-import-control.js index acf2e7923..fde6514a8 100644 --- a/src/gui/gui-import-control.js +++ b/src/gui/gui-import-control.js @@ -22,6 +22,10 @@ function DropControl(gui, el, cb) { var types = Array.from(e.clipboardData.types || []).join(','); var items = Array.from(e.clipboardData.items || []); var files; + if (GUI.textIsSelected()) { + // user is probably pasting text into an editable text field + return; + } block(e); // Browser compatibility (tested on MacOS only): // Chrome and Safari: full support diff --git a/src/gui/gui-lib.js b/src/gui/gui-lib.js index 4b88c0710..74ab2eb6e 100644 --- a/src/gui/gui-lib.js +++ b/src/gui/gui-lib.js @@ -66,6 +66,10 @@ GUI.getInputElement = function() { return (el && (el.tagName == 'INPUT' || el.contentEditable == 'true')) ? el : null; }; +GUI.textIsSelected = function() { + return !!GUI.getInputElement(); +}; + GUI.selectElement = function(el) { var range = document.createRange(), sel = window.getSelection(); From cae1de3ebc536fbaa366c67e4366cad524dafc74 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 25 Jan 2022 16:46:08 -0500 Subject: [PATCH 152/891] Show vertices in vertex editing mode --- src/gui/gui-canvas.js | 2 +- src/gui/gui-interaction-mode-control.js | 4 + src/gui/gui-interactive-selection.js | 3 +- src/gui/gui-layer-control.js | 2 +- src/gui/gui-map-extent.js | 1 + src/gui/gui-map.js | 12 +-- src/gui/gui-symbol-dragging2.js | 116 ++++++------------------ src/gui/gui-undo.js | 11 +-- 8 files changed, 44 insertions(+), 107 deletions(-) diff --git a/src/gui/gui-canvas.js b/src/gui/gui-canvas.js index 16fb21d3e..d64e69ddd 100644 --- a/src/gui/gui-canvas.js +++ b/src/gui/gui-canvas.js @@ -147,7 +147,7 @@ export function DisplayCanvas() { _self.drawVertices = function(shapes, arcs, style, filter) { var iter = new internal.ShapeIter(arcs); var t = getScaledTransform(_ext); - var radius = (style.strokeWidth * 0.8 || 2.2) * GUI.getPixelRatio() * getScaledLineScale(_ext); + var radius = (style.strokeWidth * 0.9 || 2.2) * GUI.getPixelRatio() * getScaledLineScale(_ext); var color = style.strokeColor || 'black'; var shp; _ctx.beginPath(); diff --git a/src/gui/gui-interaction-mode-control.js b/src/gui/gui-interaction-mode-control.js index f0e9377ce..40b9a66d0 100644 --- a/src/gui/gui-interaction-mode-control.js +++ b/src/gui/gui-interaction-mode-control.js @@ -5,6 +5,7 @@ export function InteractionMode(gui) { var menus = { standard: ['info', 'selection', 'data', 'box'], + polygons: ['info', 'selection', 'data', 'box', 'vertices'], lines: ['info', 'selection', 'data', 'box', 'vertices'], table: ['info', 'selection', 'data'], labels: ['info', 'selection', 'data', 'box', 'labels', 'location'], @@ -116,6 +117,9 @@ export function InteractionMode(gui) { if (internal.layerHasPaths(o.layer) && o.layer.geometry_type == 'polyline') { return menus.lines; } + if (internal.layerHasPaths(o.layer) && o.layer.geometry_type == 'polygon') { + return menus.polygons; + } return menus.standard; } diff --git a/src/gui/gui-interactive-selection.js b/src/gui/gui-interactive-selection.js index 22f30fa7d..5f648bcff 100644 --- a/src/gui/gui-interactive-selection.js +++ b/src/gui/gui-interactive-selection.js @@ -30,7 +30,7 @@ export function InteractiveSelection(gui, ext, mouse) { } // ignore keypress if no feature is selected or user is editing text - if (pinnedId() == -1 || GUI.getInputElement()) return; + if (pinnedId() == -1 || GUI.textIsSelected()) return; if (e.keyCode == 37 || e.keyCode == 39) { // L/R arrow keys @@ -188,6 +188,7 @@ export function InteractiveSelection(gui, ext, mouse) { // Hits are re-detected on 'hover' (if hit detection is active) mouse.on('hover', function(e) { + handlePointerEvent(e); if (storedData.pinned || !hitTest || !active) return; if (e.hover && isOverMap(e)) { // mouse is hovering directly over map area -- update hit detection diff --git a/src/gui/gui-layer-control.js b/src/gui/gui-layer-control.js index 21ed23e9c..a0ebc563a 100644 --- a/src/gui/gui-layer-control.js +++ b/src/gui/gui-layer-control.js @@ -283,7 +283,7 @@ export function LayerControl(gui) { GUI.onClick(entry, function() { var target = findLayerById(id); // don't select if user is typing or dragging - if (!GUI.getInputElement() && !dragging) { + if (!GUI.textIsSelected() && !dragging) { gui.clearMode(); if (!map.isActiveLayer(target.layer)) { model.selectLayer(target.layer, target.dataset); diff --git a/src/gui/gui-map-extent.js b/src/gui/gui-map-extent.js index dde9beac5..fb1a55c8d 100644 --- a/src/gui/gui-map-extent.js +++ b/src/gui/gui-map-extent.js @@ -65,6 +65,7 @@ export function MapExtent(_position) { this.maxScale = maxScale; + // Display scale, e.g. meters per pixel or degrees per pixel this.getPixelSize = function() { return 1 / this.getTransform().mx; }; diff --git a/src/gui/gui-map.js b/src/gui/gui-map.js index f18972680..f5629d6e4 100644 --- a/src/gui/gui-map.js +++ b/src/gui/gui-map.js @@ -5,7 +5,7 @@ import { SelectionTool } from './gui-selection-tool'; import { InspectionControl2 } from './gui-inspection-control2'; import { updateLayerStackOrder, filterLayerByIds } from './gui-layer-utils'; import { mapNeedsReset } from './gui-map-utils'; -import { SymbolDragging2 } from './gui-symbol-dragging2'; +import { InteractiveEditor } from './gui-symbol-dragging2'; import * as MapStyle from './gui-map-style'; import { MapExtent } from './gui-map-extent'; import { LayerStack } from './gui-layer-stack'; @@ -56,7 +56,7 @@ export function MshpMap(gui) { _mouse.disable(); }); - gui.on('undo_redo', function() { + gui.on('map-needs-refresh', function() { drawLayers(); }); @@ -218,13 +218,7 @@ export function MshpMap(gui) { }); } - if (true) { // TODO: add option to disable? - _editor = new SymbolDragging2(gui, _ext, _hit); - _editor.on('location_change', function(e) { - // TODO: look into optimizing, so only changed symbol is redrawn - drawLayers(); - }); - } + _editor = new InteractiveEditor(gui, _ext, _hit); _ext.on('change', function(e) { if (e.reset) return; // don't need to redraw map here if extent has been reset diff --git a/src/gui/gui-symbol-dragging2.js b/src/gui/gui-symbol-dragging2.js index 3d8d18fb5..f693031eb 100644 --- a/src/gui/gui-symbol-dragging2.js +++ b/src/gui/gui-symbol-dragging2.js @@ -3,21 +3,13 @@ import { isMultilineLabel, toggleTextAlign, setMultilineAttribute, autoUpdateTex import { error, internal } from './gui-core'; import { EventDispatcher } from './gui-events'; +var snapVerticesToPoint = internal.snapVerticesToPoint; + function getDisplayCoordsById(id, layer, ext) { var coords = getPointCoordsById(id, layer); return ext.translateCoords(coords[0], coords[1]); } -export function snapVerticesToPoint(ids, p, arcs, final) { - ids.forEach(function(idx) { - internal.setVertexCoords(p[0], p[1], idx, arcs); - }); - if (final) { - // kludge to get dataset to recalculate internal bounding boxes - arcs.transformPoints(function() {}); - } -} - function getPointCoordsById(id, layer) { var coords = layer && layer.geometry_type == 'point' && layer.shapes[id]; if (!coords || coords.length != 1) { @@ -33,7 +25,7 @@ function translateDeltaDisplayCoords(dx, dy, ext) { } -export function SymbolDragging2(gui, ext, hit) { +export function InteractiveEditor(gui, ext, hit) { // var targetTextNode; // text node currently being dragged var dragging = false; var activeRecord; @@ -148,10 +140,27 @@ export function SymbolDragging2(gui, ext, hit) { hit.on('click', function(e) { if (labelEditingEnabled()) { + var target = hit.getHitTarget(); onLabelClick(e); } }); + // TODO: highlight hit vertex in path edit mode + if (false) hit.on('hover', function(e) { + if (vertexEditingEnabled() && !dragging) { + onVertexHover(e); + } + }, null, 100); + + function onVertexHover(e) { + // hovering in vertex edit mode: find vertex insertion point + var target = hit.getHitTarget(); + var shp = target.layer.shapes[e.id]; + var p = ext.translatePixelCoords(e.x, e.y); + var o = internal.findInsertionPoint(p, shp, target.arcs, ext.getPixelSize()); + } + + function getVertexEventData(e) { return { FID: activeId, @@ -166,14 +175,15 @@ export function SymbolDragging2(gui, ext, hit) { var diff = translateDeltaDisplayCoords(e.dx, e.dy, ext); p[0] += diff[0]; p[1] += diff[1]; - self.dispatchEvent('location_change'); // signal map to redraw + triggerRedraw(); triggerGlobalEvent('symbol_drag', e); } function onVertexDragStart(e) { var target = hit.getHitTarget(); + var shp = target.layer.shapes[e.id]; var p = ext.translatePixelCoords(e.x, e.y); - activeVertexIds = internal.findNearestVertices(p, target.layer.shapes[e.id], target.arcs); + activeVertexIds = internal.findNearestVertices(p, shp, target.arcs); activeId = e.id; } @@ -185,7 +195,7 @@ export function SymbolDragging2(gui, ext, hit) { internal.snapPointToArcEndpoint(p, activeVertexIds, target.arcs); } snapVerticesToPoint(activeVertexIds, p, target.arcs); - self.dispatchEvent('location_change'); // signal map to redraw + triggerRedraw(); } function onLabelClick(e) { @@ -198,6 +208,10 @@ export function SymbolDragging2(gui, ext, hit) { } } + function triggerRedraw() { + gui.dispatchEvent('map-needs-refresh'); + } + function triggerGlobalEvent(type, e) { if (e.id >= 0 === false) return; var o = { @@ -281,80 +295,6 @@ export function SymbolDragging2(gui, ext, hit) { } return el.tagName == 'text' ? el : null; } - - // svg.addEventListener('mousedown', function(e) { - // var textTarget = getTextTarget(e); - // downEvt = e; - // if (!textTarget) { - // stopEditing(); - // } else if (!editing) { - // // nop - // } else if (textTarget == targetTextNode) { - // startDragging(); - // } else { - // startDragging(); - // editTextNode(textTarget); - // } - // }); - - // up event on svg - // a: currently dragging text - // -> stop dragging - // b: clicked on a text feature - // -> start editing it - - - // svg.addEventListener('mouseup', function(e) { - // var textTarget = getTextTarget(e); - // var isClick = isClickEvent(e, downEvt); - // if (isClick && textTarget && textTarget == targetTextNode && - // activeRecord && isMultilineLabel(targetTextNode)) { - // toggleTextAlign(targetTextNode, activeRecord); - // updateSymbol(); - // } - // if (dragging) { - // stopDragging(); - // } else if (isClick && textTarget) { - // editTextNode(textTarget); - // } - // }); - - // block dbl-click navigation when editing - // mouse.on('dblclick', function(e) { - // if (editing) e.stopPropagation(); - // }, null, eventPriority); - - // mouse.on('dragstart', function(e) { - // onLabelDrag(e); - // }, null, eventPriority); - - // mouse.on('drag', function(e) { - // var scale = ext.getSymbolScale() || 1; - // onLabelDrag(e); - // if (!dragging || !activeRecord) return; - // applyDelta(activeRecord, 'dx', e.dx / scale); - // applyDelta(activeRecord, 'dy', e.dy / scale); - // if (!isMultilineLabel(targetTextNode)) { - // // update anchor position of single-line labels based on label position - // // relative to anchor point, for better placement when eventual display font is - // // different from mapshaper's font. - // updateTextAnchor(targetTextNode, activeRecord); - // } - // // updateSymbol(targetTextNode, activeRecord); - // targetTextNode = updateSymbol2(targetTextNode, activeRecord, activeId); - // }, null, eventPriority); - - // mouse.on('dragend', function(e) { - // onLabelDrag(e); - // stopDragging(); - // }, null, eventPriority); - - - // function onLabelDrag(e) { - // if (dragging) { - // e.stopPropagation(); - // } - // } } function startDragging() { diff --git a/src/gui/gui-undo.js b/src/gui/gui-undo.js index 60d1f0c84..aee8c0290 100644 --- a/src/gui/gui-undo.js +++ b/src/gui/gui-undo.js @@ -1,7 +1,7 @@ import { internal } from './gui-core'; -import { snapVerticesToPoint } from './gui-symbol-dragging2'; // import { cloneShape } from '../paths/mapshaper-shape-utils'; // import { copyRecord } from '../datatable/mapshaper-data-utils'; +var snapVerticesToPoint = internal.snapVerticesToPoint; var cloneShape = internal.cloneShape; var copyRecord = internal.copyRecord; @@ -15,9 +15,6 @@ export function Undo(gui) { offset = 0; } - function refreshMap() { - gui.dispatchEvent('undo_redo'); - } function isUndoEvt(e) { return (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key == 'z'; @@ -111,7 +108,7 @@ export function Undo(gui) { this.makeVertexSetter = function(fid, ids) { var target = gui.model.getActiveLayer(); var arcs = target.dataset.arcs; - var p = arcs.getVertex2(ids[0]); + var p = internal.getVertexCoords(ids[0], arcs); return function() { snapVerticesToPoint(ids, p, arcs, true); }; @@ -130,7 +127,7 @@ export function Undo(gui) { if (item) { offset++; item.undo(); - refreshMap(); + gui.dispatchEvent('map-needs-refresh'); } }; @@ -139,7 +136,7 @@ export function Undo(gui) { offset--; var item = getHistoryItem(); item.redo(); - refreshMap(); + gui.dispatchEvent('map-needs-refresh'); }; function getHistoryItem() { From 6b5e6a114114b3ceb172c6793ac33d6926329e44 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 25 Jan 2022 16:47:01 -0500 Subject: [PATCH 153/891] Add control flow commands if/elif/else/endif --- src/cli/mapshaper-command-parser.js | 7 ++ src/cli/mapshaper-options.js | 51 ++++++++-- src/cli/mapshaper-run-command.js | 15 +++ src/cli/mapshaper-run-commands.js | 3 + src/commands/mapshaper-if-elif-else-endif.js | 98 +++++++++++++++++++ src/expressions/mapshaper-expression-utils.js | 18 ++-- src/expressions/mapshaper-expressions.js | 24 ++--- .../mapshaper-layer-expressions.js | 17 ++++ src/expressions/mapshaper-layer-proxy.js | 60 +++++++----- src/geom/mapshaper-rounding.js | 11 +++ src/mapshaper-control-flow.js | 36 +++++++ src/paths/mapshaper-arcs.js | 4 - src/paths/mapshaper-shape-iter.js | 5 + src/paths/mapshaper-vertex-utils.js | 22 +++++ src/symbols/mapshaper-arrow-symbols.js | 4 +- test/if-elif-else-test.js | 54 ++++++++++ 16 files changed, 370 insertions(+), 59 deletions(-) create mode 100644 src/commands/mapshaper-if-elif-else-endif.js create mode 100644 src/expressions/mapshaper-layer-expressions.js create mode 100644 src/mapshaper-control-flow.js create mode 100644 test/if-elif-else-test.js diff --git a/src/cli/mapshaper-command-parser.js b/src/cli/mapshaper-command-parser.js index ce58cece0..506a4afb9 100644 --- a/src/cli/mapshaper-command-parser.js +++ b/src/cli/mapshaper-command-parser.js @@ -434,6 +434,13 @@ function CommandOptions(name) { return this; }; + this.options = function(o) { + Object.keys(o).forEach(function(key) { + this.option(key, o[key]); + }, this); + return this; + }; + this.done = function() { return _command; }; diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index 2c8b19f66..beb5d8fbd 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -960,14 +960,6 @@ export function getOptionParser() { .option('target', targetOpt) .option('no-replace', noReplaceOpt); - parser.command('ignore') - // .describe('stop processing if a condition is met') - .option('empty', { - describe: 'ignore empty files', - type: 'flag' - }) - .option('target', targetOpt); - parser.command('include') .describe('import JS data and functions for use in JS expressions') .option('file', { @@ -1930,6 +1922,49 @@ export function getOptionParser() { }) .option('target', targetOpt); + parser.section('Control flow commands'); + + var ifOpts = { + expression: { + DEFAULT: true, + describe: 'JS expression targeting a single layer' + }, + empty: { + describe: 'run if layer is empty', + type: 'flag' + }, + 'not-empty': { + describe: 'run if layer is not empty', + type: 'flag' + }, + layer: { + describe: 'name or id of layer to test (default is current target)' + }, + target: targetOpt + }; + + parser.command('if') + .describe('run the following commands if a condition is met') + .options(ifOpts); + + parser.command('elif') + .describe('test an alternate condition; used after -if') + .options(ifOpts); + + parser.command('else') + .describe('run commands if all preceding -if/-elif conditions are false'); + + parser.command('endif') + .describe('mark the end of an -if sequence'); + + parser.command('ignore') + // .describe('stop processing if a condition is met') + .option('empty', { + describe: 'ignore empty files', + type: 'flag' + }) + .option('target', targetOpt); + parser.section('Informational commands'); parser.command('calc') diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index e99199cf6..3a500f1c6 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -44,6 +44,7 @@ import '../commands/mapshaper-filter-points'; import '../commands/mapshaper-filter-slivers'; import '../commands/mapshaper-fuzzy-join'; import '../commands/mapshaper-graticule'; +import { skipCommand } from '../commands/mapshaper-if-elif-else-endif'; import '../commands/mapshaper-ignore'; import '../commands/mapshaper-include'; import '../commands/mapshaper-info'; @@ -92,8 +93,13 @@ export function runCommand(command, catalog, cb) { targets, targetDataset, targetLayers, + target, arcs; + if (skipCommand(name)) { + return done(null); + } + try { // catch errors from synchronous functions T.start(); @@ -260,6 +266,14 @@ export function runCommand(command, catalog, cb) { outputLayers = targetDataset.layers; // kludge to allow layer naming below } + } else if (name == 'if' || name == 'elif') { + // target = findSingleTargetLayer(opts.layer, targets[0], catalog); + // cmd[name](target.layer, target.dataset, opts); + cmd[name](catalog, opts); + + } else if (name == 'else' || name == 'endif') { + cmd[name](); + } else if (name == 'ignore') { applyCommandToEachLayer(cmd.ignore, targetLayers, targetDataset, opts); @@ -492,6 +506,7 @@ function outputLayersAreDifferent(output, input) { }); } + // Apply a command to an array of target layers function applyCommandToEachLayer(func, targetLayers) { var args = utils.toArray(arguments).slice(2); diff --git a/src/cli/mapshaper-run-commands.js b/src/cli/mapshaper-run-commands.js index 5dff37638..12a6305c6 100644 --- a/src/cli/mapshaper-run-commands.js +++ b/src/cli/mapshaper-run-commands.js @@ -9,6 +9,7 @@ import { createAsyncContext } from '../mapshaper-state'; import { Catalog } from '../dataset/mapshaper-catalog'; import { setStateVar, runningInBrowser } from '../mapshaper-state'; import utils from '../utils/mapshaper-utils'; +import { resetControlFlow } from '../mapshaper-control-flow'; // Parse command line args into commands and run them @@ -255,6 +256,8 @@ function filterError(err) { } function runParsedCommands2(commands, catalog, cb) { + // resetting closes any unterminated -if blocks from a previous command sequence + resetControlFlow(); utils.reduceAsync(commands, catalog, nextCommand, cb); function nextCommand(catalog, cmd, next) { diff --git a/src/commands/mapshaper-if-elif-else-endif.js b/src/commands/mapshaper-if-elif-else-endif.js new file mode 100644 index 000000000..10533126a --- /dev/null +++ b/src/commands/mapshaper-if-elif-else-endif.js @@ -0,0 +1,98 @@ + +import cmd from '../mapshaper-cmd'; +import { layerIsEmpty } from '../dataset/mapshaper-layer-utils'; +import { stop } from '../utils/mapshaper-logging'; +import { + resetControlFlow, + inControlBlock, + enterActiveBranch, + enterInactiveBranch, + inActiveBranch, + blockWasActive +} from '../mapshaper-control-flow'; +import { compileLayerExpression } from '../expressions/mapshaper-layer-expressions'; + +export function skipCommand(cmdName) { + // allow all control commands to run + if (isControlFlowCommand(cmdName)) return false; + return inControlBlock() && !inActiveBranch(); +} + +cmd.if = function(catalog, opts) { + if (inControlBlock()) { + stop('Nested -if commands are not supported.'); + } + evaluateIf(catalog, opts); +}; + +cmd.elif = function(catalog, opts) { + if (!inControlBlock()) { + stop('-elif command must be preceded by an -if command.'); + } + evaluateIf(catalog, opts); +}; + +cmd.else = function() { + if (!inControlBlock()) { + stop('-else command must be preceded by an -if command.'); + } + if (blockWasActive()) { + enterInactiveBranch(); + } else { + enterActiveBranch(); + } +}; + +cmd.endif = function() { + if (!inControlBlock()) { + stop('-endif command must be preceded by an -if command.'); + } + resetControlFlow(); +}; + +function isControlFlowCommand(cmd) { + return ['if','elif','else','endif'].includes(cmd); +} + +function testLayer(catalog, opts) { + var targ = getTargetLayer(catalog, opts); + if (opts.expression) { + return compileLayerExpression(opts.expression, targ.layer, targ.dataset, opts)(); + } + if (opts.empty) { + return layerIsEmpty(targ.layer); + } + if (opts.not_empty) { + return !layerIsEmpty(targ.layer); + } + return true; +} + +function evaluateIf(catalog, opts) { + if (!blockWasActive() && testLayer(catalog, opts)) { + enterActiveBranch(); + } else { + enterInactiveBranch(); + } +} + +// layerId: optional layer identifier +// +function getTargetLayer(catalog, opts) { + var layerId = opts.layer || opts.target; + var targets = catalog.findCommandTargets(layerId); + if (targets.length === 0) { + if (layerId) { + stop('Layer not found:', layerId); + } else { + stop('Missing a target layer.'); + } + } + if (targets.length > 1 || targets[0].layers.length > 1) { + stop('Command requires a single target layer.'); + } + return { + layer: targets[0].layers[0], + dataset: targets[0].dataset + }; +} diff --git a/src/expressions/mapshaper-expression-utils.js b/src/expressions/mapshaper-expression-utils.js index ad8ce9816..2915d8f0f 100644 --- a/src/expressions/mapshaper-expression-utils.js +++ b/src/expressions/mapshaper-expression-utils.js @@ -1,15 +1,17 @@ import utils from '../utils/mapshaper-utils'; import { blend } from '../color/blending'; +import { roundToDigits2 } from '../geom/mapshaper-rounding'; -export function addUtils(env) { + +export function cleanExpression(exp) { + // workaround for problem in GNU Make v4: end-of-line backslashes inside + // quoted strings are left in the string (other shell environments remove them) + return exp.replace(/\\\n/g, ' '); +} + +export function addFeatureExpressionUtils(env) { Object.assign(env, { - round: function(val, dig) { - var k = 1; - if (!val && val !== 0) return val; // don't coerce null to 0 - dig = dig | 0; - while(dig-- > 0) k *= 10; - return Math.round(val * k) / k; - }, + round: roundToDigits2, int_median: interpolated_median, sprintf: utils.format, blend: blend diff --git a/src/expressions/mapshaper-expressions.js b/src/expressions/mapshaper-expressions.js index 794b43411..41a1837fc 100644 --- a/src/expressions/mapshaper-expressions.js +++ b/src/expressions/mapshaper-expressions.js @@ -1,5 +1,5 @@ -import { addUtils } from '../expressions/mapshaper-expression-utils'; +import { addFeatureExpressionUtils, cleanExpression } from '../expressions/mapshaper-expression-utils'; import { initFeatureProxy } from '../expressions/mapshaper-feature-proxy'; import { addLayerGetters } from '../expressions/mapshaper-layer-proxy'; import { initDataTable } from '../dataset/mapshaper-layer-utils'; @@ -14,11 +14,6 @@ export function compileValueExpression(exp, lyr, arcs, opts) { return compileFeatureExpression(exp, lyr, arcs, opts); } -export function cleanExpression(exp) { - // workaround for problem in GNU Make v4: end-of-line backslashes inside - // quoted strings are left in the string (other shell environments remove them) - return exp.replace(/\\\n/g, ' '); -} export function compileFeaturePairFilterExpression(exp, lyr, arcs) { var func = compileFeaturePairExpression(exp, lyr, arcs); @@ -154,14 +149,19 @@ export function getAssignmentObjects(exp) { export function compileExpressionToFunction(exp, opts) { // $$ added to avoid duplication with data field variables (an error condition) - var functionBody = "with($$env){with($$record){ " + (opts.returns ? 'return ' : '') + - exp + "}}"; - var func; + var functionBody, func; + if (opts.returns) { + // functionBody = 'return ' + functionBody; + functionBody = 'var $$retn = ' + exp + '; return $$retn;'; + } else { + functionBody = exp; + } + functionBody = 'with($$env){with($$record){ ' + functionBody + '}}'; try { - func = new Function("$$record,$$env", functionBody); + func = new Function('$$record,$$env', functionBody); } catch(e) { // if (opts.quiet) throw e; - stop(e.name, "in expression [" + exp + "]"); + stop(e.name, 'in expression [' + exp + ']'); } return func; } @@ -204,7 +204,7 @@ function getExpressionContext(lyr, mixins, opts) { var ctx = {}; var fields = lyr.data ? lyr.data.getFields() : []; opts = opts || {}; - addUtils(env); // mix in round(), sprintf(), etc. + addFeatureExpressionUtils(env); // mix in round(), sprintf(), etc. if (fields.length > 0) { // default to null values, so assignments to missing data properties // are applied to the data record, not the global object diff --git a/src/expressions/mapshaper-layer-expressions.js b/src/expressions/mapshaper-layer-expressions.js new file mode 100644 index 000000000..86d5760b5 --- /dev/null +++ b/src/expressions/mapshaper-layer-expressions.js @@ -0,0 +1,17 @@ +import { getLayerProxy } from './mapshaper-layer-proxy'; +import { compileExpressionToFunction } from './mapshaper-expressions'; +import { stop } from '../utils/mapshaper-logging'; +export function compileLayerExpression(expr, lyr, dataset, opts) { + var proxy = getLayerProxy(lyr, dataset.arcs, opts); + var exprOpts = Object.assign({returns: true}, opts); + var func = compileExpressionToFunction(expr, exprOpts); + var ctx = proxy; + return function() { + try { + return func.call(proxy, {}, ctx); + } catch(e) { + // if (opts.quiet) throw e; + stop(e.name, "in expression [" + expr + "]:", e.message); + } + }; +} diff --git a/src/expressions/mapshaper-layer-proxy.js b/src/expressions/mapshaper-layer-proxy.js index abeff18cb..27bc3ab31 100644 --- a/src/expressions/mapshaper-layer-proxy.js +++ b/src/expressions/mapshaper-layer-proxy.js @@ -1,5 +1,39 @@ import { getLayerBounds } from '../dataset/mapshaper-layer-utils'; import { addGetters } from '../expressions/mapshaper-expression-utils'; +import { getFeatureCount } from '../dataset/mapshaper-layer-utils'; + +// Returns an object representing a layer in a JS expression +export function getLayerProxy(lyr, arcs) { + var obj = {}; + var records = lyr.data ? lyr.data.getRecords() : null; + var getters = { + name: lyr.name, + data: records, + type: lyr.geometry_type + }; + addGetters(obj, getters); + addBBoxGetter(obj, lyr, arcs); + obj.empty = function() { + return getFeatureCount(lyr) === 0; + }; + obj.size = function() { + return getFeatureCount(lyr); + }; + return obj; +} + +export function addLayerGetters(ctx, lyr, arcs) { + var layerProxy; + addGetters(ctx, { + layer_name: lyr.name || '', // consider removing this + layer: function() { + // init on first access (to avoid overhead if not used) + if (!layerProxy) layerProxy = getLayerProxy(lyr, arcs); + return layerProxy; + } + }); + return ctx; +} export function addBBoxGetter(obj, lyr, arcs) { var bbox; @@ -29,29 +63,3 @@ function getBBox(lyr, arcs) { }); return bbox; } - -// Returns an object representing a layer in a JS expression -export function getLayerProxy(lyr, arcs) { - var obj = {}; - var records = lyr.data ? lyr.data.getRecords() : null; - var getters = { - name: lyr.name, - data: records - }; - addGetters(obj, getters); - addBBoxGetter(obj, lyr, arcs); - return obj; -} - -export function addLayerGetters(ctx, lyr, arcs) { - var layerProxy; - addGetters(ctx, { - layer_name: lyr.name || '', // consider removing this - layer: function() { - // init on first access (to avoid overhead if not used) - if (!layerProxy) layerProxy = getLayerProxy(lyr, arcs); - return layerProxy; - } - }); - return ctx; -} diff --git a/src/geom/mapshaper-rounding.js b/src/geom/mapshaper-rounding.js index 0aae69a6b..777f4346a 100644 --- a/src/geom/mapshaper-rounding.js +++ b/src/geom/mapshaper-rounding.js @@ -7,10 +7,21 @@ export function roundToSignificantDigits(n, d) { return +n.toPrecision(d); } + export function roundToDigits(n, d) { return +n.toFixed(d); // string conversion makes this slow } +// Used in mapshaper-expression-utils.js +// TODO: choose between this and the above function +export function roundToDigits2(n, d) { + var k = 1; + if (!n && n !== 0) return n; // don't coerce null to 0 + d = d | 0; + while (d-- > 0) k *= 10; + return Math.round(n * k) / k; +} + export function roundToTenths(n) { return (Math.round(n * 10)) / 10; } diff --git a/src/mapshaper-control-flow.js b/src/mapshaper-control-flow.js new file mode 100644 index 000000000..665e1be2c --- /dev/null +++ b/src/mapshaper-control-flow.js @@ -0,0 +1,36 @@ +import { getStateVar, setStateVar } from './mapshaper-state'; + +export function resetControlFlow() { + setStateVar('control', null); +} + +export function inControlBlock() { + var state = getState(); + return !!state.inControlBlock; +} + +export function enterActiveBranch() { + var state = getState(); + state.inControlBlock = true; + state.active = true; + state.complete = true; +} + +export function enterInactiveBranch() { + var state = getState(); + state.inControlBlock = true; + state.active = false; +} + +export function blockWasActive() { + return !!getState().complete; +} + +export function inActiveBranch() { + return !!getState().active; +} + +function getState() { + var o = getStateVar('control') || setStateVar('control', {}) || getStateVar('control'); + return o; +} diff --git a/src/paths/mapshaper-arcs.js b/src/paths/mapshaper-arcs.js index a5b10be63..fc92a3af5 100644 --- a/src/paths/mapshaper-arcs.js +++ b/src/paths/mapshaper-arcs.js @@ -373,10 +373,6 @@ export function ArcCollection() { return i - i2; }; - this.getVertex2 = function(i) { - return [_xx[i], _yy[i]]; - }; - this.getVertex = function(arcId, nth) { var i = this.indexOfVertex(arcId, nth); return { diff --git a/src/paths/mapshaper-shape-iter.js b/src/paths/mapshaper-shape-iter.js index cbcdc8051..650ba76da 100644 --- a/src/paths/mapshaper-shape-iter.js +++ b/src/paths/mapshaper-shape-iter.js @@ -109,6 +109,11 @@ export function FilteredArcIter(xx, yy, zz) { }; } +export function MultiShapeIter(arcs) { + var iter = new ShapeIter(arcs); + +} + // Iterate along a path made up of one or more arcs. // export function ShapeIter(arcs) { diff --git a/src/paths/mapshaper-vertex-utils.js b/src/paths/mapshaper-vertex-utils.js index 366e1028f..aa6d4dc27 100644 --- a/src/paths/mapshaper-vertex-utils.js +++ b/src/paths/mapshaper-vertex-utils.js @@ -5,6 +5,28 @@ export function findNearestVertices(p, shp, arcs) { return findVertexIds(p2.x, p2.y, arcs); } +// Given a location @p (e.g. corresponding to the mouse pointer location), +// find the midpoint of two vertices on @shp suitable for inserting a new vertex, +// but only if: +// 1. point @p is closer to the midpoint than either adjacent vertex +// 2. the segment containing @p is longer than a minimum distance in pixels. +// +export function findInsertionPoint(p, shp, arcs, pixelSize) { + var p2 = findNearestVertex(p[0], p[1], shp, arcs); + +} + +export function snapVerticesToPoint(ids, p, arcs, final) { + ids.forEach(function(idx) { + setVertexCoords(p[0], p[1], idx, arcs); + }); + if (final) { + // kludge to get dataset to recalculate internal bounding boxes + arcs.transformPoints(function() {}); + } +} + + // p: point to snap // ids: ids of nearby vertices, possibly including an arc endpoint export function snapPointToArcEndpoint(p, ids, arcs) { diff --git a/src/symbols/mapshaper-arrow-symbols.js b/src/symbols/mapshaper-arrow-symbols.js index f5de01afb..d31106641 100644 --- a/src/symbols/mapshaper-arrow-symbols.js +++ b/src/symbols/mapshaper-arrow-symbols.js @@ -142,7 +142,9 @@ export function getFilledArrowCoords(d) { if (d.anchor == 'end') { scaleAndShiftCoords(coords, 1, [-dx, -dy - headLen]); } else if (d.anchor == 'middle') { - scaleAndShiftCoords(coords, 1, [-dx/2, (-dy - headLen)/2]); + // shift midpoint away from the head a bit for a more balanced placement + // scaleAndShiftCoords(coords, 1, [-dx/2, (-dy - headLen)/2]); + scaleAndShiftCoords(coords, 1, [-dx * 0.5, -dy * 0.5 - headLen * 0.25]); } rotateCoords(coords, direction); diff --git a/test/if-elif-else-test.js b/test/if-elif-else-test.js new file mode 100644 index 000000000..bd5703867 --- /dev/null +++ b/test/if-elif-else-test.js @@ -0,0 +1,54 @@ +var api = require('..'), + assert = require('assert'); + +describe('mapshaper-if-elif-else-endif.js', function () { + it ('test empty flag', function(done) { + var data = [{name: 'a'}, {name: 'b'}]; + var cmd = `-i data.json -if empty -each 'id = this.id' -else -each 'fid = this.id' \ + -endif -each 'name = name + name' -o`; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var data = JSON.parse(out['data.json']); + assert.deepEqual(data, [{ + name: 'aa', fid: 0 + }, { + name: 'bb', fid: 1 + }]); + done(); + }); + }); + + it ('test expression', function(done) { + var data = { + type: 'GeometryCollection', + geometries: [{type: 'Point', coordinates: [1,2]}, {type: 'Point', coordinates: [2, 1]}] + }; + var cmd = `-i data.json -if false -elif 'this.type == "point"' -each 'id = this.id' \ + -elif 'this.type == "point"' -each 'id = "BAR"' -endif -o format=json`; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var data = JSON.parse(out['data.json']); + assert.deepEqual(data, [{ + id: 0 + }, { + id: 1 + }]); + done(); + }); + }); + + it ('test not-empty flag', function(done) { + var data = [{name: 'a'}, {name: 'b'}]; + var cmd = `-i data.json -if not-empty -each 'id = this.id' -else -each 'fid = this.id' \ + -endif -each 'name = name + name' -o`; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var data = JSON.parse(out['data.json']); + assert.deepEqual(data, [{ + name: 'aa', id: 0 + }, { + name: 'bb', id: 1 + }]); + done(); + }); + }); + + +}) From 174545d27c9efc89bda1e6092ce8b61f347a9875 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 25 Jan 2022 16:49:56 -0500 Subject: [PATCH 154/891] v0.5.88 --- CHANGELOG.md | 4 + package-lock.json | 3385 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- 3 files changed, 3366 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6220fe82..3d095f35e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.88 +* Added -if/-elif/-else/-endif commands for running commands selectively. +* Bug fixes + v0.5.87 * Added undo/redo to the "drag vertices" interactive editing mode. diff --git a/package-lock.json b/package-lock.json index ed0e1db45..b2e58d3e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,3345 @@ { "name": "mapshaper", - "version": "0.5.87", - "lockfileVersion": 1, + "version": "0.5.88", + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "mapshaper", + "version": "0.5.88", + "license": "MPL-2.0", + "dependencies": { + "commander": "^5.1.0", + "cookies": "^0.8.0", + "d3-color": "2.0.0", + "d3-scale-chromatic": "2.0.0", + "delaunator": "^5.0.0", + "flatbush": "^3.2.1", + "geokdbush": "^1.1.0", + "iconv-lite": "0.4.24", + "kdbush": "^3.0.0", + "mproj": "0.0.35", + "opn": "^5.3.0", + "rw": "~1.3.3", + "sync-request": "5.0.0", + "tinyqueue": "^2.0.3" + }, + "bin": { + "mapshaper": "bin/mapshaper", + "mapshaper-gui": "bin/mapshaper-gui", + "mapshaper-xl": "bin/mapshaper-xl" + }, + "devDependencies": { + "@rollup/plugin-node-resolve": "^13.0.6", + "browserify": "^17.0.0", + "csv-spectrum": "^1.0.0", + "deep-eql": ">=0.1.3", + "esm": "^3.2.25", + "mocha": "^8.4.0", + "rollup": "^2.60.0", + "shell-quote": "^1.6.1", + "underscore": "^1.13.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.0.6.tgz", + "integrity": "sha512-sFsPDMPd4gMqnh2gS0uIxELnoRUp5kBl5knxD2EO0778G1oOJv4G1vyT2cpWz75OU2jDVcXhjVUuTAczGyFNKA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^2.42.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@types/concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "node_modules/@types/form-data": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", + "integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "10.17.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.18.tgz", + "integrity": "sha512-DQ2hl/Jl3g33KuAUOcMrcAOtsbzb+y/ufakzAdeK9z/H/xsvkpbETZZbPNMIiQuk24f5ZRMCcZIViAwyFIiKmg==" + }, + "node_modules/@types/qs": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz", + "integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==" + }, + "node_modules/@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dev": true, + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1", + "util": "0.10.3" + } + }, + "node_modules/assert/node_modules/inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "node_modules/assert/node_modules/util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "dependencies": { + "inherits": "2.0.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.4.tgz", + "integrity": "sha512-SA5mXJWrId1TaQjfxUYghbqQ/hYioKmLJvPJyDuYRtXXenFNMjj4hSSt1Cf1xsuXSXrtxrVC5Ot4eU6cOtBDdA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "node_modules/browser-pack": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", + "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", + "dev": true, + "dependencies": { + "combine-source-map": "~0.8.0", + "defined": "^1.0.0", + "JSONStream": "^1.0.3", + "safe-buffer": "^5.1.1", + "through2": "^2.0.0", + "umd": "^3.0.0" + }, + "bin": { + "browser-pack": "bin/cmd.js" + } + }, + "node_modules/browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "dev": true, + "dependencies": { + "resolve": "^1.17.0" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/browserify": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-17.0.0.tgz", + "integrity": "sha512-SaHqzhku9v/j6XsQMRxPyBrSP3gnwmE27gLJYZgMT2GeK3J0+0toN+MnuNYDfHwVGQfLiMZ7KSNSIXHemy905w==", + "dev": true, + "dependencies": { + "assert": "^1.4.0", + "browser-pack": "^6.0.1", + "browser-resolve": "^2.0.0", + "browserify-zlib": "~0.2.0", + "buffer": "~5.2.1", + "cached-path-relative": "^1.0.0", + "concat-stream": "^1.6.0", + "console-browserify": "^1.1.0", + "constants-browserify": "~1.0.0", + "crypto-browserify": "^3.0.0", + "defined": "^1.0.0", + "deps-sort": "^2.0.1", + "domain-browser": "^1.2.0", + "duplexer2": "~0.1.2", + "events": "^3.0.0", + "glob": "^7.1.0", + "has": "^1.0.0", + "htmlescape": "^1.1.0", + "https-browserify": "^1.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "^7.2.1", + "JSONStream": "^1.0.3", + "labeled-stream-splicer": "^2.0.0", + "mkdirp-classic": "^0.5.2", + "module-deps": "^6.2.3", + "os-browserify": "~0.3.0", + "parents": "^1.0.1", + "path-browserify": "^1.0.0", + "process": "~0.11.0", + "punycode": "^1.3.2", + "querystring-es3": "~0.2.0", + "read-only-stream": "^2.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.4", + "shasum-object": "^1.0.0", + "shell-quote": "^1.6.1", + "stream-browserify": "^3.0.0", + "stream-http": "^3.0.0", + "string_decoder": "^1.1.1", + "subarg": "^1.0.0", + "syntax-error": "^1.1.1", + "through2": "^2.0.0", + "timers-browserify": "^1.0.1", + "tty-browserify": "0.0.1", + "url": "~0.11.0", + "util": "~0.12.0", + "vm-browserify": "^1.0.0", + "xtend": "^4.0.0" + }, + "bin": { + "browserify": "bin/cmd.js" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "dev": true, + "dependencies": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "dev": true, + "dependencies": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/browserify-sign/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "dev": true, + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "node_modules/cached-path-relative": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", + "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", + "dev": true + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.1" + } + }, + "node_modules/cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", + "dev": true, + "dependencies": { + "convert-source-map": "~1.1.0", + "inline-source-map": "~0.6.0", + "lodash.memoize": "~3.0.3", + "source-map": "~0.5.3" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + }, + "node_modules/cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "dependencies": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/csv-spectrum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/csv-spectrum/-/csv-spectrum-1.0.0.tgz", + "integrity": "sha1-WRrJ/0itTz60M4RXvJgBs0nj1ig=", + "dev": true + }, + "node_modules/d3-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", + "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==" + }, + "node_modules/d3-interpolate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", + "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==", + "dependencies": { + "d3-color": "1 - 2" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz", + "integrity": "sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA==", + "dependencies": { + "d3-color": "1 - 2", + "d3-interpolate": "1 - 2" + } + }, + "node_modules/dash-ast": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", + "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.0.0.tgz", + "integrity": "sha512-GxJC5MOg2KyQlv6WiUF/VAnMj4MWnYiXo4oLgeptOELVoknyErb4Z8+5F/IM/K4g9/80YzzatxmWcyRwUseH0A==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/deps-sort": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz", + "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==", + "dev": true, + "dependencies": { + "JSONStream": "^1.0.3", + "shasum-object": "^1.0.0", + "subarg": "^1.0.0", + "through2": "^2.0.0" + }, + "bin": { + "deps-sort": "bin/cmd.js" + } + }, + "node_modules/des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/detective": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", + "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "dev": true, + "dependencies": { + "acorn-node": "^1.6.1", + "defined": "^1.0.0", + "minimist": "^1.1.1" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true, + "engines": { + "node": ">=0.4", + "npm": ">=1.2" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dev": true, + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/es-abstract": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", + "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.10.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==", + "dev": true + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flatbush": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/flatbush/-/flatbush-3.2.1.tgz", + "integrity": "sha512-RAqcCyM18R0HhGIcZ7nTRImHnvmJAQqxSN8VIrRLPyWDuFjxluiyE99wuDqFiwNwBodlHXBQNf/9CrlfSqJq2A==", + "dependencies": { + "flatqueue": "^1.2.0" + } + }, + "node_modules/flatqueue": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/flatqueue/-/flatqueue-1.2.0.tgz", + "integrity": "sha512-Z/nhmRwSywE3xnHXHqbLzJiUZ9akOHZlB1IIqCzRRldWrxqp6EzqGVxTl9Fl5cSoUzC5ge7xq3WIPct8ADYdhw==" + }, + "node_modules/foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/geographiclib": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/geographiclib/-/geographiclib-1.48.0.tgz", + "integrity": "sha1-j/KuGFrTgPZ122okOTX63RR974I=" + }, + "node_modules/geokdbush": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/geokdbush/-/geokdbush-1.1.0.tgz", + "integrity": "sha1-ql6OeVOmWUtAqF+9thBZrw9RRo8=", + "dependencies": { + "tinyqueue": "^1.2.2" + } + }, + "node_modules/geokdbush/node_modules/tinyqueue": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz", + "integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==" + }, + "node_modules/get-assigned-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", + "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", + "dev": true + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=", + "engines": { + "node": ">=4" + } + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "engines": { + "node": ">=4.x" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/hash-base/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/hash-base/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/htmlescape": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/http-basic": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-6.0.0.tgz", + "integrity": "sha512-7ScbVjuiReYe8S+OZOpNjoKGXrbhJHIrQQe7eq1TpLTJkxH8MPKvnTUzq/TNLjww1hdFQy8yUIC42wuLhCjYcQ==", + "dependencies": { + "@types/concat-stream": "^1.6.0", + "@types/node": "^7.0.31", + "caseless": "~0.12.0", + "concat-stream": "^1.4.6", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + } + }, + "node_modules/http-basic/node_modules/@types/node": { + "version": "7.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.9.tgz", + "integrity": "sha512-usSpgoUsRtO5xNV5YEPU8PPnHisFx8u0rokj1BPVn/hDF7zwUDzVLiuKZM38B7z8V2111Fj6kd4rGtQFUZpNOw==" + }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/inline-source-map": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "dev": true, + "dependencies": { + "source-map": "~0.5.3" + } + }, + "node_modules/insert-module-globals": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.1.tgz", + "integrity": "sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==", + "dev": true, + "dependencies": { + "acorn-node": "^1.5.2", + "combine-source-map": "^0.8.0", + "concat-stream": "^1.6.1", + "is-buffer": "^1.1.0", + "JSONStream": "^1.0.3", + "path-is-absolute": "^1.0.1", + "process": "~0.11.0", + "through2": "^2.0.0", + "undeclared-identifiers": "^1.1.2", + "xtend": "^4.0.0" + }, + "bin": { + "insert-module-globals": "bin/cmd.js" + } + }, + "node_modules/is-arguments": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", + "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", + "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", + "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", + "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.9.tgz", + "integrity": "sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "node_modules/is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", + "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", + "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.5.tgz", + "integrity": "sha512-S+GRDgJlR3PyEbsX/Fobd9cqpZBuvUS+8asRqYDMLCb2qMzt1oz5m5oxQCxOgUDxiWsOVNi4yaF+/uvdlHlYug==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.2", + "call-bind": "^1.0.2", + "es-abstract": "^1.18.0-next.2", + "foreach": "^2.0.5", + "has-symbols": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", + "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/kdbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", + "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/labeled-stream-splicer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", + "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "stream-splicer": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "dependencies": { + "mime-db": "1.43.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "node_modules/mocha": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", + "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", + "dev": true, + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.1", + "debug": "4.3.1", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.0.0", + "log-symbols": "4.0.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.1.20", + "serialize-javascript": "5.0.1", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.1.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 10.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/module-deps": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.3.tgz", + "integrity": "sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==", + "dev": true, + "dependencies": { + "browser-resolve": "^2.0.0", + "cached-path-relative": "^1.0.2", + "concat-stream": "~1.6.0", + "defined": "^1.0.0", + "detective": "^5.2.0", + "duplexer2": "^0.1.2", + "inherits": "^2.0.1", + "JSONStream": "^1.0.3", + "parents": "^1.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.4.0", + "stream-combiner2": "^1.1.1", + "subarg": "^1.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + }, + "bin": { + "module-deps": "bin/cmd.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/mproj": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.35.tgz", + "integrity": "sha512-xqO9BXjTezwyPFbAShWRkYZ98DD9wWOyr86WX6miWq3brBNGypsErMmobpUx3G45SrfvyJ5jI997Zw0qr7Ko7A==", + "dependencies": { + "geographiclib": "1.48.0", + "rw": "~1.3.2" + }, + "bin": { + "mcs2cs": "bin/mcs2cs", + "mproj": "bin/mproj" + }, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.1.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", + "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", + "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/parents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "dev": true, + "dependencies": { + "path-platform": "~0.11.15" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "dev": true, + "dependencies": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104=" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-platform": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "dev": true, + "dependencies": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/promise": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", + "integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "node_modules/qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/read-only-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz", + "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" + }, + "node_modules/rollup": { + "version": "2.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.60.0.tgz", + "integrity": "sha512-cHdv9GWd58v58rdseC8e8XIaPUo8a9cgZpnCMMDGZFDZKEODOiPPEQFXLriWr/TjXzhPPmG5bkAztPsOARIcGQ==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shasum-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", + "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==", + "dev": true, + "dependencies": { + "fast-safe-stringify": "^2.0.7" + } + }, + "node_modules/shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/stream-browserify/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "dev": true, + "dependencies": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, + "node_modules/stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dev": true, + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "node_modules/stream-http/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/stream-splicer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", + "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "dev": true, + "dependencies": { + "minimist": "^1.1.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/sync-request": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-5.0.0.tgz", + "integrity": "sha512-NKhEA4WacR3mRBIFz1niXrIUTrUVFtP2spzrLMINangebvJ/EFyVv+LMJKvVl6UIrJM4Fburnnj91lRnqb4WkA==", + "dependencies": { + "http-response-object": "^3.0.1", + "sync-rpc": "^1.2.0", + "then-request": "^5.0.0" + } + }, + "node_modules/sync-rpc": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz", + "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==", + "dependencies": { + "get-port": "^3.1.0" + } + }, + "node_modules/syntax-error": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", + "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", + "dev": true, + "dependencies": { + "acorn-node": "^1.2.0" + } + }, + "node_modules/then-request": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/then-request/-/then-request-5.0.0.tgz", + "integrity": "sha512-A3uIVLD33SAvB10PfsxLuQBMV8GVC/6xKBMPOvkJchi6251e5AMJ+Yy+RVKsVsnj0iYNhN2E5SkNSi58H19wsw==", + "dependencies": { + "@types/concat-stream": "^1.6.0", + "@types/form-data": "0.0.33", + "@types/node": "^8.0.0", + "@types/qs": "^6.2.31", + "caseless": "~0.12.0", + "concat-stream": "^1.6.0", + "form-data": "^2.2.0", + "http-basic": "^6.0.0", + "http-response-object": "^3.0.1", + "promise": "^8.0.0", + "qs": "^6.4.0" + } + }, + "node_modules/then-request/node_modules/@types/node": { + "version": "8.10.59", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.59.tgz", + "integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "dev": true, + "dependencies": { + "process": "~0.11.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "node_modules/umd": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", + "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", + "dev": true, + "bin": { + "umd": "bin/cli.js" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undeclared-identifiers": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", + "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", + "dev": true, + "dependencies": { + "acorn-node": "^1.3.0", + "dash-ast": "^1.0.0", + "get-assigned-identifiers": "^1.2.0", + "simple-concat": "^1.0.0", + "xtend": "^4.0.1" + }, + "bin": { + "undeclared-identifiers": "bin.js" + } + }, + "node_modules/underscore": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", + "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", + "dev": true + }, + "node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + }, + "node_modules/util": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", + "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "safe-buffer": "^5.1.2", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.4.tgz", + "integrity": "sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.2", + "call-bind": "^1.0.0", + "es-abstract": "^1.18.0-next.1", + "foreach": "^2.0.5", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.1", + "is-typed-array": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2" + } + }, + "node_modules/workerpool": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz", + "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, "dependencies": { "@rollup/plugin-node-resolve": { "version": "13.0.6", @@ -76,16 +3413,6 @@ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -264,9 +3591,9 @@ "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", "dev": true, "requires": { - "JSONStream": "^1.0.3", "combine-source-map": "~0.8.0", "defined": "^1.0.0", + "JSONStream": "^1.0.3", "safe-buffer": "^5.1.1", "through2": "^2.0.0", "umd": "^3.0.0" @@ -293,7 +3620,6 @@ "integrity": "sha512-SaHqzhku9v/j6XsQMRxPyBrSP3gnwmE27gLJYZgMT2GeK3J0+0toN+MnuNYDfHwVGQfLiMZ7KSNSIXHemy905w==", "dev": true, "requires": { - "JSONStream": "^1.0.3", "assert": "^1.4.0", "browser-pack": "^6.0.1", "browser-resolve": "^2.0.0", @@ -315,6 +3641,7 @@ "https-browserify": "^1.0.0", "inherits": "~2.0.1", "insert-module-globals": "^7.2.1", + "JSONStream": "^1.0.3", "labeled-stream-splicer": "^2.0.0", "mkdirp-classic": "^0.5.2", "module-deps": "^6.2.3", @@ -1346,11 +4673,11 @@ "integrity": "sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==", "dev": true, "requires": { - "JSONStream": "^1.0.3", "acorn-node": "^1.5.2", "combine-source-map": "^0.8.0", "concat-stream": "^1.6.1", "is-buffer": "^1.1.0", + "JSONStream": "^1.0.3", "path-is-absolute": "^1.0.1", "process": "~0.11.0", "through2": "^2.0.0", @@ -1544,6 +4871,16 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, "kdbush": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", @@ -1705,7 +5042,6 @@ "integrity": "sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==", "dev": true, "requires": { - "JSONStream": "^1.0.3", "browser-resolve": "^2.0.0", "cached-path-relative": "^1.0.2", "concat-stream": "~1.6.0", @@ -1713,6 +5049,7 @@ "detective": "^5.2.0", "duplexer2": "^0.1.2", "inherits": "^2.0.1", + "JSONStream": "^1.0.3", "parents": "^1.0.0", "readable-stream": "^2.0.2", "resolve": "^1.4.0", @@ -2186,6 +5523,14 @@ "readable-stream": "^2.0.2" } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -2216,14 +5561,6 @@ "define-properties": "^1.1.3" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", diff --git a/package.json b/package.json index 035e5461c..4a73b07ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.87", + "version": "0.5.88", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 981a9fb24dcdbdf63e41ec4cf395e64c4e3cf948 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 26 Jan 2022 14:17:13 -0500 Subject: [PATCH 155/891] Add command reference markdown file to repo --- REFERENCE.md | 1373 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1373 insertions(+) create mode 100644 REFERENCE.md diff --git a/REFERENCE.md b/REFERENCE.md new file mode 100644 index 000000000..4c82021eb --- /dev/null +++ b/REFERENCE.md @@ -0,0 +1,1373 @@ +# COMMAND REFERENCE + +This documentation applies to version 0.5.88 of mapshaper's command line program. Run `mapshaper -v` to check your version. For an introduction to the command line tool, read [this page](https://github.com/mbloch/mapshaper/wiki/Introduction-to-the-Command-Line-Tool) first. + +## Command line syntax + +Mapshaper takes a list of commands and runs them in sequence, from left to right. A command consists of the name of a command prefixed by a hyphen, followed by options for the command. The initial import command `-i` can be omitted. Example: + +```bash +# Read a Shapefile, simplify using Douglas-Peucker, output as GeoJSON +mapshaper provinces.shp -simplify dp 20% -o format=geojson out.json +``` + +Command options can take three forms: + + - Values, like `provinces.shp` and `20%` in the above example + + - Flags, like `dp` + + - Name/value pairs, like `format=geojson` + +#### Common options + +These options are used by multiple commands + +`name=` Rename the layer (or layers) modified by a command. + +`target=` Specify the layer or layers targeted by a command. Takes the name of a layer, the number of a layer (first layer is 1), or a comma-separated list of layer names or numbers. Names may contain the `*` wildcard. + +`+` Use the output of a command to create a new layer or layers instead of replacing the target layer(s). Use together with the `name=` option to assign a name to the new layer(s). + +```bash +# Make a derived layer containing a subset of features while retaining the original layer +mapshaper states.geojson -filter 'ST == "AK"' + name=alaska -o output/ target=* +``` + +## Index of commands + +**File I/O** + +[-i (input)](#-i-input) +[-o (output)](#-o-output) + +**Editing** + +[-affine](#-affine) +[-classify](#-classify) +[-clean](#-clean) +[-clip](#-clip) +[-colorizer](#-colorizer) +[-dashlines](#-dashlines) +[-dissolve](#-dissolve) +[-dissolve2](#-dissolve2) +[-divide](#-divide) +[-dots](#-dots) +[-drop](#-drop) +[-each](#-each) +[-erase](#-erase) +[-explode](#-explode) +[-filter](#-filter) +[-filter-fields](#-filter-fields) +[-filter-islands](#-filter-islands) +[-filter-slivers](#-filter-slivers) +[-graticule](#-graticule) +[-grid](#-grid) +[-include](#-include) +[-inlay](#-inlay) +[-innerlines](#-innerlines) +[-join](#-join) +[-lines](#-lines) +[-merge-layers](#-merge-layers) +[-mosaic](#-mosaic) +[-point-grid](#-point-grid) +[-points](#-points) +[-polygons](#-polygons) +[-proj](#-proj) +[-rectangle](#-rectangle) +[-rectangles](#-rectangles) +[-rename-fields](#-rename-fields) +[-rename-layers](#-rename-layers) +[-require](#-require) +[-run](#-run) +[-shape](#-shape) +[-simplify](#-simplify) +[-sort](#-sort) +[-split](#-split) +[-split-on-grid](#-split-on-grid) +[-subdivide](#-subdivide) +[-style](#-style) +[-union](#-union) +[-uniq](#-uniq) + +**Control Flow** +[-if](#-if) +[-elif](#-elif) +[-else](#-else) +[-endif](#-endif) +[-target](#-target) + +**Information** + +[-calc](#-calc) +[-colors](#-colors) +[-encodings](#-encodings) +[-help](#-help) +[-info](#-info) +[-inspect](#-inspect) +[-projections](#-projections) +[-quiet](#-quiet) +[-verbose](#-verbose) +[-version](#-version) + + +## I/O Commands + +### -i (input) + +Input one or more files in Shapefile, JSON, DBF or delimited text format. + +The `-i` command is assumed if `mapshaper` is followed by an input filename. + +JSON files can be GeoJSON, TopoJSON, or an array of data records. + +Each named geometry object of a TopoJSON input file is imported as a separate layer. + +Mapshaper does not fully support M and Z type Shapefiles. The M and Z data is lost when these files are imported. + +By default, multiple input files are processed separately, as if running mapshaper multiple times with the same set of commands. `combine-files` and `merge-files` change this behavior. + +**Options** + +`` or `files=` File(s) to input (space-separated list). Use `-` to import TopoJSON or GeoJSON from `/dev/stdin`. + +`combine-files` Import multiple files to separate layers with shared topology. Useful for generating a single TopoJSON file containing multiple geometry objects. + +`merge-files` (Deprecated) Merge features from multiple input files into as few layers as possible. Preferred method: import files to separate layers using `-i combine-files`, then use the `-merge-layers` command to merge layers. + +`snap` Snap together vertices within a small distance threshold. This option is intended to fix minor coordinate misalignments in adjacent polygons. The snapping distance is 0.0025 of the average segment length. + +`snap-interval=` Specify snapping distance in source units. + +`precision=` (Deprecated) Round all coordinates to a specified precision, e.g. `0.001`. It is recommended to set coordinate precision on export, using `-o precision=`. + +`no-topology` Skip topology identification to speed up processing of large files. For use with commands like `-filter` that don't require topology. + +`encoding=` Specify encoding used for reading .dbf files and delimited text files. If the `encoding` option is missing, mapshaper will try to detect the encoding of .dbf files. Dbf encoding can also be set using a .cpg file. + +`id-field=` (Topo/GeoJSON) Import values of "id" property to this data field. + +`string-fields=` (CSV) List of fields to import as strings (e.g. FIPS,ZIPCODE). Using `string-fields=*` imports all fields as strings. + +`field-types=` Type hints for importing delimited text. Takes a comma-separated list of field names with type hints appended; e.g. `FIPS:str,zipcode:str`. Recognized type hints include `:str` or `:string`, `:num` or `:number`. Without a type hint, fields containing text data that looks like numeric data, like ZIP Codes, will be converted to numbers. + +`csv-skip-lines=` Number of lines to skip at the beginning of a CSV file. Useful when a CSV has been exported from a spreadsheet and there are rows of notes above the data section of the worksheet. + +`csv-lines=` Number of data records to import from a CSV file (default is all). + +`csv-field-names=` Comma-sep. list of names to assign each field. Can be used in conjunction with `csv-skip-lines=1` to replace names from an existing set of field headers. + +`csv-fields=` Comma-sep. list of fields to import from a CSV-formatted input file. Fields are filtered as the file is read, which reduces the memory needed to import very large CSV files. + +`decimal-comma` Import numbers formatted with decimal commas, not decimal points. Accepted formats: `1.000,01` `1 000,01` (both imported as as 1000.01). + +`csv-dedup-fields` Assign unique names to CSV fields with duplicate names. + +`csv-filter=` A JavaScript expression for importing a subset of the records in a CSV file. Records are filtered as the file is read, which reduces the memory needed to import very large CSV files. + +`json-path=` [JSON] Path to an array of data records or a GeoJSON object. For example, `json-path=data/counties` expects a JSON object with the following structure `{"data": {"counties": []}}`. + +`name=` Rename the imported layer (or layers). + +**Example** + +```bash +# Input a Shapefile with text data in the latin1 encoding and see what kind of data it contains +mapshaper countries_wgs84.shp encoding=latin1 -info +``` + +### -o (output) + +Save edited content to a file or files. By default, Mapshaper appends a suffix to output files if it detects a naming conflict, to avoid overwriting existing files. + +**Options** + +`||-` Name of output file or directory. Use `-` to export text-based formats to `/dev/stdout`. + +`format=shapefile|geojson|topojson|json|dbf|csv|tsv|svg` Specify output format. If the `format=` option is missing, Mapshaper tries to infer the format from the output filename. If no filename is given, Mapshaper uses the input format. The `json` format is an array of objects containing data properties for each feature. + +`target=` Specify layer(s) to export (comma-separated list). The default target is the output layer(s) of the previous command. Use `target=*` to select all layers. + +`force` Allow output files to overwrite input files (without this option, overwriting input files is not allowed). + +`cut-table` Detach attribute data from shapes and save as a JSON file. + +`drop-table` Remove attribute data from output. + +`precision=` Round all coordinates to a specified precision, e.g. `precision=0.001`. Useful for reducing the size of GeoJSON files. + +`bbox-index` Export a JSON file containing bounding boxes of each output layer. + +`encoding=` (Shapefile/CSV) Encoding of input text (by default, Shapefile encoding is auto-detected and CSV files are assumed to be UTF-8). + +`field-order=` (Shapefile/CSV) `field-order=ascending` sorts data fields in alphabetical order of field names (A-Z, case-insensitive). + +`id-field=` (Topo/GeoJSON/SVG) Specify one or more data fields to use as the "id" property of GeoJSON, TopoJSON or SVG features (comma-separated list). When exporting multiple layers, you can pass a list of field names. The first listed name that is present in a layer's attribute table will be used as the id field for that layer. + +`bbox` (Topo/GeoJSON) Add bbox property to the top-level object. + +`extension=` (Topo/GeoJSON) set file extension (default is ".json"). + +`prettify` (Topo/GeoJSON) Format output for readability. + +`singles` (TopoJSON) Save each output layer as a separate file. Each output file and the TopoJSON object that it contains are named after the corresponding data layer. + +`quantization=` (TopoJSON) Specify quantization as the maximum number of differentiable points along either dimension. Equivalent to the quantization parameter used by the [topoquantize](https://github.com/topojson/topojson-client#topoquantize) command line program. By default, mapshaper applies quantization equivalent to 0.02 of the average segment length. + +`no-quantization` (TopoJSON) Arc coordinates are encoded at full precision and without delta-encoding. + +`presimplify` (TopoJSON) Add a threshold value to each arc vertex in the z position (i.e. [x, y, z]). Useful for dynamically simplifying paths using vertex filtering. Given W as the width of the map viewport in pixels, S as the ratio of content width to viewport width, and pz as the presimplify value of a point, the following expression tests if the point should be excluded from the output path: `pz > 0 && pz < 10000 / (W * S)`. + +`topojson-precision=` (TopoJSON) Set quantization as a fraction of the average segment length. + +`ndjson` (GeoJSON/JSON) Output newline-delimited records. + +`gj2008` (GeoJSON) Generate output that is consistent with the pre-RFC 7946 GeoJSON spec (dating to 2008). Polygon rings are CW and holes are CCW, which is the opposite of the default RFC 7946-compatible output. Mapshaper's default GeoJSON output is now compatible with the current specification (RFC 7946). + +`combine-layers` (GeoJSON) Combine multiple output layers into a single GeoJSON file. + +`geojson-type=` (GeoJSON) Overrides the default output type. Possible values: "FeatureCollection", "GeometryCollection", "Feature" (for a single feature). + +`width=` (SVG/TopoJSON) Set the width of the output dataset in pixels. When used with TopoJSON output, this option switches the output coordinates from geographic units to pixels and flips the Y axis. SVG output is always in pixels (default SVG width is 800). + +`height=` (SVG/TopoJSON) Similar to the `width` option. If both `height` and `width` are set, content is centered inside the `[0, 0, width, height]` bounding box. + +`max-height=` (SVG/TopoJSON) Limit output height (units: pixels). + +`margin=` (SVG/TopoJSON) Set the margin between coordinate data and the edge of the viewport (default is 1). To assign different margins to each side, pass a list of values in the order `` (similar to the `bbox=` option found in other commands). + +`pixels=` (SVG/TopoJSON) Output area in pixels (alternative to width=). + +`id-prefix=` Prefix for namespacing layer and feature ids. + +`svg-data=` (SVG) Export a comma-seperated list of data fields as SVG data-* attributes. Attribute names should match the following regex pattern: `/^[a-z_][a-z0-9_-]*$/`. Non-conforming fields are skipped. + +`svg-scale=` (SVG) Scale SVG output using geographical units per pixel (an alternative to the `width=` option). + +`point-symbol=square` (SVG) Use squares instead of circles to symbolize point data. + + +`delimiter=` (CSV) Set the field delimiter for CSV/delimited text output; e.g. `delimiter=|`. + +`decimal-comma` (CSV) Export numbers with decimal commas instead of decimal points (common in Europe and elsewhere). + +**Example** +```bash +# Convert all the Shapefiles in one directory into GeoJSON files in a different directory. +mapshaper shapefiles/*.shp -o geojson/ format=geojson +``` + +## Editing Commands + + +### -affine + +Transform coordinates by shifting, scaling and rotating. Not recommended for unprojected datasets. + +`shift=` X,Y shift in source units (e.g. 5000,-5000) + +`scale=` Scale (default is 1) + +`rotate=` Angle of rotation in degrees (default is 0) + +`anchor=` Center of rotation/scaling (default is center of the bounding box of the selected content) + +`where=` Use a JS expression to select a subset of features. + +Common options: `target=` + +### -classify + +Apply quantile, equal-interval or categorical classification to a data field. + +`` or `field=` Name of the data field to classify. + +`save-as=` Name of a (new or existing) field to receive the output of classification. The default output field for colors is `fill` or `stroke` (depending on geometry type) and `class` for non-color output. + +`values=` List of values to assign to data classes. If the number of values differs from the number of classes given by the (optional) `classes` or `breaks` option, then interpolated values will be calculated. Mapshaper uses d3 for interpolation. + +`colors=` Takes a list of CSS colors or the name of a predefined color scheme (the [-colors](#-colors) command lists available color schemes). Similarly to the `values=` option, if the number of listed colors is different from the number of requested classes, interpolated colors are calculated. + +`non-adjacent` Assign colors to a polygon layer in a randomish pattern, trying not to assign the same color to adjacent polygons. Mapshaper's algorithm is not optimal. If mapshaper is unable to avoid giving the same color to neighboring polygons, it will print a warning. You can resolve the problem by increasing the number of colors. + +`stops=` A pair of values (0-100) for limiting the range of a color ramp. + +`null-value=` Value (or color) to use for invalid or missing data. + +`classes=` Number of data classes. This number can also be inferred from the `breaks=` or `values=` options. + +`breaks=` Specify user-defined sequential class breaks (an alternative to automatic classification using `quantile`, `equal-interval`, etc.). + +`method=` Classification method. One of: `quantile`, `equal-interval`, `nice` or `hybrid`. + +`quantile` Use quantile classification. Shortcut for `method=quantile`. + +`equal-interval` Use equal interval classification. Shortcut for `method=equal-interval`. + +`nice` Same as `method=nice`. This scheme finds equally spaced, round breakpoints that roughly divide the dataset into equal parts (similar to quantile classification). + +`invert` Reverse the order of colors/values. + +`continuous` Output continuously interpolated values (experimental). Uses linear interpolation between class breaks, which may give poor results with some distributions of data. This option is for creating unclassed/continuous-color maps. + +`index-field=` Use class ids that have been precalculated and assigned to this field. Values should be integers from `0 ... n-1` (where n is the number of classes). `-1` is the null value. + +`precision=` Round data values before classification (e.g. `precision=0.1`). + +`categories=` List of values in the source data field. Using this option triggers categorical classification. + +`other=` Default value for categorical classification. This value is used when the value of the source data field is not present in the list of values given by `categories=`. Defaults to `null-value=` or null. + +**Options for generating SVG keys** + +`key-style=` One of: simple, gradient, dataviz + +`key-name= ` Name of output SVG file + +`key-width=` Width of key in pixels + +`key-font-size=` Font size of tic labels in pixels + +`key-tile-height=` Height of color tiles in pixels + +`key-tic-length=` Length of tic mark in pixels + +`key-label-suffix=` String to append to each label + +`key-last-suffix=` String to append to the last label + + +**Examples** + +```bash +# Apply a sequential color ramp to a polygon dataset using quantiles. +mapshaper covid_cases.geojson -classify save-as=fill quantile color-scheme=Oranges classes=6 -o out.geojson +``` + +### -clean + +This command attempts to repair various kinds of abnormal geometry that might cause problems when running other mapshaper commands or when using other software. + +Features with null geometries are deleted, unless the `allow-empty` flag is used. + +Polygon features are cleaned by removing overlaps and filling small gaps between adjacent polygons. Only gaps that are completely enclosed can be filled. Areas that are contained by more than one polygon (overlaps) are assigned to the polygon with the largest area. Similarly, gaps are assigned to the largest-area polygon. This rule may give undesired results and will likely change in the future. + +Line features are cleaned by removing self-intersections within the same path. Self-intersecting paths are split at the point of intersection and converted into multiple paths within the same feature. When two separate paths intersect in-between segment endpoints, new vertices are inserted at the point of intersection. + +Point features are cleaned by removing duplicate coordinates within the same feature. + +`gap-fill-area=` (polygons) Gaps smaller than this area will be filled; larger gaps will be retained as holes in the polygon mosaic. Example values: 2km2 500m2 0. Defaults to a dynamic value calculated from the geometry of the dataset. + +`sliver-control=` (polygons) Preferentially remove slivers (polygons with a high perimeter-area ratio). Accepts values from 0-1, default is 1. Implementation: multiplies the area of gap areas by the "Polsby Popper" compactness metric before applying area threshold. + +`overlap-rule=` (polygons) Assign overlapping polygon areas to one of the overlapping features based on this rule. Possible options are: min-id, max-id, min-area, max-area (default is max-area). + +`allow-overlaps` Allow features to overlap each other. The default behavior is to remove overlaps. + +`snap-interval=` Snap vertices within a given threshold before performing other kinds of geometry repair. Defaults to a very small threshold. Uses source units. + +`rewind` Fix errors in the winding order of polygon rings. + +`allow-empty` Allow null geometries, which are removed by default. + +Common options: `target=` + + +### -clip + +Remove features or portions of features that fall outside a clipping area. + +`` or `source=` Clip to a set of polygon features. Takes the filename or layer id of the clip polygons. + +`bbox=` Delete features or portions of features that fall outside a bounding box. + +`bbox2=` Faster bounding box clipping than `bbox=` (experimental). + +`remove-slivers` Remove tiny sliver polygons created by clipping. + +Common options: [`name` `+` `target`](#common-options) + +```bash +# Example: Clip a polygon layer using another polygon layer. +mapshaper usa_counties.shp -clip land-area.shp -o clipped.shp +``` + +### -colorizer + +Define a function for converting data values to colors that can be used in subsequent calls to the `-style` command. + +`name=` Name of the colorizer function. + +`colors=` List of CSS colors. + +`random` Randomly assign colors. Uses `colors=` list if given. + +`breaks=` Ascending-order list of breaks (thresholds) for creating a sequential color scheme. + +`categories=` List of data values (keys) for creating a categorical color scheme. + +`other=` Default color for categorical scheme (defaults to `nodata` color). + +`nodata=` Color to use for invalid or missing data (default is white). + +`precision=` Rounding precision to apply to numerical data before converting to a color (e.g. 0.1). + +```bash +# Example: define a function for a sequential color scheme and assign colors based on data values +mapshaper data.json \ + -colorizer name=getColor colors='#f0f9e8,#bae4bc,#7bccc4,#2b8cbe' breaks=25,50,75 \ + -each 'color = getColor(PCT)' \ + -o output.json + +# Example: define a function for a categorical color scheme and use it to assign fill colors +mapshaper data.json \ + -colorizer name=calcFill colors='red,blue,green' categories='Republican,Democrat,Other' \ + -style fill='calcFill(PARTY)' \ + -o output.svg +``` + +### -dashlines + +Split lines into sections, with or without a gap. + +`dash-length=` Length of split-apart lines (e.g. 200km) +`gap-length=` Length of gaps between dashes (default is 0) +`scaled` Scale dashes and gaps to prevent partial dashes +`planar` Use planar geometry +`where=` Use a JS expression to select a subset of features. + +### -dissolve + +Aggregate groups of features using a data field, or aggregate all features if no field is given. For polygon layers, `-dissolve` merges adjacent polygons by erasing shared boundaries. For point layers, `-dissolve` replaces a group of points with their centroid. For polyline layers, `-dissolve` tries to merge contiguous polylines into as few polylines as possible. + +`` or `fields=` (optional) Name of a data field or fields to dissolve on. Accepts a comma-separated list of field names. + +`group-points` [points] Group the points from each dissolved group of features into a multi-point feature instead of converting multiple points into a single-point centroid feature. + +`weight=` [points] Name of a field or a JS expression for generating weighted centroids. For example, the following command estimates the "center of mass" of the U.S. population: ` mapshaper census_tracts.shp -points -dissolve weight=POPULATION -o out.shp` + +`planar` [points] Treat decimal degree coordinates as planar cartesian coordinates when calculating dissolve centroids. (By default, mapshaper calculates the centroids of lat-long point data in 3D space.) + +`calc=` Use built-in JavaScript functions to create data fields in the dissolved layer. See example below; see [-calc](#-calc) for a list of supported functions. + +`sum-fields=` Fields to sum when dissolving (comma-sep. list). + +`copy-fields=` Fields to copy when dissolving (comma-sep. list). Copies values from the first feature in each group of dissolved features. + +`multipart` Group features from the target layer into multipart features, without otherwise modifying geometry. + +`where=` Use a JS expression to select a subset of features to dissolve. + +Common options: `name=` `+` `target=` + +```bash +# Example: Aggregate county polygons to states +mapshaper counties.shp -dissolve STATE -o states.shp + +# Example: Use the calc= option to count the number of dissolved features and perform other calculations +mapshaper counties.shp -dissolve STATE calc='n = count(), total_pop = sum(POP), max_pop = max(POP), min_pop = min(POP)' +``` + + +### -dissolve2 + +Similar to `-dissolve`, but able to handle polygon datasets containing overlaps and gaps between adjacent polygons. + +`gap-fill-area=` (polygons) Gaps smaller than this area will be filled; larger gaps will be retained as holes in the polygon mosaic. Example values: 2km2 500m2 0. Defaults to a dynamic value calculated from the geometry of the dataset. + +`sliver-control=` (polygons) Preferentially remove slivers (polygons with a high perimeter-area ratio). Accepts values from 0-1, default is 1. Implementation: multiplies the area of gap areas by the "Polsby Popper" compactness metric before applying area threshold. + +`allow-overlaps` Allow dissolved groups of features to overlap each other. The default behavior is to remove overlaps. + +Other options: `` `calc=` `sum-fields=` `copy-fields=` `name=` `+` `target=` + +### -divide + +Divide a polyline layer by a polygon layer. Line features that cross polygon boundaries are divided into separate features. Data fields from the polygon layer are copied to the line layer, as in the `-join` command. + +`` or `source=` File or layer containing polygon features. + +`fields=` A comma-separated list of fields to copy from the polygon layer (see `-join` command). + +`calc=` Use JS assignments and built-in functions to convert values from the polygon layer to (new) fields the target table (see `-join` command). + +Other options: `target=` + +### -dots + +Fill polygons with random points, for making dot density maps. This command should be applied to projected layers. + +`` or `fields=` List of one or more data fields containing data for the number of dots to place in each polygon. + +`colors=` List of dot colors (one color for each field in the `fields=` parameter). Dots of different colors are placed in random sequence, so dots of one color do not consistently cover up dots of other colors in the densest areas. + +`values=` List of values to assign to dots (alternative to `colors=`). + +`save-as=` Name of a (new or existing) field to receive the assigned colors or values. (By default, colors are assigned to the `fill` field.) + +`r=` Dot radius in pixels. + +`evenness=` A value from 0-1. 0 corresponds to purely random placement, 1 maintains (fairly) even spacing between the dots within each polygon. The default is 1. + +`per-dot=` A number for scaling data values. For example, use `per-dot=100` to make a map that displays one dot per 100 people (or whatever entity is being visualized). + +`copy-fields=` List of fields to copy from the original polygon layer to each dot feature. + +`multipart` Combine groups of same-color dots into multi-part features. + +Other options: `name=` `+` `target=` + + +### -drop + +Delete the target layer(s) or elements within the target layer(s). + +`fields=` Delete a (comma-separated) list of attribute data fields. To delete all fields, use `fields=*`. + +`geometry` Delete all geometry. + +`holes` Delete any holes from a polygon layer. + +`target=` Layer(s) to target. + + +### -each + +Apply a JavaScript expression to each feature in a layer. Data properties are available as local variables. Additional feature-level properties are available as read-only properties of the `this` object. + +**Tip:** Enclose JS expressions in single quotes when using the bash shell (Mac and Linux) to avoid shell expansion of "!" and other special characters. Using the Windows command interpreter, enclose JS expressions in double quotes. + +`` or `expression=` JavaScript expression to apply to each feature. + +`where=` Secondary boolean JS expression for targetting a subset of features. + +`target=` Layer to target. + +**Properties of `this`** +*Properties are read-only unless otherwise indicated.* + +All layer types +- `this.id` Numerical id of the feature (0-based) +- `this.layer_name` Name of the layer, or `""` if layer is unnamed. +- `this.properties` Data properties (also available as local variables) (read/write) +- `this.layer` Object with "name" and "data" properties +- `this.geojson` (read/write) Converts each feature to a GeoJSON Feature object. + +Point layers +- `this.coordinates` An array of [x, y] coordinates with one or more members, or null (read/write) +- `this.x` X-coordinate of point, or `null` if geometry is empty. Refers to the first point of multi-point features. (read/write) +- `this.y` Y-coordinate of point or `null`. (read/write) + +Polygon layers +- `this.area` Area of polygon feature, after any simplification is applied. For lat-long datasets, returns area on a sphere in units of square meters. +- `this.planarArea` Calculates the planar area of lat-long datasets, as though latitude and longitude were cartesian coordinates. +- `this.originalArea` Area of polygon feature without simplification +- `this.centroidX` X-coord of centroid +- `this.centroidY` Y-coord of centroid +- `this.innerX` X-coord of an interior point (for anchoring symbols or labels) +- `this.innerY` Y-coord of an interior point + +Polyline layers +- `this.length` Length of each polyline feature. For lat-long datasets, returns length in meters. + +Polygon and polyline layers +- `this.partCount` 1 for single-part features, >1 for multi-part features, 0 for null features +- `this.isNull` True if feature has null geometry +- `this.bounds` Bounding box as array [xmin, ymin, xmax, ymax] +- `this.width` Width of bounding box +- `this.height` Height of bounding box + +**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. + +**Examples** + +```bash +# Create two fields +mapshaper counties.shp -each 'STATE_FIPS=COUNTY_FIPS.substr(0, 2), AREA=this.area' -o out.shp + +# Delete two fields +mapshaper states.shp -each 'delete STATE_NAME, delete GEOID' -o out.shp + +# Rename a field +mapshaper states.shp -each 'STATE_NAME=NAME, delete NAME' -o out.shp + +# Print the value of a field to the console +mapshaper states.shp -each 'console.log(NAME)' + +# Assign a new data record to each feature +mapshaper states.shp -each 'this.properties = {FID: this.id}' -o out.shp +``` + + +### -erase + +Remove features or portions of features that fall inside an area. + +`` or `source=` File or layer containing erase polygons. Takes the filename or layer id of the erase polygons. + +`bbox=` Delete features or portions of features that fall inside a bounding box. Similar to `-clip bbox=`. + +`bbox2=` Faster bounding box erasing than `bbox=` (experimental). + +`remove-slivers` Remove tiny sliver polygons created by erasing. + +Common options: [`name` `+` `target` ](#common-options) + +```bash +# Example: Erase a polygon layer using another polygon layer. +mapshaper usa_counties.shp -erase lakes.shp -o out.shp +``` + + +### -explode + +Divide each multi-part feature into several single-part features. + +Common options: `target=` + + +### -filter + +Apply a boolean JavaScript expression to each feature, removing features that evaluate to false. + +`` or `expression=` JS expression evaluating to `true` or `false`. Uses the same execution context as [`-each`](#-each). + +`bbox=` Retains features that intersect the given bounding box (xmin,ymin,xmax,ymax). + +`invert` Invert the filter -- retain only those features that would have been deleted. + +`remove-empty` Delete features with null geometry. May be used by itself or in combination with an ``. + +Common options: [`name` `+` `target` ](#common-options) + +```bash +# Example: Select counties from New England states +mapshaper usa_counties.shp -filter '"ME,VT,NH,MA,CT,RI".indexOf(STATE) > -1' -o ne_counties.shp +``` + + +### -filter-fields + +Delete fields in an attribute table, by listing the fields to retain. If no files are given, then all attributes are removed. + +`` or `fields=` Comma-separated list of data fields to retain. + +Common options: `target=` + +```bash +# Example: Retain two fields +mapshaper states.shp -filter-fields FID,NAME -o out.shp +``` + +### -filter-islands + +Remove small detached polygon rings (islands). + +`min-area=` Remove small-area islands using an area threshold (e.g. 10km2). + +`min-vertices=` Remove low-vertex-count islands. + +`remove-empty` Delete features with null geometry. + +[`target`](#common-options) + + +### -filter-slivers + +Remove small polygon rings. + +`min-area=` Area threshold for removal (e.g. 10km2). + +`sliver-control=` (polygons) Preferentially remove slivers (polygons with a high perimeter-area ratio). Accepts values from 0-1, default is 1. Implementation: multiplies the area of polygon rings by the "Polsby Popper" compactness metric before applying area threshold. + +`remove-empty` Delete features with null geometry. + +[`target`](#common-options) + + +### -graticule + +Create a graticule layer appropriate for a world map centered on longitude 0. + +`polygon` Create an polygon enclosing the entire area of the graticule. Useful for creating background or outline shapes for clipped projections, like Robinson or Stereographic. + +`interval=` Specify the spacing of graticule lines (in degrees). Options include: 5, 10, 15, 30, 45. Default is 10. + +### -grid + +Create a continuous grid of square or hexagonal polygons. + +The `-grid` command should have a projected layer as its target. The cells of the grid will completely enclose the bounding box of the target layer. + +This command is intended for visualizing data in a grid. Typically, you would use the `-join` command to join data from a polygon or point layer to a grid layer. Use `-join interpolate=` to interpolate data values (typically count data) from the polygon layer to the grid layer based on area. Use `-join calc=' = sum()'` or `-join calc=' = count()'` to aggregate point data values. + +`type=` Supported values: `square` `hex` `hex2`. The `hex` and `hex2` types have different rotations. + +`interval=` The length of one side of a grid cell. Example values: `500m` `2km`. + +Other options: `name=` `+` `target=` + + +### -include + +`` or `file=` Path to the external .js file to load. The file should contain a single JS object. The properties of this object are converted to variables in the JS expression used by the `-each` command. + +### -inlay + +Inscribe a polygon layer within another polygon layer. + +`` or `source=` File or layer containing polygons to inlay + +Other options: `target=` + +### -innerlines + +Create a polyline layer consisting of shared boundaries with no attribute data. + +`where=` Filter lines using a JS expression (see the `-lines where=` option). + +Other options: `name=` `+` `target=` + +```bash +# Example: Extract the boundary between two states. +mapshaper states.shp -filter 'STATE=="OR" || STATE=="WA"' -innerlines -o out.shp +``` + + +### -join + +Join attribute data from a source layer or file to a target layer. If the `keys=` option is used, Mapshaper will join records by matching the values of key fields. If the `keys=` option is missing, Mapshaper will perform a polygon-to-polygon, point-to-polygon, polygon-to-point or point-to-point spatial join. + +`` or `source=` File or layer containing data records to join. + +`keys=` Names of two fields to use as join keys, separated by a comma. The key field from the destination table is followed by the key field from the source table. If the `keys=` option is missing, mapshaper performs a spatial join. + +`calc=` Use JS assignments and built-in functions to convert values from the source table to (new) fields the target table. See the [`-calc` command reference](#-calc) for a list of supported functions. Useful for handling many-to-one joins. See example below. + +`where=` Use a boolean JS expression to filter records from the source table. The expression has the same syntax as the expression used by the `-filter` command. The functions `isMax()` `isMin()` and `isMode()` can be used in many-to-one joins to select among source records. + +`fields=` A comma-separated list of fields to copy from the external table. If the `fields` option and `calc` options are both absent, all fields are copied except the key field (if joining on keys) unless the. Use `fields=*` to copy all fields, including any key field. Use `fields=` (empty list) to copy no fields. + +`prefix=` Add a prefix to the names of fields joined from the external attribute table. + +`interpolate=` (polygon-to-polygon joins only) A list of fields to interpolate/reaggregate based on area of overlap. Intended for fields containing count data, such as population counts or vote counts. Treats data as being uniformally distributed within polygon areas. + +`point-method` (polygon-to-polygon joins only) Use an alternate method for joining two polygon layers. The default polygon-polygon join method detects areas of overlap between two polygon layers by compositing the two layers internally. This method is simpler -- it generates a temporary point layer from the source layer with the greater number of features (using the same inner-point method as the `-points inner` command), and then performs a point-to-polygon or polygon-to-point join. This method does not support the `interpolate=` option. + +`largest-overlap` (polygon-to-polygon joins only) selects a single polygon to join when multiple source polygons overlap a target polygon, based on largest area of overlap. + +`max-distance=` (point-to-point joins only) Join source layer points within this distance of a target layer point. + +`duplication` Create duplicate features in the target layer on many-to-one joins. + +`sum-fields=` (deprecated) A comma-separated list of fields to sum when several source records match the same target record. This option is equivalent to using the `sum()` function inside a `calc=` expression like this: `calc='FIELD = sum(FIELD)'`. + +`string-fields=` A comma-separated list of fields in source CSV file to import as strings (e.g. FIPS,ZIPCODE). + +`field-types=` A comma-separated list of type hints (when joining a CSV file or other delimited text file). See `-i field-types=` above. + +`force` Allow values in the target data table to be overwritten by values in the source table when both tables contain identically named fields. + +`unjoined` Copy unjoined records from the source table to a layer named "unjoined". + +`unmatched` Copy unmatched records from the destination table to a layer named "unmatched". + +Other options: `encoding=` `target=` + +**Examples** + +Join a point layer to a polygon layer (spatial join), using the `calc=` option to handle many-to-one matches. + +```bash +mapshaper states.shp -join points.shp calc='median_score = median(SCORE), mean_score = average(SCORE), join_count = count()' -o out.shp +``` + +Copy data from a csv file to the attribute table of a Shapefile by matching values from the *STATE_FIPS* field of the Shapefile and the *FIPS* field of the csv file. (The string-fields=FIPS argument prevents FIPS codes in the CSV file from being converted to numbers.) + +```bash +mapshaper states.shp -join demographics.txt keys=STATE_FIPS,FIPS string-fields=FIPS -o out.shp +``` + +### -lines + +Converts points and polygons to lines. Polygons are converted to topological boundaries. Without the `` argument, external (unshared) polygon boundaries are attributed as `TYPE: "outer", RANK: 0` and internal (shared) boundaries are `TYPE: "inner", RANK: 1`. + +`` or `fields=` (Optional) comma-separated list of attribute fields for creating a hierarchy of polygon boundaries. A single field name adds an intermediate level of hierarchy with attributes: `TYPE: , RANK: 1`, and the lowest-level internal boundaries are given attributes `TYPE: "outer", RANK: 2`. A comma-separated list of fields adds additional levels of hierarchy. + +`where=` Use a JS expression for filtering polygon boundaries using properties of adjacent polygons. The expression context has objects named A and B, which represent features on eather side of a path. B is null if a path only belongs to a single feature. + +`each=` Apply a JS expression to each line (using A and B, like the `where=` option). + +`groupby=` Convert a point layer into multiple lines, using a field value for grouping. + +Common options: `name=` `+` `target=` + +```bash +# Example: Classify national, state and county boundaries. +mapshaper counties.shp -lines STATE_FIPS -o boundaries.shp +``` + + +### -merge-layers + +Merge features from several layers into a single layer. Layers can only be merged if they have compatible geometry types. Target layers should also have compatible data fields, unless the `force` option is used. + +`force` Allow merging layers with inconsistent fields. When a layer is missing a particular field, the field will be added, with the values set to `undefined`. Using this option, you are still prevented from merging fields with different data types (e.g. a field containing numbers in one layer and strings in another). You are also still prevented from merging layers containing different geometry types. + +`flatten` (polygon layers) Remove polygon overlaps by assigning overlapping areas to the last overlapping polygon (the topmost feature if features are rendered in sequence). + +Common options: `name=` `target=` + +```bash +# Example: Combine features from several Shapefiles into a single Shapefile. +# -i combine-files is used because files are processed separately by default. +mapshaper -i OR.shp WA.shp CA.shp AK.shp combine-files \ + -merge-layers \ + -o pacific_states.shp +``` + +### -mosaic + +Flatten a polygon layer by converting overlapping areas to separate polygons. + +`calc=` Use a JavaScript expression to handle many-to-one aggregation (similar to the `calc=` option of the`-join` and `-dissolve` functions). See [-calc](#-calc) for a list of supported functions. + +Common options: `name=` `+` `target=` + + +### -point-grid + +Create a rectangular grid of points. + +`` Size of the grid, e.g. `-point-grid 100,100`. + +`interval=` Distance between adjacent points, in source units (alternative to setting the number of cols and rows). + +`bbox=` Fit the grid to a bounding box (xmin,ymin,xmax,ymax). Defaults to the bounding box of the other data layers, or of the world if no other layers are present. + +`name=` Set the name of the point grid layer + +### -points + +Create a point layer, either from polygon or polyline geometry or from values in the attribute table. By default, polygon features are replaced by a single point located at the centroid of the polygon ring, or the largest ring of a multipart polygon. By default, polyline features are replaced by a single point located at the polyline vertex that is closest to the center of the feature's bounding box (this can be used to join polylines to polygons using a point-to-polygon spatial join). + +`x=` Name of field containing x coordinate values. Common X-coordinate names are auto-detected (e.g. longitude, LON). + +`y=` Name of field containing y coordinate values. Common Y-coordinate names are auto-detected (e.g. latitude, LAT). + +`centroid` Create points at the centroid of the largest ring of each polygon feature. Point placement is currrently not affected by holes. + +`inner` Create points in the interior of the largest ring of each polygon feature. Inner points are located away from polygon boundaries. + +`vertices` Convert polygon and polyline features into point features containing the unique vertices in each shape. + +`vertices2` Convert all the vertices in polygon and polyline features into points, including duplicate coordinates (e.g. the duplicate endpoint coordinates of polygon rings). + +`endpoints` Capture the unique endpoints of polygon and polyline arcs. + +`midpoints` Find the midpoint of each path in a polyline layer. + +`interpolated` Interpolate points along polylines. Requires the `interval=` option to be set. Original vertices are replaced by interpolated vertices. + +`interval=` Distance between interpolated points (in meters if coordinates are unprojected, or projected units). + +Common options: `name=` `+` `target=` + +```bash +# Example: Create points in the interior of each polygon +mapshaper counties.shp -points inner -o points.shp + +# Example: Create points in the interior of each polygon (alternate method) +mapshaper counties.shp -each 'cx=this.innerX, cy=this.innerY' -points x=cx y=cy -o points.shp +``` + +### -polygons + +Convert a polyline layer to a polygon layer by linking together intersecting polylines to form rings. + +`gap-tolerance=` Close gaps ("undershoots") between polylines up to the distance specified by this option. + +`from-rings` Convert a layer of closed polyline rings into polygons. Nested rings in multipart features are converted into holes. + +Common options: `target=` + +### -proj + +Project a dataset using a Proj.4 string, EPSG code or alias. This command affects all layers in the dataset(s) containing the targeted layer or layers. + +`` or `crs=` Target CRS, given as a Proj.4 definition or an alias. Use the [`-projections`](#-projections) command to list available projections and aliases. In projections which require additional parameters, such as a zone in UTM, you can pass a Proj4 string enclosed in quotes. For example, `crs='+proj=utm +zone=27'`. + +`densify` Interpolate vertices along long line segments as needed to approximate curved lines. + +`match=` Match the projection of the given layer or .prj file. + +`init=` Define the pre-projected coordinate system, if unknown. This option is not needed if the source coordinate system is defined by a .prj file, or if the source CRS is WGS84. As with `crs`, you can pass a Proj4 string enclosed in quotes if the selected projection requires extra parameters, for example `init='+proj=utm +zone=33'`. + +`target=` Layer(s) to target. All layers belonging to the same dataset as a targeted layer will be reprojected. To reproject all datasets, use `target=*`. + +**Examples** +```bash +# Convert a GeoJSON file to New York Long Island state plane CRS, using a Proj.4 string +mapshaper nyc.json -proj +proj=lcc +lat_1=41.03333333333333 +lat_2=40.66666666666666 \ ++lat_0=40.16666666666666 +lon_0=-74 +x_0=300000 +y_0=0 +ellps=GRS80 +datum=NAD83 +units=m \ +-o out.json + +# Apply the same projection using an EPSG code +mapshaper nyc.json -proj EPSG:2831 -o out.json + +# Convert an unprojected U.S. Shapefile into a composite projection with Alaska +# and Hawaii repositioned and rescaled to fit in the lower left corner. +# Show Puerto Rico and the U.S. Virgin Islands +# Override the default central meridian and scale of the Alaska inset +mapshaper us_states.shp -proj albersusa +PR +VI +AK.lon_0=-141 +AK.scale=0.4 -o out.shp + +# Convert a projected Shapefile to WGS84 coordinates +mapshaper area.shp -proj wgs84 -o out.shp +``` + +### -rectangle + +Create a new layer containing a rectangular polygon. + +`bbox=` Give the coordinates of the rectangle. + +`source=` Create a bounding box around a given layer. + +`aspect-ratio=` Aspect ratio as a number or range (e.g. 2 0.8,1.6 ,2). + +`offset=` Padding as a distance or percentage of width/height (single value or list). + +`name=` Assign a name to the newly created layer. + +### -rectangles + +Create a new layer containing a rectangular polygon for each feature in the layer. + +`aspect-ratio=` Aspect ratio as a number or range (e.g. 2 0.8,1.6 ,2). + +`offset=` Padding as a distance or percentage of width/height (single value or list). + +`name=` Assign a name to the newly created layer. + +### -rename-fields + +Rename data fields. To rename a field from A to B, use the assignment operator: B=A. + +`` or `fields=` List of fields to rename as a comma-separated list. + +Common options: `target=` + +```bash +# Example: rename STATE_FIPS to FIPS and STATE_NAME to NAME +mapshaper states.shp -rename-fields FIPS=STATE_FIPS,NAME=STATE_NAME -o out.shp +``` + + +### -rename-layers + +Assign new names to layers. If fewer names are given than there are layers, the last name in the list is repeated with numbers appended (e.g. layer1, layer2). + +`` or `names=` One or more layer names (comma-separated). + +`target=` Rename a subset of all layers. + +```bash +# Example: Create a TopoJSON file with sensible object names. +mapshaper ne_50m_rivers_lake_centerlines.shp ne_50m_land.shp combine-files \ + -rename-layers water,land -o target=* layers.topojson +``` + +### -require + +Require a Node module for use in commands like `-each` and `-run`. Required modules are added to the expression context. Named expressions are accessed via thair names or aliases. Unnamed modules have their exported properties added to the expression context. + +`` or `module=` Name of a Node module or path to a module file. + +`alias=` Use an alias for a named module or module file. + +`init=` JS expression to run after the module loads. + +```bash +# Example: use the underscore module +$ mapshaper data.json \ + -require underscore alias=_ \ + -each 'id = _.uniqueId()' \ + -o data.json force +``` + +### -run + +Create mapshaper commands on-the-fly and run them. + +`` or `commands=` A JS expression for generating one or more mapshaper commands. The expression has access to a "target" object with information about the currently targeted layer, as well as modules loaded with the `-require` command. + +Common options: `target=` + +**Example:** Apply a custom projection based on the layer extent + +```bash +$ mapshaper -i country.shp -require projection.js -run 'getProjCommand(target)' -o +``` + +```javascript +// contents of projection.js file +module.exports = { + getProjCommand: function(target) { + var clon = (target.bbox[0] + target.bbox[2]) / 2, + clat = (target.bbox[1] + target.bbox[3]) / 2; + return `-proj +proj=tmerc lat_0=${clat} lon_0=${clon}`; + } +}; + +``` + + + +### -shape + +Create a new layer containing a single polyline or polygon shape. + +`coordinates=` Specify vertex coordinates as a comma-separated list. + +`offsets=` Specify vertex coordinates as a list of offsets from the previous vertex. The first vertex in the list is offset from the last coordinate in the `coordinates=` list. + +`closed` Close an open path to form a polygon shape. + +`name=` Assign a name to the newly created layer. + + +### -simplify + +Mapshaper supports Douglas-Peucker simplification and two kinds of Visvalingam simplification. + +Douglas-Peucker (a.k.a. Ramer-Douglas-Peucker) produces simplified lines that remain within a specified distance of the original line. It is effective for thinning dense vertices but tends to form spikes at high simplification. + +Visvalingam simplification iteratively removes the least important point from a polyline. The importance of points is measured using a metric based on the geometry of the triangle formed by each non-endpoint vertex and the two neighboring vertices. The `visvalingam` option uses the "effective area" metric — points forming smaller-area triangles are removed first. + +Mapshaper's default simplification method uses Visvalingam simplification but weights the effective area of each point so that smaller-angle vertices are preferentially removed, resulting in a smoother appearance. + +When working with multiple polygon and polyline layers, the `-simplify` command is applied to all of the layers. + +**Options** + +`` or `percentage=` Percentage of removable points to retain. Accepts values in the range `0%-100%` or `0-1`. + +`dp` `rdp` Use Douglas-Peucker simplification. + +`visvalingam` Use Visvalingam simplification with the "effective area" metric. + +`weighted` Use weighted Visvalingam simplification (this is the default). Points located at the vertex of more acute angles are preferentially removed, for a smoother appearance. + +`weighting=` Coefficient for weighting Visvalingam simplification (default is 0.7). Higher values produce smoother output. `weighting=0` is equivalent to unweighted Visvalingam simplification. + +`resolution=` Use an output resolution (e.g. `1000x800`) to control the amount of simplification. + +`interval=` Specify simplification amount in units of distance. Uses meters when simplifying unprojected datasets in 3D space (see `planar` option below), otherwise uses the same units as the source data. + +`variable` Apply a variable amount of simplification to the paths in a polygon or polygon layer. This flag changes the `interval=`, `percentage=` and `resolution=` options to accept JavaScript expressions instead of literal values. (See the `-each` command for information on mapshaper JS expressions). + +`planar` By default, mapshaper simplifies decimal degree coordinates in 3D space (using geocentric x,y,z coordinates). The `planar` option treats lng,lat coordinates as x,y coordinates on a Cartesian plane. + +`keep-shapes` Prevent polygon features from disappearing at high simplification. For multipart features, mapshaper preserves the part with the largest original bounding box. + +`no-repair` By default, mapshaper rolls back simplification along pairs of intersecting line segments by re-introducing removed points until either the intersection disappears or there are no more points to add. This option disables intersection repair. + +`stats` Display summary statistics relating to the geometry of simplified paths. + +**Examples** +```bash +# Simplify counties.shp using the default algorithm, retaining 10% of removable vertices. +mapshaper counties.shp -simplify 10% -o simplified.shp + +# Use Douglas-Peucker simplification with a 100 meter threshold. +mapshaper states.shp -simplify dp interval=100 -o simplified/ +``` + +### -sort + +Sort features in a data layer using a JavaScript expression. + +`` or `expression=` Apply a JavaScript expression to each feature, using the resulting values for sorting the features. Uses the same execution environment as [`-each`](#-each). + +`ascending` Sort in ascending order (this is the default). + +`descending` Sort in descending order. + +[`target`](#common-options) + + +### -split + +Distributes features in the target layer to multiple output layers. If the `fields=` option is present, features with the same attribute value are grouped together. If no data field is supplied, the input layer is split into single-feature layers. + +`` or `field=` Name of the attribute field to split on. + +Common options: `+` `target=` + +**Examples** +```bash +# Split features from a named layer into new GeoJSON files using a data field. +# Output names use the original layer name + data values, +# e.g. states-AK.json, states-AL.json, etc. +mapshaper states.shp -split STATE -o format=geojson + +# Split features from an unnamed layer into new GeoJSON files using a data field. +# Output names contain data values, +# e.g. AK.json, AL.json, etc. +mapshaper states.shp name='' -split STATE -o format=geojson + +# Split source features into individual GeoJSON files (no data field supplied). +# Output names use source layer name + ascending number, +# e.g. states-1.json, states-2.json, etc. +mapshaper states.shp -split -o format=geojson +``` + +### -split-on-grid + +Split features into separate layers using a grid of cols,rows cells. Useful for dividing a large dataset into smaller files that can be loaded dynamically into an interactive map. Use `-o bbox-index` to export a file containing the name and bounding box of the shapes in each file. Empty cells are removed from the output. + +`` Size of the grid, e.g. `-split-on-grid 12,10` + +Common options: `target=` + +### -subdivide + +Recursively divide a layer using a boolean JS expression. The expression is first evaluated against all features in the layer. If true, the features are spatially partitioned either vertically or horizontally, according to whether the aggregate bounding box is relatively tall or wide. See example below. + +Subdivide expressions can call several functions that operate on a group of features. The `sum()` function takes a feature-level expression as an argument and returns the summed result after applying the expression to each feature in the group. Similar functions include `min()` `max()` `average()` and `median()`. + +`` or `expression=` Boolean JavaScript expression + +Common options: `target=` + +**Example** +```bash +# Aggregate census tracts into groups of less than 1,000,000 population and less than 100 sq km in area. +mapshaper tracts.shp + -subdivide "sum('POPULATION') >= 1000000 && sum('this.area') > 1e8" \ + -dissolve sum-fields=POPULATION \ + -merge-layers \ + -o tract_groups.shp +``` + +### -style + +Add common SVG attributes for SVG export and display in the web UI. Attribute values take either a literal value or a JS expression. See the [`-each`](#-each) command for help with expressions. This command was named `-svg-style` in earlier versions of mapshaper. + +`where=` Boolean JS expression for targetting a subset of features. + +`class=` One or more CSS classes, separated by spaces (e.g. `class="light semi-transparent"`) + +`fill=` Fill color (e.g. `#eee` `pink` `rgba(0, 0, 0, 0.2)`) + +`fill-pattern=` Definition string for a pattern. There are four pattern types: hatches, dots, squares and dashes. The syntax for each pattern is: + +- hatches [rotation] width1 color1 [width2 color2 ...] +- dots [rotation] size color1 [color2 ...] spacing background-color +- squares [rotation] size color1 [color2 ...] spacing background-color +- dashes [rotation] dash-length space-length line-width color line-spacing background-color + +Example: `hatches 45deg 2px red 2px grey` + +`stroke=` Stroke color + +`stroke-width=` Stroke width + +`stroke-dasharray=` Dashes + +`opacity=` Symbol opacity (e.g. `opacity=0.5`) + +`r=` Circle radius. Setting this exports points as SVG `` symbols, unless the `-o point-symbol=square` option is used. + +`label-text=` Label text (set this to export points as labels). To create multiline labels, insert line delimiters into the label text. There are three possible line delimiters: the newline character, `\n` (backslash + "n"), and `
`. (When importing JSON data, `\n` in a JSON string is parsed as a newline and `\\n` is parsed as backslash + "n"). Note that Mapshaper doesn't accept multiline strings as input on the command line. + +`text-anchor=` Horizontal justification of label text. Possible values are: start, end or middle (the default). + +`dx=` X offset of labels (default is 0) + +`dy=` Y offset of labels (default is baseline-aligned) + +`font-size=` Size of label text (default is 12) + +`font-family=` CSS font family of labels (default is sans-serif) + +`font-weight=` CSS font weight property of labels (e.g. bold, 700) + +`font-style=` CSS font style property of labels (e.g. italic) + +`letter-spacing=` CSS letter-spacing property of labels + +`line-height=` Line spacing of multi-line labels (default is 1.1em). Lines are separated by newline characters in the label text. + +Common options: `target=` + +**Example** + +```bash +# Apply a 2px grey stroke and no fill to a polygon layer +mapshaper polygons.geojson \ +-style fill=none stroke='#aaa' stroke-width=2 \ +-o out.svg +``` + +### -union + +Create a composite layer (a polygon mosaic without overlaps) from two or more target polygon layers. + +Data values are copied from source features to output features. (Areal interpolation may +be added in the future. The `-join` command currently supports areal interpolation between polygon layers using the `-join interpolate=` option.) Same-named fields in source layers are renamed in the output layer. For example, two source-layer fields named "id" will be renamed to "id_1" and "id_2". + +`fields=` Fields to retain (default is all fields). + +Other options: `name=` `+` `target=` + +### -uniq + +Delete features with the same id as a previous feature + +`` or `expression=` JS expression to obtain the id of a feature. Uses the same expression syntax as [`-each`](#-each). + +`max-count=` Allow multiple features with the same id (default is 1). + +`invert` Retain only features that would ordinarily be deleted by `-uniq`. + +`verbose` Print information about each removed feature. + +`target=` + +```bash +# Example: Retain only the largest parts of each multipart polygon +mapshaper polygons.shp \ + -each 'fid = this.id' \ + -explode \ + -sort 'this.area' descending \ + -uniq 'fid' \ + -o out.shp +``` + +## Control Flow Commands + +### -if +### -elif + +The `if` command runs the following commands if a condition is met. One or more `-elif` (short for "else if") commands may be added to test for alternate conditions. + +`` or `expression=` Use a JavaScript expression to test a condition. + +`empty` Test if target layer is empty. + +`not-empty` Test if target layer contains data. + +`layer=` Name or id of layer to test (default is current target layer). + + +### -else + +Run the following commands if all preceding -if/-elif conditions are false. + +### -endif + +Mark the end of an -if/-elif/-else sequence. + +### -target + +Set default target layer + +`` or `target=` Name or id of a layer (first layer is 1). + +`type=` Type of layer(s) to match (polygon, polyline or point). This is useful when importing GeoJSON files containing several types of geometry. + +`name=` Rename the target layer. + + +## Informational Commands + + +### -calc + +Calculate basic descriptive statistics on a data table and display the results, using a JavaScript expression. The following functions are implemented: + +* `count()` +* `sum()` +* `average()` +* `median()` +* `mode()` +* `min()` +* `max()` +* `collect()` returns array containing all values +* `first()` +* `last()` +* `every()` +* `some()` + +`count()` takes no arguments. The other functions take as an argument a JS expression or field name. Argument expressions take the same form as `-each` expressions. If no records are processed, `count()` and `sum()` return `0`, and the other functions return `null`. + +**Options** + +`` or `expression=` JS expression containing calls to one or more `-calc` functions. + +`where=` Perform calculations on a subset of features, using a boolean JS expression as a filter (similar to [`-filter`](#-filter) command). + +Common options: `target=` + +**Examples** + +```bash +# Calculate the sum of a data field +mapshaper ny-census-blocks.shp -calc 'sum(POPULATION)' + +# Count census blocks in NY with zero population +mapshaper ny-census-blocks.shp -calc 'count()' where='POPULATION == 0' +``` + +### -colors +Print list of built-in color schemes. Color schemes can be used with the `color-scheme=` option of the [-classify](#-classify) command. (These color schemes come from the [d3-scale-chromatic](https://github.com/d3/d3-scale-chromatic) library.) + +### -encodings +Print list of supported text encodings (for .dbf import). + +### -help + +Print usage tips and a list of commands. + +`` Show options for a single command, e.g. `mapshaper -h join`. + +### -info + +Print information about a dataset. Useful for seeing the fields in a layer's attribute data table. Also useful for summarizing the result of a series of commands, or for debugging unexpected output. + +```bash +# Example: Get information about an unknown GeoJSON or TopoJSON dataset +mapshaper mystery_file.json -info +``` + +### -inspect + +Print information about the data attributes of a feature. + +`` or `expression=` JS expression for selecting a feature (see the [`-each`](#-each) command for documentation about JS expressions). + +Common options: `target=` + +```bash +# Example: View attribute data for a state +mapshaper states.geojson -inspect 'NAME == "Delaware"' +``` + +### -projections + +Print list of supported proj4 projection ids and projection aliases. + +### -quiet + +Inhibit console messages. + +### -verbose + +Print verbose messages, including the time taken by each processing step. + +### -version + +Print mapshaper version. From ada1632022b4c160df2b52a4b85be38bb44221f9 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 26 Jan 2022 19:55:36 -0500 Subject: [PATCH 156/891] Update reference --- REFERENCE.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/REFERENCE.md b/REFERENCE.md index 4c82021eb..f2c3c4b24 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -4,14 +4,16 @@ This documentation applies to version 0.5.88 of mapshaper's command line program ## Command line syntax -Mapshaper takes a list of commands and runs them in sequence, from left to right. A command consists of the name of a command prefixed by a hyphen, followed by options for the command. The initial import command `-i` can be omitted. Example: +Mapshaper takes a list of commands and runs them in sequence, from left to right. A command consists of the name of a command prefixed by a hyphen, followed by options for the command. The initial import command `-i` can be omitted. + +#### Example ```bash # Read a Shapefile, simplify using Douglas-Peucker, output as GeoJSON mapshaper provinces.shp -simplify dp 20% -o format=geojson out.json ``` -Command options can take three forms: +#### Command options can take three forms: - Values, like `provinces.shp` and `20%` in the above example @@ -19,9 +21,9 @@ Command options can take three forms: - Name/value pairs, like `format=geojson` -#### Common options +## Common options -These options are used by multiple commands +The following options are documented here, because they are used by many commands. `name=` Rename the layer (or layers) modified by a command. @@ -29,6 +31,7 @@ These options are used by multiple commands `+` Use the output of a command to create a new layer or layers instead of replacing the target layer(s). Use together with the `name=` option to assign a name to the new layer(s). +#### Example ```bash # Make a derived layer containing a subset of features while retaining the original layer mapshaper states.geojson -filter 'ST == "AK"' + name=alaska -o output/ target=* @@ -91,6 +94,7 @@ mapshaper states.geojson -filter 'ST == "AK"' + name=alaska -o output/ target=* [-uniq](#-uniq) **Control Flow** + [-if](#-if) [-elif](#-elif) [-else](#-else) @@ -1250,18 +1254,20 @@ mapshaper polygons.shp \ ## Control Flow Commands ### -if -### -elif -The `if` command runs the following commands if a condition is met. One or more `-elif` (short for "else if") commands may be added to test for alternate conditions. +The `if` command runs the following commands if a condition is met. `` or `expression=` Use a JavaScript expression to test a condition. -`empty` Test if target layer is empty. +`empty` Test if layer is empty. -`not-empty` Test if target layer contains data. +`not-empty` Test if layer contains data. `layer=` Name or id of layer to test (default is current target layer). +### -elif + +One or more `-elif` (short for "else if") commands may be added to test for alternate conditions, following an `-if` statement. The `-elif` command accepts the same options as the `-if` command. ### -else @@ -1273,7 +1279,7 @@ Mark the end of an -if/-elif/-else sequence. ### -target -Set default target layer +Set the target layer or layers for the following command. `` or `target=` Name or id of a layer (first layer is 1). From a982bb929c4059d76c2d179629138e3987a08e45 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 27 Jan 2022 09:33:55 -0500 Subject: [PATCH 157/891] Add this.size and this.empty getters to -if expr --- src/expressions/mapshaper-layer-proxy.js | 10 +++------- test/if-elif-else-test.js | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/expressions/mapshaper-layer-proxy.js b/src/expressions/mapshaper-layer-proxy.js index 27bc3ab31..84cce69ca 100644 --- a/src/expressions/mapshaper-layer-proxy.js +++ b/src/expressions/mapshaper-layer-proxy.js @@ -9,16 +9,12 @@ export function getLayerProxy(lyr, arcs) { var getters = { name: lyr.name, data: records, - type: lyr.geometry_type + type: lyr.geometry_type, + size: getFeatureCount(lyr), + empty: getFeatureCount(lyr) === 0 }; addGetters(obj, getters); addBBoxGetter(obj, lyr, arcs); - obj.empty = function() { - return getFeatureCount(lyr) === 0; - }; - obj.size = function() { - return getFeatureCount(lyr); - }; return obj; } diff --git a/test/if-elif-else-test.js b/test/if-elif-else-test.js index bd5703867..04dd96e8d 100644 --- a/test/if-elif-else-test.js +++ b/test/if-elif-else-test.js @@ -35,6 +35,23 @@ describe('mapshaper-if-elif-else-endif.js', function () { }); }); + + it ('test this.size getter', function(done) { + var data = { + type: 'GeometryCollection', + geometries: [{type: 'Point', coordinates: [1,2]}, {type: 'Point', coordinates: [2, 1]}] + }; + var cmd = `-i data.json -if 'this.size === 2' -dissolve -each 'foo = "bar"' -o format=json`; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var data = JSON.parse(out['data.json']); + assert.deepEqual(data, [{ + foo: 'bar' + }]); + done(); + }); + }); + + it ('test not-empty flag', function(done) { var data = [{name: 'a'}, {name: 'b'}]; var cmd = `-i data.json -if not-empty -each 'id = this.id' -else -each 'fid = this.id' \ From cf8b9688897774b0400e196f99d85d625207ac70 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 27 Jan 2022 09:34:15 -0500 Subject: [PATCH 158/891] Add documentation for -symbols command --- REFERENCE.md | 44 ++++++++++++++++++++++++++++++++++ src/cli/mapshaper-options.js | 46 ++++++++++++++++-------------------- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/REFERENCE.md b/REFERENCE.md index f2c3c4b24..c4c60fffb 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -90,6 +90,7 @@ mapshaper states.geojson -filter 'ST == "AK"' + name=alaska -o output/ target=* [-split-on-grid](#-split-on-grid) [-subdivide](#-subdivide) [-style](#-style) +[-symbols](#-symbols) [-union](#-union) [-uniq](#-uniq) @@ -1216,6 +1217,34 @@ mapshaper polygons.geojson \ -o out.svg ``` +### -symbols + +Symbolize points as polygons, circles, stars or arrows. + +`type=` Symbol type (e.g. star, polygon, circle, arrow) +`fill=` Symbol fill color +`polygons` Senerate symbols as polygons instead of SVG objects +`pixel-scale=` Set symbol scale in meters-per-pixel (for polygons option) +`rotated` Symbol is rotated to an alternate orientation +`rotation=` Rotation of symbol in degrees +`scale=` Scale symbols by a multiplier +`radius=` Distance from center to farthest point on the symbol +`sides=` (polygon) number of sides of a polygon symbol +`points=` (star) number of points +`point-ratio=` (star) ratio of minor to major radius of star +`radii=` (ring) comma-sep. list of concentric radii, ascending order +`length=` (arrow) length of arrow in pixels +`direction=` (arrow) angle off vertical (-90 = left-pointing) +`head-angle=` (arrow) angle of tip of arrow (default is 40 degrees) +`head-width=` (arrow) width of arrow head from side to side +`head-length=` (arrow) length of head (alternative to head-angle) +`stem-width=` (arrow) width of stem at its widest point +`stem-length=` (arrow) alternative to length +`stem-taper=` (arrow) factor for tapering the width of the stem (0-1) +`stem-curve=` (arrow) curvature in degrees (default is 0) +`min-stem-ratio=` (arrow) min ratio of stem to total length +`anchor=` (arrow) takes one of: start, middle, end (default is start) + ### -union Create a composite layer (a polygon mosaic without overlaps) from two or more target polygon layers. @@ -1265,6 +1294,21 @@ The `if` command runs the following commands if a condition is met. `layer=` Name or id of layer to test (default is current target layer). +**Properties of `this`** + +- `this.name` Layer name, or if layer is unnamed. +- `this.size` Number of features in the layer. +- `this.empty` True if layer contains 0 features. +- `this.data` Array of attribute data records, one object per feature. +- `this.type` Geometry type, one of: polygon, polyline, point, . +- `this.bbox` An array [xmin, ymin, xmax, ymax] with additional properties: cx, cy, height, width, left, bottom, top, right. + +**Example** + +```bash +mapshaper -i shapes.json -if '!this.empty' -dissolve -o out/dissolved.json +``` + ### -elif One or more `-elif` (short for "else if") commands may be added to test for alternate conditions, following an `-if` statement. The `-elif` command accepts the same options as the `-if` command. diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index beb5d8fbd..f1c449568 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1518,33 +1518,22 @@ export function getOptionParser() { .option('target', targetOpt); parser.command('symbols') - // .describe('symbolize points as polygons, circles, stars or arrows') + .describe('symbolize points as polygons, circles, stars or arrows') .option('type', { describe: 'symbol type (e.g. star, polygon, circle, arrow)' }) - .option('scale', { - describe: 'scale symbols by a factor', - type: 'number' - }) - .option('pixel-scale', { - describe: 'symbol scale in meters-per-pixel (see polygons option)', - type: 'number', + .option('stroke', {}) + .option('stroke-width', {}) + .option('fill', { + describe: 'symbol fill color' }) .option('polygons', { describe: 'generate symbols as polygons instead of SVG objects', type: 'flag' }) - .option('radius', { - describe: 'distance from center to farthest point on the symbol', - type: 'distance' - }) - .option('sides', { - describe: 'number of sides of a polygon symbol', - type: 'number' - }) - .option('orientation', { - // TODO: removed (replaced by flipped and rotated) - // describe: 'use orientation=b for a rotated or flipped orientation' + .option('pixel-scale', { + describe: 'set symbol scale in meters-per-pixel (for polygons option)', + type: 'number', }) // .option('flipped', { // type: 'flag', @@ -1552,11 +1541,23 @@ export function getOptionParser() { // }) .option('rotated', { type: 'flag', - describe: 'symbol is rotated to a different orientation' + describe: 'symbol is rotated to an alternate orientation' }) .option('rotation', { describe: 'rotation of symbol in degrees' }) + .option('scale', { + describe: 'scale symbols by a multiplier', + type: 'number' + }) + .option('radius', { + describe: 'distance from center to farthest point on the symbol', + type: 'distance' + }) + .option('sides', { + describe: '(polygon) number of sides of a polygon symbol', + type: 'number' + }) .option('points', { describe: '(star) number of points' }) @@ -1615,11 +1616,6 @@ export function getOptionParser() { .option('anchor', { describe: '(arrow) takes one of: start, middle, end (default is start)' }) - .option('stroke', {}) - .option('stroke-width', {}) - .option('fill', { - describe: 'symbol fill color' - }) .option('effect', {}) // .option('where', whereOpt) .option('name', nameOpt) From 25f55917e8318db6d2929483fffcdc80493c7b6f Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 27 Jan 2022 09:37:09 -0500 Subject: [PATCH 159/891] Update reference --- REFERENCE.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/REFERENCE.md b/REFERENCE.md index c4c60fffb..17d28fad9 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -1222,27 +1222,49 @@ mapshaper polygons.geojson \ Symbolize points as polygons, circles, stars or arrows. `type=` Symbol type (e.g. star, polygon, circle, arrow) + `fill=` Symbol fill color + `polygons` Senerate symbols as polygons instead of SVG objects + `pixel-scale=` Set symbol scale in meters-per-pixel (for polygons option) + `rotated` Symbol is rotated to an alternate orientation + `rotation=` Rotation of symbol in degrees + `scale=` Scale symbols by a multiplier + `radius=` Distance from center to farthest point on the symbol + `sides=` (polygon) number of sides of a polygon symbol + `points=` (star) number of points + `point-ratio=` (star) ratio of minor to major radius of star + `radii=` (ring) comma-sep. list of concentric radii, ascending order + `length=` (arrow) length of arrow in pixels + `direction=` (arrow) angle off vertical (-90 = left-pointing) + `head-angle=` (arrow) angle of tip of arrow (default is 40 degrees) + `head-width=` (arrow) width of arrow head from side to side + `head-length=` (arrow) length of head (alternative to head-angle) + `stem-width=` (arrow) width of stem at its widest point + `stem-length=` (arrow) alternative to length + `stem-taper=` (arrow) factor for tapering the width of the stem (0-1) + `stem-curve=` (arrow) curvature in degrees (default is 0) + `min-stem-ratio=` (arrow) min ratio of stem to total length + `anchor=` (arrow) takes one of: start, middle, end (default is start) ### -union From 64c536fc22a02571dfd8faa8182b73328b28387f Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 27 Jan 2022 09:40:40 -0500 Subject: [PATCH 160/891] Update reference --- REFERENCE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/REFERENCE.md b/REFERENCE.md index 17d28fad9..3a3e6fc2f 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -1221,7 +1221,7 @@ mapshaper polygons.geojson \ Symbolize points as polygons, circles, stars or arrows. -`type=` Symbol type (e.g. star, polygon, circle, arrow) +`type=` Basic types: star, polygon, circle, arrow, ring. Aliases: triangle, square, pentagon, etc. `fill=` Symbol fill color From 94df8f451d7edf5f22bde31924cc6a0fdb9fb164 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 30 Jan 2022 21:19:45 -0500 Subject: [PATCH 161/891] v0.5.89 --- CHANGELOG.md | 4 + REFERENCE.md | 8 +- bin/mapshaper-gui | 5 +- package-lock.json | 4 +- package.json | 2 +- src/cli/mapshaper-options.js | 10 +- src/gui/gui-import-control.js | 413 +++++++++++++++------------------- src/gui/gui-lib.js | 24 ++ src/gui/gui.js | 1 + 9 files changed, 222 insertions(+), 249 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d095f35e..38dd47ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.89 +* Added help documentation for -symbols command. +* Added --name option to mapshaper-gui, for setting the layer names of imported files. + v0.5.88 * Added -if/-elif/-else/-endif commands for running commands selectively. * Bug fixes diff --git a/REFERENCE.md b/REFERENCE.md index 3a3e6fc2f..676600983 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -1,6 +1,6 @@ # COMMAND REFERENCE -This documentation applies to version 0.5.88 of mapshaper's command line program. Run `mapshaper -v` to check your version. For an introduction to the command line tool, read [this page](https://github.com/mbloch/mapshaper/wiki/Introduction-to-the-Command-Line-Tool) first. +This documentation applies to version 0.5.89 of mapshaper's command line program. Run `mapshaper -v` to check your version. For an introduction to the command line tool, read [this page](https://github.com/mbloch/mapshaper/wiki/Introduction-to-the-Command-Line-Tool) first. ## Command line syntax @@ -1219,7 +1219,7 @@ mapshaper polygons.geojson \ ### -symbols -Symbolize points as polygons, circles, stars or arrows. +Symbolize points as regular polygons, circles, stars, arrows and other shapes. `type=` Basic types: star, polygon, circle, arrow, ring. Aliases: triangle, square, pentagon, etc. @@ -1247,7 +1247,7 @@ Symbolize points as polygons, circles, stars or arrows. `length=` (arrow) length of arrow in pixels -`direction=` (arrow) angle off vertical (-90 = left-pointing) +`direction=` (arrow) angle off of vertical (-90 = left-pointing) `head-angle=` (arrow) angle of tip of arrow (default is 40 degrees) @@ -1263,7 +1263,7 @@ Symbolize points as polygons, circles, stars or arrows. `stem-curve=` (arrow) curvature in degrees (default is 0) -`min-stem-ratio=` (arrow) min ratio of stem to total length +`min-stem-ratio=` (arrow) minimum ratio of stem to total length. This option scales down the entire symbol instead of making the stem shorter than the given ratio. `anchor=` (arrow) takes one of: start, middle, end (default is start) diff --git a/bin/mapshaper-gui b/bin/mapshaper-gui index 299d685de..2ad71ba9f 100755 --- a/bin/mapshaper-gui +++ b/bin/mapshaper-gui @@ -9,6 +9,7 @@ var defaultPort = 5555, .option('-s, --direct-save', 'save files outside the browser\'s download folder') .option('-f, --force-save', 'allow overwriting input files with output files') .option('-a, --display-all', 'turn on visibility of all layers') + .option('-n, --name ', 'rename input layer or layers') .option('-t, --target ', 'name of layer to select initially') .helpOption('-h, --help', 'show this help message') .version(require('../package.json').version) @@ -29,7 +30,8 @@ var defaultPort = 5555, validateFiles(dataFiles); process.on('uncaughtException', function(err) { - if (err.errno === 'EADDRINUSE') { + // added 'code' for Node.js v16 + if (err.errno === 'EADDRINUSE' || err.code === 'EADDRINUSE') { // probe for an open port, unless user has specified a non-default port if (port == defaultPort && probeCount < 10) { probeCount++; @@ -207,6 +209,7 @@ function getManifestJS(files, opts) { if (opts.directSave) o.allow_saving = true; if (opts.displayAll) o.display_all = true; if (opts.quickView) o.quick_view = true; + if (opts.name) o.name = opts.name; return "mapshaper.manifest = " + JSON.stringify(o) + ";\n"; } diff --git a/package-lock.json b/package-lock.json index b2e58d3e4..d66cd89c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapshaper", - "version": "0.5.88", + "version": "0.5.89", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mapshaper", - "version": "0.5.88", + "version": "0.5.89", "license": "MPL-2.0", "dependencies": { "commander": "^5.1.0", diff --git a/package.json b/package.json index 4a73b07ea..a21f890ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.88", + "version": "0.5.89", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index f1c449568..a8b86cf25 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -1518,9 +1518,9 @@ export function getOptionParser() { .option('target', targetOpt); parser.command('symbols') - .describe('symbolize points as polygons, circles, stars or arrows') + .describe('symbolize points as arrows, circles, stars, polygons, etc.') .option('type', { - describe: 'symbol type (e.g. star, polygon, circle, arrow)' + describe: 'symbol type (e.g. arrow, circle, square, star, polygon, ring)' }) .option('stroke', {}) .option('stroke-width', {}) @@ -1555,7 +1555,7 @@ export function getOptionParser() { type: 'distance' }) .option('sides', { - describe: '(polygon) number of sides of a polygon symbol', + describe: '(polygon) number of sides of a (regular) polygon symbol', type: 'number' }) .option('points', { @@ -1575,7 +1575,7 @@ export function getOptionParser() { }) .option('direction', { old_alias: 'arrow-direction', - describe: '(arrow) angle off vertical (-90 = left-pointing)' + describe: '(arrow) angle off of vertical (-90 = left-pointing)' }) .option('head-angle', { old_alias: 'arrow-head-angle', @@ -1610,7 +1610,7 @@ export function getOptionParser() { }) .option('min-stem-ratio', { old_alias: 'arrow-min-stem', - describe: '(arrow) min ratio of stem to total length', + describe: '(arrow) minimum ratio of stem to total length', type: 'number' }) .option('anchor', { diff --git a/src/gui/gui-import-control.js b/src/gui/gui-import-control.js index fde6514a8..3463a63b0 100644 --- a/src/gui/gui-import-control.js +++ b/src/gui/gui-import-control.js @@ -98,20 +98,19 @@ function FileChooser(el, cb) { export function ImportControl(gui, opts) { var model = gui.model; + var initialImport = true; var importCount = 0; var importTotal = 0; var overQuickView = false; - var useQuickView = opts.quick_view; // may be set by mapshaper-gui var queuedFiles = []; var manifestFiles = opts.files || []; - var cachedFiles = {}; var catalog; if (opts.catalog) { catalog = new CatalogControl(gui, opts.catalog, downloadFiles); } - new SimpleButton('#import-buttons .submit-btn').on('click', onSubmit); + new SimpleButton('#import-buttons .submit-btn').on('click', importQueuedFiles); new SimpleButton('#import-buttons .cancel-btn').on('click', gui.clearMode); new DropControl(gui, 'body', receiveFiles); new FileChooser('#file-selection-btn', receiveFiles); @@ -119,7 +118,7 @@ export function ImportControl(gui, opts) { new FileChooser('#add-file-btn', receiveFiles); initDropArea('#import-quick-drop', true); initDropArea('#import-drop'); - gui.keyboard.onMenuSubmit(El('#import-options'), onSubmit); + gui.keyboard.onMenuSubmit(El('#import-options'), importQueuedFiles); gui.addMode('import', turnOn, turnOff); gui.enterMode('import'); @@ -131,6 +130,10 @@ export function ImportControl(gui, opts) { } }); + function useQuickView() { + return initialImport && (opts.quick_view || overQuickView); + } + function initDropArea(el, isQuick) { var area = El(el) .on('dragleave', onout) @@ -148,15 +151,19 @@ export function ImportControl(gui, opts) { } } - function findMatchingShp(filename) { - // use case-insensitive matching - var base = internal.getPathBase(filename).toLowerCase(); - return model.getDatasets().filter(function(d) { - var fname = d.info.input_files && d.info.input_files[0] || ""; - var ext = internal.getFileExtension(fname).toLowerCase(); - var base2 = internal.getPathBase(fname).toLowerCase(); - return base == base2 && ext == 'shp'; - }); + async function importQueuedFiles() { + gui.container.removeClass('queued-files'); + gui.container.removeClass('splash-screen'); + var files = queuedFiles; + try { + if (files.length > 0) { + queuedFiles = []; + await importFiles(files); + } + } catch(e) { + console.error(e); + } + gui.clearMode(); } function turnOn() { @@ -177,13 +184,8 @@ export function ImportControl(gui, opts) { importCount = 0; } gui.clearProgressMessage(); - useQuickView = false; // unset 'quick view' mode, if on - close(); - } - - function close() { + initialImport = false; // unset 'quick view' mode, if on clearQueuedFiles(); - cachedFiles = {}; } function onImportComplete() { @@ -217,34 +219,6 @@ export function ImportControl(gui, opts) { }, []); } - // When a Shapefile component is at the head of the queue, move the entire - // Shapefile to the front of the queue, sorted in reverse alphabetical order, - // (a kludge), so .shp is read before .dbf and .prj - // (If a .dbf file is imported before a .shp, it becomes a separate dataset) - // TODO: import Shapefile parts without relying on this kludge - function sortQueue(queue) { - var nextFile = queue[0]; - var basename, parts; - if (!isShapefilePart(nextFile.name)) { - return queue; - } - basename = internal.getFileBase(nextFile.name).toLowerCase(); - parts = []; - queue = queue.filter(function(file) { - if (internal.getFileBase(file.name).toLowerCase() == basename) { - parts.push(file); - return false; - } - return true; - }); - parts.sort(function(a, b) { - // Sorting on LC filename so Shapefiles with mixed-case - // extensions are sorted correctly - return a.name.toLowerCase() < b.name.toLowerCase() ? 1 : -1; - }); - return parts.concat(queue); - } - function showQueuedFiles() { var list = gui.container.findChild('.dropped-file-list').empty(); queuedFiles.forEach(function(f) { @@ -252,16 +226,14 @@ export function ImportControl(gui, opts) { }); } - function receiveFiles(files) { - var prevSize = queuedFiles.length; - useQuickView = useQuickView || overQuickView; - files = handleZipFiles(utils.toArray(files)); - addFilesToQueue(files); + async function receiveFiles(files) { + // TODO: show importing message here? + var expanded = await expandFiles(files); + addFilesToQueue(expanded); if (queuedFiles.length === 0) return; gui.enterMode('import'); - - if (useQuickView) { - onSubmit(); + if (useQuickView()) { + importQueuedFiles(); } else { gui.container.addClass('queued-files'); El('#path-import-options').classed('hidden', !filesMayContainPaths(queuedFiles)); @@ -269,25 +241,51 @@ export function ImportControl(gui, opts) { } } - function filesMayContainPaths(files) { - return utils.some(files, function(f) { - var type = internal.guessInputFileType(f.name); - return type == 'shp' || type == 'json' || internal.isZipFile(f.name); - }); + async function expandFiles(files) { + var files2 = [], expanded; + for (var f of files) { + if (internal.isZipFile(f.name)) { + expanded = await readZipFile(f); + files2 = files2.concat(expanded); + } else { + files2.push(f); + } + } + return files2; } - function onSubmit() { - gui.container.removeClass('queued-files'); - gui.container.removeClass('splash-screen'); - procNextQueuedFile(); + async function importFiles(files) { + var fileData = await readFiles(files); + var importOpts = readImportOpts(); + var groups = groupFilesForImport(fileData, importOpts); + for (var group of groups) { + if (group.size > 4e7) { + gui.showProgressMessage('Importing'); + await wait(35); + } + importDataset(group, importOpts); + } } - function addDataset(dataset) { - if (!datasetIsEmpty(dataset)) { - model.addDataset(dataset); - importCount++; + function importDataset(group, importOpts) { + var optStr = GUI.formatCommandOptions(importOpts); + var dataset = internal.importContent(group, importOpts); + if (datasetIsEmpty(dataset)) return; + if (group.layername) { + dataset.layers.forEach(lyr => lyr.name = group.layername); } - procNextQueuedFile(); + // save import options for use by repair control, etc. + dataset.info.import_options = importOpts; + model.addDataset(dataset); + importCount++; + gui.session.fileImported(group.filename, optStr); + } + + function filesMayContainPaths(files) { + return utils.some(files, function(f) { + var type = internal.guessInputFileType(f.name); + return type == 'shp' || type == 'json' || internal.isZipFile(f.name); + }); } function datasetIsEmpty(dataset) { @@ -296,41 +294,27 @@ export function ImportControl(gui, opts) { }); } - function procNextQueuedFile() { - if (queuedFiles.length === 0) { - gui.clearMode(); - } else { - queuedFiles = sortQueue(queuedFiles); - readFile(queuedFiles.shift()); - } - } // TODO: support .cpg function isShapefilePart(name) { return /\.(shp|shx|dbf|prj)$/i.test(name); } - function readImportOpts() { - if (useQuickView) return {}; - var freeform = El('#import-options .advanced-options').node().value, - opts = GUI.parseFreeformOptions(freeform, 'i'); - opts.no_repair = !El("#repair-intersections-opt").node().checked; - opts.snap = !!El("#snap-points-opt").node().checked; - return opts; - } - - // for CLI output - function readImportOptsAsString() { - if (useQuickView) return ''; - var freeform = El('#import-options .advanced-options').node().value; - var opts = readImportOpts(); - if (opts.snap) freeform = 'snap ' + freeform; - return freeform.trim(); + var importOpts; + if (useQuickView()) { + importOpts = {}; // default opts using quickview + } else { + var freeform = El('#import-options .advanced-options').node().value; + importOpts = GUI.parseFreeformOptions(freeform, 'i'); + importOpts.no_repair = !El("#repair-intersections-opt").node().checked; + importOpts.snap = !!El("#snap-points-opt").node().checked; + } + return importOpts; } // @file a File object - function readFile(file) { + async function readContentFileAsync(file, cb) { var name = file.name, reader = new FileReader(), useBinary = internal.isSupportedBinaryInputType(name) || @@ -340,9 +324,9 @@ export function ImportControl(gui, opts) { reader.addEventListener('loadend', function(e) { if (!reader.result) { - handleImportError("Web browser was unable to load the file.", name); + cb(new Error()); } else { - importFileContent(name, reader.result); + cb(null, reader.result); } }); if (useBinary) { @@ -353,91 +337,6 @@ export function ImportControl(gui, opts) { } } - function importFileContent(fileName, content) { - var fileType = internal.guessInputType(fileName, content), - importOpts = readImportOpts(), - matches = findMatchingShp(fileName), - dataset, lyr; - - // Add dbf data to a previously imported .shp file with a matching name - // (.shp should have been queued before .dbf) - if (fileType == 'dbf' && matches.length > 0) { - // find an imported .shp layer that is missing attribute data - // (if multiple matches, try to use the most recently imported one) - dataset = matches.reduce(function(memo, d) { - if (!d.layers[0].data) { - memo = d; - } - return memo; - }, null); - if (dataset) { - lyr = dataset.layers[0]; - lyr.data = new internal.ShapefileTable(content, importOpts.encoding); - if (lyr.shapes && lyr.data.size() != lyr.shapes.length) { - stop("Different number of records in .shp and .dbf files"); - } - if (!lyr.geometry_type) { - // kludge: trigger display of table cells if .shp has null geometry - // TODO: test case if lyr is not the current active layer - model.updated({}); - } - procNextQueuedFile(); - return; - } - } - - if (fileType == 'shx') { - // save .shx for use when importing .shp - // (queue should be sorted so that .shx is processed before .shp) - cachedFiles[fileName.toLowerCase()] = {filename: fileName, content: content}; - procNextQueuedFile(); - return; - } - - // Add .prj file to previously imported .shp file - if (fileType == 'prj') { - matches.forEach(function(d) { - if (!d.info.prj) { - d.info.prj = content; - } - }); - procNextQueuedFile(); - return; - } - - importNewDataset(fileType, fileName, content, importOpts); - } - - function importNewDataset(fileType, fileName, content, importOpts) { - var size = content.byteLength || content.length, // ArrayBuffer or string - delay = 0; - - // show importing message if file is large - if (size > 4e7) { - gui.showProgressMessage('Importing'); - delay = 35; - } - setTimeout(function() { - var dataset; - var input = {}; - try { - input[fileType] = {filename: fileName, content: content}; - if (fileType == 'shp') { - // shx file should already be cached, if it was added together with the shp - input.shx = cachedFiles[fileName.replace(/shp$/i, 'shx').toLowerCase()] || null; - } - dataset = internal.importContent(input, importOpts); - // save import options for use by repair control, etc. - dataset.info.import_options = importOpts; - gui.session.fileImported(fileName, readImportOptsAsString()); - addDataset(dataset); - - } catch(e) { - handleImportError(e, fileName); - } - }, delay); - } - function handleImportError(e, fileName) { var msg = utils.isString(e) ? e : e.message; if (fileName) { @@ -448,34 +347,6 @@ export function ImportControl(gui, opts) { console.error(e); } - function handleZipFiles(files) { - return files.filter(function(file) { - var isZip = internal.isZipFile(file.name); - if (isZip) { - importZipFile(file); - } - return !isZip; - }); - } - - function importZipFile(file) { - // gui.showProgressMessage('Importing'); - setTimeout(function() { - GUI.readZipFile(file, function(err, files) { - if (err) { - handleImportError(err, file.name); - } else { - // don't try to import .txt files from zip files - // (these would be parsed as dsv and throw errows) - files = files.filter(function(f) { - return !/\.txt$/i.test(f.name); - }); - receiveFiles(files); - } - }); - }, 35); - } - function prepFilesForDownload(names) { var items = names.map(function(name) { var isUrl = /:\/\//.test(name); @@ -521,36 +392,106 @@ export function ImportControl(gui, opts) { }); } - function downloadNextFile_v1(memo, item, next) { - var req = new XMLHttpRequest(); - var blob; - req.responseType = 'blob'; - req.addEventListener('load', function(e) { - if (req.status == 200) { - blob = req.response; - } - }); - req.addEventListener('progress', function(e) { - if (!e.lengthComputable) return; - var pct = e.loaded / e.total; - if (catalog) catalog.progress(pct); + function wait(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + function runAsync(fn, arg) { + return new Promise((resolve, reject) => { + fn(arg, function(err, data) { + return err ? reject(err) : resolve(data); + }); }); - req.addEventListener('loadend', function() { - var err; - if (req.status == 404) { - err = "Not found: " + item.name; - } else if (!blob) { - // Errors like DNS lookup failure, no CORS headers, no network connection - // all are status 0 - it seems impossible to show a more specific message - // actual reason is displayed on the console - err = "Error loading " + item.name + ". Possible causes include: wrong URL, no network connection, server not configured for cross-domain sharing (CORS)."; + } + + async function readZipFile(file) { + var files; + await wait(35); // pause a beat so status message can display + try { + files = await runAsync(GUI.readZipFile, file); + // don't try to import .txt files from zip files + // (these would be parsed as dsv and throw errows) + files = files.filter(function(f) { + return !/\.txt$/i.test(f.name); + }); + } catch(e) { + handleImportError(e, file.name); + files = []; + } + return files; + } + + async function readFileData(file) { + try { + var content = await runAsync(readContentFileAsync, file); + return { + content: content, + size: content.byteLength || content.length, // ArrayBuffer or string + name: file.name, + basename: internal.getFileBase(file.name).toLowerCase(), + type: internal.guessInputType(file.name, content) + }; + } catch (e) { + handleImportError("Web browser was unable to load the file.", file.name); + } + return null; + } + + async function readFiles(files) { + var data = [], d; + for (var file of files) { + d = await readFileData(file); + if (d) data.push(d); + } + return data; + } + + function groupFilesForImport(data, importOpts) { + var names = importOpts.name ? [importOpts.name] : null; + if (initialImport && opts.name) { // name from mapshaper-gui --name option + names = opts.name.split(','); + } + + function key(basename, type) { + return basename + '.' + type; + } + function hasShp(basename) { + var shpKey = key(basename, 'shp'); + return data.some(d => key(d.basename, d.type) == shpKey); + } + data.forEach(d => { + if (d.type == 'shp' || !isShapefilePart(d.name)) { + d.group = key(d.basename, d.type); + d.filename = d.name; + } else if (hasShp(d.basename)) { + d.group = key(d.basename, 'shp'); + } else if (d.type == 'dbf') { + d.filename = d.name; + d.group = key(d.basename, 'dbf'); } else { - blob.name = item.basename; - memo.push(blob); + // shapefile part without a .shp file + d.group = null; } - next(err, memo); }); - req.open('GET', item.url); - req.send(); + var index = {}; + var groups = []; + data.forEach(d => { + if (!d.group) return; + var g = index[d.group]; + if (!g) { + g = {}; + g.layername = names ? names[groups.length] || names[names.length - 1] : null; + groups.push(g); + index[d.group] = g; + } + g.size = (g.size || 0) + d.size; // accumulate size + g[d.type] = { + filename: d.name, + content: d.content + }; + // kludge: stash import name for session history + if (d.filename) g.filename = d.filename; + }); + return groups; } } diff --git a/src/gui/gui-lib.js b/src/gui/gui-lib.js index 74ab2eb6e..5f2389556 100644 --- a/src/gui/gui-lib.js +++ b/src/gui/gui-lib.js @@ -116,3 +116,27 @@ GUI.parseFreeformOptions = function(raw, cmd) { } return parsed[0].options; }; + +// Convert an options object to a command line options string +// (used by gui-import-control.js) +// TODO: handle options with irregular string <-> object conversion +GUI.formatCommandOptions = function(o) { + var arr = []; + Object.keys(o).forEach(function(key) { + var name = key.replace(/_/g, '-'); + var val = o[key]; + var str; + // TODO: quote values that contain spaces + if (Array.isArray(val)) { + str = name + '=' + val.join(','); + } else if (val === true) { + str = name; + } else if (val === false) { + return; + } else { + str = name + '=' + val; + } + arr.push(str); + }); + return arr.join(' '); +}; diff --git a/src/gui/gui.js b/src/gui/gui.js index 9f79b82a9..016edefb6 100644 --- a/src/gui/gui.js +++ b/src/gui/gui.js @@ -41,6 +41,7 @@ function getImportOpts() { opts.display_all = vars['display-all'] || vars.a || !!manifest.display_all; opts.quick_view = vars['quick-view'] || vars.q || !!manifest.quick_view; opts.target = vars.target || manifest.target || null; + opts.name = vars.name || manifest.name || null; return opts; } From 22eba9ba39ce76592378cda3b6cb1805d6d0f5ea Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 2 Feb 2022 11:23:48 -0500 Subject: [PATCH 162/891] Add -print command --- src/cli/mapshaper-options.js | 4 ++++ src/cli/mapshaper-run-command.js | 6 +++++- src/commands/mapshaper-print.js | 6 ++++++ src/utils/mapshaper-logging.js | 3 ++- 4 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/commands/mapshaper-print.js diff --git a/src/cli/mapshaper-options.js b/src/cli/mapshaper-options.js index a8b86cf25..2b18008a1 100644 --- a/src/cli/mapshaper-options.js +++ b/src/cli/mapshaper-options.js @@ -2004,6 +2004,10 @@ export function getOptionParser() { .option('target', targetOpt) .validate(V.validateExpressionOpt); + parser.command('print') + .describe('print a message to stdout') + .flag('multi_arg'); + parser.command('projections') .describe('print list of supported projections'); diff --git a/src/cli/mapshaper-run-command.js b/src/cli/mapshaper-run-command.js index 3a500f1c6..05e1af1e7 100644 --- a/src/cli/mapshaper-run-command.js +++ b/src/cli/mapshaper-run-command.js @@ -59,6 +59,7 @@ import '../commands/mapshaper-polygon-grid'; import '../commands/mapshaper-point-grid'; import '../commands/mapshaper-point-to-grid'; import '../commands/mapshaper-polygons'; +import '../commands/mapshaper-print'; import '../commands/mapshaper-proj'; import '../commands/mapshaper-rectangle'; import '../commands/mapshaper-rename-layers'; @@ -149,7 +150,7 @@ export function runCommand(command, catalog, cb) { } if (!(name == 'graticule' || name == 'i' || name == 'help' || name == 'point-grid' || name == 'shape' || name == 'rectangle' || - name == 'include')) { + name == 'include' || name == 'print')) { throw new UserError("No data is available"); } } @@ -334,6 +335,9 @@ export function runCommand(command, catalog, cb) { } else if (name == 'polygons') { outputLayers = cmd.polygons(targetLayers, targetDataset, opts); + } else if (name == 'print') { + cmd.print(command._.join(' ')); + } else if (name == 'proj') { initProjLibrary(opts, function() { var err = null; diff --git a/src/commands/mapshaper-print.js b/src/commands/mapshaper-print.js new file mode 100644 index 000000000..88378a32e --- /dev/null +++ b/src/commands/mapshaper-print.js @@ -0,0 +1,6 @@ +import cmd from '../mapshaper-cmd'; +import { print } from '../utils/mapshaper-logging'; + +cmd.print = function(msgArg) { + print(msgArg || ''); +}; diff --git a/src/utils/mapshaper-logging.js b/src/utils/mapshaper-logging.js index 2451408cb..bfced3567 100644 --- a/src/utils/mapshaper-logging.js +++ b/src/utils/mapshaper-logging.js @@ -61,7 +61,8 @@ export function setLoggingFunctions(message, error, stop) { // print a message to stdout export function print() { STDOUT = true; // tell logArgs() to print to stdout, not stderr - message.apply(null, arguments); + // calling message() adds the "[command name]" prefix + _message(utils.toArray(arguments)); STDOUT = false; } From 62bf5f8ffafa6bbefcd94e3f36bfe262a2ad072f Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 2 Feb 2022 11:24:45 -0500 Subject: [PATCH 163/891] Add functions to -if expressions --- REFERENCE.md | 18 +++++++++++++ src/expressions/mapshaper-layer-proxy.js | 16 +++++++++-- test/data-utils-test.js | 34 ++++++++++++++++++++++++ test/if-elif-else-test.js | 14 ++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/REFERENCE.md b/REFERENCE.md index 676600983..037d147fb 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -110,6 +110,7 @@ mapshaper states.geojson -filter 'ST == "AK"' + name=alaska -o output/ target=* [-help](#-help) [-info](#-info) [-inspect](#-inspect) +[-print](#-print) [-projections](#-projections) [-quiet](#-quiet) [-verbose](#-verbose) @@ -1325,6 +1326,12 @@ The `if` command runs the following commands if a condition is met. - `this.type` Geometry type, one of: polygon, polyline, point, . - `this.bbox` An array [xmin, ymin, xmax, ymax] with additional properties: cx, cy, height, width, left, bottom, top, right. +**Functions of `this`** + +- `this.field_exists()` Tests if a data field exists in the target layer. +- `this.field_type()` Returns the data type of a field, or `null` if a field is empty or missing. Types include: `"string" "number" "boolean" "date" "object"`. If a field includes multiple data types (which may occur in GeoJSON), the type of the first non-empty data value is returned. +- `this.field_includes()` Tests if a given value occurs at least once in a data field. + **Example** ```bash @@ -1428,6 +1435,17 @@ Common options: `target=` mapshaper states.geojson -inspect 'NAME == "Delaware"' ``` +### -print + +Prints a message to the console or terminal (using stdout). This command is useful in combination with the `-if/-elif/-else` commands. + +```bash +# Example +mapshaper cities.json \ +-if 'this.empty' \ +-print FILE IS EMPTY +``` + ### -projections Print list of supported proj4 projection ids and projection aliases. diff --git a/src/expressions/mapshaper-layer-proxy.js b/src/expressions/mapshaper-layer-proxy.js index 84cce69ca..65112d7b2 100644 --- a/src/expressions/mapshaper-layer-proxy.js +++ b/src/expressions/mapshaper-layer-proxy.js @@ -1,6 +1,6 @@ -import { getLayerBounds } from '../dataset/mapshaper-layer-utils'; +import { getLayerBounds, getFeatureCount } from '../dataset/mapshaper-layer-utils'; import { addGetters } from '../expressions/mapshaper-expression-utils'; -import { getFeatureCount } from '../dataset/mapshaper-layer-utils'; +import { getColumnType } from '../datatable/mapshaper-data-utils'; // Returns an object representing a layer in a JS expression export function getLayerProxy(lyr, arcs) { @@ -15,6 +15,18 @@ export function getLayerProxy(lyr, arcs) { }; addGetters(obj, getters); addBBoxGetter(obj, lyr, arcs); + obj.field_exists = function(name) { + return lyr.data && lyr.data.fieldExists(name) ? true : false; + }; + obj.field_type = function(name) { + return lyr.data && getColumnType(name, lyr.data.getRecords()) || null; + }; + obj.field_includes = function(name, val) { + if (!lyr.data) return false; + return lyr.data.getRecords().some(function(rec) { + return rec && (rec[name] === val); + }); + }; return obj; } diff --git a/test/data-utils-test.js b/test/data-utils-test.js index 6d1596a14..6b182e166 100644 --- a/test/data-utils-test.js +++ b/test/data-utils-test.js @@ -8,6 +8,40 @@ describe('mapshaper-data-utils.js', function () { it('Date objects are type "date"', function() { assert.equal(getValueType(new Date()), 'date'); }); + + it('null is type null, not "object"', function() { + assert.strictEqual(getValueType(null), null) + }) + + it('undefined is type null', function() { + assert.strictEqual(getValueType(void 0), null) + assert.strictEqual(getValueType(undefined), null) + }) + + it('0 is type "number"', function() { + assert.strictEqual(getValueType(0), "number") + }) + }) + + describe('getColumnType()', function() { + var getColumnType = api.internal.getColumnType; + + it('missing field is type null', function() { + assert.strictEqual(getColumnType('foo', [{}]), null) + assert.strictEqual(getColumnType('foo', []), null) + }) + + // it('only NaN is type null', function() { + // assert.strictEqual(getColumnType('foo', [{foo: NaN}]), null) + // }) + + it('string field is type "string"', function() { + assert.strictEqual(getColumnType('foo', [{}, {foo: ''}]), 'string') + assert.strictEqual(getColumnType('foo', [{foo: 'bar'}]), 'string') + }) + it('mixed-type field: first non-empty type (TODO: rethink this)', function() { + assert.strictEqual(getColumnType('foo', [{foo: 0}, {foo: ''}]), 'number') + }) }) describe('fixInconsistentFields()', function () { diff --git a/test/if-elif-else-test.js b/test/if-elif-else-test.js index 04dd96e8d..363b4ae89 100644 --- a/test/if-elif-else-test.js +++ b/test/if-elif-else-test.js @@ -68,4 +68,18 @@ describe('mapshaper-if-elif-else-endif.js', function () { }); + it ('test field_type(), field_exists() and field_includes()', function(done) { + var data = [{name: 'a'}, {name: 'b'}]; + var cmd = `-i data.json -if 'this.field_exists("name")' -each 'a = true' -endif -if 'this.field_type("name") == "string"' -each 'b = true' -endif -if 'this.field_includes("name", "b")' -each 'c = true' -endif -o`; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var data = JSON.parse(out['data.json']); + assert.deepEqual(data, [{ + name: 'a', a: true, b: true, c: true + }, { + name: 'b', a: true, b: true, c: true + }]); + done(); + }); + }); + }) From 45ba4407315518ba6ef5dd7ce7a256d7e8bf3ad1 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 2 Feb 2022 11:27:53 -0500 Subject: [PATCH 164/891] v0.5.90 --- REFERENCE.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/REFERENCE.md b/REFERENCE.md index 037d147fb..05fe59473 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -1,6 +1,6 @@ # COMMAND REFERENCE -This documentation applies to version 0.5.89 of mapshaper's command line program. Run `mapshaper -v` to check your version. For an introduction to the command line tool, read [this page](https://github.com/mbloch/mapshaper/wiki/Introduction-to-the-Command-Line-Tool) first. +This documentation applies to version 0.5.90 of mapshaper's command line program. Run `mapshaper -v` to check your version. For an introduction to the command line tool, read [this page](https://github.com/mbloch/mapshaper/wiki/Introduction-to-the-Command-Line-Tool) first. ## Command line syntax diff --git a/package-lock.json b/package-lock.json index d66cd89c8..ba54ca885 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapshaper", - "version": "0.5.89", + "version": "0.5.90", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mapshaper", - "version": "0.5.89", + "version": "0.5.90", "license": "MPL-2.0", "dependencies": { "commander": "^5.1.0", diff --git a/package.json b/package.json index a21f890ec..9264a1345 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.89", + "version": "0.5.90", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 86a5e29c75a722d0f62381b9a77126b186825df2 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 2 Feb 2022 11:41:34 -0500 Subject: [PATCH 165/891] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38dd47ac1..9946930ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.90 +* Added -print command, for printing messages to the console or terminal. Useful in conjunction with the -if/-elif/-else commands. +* Added field_exists(), field_type() and field_includes() functions to -if expressions. + v0.5.89 * Added help documentation for -symbols command. * Added --name option to mapshaper-gui, for setting the layer names of imported files. From e8e95c61ab40b179b1c509633c47a7fe1032cdf3 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 2 Feb 2022 11:42:47 -0500 Subject: [PATCH 166/891] Bump version --- CHANGELOG.md | 2 +- REFERENCE.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9946930ab..1671d97dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -v0.5.90 +v0.5.91 * Added -print command, for printing messages to the console or terminal. Useful in conjunction with the -if/-elif/-else commands. * Added field_exists(), field_type() and field_includes() functions to -if expressions. diff --git a/REFERENCE.md b/REFERENCE.md index 05fe59473..dd6a4ac2d 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -1,6 +1,6 @@ # COMMAND REFERENCE -This documentation applies to version 0.5.90 of mapshaper's command line program. Run `mapshaper -v` to check your version. For an introduction to the command line tool, read [this page](https://github.com/mbloch/mapshaper/wiki/Introduction-to-the-Command-Line-Tool) first. +This documentation applies to version 0.5.91 of mapshaper's command line program. Run `mapshaper -v` to check your version. For an introduction to the command line tool, read [this page](https://github.com/mbloch/mapshaper/wiki/Introduction-to-the-Command-Line-Tool) first. ## Command line syntax diff --git a/package-lock.json b/package-lock.json index ba54ca885..9160e5b9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapshaper", - "version": "0.5.90", + "version": "0.5.91", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mapshaper", - "version": "0.5.90", + "version": "0.5.91", "license": "MPL-2.0", "dependencies": { "commander": "^5.1.0", diff --git a/package.json b/package.json index 9264a1345..40b9444d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.90", + "version": "0.5.91", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From b318d7a15fbc923e38afd4e0a840833fff5a1ba4 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 6 Feb 2022 17:28:43 -0500 Subject: [PATCH 167/891] Tweak vertex appearance --- src/gui/gui-canvas.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/gui/gui-canvas.js b/src/gui/gui-canvas.js index d64e69ddd..30bc67280 100644 --- a/src/gui/gui-canvas.js +++ b/src/gui/gui-canvas.js @@ -2,6 +2,7 @@ import { internal, utils, Bounds } from './gui-core'; import { El } from './gui-el'; import { GUI } from './gui-lib'; + // TODO: consider moving this upstream function getArcsForRendering(obj, ext) { var dataset = obj.source.dataset; @@ -147,17 +148,18 @@ export function DisplayCanvas() { _self.drawVertices = function(shapes, arcs, style, filter) { var iter = new internal.ShapeIter(arcs); var t = getScaledTransform(_ext); - var radius = (style.strokeWidth * 0.9 || 2.2) * GUI.getPixelRatio() * getScaledLineScale(_ext); + var radius = (style.strokeWidth > 2 ? style.strokeWidth * 0.9 : 2) * GUI.getPixelRatio() * getScaledLineScale(_ext); var color = style.strokeColor || 'black'; - var shp; _ctx.beginPath(); _ctx.fillStyle = color; for (var i=0; i Date: Sun, 6 Feb 2022 17:29:45 -0500 Subject: [PATCH 168/891] Refactor interactive editing code --- src/gui/gui-edit-labels.js | 133 +++++++++++++ src/gui/gui-edit-modes.js | 21 +++ src/gui/gui-edit-points.js | 44 +++++ src/gui/gui-edit-vertices.js | 49 +++++ src/gui/gui-map.js | 8 +- src/gui/gui-symbol-dragging2.js | 319 -------------------------------- 6 files changed, 252 insertions(+), 322 deletions(-) create mode 100644 src/gui/gui-edit-labels.js create mode 100644 src/gui/gui-edit-modes.js create mode 100644 src/gui/gui-edit-points.js create mode 100644 src/gui/gui-edit-vertices.js delete mode 100644 src/gui/gui-symbol-dragging2.js diff --git a/src/gui/gui-edit-labels.js b/src/gui/gui-edit-labels.js new file mode 100644 index 000000000..76e9a2fb1 --- /dev/null +++ b/src/gui/gui-edit-labels.js @@ -0,0 +1,133 @@ +import { error, internal } from './gui-core'; +import { isMultilineLabel, toggleTextAlign, setMultilineAttribute, autoUpdateTextAnchor, applyDelta } from './gui-svg-labels'; +import { getSvgSymbolTransform } from './gui-svg-symbols'; + +export function initLabelDragging(gui, ext, hit) { + var downEvt; + var activeId; + var activeRecord; + + function active(e) { + return e.id > -1 && gui.interaction.getMode() == 'labels'; + } + + hit.on('dragstart', function(e) { + if (!active(e)) return; + var textNode = getTextTarget3(e); + var table = hit.getTargetDataTable(); + if (!textNode || !table) return false; + activeId = e.id; + activeRecord = getLabelRecordById(activeId); + downEvt = e; + gui.dispatchEvent('label_dragstart', {FID: activeId}); + }); + + hit.on('drag', function(e) { + if (!active(e)) return; + if (e.id != activeId) { + error("Mismatched hit ids:", e.id, activeId); + } + var scale = ext.getSymbolScale() || 1; + var textNode; + applyDelta(activeRecord, 'dx', e.dx / scale); + applyDelta(activeRecord, 'dy', e.dy / scale); + textNode = getTextTarget3(e); + if (!isMultilineLabel(textNode)) { + // update anchor position of single-line labels based on label position + // relative to anchor point, for better placement when eventual display font is + // different from mapshaper's font. + autoUpdateTextAnchor(textNode, activeRecord, getDisplayCoordsById(activeId, hit.getHitTarget().layer, ext)); + } + // updateSymbol(targetTextNode, activeRecord); + updateSymbol2(textNode, activeRecord, activeId); + }); + + hit.on('dragend', function(e) { + if (!active(e)) return; + gui.dispatchEvent('label_dragend', {FID: e.id}); + activeId = -1; + activeRecord = null; + downEvt = null; + }); + + function getDisplayCoordsById(id, layer, ext) { + var coords = getPointCoordsById(id, layer); + return ext.translateCoords(coords[0], coords[1]); + } + + function getPointCoordsById(id, layer) { + var coords = layer && layer.geometry_type == 'point' && layer.shapes[id]; + if (!coords || coords.length != 1) { + return null; + } + return coords[0]; + } + + function getTextTarget3(e) { + if (e.id > -1 === false || !e.container) return null; + return getSymbolNodeById(e.id, e.container); + } + + function getSymbolNodeById(id, parent) { + // TODO: optimize selector + var sel = '[data-id="' + id + '"]'; + return parent.querySelector(sel); + } + + function getTextTarget2(e) { + var el = e && e.targetSymbol || null; + if (el && el.tagName == 'tspan') { + el = el.parentNode; + } + return el && el.tagName == 'text' ? el : null; + } + + function getTextTarget(e) { + var el = e.target; + if (el.tagName == 'tspan') { + el = el.parentNode; + } + return el.tagName == 'text' ? el : null; + } + + function getLabelRecordById(id) { + var table = hit.getTargetDataTable(); + if (id >= 0 === false || !table) return null; + // add dx and dy properties, if not available + if (!table.fieldExists('dx')) { + table.addField('dx', 0); + } + if (!table.fieldExists('dy')) { + table.addField('dy', 0); + } + if (!table.fieldExists('text-anchor')) { + table.addField('text-anchor', ''); + } + return table.getRecordAt(id); + } + + // update symbol by setting attributes + function updateSymbol(node, d) { + var a = d['text-anchor']; + if (a) node.setAttribute('text-anchor', a); + setMultilineAttribute(node, 'dx', d.dx || 0); + node.setAttribute('y', d.dy || 0); + } + + // update symbol by re-rendering it + function updateSymbol2(node, d, id) { + var o = internal.svg.importStyledLabel(d); // TODO: symbol support + var activeLayer = hit.getHitTarget().layer; + var xy = activeLayer.shapes[id][0]; + var g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + var node2; + o.properties.transform = getSvgSymbolTransform(xy, ext); + o.properties['data-id'] = id; + // o.properties['class'] = 'selected'; + g.innerHTML = internal.svg.stringify(o); + node2 = g.firstChild; + node.parentNode.replaceChild(node2, node); + gui.dispatchEvent('popup-needs-refresh'); + return node2; + } +} diff --git a/src/gui/gui-edit-modes.js b/src/gui/gui-edit-modes.js new file mode 100644 index 000000000..bc86068ec --- /dev/null +++ b/src/gui/gui-edit-modes.js @@ -0,0 +1,21 @@ +import { initLabelDragging } from './gui-edit-labels'; +import { initPointDragging } from './gui-edit-points'; +import { initVertexDragging } from './gui-edit-vertices'; + +export function initInteractiveEditing(gui, ext, hit) { + initLabelDragging(gui, ext, hit); + initPointDragging(gui, ext, hit); + initVertexDragging(gui, ext, hit); + + gui.on('interaction_mode_change', function(e) { + gui.undo.clear(); // TODO: put this elsewhere? + }); + + // function isClickEvent(up, down) { + // var elapsed = Math.abs(down.timeStamp - up.timeStamp); + // var dx = up.screenX - down.screenX; + // var dy = up.screenY - down.screenY; + // var dist = Math.sqrt(dx * dx + dy * dy); + // return dist <= 4 && elapsed < 300; + // } +} diff --git a/src/gui/gui-edit-points.js b/src/gui/gui-edit-points.js new file mode 100644 index 000000000..ae0510fec --- /dev/null +++ b/src/gui/gui-edit-points.js @@ -0,0 +1,44 @@ +import { error, internal } from './gui-core'; + +export function initPointDragging(gui, ext, hit) { + + function active(e) { + return e.id > -1 && gui.interaction.getMode() == 'location'; + } + + hit.on('dragstart', function(e) { + if (!active(e)) return; + gui.dispatchEvent('symbol_dragstart', {FID: e.id}); + }); + + hit.on('drag', function(e) { + if (!active(e)) return; + var lyr = hit.getHitTarget().layer; + var p = getPointCoordsById(e.id, lyr); + if (!p) return; + var diff = translateDeltaDisplayCoords(e.dx, e.dy, ext); + p[0] += diff[0]; + p[1] += diff[1]; + gui.dispatchEvent('map-needs-refresh'); + gui.dispatchEvent('symbol_drag', {FID: e.id}); + }); + + hit.on('dragend', function(e) { + if (!active(e)) return; + gui.dispatchEvent('symbol_dragend', {FID: e.id}); + }); + + function translateDeltaDisplayCoords(dx, dy, ext) { + var a = ext.translatePixelCoords(0, 0); + var b = ext.translatePixelCoords(dx, dy); + return [b[0] - a[0], b[1] - a[1]]; + } + + function getPointCoordsById(id, layer) { + var coords = layer && layer.geometry_type == 'point' && layer.shapes[id]; + if (!coords || coords.length != 1) { + return null; + } + return coords[0]; + } +} diff --git a/src/gui/gui-edit-vertices.js b/src/gui/gui-edit-vertices.js new file mode 100644 index 000000000..4de332548 --- /dev/null +++ b/src/gui/gui-edit-vertices.js @@ -0,0 +1,49 @@ +import { error, internal } from './gui-core'; + +export function initVertexDragging(gui, ext, hit) { + var activeId = -1; + var activeVertexIds = null; + + function active(e) { + return e.id > -1 && gui.interaction.getMode() == 'vertices'; + } + + function fire(type) { + gui.dispatchEvent(type, { + FID: activeId, + vertex_ids: activeVertexIds + }); + } + + hit.on('dragstart', function(e) { + if (!active(e)) return; + var target = hit.getHitTarget(); + var shp = target.layer.shapes[e.id]; + var p = ext.translatePixelCoords(e.x, e.y); + var nearestIds = internal.findNearestVertices(p, shp, target.arcs); + activeVertexIds = nearestIds; + activeId = e.id; + fire('vertex_dragstart'); + }); + + hit.on('drag', function(e) { + if (!active(e)) return; + if (!activeVertexIds) return; // ignore error condition + var target = hit.getHitTarget(); + var p = ext.translatePixelCoords(e.x, e.y); + if (gui.keyboard.shiftIsPressed()) { + internal.snapPointToArcEndpoint(p, activeVertexIds, target.arcs); + } + internal.snapVerticesToPoint(activeVertexIds, p, target.arcs); + gui.dispatchEvent('map-needs-refresh'); + }); + + hit.on('dragend', function(e) { + if (!active(e)) return; + // kludge to get dataset to recalculate internal bounding boxes + hit.getHitTarget().arcs.transformPoints(function() {}); + fire('vertex_dragend'); + activeVertexIds = null; + activeId = -1; + }); +} diff --git a/src/gui/gui-map.js b/src/gui/gui-map.js index f5629d6e4..96b83de38 100644 --- a/src/gui/gui-map.js +++ b/src/gui/gui-map.js @@ -5,7 +5,7 @@ import { SelectionTool } from './gui-selection-tool'; import { InspectionControl2 } from './gui-inspection-control2'; import { updateLayerStackOrder, filterLayerByIds } from './gui-layer-utils'; import { mapNeedsReset } from './gui-map-utils'; -import { InteractiveEditor } from './gui-symbol-dragging2'; +import { initInteractiveEditing } from './gui-edit-modes'; import * as MapStyle from './gui-map-style'; import { MapExtent } from './gui-map-extent'; import { LayerStack } from './gui-layer-stack'; @@ -35,7 +35,7 @@ export function MshpMap(gui) { _visibleLayers = [], // cached visible map layers _fullBounds = null, _intersectionLyr, _activeLyr, _overlayLyr, - _inspector, _stack, _editor, + _inspector, _stack, _dynamicCRS; if (gui.options.showMouseCoordinates) { @@ -218,7 +218,9 @@ export function MshpMap(gui) { }); } - _editor = new InteractiveEditor(gui, _ext, _hit); + if (gui.interaction) { + initInteractiveEditing(gui, _ext, _hit); + } _ext.on('change', function(e) { if (e.reset) return; // don't need to redraw map here if extent has been reset diff --git a/src/gui/gui-symbol-dragging2.js b/src/gui/gui-symbol-dragging2.js deleted file mode 100644 index f693031eb..000000000 --- a/src/gui/gui-symbol-dragging2.js +++ /dev/null @@ -1,319 +0,0 @@ -import { getSvgSymbolTransform } from './gui-svg-symbols'; -import { isMultilineLabel, toggleTextAlign, setMultilineAttribute, autoUpdateTextAnchor, applyDelta } from './gui-svg-labels'; -import { error, internal } from './gui-core'; -import { EventDispatcher } from './gui-events'; - -var snapVerticesToPoint = internal.snapVerticesToPoint; - -function getDisplayCoordsById(id, layer, ext) { - var coords = getPointCoordsById(id, layer); - return ext.translateCoords(coords[0], coords[1]); -} - -function getPointCoordsById(id, layer) { - var coords = layer && layer.geometry_type == 'point' && layer.shapes[id]; - if (!coords || coords.length != 1) { - return null; - } - return coords[0]; -} - -function translateDeltaDisplayCoords(dx, dy, ext) { - var a = ext.translatePixelCoords(0, 0); - var b = ext.translatePixelCoords(dx, dy); - return [b[0] - a[0], b[1] - a[1]]; -} - - -export function InteractiveEditor(gui, ext, hit) { - // var targetTextNode; // text node currently being dragged - var dragging = false; - var activeRecord; - var activeId = -1; - var self = new EventDispatcher(); - var activeVertexIds = null; // for vertex dragging - - initDragging(); - - return self; - - function labelEditingEnabled() { - return gui.interaction && gui.interaction.getMode() == 'labels' ? true : false; - } - - function locationEditingEnabled() { - return gui.interaction && gui.interaction.getMode() == 'location' ? true : false; - } - - function vertexEditingEnabled() { - return gui.interaction && gui.interaction.getMode() == 'vertices' ? true : false; - } - - // update symbol by setting attributes - function updateSymbol(node, d) { - var a = d['text-anchor']; - if (a) node.setAttribute('text-anchor', a); - setMultilineAttribute(node, 'dx', d.dx || 0); - node.setAttribute('y', d.dy || 0); - } - - // update symbol by re-rendering it - function updateSymbol2(node, d, id) { - var o = internal.svg.importStyledLabel(d); // TODO: symbol support - var activeLayer = hit.getHitTarget().layer; - var xy = activeLayer.shapes[id][0]; - var g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - var node2; - o.properties.transform = getSvgSymbolTransform(xy, ext); - o.properties['data-id'] = id; - // o.properties['class'] = 'selected'; - g.innerHTML = internal.svg.stringify(o); - node2 = g.firstChild; - node.parentNode.replaceChild(node2, node); - gui.dispatchEvent('popup-needs-refresh'); - return node2; - } - - function initDragging() { - var downEvt; - var eventPriority = 1; - - // inspector and label editing aren't fully synced - stop editing if inspector opens - // gui.on('inspector_on', function() { - // stopEditing(); - // }); - - gui.on('interaction_mode_change', function(e) { - if (e.mode != 'labels') { - stopDragging(); - } - gui.undo.clear(); // TODO: put this elsewhere? - }); - - // down event on svg - // a: off text - // -> stop editing - // b: on text - // 1: not editing -> nop - // 2: on selected text -> start dragging - // 3: on other text -> stop dragging, select new text - - hit.on('dragstart', function(e) { - if (e.id >= 0 === false) return; - if (labelEditingEnabled() && onLabelDragStart(e)) { - triggerGlobalEvent('label_dragstart', e); - startDragging(); - } else if (locationEditingEnabled()) { - triggerGlobalEvent('symbol_dragstart', e); - startDragging(); - } else if (vertexEditingEnabled()) { - onVertexDragStart(e); - triggerGlobalEvent('vertex_dragstart', e); - startDragging(); - } - }); - - hit.on('drag', function(e) { - if (labelEditingEnabled()) { - onLabelDrag(e); - } else if (locationEditingEnabled()) { - onLocationDrag(e); - } else if (vertexEditingEnabled()) { - onVertexDrag(e); - } - }); - - hit.on('dragend', function(e) { - if (locationEditingEnabled()) { - triggerGlobalEvent('symbol_dragend', e); - stopDragging(); - } else if (labelEditingEnabled()) { - triggerGlobalEvent('label_dragend', e); - stopDragging(); - } else if (vertexEditingEnabled()) { - // kludge to get dataset to recalculate internal bounding boxes - hit.getHitTarget().arcs.transformPoints(function() {}); - triggerGlobalEvent('vertex_dragend', e); - stopDragging(); - } - }); - - hit.on('click', function(e) { - if (labelEditingEnabled()) { - var target = hit.getHitTarget(); - onLabelClick(e); - } - }); - - // TODO: highlight hit vertex in path edit mode - if (false) hit.on('hover', function(e) { - if (vertexEditingEnabled() && !dragging) { - onVertexHover(e); - } - }, null, 100); - - function onVertexHover(e) { - // hovering in vertex edit mode: find vertex insertion point - var target = hit.getHitTarget(); - var shp = target.layer.shapes[e.id]; - var p = ext.translatePixelCoords(e.x, e.y); - var o = internal.findInsertionPoint(p, shp, target.arcs, ext.getPixelSize()); - } - - - function getVertexEventData(e) { - return { - FID: activeId, - vertexIds: activeVertexIds - }; - } - - function onLocationDrag(e) { - var lyr = hit.getHitTarget().layer; - var p = getPointCoordsById(e.id, lyr); - if (!p) return; - var diff = translateDeltaDisplayCoords(e.dx, e.dy, ext); - p[0] += diff[0]; - p[1] += diff[1]; - triggerRedraw(); - triggerGlobalEvent('symbol_drag', e); - } - - function onVertexDragStart(e) { - var target = hit.getHitTarget(); - var shp = target.layer.shapes[e.id]; - var p = ext.translatePixelCoords(e.x, e.y); - activeVertexIds = internal.findNearestVertices(p, shp, target.arcs); - activeId = e.id; - } - - function onVertexDrag(e) { - var target = hit.getHitTarget(); - if (!activeVertexIds) return; // ignore error condition - var p = ext.translatePixelCoords(e.x, e.y); - if (gui.keyboard.shiftIsPressed()) { - internal.snapPointToArcEndpoint(p, activeVertexIds, target.arcs); - } - snapVerticesToPoint(activeVertexIds, p, target.arcs); - triggerRedraw(); - } - - function onLabelClick(e) { - var textNode = getTextTarget3(e); - var rec = getLabelRecordById(e.id); - if (textNode && rec && isMultilineLabel(textNode)) { - toggleTextAlign(textNode, rec); - updateSymbol2(textNode, rec, e.id); - // e.stopPropagation(); // prevent pin/unpin on popup - } - } - - function triggerRedraw() { - gui.dispatchEvent('map-needs-refresh'); - } - - function triggerGlobalEvent(type, e) { - if (e.id >= 0 === false) return; - var o = { - FID: e.id, - layer_name: hit.getHitTarget().layer.name, - vertex_ids: activeVertexIds - }; - // fire event to signal external editor that symbol coords have changed - gui.dispatchEvent(type, o); - } - - function getLabelRecordById(id) { - var table = hit.getTargetDataTable(); - if (id >= 0 === false || !table) return null; - // add dx and dy properties, if not available - if (!table.fieldExists('dx')) { - table.addField('dx', 0); - } - if (!table.fieldExists('dy')) { - table.addField('dy', 0); - } - if (!table.fieldExists('text-anchor')) { - table.addField('text-anchor', ''); - } - return table.getRecordAt(id); - } - - function onLabelDragStart(e) { - var textNode = getTextTarget3(e); - var table = hit.getTargetDataTable(); - if (!textNode || !table) return false; - activeId = e.id; - activeRecord = getLabelRecordById(activeId); - downEvt = e; - return true; - } - - function onLabelDrag(e) { - var scale = ext.getSymbolScale() || 1; - var textNode; - if (!dragging) return; - if (e.id != activeId) { - error("Mismatched hit ids:", e.id, activeId); - } - applyDelta(activeRecord, 'dx', e.dx / scale); - applyDelta(activeRecord, 'dy', e.dy / scale); - textNode = getTextTarget3(e); - if (!isMultilineLabel(textNode)) { - // update anchor position of single-line labels based on label position - // relative to anchor point, for better placement when eventual display font is - // different from mapshaper's font. - autoUpdateTextAnchor(textNode, activeRecord, getDisplayCoordsById(activeId, hit.getHitTarget().layer, ext)); - } - // updateSymbol(targetTextNode, activeRecord); - updateSymbol2(textNode, activeRecord, activeId); - } - - function getSymbolNodeById(id, parent) { - // TODO: optimize selector - var sel = '[data-id="' + id + '"]'; - return parent.querySelector(sel); - } - - function getTextTarget3(e) { - if (e.id > -1 === false || !e.container) return null; - return getSymbolNodeById(e.id, e.container); - } - - function getTextTarget2(e) { - var el = e && e.targetSymbol || null; - if (el && el.tagName == 'tspan') { - el = el.parentNode; - } - return el && el.tagName == 'text' ? el : null; - } - - function getTextTarget(e) { - var el = e.target; - if (el.tagName == 'tspan') { - el = el.parentNode; - } - return el.tagName == 'text' ? el : null; - } - } - - function startDragging() { - dragging = true; - } - - function stopDragging() { - dragging = false; - activeId = -1; - activeRecord = null; - activeVertexIds = null; - } - - function isClickEvent(up, down) { - var elapsed = Math.abs(down.timeStamp - up.timeStamp); - var dx = up.screenX - down.screenX; - var dy = up.screenY - down.screenY; - var dist = Math.sqrt(dx * dx + dy * dy); - return dist <= 4 && elapsed < 300; - } - -} From 4b3cb78f83266540e03d880c7ec00c11795bf29f Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 13 Feb 2022 11:06:09 -0500 Subject: [PATCH 169/891] Set distance threshold for vertex dragging --- src/gui/gui-edit-vertices.js | 47 +++++++++++++++++++++++++----------- src/paths/mapshaper-arcs.js | 4 +++ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/gui/gui-edit-vertices.js b/src/gui/gui-edit-vertices.js index 4de332548..7cc3d7105 100644 --- a/src/gui/gui-edit-vertices.js +++ b/src/gui/gui-edit-vertices.js @@ -1,8 +1,9 @@ -import { error, internal } from './gui-core'; +import { error, internal, geom } from './gui-core'; export function initVertexDragging(gui, ext, hit) { - var activeId = -1; - var activeVertexIds = null; + var activeShapeId = -1; + var draggedVertexIds = null; + var selectedVertexId = -1; function active(e) { return e.id > -1 && gui.interaction.getMode() == 'vertices'; @@ -10,8 +11,8 @@ export function initVertexDragging(gui, ext, hit) { function fire(type) { gui.dispatchEvent(type, { - FID: activeId, - vertex_ids: activeVertexIds + FID: activeShapeId, + vertex_ids: draggedVertexIds }); } @@ -21,29 +22,47 @@ export function initVertexDragging(gui, ext, hit) { var shp = target.layer.shapes[e.id]; var p = ext.translatePixelCoords(e.x, e.y); var nearestIds = internal.findNearestVertices(p, shp, target.arcs); - activeVertexIds = nearestIds; - activeId = e.id; + var p2 = target.arcs.getVertex2(nearestIds[0]); + var dist = geom.distance2D(p[0], p[1], p2[0], p2[1]); + var pixelDist = dist / ext.getPixelSize(); + if (pixelDist > 5) { + draggedVertexIds = null; + return; + } + draggedVertexIds = nearestIds; + activeShapeId = e.id; fire('vertex_dragstart'); }); hit.on('drag', function(e) { - if (!active(e)) return; - if (!activeVertexIds) return; // ignore error condition + if (!active(e) || !draggedVertexIds) return; var target = hit.getHitTarget(); var p = ext.translatePixelCoords(e.x, e.y); if (gui.keyboard.shiftIsPressed()) { - internal.snapPointToArcEndpoint(p, activeVertexIds, target.arcs); + internal.snapPointToArcEndpoint(p, draggedVertexIds, target.arcs); } - internal.snapVerticesToPoint(activeVertexIds, p, target.arcs); + internal.snapVerticesToPoint(draggedVertexIds, p, target.arcs); gui.dispatchEvent('map-needs-refresh'); }); hit.on('dragend', function(e) { - if (!active(e)) return; + if (!active(e) || !draggedVertexIds) return; // kludge to get dataset to recalculate internal bounding boxes hit.getHitTarget().arcs.transformPoints(function() {}); fire('vertex_dragend'); - activeVertexIds = null; - activeId = -1; + draggedVertexIds = null; + activeShapeId = -1; }); + + // highlight hit vertex in path edit mode + if (false) hit.on('hover', function(e) { + if (gui.interaction.getMode() != 'vertices' || activeShapeId >= 0) return; + // hovering in vertex edit mode: find vertex insertion point + var target = hit.getHitTarget(); + var shp = target.layer.shapes[e.id]; + var p = ext.translatePixelCoords(e.x, e.y); + var o = internal.findInsertionPoint(p, shp, target.arcs, ext.getPixelSize()); + }, null, 100); + } + diff --git a/src/paths/mapshaper-arcs.js b/src/paths/mapshaper-arcs.js index fc92a3af5..8bf90ac4b 100644 --- a/src/paths/mapshaper-arcs.js +++ b/src/paths/mapshaper-arcs.js @@ -381,6 +381,10 @@ export function ArcCollection() { }; }; + this.getVertex2 = function(i) { + return [_xx[i], _yy[i]]; + }; + // @nth: index of vertex. ~(idx) starts from the opposite endpoint this.indexOfVertex = function(arcId, nth) { var absId = arcId < 0 ? ~arcId : arcId, From 674439934a9ea487df9ed1e7088ea46f22556cdd Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 3 Mar 2022 17:03:02 -0500 Subject: [PATCH 170/891] Show vertices in vertex editing mode --- src/geom/mapshaper-path-geom.js | 24 +++++++++--- src/gui/gui-canvas.js | 5 +++ src/gui/gui-edit-vertices.js | 52 ++++++++++++++++++++----- src/gui/gui-hit-test.js | 10 ++--- src/gui/gui-interactive-selection.js | 25 ++++++++++-- src/gui/gui-map-style.js | 10 ++++- src/gui/gui-map.js | 4 -- src/gui/gui-shape-hit.js | 46 +++++++++++++++++----- src/paths/mapshaper-coordinate-utils.js | 2 +- 9 files changed, 137 insertions(+), 41 deletions(-) diff --git a/src/geom/mapshaper-path-geom.js b/src/geom/mapshaper-path-geom.js index 729dca14f..53c7f6155 100644 --- a/src/geom/mapshaper-path-geom.js +++ b/src/geom/mapshaper-path-geom.js @@ -18,6 +18,7 @@ export function getPointToPathDistance(px, py, ids, arcs) { export function getPointToPathInfo(px, py, ids, arcs) { var iter = arcs.getShapeIter(ids); var pPathSq = Infinity; + var arcId; var ax, ay, bx, by, axmin, aymin, bxmin, bymin, pabSq; if (iter.hasNext()) { ax = axmin = bxmin = iter.x; @@ -29,6 +30,7 @@ export function getPointToPathInfo(px, py, ids, arcs) { pabSq = pointSegDistSq2(px, py, ax, ay, bx, by); if (pabSq < pPathSq) { pPathSq = pabSq; + arcId = iter._ids[iter._i]; // kludge axmin = ax; aymin = ay; bxmin = bx; @@ -40,7 +42,8 @@ export function getPointToPathInfo(px, py, ids, arcs) { if (pPathSq == Infinity) return {distance: Infinity}; return { segment: [[axmin, aymin], [bxmin, bymin]], - distance: Math.sqrt(pPathSq) + distance: Math.sqrt(pPathSq), + arcId: arcId }; } @@ -48,11 +51,20 @@ export function getPointToPathInfo(px, py, ids, arcs) { // Return unsigned distance of a point to the nearest point on a polygon or polyline path // export function getPointToShapeDistance(x, y, shp, arcs) { - var minDist = (shp || []).reduce(function(minDist, ids) { - var pathDist = getPointToPathDistance(x, y, ids, arcs); - return Math.min(minDist, pathDist); - }, Infinity); - return minDist; + var info = getPointToShapeInfo(x, y, shp, arcs); + return info ? info.distance : Infinity; +} + +export function getPointToShapeInfo(x, y, shp, arcs) { + return (shp || []).reduce(function(memo, ids) { + var pathInfo = getPointToPathInfo(x, y, ids, arcs); + if (!memo || pathInfo.distance < memo.distance) return pathInfo; + return memo; + }, null) || { + distance: Infinity, + arcId: -1, + segment: null + }; } // @ids array of arc ids diff --git a/src/gui/gui-canvas.js b/src/gui/gui-canvas.js index 30bc67280..d4958c633 100644 --- a/src/gui/gui-canvas.js +++ b/src/gui/gui-canvas.js @@ -150,6 +150,7 @@ export function DisplayCanvas() { var t = getScaledTransform(_ext); var radius = (style.strokeWidth > 2 ? style.strokeWidth * 0.9 : 2) * GUI.getPixelRatio() * getScaledLineScale(_ext); var color = style.strokeColor || 'black'; + var radius2 = radius * 2; _ctx.beginPath(); _ctx.fillStyle = color; for (var i=0; i -1 && gui.interaction.getMode() == 'vertices'; @@ -16,8 +16,17 @@ export function initVertexDragging(gui, ext, hit) { }); } - hit.on('dragstart', function(e) { - if (!active(e)) return; + function setHoverVertex(id) { + var target = hit.getHitTarget(); + hit.setHoverVertex(target.arcs.getVertex2(id)); + } + + function clearHoverVertex() { + hit.clearVertexOverlay(); + // gui.state.vertex_overlay = null; + } + + function findDraggableVertices(e) { var target = hit.getHitTarget(); var shp = target.layer.shapes[e.id]; var p = ext.translatePixelCoords(e.x, e.y); @@ -27,9 +36,16 @@ export function initVertexDragging(gui, ext, hit) { var pixelDist = dist / ext.getPixelSize(); if (pixelDist > 5) { draggedVertexIds = null; - return; + return null; } - draggedVertexIds = nearestIds; + return nearestIds; + } + + hit.on('dragstart', function(e) { + if (!active(e)) return; + draggedVertexIds = findDraggableVertices(e); + if (!draggedVertexIds) return; + setHoverVertex(draggedVertexIds[0]); activeShapeId = e.id; fire('vertex_dragstart'); }); @@ -42,26 +58,44 @@ export function initVertexDragging(gui, ext, hit) { internal.snapPointToArcEndpoint(p, draggedVertexIds, target.arcs); } internal.snapVerticesToPoint(draggedVertexIds, p, target.arcs); - gui.dispatchEvent('map-needs-refresh'); + setHoverVertex(draggedVertexIds[0]); + // redrawing the whole map updates the data layer as well as the overlay layer + // gui.dispatchEvent('map-needs-refresh'); }); hit.on('dragend', function(e) { if (!active(e) || !draggedVertexIds) return; // kludge to get dataset to recalculate internal bounding boxes hit.getHitTarget().arcs.transformPoints(function() {}); + clearHoverVertex(); fire('vertex_dragend'); draggedVertexIds = null; activeShapeId = -1; + // redraw data layer + gui.dispatchEvent('map-needs-refresh'); + }); + + // select clicked vertices + hit.on('click', function(e) { + if (!active(e)) return; + var vertices = findDraggableVertices(e); // same selection criteria as for dragging + // TODO }); // highlight hit vertex in path edit mode - if (false) hit.on('hover', function(e) { - if (gui.interaction.getMode() != 'vertices' || activeShapeId >= 0) return; - // hovering in vertex edit mode: find vertex insertion point + hit.on('hover', function(e) { + if (!active(e) || draggedVertexIds) return; // no hover effect while dragging + var vertexIds = findDraggableVertices(e); + if (vertexIds) { + setHoverVertex(vertexIds[0]); + return; + } var target = hit.getHitTarget(); var shp = target.layer.shapes[e.id]; var p = ext.translatePixelCoords(e.x, e.y); var o = internal.findInsertionPoint(p, shp, target.arcs, ext.getPixelSize()); + console.log('*', o, p); + clearHoverVertex(); }, null, 100); } diff --git a/src/gui/gui-hit-test.js b/src/gui/gui-hit-test.js index 78613dca4..b8d792300 100644 --- a/src/gui/gui-hit-test.js +++ b/src/gui/gui-hit-test.js @@ -2,20 +2,18 @@ import { getShapeHitTest } from './gui-shape-hit'; import { getSvgHitTest } from './gui-svg-hit'; import { internal, utils } from './gui-core'; -export function getPointerHitTest(mapLayer, ext) { +export function getPointerHitTest(mapLayer, ext, interactionMode) { var shapeTest, svgTest, targetLayer; if (!mapLayer || !internal.layerHasGeometry(mapLayer.layer)) { - return null; + return function() {return {ids: []};}; } - shapeTest = getShapeHitTest(mapLayer, ext); + shapeTest = getShapeHitTest(mapLayer, ext, interactionMode); svgTest = getSvgHitTest(mapLayer); // e: pointer event return function(e) { var p = ext.translatePixelCoords(e.x, e.y); - var data = { - ids: shapeTest(p[0], p[1]) || [] - }; + var data = shapeTest(p[0], p[1]) || {ids:[]}; var svgData = svgTest(e); // null or a data object if (svgData) { // mouse is over an SVG symbol utils.extend(data, svgData); diff --git a/src/gui/gui-interactive-selection.js b/src/gui/gui-interactive-selection.js index 5f648bcff..ab84a7db6 100644 --- a/src/gui/gui-interactive-selection.js +++ b/src/gui/gui-interactive-selection.js @@ -52,22 +52,25 @@ export function InteractiveSelection(gui, ext, mouse) { }, !!'capture'); // preempt the layer control's arrow key handler self.setLayer = function(mapLayer) { - hitTest = getPointerHitTest(mapLayer, ext); - if (!hitTest) { - hitTest = function() {return {ids: []};}; - } targetLayer = mapLayer; + updateHitTest(); }; + function updateHitTest() { + hitTest = getPointerHitTest(targetLayer, ext, interactionMode); + } + function turnOn(mode) { interactionMode = mode; active = true; + updateHitTest(); } function turnOff() { if (active) { updateSelectionState(null); // no hit data, no event active = false; + hitTest = null; } } @@ -111,6 +114,20 @@ export function InteractiveSelection(gui, ext, mouse) { } }; + self.setHoverVertex = function(p) { + var p2 = storedData.hit_coordinates; + if (!active || !p) return; + if (p2 && p2[0] == p[0] && p2[1] == p[1]) return; + storedData.hit_coordinates = p; + triggerHitEvent('change'); + }; + + self.clearVertexOverlay = function() { + if (!storedData.hit_coordinates) return; + delete storedData.hit_coordinates; + triggerHitEvent('change'); + }; + self.clearSelection = function() { updateSelectionState(null); }; diff --git a/src/gui/gui-map-style.js b/src/gui/gui-map-style.js index 27eb2d3ff..f83a6ea39 100644 --- a/src/gui/gui-map-style.js +++ b/src/gui/gui-map-style.js @@ -168,8 +168,14 @@ export function getOverlayStyle(lyr, o) { var geomType = lyr.geometry_type; var topId = o.id; // pinned id (if pinned) or hover id var topIdx = -1; - var styler = function(o, i) { - utils.extend(o, i === topIdx ? topStyle: baseStyle); + var styler = function(style, i) { + utils.extend(style, i === topIdx ? topStyle: baseStyle); + // kludge to show vertices when editing path shapes + if (o.mode == 'vertices') { + style.vertices = true; + style.vertex_overlay = o.hit_coordinates || null; + style.fillColor = null; + } }; var baseStyle = getDefaultStyle(lyr, selectionStyles[geomType]); var topStyle; diff --git a/src/gui/gui-map.js b/src/gui/gui-map.js index 96b83de38..57d3d1485 100644 --- a/src/gui/gui-map.js +++ b/src/gui/gui-map.js @@ -240,10 +240,6 @@ export function MshpMap(gui) { function updateOverlayLayer(e) { var style = MapStyle.getOverlayStyle(_activeLyr.layer, e); if (style) { - // kludge to show vertices when editing path shapes - if (gui.state.interaction_mode == 'vertices') { - style.vertices = true; - } _overlayLyr = utils.defaults({ layer: filterLayerByIds(_activeLyr.layer, style.ids), style: style diff --git a/src/gui/gui-shape-hit.js b/src/gui/gui-shape-hit.js index d821f9e61..0050622a8 100644 --- a/src/gui/gui-shape-hit.js +++ b/src/gui/gui-shape-hit.js @@ -1,12 +1,15 @@ import { utils, geom, internal, error } from './gui-core'; +import { absArcId } from '../paths/mapshaper-arc-utils'; -export function getShapeHitTest(displayLayer, ext) { +export function getShapeHitTest(displayLayer, ext, interactionMode) { var geoType = displayLayer.layer.geometry_type; var test; if (geoType == 'point' && displayLayer.style.type == 'styled') { test = getGraduatedCircleTest(getRadiusFunction(displayLayer.style)); } else if (geoType == 'point') { test = pointTest; + } else if (interactionMode == 'vertices') { + test = vertexTest; } else if (geoType == 'polyline') { test = polylineTest; } else if (geoType == 'polygon') { @@ -32,14 +35,14 @@ export function getShapeHitTest(displayLayer, ext) { } function polygonTest(x, y) { - var maxDist = getZoomAdjustedHitBuffer(5, 1), + var maxDist = getZoomAdjustedHitBuffer(10, 1), cands = findHitCandidates(x, y, maxDist), hits = [], cand, hitId; for (var i=0; i 0 && hits.length === 0) { @@ -47,7 +50,9 @@ export function getShapeHitTest(displayLayer, ext) { sortByDistance(x, y, cands, displayLayer.arcs); hits = pickNearestCandidates(cands, 0, maxDist); } - return hits; + return { + ids: utils.pluck(hits, 'id') + }; } function pickNearestCandidates(sorted, bufDist, maxDist) { @@ -62,22 +67,41 @@ export function getShapeHitTest(displayLayer, ext) { } else if (cand.dist - minDist > bufDist) { break; } - hits.push(cand.id); + hits.push(cand); } return hits; } + function vertexTest(x, y) { + var maxDist = getZoomAdjustedHitBuffer(15, 2), + bufDist = getZoomAdjustedHitBuffer(0.05), // tiny threshold for hitting almost-identical lines + cands = findHitCandidates(x, y, maxDist); + sortByDistance(x, y, cands, displayLayer.arcs); + cands = pickNearestCandidates(cands, bufDist, maxDist); + var arcs = cands.map(function(cand) { return absArcId(cand.info.arcId); }); + return { + arcs: utils.uniq(arcs), + ids: utils.pluck(cands, 'id') + }; + } + function polylineTest(x, y) { var maxDist = getZoomAdjustedHitBuffer(15, 2), bufDist = getZoomAdjustedHitBuffer(0.05), // tiny threshold for hitting almost-identical lines cands = findHitCandidates(x, y, maxDist); sortByDistance(x, y, cands, displayLayer.arcs); - return pickNearestCandidates(cands, bufDist, maxDist); + cands = pickNearestCandidates(cands, bufDist, maxDist); + return { + ids: utils.pluck(cands, 'id') + }; } function sortByDistance(x, y, cands, arcs) { + var cand; for (var i=0; i Date: Thu, 3 Mar 2022 23:36:29 -0500 Subject: [PATCH 171/891] Bug fixes for -classify command --- .../mapshaper-sequential-classifier.js | 3 +- src/color/color-schemes.js | 15 +++- src/commands/mapshaper-classify.js | 88 +++++++++++-------- 3 files changed, 66 insertions(+), 40 deletions(-) diff --git a/src/classification/mapshaper-sequential-classifier.js b/src/classification/mapshaper-sequential-classifier.js index b77d182d9..447b7a9a1 100644 --- a/src/classification/mapshaper-sequential-classifier.js +++ b/src/classification/mapshaper-sequential-classifier.js @@ -4,7 +4,7 @@ import { getRoundingFunction } from '../geom/mapshaper-rounding'; import { getNiceBreaks } from '../classification/mapshaper-nice-breaks'; import { getOutputFunction } from '../classification/mapshaper-classification'; import { makeSimpleKey, makeDatavizKey, makeGradientKey } from '../furniture/mapshaper-key'; -export function getSequentialClassifier(classValues, nullValue, dataValues, opts) { +export function getSequentialClassifier(classValues, nullValue, dataValues, method, opts) { var numValues = classValues.length; var numBuckets = opts.continuous ? numValues - 1 : numValues; @@ -12,7 +12,6 @@ export function getSequentialClassifier(classValues, nullValue, dataValues, opts // discreetly classed values var numBreaks = numBuckets - 1; var round = opts.precision ? getRoundingFunction(opts.precision) : null; - var method = opts.method || 'quantile'; var breaks, classifier, dataToClass, classToValue; if (round) { diff --git a/src/color/color-schemes.js b/src/color/color-schemes.js index 44e9a780d..50ae3bf1d 100644 --- a/src/color/color-schemes.js +++ b/src/color/color-schemes.js @@ -82,11 +82,15 @@ export function printColorSchemeNames() { } export function getCategoricalColorScheme(name, n) { + var colors; initSchemes(); - if (index.categorical.includes(name) === false) { - stop(name, 'is not a categorical color scheme'); + if (!isColorSchemeName(name)) { + stop('Unknown color scheme name:', name); + } else if (isCategoricalColorScheme(name)) { + colors = ramps[name] || require('d3-scale-chromatic')['scheme' + name]; + } else { + colors = getColorRamp(name, n); } - var colors = ramps[name] || require('d3-scale-chromatic')['scheme' + name]; if (n > colors.length) { stop(name, 'does not contain', n, 'colors'); } @@ -99,6 +103,11 @@ export function isColorSchemeName(name) { index.diverging.includes(name) || index.rainbow.includes(name); } +export function isCategoricalColorScheme(name) { + initSchemes(); + return index.categorical.includes(name); +} + export function getColorRamp(name, n, stops) { initSchemes(); var lib = require('d3-scale-chromatic'); diff --git a/src/commands/mapshaper-classify.js b/src/commands/mapshaper-classify.js index d42df0072..58e244d2b 100644 --- a/src/commands/mapshaper-classify.js +++ b/src/commands/mapshaper-classify.js @@ -2,7 +2,7 @@ import { stop, message } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import { requireDataField, initDataTable } from '../dataset/mapshaper-layer-utils'; import { getFieldValues } from '../datatable/mapshaper-data-utils'; -import { isColorSchemeName, getColorRamp, getCategoricalColorScheme } from '../color/color-schemes'; +import { isColorSchemeName, getColorRamp, getCategoricalColorScheme, isCategoricalColorScheme } from '../color/color-schemes'; import { parseColor } from '../color/color-utils'; import { getSequentialClassifier, @@ -28,34 +28,35 @@ cmd.classify = function(lyr, dataset, optsArg) { var opts = optsArg || {}; var records = lyr.data && lyr.data.getRecords(); var nullValue = opts.null_value || null; - var looksLikeColors = !!opts.colors || !!opts.color_scheme; + var valuesAreColors = !!opts.colors || !!opts.color_scheme; var colorScheme; - var classValues, classifyByValue, classifyById; - var numBuckets, numValues; + var values, classifyByValue, classifyById; + var numClasses, numValues; var dataField, outputField; + var method; // validate explicitly set classes if (opts.classes) { if (!utils.isInteger(opts.classes) || opts.classes > 1 === false) { stop('Invalid number of classes:', opts.classes, '(expected a value greater than 1)'); } - numBuckets = opts.classes; + numClasses = opts.classes; } // TODO: better validation of breaks values if (opts.breaks) { - numBuckets = opts.breaks.length + 1; + numClasses = opts.breaks.length + 1; } if (opts.index_field) { dataField = opts.index_field; - if (numBuckets > 0 === false) { + if (numClasses > 0 === false) { stop('The index-field= option requires the classes= option to be set'); } // You can't infer the number of classes by looking at index values; // this can cause unwanted interpolation if one or more values are // not present in the index field - // numBuckets = validateClassIndexField(records, opts.index_field); + // numClasses = validateClassIndexField(records, opts.index_field); } else if (opts.field) { dataField = opts.field; @@ -66,7 +67,19 @@ cmd.classify = function(lyr, dataset, optsArg) { opts.categories = getUniqFieldValues(records, dataField); } - if (opts.method == 'non-adjacent') { + // get classification method + // + if (opts.method) { + method = opts.method; + } else if (opts.categories) { + method = 'categorical'; + } else if (opts.index_field) { + method = 'indexed'; + } else { + method = 'quantile'; // TODO: validate data field + } + + if (method == 'non-adjacent') { if (lyr.geometry_type != 'polygon') { stop('The non-adjacent option requires a polygon layer'); } @@ -79,8 +92,8 @@ cmd.classify = function(lyr, dataset, optsArg) { requireDataField(lyr.data, dataField); } - if (numBuckets) { - numValues = opts.continuous ? numBuckets + 1 : numBuckets; + if (numClasses) { + numValues = opts.continuous ? numClasses + 1 : numClasses; } // support both deprecated color-scheme= option and colors= syntax @@ -95,39 +108,44 @@ cmd.classify = function(lyr, dataset, optsArg) { opts.colors.forEach(parseColor); // validate colors -- error if unparsable } + /// get values (usually colors) + /// if (colorScheme) { - // using a named color scheme: generate a ramp + if (method == 'non-adjacent') { + numClasses = numValues = numClasses || 5; + values = getCategoricalColorScheme(colorScheme, numValues); + + } else if (method == 'categorical') { + values = getCategoricalColorScheme(colorScheme, opts.categories.length); + numClasses = numValues = values.length; - if (opts.categories) { - classValues = getCategoricalColorScheme(colorScheme, opts.categories.length); - numBuckets = numValues = classValues.length; } else { - if (!numBuckets) { + if (!numClasses) { // stop('color-scheme= option requires classes= or breaks='); - numBuckets = 4; // use a default number of classes - numValues = opts.continuous ? numBuckets + 1 : numBuckets; + numClasses = 4; // use a default number of classes + numValues = opts.continuous ? numClasses + 1 : numClasses; } - classValues = getColorRamp(colorScheme, numValues, opts.stops); + values = getColorRamp(colorScheme, numValues, opts.stops); } } else if (opts.colors || opts.values) { - classValues = opts.values ? parseValues(opts.values) : opts.colors; + values = opts.values ? parseValues(opts.values) : opts.colors; if (!numValues) { - numValues = classValues.length; + numValues = values.length; } - if ((classValues.length != numValues || opts.stops) && numValues > 1) { + if ((values.length != numValues || opts.stops) && numValues > 1) { // TODO: handle numValues == 1 // TODO: check for non-interpolatable value types (e.g. boolean, text) - classValues = interpolateValuesToClasses(classValues, numValues, opts.stops); + values = interpolateValuesToClasses(values, numValues, opts.stops); } } else if (numValues > 1) { // no values were given: assign indexes for each class - classValues = getIndexValues(numValues); + values = getIndexValues(numValues); nullValue = -1; } - if (looksLikeColors) { + if (valuesAreColors) { nullValue = nullValue || '#eee'; } @@ -136,24 +154,24 @@ cmd.classify = function(lyr, dataset, optsArg) { } if (opts.invert) { - classValues = classValues.concat().reverse(); + values = values.concat().reverse(); } - if (looksLikeColors) { - message('Colors:', formatValuesForLogging(classValues)); + if (valuesAreColors) { + message('Colors:', formatValuesForLogging(values)); } // get a function to convert input data to class indexes // - if (opts.method == 'non-adjacent') { - classifyById = getNonAdjacentClassifier(lyr, dataset, classValues); + if (method == 'non-adjacent') { + classifyById = getNonAdjacentClassifier(lyr, dataset, values); } else if (opts.index_field) { // data is pre-classified... just read the index from a field - classifyByValue = getIndexedClassifier(classValues, nullValue, opts); - } else if (opts.categories) { - classifyByValue = getCategoricalClassifier(classValues, nullValue, opts); + classifyByValue = getIndexedClassifier(values, nullValue, opts); + } else if (method == 'categorical') { + classifyByValue = getCategoricalClassifier(values, nullValue, opts); } else { - classifyByValue = getSequentialClassifier(classValues, nullValue, getFieldValues(records, dataField), opts); + classifyByValue = getSequentialClassifier(values, nullValue, getFieldValues(records, dataField), method, opts); } if (classifyByValue) { @@ -166,7 +184,7 @@ cmd.classify = function(lyr, dataset, optsArg) { // get the name of the output field // - if (looksLikeColors) { + if (valuesAreColors) { outputField = lyr.geometry_type == 'polyline' ? 'stroke' : 'fill'; } else { outputField = 'class'; From 27feac23e215d35038f4fd45874a1901c27d5140 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 4 Mar 2022 09:02:50 -0500 Subject: [PATCH 172/891] Add support for running commands when GUI first loads data --- bin/mapshaper-gui | 5 +++-- package-lock.json | 16 ++++++++-------- package.json | 2 +- src/gui/gui-console.js | 7 +++++++ src/gui/gui-import-control.js | 1 + src/gui/gui.js | 7 +++++++ 6 files changed, 27 insertions(+), 11 deletions(-) diff --git a/bin/mapshaper-gui b/bin/mapshaper-gui index 2ad71ba9f..646ed067d 100755 --- a/bin/mapshaper-gui +++ b/bin/mapshaper-gui @@ -10,10 +10,11 @@ var defaultPort = 5555, .option('-f, --force-save', 'allow overwriting input files with output files') .option('-a, --display-all', 'turn on visibility of all layers') .option('-n, --name ', 'rename input layer or layers') + .option('-c, --commands ', 'console commands to run initially') .option('-t, --target ', 'name of layer to select initially') .helpOption('-h, --help', 'show this help message') .version(require('../package.json').version) - .parse(), + .parse(process.argv), opts = program.opts(), http = require("http"), path = require("path"), @@ -26,7 +27,6 @@ var defaultPort = 5555, dataFiles = expandShapefiles(program.args), probeCount = 0, sessionId = null; - validateFiles(dataFiles); process.on('uncaughtException', function(err) { @@ -210,6 +210,7 @@ function getManifestJS(files, opts) { if (opts.displayAll) o.display_all = true; if (opts.quickView) o.quick_view = true; if (opts.name) o.name = opts.name; + if (opts.commands) o.commands = opts.commands; return "mapshaper.manifest = " + JSON.stringify(o) + ";\n"; } diff --git a/package-lock.json b/package-lock.json index 9160e5b9e..cba8ac3f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.5.91", "license": "MPL-2.0", "dependencies": { - "commander": "^5.1.0", + "commander": "7.0.0", "cookies": "^0.8.0", "d3-color": "2.0.0", "d3-scale-chromatic": "2.0.0", @@ -778,11 +778,11 @@ } }, "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.0.0.tgz", + "integrity": "sha512-ovx/7NkTrnPuIV8sqk/GjUIIM1+iUQeqA3ye2VNpq9sVoiZsooObWlQy+OPWGI17GDaEoybuAGJm6U8yC077BA==", "engines": { - "node": ">= 6" + "node": ">= 10" } }, "node_modules/concat-map": { @@ -3950,9 +3950,9 @@ } }, "commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.0.0.tgz", + "integrity": "sha512-ovx/7NkTrnPuIV8sqk/GjUIIM1+iUQeqA3ye2VNpq9sVoiZsooObWlQy+OPWGI17GDaEoybuAGJm6U8yC077BA==" }, "concat-map": { "version": "0.0.1", diff --git a/package.json b/package.json index 40b9444d5..00d0d08b7 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "!.DS_Store" ], "dependencies": { - "commander": "^5.1.0", + "commander": "7.0.0", "cookies": "^0.8.0", "d3-color": "2.0.0", "d3-scale-chromatic": "2.0.0", diff --git a/src/gui/gui-console.js b/src/gui/gui-console.js index 42a3606a5..1bb26e707 100644 --- a/src/gui/gui-console.js +++ b/src/gui/gui-console.js @@ -28,6 +28,13 @@ export function Console(gui) { // expose this function, so other components can run commands (e.g. box tool) this.runMapshaperCommands = runMapshaperCommands; + this.runInitialCommands = function(str) { + str = str.trim(); + if (!str) return; + turnOn(); + submit(str); + }; + consoleMessage(PROMPT); gui.keyboard.on('keydown', onKeyDown); window.addEventListener('beforeunload', saveHistory); // save history if console is open on refresh diff --git a/src/gui/gui-import-control.js b/src/gui/gui-import-control.js index 3463a63b0..449d275b5 100644 --- a/src/gui/gui-import-control.js +++ b/src/gui/gui-import-control.js @@ -199,6 +199,7 @@ export function ImportControl(gui, opts) { } } model.updated({select: true}); + } function clearQueuedFiles() { diff --git a/src/gui/gui.js b/src/gui/gui.js index 016edefb6..a2070463c 100644 --- a/src/gui/gui.js +++ b/src/gui/gui.js @@ -45,6 +45,11 @@ function getImportOpts() { return opts; } +function getInitialConsoleCommands() { + var manifest = window.mapshaper.manifest || {}; + return manifest.commands || ''; +} + var startEditing = function() { var dataLoaded = false, importOpts = getImportOpts(), @@ -86,5 +91,7 @@ var startEditing = function() { gui.map.setLayerPinning(o, true); }); } + gui.console.runInitialCommands(getInitialConsoleCommands()); + }); }; From d1884ca6120dfb9a563e2b07bcf45c806669e023 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 4 Mar 2022 10:01:34 -0500 Subject: [PATCH 173/891] v0.5.92 --- CHANGELOG.md | 4 ++++ bin/mapshaper-gui | 22 ++++++++++++++-------- package.json | 2 +- src/gui/gui.js | 22 +++++++++++++--------- www/index.html | 2 +- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1671d97dc..8ab0a7e64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.92 +* Show vertices in the "drag vertices" editing mode. +* Added -c/--commands option to mapshaper-gui, for running console commands when data is first imported. + v0.5.91 * Added -print command, for printing messages to the console or terminal. Useful in conjunction with the -if/-elif/-else commands. * Added field_exists(), field_type() and field_includes() functions to -if expressions. diff --git a/bin/mapshaper-gui b/bin/mapshaper-gui index 646ed067d..c49c5ae1d 100755 --- a/bin/mapshaper-gui +++ b/bin/mapshaper-gui @@ -1,7 +1,8 @@ #!/usr/bin/env node var defaultPort = 5555, - program = require('commander') + commander = require('commander'), + program = new commander.Command() .name('mapshaper-gui') .usage('[options] [file ...]') .option('-p, --port ', 'http port of server on localhost', defaultPort) @@ -12,6 +13,8 @@ var defaultPort = 5555, .option('-n, --name ', 'rename input layer or layers') .option('-c, --commands ', 'console commands to run initially') .option('-t, --target ', 'name of layer to select initially') + .addOption(new commander.Option('-b, --blurb ', + 'replace the default blurb on the import screen').hideHelp()) .helpOption('-h, --help', 'show this help message') .version(require('../package.json').version) .parse(process.argv), @@ -204,13 +207,16 @@ function validateOutputFile(file) { } function getManifestJS(files, opts) { - var o = {files: files}; - if (opts.target) o.target = opts.target; - if (opts.directSave) o.allow_saving = true; - if (opts.displayAll) o.display_all = true; - if (opts.quickView) o.quick_view = true; - if (opts.name) o.name = opts.name; - if (opts.commands) o.commands = opts.commands; + var o = { + files: files, + target: opts.target, + allow_saving: opts.directSave, + display_all: opts.displayAll, + quick_view: opts.quickView, + name: opts.name, + commands: opts.commands, + blurb: opts.blurb + }; return "mapshaper.manifest = " + JSON.stringify(o) + ";\n"; } diff --git a/package.json b/package.json index 00d0d08b7..b5e2cf965 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.91", + "version": "0.5.92", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/gui/gui.js b/src/gui/gui.js index a2070463c..80442c4fc 100644 --- a/src/gui/gui.js +++ b/src/gui/gui.js @@ -20,14 +20,14 @@ onload(function() { startEditing(); }); -function getImportOpts() { +function getManifest() { + return window.mapshaper.manifest || {}; // kludge -- bin/mapshaper-gui sets this +} + +function getImportOpts(manifest) { var vars = GUI.getUrlVars(); var opts = {}; - var manifest = window.mapshaper.manifest || {}; // kludge -- bin/mapshaper-gui sets this - if (Array.isArray(manifest)) { - // old-style manifest: an array of filenames - opts.files = manifest; - } else if (manifest.files) { + if (manifest.files) { opts.files = manifest.files.concat(); } else { opts.files = []; @@ -46,15 +46,19 @@ function getImportOpts() { } function getInitialConsoleCommands() { - var manifest = window.mapshaper.manifest || {}; - return manifest.commands || ''; + return getManifest().commands || ''; } var startEditing = function() { var dataLoaded = false, - importOpts = getImportOpts(), + manifest = getManifest(), + importOpts = getImportOpts(manifest), gui = new GuiInstance('body'); + if (manifest.blurb) { + El('#splash-screen-blurb').text(manifest.blurb); + } + new AlertControl(gui); new RepairControl(gui); new SimplifyControl(gui); diff --git a/www/index.html b/www/index.html index edfb6b1af..f7b818aee 100644 --- a/www/index.html +++ b/www/index.html @@ -201,7 +201,7 @@

Method

-

Mapshaper is an editor for map data

+

Mapshaper is an editor for map data

From 7a594aff62db0c5d35b72ea9202498031f192038 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 4 Mar 2022 10:01:53 -0500 Subject: [PATCH 174/891] v0.5.92 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index cba8ac3f0..ae16917d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapshaper", - "version": "0.5.91", + "version": "0.5.92", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mapshaper", - "version": "0.5.91", + "version": "0.5.92", "license": "MPL-2.0", "dependencies": { "commander": "7.0.0", From 87842627c17349ecfbbe0ed04408bfc0783e0cbb Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 6 Mar 2022 22:55:10 -0500 Subject: [PATCH 175/891] [gui] select topmost layer after importing TopoJSON --- src/dataset/mapshaper-catalog.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/dataset/mapshaper-catalog.js b/src/dataset/mapshaper-catalog.js index 5f50bf87f..578445f0b 100644 --- a/src/dataset/mapshaper-catalog.js +++ b/src/dataset/mapshaper-catalog.js @@ -135,7 +135,16 @@ export function Catalog() { // should be in gui-model.js, moved here for testing this.getActiveLayer = function() { var targ = (this.getDefaultTargets() || [])[0]; - return targ ? {layer: targ.layers[0], dataset: targ.dataset} : null; + // var lyr = targ.layers[0]; + // Reasons to select the last layer of a multi-layer target: + // * This layer was imported last + // * This layer is displayed on top of other layers + // * This layer is at the top of the layers list + // * In TopoJSON input, it makes sense to think of the last object/layer + // as the topmost one -- it corresponds to the painter's algorithm and + // the way that objects are ordered in SVG. + var lyr = targ.layers[targ.layers.length - 1]; + return targ ? {layer: lyr, dataset: targ.dataset} : null; }; function layerObject(lyr, dataset) { From eace118def276a5cb9bd6efc2dd78beb517316c3 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 6 Mar 2022 22:59:32 -0500 Subject: [PATCH 176/891] Add support for adding vertices in 'edit vertices' mode --- src/gui/gui-canvas.js | 5 +- src/gui/gui-edit-vertices.js | 74 +++++++++++++++++++++++-- src/gui/gui-interaction-mode-control.js | 2 +- src/gui/gui-map-style.js | 2 +- src/gui/gui-shape-hit.js | 2 +- src/gui/gui-undo.js | 12 ++++ src/mapshaper-internal.js | 2 + src/paths/mapshaper-arc-utils.js | 54 ++++++++++++++++++ src/paths/mapshaper-arcs.js | 2 + src/paths/mapshaper-shape-iter.js | 2 + src/paths/mapshaper-vertex-utils.js | 33 +++++++---- src/utils/mapshaper-utils.js | 16 ++++-- www/manifest.js | 8 ++- 13 files changed, 187 insertions(+), 27 deletions(-) diff --git a/src/gui/gui-canvas.js b/src/gui/gui-canvas.js index d4958c633..9708d131f 100644 --- a/src/gui/gui-canvas.js +++ b/src/gui/gui-canvas.js @@ -150,7 +150,7 @@ export function DisplayCanvas() { var t = getScaledTransform(_ext); var radius = (style.strokeWidth > 2 ? style.strokeWidth * 0.9 : 2) * GUI.getPixelRatio() * getScaledLineScale(_ext); var color = style.strokeColor || 'black'; - var radius2 = radius * 2; + var radius2 = radius * 1.7; _ctx.beginPath(); _ctx.fillStyle = color; for (var i=0; i 0) { strokeWidth = style.strokeWidth; if (pixRatio > 1) { - // bump up thin lines on retina, but not to more than 1px (too slow) + // bump up thin lines on retina, but not to more than 1px + // (tests on Chrome showed much faster rendering of 1px lines) strokeWidth = strokeWidth < 1 ? 1 : strokeWidth * pixRatio; } ctx.lineCap = 'round'; diff --git a/src/gui/gui-edit-vertices.js b/src/gui/gui-edit-vertices.js index 2eeca0965..59baefb91 100644 --- a/src/gui/gui-edit-vertices.js +++ b/src/gui/gui-edit-vertices.js @@ -1,9 +1,14 @@ import { error, internal, geom } from './gui-core'; +var HOVER_THRESHOLD = 8; +var MIDPOINT_THRESHOLD = 12; + + export function initVertexDragging(gui, ext, hit) { var activeShapeId = -1; var draggedVertexIds = null; var selectedVertexIds = null; + var activeMidpoint; // {point, segment} function active(e) { return e.id > -1 && gui.interaction.getMode() == 'vertices'; @@ -34,16 +39,34 @@ export function initVertexDragging(gui, ext, hit) { var p2 = target.arcs.getVertex2(nearestIds[0]); var dist = geom.distance2D(p[0], p[1], p2[0], p2[1]); var pixelDist = dist / ext.getPixelSize(); - if (pixelDist > 5) { + if (pixelDist > HOVER_THRESHOLD) { draggedVertexIds = null; return null; } return nearestIds; } + function insertMidpoint(v) { + var target = hit.getHitTarget(); + internal.insertVertex(target.arcs, v.i, v.point); + } + hit.on('dragstart', function(e) { if (!active(e)) return; - draggedVertexIds = findDraggableVertices(e); + if (activeMidpoint) { + insertMidpoint(activeMidpoint); + draggedVertexIds = [activeMidpoint.i]; + // TODO: combine vertex insertion undo/redo actions with + // vertex_dragend undo/redo actions + gui.dispatchEvent('vertex_insert', { + FID: activeShapeId, + vertex_id: activeMidpoint.i, + coordinates: activeMidpoint.point + }); + activeMidpoint = null; + } else { + draggedVertexIds = findDraggableVertices(e); + } if (!draggedVertexIds) return; setHoverVertex(draggedVertexIds[0]); activeShapeId = e.id; @@ -84,6 +107,7 @@ export function initVertexDragging(gui, ext, hit) { // highlight hit vertex in path edit mode hit.on('hover', function(e) { + activeMidpoint = null; if (!active(e) || draggedVertexIds) return; // no hover effect while dragging var vertexIds = findDraggableVertices(e); if (vertexIds) { @@ -91,12 +115,52 @@ export function initVertexDragging(gui, ext, hit) { return; } var target = hit.getHitTarget(); + // vertex insertion doesn't work yet with simplification applied + if (!target.arcs.isFlat()) return; var shp = target.layer.shapes[e.id]; var p = ext.translatePixelCoords(e.x, e.y); - var o = internal.findInsertionPoint(p, shp, target.arcs, ext.getPixelSize()); - console.log('*', o, p); - clearHoverVertex(); + var midpoint = findNearestMidpoint(p, shp, target.arcs); + if (midpoint && midpoint.distance / ext.getPixelSize() < MIDPOINT_THRESHOLD) { + hit.setHoverVertex(midpoint.point); + activeMidpoint = midpoint; + } else { + clearHoverVertex(); + } }, null, 100); +} + +// Given a location @p (e.g. corresponding to the mouse pointer location), +// find the midpoint of two vertices on @shp suitable for inserting a new vertex, +// but only if: +// 1. point @p is closer to the midpoint than either adjacent vertex +// 2. the segment containing @p is longer than a minimum distance in pixels. +// +function findNearestMidpoint(p, shp, arcs) { + // var v1 = internal.findNearestVertex(p[0], p[1], shp, arcs); + // var v0 = internal.findAdjacentVertex(v1, shp, arcs, -1); + // var v2 = internal.findAdjacentVertex(v1, shp, arcs, 1); + var minDist = Infinity, v; + internal.forEachSegmentInShape(shp, arcs, function(i, j, xx, yy) { + var x1 = xx[i], + y1 = yy[i], + x2 = xx[j], + y2 = yy[j], + cx = (x1 + x2) / 2, + cy = (y1 + y2) / 2, + dist = geom.distance2D(cx, cy, p[0], p[1]); + if (dist < minDist) { + minDist = dist; + v = { + i: (i < j ? i : j) + 1, // insertion point + segment: [i, j], + point: [cx, cy], + distance: dist + }; + } + }); + return v || null; } + + diff --git a/src/gui/gui-interaction-mode-control.js b/src/gui/gui-interaction-mode-control.js index 40b9a66d0..91e9ed2c5 100644 --- a/src/gui/gui-interaction-mode-control.js +++ b/src/gui/gui-interaction-mode-control.js @@ -25,7 +25,7 @@ export function InteractionMode(gui) { data: 'edit attributes', labels: 'position labels', location: 'drag points', - vertices: 'drag vertices', + vertices: 'edit vertices', selection: 'select features', off: 'turn off' }; diff --git a/src/gui/gui-map-style.js b/src/gui/gui-map-style.js index f83a6ea39..c7a919504 100644 --- a/src/gui/gui-map-style.js +++ b/src/gui/gui-map-style.js @@ -22,7 +22,7 @@ var darkStroke = "#334", }, referenceStyle = { // outline style for reference layers type: 'outline', - strokeColors: [null, '#86c927'], + strokeColors: [null, '#78c110'], // upped saturation from #86c927 strokeWidth: 0.85, dotColor: "#73ba20", dotSize: 1 diff --git a/src/gui/gui-shape-hit.js b/src/gui/gui-shape-hit.js index 0050622a8..9af85a731 100644 --- a/src/gui/gui-shape-hit.js +++ b/src/gui/gui-shape-hit.js @@ -73,7 +73,7 @@ export function getShapeHitTest(displayLayer, ext, interactionMode) { } function vertexTest(x, y) { - var maxDist = getZoomAdjustedHitBuffer(15, 2), + var maxDist = getZoomAdjustedHitBuffer(20, 2), bufDist = getZoomAdjustedHitBuffer(0.05), // tiny threshold for hitting almost-identical lines cands = findHitCandidates(x, y, maxDist); sortByDistance(x, y, cands, displayLayer.arcs); diff --git a/src/gui/gui-undo.js b/src/gui/gui-undo.js index aee8c0290..0071bcf2d 100644 --- a/src/gui/gui-undo.js +++ b/src/gui/gui-undo.js @@ -84,6 +84,18 @@ export function Undo(gui) { this.addHistoryState(stashedUndo, redo); }, this); + gui.on('vertex_insert', function(e) { + var target = gui.model.getActiveLayer(); + var arcs = target.dataset.arcs; + var undo = function() { + internal.deleteVertex(arcs, e.vertex_id); + }; + var redo = function() { + internal.insertVertex(arcs, e.vertex_id, e.coordinates); + }; + this.addHistoryState(undo, redo); + }, this); + this.clear = function() { reset(); }; diff --git a/src/mapshaper-internal.js b/src/mapshaper-internal.js index 2f7f59e06..656c3f699 100644 --- a/src/mapshaper-internal.js +++ b/src/mapshaper-internal.js @@ -67,6 +67,7 @@ import * as Affine from './commands/mapshaper-affine'; import * as AnchorPoints from './points/mapshaper-anchor-points'; import * as ArcClassifier from './topology/mapshaper-arc-classifier'; import * as ArcDissolve from './paths/mapshaper-arc-dissolve'; +import * as ArcUtils from './paths/mapshaper-arc-utils'; import * as Bbox2Clipping from './clipping/mapshaper-bbox2-clipping'; import * as BinArray from './utils/mapshaper-binarray'; import * as BufferCommon from './buffer/mapshaper-buffer-common'; @@ -177,6 +178,7 @@ Object.assign(internal, AnchorPoints, ArcClassifier, ArcDissolve, + ArcUtils, Bbox2Clipping, BinArray, BufferCommon, diff --git a/src/paths/mapshaper-arc-utils.js b/src/paths/mapshaper-arc-utils.js index b99b3555a..b710c1488 100644 --- a/src/paths/mapshaper-arc-utils.js +++ b/src/paths/mapshaper-arc-utils.js @@ -1,3 +1,4 @@ +import utils from '../utils/mapshaper-utils'; export function absArcId(arcId) { return arcId >= 0 ? arcId : ~arcId; @@ -22,4 +23,57 @@ export function calcArcBounds(xx, yy, start, len) { return [xmin, ymin, xmax, ymax]; } +export function deleteVertex(arcs, i) { + var data = arcs.getVertexData(); + var nn = data.nn; + var n = data.xx.length; + // avoid re-allocating memory + var xx2 = new Float64Array(data.xx.buffer, 0, n-1); + var yy2 = new Float64Array(data.yy.buffer, 0, n-1); + var count = 0; + var found = false; + for (var j=0; j= i && !found) { // TODO: confirm this + nn[j] = nn[j] - 1; + found = true; + } + } + utils.copyElements(data.xx, 0, xx2, 0, i); + utils.copyElements(data.yy, 0, yy2, 0, i); + utils.copyElements(data.xx, i+1, xx2, i, n-i-1); + utils.copyElements(data.yy, i+1, yy2, i, n-i-1); + arcs.updateVertexData(nn, xx2, yy2, null); +} +export function insertVertex(arcs, i, p) { + // TODO: add extra bytes to the buffers, to reduce new memory allocation + var data = arcs.getVertexData(); + var nn = data.nn; + var n = data.xx.length; + var count = 0; + var found = false; + var xx2, yy2; + // avoid re-allocating memory on each insertion + if (data.xx.buffer.byteLength >= data.xx.length * 8 + 8) { + xx2 = new Float64Array(data.xx.buffer, 0, n+1); + yy2 = new Float64Array(data.yy.buffer, 0, n+1); + } else { + xx2 = new Float64Array(new ArrayBuffer((n + 20) * 8), 0, n+1); + yy2 = new Float64Array(new ArrayBuffer((n + 20) * 8), 0, n+1); + } + for (var j=0; j= i && !found) { // TODO: confirm this + nn[j] = nn[j] + 1; + found = true; + } + } + utils.copyElements(data.xx, 0, xx2, 0, i); + utils.copyElements(data.yy, 0, yy2, 0, i); + utils.copyElements(data.xx, i, xx2, i+1, n-i); + utils.copyElements(data.yy, i, yy2, i+1, n-i); + xx2[i] = p[0]; + yy2[i] = p[1]; + arcs.updateVertexData(nn, xx2, yy2, null); +} diff --git a/src/paths/mapshaper-arcs.js b/src/paths/mapshaper-arcs.js index 8bf90ac4b..6116c385d 100644 --- a/src/paths/mapshaper-arcs.js +++ b/src/paths/mapshaper-arcs.js @@ -489,6 +489,8 @@ export function ArcCollection() { } }; + this.isFlat = function() { return !_zz; }; + this.getRetainedInterval = function() { return _zlimit; }; diff --git a/src/paths/mapshaper-shape-iter.js b/src/paths/mapshaper-shape-iter.js index 650ba76da..422a536bb 100644 --- a/src/paths/mapshaper-shape-iter.js +++ b/src/paths/mapshaper-shape-iter.js @@ -122,6 +122,7 @@ export function ShapeIter(arcs) { this._n = 0; this.x = 0; this.y = 0; + this.i = -1; } ShapeIter.prototype.hasNext = function() { @@ -132,6 +133,7 @@ ShapeIter.prototype.hasNext = function() { if (arc.hasNext()) { this.x = arc.x; this.y = arc.y; + this.i = arc.i; return true; } this.nextArc(); diff --git a/src/paths/mapshaper-vertex-utils.js b/src/paths/mapshaper-vertex-utils.js index aa6d4dc27..9dc2f2bbc 100644 --- a/src/paths/mapshaper-vertex-utils.js +++ b/src/paths/mapshaper-vertex-utils.js @@ -5,16 +5,7 @@ export function findNearestVertices(p, shp, arcs) { return findVertexIds(p2.x, p2.y, arcs); } -// Given a location @p (e.g. corresponding to the mouse pointer location), -// find the midpoint of two vertices on @shp suitable for inserting a new vertex, -// but only if: -// 1. point @p is closer to the midpoint than either adjacent vertex -// 2. the segment containing @p is longer than a minimum distance in pixels. -// -export function findInsertionPoint(p, shp, arcs, pixelSize) { - var p2 = findNearestVertex(p[0], p[1], shp, arcs); -} export function snapVerticesToPoint(ids, p, arcs, final) { ids.forEach(function(idx) { @@ -95,7 +86,7 @@ export function setVertexCoords(x, y, i, arcs) { export function findNearestVertex(x, y, shp, arcs, spherical) { var calcLen = spherical ? geom.greatCircleDistance : geom.distance2D, minLen = Infinity, - minX, minY, dist, iter; + minX, minY, vId, dist, iter; for (var i=0; i i) error ("copy error"); + var same = src == dest || src.buffer && src.buffer == dest.buffer; var inc = 1, - offs = 0; + offs = 0, + k; if (rev) { + if (same) error('copy error'); inc = -1; offs = n - 1; } - for (var k=0; k i) { + for (k=n-1; k>=0; k--) { + dest[j + k] = src[i + k]; + } + } else { + for (k=0; k Date: Sun, 6 Mar 2022 23:16:33 -0500 Subject: [PATCH 177/891] v0.5.93 --- CHANGELOG.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ab0a7e64..c0bfbfd41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.93 +* Add a vertex in "edit vertices" mode by dragging the midpoint of a segment. + v0.5.92 * Show vertices in the "drag vertices" editing mode. * Added -c/--commands option to mapshaper-gui, for running console commands when data is first imported. diff --git a/package.json b/package.json index b5e2cf965..4a6406c3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.92", + "version": "0.5.93", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From fca5f7feafc031fa5391f8c6392ddf352a0cafb2 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sun, 6 Mar 2022 23:16:52 -0500 Subject: [PATCH 178/891] v0.5.93 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae16917d6..0bfa9906e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapshaper", - "version": "0.5.92", + "version": "0.5.93", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mapshaper", - "version": "0.5.92", + "version": "0.5.93", "license": "MPL-2.0", "dependencies": { "commander": "7.0.0", From c7835a607b860875160e9920d8ec740b7350670d Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 9 Mar 2022 10:23:22 -0500 Subject: [PATCH 179/891] Add file_exists() to -if expressions --- src/expressions/mapshaper-layer-proxy.js | 4 ++++ test/if-elif-else-test.js | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/expressions/mapshaper-layer-proxy.js b/src/expressions/mapshaper-layer-proxy.js index 65112d7b2..0c02a594d 100644 --- a/src/expressions/mapshaper-layer-proxy.js +++ b/src/expressions/mapshaper-layer-proxy.js @@ -1,6 +1,7 @@ import { getLayerBounds, getFeatureCount } from '../dataset/mapshaper-layer-utils'; import { addGetters } from '../expressions/mapshaper-expression-utils'; import { getColumnType } from '../datatable/mapshaper-data-utils'; +import cli from '../cli/mapshaper-cli-utils'; // Returns an object representing a layer in a JS expression export function getLayerProxy(lyr, arcs) { @@ -27,6 +28,9 @@ export function getLayerProxy(lyr, arcs) { return rec && (rec[name] === val); }); }; + obj.file_exists = function(name) { + return cli.isFile(name); + }; return obj; } diff --git a/test/if-elif-else-test.js b/test/if-elif-else-test.js index 363b4ae89..11f9e40a7 100644 --- a/test/if-elif-else-test.js +++ b/test/if-elif-else-test.js @@ -82,4 +82,19 @@ describe('mapshaper-if-elif-else-endif.js', function () { }); }); + + it ('test file_exists() function', function(done) { + var data = [{name: 'a'}, {name: 'b'}]; + var cmd = `-i data.json -if 'file_exists("package.json")' -each 'name = "c"' -endif -o`; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var data = JSON.parse(out['data.json']); + assert.deepEqual(data, [{ + name: 'c' + }, { + name: 'c' + }]); + done(); + }); + }); + }) From 437d65e273f9168924943185c2ea8c1a0953e8b5 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 9 Mar 2022 10:23:58 -0500 Subject: [PATCH 180/891] Vertex selection WIP --- package-lock.json | 3383 +------------------------- src/gui/gui-canvas.js | 19 +- src/gui/gui-edit-vertices.js | 35 +- src/gui/gui-interactive-selection.js | 12 + src/gui/gui-map-style.js | 1 + three_points.json | 5 - 6 files changed, 85 insertions(+), 3370 deletions(-) delete mode 100644 three_points.json diff --git a/package-lock.json b/package-lock.json index 0bfa9906e..cca1271d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,3345 +1,8 @@ { "name": "mapshaper", "version": "0.5.93", - "lockfileVersion": 2, + "lockfileVersion": 1, "requires": true, - "packages": { - "": { - "name": "mapshaper", - "version": "0.5.93", - "license": "MPL-2.0", - "dependencies": { - "commander": "7.0.0", - "cookies": "^0.8.0", - "d3-color": "2.0.0", - "d3-scale-chromatic": "2.0.0", - "delaunator": "^5.0.0", - "flatbush": "^3.2.1", - "geokdbush": "^1.1.0", - "iconv-lite": "0.4.24", - "kdbush": "^3.0.0", - "mproj": "0.0.35", - "opn": "^5.3.0", - "rw": "~1.3.3", - "sync-request": "5.0.0", - "tinyqueue": "^2.0.3" - }, - "bin": { - "mapshaper": "bin/mapshaper", - "mapshaper-gui": "bin/mapshaper-gui", - "mapshaper-xl": "bin/mapshaper-xl" - }, - "devDependencies": { - "@rollup/plugin-node-resolve": "^13.0.6", - "browserify": "^17.0.0", - "csv-spectrum": "^1.0.0", - "deep-eql": ">=0.1.3", - "esm": "^3.2.25", - "mocha": "^8.4.0", - "rollup": "^2.60.0", - "shell-quote": "^1.6.1", - "underscore": "^1.13.1" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.0.6.tgz", - "integrity": "sha512-sFsPDMPd4gMqnh2gS0uIxELnoRUp5kBl5knxD2EO0778G1oOJv4G1vyT2cpWz75OU2jDVcXhjVUuTAczGyFNKA==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "builtin-modules": "^3.1.0", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "rollup": "^2.42.0" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", - "dev": true, - "dependencies": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@types/concat-stream": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz", - "integrity": "sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true - }, - "node_modules/@types/form-data": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", - "integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "10.17.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.18.tgz", - "integrity": "sha512-DQ2hl/Jl3g33KuAUOcMrcAOtsbzb+y/ufakzAdeK9z/H/xsvkpbETZZbPNMIiQuk24f5ZRMCcZIViAwyFIiKmg==" - }, - "node_modules/@types/qs": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz", - "integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==" - }, - "node_modules/@types/resolve": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", - "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "dev": true, - "dependencies": { - "object-assign": "^4.1.1", - "util": "0.10.3" - } - }, - "node_modules/assert/node_modules/inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - }, - "node_modules/assert/node_modules/util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "dependencies": { - "inherits": "2.0.1" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.4.tgz", - "integrity": "sha512-SA5mXJWrId1TaQjfxUYghbqQ/hYioKmLJvPJyDuYRtXXenFNMjj4hSSt1Cf1xsuXSXrtxrVC5Ot4eU6cOtBDdA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true - }, - "node_modules/browser-pack": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", - "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", - "dev": true, - "dependencies": { - "combine-source-map": "~0.8.0", - "defined": "^1.0.0", - "JSONStream": "^1.0.3", - "safe-buffer": "^5.1.1", - "through2": "^2.0.0", - "umd": "^3.0.0" - }, - "bin": { - "browser-pack": "bin/cmd.js" - } - }, - "node_modules/browser-resolve": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", - "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", - "dev": true, - "dependencies": { - "resolve": "^1.17.0" - } - }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "node_modules/browserify": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/browserify/-/browserify-17.0.0.tgz", - "integrity": "sha512-SaHqzhku9v/j6XsQMRxPyBrSP3gnwmE27gLJYZgMT2GeK3J0+0toN+MnuNYDfHwVGQfLiMZ7KSNSIXHemy905w==", - "dev": true, - "dependencies": { - "assert": "^1.4.0", - "browser-pack": "^6.0.1", - "browser-resolve": "^2.0.0", - "browserify-zlib": "~0.2.0", - "buffer": "~5.2.1", - "cached-path-relative": "^1.0.0", - "concat-stream": "^1.6.0", - "console-browserify": "^1.1.0", - "constants-browserify": "~1.0.0", - "crypto-browserify": "^3.0.0", - "defined": "^1.0.0", - "deps-sort": "^2.0.1", - "domain-browser": "^1.2.0", - "duplexer2": "~0.1.2", - "events": "^3.0.0", - "glob": "^7.1.0", - "has": "^1.0.0", - "htmlescape": "^1.1.0", - "https-browserify": "^1.0.0", - "inherits": "~2.0.1", - "insert-module-globals": "^7.2.1", - "JSONStream": "^1.0.3", - "labeled-stream-splicer": "^2.0.0", - "mkdirp-classic": "^0.5.2", - "module-deps": "^6.2.3", - "os-browserify": "~0.3.0", - "parents": "^1.0.1", - "path-browserify": "^1.0.0", - "process": "~0.11.0", - "punycode": "^1.3.2", - "querystring-es3": "~0.2.0", - "read-only-stream": "^2.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.1.4", - "shasum-object": "^1.0.0", - "shell-quote": "^1.6.1", - "stream-browserify": "^3.0.0", - "stream-http": "^3.0.0", - "string_decoder": "^1.1.1", - "subarg": "^1.0.0", - "syntax-error": "^1.1.1", - "through2": "^2.0.0", - "timers-browserify": "^1.0.1", - "tty-browserify": "0.0.1", - "url": "~0.11.0", - "util": "~0.12.0", - "vm-browserify": "^1.0.0", - "xtend": "^4.0.0" - }, - "bin": { - "browserify": "bin/cmd.js" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "dev": true, - "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "node_modules/browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/browserify-sign/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "dependencies": { - "pako": "~1.0.5" - } - }, - "node_modules/buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", - "dev": true, - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "node_modules/buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "node_modules/buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true - }, - "node_modules/builtin-modules": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", - "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true - }, - "node_modules/cached-path-relative": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", - "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", - "dev": true - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camelcase": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", - "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", - "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.1" - } - }, - "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/combine-source-map": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", - "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", - "dev": true, - "dependencies": { - "convert-source-map": "~1.1.0", - "inline-source-map": "~0.6.0", - "lodash.memoize": "~3.0.3", - "source-map": "~0.5.3" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.0.0.tgz", - "integrity": "sha512-ovx/7NkTrnPuIV8sqk/GjUIIM1+iUQeqA3ye2VNpq9sVoiZsooObWlQy+OPWGI17GDaEoybuAGJm6U8yC077BA==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true - }, - "node_modules/constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true - }, - "node_modules/convert-source-map": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", - "dev": true - }, - "node_modules/cookies": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", - "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", - "dependencies": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, - "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - }, - "engines": { - "node": "*" - } - }, - "node_modules/csv-spectrum": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/csv-spectrum/-/csv-spectrum-1.0.0.tgz", - "integrity": "sha1-WRrJ/0itTz60M4RXvJgBs0nj1ig=", - "dev": true - }, - "node_modules/d3-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", - "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==" - }, - "node_modules/d3-interpolate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", - "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==", - "dependencies": { - "d3-color": "1 - 2" - } - }, - "node_modules/d3-scale-chromatic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz", - "integrity": "sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA==", - "dependencies": { - "d3-color": "1 - 2", - "d3-interpolate": "1 - 2" - } - }, - "node_modules/dash-ast": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", - "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==", - "dev": true - }, - "node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-eql": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.0.0.tgz", - "integrity": "sha512-GxJC5MOg2KyQlv6WiUF/VAnMj4MWnYiXo4oLgeptOELVoknyErb4Z8+5F/IM/K4g9/80YzzatxmWcyRwUseH0A==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "dependencies": { - "object-keys": "^1.0.12" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", - "dev": true - }, - "node_modules/delaunator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", - "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", - "dependencies": { - "robust-predicates": "^3.0.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/deps-sort": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz", - "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==", - "dev": true, - "dependencies": { - "JSONStream": "^1.0.3", - "shasum-object": "^1.0.0", - "subarg": "^1.0.0", - "through2": "^2.0.0" - }, - "bin": { - "deps-sort": "bin/cmd.js" - } - }, - "node_modules/des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/detective": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", - "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", - "dev": true, - "dependencies": { - "acorn-node": "^1.6.1", - "defined": "^1.0.0", - "minimist": "^1.1.1" - }, - "bin": { - "detective": "bin/detective.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true, - "engines": { - "node": ">=0.4", - "npm": ">=1.2" - } - }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dev": true, - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/es-abstract": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", - "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.10.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", - "dev": true - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", - "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==", - "dev": true - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "bin": { - "flat": "cli.js" - } - }, - "node_modules/flatbush": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/flatbush/-/flatbush-3.2.1.tgz", - "integrity": "sha512-RAqcCyM18R0HhGIcZ7nTRImHnvmJAQqxSN8VIrRLPyWDuFjxluiyE99wuDqFiwNwBodlHXBQNf/9CrlfSqJq2A==", - "dependencies": { - "flatqueue": "^1.2.0" - } - }, - "node_modules/flatqueue": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/flatqueue/-/flatqueue-1.2.0.tgz", - "integrity": "sha512-Z/nhmRwSywE3xnHXHqbLzJiUZ9akOHZlB1IIqCzRRldWrxqp6EzqGVxTl9Fl5cSoUzC5ge7xq3WIPct8ADYdhw==" - }, - "node_modules/foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true - }, - "node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/geographiclib": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/geographiclib/-/geographiclib-1.48.0.tgz", - "integrity": "sha1-j/KuGFrTgPZ122okOTX63RR974I=" - }, - "node_modules/geokdbush": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/geokdbush/-/geokdbush-1.1.0.tgz", - "integrity": "sha1-ql6OeVOmWUtAqF+9thBZrw9RRo8=", - "dependencies": { - "tinyqueue": "^1.2.2" - } - }, - "node_modules/geokdbush/node_modules/tinyqueue": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz", - "integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==" - }, - "node_modules/get-assigned-identifiers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", - "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", - "dev": true - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-port": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", - "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=", - "engines": { - "node": ">=4" - } - }, - "node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true, - "engines": { - "node": ">=4.x" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hash-base/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/hash-base/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/htmlescape": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", - "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/http-basic": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-6.0.0.tgz", - "integrity": "sha512-7ScbVjuiReYe8S+OZOpNjoKGXrbhJHIrQQe7eq1TpLTJkxH8MPKvnTUzq/TNLjww1hdFQy8yUIC42wuLhCjYcQ==", - "dependencies": { - "@types/concat-stream": "^1.6.0", - "@types/node": "^7.0.31", - "caseless": "~0.12.0", - "concat-stream": "^1.4.6", - "http-response-object": "^3.0.1", - "parse-cache-control": "^1.0.1" - } - }, - "node_modules/http-basic/node_modules/@types/node": { - "version": "7.10.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.9.tgz", - "integrity": "sha512-usSpgoUsRtO5xNV5YEPU8PPnHisFx8u0rokj1BPVn/hDF7zwUDzVLiuKZM38B7z8V2111Fj6kd4rGtQFUZpNOw==" - }, - "node_modules/http-response-object": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", - "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", - "dependencies": { - "@types/node": "^10.0.3" - } - }, - "node_modules/https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/inline-source-map": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", - "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", - "dev": true, - "dependencies": { - "source-map": "~0.5.3" - } - }, - "node_modules/insert-module-globals": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.1.tgz", - "integrity": "sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==", - "dev": true, - "dependencies": { - "acorn-node": "^1.5.2", - "combine-source-map": "^0.8.0", - "concat-stream": "^1.6.1", - "is-buffer": "^1.1.0", - "JSONStream": "^1.0.3", - "path-is-absolute": "^1.0.1", - "process": "~0.11.0", - "through2": "^2.0.0", - "undeclared-identifiers": "^1.1.2", - "xtend": "^4.0.0" - }, - "bin": { - "insert-module-globals": "bin/cmd.js" - } - }, - "node_modules/is-arguments": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", - "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", - "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", - "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", - "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.9.tgz", - "integrity": "sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", - "dev": true - }, - "node_modules/is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", - "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-regex": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", - "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.5.tgz", - "integrity": "sha512-S+GRDgJlR3PyEbsX/Fobd9cqpZBuvUS+8asRqYDMLCb2qMzt1oz5m5oxQCxOgUDxiWsOVNi4yaF+/uvdlHlYug==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.2", - "call-bind": "^1.0.2", - "es-abstract": "^1.18.0-next.2", - "foreach": "^2.0.5", - "has-symbols": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "engines": { - "node": ">=4" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", - "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", - "dev": true, - "engines": [ - "node >= 0.2.0" - ] - }, - "node_modules/JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "dependencies": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - }, - "bin": { - "JSONStream": "bin.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/kdbush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", - "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" - }, - "node_modules/keygrip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", - "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", - "dependencies": { - "tsscmp": "1.0.6" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/labeled-stream-splicer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", - "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "stream-splicer": "^2.0.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.memoize": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", - "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", - "dev": true - }, - "node_modules/log-symbols": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", - "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, - "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/mime-db": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", - "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.26", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", - "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", - "dependencies": { - "mime-db": "1.43.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true - }, - "node_modules/mocha": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", - "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", - "dev": true, - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.1", - "debug": "4.3.1", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.0.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.20", - "serialize-javascript": "5.0.1", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.0", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 10.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/module-deps": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.3.tgz", - "integrity": "sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==", - "dev": true, - "dependencies": { - "browser-resolve": "^2.0.0", - "cached-path-relative": "^1.0.2", - "concat-stream": "~1.6.0", - "defined": "^1.0.0", - "detective": "^5.2.0", - "duplexer2": "^0.1.2", - "inherits": "^2.0.1", - "JSONStream": "^1.0.3", - "parents": "^1.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.4.0", - "stream-combiner2": "^1.1.1", - "subarg": "^1.0.0", - "through2": "^2.0.0", - "xtend": "^4.0.0" - }, - "bin": { - "module-deps": "bin/cmd.js" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/mproj": { - "version": "0.0.35", - "resolved": "https://registry.npmjs.org/mproj/-/mproj-0.0.35.tgz", - "integrity": "sha512-xqO9BXjTezwyPFbAShWRkYZ98DD9wWOyr86WX6miWq3brBNGypsErMmobpUx3G45SrfvyJ5jI997Zw0qr7Ko7A==", - "dependencies": { - "geographiclib": "1.48.0", - "rw": "~1.3.2" - }, - "bin": { - "mcs2cs": "bin/mcs2cs", - "mproj": "bin/mproj" - }, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.1.20", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", - "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", - "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/opn": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", - "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", - "dependencies": { - "is-wsl": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, - "node_modules/parents": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", - "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", - "dev": true, - "dependencies": { - "path-platform": "~0.11.15" - } - }, - "node_modules/parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, - "dependencies": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/parse-cache-control": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", - "integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104=" - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-platform": { - "version": "0.11.15", - "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", - "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dev": true, - "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/promise": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", - "integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==", - "dependencies": { - "asap": "~2.0.6" - } - }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "node_modules/qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "node_modules/read-only-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", - "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "node_modules/robust-predicates": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz", - "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" - }, - "node_modules/rollup": { - "version": "2.60.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.60.0.tgz", - "integrity": "sha512-cHdv9GWd58v58rdseC8e8XIaPUo8a9cgZpnCMMDGZFDZKEODOiPPEQFXLriWr/TjXzhPPmG5bkAztPsOARIcGQ==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/serialize-javascript": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", - "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/shasum-object": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", - "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==", - "dev": true, - "dependencies": { - "fast-safe-stringify": "^2.0.7" - } - }, - "node_modules/shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", - "dev": true - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stream-browserify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "dev": true, - "dependencies": { - "inherits": "~2.0.4", - "readable-stream": "^3.5.0" - } - }, - "node_modules/stream-browserify/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", - "dev": true, - "dependencies": { - "duplexer2": "~0.1.0", - "readable-stream": "^2.0.2" - } - }, - "node_modules/stream-http": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", - "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", - "dev": true, - "dependencies": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "xtend": "^4.0.2" - } - }, - "node_modules/stream-http/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/stream-splicer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", - "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.2" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", - "dev": true, - "dependencies": { - "minimist": "^1.1.0" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/sync-request": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-5.0.0.tgz", - "integrity": "sha512-NKhEA4WacR3mRBIFz1niXrIUTrUVFtP2spzrLMINangebvJ/EFyVv+LMJKvVl6UIrJM4Fburnnj91lRnqb4WkA==", - "dependencies": { - "http-response-object": "^3.0.1", - "sync-rpc": "^1.2.0", - "then-request": "^5.0.0" - } - }, - "node_modules/sync-rpc": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz", - "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==", - "dependencies": { - "get-port": "^3.1.0" - } - }, - "node_modules/syntax-error": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", - "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", - "dev": true, - "dependencies": { - "acorn-node": "^1.2.0" - } - }, - "node_modules/then-request": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/then-request/-/then-request-5.0.0.tgz", - "integrity": "sha512-A3uIVLD33SAvB10PfsxLuQBMV8GVC/6xKBMPOvkJchi6251e5AMJ+Yy+RVKsVsnj0iYNhN2E5SkNSi58H19wsw==", - "dependencies": { - "@types/concat-stream": "^1.6.0", - "@types/form-data": "0.0.33", - "@types/node": "^8.0.0", - "@types/qs": "^6.2.31", - "caseless": "~0.12.0", - "concat-stream": "^1.6.0", - "form-data": "^2.2.0", - "http-basic": "^6.0.0", - "http-response-object": "^3.0.1", - "promise": "^8.0.0", - "qs": "^6.4.0" - } - }, - "node_modules/then-request/node_modules/@types/node": { - "version": "8.10.59", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.59.tgz", - "integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==" - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/timers-browserify": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", - "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", - "dev": true, - "dependencies": { - "process": "~0.11.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/tinyqueue": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", - "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "engines": { - "node": ">=0.6.x" - } - }, - "node_modules/tty-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", - "dev": true - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "node_modules/umd": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", - "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", - "dev": true, - "bin": { - "umd": "bin/cli.js" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undeclared-identifiers": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", - "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", - "dev": true, - "dependencies": { - "acorn-node": "^1.3.0", - "dash-ast": "^1.0.0", - "get-assigned-identifiers": "^1.2.0", - "simple-concat": "^1.0.0", - "xtend": "^4.0.1" - }, - "bin": { - "undeclared-identifiers": "bin.js" - } - }, - "node_modules/underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", - "dev": true - }, - "node_modules/url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - }, - "node_modules/util": { - "version": "0.12.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", - "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "safe-buffer": "^5.1.2", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.4.tgz", - "integrity": "sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.2", - "call-bind": "^1.0.0", - "es-abstract": "^1.18.0-next.1", - "foreach": "^2.0.5", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.1", - "is-typed-array": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "dependencies": { - "string-width": "^1.0.2 || 2" - } - }, - "node_modules/workerpool": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz", - "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==", - "dev": true - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - }, "dependencies": { "@rollup/plugin-node-resolve": { "version": "13.0.6", @@ -3413,6 +76,16 @@ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -3591,9 +264,9 @@ "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", "dev": true, "requires": { + "JSONStream": "^1.0.3", "combine-source-map": "~0.8.0", "defined": "^1.0.0", - "JSONStream": "^1.0.3", "safe-buffer": "^5.1.1", "through2": "^2.0.0", "umd": "^3.0.0" @@ -3620,6 +293,7 @@ "integrity": "sha512-SaHqzhku9v/j6XsQMRxPyBrSP3gnwmE27gLJYZgMT2GeK3J0+0toN+MnuNYDfHwVGQfLiMZ7KSNSIXHemy905w==", "dev": true, "requires": { + "JSONStream": "^1.0.3", "assert": "^1.4.0", "browser-pack": "^6.0.1", "browser-resolve": "^2.0.0", @@ -3641,7 +315,6 @@ "https-browserify": "^1.0.0", "inherits": "~2.0.1", "insert-module-globals": "^7.2.1", - "JSONStream": "^1.0.3", "labeled-stream-splicer": "^2.0.0", "mkdirp-classic": "^0.5.2", "module-deps": "^6.2.3", @@ -4673,11 +1346,11 @@ "integrity": "sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==", "dev": true, "requires": { + "JSONStream": "^1.0.3", "acorn-node": "^1.5.2", "combine-source-map": "^0.8.0", "concat-stream": "^1.6.1", "is-buffer": "^1.1.0", - "JSONStream": "^1.0.3", "path-is-absolute": "^1.0.1", "process": "~0.11.0", "through2": "^2.0.0", @@ -4871,16 +1544,6 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, "kdbush": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", @@ -5042,6 +1705,7 @@ "integrity": "sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==", "dev": true, "requires": { + "JSONStream": "^1.0.3", "browser-resolve": "^2.0.0", "cached-path-relative": "^1.0.2", "concat-stream": "~1.6.0", @@ -5049,7 +1713,6 @@ "detective": "^5.2.0", "duplexer2": "^0.1.2", "inherits": "^2.0.1", - "JSONStream": "^1.0.3", "parents": "^1.0.0", "readable-stream": "^2.0.2", "resolve": "^1.4.0", @@ -5523,14 +2186,6 @@ "readable-stream": "^2.0.2" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -5561,6 +2216,14 @@ "define-properties": "^1.1.3" } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", diff --git a/src/gui/gui-canvas.js b/src/gui/gui-canvas.js index 9708d131f..dd33fbbfa 100644 --- a/src/gui/gui-canvas.js +++ b/src/gui/gui-canvas.js @@ -151,24 +151,37 @@ export function DisplayCanvas() { var radius = (style.strokeWidth > 2 ? style.strokeWidth * 0.9 : 2) * GUI.getPixelRatio() * getScaledLineScale(_ext); var color = style.strokeColor || 'black'; var radius2 = radius * 1.7; + var i, j, p; _ctx.beginPath(); _ctx.fillStyle = color; - for (var i=0; i Date: Wed, 9 Mar 2022 10:25:18 -0500 Subject: [PATCH 181/891] v0.5.94 --- CHANGELOG.md | 3 +++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0bfbfd41..d0c258756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.94 +* Added file_exists() test to -if/-elif expression context. + v0.5.93 * Add a vertex in "edit vertices" mode by dragging the midpoint of a segment. diff --git a/package-lock.json b/package-lock.json index cca1271d9..76ad1ec04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.93", + "version": "0.5.94", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4a6406c3c..3f9b8fc16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.93", + "version": "0.5.94", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 5a0b200577eeab685604b5406d1d8ba7af7d8092 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 9 Mar 2022 23:42:46 -0500 Subject: [PATCH 182/891] Double-click deletes vertices in 'edit vertices' mode --- src/gui/gui-canvas.js | 11 ------- src/gui/gui-edit-vertices.js | 47 ++++++++++------------------ src/gui/gui-interactive-selection.js | 14 ++------- src/gui/gui-map-style.js | 2 +- src/gui/gui-undo.js | 13 ++++++++ 5 files changed, 32 insertions(+), 55 deletions(-) diff --git a/src/gui/gui-canvas.js b/src/gui/gui-canvas.js index dd33fbbfa..f39639a77 100644 --- a/src/gui/gui-canvas.js +++ b/src/gui/gui-canvas.js @@ -171,17 +171,6 @@ export function DisplayCanvas() { } _ctx.fill(); _ctx.closePath(); - - if (style.selected_points) { - _ctx.beginPath(); - _ctx.fillStyle = 'magenta'; - for (i=0; i -1) { + if (o.id > -1) { // pinned or hover style topStyle = getSelectedFeatureStyle(lyr, o); topIdx = ids.length; ids.push(o.id); // put the pinned/hover feature last in the render order diff --git a/src/gui/gui-undo.js b/src/gui/gui-undo.js index 0071bcf2d..833ccae67 100644 --- a/src/gui/gui-undo.js +++ b/src/gui/gui-undo.js @@ -84,6 +84,19 @@ export function Undo(gui) { this.addHistoryState(stashedUndo, redo); }, this); + gui.on('vertex_delete', function(e) { + var target = gui.model.getActiveLayer(); + var arcs = target.dataset.arcs; + var p = arcs.getVertex2(e.vertex_id); + var redo = function() { + internal.deleteVertex(arcs, e.vertex_id); + }; + var undo = function() { + internal.insertVertex(arcs, e.vertex_id, p); + }; + this.addHistoryState(undo, redo); + }, this); + gui.on('vertex_insert', function(e) { var target = gui.model.getActiveLayer(); var arcs = target.dataset.arcs; From 1df1bf75766632ec5a2d29d34124b2445cfcd935 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 10 Mar 2022 12:52:07 -0500 Subject: [PATCH 183/891] Improve vertex editing undo/redo --- src/gui/gui-edit-vertices.js | 113 +++++++++++++++-------------------- src/gui/gui-undo.js | 39 ++++++------ 2 files changed, 67 insertions(+), 85 deletions(-) diff --git a/src/gui/gui-edit-vertices.js b/src/gui/gui-edit-vertices.js index ff99a1a9d..e38f2b4c7 100644 --- a/src/gui/gui-edit-vertices.js +++ b/src/gui/gui-edit-vertices.js @@ -1,23 +1,19 @@ import { error, internal, geom, utils } from './gui-core'; +// pointer thresholds for hovering near a vertex or segment midpoint var HOVER_THRESHOLD = 8; var MIDPOINT_THRESHOLD = 12; - export function initVertexDragging(gui, ext, hit) { - var activeShapeId = -1; - var draggedVertexIds = null; - var activeMidpoint; // {point, segment} + var activeMidpoint; + var dragInfo; - function active(e) { - return e.id > -1 && gui.interaction.getMode() == 'vertices'; + function active() { + return gui.interaction.getMode() == 'vertices'; } - function fire(type) { - gui.dispatchEvent(type, { - FID: activeShapeId, - vertex_ids: draggedVertexIds - }); + function dragging() { + return active() && !!dragInfo; } function setHoverVertex(id) { @@ -39,113 +35,103 @@ export function initVertexDragging(gui, ext, hit) { var dist = geom.distance2D(p[0], p[1], p2[0], p2[1]); var pixelDist = dist / ext.getPixelSize(); if (pixelDist > HOVER_THRESHOLD) { - draggedVertexIds = null; return null; } - return nearestIds; - } - - function insertMidpoint(v) { - var target = hit.getHitTarget(); - internal.insertVertex(target.arcs, v.i, v.point); + var points = nearestIds.map(function(i) {return target.arcs.getVertex2(i);}); + return { + ids: nearestIds, + points: points + }; } hit.on('dragstart', function(e) { - if (!active(e)) return; + if (!active()) return; if (activeMidpoint) { - insertMidpoint(activeMidpoint); - draggedVertexIds = [activeMidpoint.i]; - // TODO: combine vertex insertion undo/redo actions with - // vertex_dragend undo/redo actions - gui.dispatchEvent('vertex_insert', { - FID: activeShapeId, - vertex_id: activeMidpoint.i, - coordinates: activeMidpoint.point - }); + var target = hit.getHitTarget(); + internal.insertVertex(target.arcs, activeMidpoint.i, activeMidpoint.point); + dragInfo = { + insertion: true, + ids: [activeMidpoint.i], + points: [activeMidpoint.point] + }; activeMidpoint = null; } else { - draggedVertexIds = findDraggableVertices(e); + dragInfo = findDraggableVertices(e); + } + if (dragInfo) { + setHoverVertex(dragInfo.ids[0]); } - if (!draggedVertexIds) return; - setHoverVertex(draggedVertexIds[0]); - activeShapeId = e.id; - fire('vertex_dragstart'); }); hit.on('drag', function(e) { - if (!active(e) || !draggedVertexIds) return; + if (!dragging()) return; var target = hit.getHitTarget(); var p = ext.translatePixelCoords(e.x, e.y); if (gui.keyboard.shiftIsPressed()) { - internal.snapPointToArcEndpoint(p, draggedVertexIds, target.arcs); + internal.snapPointToArcEndpoint(p, dragInfo.ids, target.arcs); } - internal.snapVerticesToPoint(draggedVertexIds, p, target.arcs); - setHoverVertex(draggedVertexIds[0]); + internal.snapVerticesToPoint(dragInfo.ids, p, target.arcs); + setHoverVertex(dragInfo.ids[0]); // redrawing the whole map updates the data layer as well as the overlay layer // gui.dispatchEvent('map-needs-refresh'); }); hit.on('dragend', function(e) { - if (!active(e) || !draggedVertexIds) return; + if (!dragging()) return; // kludge to get dataset to recalculate internal bounding boxes hit.getHitTarget().arcs.transformPoints(function() {}); clearHoverVertex(); - - - - fire('vertex_dragend'); - draggedVertexIds = null; - activeShapeId = -1; - // redraw data layer + gui.dispatchEvent('vertex_dragend', dragInfo); gui.dispatchEvent('map-needs-refresh'); + dragInfo = null; }); hit.on('dblclick', function(e) { - if (!active(e)) return; - var vertices = findDraggableVertices(e); // same selection criteria as for dragging - if (!vertices) return; + if (!active()) return; + var info = findDraggableVertices(e); // same selection criteria as for dragging + if (!info) return; var target = hit.getHitTarget(); - var vId = vertices[0]; + var vId = info.ids[0]; if (internal.vertexIsArcStart(vId, target.arcs) || internal.vertexIsArcEnd(vId, target.arcs)) { // TODO: support removing arc endpoints return; } gui.dispatchEvent('vertex_delete', { - FID: activeShapeId, vertex_id: vId }); internal.deleteVertex(target.arcs, vId); clearHoverVertex(); gui.dispatchEvent('map-needs-refresh'); - }); // highlight hit vertex in path edit mode hit.on('hover', function(e) { activeMidpoint = null; - if (!active(e) || draggedVertexIds) return; // no hover effect while dragging - var vertexIds = findDraggableVertices(e); - if (vertexIds) { - setHoverVertex(vertexIds[0]); + if (!active() || dragging()) return; // no hover effect while dragging + var info = findDraggableVertices(e); + if (info) { + // hovering near a vertex: highlight the vertex + setHoverVertex(info.ids[0]); return; } + // if hovering near a segment midpoint: show the midpoint and save midpoint info + var p = ext.translatePixelCoords(e.x, e.y); var target = hit.getHitTarget(); - // vertex insertion doesn't work yet with simplification applied - if (!target.arcs.isFlat()) return; var shp = target.layer.shapes[e.id]; - var p = ext.translatePixelCoords(e.x, e.y); var midpoint = findNearestMidpoint(p, shp, target.arcs); - if (midpoint && midpoint.distance / ext.getPixelSize() < MIDPOINT_THRESHOLD) { + if (midpoint && + midpoint.distance / ext.getPixelSize() < MIDPOINT_THRESHOLD && + target.arcs.isFlat()) { // vertex insertion not supported with simplification hit.setHoverVertex(midpoint.point); activeMidpoint = midpoint; - } else { - clearHoverVertex(); + return; } + // pointer is not over a vertex or midpoint: clear hover effect + clearHoverVertex(); }, null, 100); } - // Given a location @p (e.g. corresponding to the mouse pointer location), // find the midpoint of two vertices on @shp suitable for inserting a new vertex, // but only if: @@ -177,6 +163,3 @@ function findNearestMidpoint(p, shp, arcs) { }); return v || null; } - - - diff --git a/src/gui/gui-undo.js b/src/gui/gui-undo.js index 833ccae67..70de8ef41 100644 --- a/src/gui/gui-undo.js +++ b/src/gui/gui-undo.js @@ -74,37 +74,36 @@ export function Undo(gui) { this.addHistoryState(stashedUndo, redo); }, this); - // undo/redo vertex dragging - gui.on('vertex_dragstart', function(e) { - stashedUndo = this.makeVertexSetter(e.FID, e.vertex_ids); - }, this); - gui.on('vertex_dragend', function(e) { - var redo = this.makeVertexSetter(e.FID, e.vertex_ids); - this.addHistoryState(stashedUndo, redo); - }, this); - - gui.on('vertex_delete', function(e) { var target = gui.model.getActiveLayer(); var arcs = target.dataset.arcs; - var p = arcs.getVertex2(e.vertex_id); - var redo = function() { - internal.deleteVertex(arcs, e.vertex_id); - }; + var startPoint = e.points[0]; + var endPoint = internal.getVertexCoords(e.ids[0], arcs); var undo = function() { - internal.insertVertex(arcs, e.vertex_id, p); + if (e.insertion) { + internal.deleteVertex(arcs, e.ids[0]); + } else { + snapVerticesToPoint(e.ids, startPoint, arcs, true); + } + }; + var redo = function() { + if (e.insertion) { + internal.insertVertex(arcs, e.ids[0], e.points[0]); + } + snapVerticesToPoint(e.ids, endPoint, arcs, true); }; this.addHistoryState(undo, redo); }, this); - gui.on('vertex_insert', function(e) { + gui.on('vertex_delete', function(e) { var target = gui.model.getActiveLayer(); var arcs = target.dataset.arcs; - var undo = function() { + var p = arcs.getVertex2(e.vertex_id); + var redo = function() { internal.deleteVertex(arcs, e.vertex_id); }; - var redo = function() { - internal.insertVertex(arcs, e.vertex_id, e.coordinates); + var undo = function() { + internal.insertVertex(arcs, e.vertex_id, p); }; this.addHistoryState(undo, redo); }, this); @@ -130,7 +129,7 @@ export function Undo(gui) { }; }; - this.makeVertexSetter = function(fid, ids) { + this.makeVertexSetter = function(ids) { var target = gui.model.getActiveLayer(); var arcs = target.dataset.arcs; var p = internal.getVertexCoords(ids[0], arcs); From 71bbdf1ffd2ab7bbcf45584429254152ed7ad6ca Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 10 Mar 2022 12:53:40 -0500 Subject: [PATCH 184/891] v0.5.95 --- CHANGELOG.md | 3 +++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0c258756..b15ca5af0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.95 +* Added vertex deletion in "edit vertices" mode by double-clicking on non-endpoint vertices. + v0.5.94 * Added file_exists() test to -if/-elif expression context. diff --git a/package-lock.json b/package-lock.json index 76ad1ec04..f18af7872 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.94", + "version": "0.5.95", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3f9b8fc16..365003aa0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.94", + "version": "0.5.95", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 2eeb7c93ca03fb8471b3967171734e2695b72e75 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Sat, 12 Mar 2022 20:08:09 -0500 Subject: [PATCH 185/891] Performance optimizations --- src/gui/gui-canvas.js | 6 ++- src/gui/gui-edit-vertices.js | 57 ++++++++++++++--------------- src/gui/gui-map-style.js | 40 ++++++++++++++------ src/gui/gui-shape-hit.js | 7 +--- src/paths/mapshaper-shape-iter.js | 4 +- src/paths/mapshaper-vertex-utils.js | 23 +----------- 6 files changed, 67 insertions(+), 70 deletions(-) diff --git a/src/gui/gui-canvas.js b/src/gui/gui-canvas.js index f39639a77..c061c6294 100644 --- a/src/gui/gui-canvas.js +++ b/src/gui/gui-canvas.js @@ -148,9 +148,10 @@ export function DisplayCanvas() { _self.drawVertices = function(shapes, arcs, style, filter) { var iter = new internal.ShapeIter(arcs); var t = getScaledTransform(_ext); + var bounds = _ext.getBounds(); var radius = (style.strokeWidth > 2 ? style.strokeWidth * 0.9 : 2) * GUI.getPixelRatio() * getScaledLineScale(_ext); var color = style.strokeColor || 'black'; - var radius2 = radius * 1.7; + var i, j, p; _ctx.beginPath(); _ctx.fillStyle = color; @@ -160,6 +161,7 @@ export function DisplayCanvas() { for (j=0; j MIDPOINT_THRESHOLD) return null; + return midpoint; + } + hit.on('dragstart', function(e) { if (!active()) return; - if (activeMidpoint) { + if (insertionPoint) { var target = hit.getHitTarget(); - internal.insertVertex(target.arcs, activeMidpoint.i, activeMidpoint.point); + internal.insertVertex(target.arcs, insertionPoint.i, insertionPoint.point); dragInfo = { insertion: true, - ids: [activeMidpoint.i], - points: [activeMidpoint.point] + ids: [insertionPoint.i], + points: [insertionPoint.point] }; - activeMidpoint = null; + insertionPoint = null; } else { dragInfo = findDraggableVertices(e); } @@ -107,7 +117,7 @@ export function initVertexDragging(gui, ext, hit) { // highlight hit vertex in path edit mode hit.on('hover', function(e) { - activeMidpoint = null; + insertionPoint = null; if (!active() || dragging()) return; // no hover effect while dragging var info = findDraggableVertices(e); if (info) { @@ -116,32 +126,20 @@ export function initVertexDragging(gui, ext, hit) { return; } // if hovering near a segment midpoint: show the midpoint and save midpoint info - var p = ext.translatePixelCoords(e.x, e.y); - var target = hit.getHitTarget(); - var shp = target.layer.shapes[e.id]; - var midpoint = findNearestMidpoint(p, shp, target.arcs); - if (midpoint && - midpoint.distance / ext.getPixelSize() < MIDPOINT_THRESHOLD && - target.arcs.isFlat()) { // vertex insertion not supported with simplification - hit.setHoverVertex(midpoint.point); - activeMidpoint = midpoint; - return; + insertionPoint = findVertexInsertionPoint(e); + if (insertionPoint) { + hit.setHoverVertex(insertionPoint.point); + } else { + // pointer is not over a vertex: clear any hover effect + clearHoverVertex(); } - // pointer is not over a vertex or midpoint: clear hover effect - clearHoverVertex(); }, null, 100); } + // Given a location @p (e.g. corresponding to the mouse pointer location), -// find the midpoint of two vertices on @shp suitable for inserting a new vertex, -// but only if: -// 1. point @p is closer to the midpoint than either adjacent vertex -// 2. the segment containing @p is longer than a minimum distance in pixels. -// +// find the midpoint of two vertices on @shp suitable for inserting a new vertex function findNearestMidpoint(p, shp, arcs) { - // var v1 = internal.findNearestVertex(p[0], p[1], shp, arcs); - // var v0 = internal.findAdjacentVertex(v1, shp, arcs, -1); - // var v2 = internal.findAdjacentVertex(v1, shp, arcs, 1); var minDist = Infinity, v; internal.forEachSegmentInShape(shp, arcs, function(i, j, xx, yy) { var x1 = xx[i], @@ -156,6 +154,7 @@ function findNearestMidpoint(p, shp, arcs) { v = { i: (i < j ? i : j) + 1, // insertion point segment: [i, j], + segmentLen: geom.distance2D(x1, y1, x2, y2), point: [cx, cy], distance: dist }; diff --git a/src/gui/gui-map-style.js b/src/gui/gui-map-style.js index 8caaaa3c5..bf1000055 100644 --- a/src/gui/gui-map-style.js +++ b/src/gui/gui-map-style.js @@ -156,6 +156,20 @@ export function getActiveStyle(lyr) { return style; } +// style for vertex edit mode +function getVertexStyle(lyr, o) { + return { + ids: o.ids, + overlay: true, + strokeColor: black, + strokeWidth: 1.5, + vertices: true, + vertex_overlay: o.hit_coordinates || null, + selected_points: o.selected_points || null, + fillColor: null + }; +} + // Returns a display style for the overlay layer. // The overlay layer renders several kinds of feature, each of which is displayed // with a different style. @@ -165,18 +179,14 @@ export function getActiveStyle(lyr) { // * pinned shapes // export function getOverlayStyle(lyr, o) { + if (o.mode == 'vertices') { + return getVertexStyle(lyr, o); + } var geomType = lyr.geometry_type; var topId = o.id; // pinned id (if pinned) or hover id var topIdx = -1; var styler = function(style, i) { utils.extend(style, i === topIdx ? topStyle: baseStyle); - // kludge to show vertices when editing path shapes - if (o.mode == 'vertices') { - style.vertices = true; - style.vertex_overlay = o.hit_coordinates || null; - style.selected_points = o.selected_points || null; - style.fillColor = null; - } }; var baseStyle = getDefaultStyle(lyr, selectionStyles[geomType]); var topStyle; @@ -188,18 +198,26 @@ export function getOverlayStyle(lyr, o) { topIdx = ids.length; ids.push(o.id); // put the pinned/hover feature last in the render order } - var overlayStyle = { + var style = { styler: styler, ids: ids, overlay: true }; + // kludge to show vertices when editing path shapes + if (o.mode == 'vertices') { + style.vertices = true; + style.vertex_overlay = o.hit_coordinates || null; + style.selected_points = o.selected_points || null; + style.fillColor = null; + } + if (layerHasCanvasDisplayStyle(lyr)) { if (geomType == 'point') { - overlayStyle.styler = getOverlayPointStyler(getCanvasDisplayStyle(lyr).styler, styler); + style.styler = getOverlayPointStyler(getCanvasDisplayStyle(lyr).styler, styler); } - overlayStyle.type = 'styled'; + style.type = 'styled'; } - return ids.length > 0 ? overlayStyle : null; + return ids.length > 0 ? style : null; } function getSelectedFeatureStyle(lyr, o) { diff --git a/src/gui/gui-shape-hit.js b/src/gui/gui-shape-hit.js index 9af85a731..e65df23de 100644 --- a/src/gui/gui-shape-hit.js +++ b/src/gui/gui-shape-hit.js @@ -73,14 +73,11 @@ export function getShapeHitTest(displayLayer, ext, interactionMode) { } function vertexTest(x, y) { - var maxDist = getZoomAdjustedHitBuffer(20, 2), - bufDist = getZoomAdjustedHitBuffer(0.05), // tiny threshold for hitting almost-identical lines + var maxDist = getZoomAdjustedHitBuffer(25, 2), cands = findHitCandidates(x, y, maxDist); sortByDistance(x, y, cands, displayLayer.arcs); - cands = pickNearestCandidates(cands, bufDist, maxDist); - var arcs = cands.map(function(cand) { return absArcId(cand.info.arcId); }); + cands = pickNearestCandidates(cands, 0, maxDist); return { - arcs: utils.uniq(arcs), ids: utils.pluck(cands, 'id') }; } diff --git a/src/paths/mapshaper-shape-iter.js b/src/paths/mapshaper-shape-iter.js index 422a536bb..ccf029404 100644 --- a/src/paths/mapshaper-shape-iter.js +++ b/src/paths/mapshaper-shape-iter.js @@ -122,7 +122,7 @@ export function ShapeIter(arcs) { this._n = 0; this.x = 0; this.y = 0; - this.i = -1; + // this.i = -1; } ShapeIter.prototype.hasNext = function() { @@ -133,7 +133,7 @@ ShapeIter.prototype.hasNext = function() { if (arc.hasNext()) { this.x = arc.x; this.y = arc.y; - this.i = arc.i; + // this.i = arc.i; return true; } this.nextArc(); diff --git a/src/paths/mapshaper-vertex-utils.js b/src/paths/mapshaper-vertex-utils.js index 9dc2f2bbc..4cfd4e1de 100644 --- a/src/paths/mapshaper-vertex-utils.js +++ b/src/paths/mapshaper-vertex-utils.js @@ -6,7 +6,6 @@ export function findNearestVertices(p, shp, arcs) { } - export function snapVerticesToPoint(ids, p, arcs, final) { ids.forEach(function(idx) { setVertexCoords(p[0], p[1], idx, arcs); @@ -86,7 +85,7 @@ export function setVertexCoords(x, y, i, arcs) { export function findNearestVertex(x, y, shp, arcs, spherical) { var calcLen = spherical ? geom.greatCircleDistance : geom.distance2D, minLen = Infinity, - minX, minY, vId, dist, iter; + minX, minY, dist, iter; for (var i=0; i Date: Tue, 15 Mar 2022 17:52:33 -0400 Subject: [PATCH 186/891] v0.5.96 --- CHANGELOG.md | 4 + REFERENCE.md | 1 + package-lock.json | 14 +- package.json | 2 +- .../mapshaper-classify-methods.js | 26 +++ .../mapshaper-classify-ramps.js | 85 ++++++++ .../mapshaper-indexed-classifier.js | 2 +- src/cli/mapshaper-options.js | 2 +- src/color/color-schemes.js | 15 +- src/commands/mapshaper-classify.js | 192 ++++++------------ test/color-schemes-test.js | 25 +++ test/{color-utils.js => color-utils-test.js} | 0 12 files changed, 229 insertions(+), 139 deletions(-) create mode 100644 src/classification/mapshaper-classify-methods.js create mode 100644 src/classification/mapshaper-classify-ramps.js create mode 100644 test/color-schemes-test.js rename test/{color-utils.js => color-utils-test.js} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b15ca5af0..7805403bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.96 +* Added colors=random option to the -classify command. +* Added more sensible default behavior to the -classify command. + v0.5.95 * Added vertex deletion in "edit vertices" mode by double-clicking on non-endpoint vertices. diff --git a/REFERENCE.md b/REFERENCE.md index dd6a4ac2d..98bd8fb97 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -1331,6 +1331,7 @@ The `if` command runs the following commands if a condition is met. - `this.field_exists()` Tests if a data field exists in the target layer. - `this.field_type()` Returns the data type of a field, or `null` if a field is empty or missing. Types include: `"string" "number" "boolean" "date" "object"`. If a field includes multiple data types (which may occur in GeoJSON), the type of the first non-empty data value is returned. - `this.field_includes()` Tests if a given value occurs at least once in a data field. +- `this.file_exists()` Tests if a file exists. **Example** diff --git a/package-lock.json b/package-lock.json index f18af7872..8d19840e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.95", + "version": "0.5.96", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -469,9 +469,9 @@ "dev": true }, "cached-path-relative": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", - "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz", + "integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==", "dev": true }, "call-bind": { @@ -485,9 +485,9 @@ } }, "camelcase": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", - "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true }, "caseless": { diff --git a/package.json b/package.json index 365003aa0..5f3f5c899 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.95", + "version": "0.5.96", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/classification/mapshaper-classify-methods.js b/src/classification/mapshaper-classify-methods.js new file mode 100644 index 000000000..bcb283676 --- /dev/null +++ b/src/classification/mapshaper-classify-methods.js @@ -0,0 +1,26 @@ +import { stop } from '../utils/mapshaper-logging'; + +var sequential = ['quantile', 'nice', 'equal-interval', 'hybrid']; +var all = ['non-adjacent', 'indexed', 'categorical'].concat(sequential); + +export function getClassifyMethod(opts, dataFieldType) { + var method; + if (opts.method) { + method = opts.method; + } else if (opts.index_field) { + method = 'indexed'; + } else if (opts.categories || dataFieldType == 'string') { + method = 'categorical'; + } else if (dataFieldType == 'number') { + method = 'quantile'; // TODO: validate data field + } else { + stop('Unable to determine which classification method to use.'); + } + if (!all.includes(method)) { + stop('Not a recognized classification method:', method); + } + if (sequential.includes(method) && dataFieldType != 'number') { + stop('The', method, 'method requires a numerical data field'); + } + return method; +} diff --git a/src/classification/mapshaper-classify-ramps.js b/src/classification/mapshaper-classify-ramps.js new file mode 100644 index 000000000..53f62003c --- /dev/null +++ b/src/classification/mapshaper-classify-ramps.js @@ -0,0 +1,85 @@ +import { isColorSchemeName, getColorRamp, getCategoricalColorScheme, isCategoricalColorScheme } from '../color/color-schemes'; +import { parseColor } from '../color/color-utils'; +import { stop, message } from '../utils/mapshaper-logging'; +import utils from '../utils/mapshaper-utils'; +import { + interpolateValuesToClasses +} from '../classification/mapshaper-interpolation'; + +export function getClassValues(method, n, opts) { + var categorical = method == 'categorical' || method == 'non-adjacent'; + var colorArg = opts.colors ? opts.colors[0] : null; + var colorScheme; + + if (isColorSchemeName(colorArg)) { + colorScheme = colorArg; + } else if (colorArg == 'random') { + colorScheme = categorical ? 'Tableau20' : 'BuGn'; // TODO: randomize + } else if (opts.colors) { + // validate colors + opts.colors.forEach(parseColor); + } + + if (categorical) { + if (colorScheme && isCategoricalColorScheme(colorScheme)) { + return getCategoricalColorScheme(colorScheme, n); + } else if (colorScheme) { + // assume we have a sequential ramp + return getColorRamp(colorScheme, n, opts.stops); + } else if (opts.colors || opts.values) { + return getCategoricalValues(opts.colors || opts.values, n); + } else { + // numerical indexes seem to make sense for non-adjacent and categorical colors + return getIndexes(n); + } + } else { + // sequential values + if (colorScheme) { + return getColorRamp(colorScheme, n, opts.stops); + } else if (opts.colors || opts.values) { + return getInterpolableValues(opts.colors || opts.values, n, opts); + } else { + // TODO: rethink this + // return getInterpolableValues([0, 1], n, opts); + return getIndexes(n); + } + } +} + + +function getCategoricalValues(values, n) { + if (n != values.length) { + stop('Mismatch in number of categories and number of values'); + } + return values; +} + +function getIndexes(n) { + var vals = []; + for (var i=0; i colors.length) { - stop(name, 'does not contain', n, 'colors'); + // stop(name, 'does not contain', n, 'colors'); + message('Color scheme has', colors.length, 'colors. Using duplication to match', n, 'categories.'); + colors = wrapColors(colors, n); + } else { + colors = colors.slice(0, n); + } + return colors; +} + +export function wrapColors(colors, n) { + while (colors.length > 0 && colors.length < n) { + colors = colors.concat(colors.slice(0, n - colors.length)); } - return colors.slice(0, n); + return colors; } export function isColorSchemeName(name) { diff --git a/src/commands/mapshaper-classify.js b/src/commands/mapshaper-classify.js index 58e244d2b..b477314e0 100644 --- a/src/commands/mapshaper-classify.js +++ b/src/commands/mapshaper-classify.js @@ -2,24 +2,22 @@ import { stop, message } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import { requireDataField, initDataTable } from '../dataset/mapshaper-layer-utils'; import { getFieldValues } from '../datatable/mapshaper-data-utils'; -import { isColorSchemeName, getColorRamp, getCategoricalColorScheme, isCategoricalColorScheme } from '../color/color-schemes'; -import { parseColor } from '../color/color-utils'; import { getSequentialClassifier, } from '../classification/mapshaper-sequential-classifier'; import { getCategoricalClassifier } from '../classification/mapshaper-categorical-classifier'; +import { getNonAdjacentClassifier } from '../color/graph-color'; import { getIndexedClassifier, - validateClassIndexField + getIndexedClassCount } from '../classification/mapshaper-indexed-classifier'; -import { - interpolateValuesToClasses -} from '../classification/mapshaper-interpolation'; + import cmd from '../mapshaper-cmd'; -import { getUniqFieldValues } from '../datatable/mapshaper-data-utils'; -import { getNonAdjacentClassifier } from '../color/graph-color'; +import { getUniqFieldValues, getColumnType } from '../datatable/mapshaper-data-utils'; +import { getClassValues } from '../classification/mapshaper-classify-ramps'; +import { getClassifyMethod } from '../classification/mapshaper-classify-methods'; cmd.classify = function(lyr, dataset, optsArg) { if (!lyr.data) { @@ -27,58 +25,34 @@ cmd.classify = function(lyr, dataset, optsArg) { } var opts = optsArg || {}; var records = lyr.data && lyr.data.getRecords(); - var nullValue = opts.null_value || null; - var valuesAreColors = !!opts.colors || !!opts.color_scheme; - var colorScheme; - var values, classifyByValue, classifyById; + var valuesAreColors = !!opts.colors; + var dataField, dataFieldType, outputField; + var values, nullValue; + var classifyByValue, classifyByRecordId; var numClasses, numValues; - var dataField, outputField; var method; - // validate explicitly set classes - if (opts.classes) { - if (!utils.isInteger(opts.classes) || opts.classes > 1 === false) { - stop('Invalid number of classes:', opts.classes, '(expected a value greater than 1)'); - } - numClasses = opts.classes; - } - - // TODO: better validation of breaks values - if (opts.breaks) { - numClasses = opts.breaks.length + 1; + if (opts.color_scheme) { + stop('color-scheme is not a valid option, use colors instead'); } + // get data field to use for classification + // if (opts.index_field) { dataField = opts.index_field; - if (numClasses > 0 === false) { - stop('The index-field= option requires the classes= option to be set'); - } - // You can't infer the number of classes by looking at index values; - // this can cause unwanted interpolation if one or more values are - // not present in the index field - // numClasses = validateClassIndexField(records, opts.index_field); - } else if (opts.field) { dataField = opts.field; + dataFieldType = getColumnType(opts.field, records); } - - // expand categories if value is '*' - if (dataField && opts.categories && opts.categories.includes('*')) { - opts.categories = getUniqFieldValues(records, dataField); + if (dataField) { + requireDataField(lyr.data, dataField); } // get classification method // - if (opts.method) { - method = opts.method; - } else if (opts.categories) { - method = 'categorical'; - } else if (opts.index_field) { - method = 'indexed'; - } else { - method = 'quantile'; // TODO: validate data field - } + method = getClassifyMethod(opts, dataFieldType); + // validate classification method if (method == 'non-adjacent') { if (lyr.geometry_type != 'polygon') { stop('The non-adjacent option requires a polygon layer'); @@ -88,83 +62,70 @@ cmd.classify = function(lyr, dataset, optsArg) { } } else if (!dataField) { stop('Missing a data field to classify'); - } else { - requireDataField(lyr.data, dataField); - } - - if (numClasses) { - numValues = opts.continuous ? numClasses + 1 : numClasses; } - // support both deprecated color-scheme= option and colors= syntax - if (opts.color_scheme) { - if (!isColorSchemeName(opts.color_scheme)) { - stop('Unknown color scheme:', opts.color_scheme); - } - colorScheme = opts.color_scheme; - } else if (opts.colors && isColorSchemeName(opts.colors[0])) { - colorScheme = opts.colors[0]; - } else if (opts.colors) { - opts.colors.forEach(parseColor); // validate colors -- error if unparsable - } - - /// get values (usually colors) - /// - if (colorScheme) { - if (method == 'non-adjacent') { - numClasses = numValues = numClasses || 5; - values = getCategoricalColorScheme(colorScheme, numValues); - - } else if (method == 'categorical') { - values = getCategoricalColorScheme(colorScheme, opts.categories.length); - numClasses = numValues = values.length; - - } else { - if (!numClasses) { - // stop('color-scheme= option requires classes= or breaks='); - numClasses = 4; // use a default number of classes - numValues = opts.continuous ? numClasses + 1 : numClasses; - } - values = getColorRamp(colorScheme, numValues, opts.stops); - } - } else if (opts.colors || opts.values) { - values = opts.values ? parseValues(opts.values) : opts.colors; - if (!numValues) { - numValues = values.length; - } - if ((values.length != numValues || opts.stops) && numValues > 1) { - // TODO: handle numValues == 1 - // TODO: check for non-interpolatable value types (e.g. boolean, text) - values = interpolateValuesToClasses(values, numValues, opts.stops); + // get the number of classes and the number of values + // + // expand categories if value is '*' + if (method == 'categorical') { + if ((!opts.categories || opts.categories.includes('*')) && dataField) { + opts.categories = getUniqFieldValues(records, dataField); } - - } else if (numValues > 1) { - // no values were given: assign indexes for each class - values = getIndexValues(numValues); - nullValue = -1; } - if (valuesAreColors) { - nullValue = nullValue || '#eee'; + if (opts.classes) { + if (!utils.isInteger(opts.classes) || opts.classes > 1 === false) { + stop('Invalid number of classes:', opts.classes, '(expected a value greater than 1)'); + } + numClasses = opts.classes; + } else if (method == 'indexed' && dataField) { + numClasses = getIndexedClassCount(records, dataField); + } else if (opts.breaks) { + numClasses = opts.breaks.length + 1; + } else if (method == 'categorical' && opts.categories) { + numClasses = opts.categories.length; + } else if (opts.colors && opts.colors.length > 1) { + numClasses = opts.colors.length; + } else if (opts.values && opts.values.length > 1) { + numClasses = opts.values.length; + } else if (method == 'non-adjacent') { + numClasses = 5; + } else { + numClasses = 4; } - + numValues = opts.continuous ? numClasses + 1 : numClasses; if (numValues > 1 === false) { - stop('Missing a valid number of classes'); + stop('Missing a valid number of values'); } + // get colors or other values + // + values = getClassValues(method, numValues, opts); if (opts.invert) { values = values.concat().reverse(); } - if (valuesAreColors) { message('Colors:', formatValuesForLogging(values)); } + // get null value + // + if ('null_value' in opts) { + nullValue = opts.null_value; + } else if (valuesAreColors) { + nullValue = '#eee'; + } else if (opts.values) { + nullValue = null; + } else { + nullValue = -1; // kludge, to match behavior of getClassValues() + } + + // get a function to convert input data to class indexes // if (method == 'non-adjacent') { - classifyById = getNonAdjacentClassifier(lyr, dataset, values); + classifyByRecordId = getNonAdjacentClassifier(lyr, dataset, values); } else if (opts.index_field) { // data is pre-classified... just read the index from a field classifyByValue = getIndexedClassifier(values, nullValue, opts); @@ -175,13 +136,12 @@ cmd.classify = function(lyr, dataset, optsArg) { } if (classifyByValue) { - classifyById = function(id) { + classifyByRecordId = function(id) { var d = records[id] || {}; return classifyByValue(d[dataField]); }; } - // get the name of the output field // if (valuesAreColors) { @@ -197,23 +157,10 @@ cmd.classify = function(lyr, dataset, optsArg) { } records.forEach(function(d, i) { - d[outputField] = classifyById(i); + d[outputField] = classifyByRecordId(i); }); }; - -// convert strings to numbers if they all parse as numbers -// arr: an array of strings -function parseValues(strings) { - var values = strings; - if (strings.every(utils.parseNumber)) { - values = strings.map(function(str) { - return +str; - }); - } - return values; -} - function formatValuesForLogging(arr) { if (arr.some(val => utils.isString(val) && val.indexOf('rgb(') === 0)) { return formatColorsAsHex(arr); @@ -229,12 +176,3 @@ function formatColorsAsHex(colors) { return o.formatHex(); }); } - - -function getIndexValues(n) { - var vals = []; - for (var i=0; i Date: Thu, 17 Mar 2022 22:08:20 -0400 Subject: [PATCH 187/891] Better console command error messages --- src/cli/mapshaper-parse-commands.js | 8 ++++++-- src/commands/mapshaper-external.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/cli/mapshaper-parse-commands.js b/src/cli/mapshaper-parse-commands.js index 0f3b097c3..653e58366 100644 --- a/src/cli/mapshaper-parse-commands.js +++ b/src/cli/mapshaper-parse-commands.js @@ -24,8 +24,12 @@ export function standardizeConsoleCommands(raw) { // support multiline string of commands pasted into console str = str.split(/\n+/g).map(function(str) { var match = /^[a-z][\w-]*/.exec(str = str.trim()); - if (match && parser.isCommandName(match[0])) { - str = '-' + str; // add hyphen prefix to bare command + //if (match && parser.isCommandName(match[0])) { + if (match) { + // add hyphen prefix to bare command + // also add hyphen to non-command strings, for a better error message + // ("unsupported command" instead of "The -i command cannot be run in the browser") + str = '-' + str; } return str; }).join(' '); diff --git a/src/commands/mapshaper-external.js b/src/commands/mapshaper-external.js index a45fa4ac5..3213e8cf2 100644 --- a/src/commands/mapshaper-external.js +++ b/src/commands/mapshaper-external.js @@ -55,7 +55,7 @@ cmd.runExternalCommand = function(cmdOpts, catalog) { var name = cmdOpts.name; var cmdDefn = externalCommands[name]; if (!cmdDefn) { - stop('Unsupported command'); + stop('Unsupported command:', name); } var targetType = cmdDefn.target; var opts = parseExternalCommand(name, cmdDefn, cmdOpts._); From 9c374fffbd2bf883c1eb9683397a03b6c3e78b6f Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 17 Mar 2022 22:13:48 -0400 Subject: [PATCH 188/891] Add warning on field name collisions (join command) --- src/join/mapshaper-join-tables.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/join/mapshaper-join-tables.js b/src/join/mapshaper-join-tables.js index f58060b36..85f3794d0 100644 --- a/src/join/mapshaper-join-tables.js +++ b/src/join/mapshaper-join-tables.js @@ -242,7 +242,11 @@ export function getFieldsToJoin(destFields, srcFields, opts) { if (!opts.force && !opts.prefix) { // overwrite existing fields if the "force" option is set. // prefix also overwrites... TODO: consider changing this - joinFields = utils.difference(joinFields, destFields); + var duplicateFields = utils.intersection(joinFields, destFields); + if (duplicateFields.length > 0) { + message('Same-named fields not joined without the "force" flag:', duplicateFields); + joinFields = utils.difference(joinFields, duplicateFields); + } } return joinFields; } From 4900accee8fc7cc25dfe725e7766c2888178dd52 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Thu, 17 Mar 2022 22:15:15 -0400 Subject: [PATCH 189/891] Improvements to -classify: random colors, case insensitive color scheme name matching --- .../mapshaper-classify-methods.js | 3 +- .../mapshaper-classify-ramps.js | 48 +++++++++---------- src/color/blending.js | 4 +- src/color/color-schemes.js | 39 +++++++++++++-- src/color/color-utils.js | 18 +++++-- src/commands/mapshaper-dots.js | 4 +- 6 files changed, 78 insertions(+), 38 deletions(-) diff --git a/src/classification/mapshaper-classify-methods.js b/src/classification/mapshaper-classify-methods.js index bcb283676..b3fc3b35f 100644 --- a/src/classification/mapshaper-classify-methods.js +++ b/src/classification/mapshaper-classify-methods.js @@ -14,7 +14,8 @@ export function getClassifyMethod(opts, dataFieldType) { } else if (dataFieldType == 'number') { method = 'quantile'; // TODO: validate data field } else { - stop('Unable to determine which classification method to use.'); + // stop('Unable to determine which classification method to use.'); + stop('Missing a data field and/or classification method'); } if (!all.includes(method)) { stop('Not a recognized classification method:', method); diff --git a/src/classification/mapshaper-classify-ramps.js b/src/classification/mapshaper-classify-ramps.js index 53f62003c..dc8ea280e 100644 --- a/src/classification/mapshaper-classify-ramps.js +++ b/src/classification/mapshaper-classify-ramps.js @@ -1,5 +1,5 @@ -import { isColorSchemeName, getColorRamp, getCategoricalColorScheme, isCategoricalColorScheme } from '../color/color-schemes'; -import { parseColor } from '../color/color-utils'; +import { isColorSchemeName, getColorRamp, getCategoricalColorScheme, isCategoricalColorScheme, pickRandomColorScheme, getRandomColors } from '../color/color-schemes'; +import { validateColor, parseColor } from '../color/color-utils'; import { stop, message } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import { @@ -8,45 +8,41 @@ import { export function getClassValues(method, n, opts) { var categorical = method == 'categorical' || method == 'non-adjacent'; - var colorArg = opts.colors ? opts.colors[0] : null; + var colorArg = opts.colors && opts.colors.length == 1 ? opts.colors[0] : null; var colorScheme; - if (isColorSchemeName(colorArg)) { + if (colorArg == 'random') { + if (categorical) { + return getRandomColors(n); + } + colorScheme = pickRandomColorScheme('sequential'); + message('Randomly selected color ramp:', colorScheme); + } else if (isColorSchemeName(colorArg)) { colorScheme = colorArg; - } else if (colorArg == 'random') { - colorScheme = categorical ? 'Tableau20' : 'BuGn'; // TODO: randomize + } else if (colorArg && !parseColor(colorArg)) { + stop('Unrecognized color scheme name:', colorArg); } else if (opts.colors) { - // validate colors - opts.colors.forEach(parseColor); + opts.colors.forEach(validateColor); } - if (categorical) { - if (colorScheme && isCategoricalColorScheme(colorScheme)) { + if (colorScheme) { + if (categorical && isCategoricalColorScheme(colorScheme)) { return getCategoricalColorScheme(colorScheme, n); - } else if (colorScheme) { - // assume we have a sequential ramp + } else { return getColorRamp(colorScheme, n, opts.stops); - } else if (opts.colors || opts.values) { + } + } else if (opts.colors || opts.values) { + if (categorical) { return getCategoricalValues(opts.colors || opts.values, n); } else { - // numerical indexes seem to make sense for non-adjacent and categorical colors - return getIndexes(n); - } - } else { - // sequential values - if (colorScheme) { - return getColorRamp(colorScheme, n, opts.stops); - } else if (opts.colors || opts.values) { return getInterpolableValues(opts.colors || opts.values, n, opts); - } else { - // TODO: rethink this - // return getInterpolableValues([0, 1], n, opts); - return getIndexes(n); } + } else { + // use numerical class indexes (0, 1, ...) if no values are given + return getIndexes(n); } } - function getCategoricalValues(values, n) { if (n != values.length) { stop('Mismatch in number of categories and number of values'); diff --git a/src/color/blending.js b/src/color/blending.js index daf6d815b..47fa5d294 100644 --- a/src/color/blending.js +++ b/src/color/blending.js @@ -1,6 +1,6 @@ import utils from '../utils/mapshaper-utils'; import { stop } from '../utils/mapshaper-logging'; -import { parseColor } from '../color/color-utils'; +import { parseColor, validateColor } from '../color/color-utils'; import { formatColor } from '../color/color-utils'; export function blend(a, b) { @@ -20,7 +20,7 @@ export function blend(a, b) { weights = normalizeWeights(weights); if (!weights) return '#eee'; var blended = colors.reduce(function(memo, col, i) { - var rgb = parseColor(col); + var rgb = validateColor(col) && parseColor(col); var w = +weights[i] || 0; memo.r += rgb.r * w; memo.g += rgb.g * w; diff --git a/src/color/color-schemes.js b/src/color/color-schemes.js index 59e7dd9d0..c67ce93cf 100644 --- a/src/color/color-schemes.js +++ b/src/color/color-schemes.js @@ -7,7 +7,8 @@ var index = { categorical: [], sequential: [], rainbow: [], - diverging: [] + diverging: [], + all: [] }; var ramps; @@ -27,6 +28,19 @@ function initSchemes() { '3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9'); addCategoricalScheme('Tableau20', '4c78a89ecae9f58518ffbf7954a24b88d27ab79a20f2cf5b43989483bcb6e45756ff9d9879706ebab0acd67195fcbfd2b279a2d6a5c99e765fd8b5a5'); + index.all = [].concat(index.sequential, index.rainbow, index.diverging, index.categorical); + +} + +function standardName(name) { + if (!name) return null; + var lcname = name.toLowerCase(); + for (var i=0; i Date: Thu, 17 Mar 2022 22:31:16 -0400 Subject: [PATCH 190/891] v0.5.97 --- CHANGELOG.md | 3 +++ REFERENCE.md | 10 +++++----- package-lock.json | 2 +- package.json | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7805403bc..5713655f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.97 +* Better warnings and error messages. + v0.5.96 * Added colors=random option to the -classify command. * Added more sensible default behavior to the -classify command. diff --git a/REFERENCE.md b/REFERENCE.md index 98bd8fb97..f7cc19ffe 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -1,6 +1,6 @@ # COMMAND REFERENCE -This documentation applies to version 0.5.91 of mapshaper's command line program. Run `mapshaper -v` to check your version. For an introduction to the command line tool, read [this page](https://github.com/mbloch/mapshaper/wiki/Introduction-to-the-Command-Line-Tool) first. +This documentation applies to version 0.5.97 of mapshaper's command line program. Run `mapshaper -v` to check your version. For an introduction to the command line tool, read [this page](https://github.com/mbloch/mapshaper/wiki/Introduction-to-the-Command-Line-Tool) first. ## Command line syntax @@ -284,7 +284,7 @@ Common options: `target=` ### -classify -Apply quantile, equal-interval or categorical classification to a data field. +Assign colors or data values to each feature using one of several classification methods. Methods for sequential data include `quantile`, `equal-interval`, `hybrid` and `nice` or categorical classification to a data field. `` or `field=` Name of the data field to classify. @@ -292,9 +292,9 @@ Apply quantile, equal-interval or categorical classification to a data field. `values=` List of values to assign to data classes. If the number of values differs from the number of classes given by the (optional) `classes` or `breaks` option, then interpolated values will be calculated. Mapshaper uses d3 for interpolation. -`colors=` Takes a list of CSS colors or the name of a predefined color scheme (the [-colors](#-colors) command lists available color schemes). Similarly to the `values=` option, if the number of listed colors is different from the number of requested classes, interpolated colors are calculated. +`colors=` Takes a list of CSS colors, the name of a predefined color scheme, or `random`. Run the [-colors](#-colors) command to list all of the built-in color schemes. Similar to the `values=` option, if the number of listed colors is different from the number of requested classes, interpolated colors are calculated. -`non-adjacent` Assign colors to a polygon layer in a randomish pattern, trying not to assign the same color to adjacent polygons. Mapshaper's algorithm is not optimal. If mapshaper is unable to avoid giving the same color to neighboring polygons, it will print a warning. You can resolve the problem by increasing the number of colors. +`non-adjacent` Assign colors to a polygon layer in a randomish pattern, trying not to assign the same color to adjacent polygons. Mapshaper's algorithm balances performance and quality. Usually it can find a solution with four or five colors. If mapshaper is unable to avoid giving the same color to neighboring polygons, it will print a warning. You can resolve the problem by increasing the number of colors. `stops=` A pair of values (0-100) for limiting the range of a color ramp. @@ -304,7 +304,7 @@ Apply quantile, equal-interval or categorical classification to a data field. `breaks=` Specify user-defined sequential class breaks (an alternative to automatic classification using `quantile`, `equal-interval`, etc.). -`method=` Classification method. One of: `quantile`, `equal-interval`, `nice` or `hybrid`. +`method=` Classification method. One of: `quantile`, `equal-interval`, `nice`, `hybrid` (sequential data), `categorical`, `non-adjacent` and `indexed`. This parameter is not required if the classification method can be inferred from other options. For example, the `index-field=` parameter implies indexed classification, the `categories=` parameter implies categorical classification. `quantile` Use quantile classification. Shortcut for `method=quantile`. diff --git a/package-lock.json b/package-lock.json index 8d19840e4..1582a83ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.96", + "version": "0.5.97", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5f3f5c899..a1407e18e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.96", + "version": "0.5.97", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From dcebb2f69880d489c6d5afaedabf478992270abb Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 18 Mar 2022 16:53:56 -0400 Subject: [PATCH 191/891] Better handling of null data by -classify command --- .../mapshaper-classify-methods.js | 20 +++++++---- .../mapshaper-classify-ramps.js | 26 ++++++++++++++ src/commands/mapshaper-classify.js | 32 +++++++---------- test/classify-test.js | 36 +++++++++++++++++++ 4 files changed, 88 insertions(+), 26 deletions(-) diff --git a/src/classification/mapshaper-classify-methods.js b/src/classification/mapshaper-classify-methods.js index b3fc3b35f..567557237 100644 --- a/src/classification/mapshaper-classify-methods.js +++ b/src/classification/mapshaper-classify-methods.js @@ -1,26 +1,32 @@ import { stop } from '../utils/mapshaper-logging'; -var sequential = ['quantile', 'nice', 'equal-interval', 'hybrid']; +var sequential = ['quantile', 'nice', 'equal-interval', 'hybrid', 'breaks']; var all = ['non-adjacent', 'indexed', 'categorical'].concat(sequential); -export function getClassifyMethod(opts, dataFieldType) { +export function getClassifyMethod(opts, dataType) { var method; if (opts.method) { method = opts.method; + } else if (opts.breaks) { + method = 'breaks'; } else if (opts.index_field) { method = 'indexed'; - } else if (opts.categories || dataFieldType == 'string') { + } else if (opts.categories || dataType == 'string') { method = 'categorical'; - } else if (dataFieldType == 'number') { + } else if (dataType == 'number') { method = 'quantile'; // TODO: validate data field + } else if (dataType == 'date' || dataType == 'object') { + stop('Data type does not support classification:', dataType); + } else if (dataType === null) { + // data field is empty + return null; // kludge } else { - // stop('Unable to determine which classification method to use.'); - stop('Missing a data field and/or classification method'); + stop('Unable to determine which classification method to use.'); } if (!all.includes(method)) { stop('Not a recognized classification method:', method); } - if (sequential.includes(method) && dataFieldType != 'number') { + if (sequential.includes(method) && dataType != 'number' && dataType !== null) { stop('The', method, 'method requires a numerical data field'); } return method; diff --git a/src/classification/mapshaper-classify-ramps.js b/src/classification/mapshaper-classify-ramps.js index dc8ea280e..4660ac10b 100644 --- a/src/classification/mapshaper-classify-ramps.js +++ b/src/classification/mapshaper-classify-ramps.js @@ -1,4 +1,5 @@ import { isColorSchemeName, getColorRamp, getCategoricalColorScheme, isCategoricalColorScheme, pickRandomColorScheme, getRandomColors } from '../color/color-schemes'; +import { getValueType } from '../datatable/mapshaper-data-utils'; import { validateColor, parseColor } from '../color/color-utils'; import { stop, message } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; @@ -6,6 +7,31 @@ import { interpolateValuesToClasses } from '../classification/mapshaper-interpolation'; +export function getNullValue(opts) { + var nullValue; + if ('null_value' in opts) { + nullValue = parseNullValue(opts.null_value); + } else if (opts.colors) { + nullValue = '#eee'; + } else if (opts.values) { + nullValue = null; + } else { + nullValue = -1; // kludge, to match behavior of getClassValues() + } + return nullValue; +} + +// Parse command line string arguments to the correct data type +function parseNullValue(val) { + if (utils.isString(val) && !isNaN(+val)) { + val = +val; + } + if (val === 'null') { + val = null; + } + return val; +} + export function getClassValues(method, n, opts) { var categorical = method == 'categorical' || method == 'non-adjacent'; var colorArg = opts.colors && opts.colors.length == 1 ? opts.colors[0] : null; diff --git a/src/commands/mapshaper-classify.js b/src/commands/mapshaper-classify.js index b477314e0..c49a72515 100644 --- a/src/commands/mapshaper-classify.js +++ b/src/commands/mapshaper-classify.js @@ -16,7 +16,7 @@ import { import cmd from '../mapshaper-cmd'; import { getUniqFieldValues, getColumnType } from '../datatable/mapshaper-data-utils'; -import { getClassValues } from '../classification/mapshaper-classify-ramps'; +import { getClassValues, getNullValue } from '../classification/mapshaper-classify-ramps'; import { getClassifyMethod } from '../classification/mapshaper-classify-methods'; cmd.classify = function(lyr, dataset, optsArg) { @@ -26,7 +26,7 @@ cmd.classify = function(lyr, dataset, optsArg) { var opts = optsArg || {}; var records = lyr.data && lyr.data.getRecords(); var valuesAreColors = !!opts.colors; - var dataField, dataFieldType, outputField; + var dataField, fieldType, outputField; var values, nullValue; var classifyByValue, classifyByRecordId; var numClasses, numValues; @@ -40,9 +40,10 @@ cmd.classify = function(lyr, dataset, optsArg) { // if (opts.index_field) { dataField = opts.index_field; - } else if (opts.field) { + fieldType = getColumnType(opts.field, records); + } else if (opts.field) { dataField = opts.field; - dataFieldType = getColumnType(opts.field, records); + fieldType = getColumnType(opts.field, records); } if (dataField) { requireDataField(lyr.data, dataField); @@ -50,7 +51,7 @@ cmd.classify = function(lyr, dataset, optsArg) { // get classification method // - method = getClassifyMethod(opts, dataFieldType); + method = getClassifyMethod(opts, fieldType); // validate classification method if (method == 'non-adjacent') { @@ -68,6 +69,7 @@ cmd.classify = function(lyr, dataset, optsArg) { // get the number of classes and the number of values // // expand categories if value is '*' + // use all unique values if categories option is missing if (method == 'categorical') { if ((!opts.categories || opts.categories.includes('*')) && dataField) { opts.categories = getUniqFieldValues(records, dataField); @@ -109,24 +111,16 @@ cmd.classify = function(lyr, dataset, optsArg) { message('Colors:', formatValuesForLogging(values)); } - // get null value - // - if ('null_value' in opts) { - nullValue = opts.null_value; - } else if (valuesAreColors) { - nullValue = '#eee'; - } else if (opts.values) { - nullValue = null; - } else { - nullValue = -1; // kludge, to match behavior of getClassValues() - } - + nullValue = getNullValue(opts); // get a function to convert input data to class indexes // - if (method == 'non-adjacent') { + if (fieldType === null) { + // no valid data -- always return null value + classifyByRecordId = function() {return nullValue;}; + } else if (method == 'non-adjacent') { classifyByRecordId = getNonAdjacentClassifier(lyr, dataset, values); - } else if (opts.index_field) { + } else if (method == 'indexed') { // data is pre-classified... just read the index from a field classifyByValue = getIndexedClassifier(values, nullValue, opts); } else if (method == 'categorical') { diff --git a/test/classify-test.js b/test/classify-test.js index 7550190ba..2e4e535d2 100644 --- a/test/classify-test.js +++ b/test/classify-test.js @@ -26,6 +26,42 @@ describe('mapshaper-classify.js', function () { }) + describe('empty field tests', function() { + it('test 1', function(done) { + var data = [{foo: null}, {foo: null}]; + var cmd = '-i data.json -classify foo breaks=0,2,4 colors=random -o'; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var json = JSON.parse(out['data.json']); + assert.deepStrictEqual(json, [ + {foo: null, fill: '#eee'}, {foo: null, fill: '#eee'}]); + done(); + }); + }) + + it('test 1b', function(done) { + var data = [{foo: NaN}, {foo: NaN}]; + var cmd = '-i data.json -classify foo breaks=0,2,4 colors=random null-value=purple -o'; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var json = JSON.parse(out['data.json']); + assert.deepStrictEqual(json, [ + {foo: null, fill: 'purple'}, {foo: null, fill: 'purple'}]); + done(); + }); + }) + + + it('test 2', function(done) { + var data = [{foo: null}, {foo: null}]; + var cmd = '-i data.json -classify foo null-value=-2 -o'; + api.applyCommands(cmd, {'data.json': data}, function(err, out) { + var json = JSON.parse(out['data.json']); + assert.deepStrictEqual(json, [ + {foo: null, class: -2}, {foo: null, class: -2}]); + done(); + }); + }) + }) + it('error on unknown color scheme', function(done) { var data='value\n1\n2\n3\n4'; api.applyCommands('-i data.csv -classify value colors=blues -o', {'data.csv': data}, function(err, out) { From b38fb72f710efed914fd5ee80f1f23e2f918d5e9 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Fri, 18 Mar 2022 16:55:09 -0400 Subject: [PATCH 192/891] v0.5.98 --- CHANGELOG.md | 3 +++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5713655f0..0e9ef3b35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v0.5.98 +* Better handling of null data by the -classify command. + v0.5.97 * Better warnings and error messages. diff --git a/package-lock.json b/package-lock.json index 1582a83ce..256528b2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.97", + "version": "0.5.98", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a1407e18e..5343b556c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.5.97", + "version": "0.5.98", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", From 4b2b61b480db5bf26fd65061c9299091b4980cfb Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Tue, 22 Mar 2022 23:48:46 -0400 Subject: [PATCH 193/891] Add support for Mapbox basemaps --- .../mapshaper-classify-methods.js | 3 + src/crs/mapshaper-projections.js | 15 +- src/geom/mapshaper-rounding.js | 4 +- src/gui/gui-basemap-control.js | 170 ++++++++++++++++++ src/gui/gui-console.js | 2 +- src/gui/gui-coordinates-display.js | 9 +- src/gui/gui-display-layer.js | 95 +++++++++- src/gui/gui-edit-points.js | 17 +- src/gui/gui-edit-vertices.js | 26 ++- src/gui/gui-map.js | 14 ++ src/gui/gui-shape-hit.js | 2 +- src/gui/gui-shapes.js | 4 +- src/gui/gui-undo.js | 40 ++--- www/basemap.js | 14 ++ www/images/thumb-map.jpg | Bin 0 -> 18360 bytes www/images/thumb-satellite.jpg | Bin 0 -> 28365 bytes www/index.html | 15 +- www/page.css | 89 ++++++++- 18 files changed, 458 insertions(+), 61 deletions(-) create mode 100644 src/gui/gui-basemap-control.js create mode 100644 www/basemap.js create mode 100644 www/images/thumb-map.jpg create mode 100644 www/images/thumb-satellite.jpg diff --git a/src/classification/mapshaper-classify-methods.js b/src/classification/mapshaper-classify-methods.js index 567557237..ce723f0dd 100644 --- a/src/classification/mapshaper-classify-methods.js +++ b/src/classification/mapshaper-classify-methods.js @@ -20,6 +20,9 @@ export function getClassifyMethod(opts, dataType) { } else if (dataType === null) { // data field is empty return null; // kludge + } else if (dataType === undefined) { + // no data field was given + stop('Expected a data field to classify or the non-adjacent option'); } else { stop('Unable to determine which classification method to use.'); } diff --git a/src/crs/mapshaper-projections.js b/src/crs/mapshaper-projections.js index cb4ba46ce..b1916423a 100644 --- a/src/crs/mapshaper-projections.js +++ b/src/crs/mapshaper-projections.js @@ -87,8 +87,8 @@ export function toLngLat(xy, P) { if (isLatLngCRS(P)) { return xy.concat(); } - proj = getProjInfo(P, getCRS('wgs84')); - return proj(xy); + proj = getProjTransform(P, getCRS('wgs84')); + return proj(xy[0], xy[1]); } export function getProjInfo(dataset) { @@ -234,6 +234,17 @@ export function isLatLngCRS(P) { return P && P.is_latlong || false; } +export function isWGS84(P) { + if (!isLatLngCRS(P)) return false; + var proj4 = crsToProj4(P); + return proj4.toLowerCase().includes('84'); +} + +export function isWebMercator(P) { + if (!P) return false; + return crsToProj4(P) == '+proj=merc +a=6378137 +b=6378137'; +} + export function isLatLngDataset(dataset) { return isLatLngCRS(getDatasetCRS(dataset)); } diff --git a/src/geom/mapshaper-rounding.js b/src/geom/mapshaper-rounding.js index 777f4346a..c697c4b4f 100644 --- a/src/geom/mapshaper-rounding.js +++ b/src/geom/mapshaper-rounding.js @@ -43,8 +43,8 @@ export function getRoundingFunction(inc) { } export function getBoundsPrecisionForDisplay(bbox) { - var w = bbox[2] - bbox[0], - h = bbox[3] - bbox[1], + var w = Math.abs(bbox[2] - bbox[0]), + h = Math.abs(bbox[3] - bbox[1]), range = Math.min(w, h) + 1e-8, digits = 0; while (range < 2000) { diff --git a/src/gui/gui-basemap-control.js b/src/gui/gui-basemap-control.js new file mode 100644 index 000000000..47869adea --- /dev/null +++ b/src/gui/gui-basemap-control.js @@ -0,0 +1,170 @@ +import { internal, geom } from './gui-core'; +import { SimpleButton } from './gui-elements'; +import { El } from './gui-el'; + +function loadScript(url, cb) { + var script = document.createElement('script'); + script.onload = cb; + script.src = url; + document.head.appendChild(script); +} + +function loadStylesheet(url) { + var el = document.createElement('link'); + el.rel = 'stylesheet'; + el.type = 'text/css'; + el.media = 'screen'; + el.href = url; + document.head.appendChild(el); +} + +export function Basemap(gui, ext) { + var menu = gui.container.findChild('.basemap-options'); + var list = menu.findChild('.basemap-styles'); + var container = gui.container.findChild('.basemap-container'); + var mapEl = gui.container.findChild('.basemap'); + var extentNote = El('div').addClass('basemap-note').appendTo(container).hide(); + var params = window.mapboxParams; + var map; + var activeStyle; + var loading = false; + + gui.addMode('basemap', turnOn, turnOff, gui.container.findChild('.basemap-btn')); + // model.on('select', function() { + // TODO: hide basemap + // if (gui.getMode() == 'basemap') gui.clearMode(); + // }); + + new SimpleButton(menu.findChild('.close-btn')).on('click', function() { + gui.clearMode(); + turnOff(); + }); + + params.styles.forEach(function(style) { + var btn = El('div').html(`
${style.name}
`); + btn.findChild('.basemap-style-btn').on('click', function() { + updateStyle(style == activeStyle ? null : style); + updateButtons(); + }); + btn.appendTo(list); + }); + + function updateStyle(style) { + activeStyle = style || null; + if (!style) { + gui.map.setDisplayCRS(null); + hide(); + } else if (map) { + map.setStyle(style.url); + refresh(); + } else { + initMap(); + } + } + + function updateButtons() { + list.findChildren('.basemap-style-btn').forEach(function(el, i) { + el.classed('active', params.styles[i] == activeStyle); + }); + } + + function turnOn() { + menu.show(); + } + + function turnOff() { + menu.hide(); + } + + function enabled() { + return !!(mapEl && params); + } + + function show() { + gui.container.addClass('basemap-on'); + mapEl.node().style.display = 'block'; + } + + function hide() { + gui.container.removeClass('basemap-on'); + mapEl.node().style.display = 'none'; + } + + function getBounds() { + var bbox = ext.getBounds().toArray(); + var tr = getLonLat(bbox[2], bbox[3]); + var bl = getLonLat(bbox[0], bbox[1]); + return bl.concat(tr); + } + + function getLonLat(x, y) { + var R = 6378137; + var R2D = 180 / Math.PI; + var lon = x / R * R2D; + var lat = R2D * (Math.PI * 0.5 - 2 * Math.atan(Math.exp(-y / R))); + return [lon, lat]; + } + + function initMap() { + if (!enabled() || map || loading) return; + loading = true; + loadStylesheet(params.css); + loadScript(params.js, function() { + map = new window.mapboxgl.Map({ + accessToken: params.key, + logoPosition: 'bottom-left', + container: mapEl.node(), + style: activeStyle.url, + bounds: getBounds(), + doubleClickZoom: false, + dragPan: false, + dragRotate: false, + scrollZoom: false, + interactive: false, + keyboard: false, + maxPitch: 0, + renderWorldCopies: true // false // false prevents panning off the map + }); + map.on('load', function() { + loading = false; + refresh(); + }); + }); + } + + function checkBounds(bbox) { + var msg; + if (bbox[1] >= -85 && bbox[3] <= 85) { + extentNote.hide(); + return true; + } + if (bbox[1] > 0) msg = 'pan south to see the basemap'; + else if (bbox[3] < 0) msg = 'pan north to see the basemap'; + else msg = msg = 'zoom in to see the basemap'; + extentNote.html(msg).show(); + return false; + } + + function refresh() { + if (!enabled() || !map || loading || !activeStyle) return; + var crs = gui.map.getDisplayCRS(); + if (internal.isWGS84(crs)) { + gui.map.setDisplayCRS(internal.getCRS('webmercator')); + } else if (!internal.isWebMercator(crs)) { + return; + } + var bbox = getBounds(); + if (!checkBounds(bbox)) { + // map does not display outside these bounds + hide(); + } else { + show(); + map.resize(); + map.fitBounds(bbox, {animate: false}); + } + } + + return {refresh: refresh}; // called by map when extent changes +} + + diff --git a/src/gui/gui-console.js b/src/gui/gui-console.js index 1bb26e707..a9c1a4636 100644 --- a/src/gui/gui-console.js +++ b/src/gui/gui-console.js @@ -88,8 +88,8 @@ export function Console(gui) { // when an instance loses focus. internal.setLoggingFunctions(consoleMessage, consoleError, consoleStop); gui.container.addClass('console-open'); - gui.dispatchEvent('resize'); el.show(); + gui.dispatchEvent('resize'); input.node().focus(); history = getHistory(); } diff --git a/src/gui/gui-coordinates-display.js b/src/gui/gui-coordinates-display.js index 75c4582a7..9d46e46a0 100644 --- a/src/gui/gui-coordinates-display.js +++ b/src/gui/gui-coordinates-display.js @@ -37,15 +37,18 @@ export function CoordinatesDisplay(gui, ext, mouse) { function onMouseChange(e) { if (!enabled) return; if (isOverMap(e)) { - displayCoords(ext.translatePixelCoords(e.x, e.y)); + displayCoords(gui.map.translatePixelCoords(e.x, e.y)); } else { clearCoords(); } } function displayCoords(p) { - var decimals = internal.getBoundsPrecisionForDisplay(ext.getBounds().toArray()); - var str = internal.getRoundedCoordString(p, decimals); + var p1 = gui.map.translatePixelCoords(0, ext.height()); + var p2 = gui.map.translatePixelCoords(ext.width(), 0); + var bbox = p1.concat(p2); + var decimals = internal.getBoundsPrecisionForDisplay(bbox); + var str = internal.getRoundedCoordString(p, decimals); readout.text(str).show(); } diff --git a/src/gui/gui-display-layer.js b/src/gui/gui-display-layer.js index aa83afd31..ff9130cd4 100644 --- a/src/gui/gui-display-layer.js +++ b/src/gui/gui-display-layer.js @@ -1,9 +1,86 @@ -import { MultiScaleArcCollection } from './gui-shapes'; +import { enhanceArcCollectionForDisplay } from './gui-shapes'; import { getDisplayLayerForTable } from './gui-table'; import { needReprojectionForDisplay, projectArcsForDisplay, projectPointsForDisplay } from './gui-dynamic-crs'; import { filterLayerByIds } from './gui-layer-utils'; import { internal, Bounds, utils } from './gui-core'; +export function insertVertex(lyr, id, dataPoint) { + internal.insertVertex(lyr.source.dataset.arcs, id, dataPoint); + if (isProjectedLayer(lyr)) { + internal.insertVertex(lyr.arcs, id, lyr.projectPoint(dataPoint[0], dataPoint[1])); + } +} + +export function deleteVertex(lyr, id) { + internal.deleteVertex(lyr.arcs, id); + if (isProjectedLayer(lyr)) { + internal.deleteVertex(lyr.source.dataset.arcs, id); + } +} + +export function translateDisplayPoint(lyr, p) { + return isProjectedLayer(lyr) ? lyr.invertPoint(p[0], p[1]) : p; +} + +export function getVertexCoords(lyr, id) { + return lyr.source.dataset.arcs.getVertex2(id); +} + +export function setVertexCoords(lyr, ids, dataPoint) { + internal.snapVerticesToPoint(ids, dataPoint, lyr.source.dataset.arcs, true); + if (isProjectedLayer(lyr)) { + var p = lyr.projectPoint(dataPoint[0], dataPoint[1]); + internal.snapVerticesToPoint(ids, p, lyr.arcs, true); + } +} + +export function updateVertexCoords(lyr, ids) { + if (!isProjectedLayer(lyr)) return; + var p = lyr.arcs.getVertex2(ids[0]); + internal.snapVerticesToPoint(ids, lyr.invertPoint(p[0], p[1]), lyr.source.dataset.arcs, true); +} + +export function makePointSetter(lyr, fid) { + var dataShp = internal.cloneShape(lyr.source.layer.shapes[fid]); + var displayShp = isProjectedLayer(lyr) ? internal.cloneShape(lyr.layer.shapes[fid]) : null; + + return function() { + lyr.source.layer.shapes[fid] = dataShp; + if (isProjectedLayer(lyr)) { + if (displayShp) { + // case: layer is projected and we have saved projected coordinates + // (assumes projection has not changed) + lyr.layer.shapes[fid] = displayShp; + } else { + // case: layer was projected after this setter was created -- + // need to project the original data coords + lyr.layer.shapes[fid] = projectPointShape(dataShp, lyr.projectPoint); + } + } + }; +} + +function isProjectedLayer(lyr) { + // TODO: could do some validation on the layer's contents + return !!(lyr.source && lyr.invertPoint); +} + +// Update data coordinates by projecting display coordinates +export function updatePointCoords(lyr, fid) { + if (!isProjectedLayer(lyr)) return; + var displayShp = lyr.layer.shapes[fid]; + lyr.source.layer.shapes[fid] = projectPointShape(displayShp, lyr.invertPoint); +} + +function projectPointShape(src, proj) { + var dest = [], p; + for (var i=0; i -1 && gui.interaction.getMode() == 'location'; @@ -8,24 +10,27 @@ export function initPointDragging(gui, ext, hit) { hit.on('dragstart', function(e) { if (!active(e)) return; - gui.dispatchEvent('symbol_dragstart', {FID: e.id}); + symbolInfo = {FID: e.id, target: hit.getHitTarget()}; + gui.dispatchEvent('symbol_dragstart', symbolInfo); }); hit.on('drag', function(e) { if (!active(e)) return; - var lyr = hit.getHitTarget().layer; - var p = getPointCoordsById(e.id, lyr); + // TODO: support multi points... get id of closest part to the pointer + var p = getPointCoordsById(e.id, symbolInfo.target.layer); if (!p) return; var diff = translateDeltaDisplayCoords(e.dx, e.dy, ext); p[0] += diff[0]; p[1] += diff[1]; gui.dispatchEvent('map-needs-refresh'); - gui.dispatchEvent('symbol_drag', {FID: e.id}); + // gui.dispatchEvent('symbol_drag', {FID: e.id}); }); hit.on('dragend', function(e) { - if (!active(e)) return; - gui.dispatchEvent('symbol_dragend', {FID: e.id}); + if (!active(e) || !symbolInfo ) return; + updatePointCoords(symbolInfo.target, symbolInfo.FID); + gui.dispatchEvent('symbol_dragend', symbolInfo); + symbolInfo = null; }); function translateDeltaDisplayCoords(dx, dy, ext) { diff --git a/src/gui/gui-edit-vertices.js b/src/gui/gui-edit-vertices.js index 7c3520481..e1ada52b0 100644 --- a/src/gui/gui-edit-vertices.js +++ b/src/gui/gui-edit-vertices.js @@ -1,4 +1,5 @@ import { error, internal, geom, utils } from './gui-core'; +import { updateVertexCoords, insertVertex, getVertexCoords, translateDisplayPoint, deleteVertex } from './gui-display-layer'; // pointer thresholds for hovering near a vertex or segment midpoint var HOVER_THRESHOLD = 8; @@ -36,8 +37,11 @@ export function initVertexDragging(gui, ext, hit) { if (pixelDist > HOVER_THRESHOLD) { return null; } - var points = nearestIds.map(function(i) {return target.arcs.getVertex2(i);}); + var points = nearestIds.map(function(i) { + return getVertexCoords(target, i); // data coordinates + }); return { + target: target, ids: nearestIds, points: points }; @@ -47,8 +51,7 @@ export function initVertexDragging(gui, ext, hit) { var target = hit.getHitTarget(); if (!target.arcs.isFlat()) return null; // vertex insertion not supported with simplification var p = ext.translatePixelCoords(e.x, e.y); - var shp = target.layer.shapes[e.id]; - var midpoint = findNearestMidpoint(p, shp, target.arcs); + var midpoint = findNearestMidpoint(p, e.id, target); if (!midpoint || midpoint.distance / ext.getPixelSize() > MIDPOINT_THRESHOLD) return null; return midpoint; @@ -58,8 +61,9 @@ export function initVertexDragging(gui, ext, hit) { if (!active()) return; if (insertionPoint) { var target = hit.getHitTarget(); - internal.insertVertex(target.arcs, insertionPoint.i, insertionPoint.point); + insertVertex(target, insertionPoint.i, insertionPoint.point); dragInfo = { + target: target, insertion: true, ids: [insertionPoint.i], points: [insertionPoint.point] @@ -91,6 +95,7 @@ export function initVertexDragging(gui, ext, hit) { // kludge to get dataset to recalculate internal bounding boxes hit.getHitTarget().arcs.transformPoints(function() {}); clearHoverVertex(); + updateVertexCoords(dragInfo.target, dragInfo.ids); gui.dispatchEvent('vertex_dragend', dragInfo); gui.dispatchEvent('map-needs-refresh'); dragInfo = null; @@ -108,9 +113,10 @@ export function initVertexDragging(gui, ext, hit) { return; } gui.dispatchEvent('vertex_delete', { + target: target, vertex_id: vId }); - internal.deleteVertex(target.arcs, vId); + deleteVertex(target, vId); clearHoverVertex(); gui.dispatchEvent('map-needs-refresh'); }); @@ -128,7 +134,7 @@ export function initVertexDragging(gui, ext, hit) { // if hovering near a segment midpoint: show the midpoint and save midpoint info insertionPoint = findVertexInsertionPoint(e); if (insertionPoint) { - hit.setHoverVertex(insertionPoint.point); + hit.setHoverVertex(insertionPoint.displayPoint); } else { // pointer is not over a vertex: clear any hover effect clearHoverVertex(); @@ -139,7 +145,9 @@ export function initVertexDragging(gui, ext, hit) { // Given a location @p (e.g. corresponding to the mouse pointer location), // find the midpoint of two vertices on @shp suitable for inserting a new vertex -function findNearestMidpoint(p, shp, arcs) { +function findNearestMidpoint(p, fid, target) { + var arcs = target.arcs; + var shp = target.layer.shapes[fid]; var minDist = Infinity, v; internal.forEachSegmentInShape(shp, arcs, function(i, j, xx, yy) { var x1 = xx[i], @@ -148,6 +156,7 @@ function findNearestMidpoint(p, shp, arcs) { y2 = yy[j], cx = (x1 + x2) / 2, cy = (y1 + y2) / 2, + midpoint = [cx, cy], dist = geom.distance2D(cx, cy, p[0], p[1]); if (dist < minDist) { minDist = dist; @@ -155,7 +164,8 @@ function findNearestMidpoint(p, shp, arcs) { i: (i < j ? i : j) + 1, // insertion point segment: [i, j], segmentLen: geom.distance2D(x1, y1, x2, y2), - point: [cx, cy], + displayPoint: midpoint, + point: translateDisplayPoint(target, midpoint), distance: dist }; } diff --git a/src/gui/gui-map.js b/src/gui/gui-map.js index 57d3d1485..ea9966881 100644 --- a/src/gui/gui-map.js +++ b/src/gui/gui-map.js @@ -16,6 +16,7 @@ import { utils, internal, Bounds } from './gui-core'; import { EventDispatcher } from './gui-events'; import { ElementPosition } from './gui-element-position'; import { MouseArea } from './gui-mouse'; +import { Basemap } from './gui-basemap-control'; import { GUI } from './gui-lib'; utils.inherit(MshpMap, EventDispatcher); @@ -38,6 +39,8 @@ export function MshpMap(gui) { _inspector, _stack, _dynamicCRS; + var _basemap = new Basemap(gui, _ext); + if (gui.options.showMouseCoordinates) { new CoordinatesDisplay(gui, _ext, _mouse); } @@ -62,6 +65,8 @@ export function MshpMap(gui) { model.on('update', onUpdate); + + // Update display of segment intersections this.setIntersectionLayer = function(lyr, dataset) { if (lyr == _intersectionLyr) return; // no change @@ -79,6 +84,12 @@ export function MshpMap(gui) { target.layer.pinned = !!pinned; }; + this.translatePixelCoords = function(x, y) { + var p = _ext.translatePixelCoords(x, y); + if (!_dynamicCRS) return p; + return internal.toLngLat(p, _dynamicCRS); + }; + this.getCenterLngLat = function() { var bounds = _ext.getBounds(); var crs = this.getDisplayCRS(); @@ -131,6 +142,7 @@ export function MshpMap(gui) { // Update map extent (also triggers redraw) projectMapExtent(_ext, oldCRS, this.getDisplayCRS(), getFullBounds()); + _fullBounds = getFullBounds(); // update this so map extent doesn't get reset after next update }; // Refresh map display in response to data changes, layer selection, etc. @@ -184,6 +196,7 @@ export function MshpMap(gui) { needReset = mapNeedsReset(fullBounds, _fullBounds, _ext.getBounds(), e.flags); } + if (isFrameView()) { _nav.setZoomFactor(0.05); // slow zooming way down to allow fine-tuning frame placement // 0.03 _ext.setFrame(getFullBounds()); // TODO: remove redundancy with drawLayers() @@ -223,6 +236,7 @@ export function MshpMap(gui) { } _ext.on('change', function(e) { + _basemap.refresh(); // keep basemap synced up (if turned on) if (e.reset) return; // don't need to redraw map here if extent has been reset if (isFrameView()) { updateFrameExtent(); diff --git a/src/gui/gui-shape-hit.js b/src/gui/gui-shape-hit.js index e65df23de..40ceeefd3 100644 --- a/src/gui/gui-shape-hit.js +++ b/src/gui/gui-shape-hit.js @@ -125,7 +125,7 @@ export function getShapeHitTest(displayLayer, ext, interactionMode) { hits.push(id); } }); - // console.log(hitThreshold, bullseye); + // TODO: add info on what part of a shape gets hit return { ids: utils.uniq(hits) // multipoint features can register multiple hits }; diff --git a/src/gui/gui-shapes.js b/src/gui/gui-shapes.js index 09cb60e74..6411bcdad 100644 --- a/src/gui/gui-shapes.js +++ b/src/gui/gui-shapes.js @@ -2,7 +2,7 @@ import { internal } from './gui-core'; // Create low-detail versions of large arc collections for faster rendering // at zoomed-out scales. -export function MultiScaleArcCollection(unfilteredArcs) { +export function enhanceArcCollectionForDisplay(unfilteredArcs) { var size = unfilteredArcs.getPointCount(), filteredArcs, filteredSegLen; @@ -39,6 +39,4 @@ export function MultiScaleArcCollection(unfilteredArcs) { useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5; return useFiltering ? filteredArcs : unfilteredArcs; }; - - return unfilteredArcs; } diff --git a/src/gui/gui-undo.js b/src/gui/gui-undo.js index 70de8ef41..1090cfd4f 100644 --- a/src/gui/gui-undo.js +++ b/src/gui/gui-undo.js @@ -1,4 +1,6 @@ import { internal } from './gui-core'; +import { makePointSetter, setVertexCoords, getVertexCoords, insertVertex, deleteVertex, translateDisplayPoint } from './gui-display-layer'; + // import { cloneShape } from '../paths/mapshaper-shape-utils'; // import { copyRecord } from '../datatable/mapshaper-data-utils'; var snapVerticesToPoint = internal.snapVerticesToPoint; @@ -15,7 +17,6 @@ export function Undo(gui) { offset = 0; } - function isUndoEvt(e) { return (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key == 'z'; } @@ -37,17 +38,16 @@ export function Undo(gui) { e.stopPropagation(); e.preventDefault(); } - }, this, 10); // undo/redo point/symbol dragging // gui.on('symbol_dragstart', function(e) { - stashedUndo = this.makePointSetter(e.FID); + stashedUndo = makePointSetter(e.data.target, e.FID); }, this); gui.on('symbol_dragend', function(e) { - var redo = this.makePointSetter(e.FID); + var redo = makePointSetter(e.data.target, e.FID); this.addHistoryState(stashedUndo, redo); }, this); @@ -75,35 +75,33 @@ export function Undo(gui) { }, this); gui.on('vertex_dragend', function(e) { - var target = gui.model.getActiveLayer(); - var arcs = target.dataset.arcs; - var startPoint = e.points[0]; - var endPoint = internal.getVertexCoords(e.ids[0], arcs); + var target = e.data.target; + var startPoint = e.points[0]; // in data coords + var endPoint = getVertexCoords(target, e.ids[0]); var undo = function() { if (e.insertion) { - internal.deleteVertex(arcs, e.ids[0]); + deleteVertex(target, e.ids[0]); } else { - snapVerticesToPoint(e.ids, startPoint, arcs, true); + setVertexCoords(target, e.ids, startPoint); } }; var redo = function() { if (e.insertion) { - internal.insertVertex(arcs, e.ids[0], e.points[0]); + insertVertex(target, e.ids[0], endPoint); } - snapVerticesToPoint(e.ids, endPoint, arcs, true); + setVertexCoords(target, e.ids, endPoint); }; this.addHistoryState(undo, redo); }, this); gui.on('vertex_delete', function(e) { - var target = gui.model.getActiveLayer(); - var arcs = target.dataset.arcs; - var p = arcs.getVertex2(e.vertex_id); + // get vertex coords in data coordinates (not display coordinates); + var p = getVertexCoords(e.data.target, e.vertex_id); var redo = function() { - internal.deleteVertex(arcs, e.vertex_id); + deleteVertex(e.data.target, e.vertex_id); }; var undo = function() { - internal.insertVertex(arcs, e.vertex_id, p); + insertVertex(e.data.target, e.vertex_id, p); }; this.addHistoryState(undo, redo); }, this); @@ -112,14 +110,6 @@ export function Undo(gui) { reset(); }; - this.makePointSetter = function(i) { - var target = gui.model.getActiveLayer(); - var shp = cloneShape(target.layer.shapes[i]); - return function() { - target.layer.shapes[i] = shp; - }; - }; - this.makeDataSetter = function(id) { var target = gui.model.getActiveLayer(); var rec = copyRecord(target.layer.data.getRecordAt(id)); diff --git a/www/basemap.js b/www/basemap.js new file mode 100644 index 000000000..be29d296d --- /dev/null +++ b/www/basemap.js @@ -0,0 +1,14 @@ +window.mapboxParams = { + js: 'https://api.mapbox.com/mapbox-gl-js/v2.7.0/mapbox-gl.js', + css: 'https://api.mapbox.com/mapbox-gl-js/v2.7.0/mapbox-gl.css', + key: 'pk.eyJ1IjoiZ3JhbW1hdGEiLCJhIjoiY2wxMDNwbTVtMGRoZTNjbXQwaXU1amFrOSJ9.yH9yFKkse0gg64coHMmIuw', + styles: [{ + name: 'Map', + icon: 'images/thumb-map.jpg', + url: 'mapbox://styles/grammata/cl0ymri0f000214lc1mgtweqk' + }, { + name: 'Satellite', + icon: 'images/thumb-satellite.jpg', + url: 'mapbox://styles/grammata/cl0ymvkkl004115mlgf5o442n' + }] +}; diff --git a/www/images/thumb-map.jpg b/www/images/thumb-map.jpg new file mode 100644 index 0000000000000000000000000000000000000000..68f2753acd947ce1c195f89c75bc40ec29f71d14 GIT binary patch literal 18360 zcma%?byQnX*XDy$v{2kz+zUmELvau8g+OsA?p6vE3+^t#p~ZqrahKr1y|}v#@B7Wn znm=b}-Mg~RwPf9W_dVx%e&=QWWfAZbfQ@80c?a z104ec>p$_j#>U3N!uwAU{NF%8MnOSGL&wFyz$L)J!TG=K|KDF;mI25Brk7&?j)b|R znFYYS6951pymSKa&;h3i;z$5I00JHYA|Ap^4jm);@c?3g8{(BZ7YG8=8#!j)H$^#`(9m9>-Vwjt-*5~}EYn~2+Vg{4Ef1ii z2Ik+8Zau>lYlRektc0G2R>gq8BeAe?kpoenafVX^)qa&|R<+D3tvqKM*8Jc@6}HhG zEAyEYX8vB8bM;%<`_-pCm$G#fKhfo9qq9FoK?UwWz|s6gOeJ%YP}F;lxDuHa_Uyd( ztV4x8P=kZ*n8xz7@y4-r|B~u`;@RA)Vw*)`>YcCT)2Bk-^fQ;_hX?YP?j9K$dtq2L5Zb1LAIivl>xTwQ6pp+F zdoDTM@JvD9Fg9%-h=Wd)qNQmu^$6v$iUp~p9yF_rLv*l-9Ql$S#d;r0M6TxfLn4{i zdSezgp-drhHih&6xwT}e)6)LZHQ>UaE&i7?J_^vp9T`fx4M8ghQumAK=C`5C(s<6U>!9Rsecfg8V?~qw z(*`8(XYFzR8B?4@n<68u%OQ~FF8^o?k8fc0NtJ7mpzbMMrJnue?kZE7VIE!hwS-%l zHg=Y#PAJ$h)tyQ*rEDJ!D__zqb(>BQZ892B%}7B|YKcNa*;Xk&@Y`TDv($NGAB)Y| z6=WDK?bo3I$-PEMNn?9dC@ooDJ>NL@(U&!5;4zK`Ru zB<+tLR63uI7*82V3d^7VFhF%jEYhXV2gdg#9QfppauL_w1$T~%{973Zy1!`vYcnth zbDcXpPQl%(KQs^xeM@w3n#Zw%7f#AsSZB8QOATmPI&>Gwiz^_iVM*9r_$r?kmmd31 zS8DbfR2y`RHrEi;N9N$FZ@ik6>Q!8~WJTKVfL@efXjBa80QdTym41({ST zpW4Nd(x+}^%R`20@tH=d-uhckXx=;9xv&hshmzp=mie5s8d>|SD)nQzM{bM`Uz0pf zdpldbaeMR5wdZFz!nbi6gh7UAhMkhIGS`;DC>=cBb^|+&C_>tqFp_7t#5>aQ1hTq# zRB=i<&lr}20h(D@)hLSchh9gv)}WAQ+D=Hle6@?clwHh-Aed42X&_4l! zOYNf<*!4~rV;IOvbqIveKQY_%V7N*-9aPuGQcH-2|yKI0cYJka8x#&9|**9&K+&*5~k<2AVvW|9LGPgxfi8iWq z9bHb>XuP_k)7x%XvLq2aAzpYT;!aUVBx5Xon*V0Xk=hv!_x)SJ)>>vJ>DI3ajuPR5 zZePaF|4pQqoy7=;%ljwAo4HyqJjGt`q0eyUk!)J3fexm}f0v4R1O`UnMe_@!V8!nJ zGfd11^pZ@cIQT6$O|U>S zw}e&iytu{=$ZW-bN!Z&&#$z}Nz5sNW0xDHJ+<$MR$Ljq3)^C^TR||BD+RXWEZg!GJ z*rw*9!202mzJ|7`i`lUrEpK-4tXUf`rW8`|#OXsgTHi8?C9@tj6nC!T`#ht&KfTQs zabP4WCDvhs#n9S^Cp$=TseW)(-sGoBI^wf@WvW(Psj~P}fnPZUD?+pQ= zmK(9U^%Iw^Bl{B2$k=#@rnSoeZdX< z85gve-jZ0WFC(zvQExi!ouL|AH%Z@{3yLjYq81e-xsPZw)w>uctngj|J62Q}t9s0E zPHxvX>~d5;_Jai~?54adghc+tMM`!%-6d3y;%8-?wTSyN1#^5cjRY?2T||q)scI|O zd+x>L#yu>N&lf!pxS1?NpxJMVhAcWDMHzEy`!(ZJQ+qdLjOxE0)h*l`&)$s|IEp%B$cj}u?+unWOxUk^x<1g;M8}qW?E5HwXzS;CFsIgA#~?e5 z8{{Q;0eQd4_f7HFW-JqVRwLs8#*{Mqk*Z~>szt(52M^8Zfmh z>HnzjSw^D=iWI^jRCSU#$v^UP+DF^gDm<^~x&b!Acah>%P307AiGLCKa}5kqg=Vr9 zTkJuY-iRwmyi8xsW>lzJ3%m0h_|1yVgZYyFt|dHQnJ{Qut|t8r_1L?v^p2_Ao(ztfmPs^y; z4GPxNp6`5G&yDg1{SfnPg0?_my+%SaE~=I zvA#8VSE+hAJvOuqZK70GWl%Whj5!8=y(PmeC4#Ao=x!--`r%dlEtDQI+Xl zS?)G2-D_KB+rXy}jix(WT#-f=Jzr4TAk%9ivob|3!PZEz z-~cX@Hlst-ToqHPd^|e`wO*^!G3w$5uieA;i%)7Ojclu8td~Etapna@*Dp-nNRpaH zYiweP;W3n9btL+I?Ee1805eU!s>gP7r~|Ps^)?l0{@oYruW_V-X5(#qnNhn#5e!SX zHIh0I$AK5Xna|1dnUt`v?a_N*oPvM$k~Am_?Y6+@Z-NOMq?a3UyO*~5P1BY4KURyH zt!Z8W&1<(dG1n4sZOgOd9>^#eak2oE2$p|&QJG~}wsr$m_! z%sW>{tzQi7e9sTX`$XmIg%iP{SnXfP%naHz;HWKrrB{|3Rt?f3{R%gZt{Ax=-@X4f zc(DZ(^sv*g{ye9D72eUh_t3)5C|}o<3?s3O>`#{oTu-|=-{(;$$p`Hg#5f5J!^^K`6#^jVJN>ce<0Zre- zazcxn^WPCrqcZemQa+nc|~3lB?3`0Z!kdnD%aq4 zUf4sNEeH82_>Q`b6W0er(aY>EadX<4`G`-KHkb#Y^LFFbU|+;ar&_hr?OQM`MVk+P zIBv(;;HiUE+zD(~c?#kQ-4QMEJ}LXmeNyCjRQ3C)p0&IrU5HhHL}5ppL^;UWC8t-y z;#-n;d){x?n=h{ZS$;kYsSS?URn?`OC+r){Nx+paF+bC#?cU@=hPK2H4J*dIO}{X|uFTY7{PfC7k{`TM0&iF&A`zm)9w|Ch&dl==%DB8Ap?@Z1E_9#=n0b)b-XAr~E+L ze-56T_Ljwp>24mdadbMSf9DSdk#Nw8KZP>{IkNIl>*&tw`6$-UxIL8G@nk7|ava&^aRwcm#)mlhqoT{a#p-AmS~bcEPA zM~Uc_^ZZ!N}f{F9Q#p)Ok)Gx~~n zzt+x+lT7C$X0;ab3Pexd#08#F-}o0CD9)(lff7~|meLFP!NdvlGH;!APtkVew&uN! z6X>`H1NZ*anhCG*?ZAFFc{NXCulSy5>v!c{T zQBlq@2Ck5@Jv5Sy`tjP>>kv zrmGQOOe^d~#$)8q;Ro-#TTiyN+G?p8))y*Ej-f>}r3H&Ha~Kt>EsU2)d-2xU`(e~b zeILU5MZSYj9hc6Ae@&O}%MX-sWQVESGe$Zb=S4Ig4rlit$XyaatW8T7AD|Em!f7Q5 zFx*uSAl4&=6s#((>;Ue9Grf=27MP~9DnKu5MIpI_6f~%}CWHjhnJZ8&QSn+42O$7CXSlwjB8PhA_X%w%kX>fZHr*=nmUt?vBO04dMU=dg4b9KLUG z%dXB*mGtF?C0A}yRISI^Nd;rs{s_CvhX1=v!$2rgl^hjQG*}@xDoE}HFcC#GtD~{J z2=B)pU0J2t;6>O*|Jag>m5Jd@*sNo8*{@_HT~8@1Q`^{#7H%GbNpzp-ayOx$Y`5X5 zE}1-gMn`lOMWSM|hdLajhP>vFsM|L~gLd_+8}gzHp)`DA^L{^HG4u$}j{hw}b3} zkGpOfN&t2*Yu`H%N<*qcxEqu0^3SV|+DVGPALj&XMYlD{C9i0iyaC`DXrFFo?g;er>ey_9MHh;e!qP+lI(t3Unb)&$Ve*E-wW;^ zws&OUc4#_pGWbo2ng;33TZk`jKQ>tz2D>~3RyYqQIfha&UJHMEgAnSHiHo+zBvI61K&qa1oU|Et20wla;D4+6K-US6&7Kg<> z8|Ir7UuXR9;<6|AlPXpjrLq>LKX5;!vr@SZwr3R?b<;KkdpueHYeJL<4bnYXDTKZNw3^fZ?ZI_N z!kEszf_G!Yg-0F1_{y__miE(B;}X!Io;fxL2WOm}F(fR!7~vm6)2s2TxYS@(qt64? z`SPv^iM}1Z5r1boW=&BF*=la%L_L^~#aNJO*|@)a?~b9})*KtfbXW9b-~W-QEj^=` z$`Vz5h{8h=G2oGUy;btOlMGifP;`&O#aY4;ehhonqMhdg|MFd1?FhNeJXT7%9v<)5AcWA8%Zq%;q2)xj(F=V4U7m?vZUqVEpSl0N>66#(ZI&WCIVdyP zC*HM=U0#d35t-ZfF1zd*IV#`$Ty)iFk{(F`WA1jbF;%ug*(yF+dY2DI#=x_8(3Rc+D+o9Y?ppcZHTkK}A}oF`fWN!#iM-yIrfA9D~|F zab(LM(maC;B#cojtNrBmjq>x&Tf2_rs-h+~EuVj-Gvo(;UuEJx!9+UNMYL-%;BYw)UQX@2p##(5A4@QCzzT9nwW9-0_O}o-| z8{_8fVOHgc@mo2bH8tdJne}xpLT}1R4l37#a{xv19^Q5^OTK# z5LoP!hCaw$_)lT!pD0{5FQ8TiqCos8Ry;+6UCck``!t?e*_%Q{Vt4?74;7a#M{3F zrqRYzJnAmxIH@E)T7=*53W^Crt^<@h+sS--n);v9@6(Z=@E07gR#<-!Y*8)+urQAv zWrQ|hLnY;mCw~LfQ6@w$-#&Nos+Mj|=5OD=8)wV;MB$BtBEqmJRiyNLq{HdSnsoSf zY&#aw`Qu73^&>2D2*2!GBJ${~dGsH#oB9QIqg4>N!*EAOuDViH-Iv^4AATd@LN?HD z8YtQSbcjijsm|~-{S#qpJa#umw%rm2Un$?xKZ?4iUO?~lIAF1YcQqobj&}S>f!xj zVbRUEs)Qq~JjzYv)%SP7|D;)YfQJg$8Xl3OwyINIC(-1+8P>a0VN|JT7nM#2QC*9BN&|eA$Q6oF@0fAH~Xq$K|Eus_~d~2ZSri)QB<^*n$X%nKO zGcQqu+@n8lIKH{TRnW2pMnxF5W$W?FZliRAyBOm{i;}eI-LP$7J!Q+8U3coI35~y9 z`fTju1T7gW;EJZDG4E6}@dd5Mu{u(mz6_h$a^j4?Z-|nnQUvV?Z)6%ZBl0#|;@5t~ z?b$^lnBrTJJJR_SuSarPzQ{wFFdMo<`tdh$Cn^DWO+9~`RG$6A_krIzhW8Wnajmf4 zcb=keS+lxDnlADwt1~i!$yxGa5(zLZ02IY+Z1VWx@fHLh;ZEX140g+G+){TX=QYa_S? zp_RSmxIoiop3_BH?4R)!>8&r}X}{@i1dm}lTAd&RA5|ckUy5Yv5th{mnaCYnRjiU@ zrUYw(N5Jkly5(bW-9oN4SIXY#9>WBa^_J9l0W) ze;C^+^X|Gtr^LLP_TgvqM1;yL+C<^3P%nrUC#Wu)ii6hd-!4v3eCuH*)WYbvZW^Dh z_!E5VwMV0%3b>Cb4N!M__(`9mndnJ${Z8srrY8(5AJ82<(Kbuh zr?W4+DE^ma5CXj%(qJsfV%mKXX1m+#ynSr9$==Y^t!0YB(D#{0omwI{xDZgXiF(Rl z&b0Pu(SZBP75hYxWDv!h;7^=IlZyu$0X7_BUOF1Zua!HAezd-9(SH;oH_jnh%3^qz z5HUrv*CkJlHsx3C1KcNXX|YF>{_(J!6 zU%#r#JILT@t)4vOiQ|_a`9MtkOlk(H?O@*qGf6iDOf@&yH*nM3$sJwiS9zoK&@)(w zyJySps%S1Ong^MwvyUU4_U~BRUHvu)wH^;={A-t$=q_-c>+?*MEJYVGp0;_U%$E+0 zTOs|h`n_ok1vGp%>XLBV z2geTH_pmS7$ATFc#Oh^kwPq`hLr6xi1E_?n4yxM|T#32vrQSO>N%e0V;Z(Ncz5v29 zT7>LXOOy9K*V#XC-$uQAZ>+RQ=RH*w5C*>|t~V~&^HB+(vFCJrpS^KT1fE@{G=gj2 z$pJE79q?$E8fmdVx>uA!3)2liaoBCGsz@}{y)MW%MXX1bV9(dctc+6YXjnhqwvDzp z?@x7jLUAIsWVClK?uK8=|#iye3lC90;DPZB&_L@b)}8wZ<2M zHXuqv0FDLYwlH-yM8&wjH3p;dk*@T?gCPI>=YCoZw<<-t_~rG{C$^tIl?$ABAlBbH zy(-ufTYME->%iaZh_>E4&S3ia+8xm%(#wc`^8e6?WOZ4w16=;#+tM7WFB^~*={IWQ z6Yw%~W5`V!U`n%sJ0!nF-DU*hxMy^$w1T_;Gq$ug2-WTUhIDUM+ccmf{1xg!=AVZ+ z>Tv!+@V!zXjzq6!O9uXst#5J^ZNhbIgN^_19v{yg@}pGCQEYN{$F2~Ctcs7_jXbbE zr*R1PCvP#U%0~sUBqd|%vR5}&p{s}yzK9OZ10`0xQzFoJ2IXyL4( zZBD2O7>=o7$zK0Uob538-B7m}Wj8+B-j9IKaMfbN^n+8GrRy8JfNeSXhtf*sjq{sY z6H7j-3C^qP05A4JcYF(__wpdx4!_QO4Hwfe#;MEkN3lzb=%J;R>%G=d(52Z+~>Zf{bj)d zUHHGl-mkX#+l5F?W5)ps-j>`$w_Dz92VsjX*?GE~v+07-u^n z1~N4detQGk;+hXlEM63E&RD?!i}8#$s*L)j#ki9_j%9PKWLyzhz4DT6YAS-BkEBTqadd*FSxL zUnoJ5R=%m>5IDuFRf*mf6fV_fx97n8Oo(+ax2rnLu@H?0dC+ojGzBci1b6Gnthtxq zWAes8%*?o6o<(TlAQL95;)*()7}Qz#yRw!Af!dH7UV}iE9Y496`LD?wGKT|A3*|i#)3_$|ab~8uC zWUum^g0OZw#T<4$g#N44Z3glSKtjuwbj|@tm7cWg(GsW4-3#p<6cjCPxTq-Zzu(QF z-hYO}$>oSvP!`FqDaTY@?q*@u73pG_6=x5v#T(Qujz{u^C%L5NA*cRCRA%YEln(f6 z`0E~CXq0=_2CJ3noj0<`UP!&|oa`&J|5kR8{!7z2-??S3j&bs@miPW<`5sBN@Y?yyCviM5X&~K` ze_e`C;3z`I*f~io|H^-YFNO9krn}o~Xn*+;ccXF8aO4^{8TJe{o;4ti~ zM5UDj*KMP>T!Ye1w0s=O9uDYy#@F?3`$(Qbr-nBut_-wj;m zYbU$ki2*NlfF=*7u+Pb}1MT7V6o|H!Lx_nu=zT7?VM*kc-&>(=*OAsovX3(3z<)le z`eO_7395Ez9<<2NV-irqheU1P_DsHdx1-?-*9BMdCo*h~zBNye6goDa*c;^Am_Yv@ zIqF$W3&oSo?-XF(V#Vy^?Fh|=X?FAaw6PhN&|gm;U{sr;07%OX5mz4!Izw@n zqfkHKq<~Hj#&=E~zR&rrc)w@BW9vi^C#SNYS%9~S#kH%&k+uL`+m^x6p?)(aL4Z&h zoR(loN0GxJT31$msh9@QOGC;k_yfUZbw@~+T3)>3nQ9i6E^J8R_TcQp@w$g;p1sUn zTLNuP4{gmIFBAqQLjboHy8N$W>CE$Z+oQqrZ~jAD#`?X^sKOHQIG&<@XE|-w96K`{ zYgaN^Eo2CJUP*|$-G2`IiWRp6a?Fj{ewF7e=Tg964O8jUpPM&e*NPTF^m3=^)WfdSKgv(o^X8G zPz~GfzA=098cLrk(C`~~WAnaSk;8s#%~@1y-q^}Cb7tP$I)QJ|W{X)t!tKgl6ul>! z2VqR}b!bTl7*Nrp=b7lCKOGzjvolxx_D5dKjz+6T8qMQbPgGNe(`Jn!MJ-9WIvOmy zg5qA(5WytPv6Hy~ky?4Cbi@D~1W@)XCfW>fTUcZ{TZ{o~$MvtHMW6+D)2cwS_=?we z+vpXc?KgOSV2JEj-LL9-LtMOnOcVWbq~Os>Kj|Xmz?4o`=TA0i3xy#=a+TTrIrWHa z)aVt_4C8phRIG#%Xm;bhq48xEKNTL{z+F z^R~WKdzMpjJAdL2o?u@q7gr*<_?V|8}j@hWWIjNF~hYj zYKiNe>ZYU7oMD^r)TZIA{sX;g+w1dO4p%=#9ezCB#g10GpSY-|8#G8wyKxaswuaop z-tO@$5@!bR=Gx!y(7~J8^PPiLrL0BU!ol5GaRx)%ibBlr;10)1+5~m|_~QXr&ny`u zb6|^qM!UuxSqrIMRU}q6L0oy-d+t9C1GMP4{=U)L9(9jS69|Nv0eHhy&qdW&lhYh? z*6(lE(Jpqf6P&-cW#E7;UTHFL-`XFa{En_I2V~RsI-|^~mx=3&C+-w}^(7L?uG}=W z@8-^)W4@(^7FD)q$A{Nl#ov}&JQt(B`jty_s|OB}0UMeHB``!>!SbY z*3r9dFeka7rik5k&itxg%r^^=lJqjQt}9mwNAp;RMG7|~U46y9%C+ja<&EBo(x1U> z7-is!`Ax`Al8s+$KlPdJ)9=oy;YX~+0_qf4!B5TGvm}sP`#;eB z9i`Bvy}AC1FYF0{T#G-6uh3cUM7Kns-H?LMKkyB-h@}=tS|Iz#nUkvj$?gfLXD^S! zIbPAz{cIXY(w~K&tYLv@$lzWc!-3EX)(~vU&0p>$S2>i+dL<* z+xd*=fdB1u)4tFvZV5D$9OJ|6_Uex)JNo|Em`@0MwDR+lW~GB`N-pt{QF&4a|jc4Qk_FywD z2z(qWptGEEsyywTJ6m_k;O8BE0W`KkzazJpBcmu~jYh@n>Tmfp?<3?FwNZ?_RE$HH zC(61;_z~r91NDbV{rf1|2Ce2I%BvBa9)a4;CJgy^on3vo*T`P#`CEEvna3{4qOKFY z9}|&t#a~JYqkJK=9+{5Rou8xgdz*D0rj6^ebWhBO zN(LijhIF8}#Q^UoYpJ%TWoJs6HF5(d=yHI!@6t{Gt7N}8MX_DRbg6hF@Fu3sqOWFM z=EsySZ0DVRwY>1qHSI(UmrR8qSa&50QeJ=c)8kX`x=~yxQlRd4Rd_R3Rq4{ARe{?# z;}HoApSA41vR|@_&^ewq)O`_0H6vM#@NY>RT6}h}l<}}|$f22FL1iGSDmmXZ-|DE1BOJCH;h9gC{m_|1mi=d#yIMk@()e_K} zOM!)-FDd0FD>uAwPsrDm)SRslki)X4ue084EK72=&>oD^E=O-?ZPnwvL35w$CoPb5 z{PH?tAx%jVcmRIF>xnB#_m^E${Uo11OL;I0JZP*Ue$w* z!+3=0>Db}g*U9`X*0Hi0!rLg%CDBW(Yv7FwVJtq#wDXc@gPY|Ikc0wneoB0fU)>XR zecRS64<&8V1F?+Txh;l3{<0-;c2wF4c@g9o{cHVKr&y9o5*x(P{>U8OkgTqdyRC>7 z^_*y&0dS=KCAHjBzDQb zGDMt6bMPE#2jK5ktd-QHFnur~qs8G7;((CNAaNfji(zu_A5D3j;QWBUP9)PeQGq_n z8Xb`a`r_k-=W#mu0>D3KwmZZ~2L}E(31L?!b$$#+paMti2I6}sg9F(Moxo!=u_M&d zkcS9gk>}OwAxR)wjVaweEKy z&R^Ye&vB!e4N=Z>aFs}l!uH~#tBmZ*AYm8pIiYxK0tcqSOnZ@1Mh)X@G2JV|=la9^ zZsVy!zUGj_+2OV~;`A$%4UQkWd4^eA-P^0L-I^|+fG4^g2|8ij$s215?((M>R4a=v z^1c+am2TGMZr7CsZpIaGl{656$-4G8DSN`M4t9pKs94Vexwh@H!_T(TP}pX+{U>0G z_S9A<7&XmOuIV^y{6IsCA#tvaYl>mt%R8ow(N95ApXTNRE`q9sCbIke47O@Bf31ka z`E$SV6o=zjNjmQ|bk;jg(CaH^5}WTRfvF#8p?+p`Rbpw_jdhgcpm+5>+dO&WV15** zj9+Zo3!u|mo_);Xm8!TROIzP!J%s$K+b1+WsxZQO0W3?iiOzho@pja?;;Xf;vdvj< zri%Vp+b|Vx0vM+uwWc^#h5bC%_b7CG9jx`K#6oArYV%eM>Q~aop4pO*s5wcUZk^z9 zn?A4QseybxYr z0X8NrcH#`}=&TS9!R9Tgr-5XXpFuUfHe#wT^-EDdMAJ$LFLxk%PnW4q6vjc!4!zE) z#5GvmyEv{fgsS@wBye3{NfCiZ62wGf1~j#`A?VR7iYa7uJ)lj2=?Houmj;@4ykW+z9A zGYTw#Y)LaW9QltIiRbNJsEKA^>Fcc3ao$ ztu!BK<@uJHJ9iS7Q*la;djUSSxD!O@zhHqpmE2XMZI}1i_nf!%jXe`eZa)>x1ywMW zra^~{*oC*Swoqmr$UN7DI_^FqhRoJtfJKfLZA9yR#DA=SscWqi&9=i(`1vd~s+#&W z;v4M06bX7GCyOc6DIWb}pV7Wdr~F=(?fCTq2zHG$QZze6*RdVsm(k1?ee2n4UmbPw z=!XfH2@=zAjk<=r$|$yY6|_XWdjXVuweLk+a*{GE+RO^B^2?0mePD_akzVAVzHCWHaJh1pe=k`83l0 zbplSU?t>?j$!Z&yJwKx&WlV#&e~gXwJ*yRpo1!}f2qx_w)LiT9JeDm=Y}(Llc~exf zMCFQ-Ci8yS!opRr@~`@$&9vuj-gtr$>HH z|GJ}jb8mfn3YzRnuL!iQn5WB=K0L|2;^#5$d~mu&$URc7o1Axm!K}A|PN+EgZw~EmOiR5>KK|8%B=5Q43*4u)J$ydj@$~*JAK$MQ4i`zewkUMd z2-NBnL``?lI4Z7XRcp=x+K?$%%o@G-pfg$jZpg~8N8mp>Ngi7`q5JeK8tG2CC8uU- z0~W25NpqiOy(|(!qWueDS}%a? zrH0$N`ix&gAs~-ZsW!;OvFQQly8l`=1b_ z)%wJB7l~z+5kp$f3b{eX^pePb#^g6}D|UjJ?YXaY^&(=nC^N$a5>672MxIYD~P3Tt4i#F_)eh4wejt#~`kk^c%Kdr7*MI9~t* zz|$v|p&|D_-8gGgiUfnN3@-tLR)QYMB!C+o1>EqRLQ9AGvF4m_ckD-N^@+_3!1l~&KiyH)CRsmNws?6N?$e5(gRmo%Y}A9V zg1(W-nb7vx&qH(ip0!Bd;9#=CR)-(czIc$Ejb=~`z2Y4b<_{Nnof61&SPP#YP z$emL8s53(iW{eD`#%y6z=^zrnU`V$ca`!3sF50Kc-g~l@$mxvHY=vqIX%u-U{eHXQ{WM3nY`gZ z?us)yH#{%HwE#haCVL@2oL8ZmO_6GA_o^uLf4BtH13%6?HkJ$* zY#aCtJu@Xfg|c|z$k9W`cEXTdVy(}~L+QZt{H^l4U{yy5@Gv|0mFnXbv_FmK0(bhA z7VB7ibD^skUCQaOv8wxQu7CbK+v{N{UmwE~oGfOl;T&5Tn*@fY+!QU-G84hnGj_~G zWK&45O8P~nM{aa3p2t>59^Rd$H^OgUyCH)wfcGx|cd@Mh<|!glc+D06-0YD}2fs@dWgH)lXr3c)pEmA~XV@bT^H_ZL zKeX(4&MO+nCG#zbpQVZmBteIFguJBZhriRgQZBH(pL{-E=N z(g#v;B}6K6qxz>K^muEL?_v$>ghi(sA%rXEyWr*Ow@OIBuO(X`Uj{c;XqyL}*g97f zG=K$DxBfcV(zMvML209d37#jkTCMIjiTr_{k`oT(6;@kag9|H2b*17*+$F3{2u-vuSi zJ6mCc;steF?I?Y=C~gYL!Gigtp^HHkjT7Az5uRgz<9p`UJ0daf$U5qu_-o@@8KS@I zd1;A$y;D~un^^4GN&?|MqfrHT^XUg=$Oqnje*s)(PAxc7yM#G|{G^uAu#V!(XeApwmFsZ|r0*V14S-TgFt+8-nqn-DWw(E(9CAW{*ExGD zpSdp^QNU<)rzkvBO;P<;C4z97lQ!l9#n1GlJ-__Wi+&BQpw3#NzAe^i#_p-YzcMT;m%GT6k`XPIp2KMvWtruZT%x0=msK zyh<@U)qRV)n^{fn{#3Dh*sSXO$<6F_9)imoKL-ad0P$i;cJM*Y$ibt^s$3thJcDx5 zzPXSxV3Z#q%6T2pudD72(#fR7guFqz#MNfuYH2t##^j)`=Q4Z@0v z%30-heW*Ky{N0+XQo%>Bk|5yMqY&C$1dj&C%;%!S6#PM|O_Esqq@b;kZ5$DaXG4v) z%_6T&c#7oIab-Q4*9KEuImfvG-C2JnD3niI7i8hqkn=V|>NeK$M897}qFbnIfui)d zUGp-{)w*i^Z>J^WX?9m~$`D{K0Uu*sToBGQ>0BA*w2U}u%J3+^Yk&%)hrIg?)I*{oi90y^1le- z1)#;f0Lo`;x1y`qf2PPq{jUI>24eZBRhVR{@~t+~O`0YPZ-Z!-PsC4Q^rh;$fki`}WF7`kww zZPVmWo9byrW%ye-IUVYdeF?b{$O#Om5&`BaTF%SdJG8C?otefy)jXmt?76E*kS^(v z4^V$fDbHECZSy(KMsPnWA>UO~UN)URF4(o4F8@oIZnN0a^rjPb2d$^ zEPH}S_N=F^uZ60RlmwtQjkd_G=s~1#TeS;2W6uZmq8?s>O({T10#G0S(f1W-=Sl)l z5`dHj%FKsy5~t}%%*q~kBa?3D84N)3q^sO1RD_@<0KE2bTrhVm7=c?i|;BWPGwPtPhY61 z@prPk)gc7|C<#C?xm!l#=U_AT=9k*Oj9bO-(6rC<<2dvll&a*^*#g9p$C7z8*{~=h zc0`F8GxHDTDmiUg7v9PF<3>JYj%d;H zV99Rh;)`$9O#^YA){FQVzULHnIvO{D)tJH-B^((^GJ7N&`?(NP%o`3 zlQ!Iasx}3;Xrq1d6Oq^ttr1x1o4k0$fsBB2>-D0Hy#|s-gl>*y{{T*ZN~~21*2w~E z>pOB5ShbAe08GlmTFv z#7HA_ZSyImVx`cn9h;rWF3)<}{qC^8ZJP-%gjVqVwNOf%{c?3aZMq`D~Vt@Ck3?PL9t?vVV_V>tU!>1#|| ziKPTgH$vQ!+#kw{CjQyHMt=w(_&a2Mdr<~ZJENI)4nhnJ3X3f)y$586fQLQtia4iL zHnAj>Hh#S5O&Qcr~M<&x9cDEk9mKF?W`>A2sZF49Org2+54QDi7pd|q)3D1{$ z0hO600y}}vWsr9tdX?{fgCZE%u0aFw4%E|Q|J3&tXy-}*(c812k(eSL#i?ylHuq)! z0GyO;aZhw}`?gm;ig}}BiyrB5{2$CwvBrk+NQVrhepD8i2<_QP{EY)c`@Be?OvNQ` zK<)tg(%Q8?+4yZxm0;4p3P(6vJsuURLuF=042KLmVv(5!E4~{EapZiBFBRzCJ^aNzBXsl^CE|G{0}JWrnwuI-x@~WAvy!`Zk=SQEQDB*Dn_bLF z+CuZ(4r;WEl@G_5o!icRXwhzsZ#=CiM{-rNFn+YGRK11nvmEV0Mo7;dM-=v4X}gGb ztXtS3s^PQW%B6Mz<3$e;4bA@mF~7Am?oSkVVmsYtIPMNo+AkEV{H)S23&(B^Yc8>2 zmw9%xsA$?a-bMidG>$>W#Fi(s|{wyK7{hmy9mqxc4I`xvAs%Sn>R= z^vyA}_L-h@JI%K&!vt;5)cVuM9UeI8VOFG|B>^Z2KuQ4c%&Mj-S9C|yxcbsmzfb?! DPn`0M literal 0 HcmV?d00001 diff --git a/www/images/thumb-satellite.jpg b/www/images/thumb-satellite.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0175ee7d96cdb91dc32cbce934fe1ad7c398f9ad GIT binary patch literal 28365 zcma%iWmp?e+--0xTHGBHq_|6Q2p*(paf&-_@#4ibxRU_EiUuoMq&N-1DNvv|ZP5aS za{GVpbKlSRJkQSV^V`{(oew+vn{&=QEGK!YBXXUr9{-@%0a(uuqjOSPYasUQ71|~Vi<1io<0KmfdAN;HHKfuAo!^X$L z!~kIY&z=7l{&&W}!~$UBkO45TFme7HBIdsr#rRhb6AK%M9Dqx~CXDw~iQUkSQpA^v zBT_j-6jI)dPt9pRD5etiqE2$C(HAA?*?9&80{3eNrICGTQqbt93Ajo3ME=Q8iYi>7<0VV?D)hPp zA1_m=vp;G25fCZb{Vg9@dxy(W-o_?lI=|CNT_Vv>E9G^!YE>Q>)v8u-Jgt&^Oj3jJ zKq!1L+VuKbBay-Z=7z zP5s)CA@Y}SxYc!t^;`6L1E_2@gDXY`8UaR;JOYZyuGB9c0s26(yxB=u_pY=H;2Pq! zIxa`*;VE!qJaL=a1%;1=*C2SS1qb&zm{i*Ly)dLKxx|iA7G!SCqm3Z0V6QMXyt(zb z%Wl{sU|pRCZ&_FI3{NeeihrU?mg&2hOm>}-Y45C@t?g#DjC1(-Z6f(=)nRcuUYcY$ zHkr}xpRFN#hQb(*mr!=LQ?o+*#NUI0 zsDS!X4O)TJ+MZ88%O#P9x(+@9R_yiBiinzpLWmj}TGOApoixYB6F063{nHKp!+Hj<&6@ zz}zC0ZB1>MFD9eiJ!dGa!2vea>RQ?D{BGWNS`rK)+c}6OOI#aQ{f`SWRGXQ1QKoAS zW`G;&i_seqGHm35ZoPtsXJ{*{JdZ_Zv&gxbdag0F1q4FWa5r{0KwC53bwP#s4Bfi6bjPp zrPOz$O{8(KvWLiZY`BxiwMg&jp>rYB(vLigJbJn_>S^@XeoAkVuTD)KpMK2&ZD==y z%gpp{s?n4sbD9hK*>I!vdc8Yr~Hfu2wXFB*AiGTORW#%&a6!X1mIF1qsL zDc@z%W}^bj6-KOVEnLUrg|32iiUyp%CyczGI#Q+&0K}$oX-N)gy-{9wo-9_G&F>My z=dP@vHzth6CjdMh{Y1&rF7wK4L8hnP>4u{R_#8M{{P}j2Sra;;THir8JcA7<{_6Sc z6eLIdi)NhQ%Bl>8DmTH*jCYiyx77Tb-Y|;Sw$6MrUZwPOpo6N2OM2yU$xXW$CzhR1LOV6s z6qd$G9KS;(8{a2O&|(mW#?;%VjENG%_t4Ickvq`vRB|HxTPELB%pg7|R&Z@ls_Uzr z-YF6lAh8pUPrVZzN`Q6^dx>?~lHE%$!f` za1cI9Ep2x$kw)wtNC*99!a9a=wtY6h(DF$l<{@(VE4^@J=;`FW{>;#K ziIhttRC%*e@U*Q%p`GnFluCkk$*ww7h`Cvy-ZekV=_LHtsEg)Zf2@-n;JFvR9cYi( zff74Aig%vMFQ|tE)K{LGDQul4`W4GvZi+chb#J?wyg$w|S3_@PN4?YEy-2YIwc|@G z)&zVv&KyiI4QoN}p2Sw0E-LI`VRo%n>jcM}ew;>AD&CI*w9Yq+t`@!h z9e03C#HgbHtM+W>+GFK9e=@iPHNhoDi>HU))zwSetTr&Qschf{0l$f7`H$r}W}jbT zXTCq9?-_>^k@vx)?oZ@?yA?h;2-FAzZI$jYu0R$lcCtBG9^*ab<+Hu2Yh#R~^T@?=H@|UhcVS9|-CvS~wjMjV?*LaO3It;PRp2lBm zvxW(Dt~gfZP&8iVzRcV<5VxYx?Cl)AeiF2BfKHxPuaTCZ3Hyzzs<@7;<0XFIo;sEl zhgxAo+g9buf8KHlY|nxE@3MG2YoWidm1v^aChu`;=DDwa^3071qARHFXa5m$D+yAc z0P9O|kRx+T&J*;*xx^ZEmI52?p0T-I38-Fl4TT-`+#FGdS9L&KFGmf1p;2IuXYMy6 z`xL~-;m>20a#qLCZEb7b1|3rM&9c+c!Y$#BZ`v-%igTspw#)vmBkE{QYZa@jP2Rq5~-HAa_Q1iRV66T;4b6B zXH7@Zn(Nq=?rHvsT14kD!el(ZlM0M8Jt8h5`v-f)l~ohluGycio0jFE_z5`wY+Vo@ zJ*i}eDwSy>ut7v(IhXw;S|HP&u$?3-rbt>2vb(?G4GVi{9&yg=eK#i$-a8eIcmLA& zX192iLW`W7XU;z;Ui>ijL;_h?L#r7h2*Vy6ml5fbyv zf(W;A4lSKPWcn&9iA)d=NtNm50Zs;qtJj>%LaaNDs=kB4eLr^yhc4yfHR@C=jV+6E z=8nrnuUOWUF8Y(UzO}!7orWz*pHBv3%Pg%vP4~)P3;65H+|}Jdv;AsO=2zvXH0|n- z$NtnUlvD?nDl>=3eNv7!EUr^c+jAK|0C*~ow?)2;C&0{%f9FWgaDfN0&+hs!xU73-=B^*Dgz5b zda8)Vf%CK65@PPqTNYUFEn*u<1SSHSsu@)xZ4q3uQ3NcfDRU!ug0fcAzV`FA?HePw zGRZvuq1)XmF2hA;;O|pDN>-oB%&+j357XH|7 z0j-y3li&C(D>e$ZQDdc>)VvP{@v12lvS$W4n?CR|m`}Nv&jP!H*oq-rl`qIy_PN{Z z>-mU|h$8JP1*f@#nMsQ!8J&1zB~Ag=hw3NCYw;F>YWb$AyGRCb4lH$R!O7iB0*hYy zZkaI8q^PC?7SluT2!X&0qa=5Tvz3J7zx zK$#TbeI`p^CJs$y{tgN}DCY^m=)i-L{}nZpsEAaXz`%U`#}{le)bOW$8_VEawIftd zGvtpTmC&{TT+WSF^w+B$PEm4tOS~nRHLF$To8fM*l#7FNKHu$Daza6_VEsa@>E@4b z_$rgmTKT_h+8s7;cIt?hgP4td9=-;=eFVH*xX}xvA1!Ogfmy6YM_d;--XHWWzIOo zA&>=&TDT9vATxIWr8m)vcN5wbdAEuZwDdN4c&WkggcqJ+TcPrhli0ypV|J%8L;8zf zN?4`#OP#OlbolB^;*arBv7c=+yo4O=SJ#>iKJoGr5_rJ9_>Um<#nWfKu?e5E$JR)= zToMPB2*CW}ynlUgiVLW49f;Kfd#%5{8vFy6E8-(D5V@itv%+^`FZgwu4dkUabYDBy z&x*^Q&#;iEh|P^Okykr)$F51y0&6sHSrRQ7>b>=!HMXB;nMp3MEz3c}j{*c~2TXFFeQ^E|0?4mbkyur5eHN&~(aaj-{hS6vuQLQi~3)kg1s-kZ(GB8SzQfGvBl5 z)7Vzs0EOc3zZw0q46Bcw@^-ACeoQtp`h7gXFG>^u#@)ulR%3K|HNgVJj_zULZWHhE zraqcA%cY|(=A_K0+A2iz1i~3d&!otuikYb&wgD};1cl&2IO3r&@$pInWW6oPT};wOJg-s*raPy% z;l|fcMHHUJ#SPZ~G_6vqM3-h!O5GJ0E(x3_@@HV8w7@7dh$27+xTk8};`)3s_^8R&i^OKf3Zdu45${f$lj=KPdX4-l@q;8sd>N<&BQlCM5afIXf|(O3w`} zKYo32J$R+qypgg%Sb0RMA5FdpZUeTpWq(z8JA6X7;99E)b^FjGrD?@uDo&lqkMi6C z=uyhlG`>LygMw|9Lyp+Tx=3JVRD31AdTauARr9m19h74J-u|}($JNiWnrTsGJz}UT zO(UUfvF?M-v}8%q9sk*}Pn3gnW7H`tAbBV=*t>bSyI=WA_x5!>rqO9TB!CTGV|CBS z9C6sS{{rkM;%K5~Uo4z{5`ZnR%Or{N_P1aGp>l}S`PJO02~U%fc`DtBjS%KYFoXOagC=v%>T~Kxz;8UX1P*xic0%*H zwc0QzF;M$u`m>wSD{p6Yx7&aG33|m#=8d^W{key?2cvf&W;LI*G*p>GGh7aL1-oiO zB~fvq=HAZ3JD`Q1pdAlvro0hQ7wF}sKR)1XM<)$#W#q853;xIz+;BUX5*+b}Zhl1q z73-X?NFO4X?F28TQp%?_(W+7`4AdA@J%~d*w0l}c&_0ERb@})C z%#bv;i=;s?jIS+?bh$TR(cN$MX)?xl|Adpf&b--lro@G^4K)CD8+|$o;tfpOj(DdU z>L|f@XguHxB-gB@O6u##j=WkeZx@S12Vl$?w?jcPV;b>k zAt@`b1f7mdobpLEPRHJ(C|kZLbUZ%~Yo3-{vi=&8BDxjpUbFH-xUV~ez_&)HPG$bZ z0q)ug!Fu6Nt>w!%e%dkXo6LC&(zhw~KrcYvv1RedAtPzpNYjJg{y{VbxAwTJostvx z<_w2|!S_TzBJc9=)!prPSm7fIdO-VZ2xkYKM!D82NI<+aD%76lvS^BL$!$R!mH$ zWp6HgZ$sMEe8naLYMray+x15Co_M9ACiMk%+Qkg?qP9H`Fp?qJO6D0GdAs!LCC zTZ{5gYie5`HFk0?n}=RDP#h~%z@q&hI)<;T9HOa3C;YEy`tk|?Kmd>gMXbo^@}tLv zL$F$q$r#fs0a$Oj%8|2t0J1Wqri>+&qsR@e6faP*n`0r$EMq}P-|LWeIr>WK)eYHU ze*SXXzu%m0a_qRW~ zbisYGL!SxJsHfM}>*N>FiJ$gV8(zsBo1_Wo=NVUN-a%W$I&4Gli^QVC5p*4F-|yO9 zarct9e-2b?IY&Yg)=W2iz9pzBatJ2nSz86-|5`O9@KcN2ER3LawGI9lTsd!+4R#1B z5I2Vy3Zvn|#ncdVANV!H6VeoJ?D?9@Hk5+~0=~$pP!|R+WJ=dxedhj6KRUs+?~?)yhwN4X*@YDj@1(FQlnUAK{EDCbmh!Vi}RwTJ8=d~i--WX;VOj}g$HnaUQX zMN-abh2tf0^#l?|tvZ|`_Vfc7QW%r^EWrjfc}Zdcfg;+tCs=$(f>KM&=h`L6V1+>L zoAT^V@6vL+-9nxw>aa-UO$wdnX(<}?Ozti!yQR{SV+IAUsa5VO6ZO@= z)p&pKL=+@!VBitp$Zf(@l$ztW9`PlSCvI(lt*r)rUT21|Ei7b|>!vN$n2n3fqtZ)_ zsZED)OnU^^_xxxI7ed=NtCrp+)wdHg(fKfWtw9!i#|AjaYAG*pbrD9#gFwYza>87ct!i(CVGRkNz4O-|;`jaL#4e(;a`j(jzx>FnPpg^zf(qG~U&mw#hBYiVksJwlA3YaHLERJ3pPR({O#R%m0znPa( zPl>qxX&)h_FmaLnO`=#A(NDL(=7DH;voMucwHSy9WpULH0$x4kSNX@S*CzI)poVs# zFiS9?8-m9yv|9XKvTDBVJ7Ogj!jvl$oIWk`pMZ9#CAl`1?2WFLpgcd3i8guDZy3{I z<23D;UU&yit9k^`qKFxeRI6JqaclUyC$C&T88x;QO3kYevpx5Zj)oQJz7cc6W3W0n z7J{uFgyDfBx0eQsnujFp13zauFELuC7qeWRRi=P+Z~mO%4~2vcpc@+OEx$H3fD8%n zerzVKkyd)u%>8? zeKcP2nL-tUXNVtZ!p(FWRAFgydo!3zHn%+JS&lPAp$}WL73%BPejp&r6|`D2;~pK} z&iFE`Do$OSc#UKTcI4k1 zcZSQkQH#ooIvx@2^z1MP8RdZMAxw=7O6D-xJ)?by0wYl#;aUn%4Z8XXBfvwGi{iKl?~2r#4d3a|6MSjQepPR!GR_EE&X z9^iR#Ma5;hY}f7))|=DP?({{5GF!})(l`z+Y4}@f)7ygwVvvQoKJmlwQ_`K^v7WVi z-shzz-8k5})YjrJVFq!nb9d<2&aSJ8SuabY1WMPZ}{vuppbFX)`h5tUN zH_SmwK#%nrD09R`T${L7^$VNBD+8r~d?7)~wdhBHYq)pB<*=vBY0mNG$8msGeDy!t zf~wW?gCY=Pk zPdsIXD{D;*16MSEu#tZfs^pU3u-S;?Q8(|F>5^_>-dfDU* z3aPx(P+ew?ERWcB)mvi;eWLhB7y`c{5_I0V{VO)4{Zx;dwN7tOO z43_Px9Yn-5e6jRBNIC7`q!#!O?1gl{;YO^2U{tZ+*L5!K4(|Ik%Iv({)~r`1tw!xW z=bR#US1!Tdw7~kSrK2Qqyj`iK*)DCgzWx>z9U;ZLoTcHOVJE`v^;+Etd9E(6*+nK+ z!*h@WMqu3u&7IX{8%Umv^n9(Cqn&w9J>-J-ZOkw;b@A^(MbQ_Me%CL3OW}STY^CR< zdON>-{D8+;d~GtEw5fWQr$!uYUp)0{0O^IHYoP)uuf`;Ex8%8vguZGw$iQQyC zZwLH?mo5qIK_@wuj{x!tFA7N>eCD=+$ftq^-?`NU@{O3yz>YGR@Nx+byXa>NoGqfe ze+!ITgYJgK*arDpb_Fp(lnoYr2oqmO8ADTdUGc~C# z7S}@rme*uIDZ|>0$h&v&Dmgbk3P8-Y98|PWoW898>89vzk9Wn3H7e{2f~E&YXH8PL zW-64A%~9_<&;Fww3vZ-6`9`Ggn)6uF)Eks}Ef#Z(n-LNl!`YS#NMX~3-8>IrrT`uj zy+C5v8$%!BOi2g{^%beKS}R;@?D(0ez~1)L&&$_=<1{WFx7C3dluxKx>4d%?l$ z)22_yyLv`ayMKg4oz!Thl0ygZJFXJ@0H%kg*}%ut8oJDW>+t|x!>@0#CD-U50by|7 zs@3S|h#$g=7OVRNB5>1MukoDik_@h)og~v;C_@n>oTP3A_dR&Ea7}(Pt|B|BSmlSr zaA@}WG~)~I}^(I(QB%VR*IzLJR)~wk7y=^XR;0ku-Z~?N9~ENt&q!*qoje)j`$UR+!RU5oRU! zrS3t$25o1U{zomlSYMYwldF8DC-wRDC+2{LkwWdlxAjwQDyMfgV9HE5uSXz8+-Dic zbiRzBt*cAsszoMl4m#r+#?)2>viA4%uktBtOfWSzrr6Pl4O2cdd8dim=$pkYx{leVlkC77hstfw07k#N-gr-kXi?0;Pc}gYe%jwm+z4^oE{MbII zq}yLncs#gAu;REJbUNf@-Im=qe%?U#SdEJLgI})cIo*Xo#h1VEwARMAfA2i8 z;LFLIMqA~Yq>&We5i4c)Qtjy-V6SL+Nemgr{4OpeNxaVwGl<$IoR{2b@rwzQAcWIGlp_nhz^4--G zg zv0^LPl{*hT-z^f~KfFC)5xR2jQf#HjU9X-DoYRTnrGPP0C>A}dExGYkex>Y|Yf{p- z;7^X?ze~)ac*Ds_CKP75re~fe`@N{9GkGzo-$V*WK;Ru*4FCFi^oE5_QDgizpC5g2 zZf0DzO+DG~j(|wE_x~CF`sCGzeX8%~>cua^ygOJk*AB7Fl;Z*1>vqA3+Gjk|ZL6XI z`$ugBJruNW>$FLOZ3y=rJpZL+awxOHFTCP%|6EJeur6UcFc zcM_4kjsv3P=&j<+GFuAWg`_bvJUkChnE=LFdpT^2e0mOhRvYxmA)p$iA^F#*!88HdNS}pfEX>DBKOQtr7{>voZvmB><(M7;O9aQD z`ygN1Srbi@;0hNp@hWW$3+v3tFjZgJAN%zw?`M#IYAuJho6KjV+s6txDUoRA;DZX#WHry+`PjidOD-g54h<96+LeKBWg57nC2tV)^$MsJ!8 z*mu08>{k~Pb6h6B=Twv@WF!^#lo4?MpM0?i-Hnkaw9o0h+2H_#Oek zi+kHvXYC0x?l32%#f>yiMa}F^<@VYm!bJsjjB+HnE_?ZOsKMBra{mn#7cAHCB=(;G z@jdp)qC?_oUA`U%`4QE^gx#pUMD=nIr__8BmSaYO@AN{*$GdWW5X6o5ZbU_(tme11 zlZ4lX1jCb;vyF_&K&w~TOon(GYvCk9cr%t?c5!|OmO$i4lb>E({nW2ja70!d75Xql zWqAqp`oZ&BzBj^J^-^={2z_8FyqVE~n<+nI)3e|f&;s^FcGY4A9V8vk{2=b)&L(l? z2XHSt^kp<#GO^_5DXad%J%O61Zh>~JTtJSbv}FDKOE>8S@Y+gFHMiI+ zW!LQCnCggLHwjKsNvXsABkCh1^RPqw)7)&B8U|lK_8-zw27nlP?Jj)YrR81I{dRc2 z_Ti-w|9oHhClENWtvIvP&(~b2m(g~&bP>jDRCK37CQd~LR-{F92+#=j5Xp>0TKdAT zpAX17ChHsxV7_p|KHf=|!bhCLp5-mX*ej4y26ASDXex;7;m`wO_I^TOEF zklx=2WX;|VpS46>>z-^MB$Hm{F01MB| z18kOP{MjVZA3r>tl5l{|D z74*SX9y0=FBFODKgm<-Sr1Ux%%&f;1S!gGS0<_xu{2X>M8~DBqL|zdf^Lfj*fv zn<5?)PhV5it7DflIxv*0iPAmmP5Y;6^P%!`v$9ks&%pbi**Yw-7U1d=60{;+J$pOX zDLEVvVDdan3>w4n7eUBayS7PjTvxrHXE5~@6e-l`iDS=jw60-#3i`XT?7GXat;u%@ zZfDoBhYh7GwY+eL+eD8F@WW?Kk4UuYmdZ6taat_Xwy^xdg?03Oy?PJo7B7?V=d|9h zXiSeM`Bc49xPEU~ke%ngL2a(sdj>?NR22_c?dXWqmLgQ_w4y413n-~(>~9jNf4%pp zc3f~KbWhdv6*jY@?}?TC^M-rldSKp{+A4dEyyb*8WB0*GR|3o1Vo7U`zFB6n&T^Y0jb+Yk~qj6+U~(I&t+T;&1HZ&^H)vcW6{$Hetuh zYb$uY#iD5Q={JSX*9s#1%pU;=t;>(maNED?7-d$lecT;<@>=+vCM%*^ZN2FiDN?AK zuDgWI*mOI}GH@=JNF}4jHo2aR5(cZ)RuxmrY{6&QB#K;^7a)=xsu%}qYgvNmP2oiE zeeCb!LR43Hl4q#sL z$XIJa5ny}I(1>Z~?}!d(4&_z#S^Z>zGw?lnW(NZ8?trT>q)ur_FNwz4m7z_*dg|GU=y?cgW<50CT5 zu>J9;ZhCVEl$C6Im`ROL%&(Ei#eZv^B6XWQYxcc7pDi`*Kq}p8bO|VIT9eRegoGk1 z?A_2Z#PPFOVvNFW2d7wBW|dVtszUx|^~TaCj89yY-}s+x!zl63iQEl3w&EKriPQ+} z28!gR^Hy8}66Z;m$DqG)fq>~6Kkw8Ac%p=4@?e$Z7e94daSAeNbaEPUj7ydE$7xs& zV?ItC?o2!EdE(}iYa+gU)zxE2-~u~O&dGnq{ry*{waA+%Ygs@#|6}82;;=eat=$J5 zrxQ38gXIpM?^AA@BWlx`Ig0#4v4X;3q(LR}L#(g&41<%mo%C(Ev1%KGq7jqLpU=M( z)Ne|y_++@3TITcnMA0s;`k}m(K!?-j^-YGPm>;eeK*zT$ZW(tjL40h~7AS#Z4!^Jy z-+3AI0~>>s(|VUlkl6TXS}oYvP$0&Z@`! z)ErlPly0xYzyz2>vu--^j@e5krJ+UaT?K`}_R{Dyv87A?vA~|@sZx-2Vwt8gx(*ID z*w>=frrFc0{_^I)kAd*D_ymn@HAJwN<3XrzCW0p}0vmCew?NG$po{Q8qB7$(Fou&- zn&ERhW9i+a9)5g~^1)Q<%^SYJN$hyQy2W3$Gb_{3w*mV#x+ph4#MRO~bD7DE7YYX6=FCXVq+A)Xpq^Cy-($u;0&{-l~7E?m9avc2XVh}8r->pF`rH@)_QvMs)} z=inE8;lbt6Fv;2(_7PJE*%q5W@2n9zgb=gWbTtK=%7bpTwvt&zc#pVLH|~p^-+ru! zp^CQ073B=7sQ7NtouA778DGURB{5wfZXQG@!3cuWkrmrjN4jcR6aiwa9swa+hr;wV zMkQ`_f<#!IImMPPavlUok<9l7{x9vvXA}}uV^zb@2Z2gJ(EQABv59KhysNH+@m`OCNcr5Bmed^a+x zhEs=-K2oy@pIMJa2`Vo?H9FHyFe3gNPwbA66!-74%rfnPZ@NP#4$7nLZRbcck@+9u z`g0{Yv(wl?sNumR6@K8L;ao?h5VV-Uv{;ome*9+0MEalNdf@0txbZK$hwx}7Ykt+` zuL&2*OIyA-buCp-KJu3r7C)60`LkP77DTV9c$g9`Gc=r9_T{KWA@55T1#_{4;}Ia} zvMyQW1It&N(sL2}l#C-oknblK2Oa9s7_K#ja`rWhBrgq#pE^M+Ilxo76JEs6&R7mz zWJ@fPg;uY7fkzwGbo!PYyy8!s1g+xxyMqr6gjCkcYod^dvZ z{_VTornd9Qy}mY)n5n7(ck`D}fyKq==D52IJ|xWk%9mprM_;|_s76!%nE)Hg>eS(f z?Xd|Lu}9k_g0E9WbJnKNP(`kohU23jT&zH#VzPRWXJg&)@Ks4Mduzc)g$Bs8^YDk< zRfDbC+1jinb3|5Ac~>3lLdzskw%8;L>Y`IMdMMfcesI`_L15I*gu3Wwv2M*%&-X(C zLDuG))zStqp#rN1A(&*95=6O}nz<}fVoU(~h4pWusafSS(;U{OSQ_feapZT^b3K{2 z``?0Z_VC!J5nYg)A@CzW&uQVaPi!y*3L2OA>ub*VOGLHi+)kM82}_+PnCmzVVzRyL z@f26TUObn@>YB;Z1R%vd0%VN*p^wDL#|{6`6|SvaA^nmVI%x#8KsIHgzIMkmVQT-_D+0F04&l>Q9)WNplX8;A!IsbfF6uFmRw`>pA0kbf0)< zwcH=co*a<}r~VI=Zuezm{dnhc~rUB#CVKSOs-{ z;&R4y)G60%94fgX+Yl99NXm82uj>rMg2_PV-CPuA? zA)~_%Y_l!5qE7Y~BEMJf>ohOWM(2(^2XcoQ! zf7(fNmT*uE$}E>>^4aQ+OC;lHLTRF&D|}*b?f4eN#9yf?NI*tU+WGEiQLB^n=fm0j zYy|m7_bYjUNqi4*Fp)BcD^IsRXQ9O>yk&VzJbgza787N$F%BayD{?wQ%$-w?u#~2p zRK>@IV{>^;HHF3ziC#Tnvp{+%7msP|AM(@pe{wGk(M~6yDElU`-uqfGnDnY?xF@~c z#9Bs@?Ok@2TLaTijLXqbF^a~uoO@lXCuFa=DW4W>8grz_Y?&yC@*F`m;k{_vRNMbDxWzxFGCto$PYT~;p4OsELi_Q>`P zF*^@e7w1;sqtd!nPo<4zGu-?Fhe}>=Zfr`~n7MoC@a4~nnp)nuL6ug7V|xiDXl}8n z_Ur_Z{k)~MlPc(L_g%I8as;7uJS@Zg5g_&YL;V-k45%YF`-CZ+`);u7dIy{6WZa%F zAzTxPwFvuVtU-M{@W3RG>i(X`I{yVHuU?vt227N@P(2`Jb0z*0+wT!zO-}T&`=j2) zEM~md=?AYj1L)MXKWwIvn_Lp$4<;vy0)j9HA+LxEzB-uDp z&rXO(B_x|diuER`sJgF|`I_R2bzS5KeYZA4N@-tKm5%LBSokXphy9MWxje&X6SO!+ zX^i0!NlkpP+4UHz3pA5oOyA8lUd_>hP^x7`c!!!yx; zM{|xh-Sj@6jl@aLdxWSQU2C@_7G`mctkc%%W)|M|7dbC;?;8cbo!9NZ)K*4eNGPEN zdi#8Yk)m~V@^bYG6yIf(t$BVBpk7~e^of_=tkHtLYLAl<7Nu#WBRq?B>dXnyQfN8b zNuRzsg^2s-8mQH>p+LXvce$CBj?pIy!hIdhNqkpvg)ibR0^ip8ktX*N^HT@pA z^GuVeHanz%aWil&H>*zuI-K5i6uHQ!WjT_In>ZM=kR6L=1{c~Am=mHR$?9(l?d_&Noglta;}=ki)78@s7Jwm$ti zD?o2~xRo&Faz@ocmLgZ*r|ldoG0;VJ^PwqcNk7%q`lNzB!YWVz3j4~M<(bwe*|L7roRv>KCA&zgGNk(U>N7&${Pn7c0@eV1wFK@F)#t{SWF#5_H2pQ5s^?NcRNEH^| zlz;y&77x?F&<11DE8TmivGg^3aDPy!L{HTZA2dqJQ~MMTn$*kxn>?i%<==2|?h~qs z3dD0lYQH5UM^&xaZA0Byu2X&PuROI8(*@&N(_va#PJSS)18YqqiYg8>?#+4RO>1Gk zTf_58ur*`%gZ>qdBacx-{qG6#}ANe`d!h&&=_Ctac& zCmFpXQ%-JiE)iNPMHiurOko{V`kV+oik?4zY;8ltB&KrS5m*ndg#zaG2`3g1sx?m| z+_yf5c~^eW+57PD0=smS6rsikr6Ge11*FIpJ|Q&?ce(KS~)&U%#Dc=4<~YF?$oJP^|Ye2{BHe5Z{jy!x7N7%U2Y}|HjYo z!{;?*8NMXbWg1*kz(`rLi_Bd8iWd+CXrk2%^}o{EoC=aO2~Nmm@X+ujVdZU9Vunh` z>!kj5&BR+bEzC<{(yR3cgWyB0%k}` zOmN=kW67(FASUe=_yPr`iMul+rb-_HLeE4HvNeCbCoaqLIZ(i?r}B9dKfwfgjSKy7 zRSJUN!@(u3+kg3wKQtso>m~tlgwy8>aXMq}VrQh^{#gBSaWN}u$04`7k~j@a16x!# zxQ}7bnzda?D{TH?f&8{WGHe4+@0O><|6jEYhpu@ zdb2KDicbtTVo!NKAP>vfvFDFli`q!Dz>=x|L|{R1dY7&|MfSHeT@PwhlLtH;F!C`p zVu`14x}V*)toA)u@P{r!xy{|}#hux8?`nfhLF_WGwi_1TA-$pB)uGUI*b8n2G^|n9 zu+XT^Cn`N%etab0;#Vo8kt?xtZq;;HvX`Je@sr!dEfQFY^Q}-+XMN2q{DT02=h7uy zfa63NBKA(GekJ3avwlfo#+r=YLNyjOX_HN8T=!B+3ofPt-^Fvdr5(#DPmeHR@c2dGZvww3U@*N*$n$7U&y8uCR3(g zvfZx*u`6$0<)PE3PINZI>0)K2DLJ*P5YHGAKn%4p3_vqBl@1AoJzMbj*`{-s)@ zg&C3vr#5r%tVF;$Dc)><+984Jb$?QwykwdG_DSt%mZSRuu~l~Fn+(G|9HF~QagC!` zUAG&`;(6Ys4U{w-bx<@z1mw-qw$;?8=77Ok*qHdu`?RM_T;r4$A2nbzG5Ia8>8&Kd z5a}j{O$Ke_7k7r)1R{oXRUCD(6zZ|0Sa{Ft@>&+CMfIANEwYb7JO#&;%o+aKR{w}T z&K=>hD;lNpc@9`mQ&ekt9VkKK4uLz2&Nzs9=S57$6LDr)RzVfgayC8C^?F@#69X2A zNvcf4f={3i^6Lu0`7S1F=PYh7=iF*GP^!7+pc?+X+|pIIQLSr)=P=|R%;2Gu7sJYg zyE(vEBu#SZQT+%Ik@^+)E-n+`J-BQZ5$Z|PLnkTrrLRSfyVTRVS5ILgB{MRlFj?@y z1G8%HsLDKZoA<+4y@(?)ikezuA5YBb(*kMDFSkR8yRX4;8DTQ=iWBt7xB6Yg{|OQ< z?a>%tNN&8A#W$z)HXes7FjDF$kx-UFjNmC-jW$1|r18!l=yGtX)uG*rT$tirQPh;T z=m$f#*+){_#yH@f?GF^fyMLN9OyxWh=DxG&S};a7Fj0M72pcKREb zHMX!g8vf+}0DNl;dZ&727N1qDrcQ8x`)Y7@n01RTI92vih`9svj(ZjH^@oZ1a)Fkh zuB_gLF&VPEDBP;U$BW_}`2PO8xbw;UbEb9Al?=x#w)5&mHI)+{C_a!uikotkG%bG+8NF)2uIKgYULF9UQq@t58i+tukXwB_cwKgKdi? zKa5h;?0AT4LYX98YULy2;9yD%_CTY4pG0C@Vusz*-jMFCD;{i4HTpB-9KZO3a-N4` zHA`+#ZAG@7T8))su~5MLKeQ^do3l=Ms260oT8_1Zw+bljqTj~n`M_^&QI(g4PJp

(pjHsaun9E5@ zHqsIX;>QAwglSz%%n`D&G`P)uc~zGrBn>F{2^^da55Pux+nFiR8cf*jN^-<4#T6xF zk92}CVqVO3e!u27+e}(y7NRY{0d-oGr4?=I3OFO7lk?VLLYqTVoC`&i_qFYFYhz%> z7PHoIpDfd%xTcws5?xnlcbwauR=LAZjaAH}O_NE2s-0$8SdN0~!}}B_*V_1EW1cgO zb==pPCe_?NhukS)J`&nZg@y>P-4e=AK%y$Fw4|Bo)2Q4y5IC{poGmv$Li*tjQ^;mx zjh4!qD6l$(kZuV+`(sbR2I$Q}q%HQHc(Tw=>rL-`5LhHtX#W8EW?T+D=ILd^PPGeP zNH`1~n5eU=w5Oz{Y{pAKIM$LDtx4c*$KwKv|IzNa&r|A<`;cBr<2s!o#uPaCDdQ9M zm9!S6mz?aS46=!>6ttb?f5#ti(sd>a^ zuF#hG)gUQbhLK>#f1Q=S4(ID390*EfDI8ETRGMIF5hK$js} z)TF2ok>Nkv9R9}jWzKcR==pY{>69C(NmZSr}#u_OqwU;aJ(sK1fInJpm z>cUzr!uo|36+E+R0IqB>aQc<4HT>UMRELV?ROPDV0juse{stH|aLQd1fSP+wRFvu~ zT9WD+*b9h8`{?vwrjn7tojpkCVQ>0D5Q(QQ2C7BH(?xd_KA$|w*huu%q zPb=hVl_^d$F|{YObfx5dx_K(UI1MA#9d2b3yC!042uz1PEg*~j&^rzP0M`qv^G$iz zH)dK!ONUu%b8+R&U4RHqqLMZ#I6FqBG3qs{Y#2 zW{F6gxbi|0pxV`~w0n>_-`f+TE<*nRQfgAAQermb<;rSB(%>!V2W_lBIA}39>CDLx zE2(9(psB!(?Qd4X_rU|+eT`6M=+l^d{-VpHwh(sH=ELU#wwrS0K&A>;P^Y;2&H%ME z&?S{DkGWT{z6A|`|I%~GS(>R5S&r3FP*6vuMEXho;9_Z>=$Z39)hjZg8kk)^A1|c? zaCo^MP9@~($iIh=YERC&)_mFWAh3kITOkk2SoMNA0RI3uPaMy*-d?8Qj#=r%XXwmN zogH!}O;VQ90*VfizpLQlUQaexPm94in*OI{Cg?R4O)5)Cx{?}s9mJhHr6g^9H1ce2 zygt8Aw0b;;dv5~RjIdpK1CZjlu)&U7^r@SAXBM>~+3}_=sh|?zdgT`!f$fLqy!f9{ zF9rE2t3S|o2hloA55dt{mC(wE}*}33;Dx)8x<_Klrrv0r!Jt{6CI#6NL-}= zbPf3q;haC>UKuh~-TDCj*z7;K5>6OIG%HLBphT53IN(Thk3jz;5G0D{WzvM(# z>ZDXqw?&S%I<)(X++y(^X05zr$906LCA1Jt#qLwhkMo9?56?Of8;c$`wAo5nvA*{w z+XFP0^hYjas-9)ElH)1)n406b!{{25+sBEOd={V;jxrKdEyhL%D?!S@?s zEYo`}v@Fu?IukUiJQjYsa%}wRM5*0s5+RV{38N(_&JQ{$X)^Fge3E*5Aw-id5A&Tx_jKWS#v0*kc(-={8oI zCPAw<*i5V-QU^2Z)j8Ua--0TI7h6OEu|I?*rew;yqhh)cfaeH3A)0?K} z*!yFjNslbMTCL?uW00JuHBhEMcNFs{_kp#6zsJrRO+P(ee+qSO+MGi{u01YhnXN^J z`kY}x=Gs8t_`#Beq~~Q->JwP~UJrVNw#rcn#Oe(aET2%{eGuCvC;)MPbBFa3a*|W`3m$NBpG>yn zs?gd>lz0OM2Art8Ai+Cyb{%jv#yr;y|Hm)6KtirY@1 zvEPhuco&*2PiVu{d;eor3mlm>jAn*sq9~6u&i>M`6u&zedi~#m)Tlwa3wD(-JaupKU4J z;*;VW%!ll>>b%vHQ!ylqTDfdBz&*oR>?aQ8LR> zT39k(AN4F1Sn49?_u{}~6e;ei<#fvWI4g1IS_)|iQag!Cfg@)pEK3Ttz|htxk~ZiXz^kcgAN#(qyW)mOCx5+i~4*vLpfl zRmZ5{0d@{R%9(P@n^S5#FC@6}8>j^CEKSAE0Kfm#>Fn!H52ff(6ytG1l2akTUTI4Q zbl6{S&N-}XCO=Ph8A?^UkUdt}C-5-Pwld11(`CSwQGF<3#zwB`*s8<*aXs3ntiL)(f4K&MS5J2Buw%Z9= z;g&OU>G`GUmANa6N_oE7X-F$cT29yYx4r_LobwE-1x1Alf*EoIjnGs<3I~M);{-ZL zs$DdNRj4i8rzjz@adUp{uKBe1`HHs4%|6X5HP&PWyU>HbYL;8V+)z z=WM)@Q;XY@l2YV`z$*&(-+l2r)ly|AqcSx}>K#An6t?N?6vhc&!ALi`Kbzx7-o*Z} z=EUYq)cV*_R2XCF*c^{f+<%jcbNXI^XwgJEPknnoEZrdT8c+WKcs&r)J&=kOFDz2m z+^JWc56jX;lF&9vO~1Y-TCC+vlTV~U6zYRvQj)8#dUo{z$HlO8(jc8xu8Ov7ks34R zL|7-X($A7PKi>@vCO34_ikVde%UhkJ9?DMKtnF>^#H%)Api8b(KAJMVool~#YDJfR zG=&Z9ahSb9>9f#~@}tCQPDb>&w@;~Oo1~l#E*Rp`*!n`{S(nn|dnP+>6*d6-Y=j$I z7|7>QgeK|`#6~`m2y7wHp-9LSv(Wt6ZLL; zpV#hUtOfezrXx6KZooeFy5)5`(p`r(w6gF)8)Vi01@T zV^tVwFQPyS41KEsUiae!wN2^9LCiTtDyT5iPgI_=6on7)ljqcb@r3L2+@C6$xtPs6 z5~BlL=Ikx8Zs&umd@ZU>KsuG39a5D@jj6X4v-(Nnz{B!OnwEq*=)V^`o^Zfxru64B z)u4W%3NU$_TsD;s<-Ncsd@+5EnRB6elA_}hDy#dh}7dynS{)gE@NbbPl) z$`O{tHp1C-1eX#9uuv{ZQ1{0lduyke9`s5Rmz6NeTA7^hbjMh*NU#6{{9<<$V(JS& zNtCC#0bPgEo0X*5Rvg&jynm(d@y)Q^TlE9qaZe`Nh$NezZSmISTe4*};X_1|0TW}$ zd)C{80+5e3JmNhm!C$EomTPS=%0o=HgJ}C$D2s40xpQ~=oYtCK(qmGSxs2Cw&&vHr z9TV_ib1cR~D|NsMw;&z&9x$OzJuIckn_q5HBdO*itGMQ#M*jdl*ml3JfRgn(dYlhT zOjjdWV_34(p~6x*H$K<`%)`PQ?EJ+l_|Lj%v&qlBZvv zcimE2Y?j$=CrC?R>;M?R-lBxVV1EvM2R z`b}X$BptPZfB+Tf*;XwcPGri}6C5LSw$|;Hcu7x;Y-GB2XOWoZvh6{{v-IuhOO1^e z1K;O-3qRoFJeaIaek)K=pp`1lIHC@t$v8aQ{R4F-6Lp%SP&=q$l%%J)vt)pg&iKYt zsrf%8Ex1(KjuvaN$n8&IVPWDij%Dcs?GN7(z8b6rNUNOE+STSSKxqTxz+ zP0t!n0}T?reM+Onb?0A1_>HMe=oc$+-$DTS!s-dF zK8aj%SO{gt+-bz2UDWBaiLmDYHx-=GoS(`xxRjc_W~wze+6r!CYty9wSS3i?jBJ&D ze3lckreL7XsfqcS{@jKEOk|<3Qlhi{R`$Ym$NWG0cKtEG4n0CzU!uflNe(4wnxSHm zeaTN4oY<}B+wotb&So0a%a!LOxuhx7$w=)a*j*_gCfIq}H;fI;+2yu1u@m;fZj~t} z#QcmM(%*?JnxyHjs3qmtomz?DeBi=rQC4+psmWW;7nR&stef8zfG?D&@7Agfu+tjZ zO0MnAuABmN=?WD!be7_~6ecP_{pOF^r}Mpz7K|~s)Qqd#5lp2jK=hP^6Lk+`=K^%E zvlUM?!k(l@r$kb$=n@@qAp=TxHUpAyy-jJ+GpV`ePF__tnqz=TH+5(mr$>Ns4@2I* zG*c_iXMCM5OQ}n52Ha`(&l-w(7+!hGZhF{)(nn@SgjE=bLDWdp4(uSiO`*x<)Ef21d+I{i{ir2+$liw*fU z0{H3vH}Uf|Nkd4Ek+C;Xw)euq#gn74CPaxT=*%V1lz{V^tg)+&BhDPb9Yv((nG!`o zrIfVFuFEJTE~eY+`M?*BQB`85`dX|^l=??3>r#_`05`bCW1UkaWVyi+DhWhM3s>yO zU}DP=Pgsj_^F zZ-rphJtOpIP}||S?kH+x9nHQlMg+%<^Xds{wJh^wi<^HKCbY#!VtW9;76Z<_k$Y%S z8c!z}s-{ZF6wJj>dZSWuMJ@f%ws+8VCy~j(6V^lNw0yNDOVgcHq@`iVGF5v7+s+84 zeSXanrB<5D={AoU0c*CwN<1I!jeuV1W?eO!^j4%Wr=lf6$MkJs;^zc&T|y}_*>%43 z;9@4p3_4PiWccxg(e-~we=bo}XUwUlG;8`>Q=m2djmg2$^(NM4^zwV%<<=sVtwe&A zap&6uTJs(h=+voE-Bk^@B}Q(^TG&V^xc=9}M?L51JE^pJu<3=lptPwVJk)l;DAM1R zdmZpiCOvu<`~}H!E7ounD(YFV3Oo#B9ka(#+{lhHw82c6&l1W~og&K~ zOxw)0s)cVdxTMAH_MkM-u}Dt_!{-NE0&*`%sd6cjU#d2=PYy4z>W!3?B>E1XE^xQ& zKcury$ci#6GLWekPzyozItJp%!d`XN&v=|>Zlly|k`#z7{VZGhQjM0Yc_d)YA{|?3 z)g8))V9?Uag3y8LR{FO2!OSbpvp@7$?;+HLJW!+{Tl&t!3<;x5JvvfaK)T#k-T^1) z4*^-q#(|ZHO<3uZ=`Om_?f_2-AZ>w3R64uSs#!vPLXA8jDM4^4yZW^9J}`#nd`9T? zSxrjrgT4nyhNm6=i|w1C`D`hw*y_R5|Gm7y>dRg%njNe(tot-x<<`{6kM0MbcUo}SayN2I+4$6IPr z?!7LO3f00!C~R%4=4H`mCYPFNW^FO_j?~r?LeZ#h0rPArndvT2WUfid?k$!O3G6%q zUn(1t5_!fF#R{D=er%mquO&h(P;5XTSvMdHdBG`8vsKKQ%P_EE)07yB>1rCnPUFE* z`NpE8XAHY9GGyv(O4EstIvUg3YTlu>h$jJyEOhdlCNyZKno5~BQ^1f`esDod$(=sS zuwj=|O>65~QPsWGayAz~7$=a}@q>=H)8c}nmVxKQQ=A~6TV@vi*5Isj#BD`Rs?1jU~;lNXFH-McrkYP4}2}IubTJ6m9Q~ zpYSmn>UUAqmp|wz@1fZ)Bli}B*&$wS?}Gyz^H!`>D>96!&#C7R`HjH*VX!rogu3gf zZJ<=@jgk^BZ}`B*TQbaL#HGh#TY;*W7g_)$>Ev9FFtiAn?y%(5tqDQ{33otH-DH99 zjKwGjaoGt5G95;w3H1RPICo@?(ZbhX(~VpLStl&M|Q-t@RqH%I{ShJ)zC)@L2dlo14y zBu`KpZ~Xx8znm8JPO?26xsn*x+OAd|E@`+4P6282KIWQ^UxOAUG0x1F6i9hHC^mNE z{V-YMk*is*O(Ejc)wKd5!7529P24s+4sZndT+P)Ma_qL;csdI@0>;WW*qigdF};v_ z)9GCjL#?Aq*0i7>apwb`aXx3#APT<(j8@gXpypW2GBVvkxr{QIDM8ex9KMBqZH3JR z-!F9!EavnkG}H=pQsYuwK7}6v3O)uGseq?YsgTwJCCC#CCsbx!Az{0YLGz8x86)&u(V#uO~LbTj0V9B!mK*o^pwKJ>v=3*4spV)0i_H0=D4e;KrrKwfQ z?M?3F*>A~FzGSM2Ybl$ffsZ@ZcAt02i zP$dcm*C60nd0iny%#=LE1)14?K<+tUxXV7EvOT^rI@XLdRVkm-8)sgyq{?+_xZmzS z{bA(>DA_8HM|3|*X{d3vcV~H0iZ|H!w>ZLzbuX(~g0E0_F6FYOX~|b_zS>B6<9lD= zelQDbCrz#)=DEO3H_FUE(DMbu(oYKJ!AjT=~GUu z{-pFQz_{O~Bxyq_NCQp%HY8l)Xk!&#kxzIf4Lsv*?0X7P2}w`&0pkm5kS{Rx29ZRh zCYsXSen>`(wz4<*-x@(gD-}6axj$0qAS7u(Ci`CaqzCmQFH|coX<<4U2iE$JjJApm9TL#k{rkA$hwGrkPLJg#Q4QBMqdztvxzxB*n^Rp*`UmOGKtat}96c#*x1`GpI8$8_$)t zYk7`B;Hat!PKd!m#GyPJc)lA-KbN4_W71+&kju_f6tx$Sz|?&^T&Cj#O`2!SwX{N` zL_#e3r4gltqe_4t6@mpx8Otd*3F(CN{#;j5R}BQ(^Uo@Q`!JhtO(RTZZIap>-M|X zcv5NTO(~A!%2-Onn*sb^8|Z2`nX}$ys>^k^rnr_;Y=+x9KnHtmgCl4}IwoMO9c)H^ zNLd$h%1|WiH#n??wBbwWu5YMmT!_LW7XeFj?l1Vh4UoEaf&Tywy`(tpQht;rI;`ES zZ}ND;<9%v@(n>T^p7KyqHG14P2HtIKVAe{d7L`tYw&SI=hrY#j&#KlnzT1u5=Ei9ygrq-}00g&v+L966k$n4yx^UgtF z_ZM%qtDmt$jxhK#s=Ykw4LZEZ5@e<&Wp*lzvtXhJkN3o^G(Y!?@lgefHp=g<^=?W} zJp23O4W(ImTP((_x0wnxNQ{yY-kTv|9>5Rv!A`QVD=B7&9j{grxaKHRLK%0rptrHI zQbFGg(T=hqOv^4Bd_xt73cD(?E(8&=Q6%896I^lF6%e-8RHdM6UAH7{{{V~~56K%!f*xJc2IrR3GmHz-wrna>$OVo(q0N^0(O1$5U4^o4)>_(^#QGRnRwh{{Y?OVE&9wP!y#h09;%Ek&JaWBG4SERpLes z?n*)U9Gm|D7+$F)+UShaRgFo?&azw)mb4h{l5OJO>yB#ZzayAwAEv{veN5q@rkvGd zxF?WK403MOCHjU^ue6unakkxa(S3q}AB-+lE>a$BgZfm4+KTXXhmY(ll1H8XF-a~Z zpIB-+f-}@7Ajc)Qkgy7W&26uP&ng6Es7t9&Kv5)u0+sSkEtv!Q=~^Wwy8-s1M?ggl zu#}_(9ZD)5qE&x1uSgrbga?uXBEFj4H&1WZdlqsd8gC^s8XD-O>``9ksSmJ3nzbIOMwYnKqb@v#O_UT3v71J+K3$KRt7v9a zwqg^90;N&$17XUecWkB_R#YAvQJsd99(w*hsX z8kD4s)qFOEiTzVL=hrg=q6&IZK@Nt22=%yx?n3QA^!F7atQJ!pvY**XISKg#-v%}|L!-u>OkF0Y97U*1N}ik>PNZCml2ora zI@sr|*O${yugIB|T2djzSVL+-D*e&-Sos&i(9_;U>f3S>DA~dA%$ENEZK-w%2K!l9 zQ^h>IhXa12CL$AX5CVWgxFGvuQuLQ~A1c6YmF9|S79VZIAq}=jOQlA{*kY6LuYA1b zYp+tIQ=F2DbQS_qpyNeO*p4+5#jsg3&d?Q4bs#z(EvZOTfOs4OfX8l@)XT4;GwWPd zRD_0*1%rfhz5_r1)5m_{Dz&_69OEOV{JE3NwwF?=>S`-#D?%;zY3Eim7@=onQ{%^P zE~v&4l(OI{029T*@-UT&d5bkwa~#w^K1$^Fu9T%`Ym?w%xno{L>BCv3JCY?xEiEpk zrqBr3o$hbr499xDRI24Lm!+i+)1e^t96=UZzDGEnx;Tbcs%xrM9+a=Jn7>)@hg{)(0=Eg`quY=t4G08?j4xCD6^=I8Ye ze@7}kdd<}sj=&AXVW$vVA-1n!Yi)9Ej>zoWfO!^;3TPZiG7u|TcMYNuO-vh6J zp=KuGWk~6XKv1X^uIxr9xRzZ!ENTA$W40&kH1CsGZiz-2gD|bh5?5x?%GKOo^q~X& zFlR>Q30E@o9LUljOMn|)NcUIW7kRKPb!zZ!hMf)JD9&9EpH7(Uyr}Q}Ge`>waaLJ7 z+$Y}F!^@?-;a-c>oZ*z-ZXHz)N{Ee+#DGPU?v-*euOm8FGRIUh<#V>vQq@kX0bNSxr-DPy5w1&Sq+F(U1I8zj4Rg^m)FVytE> z2u;Fq6otcSxQNu>cr7YdbONOkFTuu%C7H(|nXf-orX?u=gtmli7H`fCibkA?)at%T zc~l(59LlFDLA5DY?zBEWH^Na^*Gq|*7=v7=x+PN>(Fr#UI+>-{%L}EX2n)Qsh4&^vFR}X7mt*i`wSJAo)1nz}rEk&!m`j>b!q+J!V8_f zN;f={iWuXU>8O0M4YZX=bc>!2{y&^#?AK7zQr=F>3T@@5TxNABfuw3z=jR;#D<(v4 zqg$UTm9p!NFIv>&igMefM)nwyJ(Gv?1IEzK)%PF963jQL#w%!}W%A*UTX{$}2W10nB>hrMiA8=vkuvR)q!D!i#r)%=lEp`& zl%^$BhNP3Br7ddkHF(DI{u{^t0Mf6TB0$SD8O~EuP_@TeMXh^~MgIWT9IJQu=SZ63 zw5bw3Lh4$Ah55z#aK347v0Rv#NNFf5Rl?7|7=Gu%^~)}2h}Ax#N-*xSE)$%8c)B*8wqRrJbbg%c2??IVKCI6 zOKQ{I3b!fKxV8uq>c8;+06*wWMu`6a{YBKI!*Gzj0QzjNd~FNKQjeaqenOr4ZCR&Z zio&$6G@JTB{{SkmEE#cc%6(MI`JJBHUJxo^TsGpZsa^&A;Z^p3Oou5hYe(95QpWcm z?}4fMh-HQ)B}$s}i26Y3LK5;paU+jHybIuHW6LqnXq6|$HOAXrO~+FCam3w8N4Pk^ zko9Xa$#Sz6gGrJQY5cy=poJa<)9s8~Lb9A{?D;M(O+ibI42NDxcXboZ@43T5Efh%b zom4sNj%CJJyKSW!QrX}We;3B-UO+QfOc~5Mfxha5XpOX|9*~5DtgUyj`LXkbW{M1n znF1Pe^;qeG!;5Yl&H}+!#9!QEeR8zx;nj#nsZU$c+#gn>tGJ)?+hOy8#0xbJ%-Nnb vF0AD$YSg9)vz@gArC{<;IIe0~o=>iY%5~&Pao`CJhZI$0-(hTTR)7E5UpS7e literal 0 HcmV?d00001 diff --git a/www/index.html b/www/index.html index f7b818aee..e558c1fac 100644 --- a/www/index.html +++ b/www/index.html @@ -73,7 +73,7 @@

- SimplifyConsoleExport + BasemapSimplifyConsoleExport
WikiGitHub @@ -148,6 +148,17 @@

File format

+ + @@ -287,6 +299,7 @@

Options

+ diff --git a/www/page.css b/www/page.css index 9d4c76ca9..5805387a9 100644 --- a/www/page.css +++ b/www/page.css @@ -107,14 +107,19 @@ body { height: 29px; } +.basemap-on .coordinate-info { + left: 100px; +} + .coordinate-info { z-index: 1; position: absolute; - bottom: 6px; - left: 6px; + bottom: 7px; + left: 7px; padding: 2px 5px 2px 5px; font-size: 11px; pointer-events: none; + background: rgba(255,255,255,0.4); } .mapshaper-logo { @@ -977,8 +982,84 @@ img.close-btn:hover, height: 100%; } +.basemap-options { + pointer-events: none; +} + +.basemap-options .info-box { + width: min-content; + pointer-events: initial; +} + +.basemap-container { + z-index: -1; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.basemap { + display: none; + position: relative; + height: 100%; +} + +.info-box p { + line-height: 1.2; + font-size: 90%; +} + +.basemap-styles { + white-space: nowrap; +} + +.basemap-note { + width: 100%; + text-align: center; + z-index: -2; + bottom: 8px; + font-size: 22px; + color: #aaa; + position: absolute; +} + +.basemap-styles > div { + display: inline-block; + margin-bottom: 8px; +} + +.basemap-styles > div:nth-child(even) { + position: relative; + left: 6px; + margin-left: 2px; +} + +.basemap-style-btn img { + display: block; + left: -3px; + position: relative; + border: 3px solid transparent; + width: 120px; + height: 90px; +} + +div.basemap-style-btn.active img { + border: 3px solid #e8ba52; +} + +.basemap-style-btn:hover img { + cursor: pointer; + border: 3px solid #ead683; +} + +.mapbox-improve-map { + display: none; +} + .mshp-main-map { - background-color: #fff; + /* background-color: #fff; */ } .map-layers.symbol-hit { @@ -1067,7 +1148,7 @@ img.close-btn:hover, clear: right; white-space: nowrap; display: inline-block; - padding: 2px 7px 4px 7px; + padding: 3px 7px 5px 7px; line-height: 11px; cursor: pointer; } From 85aeb96715372aa3c0289f7df6d87e6d1ca50fd1 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 23 Mar 2022 01:09:29 -0400 Subject: [PATCH 194/891] Remove a file --- www/basemap.js | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 www/basemap.js diff --git a/www/basemap.js b/www/basemap.js deleted file mode 100644 index be29d296d..000000000 --- a/www/basemap.js +++ /dev/null @@ -1,14 +0,0 @@ -window.mapboxParams = { - js: 'https://api.mapbox.com/mapbox-gl-js/v2.7.0/mapbox-gl.js', - css: 'https://api.mapbox.com/mapbox-gl-js/v2.7.0/mapbox-gl.css', - key: 'pk.eyJ1IjoiZ3JhbW1hdGEiLCJhIjoiY2wxMDNwbTVtMGRoZTNjbXQwaXU1amFrOSJ9.yH9yFKkse0gg64coHMmIuw', - styles: [{ - name: 'Map', - icon: 'images/thumb-map.jpg', - url: 'mapbox://styles/grammata/cl0ymri0f000214lc1mgtweqk' - }, { - name: 'Satellite', - icon: 'images/thumb-satellite.jpg', - url: 'mapbox://styles/grammata/cl0ymvkkl004115mlgf5o442n' - }] -}; From d39e2ccb948631c9eb5bfa74b0d5c6bf8ef5fb38 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 23 Mar 2022 01:26:28 -0400 Subject: [PATCH 195/891] Remove Basemap button if basemap.js is missing --- .gitignore | 1 + package.json | 1 + src/gui/gui-basemap-control.js | 47 +++++++++++++++++++--------------- src/gui/gui-map.js | 3 ++- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 632708dd2..241ce3355 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ npm-debug.log /www/mapshaper-gui.js /www/mapshaper.js /www/node_modules.js +/www/basemap.js pre-publish pre-release.js release* diff --git a/package.json b/package.json index 5343b556c..cdbe6344d 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "files": [ "/bin/**", "/www/**", + "!/www/basemap.js", "!/www/nacis/", "/mapshaper.js", "!.DS_Store" diff --git a/src/gui/gui-basemap-control.js b/src/gui/gui-basemap-control.js index 47869adea..15d4f3312 100644 --- a/src/gui/gui-basemap-control.js +++ b/src/gui/gui-basemap-control.js @@ -22,6 +22,7 @@ export function Basemap(gui, ext) { var menu = gui.container.findChild('.basemap-options'); var list = menu.findChild('.basemap-styles'); var container = gui.container.findChild('.basemap-container'); + var basemapBtn = gui.container.findChild('.basemap-btn'); var mapEl = gui.container.findChild('.basemap'); var extentNote = El('div').addClass('basemap-note').appendTo(container).hide(); var params = window.mapboxParams; @@ -29,25 +30,33 @@ export function Basemap(gui, ext) { var activeStyle; var loading = false; - gui.addMode('basemap', turnOn, turnOff, gui.container.findChild('.basemap-btn')); - // model.on('select', function() { - // TODO: hide basemap - // if (gui.getMode() == 'basemap') gui.clearMode(); - // }); - - new SimpleButton(menu.findChild('.close-btn')).on('click', function() { - gui.clearMode(); - turnOff(); - }); - - params.styles.forEach(function(style) { - var btn = El('div').html(`
${style.name}
`); - btn.findChild('.basemap-style-btn').on('click', function() { - updateStyle(style == activeStyle ? null : style); - updateButtons(); + if (params) { + init(); + } else { + basemapBtn.hide(); + } + + function init() { + gui.addMode('basemap', turnOn, turnOff, basemapBtn); + // model.on('select', function() { + // TODO: hide basemap + // if (gui.getMode() == 'basemap') gui.clearMode(); + // }); + + new SimpleButton(menu.findChild('.close-btn')).on('click', function() { + gui.clearMode(); + turnOff(); }); - btn.appendTo(list); - }); + + params.styles.forEach(function(style) { + var btn = El('div').html(`
${style.name}
`); + btn.findChild('.basemap-style-btn').on('click', function() { + updateStyle(style == activeStyle ? null : style); + updateButtons(); + }); + btn.appendTo(list); + }); + } function updateStyle(style) { activeStyle = style || null; @@ -166,5 +175,3 @@ export function Basemap(gui, ext) { return {refresh: refresh}; // called by map when extent changes } - - diff --git a/src/gui/gui-map.js b/src/gui/gui-map.js index ea9966881..d0ba87b88 100644 --- a/src/gui/gui-map.js +++ b/src/gui/gui-map.js @@ -111,6 +111,7 @@ export function MshpMap(gui) { this.getExtent = function() {return _ext;}; this.isActiveLayer = isActiveLayer; this.isVisibleLayer = isVisibleLayer; + this.getActiveLayer = function() { return _activeLyr; }; // called by layer menu after layer visibility is updated this.redraw = function() { @@ -236,7 +237,7 @@ export function MshpMap(gui) { } _ext.on('change', function(e) { - _basemap.refresh(); // keep basemap synced up (if turned on) + if (_basemap) _basemap.refresh(); // keep basemap synced up (if enabled) if (e.reset) return; // don't need to redraw map here if extent has been reset if (isFrameView()) { updateFrameExtent(); From 5f3d7aadd961c71fbce26a36dca42570bf5f263a Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 23 Mar 2022 15:05:33 -0400 Subject: [PATCH 196/891] Fix compatibility between basemap and simplify tool --- src/commands/mapshaper-proj.js | 2 ++ src/gui/gui-display-layer.js | 49 ++++++++++++++++++--------------- src/gui/gui-dynamic-crs.js | 16 +++++------ src/gui/gui-edit-points.js | 15 ++++++---- src/gui/gui-simplify-control.js | 5 +++- src/gui/gui-undo.js | 27 ++++++------------ 6 files changed, 59 insertions(+), 55 deletions(-) diff --git a/src/commands/mapshaper-proj.js b/src/commands/mapshaper-proj.js index a85b83f50..b969239e4 100644 --- a/src/commands/mapshaper-proj.js +++ b/src/commands/mapshaper-proj.js @@ -196,6 +196,7 @@ export function projectArcs(arcs, proj) { // old simplification data will not be optimal after reprojection; // re-using for now to avoid error in web ui zz = data.zz, + z = arcs.getRetainedInterval(), p; for (var i=0, n=xx.length; i -1 && gui.interaction.getMode() == 'location'; } hit.on('dragstart', function(e) { if (!active(e)) return; - symbolInfo = {FID: e.id, target: hit.getHitTarget()}; - gui.dispatchEvent('symbol_dragstart', symbolInfo); + var target = hit.getHitTarget(); + symbolInfo = { + FID: e.id, + startCoords: getPointCoords(target, e.id), + target: target + }; }); hit.on('drag', function(e) { @@ -23,12 +26,12 @@ export function initPointDragging(gui, ext, hit) { p[0] += diff[0]; p[1] += diff[1]; gui.dispatchEvent('map-needs-refresh'); - // gui.dispatchEvent('symbol_drag', {FID: e.id}); }); hit.on('dragend', function(e) { if (!active(e) || !symbolInfo ) return; - updatePointCoords(symbolInfo.target, symbolInfo.FID); + updatePointCoords(symbolInfo.target, e.id); + symbolInfo.endCoords = getPointCoords(symbolInfo.target, e.id); gui.dispatchEvent('symbol_dragend', symbolInfo); symbolInfo = null; }); diff --git a/src/gui/gui-simplify-control.js b/src/gui/gui-simplify-control.js index 14b228377..83e0fb1bd 100644 --- a/src/gui/gui-simplify-control.js +++ b/src/gui/gui-simplify-control.js @@ -2,6 +2,7 @@ import { Slider } from './gui-slider'; import { utils, internal, mapshaper } from './gui-core'; import { SimpleButton, ClickText } from './gui-elements'; import { GUI } from './gui-lib'; +import { setZ, updateZ } from './gui-display-layer'; /* How changes in the simplify control should affect other components @@ -171,6 +172,7 @@ export var SimplifyControl = function(gui) { var opts = getSimplifyOptions(); mapshaper.simplify(dataset, opts); gui.session.simplificationApplied(getSimplifyOptionsAsString()); + updateZ(gui.map.getActiveLayer()); // question: does this update all display layers? model.updated({ // trigger filtered arc rebuild without redraw if pct is 1 simplify_method: opts.percentage == 1, @@ -222,7 +224,8 @@ export var SimplifyControl = function(gui) { function onChange(pct) { if (_value != pct) { _value = pct; - model.getActiveLayer().dataset.arcs.setRetainedInterval(fromPct(pct)); + // model.getActiveLayer().dataset.arcs.setRetainedInterval(fromPct(pct)); + setZ(gui.map.getActiveLayer(), fromPct(pct)); gui.session.updateSimplificationPct(pct); model.updated({'simplify_amount': true}); updateSliderDisplay(); diff --git a/src/gui/gui-undo.js b/src/gui/gui-undo.js index 1090cfd4f..ecb241f87 100644 --- a/src/gui/gui-undo.js +++ b/src/gui/gui-undo.js @@ -1,5 +1,5 @@ import { internal } from './gui-core'; -import { makePointSetter, setVertexCoords, getVertexCoords, insertVertex, deleteVertex, translateDisplayPoint } from './gui-display-layer'; +import { setPointCoords, setVertexCoords, getVertexCoords, insertVertex, deleteVertex, translateDisplayPoint } from './gui-display-layer'; // import { cloneShape } from '../paths/mapshaper-shape-utils'; // import { copyRecord } from '../datatable/mapshaper-data-utils'; @@ -40,15 +40,15 @@ export function Undo(gui) { } }, this, 10); - // undo/redo point/symbol dragging - // - gui.on('symbol_dragstart', function(e) { - stashedUndo = makePointSetter(e.data.target, e.FID); - }, this); - gui.on('symbol_dragend', function(e) { - var redo = makePointSetter(e.data.target, e.FID); - this.addHistoryState(stashedUndo, redo); + var target = e.data.target; + var undo = function() { + setPointCoords(target, e.FID, e.startCoords); + }; + var redo = function() { + setPointCoords(target, e.FID, e.endCoords); + }; + this.addHistoryState(undo, redo); }, this); // undo/redo label dragging @@ -119,15 +119,6 @@ export function Undo(gui) { }; }; - this.makeVertexSetter = function(ids) { - var target = gui.model.getActiveLayer(); - var arcs = target.dataset.arcs; - var p = internal.getVertexCoords(ids[0], arcs); - return function() { - snapVerticesToPoint(ids, p, arcs, true); - }; - }; - this.addHistoryState = function(undo, redo) { if (offset > 0) { history.splice(-offset); From 297d6ab03395e2bbdfbf962dd6b6be80345c874b Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 23 Mar 2022 16:03:27 -0400 Subject: [PATCH 197/891] Bug fix --- src/gui/gui.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gui/gui.js b/src/gui/gui.js index 80442c4fc..baa8407dd 100644 --- a/src/gui/gui.js +++ b/src/gui/gui.js @@ -70,7 +70,9 @@ var startEditing = function() { startEditing = function() {}; window.addEventListener('beforeunload', function(e) { - if (gui.session.unsavedChanges()) { + // don't prompt if there are no datasets (this means the last layer was deleted, + // hitting the 'cancel' button would leave the interface in a bad state) + if (gui.session.unsavedChanges() && !gui.model.isEmpty()) { e.returnValue = 'There are unsaved changes.'; e.preventDefault(); } From f3ba1f8ae01c0f241d2a0c387cba3707c44b2ef6 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 23 Mar 2022 17:17:17 -0400 Subject: [PATCH 198/891] Fix compatibility between basemap and box tool, etc --- src/gui/gui-basemap-control.js | 5 +++++ src/gui/gui-box-tool.js | 16 +++++++++------- src/gui/gui-display-layer.js | 13 ++++++++++++- src/gui/gui-map-nav.js | 4 ++++ 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/gui/gui-basemap-control.js b/src/gui/gui-basemap-control.js index 15d4f3312..308e82b3a 100644 --- a/src/gui/gui-basemap-control.js +++ b/src/gui/gui-basemap-control.js @@ -48,6 +48,11 @@ export function Basemap(gui, ext) { turnOff(); }); + gui.on('map_click', function() { + // close menu if user click on the map + if (gui.getMode() == 'basemap') gui.clearMode(); + }); + params.styles.forEach(function(style) { var btn = El('div').html(`
${style.name}
`); btn.findChild('.basemap-style-btn').on('click', function() { diff --git a/src/gui/gui-box-tool.js b/src/gui/gui-box-tool.js index f190c185e..c3778a16c 100644 --- a/src/gui/gui-box-tool.js +++ b/src/gui/gui-box-tool.js @@ -4,6 +4,7 @@ import { SimpleButton } from './gui-elements'; import { internal } from './gui-core'; import { El } from './gui-el'; import { GUI } from './gui-lib'; +import { getBBoxCoords } from './gui-display-layer'; // Controls both the shift-drag zoom-to-extent tool and the shift-drag editing tool @@ -13,7 +14,7 @@ export function BoxTool(gui, ext, mouse, nav) { var popup = gui.container.findChild('.box-tool-options'); var coords = popup.findChild('.box-coords'); var _on = false; - var bboxCoords, bboxPixels; + var bboxDisplayCoords, bboxPixels, bboxDataCoords; var infoBtn = new SimpleButton(popup.findChild('.info-btn')).on('click', function() { if (coords.visible()) hideCoords(); else showCoords(); @@ -44,7 +45,7 @@ export function BoxTool(gui, ext, mouse, nav) { }); new SimpleButton(popup.findChild('.clip-btn')).on('click', function() { - runCommand('-clip bbox2=' + bboxCoords.join(',')); + runCommand('-clip bbox2=' + bboxDataCoords.join(',')); }); gui.addMode('box_tool', turnOn, turnOff); @@ -60,8 +61,8 @@ export function BoxTool(gui, ext, mouse, nav) { // Update the visible rectangle when the map view changes // (e.g. during zooming or panning) ext.on('change', function() { - if (!_on || !box.visible() || !bboxCoords) return; - var b = coordsToPix(bboxCoords); + if (!_on || !box.visible() || !bboxDisplayCoords) return; + var b = coordsToPix(bboxDisplayCoords); var pos = ext.position(); var dx = pos.pageX, dy = pos.pageY; @@ -76,7 +77,7 @@ export function BoxTool(gui, ext, mouse, nav) { gui.on('box_drag', function(e) { var b = e.page_bbox; bboxPixels = e.map_bbox; - bboxCoords = pixToCoords(bboxPixels); + bboxDisplayCoords = pixToCoords(bboxPixels); if (_on || inZoomMode()) { box.show(b[0], b[1], b[2], b[3]); } @@ -84,7 +85,8 @@ export function BoxTool(gui, ext, mouse, nav) { gui.on('box_drag_end', function(e) { bboxPixels = e.map_bbox; - bboxCoords = pixToCoords(bboxPixels); + bboxDisplayCoords = pixToCoords(bboxPixels); + bboxDataCoords = getBBoxCoords(gui.map.getActiveLayer(), bboxDisplayCoords); if (inZoomMode()) { box.hide(); nav.zoomToBbox(bboxPixels); @@ -108,7 +110,7 @@ export function BoxTool(gui, ext, mouse, nav) { function showCoords() { El(infoBtn.node()).addClass('selected-btn'); - coords.text(bboxCoords.join(',')); + coords.text(bboxDataCoords.join(',')); coords.show(); GUI.selectElement(coords.node()); } diff --git a/src/gui/gui-display-layer.js b/src/gui/gui-display-layer.js index 98a31029f..93b033850 100644 --- a/src/gui/gui-display-layer.js +++ b/src/gui/gui-display-layer.js @@ -4,7 +4,6 @@ import { needReprojectionForDisplay, projectArcsForDisplay, projectPointsForDisp import { filterLayerByIds } from './gui-layer-utils'; import { internal, Bounds, utils } from './gui-core'; - export function setZ(lyr, z) { lyr.source.dataset.arcs.setRetainedInterval(z); if (isProjectedLayer(lyr)) { @@ -40,6 +39,18 @@ export function getPointCoords(lyr, fid) { return internal.cloneShape(lyr.source.layer.shapes[fid]); } +// bbox: display coords +// intended to work with rectangular projections like Mercator +export function getBBoxCoords(lyr, bbox) { + if (!isProjectedLayer(lyr)) return bbox; + var a = translateDisplayPoint(lyr, [bbox[0], bbox[1]]); + var b = translateDisplayPoint(lyr, [bbox[2], bbox[3]]); + var bounds = new internal.Bounds(); + bounds.mergePoint(a[0], a[1]); + bounds.mergePoint(b[0], b[1]); + return bounds.toArray(); +} + export function getVertexCoords(lyr, id) { return lyr.source.dataset.arcs.getVertex2(id); } diff --git a/src/gui/gui-map-nav.js b/src/gui/gui-map-nav.js index 676fe002f..03f5011c8 100644 --- a/src/gui/gui-map-nav.js +++ b/src/gui/gui-map-nav.js @@ -43,6 +43,10 @@ export function MapNav(gui, ext, mouse) { ext.zoomToExtent(e.value, _fx, _fy); }); + mouse.on('click', function(e) { + gui.dispatchEvent('map_click', e); + }); + mouse.on('dblclick', function(e) { if (disabled()) return; zoomByPct(getZoomInPct(), e.x / ext.width(), e.y / ext.height()); From 051c463612141549453296f883f895903be6bbe5 Mon Sep 17 00:00:00 2001 From: Matthew Bloch Date: Wed, 23 Mar 2022 22:25:50 -0400 Subject: [PATCH 199/891] Basemap details --- src/crs/mapshaper-projections.js | 6 +++++- src/gui/gui-basemap-control.js | 6 ++++++ src/paths/mapshaper-arc-utils.js | 26 ++++++++++++++++++++------ www/index.html | 3 ++- www/page.css | 4 ++++ 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/crs/mapshaper-projections.js b/src/crs/mapshaper-projections.js index b1916423a..feba08bcb 100644 --- a/src/crs/mapshaper-projections.js +++ b/src/crs/mapshaper-projections.js @@ -242,7 +242,11 @@ export function isWGS84(P) { export function isWebMercator(P) { if (!P) return false; - return crsToProj4(P) == '+proj=merc +a=6378137 +b=6378137'; + var str = crsToProj4(P); + // e.g. +proj=merc +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +wktext +a=6378137 +b=6378137 +nadgrids=@null + // e.g. +proj=merc +a=6378137 +b=6378137 + // TODO: support https://proj.org/operations/projections/webmerc.html + return str.includes('+proj=merc') && str.includes('+a=6378137') && str.includes('+b=6378137'); } export function isLatLngDataset(dataset) { diff --git a/src/gui/gui-basemap-control.js b/src/gui/gui-basemap-control.js index 308e82b3a..5c192b71c 100644 --- a/src/gui/gui-basemap-control.js +++ b/src/gui/gui-basemap-control.js @@ -23,6 +23,7 @@ export function Basemap(gui, ext) { var list = menu.findChild('.basemap-styles'); var container = gui.container.findChild('.basemap-container'); var basemapBtn = gui.container.findChild('.basemap-btn'); + var basemapMsg = gui.container.findChild('.basemap-error'); var mapEl = gui.container.findChild('.basemap'); var extentNote = El('div').addClass('basemap-note').appendTo(container).hide(); var params = window.mapboxParams; @@ -83,10 +84,15 @@ export function Basemap(gui, ext) { } function turnOn() { + var crs = gui.map.getDisplayCRS(); + if (!internal.isWebMercator(crs) && !internal.isWGS84(crs)) { + basemapMsg.html('The current projection is not compatible.'); + } menu.show(); } function turnOff() { + basemapMsg.html(''); menu.hide(); } diff --git a/src/paths/mapshaper-arc-utils.js b/src/paths/mapshaper-arc-utils.js index b710c1488..323000f03 100644 --- a/src/paths/mapshaper-arc-utils.js +++ b/src/paths/mapshaper-arc-utils.js @@ -30,6 +30,8 @@ export function deleteVertex(arcs, i) { // avoid re-allocating memory var xx2 = new Float64Array(data.xx.buffer, 0, n-1); var yy2 = new Float64Array(data.yy.buffer, 0, n-1); + var zz2 = arcs.isFlat() ? null : new Float64Array(data.zz.buffer, 0, n-1); + var z = arcs.getRetainedInterval(); var count = 0; var found = false; for (var j=0; j= data.xx.length * 8 + 8) { xx2 = new Float64Array(data.xx.buffer, 0, n+1); yy2 = new Float64Array(data.yy.buffer, 0, n+1); } else { - xx2 = new Float64Array(new ArrayBuffer((n + 20) * 8), 0, n+1); - yy2 = new Float64Array(new ArrayBuffer((n + 20) * 8), 0, n+1); + xx2 = new Float64Array(new ArrayBuffer((n + 50) * 8), 0, n+1); + yy2 = new Float64Array(new ArrayBuffer((n + 50) * 8), 0, n+1); + } + if (!arcs.isFlat()) { + zz2 = new Float64Array(new ArrayBuffer((n + 1) * 8), 0, n+1); } for (var j=0; jFile format