mapshaper 0.7.17 → 0.7.19

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.
@@ -2422,232 +2422,6 @@
2422
2422
  return bytes;
2423
2423
  }
2424
2424
 
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
- };
2434
-
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;
2446
- }
2447
-
2448
- async function restoreStoredUndoUnits(units, store) {
2449
- var hydrated = await hydrateStoredUndoUnits(units, store);
2450
- internal.restoreCapturedUnits(hydrated);
2451
- }
2452
-
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;
2459
- }
2460
-
2461
- function getStoredUndoPayloadRefs(units) {
2462
- var refs = [];
2463
- units.forEach(function(unit) {
2464
- if (unit.payloadRef) refs.push(unit.payloadRef);
2465
- });
2466
- return refs;
2467
- }
2468
-
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;
2495
- }
2496
-
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;
2508
- }
2509
-
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;
2523
- }
2524
-
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;
2539
- }
2540
-
2541
- async function packLayerPayload(payload) {
2542
- if (payload.raster) {
2543
- payload = Object.assign({}, payload, {
2544
- raster: packRasterUndoPayload(payload.raster)
2545
- });
2546
- }
2547
- return payload;
2548
- }
2549
-
2550
- async function packTablePayload(payload) {
2551
- return {
2552
- packedRecords: packRecordsAsColumns(payload.records)
2553
- };
2554
- }
2555
-
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;
2568
- }
2569
-
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;
2582
- }
2583
-
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;
2593
- }
2594
-
2595
- function stripPreviewPixels(preview) {
2596
- var copy = Object.assign({}, preview);
2597
- delete copy.canvas;
2598
- delete copy.pixels;
2599
- return copy;
2600
- }
2601
-
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
2425
  // Fall back to browserify's Buffer polyfill
2652
2426
  var B = typeof Buffer != 'undefined' ? Buffer : require$1('buffer').Buffer;
2653
2427
 
@@ -2673,7 +2447,7 @@
2673
2447
  return obj === Object(obj); // via underscore
2674
2448
  }
2675
2449
 
