mapshaper 0.5.98 → 0.5.99

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.
@@ -1736,6 +1736,451 @@
1736
1736
 
1737
1737
  utils$1.inherit(Slider, EventDispatcher);
1738
1738
 
1739
+ // Create low-detail versions of large arc collections for faster rendering
1740
+ // at zoomed-out scales.
1741
+ function enhanceArcCollectionForDisplay(unfilteredArcs) {
1742
+ var size = unfilteredArcs.getPointCount(),
1743
+ filteredArcs, filteredSegLen;
1744
+
1745
+ // Only generate low-detail arcs for larger datasets
1746
+ if (size > 5e5) {
1747
+ if (!!unfilteredArcs.getVertexData().zz) {
1748
+ // Use precalculated simplification data for vertex filtering, if available
1749
+ filteredArcs = initFilteredArcs(unfilteredArcs);
1750
+ filteredSegLen = internal.getAvgSegment(filteredArcs);
1751
+ } else {
1752
+ // Use fast simplification as a fallback
1753
+ filteredSegLen = internal.getAvgSegment(unfilteredArcs) * 4;
1754
+ filteredArcs = internal.simplifyArcsFast(unfilteredArcs, filteredSegLen);
1755
+ }
1756
+ }
1757
+
1758
+ function initFilteredArcs(arcs) {
1759
+ var filterPct = 0.08;
1760
+ var nth = Math.ceil(arcs.getPointCount() / 5e5);
1761
+ var currInterval = arcs.getRetainedInterval();
1762
+ var filterZ = arcs.getThresholdByPct(filterPct, nth);
1763
+ var filteredArcs = arcs.setRetainedInterval(filterZ).getFilteredCopy();
1764
+ arcs.setRetainedInterval(currInterval); // reset current simplification
1765
+ return filteredArcs;
1766
+ }
1767
+
1768
+ unfilteredArcs.getScaledArcs = function(ext) {
1769
+ if (filteredArcs) {
1770
+ // match simplification of unfiltered arcs
1771
+ filteredArcs.setRetainedInterval(unfilteredArcs.getRetainedInterval());
1772
+ }
1773
+ // switch to filtered version of arcs at small scales
1774
+ var unitsPerPixel = 1/ext.getTransform().mx,
1775
+ useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5;
1776
+ return useFiltering ? filteredArcs : unfilteredArcs;
1777
+ };
1778
+ }
1779
+
1780
+ function getDisplayLayerForTable(table) {
1781
+ var n = table.size(),
1782
+ cellWidth = 12,
1783
+ cellHeight = 5,
1784
+ gutter = 6,
1785
+ arcs = [],
1786
+ shapes = [],
1787
+ aspectRatio = 1.1,
1788
+ x, y, col, row, blockSize;
1789
+
1790
+ if (n > 10000) {
1791
+ arcs = null;
1792
+ gutter = 0;
1793
+ cellWidth = 4;
1794
+ cellHeight = 4;
1795
+ aspectRatio = 1.45;
1796
+ } else if (n > 5000) {
1797
+ cellWidth = 5;
1798
+ gutter = 3;
1799
+ aspectRatio = 1.45;
1800
+ } else if (n > 1000) {
1801
+ gutter = 3;
1802
+ cellWidth = 8;
1803
+ aspectRatio = 1.3;
1804
+ }
1805
+
1806
+ if (n < 25) {
1807
+ blockSize = n;
1808
+ } else {
1809
+ blockSize = Math.sqrt(n * (cellWidth + gutter) / cellHeight / aspectRatio) | 0;
1810
+ }
1811
+
1812
+ for (var i=0; i<n; i++) {
1813
+ row = i % blockSize;
1814
+ col = Math.floor(i / blockSize);
1815
+ x = col * (cellWidth + gutter);
1816
+ y = cellHeight * (blockSize - row);
1817
+ if (arcs) {
1818
+ arcs.push(getArc(x, y, cellWidth, cellHeight));
1819
+ shapes.push([[i]]);
1820
+ } else {
1821
+ shapes.push([[x, y]]);
1822
+ }
1823
+ }
1824
+
1825
+ function getArc(x, y, w, h) {
1826
+ return [[x, y], [x + w, y], [x + w, y - h], [x, y - h], [x, y]];
1827
+ }
1828
+
1829
+ return {
1830
+ layer: {
1831
+ geometry_type: arcs ? 'polygon' : 'point',
1832
+ shapes: shapes,
1833
+ data: table
1834
+ },
1835
+ arcs: arcs ? new internal.ArcCollection(arcs) : null
1836
+ };
1837
+ }
1838
+
1839
+ // Assumes projections are available
1840
+
1841
+ function needReprojectionForDisplay(sourceCRS, displayCRS) {
1842
+ if (!sourceCRS || !displayCRS) {
1843
+ return false;
1844
+ }
1845
+ if (internal.crsAreEqual(sourceCRS, displayCRS)) {
1846
+ return false;
1847
+ }
1848
+ return true;
1849
+ }
1850
+
1851
+ function projectArcsForDisplay(arcs, src, dest) {
1852
+ var copy = arcs.getCopy(); // need to flatten first?
1853
+ var proj = internal.getProjTransform2(src, dest);
1854
+ // TODO: think about densification
1855
+ try {
1856
+ // fast and preserves Z values, but throws on first unprojectable point
1857
+ internal.projectArcs(copy, proj);
1858
+ } catch(e) {
1859
+ console.error(e);
1860
+ internal.projectArcs2(copy, proj);
1861
+ }
1862
+ return copy;
1863
+ }
1864
+
1865
+ function projectPointsForDisplay(lyr, src, dest) {
1866
+ var copy = utils$1.extend({}, lyr);
1867
+ var proj = internal.getProjTransform2(src, dest);
1868
+ copy.shapes = internal.cloneShapes(lyr.shapes);
1869
+ internal.projectPointLayer(copy, proj);
1870
+ return copy;
1871
+ }
1872
+
1873
+
1874
+ // Update map extent and trigger redraw, after a new display CRS has been applied
1875
+ function projectMapExtent(ext, src, dest, newBounds) {
1876
+ var oldBounds = ext.getBounds();
1877
+ var oldScale = ext.scale();
1878
+ var newCP, proj;
1879
+
1880
+ // if source or destination CRS is unknown, show full extent
1881
+ // if map is at full extent, show full extent
1882
+ // TODO: handle case that scale is 1 and map is panned away from center
1883
+ if (ext.scale() == 1 || !dest) {
1884
+ ext.setBounds(newBounds);
1885
+ ext.home(); // sets full extent and triggers redraw
1886
+ } else {
1887
+ // if map is zoomed, stay centered on the same geographic location, at the same relative scale
1888
+ proj = internal.getProjTransform2(src, dest);
1889
+ newCP = proj(oldBounds.centerX(), oldBounds.centerY());
1890
+ ext.setBounds(newBounds);
1891
+ if (!newCP) {
1892
+ // projection of center point failed; use center of bounds
1893
+ // (also consider just resetting the view using ext.home())
1894
+ newCP = [newBounds.centerX(), newBounds.centerY()];
1895
+ }
1896
+ ext.recenter(newCP[0], newCP[1], oldScale);
1897
+ }
1898
+ }
1899
+
1900
+ // Called from console; for testing dynamic crs
1901
+ function setDisplayProjection(gui, cmd) {
1902
+ var arg = cmd.replace(/^projd[ ]*/, '');
1903
+ if (arg) {
1904
+ gui.map.setDisplayCRS(internal.getCRS(arg));
1905
+ } else {
1906
+ gui.map.setDisplayCRS(null);
1907
+ }
1908
+ }
1909
+
1910
+ function filterLayerByIds(lyr, ids) {
1911
+ var shapes;
1912
+ if (lyr.shapes) {
1913
+ shapes = ids.map(function(id) {
1914
+ return lyr.shapes[id];
1915
+ });
1916
+ return utils$1.defaults({shapes: shapes, data: null}, lyr);
1917
+ }
1918
+ return lyr;
1919
+ }
1920
+
1921
+ function formatLayerNameForDisplay(name) {
1922
+ return name || '[unnamed]';
1923
+ }
1924
+
1925
+ function cleanLayerName(raw) {
1926
+ return raw.replace(/[\n\t/\\]/g, '')
1927
+ .replace(/^[\.\s]+/, '').replace(/[\.\s]+$/, '');
1928
+ }
1929
+
1930
+ function updateLayerStackOrder(layers) {
1931
+ // 1. assign ascending ids to unassigned layers above the range of other layers
1932
+ layers.forEach(function(o, i) {
1933
+ if (!o.layer.stack_id) o.layer.stack_id = 1e6 + i;
1934
+ });
1935
+ // 2. sort in ascending order
1936
+ layers.sort(function(a, b) {
1937
+ return a.layer.stack_id - b.layer.stack_id;
1938
+ });
1939
+ // 3. assign consecutve ids
1940
+ layers.forEach(function(o, i) {
1941
+ o.layer.stack_id = i + 1;
1942
+ });
1943
+ return layers;
1944
+ }
1945
+
1946
+ function sortLayersForMenuDisplay(layers) {
1947
+ layers = updateLayerStackOrder(layers);
1948
+ return layers.reverse();
1949
+ }
1950
+
1951
+ function setZ(lyr, z) {
1952
+ lyr.source.dataset.arcs.setRetainedInterval(z);
1953
+ if (isProjectedLayer(lyr)) {
1954
+ lyr.arcs.setRetainedInterval(z);
1955
+ }
1956
+ }
1957
+
1958
+ function updateZ(lyr) {
1959
+ if (isProjectedLayer(lyr) && !lyr.source.dataset.arcs.isFlat()) {
1960
+ lyr.arcs.setThresholds(lyr.source.dataset.arcs.getVertexData().zz);
1961
+ }
1962
+ }
1963
+
1964
+ function insertVertex$1(lyr, id, dataPoint) {
1965
+ internal.insertVertex(lyr.source.dataset.arcs, id, dataPoint);
1966
+ if (isProjectedLayer(lyr)) {
1967
+ internal.insertVertex(lyr.arcs, id, lyr.projectPoint(dataPoint[0], dataPoint[1]));
1968
+ }
1969
+ }
1970
+
1971
+ function deleteVertex$1(lyr, id) {
1972
+ internal.deleteVertex(lyr.arcs, id);
1973
+ if (isProjectedLayer(lyr)) {
1974
+ internal.deleteVertex(lyr.source.dataset.arcs, id);
1975
+ }
1976
+ }
1977
+
1978
+ function translateDisplayPoint(lyr, p) {
1979
+ return isProjectedLayer(lyr) ? lyr.invertPoint(p[0], p[1]) : p;
1980
+ }
1981
+
1982
+ function getPointCoords(lyr, fid) {
1983
+ return internal.cloneShape(lyr.source.layer.shapes[fid]);
1984
+ }
1985
+
1986
+ // bbox: display coords
1987
+ // intended to work with rectangular projections like Mercator
1988
+ function getBBoxCoords(lyr, bbox) {
1989
+ if (!isProjectedLayer(lyr)) return bbox;
1990
+ var a = translateDisplayPoint(lyr, [bbox[0], bbox[1]]);
1991
+ var b = translateDisplayPoint(lyr, [bbox[2], bbox[3]]);
1992
+ var bounds = new internal.Bounds();
1993
+ bounds.mergePoint(a[0], a[1]);
1994
+ bounds.mergePoint(b[0], b[1]);
1995
+ return bounds.toArray();
1996
+ }
1997
+
1998
+ function getVertexCoords(lyr, id) {
1999
+ return lyr.source.dataset.arcs.getVertex2(id);
2000
+ }
2001
+
2002
+ function setVertexCoords(lyr, ids, dataPoint) {
2003
+ internal.snapVerticesToPoint(ids, dataPoint, lyr.source.dataset.arcs, true);
2004
+ if (isProjectedLayer(lyr)) {
2005
+ var p = lyr.projectPoint(dataPoint[0], dataPoint[1]);
2006
+ internal.snapVerticesToPoint(ids, p, lyr.arcs, true);
2007
+ }
2008
+ }
2009
+
2010
+ function setPointCoords(lyr, fid, coords) {
2011
+ lyr.source.layer.shapes[fid] = coords;
2012
+ if (isProjectedLayer(lyr)) {
2013
+ lyr.layer.shapes[fid] = projectPointCoords(coords, lyr.projectPoint);
2014
+ }
2015
+ }
2016
+
2017
+ function updateVertexCoords(lyr, ids) {
2018
+ if (!isProjectedLayer(lyr)) return;
2019
+ var p = lyr.arcs.getVertex2(ids[0]);
2020
+ internal.snapVerticesToPoint(ids, lyr.invertPoint(p[0], p[1]), lyr.source.dataset.arcs, true);
2021
+ }
2022
+
2023
+ function isProjectedLayer(lyr) {
2024
+ // TODO: could do some validation on the layer's contents
2025
+ return !!(lyr.source && lyr.invertPoint);
2026
+ }
2027
+
2028
+ // Update data coordinates by projecting display coordinates
2029
+ function updatePointCoords(lyr, fid) {
2030
+ if (!isProjectedLayer(lyr)) return;
2031
+ var displayShp = lyr.layer.shapes[fid];
2032
+ lyr.source.layer.shapes[fid] = projectPointCoords(displayShp, lyr.invertPoint);
2033
+ }
2034
+
2035
+ function projectPointCoords(src, proj) {
2036
+ var dest = [], p;
2037
+ for (var i=0; i<src.length; i++) {
2038
+ p = proj(src[i][0], src[i][1]);
2039
+ if (p) dest.push(p);
2040
+ }
2041
+ return dest.length ? dest : null;
2042
+ }
2043
+
2044
+ // displayCRS: CRS to use for display, or null (which clears any current display CRS)
2045
+ function projectDisplayLayer(lyr, displayCRS) {
2046
+ var sourceCRS = internal.getDatasetCRS(lyr.source.dataset);
2047
+ var lyr2;
2048
+ if (!lyr.geographic || !sourceCRS) {
2049
+ return lyr;
2050
+ }
2051
+ if (lyr.dynamic_crs && internal.crsAreEqual(sourceCRS, lyr.dynamic_crs)) {
2052
+ return lyr;
2053
+ }
2054
+ lyr2 = getDisplayLayer(lyr.source.layer, lyr.source.dataset, {crs: displayCRS});
2055
+ // kludge: copy projection-related properties to original layer
2056
+ lyr.dynamic_crs = lyr2.dynamic_crs;
2057
+ lyr.layer = lyr2.layer;
2058
+ if (lyr.style && lyr.style.ids) {
2059
+ // re-apply layer filter
2060
+ lyr.layer = filterLayerByIds(lyr.layer, lyr.style.ids);
2061
+ }
2062
+ lyr.invertPoint = lyr2.invertPoint;
2063
+ lyr.projectPoint = lyr2.projectPoint;
2064
+ lyr.bounds = lyr2.bounds;
2065
+ lyr.arcs = lyr2.arcs;
2066
+ }
2067
+
2068
+
2069
+ // Wrap a layer in an object along with information needed for rendering
2070
+ function getDisplayLayer(layer, dataset, opts) {
2071
+ var obj = {
2072
+ layer: null,
2073
+ arcs: null,
2074
+ // display_arcs: null,
2075
+ style: null,
2076
+ invertPoint: null,
2077
+ projectPoint: null,
2078
+ source: {
2079
+ layer: layer,
2080
+ dataset: dataset
2081
+ },
2082
+ empty: internal.getFeatureCount(layer) === 0
2083
+ };
2084
+
2085
+ var sourceCRS = opts.crs && internal.getDatasetCRS(dataset); // get src iff display CRS is given
2086
+ var displayCRS = opts.crs || null;
2087
+ // display arcs may have been generated when another layer in the dataset was converted for display... re-use if available
2088
+ var displayArcs = dataset.displayArcs || null;
2089
+ var emptyArcs;
2090
+
2091
+ // Assume that dataset.displayArcs is in the display CRS
2092
+ // (it must be deleted upstream if reprojection is needed)
2093
+ if (dataset.arcs && !displayArcs) {
2094
+ // project arcs, if needed
2095
+ if (needReprojectionForDisplay(sourceCRS, displayCRS)) {
2096
+ displayArcs = projectArcsForDisplay(dataset.arcs, sourceCRS, displayCRS);
2097
+ } else {
2098
+ // Use original arcs for display if there is no dynamic reprojection
2099
+ displayArcs = dataset.arcs;
2100
+ }
2101
+
2102
+ enhanceArcCollectionForDisplay(displayArcs);
2103
+ dataset.displayArcs = displayArcs; // stash these in the dataset for other layers to use
2104
+ }
2105
+
2106
+ if (internal.layerHasFurniture(layer)) {
2107
+ obj.furniture = true;
2108
+ obj.furniture_type = internal.getFurnitureLayerType(layer);
2109
+ obj.layer = layer;
2110
+ // treating furniture layers (other than frame) as tabular for now,
2111
+ // so there is something to show if they are selected
2112
+ obj.tabular = obj.furniture_type != 'frame';
2113
+ } else if (obj.empty) {
2114
+ obj.layer = {shapes: []}; // ideally we should avoid empty layers
2115
+ } else if (!layer.geometry_type) {
2116
+ obj.tabular = true;
2117
+ } else {
2118
+ obj.geographic = true;
2119
+ obj.layer = layer;
2120
+ obj.arcs = displayArcs;
2121
+ }
2122
+
2123
+ if (obj.tabular) {
2124
+ utils$1.extend(obj, getDisplayLayerForTable(layer.data));
2125
+ }
2126
+
2127
+ // dynamic reprojection (arcs were already reprojected above)
2128
+ if (obj.geographic && needReprojectionForDisplay(sourceCRS, displayCRS)) {
2129
+ obj.dynamic_crs = displayCRS;
2130
+ obj.invertPoint = internal.getProjTransform2(displayCRS, sourceCRS);
2131
+ obj.projectPoint = internal.getProjTransform2(sourceCRS, displayCRS);
2132
+ if (internal.layerHasPoints(layer)) {
2133
+ obj.layer = projectPointsForDisplay(layer, sourceCRS, displayCRS);
2134
+ } else if (internal.layerHasPaths(layer)) {
2135
+ emptyArcs = findEmptyArcs(displayArcs);
2136
+ if (emptyArcs.length > 0) {
2137
+ // Don't try to draw paths containing coordinates that failed to project
2138
+ obj.layer = internal.filterPathLayerByArcIds(obj.layer, emptyArcs);
2139
+ }
2140
+ }
2141
+ }
2142
+
2143
+ obj.bounds = getDisplayBounds(obj.layer, obj.arcs);
2144
+ return obj;
2145
+ }
2146
+
2147
+
2148
+ function getDisplayBounds(lyr, arcs) {
2149
+ var arcBounds = arcs ? arcs.getBounds() : new Bounds(),
2150
+ bounds = arcBounds, // default display extent: all arcs in the dataset
2151
+ lyrBounds;
2152
+
2153
+ if (lyr.geometry_type == 'point') {
2154
+ lyrBounds = internal.getLayerBounds(lyr);
2155
+ if (lyrBounds && lyrBounds.hasBounds()) {
2156
+ if (lyrBounds.area() > 0 || !arcBounds.hasBounds()) {
2157
+ bounds = lyrBounds;
2158
+ } else {
2159
+ // if a point layer has no extent (e.g. contains only a single point),
2160
+ // then merge with arc bounds, to place the point in context.
2161
+ bounds = arcBounds.mergeBounds(lyrBounds);
2162
+ }
2163
+ }
2164
+ }
2165
+
2166
+ if (!bounds || !bounds.hasBounds()) { // empty layer
2167
+ bounds = new Bounds();
2168
+ }
2169
+ return bounds;
2170
+ }
2171
+
2172
+ // Returns an array of ids of empty arcs (arcs can be set to empty if errors occur while projecting them)
2173
+ function findEmptyArcs(arcs) {
2174
+ var nn = arcs.getVertexData().nn;
2175
+ var ids = [];
2176
+ for (var i=0, n=nn.length; i<n; i++) {
2177
+ if (nn[i] === 0) {
2178
+ ids.push(i);
2179
+ }
2180
+ }
2181
+ return ids;
2182
+ }
2183
+
1739
2184
  /*
1740
2185
  How changes in the simplify control should affect other components
1741
2186
 
@@ -1904,6 +2349,7 @@
1904
2349
  var opts = getSimplifyOptions();
1905
2350
  mapshaper.simplify(dataset, opts);
1906
2351
  gui.session.simplificationApplied(getSimplifyOptionsAsString());
2352
+ updateZ(gui.map.getActiveLayer()); // question: does this update all display layers?
1907
2353
  model.updated({
1908
2354
  // trigger filtered arc rebuild without redraw if pct is 1
1909
2355
  simplify_method: opts.percentage == 1,
@@ -1955,7 +2401,8 @@
1955
2401
  function onChange(pct) {
1956
2402
  if (_value != pct) {
1957
2403
  _value = pct;
1958
- model.getActiveLayer().dataset.arcs.setRetainedInterval(fromPct(pct));
2404
+ // model.getActiveLayer().dataset.arcs.setRetainedInterval(fromPct(pct));
2405
+ setZ(gui.map.getActiveLayer(), fromPct(pct));
1959
2406
  gui.session.updateSimplificationPct(pct);
1960
2407
  model.updated({'simplify_amount': true});
1961
2408
  updateSliderDisplay();
@@ -2191,77 +2638,6 @@
2191
2638
  }
2192
2639
  }
2193
2640
 
2194
- // Assumes projections are available
2195
-
2196
- function needReprojectionForDisplay(sourceCRS, displayCRS) {
2197
- if (!sourceCRS || !displayCRS) {
2198
- return false;
2199
- }
2200
- if (internal.crsAreEqual(sourceCRS, displayCRS)) {
2201
- return false;
2202
- }
2203
- return true;
2204
- }
2205
-
2206
- function projectArcsForDisplay_v1(arcs, src, dest) {
2207
- var copy = arcs.getCopy(); // need to flatten first?
2208
- var proj = internal.getProjTransform(src, dest);
2209
- internal.projectArcs(copy, proj); // need to densify arcs?
2210
- return copy;
2211
- }
2212
-
2213
- function projectArcsForDisplay(arcs, src, dest) {
2214
- var copy = arcs.getCopy(); // need to flatten first?
2215
- var proj = internal.getProjTransform2(src, dest);
2216
- internal.projectArcs2(copy, proj); // need to densify arcs?
2217
- return copy;
2218
- }
2219
-
2220
- function projectPointsForDisplay(lyr, src, dest) {
2221
- var copy = utils$1.extend({}, lyr);
2222
- var proj = internal.getProjTransform2(src, dest);
2223
- copy.shapes = internal.cloneShapes(lyr.shapes);
2224
- internal.projectPointLayer(copy, proj);
2225
- return copy;
2226
- }
2227
-
2228
-
2229
- // Update map extent and trigger redraw, after a new display CRS has been applied
2230
- function projectMapExtent(ext, src, dest, newBounds) {
2231
- var oldBounds = ext.getBounds();
2232
- var oldScale = ext.scale();
2233
- var newCP, proj;
2234
-
2235
- // if source or destination CRS is unknown, show full extent
2236
- // if map is at full extent, show full extent
2237
- // TODO: handle case that scale is 1 and map is panned away from center
2238
- if (ext.scale() == 1 || !dest) {
2239
- ext.setBounds(newBounds);
2240
- ext.home(); // sets full extent and triggers redraw
2241
- } else {
2242
- // if map is zoomed, stay centered on the same geographic location, at the same relative scale
2243
- proj = internal.getProjTransform2(src, dest);
2244
- newCP = proj(oldBounds.centerX(), oldBounds.centerY());
2245
- ext.setBounds(newBounds);
2246
- if (!newCP) {
2247
- // projection of center point failed; use center of bounds
2248
- // (also consider just resetting the view using ext.home())
2249
- newCP = [newBounds.centerX(), newBounds.centerY()];
2250
- }
2251
- ext.recenter(newCP[0], newCP[1], oldScale);
2252
- }
2253
- }
2254
-
2255
- // Called from console; for testing dynamic crs
2256
- function setDisplayProjection(gui, cmd) {
2257
- var arg = cmd.replace(/^projd[ ]*/, '');
2258
- if (arg) {
2259
- gui.map.setDisplayCRS(internal.getCRS(arg));
2260
- } else {
2261
- gui.map.setDisplayCRS(null);
2262
- }
2263
- }
2264
-
2265
2641
  function Console(gui) {
2266
2642
  var model = gui.model;
2267
2643
  var CURSOR = '$ ';
@@ -2346,8 +2722,8 @@
2346
2722
  // when an instance loses focus.
2347
2723
  internal.setLoggingFunctions(consoleMessage, consoleError, consoleStop);
2348
2724
  gui.container.addClass('console-open');
2349
- gui.dispatchEvent('resize');
2350
2725
  el.show();
2726
+ gui.dispatchEvent('resize');
2351
2727
  input.node().focus();
2352
2728
  history = getHistory();
2353
2729
  }
