mapshaper 0.5.102 → 0.5.105

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
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
+ });
1805
+ }
1840
1806
 
1841
- function needReprojectionForDisplay(sourceCRS, displayCRS) {
1842
- if (!sourceCRS || !displayCRS) {
1843
- return false;
1807
+ function saveBlobToDownloadFolder(filename, blob, done) {
1808
+ var anchor, blobUrl;
1809
+ if (window.navigator.msSaveBlob) {
1810
+ window.navigator.msSaveBlob(blob, filename);
1811
+ return done();
1844
1812
  }
1845
- if (internal.crsAreEqual(sourceCRS, displayCRS)) {
1846
- return false;
1813
+ try {
1814
+ blobUrl = URL.createObjectURL(blob);
1815
+ } catch(e) {
1816
+ done("Mapshaper can't export files from this browser. Try switching to Chrome or Firefox.");
1817
+ return;
1847
1818
  }
1848
- return true;
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);
1849
1832
  }
1850
1833
 
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;
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);
1856
1841
  }
1857
1842
 
1858
- var wgs84 = internal.getCRS('wgs84');
1859
- var toWGS84 = internal.getProjTransform2(src, wgs84);
1860
- var fromWGS84 = internal.getProjTransform2(wgs84, dest);
1843
+ function error() {
1844
+ stop.apply(null, utils$1.toArray(arguments));
1845
+ }
1861
1846
 
1862
- 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);
1876
- } 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);
1847
+ function message() {
1848
+ internal.logArgs(arguments);
1884
1849
  }
1885
- return copy;
1886
- }
1887
1850
 
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
- });
1851
+ internal.setLoggingFunctions(message, error, stop);
1897
1852
  }
1898
1853
 
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;
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
+ });
1905
1885
  }
1906
1886
 
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
- }
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;
1915
1892
 
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];
1922
- }
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;
1908
+ }
1923
1909
 
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);
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
+ });
1932
1919
  }
1933
1920
 
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
+ });
1934
1930
 
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);
1943
- }
1931
+ function loadProjLibs(libs, done) {
1932
+ var mproj = require('mproj');
1933
+ var i = 0;
1934
+ next();
1944
1935
 
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);
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
+ }
1945
+ });
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++;
1962
1957
  }
1963
1958
  }
1964
1959
 
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);
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;
1972
1969
  }
1970
+ revertLogging();
1971
+ return {
1972
+ crs: crs,
1973
+ error: err
1974
+ };
1973
1975
  }
1974
1976
 
1975
- function filterLayerByIds(lyr, ids) {
1976
- var shapes;
1977
- if (lyr.shapes) {
1978
- shapes = ids.map(function(id) {
1979
- return lyr.shapes[id];
1980
- });
1981
- return utils$1.defaults({shapes: shapes, data: null}, lyr);
1977
+ function flattenArcs(lyr) {
1978
+ lyr.source.dataset.arcs.flatten();
1979
+ if (isProjectedLayer(lyr)) {
1980
+ lyr.arcs.flatten();
1982
1981
  }
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
- }
1994
-
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;
2009
- }
2010
-
2011
- function sortLayersForMenuDisplay(layers) {
2012
- layers = updateLayerStackOrder(layers);
2013
- return layers.reverse();
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);
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);
2519
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
 
@@ -4053,8 +3838,6 @@
4053
3838
  }
4054
3839
  }
4055
3840
 
4056
- // import { cloneShape } from '../paths/mapshaper-shape-utils';
4057
- // import { copyRecord } from '../datatable/mapshaper-data-utils';
4058
3841
  var snapVerticesToPoint = internal.snapVerticesToPoint;
4059
3842
  var cloneShape = internal.cloneShape;
4060
3843
  var copyRecord = internal.copyRecord;
@@ -4408,6 +4191,10 @@
4408
4191
  setMode('off');
4409
4192
  };
4410
4193
 
4194
+ this.modeUsesPopup = function(mode) {
4195
+ return ['info', 'selection', 'data', 'box', 'labels', 'location'].includes(mode);
4196
+ };
4197
+
4411
4198
  this.getMode = getInteractionMode;
4412
4199
 
4413
4200
  this.setMode = function(mode) {
@@ -4658,24 +4445,37 @@
4658
4445
 
4659
4446
  var LOGGING = false;
4660
4447
  var STDOUT = false; // use stdout for status messages
4661
-
4662
- // These three functions can be reset by GUI using setLoggingFunctions();
4663
- var _error = function() {
4664
- var msg = utils.toArray(arguments).join(' ');
4665
- throw new Error(msg);
4666
- };
4667
-
4668
- var _stop = function() {
4669
- throw new UserError(formatLogArgs(arguments));
4670
- };
4448
+ var _error, _stop, _message;
4671
4449
 
4672
4450
  var _interrupt = function() {
4673
4451
  throw new NonFatalError(formatLogArgs(arguments));
4674
4452
  };
4675
4453
 
4676
- var _message = function() {
4677
- logArgs(arguments);
4678
- };
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
+ }
4679
4479
 
4680
4480
  function enableLogging() {
4681
4481
  LOGGING = true;
@@ -6397,7 +6197,7 @@
6397
6197
  }
6398
6198
  };
6399
6199
 
