mapshaper 0.7.15 → 0.7.17

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.
@@ -1476,29 +1476,27 @@
1476
1476
  }
1477
1477
 
1478
1478
  function SessionSnapshots(gui) {
1479
- var _menuOpen = false;
1480
- var _menuTimeout;
1481
- var btn, menu;
1482
-
1483
1479
  gui.sessionSnapshots = {
1480
+ enabled: false,
1484
1481
  saveSnapshot: function() {
1485
1482
  return saveSnapshot(gui);
1486
1483
  },
1487
1484
  restoreLatestSnapshot: function() {
1488
1485
  return restoreLatestSnapshot(gui);
1486
+ },
1487
+ renderSnapshotList: function(el) {
1488
+ return renderSnapshotList(el, gui);
1489
1489
  }
1490
1490
  };
1491
1491
 
1492
1492
  init();
1493
1493
 
1494
1494
  async function init() {
1495
- btn = gui.buttons.addButton('#ribbon-icon').addClass('menu-btn save-btn');
1496
1495
  var enabled = await isStorageEnabled();
1497
1496
  if (!enabled) {
1498
- btn.remove();
1499
1497
  return;
1500
1498
  }
1501
- menu = El('div').addClass('nav-sub-menu save-menu').appendTo(btn.node());
1499
+ gui.sessionSnapshots.enabled = true;
1502
1500
  startLifecycle();
1503
1501
  await initialCleanup();
1504
1502
 
@@ -1514,98 +1512,6 @@
1514
1512
  announceLeaving();
1515
1513
  attemptOwnDataDeletion();
1516
1514
  });
1517
-
1518
- btn.on('mouseenter', function() {
1519
- btn.addClass('hover');
1520
- clearTimeout(_menuTimeout); // prevent timed closing
1521
- if (!_menuOpen) {
1522
- openMenu();
1523
- }
1524
- });
1525
-
1526
- btn.on('mouseleave', function() {
1527
- if (!_menuOpen) {
1528
- btn.removeClass('hover');
1529
- } else {
1530
- closeMenu(200);
1531
- }
1532
- });
1533
- }
1534
-
1535
- async function renderMenu() {
1536
- var snapshots = await fetchSnapshotList();
1537
-
1538
- menu.empty();
1539
-
1540
- addMenuLink({
1541
- slug: 'stash',
1542
- // label: 'save data snapshot',
1543
- label: 'create a snapshot',
1544
- action: saveSnapshot
1545
- });
1546
-
1547
-
1548
-
1549
- // var available = await getAvailableStorage();
1550
- // if (available) {
1551
- // El('div').addClass('save-menu-entry').text(available + ' available').appendTo(menu);
1552
- // }
1553
-
1554
- // if (snapshots.length > 0) {
1555
- // El('div').addClass('save-menu-entry').text('snapshots').appendTo(menu);
1556
- // }
1557
-
1558
- snapshots.forEach(function(item, i) {
1559
- var line = El('div').appendTo(menu).addClass('save-menu-item');
1560
- El('span').appendTo(line).html(`<span class="save-item-label">#${item.number}</span> `);
1561
- // show snapshot size
1562
- El('span').appendTo(line).html(` <span class="save-item-size">${item.display_size}</span>`);
1563
- El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1564
- await restoreSnapshotById(item.id, gui);
1565
- closeMenu(100);
1566
- }).text('restore');
1567
- El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1568
- var obj = await idb$2.get(item.id);
1569
- await internal.compressSnapshotForExport(obj);
1570
- var buf = internal.pack(obj);
1571
- var fileName = `snapshot-${String(item.number).padStart(2, '0')}.msx`;
1572
- // choose output filename and directory every time, if supported
1573
- saveBlobToLocalFile2(fileName, new Blob([buf]));
1574
- }).text('export');
1575
- El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1576
- await removeSnapshotById(item.id);
1577
- closeMenu(300);
1578
- renderMenu();
1579
- }).text('remove');
1580
- });
1581
- }
1582
-
1583
- function addMenuLink(item) {
1584
- var line = El('div').appendTo(menu);
1585
- var link = El('div').addClass('save-menu-link save-menu-entry').attr('data-name', item.slug).text(item.label).appendTo(line);
1586
- link.on('click', async function(e) {
1587
- await item.action(gui);
1588
- e.stopPropagation();
1589
- });
1590
- }
1591
-
1592
- function openMenu() {
1593
- clearTimeout(_menuTimeout);
1594
- if (!_menuOpen) {
1595
- btn.addClass('open');
1596
- _menuOpen = true;
1597
- renderMenu();
1598
- }
1599
- }
1600
-
1601
- function closeMenu(delay) {
1602
- if (!_menuOpen) return;
1603
- clearTimeout(_menuTimeout);
1604
- _menuTimeout = setTimeout(function() {
1605
- _menuOpen = false;
1606
- btn.removeClass('open');
1607
- btn.removeClass('hover');
1608
- }, delay || 0);
1609
1515
  }
1610
1516
 
1611
1517
  async function saveSnapshot(gui) {
@@ -1631,10 +1537,37 @@
1631
1537
  await idb$2.set(entry.id, obj);
1632
1538
  ownSnapshotIds.add(entry.id);
1633
1539
  await addToIndex(entry);
1634
- renderMenu();
1635
1540
  }
1636
1541
  }
1637
1542
 
1543
+ async function renderSnapshotList(el, gui) {
1544
+ var snapshots = await fetchSnapshotList();
1545
+ el.empty();
1546
+ snapshots.forEach(function(item) {
1547
+ var line = El('div').appendTo(el).addClass('save-menu-item');
1548
+ El('span').appendTo(line).html(`<span class="save-item-label">#${item.number}</span> `);
1549
+ El('span').appendTo(line).html(` <span class="save-item-size">${item.display_size}</span>`);
1550
+ El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1551
+ e.stopPropagation();
1552
+ await restoreSnapshotById(item.id, gui);
1553
+ }).text('restore');
1554
+ El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1555
+ var obj;
1556
+ e.stopPropagation();
1557
+ obj = await idb$2.get(item.id);
1558
+ await internal.compressSnapshotForExport(obj);
1559
+ var buf = internal.pack(obj);
1560
+ var fileName = `snapshot-${String(item.number).padStart(2, '0')}.msx`;
1561
+ saveBlobToLocalFile2(fileName, new Blob([buf]));
1562
+ }).text('export');
1563
+ El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1564
+ e.stopPropagation();
1565
+ await removeSnapshotById(item.id);
1566
+ renderSnapshotList(el, gui);
1567
+ }).text('remove');
1568
+ });
1569
+ }
1570
+
1638
1571
  async function fetchSnapshotList() {
1639
1572
  await pruneIndexAgainstKeys();
1640
1573
  var index = await fetchIndex();
@@ -1708,6 +1641,7 @@
1708
1641
  }
1709
1642
 
1710
1643
  function importDatasets(datasets, gui) {
1644
+ regenerateRasterPreviews(datasets);
1711
1645
  gui.model.addDatasets(datasets);
1712
1646
  var target = findTargetLayer(datasets);
1713
1647
  delete target.layers[0].active; // kludge, active flag only used in snapshots now
@@ -1715,6 +1649,17 @@
1715
1649
  gui.model.updated({select: true, arc_count: true}); // arc_count to refresh display shapes
1716
1650
  }
1717
1651
 
1652
+ function regenerateRasterPreviews(datasets) {
1653
+ datasets.forEach(function(dataset) {
1654
+ dataset.layers.forEach(function(lyr) {
1655
+ var raster = lyr.raster;
1656
+ if (!raster || !raster.grid || raster.view && raster.view.preview) return;
1657
+ raster.view = raster.view || {};
1658
+ raster.view.preview = internal.createRasterPreview(raster);
1659
+ });
1660
+ });
1661
+ }
1662
+
1718
1663
  async function captureSnapshot(gui) {
1719
1664
  var lyr = gui.model.getActiveLayer()?.layer;
1720
1665
  if (!lyr) return null; // no data -- no snapshot
@@ -2477,77 +2422,303 @@
2477
2422
  return bytes;
2478
2423
  }
2479
2424
 
2480
- // Fall back to browserify's Buffer polyfill
2481
- var B = typeof Buffer != 'undefined' ? Buffer : require$1('buffer').Buffer;
2425
+ var PAYLOAD_FIELDS = {
2426
+ table: ['records'],
2427
+ 'table-records': ['records'],
2428
+ 'table-fields': ['columns'],
2429
+ 'table-schema': ['fields'],
2430
+ arcs: ['nn', 'xx', 'yy', 'zz', 'zlimit'],
2431
+ 'arcs-simplification': ['zz'],
2432
+ layer: ['shapes', 'raster']
2433
+ };
2482
2434
 
