mapshaper 0.5.103 → 0.5.106

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.");
2495
- }
2312
+ var R = 6378137;
2313
+ var D2R = Math.PI / 180;
2314
+ var R2D = 180 / Math.PI;
2496
2315
 
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);
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;
2508
2324
  }
2325
+ return true;
2326
+ }
2509
2327
 
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);
2519
- }
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;
2520
2333
  }
2521
- }
2522
2334
 
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);
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);
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);
2531
2361
  }
2362
+ return copy;
2532
2363
  }
2533
2364
 
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;
@@ -7205,7 +7007,8 @@
7205
7007
  function getBoxData(e) {
7206
7008
  var pageBox = [e.pageX, e.pageY, dragStartEvt.pageX, dragStartEvt.pageY];
7207
7009
  var mapBox = [e.x, e.y, dragStartEvt.x, dragStartEvt.y];
7208
- var tmp;
7010
+ var displayBox = pixToCoords(mapBox);
7011
+ var dataBox = getBBoxCoords(gui.map.getActiveLayer(), displayBox);
7209
7012
  if (pageBox[0] > pageBox[2]) {
7210
7013
  swapElements(pageBox, 0, 2);
7211
7014
  swapElements(mapBox, 0, 2);
@@ -7216,10 +7019,20 @@
7216
7019
  }
7217
7020
  return {
7218
7021
  map_bbox: mapBox,
7219
- page_bbox: pageBox
7022
+ page_bbox: pageBox,
7023
+ // round coords, for nicer 'info' display
7024
+ // (rounded precision should be sub-pixel)
7025
+ map_display_bbox: internal.getRoundedCoords(displayBox, internal.getBoundsPrecisionForDisplay(displayBox)),
7026
+ map_data_bbox: internal.getRoundedCoords(dataBox, internal.getBoundsPrecisionForDisplay(dataBox))
7220
7027
  };
7221
7028
  }
7222
7029
 
7030
+ function pixToCoords(bbox) {
7031
+ var a = ext.translatePixelCoords(bbox[0], bbox[1]);
7032
+ var b = ext.translatePixelCoords(bbox[2], bbox[3]);
7033
+ return [a[0], b[1], b[0], a[1]];
7034
+ }
7035
+
7223
7036
  function disabled() {
7224
7037
  return !!gui.options.disableNavigation;
7225
7038
  }
@@ -7302,17 +7115,16 @@
7302
7115
  if (!_on) return;
7303
7116
  var b = e.page_bbox;
7304
7117
  box.show(b[0], b[1], b[2], b[3]);
7305
- updateSelection(e.map_bbox, true);
7118
+ updateSelection(e.map_data_bbox, true);
7306
7119
  });
7307
7120
 
7308
7121
  gui.on('box_drag_end', function(e) {
7309
7122
  if (!_on) return;
7310
7123
  box.hide();
7311
- updateSelection(e.map_bbox);
7124
+ updateSelection(e.map_data_bbox);
7312
7125
  });
7313
7126
 