2676
- function clamp(val, min, max) {
2450
+ function clamp$1(val, min, max) {
2677
2451
  return val < min ? min : (val > max ? max : val);
2678
2452
  }
2679
2453
 
@@ -3354,7 +3128,7 @@
3354
3128
  function findValueByRank(arr, rank) {
3355
3129
  if (!arr.length || rank < 1 || rank > arr.length) error$1("[findValueByRank()] invalid input");
3356
3130
 
3357
- rank = clamp(rank | 0, 1, arr.length);
3131
+ rank = clamp$1(rank | 0, 1, arr.length);
3358
3132
  var k = rank - 1, // conv. rank to array index
3359
3133
  n = arr.length,
3360
3134
  l = 0,
@@ -3728,7 +3502,7 @@
3728
3502
  // self-import and the resulting Rollup circular-dependency warning.
3729
3503
  var utils = {
3730
3504
  addThousandsSep, addslashes, arrayToIndex,
3731
- clamp, cleanNumericString, contains, copyElements, countValues, createBuffer,
3505
+ clamp: clamp$1, cleanNumericString, contains, copyElements, countValues, createBuffer,
3732
3506
  defaults, difference,
3733
3507
  endsWith, every, expandoBuffer, extend, extendBuffer,
3734
3508
  find, findMedian, findQuantile, findRankByValue, findStringPrefix,
@@ -6504,18 +6278,244 @@
6504
6278
  return Object.keys(index);
6505
6279
  }
6506
6280
 
6507
- function createStoredUndoHistory(gui) {
6508
- var undoEntryId = 0;
6281
+ var PAYLOAD_FIELDS = {
6282
+ table: ['records'],
6283
+ 'table-records': ['records'],
6284
+ 'table-fields': ['columns'],
6285
+ 'table-schema': ['fields'],
6286
+ arcs: ['nn', 'xx', 'yy', 'zz', 'zlimit'],
6287
+ 'arcs-simplification': ['zz'],
6288
+ layer: ['shapes', 'raster']
6289
+ };
6509
6290
 
6510
- return {
6511
- addTransaction: addTransaction,
6512
- getPayloadStore: getPayloadStore
6513
- };
6291
+ async function storeUndoUnits(units, store, entryId, role) {
6292
+ var stored = [];
6293
+ try {
6294
+ for (var i = 0; i < units.length; i++) {
6295
+ stored.push(await storeUndoUnit(units[i], store, entryId, role));
6296
+ }
6297
+ } catch(e) {
6298
+ await store.delMany(getStoredUndoPayloadRefs(stored));
6299
+ throw e;
6300
+ }
6301
+ return stored;
6302
+ }
6514
6303
 
6515
- async function addTransaction(tx, opts) {
6516
- var undoUnits, redoCaptureUnits, updateFlags, store, entryId, storedUndoUnits,
6517
- storedRedoUnits, storeStart, storeMillis = 0, evictToken = {};
6518
- opts = opts || {};
6304
+ async function restoreStoredUndoUnits(units, store) {
6305
+ var hydrated = await hydrateStoredUndoUnits(units, store);
6306
+ restoreCapturedUnits(hydrated);
6307
+ }
6308
+
6309
+ async function hydrateStoredUndoUnits(units, store) {
6310
+ var hydrated = [];
6311
+ for (var i = 0; i < units.length; i++) {
6312
+ hydrated.push(await hydrateStoredUndoUnit(units[i], store));
6313
+ }
6314
+ return hydrated;
6315
+ }
6316
+
6317
+ function getStoredUndoPayloadRefs(units) {
6318
+ var refs = [];
6319
+ units.forEach(function(unit) {
6320
+ if (unit.payloadRef) refs.push(unit.payloadRef);
6321
+ });
6322
+ return refs;
6323
+ }
6324
+
6325
+ function getUndoRestoreFlags(units, baseFlags) {
6326
+ var flags = Object.assign({}, baseFlags || {}, {undo_restore: true});
6327
+ units.forEach(function(unit) {
6328
+ if (unit.type == 'changed') return;
6329
+ if (unit.type == 'arcs') {
6330
+ flags.arc_count = true;
6331
+ } else if (unit.type == 'arcs-simplification') {
6332
+ flags.simplify = true;
6333
+ } else if (unit.type == 'table' ||
6334
+ unit.type == 'table-records' ||
6335
+ unit.type == 'table-fields' ||
6336
+ unit.type == 'table-order' ||
6337
+ unit.type == 'table-schema') {
6338
+ flags.same_table = false;
6339
+ } else if (unit.type == 'catalog' || unit.type == 'dataset') {
6340
+ flags.select = true;
6341
+ flags.arc_count = true;
6342
+ } else if (unit.type == 'dataset-info') {
6343
+ flags.info = true;
6344
+ } else if (unit.type == 'layer' ||
6345
+ unit.type == 'layer-metadata' ||
6346
+ unit.type == 'layer-order') {
6347
+ flags.select = true;
6348
+ }
6349
+ });
6350
+ return flags;
6351
+ }
6352
+
6353
+ async function storeUndoUnit(unit, store, entryId, role) {
6354
+ var payload = await getUnitPayload(unit);
6355
+ var stored = stripPayload(unit);
6356
+ if (payload) {
6357
+ stored.payloadRef = await store.put(payload, {
6358
+ entryId: entryId,
6359
+ role: role,
6360
+ unitType: unit.type
6361
+ });
6362
+ }
6363
+ return stored;
6364
+ }
6365
+
6366
+ async function hydrateStoredUndoUnit(unit, store) {
6367
+ var hydrated = Object.assign({}, unit);
6368
+ var payload;
6369
+ if (unit.payloadRef) {
6370
+ payload = await store.get(unit.payloadRef);
6371
+ if (!payload) {
6372
+ throw new Error('Missing undo payload: ' + unit.payloadRef.key);
6373
+ }
6374
+ payload = unpackPayload(unit, payload);
6375
+ Object.assign(hydrated, payload);
6376
+ }
6377
+ delete hydrated.payloadRef;
6378
+ return hydrated;
6379
+ }
6380
+
6381
+ async function getUnitPayload(unit) {
6382
+ var fields = PAYLOAD_FIELDS[unit.type];
6383
+ var payload, hasPayload;
6384
+ if (!fields) return null;
6385
+ payload = {};
6386
+ fields.forEach(function(field) {
6387
+ if (field in unit) {
6388
+ payload[field] = unit[field];
6389
+ hasPayload = true;
6390
+ }
6391
+ });
6392
+ if (!hasPayload) return null;
6393
+ if (unit.type == 'layer') return packLayerPayload(payload);
6394
+ return unit.type == 'table' ? packTablePayload(payload) : payload;
6395
+ }
6396
+
6397
+ async function packLayerPayload(payload) {
6398
+ if (payload.raster) {
6399
+ payload = Object.assign({}, payload, {
6400
+ raster: packRasterUndoPayload(payload.raster)
6401
+ });
6402
+ }
6403
+ return payload;
6404
+ }
6405
+
6406
+ async function packTablePayload(payload) {
6407
+ return {
6408
+ packedRecords: packRecordsAsColumns(payload.records)
6409
+ };
6410
+ }
6411
+
6412
+ function unpackPayload(unit, payload) {
6413
+ if (unit.type == 'table' && payload.packedRecords) {
6414
+ return {
6415
+ records: unpackRecordsFromColumns(payload.packedRecords)
6416
+ };
6417
+ }
6418
+ if (unit.type == 'layer' && payload.raster) {
6419
+ return Object.assign({}, payload, {
6420
+ raster: unpackRasterUndoPayload(payload.raster)
6421
+ });
6422
+ }
6423
+ return payload;
6424
+ }
6425
+
6426
+ function packRasterUndoPayload(raster) {
6427
+ var copy = Object.assign({}, raster);
6428
+ if (raster.view) {
6429
+ copy.view = Object.assign({}, raster.view);
6430
+ if (raster.view.preview) {
6431
+ copy.view.preview = stripPreviewPixels(raster.view.preview);
6432
+ }
6433
+ }
6434
+ if (raster.preview) {
6435
+ copy.preview = stripPreviewPixels(raster.preview);
6436
+ }
6437
+ return copy;
6438
+ }
6439
+
6440
+ function unpackRasterUndoPayload(raster) {
6441
+ var copy = Object.assign({}, raster);
6442
+ if (raster.view) {
6443
+ copy.view = Object.assign({}, raster.view);
6444
+ if (raster.view.preview && !raster.view.preview.pixels && raster.grid && raster.grid.samples) {
6445
+ copy.view.preview = renderRasterPreview(raster.grid, raster.view.recipe, raster.view.preview.width, raster.view.preview.height);
6446
+ }
6447
+ }
6448
+ return copy;
6449
+ }
6450
+
6451
+ function stripPreviewPixels(preview) {
6452
+ var copy = Object.assign({}, preview);
6453
+ delete copy.canvas;
6454
+ delete copy.pixels;
6455
+ return copy;
6456
+ }
6457
+
6458
+ function packRecordsAsColumns(records) {
6459
+ var fields = getRecordFields(records);
6460
+ return {
6461
+ fields: fields,
6462
+ types: fields.map(function() { return null; }),
6463
+ data: fields.map(function(field) {
6464
+ return records.map(function(rec) {
6465
+ return rec ? rec[field] : undefined;
6466
+ });
6467
+ }),
6468
+ size: records.length
6469
+ };
6470
+ }
6471
+
6472
+ function getRecordFields(records) {
6473
+ var index = {};
6474
+ records.forEach(function(rec) {
6475
+ Object.keys(rec || {}).forEach(function(field) {
6476
+ index[field] = true;
6477
+ });
6478
+ });
6479
+ return Object.keys(index);
6480
+ }
6481
+
6482
+ function unpackRecordsFromColumns(data) {
6483
+ var records = [];
6484
+ for (var i = 0; i < data.size; i++) {
6485
+ records[i] = {};
6486
+ }
6487
+ data.fields.forEach(function(field, j) {
6488
+ var values = data.data[j];
6489
+ values.forEach(function(val, i) {
6490
+ records[i][field] = val;
6491
+ });
6492
+ });
6493
+ return records;
6494
+ }
6495
+
6496
+ function stripPayload(unit) {
6497
+ var fields = PAYLOAD_FIELDS[unit.type];
6498
+ var stripped = Object.assign({}, unit);
6499
+ if (fields) {
6500
+ fields.forEach(function(field) {
6501
+ delete stripped[field];
6502
+ });
6503
+ }
6504
+ return stripped;
6505
+ }
6506
+
6507
+ function createStoredUndoHistory(gui) {
6508
+ var undoEntryId = 0;
6509
+
6510
+ return {
6511
+ addTransaction: addTransaction,
6512
+ getPayloadStore: getPayloadStore
6513
+ };
6514
+
6515
+ async function addTransaction(tx, opts) {
6516
+ var undoUnits, redoCaptureUnits, updateFlags, store, entryId, storedUndoUnits,
6517
+ storedRedoUnits, storeStart, storeMillis = 0, evictToken = {};
6518
+ opts = opts || {};
6519
6519
  if (!tx || opts.error) return {skipped: true};
6520
6520
  undoUnits = filterUnchangedRestoreUnits(tx.getCapturedUnits());
6521
6521
  if (!undoUnits.some(isRestoreUnit)) return {
@@ -10788,6 +10788,9 @@
10788
10788
  if (e.deleteLayer) {
10789
10789
  addMenuItem('delete layer', e.deleteLayer, '');
10790
10790
  }
10791
+ if (e.styleLayer) {
10792
+ addMenuItem('style layer', e.styleLayer, '');
10793
+ }
10791
10794
  if (e.showLayerInfo) {
10792
10795
  addMenuItem('show info', e.showLayerInfo, '');
10793
10796
  }
@@ -11225,8 +11228,19 @@
11225
11228
  content.node().appendChild(renderLayerInfo(internal.getLayerInfo(target.layer, target.dataset)));
11226
11229
  }
11227
11230
 
11231
+ function styleLayer() {
11232
+ var target = findLayerById(id);
11233
+ if (!target) return;
11234
+ if (target.layer.geometry_type == 'point' && gui.pointStyleTool) {
11235
+ gui.pointStyleTool.open(target.layer, target.dataset);
11236
+ } else if (gui.layerStyleTool) {
11237
+ gui.layerStyleTool.open(target.layer, target.dataset);
11238
+ }
11239
+ }
11240
+
11228
11241
  function openLayerMenu(e) {
11229
11242
  var menuEvent = e;
11243
+ var target = findLayerById(id);
11230
11244
  e.stopPropagation();
11231
11245
  if (!isFinite(e.pageX) || !isFinite(e.pageY)) {
11232
11246
  var rect = moreBtn.node().getBoundingClientRect();
@@ -11237,6 +11251,9 @@
11237
11251
  }
11238
11252
  menuEvent.deleteLayer = deleteLayer;
11239
11253
  menuEvent.showLayerInfo = showLayerInfo;
11254
+ if (target && layerCanBeStyled(target.layer)) {
11255
+ menuEvent.styleLayer = styleLayer;
11256
+ }
11240
11257
  menuEvent.contextMenuId = 'layer-' + id;
11241
11258
  openContextMenu(menuEvent, null, null);
11242
11259
  }
@@ -11446,6 +11463,10 @@
11446
11463
  return internal.layerIsGeometric(lyr) || internal.layerHasRaster(lyr) || internal.layerHasFurniture(lyr);
11447
11464
  }
11448
11465
 
11466
+ function layerCanBeStyled(lyr) {
11467
+ return !!(lyr && (lyr.geometry_type == 'point' || lyr.geometry_type == 'polyline' || lyr.geometry_type == 'polygon'));
11468
+ }
11469
+
11449
11470
  function rowHTML(c1, c2, cname) {
11450
11471
  return utils$1.format('<div class="row%s"><div class="col1">%s</div>' +
11451
11472
  '<div class="col2">%s</div></div>', cname ? ' ' + cname : '', c1, c2);
@@ -11676,32 +11697,148 @@
11676
11697
  return value.toFixed(value < 10 && value >= 0.5 ? 1 : 0) + ' ' + units[i];
11677
11698
  }
11678
11699
 
11679
- function SessionHistory(gui) {
11680
- var commands = [];
11681
- var commandId = 0;
11682
- // index of first command after the last "save" boundary; commands at indices
11683
- // [savedAtIndex .. commands.length) are considered unsaved
11684
- var savedAtIndex = 0;
11685
- // commands that can be ignored when checking for unsaved changes
11686
- var nonEditingCommands = 'i,target,info,version,verbose,projections,inspect,help,h,encodings,calc,comment'.split(',');
11700
+ function cullCommandHistory(commands, parseCommand, isStyleProperty) {
11701
+ var out = [];
11702
+ (commands || []).forEach(function(cmd) {
11703
+ addCommand(out, cmd, parseCommand, isStyleProperty);
11704
+ });
11705
+ return out;
11706
+ }
11687
11707
 
11688
- this.unsavedChanges = function() {
11689
- for (var i = commands.length - 1; i >= savedAtIndex; i--) {
11690
- if (!commandIsActive(commands[i])) continue;
11691
- var cmdName = getCommandName(getCommandString(commands[i]));
11692
- if (nonEditingCommands.includes(cmdName)) continue;
11693
- return true;
11694
- }
11695
- return false;
11696
- };
11708
+ function addCommand(commands, cmd, parseCommand, isStyleProperty) {
11709
+ var curr = getCullableCommand(cmd, parseCommand, isStyleProperty);
11710
+ var prev;
11711
+ if (!curr) {
11712
+ commands.push(cmd);
11713
+ return;
11714
+ }
11715
+ while (commands.length > 0) {
11716
+ prev = getCullableCommand(commands[commands.length - 1], parseCommand, isStyleProperty);
11717
+ if (!prev || !commandCanCull(curr, prev)) break;
11718
+ commands.pop();
11719
+ }
11720
+ commands.push(cmd);
11721
+ }
11697
11722
 
11698
- this.isEmpty = function() {
11699
- return getActiveCommands().length === 0;
11700
- };
11723
+ function commandCanCull(curr, prev) {
11724
+ if (curr.type != prev.type || curr.target != prev.target || curr.ids != prev.ids) {
11725
+ return false;
11726
+ }
11727
+ if (curr.type == 'style') {
11728
+ return isSuperset(curr.fields, prev.fields);
11729
+ }
11730
+ return curr.key == prev.key;
11731
+ }
11701
11732
 
11702
- // Mark the current end of the history as a "saved" boundary -- called after
11703
- // data has been written somewhere durable (e.g. an -o export). Snapshots
11704
- // are session-scoped and are NOT durable, so creating one does not mark saved.
11733
+ function getCullableCommand(str, parseCommand, isStyleProperty) {
11734
+ var parsed, cmd, opts;
11735
+ if (!parseCommand || !isStyleProperty) return null;
11736
+ try {
11737
+ parsed = parseCommand(str);
11738
+ } catch(e) {
11739
+ return null;
11740
+ }
11741
+ if (!parsed || parsed.length != 1) return null;
11742
+ cmd = parsed[0];
11743
+ opts = cmd.options || {};
11744
+ if (cmd.name == 'style' || cmd.name == 'svg-style') {
11745
+ return getStyleCullInfo(opts, isStyleProperty);
11746
+ }
11747
+ if (cmd.name == 'classify') {
11748
+ return getClassifyCullInfo(opts);
11749
+ }
11750
+ return null;
11751
+ }
11752
+
11753
+ function getStyleCullInfo(opts, isStyleProperty) {
11754
+ var fields;
11755
+ if (opts.clear || opts.where) return null;
11756
+ fields = getStyleFields$1(opts, isStyleProperty);
11757
+ if (fields.length === 0) return null;
11758
+ return {
11759
+ type: 'style',
11760
+ target: getTargetKey(opts),
11761
+ ids: getIdsKey(opts),
11762
+ fields: fields
11763
+ };
11764
+ }
11765
+
11766
+ function getStyleFields$1(opts, isStyleProperty) {
11767
+ var fields = [];
11768
+ Object.keys(opts).forEach(function(name) {
11769
+ var field = name.replace(/_/g, '-');
11770
+ if (isStyleProperty(field) && fields.indexOf(field) == -1) {
11771
+ fields.push(field);
11772
+ }
11773
+ });
11774
+ return fields.sort();
11775
+ }
11776
+
11777
+ function getClassifyCullInfo(opts) {
11778
+ if (opts.where || !classifyIsRandomFill(opts)) return null;
11779
+ return {
11780
+ type: 'classify',
11781
+ target: getTargetKey(opts),
11782
+ ids: getIdsKey(opts),
11783
+ key: 'random-fill'
11784
+ };
11785
+ }
11786
+
11787
+ function classifyIsRandomFill(opts) {
11788
+ return opts.colors == 'random' && opts.method == 'non-adjacent' &&
11789
+ !opts.field && !opts.values && !opts['save-as'] && !opts.save_as;
11790
+ }
11791
+
11792
+ function getTargetKey(opts) {
11793
+ return opts.target ? String(opts.target) : '';
11794
+ }
11795
+
11796
+ function getIdsKey(opts) {
11797
+ var ids;
11798
+ if (!opts.ids) return '';
11799
+ if (Array.isArray(opts.ids)) {
11800
+ return opts.ids.concat().sort(function(a, b) {return a - b;}).join(',');
11801
+ }
11802
+ ids = String(opts.ids).split(',').map(Number);
11803
+ if (ids.every(isFinite)) {
11804
+ return ids.sort(function(a, b) {return a - b;}).join(',');
11805
+ }
11806
+ return String(opts.ids);
11807
+ }
11808
+
11809
+ function isSuperset(a, b) {
11810
+ for (var i=0; i<b.length; i++) {
11811
+ if (a.indexOf(b[i]) == -1) return false;
11812
+ }
11813
+ return true;
11814
+ }
11815
+
11816
+ function SessionHistory(gui) {
11817
+ var commands = [];
11818
+ var commandId = 0;
11819
+ // index of first command after the last "save" boundary; commands at indices
11820
+ // [savedAtIndex .. commands.length) are considered unsaved
11821
+ var savedAtIndex = 0;
11822
+ // commands that can be ignored when checking for unsaved changes
11823
+ var nonEditingCommands = 'i,target,info,version,verbose,projections,inspect,help,h,encodings,calc,comment'.split(',');
11824
+
11825
+ this.unsavedChanges = function() {
11826
+ for (var i = commands.length - 1; i >= savedAtIndex; i--) {
11827
+ if (!commandIsActive(commands[i])) continue;
11828
+ var cmdName = getCommandName(getCommandString(commands[i]));
11829
+ if (nonEditingCommands.includes(cmdName)) continue;
11830
+ return true;
11831
+ }
11832
+ return false;
11833
+ };
11834
+
11835
+ this.isEmpty = function() {
11836
+ return getActiveCommands().length === 0;
11837
+ };
11838
+
11839
+ // Mark the current end of the history as a "saved" boundary -- called after
11840
+ // data has been written somewhere durable (e.g. an -o export). Snapshots
11841
+ // are session-scoped and are NOT durable, so creating one does not mark saved.
11705
11842
  this.markSaved = function() {
11706
11843
  savedAtIndex = commands.length;
11707
11844
  };
@@ -11814,7 +11951,8 @@
11814
11951
  };
11815
11952
 
11816
11953
  this.toCommandLineString = function() {
11817
- var str = getActiveCommands().join(' \\\n ');
11954
+ var str = cullCommandHistory(getActiveCommands(), internal.parseCommands,
11955
+ internal.isSupportedSvgStyleProperty).join(' \\\n ');
11818
11956
  return 'mapshaper ' + str;
11819
11957
  };
11820
11958
 
@@ -12551,13 +12689,13 @@
12551
12689
  var menus = {
12552
12690
  standard: ['info', 'selection', 'box'],
12553
12691
  empty: ['edit_polygons', 'edit_lines', 'edit_points', 'box'],
12554
- polygons: ['info', 'selection', 'box', 'edit_polygons'],
12555
- rectangles: ['info', 'selection', 'box', 'rectangles', 'edit_polygons'],
12556
- lines: ['info', 'selection', 'box', 'edit_lines'], // 'snip_lines'
12692
+ polygons: ['info', 'selection', 'box', 'polygon_style', 'edit_polygons'],
12693
+ rectangles: ['info', 'selection', 'box', 'polygon_style', 'rectangles', 'edit_polygons'],
12694
+ lines: ['info', 'selection', 'box', 'line_style', 'edit_lines'], // 'snip_lines'
12557
12695
  table: ['info', 'selection'],
12558
12696
  raster: ['box'],
12559
- labels: ['info', 'selection', 'box', 'labels', 'edit_points'],
12560
- points: ['info', 'selection', 'box', 'edit_points'] // , 'add-points'
12697
+ labels: ['info', 'selection', 'box', 'point_style', 'labels', 'edit_points'],
12698
+ points: ['info', 'selection', 'box', 'point_style', 'edit_points'] // , 'add-points'
12561
12699
  };
12562
12700
 
12563
12701
  var prompts = {
@@ -12572,6 +12710,9 @@
12572
12710
  box: 'rectangle tool',
12573
12711
  data: 'edit attributes',
12574
12712
  label_style: 'style labels',
12713
+ point_style: 'style points',
12714
+ line_style: 'style lines',
12715
+ polygon_style: 'style polygons',
12575
12716
  labels: 'position labels',
12576
12717
  edit_points: 'add/drag points',
12577
12718
  edit_lines: 'draw/edit polylines',
@@ -12606,7 +12747,7 @@
12606
12747
  });
12607
12748
 
12608
12749
  btn.on('mouseenter', function() {
12609
- if (gui.state.label_style_panel_open) return;
12750
+ if (stylePanelIsActive()) return;
12610
12751
  btn.addClass('hover');
12611
12752
  if (_menuOpen) {
12612
12753
  clearTimeout(_menuTimeout); // prevent timed closing
@@ -12620,7 +12761,7 @@
12620
12761
 
12621
12762
  btn.on('click', function(e) {
12622
12763
  if (_editMode == 'label_style') {
12623
- setMode('info');
12764
+ setMode('off');
12624
12765
  closeMenu();
12625
12766
  } else if (active()) {
12626
12767
  setMode('off');
@@ -12645,7 +12786,7 @@
12645
12786
  };
12646
12787
 
12647
12788
  this.modeUsesHitDetection = function(mode) {
12648
- return ['info', 'selection', 'data', 'label_style', 'labels', 'edit_points', 'vertices', 'rectangles', 'edit_lines', 'edit_polygons', 'snip_lines'].includes(mode);
12789
+ return ['info', 'selection', 'data', 'label_style', 'point_style', 'line_style', 'polygon_style', 'labels', 'edit_points', 'vertices', 'rectangles', 'edit_lines', 'edit_polygons', 'snip_lines'].includes(mode);
12649
12790
  };
12650
12791
 
12651
12792
  this.modeUsesPopup = function(mode) {
@@ -12653,7 +12794,7 @@
12653
12794
  };
12654
12795
 
12655
12796
  this.modeSupportsUndo = function(mode) {
12656
- return ['data', 'label_style', 'labels', 'edit_points', 'edit_lines', 'edit_polygons', 'vertices', 'rectangles'].includes(mode);
12797
+ return ['data', 'label_style', 'point_style', 'line_style', 'polygon_style', 'labels', 'edit_points', 'edit_lines', 'edit_polygons', 'vertices', 'rectangles'].includes(mode);
12657
12798
  };
12658
12799
 
12659
12800
  this.getMode = getInteractionMode;
@@ -12677,6 +12818,11 @@
12677
12818
  return _editMode && _editMode != 'off';
12678
12819
  }
12679
12820
 
12821
+ function stylePanelIsActive() {
12822
+ return _editMode == 'label_style' || _editMode == 'point_style' ||
12823
+ _editMode == 'line_style' || _editMode == 'polygon_style';
12824
+ }
12825
+
12680
12826
  function getAvailableModes() {
12681
12827
  var o = gui.model.getActiveLayer();
12682
12828
  if (!o || !o.layer) {
@@ -12716,7 +12862,7 @@
12716
12862
  modes.forEach(function(mode) {
12717
12863
  // don't show "turn off" link if not currently editing
12718
12864
  if (_editMode == 'off' && mode == 'off') return;
12719
- var link = El('div').addClass('nav-menu-item').attr('data-name', mode).text(labels[mode]).appendTo(menu);
12865
+ var link = El('div').addClass('nav-menu-item').attr('data-name', mode).text(getModeLabel(mode)).appendTo(menu);
12720
12866
  link.on('click', function(e) {
12721
12867
  if (_editMode == mode) {
12722
12868
  // closeMenu();
@@ -12732,10 +12878,18 @@
12732
12878
  updateSelectionHighlight();
12733
12879
  }
12734
12880
 
12881
+ function getModeLabel(mode) {
12882
+ var o = gui.model.getActiveLayer();
12883
+ if (mode == 'point_style' && o && o.layer && internal.layerHasLabels(o.layer)) {
12884
+ return 'style labels';
12885
+ }
12886
+ return labels[mode];
12887
+ }
12888
+
12735
12889
  // if current editing mode is not available, turn off the tool
12736
12890
  function updateCurrentMode() {
12737
12891
  var modes = getAvailableModes();
12738
- if (modes.indexOf(_editMode) == -1 && !labelStyleModeIsAvailable()) {
12892
+ if (modes.indexOf(_editMode) == -1 && !labelStyleModeIsAvailable() && !layerStyleModeIsAvailable() && !pointStyleModeIsAvailable()) {
12739
12893
  setMode('off');
12740
12894
  }
12741
12895
  }
@@ -12745,6 +12899,17 @@
12745
12899
  return _editMode == 'label_style' && o && o.layer && internal.layerHasLabels(o.layer);
12746
12900
  }
12747
12901
 
12902
+ function layerStyleModeIsAvailable() {
12903
+ var o = gui.model.getActiveLayer();
12904
+ return _editMode == 'line_style' && o && o.layer && o.layer.geometry_type == 'polyline' ||
12905
+ _editMode == 'polygon_style' && o && o.layer && o.layer.geometry_type == 'polygon';
12906
+ }
12907
+
12908
+ function pointStyleModeIsAvailable() {
12909
+ var o = gui.model.getActiveLayer();
12910
+ return _editMode == 'point_style' && o && o.layer && o.layer.geometry_type == 'point';
12911
+ }
12912
+
12748
12913
  function openMenu() {
12749
12914
  clearTimeout(_menuTimeout);
12750
12915
  if (!_menuOpen) {
@@ -12795,7 +12960,7 @@
12795
12960
  }
12796
12961
  btn.classed('hover', _menuOpen);
12797
12962
  // btn.classed('selected', active() && !_menuOpen);
12798
- btn.classed('selected', active() && _editMode != 'label_style');
12963
+ btn.classed('selected', active());
12799
12964
  }
12800
12965
 
12801
12966
  function updateSelectionHighlight() {
@@ -13511,180 +13676,722 @@
13511
13676
  return Math.round(val * 100) / 100;
13512
13677
  }
13513
13678
 
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
13679
+ var grayscaleColorPresets = [
13680
+ '#000000', '#111111', '#222222', '#333333',
13681
+ '#444444', '#555555', '#666666', '#777777',
13682
+ '#888888', '#999999', '#aaaaaa', '#bbbbbb',
13683
+ '#cccccc', '#dddddd', '#eeeeee', '#ffffff'
13542
13684
  ];
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
13685
 
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
- });
13686
+ // Sequential ramps generated in OKLCH for smooth lightness/chroma progression,
13687
+ // then hue-normalized in the picker HSB space so every chip in a ramp reports
13688
+ // the same hue in the color picker.
13689
+ var redRamp = ['#600202', '#8a0303', '#b11b1b', '#cf3b3b', '#e26161', '#ef8b8b', '#f5b8b8', '#fbdede'];
13690
+ var orangeRamp = ['#94551e', '#a85f1e', '#bd6d26', '#d17f36', '#e39652', '#f0b37d', '#f7cda8', '#fce1ca'];
13691
+ var brownRamp = ['#442b03', '#634009', '#825a1b', '#9c763b', '#b39361', '#c8b08b', '#dcceb8', '#eee8df'];
13692
+ var greenRamp = ['#013c06', '#03580b', '#00750b', '#2e9137', '#5daa64', '#8bc190', '#b8d8bb', '#dfebe0'];
13693
+ var tealRamp = ['#023937', '#005350', '#036f6b', '#058d88', '#02aba5', '#65c4c1', '#a7d9d7', '#d7edec'];
13694
+ var blueRamp = ['#013550', '#034d73', '#016599', '#0881bf', '#39a5dd', '#77c2e8', '#aed9ef', '#daedf7'];
13695
+ var indigoRamp = ['#1d2964', '#2d3d8e', '#4053b5', '#596dd1', '#7889e1', '#9ba8eb', '#c0c8f1', '#e1e5f8'];
13696
+ var purpleRamp = ['#431b54', '#612b78', '#7f3f9a', '#9b5cb5', '#b37dc9', '#c8a0d9', '#dcc4e6', '#eee2f3'];
13697
+
13698
+ var layerColorPresetRows = [
13699
+ grayscaleColorPresets,
13700
+ redRamp.concat(orangeRamp),
13701
+ brownRamp.concat(greenRamp),
13702
+ tealRamp.concat(blueRamp),
13703
+ indigoRamp.concat(purpleRamp)
13704
+ ];
13588
13705
 
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
- }
13706
+ function ColorPicker(parent, opts) {
13707
+ opts = opts || {};
13708
+ var colorPicker = El('div').addClass('label-color-picker').appendTo(parent).hide();
13709
+ var sbCanvas, hueCanvas, sbMarker, hueMarker, pickerHsbInputs;
13710
+ var pickerColor = {h: 0, s: 0, b: 0};
13711
+ var presetRows = opts.presetRows || [grayscaleColorPresets];
13598
13712
 
13599
- updateVisibility();
13713
+ init();
13600
13714
 
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
- });
13715
+ this.toggle = function() {
13716
+ if (colorPicker.visible()) {
13717
+ this.hide();
13718
+ } else {
13719
+ colorPicker.show();
13720
+ drawColorPicker();
13721
+ }
13722
+ };
13606
13723
 
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);
13724
+ this.hide = function() {
13725
+ colorPicker.hide();
13726
+ };
13610
13727
 
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
- });
13728
+ this.visible = function() {
13729
+ return colorPicker.visible();
13730
+ };
13618
13731
 
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
- });
13732
+ this.setColor = function(color) {
13733
+ if (isHexColor(color)) {
13734
+ setPickerColor(hexToHsb(color));
13735
+ }
13736
+ };
13626
13737
 
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);
13738
+ this.getColor = function() {
13739
+ return hsbToHex(pickerColor);
13740
+ };
13641
13741
 
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);
13742
+ function init() {
13743
+ var sbWrap = El('div').addClass('label-color-canvas-wrap').appendTo(colorPicker);
13744
+ sbCanvas = El('canvas').attr('width', '256').attr('height', '256').appendTo(sbWrap);
13745
+ sbMarker = makePickerMarker().appendTo(sbWrap);
13746
+ var hueWrap = El('div').addClass('label-color-canvas-wrap').appendTo(colorPicker);
13747
+ hueCanvas = El('canvas').attr('width', '256').attr('height', '18').appendTo(hueWrap);
13748
+ hueMarker = makePickerMarker().appendTo(hueWrap);
13749
+ renderPresetRows(colorPicker);
13750
+ pickerHsbInputs = {};
13751
+ var hsbRow = El('div').addClass('label-color-picker-fields').appendTo(colorPicker);
13752
+ addPickerNumberInput(hsbRow, 'h', 'H');
13753
+ addPickerNumberInput(hsbRow, 's', 'S');
13754
+ addPickerNumberInput(hsbRow, 'b', 'B');
13755
+ El('button').appendTo(hsbRow).text('Close').on('click', function() {
13756
+ colorPicker.hide();
13646
13757
  });
13647
- fontSizeText = El('span').addClass('label-size-value').appendTo(fontSizeRow);
13648
- makePanelButton(fontSizeRow, '+', function() {
13649
- nudgeFontSize(1);
13758
+ sbCanvas.on('mousedown', function(e) {
13759
+ startCanvasDrag(e, updateSbFromEvent);
13650
13760
  });
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());
13761
+ hueCanvas.on('mousedown', function(e) {
13762
+ startCanvasDrag(e, updateHueFromEvent);
13656
13763
  });
13764
+ setPickerColor(pickerColor);
13765
+ }
13657
13766
 
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);
13767
+ function renderPresetRows(parent) {
13768
+ var container = El('div').addClass('label-color-presets').appendTo(parent);
13769
+ presetRows.forEach(function(row) {
13770
+ var rowEl = El('div').addClass('label-color-preset-row').appendTo(container);
13771
+ row.forEach(function(color, i) {
13772
+ var tile = El('div')
13773
+ .addClass('label-color-preset')
13774
+ .attr('role', 'button')
13775
+ .attr('tabindex', '0')
13776
+ .attr('title', color)
13777
+ .appendTo(rowEl)
13778
+ .css('background-color', color);
13779
+ if (row.length == 16 && i == 8) tile.addClass('label-color-preset-group-start');
13780
+ tile.on('click', function() {
13781
+ applyPreset(color);
13782
+ }).on('keydown', function(e) {
13783
+ if (e.key == 'Enter' || e.key == ' ') {
13784
+ e.preventDefault();
13785
+ applyPreset(color);
13786
+ }
13787
+ });
13788
+ });
13671
13789
  });
13790
+ }
13672
13791
 
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
- });
13792
+ function applyPreset(color) {
13793
+ setPickerColor(hexToHsb(color));
13794
+ commitPickerColor();
13795
+ }
13682
13796
 
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) {
13797
+ function addPickerNumberInput(row, name, label) {
13798
+ var wrapper = El('label').appendTo(row);
13799
+ El('span').appendTo(wrapper).text(label);
13800
+ pickerHsbInputs[name] = El('input')
13801
+ .attr('type', 'text')
13802
+ .appendTo(wrapper)
13803
+ .on('change', function() {
13804
+ var h = parseNumberField(pickerHsbInputs.h.node().value);
13805
+ var s = parseNumberField(pickerHsbInputs.s.node().value);
13806
+ var b = parseNumberField(pickerHsbInputs.b.node().value);
13807
+ if (!isFinite(h) || !isFinite(s) || !isFinite(b)) return;
13808
+ setPickerColor({
13809
+ h: degreesToByte(h),
13810
+ s: pctToByte(s),
13811
+ b: pctToByte(b)
13812
+ });
13813
+ commitPickerColor();
13814
+ });
13815
+ }
13816
+
13817
+ function startCanvasDrag(e, update) {
13818
+ var evt = e.originalEvent || e;
13819
+ colorPicker.addClass('dragging-color');
13820
+ El('body').addClass('dragging-color-picker');
13821
+ update(evt);
13822
+ document.addEventListener('mousemove', onmove);
13823
+ document.addEventListener('mouseup', onup);
13824
+ evt.preventDefault();
13825
+ function onmove(evt) {
13826
+ update(evt);
13827
+ }
13828
+ function onup() {
13829
+ document.removeEventListener('mousemove', onmove);
13830
+ document.removeEventListener('mouseup', onup);
13831
+ colorPicker.removeClass('dragging-color');
13832
+ El('body').removeClass('dragging-color-picker');
13833
+ commitPickerColor();
13834
+ }
13835
+ }
13836
+
13837
+ function updateSbFromEvent(evt) {
13838
+ var p = getCanvasPoint(sbCanvas.node(), evt);
13839
+ setPickerColor({
13840
+ h: pickerColor.h,
13841
+ s: p.x,
13842
+ b: 255 - p.y
13843
+ });
13844
+ }
13845
+
13846
+ function updateHueFromEvent(evt) {
13847
+ var p = getCanvasPoint(hueCanvas.node(), evt);
13848
+ setPickerColor({
13849
+ h: p.x,
13850
+ s: pickerColor.s,
13851
+ b: pickerColor.b
13852
+ });
13853
+ }
13854
+
13855
+ function getCanvasPoint(canvas, evt) {
13856
+ var rect = canvas.getBoundingClientRect();
13857
+ return {
13858
+ x: clamp(Math.round((evt.clientX - rect.left) / rect.width * (canvas.width - 1)), 0, canvas.width - 1),
13859
+ y: clamp(Math.round((evt.clientY - rect.top) / rect.height * (canvas.height - 1)), 0, canvas.height - 1)
13860
+ };
13861
+ }
13862
+
13863
+ function setPickerColor(hsb) {
13864
+ pickerColor = {
13865
+ h: clamp(Math.round(hsb.h), 0, 255),
13866
+ s: clamp(Math.round(hsb.s), 0, 255),
13867
+ b: clamp(Math.round(hsb.b), 0, 255)
13868
+ };
13869
+ drawColorPicker();
13870
+ updatePickerFields();
13871
+ if (opts.onPreview) opts.onPreview(hsbToHex(pickerColor));
13872
+ }
13873
+
13874
+ function updatePickerFields() {
13875
+ pickerHsbInputs.h.node().value = byteToDegrees(pickerColor.h) + '°';
13876
+ pickerHsbInputs.s.node().value = byteToPct(pickerColor.s) + '%';
13877
+ pickerHsbInputs.b.node().value = byteToPct(pickerColor.b) + '%';
13878
+ }
13879
+
13880
+ function drawColorPicker() {
13881
+ if (!sbCanvas) return;
13882
+ drawSaturationBrightnessCanvas();
13883
+ drawHueCanvas();
13884
+ }
13885
+
13886
+ function drawSaturationBrightnessCanvas() {
13887
+ var ctx = sbCanvas.node().getContext('2d');
13888
+ var image = ctx.createImageData(256, 256);
13889
+ var data = image.data;
13890
+ var i = 0;
13891
+ for (var y=0; y<256; y++) {
13892
+ for (var x=0; x<256; x++) {
13893
+ var rgb = hsbToRgb({
13894
+ h: pickerColor.h,
13895
+ s: x,
13896
+ b: 255 - y
13897
+ });
13898
+ data[i++] = rgb.r;
13899
+ data[i++] = rgb.g;
13900
+ data[i++] = rgb.b;
13901
+ data[i++] = 255;
13902
+ }
13903
+ }
13904
+ ctx.putImageData(image, 0, 0);
13905
+ positionMarker(sbMarker, pickerColor.s, 255 - pickerColor.b, pickerColor);
13906
+ }
13907
+
13908
+ function drawHueCanvas() {
13909
+ var ctx = hueCanvas.node().getContext('2d');
13910
+ var image = ctx.createImageData(256, 18);
13911
+ var data = image.data;
13912
+ var i = 0;
13913
+ for (var y=0; y<18; y++) {
13914
+ for (var x=0; x<256; x++) {
13915
+ var rgb = hsbToRgb({h: x, s: 255, b: 255});
13916
+ data[i++] = rgb.r;
13917
+ data[i++] = rgb.g;
13918
+ data[i++] = rgb.b;
13919
+ data[i++] = 255;
13920
+ }
13921
+ }
13922
+ ctx.putImageData(image, 0, 0);
13923
+ positionMarker(hueMarker, pickerColor.h, 9, {h: pickerColor.h, s: 255, b: 255});
13924
+ }
13925
+
13926
+ function commitPickerColor() {
13927
+ if (opts.onChange) opts.onChange(hsbToHex(pickerColor));
13928
+ }
13929
+
13930
+ function getMarkerColor(rgb) {
13931
+ var luminance = rgb.r * 0.2126 + rgb.g * 0.7152 + rgb.b * 0.0722;
13932
+ return luminance < 128 ? '#fff' : '#000';
13933
+ }
13934
+
13935
+ function makePickerMarker() {
13936
+ 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>');
13937
+ }
13938
+
13939
+ function positionMarker(marker, x, y, hsb) {
13940
+ var rgb = hsbToRgb(hsb);
13941
+ x = utils$1.clamp(x, 1, 254);
13942
+ y = utils$1.clamp(y, 1, 254) + 0.5;
13943
+ marker.css({
13944
+ left: x - 8,
13945
+ top: y - 8
13946
+ });
13947
+ marker.findChild('circle').attr('stroke', getMarkerColor(rgb));
13948
+ }
13949
+ }
13950
+
13951
+ function isHexColor(str) {
13952
+ return /^#[0-9a-f]{6}$/i.test(str);
13953
+ }
13954
+
13955
+ function hexToHsb(hex) {
13956
+ var r = parseInt(hex.substr(1, 2), 16);
13957
+ var g = parseInt(hex.substr(3, 2), 16);
13958
+ var b = parseInt(hex.substr(5, 2), 16);
13959
+ var max = Math.max(r, g, b);
13960
+ var min = Math.min(r, g, b);
13961
+ var d = max - min;
13962
+ var h = 0;
13963
+ if (d) {
13964
+ if (max == r) h = ((g - b) / d) % 6;
13965
+ else if (max == g) h = (b - r) / d + 2;
13966
+ else h = (r - g) / d + 4;
13967
+ h *= 60;
13968
+ if (h < 0) h += 360;
13969
+ }
13970
+ return {
13971
+ h: Math.round(h / 360 * 255),
13972
+ s: Math.round(max === 0 ? 0 : d / max * 255),
13973
+ b: max
13974
+ };
13975
+ }
13976
+
13977
+ function hsbToHex(hsb) {
13978
+ var rgb = hsbToRgb(hsb);
13979
+ return '#' + [rgb.r, rgb.g, rgb.b].map(function(val) {
13980
+ return val.toString(16).padStart(2, '0');
13981
+ }).join('');
13982
+ }
13983
+
13984
+ function hsbToRgb(hsb) {
13985
+ var h = hsb.h / 255 * 360;
13986
+ var s = hsb.s / 255;
13987
+ var v = hsb.b / 255;
13988
+ var c = v * s;
13989
+ var x = c * (1 - Math.abs((h / 60) % 2 - 1));
13990
+ var m = v - c;
13991
+ var rgb = h < 60 ? [c, x, 0] :
13992
+ h < 120 ? [x, c, 0] :
13993
+ h < 180 ? [0, c, x] :
13994
+ h < 240 ? [0, x, c] :
13995
+ h < 300 ? [x, 0, c] : [c, 0, x];
13996
+ return {
13997
+ r: Math.round((rgb[0] + m) * 255),
13998
+ g: Math.round((rgb[1] + m) * 255),
13999
+ b: Math.round((rgb[2] + m) * 255)
14000
+ };
14001
+ }
14002
+
14003
+ function byteToPct(val) {
14004
+ return Math.round(val / 255 * 100);
14005
+ }
14006
+
14007
+ function pctToByte(val) {
14008
+ return Math.round(clamp(val, 0, 100) / 100 * 255);
14009
+ }
14010
+
14011
+ function byteToDegrees(val) {
14012
+ return Math.round(val / 255 * 360);
14013
+ }
14014
+
14015
+ function degreesToByte(val) {
14016
+ return Math.round(clamp(val, 0, 360) / 360 * 255);
14017
+ }
14018
+
14019
+ function parseNumberField(str) {
14020
+ return Number(String(str).replace(/[°%]/g, '').trim());
14021
+ }
14022
+
14023
+ function clamp(val, min, max) {
14024
+ return isFinite(val) ? Math.max(min, Math.min(max, val)) : min;
14025
+ }
14026
+
14027
+ function makeStylePresetId(styles, prefix, name) {
14028
+ var base = prefix + '-' + makeSlug(name);
14029
+ var ids = (styles || []).reduce(function(memo, item) {
14030
+ if (item && item.id) memo[item.id] = true;
14031
+ if (item && !item.id && item.name) memo[prefix + '-' + makeSlug(item.name)] = true;
14032
+ return memo;
14033
+ }, {});
14034
+ var id = base;
14035
+ var i = 2;
14036
+ while (ids[id]) {
14037
+ id = base + '-' + i++;
14038
+ }
14039
+ return id;
14040
+ }
14041
+
14042
+ function makeSlug(str) {
14043
+ var slug = String(str || '')
14044
+ .toLowerCase()
14045
+ .replace(/[^a-z0-9]+/g, '-')
14046
+ .replace(/^-+|-+$/g, '');
14047
+ return slug || 'style';
14048
+ }
14049
+
14050
+ function StylePresetControl(parent, opts) {
14051
+ var row = El('div').addClass('label-style-row label-saved-style-row').appendTo(parent);
14052
+ var select, saveBtn, deleteBtn;
14053
+
14054
+ El('span').appendTo(row).text('Presets');
14055
+ select = El('select').appendTo(row).on('change', function() {
14056
+ var item = findVisibleStyle(select.node().value);
14057
+ if (item) {
14058
+ opts.applyStyle(item.style);
14059
+ }
14060
+ updateControls();
14061
+ });
14062
+ saveBtn = El('button').appendTo(row).text('Save preset').on('click', openSaveStylePopup);
14063
+ deleteBtn = El('button').appendTo(row).text('Delete').on('click', deleteSelectedStyle);
14064
+ render();
14065
+
14066
+ this.render = render;
14067
+ this.update = updateControls;
14068
+ this.clearSelection = clearSelection;
14069
+ this.select = function() {
14070
+ return select;
14071
+ };
14072
+
14073
+ function openSaveStylePopup() {
14074
+ var popup = showPopupAlert('', opts.saveTitle);
14075
+ var el = popup.container();
14076
+ el.addClass('option-menu');
14077
+ el.html(`<div><input type="text" class="style-name text-input" placeholder="style name"></div>
14078
+ <div tabindex="0" class="btn dialog-btn">Save</div>`);
14079
+ var input = el.findChild('.style-name');
14080
+ var btn = el.findChild('.btn');
14081
+ input.node().focus();
14082
+ btn.on('click', function() {
14083
+ var name = input.node().value.trim();
14084
+ if (!name) return;
14085
+ saveStyleWithName(name);
14086
+ popup.close();
14087
+ });
14088
+ input.on('keydown', function(e) {
14089
+ if (e.key == 'Enter') {
14090
+ btn.node().click();
14091
+ }
14092
+ });
14093
+ }
14094
+
14095
+ function saveStyleWithName(name) {
14096
+ var styles = getSavedStyles();
14097
+ var type = getType();
14098
+ var id = makeStylePresetId(styles, type, name);
14099
+ var item = {
14100
+ id: id,
14101
+ name: name,
14102
+ style: opts.getStyle()
14103
+ };
14104
+ if (opts.useType !== false) {
14105
+ item.type = type;
14106
+ }
14107
+ styles.push(item);
14108
+ styles.sort(opts.sort || sortByName);
14109
+ setSavedStyles(styles);
14110
+ render();
14111
+ }
14112
+
14113
+ async function deleteSelectedStyle() {
14114
+ var id = select.node().value;
14115
+ var item = findVisibleStyle(id);
14116
+ var styles;
14117
+ if (!item) return;
14118
+ if (!await showPrompt('Delete ' + opts.styleLabel + ' "' + item.name + '"?', 'Delete preset')) return;
14119
+ styles = getSavedStyles().filter(function(item) {
14120
+ return getItemId(item) != id;
14121
+ });
14122
+ setSavedStyles(styles);
14123
+ render();
14124
+ }
14125
+
14126
+ function render(selectedId) {
14127
+ var styles = getVisibleStyles();
14128
+ var value = selectedId || select.node().value;
14129
+ select.empty();
14130
+ if (styles.length > 0) {
14131
+ El('option').attr('value', '').appendTo(select).text('Apply preset...');
14132
+ styles.forEach(function(item) {
14133
+ El('option').attr('value', getItemId(item)).appendTo(select).text(item.name);
14134
+ });
14135
+ select.node().value = findVisibleStyle(value) ? value : '';
14136
+ } else {
14137
+ El('option').attr('value', '').appendTo(select).text('No presets');
14138
+ select.node().value = '';
14139
+ }
14140
+ updateControls();
14141
+ }
14142
+
14143
+ function updateControls() {
14144
+ var disabled = opts.disabled ? opts.disabled() : false;
14145
+ var hasPresets = getVisibleStyles().length > 0;
14146
+ select.node().disabled = disabled || !hasPresets;
14147
+ saveBtn.node().disabled = disabled;
14148
+ deleteBtn.node().disabled = disabled || !select.node().value;
14149
+ }
14150
+
14151
+ function clearSelection() {
14152
+ if (select.node().value) {
14153
+ select.node().value = '';
14154
+ updateControls();
14155
+ }
14156
+ }
14157
+
14158
+ function getVisibleStyles() {
14159
+ var type = getType();
14160
+ return getSavedStyles().filter(function(item) {
14161
+ return opts.filter ? opts.filter(item, type) : true;
14162
+ });
14163
+ }
14164
+
14165
+ function findVisibleStyle(id) {
14166
+ return getVisibleStyles().find(function(item) {
14167
+ return getItemId(item) == id;
14168
+ });
14169
+ }
14170
+
14171
+ function getSavedStyles() {
14172
+ var styles = GUI.getSavedValue(opts.storageKey);
14173
+ return Array.isArray(styles) ? styles : [];
14174
+ }
14175
+
14176
+ function setSavedStyles(styles) {
14177
+ GUI.setSavedValue(opts.storageKey, styles);
14178
+ }
14179
+
14180
+ function getType() {
14181
+ return typeof opts.type == 'function' ? opts.type() : opts.type;
14182
+ }
14183
+
14184
+ function getItemId(item) {
14185
+ return opts.getItemId ? opts.getItemId(item) : item.id;
14186
+ }
14187
+ }
14188
+
14189
+ function sortByName(a, b) {
14190
+ var aName = String(a.name || '').toLowerCase();
14191
+ var bName = String(b.name || '').toLowerCase();
14192
+ return aName < bName ? -1 : aName > bName ? 1 : 0;
14193
+ }
14194
+
14195
+ function runGuiEditCommand(gui, cmd, optsArg) {
14196
+ var opts = optsArg || {};
14197
+ if (!gui.console) return;
14198
+ gui.console.runMapshaperCommands(cmd, function(err, flags) {
14199
+ if (err) {
14200
+ showPopupAlert(err.message || String(err), opts.title || 'Command error');
14201
+ if (opts.onError) opts.onError(err);
14202
+ } else if (opts.onSuccess) {
14203
+ opts.onSuccess(flags);
14204
+ }
14205
+ if (opts.onDone) {
14206
+ opts.onDone(err, flags);
14207
+ }
14208
+ });
14209
+ }
14210
+
14211
+ var fontField = 'font-family';
14212
+ var fontSizeField = 'font-size';
14213
+ var fontStyleField = 'font-style';
14214
+ var fontWeightField = 'font-weight';
14215
+ var fillField = 'fill';
14216
+ var cssField = 'css';
14217
+ var iconField = 'icon';
14218
+ var iconSizeField = 'icon-size';
14219
+ var defaultFontSize = 12;
14220
+ var defaultFontStyle = 'normal';
14221
+ var defaultFontWeight = '400';
14222
+ var defaultLabelColor = '#000000';
14223
+ var defaultIconSize = 5;
14224
+ var labelStyleMode = 'label_style';
14225
+ var labelStylePanelMode = 'label_style_tool';
14226
+ var savedStylesKey$1 = 'label_styles';
14227
+ var savedStyleFields = [
14228
+ fontField,
14229
+ fontSizeField,
14230
+ fontStyleField,
14231
+ fontWeightField,
14232
+ fillField,
14233
+ cssField,
14234
+ 'label-pos',
14235
+ iconField,
14236
+ iconSizeField
14237
+ ];
14238
+ var labelPositions = ['nw', 'n', 'ne', 'w', 'c', 'e', 'sw', 's', 'se'];
14239
+ var iconTypes = [{
14240
+ name: ''
14241
+ }, {
14242
+ name: 'circle'
14243
+ }, {
14244
+ name: 'square'
14245
+ }, {
14246
+ name: 'ring'
14247
+ }, {
14248
+ name: 'star'
14249
+ }];
14250
+ var iconButtonSymbols = {
14251
+ '': '<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>',
14252
+ circle: '<circle cx="8" cy="8" r="4.25"></circle>',
14253
+ square: '<rect x="4" y="4" width="8" height="8"></rect>',
14254
+ ring: '<circle cx="8" cy="8" r="3.8"></circle>',
14255
+ 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>'
14256
+ };
14257
+
14258
+ function LabelTool(gui) {
14259
+ // Label styling is opened from the point styling entry point.
14260
+ var textBtn = El('div').hide();
14261
+ var parent = gui.container.findChild('.mshp-main-map');
14262
+ var panel = El('div').addClass('label-style-panel rollover').appendTo(parent).hide();
14263
+ var presetControl, fontSelect, fontStyleSelect, fontSizeText, colorChit, colorInput, colorPicker, cssInput, posBtns, iconBtns, iconSizeText, editingStatus, clearLink, hit;
14264
+ var fontOptionsRendered = false;
14265
+
14266
+ initPanel();
14267
+ gui.addMode(labelStylePanelMode, turnOn, turnOff);
14268
+ gui.model.on('update', updateVisibility);
14269
+ gui.model.on('update', function() {
14270
+ setTimeout(updateSelectionDisplay, 0);
14271
+ });
14272
+ gui.on('undo_redo_post', function() {
14273
+ if (panel.visible()) {
14274
+ updateControls();
14275
+ updateSelectionDisplay();
14276
+ }
14277
+ });
14278
+ gui.on('interaction_mode_change', function(e) {
14279
+ if (panel.visible() && e.mode != labelStyleMode && gui.getMode() == labelStylePanelMode) {
14280
+ gui.clearMode();
14281
+ }
14282
+ });
14283
+
14284
+ hit = gui.map.getHitControl && gui.map.getHitControl();
14285
+ if (hit) {
14286
+ hit.on('change', function(e) {
14287
+ if (e.mode == labelStyleMode) {
14288
+ updateSelectionDisplay();
14289
+ updateControls();
14290
+ }
14291
+ });
14292
+ }
14293
+
14294
+ updateVisibility();
14295
+
14296
+ this.open = function(lyr, dataset) {
14297
+ if (!lyr || !internal.layerHasLabels(lyr)) return;
14298
+ if (!gui.map.isActiveLayer(lyr)) {
14299
+ modelSelectLayer(lyr, dataset);
14300
+ }
14301
+ if (gui.getMode() == labelStylePanelMode) {
14302
+ showStylePanel();
14303
+ } else {
14304
+ gui.enterMode(labelStylePanelMode);
14305
+ }
14306
+ };
14307
+
14308
+ function initPanel() {
14309
+ var header = El('div').addClass('label-style-panel-title').appendTo(panel).text('Label styles');
14310
+ El('button').addClass('label-style-close').appendTo(header).text('×').on('click', function() {
14311
+ gui.clearMode();
14312
+ });
14313
+
14314
+ var selectRow = El('div').addClass('label-style-row label-style-selection-row').appendTo(panel);
14315
+ editingStatus = El('span').addClass('label-editing-status').appendTo(selectRow);
14316
+ clearLink = El('span').addClass('label-editing-clear colored-text').appendTo(selectRow).text('deselect').on('click', clearSelection);
14317
+
14318
+ var fontRow = El('label').addClass('label-style-row').appendTo(panel);
14319
+ El('span').appendTo(fontRow).text('Font');
14320
+ fontSelect = El('select').appendTo(fontRow).on('change', function() {
14321
+ if (fontSelect.node().value) {
14322
+ applyFont(fontSelect.node().value);
14323
+ }
14324
+ });
14325
+
14326
+ var fontStyleRow = El('label').addClass('label-style-row').appendTo(panel);
14327
+ El('span').appendTo(fontStyleRow).text('Font style');
14328
+ fontStyleSelect = El('select').appendTo(fontStyleRow).on('change', function() {
14329
+ if (fontStyleSelect.node().value) {
14330
+ applyFontStyleVariant(fontStyleSelect.node().value);
14331
+ }
14332
+ });
14333
+
14334
+ var colorSizeRow = El('div').addClass('label-style-row label-split-row').appendTo(panel);
14335
+ var colorRow = El('div').addClass('label-split-cell label-color-row').appendTo(colorSizeRow);
14336
+ El('span').appendTo(colorRow).text('Color');
14337
+ colorChit = makePanelButton(colorRow, '', toggleColorPicker).addClass('label-color-chit');
14338
+ colorInput = El('input').attr('type', 'text').appendTo(colorRow).on('change', function() {
14339
+ var color = colorInput.node().value.trim();
14340
+ if (color) {
14341
+ if (isHexColor(color)) {
14342
+ colorPicker.setColor(color);
14343
+ }
14344
+ applyLabelColor(color);
14345
+ }
14346
+ });
14347
+ initColorPicker(colorRow);
14348
+
14349
+ var fontSizeRow = El('div').addClass('label-split-cell label-size-row').appendTo(colorSizeRow);
14350
+ El('span').appendTo(fontSizeRow).text('Font size');
14351
+ makePanelButton(fontSizeRow, '−', function() {
14352
+ nudgeFontSize(-1);
14353
+ });
14354
+ fontSizeText = El('span').addClass('label-size-value').appendTo(fontSizeRow);
14355
+ makePanelButton(fontSizeRow, '+', function() {
14356
+ nudgeFontSize(1);
14357
+ });
14358
+
14359
+ var cssRow = El('label').addClass('label-style-row label-css-row').appendTo(panel);
14360
+ El('span').appendTo(cssRow).text('Inline CSS');
14361
+ cssInput = El('input').attr('type', 'text').appendTo(cssRow).on('change', function() {
14362
+ applyInlineCss(cssInput.node().value.trim());
14363
+ });
14364
+
14365
+ var iconSizeRow = El('div').addClass('label-style-row label-split-row').appendTo(panel);
14366
+ var iconRow = El('div').addClass('label-split-cell').appendTo(iconSizeRow);
14367
+ El('div').addClass('label-style-row-label').appendTo(iconRow).text('Icon');
14368
+ var iconGroup = El('div').addClass('label-icon-buttons').appendTo(iconRow);
14369
+ iconBtns = {};
14370
+ iconTypes.forEach(function(icon) {
14371
+ var btn = makePanelButton(iconGroup, '', function() {
14372
+ applyIcon(icon.name);
14373
+ })
14374
+ .attr('data-icon', icon.name || 'none')
14375
+ .attr('title', icon.name || 'no icon');
14376
+ iconBtns[icon.name] = btn;
14377
+ appendIconButtonSymbol(btn, icon.name);
14378
+ });
14379
+
14380
+ var sizeRow = El('div').addClass('label-split-cell label-icon-size-row').appendTo(iconSizeRow);
14381
+ El('span').appendTo(sizeRow).text('Icon size');
14382
+ makePanelButton(sizeRow, '−', function() {
14383
+ nudgeIconSize(-1);
14384
+ });
14385
+ iconSizeText = El('span').addClass('label-icon-size-value').appendTo(sizeRow);
14386
+ makePanelButton(sizeRow, '+', function() {
14387
+ nudgeIconSize(1);
14388
+ });
14389
+
14390
+ var posRow = El('div').addClass('label-style-row').appendTo(panel);
14391
+ El('div').addClass('label-style-row-label').appendTo(posRow).text('Position');
14392
+ var grid = El('div').addClass('label-position-grid').appendTo(posRow);
14393
+ posBtns = {};
14394
+ labelPositions.forEach(function(pos) {
13688
14395
  posBtns[pos] = makePanelButton(grid, '', function() {
13689
14396
  applyLabelPosition(pos);
13690
14397
  })
@@ -13692,1108 +14399,1653 @@
13692
14399
  .attr('title', pos);
13693
14400
  });
13694
14401
 
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);
14402
+ presetControl = new StylePresetControl(panel, {
14403
+ storageKey: savedStylesKey$1,
14404
+ type: 'label',
14405
+ useType: false,
14406
+ saveTitle: 'Save label style',
14407
+ styleLabel: 'label style',
14408
+ getStyle: getCurrentStyle,
14409
+ applyStyle: applyStyleObject,
14410
+ getItemId: getStyleId,
14411
+ disabled: function() {
14412
+ return getTargetIds().length === 0;
13700
14413
  }
13701
- updateSavedStyleControls();
13702
14414
  });
13703
- El('button').appendTo(savedRow).text('Save preset').on('click', saveCurrentStyle);
13704
- El('button').appendTo(savedRow).text('Delete').on('click', deleteSelectedStyle);
13705
- renderSavedStyles();
13706
14415
  }
13707
14416
 
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
- });
14417
+ function appendIconButtonSymbol(btn, iconName) {
14418
+ var svg = '<svg class="label-icon-symbol" viewBox="0 0 16 16" aria-hidden="true">' +
14419
+ iconButtonSymbols[iconName] + '</svg>';
14420
+ El(svg).appendTo(btn);
14421
+ }
14422
+
14423
+ function makePanelButton(parent, label, action) {
14424
+ return El('div')
14425
+ .addClass('label-panel-btn')
14426
+ .attr('role', 'button')
14427
+ .attr('tabindex', '0')
14428
+ .appendTo(parent)
14429
+ .text(label)
14430
+ .on('click', function(e) {
14431
+ if (this.classList.contains('disabled')) return;
14432
+ action(e);
14433
+ })
14434
+ .on('keydown', function(e) {
14435
+ if (e.key == 'Enter' || e.key == ' ') {
14436
+ e.preventDefault();
14437
+ if (!this.classList.contains('disabled')) action(e);
14438
+ }
14439
+ });
14440
+ }
14441
+
14442
+ function setPanelButtonDisabled(el, disabled) {
14443
+ el.classed('disabled', !!disabled)
14444
+ .attr('aria-disabled', disabled ? 'true' : 'false')
14445
+ .attr('tabindex', disabled ? '-1' : '0');
14446
+ }
14447
+
14448
+ function turnOn() {
14449
+ if (!activeLayerHasLabels()) {
14450
+ gui.clearMode();
14451
+ return;
14452
+ }
14453
+ showStylePanel();
14454
+ }
14455
+
14456
+ function showStylePanel() {
14457
+ renderFontOptions();
14458
+ gui.interaction.setMode(labelStyleMode);
14459
+ gui.state.label_style_panel_open = true;
14460
+ panel.show();
14461
+ textBtn.addClass('selected');
14462
+ updateControls();
14463
+ updateSelectionDisplay();
14464
+ }
14465
+
14466
+ function turnOff() {
14467
+ panel.hide();
14468
+ hideColorPicker();
14469
+ gui.state.label_style_panel_open = false;
14470
+ textBtn.removeClass('selected');
14471
+ clearSelectionDisplay();
14472
+ if (hit) hit.clearSelection();
14473
+ if (gui.interaction.getMode() == labelStyleMode) {
14474
+ gui.interaction.turnOff();
14475
+ }
14476
+ }
14477
+
14478
+ function updateVisibility() {
14479
+ var enabled = activeLayerHasLabels();
14480
+ textBtn.classed('disabled', !enabled);
14481
+ textBtn[enabled ? 'show' : 'hide']();
14482
+ if (!enabled && gui.getMode() == labelStylePanelMode) {
14483
+ gui.clearMode();
14484
+ }
14485
+ }
14486
+
14487
+ function activeLayerHasLabels() {
14488
+ var active = gui.model.getActiveLayer();
14489
+ return !!(active && internal.layerHasLabels(active.layer));
14490
+ }
14491
+
14492
+ function activeLayerIsPointLayer() {
14493
+ var active = gui.model.getActiveLayer();
14494
+ return !!(active && active.layer && active.layer.geometry_type == 'point');
14495
+ }
14496
+
14497
+ function modelSelectLayer(lyr, dataset) {
14498
+ if (lyr) lyr.hidden = false;
14499
+ gui.model.selectLayer(lyr, dataset);
14500
+ }
14501
+
14502
+ function renderFontOptions() {
14503
+ if (fontOptionsRendered) return;
14504
+ fontOptionsRendered = true;
14505
+ fontSelect.empty();
14506
+ El('option').attr('value', '').appendTo(fontSelect).text('');
14507
+ getInstalledFonts().forEach(function(group) {
14508
+ var optgroup = El('optgroup').attr('label', group.name).appendTo(fontSelect);
14509
+ group.fonts.forEach(function(fontName) {
14510
+ El('option').attr('value', fontName).appendTo(optgroup).text(fontName);
14511
+ });
14512
+ });
14513
+ }
14514
+
14515
+ function quoteCommandValue(str) {
14516
+ return "'" + String(str).replace(/'/g, "\\'") + "'";
14517
+ }
14518
+
14519
+ function clearSelection() {
14520
+ if (hit) hit.clearSelection();
14521
+ updateSelectionDisplay();
14522
+ updateControls();
14523
+ }
14524
+
14525
+ function getActiveLayer() {
14526
+ var active = gui.model.getActiveLayer();
14527
+ return active && active.layer;
14528
+ }
14529
+
14530
+ function getActiveTable() {
14531
+ var lyr = getActiveLayer();
14532
+ return lyr && lyr.data || null;
14533
+ }
14534
+
14535
+ function getSelectionIds() {
14536
+ return hit ? hit.getSelectionIds() : [];
14537
+ }
14538
+
14539
+ function getTargetIds() {
14540
+ var ids = getSelectionIds();
14541
+ return ids.length > 0 ? ids : getAllLabelIds();
14542
+ }
14543
+
14544
+ function getAllLabelIds() {
14545
+ var lyr = getActiveLayer();
14546
+ var ids = [];
14547
+ if (!lyr) return ids;
14548
+ for (var i=0, n=internal.getFeatureCount(lyr); i<n; i++) {
14549
+ ids.push(i);
14550
+ }
14551
+ return ids;
14552
+ }
14553
+
14554
+ function updateControls() {
14555
+ var ids = getTargetIds();
14556
+ var manualIds = getSelectionIds();
14557
+ var fontVal = getCommonValue(ids, fontField);
14558
+ var fontSizeVal = getCommonValue(ids, fontSizeField, {useDefault: true, defaultValue: defaultFontSize});
14559
+ var fontStyleVal = getCommonValue(ids, fontStyleField, {useDefault: true, defaultValue: defaultFontStyle});
14560
+ var fontWeightVal = getCommonValue(ids, fontWeightField, {useDefault: true, defaultValue: defaultFontWeight});
14561
+ var fillVal = getCommonValue(ids, fillField, {useDefault: true, defaultValue: defaultLabelColor});
14562
+ var cssVal = getCommonValue(ids, cssField);
14563
+ var posVal = getCommonValue(ids, 'label-pos');
14564
+ var iconVal = getCommonValue(ids, iconField);
14565
+ var iconSizeVal = getCommonValue(ids, iconSizeField, {useDefault: true, defaultValue: defaultIconSize});
14566
+ updateEditingStatus(manualIds.length);
14567
+ updateSavedStyleControls();
14568
+ fontSelect.node().disabled = ids.length === 0;
14569
+ fontSelect.node().value = fontVal;
14570
+ updateFontStyleControls(fontVal, fontStyleVal, fontWeightVal);
14571
+ updateFontSizeControls(ids.length ? fontSizeVal : '');
14572
+ updateColorControls(ids.length ? fillVal : '');
14573
+ updateCssControl(ids.length ? cssVal : '');
14574
+ updatePositionButtons(ids.length ? posVal : '');
14575
+ updateIconButtons(ids.length ? iconVal : '');
14576
+ updateIconSizeControls(ids.length ? iconSizeVal : '');
14577
+ }
14578
+
14579
+ function updatePositionButtons(pos) {
14580
+ var disabled = getTargetIds().length === 0;
14581
+ labelPositions.forEach(function(name) {
14582
+ posBtns[name].classed('selected', name == pos);
14583
+ setPanelButtonDisabled(posBtns[name], disabled);
14584
+ });
14585
+ }
14586
+
14587
+ function updateEditingStatus(count) {
14588
+ editingStatus.text(count > 0 ? 'Editing: ' + count + ' selected' : 'Editing: all');
14589
+ clearLink.classed('hidden', count === 0);
14590
+ }
14591
+
14592
+ function updateFontSizeControls(fontSizeVal) {
14593
+ var disabled = getTargetIds().length === 0;
14594
+ fontSizeText.text(fontSizeVal || '');
14595
+ panel.findChildren('.label-size-row .label-panel-btn').forEach(function(btn) {
14596
+ setPanelButtonDisabled(btn, disabled);
14597
+ });
14598
+ }
14599
+
14600
+ function updateFontStyleControls(fontName, fontStyleVal, fontWeightVal) {
14601
+ var disabled = getTargetIds().length === 0 || !fontName;
14602
+ fontStyleSelect.empty();
14603
+ El('option').attr('value', '').appendTo(fontStyleSelect).text('');
14604
+ if (fontName) {
14605
+ getFontStyleVariants(fontName).forEach(function(variant) {
14606
+ El('option').attr('value', variant.value).appendTo(fontStyleSelect).text(variant.label);
14607
+ });
14608
+ }
14609
+ fontStyleSelect.node().disabled = disabled;
14610
+ fontStyleSelect.node().value = fontStyleVal && fontWeightVal ?
14611
+ fontStyleVal + '|' + fontWeightVal : '';
14612
+ }
14613
+
14614
+ function updateColorControls(colorVal) {
14615
+ var disabled = getTargetIds().length === 0;
14616
+ colorInput.node().disabled = disabled;
14617
+ colorInput.node().value = colorVal || '';
14618
+ setPanelButtonDisabled(colorChit, disabled);
14619
+ colorChit.css('background-color', isHexColor(colorVal) ? colorVal : 'transparent');
14620
+ if (colorPicker.visible()) {
14621
+ return; // avoid HSB -> RGB -> HSB rounding jumps after picker commits
14622
+ }
14623
+ if (isHexColor(colorVal)) {
14624
+ colorPicker.setColor(colorVal);
14625
+ } else {
14626
+ hideColorPicker();
14627
+ }
14628
+ }
14629
+
14630
+ function updateCssControl(cssVal) {
14631
+ cssInput.node().disabled = getTargetIds().length === 0;
14632
+ cssInput.node().value = cssVal || '';
14633
+ }
14634
+
14635
+ function updateIconButtons(iconVal) {
14636
+ var disabled = getTargetIds().length === 0;
14637
+ iconTypes.forEach(function(icon) {
14638
+ iconBtns[icon.name].classed('selected', !disabled && icon.name == iconVal);
14639
+ setPanelButtonDisabled(iconBtns[icon.name], disabled);
14640
+ });
14641
+ }
14642
+
14643
+ function updateIconSizeControls(iconSizeVal) {
14644
+ var disabled = getTargetIds().length === 0;
14645
+ iconSizeText.text(iconSizeVal || '');
14646
+ panel.findChildren('.label-icon-size-row .label-panel-btn').forEach(function(btn) {
14647
+ setPanelButtonDisabled(btn, disabled);
14648
+ });
14649
+ }
14650
+
14651
+ function updateSavedStyleControls() {
14652
+ presetControl.update();
14653
+ }
13713
14654
 
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);
14655
+ function getCommonValue(ids, field, opts) {
14656
+ var table = getActiveTable();
14657
+ var records = table && table.getRecords();
14658
+ var value, val, hasValue;
14659
+ if (!records || ids.length === 0) return '';
14660
+ for (var i=0; i<ids.length; i++) {
14661
+ val = records[ids[i]] && records[ids[i]][field];
14662
+ if (!val) {
14663
+ if (opts && opts.useDefault) {
14664
+ val = opts.defaultValue;
14665
+ } else {
14666
+ return '';
14667
+ }
14668
+ } else {
14669
+ hasValue = true;
13720
14670
  }
13721
- updateCreateButton();
13722
- });
14671
+ if (i === 0) {
14672
+ value = val;
14673
+ } else if (val != value) {
14674
+ return '';
14675
+ }
14676
+ }
14677
+ return hasValue || opts && opts.useDefault ? value : '';
14678
+ }
13723
14679
 
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);
14680
+ function applyFont(fontName) {
14681
+ applyStyleValues([[fontField, fontName]]);
14682
+ }
13731
14683
 
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');
14684
+ function nudgeFontSize(delta) {
14685
+ var ids = getTargetIds();
14686
+ var size = getNumericSize(ids, fontSizeField, defaultFontSize);
14687
+ if (ids.length === 0) return;
14688
+ size = Math.max(1, size + delta);
14689
+ applyStyleValues([[fontSizeField, size]]);
14690
+ }
13735
14691
 