@@ -2902,47 +3278,6 @@
2902
3278
 
2903
3279
  utils$1.inherit(RepairControl, EventDispatcher);
2904
3280
 
2905
- function filterLayerByIds(lyr, ids) {
2906
- var shapes;
2907
- if (lyr.shapes) {
2908
- shapes = ids.map(function(id) {
2909
- return lyr.shapes[id];
2910
- });
2911
- return utils$1.defaults({shapes: shapes, data: null}, lyr);
2912
- }
2913
- return lyr;
2914
- }
2915
-
2916
- function formatLayerNameForDisplay(name) {
2917
- return name || '[unnamed]';
2918
- }
2919
-
2920
- function cleanLayerName(raw) {
2921
- return raw.replace(/[\n\t/\\]/g, '')
2922
- .replace(/^[\.\s]+/, '').replace(/[\.\s]+$/, '');
2923
- }
2924
-
2925
- function updateLayerStackOrder(layers) {
2926
- // 1. assign ascending ids to unassigned layers above the range of other layers
2927
- layers.forEach(function(o, i) {
2928
- if (!o.layer.stack_id) o.layer.stack_id = 1e6 + i;
2929
- });
2930
- // 2. sort in ascending order
2931
- layers.sort(function(a, b) {
2932
- return a.layer.stack_id - b.layer.stack_id;
2933
- });
2934
- // 3. assign consecutve ids
2935
- layers.forEach(function(o, i) {
2936
- o.layer.stack_id = i + 1;
2937
- });
2938
- return layers;
2939
- }
2940
-
2941
- function sortLayersForMenuDisplay(layers) {
2942
- layers = updateLayerStackOrder(layers);
2943
- return layers.reverse();
2944
- }
2945
-
2946
3281
  // import { groupLayersByDataset } from '../dataset/mapshaper-target-utils';