7314
- function updateSelection(bboxPixels, transient) {
7315
- var bbox = bboxToCoords(bboxPixels);
7127
+ function updateSelection(bbox, transient) {
7316
7128
  var active = gui.model.getActiveLayer();
7317
7129
  var ids = internal.findShapesIntersectingBBox(bbox, active.layer, active.dataset.arcs);
7318
7130
  if (transient) {
@@ -7326,12 +7138,6 @@
7326
7138
  _on = true;
7327
7139
  }
7328
7140
 
7329
- function bboxToCoords(bbox) {
7330
- var a = ext.translatePixelCoords(bbox[0], bbox[1]);
7331
- var b = ext.translatePixelCoords(bbox[2], bbox[3]);
7332
- return [a[0], b[1], b[0], a[1]];
7333
- }
7334
-
7335
7141
  function turnOff() {
7336
7142
  reset();
7337
7143
  _on = false;
@@ -7616,7 +7422,7 @@
7616
7422
  var _self = new EventDispatcher();
7617
7423
 
7618
7424
  gui.on('interaction_mode_change', function(e) {
7619
- if (e.mode == 'off') {
7425
+ if (!gui.interaction.modeUsesPopup(e.mode)) {
7620
7426
  inspect(-1); // clear the popup
7621
7427
  }
7622
7428
  });
@@ -7644,7 +7450,7 @@
7644
7450
 
7645
7451
  // does the attribute inspector appear on rollover
7646
7452
  function inspecting() {
7647
- return gui.interaction && gui.interaction.getMode() != 'off';
7453
+ return gui.interaction && gui.interaction.modeUsesPopup(gui.interaction.getMode());
7648
7454
  }
7649
7455
 
7650
7456
  return _self;
@@ -7986,7 +7792,8 @@
7986
7792
 
7987
7793
  function findVertexInsertionPoint(e) {
7988
7794
  var target = hit.getHitTarget();
7989
- if (!target.arcs.isFlat()) return null; // vertex insertion not supported with simplification
7795
+ //// vertex insertion not supported with simplification
7796
+ // if (!target.arcs.isFlat()) return null;
7990
7797
  var p = ext.translatePixelCoords(e.x, e.y);
7991
7798
  var midpoint = findNearestMidpoint(p, e.id, target);
7992
7799
  if (!midpoint ||
@@ -8140,10 +7947,17 @@
8140
7947
  activeStyle = { // outline style for the active layer
8141
7948
  type: 'outline',
8142
7949
  strokeColors: [lightStroke, darkStroke],
8143
- strokeWidth: 0.7,
7950
+ strokeWidth: 0.8,
8144
7951
  dotColor: "#223",
8145
7952
  dotSize: 1
8146
7953
  },
7954
+ activeStyleDarkMode = {
7955
+ type: 'outline',
7956
+ strokeColors: [lightStroke, 'white'],
7957
+ strokeWidth: 0.9,
7958
+ dotColor: 'white',
7959
+ dotSize: 1
7960
+ },
8147
7961
  activeStyleForLabels = {
8148
7962
  dotColor: "rgba(250, 0, 250, 0.45)", // violet dot with transparency
8149
7963
  dotSize: 1
@@ -8272,12 +8086,14 @@
8272
8086
  return style;
8273
8087
  }
8274
8088
 
8275
- function getActiveStyle(lyr) {
8089
+ function getActiveStyle(lyr, darkMode) {
8276
8090
  var style;
8277
8091
  if (layerHasCanvasDisplayStyle(lyr)) {
8278
8092
  style = getCanvasDisplayStyle(lyr);
8279
8093
  } else if (internal.layerHasLabels(lyr)) {
8280
8094
  style = getDefaultStyle(lyr, activeStyleForLabels);
8095
+ } else if (darkMode) {
8096
+ style = getDefaultStyle(lyr, activeStyleDarkMode);
8281
8097
  } else {
8282
8098
  style = getDefaultStyle(lyr, activeStyle);
8283
8099
  }
@@ -8292,6 +8108,7 @@
8292
8108
  strokeColor: black,
8293
8109
  strokeWidth: 1.5,
8294
8110
  vertices: true,
8111
+ vertex_overlay_color: violet,
8295
8112
  vertex_overlay: o.hit_coordinates || null,
8296
8113
  selected_points: o.selected_points || null,
8297
8114
  fillColor: null
@@ -8331,13 +8148,6 @@
8331
8148
  ids: ids,
8332
8149
  overlay: true
8333
8150
  };
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
8151
 
8342
8152
  if (layerHasCanvasDisplayStyle(lyr)) {
8343
8153
  if (geomType == 'point') {
@@ -8448,6 +8258,7 @@
8448
8258
  var _scale = 1,
8449
8259
  _cx, _cy, // center in geographic units
8450
8260
  _contentBounds,
8261
+ _strictBounds, // full extent must fit inside, if set
8451
8262
  _self = this,
8452
8263
  _frame;
8453
8264
 
@@ -8531,15 +8342,25 @@
8531
8342
  };
8532
8343
 
8533
8344
  // Update the extent of 'full' zoom without navigating the current view
8534
- this.setBounds = function(b) {
8345
+ //
8346
+ this.setBounds = function(contentBounds, strictBounds) {
8347
+ var b = contentBounds;
8535
8348
  var prev = _contentBounds;
8536
8349
  if (!b.hasBounds()) return; // kludge
8350
+ if (strictBounds) {
8351
+ _strictBounds = Array.isArray(strictBounds) ? new Bounds(strictBounds) : strictBounds;
8352
+ } else {
8353
+ _strictBounds = null;
8354
+ }
8537
8355
  _contentBounds = _frame ? b : padBounds(b, 4); // padding if not in frame mode
8356
+ if (_strictBounds) {
8357
+ _contentBounds = fitIn(_contentBounds, _strictBounds);
8358
+ }
8538
8359
  if (prev) {
8539
8360
  _scale = _scale * fillOut(_contentBounds).width() / fillOut(prev).width();
8540
8361
  } else {
8541
- _cx = b.centerX();
8542
- _cy = b.centerY();
8362
+ _cx = _contentBounds.centerX();
8363
+ _cy = _contentBounds.centerY();
8543
8364
  }
8544
8365
  };
8545
8366
 
@@ -8568,12 +8389,33 @@
8568
8389
 
8569
8390
  function recenter(cx, cy, scale, data) {
8570
8391
  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);
8392
+ if (cx == _cx && cy == _cy && scale == _scale) return;
8393
+ navigate(cx, cy, scale);
8394
+ onChange(data);
8395
+ }
8396
+
8397
+ function navigate(cx, cy, scale) {
8398
+ if (_strictBounds) {
8399
+ var full = fillOut(_contentBounds);
8400
+ var minScale = full.height() / _strictBounds.height();
8401
+ if (scale < minScale) {
8402
+ var dx = cx - _cx;
8403
+ cx = _cx + dx * (minScale - _scale) / (scale - _scale);
8404
+ scale = minScale;
8405
+ }
8406
+ var dist = full.height() / 2 / scale;
8407
+ var ymax = _strictBounds.ymax - dist;
8408
+ var ymin = _strictBounds.ymin + dist;
8409
+ if (cy > ymax ) {
8410
+ cy = ymax;
8411
+ }
8412
+ if (cy < ymin) {
8413
+ cy = ymin;
8414
+ }
8576
8415
  }
8416
+ _cx = cx;
8417
+ _cy = cy;
8418
+ _scale = scale;
8577
8419
  }
8578
8420
 
8579
8421
  function onChange(data) {
@@ -8600,15 +8442,19 @@
8600
8442
  }
8601
8443
 
8602
8444
  function calcBounds(cx, cy, scale) {
8603
- var bounds, w, h;
8445
+ var full, bounds, w, h;
8604
8446
  if (_frame) {
8605
- bounds = fillOutFrameBounds(_frame);
8447
+ full = fillOutFrameBounds(_frame);
8606
8448
  } else {
8607
- bounds = fillOut(_contentBounds);
8449
+ full = fillOut(_contentBounds);
8608
8450
  }
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);
8451
+ if (_strictBounds) {
8452
+ full = fitIn(full, _strictBounds);
8453
+ }
8454
+ w = full.width() / scale;
8455
+ h = full.height() / scale;
8456
+ bounds = new Bounds(cx - w/2, cy - h/2, cx + w/2, cy + h/2);
8457
+ return bounds;
8612
8458
  }
8613
8459
 
8614
8460
  // Calculate viewport bounds from frame data
@@ -8636,6 +8482,21 @@
8636
8482
  return b;
8637
8483
  }
8638
8484
 
8485
+ function fitIn(b, b2) {
8486
+ // only fitting vertical extent
8487
+ // (currently only used in basemap view to enforce Mapbox's vertical limits)
8488
+ if (b.height() > b2.height()) {
8489
+ b.scale(b2.height() / b.height());
8490
+ }
8491
+ if (b.ymin < b2.ymin) {
8492
+ b.shift(0, b2.ymin - b.ymin);
8493
+ }
8494
+ if (b.ymax > b2.ymax) {
8495
+ b.shift(0, b2.ymax - b.ymax);
8496
+ }
8497
+ return b;
8498
+ }
8499
+
8639
8500
  // Pad bounds vertically or horizontally to match viewport aspect ratio
8640
8501
  function fillOut(b) {
8641
8502
  var wpix = _position.width(),
@@ -8811,13 +8672,17 @@
8811
8672
  }
8812
8673
  }
8813
8674
  }
8675
+ _ctx.fill();
8676
+ _ctx.closePath();
8814
8677
 
8815
8678
  if (style.vertex_overlay) {
8679
+ _ctx.beginPath();
8680
+ _ctx.fillStyle = style.vertex_overlay_color || 'black';
8816
8681
  p = style.vertex_overlay;
8817
- drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius * 1.5, _ctx);
8682
+ drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius * 1.6, _ctx);
8683
+ _ctx.fill();
8684
+ _ctx.closePath();
8818
8685
  }
8819
- _ctx.fill();
8820
- _ctx.closePath();
8821
8686
  };
8822
8687
 
8823
8688
  // Optimized to draw paths in same-style batches (faster Canvas drawing)
@@ -9476,7 +9341,7 @@
9476
9341
  var popup = gui.container.findChild('.box-tool-options');
9477
9342
  var coords = popup.findChild('.box-coords');
9478
9343
  var _on = false;
9479
- var bboxDisplayCoords, bboxPixels, bboxDataCoords;
9344
+ var bboxData;
9480
9345
 
9481
9346
  var infoBtn = new SimpleButton(popup.findChild('.info-btn')).on('click', function() {
9482
9347
  if (coords.visible()) hideCoords(); else showCoords();
@@ -9492,131 +9357,374 @@
9492
9357
  // reset();
9493
9358
  // });
9494
9359
 
9495
- // Removing button for creating a layer containing a single rectangle.
9496
- // You can get the bbox with the Info button and create a rectangle in the console
9497
- // using -rectangle bbox=<coordinates>
9498
- // new SimpleButton(popup.findChild('.rectangle-btn')).on('click', function() {
9499
- // runCommand('-rectangle bbox=' + bbox.join(','));
9500
- // });
9360
+ // Removing button for creating a layer containing a single rectangle.
9361
+ // You can get the bbox with the Info button and create a rectangle in the console
9362
+ // using -rectangle bbox=<coordinates>
9363
+ // new SimpleButton(popup.findChild('.rectangle-btn')).on('click', function() {
9364
+ // runCommand('-rectangle bbox=' + bbox.join(','));
9365
+ // });
9366
+
9367
+ new SimpleButton(popup.findChild('.select-btn')).on('click', function() {
9368
+ gui.enterMode('selection_tool');
9369
+ gui.interaction.setMode('selection');
9370
+ // kludge to pass bbox to the selection tool
9371
+ gui.dispatchEvent('box_drag_end', bboxData);
9372
+ });
9373
+
9374
+ new SimpleButton(popup.findChild('.clip-btn')).on('click', function() {
9375
+ runCommand('-clip bbox2=' + bboxData.map_data_bbox.join(','));
9376
+ });
9377
+
9378
+ gui.addMode('box_tool', turnOn, turnOff);
9379
+
9380
+ gui.on('interaction_mode_change', function(e) {
9381
+ if (e.mode === 'box') {
9382
+ gui.enterMode('box_tool');
9383
+ } else if (gui.getMode() == 'box_tool') {
9384
+ gui.clearMode();
9385
+ }
9386
+ });
9387
+
9388
+ // Update the visible rectangle when the map view changes
9389
+ // (e.g. during zooming or panning)
9390
+ ext.on('change', function() {
9391
+ if (!_on || !box.visible() || !bboxData) return;
9392
+ var b = coordsToPix(bboxData.map_display_bbox);
9393
+ var pos = ext.position();
9394
+ var dx = pos.pageX,
9395
+ dy = pos.pageY;
9396
+ box.show(b[0] + dx, b[1] + dy, b[2] + dx, b[3] + dy);
9397
+ });
9398
+
9399
+ gui.on('box_drag_start', function() {
9400
+ box.classed('zooming', inZoomMode());
9401
+ hideCoords();
9402
+ });
9403
+
9404
+ gui.on('box_drag', function(e) {
9405
+ var b = e.page_bbox;
9406
+ bboxData = e.data;
9407
+ if (_on || inZoomMode()) {
9408
+ box.show(b[0], b[1], b[2], b[3]);
9409
+ }
9410
+ });
9411
+
9412
+ gui.on('box_drag_end', function(e) {
9413
+ bboxData = e.data;
9414
+ if (inZoomMode()) {
9415
+ box.hide();
9416
+ nav.zoomToBbox(e.map_bbox);
9417
+ } else if (_on) {
9418
+ popup.show();
9419
+ }
9420
+ });
9421
+
9422
+ function inZoomMode() {
9423
+ return !_on && gui.getMode() != 'selection_tool';
9424
+ }
9425
+
9426
+ function runCommand(cmd) {
9427
+ if (gui.console) {
9428
+ gui.console.runMapshaperCommands(cmd, function(err) {
9429
+ reset();
9430
+ });
9431
+ }
9432
+ // reset(); // TODO: exit interactive mode
9433
+ }
9434
+
9435
+ function showCoords() {
9436
+ El(infoBtn.node()).addClass('selected-btn');
9437
+ coords.text(bboxData.map_data_bbox.join(','));
9438
+ coords.show();
9439
+ GUI.selectElement(coords.node());
9440
+ }
9441
+
9442
+ function hideCoords() {
9443
+ El(infoBtn.node()).removeClass('selected-btn');
9444
+ coords.hide();
9445
+ }
9446
+
9447
+ function turnOn() {
9448
+ _on = true;
9449
+ }
9450
+
9451
+ function turnOff() {
9452
+ if (gui.interaction.getMode() == 'box') {
9453
+ // mode change was not initiated by interactive menu -- turn off interactivity
9454
+ gui.interaction.turnOff();
9455
+ }
9456
+ _on = false;
9457
+ reset();
9458
+ }
9459
+
9460
+ function reset() {
9461
+ box.hide();
9462
+ popup.hide();
9463
+ hideCoords();
9464
+ }
9465
+
9466
+ function coordsToPix(bbox) {
9467
+ var a = ext.translateCoords(bbox[0], bbox[1]);
9468
+ var b = ext.translateCoords(bbox[2], bbox[3]);
9469
+ return [a[0], b[1], b[0], a[1]];
9470
+ }
9471
+
9472
+ return self;
9473
+ }
9474
+
9475
+ // Create low-detail versions of large arc collections for faster rendering
9476
+ // at zoomed-out scales.
9477
+ function enhanceArcCollectionForDisplay(unfilteredArcs) {
9478
+ var size = unfilteredArcs.getPointCount(),
9479
+ filteredArcs, filteredSegLen;
9480
+
9481
+ // Only generate low-detail arcs for larger datasets
9482
+ if (size > 5e5) {
9483
+ if (!!unfilteredArcs.getVertexData().zz) {
9484
+ // Use precalculated simplification data for vertex filtering, if available
9485
+ filteredArcs = initFilteredArcs(unfilteredArcs);
9486
+ filteredSegLen = internal.getAvgSegment(filteredArcs);
9487
+ } else {
9488
+ // Use fast simplification as a fallback
9489
+ filteredSegLen = internal.getAvgSegment(unfilteredArcs) * 4;
9490
+ filteredArcs = internal.simplifyArcsFast(unfilteredArcs, filteredSegLen);
9491
+ }
9492
+ }
9493
+
9494
+ function initFilteredArcs(arcs) {
9495
+ var filterPct = 0.08;
9496
+ var nth = Math.ceil(arcs.getPointCount() / 5e5);
9497
+ var currInterval = arcs.getRetainedInterval();
9498
+ var filterZ = arcs.getThresholdByPct(filterPct, nth);
9499
+ var filteredArcs = arcs.setRetainedInterval(filterZ).getFilteredCopy();
9500
+ arcs.setRetainedInterval(currInterval); // reset current simplification
9501
+ return filteredArcs;
9502
+ }
9503
+
9504
+ unfilteredArcs.getScaledArcs = function(ext) {
9505
+ if (filteredArcs) {
9506
+ // match simplification of unfiltered arcs
9507
+ filteredArcs.setRetainedInterval(unfilteredArcs.getRetainedInterval());
9508
+ }
9509
+ // switch to filtered version of arcs at small scales
9510
+ var unitsPerPixel = 1/ext.getTransform().mx,
9511
+ useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5;
9512
+ return useFiltering ? filteredArcs : unfilteredArcs;
9513
+ };
9514
+ }
9515
+
9516
+ function getDisplayLayerForTable(table) {
9517
+ var n = table.size(),
9518
+ cellWidth = 12,
9519
+ cellHeight = 5,
9520
+ gutter = 6,
9521
+ arcs = [],
9522
+ shapes = [],
9523
+ aspectRatio = 1.1,
9524
+ x, y, col, row, blockSize;
9525
+
9526
+ if (n > 10000) {
9527
+ arcs = null;
9528
+ gutter = 0;
9529
+ cellWidth = 4;
9530
+ cellHeight = 4;
9531
+ aspectRatio = 1.45;
9532
+ } else if (n > 5000) {
9533
+ cellWidth = 5;
9534
+ gutter = 3;
9535
+ aspectRatio = 1.45;
9536
+ } else if (n > 1000) {
9537
+ gutter = 3;
9538
+ cellWidth = 8;
9539
+ aspectRatio = 1.3;
9540
+ }
9541
+
9542
+ if (n < 25) {
9543
+ blockSize = n;
9544
+ } else {
9545
+ blockSize = Math.sqrt(n * (cellWidth + gutter) / cellHeight / aspectRatio) | 0;
9546
+ }
9501
9547
 
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
- });
9548
+ for (var i=0; i<n; i++) {
9549
+ row = i % blockSize;
9550
+ col = Math.floor(i / blockSize);
9551
+ x = col * (cellWidth + gutter);
9552
+ y = cellHeight * (blockSize - row);
9553
+ if (arcs) {
9554
+ arcs.push(getArc(x, y, cellWidth, cellHeight));
9555
+ shapes.push([[i]]);
9556
+ } else {
9557
+ shapes.push([[x, y]]);
9558
+ }
9559
+ }
9508
9560
 
9509
- new SimpleButton(popup.findChild('.clip-btn')).on('click', function() {
9510
- runCommand('-clip bbox2=' + bboxDataCoords.join(','));
9511
- });
9561
+ function getArc(x, y, w, h) {
9562
+ return [[x, y], [x + w, y], [x + w, y - h], [x, y - h], [x, y]];
9563
+ }
9512
9564
 
9513
- gui.addMode('box_tool', turnOn, turnOff);
9565
+ return {
9566
+ layer: {
9567
+ geometry_type: arcs ? 'polygon' : 'point',
9568
+ shapes: shapes,
9569
+ data: table
9570
+ },
9571
+ arcs: arcs ? new internal.ArcCollection(arcs) : null
9572
+ };
9573
+ }
9514
9574
 
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
- });
9575
+ // displayCRS: CRS to use for display, or null (which clears any current display CRS)
9576
+ function projectDisplayLayer(lyr, displayCRS) {
9577
+ var crsInfo = getDatasetCrsInfo(lyr.source.dataset);
9578
+ var sourceCRS = crsInfo.crs;
9579
+ var lyr2;
9580
+ //if (!lyr.geographic || !sourceCRS) {
9581
+ // let getDisplayLayer() handle case of unprojectable source
9582
+ if (!lyr.geographic) {
9583
+ return lyr;
9584
+ }
9585
+ if (lyr.dynamic_crs && internal.crsAreEqual(sourceCRS, lyr.dynamic_crs)) {
9586
+ return lyr;
9587
+ }
9588
+ lyr2 = getDisplayLayer(lyr.source.layer, lyr.source.dataset, {crs: displayCRS});
9589
+ // kludge: copy projection-related properties to original layer
9590
+ lyr.dynamic_crs = lyr2.dynamic_crs;
9591
+ lyr.layer = lyr2.layer;
9522
9592
 
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
- });
9593
+ if (lyr.style && lyr.style.ids) {
9594
+ // re-apply layer filter
9595
+ lyr.layer = filterLayerByIds(lyr.layer, lyr.style.ids);
9596
+ }
9597
+ lyr.invertPoint = lyr2.invertPoint;
9598
+ lyr.projectPoint = lyr2.projectPoint;
9599
+ lyr.bounds = lyr2.bounds;
9600
+ lyr.arcs = lyr2.arcs;
9601
+ }
9533
9602
 