2483
- // We do NOT import from mapshaper-logging here to avoid a circular dependency
2484
- function error$1() {
2485
- throw new Error(Array.prototype.slice.call(arguments).join(' '));
2435
+ async function storeUndoUnits(units, store, entryId, role) {
2436
+ var stored = [];
2437
+ try {
2438
+ for (var i = 0; i < units.length; i++) {
2439
+ stored.push(await storeUndoUnit(units[i], store, entryId, role));
2440
+ }
2441
+ } catch(e) {
2442
+ await store.delMany(getStoredUndoPayloadRefs(stored));
2443
+ throw e;
2444
+ }
2445
+ return stored;
2486
2446
  }
2487
2447
 
2488
- var uniqCount = 0;
2489
- function getUniqueName(prefix) {
2490
- return (prefix || "__id_") + (++uniqCount);
2448
+ async function restoreStoredUndoUnits(units, store) {
2449
+ var hydrated = await hydrateStoredUndoUnits(units, store);
2450
+ internal.restoreCapturedUnits(hydrated);
2491
2451
  }
2492
2452
 
2493
- function isFunction(obj) {
2494
- return typeof obj == 'function';
2453
+ async function hydrateStoredUndoUnits(units, store) {
2454
+ var hydrated = [];
2455
+ for (var i = 0; i < units.length; i++) {
2456
+ hydrated.push(await hydrateStoredUndoUnit(units[i], store));
2457
+ }
2458
+ return hydrated;
2495
2459
  }
2496
2460
 
2497
- function isPromise(arg) {
2498
- return arg ? isFunction(arg.then) : false;
2461
+ function getStoredUndoPayloadRefs(units) {
2462
+ var refs = [];
2463
+ units.forEach(function(unit) {
2464
+ if (unit.payloadRef) refs.push(unit.payloadRef);
2465
+ });
2466
+ return refs;
2499
2467
  }
2500
2468
 
2501
- function isObject(obj) {
2502
- return obj === Object(obj); // via underscore
2469
+ function getUndoRestoreFlags(units, baseFlags) {
2470
+ var flags = Object.assign({}, baseFlags || {}, {undo_restore: true});
2471
+ units.forEach(function(unit) {
2472
+ if (unit.type == 'changed') return;
2473
+ if (unit.type == 'arcs') {
2474
+ flags.arc_count = true;
2475
+ } else if (unit.type == 'arcs-simplification') {
2476
+ flags.simplify = true;
2477
+ } else if (unit.type == 'table' ||
2478
+ unit.type == 'table-records' ||
2479
+ unit.type == 'table-fields' ||
2480
+ unit.type == 'table-order' ||
2481
+ unit.type == 'table-schema') {
2482
+ flags.same_table = false;
2483
+ } else if (unit.type == 'catalog' || unit.type == 'dataset') {
2484
+ flags.select = true;
2485
+ flags.arc_count = true;
2486
+ } else if (unit.type == 'dataset-info') {
2487
+ flags.info = true;
2488
+ } else if (unit.type == 'layer' ||
2489
+ unit.type == 'layer-metadata' ||
2490
+ unit.type == 'layer-order') {
2491
+ flags.select = true;
2492
+ }
2493
+ });
2494
+ return flags;
2503
2495
  }
2504
2496
 
2505
- function clamp(val, min, max) {
2506
- return val < min ? min : (val > max ? max : val);
2497
+ async function storeUndoUnit(unit, store, entryId, role) {
2498
+ var payload = await getUnitPayload(unit);
2499
+ var stored = stripPayload(unit);
2500
+ if (payload) {
2501
+ stored.payloadRef = await store.put(payload, {
2502
+ entryId: entryId,
2503
+ role: role,
2504
+ unitType: unit.type
2505
+ });
2506
+ }
2507
+ return stored;
2507
2508
  }
2508
2509
 
2509
- function isArray(obj) {
2510
- return Array.isArray(obj);
2510
+ async function hydrateStoredUndoUnit(unit, store) {
2511
+ var hydrated = Object.assign({}, unit);
2512
+ var payload;
2513
+ if (unit.payloadRef) {
2514
+ payload = await store.get(unit.payloadRef);
2515
+ if (!payload) {
2516
+ throw new Error('Missing undo payload: ' + unit.payloadRef.key);
2517
+ }
2518
+ payload = unpackPayload(unit, payload);
2519
+ Object.assign(hydrated, payload);
2520
+ }
2521
+ delete hydrated.payloadRef;
2522
+ return hydrated;
2511
2523
  }
2512
2524
 
2513
- // Is obj a valid number or NaN? (test if obj is type number)
2514
- function isNumber(obj) {
2515
- return obj != null && obj.constructor == Number;
2525
+ async function getUnitPayload(unit) {
2526
+ var fields = PAYLOAD_FIELDS[unit.type];
2527
+ var payload, hasPayload;
2528
+ if (!fields) return null;
2529
+ payload = {};
2530
+ fields.forEach(function(field) {
2531
+ if (field in unit) {
2532
+ payload[field] = unit[field];
2533
+ hasPayload = true;
2534
+ }
2535
+ });
2536
+ if (!hasPayload) return null;
2537
+ if (unit.type == 'layer') return packLayerPayload(payload);
2538
+ return unit.type == 'table' ? packTablePayload(payload) : payload;
2516
2539
  }
2517
2540
 
2518
- function isValidNumber(val) {
2519
- return isNumber(val) && !isNaN(val);
2541
+ async function packLayerPayload(payload) {
2542
+ if (payload.raster) {
2543
+ payload = Object.assign({}, payload, {
2544
+ raster: packRasterUndoPayload(payload.raster)
2545
+ });
2546
+ }
2547
+ return payload;
2520
2548
  }
2521
2549
 
2522
- // Similar to isFinite() but does not coerce strings or other types
2523
- function isFiniteNumber(val) {
2524
- return isValidNumber(val) && val !== Infinity && val !== -Infinity;
2550
+ async function packTablePayload(payload) {
2551
+ return {
2552
+ packedRecords: packRecordsAsColumns(payload.records)
2553
+ };
2525
2554
  }
2526
2555
 
2527
- // This uses type conversion
2528
- // export function isFiniteNumber(val) {
2529
- // return val > -Infinity && val < Infinity;
2530
- // }
2531
-
2532
- function isNonNegNumber(val) {
2533
- return isNumber(val) && val >= 0;
2556
+ function unpackPayload(unit, payload) {
2557
+ if (unit.type == 'table' && payload.packedRecords) {
2558
+ return {
2559
+ records: unpackRecordsFromColumns(payload.packedRecords)
2560
+ };
2561
+ }
2562
+ if (unit.type == 'layer' && payload.raster) {
2563
+ return Object.assign({}, payload, {
2564
+ raster: unpackRasterUndoPayload(payload.raster)
2565
+ });
2566
+ }
2567
+ return payload;
2534
2568
  }
2535
2569
 
2536
- function isInteger(obj) {
2537
- return isNumber(obj) && ((obj | 0) === obj);
2570
+ function packRasterUndoPayload(raster) {
2571
+ var copy = Object.assign({}, raster);
2572
+ if (raster.view) {
2573
+ copy.view = Object.assign({}, raster.view);
2574
+ if (raster.view.preview) {
2575
+ copy.view.preview = stripPreviewPixels(raster.view.preview);
2576
+ }
2577
+ }
2578
+ if (raster.preview) {
2579
+ copy.preview = stripPreviewPixels(raster.preview);
2580
+ }
2581
+ return copy;
2538
2582
  }
2539
2583
 
2540
- function isEven(obj) {
2541
- return (obj % 2) === 0;
2584
+ function unpackRasterUndoPayload(raster) {
2585
+ var copy = Object.assign({}, raster);
2586
+ if (raster.view) {
2587
+ copy.view = Object.assign({}, raster.view);
2588
+ if (raster.view.preview && !raster.view.preview.pixels && raster.grid && raster.grid.samples) {
2589
+ copy.view.preview = internal.renderRasterPreview(raster.grid, raster.view.recipe, raster.view.preview.width, raster.view.preview.height);
2590
+ }
2591
+ }
2592
+ return copy;
2542
2593
  }
2543
2594
 
2544
- function isOdd(obj) {
2545
- return (obj % 2) === 1;
2595
+ function stripPreviewPixels(preview) {
2596
+ var copy = Object.assign({}, preview);
2597
+ delete copy.canvas;
2598
+ delete copy.pixels;
2599
+ return copy;
2546
2600
  }
2547
2601
 
2548
- function isString(obj) {
2549
- return obj != null && obj.toString === String.prototype.toString;
2550
- // TODO: replace w/ something better.
2602
+ function packRecordsAsColumns(records) {
2603
+ var fields = getRecordFields(records);
2604
+ return {
2605
+ fields: fields,
2606
+ types: fields.map(function() { return null; }),
2607
+ data: fields.map(function(field) {
2608
+ return records.map(function(rec) {
2609
+ return rec ? rec[field] : undefined;
2610
+ });
2611
+ }),
2612
+ size: records.length
2613
+ };
2614
+ }
2615
+
2616
+ function getRecordFields(records) {
2617
+ var index = {};
2618
+ records.forEach(function(rec) {
2619
+ Object.keys(rec || {}).forEach(function(field) {
2620
+ index[field] = true;
2621
+ });
2622
+ });
2623
+ return Object.keys(index);
2624
+ }
2625
+
2626
+ function unpackRecordsFromColumns(data) {
2627
+ var records = [];
2628
+ for (var i = 0; i < data.size; i++) {
2629
+ records[i] = {};
2630
+ }
2631
+ data.fields.forEach(function(field, j) {
2632
+ var values = data.data[j];
2633
+ values.forEach(function(val, i) {
2634
+ records[i][field] = val;
2635
+ });
2636
+ });
2637
+ return records;
2638
+ }
2639
+
2640
+ function stripPayload(unit) {
2641
+ var fields = PAYLOAD_FIELDS[unit.type];
2642
+ var stripped = Object.assign({}, unit);
2643
+ if (fields) {
2644
+ fields.forEach(function(field) {
2645
+ delete stripped[field];
2646
+ });
2647
+ }
2648
+ return stripped;
2649
+ }
2650
+
2651
+ // Fall back to browserify's Buffer polyfill
2652
+ var B = typeof Buffer != 'undefined' ? Buffer : require$1('buffer').Buffer;
2653
+
2654
+ // We do NOT import from mapshaper-logging here to avoid a circular dependency
2655
+ function error$1() {
2656
+ throw new Error(Array.prototype.slice.call(arguments).join(' '));
2657
+ }
2658
+
2659
+ var uniqCount = 0;
2660
+ function getUniqueName(prefix) {
2661
+ return (prefix || "__id_") + (++uniqCount);
2662
+ }
2663
+
2664
+ function isFunction(obj) {
2665
+ return typeof obj == 'function';
2666
+ }
2667
+
2668
+ function isPromise(arg) {
2669
+ return arg ? isFunction(arg.then) : false;
2670
+ }
2671
+
2672
+ function isObject(obj) {
2673
+ return obj === Object(obj); // via underscore
2674
+ }
2675
+
2676
+ function clamp(val, min, max) {
2677
+ return val < min ? min : (val > max ? max : val);
2678
+ }
2679
+
2680
+ function isArray(obj) {
2681
+ return Array.isArray(obj);
2682
+ }
2683
+
2684
+ // Is obj a valid number or NaN? (test if obj is type number)
2685
+ function isNumber(obj) {
2686
+ return obj != null && obj.constructor == Number;
2687
+ }
2688
+
2689
+ function isValidNumber(val) {
2690
+ return isNumber(val) && !isNaN(val);
2691
+ }
2692
+
2693
+ // Similar to isFinite() but does not coerce strings or other types
2694
+ function isFiniteNumber(val) {
2695
+ return isValidNumber(val) && val !== Infinity && val !== -Infinity;
2696
+ }
2697
+
2698
+ // This uses type conversion
2699
+ // export function isFiniteNumber(val) {
2700
+ // return val > -Infinity && val < Infinity;
2701
+ // }
2702
+
2703
+ function isNonNegNumber(val) {
2704
+ return isNumber(val) && val >= 0;
2705
+ }
2706
+
2707
+ function isInteger(obj) {
2708
+ return isNumber(obj) && ((obj | 0) === obj);
2709
+ }
2710
+
2711
+ function isEven(obj) {
2712
+ return (obj % 2) === 0;
2713
+ }
2714
+
2715
+ function isOdd(obj) {
2716
+ return (obj % 2) === 1;
2717
+ }
2718
+
2719
+ function isString(obj) {
2720
+ return obj != null && obj.toString === String.prototype.toString;
2721
+ // TODO: replace w/ something better.
2551
2722
  }
2552
2723
 
2553
2724
  function isDate(obj) {
@@ -4629,6 +4800,10 @@
4629
4800
  return this;
4630
4801
  };
4631
4802
 
4803
+ function runningInBrowser() {
4804
+ return typeof window !== 'undefined' && typeof window.document !== 'undefined';
4805
+ }
4806
+
4632
4807
  // Lightweight hooks that make in-place mutations observable to GUI undo.
4633
4808
  // Normal CLI runs leave activeTransaction unset, so capture hooks are no-ops.
4634
4809
 
@@ -4833,6 +5008,8 @@
4833
5008
  }
4834
5009
 
4835
5010
  var DEFAULT_MAX_PREVIEW_PIXELS = 4e6;
5011
+ var MAX_EXACT_AVERAGE_PIXELS = 256;
5012
+ var MAX_APPROX_AVERAGE_STEPS = 16;
4836
5013
 
4837
5014
  function getRasterGrid(raster) {
4838
5015
  return raster && (raster.grid || raster);
@@ -4978,6 +5155,13 @@
4978
5155
  });
4979
5156
  }
4980
5157
 
5158
+ function renderRasterExportPreview(raster, bbox, width, height, opts) {
5159
+ var grid = getRasterGrid(raster);
5160
+ var recipe = getRasterViewRecipe(grid, raster.view && raster.view.recipe, opts);
5161
+ var stats = getRasterViewScalingStats(raster, recipe);
5162
+ return renderRasterGridPreview(grid, recipe, width, height, stats, bbox, getRasterExportResamplingMethod(grid, bbox, width, height));
5163
+ }
5164
+
4981
5165
  function getRasterScalingStats(grid, recipe) {
4982
5166
  recipe = getRasterViewRecipe(grid, recipe);
4983
5167
  return getScalingStats(grid.samples, grid.bands, grid.nodata, recipe);
@@ -5002,7 +5186,7 @@
5002
5186
  return stats;
5003
5187
  }
5004
5188
 
5005
- function renderRasterGridPreview(grid, recipe, width, height, statsArg, sourceBbox) {
5189
+ function renderRasterGridPreview(grid, recipe, width, height, statsArg, sourceBbox, resamplingMethod) {
5006
5190
  var samples = grid.samples;
5007
5191
  var bands = grid.bands;
5008
5192
  var noData = grid.nodata;
@@ -5010,23 +5194,29 @@
5010
5194
  var stats = statsArg || getScalingStats(samples, bands, noData, recipe);
5011
5195
  var sourceRange = recipe.scaling == 'none' ? getPixelTypeRange(grid.pixelType) : null;
5012
5196
  var displayRange = getDisplayRange(recipe.scaleRange);
5013
- var src, dest, val, isNoData, j;
5197
+ var src, dest, val, isNoData, j, sample;
5014
5198
  for (var y = 0; y < height; y++) {
5015
5199
  for (var x = 0; x < width; x++) {
5016
- src = getPreviewSourceOffset(grid, sourceBbox, x, y, width, height, bands);
5017
5200
  dest = (y * width + x) * 4;
5018
- isNoData = noData !== null && noData !== undefined && allSamplesAreNoData(samples, src, bands, noData);
5201
+ if (resamplingMethod) {
5202
+ sample = getResampledRasterSample(grid, sourceBbox, x, y, width, height, resamplingMethod);
5203
+ isNoData = !sample.valid;
5204
+ } else {
5205
+ src = getPreviewSourceOffset(grid, sourceBbox, x, y, width, height, bands);
5206
+ sample = null;
5207
+ isNoData = noData !== null && noData !== undefined && allSamplesAreNoData(samples, src, bands, noData);
5208
+ }
5019
5209
  if (bands == 1) {
5020
- val = scaleSample(samples[src], stats && stats[0], sourceRange, displayRange);
5210
+ val = scaleSample(sample ? sample.values[0] : samples[src], stats && stats[0], sourceRange, displayRange);
5021
5211
  pixels[dest] = val;
5022
5212
  pixels[dest + 1] = val;
5023
5213
  pixels[dest + 2] = val;
5024
5214
  pixels[dest + 3] = isNoData ? 0 : 255;
5025
5215
  } else {
5026
5216
  for (j = 0; j < 3; j++) {
5027
- pixels[dest + j] = scaleSample(samples[src + j], stats && stats[j], sourceRange, displayRange);
5217
+ pixels[dest + j] = scaleSample(sample ? sample.values[j] : samples[src + j], stats && stats[j], sourceRange, displayRange);
5028
5218
  }
5029
- pixels[dest + 3] = isNoData ? 0 : bands >= 4 ? scaleSample(samples[src + 3], stats && stats[3], sourceRange, [0, 255]) : 255;
5219
+ pixels[dest + 3] = isNoData ? 0 : bands >= 4 ? scaleSample(sample ? sample.values[3] : samples[src + 3], stats && stats[3], sourceRange, [0, 255]) : 255;
5030
5220
  }
5031
5221
  }
5032
5222
  }
@@ -5040,6 +5230,156 @@
5040
5230
  };
5041
5231
  }
5042
5232
 
5233
+ function getRasterExportResamplingMethod(grid, bbox, width, height) {
5234
+ var srcSize = getRasterSourcePixelSize$1(grid, bbox);
5235
+ return width < srcSize.width || height < srcSize.height ? 'average' : 'bilinear';
5236
+ }
5237
+
5238
+ function getRasterSourcePixelSize$1(grid, bbox) {
5239
+ var rb = grid.bbox;
5240
+ return {
5241
+ width: Math.abs((bbox[2] - bbox[0]) / (rb[2] - rb[0]) * grid.width),
5242
+ height: Math.abs((bbox[3] - bbox[1]) / (rb[3] - rb[1]) * grid.height)
5243
+ };
5244
+ }
5245
+
5246
+ function getResampledRasterSample(grid, bbox, x, y, width, height, method) {
5247
+ return method == 'average' ?
5248
+ getAverageRasterSample(grid, bbox, x, y, width, height) :
5249
+ getBilinearRasterSample(grid, bbox, x, y, width, height);
5250
+ }
5251
+
5252
+ function getAverageRasterSample(grid, bbox, x, y, width, height) {
5253
+ var bounds = getRasterSourcePixelBounds(grid, bbox, x, y, width, height);
5254
+ if (getSourcePixelBoundsArea(bounds) > MAX_EXACT_AVERAGE_PIXELS) {
5255
+ return getApproxAverageRasterSample(grid, bounds);
5256
+ }
5257
+ var samples = grid.samples;
5258
+ var bands = grid.bands;
5259
+ var values = new Array(bands).fill(0);
5260
+ var total = 0;
5261
+ var src, weight;
5262
+ var x0 = Math.max(0, Math.floor(bounds.x0));
5263
+ var x1 = Math.min(grid.width, Math.ceil(bounds.x1));
5264
+ var y0 = Math.max(0, Math.floor(bounds.y0));
5265
+ var y1 = Math.min(grid.height, Math.ceil(bounds.y1));
5266
+ for (var sy = y0; sy < y1; sy++) {
5267
+ for (var sx = x0; sx < x1; sx++) {
5268
+ weight = getIntervalOverlap(bounds.x0, bounds.x1, sx, sx + 1) *
5269
+ getIntervalOverlap(bounds.y0, bounds.y1, sy, sy + 1);
5270
+ if (weight <= 0) continue;
5271
+ src = (sy * grid.width + sx) * bands;
5272
+ if (sampleIsNoData(samples, src, bands, grid.nodata)) continue;
5273
+ for (var band = 0; band < bands; band++) {
5274
+ values[band] += samples[src + band] * weight;
5275
+ }
5276
+ total += weight;
5277
+ }
5278
+ }
5279
+ if (total <= 0) return {valid: false, values: values};
5280
+ for (var i = 0; i < bands; i++) values[i] /= total;
5281
+ return {valid: true, values: values};
5282
+ }
5283
+
5284
+ function getApproxAverageRasterSample(grid, bounds) {
5285
+ var samples = grid.samples;
5286
+ var bands = grid.bands;
5287
+ var values = new Array(bands).fill(0);
5288
+ var xSteps = Math.min(MAX_APPROX_AVERAGE_STEPS, Math.max(1, Math.ceil(Math.abs(bounds.x1 - bounds.x0))));
5289
+ var ySteps = Math.min(MAX_APPROX_AVERAGE_STEPS, Math.max(1, Math.ceil(Math.abs(bounds.y1 - bounds.y0))));
5290
+ var count = 0;
5291
+ var sx, sy, src;
5292
+ for (var y = 0; y < ySteps; y++) {
5293
+ for (var x = 0; x < xSteps; x++) {
5294
+ sx = Math.max(0, Math.min(grid.width - 1, Math.floor(bounds.x0 + (x + 0.5) / xSteps * (bounds.x1 - bounds.x0))));
5295
+ sy = Math.max(0, Math.min(grid.height - 1, Math.floor(bounds.y0 + (y + 0.5) / ySteps * (bounds.y1 - bounds.y0))));
5296
+ src = (sy * grid.width + sx) * bands;
5297
+ if (sampleIsNoData(samples, src, bands, grid.nodata)) continue;
5298
+ for (var band = 0; band < bands; band++) {
5299
+ values[band] += samples[src + band];
5300
+ }
5301
+ count++;
5302
+ }
5303
+ }
5304
+ if (count === 0) return {valid: false, values: values};
5305
+ for (var i = 0; i < bands; i++) values[i] /= count;
5306
+ return {valid: true, values: values};
5307
+ }
5308
+
5309
+ function getBilinearRasterSample(grid, bbox, x, y, width, height) {
5310
+ var p = getRasterSourcePixelCenter(grid, bbox, x, y, width, height);
5311
+ var x0 = Math.max(0, Math.min(grid.width - 1, Math.floor(p.x)));
5312
+ var y0 = Math.max(0, Math.min(grid.height - 1, Math.floor(p.y)));
5313
+ var x1 = Math.max(0, Math.min(grid.width - 1, x0 + 1));
5314
+ var y1 = Math.max(0, Math.min(grid.height - 1, y0 + 1));
5315
+ var tx = Math.max(0, Math.min(1, p.x - x0));
5316
+ var ty = Math.max(0, Math.min(1, p.y - y0));
5317
+ var samples = grid.samples;
5318
+ var bands = grid.bands;
5319
+ var values = new Array(bands).fill(0);
5320
+ var offsets = [
5321
+ (y0 * grid.width + x0) * bands,
5322
+ (y0 * grid.width + x1) * bands,
5323
+ (y1 * grid.width + x0) * bands,
5324
+ (y1 * grid.width + x1) * bands
5325
+ ];
5326
+ var weights = [
5327
+ (1 - tx) * (1 - ty),
5328
+ tx * (1 - ty),
5329
+ (1 - tx) * ty,
5330
+ tx * ty
5331
+ ];
5332
+ var total = 0;
5333
+ offsets.forEach(function(src, i) {
5334
+ if (weights[i] <= 0 || sampleIsNoData(samples, src, bands, grid.nodata)) return;
5335
+ for (var band = 0; band < bands; band++) {
5336
+ values[band] += samples[src + band] * weights[i];
5337
+ }
5338
+ total += weights[i];
5339
+ });
5340
+ if (total <= 0) return {valid: false, values: values};
5341
+ for (var j = 0; j < bands; j++) values[j] /= total;
5342
+ return {valid: true, values: values};
5343
+ }
5344
+
5345
+ function getRasterSourcePixelBounds(grid, bbox, x, y, width, height) {
5346
+ return {
5347
+ x0: mapXToRasterPixel(grid, bbox[0] + x / width * (bbox[2] - bbox[0])),
5348
+ x1: mapXToRasterPixel(grid, bbox[0] + (x + 1) / width * (bbox[2] - bbox[0])),
5349
+ y0: mapYToRasterPixel(grid, bbox[3] - y / height * (bbox[3] - bbox[1])),
5350
+ y1: mapYToRasterPixel(grid, bbox[3] - (y + 1) / height * (bbox[3] - bbox[1]))
5351
+ };
5352
+ }
5353
+
5354
+ function getRasterSourcePixelCenter(grid, bbox, x, y, width, height) {
5355
+ var mapX = bbox[0] + (x + 0.5) / width * (bbox[2] - bbox[0]);
5356
+ var mapY = bbox[3] - (y + 0.5) / height * (bbox[3] - bbox[1]);
5357
+ return {
5358
+ x: mapXToRasterPixel(grid, mapX) - 0.5,
5359
+ y: mapYToRasterPixel(grid, mapY) - 0.5
5360
+ };
5361
+ }
5362
+
5363
+ function mapXToRasterPixel(grid, x) {
5364
+ return (x - grid.bbox[0]) / (grid.bbox[2] - grid.bbox[0]) * grid.width;
5365
+ }
5366
+
5367
+ function mapYToRasterPixel(grid, y) {
5368
+ return (grid.bbox[3] - y) / (grid.bbox[3] - grid.bbox[1]) * grid.height;
5369
+ }
5370
+
5371
+ function getIntervalOverlap(a0, a1, b0, b1) {
5372
+ return Math.max(0, Math.min(Math.max(a0, a1), b1) - Math.max(Math.min(a0, a1), b0));
5373
+ }
5374
+
5375
+ function getSourcePixelBoundsArea(bounds) {
5376
+ return Math.abs((bounds.x1 - bounds.x0) * (bounds.y1 - bounds.y0));
5377
+ }
5378
+
5379
+ function sampleIsNoData(samples, offset, bands, noData) {
5380
+ return noData !== null && noData !== undefined && allSamplesAreNoData(samples, offset, bands, noData);
5381
+ }
5382
+
5043
5383
  function renderRawEightBitPreview(grid, width, height, sourceBbox) {
5044
5384
  var samples = grid.samples;
5045
5385
  var bands = grid.bands;
@@ -5104,7 +5444,12 @@
5104
5444
  noteLayerWillChange(lyr, {operation: 'clipRasterToBBox', unit: 'raster'});
5105
5445
  raster.grid = cropRasterGrid(grid, crop, clipBbox);
5106
5446
  raster.view = raster.view || {};
5107
- raster.view.preview = createRasterPreview(raster, opts || {});
5447
+ delete raster.view.scalingStats;
5448
+ if (runningInBrowser()) {
5449
+ raster.view.preview = createRasterPreview(raster, opts || {});
5450
+ } else {
5451
+ delete raster.view.preview;
5452
+ }
5108
5453
  clearLegacyRasterFields(raster);
5109
5454
  markLayerChanged(lyr, {operation: 'clipRasterToBBox', unit: 'raster'});
5110
5455
  return true;
@@ -6159,239 +6504,13 @@
6159
6504
  return Object.keys(index);
6160
6505
  }
6161
6506
 
6162
- var PAYLOAD_FIELDS = {
6163
- table: ['records'],
6164
- 'table-records': ['records'],
6165
- 'table-fields': ['columns'],
6166
- 'table-schema': ['fields'],
6167
- arcs: ['nn', 'xx', 'yy', 'zz', 'zlimit'],
6168
- 'arcs-simplification': ['zz'],
6169
- layer: ['shapes', 'raster']
6170
- };
6171
-
6172
- async function storeUndoUnits(units, store, entryId, role) {
6173
- var stored = [];
6174
- try {
6175
- for (var i = 0; i < units.length; i++) {
6176
- stored.push(await storeUndoUnit(units[i], store, entryId, role));
6177
- }
6178
- } catch(e) {
6179
- await store.delMany(getStoredUndoPayloadRefs(stored));
6180
- throw e;
6181
- }
6182
- return stored;
6183
- }
6184
-
6185
- async function restoreStoredUndoUnits(units, store) {
6186
- var hydrated = await hydrateStoredUndoUnits(units, store);
6187
- restoreCapturedUnits(hydrated);
6188
- }
6189
-
6190
- async function hydrateStoredUndoUnits(units, store) {
6191
- var hydrated = [];
6192
- for (var i = 0; i < units.length; i++) {
6193
- hydrated.push(await hydrateStoredUndoUnit(units[i], store));
6194
- }
6195
- return hydrated;
6196
- }
6507
+ function createStoredUndoHistory(gui) {
6508
+ var undoEntryId = 0;
6197
6509
 
6198
- function getStoredUndoPayloadRefs(units) {
6199
- var refs = [];
6200
- units.forEach(function(unit) {
6201
- if (unit.payloadRef) refs.push(unit.payloadRef);
6202
- });
6203
- return refs;
6204
- }
6205
-
6206
- function getUndoRestoreFlags(units, baseFlags) {
6207
- var flags = Object.assign({}, baseFlags || {}, {undo_restore: true});
6208
- units.forEach(function(unit) {
6209
- if (unit.type == 'changed') return;
6210
- if (unit.type == 'arcs') {
6211
- flags.arc_count = true;
6212
- } else if (unit.type == 'arcs-simplification') {
6213
- flags.simplify = true;
6214
- } else if (unit.type == 'table' ||
6215
- unit.type == 'table-records' ||
6216
- unit.type == 'table-fields' ||
6217
- unit.type == 'table-order' ||
6218
- unit.type == 'table-schema') {
6219
- flags.same_table = false;
6220
- } else if (unit.type == 'catalog' || unit.type == 'dataset') {
6221
- flags.select = true;
6222
- flags.arc_count = true;
6223
- } else if (unit.type == 'dataset-info') {
6224
- flags.info = true;
6225
- } else if (unit.type == 'layer' ||
6226
- unit.type == 'layer-metadata' ||
6227
- unit.type == 'layer-order') {
6228
- flags.select = true;
6229
- }
6230
- });
6231
- return flags;
6232
- }
6233
-
6234
- async function storeUndoUnit(unit, store, entryId, role) {
6235
- var payload = await getUnitPayload(unit);
6236
- var stored = stripPayload(unit);
6237
- if (payload) {
6238
- stored.payloadRef = await store.put(payload, {
6239
- entryId: entryId,
6240
- role: role,
6241
- unitType: unit.type
6242
- });
6243
- }
6244
- return stored;
6245
- }
6246
-
6247
- async function hydrateStoredUndoUnit(unit, store) {
6248
- var hydrated = Object.assign({}, unit);
6249
- var payload;
6250
- if (unit.payloadRef) {
6251
- payload = await store.get(unit.payloadRef);
6252
- if (!payload) {
6253
- throw new Error('Missing undo payload: ' + unit.payloadRef.key);
6254
- }
6255
- payload = unpackPayload(unit, payload);
6256
- Object.assign(hydrated, payload);
6257
- }
6258
- delete hydrated.payloadRef;
6259
- return hydrated;
6260
- }
6261
-
6262
- async function getUnitPayload(unit) {
6263
- var fields = PAYLOAD_FIELDS[unit.type];
6264
- var payload, hasPayload;
6265
- if (!fields) return null;
6266
- payload = {};
6267
- fields.forEach(function(field) {
6268
- if (field in unit) {
6269
- payload[field] = unit[field];
6270
- hasPayload = true;
6271
- }
6272
- });
6273
- if (!hasPayload) return null;
6274
- if (unit.type == 'layer') return packLayerPayload(payload);
6275
- return unit.type == 'table' ? packTablePayload(payload) : payload;
6276
- }
6277
-
6278
- async function packLayerPayload(payload) {
6279
- if (payload.raster) {
6280
- payload = Object.assign({}, payload, {
6281
- raster: packRasterUndoPayload(payload.raster)
6282
- });
6283
- }
6284
- return payload;
6285
- }
6286
-
6287
- async function packTablePayload(payload) {
6288
- return {
6289
- packedRecords: packRecordsAsColumns(payload.records)
6290
- };
6291
- }
6292
-
6293
- function unpackPayload(unit, payload) {
6294
- if (unit.type == 'table' && payload.packedRecords) {
6295
- return {
6296
- records: unpackRecordsFromColumns(payload.packedRecords)
6297
- };
6298
- }
6299
- if (unit.type == 'layer' && payload.raster) {
6300
- return Object.assign({}, payload, {
6301
- raster: unpackRasterUndoPayload(payload.raster)
6302
- });
6303
- }
6304
- return payload;
6305
- }
6306
-
6307
- function packRasterUndoPayload(raster) {
6308
- var copy = Object.assign({}, raster);
6309
- if (raster.view) {
6310
- copy.view = Object.assign({}, raster.view);
6311
- if (raster.view.preview) {
6312
- copy.view.preview = stripPreviewPixels(raster.view.preview);
6313
- }
6314
- }
6315
- if (raster.preview) {
6316
- copy.preview = stripPreviewPixels(raster.preview);
6317
- }
6318
- return copy;
6319
- }
6320
-
6321
- function unpackRasterUndoPayload(raster) {
6322
- var copy = Object.assign({}, raster);
6323
- if (raster.view) {
6324
- copy.view = Object.assign({}, raster.view);
6325
- if (raster.view.preview && !raster.view.preview.pixels && raster.grid && raster.grid.samples) {
6326
- copy.view.preview = renderRasterPreview(raster.grid, raster.view.recipe, raster.view.preview.width, raster.view.preview.height);
6327
- }
6328
- }
6329
- return copy;
6330
- }
6331
-
6332
- function stripPreviewPixels(preview) {
6333
- var copy = Object.assign({}, preview);
6334
- delete copy.canvas;
6335
- delete copy.pixels;
6336
- return copy;
6337
- }
6338
-
6339
- function packRecordsAsColumns(records) {
6340
- var fields = getRecordFields(records);
6341
- return {
6342
- fields: fields,
6343
- types: fields.map(function() { return null; }),
6344
- data: fields.map(function(field) {
6345
- return records.map(function(rec) {
6346
- return rec ? rec[field] : undefined;
6347
- });
6348
- }),
6349
- size: records.length
6350
- };
6351
- }
6352
-
6353
- function getRecordFields(records) {
6354
- var index = {};
6355
- records.forEach(function(rec) {
6356
- Object.keys(rec || {}).forEach(function(field) {
6357
- index[field] = true;
6358
- });
6359
- });
6360
- return Object.keys(index);
6361
- }
6362
-
6363
- function unpackRecordsFromColumns(data) {
6364
- var records = [];
6365
- for (var i = 0; i < data.size; i++) {
6366
- records[i] = {};
6367
- }
6368
- data.fields.forEach(function(field, j) {
6369
- var values = data.data[j];
6370
- values.forEach(function(val, i) {
6371
- records[i][field] = val;
6372
- });
6373
- });
6374
- return records;
6375
- }
6376
-
6377
- function stripPayload(unit) {
6378
- var fields = PAYLOAD_FIELDS[unit.type];
6379
- var stripped = Object.assign({}, unit);
6380
- if (fields) {
6381
- fields.forEach(function(field) {
6382
- delete stripped[field];
6383
- });
6384
- }
6385
- return stripped;
6386
- }
6387
-
6388
- function createStoredUndoHistory(gui) {
6389
- var undoEntryId = 0;
6390
-
6391
- return {
6392
- addTransaction: addTransaction,
6393
- getPayloadStore: getPayloadStore
6394
- };
6510
+ return {
6511
+ addTransaction: addTransaction,
6512
+ getPayloadStore: getPayloadStore
6513
+ };
6395
6514
 
6396
6515
  async function addTransaction(tx, opts) {
6397
6516
  var undoUnits, redoCaptureUnits, updateFlags, store, entryId, storedUndoUnits,
@@ -6752,14 +6871,38 @@
6752
6871
  async function considerReprojecting(gui, dataset, opts) {
6753
6872
  var mapCRS = gui.map.getActiveLayerCRS();
6754
6873
  var dataCRS = internal.getDatasetCRS(dataset);
6874
+ var msg, reproject;
6755
6875
  if (!dataCRS || !mapCRS || internal.crsAreEqual(mapCRS, dataCRS)) return;
6756
- var msg = `The input file ${dataset?.info?.input_files[0] || ''} has a different projection from the current selected layer. Would you like to reproject it to match?`;
6757
- var reproject = await showPrompt(msg, 'Reproject file?');
6876
+ if (!datasetCanBeReprojected(dataset, dataCRS, mapCRS)) {
6877
+ notifyProjectionMismatch(gui, dataset);
6878
+ return;
6879
+ }
6880
+ msg = `The input file ${dataset?.info?.input_files[0] || ''} has a different projection from the current selected layer. Would you like to reproject it to match?`;
6881
+ reproject = await showPrompt(msg, 'Reproject file?');
6758
6882
  if (reproject) {
6759
6883
  internal.projectDataset(dataset, dataCRS, mapCRS, {densify: true});
6760
6884
  }
6761
6885
  }
6762
6886
 
6887
+ function datasetCanBeReprojected(dataset, srcCRS, destCRS) {
6888
+ var bounds = internal.getDatasetBounds(dataset);
6889
+ var transform, p;
6890
+ if (!bounds || !bounds.hasBounds()) return false;
6891
+ transform = internal.getProjTransform2(srcCRS, destCRS);
6892
+ p = transform(bounds.centerX(), bounds.centerY());
6893
+ return !!(p && isFinite(p[0]) && isFinite(p[1]));
6894
+ }
6895
+
6896
+ function notifyProjectionMismatch(gui, dataset) {
6897
+ if (!gui.notify) return;
6898
+ gui.notify({
6899
+ severity: 'warn',
6900
+ body: `The input file ${dataset?.info?.input_files[0] || ''} has a different projection from the current selected layer, but Mapshaper cannot transform it to the current projection.`,
6901
+ dedupKey: 'projection-mismatch:' + (dataset?.info?.input_files?.[0] || '')
6902
+ });
6903
+ }
6904
+
6905
+
6763
6906
  var geopackagePromise = null;
6764
6907
  var geoParquetPromise = null;
6765
6908
  var geoTIFFPromise = null;
@@ -6903,7 +7046,7 @@
6903
7046
  async function persistRasterLayerSamples(dataset) {
6904
7047
  var promises = [];
6905
7048
  dataset.layers.forEach(function(lyr) {
6906
- var grid = lyr.raster && getRasterGrid(lyr.raster);
7049
+ var grid = lyr.raster && internal.getRasterGrid(lyr.raster);
6907
7050
  var key;
6908
7051
  if (!grid || !grid.samples) return;
6909
7052
  key = makeRasterSamplesKey(lyr.name);
@@ -8459,12 +8602,6 @@
8459
8602
  } else if (internal.layerHasRaster(layer)) {
8460
8603
  gui.geographic = true;
8461
8604
  gui.displayLayer = layer;
8462
- if (needReprojectionForDisplay(sourceCRS, displayCRS)) {
8463
- // Raster warping is not supported yet; hide rather than misplace pixels.
8464
- gui.unprojectable = true;
8465
- gui.displayLayer = {shapes: []};
8466
- notifyRasterReprojectionBlocked(layer, opts);
8467
- }
8468
8605
  } else if (layer.geometry_type) {
8469
8606
  gui.geographic = true;
8470
8607
  gui.displayLayer = layer;
@@ -8489,22 +8626,18 @@
8489
8626
  // Don't try to draw paths containing coordinates that failed to project
8490
8627
  gui.displayLayer = internal.filterPathLayerByArcIds(gui.displayLayer, emptyArcs);
8491
8628
  }
8629
+ } else if (internal.layerHasRaster(layer)) {
8630
+ gui.bounds = getProjectedRasterDisplayBounds(layer, sourceCRS, displayCRS);
8492
8631
  }
8493
8632
  }
8494
8633
 
8495
- gui.bounds = getDisplayBounds(gui.displayLayer, gui.displayArcs);
8634
+ gui.bounds = gui.bounds || getDisplayBounds(gui.displayLayer, gui.displayArcs);
8496
8635
  layer.gui = gui;
8497
8636
  }
8498
8637
 
8499
- function notifyRasterReprojectionBlocked(layer, opts) {
8500
- if (!opts.notify) return;
8501
- opts.notify({
8502
- severity: 'warn',
8503
- title: 'Raster layer hidden',
8504
- body: 'The raster layer "' + (layer.name || '[unnamed]') +
8505
- '" cannot be displayed in the current map projection because raster reprojection is not supported yet.',
8506
- dedupKey: 'raster-reprojection-blocked:' + (layer.menu_id || layer.name || '')
8507
- });
8638
+ function getProjectedRasterDisplayBounds(layer, sourceCRS, displayCRS) {
8639
+ var bbox = internal.getProjectedRasterGridBBox(layer.raster, sourceCRS, displayCRS, {raster_mesh_interval: 32});
8640
+ return bbox ? new Bounds$1(bbox) : new Bounds$1();
8508
8641
  }
8509
8642
 
8510
8643
  function getDisplayBounds(lyr, arcs) {
@@ -10655,8 +10788,8 @@
10655
10788
  if (e.deleteLayer) {
10656
10789
  addMenuItem('delete layer', e.deleteLayer, '');
10657
10790
  }
10658
- if (e.selectLayer) {
10659
- addMenuItem('select layer', e.selectLayer, '');
10791
+ if (e.showLayerInfo) {
10792
+ addMenuItem('show info', e.showLayerInfo, '');
10660
10793
  }
10661
10794
 
10662
10795
  if (lyr && lyr.gui.geographic) {
@@ -11083,6 +11216,15 @@
11083
11216
  }
11084
11217
  }
11085
11218
 
11219
+ function showLayerInfo() {
11220
+ var target = findLayerById(id);
11221
+ var popup, content;
11222
+ if (!target) return;
11223
+ popup = showPopupAlert('', 'Layer info');
11224
+ content = popup.container().addClass('layer-info-popup');
11225
+ content.node().appendChild(renderLayerInfo(internal.getLayerInfo(target.layer, target.dataset)));
11226
+ }
11227
+
11086
11228
  function openLayerMenu(e) {
11087
11229
  var menuEvent = e;
11088
11230
  e.stopPropagation();
@@ -11094,7 +11236,7 @@
11094
11236
  };
11095
11237
  }
11096
11238
  menuEvent.deleteLayer = deleteLayer;
11097
- menuEvent.selectLayer = selectLayer;
11239
+ menuEvent.showLayerInfo = showLayerInfo;
11098
11240
  menuEvent.contextMenuId = 'layer-' + id;
11099
11241
  openContextMenu(menuEvent, null, null);
11100
11242
  }
@@ -11173,7 +11315,7 @@
11173
11315
  if (isFrame) {
11174
11316
  str = 'map frame';
11175
11317
  } else if (internal.layerHasRaster(lyr)) {
11176
- str = utils$1.format('%,d x %,d %s', getRasterWidth(lyr.raster), getRasterHeight(lyr.raster), type);
11318
+ str = utils$1.format('%,d x %,d %s', internal.getRasterWidth(lyr.raster), internal.getRasterHeight(lyr.raster), type);
11177
11319
  } else if (type) {
11178
11320
  str = utils$1.format('%,d %s%s', n, type, utils$1.pluralSuffix(n));
11179
11321
  } else {
@@ -11182,44 +11324,137 @@
11182
11324
  return str;
11183
11325
  }
11184
11326
 
11185
- function renameLayer(target, name) {
11186
- var undoTransaction;
11187
- if (!target || target.layer.name == name) return;
11188
- undoTransaction = createUndoTransaction(gui, 'rename layer');
11189
- if (undoTransaction) {
11190
- undoTransaction.captureLayerMetadataBefore(target.layer, {operation: 'renameLayer', unit: 'name'});
11191
- target.layer.name = name;
11192
- markLayerChanged(target.layer, {operation: 'renameLayer', unit: 'name'});
11193
- addUndoTransactionToHistory(gui, undoTransaction, {
11194
- flags: {select: true},
11195
- entryPrefix: 'rename-layer'
11196
- });
11197
- } else {
11198
- target.layer.name = name;
11327
+ function renderLayerInfo(info) {
11328
+ var container = document.createElement('div');
11329
+ var title = document.createElement('div');
11330
+ container.className = 'console-info';
11331
+ title.className = 'console-info-title';
11332
+ title.textContent = 'Layer: ' + (info.layer_name || '[unnamed layer]');
11333
+ container.appendChild(title);
11334
+ container.appendChild(renderKeyValueTable(getInfoRows(info), 'console-info-table'));
11335
+ if (!info.raster_type) {
11336
+ container.appendChild(renderAttributeInfoTable(info.attribute_data));
11199
11337
  }
11200
- gui.session.layerRenamed(target.layer, name);
11201
- updateMenuBtn();
11338
+ return container;
11202
11339
  }
11203
11340
 
11204
- function markLayerChanged(layer, detail) {
11205
- if (internal.UndoTracking && internal.UndoTracking.markLayerChanged) {
11206
- internal.UndoTracking.markLayerChanged(layer, detail);
11341
+ function getInfoRows(info) {
11342
+ var rows = [
11343
+ ['Type', info.raster_type ? 'raster data' : info.geometry_type || 'tabular data']
11344
+ ];
11345
+ if (info.raster_type) {
11346
+ rows.push(['Size', utils$1.format('%,d x %,d x %,d', info.raster_width, info.raster_height, info.raster_bands)]);
11347
+ rows.push(['Data', info.raster_pixel_type || 'unknown']);
11348
+ } else {
11349
+ rows.push(['Records', utils$1.format('%,d', info.feature_count)]);
11350
+ }
11351
+ if (info.null_shape_count > 0) {
11352
+ rows.push(['Nulls', utils$1.format("%'d", info.null_shape_count)]);
11353
+ }
11354
+ if (info.raster_type || info.geometry_type && info.feature_count > info.null_shape_count) {
11355
+ rows.push(['Bounds', info.bbox.join(',')]);
11356
+ rows.push(['CRS', info.proj4]);
11207
11357
  }
11358
+ rows.push(['Source', info.source_file || 'n/a']);
11359
+ return rows;
11208
11360
  }
11209
11361
 
11210
- function isPinnable(lyr) {
11211
- return internal.layerIsGeometric(lyr) || internal.layerHasRaster(lyr) || internal.layerHasFurniture(lyr);
11362
+ function renderAttributeInfoTable(fields) {
11363
+ var wrapper = document.createElement('div');
11364
+ var title = document.createElement('div');
11365
+ wrapper.className = 'console-attribute-info';
11366
+ title.className = 'console-info-subtitle';
11367
+ title.textContent = 'Attribute data';
11368
+ wrapper.appendChild(title);
11369
+ if (!fields) {
11370
+ var none = document.createElement('div');
11371
+ none.className = 'console-info-empty';
11372
+ none.textContent = '[none]';
11373
+ wrapper.appendChild(none);
11374
+ return wrapper;
11375
+ }
11376
+ wrapper.appendChild(renderDataTable(
11377
+ [['Field', 'First value']].concat(fields.map(function(o) {
11378
+ var valKey = 'first_value' in o ? 'first_value' : 'value';
11379
+ return [o.field, formatInfoValue(o[valKey])];
11380
+ })),
11381
+ 'console-attribute-table'
11382
+ ));
11383
+ return wrapper;
11212
11384
  }
11213
11385
 
11214
- function rowHTML(c1, c2, cname) {
11215
- return utils$1.format('<div class="row%s"><div class="col1">%s</div>' +
11216
- '<div class="col2">%s</div></div>', cname ? ' ' + cname : '', c1, c2);
11386
+ function renderKeyValueTable(rows, className) {
11387
+ return renderDataTable(rows, className + ' console-key-value-table');
11217
11388
  }
11218
- }
11219
11389
 
11220
- // Hamburger menu in the top-right of the header. After the user starts
11221
- // editing, the splash bar (Docs / GitHub / Survey / Sponsor) is hidden by
11222
- // gui.mjs; this menu is the post-edit replacement, exposing the same links
11390
+ function renderDataTable(rows, className) {
11391
+ var table = document.createElement('table');
11392
+ var tbody = document.createElement('tbody');
11393
+ table.className = className;
11394
+ rows.forEach(function(row, i) {
11395
+ var tr = document.createElement('tr');
11396
+ row.forEach(function(val) {
11397
+ var cell = document.createElement(i === 0 && rows.length > 1 && className == 'console-attribute-table' ? 'th' : 'td');
11398
+ cell.textContent = val == null ? '' : String(val);
11399
+ tr.appendChild(cell);
11400
+ });
11401
+ tbody.appendChild(tr);
11402
+ });
11403
+ table.appendChild(tbody);
11404
+ return table;
11405
+ }
11406
+
11407
+ function formatInfoValue(val) {
11408
+ if (val === null || val === undefined) return '';
11409
+ if (utils$1.isString(val)) {
11410
+ return "'" + val.replace(/[\r\t\n]/g, function(c) {
11411
+ return c == '\n' ? '\\n' : c == '\r' ? '\\r' : '\\t';
11412
+ }) + "'";
11413
+ }
11414
+ if (utils$1.isNumber(val) || val === true || val === false) {
11415
+ return String(val);
11416
+ }
11417
+ return JSON.stringify(val);
11418
+ }
11419
+
11420
+ function renameLayer(target, name) {
11421
+ var undoTransaction;
11422
+ if (!target || target.layer.name == name) return;
11423
+ undoTransaction = createUndoTransaction(gui, 'rename layer');
11424
+ if (undoTransaction) {
11425
+ undoTransaction.captureLayerMetadataBefore(target.layer, {operation: 'renameLayer', unit: 'name'});
11426
+ target.layer.name = name;
11427
+ markLayerChanged(target.layer, {operation: 'renameLayer', unit: 'name'});
11428
+ addUndoTransactionToHistory(gui, undoTransaction, {
11429
+ flags: {select: true},
11430
+ entryPrefix: 'rename-layer'
11431
+ });
11432
+ } else {
11433
+ target.layer.name = name;
11434
+ }
11435
+ gui.session.layerRenamed(target.layer, name);
11436
+ updateMenuBtn();
11437
+ }
11438
+
11439
+ function markLayerChanged(layer, detail) {
11440
+ if (internal.UndoTracking && internal.UndoTracking.markLayerChanged) {
11441
+ internal.UndoTracking.markLayerChanged(layer, detail);
11442
+ }
11443
+ }
11444
+
11445
+ function isPinnable(lyr) {
11446
+ return internal.layerIsGeometric(lyr) || internal.layerHasRaster(lyr) || internal.layerHasFurniture(lyr);
11447
+ }
11448
+
11449
+ function rowHTML(c1, c2, cname) {
11450
+ return utils$1.format('<div class="row%s"><div class="col1">%s</div>' +
11451
+ '<div class="col2">%s</div></div>', cname ? ' ' + cname : '', c1, c2);
11452
+ }
11453
+ }
11454
+
11455
+ // Hamburger menu in the top-right of the header. After the user starts
11456
+ // editing, the splash bar (Docs / GitHub / Survey / Sponsor) is hidden by
11457
+ // gui.mjs; this menu is the post-edit replacement, exposing the same links
11223
11458
  // from a single button so the editing controls don't have to share the row.
11224
11459
 
11225
11460
 
@@ -11282,6 +11517,8 @@
11282
11517
  var note = menu.findChild('.history-menu-note');
11283
11518
  var clearBtn = menu.findChild('.history-clear-btn');
11284
11519
  var commandLogBtn = menu.findChild('.history-command-log-btn');
11520
+ var createSnapshotBtn = menu.findChild('.history-create-snapshot-btn');
11521
+ var snapshotList = menu.findChild('.history-snapshot-list');
11285
11522
 
11286
11523
  gui.appUndoIsEnabled = isAppUndoEnabled;
11287
11524
 
@@ -11334,6 +11571,13 @@
11334
11571
  gui.clearMode();
11335
11572
  });
11336
11573
 
11574
+ createSnapshotBtn.on('click', function(e) {
11575
+ e.stopPropagation();
11576
+ if (!createSnapshotBtn.hasClass('disabled') && gui.sessionSnapshots) {
11577
+ gui.sessionSnapshots.saveSnapshot().then(renderSnapshotList);
11578
+ }
11579
+ });
11580
+
11337
11581
  document.addEventListener('keydown', function(e) {
11338
11582
  if (gui.getMode() == 'history_menu' && e.key == 'Escape') {
11339
11583
  gui.clearMode();
@@ -11347,6 +11591,7 @@
11347
11591
  function turnOn() {
11348
11592
  btn.attr('aria-expanded', 'true');
11349
11593
  updateMenuState();
11594
+ renderSnapshotList();
11350
11595
  menu.show();
11351
11596
  }
11352
11597
 
@@ -11359,6 +11604,7 @@
11359
11604
  // setItemEnabled(undoBtn, gui.undo.canUndo());
11360
11605
  // setItemEnabled(redoBtn, gui.undo.canRedo());
11361
11606
  setItemEnabled(clearBtn, gui.undo.canUndo() || gui.undo.canRedo());
11607
+ setItemEnabled(createSnapshotBtn, !!(gui.sessionSnapshots && gui.sessionSnapshots.enabled));
11362
11608
  updateToggle();
11363
11609
  }
11364
11610
 
@@ -11381,6 +11627,14 @@
11381
11627
  }
11382
11628
  return Promise.resolve();
11383
11629
  }
11630
+
11631
+ function renderSnapshotList() {
11632
+ if (gui.sessionSnapshots && gui.sessionSnapshots.enabled && snapshotList) {
11633
+ gui.sessionSnapshots.renderSnapshotList(snapshotList);
11634
+ } else if (snapshotList) {
11635
+ snapshotList.empty();
11636
+ }
11637
+ }
11384
11638
  }
11385
11639
 
11386
11640
  function isAppUndoEnabled() {
@@ -12317,6 +12571,7 @@
12317
12571
  info: 'inspect features',
12318
12572
  box: 'rectangle tool',
12319
12573
  data: 'edit attributes',
12574
+ label_style: 'style labels',
12320
12575
  labels: 'position labels',
12321
12576
  edit_points: 'add/drag points',
12322
12577
  edit_lines: 'draw/edit polylines',
@@ -12351,6 +12606,7 @@
12351
12606
  });
12352
12607
 
12353
12608
  btn.on('mouseenter', function() {
12609
+ if (gui.state.label_style_panel_open) return;
12354
12610
  btn.addClass('hover');
12355
12611
  if (_menuOpen) {
12356
12612
  clearTimeout(_menuTimeout); // prevent timed closing
@@ -12363,7 +12619,10 @@
12363
12619
  });
12364
12620
 
12365
12621
  btn.on('click', function(e) {
12366
- if (active()) {
12622
+ if (_editMode == 'label_style') {
12623
+ setMode('info');
12624
+ closeMenu();
12625
+ } else if (active()) {
12367
12626
  setMode('off');
12368
12627
  closeMenu();
12369
12628
  } else if (_menuOpen) {
@@ -12374,6 +12633,7 @@
12374
12633
  }
12375
12634
  e.stopPropagation();
12376
12635
  });
12636
+
12377
12637
  }
12378
12638
 
12379
12639
  this.turnOff = function() {
@@ -12385,7 +12645,7 @@
12385
12645
  };
12386
12646
 
12387
12647
  this.modeUsesHitDetection = function(mode) {
12388
- return ['info', 'selection', 'data', 'labels', 'edit_points', 'vertices', 'rectangles', 'edit_lines', 'edit_polygons', 'snip_lines'].includes(mode);
12648
+ return ['info', 'selection', 'data', 'label_style', 'labels', 'edit_points', 'vertices', 'rectangles', 'edit_lines', 'edit_polygons', 'snip_lines'].includes(mode);
12389
12649
  };
12390
12650
 
12391
12651
  this.modeUsesPopup = function(mode) {
@@ -12393,7 +12653,7 @@
12393
12653
  };
12394
12654
 
12395
12655
  this.modeSupportsUndo = function(mode) {
12396
- return ['data', 'labels', 'edit_points', 'edit_lines', 'edit_polygons', 'vertices', 'rectangles'].includes(mode);
12656
+ return ['data', 'label_style', 'labels', 'edit_points', 'edit_lines', 'edit_polygons', 'vertices', 'rectangles'].includes(mode);
12397
12657
  };
12398
12658
 
12399
12659
  this.getMode = getInteractionMode;
@@ -12475,11 +12735,16 @@
12475
12735
  // if current editing mode is not available, turn off the tool
12476
12736
  function updateCurrentMode() {
12477
12737
  var modes = getAvailableModes();
12478
- if (modes.indexOf(_editMode) == -1) {
12738
+ if (modes.indexOf(_editMode) == -1 && !labelStyleModeIsAvailable()) {
12479
12739
  setMode('off');
12480
12740
  }
12481
12741
  }
12482
12742
 
12743
+ function labelStyleModeIsAvailable() {
12744
+ var o = gui.model.getActiveLayer();
12745
+ return _editMode == 'label_style' && o && o.layer && internal.layerHasLabels(o.layer);
12746
+ }
12747
+
12483
12748
  function openMenu() {
12484
12749
  clearTimeout(_menuTimeout);
12485
12750
  if (!_menuOpen) {
@@ -12530,7 +12795,7 @@
12530
12795
  }
12531
12796
  btn.classed('hover', _menuOpen);
12532
12797
  // btn.classed('selected', active() && !_menuOpen);
12533
- btn.classed('selected', active());
12798
+ btn.classed('selected', active() && _editMode != 'label_style');
12534
12799
  }
12535
12800
 
12536
12801
  function updateSelectionHighlight() {
@@ -12611,142 +12876,1924 @@
12611
12876
  return el.node();
12612
12877
  };
12613
12878
 
12614
- function updateVisibility() {
12615
- var shouldShow = visible && GUI.isActiveInstance(gui);
12616
- if (shouldShow) {
12617
- clearTimeout(hideTimer);
12618
- hideTimer = null;
12619
- el.css('display', 'flex');
12620
- // wait one frame so the browser registers the initial state before
12621
- // the transition kicks in
12622
- requestAnimationFrame(function() {
12623
- el.addClass('visible');
12879
+ function updateVisibility() {
12880
+ var shouldShow = visible && GUI.isActiveInstance(gui);
12881
+ if (shouldShow) {
12882
+ clearTimeout(hideTimer);
12883
+ hideTimer = null;
12884
+ el.css('display', 'flex');
12885
+ // wait one frame so the browser registers the initial state before
12886
+ // the transition kicks in
12887
+ requestAnimationFrame(function() {
12888
+ el.addClass('visible');
12889
+ });
12890
+ } else {
12891
+ el.removeClass('visible');
12892
+ // wait for the transition to finish before hiding completely
12893
+ clearTimeout(hideTimer);
12894
+ hideTimer = setTimeout(function() {
12895
+ if (!(visible && GUI.isActiveInstance(gui))) {
12896
+ el.css('display', 'none');
12897
+ }
12898
+ }, transitionMs);
12899
+ }
12900
+ }
12901
+ }
12902
+
12903
+ function ToolbarButton(parent, iconRef, opts) {
12904
+ var btn = El('div').addClass('floating-toolbar-btn').appendTo(parent);
12905
+ if (iconRef) {
12906
+ var iconNode = El('body').findChild(iconRef);
12907
+ if (iconNode) {
12908
+ var icon = iconNode.node().cloneNode(true);
12909
+ if (icon.hasAttribute('id')) icon.removeAttribute('id');
12910
+ btn.node().appendChild(icon);
12911
+ }
12912
+ }
12913
+ var enabled = true;
12914
+ var clickHandlers = [];
12915
+
12916
+ if (opts.tooltip) setTooltip(opts.tooltip);
12917
+
12918
+ // Block native dblclick to avoid the map's double-click zoom
12919
+ btn.on('dblclick', function(e) { e.stopPropagation(); });
12920
+
12921
+ btn.on('click', function(e) {
12922
+ if (!enabled) return;
12923
+ for (var i = 0; i < clickHandlers.length; i++) {
12924
+ clickHandlers[i](e);
12925
+ }
12926
+ });
12927
+
12928
+ this.on = function(event, fn) {
12929
+ if (event == 'click') {
12930
+ clickHandlers.push(fn);
12931
+ } else {
12932
+ btn.on(event, fn);
12933
+ }
12934
+ return this;
12935
+ };
12936
+
12937
+ this.enable = function() { return this.setEnabled(true); };
12938
+ this.disable = function() { return this.setEnabled(false); };
12939
+
12940
+ this.setEnabled = function(b) {
12941
+ enabled = !!b;
12942
+ btn.classed('disabled', !enabled);
12943
+ return this;
12944
+ };
12945
+
12946
+ this.setTooltip = setTooltip;
12947
+
12948
+ this.node = function() {
12949
+ return btn.node();
12950
+ };
12951
+
12952
+ function setTooltip(text) {
12953
+ btn.attr('data-tooltip', text);
12954
+ }
12955
+ }
12956
+
12957
+ // Floating toolbar that exposes undo/redo while the user is in an editing
12958
+ // interaction mode. The toolbar is the first consumer of FloatingToolbar;
12959
+ // future per-mode toolbars (e.g. feature styling) can follow the same pattern.
12960
+
12961
+ function EditToolbar(gui) {
12962
+ var toolbar = new FloatingToolbar(gui, { name: 'edit-toolbar' });
12963
+ var isMac = navigator.userAgent.includes('Mac');
12964
+ var modKey = isMac ? '\u2318' : 'Ctrl+';
12965
+ var shiftKey = isMac ? '\u21e7' : 'Shift+';
12966
+
12967
+ var undoBtn = toolbar.addButton('#undo-icon', {
12968
+ tooltip: 'Undo (' + modKey + 'Z)'
12969
+ }).on('click', function() {
12970
+ gui.undo.undo();
12971
+ });
12972
+
12973
+ var redoBtn = toolbar.addButton('#redo-icon', {
12974
+ tooltip: 'Redo (' + shiftKey + modKey + 'Z)'
12975
+ }).on('click', function() {
12976
+ gui.undo.redo();
12977
+ });
12978
+
12979
+ updateButtons();
12980
+
12981
+ gui.on('interaction_mode_change', function() {
12982
+ updateVisibility();
12983
+ // history is cleared on mode change; refresh button states next tick
12984
+ updateButtons();
12985
+ });
12986
+
12987
+ gui.on('history_change', function(e) {
12988
+ undoBtn.setEnabled(!!e.canUndo);
12989
+ redoBtn.setEnabled(!!e.canRedo);
12990
+ // Visibility may also depend on history (e.g. attribute edits via popup
12991
+ // happen in modes that don't otherwise support undo).
12992
+ updateVisibility();
12993
+ });
12994
+
12995
+ updateVisibility();
12996
+
12997
+ function updateVisibility() {
12998
+ if (!gui.interaction) {
12999
+ toolbar.hide();
13000
+ return;
13001
+ }
13002
+ var mode = gui.interaction.getMode();
13003
+ var hasHistory = gui.undo.canUndo() || gui.undo.canRedo();
13004
+ if (gui.interaction.modeSupportsUndo(mode) || hasHistory) {
13005
+ toolbar.show();
13006
+ } else {
13007
+ toolbar.hide();
13008
+ }
13009
+ }
13010
+
13011
+ function updateButtons() {
13012
+ undoBtn.setEnabled(gui.undo.canUndo());
13013
+ redoBtn.setEnabled(gui.undo.canRedo());
13014
+ }
13015
+ }
13016
+
13017
+ var sans = [{
13018
+ name: 'NYTFranklin'
13019
+ }, {
13020
+ name: 'Acumin Pro'
13021
+ }, {
13022
+ name: 'Akzidenz-Grotesk'
13023
+ }, {
13024
+ name: 'Arial'
13025
+ }, {
13026
+ name: 'Avenir',
13027
+ alternates: ['Avenir Next']
13028
+ }, {
13029
+ name: 'Bahnschrift'
13030
+ }, {
13031
+ name: 'Brandon Grotesque'
13032
+ }, {
13033
+ name: 'Cabin'
13034
+ }, {
13035
+ name: 'Calibri'
13036
+ }, {
13037
+ name: 'Candara'
13038
+ }, {
13039
+ name: 'Cantarell'
13040
+ }, {
13041
+ name: 'Century Gothic'
13042
+ }, {
13043
+ name: 'Clearview'
13044
+ }, {
13045
+ name: 'Corbel'
13046
+ }, {
13047
+ name: 'DejaVu Sans'
13048
+ }, {
13049
+ name: 'DIN 1451',
13050
+ alternates: ['DIN']
13051
+ }, {
13052
+ name: 'DM Sans'
13053
+ }, {
13054
+ name: 'FF Meta'
13055
+ }, {
13056
+ name: 'Fira Sans'
13057
+ }, {
13058
+ name: 'Frutiger',
13059
+ alternates: ['Frutiger Next']
13060
+ }, {
13061
+ name: 'Futura'
13062
+ }, {
13063
+ name: 'Gill Sans',
13064
+ alternates: ['Gill Sans MT']
13065
+ }, {
13066
+ name: 'Gotham'
13067
+ }, {
13068
+ name: 'Gudea'
13069
+ }, {
13070
+ name: 'Helvetica',
13071
+ alternates: ['Helvetica Neue']
13072
+ }, {
13073
+ name: 'IBM Plex Sans'
13074
+ }, {
13075
+ name: 'Inter'
13076
+ }, {
13077
+ name: 'Interstate'
13078
+ }, {
13079
+ name: 'ITC Stone Sans'
13080
+ }, {
13081
+ name: 'Josefin Sans'
13082
+ }, {
13083
+ name: 'Karla'
13084
+ }, {
13085
+ name: 'Lato'
13086
+ }, {
13087
+ name: 'Liberation Sans'
13088
+ }, {
13089
+ name: 'Lucida Grande',
13090
+ alternates: ['Lucida Sans', 'Lucida Sans Unicode']
13091
+ }, {
13092
+ name: 'Montserrat'
13093
+ }, {
13094
+ name: 'Museo Sans'
13095
+ }, {
13096
+ name: 'Myriad Pro'
13097
+ }, {
13098
+ name: 'Neue Haas Grotesk',
13099
+ alternates: ['Neue Haas Grotesk Text Pro']
13100
+ }, {
13101
+ name: 'Nunito Sans',
13102
+ alternates: ['Nunito']
13103
+ }, {
13104
+ name: 'Open Sans'
13105
+ }, {
13106
+ name: 'Optima'
13107
+ }, {
13108
+ name: 'Overpass'
13109
+ }, {
13110
+ name: 'Poppins'
13111
+ }, {
13112
+ name: 'Proxima Nova'
13113
+ }, {
13114
+ name: 'PT Sans'
13115
+ }, {
13116
+ name: 'Raleway'
13117
+ }, {
13118
+ name: 'Roboto',
13119
+ alternates: ['Noto Sans']
13120
+ }, {
13121
+ name: 'Rotis Sans Serif'
13122
+ }, {
13123
+ name: 'Rubik'
13124
+ }, {
13125
+ name: 'San Francisco',
13126
+ alternates: ['SF Pro']
13127
+ }, {
13128
+ name: 'Scala Sans'
13129
+ }, {
13130
+ name: 'Segoe UI'
13131
+ }, {
13132
+ name: 'Source Sans Pro',
13133
+ alternates: ['Source Sans 3']
13134
+ }, {
13135
+ name: 'Tahoma'
13136
+ }, {
13137
+ name: 'Trebuchet MS'
13138
+ }, {
13139
+ name: 'Tw Cen MT'
13140
+ }, {
13141
+ name: 'Ubuntu'
13142
+ }, {
13143
+ name: 'Univers'
13144
+ }, {
13145
+ name: 'Verdana'
13146
+ }, {
13147
+ name: 'Work Sans'
13148
+ }
13149
+ ];
13150
+
13151
+ var condensed = [{
13152
+ name: 'Arial Narrow'
13153
+ }, {
13154
+ name: 'Barlow Condensed'
13155
+ }, {
13156
+ name: 'Bebas Neue'
13157
+ }, {
13158
+ name: 'DIN Condensed'
13159
+ }, {
13160
+ name: 'Franklin Gothic',
13161
+ alternates: ['Franklin Gothic Medium']
13162
+ }, {
13163
+ name: 'Fira Sans Condensed'
13164
+ }, {
13165
+ name: 'Helvetica Condensed'
13166
+ }, {
13167
+ name: 'Impact'
13168
+ }, {
13169
+ name: 'News Gothic MT'
13170
+ }, {
13171
+ name: 'Oswald'
13172
+ }, {
13173
+ name: 'PT Sans Narrow'
13174
+ }, {
13175
+ name: 'Roboto Condensed'
13176
+ }, {
13177
+ name: 'Trade Gothic',
13178
+ alternates: ['Trade Gothic Next']
13179
+ }, {
13180
+ name: 'Univers Condensed'
13181
+ }];
13182
+
13183
+ var mono = [{
13184
+ name: 'Andale Mono'
13185
+ }, {
13186
+ name: 'Cascadia Code',
13187
+ alternates: ['Cascadia Mono']
13188
+ }, {
13189
+ name: 'Consolas'
13190
+ }, {
13191
+ name: 'Courier New'
13192
+ }, {
13193
+ name: 'DejaVu Sans Mono'
13194
+ }, {
13195
+ name: 'Fira Mono',
13196
+ alternates: ['Fira Code']
13197
+ }, {
13198
+ name: 'IBM Plex Mono'
13199
+ }, {
13200
+ name: 'Inconsolata'
13201
+ }, {
13202
+ name: 'JetBrains Mono'
13203
+ }, {
13204
+ name: 'Liberation Mono'
13205
+ }, {
13206
+ name: 'Lucida Console'
13207
+ }, {
13208
+ name: 'Menlo'
13209
+ }, {
13210
+ name: 'Monaco'
13211
+ }, {
13212
+ name: 'Roboto Mono'
13213
+ }, {
13214
+ name: 'SF Mono'
13215
+ }, {
13216
+ name: 'Source Code Pro'
13217
+ }, {
13218
+ name: 'Ubuntu Mono'
13219
+ }];
13220
+
13221
+ var serif = [{
13222
+ name: 'NYTCheltenham'
13223
+ }, {
13224
+ name: 'Adobe Caslon Pro',
13225
+ alternates: ['Caslon']
13226
+ }, {
13227
+ name: 'Adobe Garamond Pro',
13228
+ alternates: ['Garamond']
13229
+ }, {
13230
+ name: 'Baskerville'
13231
+ }, {
13232
+ name: 'Bodoni',
13233
+ alternates: ['Bodoni MT', 'Bodoni 72']
13234
+ }, {
13235
+ name: 'Book Antiqua'
13236
+ }, {
13237
+ name: 'Bookman Old Style'
13238
+ }, {
13239
+ name: 'Cambria'
13240
+ }, {
13241
+ name: 'Century Schoolbook'
13242
+ }, {
13243
+ name: 'Clarendon'
13244
+ }, {
13245
+ name: 'Cochin'
13246
+ }, {
13247
+ name: 'Constantia'
13248
+ }, {
13249
+ name: 'Crimson Pro',
13250
+ alternates: ['Crimson Text']
13251
+ }, {
13252
+ name: 'DejaVu Serif'
13253
+ }, {
13254
+ name: 'Didot'
13255
+ }, {
13256
+ name: 'EB Garamond'
13257
+ }, {
13258
+ name: 'Georgia'
13259
+ }, {
13260
+ name: 'Goudy Old Style'
13261
+ }, {
13262
+ name: 'Hoefler Text'
13263
+ }, {
13264
+ name: 'IBM Plex Serif'
13265
+ }, {
13266
+ name: 'IM Fell'
13267
+ }, {
13268
+ name: 'Liberation Serif'
13269
+ }, {
13270
+ name: 'Libre Baskerville'
13271
+ }, {
13272
+ name: 'Lora'
13273
+ }, {
13274
+ name: 'Merriweather'
13275
+ }, {
13276
+ name: 'Minion Pro'
13277
+ }, {
13278
+ name: 'Noto Serif'
13279
+ }, {
13280
+ name: 'Palatino',
13281
+ alternates: ['Palatino Linotype']
13282
+ }, {
13283
+ name: 'Perpetua'
13284
+ }, {
13285
+ name: 'Playfair Display'
13286
+ }, {
13287
+ name: 'PT Serif'
13288
+ }, {
13289
+ name: 'Roboto Slab'
13290
+ }, {
13291
+ name: 'Rockwell'
13292
+ }, {
13293
+ name: 'Sabon',
13294
+ alternates: ['Sabon Next']
13295
+ }, {
13296
+ name: 'Sitka'
13297
+ }, {
13298
+ name: 'Source Serif Pro',
13299
+ alternates: ['Source Serif 4']
13300
+ }, {
13301
+ name: 'Times New Roman'
13302
+ }, {
13303
+ name: 'Zilla Slab'
13304
+ }];
13305
+
13306
+ var fontCategories = [{
13307
+ name: 'Sans serif',
13308
+ fonts: sans
13309
+ }, {
13310
+ name: 'Condensed',
13311
+ fonts: condensed
13312
+ }, {
13313
+ name: 'Serif',
13314
+ fonts: serif
13315
+ }, {
13316
+ name: 'Monospace',
13317
+ fonts: mono
13318
+ }];
13319
+
13320
+ var cachedFonts = null;
13321
+ var cachedFontStyles = {};
13322
+ var fontWeights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
13323
+ var fontStyles = ['normal', 'italic'];
13324
+ var preferredWeights = [400, 700, 900, 500, 300, 600, 800, 200, 100];
13325
+ var weightNames = {
13326
+ 100: 'Thin',
13327
+ 200: 'Extra Light',
13328
+ 300: 'Light',
13329
+ 400: 'Regular',
13330
+ 500: 'Medium',
13331
+ 600: 'Semi Bold',
13332
+ 700: 'Bold',
13333
+ 800: 'Extra Bold',
13334
+ 900: 'Black'
13335
+ };
13336
+
13337
+ // Return a list of the above fonts that are installed on the user's computer
13338
+ // sorted by category, alphabetically within each category.
13339
+ function getInstalledFonts() {
13340
+ if (!cachedFonts) {
13341
+ cachedFonts = detectInstalledFonts();
13342
+ }
13343
+ return cachedFonts;
13344
+ }
13345
+
13346
+ function getFontStyleVariants(fontName) {
13347
+ if (!fontName) return [];
13348
+ if (!cachedFontStyles[fontName]) {
13349
+ cachedFontStyles[fontName] = detectFontStyleVariants(fontName);
13350
+ }
13351
+ return cachedFontStyles[fontName];
13352
+ }
13353
+
13354
+ function detectInstalledFonts() {
13355
+ var detector = getCanvasFontDetector();
13356
+ return fontCategories.map(function(category) {
13357
+ var fonts = category.fonts.filter(function(font) {
13358
+ return detector && fontIsInstalled(font, detector);
13359
+ });
13360
+ return {
13361
+ name: category.name,
13362
+ fonts: fonts.map(function(font) {
13363
+ return font.name;
13364
+ }).sort(sortNames)
13365
+ };
13366
+ }).filter(function(category) {
13367
+ return category.fonts.length > 0;
13368
+ });
13369
+ }
13370
+
13371
+ function fontIsInstalled(font, detector) {
13372
+ return getFontNames(font).some(function(name) {
13373
+ return detector(name);
13374
+ });
13375
+ }
13376
+
13377
+ function getFontNames(font) {
13378
+ var alternates = font.alternates || [];
13379
+ if (!Array.isArray(alternates)) {
13380
+ alternates = [alternates];
13381
+ }
13382
+ return [font.name].concat(alternates);
13383
+ }
13384
+
13385
+ function sortNames(a, b) {
13386
+ return a.toLowerCase() < b.toLowerCase() ? -1 :
13387
+ a.toLowerCase() > b.toLowerCase() ? 1 : 0;
13388
+ }
13389
+
13390
+ function getCanvasFontDetector() {
13391
+ var canvas, ctx, baselines;
13392
+ if (typeof document == 'undefined') return null;
13393
+ canvas = document.createElement('canvas');
13394
+ ctx = canvas && canvas.getContext && canvas.getContext('2d');
13395
+ if (!ctx) return null;
13396
+ baselines = getBaselineWidths(ctx);
13397
+ return function(fontName) {
13398
+ return fontIsDetectable(ctx, baselines, fontName);
13399
+ };
13400
+ }
13401
+
13402
+ function getBaselineWidths(ctx) {
13403
+ return {
13404
+ monospace: measureFont(ctx, 'monospace'),
13405
+ 'sans-serif': measureFont(ctx, 'sans-serif'),
13406
+ serif: measureFont(ctx, 'serif')
13407
+ };
13408
+ }
13409
+
13410
+ function fontIsDetectable(ctx, baselines, fontName) {
13411
+ return Object.keys(baselines).some(function(generic) {
13412
+ return measureFont(ctx, quoteFontName(fontName) + ',' + generic) != baselines[generic];
13413
+ });
13414
+ }
13415
+
13416
+ function measureFont(ctx, fontFamily) {
13417
+ ctx.font = '72px ' + fontFamily;
13418
+ return ctx.measureText('mmmmmmmmmmlliMW@#0123456789').width;
13419
+ }
13420
+
13421
+ function quoteFontName(name) {
13422
+ return '"' + String(name).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
13423
+ }
13424
+
13425
+ function detectFontStyleVariants(fontName) {
13426
+ var canvas, ctx, groups;
13427
+ if (typeof document == 'undefined') return getDefaultFontStyleVariants();
13428
+ canvas = document.createElement('canvas');
13429
+ ctx = canvas && canvas.getContext && canvas.getContext('2d');
13430
+ if (!ctx) return getDefaultFontStyleVariants();
13431
+ groups = {};
13432
+ fontStyles.forEach(function(style) {
13433
+ fontWeights.forEach(function(weight) {
13434
+ var sig = measureFontVariant(ctx, fontName, weight, style);
13435
+ var key = style + '|' + sig;
13436
+ if (!groups[key]) {
13437
+ groups[key] = {
13438
+ style: style,
13439
+ weights: []
13440
+ };
13441
+ }
13442
+ groups[key].weights.push(weight);
13443
+ });
13444
+ });
13445
+ var variants = Object.keys(groups).map(function(key) {
13446
+ var group = groups[key];
13447
+ return getFontStyleVariant(getPreferredWeight(group.weights), group.style);
13448
+ }).sort(sortFontStyleVariants);
13449
+ return variants.length > 0 ? variants : getDefaultFontStyleVariants();
13450
+ }
13451
+
13452
+ function getDefaultFontStyleVariants() {
13453
+ return [getFontStyleVariant(400, 'normal')];
13454
+ }
13455
+
13456
+ function getFontStyleVariant(weight, style) {
13457
+ return {
13458
+ label: getFontStyleLabel(weight, style),
13459
+ value: style + '|' + weight,
13460
+ style: style,
13461
+ weight: String(weight)
13462
+ };
13463
+ }
13464
+
13465
+ function getPreferredWeight(weights) {
13466
+ for (var i=0; i<preferredWeights.length; i++) {
13467
+ if (weights.indexOf(preferredWeights[i]) > -1) {
13468
+ return preferredWeights[i];
13469
+ }
13470
+ }
13471
+ return weights[0];
13472
+ }
13473
+
13474
+ function sortFontStyleVariants(a, b) {
13475
+ if (a.style != b.style) {
13476
+ return a.style == 'normal' ? -1 : 1;
13477
+ }
13478
+ return Number(a.weight) - Number(b.weight);
13479
+ }
13480
+
13481
+ function getFontStyleLabel(weight, style) {
13482
+ var name = weightNames[weight] || String(weight);
13483
+ if (style == 'italic') {
13484
+ return weight == 400 ? 'Italic' : name + ' Italic';
13485
+ }
13486
+ return name;
13487
+ }
13488
+
13489
+ function measureFontVariant(ctx, fontName, weight, style) {
13490
+ return [
13491
+ measureFontSample(ctx, fontName, weight, style, 'Hamburgefonts 0123456789'),
13492
+ measureFontSample(ctx, fontName, weight, style, 'mmmmmmmmmmlliMW@#'),
13493
+ measureFontSample(ctx, fontName, weight, style, 'Quick brown fox')
13494
+ ].join('|');
13495
+ }
13496
+
13497
+ function measureFontSample(ctx, fontName, weight, style, text) {
13498
+ var metrics;
13499
+ ctx.font = style + ' ' + weight + ' 72px ' + quoteFontName(fontName) + ',sans-serif';
13500
+ metrics = ctx.measureText(text);
13501
+ return [
13502
+ metrics.width,
13503
+ metrics.actualBoundingBoxLeft || 0,
13504
+ metrics.actualBoundingBoxRight || 0,
13505
+ metrics.actualBoundingBoxAscent || 0,
13506
+ metrics.actualBoundingBoxDescent || 0
13507
+ ].map(roundMetric).join(',');
13508
+ }
13509
+
13510
+ function roundMetric(val) {
13511
+ return Math.round(val * 100) / 100;
13512
+ }
13513
+
13514
+ var labelTextField = 'label-text';
13515
+ var fontField = 'font-family';
13516
+ var fontSizeField = 'font-size';
13517
+ var fontStyleField = 'font-style';
13518
+ var fontWeightField = 'font-weight';
13519
+ var fillField = 'fill';
13520
+ var cssField = 'css';
13521
+ var iconField = 'icon';
13522
+ var iconSizeField = 'icon-size';
13523
+ var defaultFontSize = 12;
13524
+ var defaultFontStyle = 'normal';
13525
+ var defaultFontWeight = '400';
13526
+ var defaultLabelColor = '#000000';
13527
+ var defaultIconSize = 5;
13528
+ var labelStyleMode = 'label_style';
13529
+ var labelStylePanelMode = 'label_style_tool';
13530
+ var savedStylesKey = 'label_styles';
13531
+ var labelPositionFields = internal.labelPositionFields;
13532
+ var savedStyleFields = [
13533
+ fontField,
13534
+ fontSizeField,
13535
+ fontStyleField,
13536
+ fontWeightField,
13537
+ fillField,
13538
+ cssField,
13539
+ 'label-pos',
13540
+ iconField,
13541
+ iconSizeField
13542
+ ];
13543
+ var labelPositions = ['nw', 'n', 'ne', 'w', 'c', 'e', 'sw', 's', 'se'];
13544
+ var iconTypes = [{
13545
+ name: ''
13546
+ }, {
13547
+ name: 'circle'
13548
+ }, {
13549
+ name: 'square'
13550
+ }, {
13551
+ name: 'ring'
13552
+ }, {
13553
+ name: 'star'
13554
+ }];
13555
+ var iconButtonSymbols = {
13556
+ '': '<line x1="4.25" y1="4.25" x2="11.75" y2="11.75"></line><line x1="11.75" y1="4.25" x2="4.25" y2="11.75"></line>',
13557
+ circle: '<circle cx="8" cy="8" r="4.25"></circle>',
13558
+ square: '<rect x="4" y="4" width="8" height="8"></rect>',
13559
+ ring: '<circle cx="8" cy="8" r="3.8"></circle>',
13560
+ star: '<path d="M8 3.2l1.18 2.92 3.14.22-2.42 2 .76 3.06L8 9.75 5.34 11.4l.76-3.06-2.42-2 3.14-.22L8 3.2z"></path>'
13561
+ };
13562
+
13563
+ function LabelTool(gui) {
13564
+ // button is only visible when label editing is available
13565
+ // clicking the button opens a panel for applying styles to labels
13566
+ var textBtn = gui.buttons.addButton('#text-tool-icon').addClass('menu-btn pointer-btn');
13567
+ var parent = gui.container.findChild('.mshp-main-map');
13568
+ var panel = El('div').addClass('label-style-panel rollover').appendTo(parent).hide();
13569
+ var createPanel = El('div').addClass('label-style-panel label-create-panel rollover').appendTo(parent).hide();
13570
+ var createFieldSelect, createExprInput, createCopyCheckbox, createBtn;
13571
+ var styleSelect, fontSelect, fontStyleSelect, fontSizeText, colorChit, colorInput, colorPicker, sbCanvas, hueCanvas, sbMarker, hueMarker, pickerHsbInputs, cssInput, posBtns, iconBtns, iconSizeText, hit;
13572
+ var fontOptionsRendered = false;
13573
+ var pickerColor = {h: 0, s: 0, b: 0};
13574
+ var pendingStyleHistory = null;
13575
+
13576
+ initCreatePanel();
13577
+ initPanel();
13578
+ gui.addMode(labelStylePanelMode, turnOn, turnOff, textBtn);
13579
+ gui.model.on('update', updateVisibility);
13580
+ gui.model.on('update', function() {
13581
+ setTimeout(updateSelectionDisplay, 0);
13582
+ });
13583
+ gui.on('interaction_mode_change', function(e) {
13584
+ if (panel.visible() && e.mode != labelStyleMode && gui.getMode() == labelStylePanelMode) {
13585
+ gui.clearMode();
13586
+ }
13587
+ });
13588
+
13589
+ hit = gui.map.getHitControl && gui.map.getHitControl();
13590
+ if (hit) {
13591
+ hit.on('change', function(e) {
13592
+ if (e.mode == labelStyleMode) {
13593
+ updateSelectionDisplay();
13594
+ updateControls();
13595
+ }
13596
+ });
13597
+ }
13598
+
13599
+ updateVisibility();
13600
+
13601
+ function initPanel() {
13602
+ var header = El('div').addClass('label-style-panel-title').appendTo(panel).text('Label styles');
13603
+ El('button').addClass('label-style-close').appendTo(header).text('×').on('click', function() {
13604
+ gui.clearMode();
13605
+ });
13606
+
13607
+ var selectRow = El('div').addClass('label-style-row label-style-selection-row').appendTo(panel);
13608
+ El('button').appendTo(selectRow).text('Select all').on('click', selectAllLabels);
13609
+ El('button').appendTo(selectRow).text('Clear').on('click', clearSelection);
13610
+
13611
+ var fontRow = El('label').addClass('label-style-row').appendTo(panel);
13612
+ El('span').appendTo(fontRow).text('Font');
13613
+ fontSelect = El('select').appendTo(fontRow).on('change', function() {
13614
+ if (fontSelect.node().value) {
13615
+ applyFont(fontSelect.node().value);
13616
+ }
13617
+ });
13618
+
13619
+ var fontStyleRow = El('label').addClass('label-style-row').appendTo(panel);
13620
+ El('span').appendTo(fontStyleRow).text('Font style');
13621
+ fontStyleSelect = El('select').appendTo(fontStyleRow).on('change', function() {
13622
+ if (fontStyleSelect.node().value) {
13623
+ applyFontStyleVariant(fontStyleSelect.node().value);
13624
+ }
13625
+ });
13626
+
13627
+ var colorSizeRow = El('div').addClass('label-style-row label-split-row').appendTo(panel);
13628
+ var colorRow = El('div').addClass('label-split-cell label-color-row').appendTo(colorSizeRow);
13629
+ El('span').appendTo(colorRow).text('Color');
13630
+ colorChit = makePanelButton(colorRow, '', toggleColorPicker).addClass('label-color-chit');
13631
+ colorInput = El('input').attr('type', 'text').appendTo(colorRow).on('change', function() {
13632
+ var color = colorInput.node().value.trim();
13633
+ if (color) {
13634
+ if (isHexColor(color)) {
13635
+ setPickerColor(hexToHsb(color));
13636
+ }
13637
+ applyLabelColor(color);
13638
+ }
13639
+ });
13640
+ initColorPicker(colorRow);
13641
+
13642
+ var fontSizeRow = El('div').addClass('label-split-cell label-size-row').appendTo(colorSizeRow);
13643
+ El('span').appendTo(fontSizeRow).text('Font size');
13644
+ makePanelButton(fontSizeRow, '−', function() {
13645
+ nudgeFontSize(-1);
13646
+ });
13647
+ fontSizeText = El('span').addClass('label-size-value').appendTo(fontSizeRow);
13648
+ makePanelButton(fontSizeRow, '+', function() {
13649
+ nudgeFontSize(1);
13650
+ });
13651
+
13652
+ var cssRow = El('label').addClass('label-style-row label-css-row').appendTo(panel);
13653
+ El('span').appendTo(cssRow).text('Inline CSS');
13654
+ cssInput = El('input').attr('type', 'text').appendTo(cssRow).on('change', function() {
13655
+ applyInlineCss(cssInput.node().value.trim());
13656
+ });
13657
+
13658
+ var iconSizeRow = El('div').addClass('label-style-row label-split-row').appendTo(panel);
13659
+ var iconRow = El('div').addClass('label-split-cell').appendTo(iconSizeRow);
13660
+ El('div').addClass('label-style-row-label').appendTo(iconRow).text('Icon');
13661
+ var iconGroup = El('div').addClass('label-icon-buttons').appendTo(iconRow);
13662
+ iconBtns = {};
13663
+ iconTypes.forEach(function(icon) {
13664
+ var btn = makePanelButton(iconGroup, '', function() {
13665
+ applyIcon(icon.name);
13666
+ })
13667
+ .attr('data-icon', icon.name || 'none')
13668
+ .attr('title', icon.name || 'no icon');
13669
+ iconBtns[icon.name] = btn;
13670
+ appendIconButtonSymbol(btn, icon.name);
13671
+ });
13672
+
13673
+ var sizeRow = El('div').addClass('label-split-cell label-icon-size-row').appendTo(iconSizeRow);
13674
+ El('span').appendTo(sizeRow).text('Icon size');
13675
+ makePanelButton(sizeRow, '−', function() {
13676
+ nudgeIconSize(-1);
13677
+ });
13678
+ iconSizeText = El('span').addClass('label-icon-size-value').appendTo(sizeRow);
13679
+ makePanelButton(sizeRow, '+', function() {
13680
+ nudgeIconSize(1);
13681
+ });
13682
+
13683
+ var posRow = El('div').addClass('label-style-row').appendTo(panel);
13684
+ El('div').addClass('label-style-row-label').appendTo(posRow).text('Position');
13685
+ var grid = El('div').addClass('label-position-grid').appendTo(posRow);
13686
+ posBtns = {};
13687
+ labelPositions.forEach(function(pos) {
13688
+ posBtns[pos] = makePanelButton(grid, '', function() {
13689
+ applyLabelPosition(pos);
13690
+ })
13691
+ .attr('data-position', pos)
13692
+ .attr('title', pos);
13693
+ });
13694
+
13695
+ var savedRow = El('div').addClass('label-style-row label-saved-style-row').appendTo(panel);
13696
+ El('span').appendTo(savedRow).text('Presets');
13697
+ styleSelect = El('select').appendTo(savedRow).on('change', function() {
13698
+ if (styleSelect.node().value) {
13699
+ applySavedStyle(styleSelect.node().value);
13700
+ }
13701
+ updateSavedStyleControls();
13702
+ });
13703
+ El('button').appendTo(savedRow).text('Save preset').on('click', saveCurrentStyle);
13704
+ El('button').appendTo(savedRow).text('Delete').on('click', deleteSelectedStyle);
13705
+ renderSavedStyles();
13706
+ }
13707
+
13708
+ function initCreatePanel() {
13709
+ var header = El('div').addClass('label-style-panel-title').appendTo(createPanel).text('Create labels');
13710
+ El('button').addClass('label-style-close').appendTo(header).text('×').on('click', function() {
13711
+ gui.clearMode();
13712
+ });
13713
+
13714
+ var fieldRow = El('label').addClass('label-style-row').appendTo(createPanel);
13715
+ El('span').appendTo(fieldRow).text('Label field');
13716
+ createFieldSelect = El('select').appendTo(fieldRow).on('change', function() {
13717
+ var field = createFieldSelect.node().value;
13718
+ if (field) {
13719
+ createExprInput.node().value = getFieldExpression(field);
13720
+ }
13721
+ updateCreateButton();
13722
+ });
13723
+
13724
+ var exprRow = El('label').addClass('label-style-row label-create-expression-row').appendTo(createPanel);
13725
+ El('span').appendTo(exprRow).text('or expression');
13726
+ createExprInput = El('input')
13727
+ .attr('type', 'text')
13728
+ .appendTo(exprRow)
13729
+ .on('input', updateCreateButton)
13730
+ .on('change', updateCreateButton);
13731
+
13732
+ var copyRow = El('label').addClass('label-style-row label-create-copy-row').appendTo(createPanel);
13733
+ createCopyCheckbox = El('input').attr('type', 'checkbox').appendTo(copyRow);
13734
+ El('span').appendTo(copyRow).text('Create as new layer');
13735
+
13736
+ var btnRow = El('div').addClass('label-style-row').appendTo(createPanel);
13737
+ createBtn = El('button').appendTo(btnRow).text('Create labels').on('click', createLabels);
13738
+ }
13739
+
13740
+ function appendIconButtonSymbol(btn, iconName) {
13741
+ var svg = '<svg class="label-icon-symbol" viewBox="0 0 16 16" aria-hidden="true">' +
13742
+ iconButtonSymbols[iconName] + '</svg>';
13743
+ El(svg).appendTo(btn);
13744
+ }
13745
+
13746
+ function makePanelButton(parent, label, action) {
13747
+ return El('div')
13748
+ .addClass('label-panel-btn')
13749
+ .attr('role', 'button')
13750
+ .attr('tabindex', '0')
13751
+ .appendTo(parent)
13752
+ .text(label)
13753
+ .on('click', function(e) {
13754
+ if (this.classList.contains('disabled')) return;
13755
+ action(e);
13756
+ })
13757
+ .on('keydown', function(e) {
13758
+ if (e.key == 'Enter' || e.key == ' ') {
13759
+ e.preventDefault();
13760
+ if (!this.classList.contains('disabled')) action(e);
13761
+ }
13762
+ });
13763
+ }
13764
+
13765
+ function setPanelButtonDisabled(el, disabled) {
13766
+ el.classed('disabled', !!disabled)
13767
+ .attr('aria-disabled', disabled ? 'true' : 'false')
13768
+ .attr('tabindex', disabled ? '-1' : '0');
13769
+ }
13770
+
13771
+ function turnOn() {
13772
+ if (!activeLayerIsPointLayer()) {
13773
+ gui.clearMode();
13774
+ return;
13775
+ }
13776
+ if (!activeLayerHasLabels()) {
13777
+ showCreatePanel();
13778
+ return;
13779
+ }
13780
+ showStylePanel();
13781
+ }
13782
+
13783
+ function showStylePanel() {
13784
+ renderFontOptions();
13785
+ createPanel.hide();
13786
+ gui.interaction.setMode(labelStyleMode);
13787
+ gui.state.label_style_panel_open = true;
13788
+ panel.show();
13789
+ textBtn.addClass('selected');
13790
+ updateControls();
13791
+ updateSelectionDisplay();
13792
+ }
13793
+
13794
+ function showCreatePanel() {
13795
+ panel.hide();
13796
+ hideColorPicker();
13797
+ if (gui.interaction.getMode() == labelStyleMode) {
13798
+ gui.interaction.turnOff();
13799
+ }
13800
+ gui.state.label_style_panel_open = true;
13801
+ createPanel.show();
13802
+ textBtn.addClass('selected');
13803
+ renderCreateFields();
13804
+ updateCreateButton();
13805
+ }
13806
+
13807
+ function turnOff() {
13808
+ if (panel.visible()) {
13809
+ flushPendingStyleHistory();
13810
+ }
13811
+ panel.hide();
13812
+ createPanel.hide();
13813
+ hideColorPicker();
13814
+ gui.state.label_style_panel_open = false;
13815
+ textBtn.removeClass('selected');
13816
+ clearSelectionDisplay();
13817
+ if (hit) hit.clearSelection();
13818
+ if (gui.interaction.getMode() == labelStyleMode) {
13819
+ gui.interaction.turnOff();
13820
+ }
13821
+ }
13822
+
13823
+ function updateVisibility() {
13824
+ var enabled = activeLayerIsPointLayer();
13825
+ textBtn.classed('disabled', !enabled);
13826
+ textBtn[enabled ? 'show' : 'hide']();
13827
+ if (!enabled && gui.getMode() == labelStylePanelMode) {
13828
+ gui.clearMode();
13829
+ }
13830
+ }
13831
+
13832
+ function activeLayerHasLabels() {
13833
+ var active = gui.model.getActiveLayer();
13834
+ return !!(active && internal.layerHasLabels(active.layer));
13835
+ }
13836
+
13837
+ function activeLayerIsPointLayer() {
13838
+ var active = gui.model.getActiveLayer();
13839
+ return !!(active && active.layer && active.layer.geometry_type == 'point');
13840
+ }
13841
+
13842
+ function renderFontOptions() {
13843
+ if (fontOptionsRendered) return;
13844
+ fontOptionsRendered = true;
13845
+ fontSelect.empty();
13846
+ El('option').attr('value', '').appendTo(fontSelect).text('');
13847
+ getInstalledFonts().forEach(function(group) {
13848
+ var optgroup = El('optgroup').attr('label', group.name).appendTo(fontSelect);
13849
+ group.fonts.forEach(function(fontName) {
13850
+ El('option').attr('value', fontName).appendTo(optgroup).text(fontName);
13851
+ });
13852
+ });
13853
+ }
13854
+
13855
+ function renderCreateFields() {
13856
+ var lyr = getActiveLayer();
13857
+ var records = lyr && lyr.data ? lyr.data.getRecords() : [];
13858
+ var fields = lyr && lyr.data ? lyr.data.getFields().filter(function(field) {
13859
+ return internal.getColumnType(field, records) == 'string';
13860
+ }) : [];
13861
+ var value = createFieldSelect.node().value;
13862
+ createFieldSelect.empty();
13863
+ El('option').attr('value', '').appendTo(createFieldSelect).text('');
13864
+ fields.forEach(function(field) {
13865
+ El('option').attr('value', field).appendTo(createFieldSelect).text(field);
13866
+ });
13867
+ createFieldSelect.node().disabled = fields.length === 0;
13868
+ createFieldSelect.node().value = fields.indexOf(value) > -1 ? value : '';
13869
+ }
13870
+
13871
+ function updateCreateButton() {
13872
+ createBtn.node().disabled = createExprInput.node().value.trim() === '';
13873
+ }
13874
+
13875
+ function getFieldExpression(field) {
13876
+ return 'd[' + JSON.stringify(field) + ']';
13877
+ }
13878
+
13879
+ function createLabels() {
13880
+ var active = gui.model.getActiveLayer();
13881
+ var srcLyr = active && active.layer;
13882
+ var dataset = active && active.dataset;
13883
+ var expr = createExprInput.node().value.trim();
13884
+ var lyr;
13885
+ if (!srcLyr || srcLyr.geometry_type != 'point' || !expr) return;
13886
+ lyr = createCopyCheckbox.node().checked ? makeLabelLayerCopy(srcLyr, dataset) : srcLyr;
13887
+ if (!applyLabelText(lyr, expr)) return;
13888
+ if (lyr != srcLyr) {
13889
+ addLabelLayerCopy(lyr, srcLyr, dataset);
13890
+ recordCreateLabelsCommand(expr, lyr);
13891
+ selectLabelLayerCopy(lyr, dataset);
13892
+ } else {
13893
+ gui.model.updated({style: true, same_table: true});
13894
+ recordCreateLabelsCommand(expr);
13895
+ }
13896
+ showStylePanel();
13897
+ setTimeout(function() {
13898
+ selectAllLabels();
13899
+ }, 0);
13900
+ }
13901
+
13902
+ function makeLabelLayerCopy(srcLyr, dataset) {
13903
+ var lyr = internal.copyLayer(srcLyr);
13904
+ delete lyr.gui;
13905
+ lyr.name = 'labels';
13906
+ return lyr;
13907
+ }
13908
+
13909
+ function addLabelLayerCopy(lyr, srcLyr, dataset) {
13910
+ var i = dataset.layers.indexOf(srcLyr);
13911
+ internal.noteDatasetWillChange(dataset, {operation: 'createLabelLayer', unit: 'layers'});
13912
+ dataset.layers.splice(i + 1, 0, lyr);
13913
+ internal.markDatasetChanged(dataset, {operation: 'createLabelLayer', unit: 'layers'});
13914
+ }
13915
+
13916
+ function selectLabelLayerCopy(lyr, dataset) {
13917
+ gui.model.setDefaultTarget([lyr], dataset);
13918
+ gui.model.updated({select: true});
13919
+ }
13920
+
13921
+ function applyLabelText(lyr, expr) {
13922
+ var table = internal.getLayerDataTable(lyr);
13923
+ var hasField = table.fieldExists(labelTextField);
13924
+ var records = table.getRecords();
13925
+ var accessor;
13926
+ try {
13927
+ accessor = internal.getSymbolPropertyAccessor(expr, labelTextField, lyr);
13928
+ } catch(e) {
13929
+ showPopupAlert(e.message || 'Invalid label expression', 'Create labels');
13930
+ return false;
13931
+ }
13932
+ if (hasField) {
13933
+ table.captureFieldsBefore([labelTextField], {operation: 'create-labels'});
13934
+ } else {
13935
+ table.captureSchemaBefore({operation: 'create-labels', fields: [labelTextField]});
13936
+ }
13937
+ records.forEach(function(rec, i) {
13938
+ if (!rec) rec = records[i] = {};
13939
+ rec[labelTextField] = accessor(i);
13940
+ });
13941
+ if (hasField) {
13942
+ table.markFieldsChanged([labelTextField], {operation: 'create-labels'});
13943
+ } else {
13944
+ table.markSchemaChanged({operation: 'create-labels', fields: [labelTextField]});
13945
+ }
13946
+ return true;
13947
+ }
13948
+
13949
+ function updatePendingStyleHistory(lyr, ids, fields) {
13950
+ if (!gui.session || !lyr || ids.length === 0) return;
13951
+ if (pendingStyleHistory && (pendingStyleHistory.layer != lyr ||
13952
+ !sameIds(pendingStyleHistory.ids, ids))) {
13953
+ flushPendingStyleHistory();
13954
+ }
13955
+ if (!pendingStyleHistory) {
13956
+ pendingStyleHistory = {
13957
+ layer: lyr,
13958
+ ids: ids.concat(),
13959
+ fields: []
13960
+ };
13961
+ }
13962
+ getHistoryStyleFields(fields).forEach(function(field) {
13963
+ if (pendingStyleHistory.fields.indexOf(field) == -1) {
13964
+ pendingStyleHistory.fields.push(field);
13965
+ }
13966
+ });
13967
+ }
13968
+
13969
+ function flushPendingStyleHistory() {
13970
+ var cmd;
13971
+ if (!pendingStyleHistory || !gui.session) return;
13972
+ cmd = getStyleHistoryCommand(pendingStyleHistory);
13973
+ pendingStyleHistory = null;
13974
+ if (cmd) {
13975
+ gui.session.consoleCommands(cmd);
13976
+ }
13977
+ }
13978
+
13979
+ function getStyleHistoryCommand(entry) {
13980
+ var table = entry.layer.data;
13981
+ var records = table && table.getRecords();
13982
+ var fields = entry.fields.filter(function(field) {
13983
+ return field != labelTextField;
13984
+ });
13985
+ var parts = ['-style'];
13986
+ var values;
13987
+ if (!records || fields.length === 0) return '';
13988
+ values = getHistoryStyleValues(records, entry.ids, fields);
13989
+ Object.keys(values).forEach(function(field) {
13990
+ parts.push(field + '=' + quoteCommandValue(values[field]));
13991
+ });
13992
+ if (parts.length == 1) return '';
13993
+ if (getActiveLayer() != entry.layer) {
13994
+ parts.push('target=' + internal.formatOptionValue(internal.getLayerTargetId(gui.model, entry.layer)));
13995
+ }
13996
+ if (entry.ids.length < internal.getFeatureCount(entry.layer)) {
13997
+ parts.push('where=' + quoteCommandValue(getIdsWhereExpression(entry.ids)));
13998
+ }
13999
+ return parts.join(' ');
14000
+ }
14001
+
14002
+ function getHistoryStyleValues(records, ids, fields) {
14003
+ return fields.reduce(function(memo, field) {
14004
+ var value = getCommonRecordValue(records, ids, field);
14005
+ if (value !== undefined && value !== null && value !== '') {
14006
+ memo[field] = value;
14007
+ }
14008
+ return memo;
14009
+ }, {});
14010
+ }
14011
+
14012
+ function getCommonRecordValue(records, ids, field) {
14013
+ var value, val;
14014
+ for (var i=0; i<ids.length; i++) {
14015
+ val = records[ids[i]] && records[ids[i]][field];
14016
+ if (i === 0) {
14017
+ value = val;
14018
+ } else if (val != value) {
14019
+ return undefined;
14020
+ }
14021
+ }
14022
+ return value;
14023
+ }
14024
+
14025
+ function getHistoryStyleFields(fields) {
14026
+ var out = [];
14027
+ fields.forEach(function(field) {
14028
+ if (field == 'dx' || field == 'dy' || field == 'text-anchor') return;
14029
+ if (out.indexOf(field) == -1) out.push(field);
14030
+ });
14031
+ return out;
14032
+ }
14033
+
14034
+ function getIdsWhereExpression(ids) {
14035
+ return '[' + ids.join(',') + '].indexOf($.id)>-1';
14036
+ }
14037
+
14038
+ function sameIds(a, b) {
14039
+ if (!a || !b || a.length != b.length) return false;
14040
+ for (var i=0; i<a.length; i++) {
14041
+ if (a[i] != b[i]) return false;
14042
+ }
14043
+ return true;
14044
+ }
14045
+
14046
+ function recordCreateLabelsCommand(expr, copyLyr) {
14047
+ var cmd = '';
14048
+ if (!gui.session) return;
14049
+ if (copyLyr) {
14050
+ cmd += '-filter true + name=' + internal.formatOptionValue(copyLyr.name) + ' ';
14051
+ }
14052
+ cmd += '-style label-text=' + quoteCommandValue(expr);
14053
+ gui.session.consoleCommands(cmd);
14054
+ }
14055
+
14056
+ function quoteCommandValue(str) {
14057
+ return "'" + String(str).replace(/'/g, "\\'") + "'";
14058
+ }
14059
+
14060
+ function selectAllLabels() {
14061
+ var lyr = getActiveLayer();
14062
+ var ids = [];
14063
+ if (!hit || !lyr) return;
14064
+ for (var i=0, n=internal.getFeatureCount(lyr); i<n; i++) {
14065
+ ids.push(i);
14066
+ }
14067
+ hit.clearSelection();
14068
+ if (ids.length > 0) {
14069
+ hit.addSelectionIds(ids);
14070
+ }
14071
+ updateSelectionDisplay();
14072
+ updateControls();
14073
+ }
14074
+
14075
+ function clearSelection() {
14076
+ if (hit) hit.clearSelection();
14077
+ updateSelectionDisplay();
14078
+ updateControls();
14079
+ }
14080
+
14081
+ function getActiveLayer() {
14082
+ var active = gui.model.getActiveLayer();
14083
+ return active && active.layer;
14084
+ }
14085
+
14086
+ function getActiveTable() {
14087
+ var lyr = getActiveLayer();
14088
+ return lyr && lyr.data || null;
14089
+ }
14090
+
14091
+ function getSelectionIds() {
14092
+ return hit ? hit.getSelectionIds() : [];
14093
+ }
14094
+
14095
+ function updateControls() {
14096
+ var ids = getSelectionIds();
14097
+ var fontVal = getCommonValue(ids, fontField);
14098
+ var fontSizeVal = getCommonValue(ids, fontSizeField, {useDefault: true, defaultValue: defaultFontSize});
14099
+ var fontStyleVal = getCommonValue(ids, fontStyleField, {useDefault: true, defaultValue: defaultFontStyle});
14100
+ var fontWeightVal = getCommonValue(ids, fontWeightField, {useDefault: true, defaultValue: defaultFontWeight});
14101
+ var fillVal = getCommonValue(ids, fillField, {useDefault: true, defaultValue: defaultLabelColor});
14102
+ var cssVal = getCommonValue(ids, cssField);
14103
+ var posVal = getCommonValue(ids, 'label-pos');
14104
+ var iconVal = getCommonValue(ids, iconField);
14105
+ var iconSizeVal = getCommonValue(ids, iconSizeField, {useDefault: true, defaultValue: defaultIconSize});
14106
+ styleSelect.node().disabled = ids.length === 0;
14107
+ updateSavedStyleControls();
14108
+ fontSelect.node().disabled = ids.length === 0;
14109
+ fontSelect.node().value = fontVal;
14110
+ updateFontStyleControls(fontVal, fontStyleVal, fontWeightVal);
14111
+ updateFontSizeControls(ids.length ? fontSizeVal : '');
14112
+ updateColorControls(ids.length ? fillVal : '');
14113
+ updateCssControl(ids.length ? cssVal : '');
14114
+ updatePositionButtons(ids.length ? posVal : '');
14115
+ updateIconButtons(ids.length ? iconVal : '');
14116
+ updateIconSizeControls(ids.length ? iconSizeVal : '');
14117
+ }
14118
+
14119
+ function updatePositionButtons(pos) {
14120
+ var disabled = getSelectionIds().length === 0;
14121
+ labelPositions.forEach(function(name) {
14122
+ posBtns[name].classed('selected', name == pos);
14123
+ setPanelButtonDisabled(posBtns[name], disabled);
14124
+ });
14125
+ }
14126
+
14127
+ function updateFontSizeControls(fontSizeVal) {
14128
+ var disabled = getSelectionIds().length === 0;
14129
+ fontSizeText.text(fontSizeVal || '');
14130
+ panel.findChildren('.label-size-row .label-panel-btn').forEach(function(btn) {
14131
+ setPanelButtonDisabled(btn, disabled);
14132
+ });
14133
+ }
14134
+
14135
+ function updateFontStyleControls(fontName, fontStyleVal, fontWeightVal) {
14136
+ var disabled = getSelectionIds().length === 0 || !fontName;
14137
+ fontStyleSelect.empty();
14138
+ El('option').attr('value', '').appendTo(fontStyleSelect).text('');
14139
+ if (fontName) {
14140
+ getFontStyleVariants(fontName).forEach(function(variant) {
14141
+ El('option').attr('value', variant.value).appendTo(fontStyleSelect).text(variant.label);
14142
+ });
14143
+ }
14144
+ fontStyleSelect.node().disabled = disabled;
14145
+ fontStyleSelect.node().value = fontStyleVal && fontWeightVal ?
14146
+ fontStyleVal + '|' + fontWeightVal : '';
14147
+ }
14148
+
14149
+ function updateColorControls(colorVal) {
14150
+ var disabled = getSelectionIds().length === 0;
14151
+ colorInput.node().disabled = disabled;
14152
+ colorInput.node().value = colorVal || '';
14153
+ setPanelButtonDisabled(colorChit, disabled || !isHexColor(colorVal));
14154
+ colorChit.css('background-color', isHexColor(colorVal) ? colorVal : 'transparent');
14155
+ if (colorPicker.visible()) {
14156
+ return; // avoid HSB -> RGB -> HSB rounding jumps after picker commits
14157
+ }
14158
+ if (isHexColor(colorVal)) {
14159
+ setPickerColor(hexToHsb(colorVal));
14160
+ } else {
14161
+ hideColorPicker();
14162
+ }
14163
+ }
14164
+
14165
+ function updateCssControl(cssVal) {
14166
+ cssInput.node().disabled = getSelectionIds().length === 0;
14167
+ cssInput.node().value = cssVal || '';
14168
+ }
14169
+
14170
+ function updateIconButtons(iconVal) {
14171
+ var disabled = getSelectionIds().length === 0;
14172
+ iconTypes.forEach(function(icon) {
14173
+ iconBtns[icon.name].classed('selected', !disabled && icon.name == iconVal);
14174
+ setPanelButtonDisabled(iconBtns[icon.name], disabled);
14175
+ });
14176
+ }
14177
+
14178
+ function updateIconSizeControls(iconSizeVal) {
14179
+ var disabled = getSelectionIds().length === 0;
14180
+ iconSizeText.text(iconSizeVal || '');
14181
+ panel.findChildren('.label-icon-size-row .label-panel-btn').forEach(function(btn) {
14182
+ setPanelButtonDisabled(btn, disabled);
14183
+ });
14184
+ }
14185
+
14186
+ function updateSavedStyleControls() {
14187
+ var disabled = getSelectionIds().length === 0;
14188
+ var buttons = panel.findChildren('.label-saved-style-row button');
14189
+ buttons.forEach(function(btn, i) {
14190
+ btn.node().disabled = i === 0 ? disabled : disabled || !styleSelect.node().value;
14191
+ });
14192
+ }
14193
+
14194
+ function getCommonValue(ids, field, opts) {
14195
+ var table = getActiveTable();
14196
+ var records = table && table.getRecords();
14197
+ var value, val, hasValue;
14198
+ if (!records || ids.length === 0) return '';
14199
+ for (var i=0; i<ids.length; i++) {
14200
+ val = records[ids[i]] && records[ids[i]][field];
14201
+ if (!val) {
14202
+ if (opts && opts.useDefault) {
14203
+ val = opts.defaultValue;
14204
+ } else {
14205
+ return '';
14206
+ }
14207
+ } else {
14208
+ hasValue = true;
14209
+ }
14210
+ if (i === 0) {
14211
+ value = val;
14212
+ } else if (val != value) {
14213
+ return '';
14214
+ }
14215
+ }
14216
+ return hasValue || opts && opts.useDefault ? value : '';
14217
+ }
14218
+
14219
+ function applyFont(fontName) {
14220
+ applyStyleFields([fontField], function(rec) {
14221
+ rec[fontField] = fontName;
14222
+ });
14223
+ }
14224
+
14225
+ function nudgeFontSize(delta) {
14226
+ var ids = getSelectionIds();
14227
+ var size = getNumericSize(ids, fontSizeField, defaultFontSize);
14228
+ if (ids.length === 0) return;
14229
+ size = Math.max(1, size + delta);
14230
+ applyStyleFields([fontSizeField], function(rec) {
14231
+ rec[fontSizeField] = size;
14232
+ });
14233
+ }
14234
+
14235
+ function applyFontStyleVariant(value) {
14236
+ var variant = parseFontStyleVariant(value);
14237
+ if (!variant) return;
14238
+ applyStyleFields([fontStyleField, fontWeightField], function(rec) {
14239
+ rec[fontStyleField] = variant.style;
14240
+ rec[fontWeightField] = variant.weight;
14241
+ });
14242
+ }
14243
+
14244
+ function applyLabelColor(color) {
14245
+ applyStyleFields([fillField], function(rec) {
14246
+ rec[fillField] = color;
14247
+ });
14248
+ }
14249
+
14250
+ function applyInlineCss(css) {
14251
+ applyStyleFields([cssField], function(rec) {
14252
+ rec[cssField] = css || undefined;
14253
+ });
14254
+ }
14255
+
14256
+ function saveCurrentStyle() {
14257
+ openSaveStylePopup();
14258
+ }
14259
+
14260
+ function openSaveStylePopup() {
14261
+ var popup = showPopupAlert('', 'Save label style');
14262
+ var el = popup.container();
14263
+ el.addClass('option-menu');
14264
+ el.html(`<div><input type="text" class="style-name text-input" placeholder="style name"></div>
14265
+ <div tabindex="0" class="btn dialog-btn">Save</div>`);
14266
+ var input = el.findChild('.style-name');
14267
+ var btn = el.findChild('.btn');
14268
+ input.node().focus();
14269
+ btn.on('click', function() {
14270
+ var name = input.node().value.trim();
14271
+ if (!name) return;
14272
+ saveStyleWithName(name);
14273
+ popup.close();
14274
+ });
14275
+ input.on('keydown', function(e) {
14276
+ if (e.key == 'Enter') {
14277
+ btn.node().click();
14278
+ }
14279
+ });
14280
+ }
14281
+
14282
+ function saveStyleWithName(name) {
14283
+ var styles = getSavedStyles().filter(function(item) {
14284
+ return item.name != name;
14285
+ });
14286
+ var i;
14287
+ styles.push({
14288
+ name: name,
14289
+ style: getCurrentStyle()
14290
+ });
14291
+ styles.sort(function(a, b) {
14292
+ return a.name.toLowerCase() < b.name.toLowerCase() ? -1 :
14293
+ a.name.toLowerCase() > b.name.toLowerCase() ? 1 : 0;
14294
+ });
14295
+ GUI.setSavedValue(savedStylesKey, styles);
14296
+ renderSavedStyles();
14297
+ for (i=0; i<styleSelect.node().options.length; i++) {
14298
+ if (styleSelect.node().options[i].value == name) {
14299
+ styleSelect.node().selectedIndex = i;
14300
+ break;
14301
+ }
14302
+ }
14303
+ }
14304
+
14305
+ async function deleteSelectedStyle() {
14306
+ var name = styleSelect.node().value;
14307
+ var styles;
14308
+ if (!name) return;
14309
+ if (!await showPrompt('Delete label style "' + name + '"?', 'Delete preset')) return;
14310
+ styles = getSavedStyles().filter(function(item) {
14311
+ return item.name != name;
14312
+ });
14313
+ GUI.setSavedValue(savedStylesKey, styles);
14314
+ renderSavedStyles();
14315
+ updateSavedStyleControls();
14316
+ }
14317
+
14318
+ function applySavedStyle(name) {
14319
+ var item = getSavedStyles().find(function(item) {
14320
+ return item.name == name;
14321
+ });
14322
+ if (!item) return;
14323
+ applyStyleObject(item.style);
14324
+ }
14325
+
14326
+ function getSavedStyles() {
14327
+ var styles = GUI.getSavedValue(savedStylesKey);
14328
+ return Array.isArray(styles) ? styles : [];
14329
+ }
14330
+
14331
+ function renderSavedStyles() {
14332
+ var value = styleSelect && styleSelect.node().value;
14333
+ if (!styleSelect) return;
14334
+ styleSelect.empty();
14335
+ El('option').attr('value', '').appendTo(styleSelect).text('Apply preset...');
14336
+ getSavedStyles().forEach(function(item) {
14337
+ El('option').attr('value', item.name).appendTo(styleSelect).text(item.name);
14338
+ });
14339
+ styleSelect.node().value = value || '';
14340
+ updateSavedStyleControls();
14341
+ }
14342
+
14343
+ function getCurrentStyle() {
14344
+ var style = {};
14345
+ var fontStyle = parseFontStyleVariant(fontStyleSelect.node().value);
14346
+ var icon = getSelectedIcon();
14347
+ addStyleValue(style, fontField, fontSelect.node().value);
14348
+ if (fontStyle) {
14349
+ style[fontStyleField] = fontStyle.style;
14350
+ style[fontWeightField] = fontStyle.weight;
14351
+ }
14352
+ addStyleValue(style, fontSizeField, getNumericControlValue(fontSizeText));
14353
+ addStyleValue(style, fillField, colorInput.node().value.trim());
14354
+ addStyleValue(style, cssField, cssInput.node().value.trim());
14355
+ addStyleValue(style, 'label-pos', getSelectedLabelPosition());
14356
+ addStyleValue(style, iconField, icon);
14357
+ if (icon) {
14358
+ addStyleValue(style, iconSizeField, getNumericControlValue(iconSizeText));
14359
+ }
14360
+ return style;
14361
+ }
14362
+
14363
+ function addStyleValue(style, field, value) {
14364
+ if (value || value === 0) {
14365
+ style[field] = value;
14366
+ }
14367
+ }
14368
+
14369
+ function getNumericControlValue(el) {
14370
+ var value = Number(el.text());
14371
+ return isFinite(value) && value > 0 ? value : null;
14372
+ }
14373
+
14374
+ function getSelectedLabelPosition() {
14375
+ var out = '';
14376
+ labelPositions.forEach(function(pos) {
14377
+ if (posBtns[pos].hasClass('selected')) out = pos;
14378
+ });
14379
+ return out;
14380
+ }
14381
+
14382
+ function getSelectedIcon() {
14383
+ var out = '';
14384
+ iconTypes.forEach(function(icon) {
14385
+ if (iconBtns[icon.name].hasClass('selected')) out = icon.name;
14386
+ });
14387
+ return out;
14388
+ }
14389
+
14390
+ function applyStyleObject(style) {
14391
+ var fields = Object.keys(style || {}).reduce(function(memo, field) {
14392
+ if (field == 'label-pos') {
14393
+ labelPositionFields.forEach(function(field) {
14394
+ if (memo.indexOf(field) == -1) memo.push(field);
14395
+ });
14396
+ } else if (field != 'dx' && field != 'dy') {
14397
+ memo.push(field);
14398
+ }
14399
+ return memo;
14400
+ }, []);
14401
+ applyStyleFields(fields, function(rec) {
14402
+ savedStyleFields.forEach(function(field) {
14403
+ if (field in style && field != 'label-pos') {
14404
+ rec[field] = style[field];
14405
+ }
14406
+ });
14407
+ if (style['label-pos']) {
14408
+ internal.setLabelPositionStyle(rec, style['label-pos']);
14409
+ }
14410
+ });
14411
+ }
14412
+
14413
+ function parseFontStyleVariant(value) {
14414
+ var parts = String(value).split('|');
14415
+ if (parts.length != 2) return null;
14416
+ return {
14417
+ style: parts[0],
14418
+ weight: parts[1]
14419
+ };
14420
+ }
14421
+
14422
+ function applyLabelPosition(pos) {
14423
+ applyStyleFields(labelPositionFields, function(rec) {
14424
+ internal.setLabelPositionStyle(rec, pos);
14425
+ });
14426
+ }
14427
+
14428
+ function applyIcon(iconName) {
14429
+ applyStyleFields([iconField, iconSizeField], function(rec) {
14430
+ if (iconName) {
14431
+ rec[iconField] = iconName;
14432
+ if (!(rec[iconSizeField] > 0)) {
14433
+ rec[iconSizeField] = defaultIconSize;
14434
+ }
14435
+ } else {
14436
+ rec[iconField] = undefined;
14437
+ rec[iconSizeField] = undefined;
14438
+ }
14439
+ });
14440
+ }
14441
+
14442
+ function nudgeIconSize(delta) {
14443
+ var ids = getSelectionIds();
14444
+ var size = getNumericSize(ids, iconSizeField, defaultIconSize);
14445
+ if (ids.length === 0) return;
14446
+ size = Math.max(1, size + delta);
14447
+ applyStyleFields([iconField, iconSizeField], function(rec) {
14448
+ if (!rec[iconField]) {
14449
+ rec[iconField] = 'circle';
14450
+ }
14451
+ rec[iconSizeField] = size;
14452
+ });
14453
+ }
14454
+
14455
+ function getNumericSize(ids, field, defaultValue) {
14456
+ var val = getCommonValue(ids, field);
14457
+ val = val ? Number(val) : defaultValue;
14458
+ return isFinite(val) && val > 0 ? val : defaultValue;
14459
+ }
14460
+
14461
+ function isHexColor(str) {
14462
+ return /^#[0-9a-f]{6}$/i.test(str);
14463
+ }
14464
+
14465
+ function initColorPicker(colorRow) {
14466
+ colorPicker = El('div').addClass('label-color-picker').appendTo(colorRow).hide();
14467
+ var sbWrap = El('div').addClass('label-color-canvas-wrap').appendTo(colorPicker);
14468
+ sbCanvas = El('canvas').attr('width', '256').attr('height', '256').appendTo(sbWrap);
14469
+ sbMarker = makePickerMarker().appendTo(sbWrap);
14470
+ var hueWrap = El('div').addClass('label-color-canvas-wrap').appendTo(colorPicker);
14471
+ hueCanvas = El('canvas').attr('width', '256').attr('height', '18').appendTo(hueWrap);
14472
+ hueMarker = makePickerMarker().appendTo(hueWrap);
14473
+ pickerHsbInputs = {};
14474
+ var hsbRow = El('div').addClass('label-color-picker-fields').appendTo(colorPicker);
14475
+ addPickerNumberInput(hsbRow, 'h', 'H');
14476
+ addPickerNumberInput(hsbRow, 's', 'S');
14477
+ addPickerNumberInput(hsbRow, 'b', 'B');
14478
+ El('button').appendTo(hsbRow).text('Close').on('click', closePickerColor);
14479
+ sbCanvas.on('mousedown', function(e) {
14480
+ startCanvasDrag(e, updateSbFromEvent);
14481
+ });
14482
+ hueCanvas.on('mousedown', function(e) {
14483
+ startCanvasDrag(e, updateHueFromEvent);
14484
+ });
14485
+ setPickerColor(pickerColor);
14486
+ }
14487
+
14488
+ function addPickerNumberInput(row, name, label) {
14489
+ var wrapper = El('label').appendTo(row);
14490
+ El('span').appendTo(wrapper).text(label);
14491
+ pickerHsbInputs[name] = El('input')
14492
+ .attr('type', 'text')
14493
+ .appendTo(wrapper)
14494
+ .on('change', function() {
14495
+ var h = parseNumberField(pickerHsbInputs.h.node().value);
14496
+ var s = parseNumberField(pickerHsbInputs.s.node().value);
14497
+ var b = parseNumberField(pickerHsbInputs.b.node().value);
14498
+ if (!isFinite(h) || !isFinite(s) || !isFinite(b)) return;
14499
+ setPickerColor({
14500
+ h: degreesToByte(h),
14501
+ s: pctToByte(s),
14502
+ b: pctToByte(b)
14503
+ });
14504
+ commitPickerColor();
12624
14505
  });
14506
+ }
14507
+
14508
+ function toggleColorPicker() {
14509
+ if (colorPicker.visible()) {
14510
+ hideColorPicker();
12625
14511
  } else {
12626
- el.removeClass('visible');
12627
- // wait for the transition to finish before hiding completely
12628
- clearTimeout(hideTimer);
12629
- hideTimer = setTimeout(function() {
12630
- if (!(visible && GUI.isActiveInstance(gui))) {
12631
- el.css('display', 'none');
12632
- }
12633
- }, transitionMs);
14512
+ colorPicker.show();
14513
+ drawColorPicker();
12634
14514
  }
12635
14515
  }
12636
- }
12637
14516
 
12638
- function ToolbarButton(parent, iconRef, opts) {
12639
- var btn = El('div').addClass('floating-toolbar-btn').appendTo(parent);
12640
- if (iconRef) {
12641
- var iconNode = El('body').findChild(iconRef);
12642
- if (iconNode) {
12643
- var icon = iconNode.node().cloneNode(true);
12644
- if (icon.hasAttribute('id')) icon.removeAttribute('id');
12645
- btn.node().appendChild(icon);
14517
+ function hideColorPicker() {
14518
+ colorPicker.hide();
14519
+ }
14520
+
14521
+ function startCanvasDrag(e, update) {
14522
+ var evt = e.originalEvent || e;
14523
+ colorPicker.addClass('dragging-color');
14524
+ El('body').addClass('dragging-color-picker');
14525
+ update(evt);
14526
+ document.addEventListener('mousemove', onmove);
14527
+ document.addEventListener('mouseup', onup);
14528
+ evt.preventDefault();
14529
+ function onmove(evt) {
14530
+ update(evt);
14531
+ }
14532
+ function onup() {
14533
+ document.removeEventListener('mousemove', onmove);
14534
+ document.removeEventListener('mouseup', onup);
14535
+ colorPicker.removeClass('dragging-color');
14536
+ El('body').removeClass('dragging-color-picker');
14537
+ commitPickerColor();
12646
14538
  }
12647
14539
  }
12648
- var enabled = true;
12649
- var clickHandlers = [];
12650
14540
 
12651
- if (opts.tooltip) setTooltip(opts.tooltip);
14541
+ function updateSbFromEvent(evt) {
14542
+ var p = getCanvasPoint(sbCanvas.node(), evt);
14543
+ setPickerColor({
14544
+ h: pickerColor.h,
14545
+ s: p.x,
14546
+ b: 255 - p.y
14547
+ });
14548
+ }
12652
14549
 
12653
- // Block native dblclick to avoid the map's double-click zoom
12654
- btn.on('dblclick', function(e) { e.stopPropagation(); });
14550
+ function updateHueFromEvent(evt) {
14551
+ var p = getCanvasPoint(hueCanvas.node(), evt);
14552
+ setPickerColor({
14553
+ h: p.x,
14554
+ s: pickerColor.s,
14555
+ b: pickerColor.b
14556
+ });
14557
+ }
12655
14558
 
12656
- btn.on('click', function(e) {
12657
- if (!enabled) return;
12658
- for (var i = 0; i < clickHandlers.length; i++) {
12659
- clickHandlers[i](e);
12660
- }
12661
- });
14559
+ function getCanvasPoint(canvas, evt) {
14560
+ var rect = canvas.getBoundingClientRect();
14561
+ return {
14562
+ x: clamp(Math.round((evt.clientX - rect.left) / rect.width * (canvas.width - 1)), 0, canvas.width - 1),
14563
+ y: clamp(Math.round((evt.clientY - rect.top) / rect.height * (canvas.height - 1)), 0, canvas.height - 1)
14564
+ };
14565
+ }
12662
14566
 
12663
- this.on = function(event, fn) {
12664
- if (event == 'click') {
12665
- clickHandlers.push(fn);
12666
- } else {
12667
- btn.on(event, fn);
14567
+ function setPickerColor(hsb) {
14568
+ pickerColor = {
14569
+ h: clamp(Math.round(hsb.h), 0, 255),
14570
+ s: clamp(Math.round(hsb.s), 0, 255),
14571
+ b: clamp(Math.round(hsb.b), 0, 255)
14572
+ };
14573
+ drawColorPicker();
14574
+ updatePickerFields();
14575
+ }
14576
+
14577
+ function updatePickerFields() {
14578
+ var hex = hsbToHex(pickerColor);
14579
+ pickerHsbInputs.h.node().value = byteToDegrees(pickerColor.h) + '°';
14580
+ pickerHsbInputs.s.node().value = byteToPct(pickerColor.s) + '%';
14581
+ pickerHsbInputs.b.node().value = byteToPct(pickerColor.b) + '%';
14582
+ colorInput.node().value = hex;
14583
+ colorChit.css('background-color', hex);
14584
+ }
14585
+
14586
+ function drawColorPicker() {
14587
+ if (!sbCanvas) return;
14588
+ drawSaturationBrightnessCanvas();
14589
+ drawHueCanvas();
14590
+ }
14591
+
14592
+ function drawSaturationBrightnessCanvas() {
14593
+ var ctx = sbCanvas.node().getContext('2d');
14594
+ var image = ctx.createImageData(256, 256);
14595
+ var data = image.data;
14596
+ var i = 0;
14597
+ for (var y=0; y<256; y++) {
14598
+ for (var x=0; x<256; x++) {
14599
+ var rgb = hsbToRgb({
14600
+ h: pickerColor.h,
14601
+ s: x,
14602
+ b: 255 - y
14603
+ });
14604
+ data[i++] = rgb.r;
14605
+ data[i++] = rgb.g;
14606
+ data[i++] = rgb.b;
14607
+ data[i++] = 255;
14608
+ }
12668
14609
  }
12669
- return this;
12670
- };
14610
+ ctx.putImageData(image, 0, 0);
14611
+ positionMarker(sbMarker, pickerColor.s, 255 - pickerColor.b, pickerColor);
14612
+ }
14613
+
14614
+ function drawHueCanvas() {
14615
+ var ctx = hueCanvas.node().getContext('2d');
14616
+ var image = ctx.createImageData(256, 18);
14617
+ var data = image.data;
14618
+ var i = 0;
14619
+ for (var y=0; y<18; y++) {
14620
+ for (var x=0; x<256; x++) {
14621
+ var rgb = hsbToRgb({h: x, s: 255, b: 255});
14622
+ data[i++] = rgb.r;
14623
+ data[i++] = rgb.g;
14624
+ data[i++] = rgb.b;
14625
+ data[i++] = 255;
14626
+ }
14627
+ }
14628
+ ctx.putImageData(image, 0, 0);
14629
+ positionMarker(hueMarker, pickerColor.h, 9, {h: pickerColor.h, s: 255, b: 255});
14630
+ }
12671
14631
 
12672
- this.enable = function() { return this.setEnabled(true); };
12673
- this.disable = function() { return this.setEnabled(false); };
14632
+ function commitPickerColor() {
14633
+ applyLabelColor(hsbToHex(pickerColor));
14634
+ }
12674
14635
 
12675
- this.setEnabled = function(b) {
12676
- enabled = !!b;
12677
- btn.classed('disabled', !enabled);
12678
- return this;
12679
- };
14636
+ function closePickerColor() {
14637
+ hideColorPicker();
14638
+ }
12680
14639
 
12681
- this.setTooltip = setTooltip;
14640
+ function getMarkerColor(rgb) {
14641
+ var luminance = rgb.r * 0.2126 + rgb.g * 0.7152 + rgb.b * 0.0722;
14642
+ return luminance < 128 ? '#fff' : '#000';
14643
+ }
12682
14644
 
12683
- this.node = function() {
12684
- return btn.node();
12685
- };
14645
+ function makePickerMarker() {
14646
+ return El('<svg class="label-color-marker" width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6"></circle></svg>');
14647
+ }
12686
14648
 
12687
- function setTooltip(text) {
12688
- btn.attr('data-tooltip', text);
14649
+ function positionMarker(marker, x, y, hsb) {
14650
+ var rgb = hsbToRgb(hsb);
14651
+ x = utils$1.clamp(x, 1, 254);
14652
+ y = utils$1.clamp(y, 1, 254) + 0.5;
14653
+ marker.css({
14654
+ left: x - 8,
14655
+ top: y - 8
14656
+ });
14657
+ marker.findChild('circle').attr('stroke', getMarkerColor(rgb));
14658
+ }
14659
+
14660
+ function hexToHsb(hex) {
14661
+ var r = parseInt(hex.substr(1, 2), 16);
14662
+ var g = parseInt(hex.substr(3, 2), 16);
14663
+ var b = parseInt(hex.substr(5, 2), 16);
14664
+ var max = Math.max(r, g, b);
14665
+ var min = Math.min(r, g, b);
14666
+ var d = max - min;
14667
+ var h = 0;
14668
+ if (d) {
14669
+ if (max == r) h = ((g - b) / d) % 6;
14670
+ else if (max == g) h = (b - r) / d + 2;
14671
+ else h = (r - g) / d + 4;
14672
+ h *= 60;
14673
+ if (h < 0) h += 360;
14674
+ }
14675
+ return {
14676
+ h: Math.round(h / 360 * 255),
14677
+ s: Math.round(max === 0 ? 0 : d / max * 255),
14678
+ b: max
14679
+ };
12689
14680
  }
12690
- }
12691
14681
 
12692
- // Floating toolbar that exposes undo/redo while the user is in an editing
12693
- // interaction mode. The toolbar is the first consumer of FloatingToolbar;
12694
- // future per-mode toolbars (e.g. feature styling) can follow the same pattern.
14682
+ function hsbToHex(hsb) {
14683
+ var rgb = hsbToRgb(hsb);
14684
+ return '#' + [rgb.r, rgb.g, rgb.b].map(function(val) {
14685
+ return val.toString(16).padStart(2, '0');
14686
+ }).join('');
14687
+ }
12695
14688
 
12696
- function EditToolbar(gui) {
12697
- var toolbar = new FloatingToolbar(gui, { name: 'edit-toolbar' });
12698
- var isMac = navigator.userAgent.includes('Mac');
12699
- var modKey = isMac ? '\u2318' : 'Ctrl+';
12700
- var shiftKey = isMac ? '\u21e7' : 'Shift+';
14689
+ function hsbToRgb(hsb) {
14690
+ var h = hsb.h / 255 * 360;
14691
+ var s = hsb.s / 255;
14692
+ var v = hsb.b / 255;
14693
+ var c = v * s;
14694
+ var x = c * (1 - Math.abs((h / 60) % 2 - 1));
14695
+ var m = v - c;
14696
+ var rgb = h < 60 ? [c, x, 0] :
14697
+ h < 120 ? [x, c, 0] :
14698
+ h < 180 ? [0, c, x] :
14699
+ h < 240 ? [0, x, c] :
14700
+ h < 300 ? [x, 0, c] : [c, 0, x];
14701
+ return {
14702
+ r: Math.round((rgb[0] + m) * 255),
14703
+ g: Math.round((rgb[1] + m) * 255),
14704
+ b: Math.round((rgb[2] + m) * 255)
14705
+ };
14706
+ }
12701
14707
 
12702
- var undoBtn = toolbar.addButton('#undo-icon', {
12703
- tooltip: 'Undo (' + modKey + 'Z)'
12704
- }).on('click', function() {
12705
- gui.undo.undo();
12706
- });
14708
+ function byteToPct(val) {
14709
+ return Math.round(val / 255 * 100);
14710
+ }
12707
14711
 
12708
- var redoBtn = toolbar.addButton('#redo-icon', {
12709
- tooltip: 'Redo (' + shiftKey + modKey + 'Z)'
12710
- }).on('click', function() {
12711
- gui.undo.redo();
12712
- });
14712
+ function pctToByte(val) {
14713
+ return Math.round(clamp(val, 0, 100) / 100 * 255);
14714
+ }
12713
14715
 
12714
- updateButtons();
14716
+ function byteToDegrees(val) {
14717
+ return Math.round(val / 255 * 360);
14718
+ }
12715
14719
 
12716
- gui.on('interaction_mode_change', function() {
12717
- updateVisibility();
12718
- // history is cleared on mode change; refresh button states next tick
12719
- updateButtons();
12720
- });
14720
+ function degreesToByte(val) {
14721
+ return Math.round(clamp(val, 0, 360) / 360 * 255);
14722
+ }
12721
14723
 
12722
- gui.on('history_change', function(e) {
12723
- undoBtn.setEnabled(!!e.canUndo);
12724
- redoBtn.setEnabled(!!e.canRedo);
12725
- // Visibility may also depend on history (e.g. attribute edits via popup
12726
- // happen in modes that don't otherwise support undo).
12727
- updateVisibility();
12728
- });
14724
+ function parseNumberField(str) {
14725
+ return Number(String(str).replace(/[°%]/g, '').trim());
14726
+ }
12729
14727
 
12730
- updateVisibility();
14728
+ function clamp(val, min, max) {
14729
+ return isFinite(val) ? Math.max(min, Math.min(max, val)) : min;
14730
+ }
12731
14731
 
12732
- function updateVisibility() {
12733
- if (!gui.interaction) {
12734
- toolbar.hide();
12735
- return;
14732
+ function applyStyleFields(fields, updateRecord) {
14733
+ var table = getActiveTable();
14734
+ var ids = getSelectionIds();
14735
+ var lyr = getActiveLayer();
14736
+ var hasNewFields;
14737
+ if (!table || ids.length === 0) return;
14738
+ hasNewFields = fields.some(function(field) {
14739
+ return !table.fieldExists(field);
14740
+ });
14741
+ gui.dispatchEvent('data_preupdate', {ids: ids});
14742
+ if (hasNewFields) {
14743
+ table.captureSchemaBefore({operation: 'label-style', fields: fields});
14744
+ } else {
14745
+ table.captureFieldsBefore(fields, {operation: 'label-style'});
12736
14746
  }
12737
- var mode = gui.interaction.getMode();
12738
- var hasHistory = gui.undo.canUndo() || gui.undo.canRedo();
12739
- if (gui.interaction.modeSupportsUndo(mode) || hasHistory) {
12740
- toolbar.show();
14747
+ updateRecords(ids, table, updateRecord);
14748
+ if (hasNewFields) {
14749
+ table.markSchemaChanged({operation: 'label-style'});
12741
14750
  } else {
12742
- toolbar.hide();
14751
+ table.markFieldsChanged(fields, {operation: 'label-style'});
12743
14752
  }
14753
+ gui.dispatchEvent('data_postupdate', {ids: ids});
14754
+ gui.model.updated({style: true, same_table: true});
14755
+ updatePendingStyleHistory(lyr, ids, fields);
14756
+ setTimeout(updateSelectionDisplay, 0);
14757
+ updateControls();
14758
+ updateSelectionDisplay();
12744
14759
  }
12745
14760
 
12746
- function updateButtons() {
12747
- undoBtn.setEnabled(gui.undo.canUndo());
12748
- redoBtn.setEnabled(gui.undo.canRedo());
14761
+ function updateRecords(ids, table, updateRecord) {
14762
+ var records = table.getRecords();
14763
+ ids.forEach(function(id) {
14764
+ var rec = records[id] || {};
14765
+ updateRecord(rec);
14766
+ records[id] = rec;
14767
+ });
14768
+ }
14769
+
14770
+ function updateSelectionDisplay() {
14771
+ clearSelectionDisplay();
14772
+ getSelectionIds().forEach(function(id) {
14773
+ var textNode = getTextNodeById(id);
14774
+ if (textNode) {
14775
+ textNode.classList.add('label-style-selected');
14776
+ }
14777
+ });
14778
+ }
14779
+
14780
+ function clearSelectionDisplay() {
14781
+ var lyr = hit && hit.getHitTarget();
14782
+ var container = lyr && lyr.gui && lyr.gui.svg_container;
14783
+ if (!container) return;
14784
+ container.querySelectorAll('.label-style-selected').forEach(function(node) {
14785
+ node.classList.remove('label-style-selected');
14786
+ });
14787
+ }
14788
+
14789
+ function getTextNodeById(id) {
14790
+ var lyr = hit && hit.getHitTarget();
14791
+ var container = lyr && lyr.gui && lyr.gui.svg_container;
14792
+ var symbol = container && container.querySelector('[data-id="' + id + '"]');
14793
+ if (!symbol) return null;
14794
+ return symbol.tagName == 'text' ? symbol : symbol.querySelector('text');
12749
14795
  }
14796
+
12750
14797
  }
12751
14798
 
12752
14799
  function Model(gui) {
@@ -13466,7 +15513,8 @@
13466
15513
  }
13467
15514
 
13468
15515
  function selectable() {
13469
- return interactionMode() == 'selection';
15516
+ var mode = interactionMode();
15517
+ return mode == 'selection' || mode == 'label_style';
13470
15518
  }
13471
15519
 
13472
15520
  function pinnable() {
@@ -13484,7 +15532,7 @@
13484
15532
  var mode = interactionMode();
13485
15533
  // click used to pin popup and select features
13486
15534
  return mode == 'data' || mode == 'info' || mode == 'selection' ||
13487
- mode == 'rectangles' || mode == 'edit_points';
15535
+ mode == 'label_style' || mode == 'rectangles' || mode == 'edit_points';
13488
15536
  }
13489
15537
 
13490
15538
  self.getHitId = function() {
@@ -16817,6 +18865,8 @@
16817
18865
  var style;
16818
18866
  if (layerHasDrawableStyle(lyr) && !opts.outlineMode) {
16819
18867
  style = getCanvasDisplayStyle(lyr);
18868
+ } else if (internal.layerHasLabels(lyr) && opts.interactionMode == 'label_style') {
18869
+ style = {dotSize: 0};
16820
18870
  } else if (internal.layerHasLabels(lyr) && !opts.outlineMode) {
16821
18871
  style = copyBaseStyle(activeStyleForLabels);
16822
18872
  } else if (opts.darkMode) {
@@ -17171,11 +19221,11 @@
17171
19221
  utils$1.inherit(MapExtent, EventDispatcher);
17172
19222
 
17173
19223
  var MAX_VIEWPORT_PREVIEW_PIXELS = 6e6;
17174
- var cache = new WeakMap();
17175
- var requestId = 0;
19224
+ var cache$1 = new WeakMap();
19225
+ var requestId$1 = 0;
17176
19226
 
17177
19227
  function getCachedRasterViewportPreview(layer, ext) {
17178
- var entry = cache.get(layer);
19228
+ var entry = cache$1.get(layer);
17179
19229
  var params = getRasterViewportPreviewParams(layer, ext);
17180
19230
  if (!entry || !params || entry.key != params.key) return null;
17181
19231
  return entry.preview;
@@ -17183,24 +19233,24 @@
17183
19233
 
17184
19234
  function scheduleRasterViewportPreview(layer, ext, onReady) {
17185
19235
  var params = getRasterViewportPreviewParams(layer, ext);
17186
- var entry = cache.get(layer);
19236
+ var entry = cache$1.get(layer);
17187
19237
  var id, stats, timing, preview;
17188
19238
  if (!params || !params.needed) return;
17189
19239
  if (entry && entry.key == params.key) return;
17190
- id = ++requestId;
17191
- cache.set(layer, {key: params.key, pending: id});
19240
+ id = ++requestId$1;
19241
+ cache$1.set(layer, {key: params.key, pending: id});
17192
19242
  setTimeout(function() {
17193
- var current = cache.get(layer);
19243
+ var current = cache$1.get(layer);
17194
19244
  if (!current || current.pending != id) return;
17195
19245
  timing = {};
17196
19246
  stats = getCachedRasterScalingStats(params, timing);
17197
- timing.renderStart = getTimer();
17198
- preview = renderRasterViewportPreview(params.grid, params.recipe, params.bbox, params.width, params.height, stats);
17199
- timing.renderMs = getTimer() - timing.renderStart;
19247
+ timing.renderStart = getTimer$1();
19248
+ preview = internal.renderRasterViewportPreview(params.grid, params.recipe, params.bbox, params.width, params.height, stats);
19249
+ timing.renderMs = getTimer$1() - timing.renderStart;
17200
19250
  logRasterPreviewTiming(params, timing);
17201
- current = cache.get(layer);
19251
+ current = cache$1.get(layer);
17202
19252
  if (!preview || !current || current.pending != id) return;
17203
- cache.set(layer, {
19253
+ cache$1.set(layer, {
17204
19254
  key: params.key,
17205
19255
  preview: preview
17206
19256
  });
@@ -17209,23 +19259,23 @@
17209
19259
  }
17210
19260
 
17211
19261
  function invalidateRasterViewportPreview(layer) {
17212
- cache.delete(layer);
19262
+ cache$1.delete(layer);
17213
19263
  }
17214
19264
 
17215
19265
  function getRasterViewportPreviewParams(layer, ext) {
17216
19266
  var raster = layer && layer.raster;
17217
- var grid = getRasterGrid(raster);
17218
- var rasterBbox = getRasterBBox(raster);
19267
+ var grid = internal.getRasterGrid(raster);
19268
+ var rasterBbox = internal.getRasterBBox(raster);
17219
19269
  var mapBbox = ext.getBounds().toArray();
17220
- var visibleBbox = rasterBbox && intersectBboxes(rasterBbox, mapBbox);
17221
- var preview = getRasterPreview(raster);
19270
+ var visibleBbox = rasterBbox && internal.intersectBboxes(rasterBbox, mapBbox);
19271
+ var preview = internal.getRasterPreview(raster);
17222
19272
  var recipe, pixelRatio, t, p1, p2, displayWidth, displayHeight, crop, width, height, scale, key, needed;
17223
19273
  if (!grid || !grid.samples || !visibleBbox || !grid.bbox) return null;
17224
19274
  if (!supportsNorthUpRaster(grid)) return null;
17225
19275
  crop = getRasterSourceWindow(grid, visibleBbox);
17226
19276
  if (!crop) return null;
17227
19277
  visibleBbox = crop.bbox;
17228
- recipe = getRasterViewRecipe(grid, raster.view && raster.view.recipe);
19278
+ recipe = internal.getRasterViewRecipe(grid, raster.view && raster.view.recipe);
17229
19279
  pixelRatio = GUI.getPixelRatio();
17230
19280
  t = ext.getTransform(pixelRatio);
17231
19281
  p1 = t.transform(mapBbox[0], mapBbox[3]);
@@ -17273,46 +19323,46 @@
17273
19323
  timing.statsSource = 'none';
17274
19324
  return null;
17275
19325
  }
17276
- key = getRasterScalingStatsKey(params.grid, params.recipe);
19326
+ key = internal.getRasterScalingStatsKey(params.grid, params.recipe);
17277
19327
  if (cached && cached.key == key) {
17278
19328
  timing.statsMs = 0;
17279
19329
  timing.statsSource = 'raster-view-cache';
17280
19330
  return cached.stats;
17281
19331
  }
17282
19332
  timing.statsSource = 'computed';
17283
- timing.statsStart = getTimer();
17284
- cached = getRasterViewScalingStats(params.raster, params.recipe);
17285
- timing.statsMs = getTimer() - timing.statsStart;
19333
+ timing.statsStart = getTimer$1();
19334
+ cached = internal.getRasterViewScalingStats(params.raster, params.recipe);
19335
+ timing.statsMs = getTimer$1() - timing.statsStart;
17286
19336
  return cached;
17287
19337
  }
17288
19338
 
17289
19339
  function logRasterPreviewTiming(params, timing) {
17290
- if (!rasterDebugIsOn()) return;
19340
+ if (!rasterDebugIsOn$1()) return;
17291
19341
  console.log([
17292
19342
  'Raster viewport preview:',
17293
19343
  params.width + 'x' + params.height,
17294
19344
  'from',
17295
19345
  params.sourceWidth + 'x' + params.sourceHeight,
17296
19346
  'source px,',
17297
- 'stats=' + formatMs(timing.statsMs),
19347
+ 'stats=' + formatMs$1(timing.statsMs),
17298
19348
  '(' + timing.statsSource + '),',
17299
- 'render=' + formatMs(timing.renderMs) + ',',
17300
- 'total=' + formatMs(timing.statsMs + timing.renderMs) + ',',
19349
+ 'render=' + formatMs$1(timing.renderMs) + ',',
19350
+ 'total=' + formatMs$1(timing.statsMs + timing.renderMs) + ',',
17301
19351
  'scaling=' + params.recipe.scaling,
17302
19352
  'type=' + params.recipe.type
17303
19353
  ].join(' '));
17304
19354
  }
17305
19355
 
17306
- function rasterDebugIsOn() {
19356
+ function rasterDebugIsOn$1() {
17307
19357
  var vars = GUI.getUrlVars();
17308
19358
  return vars['raster-debug'] === true || vars['raster-debug'] == '1' || vars.raster_debug === true || vars.raster_debug == '1';
17309
19359
  }
17310
19360
 
17311
- function getTimer() {
19361
+ function getTimer$1() {
17312
19362
  return typeof performance != 'undefined' && performance.now ? performance.now() : Date.now();
17313
19363
  }
17314
19364
 
17315
- function formatMs(ms) {
19365
+ function formatMs$1(ms) {
17316
19366
  return Math.round(ms * 10) / 10 + 'ms';
17317
19367
  }
17318
19368
 
@@ -17362,6 +19412,162 @@
17362
19412
  return Math.round(val * 1e9) / 1e9;
17363
19413
  }
17364
19414
 
19415
+ var MAX_REPROJECTED_PREVIEW_PIXELS = 6e6;
19416
+ var cache = new WeakMap();
19417
+ var requestId = 0;
19418
+
19419
+ function getCachedRasterReprojectedPreview(layer, ext) {
19420
+ var params = getRasterReprojectedPreviewParams(layer, ext);
19421
+ var entry = cache.get(layer);
19422
+ if (!entry || !entry.preview) return null;
19423
+ if (!params || entry.key != params.key) return entry.preview;
19424
+ return entry.preview;
19425
+ }
19426
+
19427
+ function scheduleRasterReprojectedPreview(layer, ext, onReady) {
19428
+ var params = getRasterReprojectedPreviewParams(layer, ext);
19429
+ var entry = cache.get(layer);
19430
+ var id, timing;
19431
+ if (!params) return;
19432
+ if (entry && entry.key == params.key) return;
19433
+ id = ++requestId;
19434
+ cache.set(layer, {key: params.key, pending: id});
19435
+ setTimeout(function() {
19436
+ var current = cache.get(layer);
19437
+ var grid, preview;
19438
+ if (!current || current.pending != id) return;
19439
+ timing = {};
19440
+ try {
19441
+ grid = internal.projectRasterGridForward({grid: params.grid}, params.sourceCRS, params.displayCRS, {
19442
+ raster_mesh_interval: params.meshInterval,
19443
+ output_bbox: params.bbox,
19444
+ output_width: params.width,
19445
+ output_height: params.height,
19446
+ sample_method: params.sampleMethod,
19447
+ timing: timing
19448
+ });
19449
+ } catch(e) {
19450
+ cache.delete(layer);
19451
+ if (typeof console != 'undefined' && console.warn) {
19452
+ console.warn('Unable to reproject raster preview', e);
19453
+ }
19454
+ return;
19455
+ }
19456
+ timing.renderStart = getTimer();
19457
+ preview = internal.renderRasterPreview(grid, params.recipe, grid.width, grid.height, params.stats);
19458
+ applyCoverageMask(preview, grid.coverage);
19459
+ timing.renderMs = getTimer() - timing.renderStart;
19460
+ logRasterReprojectionTiming(params, timing);
19461
+ preview.bbox = grid.bbox;
19462
+ current = cache.get(layer);
19463
+ if (!current || current.pending != id) return;
19464
+ cache.set(layer, {key: params.key, preview: preview});
19465
+ onReady();
19466
+ }, 0);
19467
+ }
19468
+
19469
+ function getRasterReprojectedPreviewParams(layer, ext) {
19470
+ var raster = layer && layer.raster;
19471
+ var gui = layer && layer.gui;
19472
+ var sourceDataset = gui && gui.source && gui.source.dataset;
19473
+ var sourceInfo = sourceDataset && internal.getDatasetCrsInfo(sourceDataset);
19474
+ var sourceCRS = sourceInfo && sourceInfo.crs;
19475
+ var displayCRS = gui && gui.dynamic_crs;
19476
+ var sourceGrid = internal.getRasterGrid(raster);
19477
+ var rasterBbox = gui && gui.bounds && gui.bounds.hasBounds() && gui.bounds.toArray();
19478
+ var viewBbox = ext && ext.getBounds().toArray();
19479
+ var bbox = rasterBbox && viewBbox && internal.intersectBboxes(rasterBbox, viewBbox);
19480
+ var size = bbox && getPreviewSize(ext, sourceGrid);
19481
+ var recipe, stats;
19482
+ if (!sourceCRS || !displayCRS || !sourceGrid || !sourceGrid.samples || !bbox || !size) return null;
19483
+ recipe = internal.getRasterViewRecipe(sourceGrid, raster.view && raster.view.recipe);
19484
+ stats = internal.getRasterViewScalingStats(raster, recipe);
19485
+ return {
19486
+ key: getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe),
19487
+ grid: sourceGrid,
19488
+ sourceCRS: sourceCRS,
19489
+ displayCRS: displayCRS,
19490
+ bbox: bbox,
19491
+ width: size.width,
19492
+ height: size.height,
19493
+ recipe: recipe,
19494
+ stats: stats,
19495
+ sampleMethod: getRasterReprojectionSampleMethod(),
19496
+ meshInterval: 32
19497
+ };
19498
+ }
19499
+
19500
+ function getPreviewSize(ext, grid) {
19501
+ var pixelRatio = GUI.getPixelRatio();
19502
+ var t = ext.getTransform(pixelRatio);
19503
+ var mapBbox = ext.getBounds().toArray();
19504
+ var p1 = t.transform(mapBbox[0], mapBbox[3]);
19505
+ var p2 = t.transform(mapBbox[2], mapBbox[1]);
19506
+ var width = Math.max(1, Math.round(Math.abs(p2[0] - p1[0])));
19507
+ var height = Math.max(1, Math.round(Math.abs(p2[1] - p1[1])));
19508
+ var scale = Math.min(1, Math.sqrt(MAX_REPROJECTED_PREVIEW_PIXELS / (width * height)));
19509
+ width = Math.min(grid.width, Math.max(1, Math.round(width * scale)));
19510
+ height = Math.min(grid.height, Math.max(1, Math.round(height * scale)));
19511
+ return {width: width, height: height};
19512
+ }
19513
+
19514
+ function getRasterReprojectedPreviewKey(layer, bbox, size, sourceCRS, displayCRS, recipe) {
19515
+ return [
19516
+ size.width,
19517
+ size.height,
19518
+ bbox.join(','),
19519
+ internal.crsToProj4(sourceCRS),
19520
+ internal.crsToProj4(displayCRS),
19521
+ recipe.type,
19522
+ recipe.scaling,
19523
+ recipe.scaleRange && recipe.scaleRange.join(','),
19524
+ recipe.percentileRange && recipe.percentileRange.join(','),
19525
+ getRasterReprojectionSampleMethod(),
19526
+ layer.raster && layer.raster.grid && layer.raster.grid.samples && layer.raster.grid.samples.length
19527
+ ].join('|');
19528
+ }
19529
+
19530
+ function applyCoverageMask(preview, coverage) {
19531
+ var pixels = preview && preview.pixels;
19532
+ if (!pixels || !coverage) return;
19533
+ for (var i = 0; i < coverage.length; i++) {
19534
+ if (!coverage[i]) pixels[i * 4 + 3] = 0;
19535
+ }
19536
+ }
19537
+
19538
+ function getRasterReprojectionSampleMethod() {
19539
+ var vars = GUI.getUrlVars();
19540
+ var val = vars['raster-bilinear'] ?? vars.raster_bilinear;
19541
+ return val === false || val == '0' ? 'nearest' : 'bilinear';
19542
+ }
19543
+
19544
+ function logRasterReprojectionTiming(params, timing) {
19545
+ if (!rasterDebugIsOn()) return;
19546
+ console.log([
19547
+ 'Raster reprojection preview:',
19548
+ params.width + 'x' + params.height,
19549
+ 'mesh=' + formatMs(timing.meshMs),
19550
+ 'rasterize=' + formatMs(timing.rasterizeMs),
19551
+ 'render=' + formatMs(timing.renderMs),
19552
+ 'total=' + formatMs((timing.meshMs || 0) + (timing.rasterizeMs || 0) + (timing.renderMs || 0)) + ',',
19553
+ 'vertices=' + timing.meshVertices,
19554
+ 'sampling=' + params.sampleMethod
19555
+ ].join(' '));
19556
+ }
19557
+
19558
+ function rasterDebugIsOn() {
19559
+ var vars = GUI.getUrlVars();
19560
+ return vars['raster-debug'] === true || vars['raster-debug'] == '1' || vars.raster_debug === true || vars.raster_debug == '1';
19561
+ }
19562
+
19563
+ function formatMs(ms) {
19564
+ return Math.round((ms || 0) * 10) / 10 + 'ms';
19565
+ }
19566
+
19567
+ function getTimer() {
19568
+ return typeof performance != 'undefined' && performance.now ? performance.now() : Date.now();
19569
+ }
19570
+
17365
19571
  var hatches = {}; // cached patterns
17366
19572
 
17367
19573
  function getCanvasFillEffect(ctx, shp, arcs, ext, style) {
@@ -17620,11 +19826,29 @@
17620
19826
  _self.drawRasterLayer = function(layer, opts) {
17621
19827
  var raster = layer.raster;
17622
19828
  var options = opts || {};
17623
- var preview = options.action == 'nav' ? null : getCachedRasterViewportPreview(layer, _ext);
17624
- var bbox = preview && preview.bbox;
19829
+ var preview = null;
19830
+ var bbox;
19831
+ if (layer.gui && layer.gui.dynamic_crs) {
19832
+ preview = getCachedRasterReprojectedPreview(layer, _ext);
19833
+ if (!preview) {
19834
+ if (options.action != 'nav' && options.onViewportPreviewReady) {
19835
+ scheduleRasterReprojectedPreview(layer, _ext, options.onViewportPreviewReady);
19836
+ }
19837
+ return;
19838
+ }
19839
+ bbox = preview.bbox;
19840
+ if (!preview.pixels || !bbox) return;
19841
+ drawRasterPreview(preview, bbox);
19842
+ if (options.action != 'nav' && options.onViewportPreviewReady) {
19843
+ scheduleRasterReprojectedPreview(layer, _ext, options.onViewportPreviewReady);
19844
+ }
19845
+ return;
19846
+ }
19847
+ preview = options.action == 'nav' ? null : getCachedRasterViewportPreview(layer, _ext);
19848
+ bbox = preview && preview.bbox;
17625
19849
  if (!preview) {
17626
- preview = raster && getRasterPreview(raster);
17627
- bbox = raster && getRasterBBox(raster);
19850
+ preview = raster && internal.getRasterPreview(raster);
19851
+ bbox = raster && internal.getRasterBBox(raster);
17628
19852
  if (options.action != 'nav' && options.onViewportPreviewReady) {
17629
19853
  scheduleRasterViewportPreview(layer, _ext, options.onViewportPreviewReady);
17630
19854
  }
@@ -18897,6 +21121,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
18897
21121
  this.isActiveLayer = isActiveLayer;
18898
21122
  this.isVisibleLayer = isVisibleLayer;
18899
21123
  this.getActiveLayer = function() { return _activeLyr; };
21124
+ this.getHitControl = function() { return _hit; };
18900
21125
  // this.getViewData = function() {
18901
21126
  // return {
18902
21127
  // isPreview: isPreviewView(),
@@ -19289,7 +21514,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
19289
21514
  _renderer.drawMainLayers(contentLayers, action);
19290
21515
 
19291
21516
  // draw hover & selection overlay
19292
- if (!_overlayLayers || action != 'nav') {
21517
+ if (!_overlayLayers || action != 'nav' || getGlobalStyleOptions().interactionMode == 'label_style') {
19293
21518
  // cache layers to use when panning/zooming
19294
21519
  _overlayLayers = getOverlayLayers(_activeLyr, _hit.getHitState(), getGlobalStyleOptions());
19295
21520
  }
@@ -20068,6 +22293,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
20068
22293
  }
20069
22294
  gui.interaction = new InteractionMode(gui);
20070
22295
  gui.editToolbar = new EditToolbar(gui);
22296
+ gui.labelTool = new LabelTool(gui);
20071
22297
 
20072
22298
  gui.showProgressMessage = function(msg) {
20073
22299
  if (!gui.progressMessage) {