2947
3282
 
2948
3283
  // Export buttons and their behavior
@@ -3669,7 +4004,6 @@
3669
4004
  offset = 0;
3670
4005
  }
3671
4006
 
3672
-
3673
4007
  function isUndoEvt(e) {
3674
4008
  return (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key == 'z';
3675
4009
  }
@@ -3691,18 +4025,17 @@
3691
4025
  e.stopPropagation();
3692
4026
  e.preventDefault();
3693
4027
  }
3694
-
3695
4028
  }, this, 10);
3696
4029
 
3697
- // undo/redo point/symbol dragging
3698
- //
3699
- gui.on('symbol_dragstart', function(e) {
3700
- stashedUndo = this.makePointSetter(e.FID);
3701
- }, this);
3702
-
3703
4030
  gui.on('symbol_dragend', function(e) {
3704
- var redo = this.makePointSetter(e.FID);
3705
- this.addHistoryState(stashedUndo, redo);
4031
+ var target = e.data.target;
4032
+ var undo = function() {
4033
+ setPointCoords(target, e.FID, e.startCoords);
4034
+ };
4035
+ var redo = function() {
4036
+ setPointCoords(target, e.FID, e.endCoords);
4037
+ };
4038
+ this.addHistoryState(undo, redo);
3706
4039
  }, this);