13736
- var btnRow = El('div').addClass('label-style-row').appendTo(createPanel);
13737
- createBtn = El('button').appendTo(btnRow).text('Create labels').on('click', createLabels);
14692
+ function applyFontStyleVariant(value) {
14693
+ var variant = parseFontStyleVariant(value);
14694
+ if (!variant) return;
14695
+ applyStyleValues([[fontStyleField, variant.style], [fontWeightField, variant.weight]]);
13738
14696
  }
13739
14697
 
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);
14698
+ function applyLabelColor(color) {
14699
+ applyStyleValues([[fillField, color]]);
13744
14700
  }
13745
14701
 
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
- });
14702
+ function applyInlineCss(css) {
14703
+ applyStyleValues([[cssField, css || '']]);
13763
14704
  }
13764
14705
 
13765
- function setPanelButtonDisabled(el, disabled) {
13766
- el.classed('disabled', !!disabled)
13767
- .attr('aria-disabled', disabled ? 'true' : 'false')
13768
- .attr('tabindex', disabled ? '-1' : '0');
14706
+ function getStyleId(item) {
14707
+ return item.id || 'label-' + item.name;
13769
14708
  }
13770
14709
 
13771
- function turnOn() {
13772
- if (!activeLayerIsPointLayer()) {
13773
- gui.clearMode();
13774
- return;
14710
+ function getCurrentStyle() {
14711
+ var style = {};
14712
+ var fontStyle = parseFontStyleVariant(fontStyleSelect.node().value);
14713
+ var icon = getSelectedIcon();
14714
+ addStyleValue(style, fontField, fontSelect.node().value);
14715
+ if (fontStyle) {
14716
+ style[fontStyleField] = fontStyle.style;
14717
+ style[fontWeightField] = fontStyle.weight;
13775
14718
  }
13776
- if (!activeLayerHasLabels()) {
13777
- showCreatePanel();
13778
- return;
14719
+ addStyleValue(style, fontSizeField, getNumericControlValue(fontSizeText));
14720
+ addStyleValue(style, fillField, colorInput.node().value.trim());
14721
+ addStyleValue(style, cssField, cssInput.node().value.trim());
14722
+ addStyleValue(style, 'label-pos', getSelectedLabelPosition());
14723
+ addStyleValue(style, iconField, icon);
14724
+ if (icon) {
14725
+ addStyleValue(style, iconSizeField, getNumericControlValue(iconSizeText));
13779
14726
  }
13780
- showStylePanel();
14727
+ return style;
13781
14728
  }
13782
14729
 
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();
14730
+ function addStyleValue(style, field, value) {
14731
+ if (value || value === 0) {
14732
+ style[field] = value;
14733
+ }
13792
14734
  }
13793
14735
 
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();
14736
+ function getNumericControlValue(el) {
14737
+ var value = Number(el.text());
14738
+ return isFinite(value) && value > 0 ? value : null;
13805
14739
  }
13806
14740
 
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
- }
14741
+ function getSelectedLabelPosition() {
14742
+ var out = '';
14743
+ labelPositions.forEach(function(pos) {
14744
+ if (posBtns[pos].hasClass('selected')) out = pos;
14745
+ });
14746
+ return out;
13821
14747
  }
13822
14748
 
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();
14749
+ function getSelectedIcon() {
14750
+ var out = '';
14751
+ iconTypes.forEach(function(icon) {
14752
+ if (iconBtns[icon.name].hasClass('selected')) out = icon.name;
14753
+ });
14754
+ return out;
14755
+ }
14756
+
14757
+ function applyStyleObject(style) {
14758
+ var styles = [];
14759
+ savedStyleFields.forEach(function(field) {
14760
+ if (field in style) {
14761
+ styles.push([field, style[field]]);
14762
+ }
14763
+ });
14764
+ applyStyleValues(styles, {preservePreset: true});
14765
+ }
14766
+
14767
+ function parseFontStyleVariant(value) {
14768
+ var parts = String(value).split('|');
14769
+ if (parts.length != 2) return null;
14770
+ return {
14771
+ style: parts[0],
14772
+ weight: parts[1]
14773
+ };
14774
+ }
14775
+
14776
+ function applyLabelPosition(pos) {
14777
+ applyStyleValues([['label-pos', pos]]);
14778
+ }
14779
+
14780
+ function applyIcon(iconName) {
14781
+ var styles = [[iconField, iconName || '']];
14782
+ if (iconName) {
14783
+ styles.push([iconSizeField, getNumericSize(getTargetIds(), iconSizeField, defaultIconSize)]);
14784
+ } else {
14785
+ styles.push([iconSizeField, 0]);
13829
14786
  }
14787
+ applyStyleValues(styles);
13830
14788
  }
13831
14789
 
13832
- function activeLayerHasLabels() {
13833
- var active = gui.model.getActiveLayer();
13834
- return !!(active && internal.layerHasLabels(active.layer));
14790
+ function nudgeIconSize(delta) {
14791
+ var ids = getTargetIds();
14792
+ var size = getNumericSize(ids, iconSizeField, defaultIconSize);
14793
+ if (ids.length === 0) return;
14794
+ size = Math.max(1, size + delta);
14795
+ applyStyleValues([[iconField, getCommonValue(ids, iconField) || 'circle'], [iconSizeField, size]]);
13835
14796
  }
13836
14797
 
13837
- function activeLayerIsPointLayer() {
13838
- var active = gui.model.getActiveLayer();
13839
- return !!(active && active.layer && active.layer.geometry_type == 'point');
14798
+ function getNumericSize(ids, field, defaultValue) {
14799
+ var val = getCommonValue(ids, field);
14800
+ val = val ? Number(val) : defaultValue;
14801
+ return isFinite(val) && val > 0 ? val : defaultValue;
13840
14802
  }
13841
14803
 
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
- });
14804
+ function initColorPicker(colorRow) {
14805
+ colorPicker = new ColorPicker(colorRow, {
14806
+ onPreview: function(hex) {
14807
+ colorInput.node().value = hex;
14808
+ colorChit.css('background-color', hex);
14809
+ },
14810
+ onChange: applyLabelColor
13852
14811
  });
13853
14812
  }
13854
14813
 
13855
- function renderCreateFields() {
14814
+ function toggleColorPicker() {
14815
+ colorPicker.toggle();
14816
+ }
14817
+
14818
+ function hideColorPicker() {
14819
+ colorPicker.hide();
14820
+ }
14821
+
14822
+ function applyStyleValues(styles, opts) {
13856
14823
  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);
14824
+ var ids = getTargetIds();
14825
+ var parts = ['-style'];
14826
+ if (!gui.console || !lyr || ids.length === 0 || styles.length === 0) return;
14827
+ if (!opts || !opts.preservePreset) {
14828
+ presetControl.clearSelection();
14829
+ }
14830
+ styles.forEach(function(style) {
14831
+ parts.push(style[0] + '=' + quoteCommandValue(style[1]));
14832
+ });
14833
+ if (ids.length < internal.getFeatureCount(lyr)) {
14834
+ parts.push('ids=' + ids.join(','));
14835
+ }
14836
+ runGuiEditCommand(gui, parts.join(' '), {
14837
+ title: 'Label styles',
14838
+ onDone: function() {
14839
+ setTimeout(updateSelectionDisplay, 0);
14840
+ updateControls();
14841
+ updateSelectionDisplay();
14842
+ }
13866
14843
  });
13867
- createFieldSelect.node().disabled = fields.length === 0;
13868
- createFieldSelect.node().value = fields.indexOf(value) > -1 ? value : '';
13869
14844
  }
13870
14845
 
13871
- function updateCreateButton() {
13872
- createBtn.node().disabled = createExprInput.node().value.trim() === '';
14846
+ function updateSelectionDisplay() {
14847
+ clearSelectionDisplay();
14848
+ getSelectionIds().forEach(function(id) {
14849
+ var textNode = getTextNodeById(id);
14850
+ if (textNode) {
14851
+ textNode.classList.add('label-style-selected');
14852
+ }
14853
+ });
13873
14854
  }
13874
14855
 
13875
- function getFieldExpression(field) {
13876
- return 'd[' + JSON.stringify(field) + ']';
14856
+ function clearSelectionDisplay() {
14857
+ var lyr = hit && hit.getHitTarget();
14858
+ var container = lyr && lyr.gui && lyr.gui.svg_container;
14859
+ if (!container) return;
14860
+ container.querySelectorAll('.label-style-selected').forEach(function(node) {
14861
+ node.classList.remove('label-style-selected');
14862
+ });
13877
14863
  }
13878
14864
 
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);
14865
+ function getTextNodeById(id) {
14866
+ var lyr = hit && hit.getHitTarget();
14867
+ var container = lyr && lyr.gui && lyr.gui.svg_container;
14868
+ var symbol = container && container.querySelector('[data-id="' + id + '"]');
14869
+ if (!symbol) return null;
14870
+ return symbol.tagName == 'text' ? symbol : symbol.querySelector('text');
13900
14871
  }
13901
14872
 
13902
- function makeLabelLayerCopy(srcLyr, dataset) {
13903
- var lyr = internal.copyLayer(srcLyr);
13904
- delete lyr.gui;
13905
- lyr.name = 'labels';
13906
- return lyr;
13907
- }
14873
+ }
14874
+
14875
+ var savedStylesKey = 'layer_style_presets';
14876
+ var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'fill', 'fill-opacity'];
13908
14877
 
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
- }
14878
+ function LayerStyleTool(gui) {
14879
+ var parent = gui.container.findChild('.mshp-main-map');
14880
+ var panel = El('div').addClass('label-style-panel layer-style-panel rollover').appendTo(parent).hide();
14881
+ var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthText, strokeWidthClickText, strokeOpacityInput, fillOpacityInput, randomFillBtn, presetControl, hit;
14882
+ var targetLayer = null;
13915
14883
 
13916
- function selectLabelLayerCopy(lyr, dataset) {
13917
- gui.model.setDefaultTarget([lyr], dataset);
13918
- gui.model.updated({select: true});
14884
+ initPanel();
14885
+ hit = gui.map.getHitControl && gui.map.getHitControl();
14886
+ if (hit) {
14887
+ hit.on('change', function(e) {
14888
+ if (targetLayer && (e.mode == 'line_style' || e.mode == 'polygon_style')) {
14889
+ updateControls();
14890
+ }
14891
+ });
13919
14892
  }
13920
14893
 
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]});
14894
+ this.open = function(lyr, dataset) {
14895
+ if (!layerCanBeStyled(lyr)) return;
14896
+ if (!gui.map.isActiveLayer(lyr)) {
14897
+ modelSelectLayer(lyr, dataset);
13936
14898
  }
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'});
14899
+ gui.interaction.setMode(lyr.geometry_type == 'polygon' ? 'polygon_style' : 'line_style');
14900
+ };
14901
+
14902
+ gui.on('interaction_mode_change', function(e) {
14903
+ if (modeMatchesActiveLayer(e.mode)) {
14904
+ turnOn();
13943
14905
  } else {
13944
- table.markSchemaChanged({operation: 'create-labels', fields: [labelTextField]});
14906
+ turnOff();
13945
14907
  }
13946
- return true;
13947
- }
14908
+ });
13948
14909
 
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();
14910
+ gui.model.on('update', function() {
14911
+ if (panel.visible() && !modeMatchesActiveLayer(gui.interaction.getMode())) {
14912
+ gui.interaction.turnOff();
14913
+ } else if (panel.visible()) {
14914
+ updateControls();
13954
14915
  }
13955
- if (!pendingStyleHistory) {
13956
- pendingStyleHistory = {
13957
- layer: lyr,
13958
- ids: ids.concat(),
13959
- fields: []
13960
- };
14916
+ });
14917
+
14918
+ gui.on('undo_redo_post', function() {
14919
+ if (panel.visible()) {
14920
+ updateControls();
13961
14921
  }
13962
- getHistoryStyleFields(fields).forEach(function(field) {
13963
- if (pendingStyleHistory.fields.indexOf(field) == -1) {
13964
- pendingStyleHistory.fields.push(field);
13965
- }
13966
- });
14922
+ });
14923
+
14924
+ function turnOn() {
14925
+ targetLayer = getActiveLayer();
14926
+ applyDefaultLineStyle();
14927
+ panel.show();
14928
+ updateControls();
13967
14929
  }
13968
14930
 
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
- }
14931
+ function turnOff() {
14932
+ panel.hide();
14933
+ strokeControl.picker.hide();
14934
+ fillControl.picker.hide();
14935
+ targetLayer = null;
13977
14936
  }