9534
- gui.on('box_drag_start', function() {
9535
- box.classed('zooming', inZoomMode());
9536
- hideCoords();
9537
- });
9538
9603
 
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
- });
9604
+ // Wrap a layer in an object along with information needed for rendering
9605
+ function getDisplayLayer(layer, dataset, opts) {
9606
+ var obj = {
9607
+ layer: null,
9608
+ arcs: null,
9609
+ // display_arcs: null,
9610
+ style: null,
9611
+ invertPoint: null,
9612
+ projectPoint: null,
9613
+ source: {
9614
+ layer: layer,
9615
+ dataset: dataset
9616
+ },
9617
+ empty: internal.getFeatureCount(layer) === 0
9618
+ };
9547
9619
 
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();
9557
- }
9558
- });
9620
+ var displayCRS = opts.crs || null;
9621
+ // display arcs may have been generated when another layer in the dataset was converted for display... re-use if available
9622
+ var displayArcs = dataset.displayArcs || null;
9623
+ var sourceCRS;
9624
+ var emptyArcs;
9559
9625
 
9560
- function inZoomMode() {
9561
- return !_on && gui.getMode() != 'selection_tool';
9626
+ if (displayCRS && layer.geometry_type) {
9627
+ var crsInfo = getDatasetCrsInfo(dataset);
9628
+ if (crsInfo.error) {
9629
+ // unprojectable dataset -- return empty layer
9630
+ obj.empty = true;
9631
+ obj.geographic = true;
9632
+ } else {
9633
+ sourceCRS = crsInfo.crs;
9634
+ }
9562
9635
  }
9563
9636
 
9564
- function runCommand(cmd) {
9565
- if (gui.console) {
9566
- gui.console.runMapshaperCommands(cmd, function(err) {
9567
- reset();
9568
- });
9637
+ // Assume that dataset.displayArcs is in the display CRS
9638
+ // (it must be deleted upstream if reprojection is needed)
9639
+ if (!obj.empty && dataset.arcs && !displayArcs) {
9640
+ // project arcs, if needed
9641
+ if (needReprojectionForDisplay(sourceCRS, displayCRS)) {
9642
+ displayArcs = projectArcsForDisplay(dataset.arcs, sourceCRS, displayCRS);
9643
+ } else {
9644
+ // Use original arcs for display if there is no dynamic reprojection
9645
+ displayArcs = dataset.arcs;
9569
9646
  }
9570
- // reset(); // TODO: exit interactive mode
9571
- }
9572
9647
 
9573
- function showCoords() {
9574
- El(infoBtn.node()).addClass('selected-btn');
9575
- coords.text(bboxDataCoords.join(','));
9576
- coords.show();
9577
- GUI.selectElement(coords.node());
9648
+ enhanceArcCollectionForDisplay(displayArcs);
9649
+ dataset.displayArcs = displayArcs; // stash these in the dataset for other layers to use
9578
9650
  }
9579
9651
 
9580
- function hideCoords() {
9581
- El(infoBtn.node()).removeClass('selected-btn');
9582
- coords.hide();
9652
+ if (internal.layerHasFurniture(layer)) {
9653
+ obj.furniture = true;
9654
+ obj.furniture_type = internal.getFurnitureLayerType(layer);
9655
+ obj.layer = layer;
9656
+ // treating furniture layers (other than frame) as tabular for now,
9657
+ // so there is something to show if they are selected
9658
+ obj.tabular = obj.furniture_type != 'frame';
9659
+ } else if (obj.empty) {
9660
+ obj.layer = {shapes: []}; // ideally we should avoid empty layers
9661
+ } else if (!layer.geometry_type) {
9662
+ obj.tabular = true;
9663
+ } else {
9664
+ obj.geographic = true;
9665
+ obj.layer = layer;
9666
+ obj.arcs = displayArcs;
9583
9667
  }
9584
9668
 
9585
- function turnOn() {
9586
- _on = true;
9669
+ if (obj.tabular) {
9670
+ utils$1.extend(obj, getDisplayLayerForTable(layer.data));
9587
9671
  }
9588
9672
 
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();
9673
+ // dynamic reprojection (arcs were already reprojected above)
9674
+ if (obj.geographic && needReprojectionForDisplay(sourceCRS, displayCRS)) {
9675
+ obj.dynamic_crs = displayCRS;
9676
+ obj.invertPoint = internal.getProjTransform2(displayCRS, sourceCRS);
9677
+ obj.projectPoint = internal.getProjTransform2(sourceCRS, displayCRS);
9678
+ if (internal.layerHasPoints(layer)) {
9679
+ obj.layer = projectPointsForDisplay(layer, sourceCRS, displayCRS);
9680
+ } else if (internal.layerHasPaths(layer)) {
9681
+ emptyArcs = findEmptyArcs(displayArcs);
9682
+ if (emptyArcs.length > 0) {
9683
+ // Don't try to draw paths containing coordinates that failed to project
9684
+ obj.layer = internal.filterPathLayerByArcIds(obj.layer, emptyArcs);
9685
+ }
9593
9686
  }
9594
- _on = false;
9595
- reset();
9596
9687
  }
9597
9688
 
9598
- function reset() {
9599
- box.hide();
9600
- popup.hide();
9601
- hideCoords();
9602
- }
9689
+ obj.bounds = getDisplayBounds(obj.layer, obj.arcs);
9690
+ return obj;
9691
+ }
9603
9692
 
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));
9693
+
9694
+ function getDisplayBounds(lyr, arcs) {
9695
+ var arcBounds = arcs ? arcs.getBounds() : new Bounds(),
9696
+ bounds = arcBounds, // default display extent: all arcs in the dataset
9697
+ lyrBounds;
9698
+
9699
+ if (lyr.geometry_type == 'point') {
9700
+ lyrBounds = internal.getLayerBounds(lyr);
9701
+ if (lyrBounds && lyrBounds.hasBounds()) {
9702
+ if (lyrBounds.area() > 0 || !arcBounds.hasBounds()) {
9703
+ bounds = lyrBounds;
9704
+ } else {
9705
+ // if a point layer has no extent (e.g. contains only a single point),
9706
+ // then merge with arc bounds, to place the point in context.
9707
+ bounds = arcBounds.mergeBounds(lyrBounds);
9708
+ }
9709
+ }
9611
9710
  }
9612
9711
 
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]];
9712
+ if (!bounds || !bounds.hasBounds()) { // empty layer
9713
+ bounds = new Bounds();
9617
9714
  }
9715
+ return bounds;
9716
+ }
9618
9717
 