3707
4040
 
3708
4041
  // undo/redo label dragging
@@ -3729,35 +4062,33 @@
3729
4062
  }, this);
3730
4063
 
3731
4064
  gui.on('vertex_dragend', function(e) {
3732
- var target = gui.model.getActiveLayer();
3733
- var arcs = target.dataset.arcs;
3734
- var startPoint = e.points[0];
3735
- var endPoint = internal.getVertexCoords(e.ids[0], arcs);
4065
+ var target = e.data.target;
4066
+ var startPoint = e.points[0]; // in data coords
4067
+ var endPoint = getVertexCoords(target, e.ids[0]);
3736
4068
  var undo = function() {
3737
4069
  if (e.insertion) {
3738
- internal.deleteVertex(arcs, e.ids[0]);
4070
+ deleteVertex$1(target, e.ids[0]);
3739
4071
  } else {
3740
- snapVerticesToPoint(e.ids, startPoint, arcs, true);
4072
+ setVertexCoords(target, e.ids, startPoint);
3741
4073
  }
3742
4074
  };
3743
4075
  var redo = function() {
3744
4076
  if (e.insertion) {
3745
- internal.insertVertex(arcs, e.ids[0], e.points[0]);
4077
+ insertVertex$1(target, e.ids[0], endPoint);
3746
4078
  }
3747
- snapVerticesToPoint(e.ids, endPoint, arcs, true);
4079
+ setVertexCoords(target, e.ids, endPoint);
3748
4080
  };
3749
4081
  this.addHistoryState(undo, redo);
3750
4082
  }, this);
3751
4083
 
3752
4084
  gui.on('vertex_delete', function(e) {
3753
- var target = gui.model.getActiveLayer();
3754
- var arcs = target.dataset.arcs;
3755
- var p = arcs.getVertex2(e.vertex_id);
4085
+ // get vertex coords in data coordinates (not display coordinates);
4086
+ var p = getVertexCoords(e.data.target, e.vertex_id);
3756
4087
  var redo = function() {
3757
- internal.deleteVertex(arcs, e.vertex_id);
4088
+ deleteVertex$1(e.data.target, e.vertex_id);
3758
4089
  };
3759
4090
  var undo = function() {
3760
- internal.insertVertex(arcs, e.vertex_id, p);
4091
+ insertVertex$1(e.data.target, e.vertex_id, p);
3761
4092
  };
3762
4093
  this.addHistoryState(undo, redo);
3763
4094
  }, this);
@@ -3766,14 +4097,6 @@
3766
4097
  reset();
3767
4098
  };
3768
4099
 
3769
- this.makePointSetter = function(i) {
3770
- var target = gui.model.getActiveLayer();
3771
- var shp = cloneShape(target.layer.shapes[i]);
3772
- return function() {
3773
- target.layer.shapes[i] = shp;
3774
- };
3775
- };
3776
-
3777
4100
  this.makeDataSetter = function(id) {
3778
4101
  var target = gui.model.getActiveLayer();
3779
4102
  var rec = copyRecord(target.layer.data.getRecordAt(id));
@@ -3783,15 +4106,6 @@
3783
4106
  };
3784
4107
  };
3785
4108
 