13978
14937
 
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;
14938
+ function initPanel() {
14939
+ var header = El('div').addClass('label-style-panel-title').appendTo(panel);
14940
+ title = El('span').appendTo(header);
14941
+ El('button').addClass('label-style-close').appendTo(header).text('×').on('click', closePanel);
14942
+
14943
+ var editRow = El('div').addClass('label-style-row label-style-selection-row layer-style-selection-row').appendTo(panel);
14944
+ editingStatus = El('span').addClass('label-editing-status').appendTo(editRow);
14945
+ clearLink = El('span').addClass('label-editing-clear colored-text').appendTo(editRow).text('deselect').on('click', clearSelection);
14946
+
14947
+ fillControl = addColorControl(panel, 'Fill', 'fill', '');
14948
+ fillOpacityInput = addStyleNumberControl(fillControl, 'Opacity', 'fill-opacity', {
14949
+ defaultValue: 1,
14950
+ parser: parseOpacityValue,
14951
+ formatter: formatOpacityPct
13984
14952
  });
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]));
14953
+ strokeControl = addColorControl(panel, 'Stroke', 'stroke', '#000000');
14954
+ strokeOpacityInput = addStyleNumberControl(strokeControl, 'Opacity', 'stroke-opacity', {
14955
+ defaultValue: 1,
14956
+ parser: parseOpacityValue,
14957
+ formatter: formatOpacityPct
14958
+ });
14959
+ strokeWidthText = addStrokeWidthControl(panel);
14960
+
14961
+ var buttonRow = El('div').addClass('label-style-row').appendTo(panel);
14962
+ randomFillBtn = El('button').appendTo(buttonRow).text('Random fills').on('click', applyRandomFillColors);
14963
+ El('button').appendTo(buttonRow).text('Clear style').on('click', clearLayerStyle);
14964
+
14965
+ presetControl = new StylePresetControl(panel, {
14966
+ storageKey: savedStylesKey,
14967
+ type: getStyleType,
14968
+ saveTitle: 'Save layer style',
14969
+ styleLabel: 'layer style',
14970
+ getStyle: getCurrentStyle,
14971
+ applyStyle: applyStyleObject,
14972
+ filter: function(item, type) {
14973
+ return item.type == type;
14974
+ },
14975
+ sort: function(a, b) {
14976
+ return getSortKey(a) < getSortKey(b) ? -1 :
14977
+ getSortKey(a) > getSortKey(b) ? 1 : 0;
14978
+ }
13991
14979
  });
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
14980
  }