9619
- return self;
9718
+ // Returns an array of ids of empty arcs (arcs can be set to empty if errors occur while projecting them)
9719
+ function findEmptyArcs(arcs) {
9720
+ var nn = arcs.getVertexData().nn;
9721
+ var ids = [];
9722
+ for (var i=0, n=nn.length; i<n; i++) {
9723
+ if (nn[i] === 0) {
9724
+ ids.push(i);
9725
+ }
9726
+ }
9727
+ return ids;
9620
9728
  }
9621
9729
 
9622
9730
  function loadScript(url, cb) {
@@ -9657,10 +9765,6 @@
9657
9765
 
9658
9766
  function init() {
9659
9767
  gui.addMode('basemap', turnOn, turnOff, basemapBtn);
9660
- // model.on('select', function() {
9661
- // TODO: hide basemap
9662
- // if (gui.getMode() == 'basemap') gui.clearMode();
9663
- // });
9664
9768
 
9665
9769
  new SimpleButton(menu.findChild('.close-btn')).on('click', function() {
9666
9770
  gui.clearMode();
@@ -9684,6 +9788,9 @@
9684
9788
 
9685
9789
  function updateStyle(style) {
9686
9790
  activeStyle = style || null;
9791
+ // TODO: consider enabling this
9792
+ // Make sure that the selected layer style gets updated in gui-map.js
9793
+ // gui.state.dark_basemap = style && style.dark || false;
9687
9794
  if (!style) {
9688
9795
  gui.map.setDisplayCRS(null);
9689
9796
  hide();
@@ -9703,11 +9810,12 @@
9703
9810
 
9704
9811
  function turnOn() {
9705
9812
  var activeLyr = gui.model.getActiveLayer();
9706
- var dataCRS = internal.getDatasetCRS(activeLyr.dataset);
9813
+ var info = getDatasetCrsInfo(activeLyr.dataset);
9814
+ var dataCRS = info.crs || null;
9707
9815
  var displayCRS = gui.map.getDisplayCRS();
9708
9816
  var warning;
9709
9817
 
9710
- if (!crsIsUsable(displayCRS) || !crsIsUsable(dataCRS)) {
9818
+ if (!dataCRS || !displayCRS || !crsIsUsable(displayCRS) || !crsIsUsable(dataCRS)) {
9711
9819
  warning = 'The current layer is not compatible with the projection used by the basemaps.';
9712
9820
  basemapWarning.html(warning).show();
9713
9821
  basemapNote.hide();
@@ -9772,16 +9880,25 @@
9772
9880
  });
9773
9881
  }
9774
9882
 
9883
+ // @bbox: latlon bounding box of current map extent
9775
9884
  function checkBounds(bbox) {
9885
+ var mpp = ext.getBounds().width() / ext.width();
9886
+ var z = scaleToZoom(mpp);
9776
9887
  var msg;
9777
- if (bbox[1] >= -85 && bbox[3] <= 85) {
9888
+ if (bbox[1] >= -85 && bbox[3] <= 85 && z <= 20) {
9778
9889
  extentNote.hide();
9779
9890
  return true;
9780
9891
  }
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();
9892
+ if (z > 20) {
9893
+ msg = 'zoom out';
9894
+ } else if (bbox[1] > 0) {
9895
+ msg = 'pan south';
9896
+ } else if (bbox[3] < 0) {
9897
+ msg = 'pan north';
9898
+ } else {
9899
+ msg = msg = 'zoom in';
9900
+ }
9901
+ extentNote.html(msg + ' to see the basemap').show();
9785
9902
  return false;
9786
9903
  }
9787
9904
 
@@ -9897,11 +10014,10 @@
9897
10014
  };
9898
10015
 
9899
10016
  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;
10017
+ if (!_activeLyr || !_activeLyr.geographic) return null;
10018
+ if (_activeLyr.dynamic_crs) return _activeLyr.dynamic_crs;
10019
+ var info = getDatasetCrsInfo(_activeLyr.source.dataset);
10020
+ return info.crs || null;
9905
10021
  };
9906
10022
 
9907
10023
  this.getExtent = function() {return _ext;};
@@ -9974,7 +10090,8 @@
9974
10090
  }
9975
10091
 
9976
10092
  _activeLyr = getDisplayLayer(e.layer, e.dataset, getDisplayOptions());
9977
- _activeLyr.style = getActiveStyle(_activeLyr.layer);
10093
+ _activeLyr.style = getActiveStyle(_activeLyr.layer, gui.state.dark_basemap);
10094
+
9978
10095
  _activeLyr.active = true;
9979
10096
  // if (_inspector) _inspector.updateLayer(_activeLyr);
9980
10097
  _hit.setLayer(_activeLyr);
@@ -9993,7 +10110,6 @@
9993
10110
  needReset = mapNeedsReset(fullBounds, _fullBounds, _ext.getBounds(), e.flags);
9994
10111
  }
9995
10112
 
9996
-
9997
10113
  if (isFrameView()) {
9998
10114
  _nav.setZoomFactor(0.05); // slow zooming way down to allow fine-tuning frame placement // 0.03
9999
10115
  _ext.setFrame(getFullBounds()); // TODO: remove redundancy with drawLayers()
@@ -10001,7 +10117,7 @@
10001
10117
  } else {
10002
10118
  _nav.setZoomFactor(1);
10003
10119
  }
10004
- _ext.setBounds(fullBounds); // update 'home' button extent
10120
+ _ext.setBounds(fullBounds, getStrictBounds()); // update 'home' button extent
10005
10121
  _fullBounds = fullBounds;
10006
10122
  if (needReset) {
10007
10123
  _ext.reset();
@@ -10091,6 +10207,13 @@
10091
10207
  _ext.reset();
10092
10208
  }
10093
10209
 
10210
+ function getStrictBounds() {
10211
+ if (internal.isWebMercator(map.getDisplayCRS())) {
10212
+ return getMapboxBounds();
10213
+ }
10214
+ return null;
10215
+ }
10216
+
10094
10217
  function getFullBounds() {
10095
10218
  var b = new Bounds();
10096
10219
  var marginPct = 0.025;
@@ -10257,7 +10380,7 @@
10257
10380
  if (layersMayHaveChanged) {
10258
10381
  // kludge to handle layer visibility toggling
10259
10382
  _ext.setFrame(isPreviewView() ? getFrameData() : null);
10260
- _ext.setBounds(getFullBounds());
10383
+ _ext.setBounds(getFullBounds(), getStrictBounds());
10261
10384
  updateLayerStyles(contentLayers);
10262
10385
  updateLayerStackOrder(model.getLayers());// update stack_id property of all layers
10263
10386
  }
@@ -10275,6 +10398,40 @@
10275
10398
  }
10276
10399
  }
10277
10400
 
10401
+ // This is a new way to handle compatibility problems between
10402
+ // interactive editing modes and other interface modes
10403
+ // (by default, interactive modes stay on when, e.g., the user clicks
10404
+ // "Export" or "Console").
10405
+ //
10406
+ function initModeRules(gui) {
10407
+
10408
+ gui.on('interaction_mode_change', function(e) {
10409
+ var imode = e.mode;
10410
+ var mode = gui.getMode();
10411
+
10412
+ // simplify and vertex editing are not compatible
10413
+ if (imode == 'vertices') {
10414
+ flattenArcs(gui.map.getActiveLayer());
10415
+
10416
+ if (mode == 'simplify') {
10417
+ gui.clearMode(); // exit simplification
10418
+ }
10419
+
10420
+ }
10421
+
10422
+ });
10423
+
10424
+ gui.on('mode', function(e) {
10425
+ var mode = e.name;
10426
+ var imode = gui.interaction.getMode();
10427
+
10428
+ // simplify and vertex editing are not compatible
10429
+ if (mode == 'simplify' && imode == 'vertices') {
10430
+ gui.interaction.turnOff();
10431
+ }
10432
+ });
10433
+ }
10434
+
10278
10435
  function GuiInstance(container, opts) {
10279
10436
  var gui = new ModeSwitcher();
10280
10437
  opts = utils$1.extend({
@@ -10298,6 +10455,8 @@
10298
10455
  gui.undo = new Undo(gui);
10299
10456
  gui.state = {};
10300
10457
 
10458
+ initModeRules(gui);
10459
+
10301
10460
  gui.showProgressMessage = function(msg) {
10302
10461
  if (!gui.progressMessage) {
10303
10462
  gui.progressMessage = El('div').addClass('progress-message')
@@ -10322,7 +10481,7 @@
10322
10481
  curr.blur();
10323
10482
  }
10324
10483
  GUI.__active = gui;
10325
- MessageProxy(gui);
10484
+ setLoggingForGUI(gui);
10326
10485
  ImportFileProxy(gui);
10327
10486
  WriteFilesProxy(gui);
10328
10487
  gui.dispatchEvent('active');