mapshaper 0.7.6 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mapshaper.js CHANGED
@@ -18443,7 +18443,7 @@
18443
18443
  function exportSVG(dataset, opts) {
18444
18444
  var namespace = 'xmlns="http://www.w3.org/2000/svg"';
18445
18445
  var defs = [];
18446
- var frame, svg, layers;
18446
+ var frame, svg, layers, metadataJSON;
18447
18447
  var style = '';
18448
18448
 
18449
18449
  // kludge for map keys
@@ -18467,6 +18467,9 @@
18467
18467
  frame = getFrameData(dataset, opts);
18468
18468
  fitDatasetToFrame(dataset, frame);
18469
18469
  setCoordinatePrecision(dataset, opts.precision || 0.01);
18470
+ if (opts.metadata) {
18471
+ metadataJSON = JSON.stringify(getGeospatialMetadata(dataset, frame));
18472
+ }
18470
18473
 
18471
18474
  // error if one or more svg_data fields are not present in any layers
18472
18475
  if (opts.svg_data) validateSvgDataFields(dataset.layers, opts.svg_data);
@@ -18486,6 +18489,10 @@
18486
18489
  return stringify(obj);
18487
18490
  }).join('\n');
18488
18491
 
18492
+ if (metadataJSON) {
18493
+ svg = getMetadataBlock(metadataJSON, [0, 0, frame.width, frame.height]) + svg;
18494
+ }
18495
+
18489
18496
  if (defs.length > 0) {
18490
18497
  svg = '<defs>\n' + utils.pluck(defs, 'svg').join('') + '</defs>\n' + svg;
18491
18498
  }
@@ -18513,6 +18520,57 @@ ${svg}
18513
18520
  }];
18514
18521
  }
18515
18522
 
18523
+ function getMetadataBlock(metadataJSON, viewBox) {
18524
+ var x = 0;
18525
+ var y = 0;
18526
+ var width = viewBox && viewBox[2] || 0;
18527
+ var height = viewBox && viewBox[3] || 0;
18528
+ return '<metadata>' + metadataJSON + '</metadata>\n' +
18529
+ '<g id="mapshaper-metadata">\n' +
18530
+ '<rect x="' + x + '" y="' + y + '" width="' + width + '" height="' + height + '" opacity="0"/>\n' +
18531
+ '<text opacity="0" font-size="0.1">' + metadataJSON + '</text>\n' +
18532
+ '</g>\n';
18533
+ }
18534
+
18535
+ function getGeospatialMetadata(dataset, frame) {
18536
+ return {
18537
+ crs: getSVGMetadataCRS(dataset),
18538
+ bbox: frame.bbox || null
18539
+ };
18540
+ }
18541
+
18542
+ function getSVGMetadataCRS(dataset) {
18543
+ var crs = getDatasetCRS(dataset);
18544
+ var info = getDatasetCrsInfo(dataset);
18545
+ var proj4 = null;
18546
+ if (crs) {
18547
+ proj4 = crsToProj4(crs);
18548
+ }
18549
+ if (proj4) return proj4;
18550
+ return getAuthorityCodeString(info) || null;
18551
+ }
18552
+
18553
+ function getAuthorityCodeString(info) {
18554
+ var authority = null;
18555
+ if (info && info.crs_string) {
18556
+ authority = parseAuthorityCodeString(info.crs_string);
18557
+ }
18558
+ if (!authority && info && info.wkt1) {
18559
+ authority = parseAuthorityCodeFromWkt(info.wkt1);
18560
+ }
18561
+ if (!authority && info && info.geopackage_crs) {
18562
+ authority = parseGeoPackageAuthority(info.geopackage_crs);
18563
+ }
18564
+ return authority ? authority.authority + ':' + authority.code : null;
18565
+ }
18566
+
18567
+ function parseGeoPackageAuthority(o) {
18568
+ if (!o) return null;
18569
+ var authority = o.organization || o.authority || null;
18570
+ var code = o.organization_coordsys_id || o.code || null;
18571
+ return authority && code != null ? {authority: String(authority).toUpperCase(), code: String(code)} : null;
18572
+ }
18573
+
18516
18574
  function exportFurnitureLayerForSVG(lyr, frame, opts) {
18517
18575
  var layerObj = getEmptyLayerForSVG(lyr, opts);
18518
18576
  layerObj.children = renderFurnitureLayer(lyr, frame);
@@ -28017,6 +28075,10 @@ ${svg}
28017
28075
  old_alias: 'json-subtree',
28018
28076
  describe: '[JSON] path to JSON input data; separator is /'
28019
28077
  })
28078
+ .option('ndjson', {
28079
+ type: 'flag',
28080
+ describe: '[JSON/GeoJSON] input is newline-delimited JSON objects'
28081
+ })
28020
28082
  .option('single-part', {
28021
28083
  type: 'flag',
28022
28084
  // describe: '[GeoJSON] split multi-part features into single-part features'
@@ -28109,10 +28171,6 @@ ${svg}
28109
28171
  describe: '[TopoJSON] export coordinates without quantization',
28110
28172
  type: 'flag'
28111
28173
  })
28112
- .option('metadata', {
28113
- // describe: '[TopoJSON] Add a metadata object containing CRS information',
28114
- type: 'flag'
28115
- })
28116
28174
  .option('no-point-quantization', {
28117
28175
  // describe: '[TopoJSON] export point coordinates without quantization',
28118
28176
  type: 'flag'
@@ -28155,6 +28213,10 @@ ${svg}
28155
28213
  describe: '[GeoJSON/JSON] output newline-delimited features or records',
28156
28214
  type: 'flag'
28157
28215
  })