6400
- self.setHoverVertex = function(p) {
6200
+ self.setHoverVertex = function(p, type) {
6401
6201
  var p2 = storedData.hit_coordinates;
6402
6202
  if (!active || !p) return;
6403
6203
  if (p2 && p2[0] == p[0] && p2[1] == p[1]) return;
@@ -7618,7 +7418,7 @@
7618
7418
  var _self = new EventDispatcher();
7619
7419
 
7620
7420
  gui.on('interaction_mode_change', function(e) {
7621
- if (e.mode == 'off') {
7421
+ if (!gui.interaction.modeUsesPopup(e.mode)) {
7622
7422
  inspect(-1); // clear the popup
7623
7423
  }
7624
7424
  });
@@ -7646,7 +7446,7 @@
7646
7446
 
7647
7447
  // does the attribute inspector appear on rollover
7648
7448
  function inspecting() {
7649
- return gui.interaction && gui.interaction.getMode() != 'off';
7449
+ return gui.interaction && gui.interaction.modeUsesPopup(gui.interaction.getMode());
7650
7450
  }
7651
7451
 
7652
7452
  return _self;
@@ -7988,7 +7788,8 @@
7988
7788
 
7989
7789
  function findVertexInsertionPoint(e) {
7990
7790
  var target = hit.getHitTarget();
7991
- 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;
7992
7793
  var p = ext.translatePixelCoords(e.x, e.y);
7993
7794
  var midpoint = findNearestMidpoint(p, e.id, target);
7994
7795
  if (!midpoint ||
@@ -8133,19 +7934,26 @@
8133
7934
  var darkStroke = "#334",
8134
7935
  lightStroke = "#b7d9ea",
8135
7936
  violet = "#cc6acc",
8136
- violetFill = "rgba(249, 170, 249, 0.32)",
7937
+ violetFill = "rgba(249, 120, 249, 0.20)",
8137
7938
  gold = "#efc100",
8138
7939
  black = "black",
8139
7940
  grey = "#888",
8140
7941
  selectionFill = "rgba(237, 214, 0, 0.12)",
8141
- hoverFill = "rgba(255, 180, 255, 0.2)",
7942
+ hoverFill = "rgba(255, 120, 255, 0.12)",
8142
7943
  activeStyle = { // outline style for the active layer
8143
7944
  type: 'outline',
8144
7945
  strokeColors: [lightStroke, darkStroke],
8145
- strokeWidth: 0.7,
7946
+ strokeWidth: 0.8,
8146
7947
  dotColor: "#223",
8147
7948
  dotSize: 1
8148
7949
  },
7950
+ activeStyleDarkMode = {
7951
+ type: 'outline',
7952
+ strokeColors: [lightStroke, 'white'],
7953
+ strokeWidth: 0.9,
7954
+ dotColor: 'white',
7955
+ dotSize: 1
7956
+ },
8149
7957
  activeStyleForLabels = {
8150
7958
  dotColor: "rgba(250, 0, 250, 0.45)", // violet dot with transparency
8151
7959
  dotSize: 1
@@ -8274,12 +8082,14 @@
8274
8082
  return style;
8275
8083
  }
8276
8084
 
8277
- function getActiveStyle(lyr) {
8085
+ function getActiveStyle(lyr, darkMode) {
8278
8086
  var style;
8279
8087
  if (layerHasCanvasDisplayStyle(lyr)) {
8280
8088
  style = getCanvasDisplayStyle(lyr);
8281
8089
  } else if (internal.layerHasLabels(lyr)) {
8282
8090
  style = getDefaultStyle(lyr, activeStyleForLabels);
8091
+ } else if (darkMode) {
8092
+ style = getDefaultStyle(lyr, activeStyleDarkMode);
8283
8093
  } else {
8284
8094
  style = getDefaultStyle(lyr, activeStyle);
8285
8095
  }
@@ -8294,6 +8104,7 @@
8294
8104
  strokeColor: black,
8295
8105
  strokeWidth: 1.5,
8296
8106
  vertices: true,
8107
+ vertex_overlay_color: violet,
8297
8108
  vertex_overlay: o.hit_coordinates || null,
8298
8109
  selected_points: o.selected_points || null,
8299
8110
  fillColor: null
@@ -8333,13 +8144,6 @@
8333
8144
  ids: ids,
8334
8145
  overlay: true
8335
8146
  };
8336
- // kludge to show vertices when editing path shapes
8337
- if (o.mode == 'vertices') {
8338
- style.vertices = true;
8339
- style.vertex_overlay = o.hit_coordinates || null;
8340
- style.selected_points = o.selected_points || null;
8341
- style.fillColor = null;
8342
- }
8343
8147
 
8344
8148
  if (layerHasCanvasDisplayStyle(lyr)) {
8345
8149
  if (geomType == 'point') {
@@ -8450,6 +8254,7 @@
8450
8254
  var _scale = 1,
8451
8255
  _cx, _cy, // center in geographic units
8452
8256
  _contentBounds,
8257
+ _strictBounds, // full extent must fit inside, if set
8453
8258
  _self = this,
8454
8259
  _frame;
8455
8260
 
@@ -8533,15 +8338,25 @@
8533
8338
  };
8534
8339
 
8535
8340
  // Update the extent of 'full' zoom without navigating the current view
8536
- this.setBounds = function(b) {
8341
+ //
8342
+ this.setBounds = function(contentBounds, strictBounds) {
8343
+ var b = contentBounds;
8537
8344
  var prev = _contentBounds;
8538
8345
  if (!b.hasBounds()) return; // kludge
8346
+ if (strictBounds) {
8347
+ _strictBounds = Array.isArray(strictBounds) ? new Bounds(strictBounds) : strictBounds;
8348
+ } else {
8349
+ _strictBounds = null;
8350
+ }
8539
8351
  _contentBounds = _frame ? b : padBounds(b, 4); // padding if not in frame mode
8352
+ if (_strictBounds) {
8353
+ _contentBounds = fitIn(_contentBounds, _strictBounds);
8354
+ }
8540
8355
  if (prev) {
8541
8356
  _scale = _scale * fillOut(_contentBounds).width() / fillOut(prev).width();
8542
8357
  } else {
8543
- _cx = b.centerX();
8544
- _cy = b.centerY();
8358
+ _cx = _contentBounds.centerX();
8359
+ _cy = _contentBounds.centerY();
8545
8360
  }
8546
8361
  };
8547
8362
 
@@ -8570,12 +8385,33 @@
8570
8385
 
8571
8386
  function recenter(cx, cy, scale, data) {
8572
8387
  scale = scale ? limitScale(scale) : _scale;
8573
- if (!(cx == _cx && cy == _cy && scale == _scale)) {
8574
- _cx = cx;
8575
- _cy = cy;
8576
- _scale = scale;
8577
- onChange(data);
8388
+ if (cx == _cx && cy == _cy && scale == _scale) return;
8389
+ navigate(cx, cy, scale);
8390
+ onChange(data);
8391
+ }
8392
+
8393
+ function navigate(cx, cy, scale) {
8394
+ if (_strictBounds) {
8395
+ var full = fillOut(_contentBounds);
8396
+ var minScale = full.height() / _strictBounds.height();
8397
+ if (scale < minScale) {
8398
+ var dx = cx - _cx;
8399
+ cx = _cx + dx * (minScale - _scale) / (scale - _scale);
8400
+ scale = minScale;
8401
+ }
8402
+ var dist = full.height() / 2 / scale;
8403
+ var ymax = _strictBounds.ymax - dist;
8404
+ var ymin = _strictBounds.ymin + dist;
8405
+ if (cy > ymax ) {
8406
+ cy = ymax;
8407
+ }
8408
+ if (cy < ymin) {
8409
+ cy = ymin;
8410
+ }
8578
8411
  }
8412
+ _cx = cx;
8413
+ _cy = cy;
8414
+ _scale = scale;
8579
8415
  }
8580
8416
 
8581
8417
  function onChange(data) {
@@ -8602,15 +8438,19 @@
8602
8438
  }
8603
8439
 
8604
8440
  function calcBounds(cx, cy, scale) {
8605
- var bounds, w, h;
8441
+ var full, bounds, w, h;
8606
8442
  if (_frame) {
8607
- bounds = fillOutFrameBounds(_frame);
8443
+ full = fillOutFrameBounds(_frame);
8608
8444
  } else {
8609
- bounds = fillOut(_contentBounds);
8445
+ full = fillOut(_contentBounds);
8446
+ }
8447
+ if (_strictBounds) {
8448
+ full = fitIn(full, _strictBounds);
8610
8449
  }
8611
- w = bounds.width() / scale;
8612
- h = bounds.height() / scale;
8613
- return new Bounds(cx - w/2, cy - h/2, cx + w/2, cy + h/2);
8450
+ w = full.width() / scale;
8451
+ h = full.height() / scale;
8452
+ bounds = new Bounds(cx - w/2, cy - h/2, cx + w/2, cy + h/2);
8453
+ return bounds;
8614
8454
  }
8615
8455
 
8616
8456
  // Calculate viewport bounds from frame data
@@ -8638,6 +8478,21 @@
8638
8478
  return b;
8639
8479
  }
8640
8480
 
8481
+ function fitIn(b, b2) {
8482
+ // only fitting vertical extent
8483
+ // (currently only used in basemap view to enforce Mapbox's vertical limits)
8484
+ if (b.height() > b2.height()) {
8485
+ b.scale(b2.height() / b.height());
8486
+ }
8487
+ if (b.ymin < b2.ymin) {
8488
+ b.shift(0, b2.ymin - b.ymin);
8489
+ }
8490
+ if (b.ymax > b2.ymax) {
8491
+ b.shift(0, b2.ymax - b.ymax);
8492
+ }
8493
+ return b;
8494
+ }
8495
+
8641
8496
  // Pad bounds vertically or horizontally to match viewport aspect ratio
8642
8497
  function fillOut(b) {
8643
8498
  var wpix = _position.width(),
@@ -8813,13 +8668,17 @@
8813
8668
  }
8814
8669
  }
8815
8670
  }
8671
+ _ctx.fill();
8672
+ _ctx.closePath();
8816
8673
 
8817
8674
  if (style.vertex_overlay) {
8675
+ _ctx.beginPath();
8676
+ _ctx.fillStyle = style.vertex_overlay_color || 'black';
8818
8677
  p = style.vertex_overlay;
8819
- drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius * 1.5, _ctx);
8678
+ drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius * 1.6, _ctx);
8679
+ _ctx.fill();
8680
+ _ctx.closePath();
8820
8681
  }
8821
- _ctx.fill();
8822
- _ctx.closePath();
8823
8682
  };
8824
8683
 
8825
8684
  // Optimized to draw paths in same-style batches (faster Canvas drawing)
@@ -9501,124 +9360,379 @@
9501
9360
  // runCommand('-rectangle bbox=' + bbox.join(','));
9502
9361
  // });
9503
9362
 
9504
- new SimpleButton(popup.findChild('.select-btn')).on('click', function() {
9505
- gui.enterMode('selection_tool');
9506
- gui.interaction.setMode('selection');
9507
- // kludge to pass bbox to the selection tool
9508
- gui.dispatchEvent('box_drag_end', {map_bbox: bboxPixels});
9509
- });
9363
+ new SimpleButton(popup.findChild('.select-btn')).on('click', function() {
9364
+ gui.enterMode('selection_tool');
9365
+ gui.interaction.setMode('selection');
9366
+ // kludge to pass bbox to the selection tool
9367
+ gui.dispatchEvent('box_drag_end', {map_bbox: bboxPixels});
9368
+ });
9369
+
9370
+ new SimpleButton(popup.findChild('.clip-btn')).on('click', function() {
9371
+ runCommand('-clip bbox2=' + bboxDataCoords.join(','));
9372
+ });
9373
+
9374
+ gui.addMode('box_tool', turnOn, turnOff);
9375
+
9376
+ gui.on('interaction_mode_change', function(e) {
9377
+ if (e.mode === 'box') {
9378
+ gui.enterMode('box_tool');
9379
+ } else if (gui.getMode() == 'box_tool') {
9380
+ gui.clearMode();
9381
+ }
9382
+ });
9383
+
9384
+ // Update the visible rectangle when the map view changes
9385
+ // (e.g. during zooming or panning)
9386
+ ext.on('change', function() {
9387
+ if (!_on || !box.visible() || !bboxDisplayCoords) return;
9388
+ var b = coordsToPix(bboxDisplayCoords);
9389
+ var pos = ext.position();
9390
+ var dx = pos.pageX,
9391
+ dy = pos.pageY;
9392
+ box.show(b[0] + dx, b[1] + dy, b[2] + dx, b[3] + dy);
9393
+ });
9394
+
9395
+ gui.on('box_drag_start', function() {
9396
+ box.classed('zooming', inZoomMode());
9397
+ hideCoords();
9398
+ });
9399
+
9400
+ gui.on('box_drag', function(e) {
9401
+ var b = e.page_bbox;
9402
+ bboxPixels = e.map_bbox;
9403
+ bboxDisplayCoords = pixToCoords(bboxPixels);
9404
+ if (_on || inZoomMode()) {
9405
+ box.show(b[0], b[1], b[2], b[3]);
9406
+ }
9407
+ });
9408
+
9409
+ gui.on('box_drag_end', function(e) {
9410
+ bboxPixels = e.map_bbox;
9411
+ bboxDisplayCoords = pixToCoords(bboxPixels);
9412
+ bboxDataCoords = getBBoxCoords(gui.map.getActiveLayer(), bboxDisplayCoords);
9413
+ if (inZoomMode()) {
9414
+ box.hide();
9415
+ nav.zoomToBbox(bboxPixels);
9416
+ } else if (_on) {
9417
+ popup.show();
9418
+ }
9419
+ });
9420
+
9421
+ function inZoomMode() {
9422
+ return !_on && gui.getMode() != 'selection_tool';
9423
+ }
9424
+
9425
+ function runCommand(cmd) {
9426
+ if (gui.console) {
9427
+ gui.console.runMapshaperCommands(cmd, function(err) {
9428
+ reset();
9429
+ });
9430
+ }
9431
+ // reset(); // TODO: exit interactive mode
9432
+ }
9433
+
9434
+ function showCoords() {
9435
+ El(infoBtn.node()).addClass('selected-btn');
9436
+ coords.text(bboxDataCoords.join(','));
9437
+ coords.show();
9438
+ GUI.selectElement(coords.node());
9439
+ }
9440
+
9441
+ function hideCoords() {
9442
+ El(infoBtn.node()).removeClass('selected-btn');
9443
+ coords.hide();
9444
+ }
9445
+
9446
+ function turnOn() {
9447
+ _on = true;
9448
+ }
9449
+
9450
+ function turnOff() {
9451
+ if (gui.interaction.getMode() == 'box') {
9452
+ // mode change was not initiated by interactive menu -- turn off interactivity
9453
+ gui.interaction.turnOff();
9454
+ }
9455
+ _on = false;
9456
+ reset();
9457
+ }
9458
+
9459
+ function reset() {
9460
+ box.hide();
9461
+ popup.hide();
9462
+ hideCoords();
9463
+ }
9464
+
9465
+ function pixToCoords(bbox) {
9466
+ var a = ext.translatePixelCoords(bbox[0], bbox[1]);
9467
+ var b = ext.translatePixelCoords(bbox[2], bbox[3]);
9468
+ var bbox2 = [a[0], b[1], b[0], a[1]];
9469
+ // round coords, for nicer 'info' display
9470
+ // (rounded precision should be sub-pixel)
9471
+ return internal.getRoundedCoords(bbox2, internal.getBoundsPrecisionForDisplay(bbox2));
9472
+ }
9473
+
9474
+ function coordsToPix(bbox) {
9475
+ var a = ext.translateCoords(bbox[0], bbox[1]);
9476
+ var b = ext.translateCoords(bbox[2], bbox[3]);
9477
+ return [a[0], b[1], b[0], a[1]];
9478
+ }
9479
+
9480
+ return self;
9481
+ }
9482
+
9483
+ // Create low-detail versions of large arc collections for faster rendering
9484
+ // at zoomed-out scales.
9485
+ function enhanceArcCollectionForDisplay(unfilteredArcs) {
9486
+ var size = unfilteredArcs.getPointCount(),
9487
+ filteredArcs, filteredSegLen;
9488
+
9489
+ // Only generate low-detail arcs for larger datasets
9490
+ if (size > 5e5) {
9491
+ if (!!unfilteredArcs.getVertexData().zz) {
9492
+ // Use precalculated simplification data for vertex filtering, if available
9493
+ filteredArcs = initFilteredArcs(unfilteredArcs);
9494
+ filteredSegLen = internal.getAvgSegment(filteredArcs);
9495
+ } else {
9496
+ // Use fast simplification as a fallback
9497
+ filteredSegLen = internal.getAvgSegment(unfilteredArcs) * 4;
9498
+ filteredArcs = internal.simplifyArcsFast(unfilteredArcs, filteredSegLen);
9499
+ }
9500
+ }
9501
+
9502
+ function initFilteredArcs(arcs) {
9503
+ var filterPct = 0.08;
9504
+ var nth = Math.ceil(arcs.getPointCount() / 5e5);
9505
+ var currInterval = arcs.getRetainedInterval();
9506
+ var filterZ = arcs.getThresholdByPct(filterPct, nth);
9507
+ var filteredArcs = arcs.setRetainedInterval(filterZ).getFilteredCopy();
9508
+ arcs.setRetainedInterval(currInterval); // reset current simplification
9509
+ return filteredArcs;
9510
+ }
9511
+
9512
+ unfilteredArcs.getScaledArcs = function(ext) {
9513
+ if (filteredArcs) {
9514
+ // match simplification of unfiltered arcs
9515
+ filteredArcs.setRetainedInterval(unfilteredArcs.getRetainedInterval());
9516
+ }
9517
+ // switch to filtered version of arcs at small scales
9518
+ var unitsPerPixel = 1/ext.getTransform().mx,
9519
+ useFiltering = filteredArcs && unitsPerPixel > filteredSegLen * 1.5;
9520
+ return useFiltering ? filteredArcs : unfilteredArcs;
9521
+ };
9522
+ }
9523
+
9524
+ function getDisplayLayerForTable(table) {
9525
+ var n = table.size(),
9526
+ cellWidth = 12,
9527
+ cellHeight = 5,
9528
+ gutter = 6,
9529
+ arcs = [],
9530
+ shapes = [],
9531
+ aspectRatio = 1.1,
9532
+ x, y, col, row, blockSize;
9533
+
9534
+ if (n > 10000) {
9535
+ arcs = null;
9536
+ gutter = 0;
9537
+ cellWidth = 4;
9538
+ cellHeight = 4;
9539
+ aspectRatio = 1.45;
9540
+ } else if (n > 5000) {
9541
+ cellWidth = 5;
9542
+ gutter = 3;
9543
+ aspectRatio = 1.45;
9544
+ } else if (n > 1000) {
9545
+ gutter = 3;
9546
+ cellWidth = 8;
9547
+ aspectRatio = 1.3;
9548
+ }
9549
+
9550
+ if (n < 25) {
9551
+ blockSize = n;
9552
+ } else {
9553
+ blockSize = Math.sqrt(n * (cellWidth + gutter) / cellHeight / aspectRatio) | 0;
9554
+ }
9555
+
9556
+ for (var i=0; i<n; i++) {
9557
+ row = i % blockSize;
9558
+ col = Math.floor(i / blockSize);
9559
+ x = col * (cellWidth + gutter);
9560
+ y = cellHeight * (blockSize - row);
9561
+ if (arcs) {
9562
+ arcs.push(getArc(x, y, cellWidth, cellHeight));
9563
+ shapes.push([[i]]);
9564
+ } else {
9565
+ shapes.push([[x, y]]);
9566
+ }
9567
+ }
9568
+
9569
+ function getArc(x, y, w, h) {
9570
+ return [[x, y], [x + w, y], [x + w, y - h], [x, y - h], [x, y]];
9571
+ }
9510
9572
 
9511
- new SimpleButton(popup.findChild('.clip-btn')).on('click', function() {
9512
- runCommand('-clip bbox2=' + bboxDataCoords.join(','));
9513
- });
9573
+ return {
9574
+ layer: {
9575
+ geometry_type: arcs ? 'polygon' : 'point',
9576
+ shapes: shapes,
9577
+ data: table
9578
+ },
9579
+ arcs: arcs ? new internal.ArcCollection(arcs) : null
9580
+ };
9581
+ }
9514
9582
 
9515
- gui.addMode('box_tool', turnOn, turnOff);
9583
+ // displayCRS: CRS to use for display, or null (which clears any current display CRS)
9584
+ function projectDisplayLayer(lyr, displayCRS) {
9585
+ var crsInfo = getDatasetCrsInfo(lyr.source.dataset);
9586
+ var sourceCRS = crsInfo.crs;
9587
+ var lyr2;
9588
+ //if (!lyr.geographic || !sourceCRS) {
9589
+ // let getDisplayLayer() handle case of unprojectable source
9590
+ if (!lyr.geographic) {
9591
+ return lyr;
9592
+ }
9593
+ if (lyr.dynamic_crs && internal.crsAreEqual(sourceCRS, lyr.dynamic_crs)) {
9594
+ return lyr;
9595
+ }
9596
+ lyr2 = getDisplayLayer(lyr.source.layer, lyr.source.dataset, {crs: displayCRS});
9597
+ // kludge: copy projection-related properties to original layer
9598
+ lyr.dynamic_crs = lyr2.dynamic_crs;
9599
+ lyr.layer = lyr2.layer;
9516
9600
 
9517
- gui.on('interaction_mode_change', function(e) {
9518
- if (e.mode === 'box') {
9519
- gui.enterMode('box_tool');
9520
- } else if (gui.getMode() == 'box_tool') {
9521
- gui.clearMode();
9522
- }
9523
- });
9601
+ if (lyr.style && lyr.style.ids) {
9602
+ // re-apply layer filter
9603
+ lyr.layer = filterLayerByIds(lyr.layer, lyr.style.ids);
9604
+ }
9605
+ lyr.invertPoint = lyr2.invertPoint;
9606
+ lyr.projectPoint = lyr2.projectPoint;
9607
+ lyr.bounds = lyr2.bounds;
9608
+ lyr.arcs = lyr2.arcs;
9609
+ }
9524
9610
 
9525
- // Update the visible rectangle when the map view changes
9526
- // (e.g. during zooming or panning)
9527
- ext.on('change', function() {
9528
- if (!_on || !box.visible() || !bboxDisplayCoords) return;
9529
- var b = coordsToPix(bboxDisplayCoords);
9530
- var pos = ext.position();
9531
- var dx = pos.pageX,
9532
- dy = pos.pageY;
9533
- box.show(b[0] + dx, b[1] + dy, b[2] + dx, b[3] + dy);
9534
- });
9535
9611
 
9536
- gui.on('box_drag_start', function() {
9537
- box.classed('zooming', inZoomMode());
9538
- hideCoords();
9539
- });
9612
+ // Wrap a layer in an object along with information needed for rendering
9613
+ function getDisplayLayer(layer, dataset, opts) {
9614
+ var obj = {
9615
+ layer: null,
9616
+ arcs: null,
9617
+ // display_arcs: null,
9618
+ style: null,
9619
+ invertPoint: null,
9620
+ projectPoint: null,
9621
+ source: {
9622
+ layer: layer,
9623
+ dataset: dataset
9624
+ },
9625
+ empty: internal.getFeatureCount(layer) === 0
9626
+ };
9540
9627
 
9541
- gui.on('box_drag', function(e) {
9542
- var b = e.page_bbox;
9543
- bboxPixels = e.map_bbox;
9544
- bboxDisplayCoords = pixToCoords(bboxPixels);
9545
- if (_on || inZoomMode()) {
9546
- box.show(b[0], b[1], b[2], b[3]);
9547
- }
9548
- });
9628
+ var displayCRS = opts.crs || null;
9629
+ // display arcs may have been generated when another layer in the dataset was converted for display... re-use if available
9630
+ var displayArcs = dataset.displayArcs || null;
9631
+ var sourceCRS;
9632
+ var emptyArcs;
9549
9633
 
9550
- gui.on('box_drag_end', function(e) {
9551
- bboxPixels = e.map_bbox;
9552
- bboxDisplayCoords = pixToCoords(bboxPixels);
9553
- bboxDataCoords = getBBoxCoords(gui.map.getActiveLayer(), bboxDisplayCoords);
9554
- if (inZoomMode()) {
9555
- box.hide();
9556
- nav.zoomToBbox(bboxPixels);
9557
- } else if (_on) {
9558
- popup.show();
9634
+ if (displayCRS && layer.geometry_type) {
9635
+ var crsInfo = getDatasetCrsInfo(dataset);
9636
+ if (crsInfo.error) {
9637
+ // unprojectable dataset -- return empty layer
9638
+ obj.empty = true;
9639
+ obj.geographic = true;
9640
+ } else {
9641
+ sourceCRS = crsInfo.crs;
9559
9642
  }
9560
- });
9561
-
9562
- function inZoomMode() {
9563
- return !_on && gui.getMode() != 'selection_tool';
9564
9643
  }
9565
9644
 
9566
- function runCommand(cmd) {
9567
- if (gui.console) {
9568
- gui.console.runMapshaperCommands(cmd, function(err) {
9569
- reset();
9570
- });
9645
+ // Assume that dataset.displayArcs is in the display CRS
9646
+ // (it must be deleted upstream if reprojection is needed)
9647
+ if (!obj.empty && dataset.arcs && !displayArcs) {
9648
+ // project arcs, if needed
9649
+ if (needReprojectionForDisplay(sourceCRS, displayCRS)) {
9650
+ displayArcs = projectArcsForDisplay(dataset.arcs, sourceCRS, displayCRS);
9651
+ } else {
9652
+ // Use original arcs for display if there is no dynamic reprojection
9653
+ displayArcs = dataset.arcs;
9571
9654
  }
9572
- // reset(); // TODO: exit interactive mode
9573
- }
9574
9655
 
9575
- function showCoords() {
9576
- El(infoBtn.node()).addClass('selected-btn');
9577
- coords.text(bboxDataCoords.join(','));
9578
- coords.show();
9579
- GUI.selectElement(coords.node());
9656
+ enhanceArcCollectionForDisplay(displayArcs);
9657
+ dataset.displayArcs = displayArcs; // stash these in the dataset for other layers to use
9580
9658
  }
9581
9659
 
9582
- function hideCoords() {
9583
- El(infoBtn.node()).removeClass('selected-btn');
9584
- coords.hide();
9660
+ if (internal.layerHasFurniture(layer)) {
9661
+ obj.furniture = true;
9662
+ obj.furniture_type = internal.getFurnitureLayerType(layer);
9663
+ obj.layer = layer;
9664
+ // treating furniture layers (other than frame) as tabular for now,
9665
+ // so there is something to show if they are selected
9666
+ obj.tabular = obj.furniture_type != 'frame';
9667
+ } else if (obj.empty) {
9668
+ obj.layer = {shapes: []}; // ideally we should avoid empty layers
9669
+ } else if (!layer.geometry_type) {
9670
+ obj.tabular = true;
9671
+ } else {
9672
+ obj.geographic = true;
9673
+ obj.layer = layer;
9674
+ obj.arcs = displayArcs;
9585
9675
  }
9586
9676
 
9587
- function turnOn() {
9588
- _on = true;
9677
+ if (obj.tabular) {
9678
+ utils$1.extend(obj, getDisplayLayerForTable(layer.data));
9589
9679
  }
9590
9680
 
9591
- function turnOff() {
9592
- if (gui.interaction.getMode() == 'box') {
9593
- // mode change was not initiated by interactive menu -- turn off interactivity
9594
- gui.interaction.turnOff();
9681
+ // dynamic reprojection (arcs were already reprojected above)
9682
+ if (obj.geographic && needReprojectionForDisplay(sourceCRS, displayCRS)) {
9683
+ obj.dynamic_crs = displayCRS;
9684
+ obj.invertPoint = internal.getProjTransform2(displayCRS, sourceCRS);
9685
+ obj.projectPoint = internal.getProjTransform2(sourceCRS, displayCRS);
9686
+ if (internal.layerHasPoints(layer)) {
9687
+ obj.layer = projectPointsForDisplay(layer, sourceCRS, displayCRS);
9688
+ } else if (internal.layerHasPaths(layer)) {
9689
+ emptyArcs = findEmptyArcs(displayArcs);
9690
+ if (emptyArcs.length > 0) {
9691
+ // Don't try to draw paths containing coordinates that failed to project
9692
+ obj.layer = internal.filterPathLayerByArcIds(obj.layer, emptyArcs);
9693
+ }
9595
9694
  }
9596
- _on = false;
9597
- reset();
9598
9695
  }
9599
9696
 
9600
- function reset() {
9601
- box.hide();
9602
- popup.hide();
9603
- hideCoords();
9604
- }
9697
+ obj.bounds = getDisplayBounds(obj.layer, obj.arcs);
9698
+ return obj;
9699
+ }
9605
9700
 
9606
- function pixToCoords(bbox) {
9607
- var a = ext.translatePixelCoords(bbox[0], bbox[1]);
9608
- var b = ext.translatePixelCoords(bbox[2], bbox[3]);
9609
- var bbox2 = [a[0], b[1], b[0], a[1]];
9610
- // round coords, for nicer 'info' display
9611
- // (rounded precision should be sub-pixel)
9612
- return internal.getRoundedCoords(bbox2, internal.getBoundsPrecisionForDisplay(bbox2));
9701
+
9702
+ function getDisplayBounds(lyr, arcs) {
9703
+ var arcBounds = arcs ? arcs.getBounds() : new Bounds(),
9704
+ bounds = arcBounds, // default display extent: all arcs in the dataset
9705
+ lyrBounds;
9706
+
9707
+ if (lyr.geometry_type == 'point') {
9708
+ lyrBounds = internal.getLayerBounds(lyr);
9709
+ if (lyrBounds && lyrBounds.hasBounds()) {
9710
+ if (lyrBounds.area() > 0 || !arcBounds.hasBounds()) {
9711
+ bounds = lyrBounds;
9712
+ } else {
9713
+ // if a point layer has no extent (e.g. contains only a single point),
9714
+ // then merge with arc bounds, to place the point in context.
9715
+ bounds = arcBounds.mergeBounds(lyrBounds);
9716
+ }
9717
+ }
9613
9718
  }
9614
9719
 
9615
- function coordsToPix(bbox) {
9616
- var a = ext.translateCoords(bbox[0], bbox[1]);
9617
- var b = ext.translateCoords(bbox[2], bbox[3]);
9618
- return [a[0], b[1], b[0], a[1]];
9720
+ if (!bounds || !bounds.hasBounds()) { // empty layer
9721
+ bounds = new Bounds();
9619
9722
  }
9723
+ return bounds;
9724
+ }
9620
9725
 
9621
- return self;
9726
+ // Returns an array of ids of empty arcs (arcs can be set to empty if errors occur while projecting them)
9727
+ function findEmptyArcs(arcs) {
9728
+ var nn = arcs.getVertexData().nn;
9729
+ var ids = [];
9730
+ for (var i=0, n=nn.length; i<n; i++) {
9731
+ if (nn[i] === 0) {
9732
+ ids.push(i);
9733
+ }
9734
+ }
9735
+ return ids;
9622
9736
  }
9623
9737
 
9624
9738
  function loadScript(url, cb) {
@@ -9659,10 +9773,6 @@
9659
9773
 
9660
9774
  function init() {
9661
9775
  gui.addMode('basemap', turnOn, turnOff, basemapBtn);
9662
- // model.on('select', function() {
9663
- // TODO: hide basemap
9664
- // if (gui.getMode() == 'basemap') gui.clearMode();
9665
- // });
9666
9776
 
9667
9777
  new SimpleButton(menu.findChild('.close-btn')).on('click', function() {
9668
9778
  gui.clearMode();
@@ -9686,6 +9796,9 @@
9686
9796
 
9687
9797
  function updateStyle(style) {
9688
9798
  activeStyle = style || null;
9799
+ // TODO: consider enabling this
9800
+ // Make sure that the selected layer style gets updated in gui-map.js
9801
+ // gui.state.dark_basemap = style && style.dark || false;
9689
9802
  if (!style) {
9690
9803
  gui.map.setDisplayCRS(null);
9691
9804
  hide();
@@ -9705,11 +9818,12 @@
9705
9818
 
9706
9819
  function turnOn() {
9707
9820
  var activeLyr = gui.model.getActiveLayer();
9708
- var dataCRS = internal.getDatasetCRS(activeLyr.dataset);
9821
+ var info = getDatasetCrsInfo(activeLyr.dataset);
9822
+ var dataCRS = info.crs || null;
9709
9823
  var displayCRS = gui.map.getDisplayCRS();
9710
9824
  var warning;
9711
9825
 
9712
- if (!crsIsUsable(displayCRS) || !crsIsUsable(dataCRS)) {
9826
+ if (!dataCRS || !displayCRS || !crsIsUsable(displayCRS) || !crsIsUsable(dataCRS)) {
9713
9827
  warning = 'The current layer is not compatible with the projection used by the basemaps.';
9714
9828
  basemapWarning.html(warning).show();
9715
9829
  basemapNote.hide();
@@ -9774,16 +9888,25 @@
9774
9888
  });
9775
9889
  }
9776
9890
 
9891
+ // @bbox: latlon bounding box of current map extent
9777
9892
  function checkBounds(bbox) {
9893
+ var mpp = ext.getBounds().width() / ext.width();
9894
+ var z = scaleToZoom(mpp);
9778
9895
  var msg;
9779
- if (bbox[1] >= -85 && bbox[3] <= 85) {
9896
+ if (bbox[1] >= -85 && bbox[3] <= 85 && z <= 20) {
9780
9897
  extentNote.hide();
9781
9898
  return true;
9782
9899
  }
9783
- if (bbox[1] > 0) msg = 'pan south to see the basemap';
9784
- else if (bbox[3] < 0) msg = 'pan north to see the basemap';
9785
- else msg = msg = 'zoom in to see the basemap';
9786
- extentNote.html(msg).show();
9900
+ if (z > 20) {
9901
+ msg = 'zoom out';
9902
+ } else if (bbox[1] > 0) {
9903
+ msg = 'pan south';
9904
+ } else if (bbox[3] < 0) {
9905
+ msg = 'pan north';
9906
+ } else {
9907
+ msg = msg = 'zoom in';
9908
+ }
9909
+ extentNote.html(msg + ' to see the basemap').show();
9787
9910
  return false;
9788
9911
  }
9789
9912
 
@@ -9899,11 +10022,10 @@
9899
10022
  };
9900
10023
 
9901
10024
  this.getDisplayCRS = function() {
9902
- var crs;
9903
- if (_activeLyr && _activeLyr.geographic) {
9904
- crs = _activeLyr.dynamic_crs || internal.getDatasetCRS(_activeLyr.source.dataset);
9905
- }
9906
- return crs || null;
10025
+ if (!_activeLyr || !_activeLyr.geographic) return null;
10026
+ if (_activeLyr.dynamic_crs) return _activeLyr.dynamic_crs;
10027
+ var info = getDatasetCrsInfo(_activeLyr.source.dataset);
10028
+ return info.crs || null;
9907
10029
  };
9908
10030
 
9909
10031
  this.getExtent = function() {return _ext;};
@@ -9976,7 +10098,8 @@
9976
10098
  }
9977
10099
 
9978
10100
  _activeLyr = getDisplayLayer(e.layer, e.dataset, getDisplayOptions());
9979
- _activeLyr.style = getActiveStyle(_activeLyr.layer);
10101
+ _activeLyr.style = getActiveStyle(_activeLyr.layer, gui.state.dark_basemap);
10102
+
9980
10103
  _activeLyr.active = true;
9981
10104
  // if (_inspector) _inspector.updateLayer(_activeLyr);
9982
10105
  _hit.setLayer(_activeLyr);
@@ -9995,7 +10118,6 @@
9995
10118
  needReset = mapNeedsReset(fullBounds, _fullBounds, _ext.getBounds(), e.flags);
9996
10119
  }
9997
10120
 
9998
-
9999
10121
  if (isFrameView()) {
10000
10122
  _nav.setZoomFactor(0.05); // slow zooming way down to allow fine-tuning frame placement // 0.03
10001
10123
  _ext.setFrame(getFullBounds()); // TODO: remove redundancy with drawLayers()
@@ -10003,7 +10125,7 @@
10003
10125
  } else {
10004
10126
  _nav.setZoomFactor(1);
10005
10127
  }
10006
- _ext.setBounds(fullBounds); // update 'home' button extent
10128
+ _ext.setBounds(fullBounds, getStrictBounds()); // update 'home' button extent
10007
10129
  _fullBounds = fullBounds;
10008
10130
  if (needReset) {
10009
10131
  _ext.reset();
@@ -10093,6 +10215,13 @@
10093
10215
  _ext.reset();
10094
10216
  }
10095
10217
 
10218
+ function getStrictBounds() {
10219
+ if (internal.isWebMercator(map.getDisplayCRS())) {
10220
+ return getMapboxBounds();
10221
+ }
10222
+ return null;
10223
+ }
10224
+
10096
10225
  function getFullBounds() {
10097
10226
  var b = new Bounds();
10098
10227
  var marginPct = 0.025;
@@ -10238,11 +10367,16 @@
10238
10367
  });
10239
10368
  }
10240
10369
 
10370
+ function drawLayers(action) {
10371
+ // This seems to smooth out navigation and keep overlay and basemap in sync.
10372
+ requestAnimationFrame(function() {drawLayers2(action);});
10373
+ }
10374
+
10241
10375
  // action:
10242
10376
  // 'nav' map was panned/zoomed -- only map extent has changed
10243
10377
  // 'hover' highlight has changed -- only draw overlay
10244
10378
  // (default) anything could have changed
10245
- function drawLayers(action) {
10379
+ function drawLayers2(action) {
10246
10380
  var layersMayHaveChanged = !action;
10247
10381
  var contentLayers = getDrawableContentLayers();
10248
10382
  var furnitureLayers = getDrawableFurnitureLayers();
@@ -10254,7 +10388,7 @@
10254
10388
  if (layersMayHaveChanged) {
10255
10389
  // kludge to handle layer visibility toggling
10256
10390
  _ext.setFrame(isPreviewView() ? getFrameData() : null);
10257
- _ext.setBounds(getFullBounds());
10391
+ _ext.setBounds(getFullBounds(), getStrictBounds());
10258
10392
  updateLayerStyles(contentLayers);
10259
10393
  updateLayerStackOrder(model.getLayers());// update stack_id property of all layers
10260
10394
  }
@@ -10272,6 +10406,40 @@
10272
10406
  }
10273
10407
  }
10274
10408
 
10409
+ // This is a new way to handle compatibility problems between
10410
+ // interactive editing modes and other interface modes
10411
+ // (by default, interactive modes stay on when, e.g., the user clicks
10412
+ // "Export" or "Console").
10413
+ //
10414
+ function initModeRules(gui) {
10415
+
10416
+ gui.on('interaction_mode_change', function(e) {
10417
+ var imode = e.mode;
10418
+ var mode = gui.getMode();
10419
+
10420
+ // simplify and vertex editing are not compatible
10421
+ if (imode == 'vertices') {
10422
+ flattenArcs(gui.map.getActiveLayer());
10423
+
10424
+ if (mode == 'simplify') {
10425
+ gui.clearMode(); // exit simplification
10426
+ }
10427
+
10428
+ }
10429
+
10430
+ });
10431
+
10432
+ gui.on('mode', function(e) {
10433
+ var mode = e.name;
10434
+ var imode = gui.interaction.getMode();
10435
+
10436
+ // simplify and vertex editing are not compatible
10437
+ if (mode == 'simplify' && imode == 'vertices') {
10438
+ gui.interaction.turnOff();
10439
+ }
10440
+ });
10441
+ }
10442
+
10275
10443
  function GuiInstance(container, opts) {
10276
10444
  var gui = new ModeSwitcher();
10277
10445
  opts = utils$1.extend({
@@ -10295,6 +10463,8 @@
10295
10463
  gui.undo = new Undo(gui);
10296
10464
  gui.state = {};
10297
10465
 
10466
+ initModeRules(gui);
10467
+
10298
10468
  gui.showProgressMessage = function(msg) {
10299
10469
  if (!gui.progressMessage) {
10300
10470
  gui.progressMessage = El('div').addClass('progress-message')
@@ -10319,7 +10489,7 @@
10319
10489
  curr.blur();
10320
10490
  }
10321
10491
  GUI.__active = gui;
10322
- MessageProxy(gui);
10492
+ setLoggingForGUI(gui);
10323
10493
  ImportFileProxy(gui);
10324
10494
  WriteFilesProxy(gui);
10325
10495
  gui.dispatchEvent('active');