mapshaper 0.7.39 → 0.7.41

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.
@@ -2447,7 +2447,7 @@
2447
2447
  return obj === Object(obj); // via underscore
2448
2448
  }
2449
2449
 
2450
- function clamp$1(val, min, max) {
2450
+ function clamp$2(val, min, max) {
2451
2451
  return val < min ? min : (val > max ? max : val);
2452
2452
  }
2453
2453
 
@@ -3128,7 +3128,7 @@
3128
3128
  function findValueByRank(arr, rank) {
3129
3129
  if (!arr.length || rank < 1 || rank > arr.length) error$1("[findValueByRank()] invalid input");
3130
3130
 
3131
- rank = clamp$1(rank | 0, 1, arr.length);
3131
+ rank = clamp$2(rank | 0, 1, arr.length);
3132
3132
  var k = rank - 1, // conv. rank to array index
3133
3133
  n = arr.length,
3134
3134
  l = 0,
@@ -3502,7 +3502,7 @@
3502
3502
  // self-import and the resulting Rollup circular-dependency warning.
3503
3503
  var utils = {
3504
3504
  addThousandsSep, addslashes, arrayToIndex,
3505
- clamp: clamp$1, cleanNumericString, contains, copyElements, countValues, createBuffer,
3505
+ clamp: clamp$2, cleanNumericString, contains, copyElements, countValues, createBuffer,
3506
3506
  defaults, difference,
3507
3507
  endsWith, every, expandoBuffer, extend, extendBuffer,
3508
3508
  find, findMedian, findQuantile, findRankByValue, findStringPrefix,
@@ -12860,15 +12860,15 @@
12860
12860
  function InteractionMode(gui) {
12861
12861
 
12862
12862
  var menus = {
12863
- standard: ['info', 'selection', 'box'],
12864
- empty: ['edit_polygons', 'edit_lines', 'edit_points', 'box'],
12865
- polygons: ['info', 'selection', 'box', 'polygon_style', 'edit_polygons'],
12866
- rectangles: ['info', 'selection', 'box', 'polygon_style', 'rectangles', 'edit_polygons'],
12867
- lines: ['info', 'selection', 'box', 'line_style', 'edit_lines'], // 'snip_lines'
12863
+ standard: ['info', 'selection', 'box', 'ruler'],
12864
+ empty: ['edit_polygons', 'edit_lines', 'edit_points', 'box', 'ruler'],
12865
+ polygons: ['info', 'selection', 'box', 'polygon_style', 'edit_polygons', 'ruler'],
12866
+ rectangles: ['info', 'selection', 'box', 'polygon_style', 'rectangles', 'edit_polygons', 'ruler'],
12867
+ lines: ['info', 'selection', 'box', 'line_style', 'edit_lines', 'ruler'], // 'snip_lines'
12868
12868
  table: ['info', 'selection'],
12869
- raster: ['box'],
12870
- labels: ['info', 'selection', 'box', 'point_style', 'labels', 'edit_points'],
12871
- points: ['info', 'selection', 'box', 'point_style', 'edit_points'] // , 'add-points'
12869
+ raster: ['ruler', 'box'],
12870
+ labels: ['info', 'selection', 'box', 'point_style', 'labels', 'edit_points', 'ruler'],
12871
+ points: ['info', 'selection', 'box', 'point_style', 'edit_points', 'ruler'] // , 'add-points'
12872
12872
  };
12873
12873
 
12874
12874
  var prompts = {
@@ -12893,6 +12893,7 @@
12893
12893
  snip_lines: 'snip polylines',
12894
12894
  vertices: 'edit vertices',
12895
12895
  selection: 'selection tool',
12896
+ ruler: 'measure distance',
12896
12897
  'add-points': 'add points',
12897
12898
  rectangles: 'drag-to-resize',
12898
12899
  off: 'turn off'
@@ -14028,16 +14029,16 @@
14028
14029
  function getCanvasPoint(canvas, evt) {
14029
14030
  var rect = canvas.getBoundingClientRect();
14030
14031
  return {
14031
- x: clamp(Math.round((evt.clientX - rect.left) / rect.width * (canvas.width - 1)), 0, canvas.width - 1),
14032
- y: clamp(Math.round((evt.clientY - rect.top) / rect.height * (canvas.height - 1)), 0, canvas.height - 1)
14032
+ x: clamp$1(Math.round((evt.clientX - rect.left) / rect.width * (canvas.width - 1)), 0, canvas.width - 1),
14033
+ y: clamp$1(Math.round((evt.clientY - rect.top) / rect.height * (canvas.height - 1)), 0, canvas.height - 1)
14033
14034
  };
14034
14035
  }
14035
14036
 
14036
14037
  function setPickerColor(hsb) {
14037
14038
  pickerColor = {
14038
- h: clamp(Math.round(hsb.h), 0, 255),
14039
- s: clamp(Math.round(hsb.s), 0, 255),
14040
- b: clamp(Math.round(hsb.b), 0, 255)
14039
+ h: clamp$1(Math.round(hsb.h), 0, 255),
14040
+ s: clamp$1(Math.round(hsb.s), 0, 255),
14041
+ b: clamp$1(Math.round(hsb.b), 0, 255)
14041
14042
  };
14042
14043
  drawColorPicker();
14043
14044
  updatePickerFields();
@@ -14178,7 +14179,7 @@
14178
14179
  }
14179
14180
 
14180
14181
  function pctToByte(val) {
14181
- return Math.round(clamp(val, 0, 100) / 100 * 255);
14182
+ return Math.round(clamp$1(val, 0, 100) / 100 * 255);
14182
14183
  }
14183
14184
 
14184
14185
  function byteToDegrees(val) {
@@ -14186,14 +14187,14 @@
14186
14187
  }
14187
14188
 
14188
14189
  function degreesToByte(val) {
14189
- return Math.round(clamp(val, 0, 360) / 360 * 255);
14190
+ return Math.round(clamp$1(val, 0, 360) / 360 * 255);
14190
14191
  }
14191
14192
 
14192
14193
  function parseNumberField(str) {
14193
14194
  return Number(String(str).replace(/[°%]/g, '').trim());
14194
14195
  }
14195
14196
 
14196
- function clamp(val, min, max) {
14197
+ function clamp$1(val, min, max) {
14197
14198
  return isFinite(val) ? Math.max(min, Math.min(max, val)) : min;
14198
14199
  }
14199
14200
 
@@ -22554,6 +22555,760 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
22554
22555
  }
22555
22556
  }
22556
22557
 
22558
+ function getDistanceDisplay(distanceMeters, unitHint) {
22559
+ var unit = unitHint || (distanceMeters >= 1000 ? 'km' : 'm');
22560
+ var value = unit == 'km' ? distanceMeters / 1000 : distanceMeters;
22561
+ return {
22562
+ value,
22563
+ unit,
22564
+ label: formatDistanceValue(value) + ' ' + unit
22565
+ };
22566
+ }
22567
+
22568
+ function formatDistanceValue(value) {
22569
+ var decimals = getDistanceDecimals(value);
22570
+ return value.toFixed(decimals);
22571
+ }
22572
+
22573
+ function getDistanceUnit(distanceMeters) {
22574
+ return distanceMeters >= 1000 ? 'km' : 'm';
22575
+ }
22576
+
22577
+ function pointIsInLngLatRange(p) {
22578
+ return !!p && p[0] >= -180 && p[0] <= 180 && p[1] >= -90 && p[1] <= 90;
22579
+ }
22580
+
22581
+ function segmentCrossesAntimeridian(a, b) {
22582
+ return pointIsInLngLatRange(a) && pointIsInLngLatRange(b) && Math.abs(a[0] - b[0]) > 180;
22583
+ }
22584
+
22585
+ function pointIsNearPole(p) {
22586
+ return !!p && Math.abs(p[1]) >= 89.9;
22587
+ }
22588
+
22589
+ function interpolateGreatCirclePoint(a, b, k) {
22590
+ var v1 = lngLatToUnitVector(a[0], a[1]);
22591
+ var v2 = lngLatToUnitVector(b[0], b[1]);
22592
+ var dot = clamp(dotProduct(v1, v2), -1, 1);
22593
+ var omega = Math.acos(dot);
22594
+ var sinOmega = Math.sin(omega);
22595
+ var u, v;
22596
+ if (k === 0) return a.concat();
22597
+ if (k === 1) return b.concat();
22598
+ if (sinOmega < 1e-12) {
22599
+ return unitVectorToLngLat(normalizeVector([
22600
+ v1[0] * (1 - k) + v2[0] * k,
22601
+ v1[1] * (1 - k) + v2[1] * k,
22602
+ v1[2] * (1 - k) + v2[2] * k
22603
+ ]));
22604
+ }
22605
+ u = Math.sin((1 - k) * omega) / sinOmega;
22606
+ v = Math.sin(k * omega) / sinOmega;
22607
+ return unitVectorToLngLat([
22608
+ v1[0] * u + v2[0] * v,
22609
+ v1[1] * u + v2[1] * v,
22610
+ v1[2] * u + v2[2] * v
22611
+ ]);
22612
+ }
22613
+
22614
+ function getDistanceDecimals(value) {
22615
+ if (value >= 100) return 0;
22616
+ if (value >= 10) return 1;
22617
+ if (value >= 1) return 2;
22618
+ return 3;
22619
+ }
22620
+
22621
+ function lngLatToUnitVector(lng, lat) {
22622
+ var D2R = Math.PI / 180;
22623
+ var lngRad = lng * D2R;
22624
+ var latRad = lat * D2R;
22625
+ var cosLat = Math.cos(latRad);
22626
+ return [
22627
+ Math.cos(lngRad) * cosLat,
22628
+ Math.sin(lngRad) * cosLat,
22629
+ Math.sin(latRad)
22630
+ ];
22631
+ }
22632
+
22633
+ function unitVectorToLngLat(v) {
22634
+ var R2D = 180 / Math.PI;
22635
+ return [
22636
+ Math.atan2(v[1], v[0]) * R2D,
22637
+ Math.atan2(v[2], Math.sqrt(v[0] * v[0] + v[1] * v[1])) * R2D
22638
+ ];
22639
+ }
22640
+
22641
+ function normalizeVector(v) {
22642
+ var len = Math.sqrt(dotProduct(v, v));
22643
+ return len > 0 ? [v[0] / len, v[1] / len, v[2] / len] : v;
22644
+ }
22645
+
22646
+ function dotProduct(a, b) {
22647
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
22648
+ }
22649
+
22650
+ function clamp(val, min, max) {
22651
+ return Math.max(min, Math.min(max, val));
22652
+ }
22653
+
22654
+ var SVG_NS = 'http://www.w3.org/2000/svg';
22655
+ var OUT_OF_RANGE_MESSAGE = 'out of range';
22656
+ var GEODESIC_PIXEL_TOLERANCE = 1.5;
22657
+ var GEODESIC_MAX_DEPTH = 14;
22658
+ var GEODESIC_MAX_SEGMENT_PX = 18;
22659
+ var GEODESIC_MAX_SUBDIVISIONS = 1500;
22660
+ var ENDPOINT_HIT_RADIUS = 10;
22661
+ var WEB_MERCATOR_HALF_WORLD = 6378137 * Math.PI;
22662
+ var WEB_MERCATOR_MAX_Y = 6378137 * Math.log(Math.tan(Math.PI * 0.25 + 84 * Math.PI / 360));
22663
+
22664
+ function RulerTool(gui, ext) {
22665
+ var mapLayers = gui.container.findChild('.map-layers');
22666
+ var parent = gui.container.findChild('.mshp-main-map');
22667
+ var svg = createSvgNode('svg');
22668
+ var directPath = createSvgNode('path');
22669
+ var geodesicPath = createSvgNode('path');
22670
+ var startMarker = createSvgNode('circle');
22671
+ var endMarker = createSvgNode('circle');
22672
+ var popup = El('div').addClass('ruler-popup rollover').appendTo(parent).hide();
22673
+ var content = El('div').addClass('ruler-popup-content').appendTo(popup);
22674
+ var closeBtn = El('button').addClass('label-style-close ruler-close-btn').appendTo(popup).text('×');
22675
+ var wgs84 = internal.parseCrsString('wgs84');
22676
+ var _on = false;
22677
+ var startPoint = null;
22678
+ var pointerPoint = null;
22679
+ var placed = false;
22680
+ var draggingEndpoint = null;
22681
+ var lastValidDragPoint = null;
22682
+
22683
+ svg.classList.add('ruler-overlay');
22684
+ directPath.classList.add('ruler-line');
22685
+ geodesicPath.classList.add('ruler-line', 'ruler-great-circle');
22686
+ startMarker.classList.add('ruler-endpoint');
22687
+ endMarker.classList.add('ruler-endpoint');
22688
+ svg.appendChild(directPath);
22689
+ svg.appendChild(geodesicPath);
22690
+ svg.appendChild(startMarker);
22691
+ svg.appendChild(endMarker);
22692
+ mapLayers.node().appendChild(svg);
22693
+
22694
+ closeBtn.on('click', function(e) {
22695
+ e.stopPropagation();
22696
+ reset();
22697
+ if (gui.interaction.getMode() == 'ruler') {
22698
+ gui.interaction.turnOff();
22699
+ }
22700
+ });
22701
+
22702
+ gui.keyboard.on('keydown', function(e) {
22703
+ if (!_on || e.keyName != 'esc') return;
22704
+ clearRuler();
22705
+ ensureRulerMode();
22706
+ e.originalEvent.preventDefault();
22707
+ e.originalEvent.stopPropagation();
22708
+ e.stopPropagation();
22709
+ }, null, 10);
22710
+
22711
+ gui.addMode('ruler_tool', turnOn, turnOff);
22712
+
22713
+ gui.on('interaction_mode_change', function(e) {
22714
+ if (e.mode == 'ruler') {
22715
+ gui.enterMode('ruler_tool');
22716
+ } else if (_on) {
22717
+ gui.clearMode();
22718
+ }
22719
+ });
22720
+
22721
+ gui.on('map_click', function(e) {
22722
+ if (!_on) return;
22723
+ handleClick(e);
22724
+ });
22725
+
22726
+ gui.map.getMouse().on('hover', function(e) {
22727
+ if (!_on || !startPoint || placed) return;
22728
+ pointerPoint = getRulerPoint(e.x, e.y);
22729
+ render();
22730
+ });
22731
+
22732
+ gui.map.getMouse().on('dragstart', function(e) {
22733
+ if (!_on || !placed) return;
22734
+ draggingEndpoint = getHitEndpoint(e.x, e.y);
22735
+ if (!draggingEndpoint) return;
22736
+ lastValidDragPoint = getEndpoint(draggingEndpoint);
22737
+ mapLayers.addClass('ruler-dragging');
22738
+ e.stopPropagation();
22739
+ }, null, 10);
22740
+
22741
+ gui.map.getMouse().on('drag', function(e) {
22742
+ var point;
22743
+ if (!draggingEndpoint) return;
22744
+ point = getRulerPoint(e.x, e.y);
22745
+ setEndpoint(draggingEndpoint, point);
22746
+ if (point.valid) {
22747
+ lastValidDragPoint = point;
22748
+ }
22749
+ render();
22750
+ e.stopPropagation();
22751
+ }, null, 10);
22752
+
22753
+ gui.map.getMouse().on('dragend', function(e) {
22754
+ if (!draggingEndpoint) return;
22755
+ if (!getEndpoint(draggingEndpoint).valid && lastValidDragPoint) {
22756
+ setEndpoint(draggingEndpoint, lastValidDragPoint);
22757
+ render();
22758
+ }
22759
+ draggingEndpoint = null;
22760
+ lastValidDragPoint = null;
22761
+ mapLayers.removeClass('ruler-dragging');
22762
+ gui.dispatchEvent('map_interaction_end');
22763
+ e.stopPropagation();
22764
+ }, null, 10);
22765
+
22766
+ gui.on('map_rendered', function() {
22767
+ if (_on) render();
22768
+ });
22769
+
22770
+ gui.map.on('display_crs_change', function() {
22771
+ if (_on) reprojectStoredRulerPoints();
22772
+ });
22773
+
22774
+ gui.model.on('update', function() {
22775
+ if (_on) reset();
22776
+ });
22777
+
22778
+ function turnOn() {
22779
+ _on = true;
22780
+ mapLayers.addClass('ruler-tool');
22781
+ reset();
22782
+ }
22783
+
22784
+ function turnOff() {
22785
+ _on = false;
22786
+ mapLayers.removeClass('ruler-tool');
22787
+ reset();
22788
+ if (gui.interaction.getMode() == 'ruler') {
22789
+ gui.interaction.turnOff();
22790
+ }
22791
+ }
22792
+
22793
+ function handleClick(e) {
22794
+ var point = getRulerPoint(e.x, e.y);
22795
+ if (!point.valid) {
22796
+ if (startPoint && !placed) {
22797
+ pointerPoint = point;
22798
+ render();
22799
+ } else {
22800
+ showOutOfRangeNote();
22801
+ }
22802
+ return;
22803
+ }
22804
+ if (!startPoint || placed) {
22805
+ startPoint = point;
22806
+ pointerPoint = null;
22807
+ placed = false;
22808
+ } else {
22809
+ pointerPoint = point;
22810
+ placed = true;
22811
+ }
22812
+ render();
22813
+ }
22814
+
22815
+ function reset() {
22816
+ clearRuler();
22817
+ }
22818
+
22819
+ function clearRuler() {
22820
+ startPoint = null;
22821
+ pointerPoint = null;
22822
+ placed = false;
22823
+ draggingEndpoint = null;
22824
+ lastValidDragPoint = null;
22825
+ mapLayers.removeClass('ruler-dragging');
22826
+ clearSvgPath(directPath);
22827
+ clearSvgPath(geodesicPath);
22828
+ hideMarker(startMarker);
22829
+ hideMarker(endMarker);
22830
+ popup.hide();
22831
+ }
22832
+
22833
+ function ensureRulerMode() {
22834
+ if (gui.interaction.getMode() != 'ruler') {
22835
+ gui.interaction.setMode('ruler');
22836
+ }
22837
+ }
22838
+
22839
+ function render() {
22840
+ var endPoint = pointerPoint;
22841
+ var measurement;
22842
+ syncOverlaySize();
22843
+ if (!startPoint || !endPoint) {
22844
+ clearSvgPath(directPath);
22845
+ clearSvgPath(geodesicPath);
22846
+ drawMarker(startMarker, startPoint);
22847
+ hideMarker(endMarker);
22848
+ popup.hide();
22849
+ return;
22850
+ }
22851
+
22852
+ if (!startPoint.valid || !endPoint.valid) {
22853
+ clearSvgPath(directPath);
22854
+ clearSvgPath(geodesicPath);
22855
+ drawMarker(startMarker, startPoint.valid ? startPoint : null, false);
22856
+ drawMarker(endMarker, placed && endPoint.valid ? endPoint : null, false);
22857
+ showOutOfRangeNote();
22858
+ return;
22859
+ }
22860
+
22861
+ measurement = getMeasurement(startPoint, endPoint);
22862
+ drawRulerLines(startPoint, endPoint, measurement);
22863
+ drawMarker(startMarker, startPoint, measurement.isLatLng);
22864
+ if (placed) {
22865
+ drawMarker(endMarker, endPoint, measurement.isLatLng);
22866
+ } else {
22867
+ hideMarker(endMarker);
22868
+ }
22869
+ showPopup(measurement);
22870
+ }
22871
+
22872
+ function drawRulerLines(a, b, measurement) {
22873
+ if (measurement.isLatLng) {
22874
+ drawPath(geodesicPath, getGeodesicPixels(a, b));
22875
+ clearSvgPath(directPath);
22876
+ } else {
22877
+ drawPath(directPath, [a.display, b.display]);
22878
+ drawPath(geodesicPath, measurement.greatCircleMeters ? getGeodesicPixels(a, b) : null);
22879
+ }
22880
+ }
22881
+
22882
+ function showPopup(measurement) {
22883
+ content.empty();
22884
+ measurement.labels.forEach(function(label) {
22885
+ El('div').appendTo(content).text(label);
22886
+ });
22887
+ if (placed) {
22888
+ closeBtn.show();
22889
+ } else {
22890
+ closeBtn.hide();
22891
+ }
22892
+ popup.show();
22893
+ }
22894
+
22895
+ function showOutOfRangeNote() {
22896
+ content.empty();
22897
+ El('div').appendTo(content).text(OUT_OF_RANGE_MESSAGE);
22898
+ if (placed) {
22899
+ closeBtn.show();
22900
+ } else {
22901
+ closeBtn.hide();
22902
+ }
22903
+ popup.show();
22904
+ }
22905
+
22906
+ function getHitEndpoint(x, y) {
22907
+ var startDist = getEndpointPixelDistance(startPoint, x, y);
22908
+ var endDist = getEndpointPixelDistance(pointerPoint, x, y);
22909
+ if (startDist > ENDPOINT_HIT_RADIUS && endDist > ENDPOINT_HIT_RADIUS) return null;
22910
+ return startDist <= endDist ? 'start' : 'end';
22911
+ }
22912
+
22913
+ function getEndpointPixelDistance(point, x, y) {
22914
+ var p;
22915
+ if (!point || !point.valid) return Infinity;
22916
+ p = ext.translateCoords(point.display[0], point.display[1]);
22917
+ return geom.distance2D(p[0], p[1], x, y);
22918
+ }
22919
+
22920
+ function getEndpoint(endpoint) {
22921
+ return endpoint == 'start' ? startPoint : pointerPoint;
22922
+ }
22923
+
22924
+ function setEndpoint(endpoint, point) {
22925
+ if (endpoint == 'start') {
22926
+ startPoint = point;
22927
+ } else {
22928
+ pointerPoint = point;
22929
+ }
22930
+ }
22931
+
22932
+ function getRulerPoint(x, y) {
22933
+ var display = ext.pixCoordsToMapCoords(x, y);
22934
+ var lyr = gui.map.getActiveLayer();
22935
+ var displayCRS = gui.map.getDisplayCRS();
22936
+ var sourceCRS = getSourceCRS();
22937
+ if (!displayPointIsInRange(display, displayCRS)) {
22938
+ return {
22939
+ display,
22940
+ data: null,
22941
+ lngLat: null,
22942
+ sourceCRS,
22943
+ valid: false
22944
+ };
22945
+ }
22946
+ var data = lyr ? translateDisplayPoint(lyr, display) : display;
22947
+ var o = validateRulerPoint(data, sourceCRS);
22948
+ return {
22949
+ display,
22950
+ data,
22951
+ lngLat: o.lngLat,
22952
+ sourceCRS,
22953
+ valid: o.valid
22954
+ };
22955
+ }
22956
+
22957
+ function reprojectStoredRulerPoints() {
22958
+ startPoint = reprojectRulerPoint(startPoint);
22959
+ pointerPoint = reprojectRulerPoint(pointerPoint);
22960
+ if (startPoint && !startPoint.valid || pointerPoint && !pointerPoint.valid) {
22961
+ showOutOfRangeNote();
22962
+ }
22963
+ render();
22964
+ }
22965
+
22966
+ function reprojectRulerPoint(point) {
22967
+ var displayCRS = gui.map.getDisplayCRS();
22968
+ var sourceCRS = getSourceCRS();
22969
+ var data = point && point.data;
22970
+ var project, display, o;
22971
+ if (!point || !data) return point;
22972
+ if (displayCRS && sourceCRS && !internal.crsHaveSameTransform(sourceCRS, displayCRS)) {
22973
+ project = internal.getProjTransform2(sourceCRS, displayCRS);
22974
+ display = project && project(data[0], data[1]);
22975
+ } else {
22976
+ display = data;
22977
+ }
22978
+ if (!displayPointIsInRange(display, displayCRS)) {
22979
+ return Object.assign({}, point, {
22980
+ display,
22981
+ sourceCRS,
22982
+ valid: false
22983
+ });
22984
+ }
22985
+ o = validateRulerPoint(data, sourceCRS);
22986
+ return {
22987
+ display,
22988
+ data,
22989
+ lngLat: o.lngLat,
22990
+ sourceCRS,
22991
+ valid: o.valid
22992
+ };
22993
+ }
22994
+
22995
+ function displayPointIsInRange(display, displayCRS) {
22996
+ if (!pointHasFiniteCoords(display)) return false;
22997
+ if (!displayCRS || !internal.isWebMercator(displayCRS)) return true;
22998
+ return display[0] >= -WEB_MERCATOR_HALF_WORLD &&
22999
+ display[0] <= WEB_MERCATOR_HALF_WORLD &&
23000
+ display[1] >= -WEB_MERCATOR_MAX_Y &&
23001
+ display[1] <= WEB_MERCATOR_MAX_Y;
23002
+ }
23003
+
23004
+ function getSourceCRS() {
23005
+ var lyr = gui.map.getActiveLayer();
23006
+ var dataset = lyr && lyr.gui && lyr.gui.source && lyr.gui.source.dataset;
23007
+ var info = dataset ? getDatasetCrsInfo(dataset) : null;
23008
+ if (info && info.crs) return info.crs;
23009
+ if (!lyr) return gui.map.getDisplayCRS() || wgs84;
23010
+ return gui.map.getDisplayCRS() || null;
23011
+ }
23012
+
23013
+ function validateRulerPoint(data, sourceCRS) {
23014
+ var lngLat;
23015
+ var valid;
23016
+ if (!pointHasFiniteCoords(data)) {
23017
+ return {valid: false, lngLat: null};
23018
+ }
23019
+ if (!sourceCRS) {
23020
+ return {valid: true, lngLat: null};
23021
+ }
23022
+ if (internal.isLatLngCRS(sourceCRS)) {
23023
+ return {
23024
+ valid: pointIsInLngLatRange(data),
23025
+ lngLat: pointIsInLngLatRange(data) ? data : null
23026
+ };
23027
+ }
23028
+ if (!internal.isInvertibleCRS(sourceCRS)) {
23029
+ return {valid: true, lngLat: null};
23030
+ }
23031
+ lngLat = internal.toLngLat(data, sourceCRS);
23032
+ valid = pointIsInLngLatRange(lngLat) && inverseProjectionLooksStable(data, lngLat, sourceCRS);
23033
+ return {
23034
+ valid,
23035
+ lngLat: valid ? lngLat : null
23036
+ };
23037
+ }
23038
+
23039
+ function inverseProjectionLooksStable(data, lngLat, sourceCRS) {
23040
+ var fwd = internal.projectPoint(lngLat, wgs84, sourceCRS);
23041
+ var tolerance = sourceCRS.to_meter ? 10 / sourceCRS.to_meter : 10;
23042
+ return pointHasFiniteCoords(fwd) &&
23043
+ geom.distance2D(data[0], data[1], fwd[0], fwd[1]) <= tolerance;
23044
+ }
23045
+
23046
+ function getMeasurement(a, b) {
23047
+ var isLatLng = internal.isLatLngCRS(a.sourceCRS);
23048
+ var projectedMeters = isLatLng ? null : getProjectedDistanceMeters(a, b);
23049
+ var greatCircleMeters = getGreatCircleDistanceMeters(a, b);
23050
+ var unit = getDistanceUnit(projectedMeters || greatCircleMeters || 0);
23051
+ var labels = [];
23052
+
23053
+ if (projectedMeters !== null) {
23054
+ labels.push('Projected: ' + getDistanceDisplay(projectedMeters, unit).label);
23055
+ if (greatCircleMeters !== null) {
23056
+ labels.push('Great circle: ' + getDistanceDisplay(greatCircleMeters, unit).label);
23057
+ }
23058
+ } else if (greatCircleMeters !== null) {
23059
+ labels.push('Great circle: ' + getDistanceDisplay(greatCircleMeters, unit).label);
23060
+ }
23061
+ return {
23062
+ isLatLng,
23063
+ projectedMeters,
23064
+ greatCircleMeters,
23065
+ labels: labels.length ? labels : ['Distance unavailable']
23066
+ };
23067
+ }
23068
+
23069
+ function getProjectedDistanceMeters(a, b) {
23070
+ var toMeter = a.sourceCRS && a.sourceCRS.to_meter || 1;
23071
+ if (!a.data || !b.data) return null;
23072
+ return geom.distance2D(a.data[0], a.data[1], b.data[0], b.data[1]) * toMeter;
23073
+ }
23074
+
23075
+ function getGreatCircleDistanceMeters(a, b) {
23076
+ if (!a.lngLat || !b.lngLat) return null;
23077
+ return geom.greatCircleDistance(a.lngLat[0], a.lngLat[1], b.lngLat[0], b.lngLat[1]);
23078
+ }
23079
+
23080
+ function getGeodesicPixels(a, b) {
23081
+ var displayCRS = gui.map.getDisplayCRS() || a.sourceCRS;
23082
+ var project = displayCRS ? internal.getProjTransform2(wgs84, displayCRS) : null;
23083
+ var points = [];
23084
+ var start = getGeodesicEndpoint(a);
23085
+ var end = getGeodesicEndpoint(b);
23086
+ var state = {subdivisions: 0};
23087
+
23088
+ if (!a.lngLat || !b.lngLat || !project || !start || !end) return null;
23089
+ points.push(start.display);
23090
+ appendAdaptiveGeodesicSegment(points, start, end, project, 0, state);
23091
+ return countPathPoints(points) > 1 ? points : null;
23092
+ }
23093
+
23094
+ function getGeodesicEndpoint(point) {
23095
+ // Anchor the rendered great-circle line to the actual ruler endpoint.
23096
+ // Some projections have inverse/forward roundoff near their boundary, so
23097
+ // projecting point.lngLat back to display coords can visibly miss the marker.
23098
+ return point.lngLat ? {lngLat: point.lngLat, display: point.display} : null;
23099
+ }
23100
+
23101
+ function appendAdaptiveGeodesicSegment(points, a, b, project, depth, state) {
23102
+ var midLngLat = getGeodesicLngLatAt(a.lngLat, b.lngLat, 0.5);
23103
+ var mid = getProjectedGeodesicPoint(midLngLat, project);
23104
+ if (++state.subdivisions > GEODESIC_MAX_SUBDIVISIONS) {
23105
+ if (segmentCrossesAntimeridian(a.lngLat, b.lngLat)) {
23106
+ addPathBreak(points);
23107
+ }
23108
+ addPathPoint(points, b.display);
23109
+ return;
23110
+ }
23111
+ if (!mid) {
23112
+ handleInvalidGeodesicMidpoint(points, a, b, midLngLat, project, depth, state);
23113
+ return;
23114
+ }
23115
+ if (segmentCrossesAntimeridian(a.lngLat, b.lngLat)) {
23116
+ if (depth < GEODESIC_MAX_DEPTH) {
23117
+ appendAdaptiveGeodesicSegment(points, a, mid, project, depth + 1, state);
23118
+ appendAdaptiveGeodesicSegment(points, mid, b, project, depth + 1, state);
23119
+ } else {
23120
+ addPathBreak(points);
23121
+ addPathPoint(points, b.display);
23122
+ }
23123
+ } else if (depth < GEODESIC_MAX_DEPTH && segmentNeedsSubdivision(a, b, mid, project)) {
23124
+ appendAdaptiveGeodesicSegment(points, a, mid, project, depth + 1, state);
23125
+ appendAdaptiveGeodesicSegment(points, mid, b, project, depth + 1, state);
23126
+ } else {
23127
+ addPathPoint(points, b.display);
23128
+ }
23129
+ }
23130
+
23131
+ function segmentNeedsSubdivision(a, b, mid, project) {
23132
+ var samples = [
23133
+ getGeodesicPointAt(a.lngLat, b.lngLat, 0.25, project),
23134
+ mid,
23135
+ getGeodesicPointAt(a.lngLat, b.lngLat, 0.75, project)
23136
+ ];
23137
+ return getScreenDistance(a.display, b.display) > GEODESIC_MAX_SEGMENT_PX ||
23138
+ getGeodesicPixelError(a.display, b.display, samples) > GEODESIC_PIXEL_TOLERANCE;
23139
+ }
23140
+
23141
+ function getGeodesicPointAt(a, b, k, project) {
23142
+ return getProjectedGeodesicPoint(getGeodesicLngLatAt(a, b, k), project);
23143
+ }
23144
+
23145
+ function getGeodesicLngLatAt(a, b, k) {
23146
+ return interpolateGreatCirclePoint(a, b, k);
23147
+ }
23148
+
23149
+ function handleInvalidGeodesicMidpoint(points, a, b, midLngLat, project, depth, state) {
23150
+ var left, right;
23151
+ if (depth >= GEODESIC_MAX_DEPTH) {
23152
+ addPathBreak(points);
23153
+ addPathPoint(points, b.display);
23154
+ return;
23155
+ }
23156
+ left = findNearestValidGeodesicPoint(a.lngLat, midLngLat, project);
23157
+ right = findNearestValidGeodesicPoint(b.lngLat, midLngLat, project);
23158
+ if (left && lngLatPointsDiffer(a.lngLat, left.lngLat)) {
23159
+ appendAdaptiveGeodesicSegment(points, a, left, project, depth + 1, state);
23160
+ }
23161
+ addPathBreak(points);
23162
+ if (right && lngLatPointsDiffer(b.lngLat, right.lngLat)) {
23163
+ addPathPoint(points, right.display);
23164
+ appendAdaptiveGeodesicSegment(points, right, b, project, depth + 1, state);
23165
+ } else {
23166
+ addPathPoint(points, b.display);
23167
+ }
23168
+ }
23169
+
23170
+ function findNearestValidGeodesicPoint(validLngLat, invalidLngLat, project) {
23171
+ var lo = 0;
23172
+ var hi = 1;
23173
+ var best = getProjectedGeodesicPoint(validLngLat, project, true);
23174
+ var p, k;
23175
+ if (!best) return null;
23176
+ for (var i = 0; i < 16; i++) {
23177
+ k = (lo + hi) / 2;
23178
+ p = getProjectedGeodesicPoint(getGeodesicLngLatAt(validLngLat, invalidLngLat, k), project);
23179
+ if (p) {
23180
+ best = p;
23181
+ lo = k;
23182
+ } else {
23183
+ hi = k;
23184
+ }
23185
+ }
23186
+ return best;
23187
+ }
23188
+
23189
+ function lngLatPointsDiffer(a, b) {
23190
+ return a[0] != b[0] || a[1] != b[1];
23191
+ }
23192
+
23193
+ function getProjectedGeodesicPoint(lngLat, project, allowNearPole) {
23194
+ var display;
23195
+ if (!pointIsInLngLatRange(lngLat) || !allowNearPole && pointIsNearPole(lngLat)) return null;
23196
+ display = project(lngLat[0], lngLat[1]);
23197
+ return pointHasFiniteCoords(display) ? {lngLat, display} : null;
23198
+ }
23199
+
23200
+ function addPathBreak(points) {
23201
+ if (points.length > 0 && points[points.length - 1] !== null) {
23202
+ points.push(null);
23203
+ }
23204
+ }
23205
+
23206
+ function addPathPoint(points, p) {
23207
+ if (points[points.length - 1] === null && points.length > 1) {
23208
+ points.push(p);
23209
+ } else if (points.length === 0 || points[points.length - 1] !== p) {
23210
+ points.push(p);
23211
+ }
23212
+ }
23213
+
23214
+ function countPathPoints(points) {
23215
+ var count = 0;
23216
+ points.forEach(function(p) {
23217
+ if (p) count++;
23218
+ });
23219
+ return count;
23220
+ }
23221
+
23222
+ function getGeodesicPixelError(a, b, samples) {
23223
+ var p1 = ext.translateCoords(a[0], a[1]);
23224
+ var p2 = ext.translateCoords(b[0], b[1]);
23225
+ return samples.reduce(function(max, sample) {
23226
+ var p3;
23227
+ if (!sample) return Infinity;
23228
+ p3 = ext.translateCoords(sample.display[0], sample.display[1]);
23229
+ return Math.max(max, pointSegDist(p3[0], p3[1], p1[0], p1[1], p2[0], p2[1]));
23230
+ }, 0);
23231
+ }
23232
+
23233
+ function getScreenDistance(a, b) {
23234
+ var p1 = ext.translateCoords(a[0], a[1]);
23235
+ var p2 = ext.translateCoords(b[0], b[1]);
23236
+ return geom.distance2D(p1[0], p1[1], p2[0], p2[1]);
23237
+ }
23238
+
23239
+ function syncOverlaySize() {
23240
+ svg.setAttribute('width', ext.width());
23241
+ svg.setAttribute('height', ext.height());
23242
+ }
23243
+
23244
+ function drawPath(path, points) {
23245
+ var d, started = false;
23246
+ if (!points || countPathPoints(points) < 2) {
23247
+ clearSvgPath(path);
23248
+ return;
23249
+ }
23250
+ d = points.map(function(p) {
23251
+ if (!p) {
23252
+ started = false;
23253
+ return '';
23254
+ }
23255
+ var q = ext.translateCoords(p[0], p[1]);
23256
+ var cmd = started ? 'L' : 'M';
23257
+ started = true;
23258
+ return cmd + round(q[0]) + ' ' + round(q[1]);
23259
+ }).filter(Boolean).join(' ');
23260
+ path.setAttribute('d', d);
23261
+ }
23262
+
23263
+ function clearSvgPath(path) {
23264
+ path.removeAttribute('d');
23265
+ }
23266
+
23267
+ function drawMarker(marker, point, useGreatCircleStyle) {
23268
+ var p;
23269
+ if (!point) {
23270
+ hideMarker(marker);
23271
+ return;
23272
+ }
23273
+ p = ext.translateCoords(point.display[0], point.display[1]);
23274
+ marker.setAttribute('cx', round(p[0]));
23275
+ marker.setAttribute('cy', round(p[1]));
23276
+ marker.setAttribute('r', 4);
23277
+ marker.classList.toggle('ruler-great-circle-endpoint', !!useGreatCircleStyle);
23278
+ marker.removeAttribute('display');
23279
+ }
23280
+
23281
+ function hideMarker(marker) {
23282
+ marker.classList.remove('ruler-great-circle-endpoint');
23283
+ marker.setAttribute('display', 'none');
23284
+ }
23285
+ }
23286
+
23287
+ function createSvgNode(name) {
23288
+ return document.createElementNS(SVG_NS, name);
23289
+ }
23290
+
23291
+ function round(n) {
23292
+ return Math.round(n * 10) / 10;
23293
+ }
23294
+
23295
+ function pointHasFiniteCoords(p) {
23296
+ return !!p && isFinite(p[0]) && isFinite(p[1]);
23297
+ }
23298
+
23299
+ function pointSegDist(px, py, ax, ay, bx, by) {
23300
+ var dx = bx - ax;
23301
+ var dy = by - ay;
23302
+ var lenSq = dx * dx + dy * dy;
23303
+ var k;
23304
+ if (lenSq === 0) {
23305
+ return geom.distance2D(px, py, ax, ay);
23306
+ }
23307
+ k = ((px - ax) * dx + (py - ay) * dy) / lenSq;
23308
+ k = Math.max(0, Math.min(1, k));
23309
+ return geom.distance2D(px, py, ax + dx * k, ay + dy * k);
23310
+ }
23311
+
22557
23312
  utils$1.inherit(MshpMap, EventDispatcher);
22558
23313
 
22559
23314
  function MshpMap(gui) {
@@ -22714,6 +23469,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
22714
23469
  // Update map extent (also triggers redraw)
22715
23470
  projectMapExtent(_ext, oldCRS, this.getDisplayCRS(), calcFullBounds());
22716
23471
  updateFullBounds();
23472
+ map.dispatchEvent('display_crs_change', {crs: this.getDisplayCRS(), prev_crs: oldCRS});
22717
23473
  };
22718
23474
 
22719
23475
  // Initialization just before displaying the map for the first time
@@ -22729,6 +23485,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
22729
23485
  new InspectionControl2(gui, _hit);
22730
23486
  new SelectionTool(gui, _ext, _hit),
22731
23487
  new BoxTool(gui, _ext, _nav),
23488
+ new RulerTool(gui, _ext),
22732
23489
  new RectangleControl(gui, _hit),
22733
23490
  initInteractiveEditing(gui, _ext, _hit);
22734
23491
  _hit.on('change', function() { drawLayers('hover'); });