mapshaper 0.7.62 → 0.7.64
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 +1846 -1011
- package/package.json +1 -1
- package/www/index.html +13 -1
- package/www/mapshaper-gui.js +1826 -137
- package/www/mapshaper.js +1846 -1011
- package/www/page.css +416 -12
package/mapshaper.js
CHANGED
|
@@ -25445,10 +25445,21 @@
|
|
|
25445
25445
|
|
|
25446
25446
|
|
|
25447
25447
|
function getFrameData(dataset, exportOpts) {
|
|
25448
|
-
var
|
|
25448
|
+
var frameTarget = resolveExportFrame({targetDataset: dataset, mode: 'cli'});
|
|
25449
|
+
var frameLyr = frameTarget && frameTarget.layer;
|
|
25449
25450
|
var data;
|
|
25450
|
-
if (
|
|
25451
|
-
data =
|
|
25451
|
+
if (exportOpts.gui_frame?.data) {
|
|
25452
|
+
data = Object.assign({}, exportOpts.gui_frame.data, {
|
|
25453
|
+
bbox: exportOpts.gui_frame.data.bbox.slice()
|
|
25454
|
+
});
|
|
25455
|
+
if (exportOpts.width > 0 || exportOpts.height > 0) {
|
|
25456
|
+
data = resizeFrameForExport(data, exportOpts);
|
|
25457
|
+
}
|
|
25458
|
+
} else if (frameLyr) {
|
|
25459
|
+
data = getFrameLayerData(frameLyr, dataset.arcs, getDatasetCRS(dataset));
|
|
25460
|
+
if (exportOpts.width > 0 || exportOpts.height > 0) {
|
|
25461
|
+
data = resizeFrameForExport(data, exportOpts);
|
|
25462
|
+
}
|
|
25452
25463
|
} else {
|
|
25453
25464
|
data = calcFrameData(dataset, exportOpts);
|
|
25454
25465
|
}
|
|
@@ -25457,6 +25468,32 @@
|
|
|
25457
25468
|
return data;
|
|
25458
25469
|
}
|
|
25459
25470
|
|
|
25471
|
+
function resizeFrameForExport(frame, opts) {
|
|
25472
|
+
var bounds = new Bounds(frame.bbox);
|
|
25473
|
+
// A stored frame is an exact geographic crop. The SVG exporter's default
|
|
25474
|
+
// 1px content margin applies only to frameless output.
|
|
25475
|
+
var outputOpts = Object.assign({}, opts, {margin: 0});
|
|
25476
|
+
var outputBounds = calcOutputBounds(bounds, outputOpts);
|
|
25477
|
+
var data = {
|
|
25478
|
+
type: 'frame',
|
|
25479
|
+
bbox: bounds.toArray(),
|
|
25480
|
+
bbox2: outputBounds.toArray(),
|
|
25481
|
+
width: Math.round(outputBounds.width()),
|
|
25482
|
+
height: Math.round(outputBounds.height()) || 1,
|
|
25483
|
+
// The authored unit still describes this frame, but an explicit output size
|
|
25484
|
+
// supersedes any fixed page shape, so the aspect is no longer pinned.
|
|
25485
|
+
aspect_ratio: null,
|
|
25486
|
+
units: frame.units || 'px'
|
|
25487
|
+
};
|
|
25488
|
+
if (data.width != frame.width || data.height != frame.height) {
|
|
25489
|
+
warn(
|
|
25490
|
+
`Output size ${data.width}×${data.height}px overrides the map frame's ` +
|
|
25491
|
+
`nominal size ${frame.width}×${frame.height}px; symbol and label sizes are not rescaled.`
|
|
25492
|
+
);
|
|
25493
|
+
}
|
|
25494
|
+
return data;
|
|
25495
|
+
}
|
|
25496
|
+
|
|
25460
25497
|
function fitDatasetToFrame(dataset, frame) {
|
|
25461
25498
|
var bounds = new Bounds(frame.bbox);
|
|
25462
25499
|
var bounds2 = frame.bbox2 ? new Bounds(frame.bbox2) : new Bounds(0, 0, frame.width, frame.height);
|
|
@@ -25467,18 +25504,29 @@
|
|
|
25467
25504
|
});
|
|
25468
25505
|
}
|
|
25469
25506
|
|
|
25470
|
-
function getFrameLayerData(lyr, arcs) {
|
|
25507
|
+
function getFrameLayerData(lyr, arcs, crs) {
|
|
25508
|
+
if (!isFrameLayer(lyr, arcs)) {
|
|
25509
|
+
error('Invalid map frame layer');
|
|
25510
|
+
}
|
|
25471
25511
|
var bounds = getLayerBounds(lyr, arcs);
|
|
25472
|
-
var d = lyr
|
|
25473
|
-
var w = d.width
|
|
25512
|
+
var d = getSingleFrameRecord(lyr);
|
|
25513
|
+
var w = d.width;
|
|
25514
|
+
var hasFixedAspect = utils.isFiniteNumber(d.frame_aspect_ratio) &&
|
|
25515
|
+
d.frame_aspect_ratio > 0;
|
|
25516
|
+
var aspectRatio = hasFixedAspect ? d.frame_aspect_ratio :
|
|
25517
|
+
bounds.width() / bounds.height();
|
|
25474
25518
|
// prevent rounding errors (like 1000.0000000002)
|
|
25475
|
-
var h = Math.round(w
|
|
25476
|
-
|
|
25519
|
+
var h = Math.round(w / aspectRatio);
|
|
25520
|
+
var data = {
|
|
25477
25521
|
type: 'frame',
|
|
25478
25522
|
width: w,
|
|
25479
25523
|
height: h,
|
|
25480
|
-
bbox: bounds.toArray()
|
|
25524
|
+
bbox: bounds.toArray(),
|
|
25525
|
+
aspect_ratio: hasFixedAspect ? d.frame_aspect_ratio : null,
|
|
25526
|
+
units: d.frame_units || 'px'
|
|
25481
25527
|
};
|
|
25528
|
+
if (crs) data.crs = crs;
|
|
25529
|
+
return data;
|
|
25482
25530
|
}
|
|
25483
25531
|
|
|
25484
25532
|
|
|
@@ -25515,9 +25563,63 @@
|
|
|
25515
25563
|
}
|
|
25516
25564
|
|
|
25517
25565
|
|
|
25566
|
+
var frameReservedFields =
|
|
25567
|
+
['type', 'width', 'height', 'frame_aspect_ratio', 'frame_units'];
|
|
25568
|
+
|
|
25569
|
+
function parseFrameSize(arg) {
|
|
25570
|
+
var str = String(arg).toLowerCase();
|
|
25571
|
+
var units = /px|pix/.test(str) && 'px' ||
|
|
25572
|
+
/pt|point/.test(str) && 'pt' ||
|
|
25573
|
+
/in/.test(str) && 'in' ||
|
|
25574
|
+
/cm/.test(str) && 'cm' ||
|
|
25575
|
+
'px';
|
|
25576
|
+
return {valuePx: parseSizeParam(arg), units: units};
|
|
25577
|
+
}
|
|
25578
|
+
|
|
25579
|
+
function formatFrameSizeForDisplay(frame) {
|
|
25580
|
+
var units = frame.units || 'px';
|
|
25581
|
+
var k = units == 'in' ? 72 :
|
|
25582
|
+
units == 'cm' ? 28.3465 : 1;
|
|
25583
|
+
return formatFrameDimension(frame.width / k) + ' × ' +
|
|
25584
|
+
formatFrameDimension(frame.height / k) + ' ' + units;
|
|
25585
|
+
}
|
|
25586
|
+
|
|
25587
|
+
function formatFrameDimension(value) {
|
|
25588
|
+
var rounded = Math.round(value * 100) / 100;
|
|
25589
|
+
return String(rounded);
|
|
25590
|
+
}
|
|
25591
|
+
|
|
25592
|
+
function isFrameReservedField(name) {
|
|
25593
|
+
return frameReservedFields.includes(name);
|
|
25594
|
+
}
|
|
25595
|
+
|
|
25596
|
+
function demoteFrameLayer(lyr, operation) {
|
|
25597
|
+
var rec = lyr.data && lyr.data.getRecords()[0];
|
|
25598
|
+
if (!rec) return;
|
|
25599
|
+
operation = operation || 'frame';
|
|
25600
|
+
lyr.data.captureTableBefore({operation: operation});
|
|
25601
|
+
frameReservedFields.forEach(function(field) {
|
|
25602
|
+
delete rec[field];
|
|
25603
|
+
});
|
|
25604
|
+
lyr.data.markChanged({operation: operation});
|
|
25605
|
+
}
|
|
25606
|
+
|
|
25607
|
+
function getSingleFrameRecord(lyr) {
|
|
25608
|
+
if (!lyr || !lyr.data || !lyr.shapes ||
|
|
25609
|
+
lyr.shapes.length != 1 || lyr.data.size() != 1) {
|
|
25610
|
+
return null;
|
|
25611
|
+
}
|
|
25612
|
+
return lyr.data.getReadOnlyRecordAt(0) || null;
|
|
25613
|
+
}
|
|
25614
|
+
|
|
25518
25615
|
// @lyr dataset layer
|
|
25519
25616
|
function isFrameLayer(lyr, arcs) {
|
|
25520
|
-
|
|
25617
|
+
var rec = getSingleFrameRecord(lyr);
|
|
25618
|
+
return !!rec &&
|
|
25619
|
+
lyr.geometry_type == 'polygon' &&
|
|
25620
|
+
rec.type == 'frame' &&
|
|
25621
|
+
utils.isFiniteNumber(rec.width) &&
|
|
25622
|
+
rec.width > 0 &&
|
|
25521
25623
|
layerIsRectangle(lyr, arcs);
|
|
25522
25624
|
}
|
|
25523
25625
|
|
|
@@ -25527,21 +25629,97 @@
|
|
|
25527
25629
|
});
|
|
25528
25630
|
}
|
|
25529
25631
|
|
|
25530
|
-
|
|
25632
|
+
function findFrames(catalog) {
|
|
25633
|
+
return catalog.getLayers().filter(function(o) {
|
|
25634
|
+
return isFrameLayer(o.layer, o.dataset.arcs);
|
|
25635
|
+
});
|
|
25636
|
+
}
|
|
25637
|
+
|
|
25638
|
+
// Remembers the frame conflict that was last reported, so that resolving the
|
|
25639
|
+
// frame on every map render does not repeat the same warning. Unlike
|
|
25640
|
+
// warnOnce(), this resets when the conflict clears, so a long-lived GUI session
|
|
25641
|
+
// warns again if the user undoes the change and then repeats it.
|
|
25642
|
+
var reportedFrameConflict = null;
|
|
25643
|
+
|
|
25644
|
+
// Generic field commands can promote an ordinary rectangle into a second frame
|
|
25645
|
+
// (e.g. -each 'type="frame", width=400'), and the GUI resolves the frame on
|
|
25646
|
+
// every render, so this has to degrade to a deterministic choice rather than an
|
|
25647
|
+
// error. Rejecting the change outright is assertSingleFrameUpdate()'s job,
|
|
25648
|
+
// where it can still be refused before it is committed.
|
|
25649
|
+
function getActiveFrame(catalog) {
|
|
25650
|
+
var frames = findFrames(catalog);
|
|
25651
|
+
var key;
|
|
25652
|
+
if (frames.length > 1) {
|
|
25653
|
+
key = frames.map(getFrameName).join(', ');
|
|
25654
|
+
if (key !== reportedFrameConflict) {
|
|
25655
|
+
reportedFrameConflict = key;
|
|
25656
|
+
warn('Multiple map frames are not supported; using',
|
|
25657
|
+
getFrameName(frames[0]) + '. Ignoring:',
|
|
25658
|
+
frames.slice(1).map(getFrameName).join(', '));
|
|
25659
|
+
}
|
|
25660
|
+
} else {
|
|
25661
|
+
reportedFrameConflict = null;
|
|
25662
|
+
}
|
|
25663
|
+
return frames[0] || null;
|
|
25664
|
+
}
|
|
25665
|
+
|
|
25531
25666
|
function findFrameDataset(catalog) {
|
|
25532
|
-
var target =
|
|
25667
|
+
var target = getActiveFrame(catalog);
|
|
25533
25668
|
return target && target.dataset || null;
|
|
25534
25669
|
}
|
|
25535
25670
|
|
|
25536
25671
|
function findFrameLayer(catalog) {
|
|
25537
|
-
var target =
|
|
25672
|
+
var target = getActiveFrame(catalog);
|
|
25538
25673
|
return target && target.layer || null;
|
|
25539
25674
|
}
|
|
25540
25675
|
|
|
25541
25676
|
function findFrame(catalog) {
|
|
25542
|
-
return
|
|
25543
|
-
|
|
25677
|
+
return getActiveFrame(catalog);
|
|
25678
|
+
}
|
|
25679
|
+
|
|
25680
|
+
function resolveExportFrame(opts) {
|
|
25681
|
+
if (opts.explicitFrame) return opts.explicitFrame;
|
|
25682
|
+
if (opts.targetDataset) {
|
|
25683
|
+
var lyr = findFrameLayerInDataset(opts.targetDataset);
|
|
25684
|
+
return lyr ? {layer: lyr, dataset: opts.targetDataset} : null;
|
|
25685
|
+
}
|
|
25686
|
+
return opts.mode == 'gui' && opts.catalog ? getActiveFrame(opts.catalog) : null;
|
|
25687
|
+
}
|
|
25688
|
+
|
|
25689
|
+
function assertSingleFrameUpdate(catalog, additions, removals) {
|
|
25690
|
+
var removed = removals || [];
|
|
25691
|
+
var frames = catalog.getLayers().filter(function(o) {
|
|
25692
|
+
return !removed.includes(o.layer) && isFrameLayer(o.layer, o.dataset.arcs);
|
|
25693
|
+
});
|
|
25694
|
+
(additions || []).forEach(function(o) {
|
|
25695
|
+
if (!removed.includes(o.layer) && isFrameLayer(o.layer, o.dataset.arcs)) {
|
|
25696
|
+
frames.push(o);
|
|
25697
|
+
}
|
|
25698
|
+
});
|
|
25699
|
+
frames = frames.filter(function(o, i) {
|
|
25700
|
+
return frames.findIndex(function(o2) {
|
|
25701
|
+
return o2.layer == o.layer;
|
|
25702
|
+
}) == i;
|
|
25703
|
+
});
|
|
25704
|
+
if (frames.length > 1) {
|
|
25705
|
+
stop$1('Multiple map frames are not supported:', frames.map(getFrameName).join(', '));
|
|
25706
|
+
}
|
|
25707
|
+
}
|
|
25708
|
+
|
|
25709
|
+
function assertCatalogCanAddDatasets(catalog, datasets, removals) {
|
|
25710
|
+
var existingDatasets = catalog.getDatasets();
|
|
25711
|
+
var additions = [];
|
|
25712
|
+
datasets.forEach(function(dataset) {
|
|
25713
|
+
if (existingDatasets.includes(dataset)) return;
|
|
25714
|
+
dataset.layers.forEach(function(lyr) {
|
|
25715
|
+
additions.push({layer: lyr, dataset: dataset});
|
|
25716
|
+
});
|
|
25544
25717
|
});
|
|
25718
|
+
assertSingleFrameUpdate(catalog, additions, removals);
|
|
25719
|
+
}
|
|
25720
|
+
|
|
25721
|
+
function getFrameName(o) {
|
|
25722
|
+
return o.layer.name || '[unnamed frame]';
|
|
25545
25723
|
}
|
|
25546
25724
|
|
|
25547
25725
|
function getFrameLayerBounds(lyr) {
|
|
@@ -25670,20 +25848,31 @@
|
|
|
25670
25848
|
|
|
25671
25849
|
var FrameUtils = /*#__PURE__*/Object.freeze({
|
|
25672
25850
|
__proto__: null,
|
|
25851
|
+
assertCatalogCanAddDatasets: assertCatalogCanAddDatasets,
|
|
25852
|
+
assertSingleFrameUpdate: assertSingleFrameUpdate,
|
|
25673
25853
|
calcFrameData: calcFrameData,
|
|
25674
25854
|
calcOutputBounds: calcOutputBounds,
|
|
25855
|
+
demoteFrameLayer: demoteFrameLayer,
|
|
25675
25856
|
findFrame: findFrame,
|
|
25676
25857
|
findFrameDataset: findFrameDataset,
|
|
25677
25858
|
findFrameLayer: findFrameLayer,
|
|
25678
25859
|
findFrameLayerInDataset: findFrameLayerInDataset,
|
|
25860
|
+
findFrames: findFrames,
|
|
25679
25861
|
fitDatasetToFrame: fitDatasetToFrame,
|
|
25862
|
+
formatFrameSizeForDisplay: formatFrameSizeForDisplay,
|
|
25863
|
+
frameReservedFields: frameReservedFields,
|
|
25864
|
+
getActiveFrame: getActiveFrame,
|
|
25680
25865
|
getFrameData: getFrameData,
|
|
25681
25866
|
getFrameLayerBounds: getFrameLayerBounds,
|
|
25682
25867
|
getFrameLayerData: getFrameLayerData,
|
|
25683
25868
|
getFrameSize: getFrameSize,
|
|
25684
25869
|
getMapFrameMetersPerPixel: getMapFrameMetersPerPixel,
|
|
25870
|
+
getSingleFrameRecord: getSingleFrameRecord,
|
|
25685
25871
|
isFrameLayer: isFrameLayer,
|
|
25686
|
-
|
|
25872
|
+
isFrameReservedField: isFrameReservedField,
|
|
25873
|
+
parseFrameSize: parseFrameSize,
|
|
25874
|
+
parseMarginOption: parseMarginOption,
|
|
25875
|
+
resolveExportFrame: resolveExportFrame
|
|
25687
25876
|
});
|
|
25688
25877
|
|
|
25689
25878
|
function addFurnitureLayer(lyr, catalog) {
|
|
@@ -27792,6 +27981,12 @@
|
|
|
27792
27981
|
// invariant under translation, rotation and scaling (Hobby 1986), which is
|
|
27793
27982
|
// what lets the tool fit in display coordinates while storing knots in map
|
|
27794
27983
|
// coordinates.
|
|
27984
|
+
//
|
|
27985
|
+
// Left alone, the method bulges: a hard turn followed by a much shorter
|
|
27986
|
+
// segment throws the long segment into a wide arc, several times the length of
|
|
27987
|
+
// the short one. Two limits below hold that in, MAX_HANDLE and
|
|
27988
|
+
// MAX_CHORD_RATIO. Both are inert on a path whose chords are of comparable
|
|
27989
|
+
// length, so an ordinary label path is fitted exactly as Hobby describes.
|
|
27795
27990
|
|
|
27796
27991
|
// Curl controls how the curve behaves at the two ends of a run. At 0 it
|
|
27797
27992
|
// approaches a straight line there; at 1 (Metafont's default) it approaches a
|
|
@@ -27879,6 +28074,24 @@
|
|
|
27879
28074
|
VEL_B = 1 / 16,
|
|
27880
28075
|
VEL_C = (3 - Math.sqrt(5)) / 2;
|
|
27881
28076
|
|
|
28077
|
+
// Longest a control point may sit from its knot, as a fraction of the chord
|
|
28078
|
+
// length that handleLimit() works out for it. See "Bulge" in the design doc: a
|
|
28079
|
+
// hard turn followed by a short segment otherwise throws the long segment into
|
|
28080
|
+
// a wide arc, which during editing is most of the time, because a short
|
|
28081
|
+
// segment is what a half-dragged knot leaves behind.
|
|
28082
|
+
//
|
|
28083
|
+
// The value is a floor, not a preference. Four points on a circle want 0.3905,
|
|
28084
|
+
// which is the roundest any sensible path asks for, so a limit just above it
|
|
28085
|
+
// never touches a curve that was not already misbehaving.
|
|
28086
|
+
var MAX_HANDLE = 0.4;
|
|
28087
|
+
|
|
28088
|
+
// How much longer one chord may be than its neighbour *for the purposes of the
|
|
28089
|
+
// solve*. Hobby equalizes mock curvature across each knot, and a short chord
|
|
28090
|
+
// carries a lot of curvature, so without this a short segment dictates a wide
|
|
28091
|
+
// arc to a long one. Capping the ratio limits how far that reaches without
|
|
28092
|
+
// moving any knot or touching the curve where the chords are even.
|
|
28093
|
+
var MAX_CHORD_RATIO = 1.5;
|
|
28094
|
+
|
|
27882
28095
|
// Fits a run of knots, returning a cubic per interval.
|
|
27883
28096
|
function fitRun(pts) {
|
|
27884
28097
|
var n = pts.length - 1, // segment count
|
|
@@ -27895,16 +28108,54 @@
|
|
|
27895
28108
|
psi.push(0);
|
|
27896
28109
|
for (i = 1; i < n; i++) psi.push(wrapAngle(om[i] - om[i - 1]));
|
|
27897
28110
|
|
|
27898
|
-
theta = solveDepartureAngles(dd, psi, n);
|
|
28111
|
+
theta = solveDepartureAngles(evenOutChords(dd, n), psi, n);
|
|
27899
28112
|
phi = getArrivalAngles(theta, psi, n);
|
|
27900
28113
|
|
|
27901
28114
|
for (i = 0; i < n; i++) {
|
|
27902
28115
|
segments.push(buildSegment(pts[i], pts[i + 1], dd[i], om[i],
|
|
27903
|
-
theta[i], phi[i + 1]
|
|
28116
|
+
theta[i], phi[i + 1],
|
|
28117
|
+
handleLimit(dd[i], i > 0 ? dd[i - 1] : Infinity),
|
|
28118
|
+
handleLimit(dd[i], i < n - 1 ? dd[i + 1] : Infinity)));
|
|
27904
28119
|
}
|
|
27905
28120
|
return segments;
|
|
27906
28121
|
}
|
|
27907
28122
|
|
|
28123
|
+
// Longest a handle may be at one end of a segment whose own chord is @own and
|
|
28124
|
+
// whose neighbour across that knot is @neighbor. (At the first and last knot
|
|
28125
|
+
// of a run there is no neighbour, and the segment's own chord is the measure.)
|
|
28126
|
+
//
|
|
28127
|
+
// The scale is the geometric mean of the two, rather than the shorter of them.
|
|
28128
|
+
// Measuring against the shorter alone holds the bulge down just as well, but
|
|
28129
|
+
// it makes a long segment meeting a short one spend nearly all of its shape at
|
|
28130
|
+
// its far end: on a 490-unit arm meeting a 50-unit leg the two handles came
|
|
28131
|
+
// out 9.8:1, so the arm ran almost straight and then hooked hard into the
|
|
28132
|
+
// knot, and the curvature either side of that knot differed by a factor of 14.
|
|
28133
|
+
// The geometric mean gives the long segment a handle in proportion to how
|
|
28134
|
+
// lopsided the pair actually is, which brings the same case to 3.1:1 and the
|
|
28135
|
+
// curvature to within a factor of 1.4 -- about what Hobby's mock curvature
|
|
28136
|
+
// leaves on an ordinary path anyway -- for two points of extra bulge.
|
|
28137
|
+
function handleLimit(own, neighbor) {
|
|
28138
|
+
return MAX_HANDLE * Math.sqrt(own * Math.min(own, neighbor));
|
|
28139
|
+
}
|
|
28140
|
+
|
|
28141
|
+
// The chord lengths the solve sees, with the ratio between neighbours held to
|
|
28142
|
+
// MAX_CHORD_RATIO. Each length becomes the smallest that any chord permits it
|
|
28143
|
+
// to be, allowing for a factor of MAX_CHORD_RATIO per step -- a forward pass
|
|
28144
|
+
// and a backward pass compute that exactly, so this needs no iteration.
|
|
28145
|
+
//
|
|
28146
|
+
// Only the tangent directions are affected. The curve is still built on the
|
|
28147
|
+
// true chord lengths.
|
|
28148
|
+
function evenOutChords(dd, n) {
|
|
28149
|
+
var out = dd.concat(), i;
|
|
28150
|
+
for (i = 1; i < n; i++) {
|
|
28151
|
+
if (out[i] > out[i - 1] * MAX_CHORD_RATIO) out[i] = out[i - 1] * MAX_CHORD_RATIO;
|
|
28152
|
+
}
|
|
28153
|
+
for (i = n - 2; i >= 0; i--) {
|
|
28154
|
+
if (out[i] > out[i + 1] * MAX_CHORD_RATIO) out[i] = out[i + 1] * MAX_CHORD_RATIO;
|
|
28155
|
+
}
|
|
28156
|
+
return out;
|
|
28157
|
+
}
|
|
28158
|
+
|
|
27908
28159
|
// The tridiagonal system of Hobby's mock-curvature conditions, in the
|
|
27909
28160
|
// departure angle at each knot measured from the chord leaving it. Written for
|
|
27910
28161
|
// unit tension throughout; the general form carries a tension per knot, which
|
|
@@ -27948,21 +28199,30 @@
|
|
|
27948
28199
|
|
|
27949
28200
|
// Turns a pair of angles into a cubic, by way of Hobby's velocity functions --
|
|
27950
28201
|
// how far along each tangent the control point sits, as a multiple of a third
|
|
27951
|
-
// of the chord.
|
|
27952
|
-
|
|
28202
|
+
// of the chord -- with each distance held to the caller's limit.
|
|
28203
|
+
//
|
|
28204
|
+
// Shortening a handle leaves its direction alone, so the tangent the two
|
|
28205
|
+
// segments share at a knot is untouched and the curve stays smooth through it.
|
|
28206
|
+
// What it gives up is Hobby's mock-curvature match across that knot, and only
|
|
28207
|
+
// at a knot lopsided enough for the limit to bite.
|
|
28208
|
+
function buildSegment(p0, p1, d, om, theta, phi, maxOut, maxIn) {
|
|
27953
28209
|
var alpha = VEL_A * (Math.sin(theta) - VEL_B * Math.sin(phi)) *
|
|
27954
28210
|
(Math.sin(phi) - VEL_B * Math.sin(theta)) *
|
|
27955
28211
|
(Math.cos(theta) - Math.cos(phi)),
|
|
27956
28212
|
rho = (2 + alpha) /
|
|
27957
28213
|
(1 + (1 - VEL_C) * Math.cos(theta) + VEL_C * Math.cos(phi)),
|
|
27958
28214
|
sigma = (2 - alpha) /
|
|
27959
|
-
(1 + (1 - VEL_C) * Math.cos(phi) + VEL_C * Math.cos(theta))
|
|
28215
|
+
(1 + (1 - VEL_C) * Math.cos(phi) + VEL_C * Math.cos(theta)),
|
|
28216
|
+
out = d * rho / 3,
|
|
28217
|
+
arr = d * sigma / 3;
|
|
28218
|
+
if (out > maxOut) out = maxOut;
|
|
28219
|
+
if (arr > maxIn) arr = maxIn;
|
|
27960
28220
|
return {
|
|
27961
28221
|
p0: p0,
|
|
27962
|
-
c1: [p0[0] +
|
|
27963
|
-
p0[1] +
|
|
27964
|
-
c2: [p1[0] -
|
|
27965
|
-
p1[1] -
|
|
28222
|
+
c1: [p0[0] + out * Math.cos(theta + om),
|
|
28223
|
+
p0[1] + out * Math.sin(theta + om)],
|
|
28224
|
+
c2: [p1[0] - arr * Math.cos(om - phi),
|
|
28225
|
+
p1[1] - arr * Math.sin(om - phi)],
|
|
27966
28226
|
p3: p1
|
|
27967
28227
|
};
|
|
27968
28228
|
}
|
|
@@ -28649,7 +28909,8 @@
|
|
|
28649
28909
|
function exportSVG(dataset, opts) {
|
|
28650
28910
|
var namespace = 'xmlns="http://www.w3.org/2000/svg"';
|
|
28651
28911
|
var defs = [];
|
|
28652
|
-
var frame,
|
|
28912
|
+
var frame, frameLyr, frameBackground, frameNeatline;
|
|
28913
|
+
var svg, layers, metadataJSON, files, svgFile;
|
|
28653
28914
|
var style = '';
|
|
28654
28915
|
|
|
28655
28916
|
// kludge for map keys
|
|
@@ -28686,6 +28947,21 @@
|
|
|
28686
28947
|
if (opts.scalebar) {
|
|
28687
28948
|
layers.push(getScalebarLayer({})); // default options
|
|
28688
28949
|
}
|
|
28950
|
+
frameLyr = layers.find(function(lyr) {
|
|
28951
|
+
return isFrameLayer(lyr, dataset.arcs);
|
|
28952
|
+
});
|
|
28953
|
+
if (frameLyr) {
|
|
28954
|
+
frameBackground = exportFrameStylePhase(frameLyr, dataset, opts, 'background');
|
|
28955
|
+
frameNeatline = exportFrameStylePhase(frameLyr, dataset, opts, 'neatline');
|
|
28956
|
+
layers = layers.filter(function(lyr) { return lyr != frameLyr; });
|
|
28957
|
+
} else if (opts.gui_frame?.style) {
|
|
28958
|
+
frameBackground = exportExplicitFrameStyle(
|
|
28959
|
+
frame, opts.gui_frame, 'background'
|
|
28960
|
+
);
|
|
28961
|
+
frameNeatline = exportExplicitFrameStyle(
|
|
28962
|
+
frame, opts.gui_frame, 'neatline'
|
|
28963
|
+
);
|
|
28964
|
+
}
|
|
28689
28965
|
svg = layers.map(function(lyr) {
|
|
28690
28966
|
var obj;
|
|
28691
28967
|
if (layerHasFurniture(lyr)) {
|
|
@@ -28698,6 +28974,8 @@
|
|
|
28698
28974
|
convertPropertiesToDefinitions(obj, defs);
|
|
28699
28975
|
return stringify(obj);
|
|
28700
28976
|
}).join('\n');
|
|
28977
|
+
if (frameBackground) svg = frameBackground + '\n' + svg;
|
|
28978
|
+
if (frameNeatline) svg += '\n' + frameNeatline;
|
|
28701
28979
|
|
|
28702
28980
|
if (metadataJSON) {
|
|
28703
28981
|
svg = getMetadataBlock(metadataJSON, [0, 0, frame.width, frame.height]) + svg;
|
|
@@ -28732,6 +29010,65 @@ ${svg}
|
|
|
28732
29010
|
return files;
|
|
28733
29011
|
}
|
|
28734
29012
|
|
|
29013
|
+
function exportFrameStylePhase(lyr, dataset, opts, phase) {
|
|
29014
|
+
var copy = copyLayer(lyr);
|
|
29015
|
+
var rec = copy.data && copy.data.getRecords()[0];
|
|
29016
|
+
var obj;
|
|
29017
|
+
if (!rec) return '';
|
|
29018
|
+
if (phase == 'background') {
|
|
29019
|
+
if (!rec.fill || rec.fill == 'none') return '';
|
|
29020
|
+
removeStylePrefix(rec, 'stroke');
|
|
29021
|
+
copy.name = (lyr.name || 'frame') + '-background';
|
|
29022
|
+
} else {
|
|
29023
|
+
if (!rec.stroke || rec.stroke == 'none' ||
|
|
29024
|
+
Number(rec['stroke-width']) <= 0) return '';
|
|
29025
|
+
removeStylePrefix(rec, 'fill');
|
|
29026
|
+
rec.fill = 'none';
|
|
29027
|
+
copy.name = (lyr.name || 'frame') + '-neatline';
|
|
29028
|
+
}
|
|
29029
|
+
obj = exportLayerForSVG(copy, dataset, opts);
|
|
29030
|
+
return stringify(obj);
|
|
29031
|
+
}
|
|
29032
|
+
|
|
29033
|
+
function removeStylePrefix(rec, prefix) {
|
|
29034
|
+
Object.keys(rec).forEach(function(key) {
|
|
29035
|
+
if (key == prefix || key.indexOf(prefix + '-') === 0) {
|
|
29036
|
+
delete rec[key];
|
|
29037
|
+
}
|
|
29038
|
+
});
|
|
29039
|
+
}
|
|
29040
|
+
|
|
29041
|
+
function exportExplicitFrameStyle(frame, context, phase) {
|
|
29042
|
+
var style = context.style || {};
|
|
29043
|
+
var properties = {
|
|
29044
|
+
id: (context.name || 'frame') + '-' + phase,
|
|
29045
|
+
x: 0,
|
|
29046
|
+
y: 0,
|
|
29047
|
+
width: frame.width,
|
|
29048
|
+
height: frame.height
|
|
29049
|
+
};
|
|
29050
|
+
if (phase == 'background') {
|
|
29051
|
+
if (!style.fill || style.fill == 'none') return '';
|
|
29052
|
+
copyProperties(properties, style,
|
|
29053
|
+
['fill', 'fill-opacity', 'fill-rule', 'opacity']);
|
|
29054
|
+
} else {
|
|
29055
|
+
if (!style.stroke || style.stroke == 'none' ||
|
|
29056
|
+
Number(style['stroke-width']) <= 0) return '';
|
|
29057
|
+
properties.fill = 'none';
|
|
29058
|
+
copyProperties(properties, style, [
|
|
29059
|
+
'stroke', 'stroke-width', 'stroke-opacity', 'stroke-linecap',
|
|
29060
|
+
'stroke-linejoin', 'stroke-dasharray', 'opacity'
|
|
29061
|
+
]);
|
|
29062
|
+
}
|
|
29063
|
+
return stringify({tag: 'rect', properties: properties});
|
|
29064
|
+
}
|
|
29065
|
+
|
|
29066
|
+
function copyProperties(dest, src, names) {
|
|
29067
|
+
names.forEach(function(name) {
|
|
29068
|
+
if (src[name] !== undefined) dest[name] = src[name];
|
|
29069
|
+
});
|
|
29070
|
+
}
|
|
29071
|
+
|
|
28735
29072
|
function getSvgFileBase(dataset, opts) {
|
|
28736
29073
|
var file = opts.file || getOutputFileBase(dataset) + '.svg';
|
|
28737
29074
|
return file.replace(/\.svg$/i, '');
|
|
@@ -28807,6 +29144,10 @@ ${svg}
|
|
|
28807
29144
|
|
|
28808
29145
|
function exportLayerForSVG(lyr, dataset, opts) {
|
|
28809
29146
|
var layerObj = getEmptyLayerForSVG(lyr, opts);
|
|
29147
|
+
if (isFrameLayer(lyr, dataset.arcs) && !frameHasExportStyle(lyr)) {
|
|
29148
|
+
layerObj.children = [];
|
|
29149
|
+
return layerObj;
|
|
29150
|
+
}
|
|
28810
29151
|
if (layerIsRectangle(lyr, dataset.arcs)) {
|
|
28811
29152
|
lyr = copyLayer(lyr);
|
|
28812
29153
|
adjustRectangleStyle(lyr);
|
|
@@ -28815,6 +29156,14 @@ ${svg}
|
|
|
28815
29156
|
return layerObj;
|
|
28816
29157
|
}
|
|
28817
29158
|
|
|
29159
|
+
function frameHasExportStyle(lyr) {
|
|
29160
|
+
var rec = lyr.data && lyr.data.getReadOnlyRecordAt(0);
|
|
29161
|
+
var fields = findStylePropertiesBySymbolGeom(Object.keys(rec || {}), 'polygon');
|
|
29162
|
+
return fields.some(function(name) {
|
|
29163
|
+
return rec[name] !== null && rec[name] !== undefined && rec[name] !== '';
|
|
29164
|
+
});
|
|
29165
|
+
}
|
|
29166
|
+
|
|
28818
29167
|
function exportRasterLayerForSVG(lyr, frame, opts) {
|
|
28819
29168
|
var raster = lyr.raster;
|
|
28820
29169
|
var rendered = renderRasterForSVG(raster, frame, opts);
|
|
@@ -42582,6 +42931,45 @@ ${svg}
|
|
|
42582
42931
|
// projected coordinate is not read as lat-long
|
|
42583
42932
|
.option('target', targetOpt);
|
|
42584
42933
|
|
|
42934
|
+
// used by GUI
|
|
42935
|
+
parser.command('update-frame')
|
|
42936
|
+
// .describe('update an existing map frame layer')
|
|
42937
|
+
.option('bbox', {
|
|
42938
|
+
describe: 'frame coordinates (xmin,ymin,xmax,ymax)',
|
|
42939
|
+
type: 'bbox'
|
|
42940
|
+
})
|
|
42941
|
+
.option('width', {
|
|
42942
|
+
describe: 'nominal frame width (e.g. 5in, 10cm, 600px)'
|
|
42943
|
+
})
|
|
42944
|
+
.option('height', {
|
|
42945
|
+
describe: 'nominal frame height'
|
|
42946
|
+
})
|
|
42947
|
+
.option('aspect-ratio', {
|
|
42948
|
+
describe: 'fixed page aspect ratio',
|
|
42949
|
+
type: 'number'
|
|
42950
|
+
})
|
|
42951
|
+
.option('auto-aspect', {
|
|
42952
|
+
describe: 'derive page height from the frame extent',
|
|
42953
|
+
type: 'flag'
|
|
42954
|
+
})
|
|
42955
|
+
.option('fix-scale', {
|
|
42956
|
+
describe: 'hold the map scale; derive the size from the new extent',
|
|
42957
|
+
type: 'flag'
|
|
42958
|
+
})
|
|
42959
|
+
.option('offset', {
|
|
42960
|
+
describe: 'padding in display units or pct of width/height, e.g. 5cm 20px 5%',
|
|
42961
|
+
type: 'strings'
|
|
42962
|
+
})
|
|
42963
|
+
.option('offsets', {
|
|
42964
|
+
describe: 'separate offsets for each side, in l,b,r,t order',
|
|
42965
|
+
type: 'strings'
|
|
42966
|
+
})
|
|
42967
|
+
.option('remove', {
|
|
42968
|
+
describe: 'demote the frame to an ordinary rectangle layer',
|
|
42969
|
+
type: 'flag'
|
|
42970
|
+
})
|
|
42971
|
+
.option('target', targetOpt);
|
|
42972
|
+
|
|
42585
42973
|
// used by GUI
|
|
42586
42974
|
parser.command('update-label')
|
|
42587
42975
|
// .describe('move the anchor or curve knots of an existing label')
|
|
@@ -42694,7 +43082,7 @@ ${svg}
|
|
|
42694
43082
|
type: 'bbox'
|
|
42695
43083
|
})
|
|
42696
43084
|
.option('offset', {
|
|
42697
|
-
describe: 'padding in display units or pct of width, e.g. 5cm 20px 5%',
|
|
43085
|
+
describe: 'padding in display units or pct of width/height, e.g. 5cm 20px 5%',
|
|
42698
43086
|
type: 'strings'
|
|
42699
43087
|
})
|
|
42700
43088
|
.option('offsets', {
|
|
@@ -42702,6 +43090,10 @@ ${svg}
|
|
|
42702
43090
|
type: 'strings'
|
|
42703
43091
|
})
|
|
42704
43092
|
.option('name', nameOpt)
|
|
43093
|
+
.option('replace', {
|
|
43094
|
+
describe: 'replace the existing map frame',
|
|
43095
|
+
type: 'flag'
|
|
43096
|
+
})
|
|
42705
43097
|
.option('target', targetOpt);
|
|
42706
43098
|
|
|
42707
43099
|
// .option('height', {
|
|
@@ -49327,12 +49719,13 @@ ${svg}
|
|
|
49327
49719
|
return layers;
|
|
49328
49720
|
};
|
|
49329
49721
|
|
|
49330
|
-
this.addDataset = function(dataset) {
|
|
49331
|
-
this.setDefaultTarget(dataset.layers, dataset);
|
|
49722
|
+
this.addDataset = function(dataset, opts) {
|
|
49723
|
+
this.setDefaultTarget(dataset.layers, dataset, opts);
|
|
49332
49724
|
return this;
|
|
49333
49725
|
};
|
|
49334
49726
|
|
|
49335
49727
|
this.addDatasets = function(datasets) {
|
|
49728
|
+
assertCatalogCanAddDatasets(this, datasets);
|
|
49336
49729
|
datasets.forEach(function(dataset) {
|
|
49337
49730
|
this.addDataset(dataset);
|
|
49338
49731
|
}, this);
|
|
@@ -49361,7 +49754,7 @@ ${svg}
|
|
|
49361
49754
|
return defaultTargets;
|
|
49362
49755
|
};
|
|
49363
49756
|
|
|
49364
|
-
this.setDefaultTarget = function(layers, dataset) {
|
|
49757
|
+
this.setDefaultTarget = function(layers, dataset, opts) {
|
|
49365
49758
|
this.setDefaultTargets([{
|
|
49366
49759
|
// Copy layers array, in case layers is a reference to dataset.layers.
|
|
49367
49760
|
// This prevents layers that are added to the dataset inside a command from
|
|
@@ -49369,12 +49762,19 @@ ${svg}
|
|
|
49369
49762
|
// by '-join unmatched unjoined'.
|
|
49370
49763
|
layers: layers.concat(),
|
|
49371
49764
|
dataset: dataset
|
|
49372
|
-
}]);
|
|
49765
|
+
}], opts);
|
|
49373
49766
|
};
|
|
49374
49767
|
|
|
49375
49768
|
// arr: array of target objects {layers:[], dataset:{}}
|
|
49376
|
-
this.setDefaultTargets = function(arr) {
|
|
49769
|
+
this.setDefaultTargets = function(arr, opts) {
|
|
49377
49770
|
if (targetsAreSame(defaultTargets, arr)) return;
|
|
49771
|
+
var newDatasets = arr.reduce(function(memo, target) {
|
|
49772
|
+
if (!datasets.includes(target.dataset) && !memo.includes(target.dataset)) {
|
|
49773
|
+
memo.push(target.dataset);
|
|
49774
|
+
}
|
|
49775
|
+
return memo;
|
|
49776
|
+
}, []);
|
|
49777
|
+
assertCatalogCanAddDatasets(this, newDatasets, opts && opts.removeLayers);
|
|
49378
49778
|
this.captureCatalogBefore({operation: 'setDefaultTargets'});
|
|
49379
49779
|
arr.forEach(function(target) {
|
|
49380
49780
|
if (datasets.indexOf(target.dataset) == -1) {
|
|
@@ -49824,165 +50224,1124 @@ ${svg}
|
|
|
49824
50224
|
return dataset;
|
|
49825
50225
|
}
|
|
49826
50226
|
|
|
49827
|
-
|
|
50227
|
+
// Returns number of arcs that were removed
|
|
50228
|
+
function editArcs(arcs, onPoint) {
|
|
50229
|
+
var nn2 = [],
|
|
50230
|
+
xx2 = [],
|
|
50231
|
+
yy2 = [],
|
|
50232
|
+
errors = 0,
|
|
50233
|
+
n;
|
|
49828
50234
|
|
|
49829
|
-
|
|
50235
|
+
arcs.forEach(function(arc, i) {
|
|
50236
|
+
editArc(arc, onPoint);
|
|
50237
|
+
});
|
|
50238
|
+
arcs.updateVertexData(nn2, xx2, yy2);
|
|
50239
|
+
return errors;
|
|
49830
50240
|
|
|
49831
|
-
|
|
49832
|
-
|
|
49833
|
-
|
|
49834
|
-
|
|
49835
|
-
|
|
49836
|
-
|
|
49837
|
-
// mutation with hand-rolled undo.
|
|
49838
|
-
//
|
|
49839
|
-
// See docs/development/label-tool-design.md.
|
|
49840
|
-
function updateLabel(targetLayers, dataset, opts) {
|
|
49841
|
-
var lyr, id, coords, rec;
|
|
49842
|
-
if (targetLayers.length > 1) {
|
|
49843
|
-
// One coordinate list describes one label, so there is no sensible reading
|
|
49844
|
-
// of this across layers.
|
|
49845
|
-
stop$1('Command expects a single target layer');
|
|
50241
|
+
function append(p) {
|
|
50242
|
+
if (p) {
|
|
50243
|
+
xx2.push(p[0]);
|
|
50244
|
+
yy2.push(p[1]);
|
|
50245
|
+
n++;
|
|
50246
|
+
}
|
|
49846
50247
|
}
|
|
49847
|
-
|
|
49848
|
-
|
|
49849
|
-
|
|
49850
|
-
|
|
49851
|
-
|
|
49852
|
-
|
|
49853
|
-
|
|
49854
|
-
|
|
49855
|
-
|
|
49856
|
-
|
|
50248
|
+
|
|
50249
|
+
function editArc(arc, cb) {
|
|
50250
|
+
var x, y, xp, yp, retn;
|
|
50251
|
+
var valid = true;
|
|
50252
|
+
var i = 0;
|
|
50253
|
+
n = 0;
|
|
50254
|
+
while (arc.hasNext()) {
|
|
50255
|
+
x = arc.x;
|
|
50256
|
+
y = arc.y;
|
|
50257
|
+
retn = cb(append, x, y, xp, yp, i++);
|
|
50258
|
+
if (retn === false) {
|
|
50259
|
+
valid = false;
|
|
50260
|
+
// assumes that it's ok for the arc iterator to be interrupted.
|
|
50261
|
+
break;
|
|
50262
|
+
}
|
|
50263
|
+
xp = x;
|
|
50264
|
+
yp = y;
|
|
50265
|
+
}
|
|
50266
|
+
if (valid && n == 1) {
|
|
50267
|
+
// only one valid point was added to this arc (invalid)
|
|
50268
|
+
// e.g. this could happen during reprojection.
|
|
50269
|
+
// making this arc empty
|
|
50270
|
+
// error("An invalid arc was created");
|
|
50271
|
+
message("An invalid arc was created");
|
|
50272
|
+
valid = false;
|
|
50273
|
+
}
|
|
50274
|
+
if (valid) {
|
|
50275
|
+
nn2.push(n);
|
|
50276
|
+
} else {
|
|
50277
|
+
// remove any points that were added for an invalid arc
|
|
50278
|
+
while (n-- > 0) {
|
|
50279
|
+
xx2.pop();
|
|
50280
|
+
yy2.pop();
|
|
50281
|
+
}
|
|
50282
|
+
nn2.push(0); // add empty arc (to preserve mapping from paths to arcs)
|
|
50283
|
+
errors++;
|
|
50284
|
+
}
|
|
49857
50285
|
}
|
|
49858
|
-
warnIfCurveIsUnprojected(dataset, coords.length);
|
|
49859
|
-
// Undo works from what a command declares it is about to change, so an edit
|
|
49860
|
-
// that does not say so is an edit that cannot be undone -- the shapes array
|
|
49861
|
-
// keeps its identity here, leaving nothing for the transaction to notice
|
|
49862
|
-
// afterwards.
|
|
49863
|
-
noteLayerWillChange(lyr, {operation: OPERATION, unit: 'shapes'});
|
|
49864
|
-
// Point shapes hold their coordinates directly, so this is the whole edit.
|
|
49865
|
-
// The pairs are copied rather than aliased: the GUI drags a live copy of the
|
|
49866
|
-
// knots and would otherwise keep a handle on the layer's own arrays.
|
|
49867
|
-
lyr.shapes[id] = coords.map(function(p) { return [p[0], p[1]]; });
|
|
49868
|
-
markLayerChanged(lyr, {operation: OPERATION, unit: 'shapes'});
|
|
49869
|
-
// Nothing about the label's text is touched, and nothing needs to be: a text
|
|
49870
|
-
// measurement describes the *text*, and moving a knot changes the length of
|
|
49871
|
-
// the *path*. The fit check compares the two at render time, against the
|
|
49872
|
-
// curve as it then stands, so a move can turn a fitting label into an
|
|
49873
|
-
// overflowing one with no measurement going out of date.
|
|
49874
50286
|
}
|
|
49875
50287
|
|
|
49876
|
-
|
|
49877
|
-
|
|
49878
|
-
|
|
49879
|
-
|
|
49880
|
-
|
|
49881
|
-
|
|
49882
|
-
|
|
49883
|
-
|
|
49884
|
-
|
|
49885
|
-
|
|
49886
|
-
|
|
49887
|
-
|
|
49888
|
-
|
|
49889
|
-
|
|
49890
|
-
|
|
50288
|
+
function DatasetEditor(dataset) {
|
|
50289
|
+
var layers = [];
|
|
50290
|
+
var arcs = [];
|
|
50291
|
+
|
|
50292
|
+
this.done = function() {
|
|
50293
|
+
dataset.layers = layers;
|
|
50294
|
+
if (arcs.length) {
|
|
50295
|
+
noteDatasetWillChange(dataset, {operation: 'DatasetEditor.done', unit: 'arcs'});
|
|
50296
|
+
dataset.arcs = new ArcCollection(arcs);
|
|
50297
|
+
markDatasetChanged(dataset, {operation: 'DatasetEditor.done', unit: 'arcs'});
|
|
50298
|
+
withActiveUndoTransaction(null, function() {
|
|
50299
|
+
buildTopology(dataset);
|
|
50300
|
+
});
|
|
50301
|
+
}
|
|
50302
|
+
};
|
|
50303
|
+
|
|
50304
|
+
this.editLayer = function(lyr, cb) {
|
|
50305
|
+
var type = lyr.geometry_type;
|
|
50306
|
+
if (dataset.layers.indexOf(lyr) != layers.length) {
|
|
50307
|
+
error('Layer was edited out-of-order');
|
|
50308
|
+
}
|
|
50309
|
+
if (!type) {
|
|
50310
|
+
layers.push(lyr);
|
|
50311
|
+
return;
|
|
50312
|
+
}
|
|
50313
|
+
var shapes = lyr.shapes.map(function(shape, shpId) {
|
|
50314
|
+
var shape2 = [], retn, input;
|
|
50315
|
+
for (var i=0, n=shape ? shape.length : 0; i<n; i++) {
|
|
50316
|
+
input = type == 'point' ? shape[i] : idsToCoords(shape[i]);
|
|
50317
|
+
retn = cb(input, i, shape);
|
|
50318
|
+
if (!Array.isArray(retn)) continue;
|
|
50319
|
+
if (type == 'point') {
|
|
50320
|
+
shape2.push(retn);
|
|
50321
|
+
} else if (type == 'polygon' || type == 'polyline') {
|
|
50322
|
+
extendPathShape(shape2, retn || []);
|
|
50323
|
+
}
|
|
50324
|
+
}
|
|
50325
|
+
return shape2.length > 0 ? shape2 : null;
|
|
50326
|
+
});
|
|
50327
|
+
noteLayerWillChange(lyr, {operation: 'DatasetEditor.editLayer', unit: 'shapes'});
|
|
50328
|
+
layers.push(Object.assign(lyr, {shapes: shapes}));
|
|
50329
|
+
markLayerChanged(lyr, {operation: 'DatasetEditor.editLayer', unit: 'shapes'});
|
|
50330
|
+
};
|
|
50331
|
+
|
|
50332
|
+
function extendPathShape(shape, parts) {
|
|
50333
|
+
for (var i=0; i<parts.length; i++) {
|
|
50334
|
+
shape.push([arcs.length]);
|
|
50335
|
+
arcs.push(parts[i]);
|
|
50336
|
+
}
|
|
49891
50337
|
}
|
|
49892
|
-
|
|
49893
|
-
|
|
50338
|
+
|
|
50339
|
+
function idsToCoords(ids) {
|
|
50340
|
+
var coords = [];
|
|
50341
|
+
var iter = dataset.arcs.getShapeIter(ids);
|
|
50342
|
+
while (iter.hasNext()) {
|
|
50343
|
+
coords.push([iter.x, iter.y]);
|
|
50344
|
+
}
|
|
50345
|
+
return coords;
|
|
49894
50346
|
}
|
|
49895
|
-
return id;
|
|
49896
50347
|
}
|
|
49897
50348
|
|
|
49898
|
-
|
|
49899
|
-
|
|
49900
|
-
|
|
50349
|
+
// Planar densification by an interval
|
|
50350
|
+
function densifyPathByInterval(coords, interval, interpolate) {
|
|
50351
|
+
if (findMaxPathInterval(coords) < interval) return coords;
|
|
50352
|
+
if (!interpolate) {
|
|
50353
|
+
interpolate = getIntervalInterpolator(interval);
|
|
49901
50354
|
}
|
|
49902
|
-
|
|
49903
|
-
|
|
49904
|
-
|
|
50355
|
+
var coords2 = [coords[0]], a, b;
|
|
50356
|
+
for (var i=1, n=coords.length; i<n; i++) {
|
|
50357
|
+
a = coords[i-1];
|
|
50358
|
+
b = coords[i];
|
|
50359
|
+
if (geom.distance2D(a[0], a[1], b[0], b[1]) > interval + 1e-4) {
|
|
50360
|
+
appendArr(coords2, interpolate(a, b));
|
|
50361
|
+
}
|
|
50362
|
+
coords2.push(b);
|
|
49905
50363
|
}
|
|
50364
|
+
return coords2;
|
|
49906
50365
|
}
|
|
49907
50366
|
|
|
49908
|
-
function
|
|
49909
|
-
return
|
|
50367
|
+
function getIntervalInterpolator(interval) {
|
|
50368
|
+
return function(a, b) {
|
|
50369
|
+
var points = [];
|
|
50370
|
+
// var rev = a[0] == b[0] ? a[1] > b[1] : a[0] > b[0];
|
|
50371
|
+
var dist = geom.distance2D(a[0], a[1], b[0], b[1]);
|
|
50372
|
+
var n = Math.round(dist / interval) - 1;
|
|
50373
|
+
var dx = (b[0] - a[0]) / (n + 1),
|
|
50374
|
+
dy = (b[1] - a[1]) / (n + 1);
|
|
50375
|
+
for (var i=1; i<=n; i++) {
|
|
50376
|
+
points.push([a[0] + dx * i, a[1] + dy * i]);
|
|
50377
|
+
}
|
|
50378
|
+
return points;
|
|
50379
|
+
};
|
|
49910
50380
|
}
|
|
49911
50381
|
|
|
49912
|
-
cmd.addShape = addShape;
|
|
49913
50382
|
|
|
49914
|
-
|
|
49915
|
-
|
|
49916
|
-
|
|
50383
|
+
// Interpolate the same points regardless of segment direction
|
|
50384
|
+
function densifyAntimeridianSegment(a, b, interval) {
|
|
50385
|
+
var y1, y2;
|
|
50386
|
+
var coords = [];
|
|
50387
|
+
var ascending = a[1] < b[1];
|
|
50388
|
+
if (a[0] != b[0]) error('Expected an edge segment');
|
|
50389
|
+
if (ascending) {
|
|
50390
|
+
y1 = a[1];
|
|
50391
|
+
y2 = b[1];
|
|
50392
|
+
} else {
|
|
50393
|
+
y1 = b[1];
|
|
50394
|
+
y2 = a[1];
|
|
49917
50395
|
}
|
|
49918
|
-
var
|
|
49919
|
-
|
|
49920
|
-
|
|
49921
|
-
|
|
49922
|
-
if (opts.no_replace || !targetLyr) {
|
|
49923
|
-
// create new layer
|
|
49924
|
-
setOutputLayerName(outputLyr, targetLyr && targetLyr.name, null, opts);
|
|
49925
|
-
return [outputLyr];
|
|
50396
|
+
var y = Math.floor(y1 / interval) * interval + interval;
|
|
50397
|
+
while (y < y2) {
|
|
50398
|
+
coords.push([a[0], y]);
|
|
50399
|
+
y += interval;
|
|
49926
50400
|
}
|
|
49927
|
-
|
|
49928
|
-
return
|
|
50401
|
+
if (!ascending) coords.reverse();
|
|
50402
|
+
return coords;
|
|
49929
50403
|
}
|
|
49930
50404
|
|
|
49931
|
-
function
|
|
49932
|
-
|
|
49933
|
-
|
|
49934
|
-
}
|
|
49935
|
-
|
|
49936
|
-
var geom = opts.coordinates && parseCoordsAsGeometry(opts.coordinates) || null;
|
|
50405
|
+
function appendArr(dest, src) {
|
|
50406
|
+
for (var i=0; i<src.length; i++) dest.push(src[i]);
|
|
50407
|
+
}
|
|
49937
50408
|
|
|
49938
|
-
|
|
49939
|
-
|
|
50409
|
+
function findMaxPathInterval(coords) {
|
|
50410
|
+
var maxSq = 0, intSq, a, b;
|
|
50411
|
+
for (var i=1, n=coords.length; i<n; i++) {
|
|
50412
|
+
a = coords[i-1];
|
|
50413
|
+
b = coords[i];
|
|
50414
|
+
intSq = geom.distanceSq(a[0], a[1], b[0], b[1]);
|
|
50415
|
+
if (intSq > maxSq) maxSq = intSq;
|
|
49940
50416
|
}
|
|
50417
|
+
return Math.sqrt(maxSq);
|
|
50418
|
+
}
|
|
49941
50419
|
|
|
49942
|
-
|
|
49943
|
-
|
|
49944
|
-
|
|
50420
|
+
function projectAndDensifyArcs(arcs, proj) {
|
|
50421
|
+
var interval = getDefaultDensifyInterval(arcs, proj);
|
|
50422
|
+
var minIntervalSq = interval * interval * 25;
|
|
50423
|
+
var p;
|
|
50424
|
+
return editArcs(arcs, onPoint);
|
|
49945
50425
|
|
|
49946
|
-
|
|
49947
|
-
|
|
49948
|
-
|
|
50426
|
+
function onPoint(append, lng, lat, prevLng, prevLat, i) {
|
|
50427
|
+
var pp = p;
|
|
50428
|
+
p = proj(lng, lat);
|
|
50429
|
+
if (!p) return false; // signal that current arc contains an error
|
|
49949
50430
|
|
|
49950
|
-
|
|
49951
|
-
if (geom.
|
|
49952
|
-
|
|
49953
|
-
|
|
49954
|
-
} else {
|
|
49955
|
-
stop$1('Expected polyline coordinates, received', geom.type);
|
|
50431
|
+
// Don't try to densify shorter segments (optimization)
|
|
50432
|
+
if (i > 0 && geom.distanceSq(p[0], p[1], pp[0], pp[1]) > minIntervalSq) {
|
|
50433
|
+
densifySegment(prevLng, prevLat, pp[0], pp[1], lng, lat, p[0], p[1], proj, interval)
|
|
50434
|
+
.forEach(append);
|
|
49956
50435
|
}
|
|
50436
|
+
append(p);
|
|
49957
50437
|
}
|
|
49958
|
-
|
|
49959
|
-
return {
|
|
49960
|
-
type: 'Feature',
|
|
49961
|
-
properties: parseProperties(opts.properties),
|
|
49962
|
-
geometry: geom
|
|
49963
|
-
};
|
|
49964
50438
|
}
|
|
49965
50439
|
|
|
49966
|
-
|
|
49967
|
-
|
|
50440
|
+
// Use the median of intervals computed by projecting segments.
|
|
50441
|
+
// We're probing a number of points, because @proj might only be valid in
|
|
50442
|
+
// a sub-region of the dataset bbox (e.g. +proj=tpers)
|
|
50443
|
+
function findDensifyInterval(bounds, xy, proj) {
|
|
50444
|
+
var steps = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
|
|
50445
|
+
var points = [];
|
|
50446
|
+
for (var i=0; i<steps.length; i++) {
|
|
50447
|
+
for (var j=0; j<steps.length; j++) {
|
|
50448
|
+
points.push([steps[i], steps[j]]);
|
|
50449
|
+
}
|
|
50450
|
+
}
|
|
50451
|
+
var intervals = points.map(function(pos) {
|
|
50452
|
+
var x = bounds.xmin + bounds.width() * pos[0];
|
|
50453
|
+
var y = bounds.ymin + bounds.height() * pos[1];
|
|
50454
|
+
var a = proj(x, y);
|
|
50455
|
+
var b = proj(x + xy[0], y + xy[1]);
|
|
50456
|
+
return a && b ? geom.distance2D(a[0], a[1], b[0], b[1]) : Infinity;
|
|
50457
|
+
}).filter(function(int) {return int < Infinity;});
|
|
50458
|
+
return intervals.length > 0 ? utils.findMedian(intervals) : Infinity;
|
|
49968
50459
|
}
|
|
49969
50460
|
|
|
49970
|
-
|
|
49971
|
-
|
|
49972
|
-
|
|
50461
|
+
// Kludgy way to get a useful interval for densifying a bounding box.
|
|
50462
|
+
// Uses a fraction of average bbox side length)
|
|
50463
|
+
// TODO: improve
|
|
50464
|
+
function findDensifyInterval2(bb, proj) {
|
|
50465
|
+
var a = proj(bb.centerX(), bb.centerY()),
|
|
50466
|
+
c = proj(bb.centerX(), bb.ymin), // right center
|
|
50467
|
+
d = proj(bb.xmax, bb.centerY()); // bottom center
|
|
50468
|
+
var interval = a && c && d ? (geom.distance2D(a[0], a[1], c[0], c[1]) +
|
|
50469
|
+
geom.distance2D(a[0], a[1], d[0], d[1])) / 5000 : Infinity;
|
|
50470
|
+
return interval;
|
|
49973
50471
|
}
|
|
49974
50472
|
|
|
49975
|
-
|
|
49976
|
-
|
|
50473
|
+
// Returns an interval in projected units
|
|
50474
|
+
function getDefaultDensifyInterval(arcs, proj) {
|
|
50475
|
+
var xy = getAvgSegment2(arcs),
|
|
50476
|
+
bb = arcs.getBounds(),
|
|
50477
|
+
intervalA = findDensifyInterval(bb, xy, proj),
|
|
50478
|
+
intervalB = findDensifyInterval2(bb, proj),
|
|
50479
|
+
interval = Math.min(intervalA, intervalB);
|
|
50480
|
+
if (interval == Infinity) {
|
|
50481
|
+
error('Densification error');
|
|
50482
|
+
}
|
|
50483
|
+
return interval;
|
|
49977
50484
|
}
|
|
49978
50485
|
|
|
49979
|
-
|
|
49980
|
-
|
|
50486
|
+
// Interpolate points into a projected line segment if needed to prevent large
|
|
50487
|
+
// deviations from path of original unprojected segment.
|
|
50488
|
+
// @points (optional) array of accumulated points
|
|
50489
|
+
function densifySegment(lng0, lat0, x0, y0, lng2, lat2, x2, y2, proj, interval, points) {
|
|
50490
|
+
// Find midpoint between two endpoints and project it (assumes longitude does
|
|
50491
|
+
// not wrap). TODO Consider bisecting along great circle path -- although this
|
|
50492
|
+
// would not be good for boundaries that follow line of constant latitude.
|
|
50493
|
+
var lng1 = (lng0 + lng2) / 2,
|
|
50494
|
+
lat1 = (lat0 + lat2) / 2,
|
|
50495
|
+
p = proj(lng1, lat1),
|
|
50496
|
+
distSq;
|
|
50497
|
+
if (!p) return; // TODO: consider if this is adequate for handling proj. errors
|
|
50498
|
+
distSq = geom.pointSegDistSq2(p[0], p[1], x0, y0, x2, y2); // sq displacement
|
|
50499
|
+
points = points || [];
|
|
50500
|
+
// Bisect current segment if the projected midpoint deviates from original
|
|
50501
|
+
// segment by more than the @interval parameter.
|
|
50502
|
+
// ... but don't bisect very small segments to prevent infinite recursion
|
|
50503
|
+
// (e.g. if projection function is discontinuous)
|
|
50504
|
+
if (distSq > interval * interval * 0.25 && geom.distance2D(lng0, lat0, lng2, lat2) > 0.01) {
|
|
50505
|
+
densifySegment(lng0, lat0, x0, y0, lng1, lat1, p[0], p[1], proj, interval, points);
|
|
50506
|
+
points.push(p);
|
|
50507
|
+
densifySegment(lng1, lat1, p[0], p[1], lng2, lat2, x2, y2, proj, interval, points);
|
|
50508
|
+
}
|
|
50509
|
+
return points;
|
|
49981
50510
|
}
|
|
49982
50511
|
|
|
49983
|
-
|
|
49984
|
-
|
|
49985
|
-
|
|
50512
|
+
// Create rectangles around each feature in a layer
|
|
50513
|
+
cmd.rectangles = function(targetLyr, targetDataset, opts) {
|
|
50514
|
+
var crsInfo = getDatasetCrsInfo(targetDataset);
|
|
50515
|
+
var records = targetLyr.data ? targetLyr.data.getRecords() : null;
|
|
50516
|
+
var geometries;
|
|
50517
|
+
|
|
50518
|
+
if (opts.bbox) {
|
|
50519
|
+
geometries = bboxExpressionToGeometries(opts.bbox, targetLyr, targetDataset);
|
|
50520
|
+
|
|
50521
|
+
} else {
|
|
50522
|
+
if (!layerHasGeometry$1(targetLyr)) {
|
|
50523
|
+
stop$1("Layer is missing geometric shapes");
|
|
50524
|
+
}
|
|
50525
|
+
geometries = shapesToBoxGeometries(targetLyr, targetDataset, opts);
|
|
50526
|
+
}
|
|
50527
|
+
|
|
50528
|
+
var geojson = {
|
|
50529
|
+
type: 'FeatureCollection',
|
|
50530
|
+
features: geometries.map(function(geom, i) {
|
|
50531
|
+
var rec = records && records[i] || null;
|
|
50532
|
+
if (rec && opts.no_replace) {
|
|
50533
|
+
rec = utils.extend({}, rec); // make a copy
|
|
50534
|
+
}
|
|
50535
|
+
return {
|
|
50536
|
+
type: 'Feature',
|
|
50537
|
+
properties: rec,
|
|
50538
|
+
geometry: geom
|
|
50539
|
+
};
|
|
50540
|
+
})
|
|
50541
|
+
};
|
|
50542
|
+
var dataset = importGeoJSON(geojson, {});
|
|
50543
|
+
setDatasetCrsInfo(dataset, crsInfo);
|
|
50544
|
+
var outputLayers = mergeDatasetsIntoDataset(targetDataset, [dataset]);
|
|
50545
|
+
setOutputLayerName(outputLayers[0], targetLyr, null, opts);
|
|
50546
|
+
return outputLayers;
|
|
50547
|
+
};
|
|
50548
|
+
|
|
50549
|
+
|
|
50550
|
+
|
|
50551
|
+
|
|
50552
|
+
function shapesToBoxGeometries(lyr, dataset, opts) {
|
|
50553
|
+
var crsInfo = getDatasetCrsInfo(dataset);
|
|
50554
|
+
return lyr.shapes.map(function(shp) {
|
|
50555
|
+
var bounds = lyr.geometry_type == 'point' ?
|
|
50556
|
+
getPointFeatureBounds(shp) : dataset.arcs.getMultiShapeBounds(shp);
|
|
50557
|
+
bounds = applyRectangleOptions(bounds, crsInfo.crs, opts);
|
|
50558
|
+
if (!bounds) return null;
|
|
50559
|
+
return bboxToPolygon(bounds.toArray(), opts);
|
|
50560
|
+
});
|
|
50561
|
+
}
|
|
50562
|
+
|
|
50563
|
+
function bboxExpressionToGeometries(exp, lyr, dataset, opts) {
|
|
50564
|
+
var compiled = compileFeatureExpression(exp, lyr, dataset.arcs, {});
|
|
50565
|
+
var n = getFeatureCount(lyr);
|
|
50566
|
+
var result;
|
|
50567
|
+
var geometries = [];
|
|
50568
|
+
for (var i=0; i<n; i++) {
|
|
50569
|
+
result = compiled(i);
|
|
50570
|
+
if (!looksLikeBbox(result)) {
|
|
50571
|
+
stop$1('Invalid bbox value (expected a GeoJSON-type bbox):', result);
|
|
50572
|
+
}
|
|
50573
|
+
geometries.push(bboxToPolygon(result));
|
|
50574
|
+
}
|
|
50575
|
+
return geometries;
|
|
50576
|
+
}
|
|
50577
|
+
|
|
50578
|
+
function looksLikeBbox(o) {
|
|
50579
|
+
if (!o || o.length != 4) return false;
|
|
50580
|
+
if (o.some(isNaN)) return false;
|
|
50581
|
+
if (o[0] <= o[2] == false || o[1] <= o[3] == false) return false;
|
|
50582
|
+
return true;
|
|
50583
|
+
}
|
|
50584
|
+
|
|
50585
|
+
// Create rectangles around one or more target layers
|
|
50586
|
+
//
|
|
50587
|
+
cmd.rectangle2 = function(target, opts) {
|
|
50588
|
+
// if target layer is a rectangle and we're applying frame properties,
|
|
50589
|
+
// turn the target into a frame instead of creating a new rectangle
|
|
50590
|
+
if (target.layers.length == 1 && opts.width &&
|
|
50591
|
+
layerIsRectangle(target.layers[0], target.dataset.arcs)) {
|
|
50592
|
+
applyFrameProperties(target.layers[0], opts);
|
|
50593
|
+
return;
|
|
50594
|
+
}
|
|
50595
|
+
var datasets = target.layers.map(function(lyr) {
|
|
50596
|
+
var dataset = cmd.rectangle({layer: lyr, dataset: target.dataset}, opts);
|
|
50597
|
+
setOutputLayerName(dataset.layers[0], lyr, null, opts);
|
|
50598
|
+
if (!opts.no_replace) {
|
|
50599
|
+
dataset.layers[0].name = lyr.name || dataset.layers[0].name;
|
|
50600
|
+
}
|
|
50601
|
+
return dataset;
|
|
50602
|
+
});
|
|
50603
|
+
return mergeDatasetsIntoDataset(target.dataset, datasets);
|
|
50604
|
+
};
|
|
50605
|
+
|
|
50606
|
+
cmd.rectangle = function(target, opts) {
|
|
50607
|
+
var bounds, crsInfo;
|
|
50608
|
+
if (opts.bbox) {
|
|
50609
|
+
bounds = new Bounds(opts.bbox);
|
|
50610
|
+
crsInfo = target && getDatasetCrsInfo(target.dataset) ||
|
|
50611
|
+
probablyDecimalDegreeBounds(bounds) && getCrsInfo('wgs84') || {};
|
|
50612
|
+
} else if (target) {
|
|
50613
|
+
bounds = getLayerBounds(target.layer, target.dataset.arcs);
|
|
50614
|
+
crsInfo = getDatasetCrsInfo(target.dataset);
|
|
50615
|
+
}
|
|
50616
|
+
bounds = bounds && applyRectangleOptions(bounds, crsInfo.crs, opts);
|
|
50617
|
+
if (!bounds || !bounds.hasBounds()) {
|
|
50618
|
+
stop$1('Missing rectangle extent');
|
|
50619
|
+
}
|
|
50620
|
+
var feature = {
|
|
50621
|
+
type: 'Feature',
|
|
50622
|
+
properties: {},
|
|
50623
|
+
geometry: bboxToPolygon(bounds.toArray(), opts)
|
|
50624
|
+
};
|
|
50625
|
+
var dataset = importGeoJSON(feature, {});
|
|
50626
|
+
applyFrameProperties(dataset.layers[0], opts);
|
|
50627
|
+
dataset.layers[0].name = opts.name || 'rectangle';
|
|
50628
|
+
setDatasetCrsInfo(dataset, crsInfo);
|
|
50629
|
+
return dataset;
|
|
50630
|
+
};
|
|
50631
|
+
|
|
50632
|
+
function applyFrameProperties(lyr, opts) {
|
|
50633
|
+
if (!opts.width) return;
|
|
50634
|
+
if (!lyr.data) initDataTable(lyr);
|
|
50635
|
+
var d = lyr.data.getRecords()[0] || {};
|
|
50636
|
+
var size = parseFrameSize(opts.width);
|
|
50637
|
+
d.width = size.valuePx;
|
|
50638
|
+
d.type = 'frame';
|
|
50639
|
+
d.frame_units = size.units;
|
|
50640
|
+
}
|
|
50641
|
+
|
|
50642
|
+
function applyRectangleOptions(bounds, crs, opts) {
|
|
50643
|
+
var isGeoBox = probablyDecimalDegreeBounds(bounds);
|
|
50644
|
+
if (opts.offset) {
|
|
50645
|
+
bounds = applyBoundsOffset(opts.offset, bounds, crs);
|
|
50646
|
+
}
|
|
50647
|
+
if (bounds.area() > 0 === false) return null;
|
|
50648
|
+
if (opts.aspect_ratio) {
|
|
50649
|
+
bounds = applyAspectRatio(opts.aspect_ratio, bounds);
|
|
50650
|
+
}
|
|
50651
|
+
if (isGeoBox) {
|
|
50652
|
+
bounds = clampToWorldBounds(bounds);
|
|
50653
|
+
}
|
|
50654
|
+
return bounds;
|
|
50655
|
+
}
|
|
50656
|
+
|
|
50657
|
+
// opt: aspect ratio as a single number or a range (e.g. "1,2");
|
|
50658
|
+
function applyAspectRatio(opt, bounds) {
|
|
50659
|
+
var range = String(opt).split(',').map(parseFloat),
|
|
50660
|
+
aspectRatio = bounds.width() / bounds.height(),
|
|
50661
|
+
min, max; // min is height limit, max is width limit
|
|
50662
|
+
if (range.length == 1) {
|
|
50663
|
+
range.push(range[0]);
|
|
50664
|
+
} else if (range[0] > range[1]) {
|
|
50665
|
+
range.reverse();
|
|
50666
|
+
}
|
|
50667
|
+
min = range[0];
|
|
50668
|
+
max = range[1];
|
|
50669
|
+
if (!min && !max) return bounds;
|
|
50670
|
+
if (!min) min = -Infinity;
|
|
50671
|
+
if (!max) max = Infinity;
|
|
50672
|
+
if (aspectRatio < min) {
|
|
50673
|
+
bounds.fillOut(min);
|
|
50674
|
+
} else if (aspectRatio > max) {
|
|
50675
|
+
bounds.fillOut(max);
|
|
50676
|
+
}
|
|
50677
|
+
return bounds;
|
|
50678
|
+
}
|
|
50679
|
+
|
|
50680
|
+
function applyBoundsOffset(offsetOpt, bounds, crs) {
|
|
50681
|
+
var offsets = convertFourSides(offsetOpt, crs, bounds);
|
|
50682
|
+
bounds.padBounds(offsets[0], offsets[1], offsets[2], offsets[3]);
|
|
50683
|
+
return bounds;
|
|
50684
|
+
}
|
|
50685
|
+
|
|
50686
|
+
function bboxToPolygon(bbox, optsArg) {
|
|
50687
|
+
var opts = optsArg || {};
|
|
50688
|
+
var coords = bboxToCoords(bbox);
|
|
50689
|
+
if (opts.interval > 0) {
|
|
50690
|
+
coords = densifyPathByInterval(coords, opts.interval);
|
|
50691
|
+
}
|
|
50692
|
+
return {
|
|
50693
|
+
type: 'Polygon',
|
|
50694
|
+
coordinates: [coords]
|
|
50695
|
+
};
|
|
50696
|
+
}
|
|
50697
|
+
|
|
50698
|
+
var Rectangle = /*#__PURE__*/Object.freeze({
|
|
50699
|
+
__proto__: null,
|
|
50700
|
+
applyAspectRatio: applyAspectRatio,
|
|
50701
|
+
bboxToPolygon: bboxToPolygon
|
|
50702
|
+
});
|
|
50703
|
+
|
|
50704
|
+
cmd.frame = function(catalog, targets, opts) {
|
|
50705
|
+
var widthPx, heightPx, aspectRatio, bbox;
|
|
50706
|
+
var existingFrame = getActiveFrame(catalog);
|
|
50707
|
+
if (opts.width) {
|
|
50708
|
+
widthPx = parseFrameSize(opts.width).valuePx;
|
|
50709
|
+
if (widthPx > 0 === false) {
|
|
50710
|
+
stop$1('Invalid width parameter:', opts.width);
|
|
50711
|
+
}
|
|
50712
|
+
}
|
|
50713
|
+
if (opts.height) {
|
|
50714
|
+
heightPx = parseFrameSize(opts.height).valuePx;
|
|
50715
|
+
if (heightPx > 0 === false) {
|
|
50716
|
+
stop$1('Invalid height parameter:', opts.height);
|
|
50717
|
+
}
|
|
50718
|
+
}
|
|
50719
|
+
if (!widthPx && !heightPx) {
|
|
50720
|
+
widthPx = 800;
|
|
50721
|
+
message('Using default 800px frame width');
|
|
50722
|
+
}
|
|
50723
|
+
|
|
50724
|
+
if (opts.aspect_ratio) {
|
|
50725
|
+
if (opts.aspect_ratio > 0 === false) {
|
|
50726
|
+
stop$1('Invalid aspect-ratio parameter:', opts.aspect_ratio);
|
|
50727
|
+
}
|
|
50728
|
+
if (!heightPx) {
|
|
50729
|
+
heightPx = roundToDigits(widthPx / opts.aspect_ratio, 1);
|
|
50730
|
+
} else if (!widthPx) {
|
|
50731
|
+
widthPx = roundToDigits(heightPx * opts.aspect_ratio, 1);
|
|
50732
|
+
}
|
|
50733
|
+
}
|
|
50734
|
+
|
|
50735
|
+
if (opts.bbox) {
|
|
50736
|
+
bbox = opts.bbox;
|
|
50737
|
+
// TODO: validate
|
|
50738
|
+
} else {
|
|
50739
|
+
var datasets = utils.pluck(targets, 'dataset');
|
|
50740
|
+
requireDatasetsHaveCompatibleCRS(datasets, 'Targets include both projected and unprojected coordinates');
|
|
50741
|
+
bbox = getTargetBbox(targets);
|
|
50742
|
+
if (!bbox) {
|
|
50743
|
+
stop$1('Command target is missing geographical bounds');
|
|
50744
|
+
}
|
|
50745
|
+
}
|
|
50746
|
+
|
|
50747
|
+
applyPercentageOffsets(bbox, opts.offset || opts.offsets);
|
|
50748
|
+
applyPixelOffsets(bbox, widthPx, heightPx, opts.offset || opts.offsets);
|
|
50749
|
+
|
|
50750
|
+
if (bbox[3] - bbox[1] > 0 === false || bbox[2] - bbox[0] > 0 === false) {
|
|
50751
|
+
stop$1('Frame has a collapsed bbox');
|
|
50752
|
+
}
|
|
50753
|
+
|
|
50754
|
+
aspectRatio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
|
|
50755
|
+
if (!widthPx) {
|
|
50756
|
+
widthPx = roundToDigits(heightPx * aspectRatio, 1);
|
|
50757
|
+
} else if (!heightPx) {
|
|
50758
|
+
heightPx = roundToDigits(widthPx / aspectRatio, 1);
|
|
50759
|
+
}
|
|
50760
|
+
|
|
50761
|
+
var feature = {
|
|
50762
|
+
type: 'Feature',
|
|
50763
|
+
properties: getFrameProperties(widthPx, heightPx, opts),
|
|
50764
|
+
geometry: bboxToPolygon(bbox)
|
|
50765
|
+
};
|
|
50766
|
+
var frameDataset = importGeoJSON(feature);
|
|
50767
|
+
// set CRS from target dataset
|
|
50768
|
+
// TODO: handle case: targets have different projections
|
|
50769
|
+
// TODO: handle case: first target is missing CRS
|
|
50770
|
+
if (targets.length > 0) {
|
|
50771
|
+
var crsInfo = getDatasetCrsInfo(targets[0].dataset);
|
|
50772
|
+
setDatasetCrsInfo(frameDataset, crsInfo);
|
|
50773
|
+
}
|
|
50774
|
+
frameDataset.layers[0].name = opts.name || 'frame';
|
|
50775
|
+
if (existingFrame) {
|
|
50776
|
+
if (!opts.replace) {
|
|
50777
|
+
stop$1('A map frame already exists:', existingFrame.layer.name || '[unnamed frame]');
|
|
50778
|
+
}
|
|
50779
|
+
demoteFrameLayer(existingFrame.layer);
|
|
50780
|
+
}
|
|
50781
|
+
catalog.addDataset(frameDataset);
|
|
50782
|
+
};
|
|
50783
|
+
|
|
50784
|
+
function getFrameProperties(width, height, opts) {
|
|
50785
|
+
var properties = {
|
|
50786
|
+
type: 'frame',
|
|
50787
|
+
width: width,
|
|
50788
|
+
height: height,
|
|
50789
|
+
frame_units: opts.width || opts.height ?
|
|
50790
|
+
parseFrameSize(opts.width || opts.height).units : 'px'
|
|
50791
|
+
};
|
|
50792
|
+
if (opts.aspect_ratio > 0 || opts.width && opts.height) {
|
|
50793
|
+
properties.frame_aspect_ratio = width / height;
|
|
50794
|
+
}
|
|
50795
|
+
return properties;
|
|
50796
|
+
}
|
|
50797
|
+
|
|
50798
|
+
function fillOutBbox(bbox, widthPx, heightPx) {
|
|
50799
|
+
var hpad = 0, vpad = 0;
|
|
50800
|
+
var w = bbox[2] - bbox[0];
|
|
50801
|
+
var h = bbox[3] - bbox[1];
|
|
50802
|
+
if (widthPx / heightPx > w / h) { // need to add horizontal padding
|
|
50803
|
+
hpad = h * widthPx / heightPx - w;
|
|
50804
|
+
} else {
|
|
50805
|
+
vpad = w * heightPx / widthPx - h;
|
|
50806
|
+
}
|
|
50807
|
+
bbox[0] -= hpad / 2;
|
|
50808
|
+
bbox[1] -= vpad / 2;
|
|
50809
|
+
bbox[2] += hpad / 2;
|
|
50810
|
+
bbox[3] += vpad / 2;
|
|
50811
|
+
}
|
|
50812
|
+
|
|
50813
|
+
function applyPercentageOffsets(bbox, arg) {
|
|
50814
|
+
var sides = getPctOffsets(arg);
|
|
50815
|
+
var l = sides[0],
|
|
50816
|
+
b = sides[1],
|
|
50817
|
+
r = sides[2],
|
|
50818
|
+
t = sides[3],
|
|
50819
|
+
w2 = (bbox[2] - bbox[0]) / (1 - l - r),
|
|
50820
|
+
h2 = (bbox[3] - bbox[1]) / (1 - t - b);
|
|
50821
|
+
bbox[0] -= l * w2;
|
|
50822
|
+
bbox[1] -= b * h2;
|
|
50823
|
+
bbox[2] += r * w2;
|
|
50824
|
+
bbox[3] += t * h2;
|
|
50825
|
+
}
|
|
50826
|
+
|
|
50827
|
+
function applyPixelOffsets(bbox, widthPx, heightPx, arg) {
|
|
50828
|
+
var sides = getPixelOffsets(arg);
|
|
50829
|
+
var l = sides[0],
|
|
50830
|
+
b = sides[1],
|
|
50831
|
+
r = sides[2],
|
|
50832
|
+
t = sides[3],
|
|
50833
|
+
scale, w;
|
|
50834
|
+
|
|
50835
|
+
if (widthPx && heightPx) {
|
|
50836
|
+
// add padding to bbox to match pixel dimensions, if needed
|
|
50837
|
+
fillOutBbox(bbox, widthPx, heightPx);
|
|
50838
|
+
}
|
|
50839
|
+
|
|
50840
|
+
w = bbox[2] - bbox[0];
|
|
50841
|
+
bbox[3] - bbox[1];
|
|
50842
|
+
|
|
50843
|
+
if (widthPx) {
|
|
50844
|
+
scale = w / (widthPx - l - r);
|
|
50845
|
+
} else {
|
|
50846
|
+
scale = w / (heightPx - t - b);
|
|
50847
|
+
}
|
|
50848
|
+
|
|
50849
|
+
bbox[0] -= scale * l;
|
|
50850
|
+
bbox[1] -= scale * b;
|
|
50851
|
+
bbox[2] += scale * r;
|
|
50852
|
+
bbox[3] += scale * t;
|
|
50853
|
+
return scale;
|
|
50854
|
+
}
|
|
50855
|
+
|
|
50856
|
+
function getPctOffsets(arg) {
|
|
50857
|
+
return adjustOffsetsArg(arg).map(str => {
|
|
50858
|
+
return str.includes('%') ? parsePercent(str) : 0;
|
|
50859
|
+
});
|
|
50860
|
+
}
|
|
50861
|
+
|
|
50862
|
+
function getPixelOffsets(arg) {
|
|
50863
|
+
return adjustOffsetsArg(arg).map(str => {
|
|
50864
|
+
return str.includes('%') ? 0 : parseSizeParam(str);
|
|
50865
|
+
});
|
|
50866
|
+
}
|
|
50867
|
+
|
|
50868
|
+
function adjustOffsetsArg(arg) {
|
|
50869
|
+
if (!arg) arg = ['0'];
|
|
50870
|
+
if (arg.length == 1) {
|
|
50871
|
+
return [arg[0], arg[0], arg[0], arg[0]];
|
|
50872
|
+
}
|
|
50873
|
+
if (arg.length != 4) {
|
|
50874
|
+
stop$1('List of offsets should have 4 values');
|
|
50875
|
+
}
|
|
50876
|
+
return arg;
|
|
50877
|
+
}
|
|
50878
|
+
|
|
50879
|
+
function getTargetBbox(targets) {
|
|
50880
|
+
var expanded = expandCommandTargets(targets);
|
|
50881
|
+
var bounds = expanded.reduce(function(memo, o) {
|
|
50882
|
+
return memo.mergeBounds(getLayerBounds(o.layer, o.dataset.arcs));
|
|
50883
|
+
}, new Bounds());
|
|
50884
|
+
return bounds.hasBounds() ? bounds.toArray() : null;
|
|
50885
|
+
}
|
|
50886
|
+
|
|
50887
|
+
// Convert width and height args to aspect ratio arg for the rectangle() function
|
|
50888
|
+
function getAspectRatioArg(widthArg, heightArg) {
|
|
50889
|
+
// heightArg is a string containing either a number or a
|
|
50890
|
+
// comma-sep. pair of numbers (range);
|
|
50891
|
+
return heightArg.split(',').map(function(opt) {
|
|
50892
|
+
var height = Number(opt),
|
|
50893
|
+
width = Number(widthArg);
|
|
50894
|
+
if (!opt) return '';
|
|
50895
|
+
return width / height;
|
|
50896
|
+
}).reverse().join(',');
|
|
50897
|
+
}
|
|
50898
|
+
|
|
50899
|
+
var Frame = /*#__PURE__*/Object.freeze({
|
|
50900
|
+
__proto__: null,
|
|
50901
|
+
applyPercentageOffsets: applyPercentageOffsets,
|
|
50902
|
+
applyPixelOffsets: applyPixelOffsets,
|
|
50903
|
+
fillOutBbox: fillOutBbox,
|
|
50904
|
+
getAspectRatioArg: getAspectRatioArg
|
|
50905
|
+
});
|
|
50906
|
+
|
|
50907
|
+
// A frame is a viewport, not data, so it can legitimately extend past the
|
|
50908
|
+
// edges of the globe: fitting a near-global extent to a fixed aspect ratio pads
|
|
50909
|
+
// it with whitespace, and that padding can reach beyond a pole. Those
|
|
50910
|
+
// coordinates have no projected equivalent, and projectAndDensifyArcs() drops a
|
|
50911
|
+
// whole arc when any of its vertices fails -- which took out three sides of the
|
|
50912
|
+
// rectangle and left a zero-area bbox. Clamping first keeps the frame
|
|
50913
|
+
// projectable; the off-globe padding is what gets lost.
|
|
50914
|
+
function clampLatLngBbox(bbox) {
|
|
50915
|
+
return [
|
|
50916
|
+
utils.clamp(bbox[0], -180, 180),
|
|
50917
|
+
utils.clamp(bbox[1], -90, 90),
|
|
50918
|
+
utils.clamp(bbox[2], -180, 180),
|
|
50919
|
+
utils.clamp(bbox[3], -90, 90)
|
|
50920
|
+
];
|
|
50921
|
+
}
|
|
50922
|
+
|
|
50923
|
+
// Project a rectangular frame as a sampled boundary, rather than projecting
|
|
50924
|
+
// only its corners. Curved projections can reach their x/y extrema between
|
|
50925
|
+
// corners, so a corner-only bbox can crop content that was inside the frame.
|
|
50926
|
+
function getProjectedFrameBounds(bbox, project, srcIsLatLng) {
|
|
50927
|
+
var b = new Bounds(srcIsLatLng ? clampLatLngBbox(bbox) : bbox);
|
|
50928
|
+
var xmin = b.xmin;
|
|
50929
|
+
var ymin = b.ymin;
|
|
50930
|
+
var xmax = b.xmax;
|
|
50931
|
+
var ymax = b.ymax;
|
|
50932
|
+
var arcs = new ArcCollection([
|
|
50933
|
+
[[xmin, ymin], [xmax, ymin]],
|
|
50934
|
+
[[xmax, ymin], [xmax, ymax]],
|
|
50935
|
+
[[xmax, ymax], [xmin, ymax]],
|
|
50936
|
+
[[xmin, ymax], [xmin, ymin]]
|
|
50937
|
+
]);
|
|
50938
|
+
projectAndDensifyArcs(arcs, project);
|
|
50939
|
+
var projected = arcs.getBounds();
|
|
50940
|
+
if (!projected.hasBounds() || projected.area() > 0 === false) {
|
|
50941
|
+
error('Unable to project map frame');
|
|
50942
|
+
}
|
|
50943
|
+
return projected;
|
|
50944
|
+
}
|
|
50945
|
+
|
|
50946
|
+
// Snapshot frame bounds before the dataset arcs are projected.
|
|
50947
|
+
function getFrameProjectionSnapshots(dataset) {
|
|
50948
|
+
return dataset.layers.reduce(function(memo, lyr) {
|
|
50949
|
+
if (isFrameLayer(lyr, dataset.arcs)) {
|
|
50950
|
+
memo.push({
|
|
50951
|
+
layer: lyr,
|
|
50952
|
+
bbox: getLayerBounds(lyr, dataset.arcs).toArray()
|
|
50953
|
+
});
|
|
50954
|
+
}
|
|
50955
|
+
return memo;
|
|
50956
|
+
}, []);
|
|
50957
|
+
}
|
|
50958
|
+
|
|
50959
|
+
// Rebuild a projected frame through the normal topology-aware replacement
|
|
50960
|
+
// path. This is safe when the frame shares an ArcCollection with other layers.
|
|
50961
|
+
function rebuildProjectedFrameLayer(snapshot, dataset, project, srcIsLatLng) {
|
|
50962
|
+
var bounds = getProjectedFrameBounds(snapshot.bbox, project, srcIsLatLng);
|
|
50963
|
+
var rec = getSingleFrameRecord(snapshot.layer) || {};
|
|
50964
|
+
var fixedAspect = utils.isFiniteNumber(rec.frame_aspect_ratio) &&
|
|
50965
|
+
rec.frame_aspect_ratio > 0 ? rec.frame_aspect_ratio : null;
|
|
50966
|
+
var props;
|
|
50967
|
+
if (fixedAspect) {
|
|
50968
|
+
// Projecting a rectangle's boundary and taking its bounds does not preserve
|
|
50969
|
+
// the rectangle's shape, so a frame that declares a ratio has to be padded
|
|
50970
|
+
// back out to it -- otherwise the geometry and the page disagree.
|
|
50971
|
+
var bbox = bounds.toArray();
|
|
50972
|
+
fillOutBbox(bbox, fixedAspect, 1);
|
|
50973
|
+
bounds = new Bounds(bbox);
|
|
50974
|
+
} else {
|
|
50975
|
+
// Height is derived from the extent, so it goes stale when the extent is
|
|
50976
|
+
// reprojected. getFrameLayerData() recomputes it, but the record is what
|
|
50977
|
+
// gets written to an output file.
|
|
50978
|
+
props = {height: Math.round(rec.width * bounds.height() / bounds.width())};
|
|
50979
|
+
}
|
|
50980
|
+
rebuildFrameLayerGeometry(snapshot.layer, dataset, bounds, props);
|
|
50981
|
+
}
|
|
50982
|
+
|
|
50983
|
+
function rebuildFrameLayerGeometry(lyr, dataset, bounds, props) {
|
|
50984
|
+
bounds = bounds instanceof Bounds ? bounds : new Bounds(bounds);
|
|
50985
|
+
var frameDataset = createFrameRectangleDataset(lyr, bounds, props);
|
|
50986
|
+
// The replacement rectangle is imported from bare GeoJSON, so it carries no
|
|
50987
|
+
// CRS. Merging it into the host dataset runs a projected/unprojected
|
|
50988
|
+
// compatibility check, and during -proj the host still reports its source CRS
|
|
50989
|
+
// while the new rectangle already holds projected coordinates -- so the
|
|
50990
|
+
// check has to compare like with like.
|
|
50991
|
+
setDatasetCrsInfo(frameDataset, getDatasetCrsInfo(dataset));
|
|
50992
|
+
replaceLayerContents(lyr, dataset, frameDataset);
|
|
50993
|
+
}
|
|
50994
|
+
|
|
50995
|
+
// Return an isolated rectangular display layer. Dynamic display projection
|
|
50996
|
+
// must not mutate source shapes or the ArcCollection shared by other layers.
|
|
50997
|
+
function getProjectedFrameDisplayLayer(lyr, arcs, project, srcIsLatLng) {
|
|
50998
|
+
if (!isFrameLayer(lyr, arcs)) return null;
|
|
50999
|
+
var bbox = getLayerBounds(lyr, arcs).toArray();
|
|
51000
|
+
var bounds = getProjectedFrameBounds(bbox, project, srcIsLatLng);
|
|
51001
|
+
var dataset = createFrameRectangleDataset(lyr, bounds);
|
|
51002
|
+
return {
|
|
51003
|
+
layer: dataset.layers[0],
|
|
51004
|
+
arcs: dataset.arcs
|
|
51005
|
+
};
|
|
51006
|
+
}
|
|
51007
|
+
|
|
51008
|
+
function createFrameRectangleDataset(lyr, bounds, props) {
|
|
51009
|
+
var record = lyr.data && lyr.data.getReadOnlyRecordAt(0);
|
|
51010
|
+
var feature = {
|
|
51011
|
+
type: 'Feature',
|
|
51012
|
+
properties: Object.assign({}, record || {}, props || {}),
|
|
51013
|
+
geometry: bboxToPolygon(bounds.toArray())
|
|
51014
|
+
};
|
|
51015
|
+
var dataset = importGeoJSON(feature);
|
|
51016
|
+
dataset.layers[0].name = lyr.name;
|
|
51017
|
+
return dataset;
|
|
51018
|
+
}
|
|
51019
|
+
|
|
51020
|
+
var FrameProjection = /*#__PURE__*/Object.freeze({
|
|
51021
|
+
__proto__: null,
|
|
51022
|
+
getFrameProjectionSnapshots: getFrameProjectionSnapshots,
|
|
51023
|
+
getProjectedFrameBounds: getProjectedFrameBounds,
|
|
51024
|
+
getProjectedFrameDisplayLayer: getProjectedFrameDisplayLayer,
|
|
51025
|
+
rebuildFrameLayerGeometry: rebuildFrameLayerGeometry,
|
|
51026
|
+
rebuildProjectedFrameLayer: rebuildProjectedFrameLayer
|
|
51027
|
+
});
|
|
51028
|
+
|
|
51029
|
+
var OPERATION$1 = 'update-frame';
|
|
51030
|
+
|
|
51031
|
+
function updateFrame(targetLayers, dataset, opts) {
|
|
51032
|
+
if (!targetLayers || targetLayers.length != 1) {
|
|
51033
|
+
stop$1('-update-frame expects a single target layer');
|
|
51034
|
+
}
|
|
51035
|
+
var lyr = targetLayers[0];
|
|
51036
|
+
if (!isFrameLayer(lyr, dataset.arcs)) {
|
|
51037
|
+
stop$1('Target layer is not a map frame:', lyr.name || '[unnamed layer]');
|
|
51038
|
+
}
|
|
51039
|
+
if (opts.remove) {
|
|
51040
|
+
if (hasUpdateOptions(opts)) {
|
|
51041
|
+
stop$1('The remove option cannot be combined with frame update options');
|
|
51042
|
+
}
|
|
51043
|
+
demoteFrameLayer(lyr, OPERATION$1);
|
|
51044
|
+
return;
|
|
51045
|
+
}
|
|
51046
|
+
if (!hasUpdateOptions(opts)) {
|
|
51047
|
+
stop$1('Missing frame update option(s)');
|
|
51048
|
+
}
|
|
51049
|
+
if (opts.aspect_ratio !== undefined && opts.auto_aspect) {
|
|
51050
|
+
stop$1('aspect-ratio= and auto-aspect are mutually exclusive');
|
|
51051
|
+
}
|
|
51052
|
+
if (opts.fix_scale && (opts.width !== undefined || opts.height !== undefined)) {
|
|
51053
|
+
stop$1('fix-scale cannot be combined with width= or height=');
|
|
51054
|
+
}
|
|
51055
|
+
|
|
51056
|
+
var rec = lyr.data.getRecords()[0];
|
|
51057
|
+
var frame = getFrameLayerData(lyr, dataset.arcs);
|
|
51058
|
+
var bbox = (opts.bbox || frame.bbox).slice();
|
|
51059
|
+
var offsetArg = opts.offset || opts.offsets;
|
|
51060
|
+
var fixedAspect = getFixedAspect(rec);
|
|
51061
|
+
var width = frame.width;
|
|
51062
|
+
var units = rec.frame_units || 'px';
|
|
51063
|
+
var widthSize, heightSize, height, effectiveAspect;
|
|
51064
|
+
|
|
51065
|
+
// 1. Aspect mode, resolved first because whether the page shape is fixed
|
|
51066
|
+
// decides whether it may constrain the extent while offsets are applied.
|
|
51067
|
+
if (opts.auto_aspect) {
|
|
51068
|
+
fixedAspect = null;
|
|
51069
|
+
} else if (opts.aspect_ratio !== undefined) {
|
|
51070
|
+
if (!utils.isFiniteNumber(opts.aspect_ratio) || opts.aspect_ratio <= 0) {
|
|
51071
|
+
stop$1('Invalid aspect-ratio parameter:', opts.aspect_ratio);
|
|
51072
|
+
}
|
|
51073
|
+
fixedAspect = opts.aspect_ratio;
|
|
51074
|
+
}
|
|
51075
|
+
|
|
51076
|
+
// 2. Extent
|
|
51077
|
+
if (offsetArg) {
|
|
51078
|
+
applyPercentageOffsets(bbox, offsetArg);
|
|
51079
|
+
// Pass a page height only when the shape is fixed. A derived height
|
|
51080
|
+
// follows the extent, so letting it pad the bbox here would hold a
|
|
51081
|
+
// re-fitted frame to its old shape instead of its new bounds.
|
|
51082
|
+
applyPixelOffsets(bbox, frame.width,
|
|
51083
|
+
fixedAspect ? frame.width / fixedAspect : null, offsetArg);
|
|
51084
|
+
}
|
|
51085
|
+
requireValidBbox(bbox);
|
|
51086
|
+
if (fixedAspect) {
|
|
51087
|
+
fillOutBbox(bbox, fixedAspect, 1);
|
|
51088
|
+
}
|
|
51089
|
+
|
|
51090
|
+
// 3. Nominal size
|
|
51091
|
+
if (opts.width !== undefined) {
|
|
51092
|
+
widthSize = parseFrameSize(opts.width);
|
|
51093
|
+
requirePositiveSize(widthSize.valuePx, 'width', opts.width);
|
|
51094
|
+
}
|
|
51095
|
+
if (opts.height !== undefined) {
|
|
51096
|
+
heightSize = parseFrameSize(opts.height);
|
|
51097
|
+
requirePositiveSize(heightSize.valuePx, 'height', opts.height);
|
|
51098
|
+
}
|
|
51099
|
+
if (widthSize && heightSize) {
|
|
51100
|
+
effectiveAspect = widthSize.valuePx / heightSize.valuePx;
|
|
51101
|
+
if (opts.aspect_ratio !== undefined &&
|
|
51102
|
+
ratiosDiffer(effectiveAspect, opts.aspect_ratio)) {
|
|
51103
|
+
stop$1('Contradictory width, height and aspect-ratio values');
|
|
51104
|
+
}
|
|
51105
|
+
width = widthSize.valuePx;
|
|
51106
|
+
units = widthSize.units;
|
|
51107
|
+
fixedAspect = effectiveAspect;
|
|
51108
|
+
fillOutBbox(bbox, fixedAspect, 1);
|
|
51109
|
+
} else if (widthSize) {
|
|
51110
|
+
width = widthSize.valuePx;
|
|
51111
|
+
units = widthSize.units;
|
|
51112
|
+
} else if (heightSize) {
|
|
51113
|
+
effectiveAspect = fixedAspect || getBboxAspect(bbox);
|
|
51114
|
+
width = heightSize.valuePx * effectiveAspect;
|
|
51115
|
+
units = heightSize.units;
|
|
51116
|
+
} else if (opts.fix_scale) {
|
|
51117
|
+
// Hold ground units per output pixel, so the page grows and shrinks with
|
|
51118
|
+
// the extent instead of the scale changing to fit the extent on it.
|
|
51119
|
+
width = frame.width * getBboxWidth(bbox) / getBboxWidth(frame.bbox);
|
|
51120
|
+
requirePositiveSize(width, 'fix-scale', width);
|
|
51121
|
+
}
|
|
51122
|
+
|
|
51123
|
+
requireValidBbox(bbox);
|
|
51124
|
+
effectiveAspect = fixedAspect || getBboxAspect(bbox);
|
|
51125
|
+
height = Math.round(width / effectiveAspect);
|
|
51126
|
+
|
|
51127
|
+
noteLayerWillChange(lyr, {operation: OPERATION$1, unit: 'shapes'});
|
|
51128
|
+
lyr.data.captureTableBefore({operation: OPERATION$1});
|
|
51129
|
+
rec.width = width;
|
|
51130
|
+
rec.height = height;
|
|
51131
|
+
rec.frame_units = units;
|
|
51132
|
+
if (fixedAspect) {
|
|
51133
|
+
rec.frame_aspect_ratio = fixedAspect;
|
|
51134
|
+
} else {
|
|
51135
|
+
delete rec.frame_aspect_ratio;
|
|
51136
|
+
}
|
|
51137
|
+
lyr.data.markChanged({operation: OPERATION$1});
|
|
51138
|
+
rebuildFrameLayerGeometry(lyr, dataset, new Bounds(bbox));
|
|
51139
|
+
markLayerChanged(lyr, {operation: OPERATION$1, unit: 'shapes'});
|
|
51140
|
+
}
|
|
51141
|
+
|
|
51142
|
+
function hasUpdateOptions(opts) {
|
|
51143
|
+
return opts.bbox !== undefined ||
|
|
51144
|
+
opts.width !== undefined ||
|
|
51145
|
+
opts.height !== undefined ||
|
|
51146
|
+
opts.aspect_ratio !== undefined ||
|
|
51147
|
+
opts.auto_aspect ||
|
|
51148
|
+
opts.fix_scale ||
|
|
51149
|
+
opts.offset !== undefined ||
|
|
51150
|
+
opts.offsets !== undefined;
|
|
51151
|
+
}
|
|
51152
|
+
|
|
51153
|
+
function getBboxWidth(bbox) {
|
|
51154
|
+
return bbox[2] - bbox[0];
|
|
51155
|
+
}
|
|
51156
|
+
|
|
51157
|
+
function getFixedAspect(rec) {
|
|
51158
|
+
return utils.isFiniteNumber(rec.frame_aspect_ratio) &&
|
|
51159
|
+
rec.frame_aspect_ratio > 0 ? rec.frame_aspect_ratio : null;
|
|
51160
|
+
}
|
|
51161
|
+
|
|
51162
|
+
function getBboxAspect(bbox) {
|
|
51163
|
+
return (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
|
|
51164
|
+
}
|
|
51165
|
+
|
|
51166
|
+
function requireValidBbox(bbox) {
|
|
51167
|
+
if (!bbox.every(utils.isFiniteNumber) ||
|
|
51168
|
+
bbox[2] - bbox[0] <= 0 ||
|
|
51169
|
+
bbox[3] - bbox[1] <= 0) {
|
|
51170
|
+
stop$1('Frame has a collapsed bbox');
|
|
51171
|
+
}
|
|
51172
|
+
}
|
|
51173
|
+
|
|
51174
|
+
function requirePositiveSize(value, name, arg) {
|
|
51175
|
+
if (!utils.isFiniteNumber(value) || value <= 0) {
|
|
51176
|
+
stop$1('Invalid ' + name + ' parameter:', arg);
|
|
51177
|
+
}
|
|
51178
|
+
}
|
|
51179
|
+
|
|
51180
|
+
function ratiosDiffer(a, b) {
|
|
51181
|
+
return Math.abs(a - b) > 1e-10 * Math.max(1, Math.abs(a), Math.abs(b));
|
|
51182
|
+
}
|
|
51183
|
+
|
|
51184
|
+
cmd.updateFrame = updateFrame;
|
|
51185
|
+
|
|
51186
|
+
var OPERATION = 'update-label';
|
|
51187
|
+
|
|
51188
|
+
cmd.updateLabel = updateLabel;
|
|
51189
|
+
|
|
51190
|
+
// Rewrites the knots of a label that already exists.
|
|
51191
|
+
//
|
|
51192
|
+
// -add-label creates and -style sets properties, so neither can move a label:
|
|
51193
|
+
// a label's knots are its geometry, not an attribute. This is the command the
|
|
51194
|
+
// GUI emits when a knot or an anchor is dragged, so that an interactive move is
|
|
51195
|
+
// an ordinary command with undo and session history rather than a direct
|
|
51196
|
+
// mutation with hand-rolled undo.
|
|
51197
|
+
//
|
|
51198
|
+
// See docs/development/label-tool-design.md.
|
|
51199
|
+
function updateLabel(targetLayers, dataset, opts) {
|
|
51200
|
+
var lyr, id, coords, rec;
|
|
51201
|
+
if (targetLayers.length > 1) {
|
|
51202
|
+
// One coordinate list describes one label, so there is no sensible reading
|
|
51203
|
+
// of this across layers.
|
|
51204
|
+
stop$1('Command expects a single target layer');
|
|
51205
|
+
}
|
|
51206
|
+
lyr = targetLayers[0];
|
|
51207
|
+
id = getTargetId(lyr, opts);
|
|
51208
|
+
coords = parseLabelCoords(opts.coordinates);
|
|
51209
|
+
rec = getLayerDataTable(lyr).getRecords()[id];
|
|
51210
|
+
if (!featureIsLabel(rec)) {
|
|
51211
|
+
// Moving an arbitrary point through a command named -update-label would be
|
|
51212
|
+
// a surprising way to succeed, and in the GUI it would mean the hit test
|
|
51213
|
+
// handed over the wrong feature.
|
|
51214
|
+
stop$1('Feature', id, 'in layer "' + layerName(lyr) + '" is not a label ' +
|
|
51215
|
+
'(it has no label-text property)');
|
|
51216
|
+
}
|
|
51217
|
+
warnIfCurveIsUnprojected(dataset, coords.length);
|
|
51218
|
+
// Undo works from what a command declares it is about to change, so an edit
|
|
51219
|
+
// that does not say so is an edit that cannot be undone -- the shapes array
|
|
51220
|
+
// keeps its identity here, leaving nothing for the transaction to notice
|
|
51221
|
+
// afterwards.
|
|
51222
|
+
noteLayerWillChange(lyr, {operation: OPERATION, unit: 'shapes'});
|
|
51223
|
+
// Point shapes hold their coordinates directly, so this is the whole edit.
|
|
51224
|
+
// The pairs are copied rather than aliased: the GUI drags a live copy of the
|
|
51225
|
+
// knots and would otherwise keep a handle on the layer's own arrays.
|
|
51226
|
+
lyr.shapes[id] = coords.map(function(p) { return [p[0], p[1]]; });
|
|
51227
|
+
markLayerChanged(lyr, {operation: OPERATION, unit: 'shapes'});
|
|
51228
|
+
// Nothing about the label's text is touched, and nothing needs to be: a text
|
|
51229
|
+
// measurement describes the *text*, and moving a knot changes the length of
|
|
51230
|
+
// the *path*. The fit check compares the two at render time, against the
|
|
51231
|
+
// curve as it then stands, so a move can turn a fitting label into an
|
|
51232
|
+
// overflowing one with no measurement going out of date.
|
|
51233
|
+
}
|
|
51234
|
+
|
|
51235
|
+
// Which feature to move. One coordinate list can only describe one label, so
|
|
51236
|
+
// this takes a single id rather than the ids= list that -style accepts.
|
|
51237
|
+
function getTargetId(lyr, opts) {
|
|
51238
|
+
var ids = opts.ids;
|
|
51239
|
+
var id;
|
|
51240
|
+
requirePointLayer(lyr);
|
|
51241
|
+
if (!ids || ids.length === 0) {
|
|
51242
|
+
stop$1('Missing required ids parameter (the feature id of the label to update)');
|
|
51243
|
+
}
|
|
51244
|
+
if (ids.length > 1) {
|
|
51245
|
+
stop$1('Command expects a single feature id; received', ids.join(','));
|
|
51246
|
+
}
|
|
51247
|
+
id = ids[0];
|
|
51248
|
+
if (id >= 0 === false || id >= getFeatureCount(lyr)) {
|
|
51249
|
+
stop$1('Layer "' + layerName(lyr) + '" has no feature with id', id);
|
|
51250
|
+
}
|
|
51251
|
+
if (!lyr.shapes || !lyr.shapes[id]) {
|
|
51252
|
+
stop$1('Feature', id, 'in layer "' + layerName(lyr) + '" has no geometry');
|
|
51253
|
+
}
|
|
51254
|
+
return id;
|
|
51255
|
+
}
|
|
51256
|
+
|
|
51257
|
+
function requirePointLayer(lyr) {
|
|
51258
|
+
if (!lyr || getFeatureCount(lyr) === 0) {
|
|
51259
|
+
stop$1('Missing a target layer containing labels');
|
|
51260
|
+
}
|
|
51261
|
+
if (lyr.geometry_type != 'point') {
|
|
51262
|
+
stop$1('Labels can only be updated in a point layer; target "' +
|
|
51263
|
+
layerName(lyr) + '" contains ' + (lyr.geometry_type || 'no geometry'));
|
|
51264
|
+
}
|
|
51265
|
+
}
|
|
51266
|
+
|
|
51267
|
+
function layerName(lyr) {
|
|
51268
|
+
return lyr && lyr.name || '[unnamed]';
|
|
51269
|
+
}
|
|
51270
|
+
|
|
51271
|
+
cmd.addShape = addShape;
|
|
51272
|
+
|
|
51273
|
+
function addShape(targetLayers, targetDataset, opts) {
|
|
51274
|
+
if (targetLayers.length > 1) {
|
|
51275
|
+
stop$1('Command expects a single target layer');
|
|
51276
|
+
}
|
|
51277
|
+
var targetLyr = targetLayers[0]; // may be undefined
|
|
51278
|
+
var targetType = !opts.no_replace && targetLyr && targetLyr.geometry_type || null;
|
|
51279
|
+
var dataset = importGeoJSON(toFeature(opts, targetType));
|
|
51280
|
+
var outputLyr = mergeDatasetsIntoDataset(targetDataset, [dataset])[0];
|
|
51281
|
+
if (opts.no_replace || !targetLyr) {
|
|
51282
|
+
// create new layer
|
|
51283
|
+
setOutputLayerName(outputLyr, targetLyr && targetLyr.name, null, opts);
|
|
51284
|
+
return [outputLyr];
|
|
51285
|
+
}
|
|
51286
|
+
// merge into target layer
|
|
51287
|
+
return cmd.mergeLayers([targetLyr, outputLyr], {force: true});
|
|
51288
|
+
}
|
|
51289
|
+
|
|
51290
|
+
function toFeature(opts, geomType) {
|
|
51291
|
+
if (opts.geojson) {
|
|
51292
|
+
return parseArg(opts.geojson);
|
|
51293
|
+
}
|
|
51294
|
+
|
|
51295
|
+
var geom = opts.coordinates && parseCoordsAsGeometry(opts.coordinates) || null;
|
|
51296
|
+
|
|
51297
|
+
if (!geom) {
|
|
51298
|
+
stop$1('Missing required shape coordinates');
|
|
51299
|
+
}
|
|
51300
|
+
|
|
51301
|
+
if (geomType == 'point' && geom.type != 'Point') {
|
|
51302
|
+
stop$1('Expected point coordinates, received', geom.type);
|
|
51303
|
+
}
|
|
51304
|
+
|
|
51305
|
+
if (geomType == 'polygon' && geom.type != 'Polygon') {
|
|
51306
|
+
stop$1('Expected polygon coordinates, received', geom.type);
|
|
51307
|
+
}
|
|
51308
|
+
|
|
51309
|
+
if (geomType == 'polyline') {
|
|
51310
|
+
if (geom.type == 'Polygon') {
|
|
51311
|
+
geom.coordinates = geom.coordinates[0];
|
|
51312
|
+
geom.type = 'LineString';
|
|
51313
|
+
} else {
|
|
51314
|
+
stop$1('Expected polyline coordinates, received', geom.type);
|
|
51315
|
+
}
|
|
51316
|
+
}
|
|
51317
|
+
|
|
51318
|
+
return {
|
|
51319
|
+
type: 'Feature',
|
|
51320
|
+
properties: parseProperties(opts.properties),
|
|
51321
|
+
geometry: geom
|
|
51322
|
+
};
|
|
51323
|
+
}
|
|
51324
|
+
|
|
51325
|
+
function parseArg(obj) {
|
|
51326
|
+
return typeof obj == 'string' ? JSON.parse(obj) : obj;
|
|
51327
|
+
}
|
|
51328
|
+
|
|
51329
|
+
function parseProperties(arg) {
|
|
51330
|
+
if (!arg) return null;
|
|
51331
|
+
return parseArg(arg);
|
|
51332
|
+
}
|
|
51333
|
+
|
|
51334
|
+
function isArrayOfNumbers(arr) {
|
|
51335
|
+
return arr.length >= 2 && arr.every(utils.isNumber);
|
|
51336
|
+
}
|
|
51337
|
+
|
|
51338
|
+
function isClosedPath$1(arr) {
|
|
51339
|
+
return isArrayOfPoints(arr) && arr.length > 3 && samePoint$2(arr[0], arr[arr.length - 1]);
|
|
51340
|
+
}
|
|
51341
|
+
|
|
51342
|
+
function samePoint$2(a, b) {
|
|
51343
|
+
return a[0] == b[0] && a[1] == b[1];
|
|
51344
|
+
}
|
|
49986
51345
|
|
|
49987
51346
|
function isArrayOfPoints(arr) {
|
|
49988
51347
|
return arr.every(isPoint);
|
|
@@ -54277,67 +55636,6 @@ ${svg}
|
|
|
54277
55636
|
|
|
54278
55637
|
}
|
|
54279
55638
|
|
|
54280
|
-
function DatasetEditor(dataset) {
|
|
54281
|
-
var layers = [];
|
|
54282
|
-
var arcs = [];
|
|
54283
|
-
|
|
54284
|
-
this.done = function() {
|
|
54285
|
-
dataset.layers = layers;
|
|
54286
|
-
if (arcs.length) {
|
|
54287
|
-
noteDatasetWillChange(dataset, {operation: 'DatasetEditor.done', unit: 'arcs'});
|
|
54288
|
-
dataset.arcs = new ArcCollection(arcs);
|
|
54289
|
-
markDatasetChanged(dataset, {operation: 'DatasetEditor.done', unit: 'arcs'});
|
|
54290
|
-
withActiveUndoTransaction(null, function() {
|
|
54291
|
-
buildTopology(dataset);
|
|
54292
|
-
});
|
|
54293
|
-
}
|
|
54294
|
-
};
|
|
54295
|
-
|
|
54296
|
-
this.editLayer = function(lyr, cb) {
|
|
54297
|
-
var type = lyr.geometry_type;
|
|
54298
|
-
if (dataset.layers.indexOf(lyr) != layers.length) {
|
|
54299
|
-
error('Layer was edited out-of-order');
|
|
54300
|
-
}
|
|
54301
|
-
if (!type) {
|
|
54302
|
-
layers.push(lyr);
|
|
54303
|
-
return;
|
|
54304
|
-
}
|
|
54305
|
-
var shapes = lyr.shapes.map(function(shape, shpId) {
|
|
54306
|
-
var shape2 = [], retn, input;
|
|
54307
|
-
for (var i=0, n=shape ? shape.length : 0; i<n; i++) {
|
|
54308
|
-
input = type == 'point' ? shape[i] : idsToCoords(shape[i]);
|
|
54309
|
-
retn = cb(input, i, shape);
|
|
54310
|
-
if (!Array.isArray(retn)) continue;
|
|
54311
|
-
if (type == 'point') {
|
|
54312
|
-
shape2.push(retn);
|
|
54313
|
-
} else if (type == 'polygon' || type == 'polyline') {
|
|
54314
|
-
extendPathShape(shape2, retn || []);
|
|
54315
|
-
}
|
|
54316
|
-
}
|
|
54317
|
-
return shape2.length > 0 ? shape2 : null;
|
|
54318
|
-
});
|
|
54319
|
-
noteLayerWillChange(lyr, {operation: 'DatasetEditor.editLayer', unit: 'shapes'});
|
|
54320
|
-
layers.push(Object.assign(lyr, {shapes: shapes}));
|
|
54321
|
-
markLayerChanged(lyr, {operation: 'DatasetEditor.editLayer', unit: 'shapes'});
|
|
54322
|
-
};
|
|
54323
|
-
|
|
54324
|
-
function extendPathShape(shape, parts) {
|
|
54325
|
-
for (var i=0; i<parts.length; i++) {
|
|
54326
|
-
shape.push([arcs.length]);
|
|
54327
|
-
arcs.push(parts[i]);
|
|
54328
|
-
}
|
|
54329
|
-
}
|
|
54330
|
-
|
|
54331
|
-
function idsToCoords(ids) {
|
|
54332
|
-
var coords = [];
|
|
54333
|
-
var iter = dataset.arcs.getShapeIter(ids);
|
|
54334
|
-
while (iter.hasNext()) {
|
|
54335
|
-
coords.push([iter.x, iter.y]);
|
|
54336
|
-
}
|
|
54337
|
-
return coords;
|
|
54338
|
-
}
|
|
54339
|
-
}
|
|
54340
|
-
|
|
54341
55639
|
function makePolylineBuffer(lyr, dataset, opts) {
|
|
54342
55640
|
time('buffer');
|
|
54343
55641
|
var spherical = isLatLngCRS(getDatasetCRS(dataset));
|
|
@@ -61481,625 +62779,211 @@ ${svg}
|
|
|
61481
62779
|
if (data.pointCount === 0) return [];
|
|
61482
62780
|
return groupPolygonRings(data.pathData, arcs, false).map(function(paths) {
|
|
61483
62781
|
return paths.map(function(path) {
|
|
61484
|
-
return path.points.map(function(p) {
|
|
61485
|
-
return p.concat();
|
|
61486
|
-
});
|
|
61487
|
-
});
|
|
61488
|
-
});
|
|
61489
|
-
}
|
|
61490
|
-
|
|
61491
|
-
function getPolygonGeometry(shape, arcs) {
|
|
61492
|
-
var coords = getPolygonMultiPolygonCoords(shape, arcs);
|
|
61493
|
-
return coords.length > 0 ? {
|
|
61494
|
-
type: 'MultiPolygon',
|
|
61495
|
-
coordinates: coords
|
|
61496
|
-
} : null;
|
|
61497
|
-
}
|
|
61498
|
-
|
|
61499
|
-
function getBufferDataset(coords) {
|
|
61500
|
-
return importGeoJSON({
|
|
61501
|
-
type: 'GeometryCollection',
|
|
61502
|
-
geometries: [{
|
|
61503
|
-
type: 'MultiPolygon',
|
|
61504
|
-
coordinates: coords
|
|
61505
|
-
}]
|
|
61506
|
-
}, {type: 'polygon'});
|
|
61507
|
-
}
|
|
61508
|
-
|
|
61509
|
-
// Union a set of winding-fill offset rings (which self-overlap) into clean,
|
|
61510
|
-
// non-self-overlapping MultiPolygon coordinates, via the winding-number
|
|
61511
|
-
// dissolve. Used by the topological pipeline to feed an ordinary polygon into
|
|
61512
|
-
// the shared mosaic (whose boundary-flood membership cannot resolve the
|
|
61513
|
-
// self-overlapping construction ring directly).
|
|
61514
|
-
function dissolveOffsetRingsToCoords(coords, opts, outlineDissolve) {
|
|
61515
|
-
if (!coords || coords.length === 0) return [];
|
|
61516
|
-
var dataset = getBufferDataset(coords);
|
|
61517
|
-
if (!dataset.arcs) return [];
|
|
61518
|
-
var dissolveOpts = outlineDissolve ?
|
|
61519
|
-
getOutlineBufferDissolveOpts(opts) :
|
|
61520
|
-
Object.assign({}, opts, {winding_fill: true});
|
|
61521
|
-
dissolveBufferDataset2(dataset, dissolveOpts);
|
|
61522
|
-
var lyr = dataset.layers[0];
|
|
61523
|
-
var shape = lyr.shapes && lyr.shapes[0];
|
|
61524
|
-
return shape ? getPolygonMultiPolygonCoords(shape, dataset.arcs) : [];
|
|
61525
|
-
}
|
|
61526
|
-
|
|
61527
|
-
function getBufferMultiPolygonCoords(paths, distance, bufferMaker) {
|
|
61528
|
-
var features, coords = [];
|
|
61529
|
-
if (paths.length === 0) return coords;
|
|
61530
|
-
features = bufferMaker(paths, distance) || [];
|
|
61531
|
-
features.forEach(function(feat) {
|
|
61532
|
-
var geom = feat && feat.geometry;
|
|
61533
|
-
if (geom && geom.type == 'MultiPolygon') {
|
|
61534
|
-
coords = coords.concat(geom.coordinates);
|
|
61535
|
-
}
|
|
61536
|
-
});
|
|
61537
|
-
return coords;
|
|
61538
|
-
}
|
|
61539
|
-
|
|
61540
|
-
function getPolygonBufferPathData(shape, uniqueArcTest) {
|
|
61541
|
-
var data = {paths: [], split: false};
|
|
61542
|
-
(shape || []).forEach(function(path) {
|
|
61543
|
-
var paths = uniqueArcTest ? splitPathAtSharedArcs(path, uniqueArcTest) :
|
|
61544
|
-
[path.concat()];
|
|
61545
|
-
if (paths.length != 1 || paths[0].length != path.length ||
|
|
61546
|
-
!paths[0].every(function(arcId, i) {
|
|
61547
|
-
return arcId == path[i];
|
|
61548
|
-
})) {
|
|
61549
|
-
data.split = true;
|
|
61550
|
-
}
|
|
61551
|
-
data.paths = data.paths.concat(paths);
|
|
61552
|
-
});
|
|
61553
|
-
return data;
|
|
61554
|
-
}
|
|
61555
|
-
|
|
61556
|
-
function splitPathAtSharedArcs(path, uniqueArcTest) {
|
|
61557
|
-
var flags = path.map(uniqueArcTest);
|
|
61558
|
-
var firstShared = flags.indexOf(false);
|
|
61559
|
-
var chains = [];
|
|
61560
|
-
var chain = [];
|
|
61561
|
-
var start, i, arcId;
|
|
61562
|
-
if (firstShared == -1) return [path.concat()];
|
|
61563
|
-
start = (firstShared + 1) % path.length;
|
|
61564
|
-
for (i = 0; i < path.length; i++) {
|
|
61565
|
-
arcId = path[(start + i) % path.length];
|
|
61566
|
-
if (uniqueArcTest(arcId)) {
|
|
61567
|
-
chain.push(arcId);
|
|
61568
|
-
} else if (chain.length > 0) {
|
|
61569
|
-
chains.push(chain);
|
|
61570
|
-
chain = [];
|
|
61571
|
-
}
|
|
61572
|
-
}
|
|
61573
|
-
if (chain.length > 0) {
|
|
61574
|
-
chains.push(chain);
|
|
61575
|
-
}
|
|
61576
|
-
return chains;
|
|
61577
|
-
}
|
|
61578
|
-
|
|
61579
|
-
function getUniqueArcTest(lyr, arcs) {
|
|
61580
|
-
var classify = getArcClassifier(lyr, arcs, {reusable: true})(function(a, b) {
|
|
61581
|
-
return b == -1 ? 'unique' : null;
|
|
61582
|
-
});
|
|
61583
|
-
return function(arcId) {
|
|
61584
|
-
return !!classify(arcId);
|
|
61585
|
-
};
|
|
61586
|
-
}
|
|
61587
|
-
|
|
61588
|
-
function ringArea(ring) {
|
|
61589
|
-
var iter = new PointIter(ring);
|
|
61590
|
-
return getSphericalPathArea2(iter);
|
|
61591
|
-
}
|
|
61592
|
-
|
|
61593
|
-
function makePointBuffer(lyr, dataset, opts) {
|
|
61594
|
-
var geojson = makePointBufferGeoJSON(lyr, dataset, opts);
|
|
61595
|
-
return importGeoJSON(geojson, {});
|
|
61596
|
-
}
|
|
61597
|
-
|
|
61598
|
-
// Make a single geodetic circle
|
|
61599
|
-
function getCircleGeoJSON(center, radius, vertices, opts) {
|
|
61600
|
-
var n = 360;
|
|
61601
|
-
var geod = getGeodeticSegmentFunction(parseCrsString$1('wgs84')); // ?
|
|
61602
|
-
if (opts.inset) {
|
|
61603
|
-
radius -= opts.inset;
|
|
61604
|
-
}
|
|
61605
|
-
return opts.geometry_type == 'polyline' ?
|
|
61606
|
-
getPointBufferLineString([center], radius, n, geod) :
|
|
61607
|
-
getPointBufferPolygon([center], radius, n, geod, true);
|
|
61608
|
-
}
|
|
61609
|
-
|
|
61610
|
-
// Convert a point layer to circles
|
|
61611
|
-
function makePointBufferGeoJSON(lyr, dataset, opts) {
|
|
61612
|
-
var vertices = opts.vertices || 72;
|
|
61613
|
-
var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
|
|
61614
|
-
var crs = getDatasetCRS(dataset);
|
|
61615
|
-
var spherical = isLatLngCRS(crs);
|
|
61616
|
-
var geod = getGeodeticSegmentFunction(crs);
|
|
61617
|
-
var geometries = lyr.shapes.map(function(shape, i) {
|
|
61618
|
-
var dist = distanceFn(i);
|
|
61619
|
-
if (!dist || !shape) return null;
|
|
61620
|
-
return getPointBufferPolygon(shape, dist, vertices, geod, spherical);
|
|
61621
|
-
});
|
|
61622
|
-
// TODO: make sure that importer supports null geometries (nonstandard GeoJSON);
|
|
61623
|
-
return {
|
|
61624
|
-
type: 'GeometryCollection',
|
|
61625
|
-
geometries: geometries
|
|
61626
|
-
};
|
|
61627
|
-
}
|
|
61628
|
-
|
|
61629
|
-
function getPointBufferPolygon(points, distance, vertices, geod, spherical) {
|
|
61630
|
-
var rings = [], coords, coords2;
|
|
61631
|
-
if (!points || !points.length) return null;
|
|
61632
|
-
for (var i=0; i<points.length; i++) {
|
|
61633
|
-
coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
|
|
61634
|
-
if (!spherical) {
|
|
61635
|
-
rings.push([coords]);
|
|
61636
|
-
} else if (countCrosses(coords) > 0) {
|
|
61637
|
-
coords2 = removePolygonCrosses([coords]);
|
|
61638
|
-
while (coords2.length > 0) rings.push([coords2.pop()]); // geojson polygon coords, no hole
|
|
61639
|
-
} else if (ringArea(coords) < 0) {
|
|
61640
|
-
// negative spherical area: CCW ring, indicating a circle of >180 degrees
|
|
61641
|
-
// that fully encloses both poles and the antimeridian.
|
|
61642
|
-
// need to add an enclosure around the entire sphere
|
|
61643
|
-
// TODO: compare to distance param as a sanity check
|
|
61644
|
-
rings.push([
|
|
61645
|
-
[[180, 90], [180, -90], [0, -90], [-180, -90], [-180, 90], [0, 90], [180, 90]],
|
|
61646
|
-
coords
|
|
61647
|
-
]);
|
|
61648
|
-
} else {
|
|
61649
|
-
rings.push([coords]);
|
|
61650
|
-
}
|
|
61651
|
-
}
|
|
61652
|
-
return {
|
|
61653
|
-
type: 'MultiPolygon',
|
|
61654
|
-
coordinates: rings
|
|
61655
|
-
};
|
|
61656
|
-
}
|
|
61657
|
-
|
|
61658
|
-
function getPointBufferLineString(points, distance, vertices, geod) {
|
|
61659
|
-
var rings = [], coords;
|
|
61660
|
-
if (!points || !points.length) return null;
|
|
61661
|
-
for (var i=0; i<points.length; i++) {
|
|
61662
|
-
coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
|
|
61663
|
-
coords = removePolylineCrosses(coords);
|
|
61664
|
-
while (coords.length > 0) rings.push(coords.pop());
|
|
61665
|
-
}
|
|
61666
|
-
return rings.length == 1 ? {
|
|
61667
|
-
type: 'LineString',
|
|
61668
|
-
coordinates: rings[0]
|
|
61669
|
-
} : {
|
|
61670
|
-
type: 'MultiLineString',
|
|
61671
|
-
coordinates: rings
|
|
61672
|
-
};
|
|
61673
|
-
}
|
|
61674
|
-
|
|
61675
|
-
// Returns array of [x, y] coordinates in a closed ring
|
|
61676
|
-
function getPointBufferCoordinates(center, meterDist, vertices, geod) {
|
|
61677
|
-
var coords = [],
|
|
61678
|
-
angle = 360 / vertices,
|
|
61679
|
-
theta;
|
|
61680
|
-
for (var i=0; i<vertices; i++) {
|
|
61681
|
-
// offsetting by half a step so 4 sides are flat, not pointy
|
|
61682
|
-
// (looks better on low-vertex circles)
|
|
61683
|
-
theta = (i + 0.5) * angle % 360;
|
|
61684
|
-
coords.push(geod(center[0], center[1], theta, meterDist));
|
|
61685
|
-
}
|
|
61686
|
-
coords.push(coords[0].concat());
|
|
61687
|
-
return coords;
|
|
61688
|
-
}
|
|
61689
|
-
|
|
61690
|
-
// Returns number of arcs that were removed
|
|
61691
|
-
function editArcs(arcs, onPoint) {
|
|
61692
|
-
var nn2 = [],
|
|
61693
|
-
xx2 = [],
|
|
61694
|
-
yy2 = [],
|
|
61695
|
-
errors = 0,
|
|
61696
|
-
n;
|
|
61697
|
-
|
|
61698
|
-
arcs.forEach(function(arc, i) {
|
|
61699
|
-
editArc(arc, onPoint);
|
|
61700
|
-
});
|
|
61701
|
-
arcs.updateVertexData(nn2, xx2, yy2);
|
|
61702
|
-
return errors;
|
|
61703
|
-
|
|
61704
|
-
function append(p) {
|
|
61705
|
-
if (p) {
|
|
61706
|
-
xx2.push(p[0]);
|
|
61707
|
-
yy2.push(p[1]);
|
|
61708
|
-
n++;
|
|
61709
|
-
}
|
|
61710
|
-
}
|
|
61711
|
-
|
|
61712
|
-
function editArc(arc, cb) {
|
|
61713
|
-
var x, y, xp, yp, retn;
|
|
61714
|
-
var valid = true;
|
|
61715
|
-
var i = 0;
|
|
61716
|
-
n = 0;
|
|
61717
|
-
while (arc.hasNext()) {
|
|
61718
|
-
x = arc.x;
|
|
61719
|
-
y = arc.y;
|
|
61720
|
-
retn = cb(append, x, y, xp, yp, i++);
|
|
61721
|
-
if (retn === false) {
|
|
61722
|
-
valid = false;
|
|
61723
|
-
// assumes that it's ok for the arc iterator to be interrupted.
|
|
61724
|
-
break;
|
|
61725
|
-
}
|
|
61726
|
-
xp = x;
|
|
61727
|
-
yp = y;
|
|
61728
|
-
}
|
|
61729
|
-
if (valid && n == 1) {
|
|
61730
|
-
// only one valid point was added to this arc (invalid)
|
|
61731
|
-
// e.g. this could happen during reprojection.
|
|
61732
|
-
// making this arc empty
|
|
61733
|
-
// error("An invalid arc was created");
|
|
61734
|
-
message("An invalid arc was created");
|
|
61735
|
-
valid = false;
|
|
61736
|
-
}
|
|
61737
|
-
if (valid) {
|
|
61738
|
-
nn2.push(n);
|
|
61739
|
-
} else {
|
|
61740
|
-
// remove any points that were added for an invalid arc
|
|
61741
|
-
while (n-- > 0) {
|
|
61742
|
-
xx2.pop();
|
|
61743
|
-
yy2.pop();
|
|
61744
|
-
}
|
|
61745
|
-
nn2.push(0); // add empty arc (to preserve mapping from paths to arcs)
|
|
61746
|
-
errors++;
|
|
61747
|
-
}
|
|
61748
|
-
}
|
|
61749
|
-
}
|
|
61750
|
-
|
|
61751
|
-
// Planar densification by an interval
|
|
61752
|
-
function densifyPathByInterval(coords, interval, interpolate) {
|
|
61753
|
-
if (findMaxPathInterval(coords) < interval) return coords;
|
|
61754
|
-
if (!interpolate) {
|
|
61755
|
-
interpolate = getIntervalInterpolator(interval);
|
|
61756
|
-
}
|
|
61757
|
-
var coords2 = [coords[0]], a, b;
|
|
61758
|
-
for (var i=1, n=coords.length; i<n; i++) {
|
|
61759
|
-
a = coords[i-1];
|
|
61760
|
-
b = coords[i];
|
|
61761
|
-
if (geom.distance2D(a[0], a[1], b[0], b[1]) > interval + 1e-4) {
|
|
61762
|
-
appendArr(coords2, interpolate(a, b));
|
|
61763
|
-
}
|
|
61764
|
-
coords2.push(b);
|
|
61765
|
-
}
|
|
61766
|
-
return coords2;
|
|
61767
|
-
}
|
|
61768
|
-
|
|
61769
|
-
function getIntervalInterpolator(interval) {
|
|
61770
|
-
return function(a, b) {
|
|
61771
|
-
var points = [];
|
|
61772
|
-
// var rev = a[0] == b[0] ? a[1] > b[1] : a[0] > b[0];
|
|
61773
|
-
var dist = geom.distance2D(a[0], a[1], b[0], b[1]);
|
|
61774
|
-
var n = Math.round(dist / interval) - 1;
|
|
61775
|
-
var dx = (b[0] - a[0]) / (n + 1),
|
|
61776
|
-
dy = (b[1] - a[1]) / (n + 1);
|
|
61777
|
-
for (var i=1; i<=n; i++) {
|
|
61778
|
-
points.push([a[0] + dx * i, a[1] + dy * i]);
|
|
61779
|
-
}
|
|
61780
|
-
return points;
|
|
61781
|
-
};
|
|
61782
|
-
}
|
|
61783
|
-
|
|
61784
|
-
|
|
61785
|
-
// Interpolate the same points regardless of segment direction
|
|
61786
|
-
function densifyAntimeridianSegment(a, b, interval) {
|
|
61787
|
-
var y1, y2;
|
|
61788
|
-
var coords = [];
|
|
61789
|
-
var ascending = a[1] < b[1];
|
|
61790
|
-
if (a[0] != b[0]) error('Expected an edge segment');
|
|
61791
|
-
if (ascending) {
|
|
61792
|
-
y1 = a[1];
|
|
61793
|
-
y2 = b[1];
|
|
61794
|
-
} else {
|
|
61795
|
-
y1 = b[1];
|
|
61796
|
-
y2 = a[1];
|
|
61797
|
-
}
|
|
61798
|
-
var y = Math.floor(y1 / interval) * interval + interval;
|
|
61799
|
-
while (y < y2) {
|
|
61800
|
-
coords.push([a[0], y]);
|
|
61801
|
-
y += interval;
|
|
61802
|
-
}
|
|
61803
|
-
if (!ascending) coords.reverse();
|
|
61804
|
-
return coords;
|
|
61805
|
-
}
|
|
61806
|
-
|
|
61807
|
-
function appendArr(dest, src) {
|
|
61808
|
-
for (var i=0; i<src.length; i++) dest.push(src[i]);
|
|
61809
|
-
}
|
|
61810
|
-
|
|
61811
|
-
function findMaxPathInterval(coords) {
|
|
61812
|
-
var maxSq = 0, intSq, a, b;
|
|
61813
|
-
for (var i=1, n=coords.length; i<n; i++) {
|
|
61814
|
-
a = coords[i-1];
|
|
61815
|
-
b = coords[i];
|
|
61816
|
-
intSq = geom.distanceSq(a[0], a[1], b[0], b[1]);
|
|
61817
|
-
if (intSq > maxSq) maxSq = intSq;
|
|
61818
|
-
}
|
|
61819
|
-
return Math.sqrt(maxSq);
|
|
61820
|
-
}
|
|
61821
|
-
|
|
61822
|
-
function projectAndDensifyArcs(arcs, proj) {
|
|
61823
|
-
var interval = getDefaultDensifyInterval(arcs, proj);
|
|
61824
|
-
var minIntervalSq = interval * interval * 25;
|
|
61825
|
-
var p;
|
|
61826
|
-
return editArcs(arcs, onPoint);
|
|
61827
|
-
|
|
61828
|
-
function onPoint(append, lng, lat, prevLng, prevLat, i) {
|
|
61829
|
-
var pp = p;
|
|
61830
|
-
p = proj(lng, lat);
|
|
61831
|
-
if (!p) return false; // signal that current arc contains an error
|
|
61832
|
-
|
|
61833
|
-
// Don't try to densify shorter segments (optimization)
|
|
61834
|
-
if (i > 0 && geom.distanceSq(p[0], p[1], pp[0], pp[1]) > minIntervalSq) {
|
|
61835
|
-
densifySegment(prevLng, prevLat, pp[0], pp[1], lng, lat, p[0], p[1], proj, interval)
|
|
61836
|
-
.forEach(append);
|
|
61837
|
-
}
|
|
61838
|
-
append(p);
|
|
61839
|
-
}
|
|
61840
|
-
}
|
|
61841
|
-
|
|
61842
|
-
// Use the median of intervals computed by projecting segments.
|
|
61843
|
-
// We're probing a number of points, because @proj might only be valid in
|
|
61844
|
-
// a sub-region of the dataset bbox (e.g. +proj=tpers)
|
|
61845
|
-
function findDensifyInterval(bounds, xy, proj) {
|
|
61846
|
-
var steps = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
|
|
61847
|
-
var points = [];
|
|
61848
|
-
for (var i=0; i<steps.length; i++) {
|
|
61849
|
-
for (var j=0; j<steps.length; j++) {
|
|
61850
|
-
points.push([steps[i], steps[j]]);
|
|
61851
|
-
}
|
|
61852
|
-
}
|
|
61853
|
-
var intervals = points.map(function(pos) {
|
|
61854
|
-
var x = bounds.xmin + bounds.width() * pos[0];
|
|
61855
|
-
var y = bounds.ymin + bounds.height() * pos[1];
|
|
61856
|
-
var a = proj(x, y);
|
|
61857
|
-
var b = proj(x + xy[0], y + xy[1]);
|
|
61858
|
-
return a && b ? geom.distance2D(a[0], a[1], b[0], b[1]) : Infinity;
|
|
61859
|
-
}).filter(function(int) {return int < Infinity;});
|
|
61860
|
-
return intervals.length > 0 ? utils.findMedian(intervals) : Infinity;
|
|
61861
|
-
}
|
|
61862
|
-
|
|
61863
|
-
// Kludgy way to get a useful interval for densifying a bounding box.
|
|
61864
|
-
// Uses a fraction of average bbox side length)
|
|
61865
|
-
// TODO: improve
|
|
61866
|
-
function findDensifyInterval2(bb, proj) {
|
|
61867
|
-
var a = proj(bb.centerX(), bb.centerY()),
|
|
61868
|
-
c = proj(bb.centerX(), bb.ymin), // right center
|
|
61869
|
-
d = proj(bb.xmax, bb.centerY()); // bottom center
|
|
61870
|
-
var interval = a && c && d ? (geom.distance2D(a[0], a[1], c[0], c[1]) +
|
|
61871
|
-
geom.distance2D(a[0], a[1], d[0], d[1])) / 5000 : Infinity;
|
|
61872
|
-
return interval;
|
|
61873
|
-
}
|
|
61874
|
-
|
|
61875
|
-
// Returns an interval in projected units
|
|
61876
|
-
function getDefaultDensifyInterval(arcs, proj) {
|
|
61877
|
-
var xy = getAvgSegment2(arcs),
|
|
61878
|
-
bb = arcs.getBounds(),
|
|
61879
|
-
intervalA = findDensifyInterval(bb, xy, proj),
|
|
61880
|
-
intervalB = findDensifyInterval2(bb, proj),
|
|
61881
|
-
interval = Math.min(intervalA, intervalB);
|
|
61882
|
-
if (interval == Infinity) {
|
|
61883
|
-
error('Densification error');
|
|
61884
|
-
}
|
|
61885
|
-
return interval;
|
|
61886
|
-
}
|
|
61887
|
-
|
|
61888
|
-
// Interpolate points into a projected line segment if needed to prevent large
|
|
61889
|
-
// deviations from path of original unprojected segment.
|
|
61890
|
-
// @points (optional) array of accumulated points
|
|
61891
|
-
function densifySegment(lng0, lat0, x0, y0, lng2, lat2, x2, y2, proj, interval, points) {
|
|
61892
|
-
// Find midpoint between two endpoints and project it (assumes longitude does
|
|
61893
|
-
// not wrap). TODO Consider bisecting along great circle path -- although this
|
|
61894
|
-
// would not be good for boundaries that follow line of constant latitude.
|
|
61895
|
-
var lng1 = (lng0 + lng2) / 2,
|
|
61896
|
-
lat1 = (lat0 + lat2) / 2,
|
|
61897
|
-
p = proj(lng1, lat1),
|
|
61898
|
-
distSq;
|
|
61899
|
-
if (!p) return; // TODO: consider if this is adequate for handling proj. errors
|
|
61900
|
-
distSq = geom.pointSegDistSq2(p[0], p[1], x0, y0, x2, y2); // sq displacement
|
|
61901
|
-
points = points || [];
|
|
61902
|
-
// Bisect current segment if the projected midpoint deviates from original
|
|
61903
|
-
// segment by more than the @interval parameter.
|
|
61904
|
-
// ... but don't bisect very small segments to prevent infinite recursion
|
|
61905
|
-
// (e.g. if projection function is discontinuous)
|
|
61906
|
-
if (distSq > interval * interval * 0.25 && geom.distance2D(lng0, lat0, lng2, lat2) > 0.01) {
|
|
61907
|
-
densifySegment(lng0, lat0, x0, y0, lng1, lat1, p[0], p[1], proj, interval, points);
|
|
61908
|
-
points.push(p);
|
|
61909
|
-
densifySegment(lng1, lat1, p[0], p[1], lng2, lat2, x2, y2, proj, interval, points);
|
|
61910
|
-
}
|
|
61911
|
-
return points;
|
|
61912
|
-
}
|
|
61913
|
-
|
|
61914
|
-
// Create rectangles around each feature in a layer
|
|
61915
|
-
cmd.rectangles = function(targetLyr, targetDataset, opts) {
|
|
61916
|
-
var crsInfo = getDatasetCrsInfo(targetDataset);
|
|
61917
|
-
var records = targetLyr.data ? targetLyr.data.getRecords() : null;
|
|
61918
|
-
var geometries;
|
|
61919
|
-
|
|
61920
|
-
if (opts.bbox) {
|
|
61921
|
-
geometries = bboxExpressionToGeometries(opts.bbox, targetLyr, targetDataset);
|
|
61922
|
-
|
|
61923
|
-
} else {
|
|
61924
|
-
if (!layerHasGeometry$1(targetLyr)) {
|
|
61925
|
-
stop$1("Layer is missing geometric shapes");
|
|
61926
|
-
}
|
|
61927
|
-
geometries = shapesToBoxGeometries(targetLyr, targetDataset, opts);
|
|
61928
|
-
}
|
|
61929
|
-
|
|
61930
|
-
var geojson = {
|
|
61931
|
-
type: 'FeatureCollection',
|
|
61932
|
-
features: geometries.map(function(geom, i) {
|
|
61933
|
-
var rec = records && records[i] || null;
|
|
61934
|
-
if (rec && opts.no_replace) {
|
|
61935
|
-
rec = utils.extend({}, rec); // make a copy
|
|
61936
|
-
}
|
|
61937
|
-
return {
|
|
61938
|
-
type: 'Feature',
|
|
61939
|
-
properties: rec,
|
|
61940
|
-
geometry: geom
|
|
61941
|
-
};
|
|
61942
|
-
})
|
|
61943
|
-
};
|
|
61944
|
-
var dataset = importGeoJSON(geojson, {});
|
|
61945
|
-
setDatasetCrsInfo(dataset, crsInfo);
|
|
61946
|
-
var outputLayers = mergeDatasetsIntoDataset(targetDataset, [dataset]);
|
|
61947
|
-
setOutputLayerName(outputLayers[0], targetLyr, null, opts);
|
|
61948
|
-
return outputLayers;
|
|
61949
|
-
};
|
|
61950
|
-
|
|
61951
|
-
|
|
61952
|
-
|
|
61953
|
-
|
|
61954
|
-
function shapesToBoxGeometries(lyr, dataset, opts) {
|
|
61955
|
-
var crsInfo = getDatasetCrsInfo(dataset);
|
|
61956
|
-
return lyr.shapes.map(function(shp) {
|
|
61957
|
-
var bounds = lyr.geometry_type == 'point' ?
|
|
61958
|
-
getPointFeatureBounds(shp) : dataset.arcs.getMultiShapeBounds(shp);
|
|
61959
|
-
bounds = applyRectangleOptions(bounds, crsInfo.crs, opts);
|
|
61960
|
-
if (!bounds) return null;
|
|
61961
|
-
return bboxToPolygon(bounds.toArray(), opts);
|
|
62782
|
+
return path.points.map(function(p) {
|
|
62783
|
+
return p.concat();
|
|
62784
|
+
});
|
|
62785
|
+
});
|
|
61962
62786
|
});
|
|
61963
62787
|
}
|
|
61964
62788
|
|
|
61965
|
-
function
|
|
61966
|
-
var
|
|
61967
|
-
|
|
61968
|
-
|
|
61969
|
-
|
|
61970
|
-
|
|
61971
|
-
result = compiled(i);
|
|
61972
|
-
if (!looksLikeBbox(result)) {
|
|
61973
|
-
stop$1('Invalid bbox value (expected a GeoJSON-type bbox):', result);
|
|
61974
|
-
}
|
|
61975
|
-
geometries.push(bboxToPolygon(result));
|
|
61976
|
-
}
|
|
61977
|
-
return geometries;
|
|
62789
|
+
function getPolygonGeometry(shape, arcs) {
|
|
62790
|
+
var coords = getPolygonMultiPolygonCoords(shape, arcs);
|
|
62791
|
+
return coords.length > 0 ? {
|
|
62792
|
+
type: 'MultiPolygon',
|
|
62793
|
+
coordinates: coords
|
|
62794
|
+
} : null;
|
|
61978
62795
|
}
|
|
61979
62796
|
|
|
61980
|
-
function
|
|
61981
|
-
|
|
61982
|
-
|
|
61983
|
-
|
|
61984
|
-
|
|
62797
|
+
function getBufferDataset(coords) {
|
|
62798
|
+
return importGeoJSON({
|
|
62799
|
+
type: 'GeometryCollection',
|
|
62800
|
+
geometries: [{
|
|
62801
|
+
type: 'MultiPolygon',
|
|
62802
|
+
coordinates: coords
|
|
62803
|
+
}]
|
|
62804
|
+
}, {type: 'polygon'});
|
|
61985
62805
|
}
|
|
61986
62806
|
|
|
61987
|
-
//
|
|
61988
|
-
//
|
|
61989
|
-
|
|
61990
|
-
|
|
61991
|
-
|
|
61992
|
-
|
|
61993
|
-
|
|
61994
|
-
|
|
61995
|
-
|
|
61996
|
-
|
|
61997
|
-
|
|
61998
|
-
|
|
61999
|
-
|
|
62000
|
-
|
|
62001
|
-
|
|
62807
|
+
// Union a set of winding-fill offset rings (which self-overlap) into clean,
|
|
62808
|
+
// non-self-overlapping MultiPolygon coordinates, via the winding-number
|
|
62809
|
+
// dissolve. Used by the topological pipeline to feed an ordinary polygon into
|
|
62810
|
+
// the shared mosaic (whose boundary-flood membership cannot resolve the
|
|
62811
|
+
// self-overlapping construction ring directly).
|
|
62812
|
+
function dissolveOffsetRingsToCoords(coords, opts, outlineDissolve) {
|
|
62813
|
+
if (!coords || coords.length === 0) return [];
|
|
62814
|
+
var dataset = getBufferDataset(coords);
|
|
62815
|
+
if (!dataset.arcs) return [];
|
|
62816
|
+
var dissolveOpts = outlineDissolve ?
|
|
62817
|
+
getOutlineBufferDissolveOpts(opts) :
|
|
62818
|
+
Object.assign({}, opts, {winding_fill: true});
|
|
62819
|
+
dissolveBufferDataset2(dataset, dissolveOpts);
|
|
62820
|
+
var lyr = dataset.layers[0];
|
|
62821
|
+
var shape = lyr.shapes && lyr.shapes[0];
|
|
62822
|
+
return shape ? getPolygonMultiPolygonCoords(shape, dataset.arcs) : [];
|
|
62823
|
+
}
|
|
62824
|
+
|
|
62825
|
+
function getBufferMultiPolygonCoords(paths, distance, bufferMaker) {
|
|
62826
|
+
var features, coords = [];
|
|
62827
|
+
if (paths.length === 0) return coords;
|
|
62828
|
+
features = bufferMaker(paths, distance) || [];
|
|
62829
|
+
features.forEach(function(feat) {
|
|
62830
|
+
var geom = feat && feat.geometry;
|
|
62831
|
+
if (geom && geom.type == 'MultiPolygon') {
|
|
62832
|
+
coords = coords.concat(geom.coordinates);
|
|
62002
62833
|
}
|
|
62003
|
-
return dataset;
|
|
62004
62834
|
});
|
|
62005
|
-
return
|
|
62006
|
-
}
|
|
62835
|
+
return coords;
|
|
62836
|
+
}
|
|
62007
62837
|
|
|
62008
|
-
|
|
62009
|
-
var
|
|
62010
|
-
|
|
62011
|
-
|
|
62012
|
-
|
|
62013
|
-
|
|
62014
|
-
|
|
62015
|
-
|
|
62016
|
-
|
|
62838
|
+
function getPolygonBufferPathData(shape, uniqueArcTest) {
|
|
62839
|
+
var data = {paths: [], split: false};
|
|
62840
|
+
(shape || []).forEach(function(path) {
|
|
62841
|
+
var paths = uniqueArcTest ? splitPathAtSharedArcs(path, uniqueArcTest) :
|
|
62842
|
+
[path.concat()];
|
|
62843
|
+
if (paths.length != 1 || paths[0].length != path.length ||
|
|
62844
|
+
!paths[0].every(function(arcId, i) {
|
|
62845
|
+
return arcId == path[i];
|
|
62846
|
+
})) {
|
|
62847
|
+
data.split = true;
|
|
62848
|
+
}
|
|
62849
|
+
data.paths = data.paths.concat(paths);
|
|
62850
|
+
});
|
|
62851
|
+
return data;
|
|
62852
|
+
}
|
|
62853
|
+
|
|
62854
|
+
function splitPathAtSharedArcs(path, uniqueArcTest) {
|
|
62855
|
+
var flags = path.map(uniqueArcTest);
|
|
62856
|
+
var firstShared = flags.indexOf(false);
|
|
62857
|
+
var chains = [];
|
|
62858
|
+
var chain = [];
|
|
62859
|
+
var start, i, arcId;
|
|
62860
|
+
if (firstShared == -1) return [path.concat()];
|
|
62861
|
+
start = (firstShared + 1) % path.length;
|
|
62862
|
+
for (i = 0; i < path.length; i++) {
|
|
62863
|
+
arcId = path[(start + i) % path.length];
|
|
62864
|
+
if (uniqueArcTest(arcId)) {
|
|
62865
|
+
chain.push(arcId);
|
|
62866
|
+
} else if (chain.length > 0) {
|
|
62867
|
+
chains.push(chain);
|
|
62868
|
+
chain = [];
|
|
62869
|
+
}
|
|
62017
62870
|
}
|
|
62018
|
-
|
|
62019
|
-
|
|
62020
|
-
stop$1('Missing rectangle extent');
|
|
62871
|
+
if (chain.length > 0) {
|
|
62872
|
+
chains.push(chain);
|
|
62021
62873
|
}
|
|
62022
|
-
|
|
62023
|
-
|
|
62024
|
-
|
|
62025
|
-
|
|
62874
|
+
return chains;
|
|
62875
|
+
}
|
|
62876
|
+
|
|
62877
|
+
function getUniqueArcTest(lyr, arcs) {
|
|
62878
|
+
var classify = getArcClassifier(lyr, arcs, {reusable: true})(function(a, b) {
|
|
62879
|
+
return b == -1 ? 'unique' : null;
|
|
62880
|
+
});
|
|
62881
|
+
return function(arcId) {
|
|
62882
|
+
return !!classify(arcId);
|
|
62026
62883
|
};
|
|
62027
|
-
|
|
62028
|
-
applyFrameProperties(dataset.layers[0], opts);
|
|
62029
|
-
dataset.layers[0].name = opts.name || 'rectangle';
|
|
62030
|
-
setDatasetCrsInfo(dataset, crsInfo);
|
|
62031
|
-
return dataset;
|
|
62032
|
-
};
|
|
62884
|
+
}
|
|
62033
62885
|
|
|
62034
|
-
function
|
|
62035
|
-
|
|
62036
|
-
|
|
62037
|
-
var d = lyr.data.getRecords()[0] || {};
|
|
62038
|
-
d.width = parseSizeParam(opts.width);
|
|
62039
|
-
d.type = 'frame';
|
|
62886
|
+
function ringArea(ring) {
|
|
62887
|
+
var iter = new PointIter(ring);
|
|
62888
|
+
return getSphericalPathArea2(iter);
|
|
62040
62889
|
}
|
|
62041
62890
|
|
|
62042
|
-
function
|
|
62043
|
-
var
|
|
62044
|
-
|
|
62045
|
-
bounds = applyBoundsOffset(opts.offset, bounds, crs);
|
|
62046
|
-
}
|
|
62047
|
-
if (bounds.area() > 0 === false) return null;
|
|
62048
|
-
if (opts.aspect_ratio) {
|
|
62049
|
-
bounds = applyAspectRatio(opts.aspect_ratio, bounds);
|
|
62050
|
-
}
|
|
62051
|
-
if (isGeoBox) {
|
|
62052
|
-
bounds = clampToWorldBounds(bounds);
|
|
62053
|
-
}
|
|
62054
|
-
return bounds;
|
|
62891
|
+
function makePointBuffer(lyr, dataset, opts) {
|
|
62892
|
+
var geojson = makePointBufferGeoJSON(lyr, dataset, opts);
|
|
62893
|
+
return importGeoJSON(geojson, {});
|
|
62055
62894
|
}
|
|
62056
62895
|
|
|
62057
|
-
//
|
|
62058
|
-
function
|
|
62059
|
-
var
|
|
62060
|
-
|
|
62061
|
-
|
|
62062
|
-
|
|
62063
|
-
range.push(range[0]);
|
|
62064
|
-
} else if (range[0] > range[1]) {
|
|
62065
|
-
range.reverse();
|
|
62066
|
-
}
|
|
62067
|
-
min = range[0];
|
|
62068
|
-
max = range[1];
|
|
62069
|
-
if (!min && !max) return bounds;
|
|
62070
|
-
if (!min) min = -Infinity;
|
|
62071
|
-
if (!max) max = Infinity;
|
|
62072
|
-
if (aspectRatio < min) {
|
|
62073
|
-
bounds.fillOut(min);
|
|
62074
|
-
} else if (aspectRatio > max) {
|
|
62075
|
-
bounds.fillOut(max);
|
|
62896
|
+
// Make a single geodetic circle
|
|
62897
|
+
function getCircleGeoJSON(center, radius, vertices, opts) {
|
|
62898
|
+
var n = 360;
|
|
62899
|
+
var geod = getGeodeticSegmentFunction(parseCrsString$1('wgs84')); // ?
|
|
62900
|
+
if (opts.inset) {
|
|
62901
|
+
radius -= opts.inset;
|
|
62076
62902
|
}
|
|
62077
|
-
return
|
|
62903
|
+
return opts.geometry_type == 'polyline' ?
|
|
62904
|
+
getPointBufferLineString([center], radius, n, geod) :
|
|
62905
|
+
getPointBufferPolygon([center], radius, n, geod, true);
|
|
62078
62906
|
}
|
|
62079
62907
|
|
|
62080
|
-
|
|
62081
|
-
|
|
62082
|
-
|
|
62083
|
-
|
|
62908
|
+
// Convert a point layer to circles
|
|
62909
|
+
function makePointBufferGeoJSON(lyr, dataset, opts) {
|
|
62910
|
+
var vertices = opts.vertices || 72;
|
|
62911
|
+
var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
|
|
62912
|
+
var crs = getDatasetCRS(dataset);
|
|
62913
|
+
var spherical = isLatLngCRS(crs);
|
|
62914
|
+
var geod = getGeodeticSegmentFunction(crs);
|
|
62915
|
+
var geometries = lyr.shapes.map(function(shape, i) {
|
|
62916
|
+
var dist = distanceFn(i);
|
|
62917
|
+
if (!dist || !shape) return null;
|
|
62918
|
+
return getPointBufferPolygon(shape, dist, vertices, geod, spherical);
|
|
62919
|
+
});
|
|
62920
|
+
// TODO: make sure that importer supports null geometries (nonstandard GeoJSON);
|
|
62921
|
+
return {
|
|
62922
|
+
type: 'GeometryCollection',
|
|
62923
|
+
geometries: geometries
|
|
62924
|
+
};
|
|
62084
62925
|
}
|
|
62085
62926
|
|
|
62086
|
-
function
|
|
62087
|
-
var
|
|
62088
|
-
|
|
62089
|
-
|
|
62090
|
-
coords =
|
|
62927
|
+
function getPointBufferPolygon(points, distance, vertices, geod, spherical) {
|
|
62928
|
+
var rings = [], coords, coords2;
|
|
62929
|
+
if (!points || !points.length) return null;
|
|
62930
|
+
for (var i=0; i<points.length; i++) {
|
|
62931
|
+
coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
|
|
62932
|
+
if (!spherical) {
|
|
62933
|
+
rings.push([coords]);
|
|
62934
|
+
} else if (countCrosses(coords) > 0) {
|
|
62935
|
+
coords2 = removePolygonCrosses([coords]);
|
|
62936
|
+
while (coords2.length > 0) rings.push([coords2.pop()]); // geojson polygon coords, no hole
|
|
62937
|
+
} else if (ringArea(coords) < 0) {
|
|
62938
|
+
// negative spherical area: CCW ring, indicating a circle of >180 degrees
|
|
62939
|
+
// that fully encloses both poles and the antimeridian.
|
|
62940
|
+
// need to add an enclosure around the entire sphere
|
|
62941
|
+
// TODO: compare to distance param as a sanity check
|
|
62942
|
+
rings.push([
|
|
62943
|
+
[[180, 90], [180, -90], [0, -90], [-180, -90], [-180, 90], [0, 90], [180, 90]],
|
|
62944
|
+
coords
|
|
62945
|
+
]);
|
|
62946
|
+
} else {
|
|
62947
|
+
rings.push([coords]);
|
|
62948
|
+
}
|
|
62091
62949
|
}
|
|
62092
62950
|
return {
|
|
62093
|
-
type: '
|
|
62094
|
-
coordinates:
|
|
62951
|
+
type: 'MultiPolygon',
|
|
62952
|
+
coordinates: rings
|
|
62095
62953
|
};
|
|
62096
62954
|
}
|
|
62097
62955
|
|
|
62098
|
-
|
|
62099
|
-
|
|
62100
|
-
|
|
62101
|
-
|
|
62102
|
-
|
|
62956
|
+
function getPointBufferLineString(points, distance, vertices, geod) {
|
|
62957
|
+
var rings = [], coords;
|
|
62958
|
+
if (!points || !points.length) return null;
|
|
62959
|
+
for (var i=0; i<points.length; i++) {
|
|
62960
|
+
coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
|
|
62961
|
+
coords = removePolylineCrosses(coords);
|
|
62962
|
+
while (coords.length > 0) rings.push(coords.pop());
|
|
62963
|
+
}
|
|
62964
|
+
return rings.length == 1 ? {
|
|
62965
|
+
type: 'LineString',
|
|
62966
|
+
coordinates: rings[0]
|
|
62967
|
+
} : {
|
|
62968
|
+
type: 'MultiLineString',
|
|
62969
|
+
coordinates: rings
|
|
62970
|
+
};
|
|
62971
|
+
}
|
|
62972
|
+
|
|
62973
|
+
// Returns array of [x, y] coordinates in a closed ring
|
|
62974
|
+
function getPointBufferCoordinates(center, meterDist, vertices, geod) {
|
|
62975
|
+
var coords = [],
|
|
62976
|
+
angle = 360 / vertices,
|
|
62977
|
+
theta;
|
|
62978
|
+
for (var i=0; i<vertices; i++) {
|
|
62979
|
+
// offsetting by half a step so 4 sides are flat, not pointy
|
|
62980
|
+
// (looks better on low-vertex circles)
|
|
62981
|
+
theta = (i + 0.5) * angle % 360;
|
|
62982
|
+
coords.push(geod(center[0], center[1], theta, meterDist));
|
|
62983
|
+
}
|
|
62984
|
+
coords.push(coords[0].concat());
|
|
62985
|
+
return coords;
|
|
62986
|
+
}
|
|
62103
62987
|
|
|
62104
62988
|
function getSemiMinorAxis(P) {
|
|
62105
62989
|
return P.a * Math.sqrt(1 - (P.es || 0));
|
|
@@ -63342,6 +64226,22 @@ ${svg}
|
|
|
63342
64226
|
// TODO: add more projections
|
|
63343
64227
|
//
|
|
63344
64228
|
function expandProjDefn(str, dataset, targetLayers) {
|
|
64229
|
+
return expandProjDefnWithBounds(str, function() {
|
|
64230
|
+
return getBBox(dataset, targetLayers); // TODO: support projected datasets
|
|
64231
|
+
});
|
|
64232
|
+
}
|
|
64233
|
+
|
|
64234
|
+
// Variant for a command with more than one target dataset. Auto-fitted
|
|
64235
|
+
// parameters describe the combined extent of the targets, so that a single
|
|
64236
|
+
// -proj command produces one destination CRS instead of fitting a different
|
|
64237
|
+
// projection to each dataset it happens to visit.
|
|
64238
|
+
function expandProjDefnForTargets(str, targets) {
|
|
64239
|
+
return expandProjDefnWithBounds(str, function() {
|
|
64240
|
+
return getTargetsBBox(targets);
|
|
64241
|
+
});
|
|
64242
|
+
}
|
|
64243
|
+
|
|
64244
|
+
function expandProjDefnWithBounds(str, getTargetBBox) {
|
|
63345
64245
|
var mproj = require$1('mproj');
|
|
63346
64246
|
var proj4, params, bbox, isConic2SP, isCentered, isUtm, decimals;
|
|
63347
64247
|
if (str in mproj.internal.pj_list === false) {
|
|
@@ -63353,7 +64253,7 @@ ${svg}
|
|
|
63353
64253
|
isUtm = str == 'utm';
|
|
63354
64254
|
proj4 = '+proj=' + str;
|
|
63355
64255
|
if (isConic2SP || isCentered || isUtm) {
|
|
63356
|
-
bbox =
|
|
64256
|
+
bbox = getTargetBBox();
|
|
63357
64257
|
decimals = getBoundsPrecisionForDisplay(bbox);
|
|
63358
64258
|
if (isUtm) {
|
|
63359
64259
|
params = getUtmParams(bbox);
|
|
@@ -63366,6 +64266,17 @@ ${svg}
|
|
|
63366
64266
|
return proj4;
|
|
63367
64267
|
}
|
|
63368
64268
|
|
|
64269
|
+
function getTargetsBBox(targets) {
|
|
64270
|
+
var bounds = new Bounds();
|
|
64271
|
+
targets.forEach(function(targ) {
|
|
64272
|
+
bounds.mergeBounds(getBBox(targ.dataset, targ.layers));
|
|
64273
|
+
});
|
|
64274
|
+
if (!bounds.hasBounds()) {
|
|
64275
|
+
stop$1('Command target is missing geographical bounds');
|
|
64276
|
+
}
|
|
64277
|
+
return bounds.toArray();
|
|
64278
|
+
}
|
|
64279
|
+
|
|
63369
64280
|
function getBBox(dataset, targetLayers) {
|
|
63370
64281
|
var source = targetLayers && targetLayers.length > 0 ? {
|
|
63371
64282
|
arcs: dataset.arcs,
|
|
@@ -63475,6 +64386,7 @@ ${svg}
|
|
|
63475
64386
|
var ProjectionParams = /*#__PURE__*/Object.freeze({
|
|
63476
64387
|
__proto__: null,
|
|
63477
64388
|
expandProjDefn: expandProjDefn,
|
|
64389
|
+
expandProjDefnForTargets: expandProjDefnForTargets,
|
|
63478
64390
|
getAutoFitBBox: getAutoFitBBox,
|
|
63479
64391
|
getCenterParams: getCenterParams,
|
|
63480
64392
|
getConicParams: getConicParams,
|
|
@@ -64936,32 +65848,112 @@ ${svg}
|
|
|
64936
65848
|
return a && b && a[0] == b[0] && a[1] == b[1];
|
|
64937
65849
|
}
|
|
64938
65850
|
|
|
64939
|
-
|
|
64940
|
-
|
|
65851
|
+
// Run -proj over all the targets of a single command.
|
|
65852
|
+
// The destination CRS is resolved once here rather than separately for each
|
|
65853
|
+
// target, for two reasons. Auto-fitted parameters (e.g. the standard parallels
|
|
65854
|
+
// that "lcc" expands to) must describe the whole command target, not whichever
|
|
65855
|
+
// dataset happens to be projected first. And the map frame must receive the
|
|
65856
|
+
// same CRS as the content instead of re-deriving parameters from its own extent.
|
|
65857
|
+
cmd.projTargets = function(targets, catalog, opts) {
|
|
65858
|
+
var destInfo;
|
|
65859
|
+
targets.forEach(function(targ) {
|
|
65860
|
+
applyProjInit(targ.dataset, catalog, opts);
|
|
65861
|
+
});
|
|
65862
|
+
destInfo = resolveProjDestInfo(targets, catalog, opts);
|
|
65863
|
+
targets.forEach(function(targ) {
|
|
65864
|
+
projectTarget(targ.dataset, opts, targ.layers, destInfo);
|
|
65865
|
+
});
|
|
65866
|
+
if (destInfo && destInfo.crs) {
|
|
65867
|
+
projectMapFrame(targets, catalog, opts, destInfo);
|
|
65868
|
+
warnAboutUnprojectedLayers(catalog, destInfo);
|
|
65869
|
+
}
|
|
65870
|
+
};
|
|
65871
|
+
|
|
65872
|
+
cmd.proj = function(dataset, catalog, opts, targetLayers, destInfo) {
|
|
65873
|
+
applyProjInit(dataset, catalog, opts);
|
|
65874
|
+
if (!destInfo) {
|
|
65875
|
+
destInfo = resolveProjDestInfo([{dataset: dataset, layers: targetLayers}], catalog, opts);
|
|
65876
|
+
}
|
|
65877
|
+
projectTarget(dataset, opts, targetLayers, destInfo);
|
|
65878
|
+
};
|
|
65879
|
+
|
|
65880
|
+
function projectTarget(dataset, opts, targetLayers, destInfo) {
|
|
64941
65881
|
var implicitlyProjectedNames = getImplicitlyTargetedLayerNames(dataset, targetLayers, layerHasGeometry$1);
|
|
64942
65882
|
// A preserved GeoJSON "crs" member becomes invalid once the CRS changes.
|
|
64943
65883
|
deleteGeoJSONMetadataCRS(dataset);
|
|
64944
|
-
if (
|
|
64945
|
-
|
|
64946
|
-
|
|
64947
|
-
|
|
65884
|
+
if (!destInfo) return;
|
|
65885
|
+
var didProject = projCmd(dataset, destInfo, opts);
|
|
65886
|
+
if (didProject && implicitlyProjectedNames.length > 0) {
|
|
65887
|
+
message(
|
|
65888
|
+
'Also projected non-target layer' + utils.pluralSuffix(implicitlyProjectedNames.length) +
|
|
65889
|
+
' from the same dataset: ' + implicitlyProjectedNames.join(', ')
|
|
65890
|
+
);
|
|
64948
65891
|
}
|
|
65892
|
+
}
|
|
65893
|
+
|
|
65894
|
+
function applyProjInit(dataset, catalog, opts) {
|
|
65895
|
+
if (!opts.init) return;
|
|
65896
|
+
var srcInfo = fetchCrsInfo(opts.init, catalog);
|
|
65897
|
+
if (!srcInfo.crs) stop$1("Unknown projection source:", opts.init);
|
|
65898
|
+
setDatasetCrsInfo(dataset, srcInfo);
|
|
65899
|
+
}
|
|
65900
|
+
|
|
65901
|
+
// @targets [{dataset, layers}, ...] -- all the targets of one -proj command
|
|
65902
|
+
function resolveProjDestInfo(targets, catalog, opts) {
|
|
64949
65903
|
if (opts.match) {
|
|
64950
|
-
|
|
64951
|
-
}
|
|
64952
|
-
|
|
64953
|
-
|
|
64954
|
-
|
|
64955
|
-
|
|
64956
|
-
|
|
64957
|
-
|
|
64958
|
-
|
|
64959
|
-
|
|
64960
|
-
|
|
64961
|
-
|
|
64962
|
-
|
|
65904
|
+
return fetchCrsInfo(opts.match, catalog);
|
|
65905
|
+
}
|
|
65906
|
+
if (!opts.crs) return null;
|
|
65907
|
+
return getCrsInfo(expandProjDefnForTargets(opts.crs, targets));
|
|
65908
|
+
}
|
|
65909
|
+
|
|
65910
|
+
// A map frame is composition state, not content: its rectangle describes a
|
|
65911
|
+
// region of the map's coordinate space, so it has to follow the map's CRS even
|
|
65912
|
+
// when the command targeted only a data layer. The GUI deliberately keeps the
|
|
65913
|
+
// frame out of default targets, so nothing else reaches it.
|
|
65914
|
+
function projectMapFrame(targets, catalog, opts, destInfo) {
|
|
65915
|
+
var frame = catalog ? getActiveFrame(catalog) : null;
|
|
65916
|
+
var frameOpts = opts;
|
|
65917
|
+
var frameCrs;
|
|
65918
|
+
if (!frame) return;
|
|
65919
|
+
if (targets.some(function(targ) { return targ.dataset == frame.dataset; })) {
|
|
65920
|
+
return; // already projected as an ordinary target
|
|
65921
|
+
}
|
|
65922
|
+
frameCrs = getDatasetCrsInfo(frame.dataset).crs;
|
|
65923
|
+
if (frameCrs && crsAreEqual(frameCrs, destInfo.crs)) {
|
|
65924
|
+
return; // already in the destination CRS; say nothing
|
|
65925
|
+
}
|
|
65926
|
+
if (!frameCrs) {
|
|
65927
|
+
warn('Unable to reproject the map frame: its coordinate system is unknown.');
|
|
65928
|
+
return;
|
|
64963
65929
|
}
|
|
64964
|
-
|
|
65930
|
+
if (opts.init) {
|
|
65931
|
+
// init= describes the CRS of the targeted data, not the frame's.
|
|
65932
|
+
frameOpts = Object.assign({}, opts);
|
|
65933
|
+
delete frameOpts.init;
|
|
65934
|
+
}
|
|
65935
|
+
cmd.proj(frame.dataset, catalog, frameOpts, frame.dataset.layers, destInfo);
|
|
65936
|
+
message('Also reprojected the map frame.');
|
|
65937
|
+
}
|
|
65938
|
+
|
|
65939
|
+
// -proj only applies to the command's targets, so layers in other datasets are
|
|
65940
|
+
// left behind in the old CRS. That usually surfaces much later as a blank
|
|
65941
|
+
// export or an "unable to combine" error, so name them while the cause is clear.
|
|
65942
|
+
function warnAboutUnprojectedLayers(catalog, destInfo) {
|
|
65943
|
+
var names = [];
|
|
65944
|
+
if (!catalog) return;
|
|
65945
|
+
catalog.getLayers().forEach(function(o) {
|
|
65946
|
+
var crs;
|
|
65947
|
+
if (!layerHasGeometry$1(o.layer) && !layerHasRaster(o.layer)) return;
|
|
65948
|
+
crs = getDatasetCrsInfo(o.dataset).crs;
|
|
65949
|
+
if (crs && !crsAreEqual(crs, destInfo.crs)) {
|
|
65950
|
+
names.push(o.layer.name || '[unnamed]');
|
|
65951
|
+
}
|
|
65952
|
+
});
|
|
65953
|
+
if (names.length === 0) return;
|
|
65954
|
+
warn(`Layer${utils.pluralSuffix(names.length)} not projected by this command: ` +
|
|
65955
|
+
names.join(', ') + '. Use target=* to project all layers.');
|
|
65956
|
+
}
|
|
64965
65957
|
|
|
64966
65958
|
function projCmd(dataset, destInfo, opts) {
|
|
64967
65959
|
// modify copy of coordinate data when running in web UI, so original shapes
|
|
@@ -65082,6 +66074,7 @@ ${svg}
|
|
|
65082
66074
|
stop$1('Unable to project from a coordinate system that has no inverse transform');
|
|
65083
66075
|
}
|
|
65084
66076
|
var proj = getProjTransform2(src, dest); // v2 returns null points instead of throwing an error
|
|
66077
|
+
var frameSnapshots = getFrameProjectionSnapshots(dataset);
|
|
65085
66078
|
var badArcs = 0;
|
|
65086
66079
|
var badPoints = 0;
|
|
65087
66080
|
var healAntimeridian = isLatLngCRS(src) &&
|
|
@@ -65115,6 +66108,10 @@ ${svg}
|
|
|
65115
66108
|
cleanProjectedPathLayers(dataset, {heal_antimeridian: healAntimeridian});
|
|
65116
66109
|
}
|
|
65117
66110
|
|
|
66111
|
+
frameSnapshots.forEach(function(snapshot) {
|
|
66112
|
+
rebuildProjectedFrameLayer(snapshot, dataset, proj, isLatLngCRS(src));
|
|
66113
|
+
});
|
|
66114
|
+
|
|
65118
66115
|
if (badArcs > 0 && !opts.quiet) {
|
|
65119
66116
|
message(`Removed ${badArcs} ${badArcs == 1 ? 'path' : 'paths'} containing unprojectable vertices.`);
|
|
65120
66117
|
}
|
|
@@ -73064,185 +74061,6 @@ ${svg}
|
|
|
73064
74061
|
return parsed[0];
|
|
73065
74062
|
}
|
|
73066
74063
|
|
|
73067
|
-
cmd.frame = function(catalog, targets, opts) {
|
|
73068
|
-
var widthPx, heightPx, aspectRatio, bbox;
|
|
73069
|
-
if (opts.width) {
|
|
73070
|
-
widthPx = parseSizeParam(opts.width);
|
|
73071
|
-
if (widthPx > 0 === false) {
|
|
73072
|
-
stop$1('Invalid width parameter:', opts.width);
|
|
73073
|
-
}
|
|
73074
|
-
}
|
|
73075
|
-
if (opts.height) {
|
|
73076
|
-
heightPx = parseSizeParam(opts.height);
|
|
73077
|
-
if (heightPx > 0 === false) {
|
|
73078
|
-
stop$1('Invalid height parameter:', opts.height);
|
|
73079
|
-
}
|
|
73080
|
-
}
|
|
73081
|
-
if (!widthPx && !heightPx) {
|
|
73082
|
-
widthPx = 800;
|
|
73083
|
-
message('Using default 800px frame width');
|
|
73084
|
-
}
|
|
73085
|
-
|
|
73086
|
-
if (opts.aspect_ratio) {
|
|
73087
|
-
if (opts.aspect_ratio > 0 === false) {
|
|
73088
|
-
stop$1('Invalid aspect-ratio parameter:', opts.aspect_ratio);
|
|
73089
|
-
}
|
|
73090
|
-
if (!heightPx) {
|
|
73091
|
-
heightPx = roundToDigits(widthPx / opts.aspect_ratio, 1);
|
|
73092
|
-
} else if (!widthPx) {
|
|
73093
|
-
widthPx = roundToDigits(heightPx * opts.aspect_ratio, 1);
|
|
73094
|
-
}
|
|
73095
|
-
}
|
|
73096
|
-
|
|
73097
|
-
if (opts.bbox) {
|
|
73098
|
-
bbox = opts.bbox;
|
|
73099
|
-
// TODO: validate
|
|
73100
|
-
} else {
|
|
73101
|
-
var datasets = utils.pluck(targets, 'dataset');
|
|
73102
|
-
requireDatasetsHaveCompatibleCRS(datasets, 'Targets include both projected and unprojected coordinates');
|
|
73103
|
-
bbox = getTargetBbox(targets);
|
|
73104
|
-
if (!bbox) {
|
|
73105
|
-
stop$1('Command target is missing geographical bounds');
|
|
73106
|
-
}
|
|
73107
|
-
}
|
|
73108
|
-
|
|
73109
|
-
applyPercentageOffsets(bbox, opts.offset || opts.offsets);
|
|
73110
|
-
applyPixelOffsets(bbox, widthPx, heightPx, opts.offset || opts.offsets);
|
|
73111
|
-
|
|
73112
|
-
if (bbox[3] - bbox[1] > 0 === false || bbox[2] - bbox[0] > 0 === false) {
|
|
73113
|
-
stop$1('Frame has a collapsed bbox');
|
|
73114
|
-
}
|
|
73115
|
-
|
|
73116
|
-
aspectRatio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
|
|
73117
|
-
if (!widthPx) {
|
|
73118
|
-
widthPx = roundToDigits(heightPx * aspectRatio, 1);
|
|
73119
|
-
} else if (!heightPx) {
|
|
73120
|
-
heightPx = roundToDigits(widthPx / aspectRatio, 1);
|
|
73121
|
-
}
|
|
73122
|
-
|
|
73123
|
-
var feature = {
|
|
73124
|
-
type: 'Feature',
|
|
73125
|
-
properties: {type: 'frame', width: widthPx, height: heightPx},
|
|
73126
|
-
geometry: bboxToPolygon(bbox)
|
|
73127
|
-
};
|
|
73128
|
-
var frameDataset = importGeoJSON(feature);
|
|
73129
|
-
// set CRS from target dataset
|
|
73130
|
-
// TODO: handle case: targets have different projections
|
|
73131
|
-
// TODO: handle case: first target is missing CRS
|
|
73132
|
-
if (targets.length > 0) {
|
|
73133
|
-
var crsInfo = getDatasetCrsInfo(targets[0].dataset);
|
|
73134
|
-
setDatasetCrsInfo(frameDataset, crsInfo);
|
|
73135
|
-
}
|
|
73136
|
-
frameDataset.layers[0].name = opts.name || 'frame';
|
|
73137
|
-
catalog.addDataset(frameDataset);
|
|
73138
|
-
};
|
|
73139
|
-
|
|
73140
|
-
function fillOutBbox(bbox, widthPx, heightPx) {
|
|
73141
|
-
var hpad = 0, vpad = 0;
|
|
73142
|
-
var w = bbox[2] - bbox[0];
|
|
73143
|
-
var h = bbox[3] - bbox[1];
|
|
73144
|
-
if (widthPx / heightPx > w / h) { // need to add horizontal padding
|
|
73145
|
-
hpad = h * widthPx / heightPx - w;
|
|
73146
|
-
} else {
|
|
73147
|
-
vpad = w * heightPx / widthPx - h;
|
|
73148
|
-
}
|
|
73149
|
-
bbox[0] -= hpad / 2;
|
|
73150
|
-
bbox[1] -= vpad / 2;
|
|
73151
|
-
bbox[2] += hpad / 2;
|
|
73152
|
-
bbox[3] += vpad / 2;
|
|
73153
|
-
}
|
|
73154
|
-
|
|
73155
|
-
function applyPercentageOffsets(bbox, arg) {
|
|
73156
|
-
var sides = getPctOffsets(arg);
|
|
73157
|
-
var l = sides[0],
|
|
73158
|
-
b = sides[1],
|
|
73159
|
-
r = sides[2],
|
|
73160
|
-
t = sides[3],
|
|
73161
|
-
w2 = (bbox[2] - bbox[0]) / (1 - l - r),
|
|
73162
|
-
h2 = (bbox[3] - bbox[1]) / (1 - t - b);
|
|
73163
|
-
bbox[0] -= l * w2;
|
|
73164
|
-
bbox[1] -= b * h2;
|
|
73165
|
-
bbox[2] += r * w2;
|
|
73166
|
-
bbox[3] += t * h2;
|
|
73167
|
-
}
|
|
73168
|
-
|
|
73169
|
-
function applyPixelOffsets(bbox, widthPx, heightPx, arg) {
|
|
73170
|
-
var sides = getPixelOffsets(arg);
|
|
73171
|
-
var l = sides[0],
|
|
73172
|
-
b = sides[1],
|
|
73173
|
-
r = sides[2],
|
|
73174
|
-
t = sides[3],
|
|
73175
|
-
scale, w;
|
|
73176
|
-
|
|
73177
|
-
if (widthPx && heightPx) {
|
|
73178
|
-
// add padding to bbox to match pixel dimensions, if needed
|
|
73179
|
-
fillOutBbox(bbox, widthPx, heightPx);
|
|
73180
|
-
}
|
|
73181
|
-
|
|
73182
|
-
w = bbox[2] - bbox[0];
|
|
73183
|
-
bbox[3] - bbox[1];
|
|
73184
|
-
|
|
73185
|
-
if (widthPx) {
|
|
73186
|
-
scale = w / (widthPx - l - r);
|
|
73187
|
-
} else {
|
|
73188
|
-
scale = w / (heightPx - t - b);
|
|
73189
|
-
}
|
|
73190
|
-
|
|
73191
|
-
bbox[0] -= scale * l;
|
|
73192
|
-
bbox[1] -= scale * b;
|
|
73193
|
-
bbox[2] += scale * r;
|
|
73194
|
-
bbox[3] += scale * t;
|
|
73195
|
-
return scale;
|
|
73196
|
-
}
|
|
73197
|
-
|
|
73198
|
-
function getPctOffsets(arg) {
|
|
73199
|
-
return adjustOffsetsArg(arg).map(str => {
|
|
73200
|
-
return str.includes('%') ? parsePercent(str) : 0;
|
|
73201
|
-
});
|
|
73202
|
-
}
|
|
73203
|
-
|
|
73204
|
-
function getPixelOffsets(arg) {
|
|
73205
|
-
return adjustOffsetsArg(arg).map(str => {
|
|
73206
|
-
return str.includes('%') ? 0 : parseSizeParam(str);
|
|
73207
|
-
});
|
|
73208
|
-
}
|
|
73209
|
-
|
|
73210
|
-
function adjustOffsetsArg(arg) {
|
|
73211
|
-
if (!arg) arg = ['0'];
|
|
73212
|
-
if (arg.length == 1) {
|
|
73213
|
-
return [arg[0], arg[0], arg[0], arg[0]];
|
|
73214
|
-
}
|
|
73215
|
-
if (arg.length != 4) {
|
|
73216
|
-
stop$1('List of offsets should have 4 values');
|
|
73217
|
-
}
|
|
73218
|
-
return arg;
|
|
73219
|
-
}
|
|
73220
|
-
|
|
73221
|
-
function getTargetBbox(targets) {
|
|
73222
|
-
var expanded = expandCommandTargets(targets);
|
|
73223
|
-
var bounds = expanded.reduce(function(memo, o) {
|
|
73224
|
-
return memo.mergeBounds(getLayerBounds(o.layer, o.dataset.arcs));
|
|
73225
|
-
}, new Bounds());
|
|
73226
|
-
return bounds.hasBounds() ? bounds.toArray() : null;
|
|
73227
|
-
}
|
|
73228
|
-
|
|
73229
|
-
// Convert width and height args to aspect ratio arg for the rectangle() function
|
|
73230
|
-
function getAspectRatioArg(widthArg, heightArg) {
|
|
73231
|
-
// heightArg is a string containing either a number or a
|
|
73232
|
-
// comma-sep. pair of numbers (range);
|
|
73233
|
-
return heightArg.split(',').map(function(opt) {
|
|
73234
|
-
var height = Number(opt),
|
|
73235
|
-
width = Number(widthArg);
|
|
73236
|
-
if (!opt) return '';
|
|
73237
|
-
return width / height;
|
|
73238
|
-
}).reverse().join(',');
|
|
73239
|
-
}
|
|
73240
|
-
|
|
73241
|
-
var Frame = /*#__PURE__*/Object.freeze({
|
|
73242
|
-
__proto__: null,
|
|
73243
|
-
getAspectRatioArg: getAspectRatioArg
|
|
73244
|
-
});
|
|
73245
|
-
|
|
73246
74064
|
// Remove intricate sub-scale detail from a single arc so that -smooth has a
|
|
73247
74065
|
// clean line to work with, WITHOUT thinning the rest of the line: smooth makes a
|
|
73248
74066
|
// better approximation of the original when it has more detail to work with, so
|
|
@@ -78009,7 +78827,7 @@ ${svg}
|
|
|
78009
78827
|
if (lyr) return getFrameLayerData(lyr, dataset.arcs);
|
|
78010
78828
|
// -frame adds the frame it creates to the catalog as a separate dataset, so a
|
|
78011
78829
|
// catalog-wide search is needed to find it (-scalebar does the same).
|
|
78012
|
-
target = catalog ?
|
|
78830
|
+
target = catalog ? getActiveFrame(catalog) : null;
|
|
78013
78831
|
return target ? getFrameLayerData(target.layer, target.dataset.arcs) : null;
|
|
78014
78832
|
}
|
|
78015
78833
|
|
|
@@ -81404,6 +82222,9 @@ ${svg}
|
|
|
81404
82222
|
job.catalog.captureCatalogBefore({operation: 'addLayer'});
|
|
81405
82223
|
job.catalog.addDataset(cmd.addLayer(targetDataset, opts));
|
|
81406
82224
|
|
|
82225
|
+
} else if (name == 'update-frame') {
|
|
82226
|
+
cmd.updateFrame(targetLayers, targetDataset, opts);
|
|
82227
|
+
|
|
81407
82228
|
} else if (name == 'update-label') {
|
|
81408
82229
|
cmd.updateLabel(targetLayers, targetDataset, opts);
|
|
81409
82230
|
|
|
@@ -81625,9 +82446,7 @@ ${svg}
|
|
|
81625
82446
|
} else if (name == 'proj') {
|
|
81626
82447
|
await initProjLibrary(opts);
|
|
81627
82448
|
job.resumeCommand();
|
|
81628
|
-
targets.
|
|
81629
|
-
cmd.proj(targ.dataset, job.catalog, opts, targ.layers);
|
|
81630
|
-
});
|
|
82449
|
+
cmd.projTargets(targets, job.catalog, opts);
|
|
81631
82450
|
|
|
81632
82451
|
} else if (name == 'rectangle') {
|
|
81633
82452
|
if (source || opts.bbox || targets.length === 0) {
|
|
@@ -81733,7 +82552,15 @@ ${svg}
|
|
|
81733
82552
|
}
|
|
81734
82553
|
|
|
81735
82554
|
if (outputDataset) {
|
|
81736
|
-
|
|
82555
|
+
var removedLayers = targetLayers && !opts.no_replace ? targetLayers : [];
|
|
82556
|
+
assertSingleFrameUpdate(
|
|
82557
|
+
job.catalog,
|
|
82558
|
+
outputDataset.layers.map(function(lyr) {
|
|
82559
|
+
return {layer: lyr, dataset: outputDataset};
|
|
82560
|
+
}),
|
|
82561
|
+
removedLayers
|
|
82562
|
+
);
|
|
82563
|
+
job.catalog.addDataset(outputDataset, {removeLayers: removedLayers}); // also sets default target
|
|
81737
82564
|
outputLayers = outputDataset.layers;
|
|
81738
82565
|
if (targetLayers && !opts.no_replace) {
|
|
81739
82566
|
// remove target layers from target dataset
|
|
@@ -81743,6 +82570,13 @@ ${svg}
|
|
|
81743
82570
|
}
|
|
81744
82571
|
} else if (outputLayers && targetDataset && outputLayers != targetDataset.layers) {
|
|
81745
82572
|
// integrate output layers into the target dataset
|
|
82573
|
+
assertSingleFrameUpdate(
|
|
82574
|
+
job.catalog,
|
|
82575
|
+
outputLayers.map(function(lyr) {
|
|
82576
|
+
return {layer: lyr, dataset: targetDataset};
|
|
82577
|
+
}),
|
|
82578
|
+
opts.no_replace ? [] : targetLayers
|
|
82579
|
+
);
|
|
81746
82580
|
if (opts.no_replace) {
|
|
81747
82581
|
// make sure commands do not return input layers with 'no_replace' option
|
|
81748
82582
|
if (!outputLayersAreDifferent(outputLayers, targetLayers || [])) {
|
|
@@ -81955,7 +82789,7 @@ ${svg}
|
|
|
81955
82789
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
81956
82790
|
}
|
|
81957
82791
|
|
|
81958
|
-
var version = "0.7.
|
|
82792
|
+
var version = "0.7.64";
|
|
81959
82793
|
|
|
81960
82794
|
// Parse command line args into commands and run them
|
|
81961
82795
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
|
@@ -84639,6 +85473,7 @@ ${svg}
|
|
|
84639
85473
|
FileTypes,
|
|
84640
85474
|
FilterGeom,
|
|
84641
85475
|
Frame,
|
|
85476
|
+
FrameProjection,
|
|
84642
85477
|
FrameUtils,
|
|
84643
85478
|
Furniture,
|
|
84644
85479
|
Geodesic,
|