14001
14981
 
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;
14982
+ function addColorControl(parent, label, field, defaultColor) {
14983
+ var row = El('div').addClass('label-style-row label-color-row layer-color-row').appendTo(parent);
14984
+ var control = {
14985
+ field: field,
14986
+ defaultColor: defaultColor,
14987
+ row: row,
14988
+ chit: null,
14989
+ input: null,
14990
+ controls: null,
14991
+ picker: null
14992
+ };
14993
+ var controlLine = El('div').addClass('layer-style-control-line').appendTo(row);
14994
+ var colorCell = El('div').addClass('layer-color-cell').appendTo(controlLine);
14995
+ control.controls = El('div').addClass('layer-row-controls').appendTo(controlLine);
14996
+ El('span').appendTo(colorCell).text(label);
14997
+ control.chit = makePanelButton(colorCell, '', function() {
14998
+ control.picker.toggle();
14999
+ }).addClass('label-color-chit');
15000
+ control.input = El('input').attr('type', 'text').appendTo(colorCell).on('change', function() {
15001
+ var color = control.input.node().value.trim();
15002
+ if (!color) return;
15003
+ if (isHexColor(color)) {
15004
+ control.picker.setColor(color);
14007
15005
  }
14008
- return memo;
14009
- }, {});
15006
+ applyColorControlStyle(control, color);
15007
+ });
15008
+ control.picker = new ColorPicker(row, {
15009
+ presetRows: layerColorPresetRows,
15010
+ onPreview: function(hex) {
15011
+ setColorControlValue(control, hex);
15012
+ },
15013
+ onChange: function(hex) {
15014
+ applyColorControlStyle(control, hex);
15015
+ }
15016
+ });
15017
+ return control;
14010
15018
  }
14011
15019
 
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;
15020
+ function addStrokeWidthControl(parent) {
15021
+ var row = El('div').addClass('label-style-row layer-stroke-width-row').appendTo(parent);
15022
+ var control = El('div').addClass('layer-number-control layer-stroke-width-control').appendTo(row);
15023
+ var buttonRow;
15024
+ El('span').appendTo(control).text('Stroke width');
15025
+ buttonRow = El('div').addClass('layer-stepper-control').appendTo(control);
15026
+ makePanelButton(buttonRow, '−', function() {
15027
+ nudgeStrokeWidth(-1);
15028
+ });
15029
+ var text = El('span').addClass('layer-stroke-width-value').appendTo(buttonRow);
15030
+ strokeWidthClickText = new ClickText2(text);
15031
+ strokeWidthClickText.on('change', function() {
15032
+ var value = parsePositiveNumber(strokeWidthClickText.value());
15033
+ if (value === null) {
15034
+ updateStrokeWidthControl();
15035
+ return;
14020
15036
  }
14021
- }
14022
- return value;
15037
+ applyStrokeWidthStyle(value);
15038
+ });
15039
+ makePanelButton(buttonRow, '+', function() {
15040
+ nudgeStrokeWidth(1);
15041
+ });
15042
+ return text;
14023
15043
  }
14024
15044
 
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;
15045
+ function addStyleNumberControl(colorControl, label, field, opts) {
15046
+ var control = El('label').addClass('layer-number-control').appendTo(colorControl.controls);
15047
+ El('span').appendTo(control).text(label);
15048
+ return El('input')
15049
+ .attr('type', 'text')
15050
+ .appendTo(control)
15051
+ .on('change', function() {
15052
+ var value = opts.parser(this.value);
15053
+ if (value === null) return;
15054
+ applyLayerStyle(field, value);
15055
+ });
14032
15056
  }
14033
15057
 
14034
- function getIdsWhereExpression(ids) {
14035
- return '[' + ids.join(',') + '].indexOf($.id)>-1';
15058
+ function makePanelButton(parent, label, action) {
15059
+ return El('div')
15060
+ .addClass('label-panel-btn')
15061
+ .attr('role', 'button')
15062
+ .attr('tabindex', '0')
15063
+ .appendTo(parent)
15064
+ .text(label)
15065
+ .on('click', function(e) {
15066
+ action(e);
15067
+ })
15068
+ .on('keydown', function(e) {
15069
+ if (e.key == 'Enter' || e.key == ' ') {
15070
+ e.preventDefault();
15071
+ action(e);
15072
+ }
15073
+ });
14036
15074
  }
14037
15075
 
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;
15076
+ function updateControls() {
15077
+ syncTargetLayer();
15078
+ var geom = targetLayer && targetLayer.geometry_type;
15079
+ var manualIds = getSelectionIds();
15080
+ if (!targetLayer) return;
15081
+ title.text(geom == 'polygon' ? 'Polygon styles' : 'Line styles');
15082
+ updateEditingStatus(manualIds.length);
15083
+ strokeControl.row.show();
15084
+ fillControl.row.classed('hidden', geom != 'polygon');
15085
+ updateColorControl(strokeControl);
15086
+ updateColorControl(fillControl);
15087
+ updateStrokeWidthControl();
15088
+ updateNumberControl(strokeOpacityInput, 'stroke-opacity', 1, formatOpacityPct);
15089
+ updateNumberControl(fillOpacityInput, 'fill-opacity', 1, formatOpacityPct);
15090
+ randomFillBtn.classed('hidden', geom != 'polygon');
15091
+ presetControl.render();
15092
+ updateSavedStyleControls();
14044
15093
  }
14045
15094
 
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) + ' ';
15095
+ function updateColorControl(control) {
15096
+ var value = getCommonStyleValue(control.field);
15097
+ setColorControlValue(control, value);
15098
+ if (isHexColor(value)) {
15099
+ control.picker.setColor(value);
15100
+ } else {
15101
+ control.picker.hide();
14051
15102
  }
14052
- cmd += '-style label-text=' + quoteCommandValue(expr);
14053
- gui.session.consoleCommands(cmd);
14054
15103
  }
14055
15104
 
