mapshaper 0.5.103 → 0.5.104

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,281 +1736,249 @@
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;
1739
+ function saveZipFile(zipfileName, files, done) {
1740
+ var zip = window.zip; // assumes zip library is loaded globally
1741
+ var toAdd = files;
1742
+ var zipWriter;
1743
+ try {
1744
+ zip.createWriter(new zip.BlobWriter("application/zip"), function(writer) {
1745
+ zipWriter = writer;
1746
+ nextFile();
1747
+ }, zipError);
1748
+ } catch(e) {
1749
+ done("This browser doesn't support Zip file creation.");
1766
1750
  }
1767
1751
 
1768
- unfilteredArcs.getScaledArcs = function(ext) {
1769
- if (filteredArcs) {
1770
- // match simplification of unfiltered arcs
1771
- filteredArcs.setRetainedInterval(unfilteredArcs.getRetainedInterval());
1752
+ function zipError(err) {
1753
+ var str = "Error creating Zip file";
1754
+ var msg = '';
1755
+ // error events thrown by Zip library seem to be missing a message
1756
+ if (err && err.message) {
1757
+ msg = err.message;
1772
1758
  }
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;
1759
+ if (msg) {
1760
+ str += ": " + msg;
1761
+ }
1762
+ done(str);
1810
1763
  }
1811
1764
 
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]]);
1765
+ function nextFile() {
1766
+ if (toAdd.length === 0) {
1767
+ zipWriter.close(function(blob) {
1768
+ saveBlobToDownloadFolder(zipfileName, blob, done);
1769
+ });
1820
1770
  } else {
1821
- shapes.push([[x, y]]);
1771
+ var obj = toAdd.pop(),
1772
+ blob = new Blob([obj.content]);
1773
+ zipWriter.add(obj.filename, new zip.BlobReader(blob), nextFile);
1822
1774
  }
1823
1775
  }
1776
+ }
1824
1777
 
