mapshaper 0.7.67 → 0.7.69
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 +1200 -462
- package/package.json +1 -1
- package/www/index.html +10 -3
- package/www/mapshaper-gui.js +1258 -369
- package/www/mapshaper.js +1200 -462
- package/www/page.css +360 -113
package/mapshaper.js
CHANGED
|
@@ -25424,6 +25424,9 @@
|
|
|
25424
25424
|
// type/data accessors -- doesn't form an import cycle with the
|
|
25425
25425
|
// scalebar-aware renderer registry that lives in furniture.mjs.
|
|
25426
25426
|
|
|
25427
|
+
// Must match the renderers registered in mapshaper-furniture.mjs
|
|
25428
|
+
var furnitureTypes = ['scalebar'];
|
|
25429
|
+
|
|
25427
25430
|
// @lyr dataset layer
|
|
25428
25431
|
function getFurnitureLayerType(lyr) {
|
|
25429
25432
|
var rec = lyr.data && lyr.data.getReadOnlyRecordAt(0);
|
|
@@ -25434,6 +25437,12 @@
|
|
|
25434
25437
|
return lyr.data && lyr.data.getReadOnlyRecordAt(0);
|
|
25435
25438
|
}
|
|
25436
25439
|
|
|
25440
|
+
// A data-only layer holding the settings of a map element such as a scalebar
|
|
25441
|
+
function layerIsFurniture(lyr) {
|
|
25442
|
+
return !!lyr && !lyr.geometry_type &&
|
|
25443
|
+
furnitureTypes.includes(getFurnitureLayerType(lyr));
|
|
25444
|
+
}
|
|
25445
|
+
|
|
25437
25446
|
/*
|
|
25438
25447
|
{
|
|
25439
25448
|
width: size[0],
|
|
@@ -25629,6 +25638,27 @@
|
|
|
25629
25638
|
});
|
|
25630
25639
|
}
|
|
25631
25640
|
|
|
25641
|
+
// Furniture (scalebars, etc.) belongs to a frame by sharing its dataset.
|
|
25642
|
+
// Furniture in a dataset without a frame is an ordinary standalone layer.
|
|
25643
|
+
function getFrameFurnitureLayers(dataset) {
|
|
25644
|
+
if (!dataset || !findFrameLayerInDataset(dataset)) return [];
|
|
25645
|
+
return dataset.layers.filter(layerIsFurniture);
|
|
25646
|
+
}
|
|
25647
|
+
|
|
25648
|
+
function findFrameFurnitureLayer(dataset, type) {
|
|
25649
|
+
return getFrameFurnitureLayers(dataset).find(function(lyr) {
|
|
25650
|
+
return getFurnitureLayerType(lyr) == type;
|
|
25651
|
+
}) || null;
|
|
25652
|
+
}
|
|
25653
|
+
|
|
25654
|
+
// True for a map frame and for the furniture that belongs to it: layers that
|
|
25655
|
+
// describe the map layout rather than holding map content.
|
|
25656
|
+
function isFrameComponentLayer(lyr, dataset) {
|
|
25657
|
+
if (!dataset) return false;
|
|
25658
|
+
if (isFrameLayer(lyr, dataset.arcs)) return true;
|
|
25659
|
+
return layerIsFurniture(lyr) && !!findFrameLayerInDataset(dataset);
|
|
25660
|
+
}
|
|
25661
|
+
|
|
25632
25662
|
function findFrames(catalog) {
|
|
25633
25663
|
return catalog.getLayers().filter(function(o) {
|
|
25634
25664
|
return isFrameLayer(o.layer, o.dataset.arcs);
|
|
@@ -25855,6 +25885,7 @@
|
|
|
25855
25885
|
demoteFrameLayer: demoteFrameLayer,
|
|
25856
25886
|
findFrame: findFrame,
|
|
25857
25887
|
findFrameDataset: findFrameDataset,
|
|
25888
|
+
findFrameFurnitureLayer: findFrameFurnitureLayer,
|
|
25858
25889
|
findFrameLayer: findFrameLayer,
|
|
25859
25890
|
findFrameLayerInDataset: findFrameLayerInDataset,
|
|
25860
25891
|
findFrames: findFrames,
|
|
@@ -25863,11 +25894,13 @@
|
|
|
25863
25894
|
frameReservedFields: frameReservedFields,
|
|
25864
25895
|
getActiveFrame: getActiveFrame,
|
|
25865
25896
|
getFrameData: getFrameData,
|
|
25897
|
+
getFrameFurnitureLayers: getFrameFurnitureLayers,
|
|
25866
25898
|
getFrameLayerBounds: getFrameLayerBounds,
|
|
25867
25899
|
getFrameLayerData: getFrameLayerData,
|
|
25868
25900
|
getFrameSize: getFrameSize,
|
|
25869
25901
|
getMapFrameMetersPerPixel: getMapFrameMetersPerPixel,
|
|
25870
25902
|
getSingleFrameRecord: getSingleFrameRecord,
|
|
25903
|
+
isFrameComponentLayer: isFrameComponentLayer,
|
|
25871
25904
|
isFrameLayer: isFrameLayer,
|
|
25872
25905
|
isFrameReservedField: isFrameReservedField,
|
|
25873
25906
|
parseFrameSize: parseFrameSize,
|
|
@@ -25875,12 +25908,53 @@
|
|
|
25875
25908
|
resolveExportFrame: resolveExportFrame
|
|
25876
25909
|
});
|
|
25877
25910
|
|
|
25911
|
+
// Adds a furniture layer as a standalone dataset, without changing the
|
|
25912
|
+
// default target (so a following -o still exports the content layers)
|
|
25878
25913
|
function addFurnitureLayer(lyr, catalog) {
|
|
25879
25914
|
var o = {
|
|
25880
25915
|
info: {},
|
|
25881
25916
|
layers: [lyr]
|
|
25882
25917
|
};
|
|
25918
|
+
catalog.captureCatalogBefore({operation: 'addFurnitureLayer'});
|
|
25883
25919
|
catalog.getDatasets().push(o);
|
|
25920
|
+
catalog.markCatalogChanged({operation: 'addFurnitureLayer'});
|
|
25921
|
+
}
|
|
25922
|
+
|
|
25923
|
+
// Adds a furniture layer to a frame's dataset, replacing any existing
|
|
25924
|
+
// furniture of the same type. Passing a null layer removes it.
|
|
25925
|
+
// Returns the replaced or removed layer, if any
|
|
25926
|
+
function setFrameFurnitureLayer(frameDataset, type, lyr) {
|
|
25927
|
+
var layers = frameDataset.layers;
|
|
25928
|
+
var idx = layers.findIndex(function(o) {
|
|
25929
|
+
return layerIsFurniture(o) && getFurnitureLayerType(o) == type;
|
|
25930
|
+
});
|
|
25931
|
+
var prev = idx > -1 ? layers[idx] : null;
|
|
25932
|
+
if (!prev && !lyr) return null;
|
|
25933
|
+
noteDatasetWillChange(frameDataset, {operation: 'setFrameFurniture', unit: 'layers'});
|
|
25934
|
+
layers = layers.slice();
|
|
25935
|
+
if (prev && lyr) {
|
|
25936
|
+
lyr.name = prev.name;
|
|
25937
|
+
layers[idx] = lyr;
|
|
25938
|
+
} else if (prev) {
|
|
25939
|
+
layers.splice(idx, 1);
|
|
25940
|
+
} else {
|
|
25941
|
+
layers.push(lyr);
|
|
25942
|
+
}
|
|
25943
|
+
frameDataset.layers = layers;
|
|
25944
|
+
markDatasetChanged(frameDataset, {operation: 'setFrameFurniture', unit: 'layers'});
|
|
25945
|
+
return prev;
|
|
25946
|
+
}
|
|
25947
|
+
|
|
25948
|
+
// Moves a replaced frame's furniture into the dataset of the frame replacing it
|
|
25949
|
+
function moveFrameFurniture(fromDataset, toDataset) {
|
|
25950
|
+
var furniture = fromDataset.layers.filter(layerIsFurniture);
|
|
25951
|
+
if (furniture.length === 0) return;
|
|
25952
|
+
noteDatasetWillChange(fromDataset, {operation: 'moveFrameFurniture', unit: 'layers'});
|
|
25953
|
+
fromDataset.layers = fromDataset.layers.filter(function(lyr) {
|
|
25954
|
+
return !furniture.includes(lyr);
|
|
25955
|
+
});
|
|
25956
|
+
markDatasetChanged(fromDataset, {operation: 'moveFrameFurniture', unit: 'layers'});
|
|
25957
|
+
toDataset.layers = toDataset.layers.concat(furniture);
|
|
25884
25958
|
}
|
|
25885
25959
|
|
|
25886
25960
|
// Pure geometry-to-SVG-path primitives. Extracted from geojson-to-svg.mjs
|
|
@@ -27236,9 +27310,16 @@
|
|
|
27236
27310
|
// The two copies are identical apart from their paint, so they sit in exactly
|
|
27237
27311
|
// the same place whatever the label's position, alignment or path.
|
|
27238
27312
|
//
|
|
27239
|
-
// The halo copy is unfilled
|
|
27240
|
-
// the stroke's inner half and nothing else
|
|
27241
|
-
//
|
|
27313
|
+
// The halo copy is unfilled, which matches paint-order, where the fill covers
|
|
27314
|
+
// the stroke's inner half and nothing else. Its halo-opacity is written as
|
|
27315
|
+
// opacity rather than stroke-opacity, which Illustrator ignores on import; the
|
|
27316
|
+
// two look the same on a copy with nothing painted but its stroke.
|
|
27317
|
+
//
|
|
27318
|
+
// Illustrator's GPU preview compounds a translucent halo where glyphs
|
|
27319
|
+
// overlap; its CPU preview and its output do not. Nothing in the SVG was
|
|
27320
|
+
// found to avoid it: filling the copy (imported as two objects, and no
|
|
27321
|
+
// better), and putting the opacity on a group or two nested groups around
|
|
27322
|
+
// the copy, all show the same thing.
|
|
27242
27323
|
//
|
|
27243
27324
|
// Things that apply to the label as a whole move to the group: its transform,
|
|
27244
27325
|
// which places it, and its opacity, which would otherwise fade each copy on its
|
|
@@ -27269,6 +27350,10 @@
|
|
|
27269
27350
|
delete halo.properties['paint-order'];
|
|
27270
27351
|
delete halo.properties['fill-opacity'];
|
|
27271
27352
|
halo.properties.fill = 'none';
|
|
27353
|
+
if ('stroke-opacity' in halo.properties) {
|
|
27354
|
+
halo.properties.opacity = halo.properties['stroke-opacity'];
|
|
27355
|
+
delete halo.properties['stroke-opacity'];
|
|
27356
|
+
}
|
|
27272
27357
|
HALO_STROKE_PROPERTIES.forEach(function(k) {
|
|
27273
27358
|
delete text.properties[k];
|
|
27274
27359
|
});
|
|
@@ -27340,6 +27425,23 @@
|
|
|
27340
27425
|
// different one would put the label somewhere it is not drawn.
|
|
27341
27426
|
var DEFAULT_LABEL_FONT_SIZE = 12;
|
|
27342
27427
|
|
|
27428
|
+
// The line height a label is drawn with when it carries none, as a multiple of
|
|
27429
|
+
// its font size.
|
|
27430
|
+
var DEFAULT_LINE_HEIGHT = 1.1;
|
|
27431
|
+
|
|
27432
|
+
// The dy of each line after a label's first, from its line-height.
|
|
27433
|
+
//
|
|
27434
|
+
// A bare number is a multiple of the font size, as in CSS, and not px, as a
|
|
27435
|
+
// bare dy would be in SVG -- so it is written in ems. Nobody who types 1.3
|
|
27436
|
+
// into a field called line height wants lines 1.3px apart, and a multiple
|
|
27437
|
+
// keeps its proportions when the font size changes. Values with units are
|
|
27438
|
+
// passed through.
|
|
27439
|
+
function getLineHeightDy(val) {
|
|
27440
|
+
var measure = val === undefined || val === null || val === '' ?
|
|
27441
|
+
DEFAULT_LINE_HEIGHT : parseSvgMeasure(val);
|
|
27442
|
+
return utils.isFiniteNumber(measure) ? measure + 'em' : measure;
|
|
27443
|
+
}
|
|
27444
|
+
|
|
27343
27445
|
function toLabelString(val) {
|
|
27344
27446
|
if (val || val === 0 || val === false) return String(val);
|
|
27345
27447
|
return '';
|
|
@@ -27387,7 +27489,7 @@
|
|
|
27387
27489
|
value: line,
|
|
27388
27490
|
properties: {
|
|
27389
27491
|
x: dx,
|
|
27390
|
-
dy: rec['line-height']
|
|
27492
|
+
dy: getLineHeightDy(rec['line-height'])
|
|
27391
27493
|
}
|
|
27392
27494
|
};
|
|
27393
27495
|
obj.children.push(tspan);
|
|
@@ -27481,8 +27583,10 @@
|
|
|
27481
27583
|
var SvgLabels = /*#__PURE__*/Object.freeze({
|
|
27482
27584
|
__proto__: null,
|
|
27483
27585
|
DEFAULT_LABEL_FONT_SIZE: DEFAULT_LABEL_FONT_SIZE,
|
|
27586
|
+
DEFAULT_LINE_HEIGHT: DEFAULT_LINE_HEIGHT,
|
|
27484
27587
|
LABEL_SOFT_BREAK: LABEL_SOFT_BREAK,
|
|
27485
27588
|
getDrawnLabelOffset: getDrawnLabelOffset,
|
|
27589
|
+
getLineHeightDy: getLineHeightDy,
|
|
27486
27590
|
labelNewlineRxp: labelNewlineRxp,
|
|
27487
27591
|
removeSoftBreaks: removeSoftBreaks,
|
|
27488
27592
|
renderLabel: renderLabel$1,
|
|
@@ -27505,7 +27609,6 @@
|
|
|
27505
27609
|
|
|
27506
27610
|
var DEFAULT_PADDING$1 = 3;
|
|
27507
27611
|
var DEFAULT_LINE_WIDTH = 1;
|
|
27508
|
-
var DEFAULT_LINE_HEIGHT = '1.1em';
|
|
27509
27612
|
// Space left between the anchor's symbol and the end of the line
|
|
27510
27613
|
var SYMBOL_CLEARANCE = 2;
|
|
27511
27614
|
// How far an automatic curve bows out, as a fraction of its chord
|
|
@@ -27526,6 +27629,9 @@
|
|
|
27526
27629
|
var ASCENT = 0.8;
|
|
27527
27630
|
var DESCENT = 0.2;
|
|
27528
27631
|
var MIDLINE = 0.35;
|
|
27632
|
+
// Average advance of a character, in ems, for estimating the width of text
|
|
27633
|
+
// that cannot be measured
|
|
27634
|
+
var AVERAGE_CHAR_WIDTH = 0.6;
|
|
27529
27635
|
|
|
27530
27636
|
function labelHasCallout(rec) {
|
|
27531
27637
|
var type = rec && rec.callout ? parseCalloutType(rec.callout) : null;
|
|
@@ -27589,18 +27695,24 @@
|
|
|
27589
27695
|
// no measurement the box takes the label's width if it is a text block, and is
|
|
27590
27696
|
// otherwise a vertical line through the text's origin -- a callout then meets
|
|
27591
27697
|
// the text where it starts rather than not being drawn.
|
|
27592
|
-
|
|
27698
|
+
//
|
|
27699
|
+
// opts.estimate_width: with no measurement and no label-width, estimate the
|
|
27700
|
+
// width from the length of the longest line instead of taking it as zero
|
|
27701
|
+
function getLabelTextBox(rec, opts) {
|
|
27593
27702
|
var fontSize = getFontSizeInPx$1(rec);
|
|
27594
27703
|
var offset = getDrawnLabelOffset(rec);
|
|
27595
27704
|
var lines = splitLabelLines(toLabelString(rec['label-text']));
|
|
27596
|
-
var lineHeight = measureToPx(rec['line-height']
|
|
27705
|
+
var lineHeight = measureToPx(getLineHeightDy(rec['line-height']), fontSize);
|
|
27597
27706
|
var width = getMeasuredTextWidth(rec) || getNumber(rec['label-width'], 0);
|
|
27707
|
+
if (!width && opts && opts.estimate_width) {
|
|
27708
|
+
width = estimateTextWidth(lines, fontSize);
|
|
27709
|
+
}
|
|
27598
27710
|
// An anchor the record does not set is inherited from the layer's group,
|
|
27599
27711
|
// which is 'middle' -- see getLabelTextDefaults().
|
|
27600
27712
|
var anchor = offset['text-anchor'] || 'middle';
|
|
27601
27713
|
var xmin = offset.dx - width * (anchor == 'middle' ? 0.5 : anchor == 'end' ? 1 : 0);
|
|
27602
27714
|
var baseline = offset.dy + getBaselineShift(rec['dominant-baseline']) * fontSize;
|
|
27603
|
-
if (!(lineHeight > 0)) lineHeight = fontSize *
|
|
27715
|
+
if (!(lineHeight > 0)) lineHeight = fontSize * DEFAULT_LINE_HEIGHT;
|
|
27604
27716
|
return {
|
|
27605
27717
|
xmin: xmin,
|
|
27606
27718
|
xmax: xmin + width,
|
|
@@ -27610,6 +27722,18 @@
|
|
|
27610
27722
|
};
|
|
27611
27723
|
}
|
|
27612
27724
|
|
|
27725
|
+
function estimateTextWidth(lines, fontSize) {
|
|
27726
|
+
var chars = lines.reduce(function(max, line) {
|
|
27727
|
+
return Math.max(max, Array.from(line).length);
|
|
27728
|
+
}, 0);
|
|
27729
|
+
return chars * AVERAGE_CHAR_WIDTH * fontSize;
|
|
27730
|
+
}
|
|
27731
|
+
|
|
27732
|
+
// The size a label's text is drawn at, in px
|
|
27733
|
+
function getLabelFontSize(rec) {
|
|
27734
|
+
return getFontSizeInPx$1(rec);
|
|
27735
|
+
}
|
|
27736
|
+
|
|
27613
27737
|
// Where a line meets a callout's text and how it gets there from the anchor.
|
|
27614
27738
|
//
|
|
27615
27739
|
// o.type 'line', 'elbow' or 'curve'
|
|
@@ -27636,7 +27760,7 @@
|
|
|
27636
27760
|
// point where the line meets it, the elbow's corner or the curve's midpoint
|
|
27637
27761
|
// (null for a straight line), and where the line stops short of the anchor.
|
|
27638
27762
|
function getCalloutShape(o) {
|
|
27639
|
-
var box = padBox(o.box, o.padding || 0);
|
|
27763
|
+
var box = padBox$1(o.box, o.padding || 0);
|
|
27640
27764
|
var a = [0, 0];
|
|
27641
27765
|
var size = o.endSize > 0 ? o.endSize : getDefaultCalloutEndSize(o.end, o.width);
|
|
27642
27766
|
var t, v, path, tip, dir, out, lineWidth, headLen, curved;
|
|
@@ -27997,7 +28121,7 @@
|
|
|
27997
28121
|
|
|
27998
28122
|
// Utilities
|
|
27999
28123
|
|
|
28000
|
-
function padBox(box, pad) {
|
|
28124
|
+
function padBox$1(box, pad) {
|
|
28001
28125
|
return {
|
|
28002
28126
|
xmin: box.xmin - pad,
|
|
28003
28127
|
xmax: box.xmax + pad,
|
|
@@ -28070,6 +28194,7 @@
|
|
|
28070
28194
|
getControlPoint: getControlPoint,
|
|
28071
28195
|
getDefaultCalloutEndSize: getDefaultCalloutEndSize,
|
|
28072
28196
|
getLabelCalloutShape: getLabelCalloutShape,
|
|
28197
|
+
getLabelFontSize: getLabelFontSize,
|
|
28073
28198
|
getLabelTextBox: getLabelTextBox,
|
|
28074
28199
|
labelHasCallout: labelHasCallout,
|
|
28075
28200
|
renderLabelCallout: renderLabelCallout
|
|
@@ -28366,28 +28491,68 @@
|
|
|
28366
28491
|
|
|
28367
28492
|
var SvgSymbols = /*#__PURE__*/Object.freeze({
|
|
28368
28493
|
__proto__: null,
|
|
28494
|
+
featureHasIcon: featureHasIcon,
|
|
28369
28495
|
getAnchorSymbolRadius: getAnchorSymbolRadius,
|
|
28496
|
+
getIconRadius: getIconRadius,
|
|
28370
28497
|
getTransform: getTransform,
|
|
28371
28498
|
renderPoint: renderPoint,
|
|
28372
28499
|
symbolRenderers: symbolRenderers
|
|
28373
28500
|
});
|
|
28374
28501
|
|
|
28502
|
+
// Command options that are stored in the scalebar's data record
|
|
28503
|
+
var scalebarFields = ['label', 'units', 'style', 'font_size', 'font_family',
|
|
28504
|
+
'font_style', 'font_weight', 'color', 'tic_length', 'bar_width', 'label_offset',
|
|
28505
|
+
'position', 'label_position', 'dual_units', 'margin'];
|
|
28506
|
+
|
|
28507
|
+
// With a map frame, the scalebar belongs to the frame, and running the
|
|
28508
|
+
// command again replaces it. Without a frame, each run adds a standalone
|
|
28509
|
+
// scalebar layer, which is exported if it is included in the output layers.
|
|
28375
28510
|
cmd.scalebar = function(catalog, opts) {
|
|
28511
|
+
var frame = getActiveFrame(catalog);
|
|
28512
|
+
if (opts.remove) {
|
|
28513
|
+
if (!frame || !setFrameFurnitureLayer(frame.dataset, 'scalebar', null)) {
|
|
28514
|
+
stop$1('The map frame has no scalebar to remove');
|
|
28515
|
+
}
|
|
28516
|
+
return;
|
|
28517
|
+
}
|
|
28518
|
+
validateScalebarOpts(opts);
|
|
28376
28519
|
var lyr = getScalebarLayer(opts);
|
|
28377
|
-
if (
|
|
28378
|
-
|
|
28520
|
+
if (frame) {
|
|
28521
|
+
setFrameFurnitureLayer(frame.dataset, 'scalebar', lyr);
|
|
28522
|
+
} else {
|
|
28523
|
+
addFurnitureLayer(lyr, catalog);
|
|
28379
28524
|
}
|
|
28380
|
-
addFurnitureLayer(lyr, catalog);
|
|
28381
28525
|
};
|
|
28382
28526
|
|
|
28383
28527
|
function getScalebarLayer(opts) {
|
|
28384
|
-
var
|
|
28528
|
+
var rec = {type: 'scalebar'};
|
|
28529
|
+
scalebarFields.forEach(function(k) {
|
|
28530
|
+
if (opts[k] !== undefined && opts[k] !== null && opts[k] !== '') {
|
|
28531
|
+
rec[k] = opts[k];
|
|
28532
|
+
}
|
|
28533
|
+
});
|
|
28385
28534
|
return {
|
|
28386
|
-
name:
|
|
28387
|
-
data: new DataTable([
|
|
28535
|
+
name: 'scalebar',
|
|
28536
|
+
data: new DataTable([rec])
|
|
28388
28537
|
};
|
|
28389
28538
|
}
|
|
28390
28539
|
|
|
28540
|
+
function validateScalebarOpts(opts) {
|
|
28541
|
+
if (opts.label) {
|
|
28542
|
+
splitScalebarLabel(opts.label).forEach(function(str) {
|
|
28543
|
+
if (!parseScalebarUnits(str) && !isBareDistance(str)) {
|
|
28544
|
+
stop$1(`Expected a distance or units of km, meters, miles or feet in scalebar label (received ${str})`);
|
|
28545
|
+
}
|
|
28546
|
+
});
|
|
28547
|
+
}
|
|
28548
|
+
if (opts.units && !parseLabelUnits(opts.units)) {
|
|
28549
|
+
stop$1('Unsupported scalebar units:', opts.units);
|
|
28550
|
+
}
|
|
28551
|
+
if (opts.style && !/^[ab]$/i.test(opts.style)) {
|
|
28552
|
+
stop$1('Unsupported scalebar style:', opts.style);
|
|
28553
|
+
}
|
|
28554
|
+
}
|
|
28555
|
+
|
|
28391
28556
|
function renderScalebar(d, frame) {
|
|
28392
28557
|
if (!frame.crs) {
|
|
28393
28558
|
message('Unable to render scalebar: unknown CRS.');
|
|
@@ -28400,14 +28565,15 @@
|
|
|
28400
28565
|
var opts = getScalebarOpts(d);
|
|
28401
28566
|
var metersPerPx = getMapFrameMetersPerPixel(frame);
|
|
28402
28567
|
var frameWidthPx = frame.width;
|
|
28403
|
-
var labels = d.label ? d.label
|
|
28404
|
-
var
|
|
28405
|
-
var
|
|
28406
|
-
var
|
|
28568
|
+
var labels = d.label ? splitScalebarLabel(d.label) : [];
|
|
28569
|
+
var units = parseLabelUnits(d.units) || 'mile';
|
|
28570
|
+
var label1 = getScalebarLabel(labels[0], frameWidthPx, metersPerPx, units);
|
|
28571
|
+
var props1 = parseScalebarLabel(label1);
|
|
28572
|
+
var label2 = null;
|
|
28407
28573
|
var props2, length2;
|
|
28408
28574
|
|
|
28409
28575
|
if (props1.km > 0 === false) {
|
|
28410
|
-
message('Unusable scalebar label:',
|
|
28576
|
+
message('Unusable scalebar label:', labels[0] || '');
|
|
28411
28577
|
return [];
|
|
28412
28578
|
}
|
|
28413
28579
|
|
|
@@ -28416,6 +28582,11 @@
|
|
|
28416
28582
|
stop$1("Null scalebar length");
|
|
28417
28583
|
}
|
|
28418
28584
|
|
|
28585
|
+
if (opts.style == 'b' && labels[1]) {
|
|
28586
|
+
label2 = completeLabel(labels[1], getOtherUnits(props1.units));
|
|
28587
|
+
} else if (opts.style == 'b' && opts.dual_units) {
|
|
28588
|
+
label2 = getAutoSecondLabel(length1, metersPerPx, getOtherUnits(props1.units));
|
|
28589
|
+
}
|
|
28419
28590
|
if (label2) {
|
|
28420
28591
|
props2 = parseScalebarLabel(label2);
|
|
28421
28592
|
length2 = Math.round(props2.km / metersPerPx * 1000);
|
|
@@ -28468,8 +28639,13 @@
|
|
|
28468
28639
|
};
|
|
28469
28640
|
|
|
28470
28641
|
function getScalebarOpts(d) {
|
|
28471
|
-
var style = d.style == 'b' || d.style == 'B' ? 'b' : 'a';
|
|
28472
|
-
|
|
28642
|
+
var style = d.style == 'b' || d.style == 'B' || !d.style && d.dual_units ? 'b' : 'a';
|
|
28643
|
+
var opts = Object.assign({}, defaultOpts, styleOpts[style]);
|
|
28644
|
+
Object.keys(d).forEach(function(k) {
|
|
28645
|
+
if (d[k] !== undefined && d[k] !== null && d[k] !== '') opts[k] = d[k];
|
|
28646
|
+
});
|
|
28647
|
+
opts.style = style;
|
|
28648
|
+
return opts;
|
|
28473
28649
|
}
|
|
28474
28650
|
|
|
28475
28651
|
function renderAsSvg(length, text, length2, text2, opts) {
|
|
@@ -28487,12 +28663,11 @@
|
|
|
28487
28663
|
};
|
|
28488
28664
|
}
|
|
28489
28665
|
|
|
28490
|
-
// TODO: generalize to other kinds of furniture as they are developed
|
|
28491
28666
|
function getScalebarPosition(opts) {
|
|
28492
28667
|
var pos = opts.position || 'top-left';
|
|
28493
28668
|
return {
|
|
28494
|
-
ypos: pos.includes('
|
|
28495
|
-
xpos: pos.includes('
|
|
28669
|
+
ypos: pos.includes('bottom') ? 'bottom' : 'top',
|
|
28670
|
+
xpos: pos.includes('right') ? 'right' : 'left'
|
|
28496
28671
|
};
|
|
28497
28672
|
}
|
|
28498
28673
|
|
|
@@ -28506,12 +28681,16 @@
|
|
|
28506
28681
|
'label-text': text,
|
|
28507
28682
|
'font-size': opts.font_size,
|
|
28508
28683
|
'text-anchor': labelPos.anchor,
|
|
28509
|
-
'dominant-baseline': labelPos.ypos == 'top' ? 'auto' : 'hanging'
|
|
28684
|
+
'dominant-baseline': labelPos.ypos == 'top' ? 'auto' : 'hanging',
|
|
28510
28685
|
//// 'dominant-baseline': labelPos == 'top' ? 'text-after-edge' : 'text-before-edge'
|
|
28511
28686
|
// 'text-after-edge' is buggy in Safari and unsupported by Illustrator,
|
|
28512
28687
|
// so I'm using 'hanging' and 'auto', which seem to be well supported.
|
|
28513
28688
|
// downside: requires a kludgy multiplier to calculate scalebar height (see above)
|
|
28514
28689
|
};
|
|
28690
|
+
if (opts.color) labelOpts.fill = opts.color;
|
|
28691
|
+
if (opts.font_family) labelOpts['font-family'] = opts.font_family;
|
|
28692
|
+
if (opts.font_style) labelOpts['font-style'] = opts.font_style;
|
|
28693
|
+
if (opts.font_weight) labelOpts['font-weight'] = opts.font_weight;
|
|
28515
28694
|
return symbolRenderers.label(labelOpts, anchorX, anchorY);
|
|
28516
28695
|
}
|
|
28517
28696
|
|
|
@@ -28571,7 +28750,7 @@
|
|
|
28571
28750
|
}
|
|
28572
28751
|
var bar = importMultiLineString(coords);
|
|
28573
28752
|
Object.assign(bar.properties, {
|
|
28574
|
-
stroke: 'black',
|
|
28753
|
+
stroke: opts.color || 'black',
|
|
28575
28754
|
fill: 'none',
|
|
28576
28755
|
'stroke-width': opts.bar_width,
|
|
28577
28756
|
'stroke-linecap': 'butt',
|
|
@@ -28580,35 +28759,132 @@
|
|
|
28580
28759
|
return bar;
|
|
28581
28760
|
}
|
|
28582
28761
|
|
|
28583
|
-
//
|
|
28762
|
+
// Kilometers per unit
|
|
28763
|
+
var unitSizes = {
|
|
28764
|
+
km: 1,
|
|
28765
|
+
m: 0.001,
|
|
28766
|
+
mile: 1.60934,
|
|
28767
|
+
ft: 0.0003048
|
|
28768
|
+
};
|
|
28769
|
+
|
|
28770
|
+
// Candidate lengths for automatic labels, in ascending order of distance
|
|
28771
|
+
var autoLengths = {
|
|
28772
|
+
imperial: [
|
|
28773
|
+
['ft', '10 20 25 50 100 200 250 500'],
|
|
28774
|
+
// note: removed 1.5 12 and 1,200
|
|
28775
|
+
['mile', '1/8 1/5 1/4 1/2 1 2 3 4 5 8 10 15 20 25 30 40 50 75 100 150 200 250 ' +
|
|
28776
|
+
'300 350 400 500 750 1,000 1,500 2,000 2,500 3,000 4,000 5,000']
|
|
28777
|
+
],
|
|
28778
|
+
metric: [
|
|
28779
|
+
['m', '5 10 20 25 50 100 200 250 500'],
|
|
28780
|
+
['km', '1 2 3 4 5 8 10 15 20 25 30 40 50 75 100 150 200 250 300 350 ' +
|
|
28781
|
+
'400 500 750 1,000 1,500 2,000 2,500 3,000 4,000 5,000']
|
|
28782
|
+
]
|
|
28783
|
+
};
|
|
28784
|
+
|
|
28785
|
+
function getAutoLabels(system) {
|
|
28786
|
+
return autoLengths[system].reduce(function(memo, group) {
|
|
28787
|
+
return memo.concat(group[1].split(' ').map(function(str) {
|
|
28788
|
+
return formatDistanceLabel(str, group[0]);
|
|
28789
|
+
}));
|
|
28790
|
+
}, []);
|
|
28791
|
+
}
|
|
28792
|
+
|
|
28793
|
+
function getUnitSystem(units) {
|
|
28794
|
+
return units == 'km' || units == 'm' ? 'metric' : 'imperial';
|
|
28795
|
+
}
|
|
28796
|
+
|
|
28797
|
+
function getOtherUnits(units) {
|
|
28798
|
+
return getUnitSystem(units) == 'metric' ? 'mile' : 'km';
|
|
28799
|
+
}
|
|
28800
|
+
|
|
28801
|
+
// The unit system of the units= option: returns 'km', 'mile' or ''
|
|
28802
|
+
function parseUnitsOption(str) {
|
|
28803
|
+
var units = parseLabelUnits(str);
|
|
28804
|
+
return units ? (getUnitSystem(units) == 'metric' ? 'km' : 'mile') : '';
|
|
28805
|
+
}
|
|
28806
|
+
|
|
28807
|
+
// Accepts the units= option, which also gives the units of a label that is a
|
|
28808
|
+
// bare number; returns 'km', 'm', 'mile', 'ft' or ''
|
|
28809
|
+
function parseLabelUnits(str) {
|
|
28810
|
+
var s = String(str || '').toLowerCase();
|
|
28811
|
+
if (/^(km|kilomet(er|re)s?|metric)$/.test(s)) return 'km';
|
|
28812
|
+
if (/^(m|meters?|metres?)$/.test(s)) return 'm';
|
|
28813
|
+
if (/^(mi|miles?|imperial)$/.test(s)) return 'mile';
|
|
28814
|
+
if (/^(ft|feet)$/.test(s)) return 'ft';
|
|
28815
|
+
return '';
|
|
28816
|
+
}
|
|
28817
|
+
|
|
28818
|
+
// Splits a dual-unit label like "100 km,50 miles". Only a comma after the units
|
|
28819
|
+
// separates two distances, so "1,000 km" and a bare "1,500" stay whole.
|
|
28820
|
+
function splitScalebarLabel(label) {
|
|
28821
|
+
return String(label).split(/(?<=[^\d\s]),\s*/).map(function(str) {
|
|
28822
|
+
return str.trim();
|
|
28823
|
+
}).filter(Boolean);
|
|
28824
|
+
}
|
|
28825
|
+
|
|
28826
|
+
// A distance with no units, e.g. "150" or "1/2" or "1,000"
|
|
28827
|
+
function isBareDistance(str) {
|
|
28828
|
+
return /^[\d\s.,/]+$/.test(str) && parseScalebarNumber(str) > 0;
|
|
28829
|
+
}
|
|
28830
|
+
|
|
28831
|
+
// Adds units to a label that is a bare number
|
|
28832
|
+
function completeLabel(label, units) {
|
|
28833
|
+
return isBareDistance(label) ? formatDistanceLabel(label.trim(), units) : label;
|
|
28834
|
+
}
|
|
28835
|
+
|
|
28836
|
+
// label: a label from the label= option, or empty for an automatic label
|
|
28837
|
+
function getScalebarLabel(label, frameWidthPx, metersPerPx, defaultUnits) {
|
|
28838
|
+
var units;
|
|
28839
|
+
if (label && parseScalebarNumber(label) > 0) return completeLabel(label, defaultUnits);
|
|
28840
|
+
// A label with units but no number gets an automatic length in those units
|
|
28841
|
+
units = label && parseScalebarUnits(label) || defaultUnits;
|
|
28842
|
+
return getAutoScalebarLabel(frameWidthPx, metersPerPx, units);
|
|
28843
|
+
}
|
|
28844
|
+
|
|
28845
|
+
// unit: 'km' || 'mile' (or 'm', 'ft' for the same systems)
|
|
28846
|
+
// The longest length that fits within 20% of the map's width, or the shortest
|
|
28847
|
+
// that is at least 70px long if the map is too narrow for that
|
|
28584
28848
|
function getAutoScalebarLabel(mapWidth, metersPerPx, unit) {
|
|
28585
|
-
var
|
|
28586
|
-
var
|
|
28587
|
-
|
|
28588
|
-
var
|
|
28589
|
-
|
|
28590
|
-
|
|
28591
|
-
|
|
28592
|
-
|
|
28593
|
-
|
|
28594
|
-
|
|
28595
|
-
|
|
28596
|
-
|
|
28597
|
-
|
|
28849
|
+
var minKm = metersPerPx * 70 / 1000;
|
|
28850
|
+
var maxKm = metersPerPx * mapWidth * 0.2 / 1000;
|
|
28851
|
+
var labels = getAutoLabels(getUnitSystem(unit));
|
|
28852
|
+
var longest = labels.filter(function(label) {
|
|
28853
|
+
var km = parseScalebarLabelToKm(label);
|
|
28854
|
+
return km >= minKm && km <= maxKm;
|
|
28855
|
+
}).pop();
|
|
28856
|
+
return longest || labels.find(function(label) {
|
|
28857
|
+
return parseScalebarLabelToKm(label) >= minKm;
|
|
28858
|
+
}) || '';
|
|
28859
|
+
}
|
|
28860
|
+
|
|
28861
|
+
// The longest automatic length in the given units that fits within the
|
|
28862
|
+
// first part of a dual-unit scalebar
|
|
28863
|
+
function getAutoSecondLabel(maxPx, metersPerPx, unit) {
|
|
28864
|
+
var maxKm = maxPx * metersPerPx / 1000;
|
|
28865
|
+
var minKm = maxKm / 4;
|
|
28866
|
+
var labels = getAutoLabels(getUnitSystem(unit)).filter(function(label) {
|
|
28867
|
+
var km = parseScalebarLabelToKm(label);
|
|
28868
|
+
return km <= maxKm && km >= minKm;
|
|
28869
|
+
});
|
|
28870
|
+
return labels.pop() || null;
|
|
28598
28871
|
}
|
|
28599
28872
|
|
|
28600
28873
|
function formatDistanceLabel(numStr, unit) {
|
|
28601
28874
|
var num = parseScalebarNumber(numStr);
|
|
28602
|
-
var unitStr = unit == 'km' && 'KM' ||
|
|
28875
|
+
var unitStr = unit == 'km' && 'KM' ||
|
|
28876
|
+
unit == 'm' && (num > 1 ? 'METERS' : 'METER') ||
|
|
28877
|
+
unit == 'ft' && (num > 1 ? 'FEET' : 'FOOT') ||
|
|
28878
|
+
num > 1 && 'MILES' || 'MILE';
|
|
28603
28879
|
return numStr + ' ' + unitStr;
|
|
28604
28880
|
}
|
|
28605
28881
|
|
|
28606
|
-
// See test/
|
|
28882
|
+
// See test/scalebar-test.mjs for examples of supported formats
|
|
28607
28883
|
function parseScalebarLabelToKm(str) {
|
|
28608
28884
|
var units = parseScalebarUnits(str);
|
|
28609
28885
|
var value = parseScalebarNumber(str);
|
|
28610
28886
|
if (!units || !value) return NaN;
|
|
28611
|
-
return
|
|
28887
|
+
return value * unitSizes[units];
|
|
28612
28888
|
}
|
|
28613
28889
|
|
|
28614
28890
|
function parseScalebarLabel(label) {
|
|
@@ -28616,7 +28892,7 @@
|
|
|
28616
28892
|
var units = label ? parseScalebarUnits(label) : 'mile';
|
|
28617
28893
|
var km = NaN;
|
|
28618
28894
|
if (units && num) {
|
|
28619
|
-
km =
|
|
28895
|
+
km = num * unitSizes[units];
|
|
28620
28896
|
}
|
|
28621
28897
|
return {
|
|
28622
28898
|
number: num,
|
|
@@ -28625,11 +28901,14 @@
|
|
|
28625
28901
|
};
|
|
28626
28902
|
}
|
|
28627
28903
|
|
|
28904
|
+
// Returns 'km', 'm', 'mile', 'ft' or ''
|
|
28628
28905
|
function parseScalebarUnits(str) {
|
|
28629
|
-
var
|
|
28630
|
-
|
|
28631
|
-
|
|
28632
|
-
return
|
|
28906
|
+
var s = String(str).trim().toLowerCase();
|
|
28907
|
+
if (/(miles?|mi[.]?|英里)$/.test(s)) return 'mile';
|
|
28908
|
+
if (/(k\.m\.|km|kilomet(er|re)s?|kilom.tres?|公里)$/.test(s)) return 'km';
|
|
28909
|
+
if (/(^|[\d\s.])(feet|foot|ft[.]?|英尺)$/.test(s)) return 'ft';
|
|
28910
|
+
if (/(^|[\d\s.])(m|m\.|meters?|metres?|mètres?|米)$/.test(s)) return 'm';
|
|
28911
|
+
return '';
|
|
28633
28912
|
}
|
|
28634
28913
|
|
|
28635
28914
|
function parseScalebarNumber(str) {
|
|
@@ -28649,8 +28928,12 @@
|
|
|
28649
28928
|
__proto__: null,
|
|
28650
28929
|
formatDistanceLabel: formatDistanceLabel,
|
|
28651
28930
|
getScalebarLayer: getScalebarLayer,
|
|
28931
|
+
parseLabelUnits: parseLabelUnits,
|
|
28652
28932
|
parseScalebarLabelToKm: parseScalebarLabelToKm,
|
|
28653
|
-
|
|
28933
|
+
parseScalebarUnits: parseScalebarUnits,
|
|
28934
|
+
parseUnitsOption: parseUnitsOption,
|
|
28935
|
+
renderScalebar: renderScalebar,
|
|
28936
|
+
splitScalebarLabel: splitScalebarLabel
|
|
28654
28937
|
});
|
|
28655
28938
|
|
|
28656
28939
|
var furnitureRenderers = {
|
|
@@ -28672,24 +28955,33 @@
|
|
|
28672
28955
|
function renderFurnitureLayer(lyr, frame) {
|
|
28673
28956
|
var d = getFurnitureLayerData(lyr);
|
|
28674
28957
|
var renderer = furnitureRenderers[d.type];
|
|
28958
|
+
var problem;
|
|
28675
28959
|
if (!renderer) {
|
|
28676
28960
|
stop$1('Missing renderer for', d.type, 'element');
|
|
28677
28961
|
}
|
|
28678
|
-
|
|
28679
|
-
|
|
28680
|
-
|
|
28681
|
-
if (!isProjectedCRS(frame.crs)) {
|
|
28682
|
-
stop$1(`Unable to render ${d.type} (map is unprojected)`);
|
|
28962
|
+
problem = getFurnitureFrameProblem(frame);
|
|
28963
|
+
if (problem) {
|
|
28964
|
+
stop$1(`Unable to render ${d.type} (${problem})`);
|
|
28683
28965
|
}
|
|
28684
28966
|
return renderer(d, frame) || [];
|
|
28685
28967
|
}
|
|
28686
28968
|
|
|
28969
|
+
// Returns a reason why furniture can't be drawn in a frame, or null
|
|
28970
|
+
function getFurnitureFrameProblem(frame) {
|
|
28971
|
+
if (!frame) return 'missing map frame';
|
|
28972
|
+
if (!frame.crs) return 'unknown map projection';
|
|
28973
|
+
if (!isProjectedCRS(frame.crs)) return 'map is unprojected';
|
|
28974
|
+
return null;
|
|
28975
|
+
}
|
|
28976
|
+
|
|
28687
28977
|
var Furniture = /*#__PURE__*/Object.freeze({
|
|
28688
28978
|
__proto__: null,
|
|
28979
|
+
getFurnitureFrameProblem: getFurnitureFrameProblem,
|
|
28689
28980
|
getFurnitureLayerData: getFurnitureLayerData,
|
|
28690
28981
|
getFurnitureLayerType: getFurnitureLayerType,
|
|
28691
28982
|
isFurnitureLayer: isFurnitureLayer,
|
|
28692
28983
|
layerHasFurniture: layerHasFurniture,
|
|
28984
|
+
layerIsFurniture: layerIsFurniture,
|
|
28693
28985
|
renderFurnitureLayer: renderFurnitureLayer
|
|
28694
28986
|
});
|
|
28695
28987
|
|
|
@@ -39146,6 +39438,9 @@ ${svg}
|
|
|
39146
39438
|
async function exportDatasets(datasets, opts) {
|
|
39147
39439
|
var format = getOutputFormat(datasets[0], opts);
|
|
39148
39440
|
var files;
|
|
39441
|
+
if (format != 'svg' && format != PACKAGE_EXT) {
|
|
39442
|
+
datasets = removeFurnitureLayers(datasets);
|
|
39443
|
+
}
|
|
39149
39444
|
validateRasterExportFormat(datasets, format);
|
|
39150
39445
|
if (format != 'geoparquet' && opts.level !== undefined) {
|
|
39151
39446
|
error('The level= option only applies to GeoParquet output');
|
|
@@ -39221,6 +39516,25 @@ ${svg}
|
|
|
39221
39516
|
return files;
|
|
39222
39517
|
}
|
|
39223
39518
|
|
|
39519
|
+
// Scalebars and other map furniture only have a meaning in SVG output
|
|
39520
|
+
function removeFurnitureLayers(datasets) {
|
|
39521
|
+
var hasFurniture = datasets.some(function(dataset) {
|
|
39522
|
+
return dataset.layers.some(layerIsFurniture);
|
|
39523
|
+
});
|
|
39524
|
+
if (!hasFurniture) return datasets;
|
|
39525
|
+
datasets = datasets.map(function(dataset) {
|
|
39526
|
+
return utils.defaults({
|
|
39527
|
+
layers: dataset.layers.filter(function(lyr) { return !layerIsFurniture(lyr); })
|
|
39528
|
+
}, dataset);
|
|
39529
|
+
}).filter(function(dataset) {
|
|
39530
|
+
return dataset.layers.length > 0;
|
|
39531
|
+
});
|
|
39532
|
+
if (datasets.length === 0) {
|
|
39533
|
+
stop$1('Map furniture layers (e.g. scalebars) can only be exported as SVG');
|
|
39534
|
+
}
|
|
39535
|
+
return datasets;
|
|
39536
|
+
}
|
|
39537
|
+
|
|
39224
39538
|
function validateRasterExportFormat(datasets, format) {
|
|
39225
39539
|
if (format == 'geotiff') {
|
|
39226
39540
|
if (datasetsHaveVectorLayers(datasets)) {
|
|
@@ -41392,7 +41706,7 @@ ${svg}
|
|
|
41392
41706
|
'font-weight': {describe: 'normal, bold or a numeric weight'},
|
|
41393
41707
|
'font-stretch': {describe: 'e.g. condensed'},
|
|
41394
41708
|
'letter-spacing': {describe: 'extra space between letters'},
|
|
41395
|
-
'line-height': {describe: 'spacing between lines of a multi-line label'},
|
|
41709
|
+
'line-height': {describe: 'spacing between lines of a multi-line label; a bare number is a multiple of the font size'},
|
|
41396
41710
|
'text-anchor': {describe: 'start, middle or end'},
|
|
41397
41711
|
'label-align': {describe: 'how the lines of a multi-line label align: left, center or right'},
|
|
41398
41712
|
'dominant-baseline': {describe: 'vertical alignment, e.g. central'},
|
|
@@ -43478,7 +43792,7 @@ ${svg}
|
|
|
43478
43792
|
describe: 'CSS letter-spacing property of labels'
|
|
43479
43793
|
})
|
|
43480
43794
|
.option('line-height', {
|
|
43481
|
-
describe: 'line spacing of multi-line labels (default is 1.
|
|
43795
|
+
describe: 'line spacing of multi-line labels; a bare number is a multiple of the font size, as in CSS (default is 1.1)'
|
|
43482
43796
|
})
|
|
43483
43797
|
.option('dominant-baseline', {
|
|
43484
43798
|
describe: 'vertical alignment of labels (e.g. central)'
|
|
@@ -43812,6 +44126,13 @@ ${svg}
|
|
|
43812
44126
|
describe: 'frame coordinates (xmin,ymin,xmax,ymax)',
|
|
43813
44127
|
type: 'bbox'
|
|
43814
44128
|
})
|
|
44129
|
+
.option('fit', {
|
|
44130
|
+
describe: 'layer(s) to fit the frame to, including their symbols and labels'
|
|
44131
|
+
})
|
|
44132
|
+
.option('ignore-symbols', {
|
|
44133
|
+
describe: 'with fit=, fit to point locations, not symbol and label extents',
|
|
44134
|
+
type: 'flag'
|
|
44135
|
+
})
|
|
43815
44136
|
.option('width', {
|
|
43816
44137
|
describe: 'nominal frame width (e.g. 5in, 10cm, 600px)'
|
|
43817
44138
|
})
|
|
@@ -43963,6 +44284,10 @@ ${svg}
|
|
|
43963
44284
|
describe: 'separate offsets for each side, in l,b,r,t order',
|
|
43964
44285
|
type: 'strings'
|
|
43965
44286
|
})
|
|
44287
|
+
.option('ignore-symbols', {
|
|
44288
|
+
describe: 'fit to point locations, not the extent of symbols and labels',
|
|
44289
|
+
type: 'flag'
|
|
44290
|
+
})
|
|
43966
44291
|
.option('name', nameOpt)
|
|
43967
44292
|
.option('replace', {
|
|
43968
44293
|
describe: 'replace the existing map frame',
|
|
@@ -44067,10 +44392,13 @@ ${svg}
|
|
|
44067
44392
|
.option('target', targetOpt);
|
|
44068
44393
|
|
|
44069
44394
|
parser.command('scalebar')
|
|
44070
|
-
.describe('add a
|
|
44395
|
+
.describe('add a scale bar to the map frame and SVG output')
|
|
44071
44396
|
.option('label', {
|
|
44072
44397
|
DEFAULT: true,
|
|
44073
|
-
describe: 'distance label, e.g. "35 miles"'
|
|
44398
|
+
describe: 'distance label, e.g. "35 miles" (default is automatic)'
|
|
44399
|
+
})
|
|
44400
|
+
.option('units', {
|
|
44401
|
+
describe: 'units of a bare-number or automatic label: km, meters, miles or feet (default is miles)'
|
|
44074
44402
|
})
|
|
44075
44403
|
.option('style', {
|
|
44076
44404
|
describe: 'two options: a or b'
|
|
@@ -44078,6 +44406,18 @@ ${svg}
|
|
|
44078
44406
|
.option('font-size', {
|
|
44079
44407
|
type: 'number'
|
|
44080
44408
|
})
|
|
44409
|
+
.option('font-family', {
|
|
44410
|
+
describe: 'font family of labels'
|
|
44411
|
+
})
|
|
44412
|
+
.option('font-style', {
|
|
44413
|
+
describe: 'font style of labels, e.g. italic'
|
|
44414
|
+
})
|
|
44415
|
+
.option('font-weight', {
|
|
44416
|
+
describe: 'font weight of labels, e.g. bold'
|
|
44417
|
+
})
|
|
44418
|
+
.option('color', {
|
|
44419
|
+
describe: 'color of bar and labels (default is black)'
|
|
44420
|
+
})
|
|
44081
44421
|
.option('tic-length', {
|
|
44082
44422
|
describe: 'length of tic marks (style b)',
|
|
44083
44423
|
type: 'number'
|
|
@@ -44090,18 +44430,22 @@ ${svg}
|
|
|
44090
44430
|
type: 'number'
|
|
44091
44431
|
})
|
|
44092
44432
|
.option('position', {
|
|
44093
|
-
describe: '
|
|
44433
|
+
describe: 'top-left (default), top-right, bottom-left or bottom-right'
|
|
44094
44434
|
})
|
|
44095
44435
|
.option('label-position', {
|
|
44096
44436
|
describe: 'top, bottom, top-center (style a), etc'
|
|
44097
44437
|
})
|
|
44098
44438
|
.option('dual-units', {
|
|
44099
|
-
|
|
44439
|
+
describe: 'show both metric and imperial units (style b)',
|
|
44100
44440
|
type: 'flag'
|
|
44101
44441
|
})
|
|
44102
44442
|
.option('margin', {
|
|
44103
44443
|
describe: 'offset in pixels from edge of map',
|
|
44104
44444
|
type: 'number'
|
|
44445
|
+
})
|
|
44446
|
+
.option('remove', {
|
|
44447
|
+
describe: 'remove the scale bar from the map frame',
|
|
44448
|
+
type: 'flag'
|
|
44105
44449
|
});
|
|
44106
44450
|
|
|
44107
44451
|
parser.command('shape')
|
|
@@ -51575,8 +51919,639 @@ ${svg}
|
|
|
51575
51919
|
bboxToPolygon: bboxToPolygon
|
|
51576
51920
|
});
|
|
51577
51921
|
|
|
51922
|
+
// Apply rotation, scale and/or shift to some or all of the features in a dataset
|
|
51923
|
+
//
|
|
51924
|
+
cmd.affine = function(targetLayers, dataset, opts) {
|
|
51925
|
+
// Need to separate the targeted shapes from any other shapes that share
|
|
51926
|
+
// the same topology. So we duplicate any arcs that are shared by the targeted
|
|
51927
|
+
// shapes and their topological neighbors and remap arc references in the
|
|
51928
|
+
// neighbors to point to the copies.
|
|
51929
|
+
// TODO: explore alternative: if some arcs are shared between transformed and
|
|
51930
|
+
// non-transformed shapes, first remove topology, then tranform, then rebuild topology
|
|
51931
|
+
//
|
|
51932
|
+
var rotateArg = opts.rotate || 0;
|
|
51933
|
+
var scaleArg = opts.scale || 1;
|
|
51934
|
+
var shiftArg = opts.shift ? convertIntervalPair(opts.shift, getDatasetCRS(dataset)) : [0, 0];
|
|
51935
|
+
var arcs = dataset.arcs;
|
|
51936
|
+
var targetShapes = [];
|
|
51937
|
+
var otherShapes = [];
|
|
51938
|
+
var targetPoints = [];
|
|
51939
|
+
var targetPointLayers = [];
|
|
51940
|
+
var targetFlags, otherFlags, transform;
|
|
51941
|
+
dataset.layers.filter(layerHasGeometry$1).forEach(function(lyr) {
|
|
51942
|
+
var hits = [],
|
|
51943
|
+
misses = [],
|
|
51944
|
+
test;
|
|
51945
|
+
if (targetLayers.indexOf(lyr) == -1) {
|
|
51946
|
+
misses = lyr.shapes;
|
|
51947
|
+
} else if (opts.where) {
|
|
51948
|
+
test = compileFeatureExpression(opts.where, lyr, dataset.arcs);
|
|
51949
|
+
lyr.shapes.forEach(function(shp, i) {
|
|
51950
|
+
(test(i) ? hits : misses).push(shp);
|
|
51951
|
+
});
|
|
51952
|
+
} else {
|
|
51953
|
+
hits = lyr.shapes;
|
|
51954
|
+
}
|
|
51955
|
+
if (lyr.geometry_type == 'point') {
|
|
51956
|
+
targetPoints = targetPoints.concat(hits);
|
|
51957
|
+
if (hits.length > 0) targetPointLayers.push(lyr);
|
|
51958
|
+
} else {
|
|
51959
|
+
targetShapes = targetShapes.concat(hits);
|
|
51960
|
+
otherShapes = otherShapes.concat(misses);
|
|
51961
|
+
}
|
|
51962
|
+
});
|
|
51963
|
+
var anchorArg = getAffineAnchor({arcs: dataset.arcs, layers: [{
|
|
51964
|
+
geometry_type: 'point', shapes: targetPoints}, {geometry_type: 'polyline',
|
|
51965
|
+
shapes: targetShapes}]}, opts);
|
|
51966
|
+
transform = getAffineTransform(rotateArg, scaleArg, shiftArg, anchorArg);
|
|
51967
|
+
if (opts.fit_bbox) {
|
|
51968
|
+
transform = getFitBoxTransform(opts.fit_bbox, targetPoints, targetShapes, arcs);
|
|
51969
|
+
}
|
|
51970
|
+
if (targetShapes.length > 0) {
|
|
51971
|
+
targetFlags = new Uint8Array(arcs.size());
|
|
51972
|
+
otherFlags = new Uint8Array(arcs.size());
|
|
51973
|
+
countArcsInShapes(targetShapes, targetFlags);
|
|
51974
|
+
if (otherShapes.length > 0) {
|
|
51975
|
+
countArcsInShapes(otherShapes, otherFlags);
|
|
51976
|
+
applyArrayMask(otherFlags, targetFlags);
|
|
51977
|
+
noteDatasetWillChange(dataset, {operation: 'affine', unit: 'arcs'});
|
|
51978
|
+
dataset.arcs = duplicateSelectedArcs(otherShapes, arcs, otherFlags);
|
|
51979
|
+
markDatasetChanged(dataset, {operation: 'affine', unit: 'arcs'});
|
|
51980
|
+
}
|
|
51981
|
+
dataset.arcs.transformPoints(function(x, y, arcId) {
|
|
51982
|
+
if (arcId < targetFlags.length && targetFlags[arcId] > 0) {
|
|
51983
|
+
return transform(x, y);
|
|
51984
|
+
}
|
|
51985
|
+
});
|
|
51986
|
+
}
|
|
51987
|
+
targetPointLayers.forEach(function(lyr) {
|
|
51988
|
+
noteLayerWillChange(lyr, {operation: 'affine', unit: 'shapes'});
|
|
51989
|
+
});
|
|
51990
|
+
forEachPoint(targetPoints, function(p) {
|
|
51991
|
+
var p2 = transform(p[0], p[1]);
|
|
51992
|
+
p[0] = p2[0];
|
|
51993
|
+
p[1] = p2[1];
|
|
51994
|
+
});
|
|
51995
|
+
targetPointLayers.forEach(function(lyr) {
|
|
51996
|
+
markLayerChanged(lyr, {operation: 'affine', unit: 'shapes'});
|
|
51997
|
+
});
|
|
51998
|
+
};
|
|
51999
|
+
|
|
52000
|
+
function getAffineAnchor(dataset, opts) {
|
|
52001
|
+
var anchor, bounds;
|
|
52002
|
+
if (opts.anchor) {
|
|
52003
|
+
anchor = opts.anchor;
|
|
52004
|
+
} else {
|
|
52005
|
+
// get bounds of selected shapes to calculate center of rotation/scale
|
|
52006
|
+
bounds = getDatasetBounds(dataset);
|
|
52007
|
+
anchor = [bounds.centerX(), bounds.centerY()];
|
|
52008
|
+
}
|
|
52009
|
+
return anchor;
|
|
52010
|
+
}
|
|
52011
|
+
|
|
52012
|
+
// TODO: handle problems with unprojected datasets
|
|
52013
|
+
// option 1: don't allow affine transformation of unprojected data
|
|
52014
|
+
// option 2: error if transformed data exceeds valid coordinate range
|
|
52015
|
+
// source: http://mathworld.wolfram.com/AffineTransformation.html
|
|
52016
|
+
function getAffineTransform(rotation, scale, shift, anchor) {
|
|
52017
|
+
var angle = rotation * Math.PI / 180;
|
|
52018
|
+
var a = scale * Math.cos(angle);
|
|
52019
|
+
var b = -scale * Math.sin(angle);
|
|
52020
|
+
return function(x, y) {
|
|
52021
|
+
var x2 = a * (x - anchor[0]) - b * (y - anchor[1]) + shift[0] + anchor[0];
|
|
52022
|
+
var y2 = b * (x - anchor[0]) + a * (y - anchor[1]) + shift[1] + anchor[1];
|
|
52023
|
+
return [x2, y2];
|
|
52024
|
+
};
|
|
52025
|
+
}
|
|
52026
|
+
|
|
52027
|
+
function getFitBoxTransform(bbox, points, shapes, arcs) {
|
|
52028
|
+
var dataset = {
|
|
52029
|
+
arcs: arcs,
|
|
52030
|
+
layers: []
|
|
52031
|
+
};
|
|
52032
|
+
if (points && points.length) {
|
|
52033
|
+
dataset.layers.push({
|
|
52034
|
+
geometry_type: 'point',
|
|
52035
|
+
shapes: points
|
|
52036
|
+
});
|
|
52037
|
+
}
|
|
52038
|
+
if (shapes && shapes.length) {
|
|
52039
|
+
dataset.layers.push({
|
|
52040
|
+
geometry_type: 'polyline',
|
|
52041
|
+
shapes: shapes
|
|
52042
|
+
});
|
|
52043
|
+
}
|
|
52044
|
+
var frame = calcFrameData(dataset, {fit_bbox: bbox});
|
|
52045
|
+
var fromBounds = new Bounds(frame.bbox);
|
|
52046
|
+
var toBounds = new Bounds(frame.bbox2);
|
|
52047
|
+
var fwd = fromBounds.getTransform(toBounds, false);
|
|
52048
|
+
return function(x, y) {
|
|
52049
|
+
return fwd.transform(x, y);
|
|
52050
|
+
};
|
|
52051
|
+
}
|
|
52052
|
+
|
|
52053
|
+
function applyArrayMask(destArr, maskArr) {
|
|
52054
|
+
for (var i=0, n=destArr.length; i<n; i++) {
|
|
52055
|
+
if (maskArr[i] === 0) destArr[i] = 0;
|
|
52056
|
+
}
|
|
52057
|
+
}
|
|
52058
|
+
|
|
52059
|
+
function duplicateSelectedArcs(shapes, arcs, flags) {
|
|
52060
|
+
var arcCount = 0;
|
|
52061
|
+
var vertexCount = 0;
|
|
52062
|
+
var data = arcs.getVertexData();
|
|
52063
|
+
var xx = [], yy = [], nn = [], map = [], n;
|
|
52064
|
+
for (var i=0, len=flags.length; i<len; i++) {
|
|
52065
|
+
if (flags[i] > 0) {
|
|
52066
|
+
map[i] = arcs.size() + arcCount;
|
|
52067
|
+
n = data.nn[i];
|
|
52068
|
+
utils.copyElements(data.xx, data.ii[i], xx, vertexCount, n);
|
|
52069
|
+
utils.copyElements(data.yy, data.ii[i], yy, vertexCount, n);
|
|
52070
|
+
nn.push(n);
|
|
52071
|
+
vertexCount += n;
|
|
52072
|
+
arcCount++;
|
|
52073
|
+
}
|
|
52074
|
+
}
|
|
52075
|
+
forEachArcId(shapes, function(id) {
|
|
52076
|
+
var absId = absArcId(id);
|
|
52077
|
+
if (flags[absId] > 0) {
|
|
52078
|
+
return id < 0 ? ~map[absId] : map[absId];
|
|
52079
|
+
}
|
|
52080
|
+
});
|
|
52081
|
+
return mergeArcs([arcs, new ArcCollection(nn, xx, yy)]);
|
|
52082
|
+
}
|
|
52083
|
+
|
|
52084
|
+
var roundCoord$1 = getRoundingFunction(0.01);
|
|
52085
|
+
|
|
52086
|
+
function getSymbolFillColor(d) {
|
|
52087
|
+
return d.fill || 'magenta';
|
|
52088
|
+
}
|
|
52089
|
+
|
|
52090
|
+
function getSymbolStrokeColor(d) {
|
|
52091
|
+
return d.stroke || d.fill || 'magenta';
|
|
52092
|
+
}
|
|
52093
|
+
|
|
52094
|
+
function applySymbolStyles(sym, d) {
|
|
52095
|
+
if (sym.type == 'polyline') {
|
|
52096
|
+
sym.stroke = getSymbolStrokeColor(d);
|
|
52097
|
+
} else {
|
|
52098
|
+
sym.fill = getSymbolFillColor(d);
|
|
52099
|
+
}
|
|
52100
|
+
if (d.opacity) {
|
|
52101
|
+
sym.opacity = d.opacity;
|
|
52102
|
+
}
|
|
52103
|
+
return sym;
|
|
52104
|
+
}
|
|
52105
|
+
|
|
52106
|
+
function getSymbolRadius(d) {
|
|
52107
|
+
if (d.radius === 0 || d.length === 0 || d.r === 0) return 0;
|
|
52108
|
+
return d.radius || d.length || d.r || 5; // use a default value
|
|
52109
|
+
}
|
|
52110
|
+
|
|
52111
|
+
// Converts the value of a list-valued option into an array of items. The value
|
|
52112
|
+
// arrives either as a whole string (from a direct call, rather than from the
|
|
52113
|
+
// -symbols option accessor) or as an array of items already resolved one by
|
|
52114
|
+
// one. An item that holds an array of its own, as an expression like
|
|
52115
|
+
// [R, R * 0.5] does, is flattened into the list. Items themselves are left
|
|
52116
|
+
// unsplit, because a single item can contain a comma, as rgba() colors do.
|
|
52117
|
+
function toItemList(val) {
|
|
52118
|
+
var items = utils.isString(val) ? splitListItems(val) : [].concat(val);
|
|
52119
|
+
var list = [];
|
|
52120
|
+
for (var i=0; i<items.length; i++) {
|
|
52121
|
+
if (Array.isArray(items[i])) {
|
|
52122
|
+
list = list.concat(items[i]);
|
|
52123
|
+
} else {
|
|
52124
|
+
list.push(items[i]);
|
|
52125
|
+
}
|
|
52126
|
+
}
|
|
52127
|
+
return list;
|
|
52128
|
+
}
|
|
52129
|
+
|
|
52130
|
+
// Like toItemList(), for options that take numbers. An item that is a string
|
|
52131
|
+
// containing commas is split, so that a data field holding a value like "2,4"
|
|
52132
|
+
// works as a list. Values that aren't numbers become NaN, for the caller to
|
|
52133
|
+
// report or ignore.
|
|
52134
|
+
function toNumberList(val) {
|
|
52135
|
+
var list = [];
|
|
52136
|
+
toItemList(val).forEach(function(item) {
|
|
52137
|
+
if (utils.isString(item) && item.indexOf(',') > -1) {
|
|
52138
|
+
list = list.concat(parseNumberList(item));
|
|
52139
|
+
} else {
|
|
52140
|
+
list.push(Number(item));
|
|
52141
|
+
}
|
|
52142
|
+
});
|
|
52143
|
+
return list;
|
|
52144
|
+
}
|
|
52145
|
+
|
|
52146
|
+
// Returns the radius of the smallest circle centered on a symbol's point that
|
|
52147
|
+
// covers the symbol, or null if the symbol contains a part whose reach can't be
|
|
52148
|
+
// measured.
|
|
52149
|
+
//
|
|
52150
|
+
// Only a symbol drawn around its point can be described this way: a circle, or
|
|
52151
|
+
// a group of the circles and polygons that make up ring, pie and donut symbols.
|
|
52152
|
+
// An arrow is drawn from its point outward, so the circle around its point that
|
|
52153
|
+
// covers it says nothing useful about the space it occupies -- arrows are plain
|
|
52154
|
+
// polygon symbols, which reach this function only as group parts, which they
|
|
52155
|
+
// never are.
|
|
52156
|
+
function getSymbolBoundingRadius(sym) {
|
|
52157
|
+
var max = 0;
|
|
52158
|
+
var parts, r, i;
|
|
52159
|
+
if (!sym) return null;
|
|
52160
|
+
if (sym.type == 'circle') {
|
|
52161
|
+
// Half of a stroke lies outside the circle it follows, which is how a ring
|
|
52162
|
+
// symbol paints its bands.
|
|
52163
|
+
return getPositiveNumber(sym.r) + getStrokeOutset(sym);
|
|
52164
|
+
}
|
|
52165
|
+
if (sym.type == 'polygon') {
|
|
52166
|
+
forEachSymbolCoord(sym.coordinates || [], function(p) {
|
|
52167
|
+
var dist = Math.sqrt(p[0] * p[0] + p[1] * p[1]);
|
|
52168
|
+
if (dist > max) max = dist;
|
|
52169
|
+
});
|
|
52170
|
+
return max + getStrokeOutset(sym);
|
|
52171
|
+
}
|
|
52172
|
+
if (sym.type == 'group') {
|
|
52173
|
+
parts = sym.parts || [];
|
|
52174
|
+
for (i=0; i<parts.length; i++) {
|
|
52175
|
+
r = getSymbolBoundingRadius(parts[i]);
|
|
52176
|
+
if (r === null) return null;
|
|
52177
|
+
if (r > max) max = r;
|
|
52178
|
+
}
|
|
52179
|
+
return max;
|
|
52180
|
+
}
|
|
52181
|
+
return null;
|
|
52182
|
+
}
|
|
52183
|
+
|
|
52184
|
+
// A stroke straddles the path it is drawn on, so half of its width extends
|
|
52185
|
+
// beyond the shape.
|
|
52186
|
+
function getStrokeOutset(sym) {
|
|
52187
|
+
var width = sym['stroke-width'];
|
|
52188
|
+
if (!sym.stroke || sym.stroke == 'none') return 0;
|
|
52189
|
+
// an SVG stroke is one pixel wide unless stroke-width says otherwise
|
|
52190
|
+
if (width === undefined || width === null || width === '') return 0.5;
|
|
52191
|
+
return getPositiveNumber(width) / 2;
|
|
52192
|
+
}
|
|
52193
|
+
|
|
52194
|
+
function getPositiveNumber(val) {
|
|
52195
|
+
var num = +val;
|
|
52196
|
+
return num > 0 ? num : 0;
|
|
52197
|
+
}
|
|
52198
|
+
|
|
52199
|
+
function forEachSymbolCoord(coords, cb) {
|
|
52200
|
+
var isPoint = coords && utils.isNumber(coords[0]);
|
|
52201
|
+
var isNested = !isPoint && coords && Array.isArray(coords[0]);
|
|
52202
|
+
if (isPoint) return cb(coords);
|
|
52203
|
+
for (var i=0; i<coords.length; i++) {
|
|
52204
|
+
if (isNested) forEachSymbolCoord(coords[i], cb);
|
|
52205
|
+
}
|
|
52206
|
+
}
|
|
52207
|
+
|
|
52208
|
+
function flipY(coords) {
|
|
52209
|
+
forEachSymbolCoord(coords, function(p) {
|
|
52210
|
+
p[1] = -p[1];
|
|
52211
|
+
});
|
|
52212
|
+
}
|
|
52213
|
+
|
|
52214
|
+
function scaleAndShiftCoords(coords, scale, shift) {
|
|
52215
|
+
forEachSymbolCoord(coords, function(xy) {
|
|
52216
|
+
xy[0] = xy[0] * scale + shift[0];
|
|
52217
|
+
xy[1] = xy[1] * scale + shift[1];
|
|
52218
|
+
});
|
|
52219
|
+
}
|
|
52220
|
+
|
|
52221
|
+
function roundCoordsForSVG(coords) {
|
|
52222
|
+
forEachSymbolCoord(coords, function(p) {
|
|
52223
|
+
p[0] = roundCoord$1(p[0]);
|
|
52224
|
+
p[1] = roundCoord$1(p[1]);
|
|
52225
|
+
});
|
|
52226
|
+
}
|
|
52227
|
+
|
|
52228
|
+
function rotateCoords(coords, rotation) {
|
|
52229
|
+
if (!rotation) return;
|
|
52230
|
+
var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
|
|
52231
|
+
forEachSymbolCoord(coords, function(p) {
|
|
52232
|
+
var p2 = f(p[0], p[1]);
|
|
52233
|
+
p[0] = p2[0];
|
|
52234
|
+
p[1] = p2[1];
|
|
52235
|
+
});
|
|
52236
|
+
}
|
|
52237
|
+
|
|
52238
|
+
// The space that a point's symbol and label take up when drawn, for fitting a
|
|
52239
|
+
// map frame around them.
|
|
52240
|
+
//
|
|
52241
|
+
// Boxes are [xmin, ymin, xmax, ymax] in px, relative to the point, with y down:
|
|
52242
|
+
// the space symbols are drawn in (see renderPoint() in svg-symbols.mjs, which
|
|
52243
|
+
// this follows). A box can leave out parts of what gets drawn -- anti-aliasing,
|
|
52244
|
+
// miter joins, a curve bowing past its control points -- but not by more than a
|
|
52245
|
+
// pixel or so.
|
|
52246
|
+
|
|
52247
|
+
// The box around what renderPoint() draws for @rec, or null if it draws nothing
|
|
52248
|
+
// that can be sized.
|
|
52249
|
+
// @cache: optional Map, for reusing the boxes of svg-symbol strings, which are
|
|
52250
|
+
// often the same for every feature in a layer
|
|
52251
|
+
function getPointSymbolBox(rec, cache) {
|
|
52252
|
+
var box = null;
|
|
52253
|
+
if (!rec) return null;
|
|
52254
|
+
if (featureHasSvgSymbol(rec)) {
|
|
52255
|
+
box = getSymbolBox(rec, cache);
|
|
52256
|
+
}
|
|
52257
|
+
if (featureHasLabel(rec)) {
|
|
52258
|
+
box = mergeBoxes(box, getLabelBox(rec));
|
|
52259
|
+
}
|
|
52260
|
+
return box;
|
|
52261
|
+
}
|
|
52262
|
+
|
|
52263
|
+
// How far the text of a label drawn along a path can reach from the path, in
|
|
52264
|
+
// px. The side the text sits on turns with the path, so it is allowed for on
|
|
52265
|
+
// every side.
|
|
52266
|
+
function getPathLabelPadding(rec) {
|
|
52267
|
+
return getLabelFontSize(rec) + getHaloWidth(rec);
|
|
52268
|
+
}
|
|
52269
|
+
|
|
52270
|
+
function getLabelBox(rec) {
|
|
52271
|
+
var o = getLabelTextBox(rec, {estimate_width: true});
|
|
52272
|
+
var pad = getHaloWidth(rec);
|
|
52273
|
+
var box = [o.xmin - pad, o.ymin - pad, o.xmax + pad, o.ymax + pad];
|
|
52274
|
+
var via = labelHasCallout(rec) ? parsePointPair(rec['callout-via'] || '') : null;
|
|
52275
|
+
if (via) {
|
|
52276
|
+
box = mergeBoxes(box, [via[0], via[1], via[0], via[1]]);
|
|
52277
|
+
}
|
|
52278
|
+
return box;
|
|
52279
|
+
}
|
|
52280
|
+
|
|
52281
|
+
function getHaloWidth(rec) {
|
|
52282
|
+
var w = +rec['halo-width'];
|
|
52283
|
+
return w > 0 ? w : 0;
|
|
52284
|
+
}
|
|
52285
|
+
|
|
52286
|
+
function getSymbolBox(d, cache) {
|
|
52287
|
+
var sym = d['svg-symbol'];
|
|
52288
|
+
if (sym) return getComplexSymbolBox(sym, cache);
|
|
52289
|
+
if (featureHasIcon(d)) return getIconBox(d);
|
|
52290
|
+
if (d.r > 0) return getSquareBox(+d.r + getStrokeOutset(d), 0, 0);
|
|
52291
|
+
return null;
|
|
52292
|
+
}
|
|
52293
|
+
|
|
52294
|
+
function getIconBox(d) {
|
|
52295
|
+
var type = d.icon || 'circle';
|
|
52296
|
+
var r = getIconRadius(d, type);
|
|
52297
|
+
var sw;
|
|
52298
|
+
if (!(r > 0)) return null;
|
|
52299
|
+
if (type == 'ring') {
|
|
52300
|
+
// drawn as a stroke, one px wide unless stroke-width says otherwise
|
|
52301
|
+
sw = +d['stroke-width'];
|
|
52302
|
+
return getSquareBox(r + (sw > 0 ? sw : 1) / 2, 0, 0);
|
|
52303
|
+
}
|
|
52304
|
+
if (type == 'circle' || type == 'square' || type == 'star') {
|
|
52305
|
+
return getSquareBox(r + getStrokeOutset(d), 0, 0);
|
|
52306
|
+
}
|
|
52307
|
+
return null; // unsupported icons are not drawn
|
|
52308
|
+
}
|
|
52309
|
+
|
|
52310
|
+
function getComplexSymbolBox(sym, cache) {
|
|
52311
|
+
var box;
|
|
52312
|
+
if (!utils.isString(sym)) return getSymbolPartBox(sym, 0, 0);
|
|
52313
|
+
if (cache && cache.has(sym)) return cache.get(sym);
|
|
52314
|
+
try {
|
|
52315
|
+
box = getSymbolPartBox(JSON.parse(sym), 0, 0);
|
|
52316
|
+
} catch(e) {
|
|
52317
|
+
box = null;
|
|
52318
|
+
}
|
|
52319
|
+
if (cache) cache.set(sym, box);
|
|
52320
|
+
return box;
|
|
52321
|
+
}
|
|
52322
|
+
|
|
52323
|
+
// Follows the renderers in svg-symbols.mjs: @x, @y is where the part is drawn,
|
|
52324
|
+
// which circles, squares, images, lines and labels are placed at and polygons
|
|
52325
|
+
// and polylines ignore.
|
|
52326
|
+
function getSymbolPartBox(sym, x, y) {
|
|
52327
|
+
var type = sym && sym.type;
|
|
52328
|
+
var w, h;
|
|
52329
|
+
if (!sym || sym.tag) return null; // raw SVG can't be sized
|
|
52330
|
+
if (type == 'circle' || type == 'square') {
|
|
52331
|
+
return getSquareBox(getPositive(sym.r) + getStrokeOutset(sym), x, y);
|
|
52332
|
+
}
|
|
52333
|
+
if (type == 'image') {
|
|
52334
|
+
w = sym.width || 20;
|
|
52335
|
+
h = sym.height || 20;
|
|
52336
|
+
return [x - w / 2, y - h / 2, x + w / 2, y + h / 2];
|
|
52337
|
+
}
|
|
52338
|
+
if (type == 'polygon') {
|
|
52339
|
+
return padBox(getCoordsBox(sym.coordinates), getStrokeOutset(sym));
|
|
52340
|
+
}
|
|
52341
|
+
if (type == 'polyline') {
|
|
52342
|
+
return padBox(getCoordsBox(sym.coordinates), getLineOutset(sym));
|
|
52343
|
+
}
|
|
52344
|
+
if (type == 'line') {
|
|
52345
|
+
return padBox(getCoordsBox([[x, y], [x + (sym.dx || 0), y + (sym.dy || 0)]]),
|
|
52346
|
+
getLineOutset(sym));
|
|
52347
|
+
}
|
|
52348
|
+
if (type == 'label') {
|
|
52349
|
+
return shiftBox(getLabelBox(sym), x, y);
|
|
52350
|
+
}
|
|
52351
|
+
if (type == 'group') {
|
|
52352
|
+
return getGroupBox(sym, x, y);
|
|
52353
|
+
}
|
|
52354
|
+
return null; // offset draws nothing, and unknown types are not drawn
|
|
52355
|
+
}
|
|
52356
|
+
|
|
52357
|
+
// A line part moves the parts after it to its far end; an offset part moves
|
|
52358
|
+
// them into a group translated by its offset.
|
|
52359
|
+
function getGroupBox(sym, x, y) {
|
|
52360
|
+
var box = null;
|
|
52361
|
+
var ox = 0, oy = 0;
|
|
52362
|
+
(sym.parts || []).forEach(function(part) {
|
|
52363
|
+
box = mergeBoxes(box, shiftBox(getSymbolPartBox(part, x, y), ox, oy));
|
|
52364
|
+
if (!part) return;
|
|
52365
|
+
if (part.type == 'line') {
|
|
52366
|
+
x += part.dx || 0;
|
|
52367
|
+
y += part.dy || 0;
|
|
52368
|
+
} else if (part.type == 'offset') {
|
|
52369
|
+
ox += x + (part.dx || 0);
|
|
52370
|
+
oy += y + (part.dy || 0);
|
|
52371
|
+
x = y = 0;
|
|
52372
|
+
}
|
|
52373
|
+
});
|
|
52374
|
+
return box;
|
|
52375
|
+
}
|
|
52376
|
+
|
|
52377
|
+
// A stroked line is one px wide unless stroke-width says otherwise
|
|
52378
|
+
function getLineOutset(sym) {
|
|
52379
|
+
var sw = +sym['stroke-width'];
|
|
52380
|
+
return (sw > 0 ? sw : 1) / 2;
|
|
52381
|
+
}
|
|
52382
|
+
|
|
52383
|
+
function getCoordsBox(coords) {
|
|
52384
|
+
var box = null;
|
|
52385
|
+
forEachSymbolCoord(coords || [], function(p) {
|
|
52386
|
+
if (!isFinite(p[0]) || !isFinite(p[1])) return;
|
|
52387
|
+
box = mergeBoxes(box, [p[0], p[1], p[0], p[1]]);
|
|
52388
|
+
});
|
|
52389
|
+
return box;
|
|
52390
|
+
}
|
|
52391
|
+
|
|
52392
|
+
function getSquareBox(r, x, y) {
|
|
52393
|
+
return r > 0 ? [x - r, y - r, x + r, y + r] : null;
|
|
52394
|
+
}
|
|
52395
|
+
|
|
52396
|
+
function padBox(box, pad) {
|
|
52397
|
+
return box ? [box[0] - pad, box[1] - pad, box[2] + pad, box[3] + pad] : null;
|
|
52398
|
+
}
|
|
52399
|
+
|
|
52400
|
+
function shiftBox(box, dx, dy) {
|
|
52401
|
+
if (!box || !dx && !dy) return box;
|
|
52402
|
+
return [box[0] + dx, box[1] + dy, box[2] + dx, box[3] + dy];
|
|
52403
|
+
}
|
|
52404
|
+
|
|
52405
|
+
function mergeBoxes(a, b) {
|
|
52406
|
+
if (!a) return b;
|
|
52407
|
+
if (!b) return a;
|
|
52408
|
+
return [Math.min(a[0], b[0]), Math.min(a[1], b[1]),
|
|
52409
|
+
Math.max(a[2], b[2]), Math.max(a[3], b[3])];
|
|
52410
|
+
}
|
|
52411
|
+
|
|
52412
|
+
function getPositive(val) {
|
|
52413
|
+
var num = +val;
|
|
52414
|
+
return num > 0 ? num : 0;
|
|
52415
|
+
}
|
|
52416
|
+
|
|
52417
|
+
// Fitting a map frame to its content, including the symbols and labels drawn
|
|
52418
|
+
// at points, which take up room that the point coordinates alone do not.
|
|
52419
|
+
//
|
|
52420
|
+
// Symbols are sized in output pixels, and how many map units a pixel covers is
|
|
52421
|
+
// set by the frame being fitted, so the extent can't simply be padded. It is
|
|
52422
|
+
// found by solving for the scale instead: the smallest scale s (map units per
|
|
52423
|
+
// output pixel) at which the content -- points and their symbols, drawn at s --
|
|
52424
|
+
// gets a frame whose own scale is no larger than s.
|
|
52425
|
+
//
|
|
52426
|
+
// Every point's box moves each side of the extent linearly with s, so the
|
|
52427
|
+
// extent's width and height are convex, piecewise-linear functions of s, and so
|
|
52428
|
+
// is the frame scale they produce. Newton's method converges on the smallest
|
|
52429
|
+
// fixed point of such a function from below, without overshooting, in as many
|
|
52430
|
+
// steps as there are pieces along the way -- usually two or three.
|
|
52431
|
+
|
|
52432
|
+
var MAX_ITERATIONS = 50;
|
|
52433
|
+
var TOLERANCE = 1e-9;
|
|
52434
|
+
|
|
52435
|
+
// Returns the extent of @targets as a bbox array, padded to hold the symbols
|
|
52436
|
+
// and labels at their points, or null if the targets have no extent.
|
|
52437
|
+
//
|
|
52438
|
+
// @targets [{layer, dataset}]
|
|
52439
|
+
// @getScale function(bbox) -> map units per output px of a frame fitted to
|
|
52440
|
+
// @bbox; this is where the caller's offsets and page shape come in.
|
|
52441
|
+
// May return NaN for a bbox the frame can't be fitted to.
|
|
52442
|
+
// @opts.ignore_symbols fit to the coordinates alone
|
|
52443
|
+
function getFrameContentBbox(targets, getScale, opts) {
|
|
52444
|
+
// a data-only layer has no bounds
|
|
52445
|
+
var bounds = targets.reduce(function(memo, o) {
|
|
52446
|
+
var b = getLayerBounds(o.layer, o.dataset.arcs);
|
|
52447
|
+
return b ? memo.mergeBounds(b) : memo;
|
|
52448
|
+
}, new Bounds());
|
|
52449
|
+
var raw, items, bbox;
|
|
52450
|
+
if (!bounds.hasBounds()) return null;
|
|
52451
|
+
raw = bounds.toArray();
|
|
52452
|
+
if (opts && opts.ignore_symbols) return raw;
|
|
52453
|
+
items = getSymbolExtents(targets);
|
|
52454
|
+
if (items.length === 0) return raw;
|
|
52455
|
+
bbox = fitBboxToSymbols(raw, items, getScale);
|
|
52456
|
+
if (!bbox) {
|
|
52457
|
+
warn('Symbols and labels are too large to fit in the frame; ' +
|
|
52458
|
+
'fitting the frame to point locations only.');
|
|
52459
|
+
return raw;
|
|
52460
|
+
}
|
|
52461
|
+
return bbox;
|
|
52462
|
+
}
|
|
52463
|
+
|
|
52464
|
+
// The map units per output px of a frame with extent @bbox and nominal width
|
|
52465
|
+
// @width. A frame with a fixed page shape is filled out to that shape when it is
|
|
52466
|
+
// rendered (see fitDatasetToFrame()), so its scale is set by whichever of its
|
|
52467
|
+
// width and height is the tighter fit.
|
|
52468
|
+
function getFrameScale(bbox, width, fixedAspect) {
|
|
52469
|
+
var w = bbox[2] - bbox[0];
|
|
52470
|
+
var h = bbox[3] - bbox[1];
|
|
52471
|
+
return fixedAspect > 0 ? Math.max(w, h * fixedAspect) / width : w / width;
|
|
52472
|
+
}
|
|
52473
|
+
|
|
52474
|
+
// @raw bbox of the content without symbols
|
|
52475
|
+
// @items flat array of [x, y, xmin, ymin, xmax, ymax, ...]: each point and the
|
|
52476
|
+
// box around it in px, oriented like the map (y up)
|
|
52477
|
+
// Returns the padded bbox, or null if there is no scale at which the symbols
|
|
52478
|
+
// fit -- when they add up to more than the width of the page.
|
|
52479
|
+
function fitBboxToSymbols(raw, items, getScale) {
|
|
52480
|
+
var s = getScale(raw);
|
|
52481
|
+
var bbox, h, delta, slope, i;
|
|
52482
|
+
for (i = 0; i < MAX_ITERATIONS; i++) {
|
|
52483
|
+
if (!isValidScale(s)) return null;
|
|
52484
|
+
bbox = expandBbox(raw, items, s);
|
|
52485
|
+
h = getScale(bbox);
|
|
52486
|
+
if (!isValidScale(h)) return null;
|
|
52487
|
+
// Content with no extent of its own, like a single point, stops here with
|
|
52488
|
+
// s = 0: nothing about it sets a scale.
|
|
52489
|
+
if (h <= s * (1 + TOLERANCE)) return bbox;
|
|
52490
|
+
if (!(s > 0)) return null;
|
|
52491
|
+
delta = s * 1e-7;
|
|
52492
|
+
slope = (getScale(expandBbox(raw, items, s + delta)) - h) / delta;
|
|
52493
|
+
if (!(slope < 1)) return null;
|
|
52494
|
+
s += (h - s) / (1 - slope);
|
|
52495
|
+
}
|
|
52496
|
+
return expandBbox(raw, items, s);
|
|
52497
|
+
}
|
|
52498
|
+
|
|
52499
|
+
function isValidScale(s) {
|
|
52500
|
+
return s >= 0 && s < Infinity;
|
|
52501
|
+
}
|
|
52502
|
+
|
|
52503
|
+
function expandBbox(raw, items, s) {
|
|
52504
|
+
var xmin = raw[0], ymin = raw[1], xmax = raw[2], ymax = raw[3];
|
|
52505
|
+
var x, y, v;
|
|
52506
|
+
for (var i = 0, n = items.length; i < n; i += 6) {
|
|
52507
|
+
x = items[i];
|
|
52508
|
+
y = items[i + 1];
|
|
52509
|
+
v = x + items[i + 2] * s;
|
|
52510
|
+
if (v < xmin) xmin = v;
|
|
52511
|
+
v = y + items[i + 3] * s;
|
|
52512
|
+
if (v < ymin) ymin = v;
|
|
52513
|
+
v = x + items[i + 4] * s;
|
|
52514
|
+
if (v > xmax) xmax = v;
|
|
52515
|
+
v = y + items[i + 5] * s;
|
|
52516
|
+
if (v > ymax) ymax = v;
|
|
52517
|
+
}
|
|
52518
|
+
return [xmin, ymin, xmax, ymax];
|
|
52519
|
+
}
|
|
52520
|
+
|
|
52521
|
+
// Boxes come from the renderer's space, where y is down, and are flipped here
|
|
52522
|
+
// to the map's.
|
|
52523
|
+
function getSymbolExtents(targets) {
|
|
52524
|
+
var items = [];
|
|
52525
|
+
var cache = new Map();
|
|
52526
|
+
targets.forEach(function(o) {
|
|
52527
|
+
var lyr = o.layer;
|
|
52528
|
+
var records;
|
|
52529
|
+
if (lyr.geometry_type != 'point' || !lyr.shapes || !lyr.data) return;
|
|
52530
|
+
records = lyr.data.getRecords();
|
|
52531
|
+
lyr.shapes.forEach(function(shp, i) {
|
|
52532
|
+
var rec = records[i];
|
|
52533
|
+
var box, pad;
|
|
52534
|
+
if (!shp || !rec) return;
|
|
52535
|
+
if (shp.length > 1 && featureHasLabel(rec)) {
|
|
52536
|
+
// a multipoint label is drawn along a path through its points,
|
|
52537
|
+
// with no symbol (see featureIsPathLabel())
|
|
52538
|
+
pad = getPathLabelPadding(rec);
|
|
52539
|
+
box = [-pad, -pad, pad, pad];
|
|
52540
|
+
} else {
|
|
52541
|
+
box = getPointSymbolBox(rec, cache);
|
|
52542
|
+
}
|
|
52543
|
+
if (!box) return;
|
|
52544
|
+
for (var j = 0; j < shp.length; j++) {
|
|
52545
|
+
if (!shp[j] || !isFinite(shp[j][0]) || !isFinite(shp[j][1])) continue;
|
|
52546
|
+
items.push(shp[j][0], shp[j][1], box[0], -box[3], box[2], -box[1]);
|
|
52547
|
+
}
|
|
52548
|
+
});
|
|
52549
|
+
});
|
|
52550
|
+
return items;
|
|
52551
|
+
}
|
|
52552
|
+
|
|
51578
52553
|
cmd.frame = function(catalog, targets, opts) {
|
|
51579
|
-
var widthPx, heightPx,
|
|
52554
|
+
var widthPx, heightPx, bbox;
|
|
51580
52555
|
var existingFrame = getActiveFrame(catalog);
|
|
51581
52556
|
if (opts.width) {
|
|
51582
52557
|
widthPx = parseFrameSize(opts.width).valuePx;
|
|
@@ -51612,30 +52587,29 @@ ${svg}
|
|
|
51612
52587
|
} else {
|
|
51613
52588
|
var datasets = utils.pluck(targets, 'dataset');
|
|
51614
52589
|
requireDatasetsHaveCompatibleCRS(datasets, 'Targets include both projected and unprojected coordinates');
|
|
51615
|
-
bbox =
|
|
52590
|
+
bbox = getFrameContentBbox(expandCommandTargets(targets), function(contentBbox) {
|
|
52591
|
+
var extent = getFrameExtent(contentBbox, widthPx, heightPx, opts);
|
|
52592
|
+
return getFrameScale(extent.bbox, extent.width, getFixedAspect$1(extent, opts));
|
|
52593
|
+
}, opts);
|
|
52594
|
+
if (!bbox && !expandCommandTargets(targets).some(function(o) {
|
|
52595
|
+
return layerHasGeometry$1(o.layer);
|
|
52596
|
+
})) {
|
|
52597
|
+
stop$1('Unable to fit a frame to a layer with no geometry. Target a layer containing shapes, or use the bbox= option.');
|
|
52598
|
+
}
|
|
51616
52599
|
if (!bbox) {
|
|
51617
52600
|
stop$1('Command target is missing geographical bounds');
|
|
51618
52601
|
}
|
|
51619
52602
|
}
|
|
51620
52603
|
|
|
51621
|
-
|
|
51622
|
-
|
|
51623
|
-
|
|
51624
|
-
if (bbox[3] - bbox[1] > 0 === false || bbox[2] - bbox[0] > 0 === false) {
|
|
52604
|
+
var extent = getFrameExtent(bbox, widthPx, heightPx, opts);
|
|
52605
|
+
if (!extent.valid) {
|
|
51625
52606
|
stop$1('Frame has a collapsed bbox');
|
|
51626
52607
|
}
|
|
51627
52608
|
|
|
51628
|
-
aspectRatio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
|
|
51629
|
-
if (!widthPx) {
|
|
51630
|
-
widthPx = roundToDigits(heightPx * aspectRatio, 1);
|
|
51631
|
-
} else if (!heightPx) {
|
|
51632
|
-
heightPx = roundToDigits(widthPx / aspectRatio, 1);
|
|
51633
|
-
}
|
|
51634
|
-
|
|
51635
52609
|
var feature = {
|
|
51636
52610
|
type: 'Feature',
|
|
51637
|
-
properties: getFrameProperties(
|
|
51638
|
-
geometry: bboxToPolygon(bbox)
|
|
52611
|
+
properties: getFrameProperties(extent.width, extent.height, opts),
|
|
52612
|
+
geometry: bboxToPolygon(extent.bbox)
|
|
51639
52613
|
};
|
|
51640
52614
|
var frameDataset = importGeoJSON(feature);
|
|
51641
52615
|
// set CRS from target dataset
|
|
@@ -51645,16 +52619,47 @@ ${svg}
|
|
|
51645
52619
|
var crsInfo = getDatasetCrsInfo(targets[0].dataset);
|
|
51646
52620
|
setDatasetCrsInfo(frameDataset, crsInfo);
|
|
51647
52621
|
}
|
|
51648
|
-
frameDataset.layers[0]
|
|
52622
|
+
var frameLyr = frameDataset.layers[0];
|
|
52623
|
+
frameLyr.name = opts.name || 'frame';
|
|
51649
52624
|
if (existingFrame) {
|
|
51650
52625
|
if (!opts.replace) {
|
|
51651
52626
|
stop$1('A map frame already exists:', existingFrame.layer.name || '[unnamed frame]');
|
|
51652
52627
|
}
|
|
52628
|
+
moveFrameFurniture(existingFrame.dataset, frameDataset);
|
|
51653
52629
|
demoteFrameLayer(existingFrame.layer);
|
|
51654
52630
|
}
|
|
51655
|
-
|
|
52631
|
+
// target the frame, not any furniture that came with it
|
|
52632
|
+
catalog.setDefaultTarget([frameLyr], frameDataset);
|
|
51656
52633
|
};
|
|
51657
52634
|
|
|
52635
|
+
// The frame's extent and nominal size, from the extent of its content and the
|
|
52636
|
+
// size options. Pure, so that fitting to symbols can ask what scale an extent
|
|
52637
|
+
// would give the frame.
|
|
52638
|
+
function getFrameExtent(contentBbox, widthPx, heightPx, opts) {
|
|
52639
|
+
var offsets = opts.offset || opts.offsets;
|
|
52640
|
+
var bbox = contentBbox.slice();
|
|
52641
|
+
var aspectRatio;
|
|
52642
|
+
applyPercentageOffsets(bbox, offsets);
|
|
52643
|
+
applyPixelOffsets(bbox, widthPx, heightPx, offsets);
|
|
52644
|
+
aspectRatio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
|
|
52645
|
+
if (!widthPx) {
|
|
52646
|
+
widthPx = roundToDigits(heightPx * aspectRatio, 1);
|
|
52647
|
+
} else if (!heightPx) {
|
|
52648
|
+
heightPx = roundToDigits(widthPx / aspectRatio, 1);
|
|
52649
|
+
}
|
|
52650
|
+
return {
|
|
52651
|
+
bbox: bbox,
|
|
52652
|
+
width: widthPx,
|
|
52653
|
+
height: heightPx,
|
|
52654
|
+
valid: bbox[3] - bbox[1] > 0 && bbox[2] - bbox[0] > 0
|
|
52655
|
+
};
|
|
52656
|
+
}
|
|
52657
|
+
|
|
52658
|
+
function getFixedAspect$1(extent, opts) {
|
|
52659
|
+
return opts.aspect_ratio > 0 || opts.width && opts.height ?
|
|
52660
|
+
extent.width / extent.height : null;
|
|
52661
|
+
}
|
|
52662
|
+
|
|
51658
52663
|
function getFrameProperties(width, height, opts) {
|
|
51659
52664
|
var properties = {
|
|
51660
52665
|
type: 'frame',
|
|
@@ -51750,14 +52755,6 @@ ${svg}
|
|
|
51750
52755
|
return arg;
|
|
51751
52756
|
}
|
|
51752
52757
|
|
|
51753
|
-
function getTargetBbox(targets) {
|
|
51754
|
-
var expanded = expandCommandTargets(targets);
|
|
51755
|
-
var bounds = expanded.reduce(function(memo, o) {
|
|
51756
|
-
return memo.mergeBounds(getLayerBounds(o.layer, o.dataset.arcs));
|
|
51757
|
-
}, new Bounds());
|
|
51758
|
-
return bounds.hasBounds() ? bounds.toArray() : null;
|
|
51759
|
-
}
|
|
51760
|
-
|
|
51761
52758
|
// Convert width and height args to aspect ratio arg for the rectangle() function
|
|
51762
52759
|
function getAspectRatioArg(widthArg, heightArg) {
|
|
51763
52760
|
// heightArg is a string containing either a number or a
|
|
@@ -51902,7 +52899,8 @@ ${svg}
|
|
|
51902
52899
|
|
|
51903
52900
|
var OPERATION$1 = 'update-frame';
|
|
51904
52901
|
|
|
51905
|
-
|
|
52902
|
+
// @fitTargets: [{layer, dataset}] named by the fit= option
|
|
52903
|
+
function updateFrame(targetLayers, dataset, opts, fitTargets) {
|
|
51906
52904
|
if (!targetLayers || targetLayers.length != 1) {
|
|
51907
52905
|
stop$1('-update-frame expects a single target layer');
|
|
51908
52906
|
}
|
|
@@ -51926,15 +52924,14 @@ ${svg}
|
|
|
51926
52924
|
if (opts.fix_scale && (opts.width !== undefined || opts.height !== undefined)) {
|
|
51927
52925
|
stop$1('fix-scale cannot be combined with width= or height=');
|
|
51928
52926
|
}
|
|
52927
|
+
if (opts.bbox !== undefined && opts.fit !== undefined) {
|
|
52928
|
+
stop$1('bbox= and fit= are mutually exclusive');
|
|
52929
|
+
}
|
|
51929
52930
|
|
|
51930
52931
|
var rec = lyr.data.getRecords()[0];
|
|
51931
52932
|
var frame = getFrameLayerData(lyr, dataset.arcs);
|
|
51932
|
-
var bbox = (opts.bbox || frame.bbox).slice();
|
|
51933
|
-
var offsetArg = opts.offset || opts.offsets;
|
|
51934
52933
|
var fixedAspect = getFixedAspect(rec);
|
|
51935
|
-
var
|
|
51936
|
-
var units = rec.frame_units || 'px';
|
|
51937
|
-
var widthSize, heightSize, height, effectiveAspect;
|
|
52934
|
+
var sizes, contentBbox, update;
|
|
51938
52935
|
|
|
51939
52936
|
// 1. Aspect mode, resolved first because whether the page shape is fixed
|
|
51940
52937
|
// decides whether it may constrain the extent while offsets are applied.
|
|
@@ -51946,8 +52943,87 @@ ${svg}
|
|
|
51946
52943
|
}
|
|
51947
52944
|
fixedAspect = opts.aspect_ratio;
|
|
51948
52945
|
}
|
|
52946
|
+
sizes = parseSizeOptions(opts);
|
|
52947
|
+
|
|
52948
|
+
// 2. Extent, which fit= finds by trying extents out on steps 3 and 4
|
|
52949
|
+
if (opts.fit !== undefined) {
|
|
52950
|
+
contentBbox = getFrameContentBbox(getFitTargets(fitTargets, dataset), function(bbox) {
|
|
52951
|
+
var o = resolveFrameUpdate(bbox, frame, fixedAspect, sizes, opts);
|
|
52952
|
+
return getFrameScale(o.bbox, o.width, o.fixedAspect);
|
|
52953
|
+
}, opts);
|
|
52954
|
+
if (!contentBbox) {
|
|
52955
|
+
stop$1('Layers to fit are missing geographical bounds');
|
|
52956
|
+
}
|
|
52957
|
+
} else {
|
|
52958
|
+
contentBbox = opts.bbox || frame.bbox;
|
|
52959
|
+
}
|
|
52960
|
+
|
|
52961
|
+
update = resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, opts);
|
|
52962
|
+
if (!update.valid) {
|
|
52963
|
+
stop$1('Frame has a collapsed bbox');
|
|
52964
|
+
}
|
|
52965
|
+
if (opts.fix_scale) {
|
|
52966
|
+
requirePositiveSize(update.width, 'fix-scale', update.width);
|
|
52967
|
+
}
|
|
52968
|
+
|
|
52969
|
+
noteLayerWillChange(lyr, {operation: OPERATION$1, unit: 'shapes'});
|
|
52970
|
+
lyr.data.captureTableBefore({operation: OPERATION$1});
|
|
52971
|
+
rec.width = update.width;
|
|
52972
|
+
rec.height = update.height;
|
|
52973
|
+
rec.frame_units = update.units;
|
|
52974
|
+
if (update.fixedAspect) {
|
|
52975
|
+
rec.frame_aspect_ratio = update.fixedAspect;
|
|
52976
|
+
} else {
|
|
52977
|
+
delete rec.frame_aspect_ratio;
|
|
52978
|
+
}
|
|
52979
|
+
lyr.data.markChanged({operation: OPERATION$1});
|
|
52980
|
+
rebuildFrameLayerGeometry(lyr, dataset, new Bounds(update.bbox));
|
|
52981
|
+
markLayerChanged(lyr, {operation: OPERATION$1, unit: 'shapes'});
|
|
52982
|
+
}
|
|
52983
|
+
|
|
52984
|
+
// The frame's layers are not content to fit, and a frame is only fitted to
|
|
52985
|
+
// layers it shares coordinates with.
|
|
52986
|
+
function getFitTargets(fitTargets, frameDataset) {
|
|
52987
|
+
var targets = (fitTargets || []).filter(function(o) {
|
|
52988
|
+
return !isFrameLayer(o.layer, o.dataset.arcs);
|
|
52989
|
+
});
|
|
52990
|
+
if (targets.length === 0) {
|
|
52991
|
+
stop$1('fit= found no layers to fit the frame to');
|
|
52992
|
+
}
|
|
52993
|
+
requireDatasetsHaveCompatibleCRS(
|
|
52994
|
+
utils.uniq(targets.map(function(o) { return o.dataset; }).concat(frameDataset)),
|
|
52995
|
+
'Layers to fit and the frame have incompatible coordinates'
|
|
52996
|
+
);
|
|
52997
|
+
return targets;
|
|
52998
|
+
}
|
|
52999
|
+
|
|
53000
|
+
function parseSizeOptions(opts) {
|
|
53001
|
+
var sizes = {width: null, height: null};
|
|
53002
|
+
if (opts.width !== undefined) {
|
|
53003
|
+
sizes.width = parseFrameSize(opts.width);
|
|
53004
|
+
requirePositiveSize(sizes.width.valuePx, 'width', opts.width);
|
|
53005
|
+
}
|
|
53006
|
+
if (opts.height !== undefined) {
|
|
53007
|
+
sizes.height = parseFrameSize(opts.height);
|
|
53008
|
+
requirePositiveSize(sizes.height.valuePx, 'height', opts.height);
|
|
53009
|
+
}
|
|
53010
|
+
if (sizes.width && sizes.height && opts.aspect_ratio !== undefined &&
|
|
53011
|
+
ratiosDiffer(sizes.width.valuePx / sizes.height.valuePx, opts.aspect_ratio)) {
|
|
53012
|
+
stop$1('Contradictory width, height and aspect-ratio values');
|
|
53013
|
+
}
|
|
53014
|
+
return sizes;
|
|
53015
|
+
}
|
|
51949
53016
|
|
|
51950
|
-
|
|
53017
|
+
// Steps 3 and 4 as a pure function of the content extent, so that fit= can
|
|
53018
|
+
// ask what scale an extent would give the frame.
|
|
53019
|
+
function resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, opts) {
|
|
53020
|
+
var bbox = contentBbox.slice();
|
|
53021
|
+
var offsetArg = opts.offset || opts.offsets;
|
|
53022
|
+
var width = frame.width;
|
|
53023
|
+
var units = frame.units || 'px';
|
|
53024
|
+
var valid, effectiveAspect;
|
|
53025
|
+
|
|
53026
|
+
// 3. Padding
|
|
51951
53027
|
if (offsetArg) {
|
|
51952
53028
|
applyPercentageOffsets(bbox, offsetArg);
|
|
51953
53029
|
// Pass a page height only when the shape is fixed. A derived height
|
|
@@ -51956,65 +53032,44 @@ ${svg}
|
|
|
51956
53032
|
applyPixelOffsets(bbox, frame.width,
|
|
51957
53033
|
fixedAspect ? frame.width / fixedAspect : null, offsetArg);
|
|
51958
53034
|
}
|
|
51959
|
-
|
|
53035
|
+
valid = isValidBbox(bbox);
|
|
51960
53036
|
if (fixedAspect) {
|
|
51961
53037
|
fillOutBbox(bbox, fixedAspect, 1);
|
|
51962
53038
|
}
|
|
51963
53039
|
|
|
51964
|
-
//
|
|
51965
|
-
if (
|
|
51966
|
-
|
|
51967
|
-
|
|
51968
|
-
|
|
51969
|
-
if (opts.height !== undefined) {
|
|
51970
|
-
heightSize = parseFrameSize(opts.height);
|
|
51971
|
-
requirePositiveSize(heightSize.valuePx, 'height', opts.height);
|
|
51972
|
-
}
|
|
51973
|
-
if (widthSize && heightSize) {
|
|
51974
|
-
effectiveAspect = widthSize.valuePx / heightSize.valuePx;
|
|
51975
|
-
if (opts.aspect_ratio !== undefined &&
|
|
51976
|
-
ratiosDiffer(effectiveAspect, opts.aspect_ratio)) {
|
|
51977
|
-
stop$1('Contradictory width, height and aspect-ratio values');
|
|
51978
|
-
}
|
|
51979
|
-
width = widthSize.valuePx;
|
|
51980
|
-
units = widthSize.units;
|
|
51981
|
-
fixedAspect = effectiveAspect;
|
|
53040
|
+
// 4. Nominal size
|
|
53041
|
+
if (sizes.width && sizes.height) {
|
|
53042
|
+
width = sizes.width.valuePx;
|
|
53043
|
+
units = sizes.width.units;
|
|
53044
|
+
fixedAspect = sizes.width.valuePx / sizes.height.valuePx;
|
|
51982
53045
|
fillOutBbox(bbox, fixedAspect, 1);
|
|
51983
|
-
} else if (
|
|
51984
|
-
width =
|
|
51985
|
-
units =
|
|
51986
|
-
} else if (
|
|
53046
|
+
} else if (sizes.width) {
|
|
53047
|
+
width = sizes.width.valuePx;
|
|
53048
|
+
units = sizes.width.units;
|
|
53049
|
+
} else if (sizes.height) {
|
|
51987
53050
|
effectiveAspect = fixedAspect || getBboxAspect(bbox);
|
|
51988
|
-
width =
|
|
51989
|
-
units =
|
|
53051
|
+
width = sizes.height.valuePx * effectiveAspect;
|
|
53052
|
+
units = sizes.height.units;
|
|
51990
53053
|
} else if (opts.fix_scale) {
|
|
51991
53054
|
// Hold ground units per output pixel, so the page grows and shrinks with
|
|
51992
53055
|
// the extent instead of the scale changing to fit the extent on it.
|
|
51993
53056
|
width = frame.width * getBboxWidth(bbox) / getBboxWidth(frame.bbox);
|
|
51994
|
-
requirePositiveSize(width, 'fix-scale', width);
|
|
51995
53057
|
}
|
|
51996
53058
|
|
|
51997
|
-
requireValidBbox(bbox);
|
|
51998
53059
|
effectiveAspect = fixedAspect || getBboxAspect(bbox);
|
|
51999
|
-
|
|
52000
|
-
|
|
52001
|
-
|
|
52002
|
-
|
|
52003
|
-
|
|
52004
|
-
|
|
52005
|
-
|
|
52006
|
-
|
|
52007
|
-
rec.frame_aspect_ratio = fixedAspect;
|
|
52008
|
-
} else {
|
|
52009
|
-
delete rec.frame_aspect_ratio;
|
|
52010
|
-
}
|
|
52011
|
-
lyr.data.markChanged({operation: OPERATION$1});
|
|
52012
|
-
rebuildFrameLayerGeometry(lyr, dataset, new Bounds(bbox));
|
|
52013
|
-
markLayerChanged(lyr, {operation: OPERATION$1, unit: 'shapes'});
|
|
53060
|
+
return {
|
|
53061
|
+
bbox: bbox,
|
|
53062
|
+
width: width,
|
|
53063
|
+
height: Math.round(width / effectiveAspect),
|
|
53064
|
+
units: units,
|
|
53065
|
+
fixedAspect: fixedAspect,
|
|
53066
|
+
valid: valid && isValidBbox(bbox)
|
|
53067
|
+
};
|
|
52014
53068
|
}
|
|
52015
53069
|
|
|
52016
53070
|
function hasUpdateOptions(opts) {
|
|
52017
53071
|
return opts.bbox !== undefined ||
|
|
53072
|
+
opts.fit !== undefined ||
|
|
52018
53073
|
opts.width !== undefined ||
|
|
52019
53074
|
opts.height !== undefined ||
|
|
52020
53075
|
opts.aspect_ratio !== undefined ||
|
|
@@ -52037,12 +53092,10 @@ ${svg}
|
|
|
52037
53092
|
return (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
|
|
52038
53093
|
}
|
|
52039
53094
|
|
|
52040
|
-
function
|
|
52041
|
-
|
|
52042
|
-
|
|
52043
|
-
|
|
52044
|
-
stop$1('Frame has a collapsed bbox');
|
|
52045
|
-
}
|
|
53095
|
+
function isValidBbox(bbox) {
|
|
53096
|
+
return bbox.every(utils.isFiniteNumber) &&
|
|
53097
|
+
bbox[2] - bbox[0] > 0 &&
|
|
53098
|
+
bbox[3] - bbox[1] > 0;
|
|
52046
53099
|
}
|
|
52047
53100
|
|
|
52048
53101
|
function requirePositiveSize(value, name, arg) {
|
|
@@ -52272,168 +53325,6 @@ ${svg}
|
|
|
52272
53325
|
stop$1('Unable to import coordinates');
|
|
52273
53326
|
}
|
|
52274
53327
|
|
|
52275
|
-
// Apply rotation, scale and/or shift to some or all of the features in a dataset
|
|
52276
|
-
//
|
|
52277
|
-
cmd.affine = function(targetLayers, dataset, opts) {
|
|
52278
|
-
// Need to separate the targeted shapes from any other shapes that share
|
|
52279
|
-
// the same topology. So we duplicate any arcs that are shared by the targeted
|
|
52280
|
-
// shapes and their topological neighbors and remap arc references in the
|
|
52281
|
-
// neighbors to point to the copies.
|
|
52282
|
-
// TODO: explore alternative: if some arcs are shared between transformed and
|
|
52283
|
-
// non-transformed shapes, first remove topology, then tranform, then rebuild topology
|
|
52284
|
-
//
|
|
52285
|
-
var rotateArg = opts.rotate || 0;
|
|
52286
|
-
var scaleArg = opts.scale || 1;
|
|
52287
|
-
var shiftArg = opts.shift ? convertIntervalPair(opts.shift, getDatasetCRS(dataset)) : [0, 0];
|
|
52288
|
-
var arcs = dataset.arcs;
|
|
52289
|
-
var targetShapes = [];
|
|
52290
|
-
var otherShapes = [];
|
|
52291
|
-
var targetPoints = [];
|
|
52292
|
-
var targetPointLayers = [];
|
|
52293
|
-
var targetFlags, otherFlags, transform;
|
|
52294
|
-
dataset.layers.filter(layerHasGeometry$1).forEach(function(lyr) {
|
|
52295
|
-
var hits = [],
|
|
52296
|
-
misses = [],
|
|
52297
|
-
test;
|
|
52298
|
-
if (targetLayers.indexOf(lyr) == -1) {
|
|
52299
|
-
misses = lyr.shapes;
|
|
52300
|
-
} else if (opts.where) {
|
|
52301
|
-
test = compileFeatureExpression(opts.where, lyr, dataset.arcs);
|
|
52302
|
-
lyr.shapes.forEach(function(shp, i) {
|
|
52303
|
-
(test(i) ? hits : misses).push(shp);
|
|
52304
|
-
});
|
|
52305
|
-
} else {
|
|
52306
|
-
hits = lyr.shapes;
|
|
52307
|
-
}
|
|
52308
|
-
if (lyr.geometry_type == 'point') {
|
|
52309
|
-
targetPoints = targetPoints.concat(hits);
|
|
52310
|
-
if (hits.length > 0) targetPointLayers.push(lyr);
|
|
52311
|
-
} else {
|
|
52312
|
-
targetShapes = targetShapes.concat(hits);
|
|
52313
|
-
otherShapes = otherShapes.concat(misses);
|
|
52314
|
-
}
|
|
52315
|
-
});
|
|
52316
|
-
var anchorArg = getAffineAnchor({arcs: dataset.arcs, layers: [{
|
|
52317
|
-
geometry_type: 'point', shapes: targetPoints}, {geometry_type: 'polyline',
|
|
52318
|
-
shapes: targetShapes}]}, opts);
|
|
52319
|
-
transform = getAffineTransform(rotateArg, scaleArg, shiftArg, anchorArg);
|
|
52320
|
-
if (opts.fit_bbox) {
|
|
52321
|
-
transform = getFitBoxTransform(opts.fit_bbox, targetPoints, targetShapes, arcs);
|
|
52322
|
-
}
|
|
52323
|
-
if (targetShapes.length > 0) {
|
|
52324
|
-
targetFlags = new Uint8Array(arcs.size());
|
|
52325
|
-
otherFlags = new Uint8Array(arcs.size());
|
|
52326
|
-
countArcsInShapes(targetShapes, targetFlags);
|
|
52327
|
-
if (otherShapes.length > 0) {
|
|
52328
|
-
countArcsInShapes(otherShapes, otherFlags);
|
|
52329
|
-
applyArrayMask(otherFlags, targetFlags);
|
|
52330
|
-
noteDatasetWillChange(dataset, {operation: 'affine', unit: 'arcs'});
|
|
52331
|
-
dataset.arcs = duplicateSelectedArcs(otherShapes, arcs, otherFlags);
|
|
52332
|
-
markDatasetChanged(dataset, {operation: 'affine', unit: 'arcs'});
|
|
52333
|
-
}
|
|
52334
|
-
dataset.arcs.transformPoints(function(x, y, arcId) {
|
|
52335
|
-
if (arcId < targetFlags.length && targetFlags[arcId] > 0) {
|
|
52336
|
-
return transform(x, y);
|
|
52337
|
-
}
|
|
52338
|
-
});
|
|
52339
|
-
}
|
|
52340
|
-
targetPointLayers.forEach(function(lyr) {
|
|
52341
|
-
noteLayerWillChange(lyr, {operation: 'affine', unit: 'shapes'});
|
|
52342
|
-
});
|
|
52343
|
-
forEachPoint(targetPoints, function(p) {
|
|
52344
|
-
var p2 = transform(p[0], p[1]);
|
|
52345
|
-
p[0] = p2[0];
|
|
52346
|
-
p[1] = p2[1];
|
|
52347
|
-
});
|
|
52348
|
-
targetPointLayers.forEach(function(lyr) {
|
|
52349
|
-
markLayerChanged(lyr, {operation: 'affine', unit: 'shapes'});
|
|
52350
|
-
});
|
|
52351
|
-
};
|
|
52352
|
-
|
|
52353
|
-
function getAffineAnchor(dataset, opts) {
|
|
52354
|
-
var anchor, bounds;
|
|
52355
|
-
if (opts.anchor) {
|
|
52356
|
-
anchor = opts.anchor;
|
|
52357
|
-
} else {
|
|
52358
|
-
// get bounds of selected shapes to calculate center of rotation/scale
|
|
52359
|
-
bounds = getDatasetBounds(dataset);
|
|
52360
|
-
anchor = [bounds.centerX(), bounds.centerY()];
|
|
52361
|
-
}
|
|
52362
|
-
return anchor;
|
|
52363
|
-
}
|
|
52364
|
-
|
|
52365
|
-
// TODO: handle problems with unprojected datasets
|
|
52366
|
-
// option 1: don't allow affine transformation of unprojected data
|
|
52367
|
-
// option 2: error if transformed data exceeds valid coordinate range
|
|
52368
|
-
// source: http://mathworld.wolfram.com/AffineTransformation.html
|
|
52369
|
-
function getAffineTransform(rotation, scale, shift, anchor) {
|
|
52370
|
-
var angle = rotation * Math.PI / 180;
|
|
52371
|
-
var a = scale * Math.cos(angle);
|
|
52372
|
-
var b = -scale * Math.sin(angle);
|
|
52373
|
-
return function(x, y) {
|
|
52374
|
-
var x2 = a * (x - anchor[0]) - b * (y - anchor[1]) + shift[0] + anchor[0];
|
|
52375
|
-
var y2 = b * (x - anchor[0]) + a * (y - anchor[1]) + shift[1] + anchor[1];
|
|
52376
|
-
return [x2, y2];
|
|
52377
|
-
};
|
|
52378
|
-
}
|
|
52379
|
-
|
|
52380
|
-
function getFitBoxTransform(bbox, points, shapes, arcs) {
|
|
52381
|
-
var dataset = {
|
|
52382
|
-
arcs: arcs,
|
|
52383
|
-
layers: []
|
|
52384
|
-
};
|
|
52385
|
-
if (points && points.length) {
|
|
52386
|
-
dataset.layers.push({
|
|
52387
|
-
geometry_type: 'point',
|
|
52388
|
-
shapes: points
|
|
52389
|
-
});
|
|
52390
|
-
}
|
|
52391
|
-
if (shapes && shapes.length) {
|
|
52392
|
-
dataset.layers.push({
|
|
52393
|
-
geometry_type: 'polyline',
|
|
52394
|
-
shapes: shapes
|
|
52395
|
-
});
|
|
52396
|
-
}
|
|
52397
|
-
var frame = calcFrameData(dataset, {fit_bbox: bbox});
|
|
52398
|
-
var fromBounds = new Bounds(frame.bbox);
|
|
52399
|
-
var toBounds = new Bounds(frame.bbox2);
|
|
52400
|
-
var fwd = fromBounds.getTransform(toBounds, false);
|
|
52401
|
-
return function(x, y) {
|
|
52402
|
-
return fwd.transform(x, y);
|
|
52403
|
-
};
|
|
52404
|
-
}
|
|
52405
|
-
|
|
52406
|
-
function applyArrayMask(destArr, maskArr) {
|
|
52407
|
-
for (var i=0, n=destArr.length; i<n; i++) {
|
|
52408
|
-
if (maskArr[i] === 0) destArr[i] = 0;
|
|
52409
|
-
}
|
|
52410
|
-
}
|
|
52411
|
-
|
|
52412
|
-
function duplicateSelectedArcs(shapes, arcs, flags) {
|
|
52413
|
-
var arcCount = 0;
|
|
52414
|
-
var vertexCount = 0;
|
|
52415
|
-
var data = arcs.getVertexData();
|
|
52416
|
-
var xx = [], yy = [], nn = [], map = [], n;
|
|
52417
|
-
for (var i=0, len=flags.length; i<len; i++) {
|
|
52418
|
-
if (flags[i] > 0) {
|
|
52419
|
-
map[i] = arcs.size() + arcCount;
|
|
52420
|
-
n = data.nn[i];
|
|
52421
|
-
utils.copyElements(data.xx, data.ii[i], xx, vertexCount, n);
|
|
52422
|
-
utils.copyElements(data.yy, data.ii[i], yy, vertexCount, n);
|
|
52423
|
-
nn.push(n);
|
|
52424
|
-
vertexCount += n;
|
|
52425
|
-
arcCount++;
|
|
52426
|
-
}
|
|
52427
|
-
}
|
|
52428
|
-
forEachArcId(shapes, function(id) {
|
|
52429
|
-
var absId = absArcId(id);
|
|
52430
|
-
if (flags[absId] > 0) {
|
|
52431
|
-
return id < 0 ? ~map[absId] : map[absId];
|
|
52432
|
-
}
|
|
52433
|
-
});
|
|
52434
|
-
return mergeArcs([arcs, new ArcCollection(nn, xx, yy)]);
|
|
52435
|
-
}
|
|
52436
|
-
|
|
52437
53328
|
const epsilon = 1.1102230246251565e-16;
|
|
52438
53329
|
const splitter = 134217729;
|
|
52439
53330
|
const resulterrbound = (3 + 8 * epsilon) * epsilon;
|
|
@@ -75976,7 +76867,7 @@ ${svg}
|
|
|
75976
76867
|
|
|
75977
76868
|
function createMeridianPart(x, ymin, ymax) {
|
|
75978
76869
|
var coords = densifyPathByInterval([[x, ymin], [x, ymax]], precision);
|
|
75979
|
-
meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord
|
|
76870
|
+
meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord(x)}));
|
|
75980
76871
|
}
|
|
75981
76872
|
|
|
75982
76873
|
function createParallel(y) {
|
|
@@ -76010,7 +76901,7 @@ ${svg}
|
|
|
76010
76901
|
}
|
|
76011
76902
|
|
|
76012
76903
|
// remove tiny offsets
|
|
76013
|
-
function roundCoord
|
|
76904
|
+
function roundCoord(x) {
|
|
76014
76905
|
return +x.toFixed(3) || 0;
|
|
76015
76906
|
}
|
|
76016
76907
|
|
|
@@ -79050,160 +79941,6 @@ ${svg}
|
|
|
79050
79941
|
};
|
|
79051
79942
|
}
|
|
79052
79943
|
|
|
79053
|
-
var roundCoord = getRoundingFunction(0.01);
|
|
79054
|
-
|
|
79055
|
-
function getSymbolFillColor(d) {
|
|
79056
|
-
return d.fill || 'magenta';
|
|
79057
|
-
}
|
|
79058
|
-
|
|
79059
|
-
function getSymbolStrokeColor(d) {
|
|
79060
|
-
return d.stroke || d.fill || 'magenta';
|
|
79061
|
-
}
|
|
79062
|
-
|
|
79063
|
-
function applySymbolStyles(sym, d) {
|
|
79064
|
-
if (sym.type == 'polyline') {
|
|
79065
|
-
sym.stroke = getSymbolStrokeColor(d);
|
|
79066
|
-
} else {
|
|
79067
|
-
sym.fill = getSymbolFillColor(d);
|
|
79068
|
-
}
|
|
79069
|
-
if (d.opacity) {
|
|
79070
|
-
sym.opacity = d.opacity;
|
|
79071
|
-
}
|
|
79072
|
-
return sym;
|
|
79073
|
-
}
|
|
79074
|
-
|
|
79075
|
-
function getSymbolRadius(d) {
|
|
79076
|
-
if (d.radius === 0 || d.length === 0 || d.r === 0) return 0;
|
|
79077
|
-
return d.radius || d.length || d.r || 5; // use a default value
|
|
79078
|
-
}
|
|
79079
|
-
|
|
79080
|
-
// Converts the value of a list-valued option into an array of items. The value
|
|
79081
|
-
// arrives either as a whole string (from a direct call, rather than from the
|
|
79082
|
-
// -symbols option accessor) or as an array of items already resolved one by
|
|
79083
|
-
// one. An item that holds an array of its own, as an expression like
|
|
79084
|
-
// [R, R * 0.5] does, is flattened into the list. Items themselves are left
|
|
79085
|
-
// unsplit, because a single item can contain a comma, as rgba() colors do.
|
|
79086
|
-
function toItemList(val) {
|
|
79087
|
-
var items = utils.isString(val) ? splitListItems(val) : [].concat(val);
|
|
79088
|
-
var list = [];
|
|
79089
|
-
for (var i=0; i<items.length; i++) {
|
|
79090
|
-
if (Array.isArray(items[i])) {
|
|
79091
|
-
list = list.concat(items[i]);
|
|
79092
|
-
} else {
|
|
79093
|
-
list.push(items[i]);
|
|
79094
|
-
}
|
|
79095
|
-
}
|
|
79096
|
-
return list;
|
|
79097
|
-
}
|
|
79098
|
-
|
|
79099
|
-
// Like toItemList(), for options that take numbers. An item that is a string
|
|
79100
|
-
// containing commas is split, so that a data field holding a value like "2,4"
|
|
79101
|
-
// works as a list. Values that aren't numbers become NaN, for the caller to
|
|
79102
|
-
// report or ignore.
|
|
79103
|
-
function toNumberList(val) {
|
|
79104
|
-
var list = [];
|
|
79105
|
-
toItemList(val).forEach(function(item) {
|
|
79106
|
-
if (utils.isString(item) && item.indexOf(',') > -1) {
|
|
79107
|
-
list = list.concat(parseNumberList(item));
|
|
79108
|
-
} else {
|
|
79109
|
-
list.push(Number(item));
|
|
79110
|
-
}
|
|
79111
|
-
});
|
|
79112
|
-
return list;
|
|
79113
|
-
}
|
|
79114
|
-
|
|
79115
|
-
// Returns the radius of the smallest circle centered on a symbol's point that
|
|
79116
|
-
// covers the symbol, or null if the symbol contains a part whose reach can't be
|
|
79117
|
-
// measured.
|
|
79118
|
-
//
|
|
79119
|
-
// Only a symbol drawn around its point can be described this way: a circle, or
|
|
79120
|
-
// a group of the circles and polygons that make up ring, pie and donut symbols.
|
|
79121
|
-
// An arrow is drawn from its point outward, so the circle around its point that
|
|
79122
|
-
// covers it says nothing useful about the space it occupies -- arrows are plain
|
|
79123
|
-
// polygon symbols, which reach this function only as group parts, which they
|
|
79124
|
-
// never are.
|
|
79125
|
-
function getSymbolBoundingRadius(sym) {
|
|
79126
|
-
var max = 0;
|
|
79127
|
-
var parts, r, i;
|
|
79128
|
-
if (!sym) return null;
|
|
79129
|
-
if (sym.type == 'circle') {
|
|
79130
|
-
// Half of a stroke lies outside the circle it follows, which is how a ring
|
|
79131
|
-
// symbol paints its bands.
|
|
79132
|
-
return getPositiveNumber(sym.r) + getStrokeOutset(sym);
|
|
79133
|
-
}
|
|
79134
|
-
if (sym.type == 'polygon') {
|
|
79135
|
-
forEachSymbolCoord(sym.coordinates || [], function(p) {
|
|
79136
|
-
var dist = Math.sqrt(p[0] * p[0] + p[1] * p[1]);
|
|
79137
|
-
if (dist > max) max = dist;
|
|
79138
|
-
});
|
|
79139
|
-
return max + getStrokeOutset(sym);
|
|
79140
|
-
}
|
|
79141
|
-
if (sym.type == 'group') {
|
|
79142
|
-
parts = sym.parts || [];
|
|
79143
|
-
for (i=0; i<parts.length; i++) {
|
|
79144
|
-
r = getSymbolBoundingRadius(parts[i]);
|
|
79145
|
-
if (r === null) return null;
|
|
79146
|
-
if (r > max) max = r;
|
|
79147
|
-
}
|
|
79148
|
-
return max;
|
|
79149
|
-
}
|
|
79150
|
-
return null;
|
|
79151
|
-
}
|
|
79152
|
-
|
|
79153
|
-
// A stroke straddles the path it is drawn on, so half of its width extends
|
|
79154
|
-
// beyond the shape.
|
|
79155
|
-
function getStrokeOutset(sym) {
|
|
79156
|
-
var width = sym['stroke-width'];
|
|
79157
|
-
if (!sym.stroke || sym.stroke == 'none') return 0;
|
|
79158
|
-
// an SVG stroke is one pixel wide unless stroke-width says otherwise
|
|
79159
|
-
if (width === undefined || width === null || width === '') return 0.5;
|
|
79160
|
-
return getPositiveNumber(width) / 2;
|
|
79161
|
-
}
|
|
79162
|
-
|
|
79163
|
-
function getPositiveNumber(val) {
|
|
79164
|
-
var num = +val;
|
|
79165
|
-
return num > 0 ? num : 0;
|
|
79166
|
-
}
|
|
79167
|
-
|
|
79168
|
-
function forEachSymbolCoord(coords, cb) {
|
|
79169
|
-
var isPoint = coords && utils.isNumber(coords[0]);
|
|
79170
|
-
var isNested = !isPoint && coords && Array.isArray(coords[0]);
|
|
79171
|
-
if (isPoint) return cb(coords);
|
|
79172
|
-
for (var i=0; i<coords.length; i++) {
|
|
79173
|
-
if (isNested) forEachSymbolCoord(coords[i], cb);
|
|
79174
|
-
}
|
|
79175
|
-
}
|
|
79176
|
-
|
|
79177
|
-
function flipY(coords) {
|
|
79178
|
-
forEachSymbolCoord(coords, function(p) {
|
|
79179
|
-
p[1] = -p[1];
|
|
79180
|
-
});
|
|
79181
|
-
}
|
|
79182
|
-
|
|
79183
|
-
function scaleAndShiftCoords(coords, scale, shift) {
|
|
79184
|
-
forEachSymbolCoord(coords, function(xy) {
|
|
79185
|
-
xy[0] = xy[0] * scale + shift[0];
|
|
79186
|
-
xy[1] = xy[1] * scale + shift[1];
|
|
79187
|
-
});
|
|
79188
|
-
}
|
|
79189
|
-
|
|
79190
|
-
function roundCoordsForSVG(coords) {
|
|
79191
|
-
forEachSymbolCoord(coords, function(p) {
|
|
79192
|
-
p[0] = roundCoord(p[0]);
|
|
79193
|
-
p[1] = roundCoord(p[1]);
|
|
79194
|
-
});
|
|
79195
|
-
}
|
|
79196
|
-
|
|
79197
|
-
function rotateCoords(coords, rotation) {
|
|
79198
|
-
if (!rotation) return;
|
|
79199
|
-
var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
|
|
79200
|
-
forEachSymbolCoord(coords, function(p) {
|
|
79201
|
-
var p2 = f(p[0], p[1]);
|
|
79202
|
-
p[0] = p2[0];
|
|
79203
|
-
p[1] = p2[1];
|
|
79204
|
-
});
|
|
79205
|
-
}
|
|
79206
|
-
|
|
79207
79944
|
// Collision reduction for circular symbols, in the spirit of d3-force's
|
|
79208
79945
|
// forceCollide. Overlapping symbols are pushed apart, but each one is kept
|
|
79209
79946
|
// within a fixed distance of its true position.
|
|
@@ -83097,7 +83834,8 @@ ${svg}
|
|
|
83097
83834
|
job.catalog.addDataset(cmd.addLayer(targetDataset, opts));
|
|
83098
83835
|
|
|
83099
83836
|
} else if (name == 'update-frame') {
|
|
83100
|
-
cmd.updateFrame(targetLayers, targetDataset, opts
|
|
83837
|
+
cmd.updateFrame(targetLayers, targetDataset, opts,
|
|
83838
|
+
opts.fit ? expandCommandTargets(job.catalog.findCommandTargets(opts.fit)) : null);
|
|
83101
83839
|
|
|
83102
83840
|
} else if (name == 'update-label') {
|
|
83103
83841
|
cmd.updateLabel(targetLayers, targetDataset, opts);
|
|
@@ -83663,7 +84401,7 @@ ${svg}
|
|
|
83663
84401
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
83664
84402
|
}
|
|
83665
84403
|
|
|
83666
|
-
var version = "0.7.
|
|
84404
|
+
var version = "0.7.69";
|
|
83667
84405
|
|
|
83668
84406
|
// Parse command line args into commands and run them
|
|
83669
84407
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|