3786
- this.makeVertexSetter = function(ids) {
3787
- var target = gui.model.getActiveLayer();
3788
- var arcs = target.dataset.arcs;
3789
- var p = internal.getVertexCoords(ids[0], arcs);
3790
- return function() {
3791
- snapVerticesToPoint(ids, p, arcs, true);
3792
- };
3793
- };
3794
-
3795
4109
  this.addHistoryState = function(undo, redo) {
3796
4110
  if (offset > 0) {
3797
4111
  history.splice(-offset);
@@ -5503,6 +5817,8 @@
5503
5817
  // avoid re-allocating memory
5504
5818
  var xx2 = new Float64Array(data.xx.buffer, 0, n-1);
5505
5819
  var yy2 = new Float64Array(data.yy.buffer, 0, n-1);
5820
+ var zz2 = arcs.isFlat() ? null : new Float64Array(data.zz.buffer, 0, n-1);
5821
+ var z = arcs.getRetainedInterval();
5506
5822
  var count = 0;
5507
5823
  var found = false;
5508
5824
  for (var j=0; j<nn.length; j++) {
@@ -5516,24 +5832,31 @@
5516
5832
  utils.copyElements(data.yy, 0, yy2, 0, i);
5517
5833
  utils.copyElements(data.xx, i+1, xx2, i, n-i-1);
5518
5834
  utils.copyElements(data.yy, i+1, yy2, i, n-i-1);
5519
- arcs.updateVertexData(nn, xx2, yy2, null);
5835
+ if (zz2) {
5836
+ utils.copyElements(data.zz, 0, zz2, 0, i);
5837
+ utils.copyElements(data.zz, i+1, zz2, i, n-i-1);
5838
+ }
5839
+ arcs.updateVertexData(nn, xx2, yy2, zz2);
5840
+ arcs.setRetainedInterval(z);
5520
5841
  }
5521
5842
 
5522
5843
  function insertVertex(arcs, i, p) {
5523
- // TODO: add extra bytes to the buffers, to reduce new memory allocation
5524
5844
  var data = arcs.getVertexData();
5525
5845
  var nn = data.nn;
5526
5846
  var n = data.xx.length;
5527
5847
  var count = 0;
5528
5848
  var found = false;
5529
- var xx2, yy2;
5849
+ var xx2, yy2, zz2;
5530
5850
  // avoid re-allocating memory on each insertion
5531
5851
  if (data.xx.buffer.byteLength >= data.xx.length * 8 + 8) {
5532
5852
  xx2 = new Float64Array(data.xx.buffer, 0, n+1);
5533
5853
  yy2 = new Float64Array(data.yy.buffer, 0, n+1);
5534
5854
  } else {
5535
- xx2 = new Float64Array(new ArrayBuffer((n + 20) * 8), 0, n+1);
5536
- yy2 = new Float64Array(new ArrayBuffer((n + 20) * 8), 0, n+1);
5855
+ xx2 = new Float64Array(new ArrayBuffer((n + 50) * 8), 0, n+1);
5856
+ yy2 = new Float64Array(new ArrayBuffer((n + 50) * 8), 0, n+1);
5857
+ }
5858
+ if (!arcs.isFlat()) {
5859
+ zz2 = new Float64Array(new ArrayBuffer((n + 1) * 8), 0, n+1);
5537
5860
  }
5538
5861
  for (var j=0; j<nn.length; j++) {
5539
5862
  count += nn[j];
@@ -5548,7 +5871,12 @@
5548
5871
  utils.copyElements(data.yy, i, yy2, i+1, n-i);
5549
5872
  xx2[i] = p[0];
5550
5873
  yy2[i] = p[1];
5551
- arcs.updateVertexData(nn, xx2, yy2, null);
5874
+ if (zz2) {
5875
+ zz2[i] = Infinity;
5876
+ utils.copyElements(data.zz, 0, zz2, 0, i);
5877
+ utils.copyElements(data.zz, i, zz2, i+1, n-i);
5878
+ }
5879
+ arcs.updateVertexData(nn, xx2, yy2, zz2);
5552
5880
  }
5553
5881
 
5554
5882
  function getShapeHitTest(displayLayer, ext, interactionMode) {
@@ -5675,7 +6003,7 @@
5675
6003
  hits.push(id);
5676
6004
  }
5677
6005
  });
5678
- // console.log(hitThreshold, bullseye);
6006
+ // TODO: add info on what part of a shape gets hit
5679
6007
  return {
5680
6008
  ids: utils$1.uniq(hits) // multipoint features can register multiple hits
5681
6009
  };
@@ -6283,15 +6611,18 @@
6283
6611
  function onMouseChange(e) {
6284
6612
  if (!enabled) return;
6285
6613
  if (isOverMap(e)) {
6286
- displayCoords(ext.translatePixelCoords(e.x, e.y));
6614
+ displayCoords(gui.map.translatePixelCoords(e.x, e.y));
6287
6615
  } else {
6288
6616
  clearCoords();
6289
6617
  }
6290
6618
  }
6291
6619
 
6292
6620
  function displayCoords(p) {
6293
- var decimals = internal.getBoundsPrecisionForDisplay(ext.getBounds().toArray());
6294
- var str = internal.getRoundedCoordString(p, decimals);
6621
+ var p1 = gui.map.translatePixelCoords(0, ext.height());
6622
+ var p2 = gui.map.translatePixelCoords(ext.width(), 0);
6623
+ var bbox = p1.concat(p2);
6624
+ var decimals = internal.getBoundsPrecisionForDisplay(bbox);
6625
+ var str = internal.getRoundedCoordString(p, decimals);
6295
6626
  readout.text(str).show();
6296
6627
  }
6297
6628
 
@@ -6756,6 +7087,10 @@
6756
7087
  ext.zoomToExtent(e.value, _fx, _fy);
6757
7088
  });
6758
7089
 