28216
+ .option('metadata', {
28217
+ describe: '[SVG/TopoJSON] include metadata in output',
28218
+ type: 'flag'
28219
+ })
28158
28220
  .option('width', {
28159
28221
  describe: '[SVG/TopoJSON] pixel width of output (SVG default is 800)',
28160
28222
  type: 'number'
@@ -28217,10 +28279,7 @@ ${svg}
28217
28279
  .option('final', {
28218
28280
  type: 'flag' // for testing
28219
28281
  })
28220
- .option('metadata', {
28221
- // describe: '[TopoJSON] add a metadata object',
28222
- type: 'flag'
28223
- });
28282
+ ;
28224
28283
 
28225
28284
  parser.section('Editing commands');
28226
28285
 
@@ -32479,7 +32538,13 @@ ${svg}
32479
32538
  var str = readFirstChars(reader, 1000);
32480
32539
  var type = identifyJSONString(str, opts);
32481
32540
  var dataset, retn;
32482
- if (type == 'geojson') { // consider only for larger files
32541
+ if (opts.ndjson) {
32542
+ // NDJSON can represent either newline-delimited GeoJSON features/geometries
32543
+ // or plain JSON records. Defer type detection until after line parsing.
32544
+ retn = {
32545
+ content: reader.toString('utf8')
32546
+ };
32547
+ } else if (type == 'geojson') { // consider only for larger files
32483
32548
  dataset = importGeoJSONFile(reader, opts);
32484
32549
  retn = {
32485
32550
  dataset: dataset,
@@ -32531,6 +32596,12 @@ ${svg}
32531
32596
  }
32532
32597
 
32533
32598
  if (content) {
32599
+ if (opts.ndjson) {
32600
+ var nd = importNDJSON(content, opts);
32601
+ retn.dataset = nd.dataset;
32602
+ retn.format = nd.format;
32603
+ return retn;
32604
+ }
32534
32605
  if (utils.isString(content)) {
32535
32606
  try {
32536
32607
  content = JSON.parse(content); // ~3sec for 100MB string
@@ -32569,6 +32640,54 @@ ${svg}
32569
32640
  });
32570
32641
  }
32571
32642
 
32643
+ function importNDJSON(content, opts) {
32644
+ var lines = utils.isString(content) ? utils.splitLines(content) : [];
32645
+ var objects = [];
32646
+ var firstGeo = null;
32647
+ for (var i = 0; i < lines.length; i++) {
32648
+ var raw = lines[i].trim();
32649
+ if (!raw) continue;
32650
+ var obj;
32651
+ try {
32652
+ obj = JSON.parse(raw);
32653
+ } catch (e) {
32654
+ stop$1('NDJSON parsing error on line ' + (i + 1) + ': ' + e.message);
32655
+ }
32656
+ objects.push(obj);
32657
+ if (firstGeo === null) {
32658
+ firstGeo = isGeoJSONObject(obj);
32659
+ } else if (firstGeo !== isGeoJSONObject(obj)) {
32660
+ stop$1('NDJSON input mixes GeoJSON and non-GeoJSON objects');
32661
+ }
32662
+ }
32663
+ if (objects.length === 0) {
32664
+ stop$1('NDJSON input does not contain any JSON objects');
32665
+ }
32666
+ if (!firstGeo) {
32667
+ return {dataset: importJSONTable(objects), format: 'json'};
32668
+ }
32669
+ var parser = new GeoJSONParser(getGeoJSONImportOpts(opts));
32670
+ objects.forEach(function(obj) {
32671
+ if (obj && obj.type == 'FeatureCollection') {
32672
+ (obj.features || []).forEach(parser.parseObject);
32673
+ } else if (obj && obj.type == 'GeometryCollection') {
32674
+ (obj.geometries || []).forEach(parser.parseObject);
32675
+ } else {
32676
+ parser.parseObject(obj);
32677
+ }
32678
+ });
32679
+ return {dataset: parser.done(), format: 'geojson'};
32680
+ }
32681
+
32682
+ function isGeoJSONObject(obj) {
32683
+ if (!obj || typeof obj != 'object') return false;
32684
+ var type = obj.type;
32685
+ return type == 'Feature' || type == 'FeatureCollection' ||
32686
+ type == 'GeometryCollection' || type == 'Point' || type == 'MultiPoint' ||
32687
+ type == 'LineString' || type == 'MultiLineString' ||
32688
+ type == 'Polygon' || type == 'MultiPolygon';
32689
+ }
32690
+
32572
32691
  // path: path from top-level to the target object
32573
32692
  function selectFromObject(o, path) {
32574
32693
  var arrayRxp = /(.*)\[([0-9]+)\]$/; // array bracket notation w/ index
@@ -33159,6 +33278,7 @@ ${svg}
33159
33278
  var Parser = typeof DOMParser == 'undefined' ? require$1('@xmldom/xmldom').DOMParser : DOMParser;
33160
33279
  var doc = new Parser().parseFromString(str, 'text/xml');
33161
33280
  var root = doc && doc.documentElement;
33281
+ var geometadata = parseSVGGeometadata(root, str);
33162
33282
  var groups = getTopLevelLayerGroups(root);
33163
33283
  var layerData = [];
33164
33284
  var datasets = [];
@@ -33175,7 +33295,11 @@ ${svg}
33175
33295
  });
33176
33296
  });
33177
33297
 
33178
- flipYCoordinates(layerData);
33298
+ if (geometadata && geometadata.bbox && geometadata.viewBox) {
33299
+ remapCoordinatesFromSVGToMap(layerData, geometadata.viewBox, geometadata.bbox);
33300
+ } else {
33301
+ flipYCoordinates(layerData);
33302
+ }
33179
33303
 
33180
33304
  layerData.forEach(function(layer) {
33181
33305
  datasets.push(importLayerFeatures(layer.name, layer.features, opts));
@@ -33185,9 +33309,237 @@ ${svg}
33185
33309
  return {layers: [], info: {}};
33186
33310
  }
33187
33311
  if (datasets.length === 1) {
33188
- return datasets[0];
33312
+ return applySVGGeometadata(datasets[0], geometadata);
33189
33313
  }
33190
- return mergeDatasets(datasets);
33314
+ return applySVGGeometadata(mergeDatasets(datasets), geometadata);
33315
+ }
33316
+
33317
+ function applySVGGeometadata(dataset, geometadata) {
33318
+ if (geometadata && geometadata.crs) {
33319
+ setDatasetCrsInfo(dataset, getCrsInfo(geometadata.crs));
33320
+ }
33321
+ return dataset;
33322
+ }
33323
+
33324
+ function parseSVGGeometadata(root, svgText) {
33325
+ var json = findMetadataJSONString(root, svgText);
33326
+ var obj;
33327
+ var metadataViewport = getMetadataViewportRect(root);
33328
+ if (!json) return null;
33329
+ obj = parseMetadataJSONObject(json);
33330
+ if (!obj) return null;
33331
+ return {
33332
+ crs: normalizeMetadataCRS(obj.crs),
33333
+ bbox: normalizeMetadataBBox(obj.bbox),
33334
+ viewBox: metadataViewport || parseSVGViewBox(root && root.getAttribute && root.getAttribute('viewBox'))
33335
+ };
33336
+ }
33337
+
33338
+ function findMetadataJSONString(root, svgText) {
33339
+ var text = getMetadataTagText(root) || getHiddenMetadataText(root);
33340
+ if (!text && svgText) {
33341
+ text = extractHiddenMetadataFromSource(svgText);
33342
+ }
33343
+ return normalizeMetadataJSONText(text);
33344
+ }
33345
+
33346
+ function getMetadataTagText(root) {
33347
+ var nodes = getElementChildren(root);
33348
+ for (var i = 0; i < nodes.length; i++) {
33349
+ if (getTagName(nodes[i]) == 'metadata') {
33350
+ return nodes[i].textContent || '';
33351
+ }
33352
+ }
33353
+ return null;
33354
+ }
33355
+
33356
+ function getHiddenMetadataText(root) {
33357
+ var node = getMetadataGroupNode(root);
33358
+ if (node) {
33359
+ return node.textContent || '';
33360
+ }
33361
+ return null;
33362
+ }
33363
+
33364
+ function getMetadataGroupNode(root) {
33365
+ var nodes = getElementChildren(root);
33366
+ var i, node;
33367
+ for (i = 0; i < nodes.length; i++) {
33368
+ node = nodes[i];
33369
+ if (getTagName(node) == 'g' && node.getAttribute && node.getAttribute('id') == 'mapshaper-metadata') {
33370
+ return node;
33371
+ }
33372
+ }
33373
+ return null;
33374
+ }
33375
+
33376
+ function getMetadataViewportRect(root) {
33377
+ var node = getMetadataGroupNode(root);
33378
+ var children, i, rect;
33379
+ if (!node) return null;
33380
+ children = getElementChildren(node);
33381
+ for (i = 0; i < children.length; i++) {
33382
+ if (getTagName(children[i]) == 'rect') {
33383
+ rect = parseMetadataRectNode(children[i]);
33384
+ if (rect) return rect;
33385
+ }
33386
+ }
33387
+ return null;
33388
+ }
33389
+
33390
+ function parseMetadataRectNode(node) {
33391
+ var x = parseNumber(node.getAttribute('x')) || 0;
33392
+ var y = parseNumber(node.getAttribute('y')) || 0;
33393
+ var width = parseNumber(node.getAttribute('width'));
33394
+ var height = parseNumber(node.getAttribute('height'));
33395
+ var pts, i, p;
33396
+ var xmin = Infinity, ymin = Infinity, xmax = -Infinity, ymax = -Infinity;
33397
+ if (!isFiniteNumber(width) || !isFiniteNumber(height) || width === 0 || height === 0) {
33398
+ return null;
33399
+ }
33400
+ pts = [
33401
+ [x, y],
33402
+ [x + width, y],
33403
+ [x, y + height],
33404
+ [x + width, y + height]
33405
+ ];
33406
+ for (i = 0; i < pts.length; i++) {
33407
+ p = applyNodeTransformChain(node, pts[i]);
33408
+ xmin = Math.min(xmin, p[0]);
33409
+ ymin = Math.min(ymin, p[1]);
33410
+ xmax = Math.max(xmax, p[0]);
33411
+ ymax = Math.max(ymax, p[1]);
33412
+ }
33413
+ if (!isFiniteNumber(xmin) || !isFiniteNumber(ymin) || !isFiniteNumber(xmax) || !isFiniteNumber(ymax)) {
33414
+ return null;
33415
+ }
33416
+ return [xmin, ymin, xmax - xmin, ymax - ymin];
33417
+ }
33418
+
33419
+ function applyNodeTransformChain(node, point) {
33420
+ var chain = [];
33421
+ var p = [point[0], point[1]];
33422
+ var i;
33423
+ while (node && node.nodeType == 1) {
33424
+ chain.unshift(node);
33425
+ node = node.parentNode;
33426
+ }
33427
+ for (i = 0; i < chain.length; i++) {
33428
+ p = applyTransformString(chain[i].getAttribute && chain[i].getAttribute('transform'), p);
33429
+ }
33430
+ return p;
33431
+ }
33432
+
33433
+ function applyTransformString(str, point) {
33434
+ var re = /([a-z]+)\s*\(([^)]*)\)/ig;
33435
+ var m = null;
33436
+ var p = [point[0], point[1]];
33437
+ while ((m = re.exec(String(str || '')))) {
33438
+ p = applyTransformCommand(m[1].toLowerCase(), parseTransformNumbers(m[2]), p);
33439
+ }
33440
+ return p;
33441
+ }
33442
+
33443
+ function parseTransformNumbers(str) {
33444
+ return String(str || '')
33445
+ .trim()
33446
+ .split(/[\s,]+/)
33447
+ .filter(Boolean)
33448
+ .map(Number)
33449
+ .filter(function(v) { return isFiniteNumber(v); });
33450
+ }
33451
+
33452
+ function applyTransformCommand(cmd, nums, point) {
33453
+ var x = point[0], y = point[1];
33454
+ var sx, sy;
33455
+ if (cmd == 'translate') {
33456
+ return [x + (nums[0] || 0), y + (nums.length > 1 ? nums[1] : 0)];
33457
+ }
33458
+ if (cmd == 'scale') {
33459
+ sx = nums.length > 0 ? nums[0] : 1;
33460
+ sy = nums.length > 1 ? nums[1] : sx;
33461
+ return [x * sx, y * sy];
33462
+ }
33463
+ if (cmd == 'matrix' && nums.length >= 6) {
33464
+ return [
33465
+ nums[0] * x + nums[2] * y + nums[4],
33466
+ nums[1] * x + nums[3] * y + nums[5]
33467
+ ];
33468
+ }
33469
+ return point;
33470
+ }
33471
+
33472
+ function extractHiddenMetadataFromSource(str) {
33473
+ var m = String(str || '').match(/<g[^>]*\bid\s*=\s*["']mapshaper-metadata["'][^>]*>([\s\S]*?)<\/g>/i);
33474
+ if (!m) return null;
33475
+ return stripMarkupTags(m[1]);
33476
+ }
33477
+
33478
+ function stripMarkupTags(str) {
33479
+ return String(str || '').replace(/<[^>]*>/g, '');
33480
+ }
33481
+
33482
+ function normalizeMetadataJSONText(str) {
33483
+ if (!str) return null;
33484
+ return decodeHTMLEntities(stripMarkupTags(String(str))).trim();
33485
+ }
33486
+
33487
+ function decodeHTMLEntities(str) {
33488
+ return String(str || '')
33489
+ .replace(/&#34;|&#x22;/gi, '"')
33490
+ .replace(/&#39;|&#x27;/gi, "'")
33491
+ .replace(/&#38;|&#x26;/gi, '&')
33492
+ .replace(/&#60;|&#x3c;/gi, '<')
33493
+ .replace(/&#62;|&#x3e;/gi, '>')
33494
+ .replace(/&quot;/g, '"')
33495
+ .replace(/&apos;/g, "'")
33496
+ .replace(/&amp;/g, '&')
33497
+ .replace(/&lt;/g, '<')
33498
+ .replace(/&gt;/g, '>');
33499
+ }
33500
+
33501
+ function parseMetadataJSONObject(text) {
33502
+ try {
33503
+ return JSON.parse(text);
33504
+ } catch (e) {
33505
+ return null;
33506
+ }
33507
+ }
33508
+
33509
+ function normalizeMetadataCRS(crs) {
33510
+ return typeof crs == 'string' && crs.trim() ? crs.trim() : null;
33511
+ }
33512
+
33513
+ function normalizeMetadataBBox(bbox) {
33514
+ if (!Array.isArray(bbox) || bbox.length != 4) return null;
33515
+ var out = bbox.map(function(v) { return Number(v); });
33516
+ return out.every(isFiniteNumber) ? out : null;
33517
+ }
33518
+
33519
+ function parseSVGViewBox(str) {
33520
+ var parts = String(str || '').trim().split(/[\s,]+/).map(Number);
33521
+ if (parts.length != 4 || !parts.every(isFiniteNumber) || parts[2] === 0 || parts[3] === 0) {
33522
+ return null;
33523
+ }
33524
+ return parts;
33525
+ }
33526
+
33527
+ function remapCoordinatesFromSVGToMap(layerData, viewBox, bbox) {
33528
+ var vx = viewBox[0], vy = viewBox[1], vw = viewBox[2], vh = viewBox[3];
33529
+ var minX = bbox[0], minY = bbox[1], maxX = bbox[2], maxY = bbox[3];
33530
+ var scaleX = (maxX - minX) / vw;
33531
+ var scaleY = (maxY - minY) / vh;
33532
+
33533
+ layerData.forEach(function(layer) {
33534
+ layer.features.forEach(function(feature) {
33535
+ forEachGeometryCoordinate(feature && feature.geometry, function(coord) {
33536
+ var sx = coord[0];
33537
+ var sy = coord[1];
33538
+ coord[0] = minX + (sx - vx) * scaleX;
33539
+ coord[1] = maxY - (sy - vy) * scaleY;
33540
+ });
33541
+ });
33542
+ });
33191
33543
  }
33192
33544
 
33193
33545
  function importLayerFeatures(layerName, features, opts) {
@@ -33215,6 +33567,7 @@ ${svg}
33215
33567
  childNodes.forEach(function(node) {
33216
33568
  var tag = getTagName(node);
33217
33569
  if (tag == 'defs') return;
33570
+ if (tag == 'g' && node.getAttribute && node.getAttribute('id') == 'mapshaper-metadata') return;
33218
33571
  if (tag == 'g') {
33219
33572
  groups.push({
33220
33573
  node: node,
@@ -41265,7 +41618,7 @@ ${svg}
41265
41618
  // @type: 'clip' or 'erase'
41266
41619
  function clipLayers(targetLayers, clipSrc, targetDataset, type, opts) {
41267
41620
  profileStart('clipLayers');
41268
- opts = opts || {no_fcleanup: true}; // TODO: update testing functions
41621
+ opts = opts || {no_cleanup: true}; // TODO: update testing functions
41269
41622
  var usingPathClip = utils.some(targetLayers, layerHasPaths);
41270
41623
  var mergedDataset, clipLyr, nodes, result;
41271
41624
  var clipDataset = normalizeOverlaySource(clipSrc, targetDataset, opts);
@@ -48147,17 +48500,62 @@ ${svg}
48147
48500
  });
48148
48501
  }
48149
48502
 
48503
+ function parseWKTPoint(val) {
48504
+ if (!utils.isString(val)) return null;
48505
+ // Support 2D POINT WKT, case-insensitive, with optional Z/M tokens.
48506
+ // Examples:
48507
+ // POINT (1 2)
48508
+ // point(1 2)
48509
+ // POINT Z (1 2 3)
48510
+ // POINT M (1 2 3)
48511
+ // POINT ZM (1 2 3 4)
48512
+ var m = /^\s*POINT(?:\s+Z(?:M)?|\s+M)?\s*\(\s*([^\s,]+)\s+([^\s,]+)(?:\s+[^)]*)?\)\s*$/i.exec(val);
48513
+ if (!m) return null;
48514
+ var x = coordinateFromValue(m[1]);
48515
+ var y = coordinateFromValue(m[2]);
48516
+ if (isNaN(x) || isNaN(y)) return null;
48517
+ return [x, y];
48518
+ }
48519
+
48520
+ function findWKTField(records, fields) {
48521
+ var sampleSize = Math.min(records.length, 50);
48522
+ return utils.find(fields, function(name) {
48523
+ var seen = 0;
48524
+ var matches = 0;
48525
+ for (var i = 0; i < sampleSize; i++) {
48526
+ var rec = records[i];
48527
+ if (!rec) continue;
48528
+ var val = rec[name];
48529
+ if (!utils.isString(val) || val.trim() === '') continue;
48530
+ seen++;
48531
+ if (parseWKTPoint(val)) matches++;
48532
+ }
48533
+ // Require at least one non-empty sample and majority parseable as POINT.
48534
+ return seen > 0 && matches / seen >= 0.6;
48535
+ });
48536
+ }
48537
+
48150
48538
  function pointsFromDataTableAuto(data) {
48151
48539
  var fields = data ? data.getFields() : [];
48540
+ var records = data ? data.getRecords() : [];
48152
48541
  var opts = {
48153
48542
  x: findXField(fields),
48154
48543
  y: findYField(fields)
48155
48544
  };
48545
+ if (!opts.x || !opts.y) {
48546
+ opts.wkt = findWKTField(records, fields);
48547
+ }
48156
48548
  return pointsFromDataTable(data, opts);
48157
48549
  }
48158
48550
 
48159
48551
  function pointsFromDataTable(data, opts) {
48160
48552
  if (!data) stop$1("Layer is missing a data table");
48553
+ if (opts.wkt && data.fieldExists(opts.wkt)) {
48554
+ return data.getRecords().map(function(rec) {
48555
+ var p = parseWKTPoint(rec[opts.wkt]);
48556
+ return p ? [p] : null;
48557
+ });
48558
+ }
48161
48559
  if (!opts.x || !opts.y || !data.fieldExists(opts.x) || !data.fieldExists(opts.y)) {
48162
48560
  stop$1("Missing x,y data fields");
48163
48561
  }
@@ -48175,8 +48573,10 @@ ${svg}
48175
48573
  var Points = /*#__PURE__*/Object.freeze({
48176
48574
  __proto__: null,
48177
48575
  coordinateFromValue: coordinateFromValue,
48576
+ findWKTField: findWKTField,
48178
48577
  findXField: findXField,
48179
48578
  findYField: findYField,
48579
+ parseWKTPoint: parseWKTPoint,
48180
48580
  pointsFromPolygons: pointsFromPolygons
48181
48581
  });
48182
48582
 
@@ -53074,7 +53474,7 @@ ${svg}
53074
53474
  });
53075
53475
  }
53076
53476
 
53077
- var version = "0.7.6";
53477
+ var version = "0.7.7";
53078
53478
 
53079
53479
  // Parse command line args into commands and run them
53080
53480
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "A tool for editing vector datasets for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
package/www/index.html CHANGED
@@ -1,22 +1,18 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <title>mapshaper</title>
5
- <meta name="Description" content="A tool for topologically aware shape simplification. Reads and writes Shapefile, GeoJSON, TopoJSON, KML, GeoPackage and FlatGeobuf formats.">
4
+ <title>Mapshaper</title>
5
+ <meta name="Description" content="A topology-aware tool for editing and converting geospatial vector data. Works with Shapefile, GeoJSON, TopoJSON, GeoPackage, FlatGeobuf and KML — in the browser or on the command line.">
6
6
  <meta charset="UTF-8">
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
8
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
9
9
  <script>
10
- // If the page is opened with preloaded data (?files= or ?catalog= in the
11
- // URL), tag <html> so CSS hides the splash-screen header links right away.
12
- // Without this they flash into view for ~a second before the editor mounts
13
- // and hides them. Done as the first script in <head> so the class is set
14
- // before any layout. bin/mapshaper-gui adds the same class server-side
15
- // when files are passed on the command line.
16
10
  (function() {
17
11
  try {
18
12
  var s = location.search || '';
19
- if (s.indexOf('files=') !== -1 || s.indexOf('catalog=') !== -1) {
13
+ if (!window.location.hostname.endsWith("mapshaper.org") ||
14
+ s.indexOf('files=') !== -1 || s.indexOf('catalog=') !== -1) {
15
+ // hide spash-screen header links
20
16
  document.documentElement.className += ' mapshaper-preload';
21
17
  }
22
18
  } catch (e) {}
@@ -26,7 +22,7 @@
26
22
  <link rel="stylesheet" href="elements.css">
27
23
  <link rel="icon" type="image/png" href="images/icon.png">
28
24
  </head>
29
- <body class="mapshaper-preload">
25
+ <body>
30
26
  <div class="hidden">
31
27
  <svg version="1.1" id="home-icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
32
28
  y="0px" width="14px" height="19px" viewBox="0 0 14 16">
@@ -72,7 +68,7 @@
72
68
  <div class="page-header">
73
69
  <div class="mapshaper-logo">map<span class="logo-highlight">shaper</span></div>
74
70
 
75
- <div class="layer-control-btn"><span class="btn header-btn layer-name"></span></div>
71
+ <div class="layer-control-btn icon-duo"><span class="btn header-btn layer-name"></span></div>
76
72
 
77
73
  <div class="simplify-control-wrapper"><div class="simplify-control"><div class="header-btn btn simplify-settings-btn">Settings</div>
78
74
  <div class="slider">
@@ -15412,7 +15412,8 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
15412
15412
  gui = new GuiInstance('body');
15413
15413
 
15414
15414
  if (!importOpts.files?.length) {
15415
- El('body').removeClass('mapshaper-preload');
15415
+ // show header links if not preloading files
15416
+ document.documentElement.classList.remove('mapshaper-preload');
15416
15417
  }
15417
15418
 
15418
15419
  new AlertControl(gui);
package/www/mapshaper.js CHANGED
@@ -18443,7 +18443,7 @@
18443
18443
  function exportSVG(dataset, opts) {
18444
18444
  var namespace = 'xmlns="http://www.w3.org/2000/svg"';
18445
18445
  var defs = [];
18446
- var frame, svg, layers;
18446
+ var frame, svg, layers, metadataJSON;
18447
18447
  var style = '';
18448
18448
 
18449
18449
  // kludge for map keys
@@ -18467,6 +18467,9 @@
18467
18467
  frame = getFrameData(dataset, opts);
18468
18468
  fitDatasetToFrame(dataset, frame);
18469
18469
  setCoordinatePrecision(dataset, opts.precision || 0.01);
18470
+ if (opts.metadata) {
18471
+ metadataJSON = JSON.stringify(getGeospatialMetadata(dataset, frame));
18472
+ }
18470
18473
 
18471
18474
  // error if one or more svg_data fields are not present in any layers
18472
18475
  if (opts.svg_data) validateSvgDataFields(dataset.layers, opts.svg_data);
@@ -18486,6 +18489,10 @@
18486
18489
  return stringify(obj);
18487
18490
  }).join('\n');
18488
18491
 
18492
+ if (metadataJSON) {
18493
+ svg = getMetadataBlock(metadataJSON, [0, 0, frame.width, frame.height]) + svg;
18494
+ }
18495
+
18489
18496
  if (defs.length > 0) {
18490
18497
  svg = '<defs>\n' + utils.pluck(defs, 'svg').join('') + '</defs>\n' + svg;
18491
18498
  }
@@ -18513,6 +18520,57 @@ ${svg}
18513
18520
  }];
18514
18521
  }
18515
18522
 
18523
+ function getMetadataBlock(metadataJSON, viewBox) {
18524
+ var x = 0;
18525
+ var y = 0;
18526
+ var width = viewBox && viewBox[2] || 0;
18527
+ var height = viewBox && viewBox[3] || 0;
18528
+ return '<metadata>' + metadataJSON + '</metadata>\n' +
18529
+ '<g id="mapshaper-metadata">\n' +
18530
+ '<rect x="' + x + '" y="' + y + '" width="' + width + '" height="' + height + '" opacity="0"/>\n' +
18531
+ '<text opacity="0" font-size="0.1">' + metadataJSON + '</text>\n' +
18532
+ '</g>\n';
18533
+ }
18534
+
18535
+ function getGeospatialMetadata(dataset, frame) {
18536
+ return {
18537
+ crs: getSVGMetadataCRS(dataset),
18538
+ bbox: frame.bbox || null
18539
+ };
18540
+ }
18541
+
18542
+ function getSVGMetadataCRS(dataset) {
18543
+ var crs = getDatasetCRS(dataset);
18544
+ var info = getDatasetCrsInfo(dataset);
18545
+ var proj4 = null;
18546
+ if (crs) {
18547
+ proj4 = crsToProj4(crs);
18548
+ }
18549
+ if (proj4) return proj4;
18550
+ return getAuthorityCodeString(info) || null;
18551
+ }
18552
+
18553
+ function getAuthorityCodeString(info) {
18554
+ var authority = null;
18555
+ if (info && info.crs_string) {
18556
+ authority = parseAuthorityCodeString(info.crs_string);
18557
+ }
18558
+ if (!authority && info && info.wkt1) {
18559
+ authority = parseAuthorityCodeFromWkt(info.wkt1);
18560
+ }
18561
+ if (!authority && info && info.geopackage_crs) {
18562
+ authority = parseGeoPackageAuthority(info.geopackage_crs);
18563
+ }
18564
+ return authority ? authority.authority + ':' + authority.code : null;
18565
+ }
18566
+
18567
+ function parseGeoPackageAuthority(o) {
18568
+ if (!o) return null;
18569
+ var authority = o.organization || o.authority || null;
18570
+ var code = o.organization_coordsys_id || o.code || null;
18571
+ return authority && code != null ? {authority: String(authority).toUpperCase(), code: String(code)} : null;
18572
+ }
18573
+
18516
18574
  function exportFurnitureLayerForSVG(lyr, frame, opts) {
18517
18575
  var layerObj = getEmptyLayerForSVG(lyr, opts);
18518
18576
  layerObj.children = renderFurnitureLayer(lyr, frame);
@@ -28017,6 +28075,10 @@ ${svg}
28017
28075
  old_alias: 'json-subtree',
28018
28076
  describe: '[JSON] path to JSON input data; separator is /'
28019
28077
  })
28078
+ .option('ndjson', {
28079
+ type: 'flag',
28080
+ describe: '[JSON/GeoJSON] input is newline-delimited JSON objects'
28081
+ })
28020
28082
  .option('single-part', {
28021
28083
  type: 'flag',
28022
28084
  // describe: '[GeoJSON] split multi-part features into single-part features'
@@ -28109,10 +28171,6 @@ ${svg}
28109
28171
  describe: '[TopoJSON] export coordinates without quantization',
28110
28172
  type: 'flag'
28111
28173
  })
28112
- .option('metadata', {
28113
- // describe: '[TopoJSON] Add a metadata object containing CRS information',
28114
- type: 'flag'
28115
- })
28116
28174
  .option('no-point-quantization', {
28117
28175
  // describe: '[TopoJSON] export point coordinates without quantization',
28118
28176
  type: 'flag'
@@ -28155,6 +28213,10 @@ ${svg}
28155
28213
  describe: '[GeoJSON/JSON] output newline-delimited features or records',
28156
28214
  type: 'flag'
28157
28215
  })
28216
+ .option('metadata', {
28217
+ describe: '[SVG/TopoJSON] include metadata in output',
28218
+ type: 'flag'
28219
+ })
28158
28220
  .option('width', {
28159
28221
  describe: '[SVG/TopoJSON] pixel width of output (SVG default is 800)',
28160
28222
  type: 'number'
@@ -28217,10 +28279,7 @@ ${svg}
28217
28279
  .option('final', {
28218
28280
  type: 'flag' // for testing
28219
28281
  })
28220
- .option('metadata', {
28221
- // describe: '[TopoJSON] add a metadata object',
28222
- type: 'flag'
28223
- });
28282
+ ;
28224
28283
 
28225
28284
  parser.section('Editing commands');
28226
28285
 
@@ -32479,7 +32538,13 @@ ${svg}
32479
32538
  var str = readFirstChars(reader, 1000);
32480
32539
  var type = identifyJSONString(str, opts);
32481
32540
  var dataset, retn;
32482
- if (type == 'geojson') { // consider only for larger files
32541
+ if (opts.ndjson) {
32542
+ // NDJSON can represent either newline-delimited GeoJSON features/geometries
32543
+ // or plain JSON records. Defer type detection until after line parsing.
32544
+ retn = {
32545
+ content: reader.toString('utf8')
32546
+ };
32547
+ } else if (type == 'geojson') { // consider only for larger files
32483
32548
  dataset = importGeoJSONFile(reader, opts);
32484
32549
  retn = {
32485
32550
  dataset: dataset,
@@ -32531,6 +32596,12 @@ ${svg}
32531
32596
  }
32532
32597
 
32533
32598
  if (content) {
32599
+ if (opts.ndjson) {
32600
+ var nd = importNDJSON(content, opts);
32601
+ retn.dataset = nd.dataset;
32602
+ retn.format = nd.format;
32603
+ return retn;
32604
+ }
32534
32605
  if (utils.isString(content)) {
32535
32606
  try {
32536
32607
  content = JSON.parse(content); // ~3sec for 100MB string
@@ -32569,6 +32640,54 @@ ${svg}
32569
32640
  });
32570
32641
  }
32571
32642
 
32643
+ function importNDJSON(content, opts) {
32644
+ var lines = utils.isString(content) ? utils.splitLines(content) : [];
32645
+ var objects = [];
32646
+ var firstGeo = null;
32647
+ for (var i = 0; i < lines.length; i++) {
32648
+ var raw = lines[i].trim();
32649
+ if (!raw) continue;
32650
+ var obj;
32651
+ try {
32652
+ obj = JSON.parse(raw);
32653
+ } catch (e) {
32654
+ stop$1('NDJSON parsing error on line ' + (i + 1) + ': ' + e.message);
32655
+ }
32656
+ objects.push(obj);
32657
+ if (firstGeo === null) {
32658
+ firstGeo = isGeoJSONObject(obj);
32659
+ } else if (firstGeo !== isGeoJSONObject(obj)) {
32660
+ stop$1('NDJSON input mixes GeoJSON and non-GeoJSON objects');
32661
+ }
32662
+ }
32663
+ if (objects.length === 0) {
32664
+ stop$1('NDJSON input does not contain any JSON objects');
32665
+ }
32666
+ if (!firstGeo) {
32667
+ return {dataset: importJSONTable(objects), format: 'json'};
32668
+ }
32669
+ var parser = new GeoJSONParser(getGeoJSONImportOpts(opts));
32670
+ objects.forEach(function(obj) {
32671
+ if (obj && obj.type == 'FeatureCollection') {
32672
+ (obj.features || []).forEach(parser.parseObject);
32673
+ } else if (obj && obj.type == 'GeometryCollection') {
32674
+ (obj.geometries || []).forEach(parser.parseObject);
32675
+ } else {
32676
+ parser.parseObject(obj);
32677
+ }
32678
+ });
32679
+ return {dataset: parser.done(), format: 'geojson'};
32680
+ }
32681
+
32682
+ function isGeoJSONObject(obj) {
32683
+ if (!obj || typeof obj != 'object') return false;
32684
+ var type = obj.type;
32685
+ return type == 'Feature' || type == 'FeatureCollection' ||
32686
+ type == 'GeometryCollection' || type == 'Point' || type == 'MultiPoint' ||
32687
+ type == 'LineString' || type == 'MultiLineString' ||
32688
+ type == 'Polygon' || type == 'MultiPolygon';
32689
+ }
32690
+
32572
32691
  // path: path from top-level to the target object
32573
32692
  function selectFromObject(o, path) {
32574
32693
  var arrayRxp = /(.*)\[([0-9]+)\]$/; // array bracket notation w/ index
@@ -33159,6 +33278,7 @@ ${svg}
33159
33278
  var Parser = typeof DOMParser == 'undefined' ? require$1('@xmldom/xmldom').DOMParser : DOMParser;
33160
33279
  var doc = new Parser().parseFromString(str, 'text/xml');
33161
33280
  var root = doc && doc.documentElement;
33281
+ var geometadata = parseSVGGeometadata(root, str);
33162
33282
  var groups = getTopLevelLayerGroups(root);
33163
33283
  var layerData = [];
33164
33284
  var datasets = [];
@@ -33175,7 +33295,11 @@ ${svg}
33175
33295
  });
33176
33296
  });
33177
33297
 
33178
- flipYCoordinates(layerData);
33298
+ if (geometadata && geometadata.bbox && geometadata.viewBox) {
33299
+ remapCoordinatesFromSVGToMap(layerData, geometadata.viewBox, geometadata.bbox);
33300
+ } else {
33301
+ flipYCoordinates(layerData);
33302
+ }
33179
33303
 
33180
33304
  layerData.forEach(function(layer) {
33181
33305
  datasets.push(importLayerFeatures(layer.name, layer.features, opts));
@@ -33185,9 +33309,237 @@ ${svg}
33185
33309
  return {layers: [], info: {}};
33186
33310
  }
33187
33311
  if (datasets.length === 1) {
33188
- return datasets[0];
33312
+ return applySVGGeometadata(datasets[0], geometadata);
33189
33313
  }
33190
- return mergeDatasets(datasets);
33314
+ return applySVGGeometadata(mergeDatasets(datasets), geometadata);
33315
+ }
33316
+
33317
+ function applySVGGeometadata(dataset, geometadata) {
33318
+ if (geometadata && geometadata.crs) {
33319
+ setDatasetCrsInfo(dataset, getCrsInfo(geometadata.crs));
33320
+ }
33321
+ return dataset;
33322
+ }
33323
+
33324
+ function parseSVGGeometadata(root, svgText) {
33325
+ var json = findMetadataJSONString(root, svgText);
33326
+ var obj;
33327
+ var metadataViewport = getMetadataViewportRect(root);
33328
+ if (!json) return null;
33329
+ obj = parseMetadataJSONObject(json);
33330
+ if (!obj) return null;
33331
+ return {
33332
+ crs: normalizeMetadataCRS(obj.crs),
33333
+ bbox: normalizeMetadataBBox(obj.bbox),
33334
+ viewBox: metadataViewport || parseSVGViewBox(root && root.getAttribute && root.getAttribute('viewBox'))
33335
+ };
33336
+ }
33337
+
33338
+ function findMetadataJSONString(root, svgText) {
33339
+ var text = getMetadataTagText(root) || getHiddenMetadataText(root);
33340
+ if (!text && svgText) {
33341
+ text = extractHiddenMetadataFromSource(svgText);
33342
+ }
33343
+ return normalizeMetadataJSONText(text);
33344
+ }
33345
+
33346
+ function getMetadataTagText(root) {
33347
+ var nodes = getElementChildren(root);
33348
+ for (var i = 0; i < nodes.length; i++) {
33349
+ if (getTagName(nodes[i]) == 'metadata') {
33350
+ return nodes[i].textContent || '';
33351
+ }
33352
+ }
33353
+ return null;
33354
+ }
33355
+
33356
+ function getHiddenMetadataText(root) {
33357
+ var node = getMetadataGroupNode(root);
33358
+ if (node) {
33359
+ return node.textContent || '';
33360
+ }
33361
+ return null;
33362
+ }
33363
+
33364
+ function getMetadataGroupNode(root) {
33365
+ var nodes = getElementChildren(root);
33366
+ var i, node;
33367
+ for (i = 0; i < nodes.length; i++) {
33368
+ node = nodes[i];
33369
+ if (getTagName(node) == 'g' && node.getAttribute && node.getAttribute('id') == 'mapshaper-metadata') {
33370
+ return node;
33371
+ }
33372
+ }
33373
+ return null;
33374
+ }
33375
+
33376
+ function getMetadataViewportRect(root) {
33377
+ var node = getMetadataGroupNode(root);
33378
+ var children, i, rect;
33379
+ if (!node) return null;
33380
+ children = getElementChildren(node);
33381
+ for (i = 0; i < children.length; i++) {
33382
+ if (getTagName(children[i]) == 'rect') {
33383
+ rect = parseMetadataRectNode(children[i]);
33384
+ if (rect) return rect;
33385
+ }
33386
+ }
33387
+ return null;
33388
+ }
33389
+
33390
+ function parseMetadataRectNode(node) {
33391
+ var x = parseNumber(node.getAttribute('x')) || 0;
33392
+ var y = parseNumber(node.getAttribute('y')) || 0;
33393
+ var width = parseNumber(node.getAttribute('width'));
33394
+ var height = parseNumber(node.getAttribute('height'));
33395
+ var pts, i, p;
33396
+ var xmin = Infinity, ymin = Infinity, xmax = -Infinity, ymax = -Infinity;
33397
+ if (!isFiniteNumber(width) || !isFiniteNumber(height) || width === 0 || height === 0) {
33398
+ return null;
33399
+ }
33400
+ pts = [
33401
+ [x, y],
33402
+ [x + width, y],
33403
+ [x, y + height],
33404
+ [x + width, y + height]
33405
+ ];
33406
+ for (i = 0; i < pts.length; i++) {
33407
+ p = applyNodeTransformChain(node, pts[i]);
33408
+ xmin = Math.min(xmin, p[0]);
33409
+ ymin = Math.min(ymin, p[1]);
33410
+ xmax = Math.max(xmax, p[0]);
33411
+ ymax = Math.max(ymax, p[1]);
33412
+ }
33413
+ if (!isFiniteNumber(xmin) || !isFiniteNumber(ymin) || !isFiniteNumber(xmax) || !isFiniteNumber(ymax)) {
33414
+ return null;
33415
+ }
33416
+ return [xmin, ymin, xmax - xmin, ymax - ymin];
33417
+ }
33418
+
33419
+ function applyNodeTransformChain(node, point) {
33420
+ var chain = [];
33421
+ var p = [point[0], point[1]];
33422
+ var i;
33423
+ while (node && node.nodeType == 1) {
33424
+ chain.unshift(node);
33425
+ node = node.parentNode;
33426
+ }
33427
+ for (i = 0; i < chain.length; i++) {
33428
+ p = applyTransformString(chain[i].getAttribute && chain[i].getAttribute('transform'), p);
33429
+ }
33430
+ return p;
33431
+ }
33432
+
33433
+ function applyTransformString(str, point) {
33434
+ var re = /([a-z]+)\s*\(([^)]*)\)/ig;
33435
+ var m = null;
33436
+ var p = [point[0], point[1]];
33437
+ while ((m = re.exec(String(str || '')))) {
33438
+ p = applyTransformCommand(m[1].toLowerCase(), parseTransformNumbers(m[2]), p);
33439
+ }
33440
+ return p;
33441
+ }
33442
+
33443
+ function parseTransformNumbers(str) {
33444
+ return String(str || '')
33445
+ .trim()
33446
+ .split(/[\s,]+/)
33447
+ .filter(Boolean)
33448
+ .map(Number)
33449
+ .filter(function(v) { return isFiniteNumber(v); });
33450
+ }
33451
+
33452
+ function applyTransformCommand(cmd, nums, point) {
33453
+ var x = point[0], y = point[1];
33454
+ var sx, sy;
33455
+ if (cmd == 'translate') {
33456
+ return [x + (nums[0] || 0), y + (nums.length > 1 ? nums[1] : 0)];
33457
+ }
33458
+ if (cmd == 'scale') {
33459
+ sx = nums.length > 0 ? nums[0] : 1;
33460
+ sy = nums.length > 1 ? nums[1] : sx;
33461
+ return [x * sx, y * sy];
33462
+ }
33463
+ if (cmd == 'matrix' && nums.length >= 6) {
33464
+ return [
33465
+ nums[0] * x + nums[2] * y + nums[4],
33466
+ nums[1] * x + nums[3] * y + nums[5]
33467
+ ];
33468
+ }
33469
+ return point;
33470
+ }
33471
+
33472
+ function extractHiddenMetadataFromSource(str) {
33473
+ var m = String(str || '').match(/<g[^>]*\bid\s*=\s*["']mapshaper-metadata["'][^>]*>([\s\S]*?)<\/g>/i);
33474
+ if (!m) return null;
33475
+ return stripMarkupTags(m[1]);
33476
+ }
33477
+
33478
+ function stripMarkupTags(str) {
33479
+ return String(str || '').replace(/<[^>]*>/g, '');
33480
+ }
33481
+
33482
+ function normalizeMetadataJSONText(str) {
33483
+ if (!str) return null;
33484
+ return decodeHTMLEntities(stripMarkupTags(String(str))).trim();
33485
+ }
33486
+
33487
+ function decodeHTMLEntities(str) {
33488
+ return String(str || '')
33489
+ .replace(/&#34;|&#x22;/gi, '"')
33490
+ .replace(/&#39;|&#x27;/gi, "'")
33491
+ .replace(/&#38;|&#x26;/gi, '&')
33492
+ .replace(/&#60;|&#x3c;/gi, '<')
33493
+ .replace(/&#62;|&#x3e;/gi, '>')
33494
+ .replace(/&quot;/g, '"')
33495
+ .replace(/&apos;/g, "'")
33496
+ .replace(/&amp;/g, '&')
33497
+ .replace(/&lt;/g, '<')
33498
+ .replace(/&gt;/g, '>');
33499
+ }
33500
+
33501
+ function parseMetadataJSONObject(text) {
33502
+ try {
33503
+ return JSON.parse(text);
33504
+ } catch (e) {
33505
+ return null;
33506
+ }
33507
+ }
33508
+
33509
+ function normalizeMetadataCRS(crs) {
33510
+ return typeof crs == 'string' && crs.trim() ? crs.trim() : null;
33511
+ }
33512
+
33513
+ function normalizeMetadataBBox(bbox) {
33514
+ if (!Array.isArray(bbox) || bbox.length != 4) return null;
33515
+ var out = bbox.map(function(v) { return Number(v); });
33516
+ return out.every(isFiniteNumber) ? out : null;
33517
+ }
33518
+
33519
+ function parseSVGViewBox(str) {
33520
+ var parts = String(str || '').trim().split(/[\s,]+/).map(Number);
33521
+ if (parts.length != 4 || !parts.every(isFiniteNumber) || parts[2] === 0 || parts[3] === 0) {
33522
+ return null;
33523
+ }
33524
+ return parts;
33525
+ }
33526
+
33527
+ function remapCoordinatesFromSVGToMap(layerData, viewBox, bbox) {
33528
+ var vx = viewBox[0], vy = viewBox[1], vw = viewBox[2], vh = viewBox[3];
33529
+ var minX = bbox[0], minY = bbox[1], maxX = bbox[2], maxY = bbox[3];
33530
+ var scaleX = (maxX - minX) / vw;
33531
+ var scaleY = (maxY - minY) / vh;
33532
+
33533
+ layerData.forEach(function(layer) {
33534
+ layer.features.forEach(function(feature) {
33535
+ forEachGeometryCoordinate(feature && feature.geometry, function(coord) {
33536
+ var sx = coord[0];
33537
+ var sy = coord[1];
33538
+ coord[0] = minX + (sx - vx) * scaleX;
33539
+ coord[1] = maxY - (sy - vy) * scaleY;
33540
+ });
33541
+ });
33542
+ });
33191
33543
  }
33192
33544
 
33193
33545
  function importLayerFeatures(layerName, features, opts) {
@@ -33215,6 +33567,7 @@ ${svg}
33215
33567
  childNodes.forEach(function(node) {
33216
33568
  var tag = getTagName(node);
33217
33569
  if (tag == 'defs') return;
33570
+ if (tag == 'g' && node.getAttribute && node.getAttribute('id') == 'mapshaper-metadata') return;
33218
33571
  if (tag == 'g') {
33219
33572
  groups.push({
33220
33573
  node: node,
@@ -41265,7 +41618,7 @@ ${svg}
41265
41618
  // @type: 'clip' or 'erase'
41266
41619
  function clipLayers(targetLayers, clipSrc, targetDataset, type, opts) {
41267
41620
  profileStart('clipLayers');
41268
- opts = opts || {no_fcleanup: true}; // TODO: update testing functions
41621
+ opts = opts || {no_cleanup: true}; // TODO: update testing functions
41269
41622
  var usingPathClip = utils.some(targetLayers, layerHasPaths);
41270
41623
  var mergedDataset, clipLyr, nodes, result;
41271
41624
  var clipDataset = normalizeOverlaySource(clipSrc, targetDataset, opts);
@@ -48147,17 +48500,62 @@ ${svg}
48147
48500
  });
48148
48501
  }
48149
48502
 
48503
+ function parseWKTPoint(val) {
48504
+ if (!utils.isString(val)) return null;
48505
+ // Support 2D POINT WKT, case-insensitive, with optional Z/M tokens.
48506
+ // Examples:
48507
+ // POINT (1 2)
48508
+ // point(1 2)
48509
+ // POINT Z (1 2 3)
48510
+ // POINT M (1 2 3)
48511
+ // POINT ZM (1 2 3 4)
48512
+ var m = /^\s*POINT(?:\s+Z(?:M)?|\s+M)?\s*\(\s*([^\s,]+)\s+([^\s,]+)(?:\s+[^)]*)?\)\s*$/i.exec(val);
48513
+ if (!m) return null;
48514
+ var x = coordinateFromValue(m[1]);
48515
+ var y = coordinateFromValue(m[2]);
48516
+ if (isNaN(x) || isNaN(y)) return null;
48517
+ return [x, y];
48518
+ }
48519
+
48520
+ function findWKTField(records, fields) {
48521
+ var sampleSize = Math.min(records.length, 50);
48522
+ return utils.find(fields, function(name) {
48523
+ var seen = 0;
48524
+ var matches = 0;
48525
+ for (var i = 0; i < sampleSize; i++) {
48526
+ var rec = records[i];
48527
+ if (!rec) continue;
48528
+ var val = rec[name];
48529
+ if (!utils.isString(val) || val.trim() === '') continue;
48530
+ seen++;
48531
+ if (parseWKTPoint(val)) matches++;
48532
+ }
48533
+ // Require at least one non-empty sample and majority parseable as POINT.
48534
+ return seen > 0 && matches / seen >= 0.6;
48535
+ });
48536
+ }
48537
+
48150
48538
  function pointsFromDataTableAuto(data) {
48151
48539
  var fields = data ? data.getFields() : [];
48540
+ var records = data ? data.getRecords() : [];
48152
48541
  var opts = {
48153
48542
  x: findXField(fields),
48154
48543
  y: findYField(fields)
48155
48544
  };
48545
+ if (!opts.x || !opts.y) {
48546
+ opts.wkt = findWKTField(records, fields);
48547
+ }
48156
48548
  return pointsFromDataTable(data, opts);
48157
48549
  }
48158
48550
 
48159
48551
  function pointsFromDataTable(data, opts) {
48160
48552
  if (!data) stop$1("Layer is missing a data table");
48553
+ if (opts.wkt && data.fieldExists(opts.wkt)) {
48554
+ return data.getRecords().map(function(rec) {
48555
+ var p = parseWKTPoint(rec[opts.wkt]);
48556
+ return p ? [p] : null;
48557
+ });
48558
+ }
48161
48559
  if (!opts.x || !opts.y || !data.fieldExists(opts.x) || !data.fieldExists(opts.y)) {
48162
48560
  stop$1("Missing x,y data fields");
48163
48561
  }
@@ -48175,8 +48573,10 @@ ${svg}
48175
48573
  var Points = /*#__PURE__*/Object.freeze({
48176
48574
  __proto__: null,
48177
48575
  coordinateFromValue: coordinateFromValue,
48576
+ findWKTField: findWKTField,
48178
48577
  findXField: findXField,
48179
48578
  findYField: findYField,
48579
+ parseWKTPoint: parseWKTPoint,
48180
48580
  pointsFromPolygons: pointsFromPolygons
48181
48581
  });
48182
48582
 
@@ -53074,7 +53474,7 @@ ${svg}
53074
53474
  });
53075
53475
  }
53076
53476
 
53077
- var version = "0.7.6";
53477
+ var version = "0.7.7";
53078
53478
 
53079
53479
  // Parse command line args into commands and run them
53080
53480
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
package/www/page.css CHANGED
@@ -172,12 +172,20 @@ body.map-view {
172
172
  display: none;
173
173
  }
174
174
 
175
+ /* On narrow screens, splash links collide with the logo; keep the header clean
176
+ until data is loaded and the editor controls take over. */
177
+ @media (max-width: 500px) {
178
+ #splash-buttons {
179
+ display: none;
180
+ }
181
+ }
182
+
175
183
  .page-header-buttons {
176
184
  z-index: 20;
177
185
  position: absolute;
178
186
  top: 0px;
179
187
  right: 0px;
180
- margin: 0 3px 3px 0;
188
+ margin: 0 2px 3px 0;
181
189
  }
182
190
 
183
191
  .btn.header-btn {
@@ -218,7 +226,6 @@ body.map-view {
218
226
 
219
227
  #header-menu-btn {
220
228
  cursor: pointer;
221
- padding: 8px 7px 4px 7px;
222
229
  user-select: none;
223
230
  /* All header buttons are 29px tall, so top-align the hamburger to keep its
224
231
  row baseline-neutral. Without this, the SVG-only button (no text) makes
@@ -843,6 +850,84 @@ body.simplify .layer-control-btn {
843
850
  display: none;
844
851
  }
845
852
 
853
+ /* On small screens, move the layer menu trigger out of the header center and
854
+ render it as an icon-only floating map button. */
855
+ @media (max-width: 700px) {
856
+ .info-box {
857
+ margin-top: 8px;
858
+ }
859
+
860
+ div.nav-buttons {
861
+ top: 2px;
862
+ }
863
+
864
+ .btn.header-btn {
865
+ padding-left: 5px;
866
+ padding-right: 5px;
867
+ }
868
+
869
+ .layer-control-btn {
870
+ width: auto;
871
+ left: 9px;
872
+ top: 38px; /* below the 30px header */
873
+ text-align: left;
874
+ }
875
+
876
+ .layer-control-btn > .btn.header-btn {
877
+ margin-right: 0;
878
+ width: 30px;
879
+ height: 30px;
880
+ box-sizing: content-box;
881
+ padding: 0;
882
+ border-radius: 4px;
883
+ border: 2px solid rgba(255, 255, 255, 0.45);
884
+ display: inline-flex;
885
+ align-items: center;
886
+ justify-content: center;
887
+ }
888
+
889
+ /* The clickable header button element itself has class "layer-name", so
890
+ hide text on the button directly (not a child selector). */
891
+ .layer-control-btn > .btn.header-btn.layer-name {
892
+ color: transparent;
893
+ font-size: 0;
894
+ line-height: 0;
895
+ text-indent: -9999px;
896
+ overflow: hidden;
897
+ }
898
+
899
+ /* Mobile layer-button icon */
900
+ .layer-control-btn > .btn.header-btn::before {
901
+ content: "";
902
+ display: block;
903
+ }
904
+
905
+ /* Two diagonally offset squares */
906
+ .layer-control-btn.icon-duo > .btn.header-btn::before {
907
+ width: 13px;
908
+ height: 13px;
909
+ margin: 2px 0 0 2px;
910
+ border: 1px solid rgba(255, 255, 255, 0.95);
911
+ border-radius: 1px;
912
+ box-shadow: -4px -4px 0 -1px rgba(255, 255, 255, 0.72);
913
+ }
914
+
915
+ .mshp-main-map .basemap-overlay-buttons {
916
+ top: 10px; /* map-relative offset to align with page-level layer button */
917
+ right: 33px;
918
+ padding: 0;
919
+ background: transparent;
920
+ border-radius: 0;
921
+ }
922
+
923
+ .mshp-main-map .basemap-overlay-buttons .basemap-style-btn.basemap-overlay-btn {
924
+ width: 30px !important;
925
+ height: 30px !important;
926
+ margin: 0 0 0 4px !important;
927
+ border-radius: 4px !important;
928
+ }
929
+ }
930
+
846
931
  .layer-control .info-box {
847
932
  padding: 0;
848
933
  pointer-events: none;
@@ -1644,7 +1729,7 @@ body.pan.panning .map-layers:not(.drawing) {
1644
1729
  position: relative;
1645
1730
  cursor:pointer;
1646
1731
  /* padding: 3px 3px 5px 3px; */
1647
- padding: 4px;
1732
+ padding: 4px 5px;
1648
1733
  line-height: 1;
1649
1734
  }
1650
1735