14056
- function quoteCommandValue(str) {
14057
- return "'" + String(str).replace(/'/g, "\\'") + "'";
15105
+ function setColorControlValue(control, value) {
15106
+ control.input.node().value = value || '';
15107
+ control.chit.css('background-color', isHexColor(value) ? value : 'transparent');
14058
15108
  }
14059
15109
 
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);
15110
+ function updateNumberControl(input, field, defaultValue, formatter) {
15111
+ var value = getCommonStyleValue(field);
15112
+ input.node().value = formatter(value === '' || value === undefined || value === null ? defaultValue : value);
15113
+ }
15114
+
15115
+ function updateStrokeWidthControl() {
15116
+ var value = getCommonStyleValue('stroke-width');
15117
+ strokeWidthClickText.value(formatNumberValue(value === '' || value === undefined || value === null ? getDefaultStrokeWidth() : value));
15118
+ }
15119
+
15120
+ function nudgeStrokeWidth(direction) {
15121
+ var value = Number(getCommonStyleValue('stroke-width'));
15122
+ if (!isFinite(value)) value = getDefaultStrokeWidth();
15123
+ applyStrokeWidthStyle(getNextStrokeWidth(value, direction));
15124
+ }
15125
+
15126
+ function getNextStrokeWidth(value, direction) {
15127
+ var baseSteps = [0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4];
15128
+ var i;
15129
+ if (direction > 0) {
15130
+ for (i=0; i<baseSteps.length; i++) {
15131
+ if (value < baseSteps[i]) return baseSteps[i];
15132
+ }
15133
+ return Math.floor(value) + 1;
14066
15134
  }
14067
- hit.clearSelection();
14068
- if (ids.length > 0) {
14069
- hit.addSelectionIds(ids);
15135
+ for (i=baseSteps.length - 1; i>=0; i--) {
15136
+ if (value > baseSteps[i]) return baseSteps[i];
14070
15137
  }
14071
- updateSelectionDisplay();
14072
- updateControls();
15138
+ return baseSteps[0];
14073
15139
  }
14074
15140
 
14075
- function clearSelection() {
14076
- if (hit) hit.clearSelection();
14077
- updateSelectionDisplay();
14078
- updateControls();
15141
+ function applyLayerStyle(field, value) {
15142
+ runStyleCommand([[field, value]]);
14079
15143
  }
14080
15144
 
14081
- function getActiveLayer() {
14082
- var active = gui.model.getActiveLayer();
14083
- return active && active.layer;
15145
+ function applyColorControlStyle(control, color) {
15146
+ var styles = [[control.field, color]];
15147
+ if (control.field == 'stroke' && strokeWidthIsUnsetForTargets()) {
15148
+ styles.push(['stroke-width', 1]);
15149
+ }
15150
+ runStyleCommand(styles);
14084
15151
  }
14085
15152
 
14086
- function getActiveTable() {
14087
- var lyr = getActiveLayer();
14088
- return lyr && lyr.data || null;
15153
+ function applyStrokeWidthStyle(value) {
15154
+ var styles = [['stroke-width', value]];
15155
+ if (value > 0 && styleFieldIsUnsetForTargets('stroke')) {
15156
+ styles.push(['stroke', strokeControl.defaultColor]);
15157
+ }
15158
+ runStyleCommand(styles);
14089
15159
  }
14090
15160
 
14091
- function getSelectionIds() {
14092
- return hit ? hit.getSelectionIds() : [];
15161
+ function runStyleCommand(styles, opts) {
15162
+ var parts = ['-style'];
15163
+ syncTargetLayer();
15164
+ var ids = getTargetIds();
15165
+ if (!gui.console || !targetLayer || ids.length === 0) return;
15166
+ if (!opts || !opts.preservePreset) {
15167
+ presetControl.clearSelection();
15168
+ }
15169
+ styles.forEach(function(style) {
15170
+ parts.push(style[0] + '=' + quoteCommandValue(style[1]));
15171
+ });
15172
+ addTargetOption(parts);
15173
+ if (ids.length < internal.getFeatureCount(targetLayer)) {
15174
+ parts.push('ids=' + ids.join(','));
15175
+ }
15176
+ runCommand(parts.join(' '), 'Style layer');
14093
15177
  }
14094
15178
 
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 : '');
15179
+ function applyDefaultLineStyle() {
15180
+ var styles = [];
15181
+ if (!targetLayer || targetLayer.geometry_type != 'polyline') return;
15182
+ if (styleFieldIsUnsetForTargets('stroke-width')) {
15183
+ styles.push(['stroke-width', 1]);
15184
+ }
15185
+ if (styles.length > 0) {
15186
+ runStyleCommand(styles);
15187
+ }
14117
15188
  }
14118
15189
 
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
- });
15190
+ function applyRandomFillColors() {
15191
+ var cmd = '-classify colors=random non-adjacent';
15192
+ syncTargetLayer();
15193
+ if (!gui.console || !targetLayer || targetLayer.geometry_type != 'polygon') return;
15194
+ if (getActiveLayer() != targetLayer) {
15195
+ cmd += ' target=' + internal.formatOptionValue(internal.getLayerTargetId(gui.model, targetLayer));
15196
+ }
15197
+ presetControl.clearSelection();
15198
+ runCommand(cmd, 'Random fill colors');
14125
15199
  }
14126
15200
 
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);
15201
+ function runCommand(cmd, title) {
15202
+ runGuiEditCommand(gui, cmd, {
15203
+ title: title,
15204
+ onDone: updateControls
14132
15205
  });
14133
15206
  }
14134
15207
 
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
- });
15208
+ function applyStyleObject(style) {
15209
+ var styles = [];
15210
+ styleFields.forEach(function(field) {
15211
+ if (field in style) {
15212
+ styles.push([field, style[field]]);
15213
+ }
15214
+ });
15215
+ if (styles.length > 0) {
15216
+ runStyleCommand(styles, {preservePreset: true});
14143
15217
  }
14144
- fontStyleSelect.node().disabled = disabled;
14145
- fontStyleSelect.node().value = fontStyleVal && fontWeightVal ?
14146
- fontStyleVal + '|' + fontWeightVal : '';
14147
15218
  }
14148
15219
 
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
15220
+ function getCurrentStyle() {
15221
+ var style = {};
15222
+ addStyleValue(style, 'stroke', getControlValue(strokeControl.input));
15223
+ addStyleValue(style, 'stroke-width', parsePositiveNumber(strokeWidthClickText.value()));
15224
+ addStyleValue(style, 'stroke-opacity', parseOpacityValue(strokeOpacityInput.node().value));
15225
+ if (targetLayer && targetLayer.geometry_type == 'polygon') {
15226
+ addStyleValue(style, 'fill', getControlValue(fillControl.input));
15227
+ addStyleValue(style, 'fill-opacity', parseOpacityValue(fillOpacityInput.node().value));
14157
15228
  }
14158
- if (isHexColor(colorVal)) {
14159
- setPickerColor(hexToHsb(colorVal));
14160
- } else {
14161
- hideColorPicker();
15229
+ return style;
15230
+ }
15231
+
15232
+ function addStyleValue(style, field, value) {
15233
+ if (value || value === 0) {
15234
+ style[field] = value;
14162
15235
  }
14163
15236
  }
14164
15237
 
14165
- function updateCssControl(cssVal) {
14166
- cssInput.node().disabled = getSelectionIds().length === 0;
14167
- cssInput.node().value = cssVal || '';
15238
+ function getControlValue(input) {
15239
+ return input.node().value.trim();
14168
15240
  }
14169
15241
 
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
- });
15242
+ function updateSavedStyleControls() {
15243
+ presetControl.update();
14176
15244
  }
14177
15245
 
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
- });
15246
+ function getStyleType() {
15247
+ return targetLayer && targetLayer.geometry_type == 'polygon' ? 'polygon' : 'line';
14184
15248
  }
14185
15249
 
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
- });
15250
+ function getSortKey(item) {
15251
+ return (item.type || '') + '\t' + String(item.name || '').toLowerCase() + '\t' + (item.id || '');
14192
15252
  }
14193
15253
 
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 '';
15254
+ function clearLayerStyle() {
15255
+ var parts = ['-style clear'];
15256
+ syncTargetLayer();
15257
+ if (!gui.console || !targetLayer) return;
15258
+ presetControl.clearSelection();
15259
+ addTargetOption(parts);
15260
+ runCommand(parts.join(' '), 'Clear style');
15261
+ }
15262
+
15263
+ function addTargetOption(parts) {
15264
+ if (getActiveLayer() != targetLayer) {
15265
+ parts.push('target=' + internal.formatOptionValue(internal.getLayerTargetId(gui.model, targetLayer)));
15266
+ }
15267
+ }
15268
+
15269
+ function getCommonStyleValue(field, idsArg) {
15270
+ var records = targetLayer && targetLayer.data && targetLayer.data.getRecords();
15271
+ var ids;
15272
+ var value, val;
15273
+ if (!records) return '';
15274
+ ids = idsArg || getTargetIds();
15275
+ if (ids.length === 0) return '';
14199
15276
  for (var i=0; i<ids.length; i++) {
14200
15277
  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
15278
  if (i === 0) {
14211
15279
  value = val;
14212
15280
  } else if (val != value) {
14213
15281
  return '';
14214
15282
  }
14215
15283
  }
14216
- return hasValue || opts && opts.useDefault ? value : '';
15284
+ return value;
15285
+ }
15286
+
15287
+ function strokeWidthIsUnsetForTargets() {
15288
+ return styleFieldIsUnsetForTargets('stroke-width');
15289
+ }
15290
+
15291
+ function styleFieldIsUnsetForTargets(field) {
15292
+ var records = targetLayer && targetLayer.data && targetLayer.data.getRecords();
15293
+ var ids = getTargetIds();
15294
+ var val;
15295
+ if (!records) return true;
15296
+ for (var i=0; i<ids.length; i++) {
15297
+ val = records[ids[i]] && records[ids[i]][field];
15298
+ if (val !== undefined && val !== null && val !== '') {
15299
+ return false;
15300
+ }
15301
+ }
15302
+ return ids.length > 0;
15303
+ }
15304
+
15305
+ function getDefaultStrokeWidth() {
15306
+ return targetLayer && targetLayer.geometry_type == 'polyline' ? 1 : 0;
15307
+ }
15308
+
15309
+ function getAllFeatureIds(lyr) {
15310
+ var ids = [];
15311
+ for (var i=0, n=internal.getFeatureCount(lyr); i<n; i++) {
15312
+ ids.push(i);
15313
+ }
15314
+ return ids;
14217
15315
  }
14218
15316
 
14219
- function applyFont(fontName) {
14220
- applyStyleFields([fontField], function(rec) {
14221
- rec[fontField] = fontName;
14222
- });
15317
+ function getSelectionIds() {
15318
+ return hit ? hit.getSelectionIds() : [];
14223
15319
  }
14224
15320
 
14225
- function nudgeFontSize(delta) {
15321
+ function getTargetIds() {
14226
15322
  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
- });
15323
+ if (!targetLayer) return [];
15324
+ return ids.length > 0 ? ids : getAllFeatureIds(targetLayer);
14233
15325
  }
14234
15326
 
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
- });
15327
+ function clearSelection() {
15328
+ if (hit) hit.clearSelection();
15329
+ updateControls();
14242
15330
  }
14243
15331
 
14244
- function applyLabelColor(color) {
14245
- applyStyleFields([fillField], function(rec) {
14246
- rec[fillField] = color;
14247
- });
15332
+ function updateEditingStatus(count) {
15333
+ editingStatus.text(count > 0 ? 'Editing: ' + count + ' selected' : 'Editing: all');
15334
+ clearLink.classed('hidden', count === 0);
14248
15335
  }
14249
15336
 
14250
- function applyInlineCss(css) {
14251
- applyStyleFields([cssField], function(rec) {
14252
- rec[cssField] = css || undefined;
14253
- });
15337
+ function getActiveLayer() {
15338
+ var active = gui.model.getActiveLayer();
15339
+ return active && active.layer;
14254
15340
  }
14255
15341
 
14256
- function saveCurrentStyle() {
14257
- openSaveStylePopup();
15342
+ function syncTargetLayer() {
15343
+ var lyr = getActiveLayer();
15344
+ if (lyr == targetLayer) return;
15345
+ if (layerCanBeStyled(lyr)) {
15346
+ targetLayer = lyr;
15347
+ if (hit) hit.clearSelection();
15348
+ } else {
15349
+ targetLayer = null;
15350
+ }
14258
15351
  }
14259
15352
 
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
- });
15353
+ function closePanel() {
15354
+ turnOff();
15355
+ if (gui.interaction.getMode() == 'line_style' || gui.interaction.getMode() == 'polygon_style') {
15356
+ gui.interaction.turnOff();
15357
+ }
14280
15358
  }
14281
15359
 
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
- }
15360
+ function layerCanBeStyled(lyr) {
15361
+ return !!(lyr && (lyr.geometry_type == 'polyline' || lyr.geometry_type == 'polygon'));
14303
15362
  }
14304
15363
 
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();
15364
+ function modeMatchesActiveLayer(mode) {
15365
+ var lyr = getActiveLayer();
15366
+ return mode == 'line_style' && lyr && lyr.geometry_type == 'polyline' ||
15367
+ mode == 'polygon_style' && lyr && lyr.geometry_type == 'polygon';
14316
15368
  }
14317
15369
 
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);
15370
+ function modelSelectLayer(lyr, dataset) {
15371
+ if (lyr) lyr.hidden = false;
15372
+ gui.model.selectLayer(lyr, dataset);
14324
15373
  }
14325
15374
 
14326
- function getSavedStyles() {
14327
- var styles = GUI.getSavedValue(savedStylesKey);
14328
- return Array.isArray(styles) ? styles : [];
15375
+ function parseOpacityValue(str) {
15376
+ var pct = Number(String(str).replace('%', '').trim());
15377
+ if (!isFinite(pct)) return null;
15378
+ return Math.max(0, Math.min(100, pct)) / 100;
14329
15379
  }
14330
15380
 
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();
15381
+ function parsePositiveNumber(str) {
15382
+ var val = Number(String(str).trim());
15383
+ return isFinite(val) && val >= 0 ? val : null;
14341
15384
  }
14342
15385
 
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;
15386
+ function formatOpacityPct(val) {
15387
+ val = val === '' || val === undefined || val === null ? 1 : Number(val);
15388
+ return isFinite(val) ? Math.round(Math.max(0, Math.min(1, val)) * 100) + '%' : '';
15389
+ }
15390
+
15391
+ function formatNumberValue(val) {
15392
+ val = Number(val);
15393
+ return isFinite(val) ? String(val) : '';
15394
+ }
15395
+
15396
+ function quoteCommandValue(str) {
15397
+ return "'" + String(str).replace(/'/g, "\\'") + "'";
15398
+ }
15399
+ }
15400
+
15401
+ var defaultCircleRadius = 0;
15402
+ var defaultCreatedCircleRadius = 3;
15403
+ var defaultCircleFill = '#000000';
15404
+
15405
+ function PointStyleTool(gui) {
15406
+ var parent = gui.container.findChild('.mshp-main-map');
15407
+ var panel = El('div').addClass('label-style-panel point-style-panel rollover').appendTo(parent).hide();
15408
+ var title, noteSection, labelsSection, circlesSection, symbolsSection, labelNoteSection;
15409
+ var circleSectionLabel;
15410
+ var symbolNote;
15411
+ var createFieldSelect, createExprInput, createCopyCheckbox, createLabelsBtn;
15412
+ var editingRow, editingStatus, clearLink;
15413
+ var createCirclesRow;
15414
+ var circleControlRows = [];
15415
+ var circleRadiusClickText, circleFillControl, circleStrokeControl;
15416
+ var circleFillOpacityInput, circleStrokeOpacityInput, circleStrokeWidthClickText;
15417
+ var targetLayer = null;
15418
+ var hit = null;
15419
+
15420
+ initPanel();
15421
+
15422
+ this.open = function(lyr, dataset) {
15423
+ if (!layerCanBeStyled(lyr)) return;
15424
+ if (!gui.map.isActiveLayer(lyr)) {
15425
+ modelSelectLayer(lyr, dataset);
14351
15426
  }
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));
15427
+ gui.interaction.setMode('point_style');
15428
+ };
15429
+
15430
+ gui.on('interaction_mode_change', function(e) {
15431
+ if (modeMatchesActiveLayer(e.mode)) {
15432
+ turnOn();
15433
+ } else {
15434
+ turnOff();
14359
15435
  }
14360
- return style;
14361
- }
15436
+ });
14362
15437
 