7090
+ mouse.on('click', function(e) {
7091
+ gui.dispatchEvent('map_click', e);
7092
+ });
7093
+
6759
7094
  mouse.on('dblclick', function(e) {
6760
7095
  if (disabled()) return;
6761
7096
  zoomByPct(getZoomInPct(), e.x / ext.width(), e.y / ext.height());
@@ -7491,31 +7826,38 @@
7491
7826
  }
7492
7827
 
7493
7828
  function initPointDragging(gui, ext, hit) {
7494
-
7829
+ var symbolInfo;
7495
7830
  function active(e) {
7496
7831
  return e.id > -1 && gui.interaction.getMode() == 'location';
7497
7832
  }
7498
7833
 
7499
7834
  hit.on('dragstart', function(e) {
7500
7835
  if (!active(e)) return;
7501
- gui.dispatchEvent('symbol_dragstart', {FID: e.id});
7836
+ var target = hit.getHitTarget();
7837
+ symbolInfo = {
7838
+ FID: e.id,
7839
+ startCoords: getPointCoords(target, e.id),
7840
+ target: target
7841
+ };
7502
7842
  });
7503
7843
 
7504
7844
  hit.on('drag', function(e) {
7505
7845
  if (!active(e)) return;
7506
- var lyr = hit.getHitTarget().layer;
7507
- var p = getPointCoordsById(e.id, lyr);
7846
+ // TODO: support multi points... get id of closest part to the pointer
7847
+ var p = getPointCoordsById(e.id, symbolInfo.target.layer);
7508
7848
  if (!p) return;
7509
7849
  var diff = translateDeltaDisplayCoords(e.dx, e.dy, ext);
7510
7850
  p[0] += diff[0];
7511
7851
  p[1] += diff[1];
7512
7852
  gui.dispatchEvent('map-needs-refresh');
7513
- gui.dispatchEvent('symbol_drag', {FID: e.id});
7514
7853
  });
7515
7854
 
7516
7855
  hit.on('dragend', function(e) {
7517
- if (!active(e)) return;
7518
- gui.dispatchEvent('symbol_dragend', {FID: e.id});
7856
+ if (!active(e) || !symbolInfo ) return;
7857
+ updatePointCoords(symbolInfo.target, e.id);
7858
+ symbolInfo.endCoords = getPointCoords(symbolInfo.target, e.id);
7859
+ gui.dispatchEvent('symbol_dragend', symbolInfo);
7860
+ symbolInfo = null;
7519
7861
  });
7520
7862
 
7521
7863
  function translateDeltaDisplayCoords(dx, dy, ext) {
@@ -7569,8 +7911,11 @@
7569
7911
  if (pixelDist > HOVER_THRESHOLD) {
7570
7912
  return null;
7571
7913
  }
7572
- var points = nearestIds.map(function(i) {return target.arcs.getVertex2(i);});
7914
+ var points = nearestIds.map(function(i) {
7915
+ return getVertexCoords(target, i); // data coordinates
7916
+ });
7573
7917
  return {
7918
+ target: target,
7574
7919
  ids: nearestIds,
7575
7920
  points: points
7576
7921
  };
@@ -7580,8 +7925,7 @@
7580
7925
  var target = hit.getHitTarget();
7581
7926
  if (!target.arcs.isFlat()) return null; // vertex insertion not supported with simplification
7582
7927
  var p = ext.translatePixelCoords(e.x, e.y);
7583
- var shp = target.layer.shapes[e.id];
7584
- var midpoint = findNearestMidpoint(p, shp, target.arcs);
7928
+ var midpoint = findNearestMidpoint(p, e.id, target);
7585
7929
  if (!midpoint ||
7586
7930
  midpoint.distance / ext.getPixelSize() > MIDPOINT_THRESHOLD) return null;
7587
7931
  return midpoint;
@@ -7591,8 +7935,9 @@
7591
7935
  if (!active()) return;
7592
7936
  if (insertionPoint) {
7593
7937
  var target = hit.getHitTarget();
7594
- internal.insertVertex(target.arcs, insertionPoint.i, insertionPoint.point);
7938
+ insertVertex$1(target, insertionPoint.i, insertionPoint.point);
7595
7939
  dragInfo = {
7940
+ target: target,
7596
7941
  insertion: true,
7597
7942
  ids: [insertionPoint.i],
7598
7943
  points: [insertionPoint.point]
@@ -7624,6 +7969,7 @@
7624
7969
  // kludge to get dataset to recalculate internal bounding boxes
7625
7970
  hit.getHitTarget().arcs.transformPoints(function() {});
7626
7971
  clearHoverVertex();
7972
+ updateVertexCoords(dragInfo.target, dragInfo.ids);
7627
7973
  gui.dispatchEvent('vertex_dragend', dragInfo);
7628
7974
  gui.dispatchEvent('map-needs-refresh');
7629
7975
  dragInfo = null;
@@ -7641,9 +7987,10 @@
7641
7987
  return;
7642
7988
  }
7643
7989
  gui.dispatchEvent('vertex_delete', {
7990
+ target: target,
7644
7991
  vertex_id: vId
7645
7992
  });
7646
- internal.deleteVertex(target.arcs, vId);
7993
+ deleteVertex$1(target, vId);
7647
7994
  clearHoverVertex();
7648
7995
  gui.dispatchEvent('map-needs-refresh');
7649
7996
  });
@@ -7661,7 +8008,7 @@
7661
8008
  // if hovering near a segment midpoint: show the midpoint and save midpoint info
7662
8009
  insertionPoint = findVertexInsertionPoint(e);
7663
8010
  if (insertionPoint) {
7664
- hit.setHoverVertex(insertionPoint.point);
8011
+ hit.setHoverVertex(insertionPoint.displayPoint);
7665
8012
  } else {
7666
8013
  // pointer is not over a vertex: clear any hover effect
7667
8014
  clearHoverVertex();
@@ -7672,7 +8019,9 @@
7672
8019
 
7673
8020
  // Given a location @p (e.g. corresponding to the mouse pointer location),
7674
8021
  // find the midpoint of two vertices on @shp suitable for inserting a new vertex
7675
- function findNearestMidpoint(p, shp, arcs) {
8022
+ function findNearestMidpoint(p, fid, target) {
8023
+ var arcs = target.arcs;
8024
+ var shp = target.layer.shapes[fid];
7676
8025
  var minDist = Infinity, v;
7677
8026
  internal.forEachSegmentInShape(shp, arcs, function(i, j, xx, yy) {
7678
8027
  var x1 = xx[i],
@@ -7681,6 +8030,7 @@
7681
8030
  y2 = yy[j],
7682
8031
  cx = (x1 + x2) / 2,
7683
8032
  cy = (y1 + y2) / 2,
8033
+ midpoint = [cx, cy],
7684
8034
  dist = geom.distance2D(cx, cy, p[0], p[1]);
7685
8035
  if (dist < minDist) {
7686
8036
  minDist = dist;
@@ -7688,7 +8038,8 @@
7688
8038
  i: (i < j ? i : j) + 1, // insertion point
7689
8039
  segment: [i, j],
7690
8040
  segmentLen: geom.distance2D(x1, y1, x2, y2),
7691
- point: [cx, cy],
8041
+ displayPoint: midpoint,
8042
+ point: translateDisplayPoint(target, midpoint),
7692
8043
  distance: dist
7693
8044
  };
7694
8045
  }
@@ -9062,7 +9413,7 @@
9062
9413
  var popup = gui.container.findChild('.box-tool-options');
9063
9414
  var coords = popup.findChild('.box-coords');
9064
9415
  var _on = false;
9065
- var bboxCoords, bboxPixels;
9416
+ var bboxDisplayCoords, bboxPixels, bboxDataCoords;
9066
9417
 
9067
9418
  var infoBtn = new SimpleButton(popup.findChild('.info-btn')).on('click', function() {
9068
9419
  if (coords.visible()) hideCoords(); else showCoords();
@@ -9093,7 +9444,7 @@
9093
9444
  });
9094
9445
 
9095
9446
  new SimpleButton(popup.findChild('.clip-btn')).on('click', function() {
9096
- runCommand('-clip bbox2=' + bboxCoords.join(','));
9447
+ runCommand('-clip bbox2=' + bboxDataCoords.join(','));
9097
9448
  });
9098
9449
 
9099
9450
  gui.addMode('box_tool', turnOn, turnOff);
@@ -9109,8 +9460,8 @@
9109
9460
  // Update the visible rectangle when the map view changes
9110
9461
  // (e.g. during zooming or panning)
9111
9462
  ext.on('change', function() {
9112
- if (!_on || !box.visible() || !bboxCoords) return;
9113
- var b = coordsToPix(bboxCoords);
9463
+ if (!_on || !box.visible() || !bboxDisplayCoords) return;
9464
+ var b = coordsToPix(bboxDisplayCoords);
9114
9465
  var pos = ext.position();
9115
9466
  var dx = pos.pageX,
9116
9467
  dy = pos.pageY;
@@ -9125,7 +9476,7 @@
9125
9476
  gui.on('box_drag', function(e) {
9126
9477
  var b = e.page_bbox;
9127
9478
  bboxPixels = e.map_bbox;
9128
- bboxCoords = pixToCoords(bboxPixels);
9479
+ bboxDisplayCoords = pixToCoords(bboxPixels);
9129
9480
  if (_on || inZoomMode()) {
9130
9481
  box.show(b[0], b[1], b[2], b[3]);
9131
9482
  }
@@ -9133,7 +9484,8 @@
9133
9484
 
9134
9485
  gui.on('box_drag_end', function(e) {
9135
9486
  bboxPixels = e.map_bbox;
9136
- bboxCoords = pixToCoords(bboxPixels);
9487
+ bboxDisplayCoords = pixToCoords(bboxPixels);
9488
+ bboxDataCoords = getBBoxCoords(gui.map.getActiveLayer(), bboxDisplayCoords);
9137
9489
  if (inZoomMode()) {
9138
9490
  box.hide();
9139
9491
  nav.zoomToBbox(bboxPixels);
@@ -9157,7 +9509,7 @@
9157
9509
 
9158
9510
  function showCoords() {
9159
9511
  El(infoBtn.node()).addClass('selected-btn');
9160
- coords.text(bboxCoords.join(','));
9512
+ coords.text(bboxDataCoords.join(','));
9161
9513
  coords.show();
9162
9514
  GUI.selectElement(coords.node());
9163
9515
  }
@@ -9204,238 +9556,189 @@
9204
9556
  return self;
9205
9557
  }
9206
9558
 
9207
- // Create low-detail versions of large arc collections for faster rendering
9208
- // at zoomed-out scales.
9209
- function MultiScaleArcCollection(unfilteredArcs) {
9210
- var size = unfilteredArcs.getPointCount(),
9211
- filteredArcs, filteredSegLen;
9212
-
9213
- // Only generate low-detail arcs for larger datasets
9214
- if (size > 5e5) {
9215
- if (!!unfilteredArcs.getVertexData().zz) {
9216
- // Use precalculated simplification data for vertex filtering, if available
9217
- filteredArcs = initFilteredArcs(unfilteredArcs);
9218
- filteredSegLen = internal.getAvgSegment(filteredArcs);
9219
- } else {
9220
- // Use fast simplification as a fallback
9221
- filteredSegLen = internal.getAvgSegment(unfilteredArcs) * 4;
9222
- filteredArcs = internal.simplifyArcsFast(unfilteredArcs, filteredSegLen);
9223
- }
9224
- }
9225
-
9226
- function initFilteredArcs(arcs) {
9227
- var filterPct = 0.08;
9228
- var nth = Math.ceil(arcs.getPointCount() / 5e5);
9229
- var currInterval = arcs.getRetainedInterval();
9230
- var filterZ = arcs.getThresholdByPct(filterPct, nth);
9231
- var filteredArcs = arcs.setRetainedInterval(filterZ).getFilteredCopy();
9232
- arcs.setRetainedInterval(currInterval); // reset current simplification
9233
- return filteredArcs;
9559
+ function loadScript(url, cb) {
9560
+ var script = document.createElement('script');
9561
+ script.onload = cb;
9562
+ script.src = url;
9563
+ document.head.appendChild(script);
9564
+ }
9565
+
9566
+ function loadStylesheet(url) {
9567
+ var el = document.createElement('link');
9568
+ el.rel = 'stylesheet';
9569
+ el.type = 'text/css';
9570
+ el.media = 'screen';
9571
+ el.href = url;
9572
+ document.head.appendChild(el);
9573
+ }
9574
+
9575
+ function Basemap(gui, ext) {
9576
+ var menu = gui.container.findChild('.basemap-options');
9577
+ var list = menu.findChild('.basemap-styles');
9578
+ var container = gui.container.findChild('.basemap-container');
9579
+ var basemapBtn = gui.container.findChild('.basemap-btn');
9580
+ var basemapMsg = gui.container.findChild('.basemap-error');
9581
+ var mapEl = gui.container.findChild('.basemap');
9582
+ var extentNote = El('div').addClass('basemap-note').appendTo(container).hide();
9583
+ var params = window.mapboxParams;
9584
+ var map;
9585
+ var activeStyle;
9586
+ var loading = false;
9587
+
9588
+ if (params) {
9589
+ init();
9590
+ } else {
9591
+ basemapBtn.hide();
9234
9592
  }
9235
9593
 
9236
- unfilteredArcs.getScaledArcs = function(ext) {
9237
- if (filteredArcs) {
9238
- // match simplification of unfiltered arcs
9239
- filteredArcs.setRetainedInterval(unfilteredArcs.getRetainedInterval());
9240
- }
9241
- // switch to filtered version of arcs at small scales
9242
- var unitsPerPixel = 1/ext.getTransform().mx,
9243
- useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5;
9244
- return useFiltering ? filteredArcs : unfilteredArcs;
9245
- };
9246
-
9247
- return unfilteredArcs;
9248
- }
9594
+ function init() {
9595
+ gui.addMode('basemap', turnOn, turnOff, basemapBtn);
9596
+ // model.on('select', function() {
9597
+ // TODO: hide basemap
9598
+ // if (gui.getMode() == 'basemap') gui.clearMode();
9599
+ // });
9249
9600
 
9250
- function getDisplayLayerForTable(table) {
9251
- var n = table.size(),
9252
- cellWidth = 12,
9253
- cellHeight = 5,
9254
- gutter = 6,
9255
- arcs = [],
9256
- shapes = [],
9257
- aspectRatio = 1.1,
9258
- x, y, col, row, blockSize;
9601
+ new SimpleButton(menu.findChild('.close-btn')).on('click', function() {
9602
+ gui.clearMode();
9603
+ turnOff();
9604
+ });
9259
9605
 
9260
- if (n > 10000) {
9261
- arcs = null;
9262
- gutter = 0;
9263
- cellWidth = 4;
9264
- cellHeight = 4;
9265
- aspectRatio = 1.45;
9266
- } else if (n > 5000) {
9267
- cellWidth = 5;
9268
- gutter = 3;
9269
- aspectRatio = 1.45;
9270
- } else if (n > 1000) {
9271
- gutter = 3;
9272
- cellWidth = 8;
9273
- aspectRatio = 1.3;
9274
- }
9606
+ gui.on('map_click', function() {
9607
+ // close menu if user click on the map
9608
+ if (gui.getMode() == 'basemap') gui.clearMode();
9609
+ });
9275
9610
 
9276
- if (n < 25) {
9277
- blockSize = n;
9278
- } else {
9279
- blockSize = Math.sqrt(n * (cellWidth + gutter) / cellHeight / aspectRatio) | 0;
9611
+ params.styles.forEach(function(style) {
9612
+ var btn = El('div').html(`<div class="basemap-style-btn"><img src="${style.icon}"></img></div><div class="basemap-style-label">${style.name}</div>`);
9613
+ btn.findChild('.basemap-style-btn').on('click', function() {
9614
+ updateStyle(style == activeStyle ? null : style);
9615
+ updateButtons();
9616
+ });
9617
+ btn.appendTo(list);
9618
+ });
9280
9619
  }
9281
9620
 
9282
- for (var i=0; i<n; i++) {
9283
- row = i % blockSize;
9284
- col = Math.floor(i / blockSize);
9285
- x = col * (cellWidth + gutter);
9286
- y = cellHeight * (blockSize - row);
9287
- if (arcs) {
9288
- arcs.push(getArc(x, y, cellWidth, cellHeight));
9289
- shapes.push([[i]]);
9621
+ function updateStyle(style) {
9622
+ activeStyle = style || null;
9623
+ if (!style) {
9624
+ gui.map.setDisplayCRS(null);
9625
+ hide();
9626
+ } else if (map) {
9627
+ map.setStyle(style.url);
9628
+ refresh();
9290
9629
  } else {
9291
- shapes.push([[x, y]]);
9630
+ initMap();
9292
9631
  }
9293
9632
  }
9294
9633
 
9295
- function getArc(x, y, w, h) {
9296
- return [[x, y], [x + w, y], [x + w, y - h], [x, y - h], [x, y]];
9297
- }
9298
-
9299
- return {
9300
- layer: {
9301
- geometry_type: arcs ? 'polygon' : 'point',
9302
- shapes: shapes,
9303
- data: table
9304
- },
9305
- arcs: arcs ? new internal.ArcCollection(arcs) : null
9306
- };
9307
- }
9308
-
9309
- // displayCRS: CRS to use for display, or null (which clears any current display CRS)
9310
- function projectDisplayLayer(lyr, displayCRS) {
9311
- var sourceCRS = internal.getDatasetCRS(lyr.source.dataset);
9312
- var lyr2;
9313
- if (!lyr.geographic || !sourceCRS) {
9314
- return lyr;
9315
- }
9316
- if (lyr.dynamic_crs && internal.crsAreEqual(sourceCRS, lyr.dynamic_crs)) {
9317
- return lyr;
9318
- }
9319
- lyr2 = getDisplayLayer(lyr.source.layer, lyr.source.dataset, {crs: displayCRS});
9320
- // kludge: copy projection-related properties to original layer
9321
- lyr.dynamic_crs = lyr2.dynamic_crs;
9322
- lyr.layer = lyr2.layer;
9323
- if (lyr.style && lyr.style.ids) {
9324
- // re-apply layer filter
9325
- lyr.layer = filterLayerByIds(lyr.layer, lyr.style.ids);
9634
+ function updateButtons() {
9635
+ list.findChildren('.basemap-style-btn').forEach(function(el, i) {
9636
+ el.classed('active', params.styles[i] == activeStyle);
9637
+ });
9326
9638
  }
9327
- lyr.bounds = lyr2.bounds;
9328
- lyr.arcs = lyr2.arcs;
9329
- }
9330
-
9331
-
9332
- // Wrap a layer in an object along with information needed for rendering
9333
- function getDisplayLayer(layer, dataset, opts) {
9334
- var obj = {
9335
- layer: null,
9336
- arcs: null,
9337
- // display_arcs: null,
9338
- style: null,
9339
- source: {
9340
- layer: layer,
9341
- dataset: dataset
9342
- },
9343
- empty: internal.getFeatureCount(layer) === 0
9344
- };
9345
-
9346
- var sourceCRS = opts.crs && internal.getDatasetCRS(dataset); // get src iff display CRS is given
9347
- var displayCRS = opts.crs || null;
9348
- var displayArcs = dataset.displayArcs;
9349
- var emptyArcs;
9350
9639
 
9351
- // Assume that dataset.displayArcs is in the display CRS
9352
- // (it should have been deleted upstream if reprojection is needed)
9353
- if (dataset.arcs && !displayArcs) {
9354
- // project arcs, if needed
9355
- if (needReprojectionForDisplay(sourceCRS, displayCRS)) {
9356
- displayArcs = projectArcsForDisplay(dataset.arcs, sourceCRS, displayCRS);
9357
- } else {
9358
- displayArcs = dataset.arcs;
9640
+ function turnOn() {
9641
+ var crs = gui.map.getDisplayCRS();
9642
+ if (!internal.isWebMercator(crs) && !internal.isWGS84(crs)) {
9643
+ basemapMsg.html('The current projection is not compatible.');
9359
9644
  }
9360
-
9361
- // init filtered arcs
9362
- dataset.displayArcs = new MultiScaleArcCollection(displayArcs);
9645
+ menu.show();
9363
9646
  }
9364
9647
 
9365
- if (internal.layerHasFurniture(layer)) {
9366
- obj.furniture = true;
9367
- obj.furniture_type = internal.getFurnitureLayerType(layer);
9368
- obj.layer = layer;
9369
- // treating furniture layers (other than frame) as tabular for now,
9370
- // so there is something to show if they are selected
9371
- obj.tabular = obj.furniture_type != 'frame';
9372
- } else if (obj.empty) {
9373
- obj.layer = {shapes: []}; // ideally we should avoid empty layers
9374
- } else if (!layer.geometry_type) {
9375
- obj.tabular = true;
9376
- } else {
9377
- obj.geographic = true;
9378
- obj.layer = layer;
9379
- obj.arcs = displayArcs;
9648
+ function turnOff() {
9649
+ basemapMsg.html('');
9650
+ menu.hide();
9380
9651
  }
9381
9652
 
9382
- if (obj.tabular) {
9383
- utils$1.extend(obj, getDisplayLayerForTable(layer.data));
9653
+ function enabled() {
9654
+ return !!(mapEl && params);
9384
9655
  }
9385
9656
 
9386
- // dynamic reprojection (arcs were already reprojected above)
9387
- if (obj.geographic && needReprojectionForDisplay(sourceCRS, displayCRS)) {
9388
- obj.dynamic_crs = displayCRS;
9389
- if (internal.layerHasPoints(layer)) {
9390
- obj.layer = projectPointsForDisplay(layer, sourceCRS, displayCRS);
9391
- } else if (internal.layerHasPaths(layer)) {
9392
- emptyArcs = findEmptyArcs(displayArcs);
9393
- if (emptyArcs.length > 0) {
9394
- // Don't try to draw paths containing coordinates that failed to project
9395
- obj.layer = internal.filterPathLayerByArcIds(obj.layer, emptyArcs);
9396
- }
9397
- }
9657
+ function show() {
9658
+ gui.container.addClass('basemap-on');
9659
+ mapEl.node().style.display = 'block';
9398
9660
  }
9399
9661
 
9400
- obj.bounds = getDisplayBounds(obj.layer, obj.arcs);
9401
- return obj;
9402
- }
9662
+ function hide() {
9663
+ gui.container.removeClass('basemap-on');
9664
+ mapEl.node().style.display = 'none';
9665
+ }
9403
9666
 
9667
+ function getBounds() {
9668
+ var bbox = ext.getBounds().toArray();
9669
+ var tr = getLonLat(bbox[2], bbox[3]);
9670
+ var bl = getLonLat(bbox[0], bbox[1]);
9671
+ return bl.concat(tr);
9672
+ }
9404
9673
 
9405
- function getDisplayBounds(lyr, arcs) {
9406
- var arcBounds = arcs ? arcs.getBounds() : new Bounds(),
9407
- bounds = arcBounds, // default display extent: all arcs in the dataset
9408
- lyrBounds;
9674
+ function getLonLat(x, y) {
9675
+ var R = 6378137;
9676
+ var R2D = 180 / Math.PI;
9677
+ var lon = x / R * R2D;
9678
+ var lat = R2D * (Math.PI * 0.5 - 2 * Math.atan(Math.exp(-y / R)));
9679
+ return [lon, lat];
9680
+ }
9409
9681
 
9410
- if (lyr.geometry_type == 'point') {
9411
- lyrBounds = internal.getLayerBounds(lyr);
9412
- if (lyrBounds && lyrBounds.hasBounds()) {
9413
- if (lyrBounds.area() > 0 || !arcBounds.hasBounds()) {
9414
- bounds = lyrBounds;
9415
- } else {
9416
- // if a point layer has no extent (e.g. contains only a single point),
9417
- // then merge with arc bounds, to place the point in context.
9418
- bounds = arcBounds.mergeBounds(lyrBounds);
9419
- }
9420
- }
9682
+ function initMap() {
9683
+ if (!enabled() || map || loading) return;
9684
+ loading = true;
9685
+ loadStylesheet(params.css);
9686
+ loadScript(params.js, function() {
9687
+ map = new window.mapboxgl.Map({
9688
+ accessToken: params.key,
9689
+ logoPosition: 'bottom-left',
9690
+ container: mapEl.node(),
9691
+ style: activeStyle.url,
9692
+ bounds: getBounds(),
9693
+ doubleClickZoom: false,
9694
+ dragPan: false,
9695
+ dragRotate: false,
9696
+ scrollZoom: false,
9697
+ interactive: false,
9698
+ keyboard: false,
9699
+ maxPitch: 0,
9700
+ renderWorldCopies: true // false // false prevents panning off the map
9701
+ });
9702
+ map.on('load', function() {
9703
+ loading = false;
9704
+ refresh();
9705
+ });
9706
+ });
9421
9707
  }
9422
9708
 
9423
- if (!bounds || !bounds.hasBounds()) { // empty layer
9424
- bounds = new Bounds();
9709
+ function checkBounds(bbox) {
9710
+ var msg;
9711
+ if (bbox[1] >= -85 && bbox[3] <= 85) {
9712
+ extentNote.hide();
9713
+ return true;
9714
+ }
9715
+ if (bbox[1] > 0) msg = 'pan south to see the basemap';
9716
+ else if (bbox[3] < 0) msg = 'pan north to see the basemap';
9717
+ else msg = msg = 'zoom in to see the basemap';
9718
+ extentNote.html(msg).show();
9719
+ return false;
9425
9720
  }
9426
- return bounds;
9427
- }
9428
9721
 
9429
- // Returns an array of ids of empty arcs (arcs can be set to empty if errors occur while projecting them)
9430
- function findEmptyArcs(arcs) {
9431
- var nn = arcs.getVertexData().nn;
9432
- var ids = [];
9433
- for (var i=0, n=nn.length; i<n; i++) {
9434
- if (nn[i] === 0) {
9435
- ids.push(i);
9722
+ function refresh() {
9723
+ if (!enabled() || !map || loading || !activeStyle) return;
9724
+ var crs = gui.map.getDisplayCRS();
9725
+ if (internal.isWGS84(crs)) {
9726
+ gui.map.setDisplayCRS(internal.getCRS('webmercator'));
9727
+ } else if (!internal.isWebMercator(crs)) {
9728
+ return;
9729
+ }
9730
+ var bbox = getBounds();
9731
+ if (!checkBounds(bbox)) {
9732
+ // map does not display outside these bounds
9733
+ hide();
9734
+ } else {
9735
+ show();
9736
+ map.resize();
9737
+ map.fitBounds(bbox, {animate: false});
9436
9738
  }
9437
9739
  }
9438
- return ids;
9740
+
9741
+ return {refresh: refresh}; // called by map when extent changes
9439
9742
  }
9440
9743
 
9441
9744
  utils$1.inherit(MshpMap, EventDispatcher);
@@ -9458,6 +9761,8 @@
9458
9761
  _inspector, _stack,
9459
9762
  _dynamicCRS;
9460
9763
 
9764
+ var _basemap = new Basemap(gui, _ext);
9765
+
9461
9766
  if (gui.options.showMouseCoordinates) {
9462
9767
  new CoordinatesDisplay(gui, _ext, _mouse);
9463
9768
  }
@@ -9482,6 +9787,8 @@
9482
9787
 
9483
9788
  model.on('update', onUpdate);
9484
9789
 
9790
+
9791
+
9485
9792
  // Update display of segment intersections
9486
9793
  this.setIntersectionLayer = function(lyr, dataset) {
9487
9794
  if (lyr == _intersectionLyr) return; // no change
@@ -9499,6 +9806,12 @@
9499
9806
  target.layer.pinned = !!pinned;
9500
9807
  };
9501
9808
 
9809
+ this.translatePixelCoords = function(x, y) {
9810
+ var p = _ext.translatePixelCoords(x, y);
9811
+ if (!_dynamicCRS) return p;
9812
+ return internal.toLngLat(p, _dynamicCRS);
9813
+ };
9814
+
9502
9815
  this.getCenterLngLat = function() {
9503
9816
  var bounds = _ext.getBounds();
9504
9817
  var crs = this.getDisplayCRS();
@@ -9520,6 +9833,7 @@
9520
9833
  this.getExtent = function() {return _ext;};
9521
9834
  this.isActiveLayer = isActiveLayer;
9522
9835
  this.isVisibleLayer = isVisibleLayer;
9836
+ this.getActiveLayer = function() { return _activeLyr; };
9523
9837
 
9524
9838
  // called by layer menu after layer visibility is updated
9525
9839
  this.redraw = function() {
@@ -9551,6 +9865,7 @@
9551
9865
 
9552
9866
  // Update map extent (also triggers redraw)
9553
9867
  projectMapExtent(_ext, oldCRS, this.getDisplayCRS(), getFullBounds());
9868
+ _fullBounds = getFullBounds(); // update this so map extent doesn't get reset after next update
9554
9869
  };
9555
9870
 
9556
9871
  // Refresh map display in response to data changes, layer selection, etc.
@@ -9604,6 +9919,7 @@
9604
9919
  needReset = mapNeedsReset(fullBounds, _fullBounds, _ext.getBounds(), e.flags);
9605
9920
  }
9606
9921
 
9922
+
9607
9923
  if (isFrameView()) {
9608
9924
  _nav.setZoomFactor(0.05); // slow zooming way down to allow fine-tuning frame placement // 0.03
9609
9925
  _ext.setFrame(getFullBounds()); // TODO: remove redundancy with drawLayers()
@@ -9643,6 +9959,7 @@
9643
9959
  }
9644
9960
 
9645
9961
  _ext.on('change', function(e) {
9962
+ if (_basemap) _basemap.refresh(); // keep basemap synced up (if enabled)
9646
9963
  if (e.reset) return; // don't need to redraw map here if extent has been reset
9647
9964
  if (isFrameView()) {
9648
9965
  updateFrameExtent();
@@ -10013,7 +10330,9 @@
10013
10330
  startEditing = function() {};
10014
10331
 
10015
10332
  window.addEventListener('beforeunload', function(e) {
10016
- if (gui.session.unsavedChanges()) {
10333
+ // don't prompt if there are no datasets (this means the last layer was deleted,
10334
+ // hitting the 'cancel' button would leave the interface in a bad state)
10335
+ if (gui.session.unsavedChanges() && !gui.model.isEmpty()) {
10017
10336
  e.returnValue = 'There are unsaved changes.';
10018
10337
  e.preventDefault();
10019
10338
  }