mapshaper 0.7.50 → 0.7.52
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.
- package/mapshaper.js +494 -36
- package/package.json +3 -2
- package/www/mapshaper-gui.js +467 -125
- package/www/mapshaper.js +494 -36
- package/www/page.css +5 -0
package/www/mapshaper-gui.js
CHANGED
|
@@ -6876,31 +6876,37 @@
|
|
|
6876
6876
|
// instead.
|
|
6877
6877
|
|
|
6878
6878
|
|
|
6879
|
+
// Undo is on unless it has been turned off. Undoing is what a user expects by
|
|
6880
|
+
// default; the History menu's checkbox exists so that sessions working with
|
|
6881
|
+
// data too large to store restore payloads for can opt out.
|
|
6882
|
+
var APP_UNDO_DEFAULT = true;
|
|
6883
|
+
|
|
6879
6884
|
// Returns true if the manifest, URL query, gui-installed checker, or
|
|
6880
6885
|
// localStorage indicates that app-level undo should be active.
|
|
6881
6886
|
function appUndoIsEnabled(gui) {
|
|
6882
6887
|
var opt = gui && gui.options && (gui.options.undoCommands || gui.options.appUndo);
|
|
6883
6888
|
var query = getUndoQueryValue();
|
|
6889
|
+
if (query == 'off') return false;
|
|
6884
6890
|
if (opt === true || query == 'on' || query == 'commands') return true;
|
|
6885
6891
|
if (gui && gui.appUndoIsEnabled) return gui.appUndoIsEnabled();
|
|
6886
6892
|
return appUndoSettingIsOn();
|
|
6887
6893
|
}
|
|
6888
6894
|
|
|
6889
|
-
// True when the undo URL flag
|
|
6890
|
-
// Mirrors the toggle-disable behavior in HistoryMenu.
|
|
6895
|
+
// True when the undo URL flag pins app undo on or off regardless of UI
|
|
6896
|
+
// settings. Mirrors the toggle-disable behavior in HistoryMenu.
|
|
6891
6897
|
function appUndoForcedByUrl() {
|
|
6892
6898
|
var query = getUndoQueryValue();
|
|
6893
|
-
return query == 'on' || query == 'commands';
|
|
6899
|
+
return query == 'on' || query == 'commands' || query == 'off';
|
|
6894
6900
|
}
|
|
6895
6901
|
|
|
6896
|
-
// Read the persisted
|
|
6897
|
-
//
|
|
6902
|
+
// Read the persisted setting. Only an explicit opt-out turns undo off, so a
|
|
6903
|
+
// first-time session (and one where storage is unreadable) gets the default.
|
|
6898
6904
|
function appUndoSettingIsOn() {
|
|
6899
6905
|
try {
|
|
6900
|
-
|
|
6901
|
-
|
|
6906
|
+
if (typeof window == 'undefined' || !window.localStorage) return APP_UNDO_DEFAULT;
|
|
6907
|
+
return window.localStorage.getItem('mapshaper.undo') != 'off';
|
|
6902
6908
|
} catch(e) {
|
|
6903
|
-
return
|
|
6909
|
+
return APP_UNDO_DEFAULT;
|
|
6904
6910
|
}
|
|
6905
6911
|
}
|
|
6906
6912
|
|
|
@@ -8067,6 +8073,15 @@
|
|
|
8067
8073
|
});
|
|
8068
8074
|
}
|
|
8069
8075
|
|
|
8076
|
+
// A .aux.xml sidecar is named after the whole raster file
|
|
8077
|
+
// (world.tif.aux.xml), so it joins that file's group rather than one of its
|
|
8078
|
+
// own. Returns null if the raster it names was not loaded with it.
|
|
8079
|
+
function auxSourceGroup(d) {
|
|
8080
|
+
var sourceName = internal.getAuxSourceFilename(d.name).toLowerCase();
|
|
8081
|
+
var source = data.find(f => f.name.toLowerCase() == sourceName);
|
|
8082
|
+
return source ? key(fileBase(source), fileType(source)) : null;
|
|
8083
|
+
}
|
|
8084
|
+
|
|
8070
8085
|
data.forEach(d => {
|
|
8071
8086
|
var basename = fileBase(d);
|
|
8072
8087
|
var type = fileType(d);
|
|
@@ -8078,6 +8093,8 @@
|
|
|
8078
8093
|
} else if (type == 'dbf') {
|
|
8079
8094
|
d.filename = d.name;
|
|
8080
8095
|
d.group = key(basename, 'dbf');
|
|
8096
|
+
} else if (type == 'aux' && auxSourceGroup(d)) {
|
|
8097
|
+
d.group = auxSourceGroup(d);
|
|
8081
8098
|
} else if ((type == 'png' || type == 'jpeg') || isRasterImagePart(d.name) && hasRasterImage(basename)) {
|
|
8082
8099
|
d.group = key(basename, type == 'png' || type == 'jpeg' ? type : getRasterImageGroupType(data, basename));
|
|
8083
8100
|
if (type == 'png' || type == 'jpeg') d.filename = d.name;
|
|
@@ -8916,6 +8933,44 @@
|
|
|
8916
8933
|
}
|
|
8917
8934
|
|
|
8918
8935
|
|
|
8936
|
+
// Divide an arc at one or two cut locations, appending the pieces to the end of
|
|
8937
|
+
// the arc collection (the original arc is left in place).
|
|
8938
|
+
// cuts: cut descriptors ordered from the start of the arc, each containing an
|
|
8939
|
+
// arc-relative vertex offset plus source and display coordinates of the cut
|
|
8940
|
+
// location (both null when cutting at an existing vertex).
|
|
8941
|
+
// Returns the ids of the new arcs.
|
|
8942
|
+
function splitArc(lyr, arcId, cuts) {
|
|
8943
|
+
var ids = internal.splitArcAtCuts(lyr.gui.source.dataset.arcs, arcId,
|
|
8944
|
+
cuts.map(function(cut) {
|
|
8945
|
+
return {offset: cut.offset, point: cut.point || null};
|
|
8946
|
+
}));
|
|
8947
|
+
if (isProjectedLayer(lyr)) {
|
|
8948
|
+
internal.splitArcAtCuts(lyr.gui.displayArcs, arcId,
|
|
8949
|
+
cuts.map(function(cut) {
|
|
8950
|
+
return {offset: cut.offset, point: cut.displayPoint || null};
|
|
8951
|
+
}));
|
|
8952
|
+
}
|
|
8953
|
+
return ids;
|
|
8954
|
+
}
|
|
8955
|
+
|
|
8956
|
+
function deleteLastArcs(lyr, count) {
|
|
8957
|
+
for (var i=0; i<count; i++) {
|
|
8958
|
+
internal.deleteLastArc(lyr.gui.source.dataset.arcs);
|
|
8959
|
+
if (isProjectedLayer(lyr)) {
|
|
8960
|
+
internal.deleteLastArc(lyr.gui.displayArcs);
|
|
8961
|
+
}
|
|
8962
|
+
}
|
|
8963
|
+
}
|
|
8964
|
+
|
|
8965
|
+
// Add a path feature to the end of a layer.
|
|
8966
|
+
// shp: an array of parts; rec: an attribute record, or null for an empty one
|
|
8967
|
+
function appendFeature(lyr, shp, rec) {
|
|
8968
|
+
lyr.shapes.push(shp);
|
|
8969
|
+
if (lyr.data) {
|
|
8970
|
+
lyr.data.getRecords().push(rec || getEmptyDataRecord(lyr.data));
|
|
8971
|
+
}
|
|
8972
|
+
}
|
|
8973
|
+
|
|
8919
8974
|
function deleteLastPath(lyr) {
|
|
8920
8975
|
var arcId = lyr.gui.displayArcs.size() - 1;
|
|
8921
8976
|
if (lyr.data) {
|
|
@@ -10641,6 +10696,8 @@
|
|
|
10641
10696
|
var unsupportedMsg = "Exporting is not supported in this browser";
|
|
10642
10697
|
var menu = gui.container.findChild('.export-options').on('click', GUI.handleDirectEvent(gui.clearMode));
|
|
10643
10698
|
var layersArr = [];
|
|
10699
|
+
var menuFormats = null; // formats the format menu was built with
|
|
10700
|
+
var formatPickedByUser = false;
|
|
10644
10701
|
var toggleBtn = null; // checkbox <input> for toggling layer selection
|
|
10645
10702
|
var exportBtn = gui.container.findChild('.export-btn').addClass('disabled');
|
|
10646
10703
|
var ofileName = gui.container.findChild('#ofile-name');
|
|
@@ -10709,6 +10766,7 @@
|
|
|
10709
10766
|
|
|
10710
10767
|
function turnOn() {
|
|
10711
10768
|
layersArr = initLayerMenu();
|
|
10769
|
+
formatPickedByUser = false;
|
|
10712
10770
|
// initZipOption();
|
|
10713
10771
|
initFormatMenu();
|
|
10714
10772
|
updateExportCheckboxes();
|
|
@@ -10890,6 +10948,7 @@
|
|
|
10890
10948
|
}
|
|
10891
10949
|
|
|
10892
10950
|
function updateToggleBtn() {
|
|
10951
|
+
refreshFormatMenu(); // the layer selection decides which formats apply
|
|
10893
10952
|
updateExportCheckboxes(); // checkbox visibility is affected by number of export layers
|
|
10894
10953
|
if (!toggleBtn) return;
|
|
10895
10954
|
var state = getSelectionState();
|
|
@@ -10912,13 +10971,17 @@
|
|
|
10912
10971
|
}
|
|
10913
10972
|
|
|
10914
10973
|
function getDefaultExportFormat() {
|
|
10974
|
+
var layers = getExportFormatLayers();
|
|
10915
10975
|
var active = model.getActiveLayer();
|
|
10916
10976
|
var dataset = active.dataset;
|
|
10917
10977
|
var inputFmt = dataset.info && dataset.info.input_formats &&
|
|
10918
10978
|
dataset.info.input_formats[0];
|
|
10919
|
-
//
|
|
10920
|
-
//
|
|
10921
|
-
if (
|
|
10979
|
+
// Rasters and nothing else can only go to GeoTIFF, the one format that
|
|
10980
|
+
// takes their pixels.
|
|
10981
|
+
if (layers.length > 0 && layers.every(hasRaster)) return 'geotiff';
|
|
10982
|
+
// SVG is the default for a selection that mixes rasters with vector layers,
|
|
10983
|
+
// being the only format that accepts both.
|
|
10984
|
+
if (layers.some(hasRaster)) return 'svg';
|
|
10922
10985
|
return getExportFormats().includes(inputFmt) ? inputFmt : 'geojson';
|
|
10923
10986
|
}
|
|
10924
10987
|
|
|
@@ -10926,16 +10989,26 @@
|
|
|
10926
10989
|
var formats = ['shapefile', 'json', 'geojson', 'dsv', 'topojson', 'flatgeobuf', 'geopackage', 'geoparquet', 'kml', 'svg', internal.PACKAGE_EXT];
|
|
10927
10990
|
// GeoTIFF is the one format here that only accepts raster layers, so it is
|
|
10928
10991
|
// offered only when there is a raster to export.
|
|
10929
|
-
if (
|
|
10992
|
+
if (getExportFormatLayers().some(hasRaster)) formats.push('geotiff');
|
|
10930
10993
|
return formats;
|
|
10931
10994
|
}
|
|
10932
10995
|
|
|
10933
|
-
|
|
10996
|
+
// The layers the format menu describes: the ones checked for export, falling
|
|
10997
|
+
// back to the active layer when the menu is not up yet or nothing is checked.
|
|
10998
|
+
function getExportFormatLayers() {
|
|
10999
|
+
var selected = layersArr.length ? getSelectedLayerEntries() : [];
|
|
10934
11000
|
var active = model.getActiveLayer();
|
|
10935
|
-
|
|
11001
|
+
if (selected.length > 0) {
|
|
11002
|
+
return selected.map(function(o) { return o.layer; });
|
|
11003
|
+
}
|
|
11004
|
+
return active && active.layer ? [active.layer] : [];
|
|
11005
|
+
}
|
|
11006
|
+
|
|
11007
|
+
function hasRaster(lyr) {
|
|
11008
|
+
return internal.layerHasRaster(lyr);
|
|
10936
11009
|
}
|
|
10937
11010
|
|
|
10938
|
-
function initFormatMenu() {
|
|
11011
|
+
function initFormatMenu(preferredFmt) {
|
|
10939
11012
|
var formats = getExportFormats();
|
|
10940
11013
|
var items = formats.map(function(fmt) {
|
|
10941
11014
|
return utils$1.format('<td><label><input type="radio" name="format" value="%s"' +
|
|
@@ -10947,13 +11020,39 @@
|
|
|
10947
11020
|
}
|
|
10948
11021
|
table += '</table>';
|
|
10949
11022
|
menu.findChild('.export-formats').html(table);
|
|
10950
|
-
|
|
11023
|
+
menuFormats = formats;
|
|
11024
|
+
setSelectedFormat(formats.includes(preferredFmt) ? preferredFmt :
|
|
11025
|
+
getDefaultExportFormat());
|
|
10951
11026
|
// update save-as settings when value changes
|
|
10952
11027
|
menu.findChildren('input[type="radio"]').forEach(el => {
|
|
10953
|
-
el.on('change',
|
|
11028
|
+
el.on('change', onFormatChange);
|
|
10954
11029
|
});
|
|
10955
11030
|
}
|
|
10956
11031
|
|
|
11032
|
+
function onFormatChange() {
|
|
11033
|
+
// A format the user picked is theirs to keep, however the layer selection
|
|
11034
|
+
// changes afterwards.
|
|
11035
|
+
formatPickedByUser = true;
|
|
11036
|
+
updateExportCheckboxes();
|
|
11037
|
+
}
|
|
11038
|
+
|
|
11039
|
+
function setSelectedFormat(fmt) {
|
|
11040
|
+
var el = menu.findChild('.export-formats input[value="' + fmt + '"]');
|
|
11041
|
+
if (el) el.node().checked = true;
|
|
11042
|
+
}
|
|
11043
|
+
|
|
11044
|
+
// Which formats apply depends on what is checked for export, so unchecking
|
|
11045
|
+
// the vector layers of a mixed session both offers GeoTIFF and makes it the
|
|
11046
|
+
// default -- unless the user has already chosen a format.
|
|
11047
|
+
function refreshFormatMenu() {
|
|
11048
|
+
var formats = getExportFormats();
|
|
11049
|
+
if (formats.join(',') != (menuFormats || []).join(',')) {
|
|
11050
|
+
initFormatMenu(formatPickedByUser ? getSelectedFormat() : null);
|
|
11051
|
+
} else if (!formatPickedByUser) {
|
|
11052
|
+
setSelectedFormat(getDefaultExportFormat());
|
|
11053
|
+
}
|
|
11054
|
+
}
|
|
11055
|
+
|
|
10957
11056
|
|
|
10958
11057
|
// function getInputFormats() {
|
|
10959
11058
|
// return model.getDatasets().reduce(function(memo, d) {
|
|
@@ -12095,6 +12194,7 @@
|
|
|
12095
12194
|
}
|
|
12096
12195
|
|
|
12097
12196
|
function isAppUndoEnabled() {
|
|
12197
|
+
if (getUndoQueryValue() == 'off') return false;
|
|
12098
12198
|
return appUndoForcedByUrl() || appUndoSettingIsOn();
|
|
12099
12199
|
}
|
|
12100
12200
|
|
|
@@ -12461,6 +12561,60 @@
|
|
|
12461
12561
|
}
|
|
12462
12562
|
}
|
|
12463
12563
|
|
|
12564
|
+
// Divide one path of one feature at one location (open path) or two locations
|
|
12565
|
+
// (ring). See internal.planPathDivision() for the cut format.
|
|
12566
|
+
//
|
|
12567
|
+
// The shorter of the two resulting paths is split off into a new feature at the
|
|
12568
|
+
// end of the layer, with a copy of the original feature's attributes; the
|
|
12569
|
+
// longer one stays in place. For a multipart feature this leaves the longer
|
|
12570
|
+
// path connected to the rest of the feature.
|
|
12571
|
+
//
|
|
12572
|
+
// Returns a descriptor for undoing the edit, or null if the cuts are degenerate.
|
|
12573
|
+
//
|
|
12574
|
+
function snipPath(lyr, fid, partId, cuts) {
|
|
12575
|
+
var arcs = lyr.gui.source.dataset.arcs;
|
|
12576
|
+
var shp = lyr.shapes[fid];
|
|
12577
|
+
var path = shp[partId];
|
|
12578
|
+
var closed = geom.pathIsClosed(path, arcs);
|
|
12579
|
+
var prevShape = shp.map(function(part) { return part.concat(); });
|
|
12580
|
+
var arcCount = arcs.size();
|
|
12581
|
+
var plan = internal.planPathDivision(path, cuts, function(arcId) {
|
|
12582
|
+
return arcs.getVertexData().nn[arcId];
|
|
12583
|
+
}, arcCount, closed);
|
|
12584
|
+
var newShape, keep, remove, rec;
|
|
12585
|
+
if (!plan) return null;
|
|
12586
|
+
plan.splits.forEach(function(split) {
|
|
12587
|
+
splitArc(lyr, split.arcId, split.cuts);
|
|
12588
|
+
});
|
|
12589
|
+
if (internal.getPlanarPathLength(plan.a, arcs) >=
|
|
12590
|
+
internal.getPlanarPathLength(plan.b, arcs)) {
|
|
12591
|
+
keep = plan.a;
|
|
12592
|
+
remove = plan.b;
|
|
12593
|
+
} else {
|
|
12594
|
+
keep = plan.b;
|
|
12595
|
+
remove = plan.a;
|
|
12596
|
+
}
|
|
12597
|
+
newShape = shp.concat();
|
|
12598
|
+
newShape[partId] = keep;
|
|
12599
|
+
lyr.shapes[fid] = newShape;
|
|
12600
|
+
rec = lyr.data ? internal.copyRecord(lyr.data.getRecordAt(fid)) : null;
|
|
12601
|
+
appendFeature(lyr, [remove], rec);
|
|
12602
|
+
return {
|
|
12603
|
+
fid: fid,
|
|
12604
|
+
partId: partId,
|
|
12605
|
+
cuts: cuts,
|
|
12606
|
+
prevShape: prevShape,
|
|
12607
|
+
newFeatureId: lyr.shapes.length - 1,
|
|
12608
|
+
appendedArcCount: arcs.size() - arcCount
|
|
12609
|
+
};
|
|
12610
|
+
}
|
|
12611
|
+
|
|
12612
|
+
function undoSnip(lyr, result) {
|
|
12613
|
+
deleteFeature(lyr, result.newFeatureId);
|
|
12614
|
+
lyr.shapes[result.fid] = result.prevShape;
|
|
12615
|
+
deleteLastArcs(lyr, result.appendedArcCount);
|
|
12616
|
+
}
|
|
12617
|
+
|
|
12464
12618
|
var copyRecord = internal.copyRecord;
|
|
12465
12619
|
|
|
12466
12620
|
function isUndoEvt(e) {
|
|
@@ -12631,6 +12785,22 @@
|
|
|
12631
12785
|
addHistoryState(undo, redo);
|
|
12632
12786
|
});
|
|
12633
12787
|
|
|
12788
|
+
gui.on('snip', function(e) {
|
|
12789
|
+
var target = e.target;
|
|
12790
|
+
var result = e.result;
|
|
12791
|
+
var undo = function() {
|
|
12792
|
+
undoSnip(target, result);
|
|
12793
|
+
gui.model.updated({arc_count: true});
|
|
12794
|
+
};
|
|
12795
|
+
var redo = function() {
|
|
12796
|
+
// re-snipping restores the same arc ids, because the arcs added by the
|
|
12797
|
+
// previous snip were removed from the end of the collection
|
|
12798
|
+
result = snipPath(target, result.fid, result.partId, result.cuts);
|
|
12799
|
+
gui.model.updated({arc_count: true});
|
|
12800
|
+
};
|
|
12801
|
+
addHistoryState(undo, redo);
|
|
12802
|
+
});
|
|
12803
|
+
|
|
12634
12804
|
gui.on('point_add', function(e) {
|
|
12635
12805
|
var redo = function() {
|
|
12636
12806
|
appendNewPoint(e.data.target, e.p);
|
|
@@ -13112,7 +13282,7 @@
|
|
|
13112
13282
|
empty: ['edit_polygons', 'edit_lines', 'edit_points', 'box', 'ruler'],
|
|
13113
13283
|
polygons: ['info', 'selection', 'box', 'polygon_style', 'edit_polygons', 'ruler'],
|
|
13114
13284
|
rectangles: ['info', 'selection', 'box', 'polygon_style', 'rectangles', 'edit_polygons', 'ruler'],
|
|
13115
|
-
lines: ['info', 'selection', 'box', 'line_style', 'edit_lines', '
|
|
13285
|
+
lines: ['info', 'selection', 'box', 'line_style', 'edit_lines', 'snip_lines', 'ruler'],
|
|
13116
13286
|
table: ['info', 'selection'],
|
|
13117
13287
|
raster: ['ruler', 'box'],
|
|
13118
13288
|
labels: ['info', 'selection', 'box', 'point_style', 'labels', 'edit_points', 'ruler'],
|
|
@@ -13216,7 +13386,7 @@
|
|
|
13216
13386
|
};
|
|
13217
13387
|
|
|
13218
13388
|
this.modeSupportsUndo = function(mode) {
|
|
13219
|
-
return ['data', 'label_style', 'point_style', 'line_style', 'polygon_style', 'labels', 'edit_points', 'edit_lines', 'edit_polygons', 'vertices', 'rectangles'].includes(mode);
|
|
13389
|
+
return ['data', 'label_style', 'point_style', 'line_style', 'polygon_style', 'labels', 'edit_points', 'edit_lines', 'edit_polygons', 'snip_lines', 'vertices', 'rectangles'].includes(mode);
|
|
13220
13390
|
};
|
|
13221
13391
|
|
|
13222
13392
|
this.getMode = getInteractionMode;
|
|
@@ -16745,7 +16915,8 @@
|
|
|
16745
16915
|
test = polygonVertexTest;
|
|
16746
16916
|
} else if (
|
|
16747
16917
|
interactionMode == 'vertices' ||
|
|
16748
|
-
interactionMode == 'edit_lines'
|
|
16918
|
+
interactionMode == 'edit_lines' ||
|
|
16919
|
+
interactionMode == 'snip_lines') {
|
|
16749
16920
|
test = vertexTest;
|
|
16750
16921
|
} else if (geoType == 'polyline') {
|
|
16751
16922
|
test = polylineTest;
|
|
@@ -17270,6 +17441,22 @@
|
|
|
17270
17441
|
triggerChangeEvent();
|
|
17271
17442
|
};
|
|
17272
17443
|
|
|
17444
|
+
// Mark the location of a snip that has been chosen but not yet applied
|
|
17445
|
+
// (the first of the two cuts needed to divide a ring).
|
|
17446
|
+
self.setPendingSnip = function(p) {
|
|
17447
|
+
var p2 = storedData.snip_coordinates;
|
|
17448
|
+
if (!active || !p) return;
|
|
17449
|
+
if (p2 && p2[0] == p[0] && p2[1] == p[1]) return;
|
|
17450
|
+
storedData.snip_coordinates = p;
|
|
17451
|
+
triggerChangeEvent();
|
|
17452
|
+
};
|
|
17453
|
+
|
|
17454
|
+
self.clearPendingSnip = function() {
|
|
17455
|
+
if (!storedData.snip_coordinates) return;
|
|
17456
|
+
delete storedData.snip_coordinates;
|
|
17457
|
+
triggerChangeEvent();
|
|
17458
|
+
};
|
|
17459
|
+
|
|
17273
17460
|
self.clearSelection = function() {
|
|
17274
17461
|
updateSelectionState(null);
|
|
17275
17462
|
};
|
|
@@ -17522,6 +17709,11 @@
|
|
|
17522
17709
|
(type == 'hover' || type == 'dblclick')) {
|
|
17523
17710
|
return true; // special case -- using hover for line drawing animation
|
|
17524
17711
|
}
|
|
17712
|
+
if (mode == 'snip_lines' && (type == 'hover' || type == 'click')) {
|
|
17713
|
+
// the snip tool tracks vertices under the pointer, so it needs hover
|
|
17714
|
+
// events as soon as a feature is hit, not one event later
|
|
17715
|
+
return true;
|
|
17716
|
+
}
|
|
17525
17717
|
|
|
17526
17718
|
// ignore pointer events when no features are being hit
|
|
17527
17719
|
// (don't block pan and other navigation when events aren't being used for editing)
|
|
@@ -17539,7 +17731,8 @@
|
|
|
17539
17731
|
}
|
|
17540
17732
|
|
|
17541
17733
|
function possiblyStopPropagation(e) {
|
|
17542
|
-
|
|
17734
|
+
var mode = interactionMode();
|
|
17735
|
+
if (mode == 'edit_lines' || mode == 'edit_polygons' || mode == 'snip_lines') {
|
|
17543
17736
|
// handled conditionally in the control
|
|
17544
17737
|
return;
|
|
17545
17738
|
}
|
|
@@ -19127,6 +19320,7 @@
|
|
|
19127
19320
|
// hoverFill = "rgba(255, 120, 255, 0.12)",
|
|
19128
19321
|
hoverFill = "rgba(0, 0, 0, 0.08)",
|
|
19129
19322
|
grey = "#888",
|
|
19323
|
+
orange = "#f28100",
|
|
19130
19324
|
violet = "#cc6acc",
|
|
19131
19325
|
black = 'black',
|
|
19132
19326
|
violetFill = "rgba(249, 120, 249, 0.25)",
|
|
@@ -19358,20 +19552,30 @@
|
|
|
19358
19552
|
|
|
19359
19553
|
// style for vertex edit mode
|
|
19360
19554
|
function getLineEditingStyle(o) {
|
|
19555
|
+
var isVertex = o.hit_type == 'vertex' || o.hit_type == 'disabled';
|
|
19361
19556
|
return {
|
|
19362
19557
|
ids: o.ids,
|
|
19363
19558
|
overlay: true,
|
|
19364
19559
|
strokeColor: black,
|
|
19365
19560
|
strokeWidth: 1.2,
|
|
19366
19561
|
vertices: true,
|
|
19367
|
-
vertex_overlay_color: o.hit_type
|
|
19368
|
-
vertex_overlay_scale:
|
|
19562
|
+
vertex_overlay_color: getVertexOverlayColor(o.hit_type),
|
|
19563
|
+
vertex_overlay_scale: isVertex ? 2.5 : 2,
|
|
19369
19564
|
vertex_overlay: o.hit_coordinates || null,
|
|
19565
|
+
pending_snip: o.snip_coordinates || null,
|
|
19566
|
+
pending_snip_color: orange,
|
|
19370
19567
|
selected_points: o.selected_points || null,
|
|
19371
19568
|
fillColor: null
|
|
19372
19569
|
};
|
|
19373
19570
|
}
|
|
19374
19571
|
|
|
19572
|
+
function getVertexOverlayColor(hitType) {
|
|
19573
|
+
if (hitType == 'vertex') return violet;
|
|
19574
|
+
// a muted dot marks a vertex that the current tool can not act on
|
|
19575
|
+
if (hitType == 'disabled') return grey;
|
|
19576
|
+
return black;
|
|
19577
|
+
}
|
|
19578
|
+
|
|
19375
19579
|
function getSelectedFeatureStyle(lyr, o, opts) {
|
|
19376
19580
|
var isPinned = o.pinned;
|
|
19377
19581
|
var inSelection = o.ids.indexOf(o.id) > -1;
|
|
@@ -20418,147 +20622,275 @@
|
|
|
20418
20622
|
}
|
|
20419
20623
|
}
|
|
20420
20624
|
|
|
20421
|
-
|
|
20422
|
-
// find the feature part and arc containing the vertex
|
|
20423
|
-
// divide the one feature into two features
|
|
20424
|
-
// divide the arc into two new arcs
|
|
20425
|
-
var arc1, arc2;
|
|
20426
|
-
// divide the feature into two parts
|
|
20427
|
-
}
|
|
20428
|
-
|
|
20429
|
-
function mergeLinesAtVertex(lyr) {
|
|
20430
|
-
|
|
20431
|
-
}
|
|
20432
|
-
|
|
20433
|
-
// TODO: support snipping rings (by snipping in two places)
|
|
20434
|
-
|
|
20435
|
-
|
|
20436
|
-
// pixel distance threshold for hovering near a vertex or segment midpoint
|
|
20625
|
+
// pixel distance threshold for hovering near a vertex or a segment
|
|
20437
20626
|
var HOVER_THRESHOLD = 10;
|
|
20438
20627
|
|
|
20439
20628
|
function initSnipTool(gui, ext, hit) {
|
|
20440
|
-
var
|
|
20441
|
-
var
|
|
20442
|
-
var
|
|
20629
|
+
var hoverInfo = null;
|
|
20630
|
+
var pendingCut = null; // first of the two cuts needed to divide a ring
|
|
20631
|
+
var sessionCount = 0;
|
|
20632
|
+
var alert;
|
|
20633
|
+
|
|
20634
|
+
function active() {
|
|
20635
|
+
return gui.interaction.getMode() == 'snip_lines' && !!hit.getHitTarget();
|
|
20636
|
+
}
|
|
20443
20637
|
|
|
20444
20638
|
gui.on('interaction_mode_change', function(e) {
|
|
20445
|
-
if (
|
|
20639
|
+
if (e.mode == 'snip_lines') {
|
|
20640
|
+
turnOn();
|
|
20641
|
+
} else {
|
|
20446
20642
|
turnOff();
|
|
20447
20643
|
}
|
|
20448
|
-
|
|
20644
|
+
updateCursor();
|
|
20449
20645
|
}, null, 10); // higher priority than hit control, so turnOff() has correct hit target
|
|
20450
20646
|
|
|
20647
|
+
gui.on('undo_redo_pre', function() {
|
|
20648
|
+
clearPendingCut();
|
|
20649
|
+
hoverInfo = null;
|
|
20650
|
+
});
|
|
20651
|
+
|
|
20652
|
+
// A pending cut describes a location in the target layer's current geometry,
|
|
20653
|
+
// and choosing one does not change the model -- so any model update means
|
|
20654
|
+
// something else has happened and the cut is stale.
|
|
20655
|
+
gui.model.on('update', function() {
|
|
20656
|
+
clearPendingCut();
|
|
20657
|
+
hoverInfo = null;
|
|
20658
|
+
});
|
|
20659
|
+
|
|
20660
|
+
function turnOn() {
|
|
20661
|
+
hoverInfo = null;
|
|
20662
|
+
if (sessionCount === 0) {
|
|
20663
|
+
showInstructions();
|
|
20664
|
+
}
|
|
20665
|
+
sessionCount++;
|
|
20666
|
+
}
|
|
20667
|
+
|
|
20668
|
+
function turnOff() {
|
|
20669
|
+
clearPendingCut();
|
|
20670
|
+
clearHoverVertex();
|
|
20671
|
+
hideInstructions();
|
|
20672
|
+
}
|
|
20673
|
+
|
|
20674
|
+
function showInstructions() {
|
|
20675
|
+
var msg = 'Instructions: click a line to snip it apart. Snipping a ring ' +
|
|
20676
|
+
'takes two clicks -- the ring divides at the second one.';
|
|
20677
|
+
alert = showPopupAlert(msg, null, {non_blocking: true, max_width: '350px'});
|
|
20678
|
+
}
|
|
20679
|
+
|
|
20680
|
+
function hideInstructions() {
|
|
20681
|
+
if (!alert) return;
|
|
20682
|
+
alert.close('fade');
|
|
20683
|
+
alert = null;
|
|
20684
|
+
}
|
|
20685
|
+
|
|
20686
|
+
function clearHoverVertex() {
|
|
20687
|
+
hit.clearHoverVertex();
|
|
20688
|
+
hoverInfo = null;
|
|
20689
|
+
}
|
|
20690
|
+
|
|
20691
|
+
function clearPendingCut() {
|
|
20692
|
+
if (!pendingCut) return;
|
|
20693
|
+
pendingCut = null;
|
|
20694
|
+
hit.clearPendingSnip();
|
|
20695
|
+
}
|
|
20696
|
+
|
|
20697
|
+
function setPendingCut(info) {
|
|
20698
|
+
pendingCut = info;
|
|
20699
|
+
hit.setPendingSnip(info.markerPoint);
|
|
20700
|
+
}
|
|
20701
|
+
|
|
20702
|
+
// The pending cut is described in terms of the target layer's current arcs
|
|
20703
|
+
// and feature ids, so it has to be discarded if either could have changed.
|
|
20704
|
+
function pendingCutIsValid() {
|
|
20705
|
+
return !!pendingCut && pendingCut.target === hit.getHitTarget();
|
|
20706
|
+
}
|
|
20451
20707
|
|
|
20452
|
-
// hover event highlights the nearest point in close proximity to the pointer
|
|
20453
|
-
// ... or the closest point along the segment (for adding a new vertex)
|
|
20454
20708
|
hit.on('hover', function(e) {
|
|
20455
20709
|
if (!active()) return;
|
|
20456
|
-
|
|
20457
|
-
|
|
20458
|
-
|
|
20459
|
-
|
|
20460
|
-
|
|
20461
|
-
|
|
20462
|
-
|
|
20463
|
-
hit.setHoverVertex(hoverVertexInfo.displayPoint, hoverVertexInfo.type);
|
|
20710
|
+
if (pendingCut && !pendingCutIsValid()) {
|
|
20711
|
+
clearPendingCut();
|
|
20712
|
+
}
|
|
20713
|
+
hoverInfo = e.id >= 0 ? findSnipTarget(e) : null;
|
|
20714
|
+
if (hoverInfo) {
|
|
20715
|
+
hit.setHoverVertex(hoverInfo.markerPoint,
|
|
20716
|
+
hoverInfo.disabled ? 'disabled' : hoverInfo.type);
|
|
20464
20717
|
} else {
|
|
20465
|
-
clearHoverVertex();
|
|
20718
|
+
hit.clearHoverVertex();
|
|
20466
20719
|
}
|
|
20467
|
-
|
|
20468
|
-
|
|
20720
|
+
if (pendingCut) {
|
|
20721
|
+
hit.setPendingSnip(pendingCut.markerPoint);
|
|
20722
|
+
}
|
|
20723
|
+
updateCursor();
|
|
20469
20724
|
}, null, 100);
|
|
20470
20725
|
|
|
20471
20726
|
hit.on('click', function(e) {
|
|
20472
|
-
|
|
20473
|
-
var
|
|
20474
|
-
if (
|
|
20475
|
-
|
|
20727
|
+
var info = hoverInfo;
|
|
20728
|
+
var cuts;
|
|
20729
|
+
if (!active() || !info || info.disabled) return;
|
|
20730
|
+
// a snip invalidates the hover data (feature ids and arcs both change), so
|
|
20731
|
+
// clear it -- this also makes the second click of a double-click a no-op
|
|
20732
|
+
hoverInfo = null;
|
|
20733
|
+
if (!info.ring) {
|
|
20734
|
+
clearPendingCut();
|
|
20735
|
+
snip(info, [info.cut]);
|
|
20476
20736
|
return;
|
|
20477
20737
|
}
|
|
20478
|
-
|
|
20479
|
-
|
|
20480
|
-
|
|
20481
|
-
|
|
20738
|
+
if (!pendingCutIsValid() || pendingCut.fid !== info.fid ||
|
|
20739
|
+
pendingCut.partId !== info.partId) {
|
|
20740
|
+
// first cut on this ring: remember it, don't change any geometry
|
|
20741
|
+
setPendingCut(info);
|
|
20742
|
+
return;
|
|
20743
|
+
}
|
|
20744
|
+
if (cutsAreAdjacent(pendingCut, info)) {
|
|
20745
|
+
clearPendingCut(); // clicking the pending cut again cancels it
|
|
20746
|
+
return;
|
|
20482
20747
|
}
|
|
20748
|
+
cuts = [pendingCut.cut, info.cut];
|
|
20749
|
+
clearPendingCut();
|
|
20750
|
+
snip(info, cuts);
|
|
20751
|
+
});
|
|
20483
20752
|
|
|
20484
|
-
|
|
20753
|
+
gui.keyboard.on('keydown', function(e) {
|
|
20754
|
+
if (!active() || !pendingCut || e.keyName != 'esc') return;
|
|
20755
|
+
e.stopPropagation();
|
|
20756
|
+
e.originalEvent.preventDefault();
|
|
20757
|
+
clearPendingCut();
|
|
20758
|
+
}, null, 10);
|
|
20485
20759
|
|
|
20486
|
-
|
|
20760
|
+
function snip(info, cuts) {
|
|
20761
|
+
var target = hit.getHitTarget();
|
|
20762
|
+
var result = snipPath(target, info.fid, info.partId, cuts);
|
|
20763
|
+
if (!result) return;
|
|
20764
|
+
gui.dispatchEvent('snip', {target: target, result: result});
|
|
20765
|
+
hit.clearHoverVertex();
|
|
20766
|
+
hideInstructions();
|
|
20767
|
+
gui.model.updated({arc_count: true}); // update display arcs and redraw
|
|
20768
|
+
}
|
|
20487
20769
|
|
|
20488
|
-
|
|
20770
|
+
// Test if two cuts are close enough on screen to be treated as the same place
|
|
20771
|
+
function cutsAreAdjacent(a, b) {
|
|
20772
|
+
var dist = geom.distance2D(a.markerPoint[0], a.markerPoint[1],
|
|
20773
|
+
b.markerPoint[0], b.markerPoint[1]);
|
|
20774
|
+
return dist / ext.getPixelSize() < HOVER_THRESHOLD;
|
|
20775
|
+
}
|
|
20489
20776
|
|
|
20490
|
-
|
|
20491
|
-
|
|
20492
|
-
|
|
20493
|
-
|
|
20494
|
-
var target = hit.getHitTarget();
|
|
20495
|
-
var shp = target.shapes[e.id];
|
|
20496
|
-
var p = ext.pixCoordsToMapCoords(e.x, e.y);
|
|
20497
|
-
var ids = internal.findNearestVertices(p, shp, target.gui.displayArcs);
|
|
20498
|
-
var p2 = target.gui.displayArcs.getVertex2(ids[0]);
|
|
20499
|
-
var dist = geom.distance2D(p[0], p[1], p2[0], p2[1]);
|
|
20500
|
-
var pixelDist = dist / ext.getPixelSize();
|
|
20501
|
-
if (pixelDist > HOVER_THRESHOLD) {
|
|
20502
|
-
return null;
|
|
20503
|
-
}
|
|
20504
|
-
var point = getVertexCoords(target, ids[0]); // data coordinates
|
|
20505
|
-
var displayPoint = target.gui.displayArcs.getVertex2(ids[0]);
|
|
20506
|
-
return {target, ids, point, displayPoint, type: 'vertex'};
|
|
20777
|
+
function updateCursor() {
|
|
20778
|
+
var el = gui.container.findChild('.map-layers');
|
|
20779
|
+
el.classed('snip-tool', active());
|
|
20780
|
+
el.classed('no-snip', !!(hoverInfo && hoverInfo.disabled));
|
|
20507
20781
|
}
|
|
20508
20782
|
|
|
20509
|
-
|
|
20783
|
+
// Find the snip location under the pointer: the nearest vertex within the
|
|
20784
|
+
// hover threshold, or failing that the nearest point on a segment.
|
|
20785
|
+
// Returns null if the pointer is not close enough to the hit feature.
|
|
20786
|
+
function findSnipTarget(e) {
|
|
20510
20787
|
var target = hit.getHitTarget();
|
|
20511
|
-
|
|
20512
|
-
|
|
20513
|
-
var
|
|
20514
|
-
var
|
|
20515
|
-
var
|
|
20516
|
-
|
|
20517
|
-
|
|
20518
|
-
|
|
20519
|
-
|
|
20520
|
-
|
|
20521
|
-
|
|
20522
|
-
|
|
20523
|
-
|
|
20524
|
-
|
|
20525
|
-
|
|
20526
|
-
|
|
20527
|
-
|
|
20528
|
-
|
|
20529
|
-
|
|
20530
|
-
|
|
20531
|
-
|
|
20788
|
+
var shp = target && target.shapes[e.id];
|
|
20789
|
+
var arcs = target && target.gui.displayArcs;
|
|
20790
|
+
var bestVertex = null;
|
|
20791
|
+
var bestSegment = null;
|
|
20792
|
+
var p, threshold, data;
|
|
20793
|
+
if (!shp || !arcs) return null;
|
|
20794
|
+
p = ext.pixCoordsToMapCoords(e.x, e.y);
|
|
20795
|
+
threshold = HOVER_THRESHOLD * ext.getPixelSize();
|
|
20796
|
+
data = arcs.getVertexData();
|
|
20797
|
+
|
|
20798
|
+
shp.forEach(function(path, partId) {
|
|
20799
|
+
path.forEach(function(arcId, seq) {
|
|
20800
|
+
var absId = internal.absArcId(arcId);
|
|
20801
|
+
var fwd = arcId >= 0;
|
|
20802
|
+
var start = data.ii[absId];
|
|
20803
|
+
var n = data.nn[absId];
|
|
20804
|
+
var i, v1, v2, dist, p2;
|
|
20805
|
+
if (arcIsOutOfRange(data.bb, absId, p, threshold)) return;
|
|
20806
|
+
for (i=0; i<n; i++) {
|
|
20807
|
+
v1 = fwd ? start + i : start + n - 1 - i;
|
|
20808
|
+
dist = geom.distance2D(p[0], p[1], data.xx[v1], data.yy[v1]);
|
|
20809
|
+
if (dist < threshold && (!bestVertex || dist < bestVertex.dist)) {
|
|
20810
|
+
bestVertex = {partId: partId, seq: seq, offset: i, dist: dist, vid: v1};
|
|
20811
|
+
}
|
|
20812
|
+
if (i === n - 1) break;
|
|
20813
|
+
v2 = fwd ? v1 + 1 : v1 - 1;
|
|
20814
|
+
p2 = internal.findClosestPointOnSeg(p[0], p[1], data.xx[v1],
|
|
20815
|
+
data.yy[v1], data.xx[v2], data.yy[v2], 0);
|
|
20816
|
+
dist = geom.distance2D(p[0], p[1], p2[0], p2[1]);
|
|
20817
|
+
if (dist < threshold && (!bestSegment || dist < bestSegment.dist)) {
|
|
20818
|
+
bestSegment = {
|
|
20819
|
+
partId: partId, seq: seq, offset: i, dist: dist, displayPoint: p2,
|
|
20820
|
+
t: getSegmentPosition(data.xx[v1], data.yy[v1], data.xx[v2],
|
|
20821
|
+
data.yy[v2], p2)
|
|
20822
|
+
};
|
|
20823
|
+
}
|
|
20824
|
+
}
|
|
20825
|
+
});
|
|
20532
20826
|
});
|
|
20533
20827
|
|
|
20534
|
-
if (
|
|
20535
|
-
|
|
20536
|
-
|
|
20537
|
-
closest.point = translateDisplayPoint(target, closest.displayPoint);
|
|
20538
|
-
closest.type = 'interpolated';
|
|
20539
|
-
closest.target = target;
|
|
20540
|
-
return closest;
|
|
20828
|
+
if (bestVertex) return getVertexTarget(target, e.id, bestVertex);
|
|
20829
|
+
if (bestSegment) return getSegmentTarget(target, e.id, bestSegment);
|
|
20830
|
+
return null;
|
|
20541
20831
|
}
|
|
20542
20832
|
|
|
20543
|
-
function
|
|
20544
|
-
var
|
|
20545
|
-
return
|
|
20546
|
-
|
|
20833
|
+
function getVertexTarget(target, fid, info) {
|
|
20834
|
+
var displayPoint = target.gui.displayArcs.getVertex2(info.vid);
|
|
20835
|
+
return {
|
|
20836
|
+
target: target,
|
|
20837
|
+
fid: fid,
|
|
20838
|
+
partId: info.partId,
|
|
20839
|
+
type: 'vertex',
|
|
20840
|
+
ring: pathIsRing(target, fid, info.partId),
|
|
20841
|
+
disabled: !pathIsRing(target, fid, info.partId) &&
|
|
20842
|
+
vertexIsPathEndpoint(target, fid, info),
|
|
20843
|
+
markerPoint: displayPoint,
|
|
20844
|
+
cut: {seq: info.seq, offset: info.offset, point: null, displayPoint: null, t: 0}
|
|
20845
|
+
};
|
|
20547
20846
|
}
|
|
20548
20847
|
|
|
20549
|
-
function
|
|
20550
|
-
|
|
20551
|
-
|
|
20848
|
+
function getSegmentTarget(target, fid, info) {
|
|
20849
|
+
return {
|
|
20850
|
+
target: target,
|
|
20851
|
+
fid: fid,
|
|
20852
|
+
partId: info.partId,
|
|
20853
|
+
type: 'interpolated',
|
|
20854
|
+
ring: pathIsRing(target, fid, info.partId),
|
|
20855
|
+
disabled: false,
|
|
20856
|
+
markerPoint: info.displayPoint,
|
|
20857
|
+
cut: {
|
|
20858
|
+
seq: info.seq,
|
|
20859
|
+
offset: info.offset,
|
|
20860
|
+
point: translateDisplayPoint(target, info.displayPoint),
|
|
20861
|
+
displayPoint: info.displayPoint,
|
|
20862
|
+
t: info.t
|
|
20863
|
+
}
|
|
20864
|
+
};
|
|
20552
20865
|
}
|
|
20553
20866
|
|
|
20554
|
-
function
|
|
20555
|
-
return
|
|
20867
|
+
function pathIsRing(target, fid, partId) {
|
|
20868
|
+
return geom.pathIsClosed(target.shapes[fid][partId], target.gui.displayArcs);
|
|
20556
20869
|
}
|
|
20557
20870
|
|
|
20558
|
-
|
|
20559
|
-
|
|
20871
|
+
// Test if a vertex is one of the two endpoints of an open path (as opposed to
|
|
20872
|
+
// an interior node between two of the path's arcs, which is snippable).
|
|
20873
|
+
function vertexIsPathEndpoint(target, fid, info) {
|
|
20874
|
+
var path = target.shapes[fid][info.partId];
|
|
20875
|
+
var data = target.gui.displayArcs.getVertexData();
|
|
20876
|
+
var lastId = internal.absArcId(path[path.length - 1]);
|
|
20877
|
+
if (info.seq === 0 && info.offset === 0) return true;
|
|
20878
|
+
return info.seq === path.length - 1 && info.offset === data.nn[lastId] - 1;
|
|
20560
20879
|
}
|
|
20880
|
+
}
|
|
20561
20881
|
|
|
20882
|
+
// Test if an arc's bounding box is too far from a point to contain a hit
|
|
20883
|
+
function arcIsOutOfRange(bb, arcId, p, dist) {
|
|
20884
|
+
var i = arcId * 4;
|
|
20885
|
+
return p[0] + dist < bb[i] || p[0] - dist > bb[i + 2] ||
|
|
20886
|
+
p[1] + dist < bb[i + 1] || p[1] - dist > bb[i + 3];
|
|
20887
|
+
}
|
|
20888
|
+
|
|
20889
|
+
// Position of point p along the segment from (x1, y1) to (x2, y2), as a
|
|
20890
|
+
// fraction of the segment's length
|
|
20891
|
+
function getSegmentPosition(x1, y1, x2, y2, p) {
|
|
20892
|
+
var len = geom.distance2D(x1, y1, x2, y2);
|
|
20893
|
+
return len > 0 ? geom.distance2D(x1, y1, p[0], p[1]) / len : 0;
|
|
20562
20894
|
}
|
|
20563
20895
|
|
|
20564
20896
|
function initInteractiveEditing(gui, ext, hit) {
|
|
@@ -21760,6 +22092,15 @@
|
|
|
21760
22092
|
_ctx.fill();
|
|
21761
22093
|
_ctx.closePath();
|
|
21762
22094
|
}
|
|
22095
|
+
|
|
22096
|
+
if (style.pending_snip) {
|
|
22097
|
+
_ctx.beginPath();
|
|
22098
|
+
_ctx.fillStyle = style.pending_snip_color || 'black';
|
|
22099
|
+
p = style.pending_snip;
|
|
22100
|
+
drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius * 2.5, _ctx);
|
|
22101
|
+
_ctx.fill();
|
|
22102
|
+
_ctx.closePath();
|
|
22103
|
+
}
|
|
21763
22104
|
};
|
|
21764
22105
|
|
|
21765
22106
|
// Optimized to draw paths in same-style batches (faster Canvas drawing)
|
|
@@ -25430,6 +25771,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
|
|
|
25430
25771
|
return {
|
|
25431
25772
|
layerCount: dataset.layers ? dataset.layers.length : 0,
|
|
25432
25773
|
arcCount: dataset.arcs ? dataset.arcs.size() : 0,
|
|
25774
|
+
crs_string: dataset.info && dataset.info.crs_string || null,
|
|
25433
25775
|
layers: (dataset.layers || []).map(function(lyr) {
|
|
25434
25776
|
return {
|
|
25435
25777
|
name: getLayerName(lyr),
|