14363
- function addStyleValue(style, field, value) {
14364
- if (value || value === 0) {
14365
- style[field] = value;
15438
+ gui.model.on('update', function() {
15439
+ if (panel.visible() && !modeMatchesActiveLayer(gui.interaction.getMode())) {
15440
+ gui.interaction.turnOff();
15441
+ } else if (panel.visible()) {
15442
+ updateControls();
14366
15443
  }
15444
+ });
15445
+
15446
+ hit = gui.map.getHitControl && gui.map.getHitControl();
15447
+ if (hit) {
15448
+ hit.on('change', function(e) {
15449
+ if (e.mode == 'point_style') {
15450
+ updateControls();
15451
+ }
15452
+ });
14367
15453
  }
14368
15454
 
14369
- function getNumericControlValue(el) {
14370
- var value = Number(el.text());
14371
- return isFinite(value) && value > 0 ? value : null;
15455
+ function initPanel() {
15456
+ var header = El('div').addClass('label-style-panel-title').appendTo(panel);
15457
+ title = El('span').appendTo(header).text('Point symbols');
15458
+ El('button').addClass('label-style-close').appendTo(header).text('×').on('click', closePanel);
15459
+
15460
+ initNoteSections();
15461
+ initCreateLabelsSection();
15462
+ initCreateCirclesSection();
15463
+ initSymbolsSection();
14372
15464
  }
14373
15465
 
14374
- function getSelectedLabelPosition() {
14375
- var out = '';
14376
- labelPositions.forEach(function(pos) {
14377
- if (posBtns[pos].hasClass('selected')) out = pos;
14378
- });
14379
- return out;
15466
+ function initNoteSections() {
15467
+ noteSection = El('div').addClass('point-style-section point-style-note-section').appendTo(panel);
15468
+ El('div').addClass('point-style-note').appendTo(noteSection).text('This layer contains unstyled points.');
15469
+
15470
+ labelNoteSection = El('div').addClass('point-style-section point-label-note-section').appendTo(panel);
15471
+ El('div').addClass('point-style-note').appendTo(labelNoteSection)
15472
+ .text('This layer is rendered as labels. Use the label style tool to edit label styles.');
14380
15473
  }
14381
15474
 
14382
- function getSelectedIcon() {
14383
- var out = '';
14384
- iconTypes.forEach(function(icon) {
14385
- if (iconBtns[icon.name].hasClass('selected')) out = icon.name;
15475
+ function initCreateLabelsSection() {
15476
+ labelsSection = El('div').addClass('point-style-section').appendTo(panel);
15477
+ El('div').addClass('label-style-row-label').appendTo(labelsSection).text('Labels');
15478
+
15479
+ var fieldRow = El('label').addClass('label-style-row').appendTo(labelsSection);
15480
+ El('span').appendTo(fieldRow).text('Label field');
15481
+ createFieldSelect = El('select').appendTo(fieldRow).on('change', function() {
15482
+ var field = createFieldSelect.node().value;
15483
+ if (field) {
15484
+ createExprInput.node().value = getFieldExpression(field);
15485
+ }
15486
+ updateCreateLabelsButton();
14386
15487
  });
14387
- return out;
14388
- }
14389
15488
 
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);
15489
+ var exprRow = El('label').addClass('label-style-row label-create-expression-row').appendTo(labelsSection);
15490
+ El('span').appendTo(exprRow).text('or expression');
15491
+ createExprInput = El('input')
15492
+ .attr('type', 'text')
15493
+ .appendTo(exprRow)
15494
+ .on('input', updateCreateLabelsButton)
15495
+ .on('change', updateCreateLabelsButton);
15496
+
15497
+ var btnRow = El('div').addClass('label-style-row point-create-labels-row').appendTo(labelsSection);
15498
+ createLabelsBtn = El('button').appendTo(btnRow).text('Create').on('click', createLabels);
15499
+ var copyLabel = El('label').addClass('point-create-copy-label').appendTo(btnRow);
15500
+ createCopyCheckbox = El('input').attr('type', 'checkbox').appendTo(copyLabel);
15501
+ El('span').appendTo(copyLabel).text('as new layer');
15502
+ }
15503
+
15504
+ function initCreateCirclesSection() {
15505
+ circlesSection = El('div').addClass('point-style-section point-circle-section').appendTo(panel);
15506
+ circleSectionLabel = El('div').addClass('label-style-row-label').appendTo(circlesSection).text('Circles');
15507
+ El('div').addClass('point-style-note point-circle-note').appendTo(circlesSection)
15508
+ .text('Use the -style command in the console to create proportional circles.');
15509
+
15510
+ createCirclesRow = El('div').addClass('label-style-row point-create-circles-row').appendTo(circlesSection);
15511
+ El('button').appendTo(createCirclesRow).text('Create').on('click', createSimpleCircles);
15512
+ El('span').appendTo(createCirclesRow).text('simple circles');
15513
+
15514
+ editingRow = El('div').addClass('label-style-row label-style-selection-row point-style-selection-row').appendTo(circlesSection);
15515
+ editingStatus = El('span').addClass('label-editing-status').appendTo(editingRow);
15516
+ clearLink = El('span').addClass('label-editing-clear colored-text').appendTo(editingRow).text('deselect').on('click', clearSelection);
15517
+
15518
+ var fillRow = El('div').addClass('label-style-row point-symbol-row').appendTo(circlesSection);
15519
+ circleFillControl = addCircleColorControl(fillRow, 'Fill');
15520
+ circleFillOpacityInput = addCircleNumberControl(fillRow, 'Opacity', '100%');
15521
+ circleControlRows.push(fillRow);
15522
+
15523
+ var strokeRow = El('div').addClass('label-style-row point-symbol-row').appendTo(circlesSection);
15524
+ circleStrokeControl = addCircleColorControl(strokeRow, 'Stroke');
15525
+ circleStrokeOpacityInput = addCircleNumberControl(strokeRow, 'Opacity', '100%');
15526
+ circleControlRows.push(strokeRow);
15527
+
15528
+ var sizeRow = El('div').addClass('label-style-row point-symbol-size-row').appendTo(circlesSection);
15529
+ addCircleStepperControl(sizeRow, 'Stroke width', function() {
15530
+ return getCircleStrokeWidth();
15531
+ }, function(direction) {
15532
+ setCircleStrokeWidth(getNextStrokeWidth(getCircleStrokeWidth(), direction));
15533
+ applyCircleStyles();
15534
+ }, function(value) {
15535
+ setCircleStrokeWidth(value);
15536
+ applyCircleStyles();
15537
+ }, function(clickText) {
15538
+ circleStrokeWidthClickText = clickText;
15539
+ });
15540
+
15541
+ addCircleStepperControl(sizeRow, 'Radius', function() {
15542
+ return getCircleRadiusForNudge();
15543
+ }, function(direction) {
15544
+ setCircleRadius(getNextCircleRadius(getCircleRadiusForNudge(), direction));
15545
+ applyCircleStyles();
15546
+ }, function(value) {
15547
+ setCircleRadius(value);
15548
+ applyCircleStyles();
15549
+ }, function(clickText, text) {
15550
+ circleRadiusClickText = clickText;
15551
+ text.addClass('label-icon-size-value');
15552
+ });
15553
+ circleControlRows.push(sizeRow);
15554
+ }
15555
+
15556
+ function initSymbolsSection() {
15557
+ symbolsSection = El('div').addClass('point-style-section point-symbol-info-section').appendTo(panel);
15558
+ El('div').addClass('label-style-row-label').appendTo(symbolsSection).text('Symbols');
15559
+ symbolNote = El('div').addClass('point-style-note').appendTo(symbolsSection)
15560
+ .text('Use the -symbols command in the console to create arrows and other symbols.');
15561
+ }
15562
+
15563
+ function addCircleColorControl(row, label) {
15564
+ var colorCell = El('div').addClass('point-symbol-color-cell label-color-row').appendTo(row);
15565
+ var control = {};
15566
+ El('span').appendTo(colorCell).text(label);
15567
+ control.chit = makePanelButton(colorCell, '', function() {
15568
+ control.picker.toggle();
15569
+ }).addClass('label-color-chit');
15570
+ control.input = El('input').attr('type', 'text').appendTo(colorCell).on('change', function() {
15571
+ var color = control.input.node().value.trim();
15572
+ if (isHexColor(color)) {
15573
+ control.picker.setColor(color);
14398
15574
  }
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']);
15575
+ applyCircleStyles();
15576
+ });
15577
+ control.picker = new ColorPicker(colorCell, {
15578
+ presetRows: layerColorPresetRows,
15579
+ onPreview: function(hex) {
15580
+ setCircleColor(control, hex);
15581
+ },
15582
+ onChange: function(hex) {
15583
+ setCircleColor(control, hex);
15584
+ applyCircleStyles();
15585
+ }
15586
+ });
15587
+ setCircleColor(control, '');
15588
+ return control;
15589
+ }
15590
+
15591
+ function addCircleStepperControl(row, label, getValue, nudgeValue, setValue, capture) {
15592
+ var control;
15593
+ var buttonRow;
15594
+ var text;
15595
+ var clickText;
15596
+ control = El('div').addClass('point-symbol-stepper-control').appendTo(row);
15597
+ El('span').appendTo(control).text(label);
15598
+ buttonRow = El('div').addClass('layer-stepper-control').appendTo(control);
15599
+ makePanelButton(buttonRow, '−', function() {
15600
+ nudgeValue(-1);
15601
+ });
15602
+ text = El('span').addClass('layer-stroke-width-value').appendTo(buttonRow);
15603
+ clickText = new ClickText2(text);
15604
+ clickText.on('change', function() {
15605
+ var value = parsePositiveNumber(clickText.value());
15606
+ if (value === null) {
15607
+ clickText.value(formatNumberValue(getValue()));
15608
+ return;
14409
15609
  }
15610
+ setValue(value);
15611
+ });
15612
+ makePanelButton(buttonRow, '+', function() {
15613
+ nudgeValue(1);
14410
15614
  });
15615
+ capture(clickText, text);
14411
15616
  }
14412
15617
 
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
- };
15618
+ function addCircleNumberControl(row, label, value) {
15619
+ var control = El('label').addClass('layer-number-control').appendTo(row);
15620
+ var input;
15621
+ El('span').appendTo(control).text(label);
15622
+ input = El('input').attr('type', 'text').appendTo(control).on('change', applyCircleStyles);
15623
+ input.node().value = value;
15624
+ return input;
14420
15625
  }
14421
15626
 
14422
- function applyLabelPosition(pos) {
14423
- applyStyleFields(labelPositionFields, function(rec) {
14424
- internal.setLabelPositionStyle(rec, pos);
14425
- });
15627
+ function makePanelButton(parent, label, action) {
15628
+ return El('div')
15629
+ .addClass('label-panel-btn')
15630
+ .attr('role', 'button')
15631
+ .attr('tabindex', '0')
15632
+ .appendTo(parent)
15633
+ .text(label)
15634
+ .on('click', function(e) {
15635
+ action(e);
15636
+ })
15637
+ .on('keydown', function(e) {
15638
+ if (e.key == 'Enter' || e.key == ' ') {
15639
+ e.preventDefault();
15640
+ action(e);
15641
+ }
15642
+ });
14426
15643
  }
14427
15644
 
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
- });
15645
+ function turnOn() {
15646
+ targetLayer = getActiveLayer();
15647
+ if (getPointRepresentation() == 'label' && gui.labelTool && gui.labelTool.open) {
15648
+ gui.labelTool.open(targetLayer);
15649
+ targetLayer = null;
15650
+ return;
15651
+ }
15652
+ renderCreateFields();
15653
+ updateControls();
15654
+ panel.show();
14440
15655
  }
14441
15656
 
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
- });
15657
+ function turnOff() {
15658
+ panel.hide();
15659
+ circleFillControl.picker.hide();
15660
+ circleStrokeControl.picker.hide();
15661
+ targetLayer = null;
14453
15662
  }
14454
15663
 
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;
15664
+ function closePanel() {
15665
+ turnOff();
15666
+ if (gui.interaction.getMode() == 'point_style') {
15667
+ gui.interaction.turnOff();
15668
+ }
14459
15669
  }
14460
15670
 
14461
- function isHexColor(str) {
14462
- return /^#[0-9a-f]{6}$/i.test(str);
15671
+ function updateControls() {
15672
+ syncTargetLayer();
15673
+ var representation = getPointRepresentation();
15674
+ updateCreateLabelsButton();
15675
+ title.text(representation == 'circle' ? 'Circle styles' : 'Point symbols');
15676
+ updateEditingStatus(getSelectionIds().length);
15677
+ toggleSection(noteSection, representation == 'unstyled');
15678
+ toggleSection(labelsSection, representation == 'unstyled');
15679
+ toggleSection(circlesSection, representation == 'unstyled' || representation == 'circle');
15680
+ toggleSection(symbolsSection, representation == 'unstyled' || representation == 'svg-symbol');
15681
+ toggleSection(labelNoteSection, representation == 'label');
15682
+ updateSectionBorders([noteSection, labelNoteSection, labelsSection, circlesSection, symbolsSection]);
15683
+ symbolNote.text(representation == 'svg-symbol' ?
15684
+ 'This layer uses SVG symbols. Use the -symbols command in the console to create arrows and other symbols.' :
15685
+ 'Use the -symbols command in the console to create arrows and other symbols.');
15686
+ updateCircleSection(representation);
15687
+ updateCircleControls(representation);
15688
+ }
15689
+
15690
+ function toggleSection(section, visible) {
15691
+ if (visible) {
15692
+ section.show();
15693
+ } else {
15694
+ section.hide();
15695
+ }
14463
15696
  }
14464
15697
 
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);
15698
+ function updateSectionBorders(sections) {
15699
+ var foundFirst = false;
15700
+ sections.forEach(function(section) {
15701
+ var visible = section.visible();
15702
+ section.classed('point-style-first-visible', visible && !foundFirst);
15703
+ if (visible) foundFirst = true;
14481
15704
  });
14482
- hueCanvas.on('mousedown', function(e) {
14483
- startCanvasDrag(e, updateHueFromEvent);
15705
+ }
15706
+
15707
+ function updateCircleSection(representation) {
15708
+ var showCreate = representation == 'unstyled';
15709
+ circleSectionLabel.classed('hidden', !showCreate);
15710
+ createCirclesRow.classed('hidden', !showCreate);
15711
+ editingRow.classed('hidden', representation != 'circle');
15712
+ circlesSection.findChild('.point-circle-note').classed('hidden', !showCreate);
15713
+ circleControlRows.forEach(function(row) {
15714
+ row.classed('hidden', showCreate);
15715
+ });
15716
+ }
15717
+
15718
+ function updateCircleControls(representation) {
15719
+ var radius = getCommonValue('r');
15720
+ var fill = getCommonValue('fill');
15721
+ var stroke = getCommonValue('stroke');
15722
+ var fillOpacity = getCommonValue('fill-opacity');
15723
+ var strokeOpacity = getCommonValue('stroke-opacity');
15724
+ var strokeWidth = getCommonValue('stroke-width');
15725
+ setCircleRadius(radius);
15726
+ setCircleColor(circleFillControl, fill);
15727
+ setCircleColor(circleStrokeControl, stroke);
15728
+ circleFillOpacityInput.node().value = formatOpacityPct(fillOpacity === '' ? 1 : fillOpacity);
15729
+ circleStrokeOpacityInput.node().value = formatOpacityPct(strokeOpacity === '' ? 1 : strokeOpacity);
15730
+ setCircleStrokeWidth(strokeWidth === '' ? 0 : strokeWidth);
15731
+ if (representation != 'circle' && representation != 'unstyled') {
15732
+ setCircleRadius(defaultCircleRadius);
15733
+ setCircleColor(circleFillControl, '');
15734
+ setCircleColor(circleStrokeControl, '');
15735
+ circleFillOpacityInput.node().value = '100%';
15736
+ circleStrokeOpacityInput.node().value = '100%';
15737
+ setCircleStrokeWidth(0);
15738
+ }
15739
+ }
15740
+
15741
+ function renderCreateFields() {
15742
+ var lyr = getActiveLayer();
15743
+ var records = lyr && lyr.data ? lyr.data.getRecords() : [];
15744
+ var fields = lyr && lyr.data ? lyr.data.getFields().filter(function(field) {
15745
+ return internal.getColumnType(field, records) == 'string';
15746
+ }) : [];
15747
+ var value = createFieldSelect.node().value;
15748
+ createFieldSelect.empty();
15749
+ El('option').attr('value', '').appendTo(createFieldSelect).text('');
15750
+ fields.forEach(function(field) {
15751
+ El('option').attr('value', field).appendTo(createFieldSelect).text(field);
14484
15752
  });
14485
- setPickerColor(pickerColor);
15753
+ createFieldSelect.node().disabled = fields.length === 0;
15754
+ createFieldSelect.node().value = fields.indexOf(value) > -1 ? value : '';
14486
15755
  }
14487
15756
 
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();
14505
- });
15757
+ function updateCreateLabelsButton() {
15758
+ createLabelsBtn.node().disabled = createExprInput.node().value.trim() === '';
14506
15759
  }
14507
15760
 
14508
- function toggleColorPicker() {
14509
- if (colorPicker.visible()) {
14510
- hideColorPicker();
14511
- } else {
14512
- colorPicker.show();
14513
- drawColorPicker();
14514
- }
15761
+ function createLabels() {
15762
+ var expr = createExprInput.node().value.trim();
15763
+ var cmd;
15764
+ if (!expr || !gui.console) return;
15765
+ cmd = createCopyCheckbox.node().checked ?
15766
+ '-filter true + name=labels -style label-text=' + quoteCommandValue(expr) :
15767
+ '-style label-text=' + quoteCommandValue(expr);
15768
+ runGuiEditCommand(gui, cmd, {
15769
+ title: 'Create labels',
15770
+ onSuccess: openLabelStyles
15771
+ });
14515
15772
  }
14516
15773
 
14517
- function hideColorPicker() {
14518
- colorPicker.hide();
15774
+ function openLabelStyles() {
15775
+ var active = gui.model.getActiveLayer();
15776
+ if (active && active.layer && internal.layerHasLabels(active.layer) &&
15777
+ gui.labelTool && gui.labelTool.open) {
15778
+ gui.labelTool.open(active.layer, active.dataset);
15779
+ } else {
15780
+ updateControls();
15781
+ }
15782
+ }
15783
+
15784
+ function createSimpleCircles() {
15785
+ runCommand('-style r=' + defaultCreatedCircleRadius +
15786
+ ' fill=' + quoteCommandValue(defaultCircleFill) +
15787
+ ' fill-opacity=1 stroke-opacity=1 stroke-width=0', 'Create circles');
15788
+ }
15789
+
15790
+ function applyCircleStyles() {
15791
+ var representation = getPointRepresentation();
15792
+ var radius = getCircleRadius();
15793
+ var fill = circleFillControl.input.node().value.trim();
15794
+ var fillOpacity = parseOpacityValue(circleFillOpacityInput.node().value);
15795
+ var stroke = circleStrokeControl.input.node().value.trim();
15796
+ var strokeOpacity = parseOpacityValue(circleStrokeOpacityInput.node().value);
15797
+ var strokeWidth = getCircleStrokeWidth();
15798
+ var args;
15799
+ if (!gui.console || !(representation == 'unstyled' || representation == 'circle') ||
15800
+ (radius !== null && !(radius >= 0)) || fillOpacity === null || strokeOpacity === null || strokeWidth === null) return;
15801
+ if (radius !== null && radius > 0 && !fill && !stroke && styleFieldIsUnset('fill') && styleFieldIsUnset('stroke')) {
15802
+ fill = defaultCircleFill;
15803
+ setCircleColor(circleFillControl, fill);
15804
+ }
15805
+ args = [
15806
+ 'fill-opacity=' + fillOpacity,
15807
+ 'stroke-opacity=' + strokeOpacity,
15808
+ 'stroke-width=' + strokeWidth
15809
+ ];
15810
+ if (radius !== null) args.unshift('r=' + radius);
15811
+ if (fill) args.push('fill=' + quoteCommandValue(fill));
15812
+ if (stroke) args.push('stroke=' + quoteCommandValue(stroke));
15813
+ runStyleCommand(args, 'Create circles');
14519
15814
  }
14520
15815
 
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();
15816
+ function runStyleCommand(args, title) {
15817
+ syncTargetLayer();
15818
+ var ids = getTargetIds();
15819
+ var parts = ['-style'].concat(args);
15820
+ if (!targetLayer || ids.length === 0) return;
15821
+ addTargetOption(parts);
15822
+ if (ids.length < internal.getFeatureCount(targetLayer)) {
15823
+ parts.push('ids=' + ids.join(','));
14538
15824
  }
15825
+ runCommand(parts.join(' '), title);
14539
15826
  }
14540
15827
 
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
15828
+ function runCommand(cmd, title) {
15829
+ runGuiEditCommand(gui, cmd, {
15830
+ title: title,
15831
+ onSuccess: updateControls
14547
15832
  });
14548
15833
  }
14549
15834
 
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
- });
15835
+ function getCircleRadius() {
15836
+ var str = String(circleRadiusClickText.value()).trim();
15837
+ var radius = Number(str);
15838
+ if (str === '') return null;
15839
+ return isFinite(radius) && radius >= 0 ? radius : defaultCircleRadius;
14557
15840
  }
14558
15841
 
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
- };
15842
+ function getCircleRadiusForNudge() {
15843
+ var radius = getCircleRadius();
15844
+ return radius === null ? getMostCommonNumberValue('r', defaultCircleRadius) : radius;
14565
15845
  }
14566
15846
 
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();
15847
+ function setCircleRadius(value) {
15848
+ circleRadiusClickText.value(value === '' ? '' : formatNumberValue(value));
14575
15849
  }
14576
15850
 
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);
15851
+ function getCircleStrokeWidth() {
15852
+ var width = Number(circleStrokeWidthClickText.value());
15853
+ return isFinite(width) && width >= 0 ? width : 0;
14584
15854
  }
14585
15855
 