1825
- function getArc(x, y, w, h) {
1826
- return [[x, y], [x + w, y], [x + w, y - h], [x, y - h], [x, y]];
1778
+ function saveFilesToServer(paths, data, done) {
1779
+ var i = -1;
1780
+ next();
1781
+ function next(err) {
1782
+ i++;
1783
+ if (err) return done(err);
1784
+ if (i >= data.length) return done();
1785
+ saveBlobToServer(paths[i], new Blob([data[i]]), next);
1827
1786
  }
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
1787
  }
1838
1788
 
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;
1789
+ function saveBlobToServer(path, blob, done) {
1790
+ var q = '?file=' + encodeURIComponent(path);
1791
+ var url = window.location.origin + '/save' + q;
1792
+ window.fetch(url, {
1793
+ method: 'POST',
1794
+ credentials: 'include',
1795
+ body: blob
1796
+ }).then(function(resp) {
1797
+ if (resp.status == 400) {
1798
+ return resp.text();
1799
+ }
1800
+ }).then(function(err) {
1801
+ done(err);
1802
+ }).catch(function(resp) {
1803
+ done('connection to server was lost');
1804
+ });
1849
1805
  }
1850
1806
 
1851
- function projectArcsForDisplay(arcs, src, dest) {
1852
- var copy = arcs.getCopy(); // need to flatten first?
1853
- var destIsWebMerc = internal.isWebMercator(dest);
1854
- if (destIsWebMerc && internal.isWebMercator(src)) {
1855
- return copy;
1807
+ function saveBlobToDownloadFolder(filename, blob, done) {
1808
+ var anchor, blobUrl;
1809
+ if (window.navigator.msSaveBlob) {
1810
+ window.navigator.msSaveBlob(blob, filename);
1811
+ return done();
1856
1812
  }
1857
-
1858
- var wgs84 = internal.getCRS('wgs84');
1859
- var toWGS84 = internal.getProjTransform2(src, wgs84);
1860
- var fromWGS84 = internal.getProjTransform2(wgs84, dest);
1861
-
1862
1813
  try {
1863
- // first try projectArcs() -- it's fast and preserves arc ids
1864
- // (so vertex editing doesn't break)
1865
- if (!internal.isWGS84(src)) {
1866
- // use wgs84 as a pivot CRS, so we can handle polar coordinates
1867
- // that can't be projected to Mercator
1868
- internal.projectArcs(copy, toWGS84);
1869
- }
1870
- if (destIsWebMerc) {
1871
- // handle polar points by clamping them to they will project
1872
- // (downside: may cause unexpected behavior when editing vertices interactively)
1873
- clampY(copy);
1874
- }
1875
- internal.projectArcs(copy, fromWGS84);
1814
+ blobUrl = URL.createObjectURL(blob);
1876
1815
  } catch(e) {
1877
- console.error(e);
1878
- // use the more robust projectArcs2 if projectArcs throws an error
1879
- // downside: projectArcs2 discards Z values and changes arc indexing,
1880
- // which will break vertex editing.
1881
- var reproject = internal.getProjTransform2(src, dest);
1882
- copy = arcs.getCopy();
1883
- internal.projectArcs2(copy, reproject);
1816
+ done("Mapshaper can't export files from this browser. Try switching to Chrome or Firefox.");
1817
+ return;
1884
1818
  }
1885
- return copy;
1819
+ anchor = El('a').attr('href', '#').appendTo('body').node();
1820
+ anchor.href = blobUrl;
1821
+ anchor.download = filename;
1822
+ var clickEvent = document.createEvent("MouseEvent");
1823
+ clickEvent.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false,
1824
+ false, false, false, 0, null);
1825
+ anchor.dispatchEvent(clickEvent);
1826
+ setTimeout(function() {
1827
+ // Revoke blob url to release memory; timeout needed in firefox
1828
+ URL.revokeObjectURL(blobUrl);
1829
+ anchor.parentNode.removeChild(anchor);
1830
+ done();
1831
+ }, 400);
1886
1832
  }
1887
1833
 
1888
- function clampY(arcs) {
1889
- var max = 89.9,
1890
- min = -89.9,
1891
- bbox = arcs.getBounds().toArray();
1892
- if (bbox[1] >= min && bbox[3] <= max) return;
1893
- arcs.transformPoints(function(x, y) {
1894
- if (y > max) return [x, max];
1895
- if (y < min) return [x, min];
1896
- });
1897
- }
1834
+ // replace default error, stop and message functions
1835
+ function setLoggingForGUI(gui) {
1836
+ function stop() {
1837
+ // Show a popup error message, then throw an error
1838
+ var msg = GUI.formatMessageArgs(arguments);
1839
+ gui.alert(msg);
1840
+ throw new Error(msg);
1841
+ }
1898
1842
 
1899
- function projectPointsForDisplay(lyr, src, dest) {
1900
- var copy = utils$1.extend({}, lyr);
1901
- var proj = internal.getProjTransform2(src, dest);
1902
- copy.shapes = internal.cloneShapes(lyr.shapes);
1903
- internal.projectPointLayer(copy, proj);
1904
- return copy;
1905
- }
1843
+ function error() {
1844
+ stop.apply(null, utils$1.toArray(arguments));
1845
+ }
1906
1846
 
1907
- function toWebMercator(lng, lat) {
1908
- var R = 6378137;
1909
- var D2R = Math.PI / 180;
1910
- var k = Math.cos(lat * D2R);
1911
- var x = R * lng * D2R;
1912
- var y = R * Math.log(Math.tan(Math.PI * 0.25 + lat * D2R * 0.5));
1913
- return [x, y];
1914
- }
1847
+ function message() {
1848
+ internal.logArgs(arguments);
1849
+ }
1915
1850
 
1916
- function fromWebMercator(x, y) {
1917
- var R = 6378137;
1918
- var R2D = 180 / Math.PI;
1919
- var lon = x / R * R2D;
1920
- var lat = R2D * (Math.PI * 0.5 - 2 * Math.atan(Math.exp(-y / R)));
1921
- return [lon, lat];
1851
+ internal.setLoggingFunctions(message, error, stop);
1922
1852
  }
1923
1853
 
1924
- function clampToMapboxBounds(bounds) {
1925
- var ymax = toWebMercator(0, 84)[1];
1926
- var ymin = toWebMercator(0, -84)[1];
1927
- var hmin = 1000;
1928
- bounds.ymin = Math.max(bounds.ymin, ymin);
1929
- bounds.ymax = Math.max(bounds.ymax, ymin + hmin);
1930
- bounds.ymin = Math.min(bounds.ymin, ymax - hmin);
1931
- bounds.ymax = Math.min(bounds.ymax, ymax);
1854
+ function WriteFilesProxy(gui) {
1855
+ // replace CLI version of writeFiles()
1856
+ internal.replaceWriteFiles(function(files, opts, done) {
1857
+ var filename;
1858
+ if (!utils$1.isArray(files) || files.length === 0) {
1859
+ done("Nothing to export");
1860
+ } else if (GUI.canSaveToServer() && !opts.save_to_download_folder) {
1861
+ var paths = internal.getOutputPaths(utils$1.pluck(files, 'filename'), opts);
1862
+ var data = utils$1.pluck(files, 'content');
1863
+ saveFilesToServer(paths, data, function(err) {
1864
+ var msg;
1865
+ if (err) {
1866
+ msg = "<b>Direct save failed</b><br>Reason: " + err + ".";
1867
+ msg += "<br>Saving to download folder instead.";
1868
+ gui.alert(msg);
1869
+ // fall back to standard method if saving to server fails
1870
+ internal.writeFiles(files, {save_to_download_folder: true}, done);
1871
+ } else {
1872
+ if (files.length >= 1) {
1873
+ gui.alert('<b>Saved</b><br>' + paths.join('<br>'));
1874
+ }
1875
+ done();
1876
+ }
1877
+ });
1878
+ } else if (files.length == 1) {
1879
+ saveBlobToDownloadFolder(files[0].filename, new Blob([files[0].content]), done);
1880
+ } else {
1881
+ filename = internal.getCommonFileBase(utils$1.pluck(files, 'filename')) || "output";
1882
+ saveZipFile(filename + ".zip", files, done);
1883
+ }
1884
+ });
1932
1885
  }
1933
1886
 
1887
+ // Replaces functions for reading from files with functions that try to match
1888
+ // already-loaded datasets.
1889
+ //
1890
+ function ImportFileProxy(gui) {
1891
+ var model = gui.model;
1934
1892
 
1935
- // Update map extent and trigger redraw, after a new display CRS has been applied
1936
- function projectMapExtent(ext, src, dest, newBounds) {
1937
- var oldBounds = ext.getBounds();
1938
- var oldScale = ext.scale();
1939
- var newCP, proj;
1940
-
1941
- if (dest && internal.isWebMercator(dest)) {
1942
- clampToMapboxBounds(newBounds);
1893
+ // Try to match an imported dataset or layer.
1894
+ // TODO: think about handling import options
1895
+ function find(src) {
1896
+ var datasets = model.getDatasets();
1897
+ var retn = datasets.reduce(function(memo, d) {
1898
+ var lyr;
1899
+ if (memo) return memo; // already found a match
1900
+ // try to match import filename of this dataset
1901
+ if (d.info.input_files[0] == src) return d;
1902
+ // try to match name of a layer in this dataset
1903
+ lyr = utils$1.find(d.layers, function(lyr) {return lyr.name == src;});
1904
+ return lyr ? internal.isolateLayer(lyr, d) : null;
1905
+ }, null);
1906
+ if (!retn) stop$1("Missing data layer [" + src + "]");
1907
+ return retn;
1943
1908
  }
1944
1909
 
1945
- // if source or destination CRS is unknown, show full extent
1946
- // if map is at full extent, show full extent
1947
- // TODO: handle case that scale is 1 and map is panned away from center
1948
- if (ext.scale() == 1 || !dest) {
1949
- ext.setBounds(newBounds);
1950
- ext.home(); // sets full extent and triggers redraw
1951
- } else {
1952
- // if map is zoomed, stay centered on the same geographic location, at the same relative scale
1953
- proj = internal.getProjTransform2(src, dest);
1954
- newCP = proj(oldBounds.centerX(), oldBounds.centerY());
1955
- ext.setBounds(newBounds);
1956
- if (!newCP) {
1957
- // projection of center point failed; use center of bounds
1958
- // (also consider just resetting the view using ext.home())
1959
- newCP = [newBounds.centerX(), newBounds.centerY()];
1960
- }
1961
- ext.recenter(newCP[0], newCP[1], oldScale);
1962
- }
1910
+ internal.replaceImportFile(function(src, opts) {
1911
+ var dataset = find(src);
1912
+ // Return a copy with layers duplicated, so changes won't affect original layers
1913
+ // This makes an (unsafe) assumption that the dataset arcs won't be changed...
1914
+ // need to rethink this.
1915
+ return utils$1.defaults({
1916
+ layers: dataset.layers.map(internal.copyLayer)
1917
+ }, dataset);
1918
+ });
1963
1919
  }
1964
1920
 
1965
- // Called from console; for testing dynamic crs
1966
- function setDisplayProjection(gui, cmd) {
1967
- var arg = cmd.replace(/^projd[ ]*/, '');
1968
- if (arg) {
1969
- gui.map.setDisplayCRS(internal.getCRS(arg));
1970
- } else {
1971
- gui.map.setDisplayCRS(null);
1972
- }
1973
- }
1921
+ // load Proj.4 CRS definition files dynamically
1922
+ //
1923
+ internal.setProjectionLoader(function(opts, done) {
1924
+ var mproj = require('mproj');
1925
+ var libs = internal.findProjLibs([opts.init || '', opts.match || '', opts.crs || ''].join(' '));
1926
+ // skip loaded libs
1927
+ libs = libs.filter(function(name) {return !mproj.internal.mproj_search_libcache(name);});
1928
+ loadProjLibs(libs, done);
1929
+ });
1930
+
1931
+ function loadProjLibs(libs, done) {
1932
+ var mproj = require('mproj');
1933
+ var i = 0;
1934
+ next();
1974
1935
 
1975
- function filterLayerByIds(lyr, ids) {
1976
- var shapes;
1977
- if (lyr.shapes) {
1978
- shapes = ids.map(function(id) {
1979
- return lyr.shapes[id];
1936
+ function next() {
1937
+ var libName = libs[i];
1938
+ var content, req;
1939
+ if (!libName) return done();
1940
+ req = new XMLHttpRequest();
1941
+ req.addEventListener('load', function(e) {
1942
+ if (req.status == 200) {
1943
+ content = req.response;
1944
+ }
1980
1945
  });
1981
- return utils$1.defaults({shapes: shapes, data: null}, lyr);
1946
+ req.addEventListener('loadend', function() {
1947
+ if (content) {
1948
+ mproj.internal.mproj_insert_libcache(libName, content);
1949
+ }
1950
+ // TODO: consider stopping with an error message if no content was loaded
1951
+ // (currently, a less specific error will occur when mapshaper tries to use the library)
1952
+ next();
1953
+ });
1954
+ req.open('GET', 'assets/' + libName);
1955
+ req.send();
1956
+ i++;
1982
1957
  }
1983
- return lyr;
1984
- }
1985
-
1986
- function formatLayerNameForDisplay(name) {
1987
- return name || '[unnamed]';
1988
- }
1989
-
1990
- function cleanLayerName(raw) {
1991
- return raw.replace(/[\n\t/\\]/g, '')
1992
- .replace(/^[\.\s]+/, '').replace(/[\.\s]+$/, '');
1993
1958
  }
1994
1959
 
1995
- function updateLayerStackOrder(layers) {
1996
- // 1. assign ascending ids to unassigned layers above the range of other layers
1997
- layers.forEach(function(o, i) {
1998
- if (!o.layer.stack_id) o.layer.stack_id = 1e6 + i;
1999
- });
2000
- // 2. sort in ascending order
2001
- layers.sort(function(a, b) {
2002
- return a.layer.stack_id - b.layer.stack_id;
2003
- });
2004
- // 3. assign consecutve ids
2005
- layers.forEach(function(o, i) {
2006
- o.layer.stack_id = i + 1;
2007
- });
2008
- return layers;
1960
+ function getDatasetCrsInfo(dataset) {
1961
+ var revertLogging = internal.getLoggingSetter();
1962
+ var crs, err;
1963
+ // prevent GUI message popup on error
1964
+ internal.setLoggingForCLI();
1965
+ try {
1966
+ crs = internal.getDatasetCRS(dataset);
1967
+ } catch(e) {
1968
+ err = e.message;
1969
+ }
1970
+ revertLogging();
1971
+ return {
1972
+ crs: crs,
1973
+ error: err
1974
+ };
2009
1975
  }
2010
1976
 
2011
- function sortLayersForMenuDisplay(layers) {
2012
- layers = updateLayerStackOrder(layers);
2013
- return layers.reverse();
1977
+ function flattenArcs(lyr) {
1978
+ lyr.source.dataset.arcs.flatten();
1979
+ if (isProjectedLayer(lyr)) {
1980
+ lyr.arcs.flatten();
1981
+ }
2014
1982
  }
2015
1983
 
2016
1984
  function setZ(lyr, z) {
@@ -2106,146 +2074,6 @@
2106
2074
  return dest.length ? dest : null;
2107
2075
  }
2108
2076
 
2109
- // displayCRS: CRS to use for display, or null (which clears any current display CRS)
2110
- function projectDisplayLayer(lyr, displayCRS) {
2111
- var sourceCRS = internal.getDatasetCRS(lyr.source.dataset);
2112
- var lyr2;
2113
- if (!lyr.geographic || !sourceCRS) {
2114
- return lyr;
2115
- }
2116
- if (lyr.dynamic_crs && internal.crsAreEqual(sourceCRS, lyr.dynamic_crs)) {
2117
- return lyr;
2118
- }
2119
- lyr2 = getDisplayLayer(lyr.source.layer, lyr.source.dataset, {crs: displayCRS});
2120
- // kludge: copy projection-related properties to original layer
2121
- lyr.dynamic_crs = lyr2.dynamic_crs;
2122
- lyr.layer = lyr2.layer;
2123
- if (lyr.style && lyr.style.ids) {
2124
- // re-apply layer filter
2125
- lyr.layer = filterLayerByIds(lyr.layer, lyr.style.ids);
2126
- }
2127
- lyr.invertPoint = lyr2.invertPoint;
2128
- lyr.projectPoint = lyr2.projectPoint;
2129
- lyr.bounds = lyr2.bounds;
2130
- lyr.arcs = lyr2.arcs;
2131
- }
2132
-
2133
-
2134
- // Wrap a layer in an object along with information needed for rendering
2135
- function getDisplayLayer(layer, dataset, opts) {
2136
- var obj = {
2137
- layer: null,
2138
- arcs: null,
2139
- // display_arcs: null,
2140
- style: null,
2141
- invertPoint: null,
2142
- projectPoint: null,
2143
- source: {
2144
- layer: layer,
2145
- dataset: dataset
2146
- },
2147
- empty: internal.getFeatureCount(layer) === 0
2148
- };
2149
-
2150
- var sourceCRS = opts.crs && internal.getDatasetCRS(dataset); // get src iff display CRS is given
2151
- var displayCRS = opts.crs || null;
2152
- // display arcs may have been generated when another layer in the dataset was converted for display... re-use if available
2153
- var displayArcs = dataset.displayArcs || null;
2154
- var emptyArcs;
2155
-
2156
- // Assume that dataset.displayArcs is in the display CRS
2157
- // (it must be deleted upstream if reprojection is needed)
2158
- if (dataset.arcs && !displayArcs) {
2159
- // project arcs, if needed
2160
- if (needReprojectionForDisplay(sourceCRS, displayCRS)) {
2161
- displayArcs = projectArcsForDisplay(dataset.arcs, sourceCRS, displayCRS);
2162
- } else {
2163
- // Use original arcs for display if there is no dynamic reprojection
2164
- displayArcs = dataset.arcs;
2165
- }
2166
-
2167
- enhanceArcCollectionForDisplay(displayArcs);
2168
- dataset.displayArcs = displayArcs; // stash these in the dataset for other layers to use
2169
- }
2170
-
2171
- if (internal.layerHasFurniture(layer)) {
2172
- obj.furniture = true;
2173
- obj.furniture_type = internal.getFurnitureLayerType(layer);
2174
- obj.layer = layer;
2175
- // treating furniture layers (other than frame) as tabular for now,
2176
- // so there is something to show if they are selected
2177
- obj.tabular = obj.furniture_type != 'frame';
2178
- } else if (obj.empty) {
2179
- obj.layer = {shapes: []}; // ideally we should avoid empty layers
2180
- } else if (!layer.geometry_type) {
2181
- obj.tabular = true;
2182
- } else {
2183
- obj.geographic = true;
2184
- obj.layer = layer;
2185
- obj.arcs = displayArcs;
2186
- }
2187
-
2188
- if (obj.tabular) {
2189
- utils$1.extend(obj, getDisplayLayerForTable(layer.data));
2190
- }
2191
-
2192
- // dynamic reprojection (arcs were already reprojected above)
2193
- if (obj.geographic && needReprojectionForDisplay(sourceCRS, displayCRS)) {
2194
- obj.dynamic_crs = displayCRS;
2195
- obj.invertPoint = internal.getProjTransform2(displayCRS, sourceCRS);
2196
- obj.projectPoint = internal.getProjTransform2(sourceCRS, displayCRS);
2197
- if (internal.layerHasPoints(layer)) {
2198
- obj.layer = projectPointsForDisplay(layer, sourceCRS, displayCRS);
2199
- } else if (internal.layerHasPaths(layer)) {
2200
- emptyArcs = findEmptyArcs(displayArcs);
2201
- if (emptyArcs.length > 0) {
2202
- // Don't try to draw paths containing coordinates that failed to project
2203
- obj.layer = internal.filterPathLayerByArcIds(obj.layer, emptyArcs);
2204
- }
2205
- }
2206
- }
2207
-
2208
- obj.bounds = getDisplayBounds(obj.layer, obj.arcs);
2209
- return obj;
2210
- }
2211
-
2212
-
2213
- function getDisplayBounds(lyr, arcs) {
2214
- var arcBounds = arcs ? arcs.getBounds() : new Bounds(),
2215
- bounds = arcBounds, // default display extent: all arcs in the dataset
2216
- lyrBounds;
2217
-
2218
- if (lyr.geometry_type == 'point') {
2219
- lyrBounds = internal.getLayerBounds(lyr);
2220
- if (lyrBounds && lyrBounds.hasBounds()) {
2221
- if (lyrBounds.area() > 0 || !arcBounds.hasBounds()) {
2222
- bounds = lyrBounds;
2223
- } else {
2224
- // if a point layer has no extent (e.g. contains only a single point),
2225
- // then merge with arc bounds, to place the point in context.
2226
- bounds = arcBounds.mergeBounds(lyrBounds);
2227
- }
2228
- }
2229
- }
2230
-
2231
- if (!bounds || !bounds.hasBounds()) { // empty layer
2232
- bounds = new Bounds();
2233
- }
2234
- return bounds;
2235
- }
2236
-
2237
- // Returns an array of ids of empty arcs (arcs can be set to empty if errors occur while projecting them)
2238
- function findEmptyArcs(arcs) {
2239
- var nn = arcs.getVertexData().nn;
2240
- var ids = [];
2241
- for (var i=0, n=nn.length; i<n; i++) {
2242
- if (nn[i] === 0) {
2243
- ids.push(i);
2244
- }
2245
- }
2246
- return ids;
2247
- }
2248
-
2249
2077
  /*
2250
2078
  How changes in the simplify control should affect other components
2251
2079
 
@@ -2481,225 +2309,141 @@
2481
2309
  }
2482
2310
  };
2483
2311
 
2484
- function saveZipFile(zipfileName, files, done) {
2485
- var zip = window.zip; // assumes zip library is loaded globally
2486
- var toAdd = files;
2487
- var zipWriter;
2488
- try {
2489
- zip.createWriter(new zip.BlobWriter("application/zip"), function(writer) {
2490
- zipWriter = writer;
2491
- nextFile();
2492
- }, zipError);
2493
- } catch(e) {
2494
- done("This browser doesn't support Zip file creation.");
2312
+ var R = 6378137;
2313
+ var D2R = Math.PI / 180;
2314
+ var R2D = 180 / Math.PI;
2315
+
2316
+ // Assumes projections are available
2317
+
2318
+ function needReprojectionForDisplay(sourceCRS, displayCRS) {
2319
+ if (!sourceCRS || !displayCRS) {
2320
+ return false;
2321
+ }
2322
+ if (internal.crsAreEqual(sourceCRS, displayCRS)) {
2323
+ return false;
2495
2324
  }
2325
+ return true;
2326
+ }
2496
2327
 
2497
- function zipError(err) {
2498
- var str = "Error creating Zip file";
2499
- var msg = '';
2500
- // error events thrown by Zip library seem to be missing a message
2501
- if (err && err.message) {
2502
- msg = err.message;
2503
- }
2504
- if (msg) {
2505
- str += ": " + msg;
2506
- }
2507
- done(str);
2328
+ function projectArcsForDisplay(arcs, src, dest) {
2329
+ var copy = arcs.getCopy(); // need to flatten first?
2330
+ var destIsWebMerc = internal.isWebMercator(dest);
2331
+ if (destIsWebMerc && internal.isWebMercator(src)) {
2332
+ return copy;
2508
2333
  }
2509
2334
 
2510
- function nextFile() {
2511
- if (toAdd.length === 0) {
2512
- zipWriter.close(function(blob) {
2513
- saveBlobToDownloadFolder(zipfileName, blob, done);
2514
- });
2515
- } else {
2516
- var obj = toAdd.pop(),
2517
- blob = new Blob([obj.content]);
2518
- zipWriter.add(obj.filename, new zip.BlobReader(blob), nextFile);
2335
+ var wgs84 = internal.getCRS('wgs84');
2336
+ var toWGS84 = internal.getProjTransform2(src, wgs84);
2337
+ var fromWGS84 = internal.getProjTransform2(wgs84, dest);
2338
+
2339
+ try {
2340
+ // first try projectArcs() -- it's fast and preserves arc ids
2341
+ // (so vertex editing doesn't break)
2342
+ if (!internal.isWGS84(src)) {
2343
+ // use wgs84 as a pivot CRS, so we can handle polar coordinates
2344
+ // that can't be projected to Mercator
2345
+ internal.projectArcs(copy, toWGS84);
2519
2346
  }
2347
+ if (destIsWebMerc) {
2348
+ // handle polar points by clamping them to they will project
2349
+ // (downside: may cause unexpected behavior when editing vertices interactively)
2350
+ clampY(copy);
2351
+ }
2352
+ internal.projectArcs(copy, fromWGS84);
2353
+ } catch(e) {
2354
+ console.error(e);
2355
+ // use the more robust projectArcs2 if projectArcs throws an error
2356
+ // downside: projectArcs2 discards Z values and changes arc indexing,
2357
+ // which will break vertex editing.
2358
+ var reproject = internal.getProjTransform2(src, dest);
2359
+ copy = arcs.getCopy();
2360
+ internal.projectArcs2(copy, reproject);
2520
2361
  }
2362
+ return copy;
2521
2363
  }
2522
2364
 
2523
- function saveFilesToServer(paths, data, done) {
2524
- var i = -1;
2525
- next();
2526
- function next(err) {
2527
- i++;
2528
- if (err) return done(err);
2529
- if (i >= data.length) return done();
2530
- saveBlobToServer(paths[i], new Blob([data[i]]), next);
2531
- }
2532
- }
2533
-
2534
- function saveBlobToServer(path, blob, done) {
2535
- var q = '?file=' + encodeURIComponent(path);
2536
- var url = window.location.origin + '/save' + q;
2537
- window.fetch(url, {
2538
- method: 'POST',
2539
- credentials: 'include',
2540
- body: blob
2541
- }).then(function(resp) {
2542
- if (resp.status == 400) {
2543
- return resp.text();
2544
- }
2545
- }).then(function(err) {
2546
- done(err);
2547
- }).catch(function(resp) {
2548
- done('connection to server was lost');
2365
+ function clampY(arcs) {
2366
+ var max = 89.9,
2367
+ min = -89.9,
2368
+ bbox = arcs.getBounds().toArray();
2369
+ if (bbox[1] >= min && bbox[3] <= max) return;
2370
+ arcs.transformPoints(function(x, y) {
2371
+ if (y > max) return [x, max];
2372
+ if (y < min) return [x, min];
2549
2373
  });
2550
2374
  }
2551
2375
 
2552
- function saveBlobToDownloadFolder(filename, blob, done) {
2553
- var anchor, blobUrl;
2554
- if (window.navigator.msSaveBlob) {
2555
- window.navigator.msSaveBlob(blob, filename);
2556
- return done();
2557
- }
2558
- try {
2559
- blobUrl = URL.createObjectURL(blob);
2560
- } catch(e) {
2561
- done("Mapshaper can't export files from this browser. Try switching to Chrome or Firefox.");
2562
- return;
2563
- }
2564
- anchor = El('a').attr('href', '#').appendTo('body').node();
2565
- anchor.href = blobUrl;
2566
- anchor.download = filename;
2567
- var clickEvent = document.createEvent("MouseEvent");
2568
- clickEvent.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false,
2569
- false, false, false, 0, null);
2570
- anchor.dispatchEvent(clickEvent);
2571
- setTimeout(function() {
2572
- // Revoke blob url to release memory; timeout needed in firefox
2573
- URL.revokeObjectURL(blobUrl);
2574
- anchor.parentNode.removeChild(anchor);
2575
- done();
2576
- }, 400);
2376
+ function projectPointsForDisplay(lyr, src, dest) {
2377
+ var copy = utils$1.extend({}, lyr);
2378
+ var proj = internal.getProjTransform2(src, dest);
2379
+ copy.shapes = internal.cloneShapes(lyr.shapes);
2380
+ internal.projectPointLayer(copy, proj);
2381
+ return copy;
2577
2382
  }
2578
2383
 
2579
- function MessageProxy(gui) {
2580
- // replace stop function
2581
- var stop = function() {
2582
- // Show a popup error message, then throw an error
2583
- var msg = GUI.formatMessageArgs(arguments);
2584
- gui.alert(msg);
2585
- throw new Error(msg);
2586
- };
2587
2384
 
2588
- // Replace error function in mapshaper lib
2589
- var error = function() {
2590
- stop.apply(null, utils$1.toArray(arguments));
2591
- };
2385
+ function toWebMercator(lng, lat) {
2386
+ var k = Math.cos(lat * D2R);
2387
+ var x = R * lng * D2R;
2388
+ var y = R * Math.log(Math.tan(Math.PI * 0.25 + lat * D2R * 0.5));
2389
+ return [x, y];
2390
+ }
2592
2391
 
2593
- var message = function() {
2594
- internal.logArgs(arguments); // reset default
2595
- };
2392
+ function fromWebMercator(x, y) {
2393
+ var lon = x / R * R2D;
2394
+ var lat = R2D * (Math.PI * 0.5 - 2 * Math.atan(Math.exp(-y / R)));
2395
+ return [lon, lat];
2396
+ }
2596
2397
 
2597
- internal.setLoggingFunctions(message, error, stop);
2398
+ function scaleToZoom(metersPerPix) {
2399
+ return Math.log(40075017 / 512 / metersPerPix) / Math.log(2);
2598
2400
  }
2599
2401
 
2600
- function WriteFilesProxy(gui) {
2601
- // replace CLI version of writeFiles()
2602
- internal.replaceWriteFiles(function(files, opts, done) {
2603
- var filename;
2604
- if (!utils$1.isArray(files) || files.length === 0) {
2605
- done("Nothing to export");
2606
- } else if (GUI.canSaveToServer() && !opts.save_to_download_folder) {
2607
- var paths = internal.getOutputPaths(utils$1.pluck(files, 'filename'), opts);
2608
- var data = utils$1.pluck(files, 'content');
2609
- saveFilesToServer(paths, data, function(err) {
2610
- var msg;
2611
- if (err) {
2612
- msg = "<b>Direct save failed</b><br>Reason: " + err + ".";
2613
- msg += "<br>Saving to download folder instead.";
2614
- gui.alert(msg);
2615
- // fall back to standard method if saving to server fails
2616
- internal.writeFiles(files, {save_to_download_folder: true}, done);
2617
- } else {
2618
- if (files.length >= 1) {
2619
- gui.alert('<b>Saved</b><br>' + paths.join('<br>'));
2620
- }
2621
- done();
2622
- }
2623
- });
2624
- } else if (files.length == 1) {
2625
- saveBlobToDownloadFolder(files[0].filename, new Blob([files[0].content]), done);
2626
- } else {
2627
- filename = internal.getCommonFileBase(utils$1.pluck(files, 'filename')) || "output";
2628
- saveZipFile(filename + ".zip", files, done);
2629
- }
2630
- });
2402
+ function getMapboxBounds() {
2403
+ var ymax = toWebMercator(0, 84)[1];
2404
+ var ymin = toWebMercator(0, -84)[1];
2405
+ return [-Infinity, ymin, Infinity, ymax];
2631
2406
  }
2632
2407
 
2633
- // Replaces functions for reading from files with functions that try to match
2634
- // already-loaded datasets.
2635
- //
2636
- function ImportFileProxy(gui) {
2637
- var model = gui.model;
2638
2408
 
2639
- // Try to match an imported dataset or layer.
2640
- // TODO: think about handling import options
2641
- function find(src) {
2642
- var datasets = model.getDatasets();
2643
- var retn = datasets.reduce(function(memo, d) {
2644
- var lyr;
2645
- if (memo) return memo; // already found a match
2646
- // try to match import filename of this dataset
2647
- if (d.info.input_files[0] == src) return d;
2648
- // try to match name of a layer in this dataset
2649
- lyr = utils$1.find(d.layers, function(lyr) {return lyr.name == src;});
2650
- return lyr ? internal.isolateLayer(lyr, d) : null;
2651
- }, null);
2652
- if (!retn) stop$1("Missing data layer [" + src + "]");
2653
- return retn;
2409
+ // Update map extent and trigger redraw, after a new display CRS has been applied
2410
+ function projectMapExtent(ext, src, dest, newBounds) {
2411
+ var oldBounds = ext.getBounds();
2412
+ var oldScale = ext.scale();
2413
+ var newCP, proj, strictBounds;
2414
+
2415
+ if (dest && internal.isWebMercator(dest)) {
2416
+ // clampToMapboxBounds(newBounds);
2417
+ strictBounds = getMapboxBounds();
2654
2418
  }
2655
2419
 
2656
- internal.replaceImportFile(function(src, opts) {
2657
- var dataset = find(src);
2658
- // Return a copy with layers duplicated, so changes won't affect original layers
2659
- // This makes an (unsafe) assumption that the dataset arcs won't be changed...
2660
- // need to rethink this.
2661
- return utils$1.defaults({
2662
- layers: dataset.layers.map(internal.copyLayer)
2663
- }, dataset);
2664
- });
2420
+ // if source or destination CRS is unknown, show full extent
2421
+ // if map is at full extent, show full extent
2422
+ // TODO: handle case that scale is 1 and map is panned away from center
2423
+ if (ext.scale() == 1 || !dest) {
2424
+ ext.setBounds(newBounds, strictBounds);
2425
+ ext.home(); // sets full extent and triggers redraw
2426
+ } else {
2427
+ // if map is zoomed, stay centered on the same geographic location, at the same relative scale
2428
+ proj = internal.getProjTransform2(src, dest);
2429
+ newCP = proj(oldBounds.centerX(), oldBounds.centerY());
2430
+ ext.setBounds(newBounds, strictBounds);
2431
+ if (!newCP) {
2432
+ // projection of center point failed; use center of bounds
2433
+ // (also consider just resetting the view using ext.home())
2434
+ newCP = [newBounds.centerX(), newBounds.centerY()];
2435
+ }
2436
+ ext.recenter(newCP[0], newCP[1], oldScale);
2437
+ }
2665
2438
  }
2666
2439
 
2667
- // load Proj.4 CRS definition files dynamically
2668
- //
2669
- internal.setProjectionLoader(function(opts, done) {
2670
- var mproj = require('mproj');
2671
- var libs = internal.findProjLibs([opts.init || '', opts.match || '', opts.crs || ''].join(' '));
2672
- // skip loaded libs
2673
- libs = libs.filter(function(name) {return !mproj.internal.mproj_search_libcache(name);});
2674
- loadProjLibs(libs, done);
2675
- });
2676
-
2677
- function loadProjLibs(libs, done) {
2678
- var mproj = require('mproj');
2679
- var i = 0;
2680
- next();
2681
-
2682
- function next() {
2683
- var libName = libs[i];
2684
- var content, req;
2685
- if (!libName) return done();
2686
- req = new XMLHttpRequest();
2687
- req.addEventListener('load', function(e) {
2688
- if (req.status == 200) {
2689
- content = req.response;
2690
- }
2691
- });
2692
- req.addEventListener('loadend', function() {
2693
- if (content) {
2694
- mproj.internal.mproj_insert_libcache(libName, content);
2695
- }
2696
- // TODO: consider stopping with an error message if no content was loaded
2697
- // (currently, a less specific error will occur when mapshaper tries to use the library)
2698
- next();
2699
- });
2700
- req.open('GET', 'assets/' + libName);
2701
- req.send();
2702
- i++;
2440
+ // Called from console; for testing dynamic crs
2441
+ function setDisplayProjection(gui, cmd) {
2442
+ var arg = cmd.replace(/^projd[ ]*/, '');
2443
+ if (arg) {
2444
+ gui.map.setDisplayCRS(internal.getCRS(arg));
2445
+ } else {
2446
+ gui.map.setDisplayCRS(null);
2703
2447
  }
2704
2448
  }
2705
2449
 
@@ -2799,7 +2543,7 @@
2799
2543
  btn.removeClass('active');
2800
2544
  _isOpen = false;
2801
2545
  if (GUI.isActiveInstance(gui)) {
2802
- MessageProxy(gui); // reset stop, message and error functions
2546
+ setLoggingForGUI(gui); // reset stop, message and error functions
2803
2547
  }
2804
2548
  el.hide();
2805
2549
  input.node().blur();
@@ -3341,7 +3085,48 @@
3341
3085
  }
3342
3086
  }
3343
3087
 
3344
- utils$1.inherit(RepairControl, EventDispatcher);
3088
+ utils$1.inherit(RepairControl, EventDispatcher);
3089
+
3090
+ function filterLayerByIds(lyr, ids) {
3091
+ var shapes;
3092
+ if (lyr.shapes) {
3093
+ shapes = ids.map(function(id) {
3094
+ return lyr.shapes[id];
3095
+ });
3096
+ return utils$1.defaults({shapes: shapes, data: null}, lyr);
3097
+ }
3098
+ return lyr;
3099
+ }
3100
+
3101
+ function formatLayerNameForDisplay(name) {
3102
+ return name || '[unnamed]';
3103
+ }
3104
+
3105
+ function cleanLayerName(raw) {
3106
+ return raw.replace(/[\n\t/\\]/g, '')
3107
+ .replace(/^[\.\s]+/, '').replace(/[\.\s]+$/, '');
3108
+ }
3109
+
3110
+ function updateLayerStackOrder(layers) {
3111
+ // 1. assign ascending ids to unassigned layers above the range of other layers
3112
+ layers.forEach(function(o, i) {
3113
+ if (!o.layer.stack_id) o.layer.stack_id = 1e6 + i;
3114
+ });
3115
+ // 2. sort in ascending order
3116
+ layers.sort(function(a, b) {
3117
+ return a.layer.stack_id - b.layer.stack_id;
3118
+ });
3119
+ // 3. assign consecutve ids
3120
+ layers.forEach(function(o, i) {
3121
+ o.layer.stack_id = i + 1;
3122
+ });
3123
+ return layers;
3124
+ }
3125
+
3126
+ function sortLayersForMenuDisplay(layers) {
3127
+ layers = updateLayerStackOrder(layers);
3128
+ return layers.reverse();
3129
+ }
3345
3130
 
3346
3131
  // import { groupLayersByDataset } from '../dataset/mapshaper-target-utils';
3347
3132
 
@@ -4406,6 +4191,10 @@
4406
4191
  setMode('off');
4407
4192
  };
4408
4193
 
4194
+ this.modeUsesPopup = function(mode) {
4195
+ return ['info', 'selection', 'data', 'box', 'labels', 'location'].includes(mode);
4196
+ };
4197
+
4409
4198
  this.getMode = getInteractionMode;
4410
4199
 
4411
4200
  this.setMode = function(mode) {
@@ -4656,24 +4445,37 @@
4656
4445
 
4657
4446
  var LOGGING = false;
4658
4447
  var STDOUT = false; // use stdout for status messages
4659
-
4660
- // These three functions can be reset by GUI using setLoggingFunctions();
4661
- var _error = function() {
4662
- var msg = utils.toArray(arguments).join(' ');
4663
- throw new Error(msg);
4664
- };
4665
-
4666
- var _stop = function() {
4667
- throw new UserError(formatLogArgs(arguments));
4668
- };
4448
+ var _error, _stop, _message;
4669
4449
 
4670
4450
  var _interrupt = function() {
4671
4451
  throw new NonFatalError(formatLogArgs(arguments));
4672
4452
  };
4673
4453
 
4674
- var _message = function() {
4675
- logArgs(arguments);
4676
- };
4454
+ setLoggingForCLI();
4455
+
4456
+ function getLoggingSetter() {
4457
+ var e = _error, s = _stop, m = _message;
4458
+ return function() {
4459
+ setLoggingFunctions(m, e, s);
4460
+ };
4461
+ }
4462
+
4463
+ function setLoggingForCLI() {
4464
+ function stop() {
4465
+ throw new UserError(formatLogArgs(arguments));
4466
+ }
4467
+
4468
+ function error() {
4469
+ var msg = utils.toArray(arguments).join(' ');
4470
+ throw new Error(msg);
4471
+ }
4472
+
4473
+ function message() {
4474
+ logArgs(arguments);
4475
+ }
4476
+
4477
+ setLoggingFunctions(message, error, stop);
4478
+ }
4677
4479
 
4678
4480
  function enableLogging() {
4679
4481
  LOGGING = true;
@@ -6395,7 +6197,7 @@
6395
6197
  }
6396
6198
  };
6397
6199
 
6398
- self.setHoverVertex = function(p) {
6200
+ self.setHoverVertex = function(p, type) {
6399
6201
  var p2 = storedData.hit_coordinates;
6400
6202
  if (!active || !p) return;
6401
6203
  if (p2 && p2[0] == p[0] && p2[1] == p[1]) return;
@@ -7616,7 +7418,7 @@
7616
7418
  var _self = new EventDispatcher();
7617
7419
 
7618
7420
  gui.on('interaction_mode_change', function(e) {
7619
- if (e.mode == 'off') {
7421
+ if (!gui.interaction.modeUsesPopup(e.mode)) {
7620
7422
  inspect(-1); // clear the popup
7621
7423
  }
7622
7424
  });
@@ -7644,7 +7446,7 @@
7644
7446
 
7645
7447
  // does the attribute inspector appear on rollover
7646
7448
  function inspecting() {
7647
- return gui.interaction && gui.interaction.getMode() != 'off';
7449
+ return gui.interaction && gui.interaction.modeUsesPopup(gui.interaction.getMode());
7648
7450
  }
7649
7451
 
7650
7452
  return _self;
@@ -7986,7 +7788,8 @@
7986
7788
 
7987
7789
  function findVertexInsertionPoint(e) {
7988
7790
  var target = hit.getHitTarget();
7989
- if (!target.arcs.isFlat()) return null; // vertex insertion not supported with simplification
7791
+ //// vertex insertion not supported with simplification
7792
+ // if (!target.arcs.isFlat()) return null;
7990
7793
  var p = ext.translatePixelCoords(e.x, e.y);
7991
7794
  var midpoint = findNearestMidpoint(p, e.id, target);
7992
7795
  if (!midpoint ||
@@ -8140,10 +7943,17 @@
8140
7943
  activeStyle = { // outline style for the active layer
8141
7944
  type: 'outline',
8142
7945
  strokeColors: [lightStroke, darkStroke],
8143
- strokeWidth: 0.7,
7946
+ strokeWidth: 0.8,
8144
7947
  dotColor: "#223",
8145
7948
  dotSize: 1
8146
7949
  },
7950
+ activeStyleDarkMode = {
7951
+ type: 'outline',
7952
+ strokeColors: [lightStroke, 'white'],
7953
+ strokeWidth: 0.9,
7954
+ dotColor: 'white',
7955
+ dotSize: 1
7956
+ },
8147
7957
  activeStyleForLabels = {
8148
7958
  dotColor: "rgba(250, 0, 250, 0.45)", // violet dot with transparency
8149
7959
  dotSize: 1
@@ -8272,12 +8082,14 @@
8272
8082
  return style;
8273
8083
  }
8274
8084
 
8275
- function getActiveStyle(lyr) {
8085
+ function getActiveStyle(lyr, darkMode) {
8276
8086
  var style;
8277
8087
  if (layerHasCanvasDisplayStyle(lyr)) {
8278
8088
  style = getCanvasDisplayStyle(lyr);
8279
8089
  } else if (internal.layerHasLabels(lyr)) {
8280
8090
  style = getDefaultStyle(lyr, activeStyleForLabels);
8091
+ } else if (darkMode) {
8092
+ style = getDefaultStyle(lyr, activeStyleDarkMode);
8281
8093
  } else {
8282
8094
  style = getDefaultStyle(lyr, activeStyle);
8283
8095
  }
@@ -8292,6 +8104,7 @@
8292
8104
  strokeColor: black,
8293
8105
  strokeWidth: 1.5,
8294
8106
  vertices: true,
8107
+ vertex_overlay_color: violet,
8295
8108
  vertex_overlay: o.hit_coordinates || null,
8296
8109
  selected_points: o.selected_points || null,
8297
8110
  fillColor: null
@@ -8331,13 +8144,6 @@
8331
8144
  ids: ids,
8332
8145
  overlay: true
8333
8146
  };
8334
- // kludge to show vertices when editing path shapes
8335
- if (o.mode == 'vertices') {
8336
- style.vertices = true;
8337
- style.vertex_overlay = o.hit_coordinates || null;
8338
- style.selected_points = o.selected_points || null;
8339
- style.fillColor = null;
8340
- }
8341
8147
 
8342
8148
  if (layerHasCanvasDisplayStyle(lyr)) {
8343
8149
  if (geomType == 'point') {
@@ -8448,6 +8254,7 @@
8448
8254
  var _scale = 1,
8449
8255
  _cx, _cy, // center in geographic units
8450
8256
  _contentBounds,
8257
+ _strictBounds, // full extent must fit inside, if set
8451
8258
  _self = this,
8452
8259
  _frame;
8453
8260
 
@@ -8531,15 +8338,25 @@
8531
8338
  };
8532
8339
 
8533
8340
  // Update the extent of 'full' zoom without navigating the current view
8534
- this.setBounds = function(b) {
8341
+ //
8342
+ this.setBounds = function(contentBounds, strictBounds) {
8343
+ var b = contentBounds;
8535
8344
  var prev = _contentBounds;
8536
8345
  if (!b.hasBounds()) return; // kludge
8346
+ if (strictBounds) {
8347
+ _strictBounds = Array.isArray(strictBounds) ? new Bounds(strictBounds) : strictBounds;
8348
+ } else {
8349
+ _strictBounds = null;
8350
+ }
8537
8351
  _contentBounds = _frame ? b : padBounds(b, 4); // padding if not in frame mode
8352
+ if (_strictBounds) {
8353
+ _contentBounds = fitIn(_contentBounds, _strictBounds);
8354
+ }
8538
8355
  if (prev) {
8539
8356
  _scale = _scale * fillOut(_contentBounds).width() / fillOut(prev).width();
8540
8357
  } else {
8541
- _cx = b.centerX();
8542
- _cy = b.centerY();
8358
+ _cx = _contentBounds.centerX();
8359
+ _cy = _contentBounds.centerY();
8543
8360
  }
8544
8361
  };
8545
8362
 
@@ -8568,12 +8385,25 @@
8568
8385
 
8569
8386
  function recenter(cx, cy, scale, data) {
8570
8387
  scale = scale ? limitScale(scale) : _scale;
8571
- if (!(cx == _cx && cy == _cy && scale == _scale)) {
8572
- _cx = cx;
8573
- _cy = cy;
8574
- _scale = scale;
8575
- onChange(data);
8388
+ if (cx == _cx && cy == _cy && scale == _scale) return;
8389
+ if (_strictBounds) {
8390
+ scale = Math.max(1, scale);
8576
8391
  }
8392
+ _cx = cx;
8393
+ _cy = cy;
8394
+ _scale = scale;
8395
+ limitExtent();
8396
+ onChange(data);
8397
+ }
8398
+
8399
+ function limitExtent() {
8400
+ if (!_strictBounds) return;
8401
+ if (_scale < 1) _scale = 1;
8402
+ var dist = _strictBounds.height() / 2 / _scale;
8403
+ var ymax = _strictBounds.ymax - dist;
8404
+ var ymin = _strictBounds.ymin + dist;
8405
+ if (_cy > ymax) _cy = ymax;
8406
+ if (_cy < ymin) _cy = ymin;
8577
8407
  }
8578
8408
 
8579
8409
  function onChange(data) {
@@ -8600,15 +8430,19 @@
8600
8430
  }
8601
8431
 
8602
8432
  function calcBounds(cx, cy, scale) {
8603
- var bounds, w, h;
8433
+ var full, bounds, w, h;
8604
8434
  if (_frame) {
8605
- bounds = fillOutFrameBounds(_frame);
8435
+ full = fillOutFrameBounds(_frame);
8606
8436
  } else {
8607
- bounds = fillOut(_contentBounds);
8437
+ full = fillOut(_contentBounds);
8608
8438
  }
8609
- w = bounds.width() / scale;
8610
- h = bounds.height() / scale;
8611
- return new Bounds(cx - w/2, cy - h/2, cx + w/2, cy + h/2);
8439
+ if (_strictBounds) {
8440
+ full = fitIn(full, _strictBounds);
8441
+ }
8442
+ w = full.width() / scale;
8443
+ h = full.height() / scale;
8444
+ bounds = new Bounds(cx - w/2, cy - h/2, cx + w/2, cy + h/2);
8445
+ return bounds;
8612
8446
  }
8613
8447
 
8614
8448
  // Calculate viewport bounds from frame data
@@ -8636,6 +8470,22 @@
8636
8470
  return b;
8637
8471
  }
8638
8472
 
8473
+ function fitIn(b, b2) {
8474
+ // only fitting vertical extent
8475
+ // (currently only used in basemap view to enforce Mapbox's vertical limits)
8476
+
8477
+ if (b.height() > b2.height()) {
8478
+ b.scale(b2.height() / b.height());
8479
+ }
8480
+ if (b.ymin < b2.ymin) {
8481
+ b.shift(0, b2.ymin - b.ymin);
8482
+ }
8483
+ if (b.ymax > b2.ymax) {
8484
+ b.shift(0, b2.ymax - b.ymax);
8485
+ }
8486
+ return b;
8487
+ }
8488
+
8639
8489
  // Pad bounds vertically or horizontally to match viewport aspect ratio
8640
8490
  function fillOut(b) {
8641
8491
  var wpix = _position.width(),
@@ -8811,13 +8661,17 @@
8811
8661
  }
8812
8662
  }
8813
8663
  }
8664
+ _ctx.fill();
8665
+ _ctx.closePath();
8814
8666
 
8815
8667
  if (style.vertex_overlay) {
8668
+ _ctx.beginPath();
8669
+ _ctx.fillStyle = style.vertex_overlay_color || 'black';
8816
8670
  p = style.vertex_overlay;
8817
- drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius * 1.5, _ctx);
8671
+ drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius * 1.6, _ctx);
8672
+ _ctx.fill();
8673
+ _ctx.closePath();
8818
8674
  }
8819
- _ctx.fill();
8820
- _ctx.closePath();
8821
8675
  };
8822
8676
 
8823
8677
  // Optimized to draw paths in same-style batches (faster Canvas drawing)
@@ -9499,124 +9353,379 @@
9499
9353
  // runCommand('-rectangle bbox=' + bbox.join(','));
9500
9354
  // });
9501
9355
 
9502
- new SimpleButton(popup.findChild('.select-btn')).on('click', function() {
9503
- gui.enterMode('selection_tool');
9504
- gui.interaction.setMode('selection');
9505
- // kludge to pass bbox to the selection tool
9506
- gui.dispatchEvent('box_drag_end', {map_bbox: bboxPixels});
9507
- });
9356
+ new SimpleButton(popup.findChild('.select-btn')).on('click', function() {
9357
+ gui.enterMode('selection_tool');
9358
+ gui.interaction.setMode('selection');
9359
+ // kludge to pass bbox to the selection tool
9360
+ gui.dispatchEvent('box_drag_end', {map_bbox: bboxPixels});
9361
+ });
9362
+
9363
+ new SimpleButton(popup.findChild('.clip-btn')).on('click', function() {
9364
+ runCommand('-clip bbox2=' + bboxDataCoords.join(','));
9365
+ });
9366
+
9367
+ gui.addMode('box_tool', turnOn, turnOff);
9368
+
9369
+ gui.on('interaction_mode_change', function(e) {
9370
+ if (e.mode === 'box') {
9371
+ gui.enterMode('box_tool');
9372
+ } else if (gui.getMode() == 'box_tool') {
9373
+ gui.clearMode();
9374
+ }
9375
+ });
9376
+
9377
+ // Update the visible rectangle when the map view changes
9378
+ // (e.g. during zooming or panning)
9379
+ ext.on('change', function() {
9380
+ if (!_on || !box.visible() || !bboxDisplayCoords) return;
9381
+ var b = coordsToPix(bboxDisplayCoords);
9382
+ var pos = ext.position();
9383
+ var dx = pos.pageX,
9384
+ dy = pos.pageY;
9385
+ box.show(b[0] + dx, b[1] + dy, b[2] + dx, b[3] + dy);
9386
+ });
9387
+
9388
+ gui.on('box_drag_start', function() {
9389
+ box.classed('zooming', inZoomMode());
9390
+ hideCoords();
9391
+ });
9392
+
9393
+ gui.on('box_drag', function(e) {
9394
+ var b = e.page_bbox;
9395
+ bboxPixels = e.map_bbox;
9396
+ bboxDisplayCoords = pixToCoords(bboxPixels);
9397
+ if (_on || inZoomMode()) {
9398
+ box.show(b[0], b[1], b[2], b[3]);
9399
+ }
9400
+ });
9401
+
9402
+ gui.on('box_drag_end', function(e) {
9403
+ bboxPixels = e.map_bbox;
9404
+ bboxDisplayCoords = pixToCoords(bboxPixels);
9405
+ bboxDataCoords = getBBoxCoords(gui.map.getActiveLayer(), bboxDisplayCoords);
9406
+ if (inZoomMode()) {
9407
+ box.hide();
9408
+ nav.zoomToBbox(bboxPixels);
9409
+ } else if (_on) {
9410
+ popup.show();
9411
+ }
9412
+ });
9413
+
9414
+ function inZoomMode() {
9415
+ return !_on && gui.getMode() != 'selection_tool';
9416
+ }
9417
+
9418
+ function runCommand(cmd) {
9419
+ if (gui.console) {
9420
+ gui.console.runMapshaperCommands(cmd, function(err) {
9421
+ reset();
9422
+ });
9423
+ }
9424
+ // reset(); // TODO: exit interactive mode
9425
+ }
9426
+
9427
+ function showCoords() {
9428
+ El(infoBtn.node()).addClass('selected-btn');
9429
+ coords.text(bboxDataCoords.join(','));
9430
+ coords.show();
9431
+ GUI.selectElement(coords.node());
9432
+ }
9433
+
9434
+ function hideCoords() {
9435
+ El(infoBtn.node()).removeClass('selected-btn');
9436
+ coords.hide();
9437
+ }
9438
+
9439
+ function turnOn() {
9440
+ _on = true;
9441
+ }
9442
+
9443
+ function turnOff() {
9444
+ if (gui.interaction.getMode() == 'box') {
9445
+ // mode change was not initiated by interactive menu -- turn off interactivity
9446
+ gui.interaction.turnOff();
9447
+ }
9448
+ _on = false;
9449
+ reset();
9450
+ }
9451
+
9452
+ function reset() {
9453
+ box.hide();
9454
+ popup.hide();
9455
+ hideCoords();
9456
+ }
9457
+
9458
+ function pixToCoords(bbox) {
9459
+ var a = ext.translatePixelCoords(bbox[0], bbox[1]);
9460
+ var b = ext.translatePixelCoords(bbox[2], bbox[3]);
9461
+ var bbox2 = [a[0], b[1], b[0], a[1]];
9462
+ // round coords, for nicer 'info' display
9463
+ // (rounded precision should be sub-pixel)
9464
+ return internal.getRoundedCoords(bbox2, internal.getBoundsPrecisionForDisplay(bbox2));
9465
+ }
9466
+
9467
+ function coordsToPix(bbox) {
9468
+ var a = ext.translateCoords(bbox[0], bbox[1]);
9469
+ var b = ext.translateCoords(bbox[2], bbox[3]);
9470
+ return [a[0], b[1], b[0], a[1]];
9471
+ }
9472
+
9473
+ return self;
9474
+ }
9475
+
9476
+ // Create low-detail versions of large arc collections for faster rendering
9477
+ // at zoomed-out scales.
9478
+ function enhanceArcCollectionForDisplay(unfilteredArcs) {
9479
+ var size = unfilteredArcs.getPointCount(),
9480
+ filteredArcs, filteredSegLen;
9481
+
9482
+ // Only generate low-detail arcs for larger datasets
9483
+ if (size > 5e5) {
9484
+ if (!!unfilteredArcs.getVertexData().zz) {
9485
+ // Use precalculated simplification data for vertex filtering, if available
9486
+ filteredArcs = initFilteredArcs(unfilteredArcs);
9487
+ filteredSegLen = internal.getAvgSegment(filteredArcs);
9488
+ } else {
9489
+ // Use fast simplification as a fallback
9490
+ filteredSegLen = internal.getAvgSegment(unfilteredArcs) * 4;
9491
+ filteredArcs = internal.simplifyArcsFast(unfilteredArcs, filteredSegLen);
9492
+ }
9493
+ }
9494
+
9495
+ function initFilteredArcs(arcs) {
9496
+ var filterPct = 0.08;
9497
+ var nth = Math.ceil(arcs.getPointCount() / 5e5);
9498
+ var currInterval = arcs.getRetainedInterval();
9499
+ var filterZ = arcs.getThresholdByPct(filterPct, nth);
9500
+ var filteredArcs = arcs.setRetainedInterval(filterZ).getFilteredCopy();
9501
+ arcs.setRetainedInterval(currInterval); // reset current simplification
9502
+ return filteredArcs;
9503
+ }
9504
+
9505
+ unfilteredArcs.getScaledArcs = function(ext) {
9506
+ if (filteredArcs) {
9507
+ // match simplification of unfiltered arcs
9508
+ filteredArcs.setRetainedInterval(unfilteredArcs.getRetainedInterval());
9509
+ }
9510
+ // switch to filtered version of arcs at small scales
9511
+ var unitsPerPixel = 1/ext.getTransform().mx,
9512
+ useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5;
9513
+ return useFiltering ? filteredArcs : unfilteredArcs;
9514
+ };
9515
+ }
9516
+
9517
+ function getDisplayLayerForTable(table) {
9518
+ var n = table.size(),
9519
+ cellWidth = 12,
9520
+ cellHeight = 5,
9521
+ gutter = 6,
9522
+ arcs = [],
9523
+ shapes = [],
9524
+ aspectRatio = 1.1,
9525
+ x, y, col, row, blockSize;
9526
+
9527
+ if (n > 10000) {
9528
+ arcs = null;
9529
+ gutter = 0;
9530
+ cellWidth = 4;
9531
+ cellHeight = 4;
9532
+ aspectRatio = 1.45;
9533
+ } else if (n > 5000) {
9534
+ cellWidth = 5;
9535
+ gutter = 3;
9536
+ aspectRatio = 1.45;
9537
+ } else if (n > 1000) {
9538
+ gutter = 3;
9539
+ cellWidth = 8;
9540
+ aspectRatio = 1.3;
9541
+ }
9542
+
9543
+ if (n < 25) {
9544
+ blockSize = n;
9545
+ } else {
9546
+ blockSize = Math.sqrt(n * (cellWidth + gutter) / cellHeight / aspectRatio) | 0;
9547
+ }
9548
+
9549
+ for (var i=0; i<n; i++) {
9550
+ row = i % blockSize;
9551
+ col = Math.floor(i / blockSize);
9552
+ x = col * (cellWidth + gutter);
9553
+ y = cellHeight * (blockSize - row);
9554
+ if (arcs) {
9555
+ arcs.push(getArc(x, y, cellWidth, cellHeight));
9556
+ shapes.push([[i]]);
9557
+ } else {
9558
+ shapes.push([[x, y]]);
9559
+ }
9560
+ }
9561
+
9562
+ function getArc(x, y, w, h) {
9563
+ return [[x, y], [x + w, y], [x + w, y - h], [x, y - h], [x, y]];
9564
+ }
9508
9565
 
9509
- new SimpleButton(popup.findChild('.clip-btn')).on('click', function() {
9510
- runCommand('-clip bbox2=' + bboxDataCoords.join(','));
9511
- });
9566
+ return {
9567
+ layer: {
9568
+ geometry_type: arcs ? 'polygon' : 'point',
9569
+ shapes: shapes,
9570
+ data: table
9571
+ },
9572
+ arcs: arcs ? new internal.ArcCollection(arcs) : null
9573
+ };
9574
+ }
9512
9575
 
9513
- gui.addMode('box_tool', turnOn, turnOff);
9576
+ // displayCRS: CRS to use for display, or null (which clears any current display CRS)
9577
+ function projectDisplayLayer(lyr, displayCRS) {
9578
+ var crsInfo = getDatasetCrsInfo(lyr.source.dataset);
9579
+ var sourceCRS = crsInfo.crs;
9580
+ var lyr2;
9581
+ //if (!lyr.geographic || !sourceCRS) {
9582
+ // let getDisplayLayer() handle case of unprojectable source
9583
+ if (!lyr.geographic) {
9584
+ return lyr;
9585
+ }
9586
+ if (lyr.dynamic_crs && internal.crsAreEqual(sourceCRS, lyr.dynamic_crs)) {
9587
+ return lyr;
9588
+ }
9589
+ lyr2 = getDisplayLayer(lyr.source.layer, lyr.source.dataset, {crs: displayCRS});
9590
+ // kludge: copy projection-related properties to original layer
9591
+ lyr.dynamic_crs = lyr2.dynamic_crs;
9592
+ lyr.layer = lyr2.layer;
9514
9593
 
9515
- gui.on('interaction_mode_change', function(e) {
9516
- if (e.mode === 'box') {
9517
- gui.enterMode('box_tool');
9518
- } else if (gui.getMode() == 'box_tool') {
9519
- gui.clearMode();
9520
- }
9521
- });
9594
+ if (lyr.style && lyr.style.ids) {
9595
+ // re-apply layer filter
9596
+ lyr.layer = filterLayerByIds(lyr.layer, lyr.style.ids);
9597
+ }
9598
+ lyr.invertPoint = lyr2.invertPoint;
9599
+ lyr.projectPoint = lyr2.projectPoint;
9600
+ lyr.bounds = lyr2.bounds;
9601
+ lyr.arcs = lyr2.arcs;
9602
+ }
9522
9603
 
9523
- // Update the visible rectangle when the map view changes
9524
- // (e.g. during zooming or panning)
9525
- ext.on('change', function() {
9526
- if (!_on || !box.visible() || !bboxDisplayCoords) return;
9527
- var b = coordsToPix(bboxDisplayCoords);
9528
- var pos = ext.position();
9529
- var dx = pos.pageX,
9530
- dy = pos.pageY;
9531
- box.show(b[0] + dx, b[1] + dy, b[2] + dx, b[3] + dy);
9532
- });
9533
9604
 
9534
- gui.on('box_drag_start', function() {
9535
- box.classed('zooming', inZoomMode());
9536
- hideCoords();
9537
- });
9605
+ // Wrap a layer in an object along with information needed for rendering
9606
+ function getDisplayLayer(layer, dataset, opts) {
9607
+ var obj = {
9608
+ layer: null,
9609
+ arcs: null,
9610
+ // display_arcs: null,
9611
+ style: null,
9612
+ invertPoint: null,
9613
+ projectPoint: null,
9614
+ source: {
9615
+ layer: layer,
9616
+ dataset: dataset
9617
+ },
9618
+ empty: internal.getFeatureCount(layer) === 0
9619
+ };
9538
9620
 
9539
- gui.on('box_drag', function(e) {
9540
- var b = e.page_bbox;
9541
- bboxPixels = e.map_bbox;
9542
- bboxDisplayCoords = pixToCoords(bboxPixels);
9543
- if (_on || inZoomMode()) {
9544
- box.show(b[0], b[1], b[2], b[3]);
9545
- }
9546
- });
9621
+ var displayCRS = opts.crs || null;
9622
+ // display arcs may have been generated when another layer in the dataset was converted for display... re-use if available
9623
+ var displayArcs = dataset.displayArcs || null;
9624
+ var sourceCRS;
9625
+ var emptyArcs;
9547
9626
 
9548
- gui.on('box_drag_end', function(e) {
9549
- bboxPixels = e.map_bbox;
9550
- bboxDisplayCoords = pixToCoords(bboxPixels);
9551
- bboxDataCoords = getBBoxCoords(gui.map.getActiveLayer(), bboxDisplayCoords);
9552
- if (inZoomMode()) {
9553
- box.hide();
9554
- nav.zoomToBbox(bboxPixels);
9555
- } else if (_on) {
9556
- popup.show();
9627
+ if (displayCRS && layer.geometry_type) {
9628
+ var crsInfo = getDatasetCrsInfo(dataset);
9629
+ if (crsInfo.error) {
9630
+ // unprojectable dataset -- return empty layer
9631
+ obj.empty = true;
9632
+ obj.geographic = true;
9633
+ } else {
9634
+ sourceCRS = crsInfo.crs;
9557
9635
  }
9558
- });
9559
-
9560
- function inZoomMode() {
9561
- return !_on && gui.getMode() != 'selection_tool';
9562
9636
  }
9563
9637
 
9564
- function runCommand(cmd) {
9565
- if (gui.console) {
9566
- gui.console.runMapshaperCommands(cmd, function(err) {
9567
- reset();
9568
- });
9638
+ // Assume that dataset.displayArcs is in the display CRS
9639
+ // (it must be deleted upstream if reprojection is needed)
9640
+ if (!obj.empty && dataset.arcs && !displayArcs) {
9641
+ // project arcs, if needed
9642
+ if (needReprojectionForDisplay(sourceCRS, displayCRS)) {
9643
+ displayArcs = projectArcsForDisplay(dataset.arcs, sourceCRS, displayCRS);
9644
+ } else {
9645
+ // Use original arcs for display if there is no dynamic reprojection
9646
+ displayArcs = dataset.arcs;
9569
9647
  }
9570
- // reset(); // TODO: exit interactive mode
9571
- }
9572
9648
 
9573
- function showCoords() {
9574
- El(infoBtn.node()).addClass('selected-btn');
9575
- coords.text(bboxDataCoords.join(','));
9576
- coords.show();
9577
- GUI.selectElement(coords.node());
9649
+ enhanceArcCollectionForDisplay(displayArcs);
9650
+ dataset.displayArcs = displayArcs; // stash these in the dataset for other layers to use
9578
9651
  }
9579
9652
 
9580
- function hideCoords() {
9581
- El(infoBtn.node()).removeClass('selected-btn');
9582
- coords.hide();
9653
+ if (internal.layerHasFurniture(layer)) {
9654
+ obj.furniture = true;
9655
+ obj.furniture_type = internal.getFurnitureLayerType(layer);
9656
+ obj.layer = layer;
9657
+ // treating furniture layers (other than frame) as tabular for now,
9658
+ // so there is something to show if they are selected
9659
+ obj.tabular = obj.furniture_type != 'frame';
9660
+ } else if (obj.empty) {
9661
+ obj.layer = {shapes: []}; // ideally we should avoid empty layers
9662
+ } else if (!layer.geometry_type) {
9663
+ obj.tabular = true;
9664
+ } else {
9665
+ obj.geographic = true;
9666
+ obj.layer = layer;
9667
+ obj.arcs = displayArcs;
9583
9668
  }
9584
9669
 
9585
- function turnOn() {
9586
- _on = true;
9670
+ if (obj.tabular) {
9671
+ utils$1.extend(obj, getDisplayLayerForTable(layer.data));
9587
9672
  }
9588
9673
 
9589
- function turnOff() {
9590
- if (gui.interaction.getMode() == 'box') {
9591
- // mode change was not initiated by interactive menu -- turn off interactivity
9592
- gui.interaction.turnOff();
9674
+ // dynamic reprojection (arcs were already reprojected above)
9675
+ if (obj.geographic && needReprojectionForDisplay(sourceCRS, displayCRS)) {
9676
+ obj.dynamic_crs = displayCRS;
9677
+ obj.invertPoint = internal.getProjTransform2(displayCRS, sourceCRS);
9678
+ obj.projectPoint = internal.getProjTransform2(sourceCRS, displayCRS);
9679
+ if (internal.layerHasPoints(layer)) {
9680
+ obj.layer = projectPointsForDisplay(layer, sourceCRS, displayCRS);
9681
+ } else if (internal.layerHasPaths(layer)) {
9682
+ emptyArcs = findEmptyArcs(displayArcs);
9683
+ if (emptyArcs.length > 0) {
9684
+ // Don't try to draw paths containing coordinates that failed to project
9685
+ obj.layer = internal.filterPathLayerByArcIds(obj.layer, emptyArcs);
9686
+ }
9593
9687
  }
9594
- _on = false;
9595
- reset();
9596
9688
  }
9597
9689
 
9598
- function reset() {
9599
- box.hide();
9600
- popup.hide();
9601
- hideCoords();
9602
- }
9690
+ obj.bounds = getDisplayBounds(obj.layer, obj.arcs);
9691
+ return obj;
9692
+ }
9603
9693
 
9604
- function pixToCoords(bbox) {
9605
- var a = ext.translatePixelCoords(bbox[0], bbox[1]);
9606
- var b = ext.translatePixelCoords(bbox[2], bbox[3]);
9607
- var bbox2 = [a[0], b[1], b[0], a[1]];
9608
- // round coords, for nicer 'info' display
9609
- // (rounded precision should be sub-pixel)
9610
- return internal.getRoundedCoords(bbox2, internal.getBoundsPrecisionForDisplay(bbox2));
9694
+
9695
+ function getDisplayBounds(lyr, arcs) {
9696
+ var arcBounds = arcs ? arcs.getBounds() : new Bounds(),
9697
+ bounds = arcBounds, // default display extent: all arcs in the dataset
9698
+ lyrBounds;
9699
+
9700
+ if (lyr.geometry_type == 'point') {
9701
+ lyrBounds = internal.getLayerBounds(lyr);
9702
+ if (lyrBounds && lyrBounds.hasBounds()) {
9703
+ if (lyrBounds.area() > 0 || !arcBounds.hasBounds()) {
9704
+ bounds = lyrBounds;
9705
+ } else {
9706
+ // if a point layer has no extent (e.g. contains only a single point),
9707
+ // then merge with arc bounds, to place the point in context.
9708
+ bounds = arcBounds.mergeBounds(lyrBounds);
9709
+ }
9710
+ }
9611
9711
  }
9612
9712
 
9613
- function coordsToPix(bbox) {
9614
- var a = ext.translateCoords(bbox[0], bbox[1]);
9615
- var b = ext.translateCoords(bbox[2], bbox[3]);
9616
- return [a[0], b[1], b[0], a[1]];
9713
+ if (!bounds || !bounds.hasBounds()) { // empty layer
9714
+ bounds = new Bounds();
9617
9715
  }
9716
+ return bounds;
9717
+ }
9618
9718
 
9619
- return self;
9719
+ // Returns an array of ids of empty arcs (arcs can be set to empty if errors occur while projecting them)
9720
+ function findEmptyArcs(arcs) {
9721
+ var nn = arcs.getVertexData().nn;
9722
+ var ids = [];
9723
+ for (var i=0, n=nn.length; i<n; i++) {
9724
+ if (nn[i] === 0) {
9725
+ ids.push(i);
9726
+ }
9727
+ }
9728
+ return ids;
9620
9729
  }
9621
9730
 
9622
9731
  function loadScript(url, cb) {
@@ -9657,10 +9766,6 @@
9657
9766
 
9658
9767
  function init() {
9659
9768
  gui.addMode('basemap', turnOn, turnOff, basemapBtn);
9660
- // model.on('select', function() {
9661
- // TODO: hide basemap
9662
- // if (gui.getMode() == 'basemap') gui.clearMode();
9663
- // });
9664
9769
 
9665
9770
  new SimpleButton(menu.findChild('.close-btn')).on('click', function() {
9666
9771
  gui.clearMode();
@@ -9684,6 +9789,9 @@
9684
9789
 
9685
9790
  function updateStyle(style) {
9686
9791
  activeStyle = style || null;
9792
+ // TODO: consider enabling this
9793
+ // Make sure that the selected layer style gets updated in gui-map.js
9794
+ // gui.state.dark_basemap = style && style.dark || false;
9687
9795
  if (!style) {
9688
9796
  gui.map.setDisplayCRS(null);
9689
9797
  hide();
@@ -9703,11 +9811,12 @@
9703
9811
 
9704
9812
  function turnOn() {
9705
9813
  var activeLyr = gui.model.getActiveLayer();
9706
- var dataCRS = internal.getDatasetCRS(activeLyr.dataset);
9814
+ var info = getDatasetCrsInfo(activeLyr.dataset);
9815
+ var dataCRS = info.crs || null;
9707
9816
  var displayCRS = gui.map.getDisplayCRS();
9708
9817
  var warning;
9709
9818
 
9710
- if (!crsIsUsable(displayCRS) || !crsIsUsable(dataCRS)) {
9819
+ if (!dataCRS || !displayCRS || !crsIsUsable(displayCRS) || !crsIsUsable(dataCRS)) {
9711
9820
  warning = 'The current layer is not compatible with the projection used by the basemaps.';
9712
9821
  basemapWarning.html(warning).show();
9713
9822
  basemapNote.hide();
@@ -9772,16 +9881,25 @@
9772
9881
  });
9773
9882
  }
9774
9883
 
9884
+ // @bbox: latlon bounding box of current map extent
9775
9885
  function checkBounds(bbox) {
9886
+ var mpp = ext.getBounds().width() / ext.width();
9887
+ var z = scaleToZoom(mpp);
9776
9888
  var msg;
9777
- if (bbox[1] >= -85 && bbox[3] <= 85) {
9889
+ if (bbox[1] >= -85 && bbox[3] <= 85 && z <= 20) {
9778
9890
  extentNote.hide();
9779
9891
  return true;
9780
9892
  }
9781
- if (bbox[1] > 0) msg = 'pan south to see the basemap';
9782
- else if (bbox[3] < 0) msg = 'pan north to see the basemap';
9783
- else msg = msg = 'zoom in to see the basemap';
9784
- extentNote.html(msg).show();
9893
+ if (z > 20) {
9894
+ msg = 'zoom out';
9895
+ } else if (bbox[1] > 0) {
9896
+ msg = 'pan south';
9897
+ } else if (bbox[3] < 0) {
9898
+ msg = 'pan north';
9899
+ } else {
9900
+ msg = msg = 'zoom in';
9901
+ }
9902
+ extentNote.html(msg + ' to see the basemap').show();
9785
9903
  return false;
9786
9904
  }
9787
9905
 
@@ -9897,11 +10015,10 @@
9897
10015
  };
9898
10016
 
9899
10017
  this.getDisplayCRS = function() {
9900
- var crs;
9901
- if (_activeLyr && _activeLyr.geographic) {
9902
- crs = _activeLyr.dynamic_crs || internal.getDatasetCRS(_activeLyr.source.dataset);
9903
- }
9904
- return crs || null;
10018
+ if (!_activeLyr || !_activeLyr.geographic) return null;
10019
+ if (_activeLyr.dynamic_crs) return _activeLyr.dynamic_crs;
10020
+ var info = getDatasetCrsInfo(_activeLyr.source.dataset);
10021
+ return info.crs || null;
9905
10022
  };
9906
10023
 
9907
10024
  this.getExtent = function() {return _ext;};
@@ -9974,7 +10091,8 @@
9974
10091
  }
9975
10092
 
9976
10093
  _activeLyr = getDisplayLayer(e.layer, e.dataset, getDisplayOptions());
9977
- _activeLyr.style = getActiveStyle(_activeLyr.layer);
10094
+ _activeLyr.style = getActiveStyle(_activeLyr.layer, gui.state.dark_basemap);
10095
+
9978
10096
  _activeLyr.active = true;
9979
10097
  // if (_inspector) _inspector.updateLayer(_activeLyr);
9980
10098
  _hit.setLayer(_activeLyr);
@@ -9993,7 +10111,6 @@
9993
10111
  needReset = mapNeedsReset(fullBounds, _fullBounds, _ext.getBounds(), e.flags);
9994
10112
  }
9995
10113
 
9996
-
9997
10114
  if (isFrameView()) {
9998
10115
  _nav.setZoomFactor(0.05); // slow zooming way down to allow fine-tuning frame placement // 0.03
9999
10116
  _ext.setFrame(getFullBounds()); // TODO: remove redundancy with drawLayers()
@@ -10001,7 +10118,7 @@
10001
10118
  } else {
10002
10119
  _nav.setZoomFactor(1);
10003
10120
  }
10004
- _ext.setBounds(fullBounds); // update 'home' button extent
10121
+ _ext.setBounds(fullBounds, getStrictBounds()); // update 'home' button extent
10005
10122
  _fullBounds = fullBounds;
10006
10123
  if (needReset) {
10007
10124
  _ext.reset();
@@ -10091,6 +10208,13 @@
10091
10208
  _ext.reset();
10092
10209
  }
10093
10210
 
10211
+ function getStrictBounds() {
10212
+ if (internal.isWebMercator(map.getDisplayCRS())) {
10213
+ return getMapboxBounds();
10214
+ }
10215
+ return null;
10216
+ }
10217
+
10094
10218
  function getFullBounds() {
10095
10219
  var b = new Bounds();
10096
10220
  var marginPct = 0.025;
@@ -10257,7 +10381,7 @@
10257
10381
  if (layersMayHaveChanged) {
10258
10382
  // kludge to handle layer visibility toggling
10259
10383
  _ext.setFrame(isPreviewView() ? getFrameData() : null);
10260
- _ext.setBounds(getFullBounds());
10384
+ _ext.setBounds(getFullBounds(), getStrictBounds());
10261
10385
  updateLayerStyles(contentLayers);
10262
10386
  updateLayerStackOrder(model.getLayers());// update stack_id property of all layers
10263
10387
  }
@@ -10275,6 +10399,40 @@
10275
10399
  }
10276
10400
  }
10277
10401
 
10402
+ // This is a new way to handle compatibility problems between
10403
+ // interactive editing modes and other interface modes
10404
+ // (by default, interactive modes stay on when, e.g., the user clicks
10405
+ // "Export" or "Console").
10406
+ //
10407
+ function initModeRules(gui) {
10408
+
10409
+ gui.on('interaction_mode_change', function(e) {
10410
+ var imode = e.mode;
10411
+ var mode = gui.getMode();
10412
+
10413
+ // simplify and vertex editing are not compatible
10414
+ if (imode == 'vertices') {
10415
+ flattenArcs(gui.map.getActiveLayer());
10416
+
10417
+ if (mode == 'simplify') {
10418
+ gui.clearMode(); // exit simplification
10419
+ }
10420
+
10421
+ }
10422
+
10423
+ });
10424
+
10425
+ gui.on('mode', function(e) {
10426
+ var mode = e.name;
10427
+ var imode = gui.interaction.getMode();
10428
+
10429
+ // simplify and vertex editing are not compatible
10430
+ if (mode == 'simplify' && imode == 'vertices') {
10431
+ gui.interaction.turnOff();
10432
+ }
10433
+ });
10434
+ }
10435
+
10278
10436
  function GuiInstance(container, opts) {
10279
10437
  var gui = new ModeSwitcher();
10280
10438
  opts = utils$1.extend({
@@ -10298,6 +10456,8 @@
10298
10456
  gui.undo = new Undo(gui);
10299
10457
  gui.state = {};
10300
10458
 
10459
+ initModeRules(gui);
10460
+
10301
10461
  gui.showProgressMessage = function(msg) {
10302
10462
  if (!gui.progressMessage) {
10303
10463
  gui.progressMessage = El('div').addClass('progress-message')
@@ -10322,7 +10482,7 @@
10322
10482
  curr.blur();
10323
10483
  }
10324
10484
  GUI.__active = gui;
10325
- MessageProxy(gui);
10485
+ setLoggingForGUI(gui);
10326
10486
  ImportFileProxy(gui);
10327
10487
  WriteFilesProxy(gui);
10328
10488
  gui.dispatchEvent('active');