14586
- function drawColorPicker() {
14587
- if (!sbCanvas) return;
14588
- drawSaturationBrightnessCanvas();
14589
- drawHueCanvas();
15856
+ function setCircleStrokeWidth(value) {
15857
+ circleStrokeWidthClickText.value(formatNumberValue(value));
14590
15858
  }
14591
15859
 
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;
15860
+ function getNextStrokeWidth(value, direction) {
15861
+ var baseSteps = [0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4];
15862
+ var i;
15863
+ if (direction > 0) {
15864
+ for (i=0; i<baseSteps.length; i++) {
15865
+ if (value < baseSteps[i]) return baseSteps[i];
14608
15866
  }
15867
+ return Math.floor(value) + 1;
14609
15868
  }
14610
- ctx.putImageData(image, 0, 0);
14611
- positionMarker(sbMarker, pickerColor.s, 255 - pickerColor.b, pickerColor);
15869
+ for (i=baseSteps.length - 1; i>=0; i--) {
15870
+ if (value > baseSteps[i]) return baseSteps[i];
15871
+ }
15872
+ return baseSteps[0];
14612
15873
  }
14613
15874
 
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;
15875
+ function getNextCircleRadius(value, direction) {
15876
+ var baseSteps = [0, 0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
15877
+ var i;
15878
+ if (direction > 0) {
15879
+ for (i=0; i<baseSteps.length; i++) {
15880
+ if (value < baseSteps[i]) return baseSteps[i];
14626
15881
  }
15882
+ return Math.floor(value) + 1;
14627
15883
  }
14628
- ctx.putImageData(image, 0, 0);
14629
- positionMarker(hueMarker, pickerColor.h, 9, {h: pickerColor.h, s: 255, b: 255});
15884
+ for (i=baseSteps.length - 1; i>=0; i--) {
15885
+ if (value > baseSteps[i]) return baseSteps[i];
15886
+ }
15887
+ return baseSteps[0];
14630
15888
  }
14631
15889
 
14632
- function commitPickerColor() {
14633
- applyLabelColor(hsbToHex(pickerColor));
15890
+ function getPointRepresentation() {
15891
+ var lyr = targetLayer || getActiveLayer();
15892
+ var table = lyr && lyr.data;
15893
+ if (table && table.fieldExists('svg-symbol')) return 'svg-symbol';
15894
+ if (table && table.fieldExists('label-text')) return 'label';
15895
+ if (table && table.fieldExists('r')) return 'circle';
15896
+ return 'unstyled';
14634
15897
  }
14635
15898
 
14636
- function closePickerColor() {
14637
- hideColorPicker();
15899
+ function getCommonValue(field) {
15900
+ var table = targetLayer && targetLayer.data;
15901
+ var records = table && table.getRecords();
15902
+ var ids = getTargetIds();
15903
+ var value, val;
15904
+ if (!records || ids.length === 0) return '';
15905
+ for (var i=0; i<ids.length; i++) {
15906
+ val = records[ids[i]] && records[ids[i]][field];
15907
+ if (i === 0) {
15908
+ value = val;
15909
+ } else if (val != value) {
15910
+ return '';
15911
+ }
15912
+ }
15913
+ return value === undefined || value === null ? '' : value;
14638
15914
  }
14639
15915
 
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';
15916
+ function getMostCommonNumberValue(field, defaultValue) {
15917
+ var table = targetLayer && targetLayer.data;
15918
+ var records = table && table.getRecords();
15919
+ var ids = getTargetIds();
15920
+ var counts = {};
15921
+ var bestValue = null;
15922
+ var bestCount = 0;
15923
+ var val, key;
15924
+ if (!records || ids.length === 0) return defaultValue;
15925
+ for (var i=0; i<ids.length; i++) {
15926
+ val = Number(records[ids[i]] && records[ids[i]][field]);
15927
+ if (!isFinite(val)) continue;
15928
+ key = String(val);
15929
+ counts[key] = (counts[key] || 0) + 1;
15930
+ if (counts[key] > bestCount) {
15931
+ bestCount = counts[key];
15932
+ bestValue = val;
15933
+ }
15934
+ }
15935
+ return bestValue === null ? defaultValue : bestValue;
14643
15936
  }
14644
15937
 
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>');
15938
+ function styleFieldIsUnset(field) {
15939
+ var table = targetLayer && targetLayer.data;
15940
+ var records = table && table.getRecords();
15941
+ var ids = getTargetIds();
15942
+ var val;
15943
+ if (!records || ids.length === 0) return true;
15944
+ for (var i=0; i<ids.length; i++) {
15945
+ val = records[ids[i]] && records[ids[i]][field];
15946
+ if (val !== undefined && val !== null && val !== '') return false;
15947
+ }
15948
+ return true;
14647
15949
  }
14648
15950
 
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));
15951
+ function addTargetOption(parts) {
15952
+ if (getActiveLayer() != targetLayer) {
15953
+ parts.push('target=' + internal.formatOptionValue(internal.getLayerTargetId(gui.model, targetLayer)));
15954
+ }
15955
+ }
15956
+
15957
+ function getSelectionIds() {
15958
+ return hit ? hit.getSelectionIds() : [];
15959
+ }
15960
+
15961
+ function getTargetIds() {
15962
+ var ids = getSelectionIds();
15963
+ if (!targetLayer) return [];
15964
+ return ids.length > 0 ? ids : getAllFeatureIds(targetLayer);
14658
15965
  }
14659
15966
 
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;
15967
+ function getAllFeatureIds(lyr) {
15968
+ var ids = [];
15969
+ for (var i=0, n=internal.getFeatureCount(lyr); i<n; i++) {
15970
+ ids.push(i);
14674
15971
  }
14675
- return {
14676
- h: Math.round(h / 360 * 255),
14677
- s: Math.round(max === 0 ? 0 : d / max * 255),
14678
- b: max
14679
- };
15972
+ return ids;
14680
15973
  }
14681
15974
 
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('');
15975
+ function clearSelection() {
15976
+ if (hit) hit.clearSelection();
15977
+ updateControls();
14687
15978
  }
14688
15979
 
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
- };
15980
+ function updateEditingStatus(count) {
15981
+ editingStatus.text(count > 0 ? 'Editing: ' + count + ' selected' : 'Editing: all');
15982
+ clearLink.classed('hidden', count === 0);
14706
15983
  }
14707
15984
 
14708
- function byteToPct(val) {
14709
- return Math.round(val / 255 * 100);
15985
+ function setCircleColor(control, color) {
15986
+ control.input.node().value = color;
15987
+ control.chit.css('background-color', isHexColor(color) ? color : 'transparent');
14710
15988
  }
14711
15989
 
14712
- function pctToByte(val) {
14713
- return Math.round(clamp(val, 0, 100) / 100 * 255);
15990
+ function parseOpacityValue(str) {
15991
+ var pct = Number(String(str).replace('%', '').trim());
15992
+ if (!isFinite(pct)) return null;
15993
+ return Math.max(0, Math.min(100, pct)) / 100;
14714
15994
  }
14715
15995
 
14716
- function byteToDegrees(val) {
14717
- return Math.round(val / 255 * 360);
15996
+ function parsePositiveNumber(str) {
15997
+ if (String(str).trim() === '') return null;
15998
+ var val = Number(String(str).trim());
15999
+ return isFinite(val) && val >= 0 ? val : null;
14718
16000
  }
14719
16001
 
14720
- function degreesToByte(val) {
14721
- return Math.round(clamp(val, 0, 360) / 360 * 255);
16002
+ function formatOpacityPct(val) {
16003
+ val = Number(val);
16004
+ return isFinite(val) ? Math.round(Math.max(0, Math.min(1, val)) * 100) + '%' : '';
14722
16005
  }
14723
16006
 
14724
- function parseNumberField(str) {
14725
- return Number(String(str).replace(/[°%]/g, '').trim());
16007
+ function formatNumberValue(val) {
16008
+ val = Number(val);
16009
+ return isFinite(val) ? String(val) : '';
14726
16010
  }
14727
16011
 
14728
- function clamp(val, min, max) {
14729
- return isFinite(val) ? Math.max(min, Math.min(max, val)) : min;
16012
+ function getFieldExpression(field) {
16013
+ return 'd[' + JSON.stringify(field) + ']';
14730
16014
  }
14731
16015
 
14732
- function applyStyleFields(fields, updateRecord) {
14733
- var table = getActiveTable();
14734
- var ids = getSelectionIds();
16016
+ function getActiveLayer() {
16017
+ var active = gui.model.getActiveLayer();
16018
+ return active && active.layer;
16019
+ }
16020
+
16021
+ function syncTargetLayer() {
14735
16022
  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'});
14746
- }
14747
- updateRecords(ids, table, updateRecord);
14748
- if (hasNewFields) {
14749
- table.markSchemaChanged({operation: 'label-style'});
16023
+ if (lyr == targetLayer) return;
16024
+ if (layerCanBeStyled(lyr)) {
16025
+ targetLayer = lyr;
16026
+ if (hit) hit.clearSelection();
14750
16027
  } else {
14751
- table.markFieldsChanged(fields, {operation: 'label-style'});
16028
+ targetLayer = null;
14752
16029
  }
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();
14759
16030
  }
14760
16031
 
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
- });
16032
+ function modelSelectLayer(lyr, dataset) {
16033
+ if (lyr) lyr.hidden = false;
16034
+ gui.model.selectLayer(lyr, dataset);
14768
16035
  }
14769
16036
 
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
- });
16037
+ function modeMatchesActiveLayer(mode) {
16038
+ var lyr = getActiveLayer();
16039
+ return mode == 'point_style' && layerCanBeStyled(lyr);
14778
16040
  }
14779
16041
 
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
- });
16042
+ function layerCanBeStyled(lyr) {
16043
+ return !!(lyr && lyr.geometry_type == 'point');
14787
16044
  }
14788
16045
 
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');
16046
+ function quoteCommandValue(str) {
16047
+ return "'" + String(str).replace(/'/g, "\\'") + "'";
14795
16048
  }
14796
-
14797
16049
  }
14798
16050
 
14799
16051
  function Model(gui) {
@@ -15438,6 +16690,7 @@
15438
16690
  var targetLayer;
15439
16691
  var hitTest;
15440
16692
  var pinnedOn; // used in multi-edit mode (selection) for toggling pinning behavior
16693
+ var suppressChangeEvent = false;
15441
16694
 
15442
16695
  // event priority is higher than navigation, so stopping propagation disables
15443
16696
  // pan navigation
@@ -15514,7 +16767,8 @@
15514
16767
 
15515
16768
  function selectable() {
15516
16769
  var mode = interactionMode();
15517
- return mode == 'selection' || mode == 'label_style';
16770
+ return mode == 'selection' || mode == 'label_style' ||
16771
+ mode == 'point_style' || mode == 'line_style' || mode == 'polygon_style';
15518
16772
  }
15519
16773
 
15520
16774
  function pinnable() {
@@ -15532,7 +16786,8 @@
15532
16786
  var mode = interactionMode();
15533
16787
  // click used to pin popup and select features
15534
16788
  return mode == 'data' || mode == 'info' || mode == 'selection' ||
15535
- mode == 'label_style' || mode == 'rectangles' || mode == 'edit_points';
16789
+ mode == 'label_style' || mode == 'point_style' || mode == 'line_style' || mode == 'polygon_style' ||
16790
+ mode == 'rectangles' || mode == 'edit_points';
15536
16791
  }
15537
16792
 
15538
16793
  self.getHitId = function() {
@@ -15656,12 +16911,13 @@
15656
16911
  // make sure popup is unpinned and turned off when switching editing modes
15657
16912
  // (some modes do not support pinning)
15658
16913
  gui.on('interaction_mode_change', function(e) {
15659
- self.clearSelection();
16914
+ clearSelectionSilently();
15660
16915
  if (gui.interaction.modeUsesHitDetection(e.mode)) {
15661
16916
  turnOn(e.mode);
15662
16917
  } else {
15663
16918
  turnOff();
15664
16919
  }
16920
+ gui.dispatchEvent('map-needs-refresh');
15665
16921
  });
15666
16922
 
15667
16923
  gui.on('undo_redo_pre', function() {
@@ -15685,7 +16941,7 @@
15685
16941
 
15686
16942
  // TODO: move pinning to inspection control?
15687
16943
  if (clickable()) {
15688
- updateSelectionState(convertClickDataToSelectionData(hitTest(e)));
16944
+ updateSelectionState(convertClickDataToSelectionData(hitTest(e), e));
15689
16945
  }
15690
16946
 
15691
16947
  if (pinned && interactionMode() == 'edit_points') {
@@ -15727,7 +16983,7 @@
15727
16983
 
15728
16984
  // Translates feature hit data from a mouse click into feature selection data
15729
16985
  // hitData: hit data from a mouse click
15730
- function convertClickDataToSelectionData(hitData) {
16986
+ function convertClickDataToSelectionData(hitData, e) {
15731
16987
  // mergeCurrentState(hitData);
15732
16988
  // TOGGLE pinned state under some conditions
15733
16989
  var id = hitData.ids.length > 0 ? hitData.ids[0] : -1;
@@ -15744,13 +17000,32 @@
15744
17000
  }
15745
17001
  if (selectable()) {
15746
17002
  if (id > -1) {
15747
- selectionIds = toggleId(id, selectionIds);
17003
+ selectionIds = styleSelectionMode() ?
17004
+ selectStyleFeature(id, e) :
17005
+ toggleId(id, selectionIds);
15748
17006
  }
15749
17007
  hitData.ids = selectionIds;
15750
17008
  }
15751
17009
  return hitData;
15752
17010
  }
15753
17011
 
17012
+ function styleSelectionMode() {
17013
+ var mode = interactionMode();
17014
+ return mode == 'label_style' || mode == 'point_style' ||
17015
+ mode == 'line_style' || mode == 'polygon_style';
17016
+ }
17017
+
17018
+ function selectStyleFeature(id, e) {
17019
+ return eventUsesAdditiveSelection(e) ?
17020
+ toggleId(id, selectionIds) :
17021
+ [id];
17022
+ }
17023
+
17024
+ function eventUsesAdditiveSelection(e) {
17025
+ var original = e && e.originalEvent || e;
17026
+ return !!(original && original.shiftKey);
17027
+ }
17028
+
15754
17029
  function mergeSelectionModeHoverData(hitData) {
15755
17030
  if (hitData.ids.length === 0 || selectionIds.includes(hitData.ids[0])) {
15756
17031
  hitData.ids = selectionIds;
@@ -15806,11 +17081,17 @@
15806
17081
 
15807
17082
  storedData = newData;
15808
17083
  gui.container.findChild('.map-layers').classed('symbol-hit', nonEmpty);
15809
- if (active) {
17084
+ if (active && !suppressChangeEvent) {
15810
17085
  triggerChangeEvent();
15811
17086
  }
15812
17087
  }
15813
17088
 
17089
+ function clearSelectionSilently() {
17090
+ suppressChangeEvent = true;
17091
+ self.clearSelection();
17092
+ suppressChangeEvent = false;
17093
+ }
17094
+
15814
17095
  // check if an event is used in the current interaction mode
15815
17096
  function eventIsEnabled(type) {
15816
17097
  var mode = interactionMode();
@@ -16850,7 +18131,9 @@
16850
18131
 
16851
18132
  function useBoxZoom() {
16852
18133
  var mode = gui.getMode();
16853
- return !'selection_tool,box_tool,rectangle_tool,drawing_tool'.includes(mode);
18134
+ var interactionMode = gui.interaction && gui.interaction.getMode();
18135
+ return !'selection_tool,box_tool,rectangle_tool,drawing_tool'.includes(mode) &&
18136
+ !'label_style,point_style,line_style,polygon_style'.includes(interactionMode);
16854
18137
  }
16855
18138
 
16856
18139
  function getBoxData(e1, e2) {
@@ -17472,6 +18755,30 @@
17472
18755
  strokeWidth: 2.5
17473
18756
  }
17474
18757
  },
18758
+ styleSelectionStyles = {
18759
+ polygon: {
18760
+ fillColor: null,
18761
+ strokeColor: 'rgb(255, 198, 0)',
18762
+ strokeOpacity: 0.38,
18763
+ strokeWidth: 5,
18764
+ strokeOverlay: true,
18765
+ batchOverlay: true
18766
+ }, polyline: {
18767
+ fillColor: null,
18768
+ strokeColor: 'rgb(255, 198, 0)',
18769
+ strokeOpacity: 0.38,
18770
+ strokeWidth: 5,
18771
+ strokeOverlay: true,
18772
+ batchOverlay: true
18773
+ }, point: {
18774
+ fillColor: null,
18775
+ strokeColor: 'rgb(255, 198, 0)',
18776
+ strokeOpacity: 0.38,
18777
+ strokeWidth: 5,
18778
+ strokeOverlay: true,
18779
+ batchOverlay: true
18780
+ }
18781
+ },
17475
18782
  // currently not used -- selection hover is not styled
17476
18783
  selectionHoverStyles = {
17477
18784
  polygon: {
@@ -17522,11 +18829,22 @@
17522
18829
  return [lyr];
17523
18830
  }
17524
18831
  layers = [];
18832
+ if (styleOpts.interactionMode == 'line_style' || styleOpts.interactionMode == 'polygon_style' ||
18833
+ styleOpts.interactionMode == 'point_style') {
18834
+ ids = hitData.ids || [];
18835
+ if (ids.length > 0) {
18836
+ lyr = getOverlayLayer(activeLyr, ids);
18837
+ outlineStyle = getSelectionStyle(displayLyr.geometry_type, styleOpts);
18838
+ lyr.gui.style = getOverlayStyle(activeLyr, ids, outlineStyle);
18839
+ layers.push(lyr);
18840
+ }
18841
+ return layers;
18842
+ }
17525
18843
  // layer containing selected features, not including hover or pinned feature
17526
18844
  ids = utils$1.difference(hitData.ids || [], [hitData.id]);
17527
18845
  if (ids.length > 0) {
17528
18846
  lyr = getOverlayLayer(activeLyr, ids);
17529
- outlineStyle = selectionStyles[displayLyr.geometry_type];
18847
+ outlineStyle = getSelectionStyle(displayLyr.geometry_type, styleOpts);
17530
18848
  lyr.gui.style = getOverlayStyle(activeLyr, ids, outlineStyle);
17531
18849
  layers.push(lyr);
17532
18850
  }
@@ -17558,32 +18876,58 @@
17558
18876
  return;
17559
18877
  }
17560
18878
  var idx = ids[i];
18879
+ if (outlineStyle.strokeOverlay) {
18880
+ delete style.strokeWidth;
18881
+ delete style.strokeColor;
18882
+ }
17561
18883
  baseStyler(style, idx);
17562
18884
  if (geomType == 'point') {
17563
- if (style.radius > 0) {
17564
- style.radius += 0.8;
17565
- if (style.strokeWidth > 0) {
17566
- style.strokeColor = outlineStyle.dotColor;
18885
+ if (outlineStyle.strokeOverlay) {
18886
+ if (style.radius > 0) {
18887
+ style.radius += (style.strokeWidth || 0) / 2 + outlineStyle.strokeWidth / 2;
18888
+ }
18889
+ style.fillColor = null;
18890
+ style.strokeColor = outlineStyle.strokeColor;
18891
+ style.strokeWidth = outlineStyle.strokeWidth;
18892
+ } else {
18893
+ if (style.radius > 0) {
18894
+ style.radius += 0.8;
18895
+ if (style.strokeWidth > 0) {
18896
+ style.strokeColor = outlineStyle.dotColor;
18897
+ }
17567
18898
  }
18899
+ style.fillColor = outlineStyle.dotColor;
17568
18900
  }
17569
- style.fillColor = outlineStyle.dotColor;
17570
18901
  } else {
17571
18902
  style.strokeColor = outlineStyle.strokeColor;
17572
18903
  style.fillColor = outlineStyle.fillColor;
17573
- style.strokeWidth = Math.max(outlineStyle.strokeWidth, style.strokeWidth || 0);
18904
+ style.strokeWidth = outlineStyle.strokeOverlay ?
18905
+ (style.strokeWidth || 0) + outlineStyle.strokeWidth :
18906
+ Math.max(outlineStyle.strokeWidth, style.strokeWidth || 0);
17574
18907
  }
17575
18908
  style.opacity = 1;
17576
18909
  style.fillOpacity = 1;
17577
- style.strokeOpacity = 1;
18910
+ style.strokeOpacity = outlineStyle.strokeOpacity >= 0 ? outlineStyle.strokeOpacity : 1;
17578
18911
  };
17579
18912
  var style = Object.assign({}, baseStyle, {ids, overlay: true, type: 'styled', styler});
17580
- if (baseStyle.dotSize > 0) {
18913
+ if (outlineStyle.batchOverlay) {
18914
+ style.batchOverlay = true;
18915
+ }
18916
+ if (baseStyle.dotSize > 0 && outlineStyle.dotSize > 0) {
17581
18917
  // dot size must be a static property (not applied by styler function)
17582
18918
  style.dotSize = outlineStyle.dotSize;
17583
18919
  }
17584
18920
  return style;
17585
18921
  }
17586
18922
 
18923
+ function getSelectionStyle(geomType, styleOpts) {
18924
+ if (styleOpts.interactionMode == 'line_style' || styleOpts.interactionMode == 'polygon_style' ||
18925
+ styleOpts.interactionMode == 'point_style') {
18926
+ return styleSelectionStyles[geomType] || selectionStyles[geomType];
18927
+ }
18928
+ return selectionStyles[geomType];
18929
+ }
18930
+
17587
18931
  // style for vertex edit mode
17588
18932
  function getVertexStyle(o) {
17589
18933
  return {
@@ -19914,21 +21258,24 @@
19914
21258
  var draw = getShapePencil(arcs, _ext);
19915
21259
  var key, item, shp;
19916
21260
  var styler = style.styler || null;
21261
+ var drawStyle = styler ? utils$1.defaults({}, style) : style;
19917
21262
  for (var i=0; i<shapes.length; i++) {
19918
21263
  shp = shapes[i];
19919
21264
  if (!shp || filter && !filter(i)) continue;
19920
21265
  if (styler) {
19921
- styler(style, i);
21266
+ styler(drawStyle, i);
19922
21267
  }
19923
- if (style.overlay || style.opacity < 1 || style.fillOpacity < 1 || style.strokeOpacity < 1 || style.fillEffect) {
21268
+ if (!drawStyle.batchOverlay && (drawStyle.overlay ||
21269
+ drawStyle.opacity < 1 || drawStyle.fillOpacity < 1 ||
21270
+ drawStyle.strokeOpacity < 1 || drawStyle.fillEffect)) {
19924
21271
  // don't batch shapes with opacity, in case they overlap
19925
- drawPaths([shp], startPath, draw, style);
21272
+ drawPaths([shp], startPath, draw, drawStyle);
19926
21273
  continue;
19927
21274
  }
19928
- key = getStyleKey(style);
21275
+ key = getStyleKey(drawStyle);
19929
21276
  if (key in styleIndex === false) {
19930
21277
  styleIndex[key] = {
19931
- style: utils$1.defaults({}, style),
21278
+ style: utils$1.defaults({}, drawStyle),
19932
21279
  shapes: []
19933
21280
  };
19934
21281
  }
@@ -22294,6 +23641,8 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
22294
23641
  gui.interaction = new InteractionMode(gui);
22295
23642
  gui.editToolbar = new EditToolbar(gui);
22296
23643
  gui.labelTool = new LabelTool(gui);
23644
+ gui.layerStyleTool = new LayerStyleTool(gui);
23645
+ gui.pointStyleTool = new PointStyleTool(gui);
22297
23646
 
22298
23647
  gui.showProgressMessage = function(msg) {
22299
23648
  if (!gui.progressMessage) {