mapshaper 0.7.17 → 0.7.18

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,1625 @@
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
+ }
13908
14874
 
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
- }
14875
+ var savedStylesKey = 'layer_style_presets';
14876
+ var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'fill', 'fill-opacity'];
14877
+
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
+ });
15056
+ }
15057
+
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
+ });
14032
15074
  }
14033
15075
 
14034
- function getIdsWhereExpression(ids) {
14035
- return '[' + ids.join(',') + '].indexOf($.id)>-1';
15076
+ function updateControls() {
15077
+ var geom = targetLayer && targetLayer.geometry_type;
15078
+ var manualIds = getSelectionIds();
15079
+ if (!targetLayer) return;
15080
+ title.text(geom == 'polygon' ? 'Polygon styles' : 'Line styles');
15081
+ updateEditingStatus(manualIds.length);
15082
+ strokeControl.row.show();
15083
+ fillControl.row.classed('hidden', geom != 'polygon');
15084
+ updateColorControl(strokeControl);
15085
+ updateColorControl(fillControl);
15086
+ updateStrokeWidthControl();
15087
+ updateNumberControl(strokeOpacityInput, 'stroke-opacity', 1, formatOpacityPct);
15088
+ updateNumberControl(fillOpacityInput, 'fill-opacity', 1, formatOpacityPct);
15089
+ randomFillBtn.classed('hidden', geom != 'polygon');
15090
+ presetControl.render();
15091
+ updateSavedStyleControls();
14036
15092
  }
14037
15093
 
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;
15094
+ function updateColorControl(control) {
15095
+ var value = getCommonStyleValue(control.field);
15096
+ setColorControlValue(control, value);
15097
+ if (isHexColor(value)) {
15098
+ control.picker.setColor(value);
15099
+ } else {
15100
+ control.picker.hide();
14042
15101
  }
14043
- return true;
14044
15102
  }
14045
15103
 
14046
- function recordCreateLabelsCommand(expr, copyLyr) {
14047
- var cmd = '';
14048
- if (!gui.session) return;
14049
- if (copyLyr) {
14050
- cmd += '-filter true + name=' + internal.formatOptionValue(copyLyr.name) + ' ';
14051
- }
14052
- cmd += '-style label-text=' + quoteCommandValue(expr);
14053
- gui.session.consoleCommands(cmd);
15104
+ function setColorControlValue(control, value) {
15105
+ control.input.node().value = value || '';
15106
+ control.chit.css('background-color', isHexColor(value) ? value : 'transparent');
14054
15107
  }
14055
15108
 
14056
- function quoteCommandValue(str) {
14057
- return "'" + String(str).replace(/'/g, "\\'") + "'";
15109
+ function updateNumberControl(input, field, defaultValue, formatter) {
15110
+ var value = getCommonStyleValue(field);
15111
+ input.node().value = formatter(value === '' || value === undefined || value === null ? defaultValue : value);
14058
15112
  }
14059
15113
 
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);
15114
+ function updateStrokeWidthControl() {
15115
+ var value = getCommonStyleValue('stroke-width');
15116
+ strokeWidthClickText.value(formatNumberValue(value === '' || value === undefined || value === null ? getDefaultStrokeWidth() : value));
15117
+ }
15118
+
15119
+ function nudgeStrokeWidth(direction) {
15120
+ var value = Number(getCommonStyleValue('stroke-width'));
15121
+ if (!isFinite(value)) value = getDefaultStrokeWidth();
15122
+ applyStrokeWidthStyle(getNextStrokeWidth(value, direction));
15123
+ }
15124
+
15125
+ function getNextStrokeWidth(value, direction) {
15126
+ var baseSteps = [0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4];
15127
+ var i;
15128
+ if (direction > 0) {
15129
+ for (i=0; i<baseSteps.length; i++) {
15130
+ if (value < baseSteps[i]) return baseSteps[i];
15131
+ }
15132
+ return Math.floor(value) + 1;
14066
15133
  }
14067
- hit.clearSelection();
14068
- if (ids.length > 0) {
14069
- hit.addSelectionIds(ids);
15134
+ for (i=baseSteps.length - 1; i>=0; i--) {
15135
+ if (value > baseSteps[i]) return baseSteps[i];
14070
15136
  }
14071
- updateSelectionDisplay();
14072
- updateControls();
15137
+ return baseSteps[0];
14073
15138
  }
14074
15139
 
14075
- function clearSelection() {
14076
- if (hit) hit.clearSelection();
14077
- updateSelectionDisplay();
14078
- updateControls();
15140
+ function applyLayerStyle(field, value) {
15141
+ runStyleCommand([[field, value]]);
14079
15142
  }
14080
15143
 
14081
- function getActiveLayer() {
14082
- var active = gui.model.getActiveLayer();
14083
- return active && active.layer;
15144
+ function applyColorControlStyle(control, color) {
15145
+ var styles = [[control.field, color]];
15146
+ if (control.field == 'stroke' && strokeWidthIsUnsetForTargets()) {
15147
+ styles.push(['stroke-width', 1]);
15148
+ }
15149
+ runStyleCommand(styles);
14084
15150
  }
14085
15151
 
14086
- function getActiveTable() {
14087
- var lyr = getActiveLayer();
14088
- return lyr && lyr.data || null;
15152
+ function applyStrokeWidthStyle(value) {
15153
+ var styles = [['stroke-width', value]];
15154
+ if (value > 0 && styleFieldIsUnsetForTargets('stroke')) {
15155
+ styles.push(['stroke', strokeControl.defaultColor]);
15156
+ }
15157
+ runStyleCommand(styles);
14089
15158
  }
14090
15159
 
14091
- function getSelectionIds() {
14092
- return hit ? hit.getSelectionIds() : [];
15160
+ function runStyleCommand(styles, opts) {
15161
+ var parts = ['-style'];
15162
+ var ids = getTargetIds();
15163
+ if (!gui.console || !targetLayer || ids.length === 0) return;
15164
+ if (!opts || !opts.preservePreset) {
15165
+ presetControl.clearSelection();
15166
+ }
15167
+ styles.forEach(function(style) {
15168
+ parts.push(style[0] + '=' + quoteCommandValue(style[1]));
15169
+ });
15170
+ addTargetOption(parts);
15171
+ if (ids.length < internal.getFeatureCount(targetLayer)) {
15172
+ parts.push('ids=' + ids.join(','));
15173
+ }
15174
+ runCommand(parts.join(' '), 'Style layer');
14093
15175
  }
14094
15176
 
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 : '');
15177
+ function applyDefaultLineStyle() {
15178
+ var styles = [];
15179
+ if (!targetLayer || targetLayer.geometry_type != 'polyline') return;
15180
+ if (styleFieldIsUnsetForTargets('stroke-width')) {
15181
+ styles.push(['stroke-width', 1]);
15182
+ }
15183
+ if (styles.length > 0) {
15184
+ runStyleCommand(styles);
15185
+ }
14117
15186
  }
14118
15187
 
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
- });
15188
+ function applyRandomFillColors() {
15189
+ var cmd = '-classify colors=random non-adjacent';
15190
+ if (!gui.console || !targetLayer || targetLayer.geometry_type != 'polygon') return;
15191
+ if (getActiveLayer() != targetLayer) {
15192
+ cmd += ' target=' + internal.formatOptionValue(internal.getLayerTargetId(gui.model, targetLayer));
15193
+ }
15194
+ presetControl.clearSelection();
15195
+ runCommand(cmd, 'Random fill colors');
14125
15196
  }
14126
15197
 
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);
15198
+ function runCommand(cmd, title) {
15199
+ runGuiEditCommand(gui, cmd, {
15200
+ title: title,
15201
+ onDone: updateControls
14132
15202
  });
14133
15203
  }
14134
15204
 
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
- });
15205
+ function applyStyleObject(style) {
15206
+ var styles = [];
15207
+ styleFields.forEach(function(field) {
15208
+ if (field in style) {
15209
+ styles.push([field, style[field]]);
15210
+ }
15211
+ });
15212
+ if (styles.length > 0) {
15213
+ runStyleCommand(styles, {preservePreset: true});
14143
15214
  }
14144
- fontStyleSelect.node().disabled = disabled;
14145
- fontStyleSelect.node().value = fontStyleVal && fontWeightVal ?
14146
- fontStyleVal + '|' + fontWeightVal : '';
14147
15215
  }
14148
15216
 
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
15217
+ function getCurrentStyle() {
15218
+ var style = {};
15219
+ addStyleValue(style, 'stroke', getControlValue(strokeControl.input));
15220
+ addStyleValue(style, 'stroke-width', parsePositiveNumber(strokeWidthClickText.value()));
15221
+ addStyleValue(style, 'stroke-opacity', parseOpacityValue(strokeOpacityInput.node().value));
15222
+ if (targetLayer && targetLayer.geometry_type == 'polygon') {
15223
+ addStyleValue(style, 'fill', getControlValue(fillControl.input));
15224
+ addStyleValue(style, 'fill-opacity', parseOpacityValue(fillOpacityInput.node().value));
14157
15225
  }
14158
- if (isHexColor(colorVal)) {
14159
- setPickerColor(hexToHsb(colorVal));
14160
- } else {
14161
- hideColorPicker();
15226
+ return style;
15227
+ }
15228
+
15229
+ function addStyleValue(style, field, value) {
15230
+ if (value || value === 0) {
15231
+ style[field] = value;
14162
15232
  }
14163
15233
  }
14164
15234
 
14165
- function updateCssControl(cssVal) {
14166
- cssInput.node().disabled = getSelectionIds().length === 0;
14167
- cssInput.node().value = cssVal || '';
15235
+ function getControlValue(input) {
15236
+ return input.node().value.trim();
14168
15237
  }
14169
15238
 
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
- });
15239
+ function updateSavedStyleControls() {
15240
+ presetControl.update();
14176
15241
  }
14177
15242
 
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
- });
15243
+ function getStyleType() {
15244
+ return targetLayer && targetLayer.geometry_type == 'polygon' ? 'polygon' : 'line';
14184
15245
  }
14185
15246
 
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
- });
15247
+ function getSortKey(item) {
15248
+ return (item.type || '') + '\t' + String(item.name || '').toLowerCase() + '\t' + (item.id || '');
14192
15249
  }
14193
15250
 
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 '';
15251
+ function clearLayerStyle() {
15252
+ var parts = ['-style clear'];
15253
+ if (!gui.console || !targetLayer) return;
15254
+ presetControl.clearSelection();
15255
+ addTargetOption(parts);
15256
+ runCommand(parts.join(' '), 'Clear style');
15257
+ }
15258
+
15259
+ function addTargetOption(parts) {
15260
+ if (getActiveLayer() != targetLayer) {
15261
+ parts.push('target=' + internal.formatOptionValue(internal.getLayerTargetId(gui.model, targetLayer)));
15262
+ }
15263
+ }
15264
+
15265
+ function getCommonStyleValue(field, idsArg) {
15266
+ var records = targetLayer && targetLayer.data && targetLayer.data.getRecords();
15267
+ var ids;
15268
+ var value, val;
15269
+ if (!records) return '';
15270
+ ids = idsArg || getTargetIds();
15271
+ if (ids.length === 0) return '';
14199
15272
  for (var i=0; i<ids.length; i++) {
14200
15273
  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
15274
  if (i === 0) {
14211
15275
  value = val;
14212
15276
  } else if (val != value) {
14213
15277
  return '';
14214
15278
  }
14215
15279
  }
14216
- return hasValue || opts && opts.useDefault ? value : '';
15280
+ return value;
15281
+ }
15282
+
15283
+ function strokeWidthIsUnsetForTargets() {
15284
+ return styleFieldIsUnsetForTargets('stroke-width');
15285
+ }
15286
+
15287
+ function styleFieldIsUnsetForTargets(field) {
15288
+ var records = targetLayer && targetLayer.data && targetLayer.data.getRecords();
15289
+ var ids = getTargetIds();
15290
+ var val;
15291
+ if (!records) return true;
15292
+ for (var i=0; i<ids.length; i++) {
15293
+ val = records[ids[i]] && records[ids[i]][field];
15294
+ if (val !== undefined && val !== null && val !== '') {
15295
+ return false;
15296
+ }
15297
+ }
15298
+ return ids.length > 0;
15299
+ }
15300
+
15301
+ function getDefaultStrokeWidth() {
15302
+ return targetLayer && targetLayer.geometry_type == 'polyline' ? 1 : 0;
15303
+ }
15304
+
15305
+ function getAllFeatureIds(lyr) {
15306
+ var ids = [];
15307
+ for (var i=0, n=internal.getFeatureCount(lyr); i<n; i++) {
15308
+ ids.push(i);
15309
+ }
15310
+ return ids;
14217
15311
  }
14218
15312
 
14219
- function applyFont(fontName) {
14220
- applyStyleFields([fontField], function(rec) {
14221
- rec[fontField] = fontName;
14222
- });
15313
+ function getSelectionIds() {
15314
+ return hit ? hit.getSelectionIds() : [];
14223
15315
  }
14224
15316
 
14225
- function nudgeFontSize(delta) {
15317
+ function getTargetIds() {
14226
15318
  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
- });
15319
+ if (!targetLayer) return [];
15320
+ return ids.length > 0 ? ids : getAllFeatureIds(targetLayer);
14233
15321
  }
14234
15322
 
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
- });
15323
+ function clearSelection() {
15324
+ if (hit) hit.clearSelection();
15325
+ updateControls();
14242
15326
  }
14243
15327
 
14244
- function applyLabelColor(color) {
14245
- applyStyleFields([fillField], function(rec) {
14246
- rec[fillField] = color;
14247
- });
15328
+ function updateEditingStatus(count) {
15329
+ editingStatus.text(count > 0 ? 'Editing: ' + count + ' selected' : 'Editing: all');
15330
+ clearLink.classed('hidden', count === 0);
14248
15331
  }
14249
15332
 
14250
- function applyInlineCss(css) {
14251
- applyStyleFields([cssField], function(rec) {
14252
- rec[cssField] = css || undefined;
14253
- });
15333
+ function getActiveLayer() {
15334
+ var active = gui.model.getActiveLayer();
15335
+ return active && active.layer;
14254
15336
  }
14255
15337
 
14256
- function saveCurrentStyle() {
14257
- openSaveStylePopup();
15338
+ function closePanel() {
15339
+ turnOff();
15340
+ if (gui.interaction.getMode() == 'line_style' || gui.interaction.getMode() == 'polygon_style') {
15341
+ gui.interaction.turnOff();
15342
+ }
14258
15343
  }
14259
15344
 
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
- });
15345
+ function layerCanBeStyled(lyr) {
15346
+ return !!(lyr && (lyr.geometry_type == 'polyline' || lyr.geometry_type == 'polygon'));
14280
15347
  }
14281
15348
 
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
- }
15349
+ function modeMatchesActiveLayer(mode) {
15350
+ var lyr = getActiveLayer();
15351
+ return mode == 'line_style' && lyr && lyr.geometry_type == 'polyline' ||
15352
+ mode == 'polygon_style' && lyr && lyr.geometry_type == 'polygon';
14303
15353
  }
14304
15354
 
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();
15355
+ function modelSelectLayer(lyr, dataset) {
15356
+ if (lyr) lyr.hidden = false;
15357
+ gui.model.selectLayer(lyr, dataset);
14316
15358
  }
14317
15359
 
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);
15360
+ function parseOpacityValue(str) {
15361
+ var pct = Number(String(str).replace('%', '').trim());
15362
+ if (!isFinite(pct)) return null;
15363
+ return Math.max(0, Math.min(100, pct)) / 100;
14324
15364
  }
14325
15365
 
14326
- function getSavedStyles() {
14327
- var styles = GUI.getSavedValue(savedStylesKey);
14328
- return Array.isArray(styles) ? styles : [];
15366
+ function parsePositiveNumber(str) {
15367
+ var val = Number(String(str).trim());
15368
+ return isFinite(val) && val >= 0 ? val : null;
14329
15369
  }
14330
15370
 
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();
15371
+ function formatOpacityPct(val) {
15372
+ val = val === '' || val === undefined || val === null ? 1 : Number(val);
15373
+ return isFinite(val) ? Math.round(Math.max(0, Math.min(1, val)) * 100) + '%' : '';
14341
15374
  }
14342
15375
 
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;
15376
+ function formatNumberValue(val) {
15377
+ val = Number(val);
15378
+ return isFinite(val) ? String(val) : '';
15379
+ }
15380
+
15381
+ function quoteCommandValue(str) {
15382
+ return "'" + String(str).replace(/'/g, "\\'") + "'";
15383
+ }
15384
+ }
15385
+
15386
+ var defaultCircleRadius = 0;
15387
+ var defaultCreatedCircleRadius = 3;
15388
+ var defaultCircleFill = '#000000';
15389
+
15390
+ function PointStyleTool(gui) {
15391
+ var parent = gui.container.findChild('.mshp-main-map');
15392
+ var panel = El('div').addClass('label-style-panel point-style-panel rollover').appendTo(parent).hide();
15393
+ var title, noteSection, labelsSection, circlesSection, symbolsSection, labelNoteSection;
15394
+ var circleSectionLabel;
15395
+ var symbolNote;
15396
+ var createFieldSelect, createExprInput, createCopyCheckbox, createLabelsBtn;
15397
+ var editingRow, editingStatus, clearLink;
15398
+ var createCirclesRow;
15399
+ var circleControlRows = [];
15400
+ var circleRadiusClickText, circleFillControl, circleStrokeControl;
15401
+ var circleFillOpacityInput, circleStrokeOpacityInput, circleStrokeWidthClickText;
15402
+ var targetLayer = null;
15403
+ var hit = null;
15404
+
15405
+ initPanel();
15406
+
15407
+ this.open = function(lyr, dataset) {
15408
+ if (!layerCanBeStyled(lyr)) return;
15409
+ if (!gui.map.isActiveLayer(lyr)) {
15410
+ modelSelectLayer(lyr, dataset);
14351
15411
  }
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));
15412
+ gui.interaction.setMode('point_style');
15413
+ };
15414
+
15415
+ gui.on('interaction_mode_change', function(e) {
15416
+ if (modeMatchesActiveLayer(e.mode)) {
15417
+ turnOn();
15418
+ } else {
15419
+ turnOff();
14359
15420
  }
14360
- return style;
14361
- }
15421
+ });
14362
15422
 
14363
- function addStyleValue(style, field, value) {
14364
- if (value || value === 0) {
14365
- style[field] = value;
15423
+ gui.model.on('update', function() {
15424
+ if (panel.visible() && !modeMatchesActiveLayer(gui.interaction.getMode())) {
15425
+ gui.interaction.turnOff();
15426
+ } else if (panel.visible()) {
15427
+ updateControls();
14366
15428
  }
15429
+ });
15430
+
15431
+ hit = gui.map.getHitControl && gui.map.getHitControl();
15432
+ if (hit) {
15433
+ hit.on('change', function(e) {
15434
+ if (e.mode == 'point_style') {
15435
+ updateControls();
15436
+ }
15437
+ });
14367
15438
  }
14368
15439
 
14369
- function getNumericControlValue(el) {
14370
- var value = Number(el.text());
14371
- return isFinite(value) && value > 0 ? value : null;
15440
+ function initPanel() {
15441
+ var header = El('div').addClass('label-style-panel-title').appendTo(panel);
15442
+ title = El('span').appendTo(header).text('Point symbols');
15443
+ El('button').addClass('label-style-close').appendTo(header).text('×').on('click', closePanel);
15444
+
15445
+ initNoteSections();
15446
+ initCreateLabelsSection();
15447
+ initCreateCirclesSection();
15448
+ initSymbolsSection();
14372
15449
  }
14373
15450
 
14374
- function getSelectedLabelPosition() {
14375
- var out = '';
14376
- labelPositions.forEach(function(pos) {
14377
- if (posBtns[pos].hasClass('selected')) out = pos;
14378
- });
14379
- return out;
15451
+ function initNoteSections() {
15452
+ noteSection = El('div').addClass('point-style-section point-style-note-section').appendTo(panel);
15453
+ El('div').addClass('point-style-note').appendTo(noteSection).text('This layer contains unstyled points.');
15454
+
15455
+ labelNoteSection = El('div').addClass('point-style-section point-label-note-section').appendTo(panel);
15456
+ El('div').addClass('point-style-note').appendTo(labelNoteSection)
15457
+ .text('This layer is rendered as labels. Use the label style tool to edit label styles.');
14380
15458
  }
14381
15459
 
14382
- function getSelectedIcon() {
14383
- var out = '';
14384
- iconTypes.forEach(function(icon) {
14385
- if (iconBtns[icon.name].hasClass('selected')) out = icon.name;
15460
+ function initCreateLabelsSection() {
15461
+ labelsSection = El('div').addClass('point-style-section').appendTo(panel);
15462
+ El('div').addClass('label-style-row-label').appendTo(labelsSection).text('Labels');
15463
+
15464
+ var fieldRow = El('label').addClass('label-style-row').appendTo(labelsSection);
15465
+ El('span').appendTo(fieldRow).text('Label field');
15466
+ createFieldSelect = El('select').appendTo(fieldRow).on('change', function() {
15467
+ var field = createFieldSelect.node().value;
15468
+ if (field) {
15469
+ createExprInput.node().value = getFieldExpression(field);
15470
+ }
15471
+ updateCreateLabelsButton();
14386
15472
  });
14387
- return out;
14388
- }
14389
15473
 
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);
15474
+ var exprRow = El('label').addClass('label-style-row label-create-expression-row').appendTo(labelsSection);
15475
+ El('span').appendTo(exprRow).text('or expression');
15476
+ createExprInput = El('input')
15477
+ .attr('type', 'text')
15478
+ .appendTo(exprRow)
15479
+ .on('input', updateCreateLabelsButton)
15480
+ .on('change', updateCreateLabelsButton);
15481
+
15482
+ var btnRow = El('div').addClass('label-style-row point-create-labels-row').appendTo(labelsSection);
15483
+ createLabelsBtn = El('button').appendTo(btnRow).text('Create').on('click', createLabels);
15484
+ var copyLabel = El('label').addClass('point-create-copy-label').appendTo(btnRow);
15485
+ createCopyCheckbox = El('input').attr('type', 'checkbox').appendTo(copyLabel);
15486
+ El('span').appendTo(copyLabel).text('as new layer');
15487
+ }
15488
+
15489
+ function initCreateCirclesSection() {
15490
+ circlesSection = El('div').addClass('point-style-section point-circle-section').appendTo(panel);
15491
+ circleSectionLabel = El('div').addClass('label-style-row-label').appendTo(circlesSection).text('Circles');
15492
+ El('div').addClass('point-style-note point-circle-note').appendTo(circlesSection)
15493
+ .text('Use the -style command in the console to create proportional circles.');
15494
+
15495
+ createCirclesRow = El('div').addClass('label-style-row point-create-circles-row').appendTo(circlesSection);
15496
+ El('button').appendTo(createCirclesRow).text('Create').on('click', createSimpleCircles);
15497
+ El('span').appendTo(createCirclesRow).text('simple circles');
15498
+
15499
+ editingRow = El('div').addClass('label-style-row label-style-selection-row point-style-selection-row').appendTo(circlesSection);
15500
+ editingStatus = El('span').addClass('label-editing-status').appendTo(editingRow);
15501
+ clearLink = El('span').addClass('label-editing-clear colored-text').appendTo(editingRow).text('deselect').on('click', clearSelection);
15502
+
15503
+ var fillRow = El('div').addClass('label-style-row point-symbol-row').appendTo(circlesSection);
15504
+ circleFillControl = addCircleColorControl(fillRow, 'Fill');
15505
+ circleFillOpacityInput = addCircleNumberControl(fillRow, 'Opacity', '100%');
15506
+ circleControlRows.push(fillRow);
15507
+
15508
+ var strokeRow = El('div').addClass('label-style-row point-symbol-row').appendTo(circlesSection);
15509
+ circleStrokeControl = addCircleColorControl(strokeRow, 'Stroke');
15510
+ circleStrokeOpacityInput = addCircleNumberControl(strokeRow, 'Opacity', '100%');
15511
+ circleControlRows.push(strokeRow);
15512
+
15513
+ var sizeRow = El('div').addClass('label-style-row point-symbol-size-row').appendTo(circlesSection);
15514
+ addCircleStepperControl(sizeRow, 'Stroke width', function() {
15515
+ return getCircleStrokeWidth();
15516
+ }, function(direction) {
15517
+ setCircleStrokeWidth(getNextStrokeWidth(getCircleStrokeWidth(), direction));
15518
+ applyCircleStyles();
15519
+ }, function(value) {
15520
+ setCircleStrokeWidth(value);
15521
+ applyCircleStyles();
15522
+ }, function(clickText) {
15523
+ circleStrokeWidthClickText = clickText;
15524
+ });
15525
+
15526
+ addCircleStepperControl(sizeRow, 'Radius', function() {
15527
+ return getCircleRadiusForNudge();
15528
+ }, function(direction) {
15529
+ setCircleRadius(getNextCircleRadius(getCircleRadiusForNudge(), direction));
15530
+ applyCircleStyles();
15531
+ }, function(value) {
15532
+ setCircleRadius(value);
15533
+ applyCircleStyles();
15534
+ }, function(clickText, text) {
15535
+ circleRadiusClickText = clickText;
15536
+ text.addClass('label-icon-size-value');
15537
+ });
15538
+ circleControlRows.push(sizeRow);
15539
+ }
15540
+
15541
+ function initSymbolsSection() {
15542
+ symbolsSection = El('div').addClass('point-style-section point-symbol-info-section').appendTo(panel);
15543
+ El('div').addClass('label-style-row-label').appendTo(symbolsSection).text('Symbols');
15544
+ symbolNote = El('div').addClass('point-style-note').appendTo(symbolsSection)
15545
+ .text('Use the -symbols command in the console to create arrows and other symbols.');
15546
+ }
15547
+
15548
+ function addCircleColorControl(row, label) {
15549
+ var colorCell = El('div').addClass('point-symbol-color-cell label-color-row').appendTo(row);
15550
+ var control = {};
15551
+ El('span').appendTo(colorCell).text(label);
15552
+ control.chit = makePanelButton(colorCell, '', function() {
15553
+ control.picker.toggle();
15554
+ }).addClass('label-color-chit');
15555
+ control.input = El('input').attr('type', 'text').appendTo(colorCell).on('change', function() {
15556
+ var color = control.input.node().value.trim();
15557
+ if (isHexColor(color)) {
15558
+ control.picker.setColor(color);
14398
15559
  }
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']);
15560
+ applyCircleStyles();
15561
+ });
15562
+ control.picker = new ColorPicker(colorCell, {
15563
+ presetRows: layerColorPresetRows,
15564
+ onPreview: function(hex) {
15565
+ setCircleColor(control, hex);
15566
+ },
15567
+ onChange: function(hex) {
15568
+ setCircleColor(control, hex);
15569
+ applyCircleStyles();
15570
+ }
15571
+ });
15572
+ setCircleColor(control, '');
15573
+ return control;
15574
+ }
15575
+
15576
+ function addCircleStepperControl(row, label, getValue, nudgeValue, setValue, capture) {
15577
+ var control;
15578
+ var buttonRow;
15579
+ var text;
15580
+ var clickText;
15581
+ control = El('div').addClass('point-symbol-stepper-control').appendTo(row);
15582
+ El('span').appendTo(control).text(label);
15583
+ buttonRow = El('div').addClass('layer-stepper-control').appendTo(control);
15584
+ makePanelButton(buttonRow, '−', function() {
15585
+ nudgeValue(-1);
15586
+ });
15587
+ text = El('span').addClass('layer-stroke-width-value').appendTo(buttonRow);
15588
+ clickText = new ClickText2(text);
15589
+ clickText.on('change', function() {
15590
+ var value = parsePositiveNumber(clickText.value());
15591
+ if (value === null) {
15592
+ clickText.value(formatNumberValue(getValue()));
15593
+ return;
14409
15594
  }
15595
+ setValue(value);
14410
15596
  });
15597
+ makePanelButton(buttonRow, '+', function() {
15598
+ nudgeValue(1);
15599
+ });
15600
+ capture(clickText, text);
14411
15601
  }
14412
15602
 
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
- };
15603
+ function addCircleNumberControl(row, label, value) {
15604
+ var control = El('label').addClass('layer-number-control').appendTo(row);
15605
+ var input;
15606
+ El('span').appendTo(control).text(label);
15607
+ input = El('input').attr('type', 'text').appendTo(control).on('change', applyCircleStyles);
15608
+ input.node().value = value;
15609
+ return input;
14420
15610
  }
14421
15611
 
14422
- function applyLabelPosition(pos) {
14423
- applyStyleFields(labelPositionFields, function(rec) {
14424
- internal.setLabelPositionStyle(rec, pos);
14425
- });
15612
+ function makePanelButton(parent, label, action) {
15613
+ return El('div')
15614
+ .addClass('label-panel-btn')
15615
+ .attr('role', 'button')
15616
+ .attr('tabindex', '0')
15617
+ .appendTo(parent)
15618
+ .text(label)
15619
+ .on('click', function(e) {
15620
+ action(e);
15621
+ })
15622
+ .on('keydown', function(e) {
15623
+ if (e.key == 'Enter' || e.key == ' ') {
15624
+ e.preventDefault();
15625
+ action(e);
15626
+ }
15627
+ });
14426
15628
  }
14427
15629
 
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
- });
15630
+ function turnOn() {
15631
+ targetLayer = getActiveLayer();
15632
+ if (getPointRepresentation() == 'label' && gui.labelTool && gui.labelTool.open) {
15633
+ gui.labelTool.open(targetLayer);
15634
+ targetLayer = null;
15635
+ return;
15636
+ }
15637
+ renderCreateFields();
15638
+ updateControls();
15639
+ panel.show();
14440
15640
  }
14441
15641
 
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
- });
15642
+ function turnOff() {
15643
+ panel.hide();
15644
+ circleFillControl.picker.hide();
15645
+ circleStrokeControl.picker.hide();
15646
+ targetLayer = null;
14453
15647
  }
14454
15648
 
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;
15649
+ function closePanel() {
15650
+ turnOff();
15651
+ if (gui.interaction.getMode() == 'point_style') {
15652
+ gui.interaction.turnOff();
15653
+ }
14459
15654
  }
14460
15655
 
14461
- function isHexColor(str) {
14462
- return /^#[0-9a-f]{6}$/i.test(str);
15656
+ function updateControls() {
15657
+ var representation = getPointRepresentation();
15658
+ updateCreateLabelsButton();
15659
+ title.text(representation == 'circle' ? 'Circle styles' : 'Point symbols');
15660
+ updateEditingStatus(getSelectionIds().length);
15661
+ toggleSection(noteSection, representation == 'unstyled');
15662
+ toggleSection(labelsSection, representation == 'unstyled');
15663
+ toggleSection(circlesSection, representation == 'unstyled' || representation == 'circle');
15664
+ toggleSection(symbolsSection, representation == 'unstyled' || representation == 'svg-symbol');
15665
+ toggleSection(labelNoteSection, representation == 'label');
15666
+ updateSectionBorders([noteSection, labelNoteSection, labelsSection, circlesSection, symbolsSection]);
15667
+ symbolNote.text(representation == 'svg-symbol' ?
15668
+ 'This layer uses SVG symbols. Use the -symbols command in the console to create arrows and other symbols.' :
15669
+ 'Use the -symbols command in the console to create arrows and other symbols.');
15670
+ updateCircleSection(representation);
15671
+ updateCircleControls(representation);
15672
+ }
15673
+
15674
+ function toggleSection(section, visible) {
15675
+ if (visible) {
15676
+ section.show();
15677
+ } else {
15678
+ section.hide();
15679
+ }
14463
15680
  }
14464
15681
 
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);
15682
+ function updateSectionBorders(sections) {
15683
+ var foundFirst = false;
15684
+ sections.forEach(function(section) {
15685
+ var visible = section.visible();
15686
+ section.classed('point-style-first-visible', visible && !foundFirst);
15687
+ if (visible) foundFirst = true;
14481
15688
  });
14482
- hueCanvas.on('mousedown', function(e) {
14483
- startCanvasDrag(e, updateHueFromEvent);
15689
+ }
15690
+
15691
+ function updateCircleSection(representation) {
15692
+ var showCreate = representation == 'unstyled';
15693
+ circleSectionLabel.classed('hidden', !showCreate);
15694
+ createCirclesRow.classed('hidden', !showCreate);
15695
+ editingRow.classed('hidden', representation != 'circle');
15696
+ circlesSection.findChild('.point-circle-note').classed('hidden', !showCreate);
15697
+ circleControlRows.forEach(function(row) {
15698
+ row.classed('hidden', showCreate);
15699
+ });
15700
+ }
15701
+
15702
+ function updateCircleControls(representation) {
15703
+ var radius = getCommonValue('r');
15704
+ var fill = getCommonValue('fill');
15705
+ var stroke = getCommonValue('stroke');
15706
+ var fillOpacity = getCommonValue('fill-opacity');
15707
+ var strokeOpacity = getCommonValue('stroke-opacity');
15708
+ var strokeWidth = getCommonValue('stroke-width');
15709
+ setCircleRadius(radius);
15710
+ setCircleColor(circleFillControl, fill);
15711
+ setCircleColor(circleStrokeControl, stroke);
15712
+ circleFillOpacityInput.node().value = formatOpacityPct(fillOpacity === '' ? 1 : fillOpacity);
15713
+ circleStrokeOpacityInput.node().value = formatOpacityPct(strokeOpacity === '' ? 1 : strokeOpacity);
15714
+ setCircleStrokeWidth(strokeWidth === '' ? 0 : strokeWidth);
15715
+ if (representation != 'circle' && representation != 'unstyled') {
15716
+ setCircleRadius(defaultCircleRadius);
15717
+ setCircleColor(circleFillControl, '');
15718
+ setCircleColor(circleStrokeControl, '');
15719
+ circleFillOpacityInput.node().value = '100%';
15720
+ circleStrokeOpacityInput.node().value = '100%';
15721
+ setCircleStrokeWidth(0);
15722
+ }
15723
+ }
15724
+
15725
+ function renderCreateFields() {
15726
+ var lyr = getActiveLayer();
15727
+ var records = lyr && lyr.data ? lyr.data.getRecords() : [];
15728
+ var fields = lyr && lyr.data ? lyr.data.getFields().filter(function(field) {
15729
+ return internal.getColumnType(field, records) == 'string';
15730
+ }) : [];
15731
+ var value = createFieldSelect.node().value;
15732
+ createFieldSelect.empty();
15733
+ El('option').attr('value', '').appendTo(createFieldSelect).text('');
15734
+ fields.forEach(function(field) {
15735
+ El('option').attr('value', field).appendTo(createFieldSelect).text(field);
14484
15736
  });
14485
- setPickerColor(pickerColor);
15737
+ createFieldSelect.node().disabled = fields.length === 0;
15738
+ createFieldSelect.node().value = fields.indexOf(value) > -1 ? value : '';
14486
15739
  }
14487
15740
 
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
- });
15741
+ function updateCreateLabelsButton() {
15742
+ createLabelsBtn.node().disabled = createExprInput.node().value.trim() === '';
14506
15743
  }
14507
15744
 
14508
- function toggleColorPicker() {
14509
- if (colorPicker.visible()) {
14510
- hideColorPicker();
14511
- } else {
14512
- colorPicker.show();
14513
- drawColorPicker();
14514
- }
15745
+ function createLabels() {
15746
+ var expr = createExprInput.node().value.trim();
15747
+ var cmd;
15748
+ if (!expr || !gui.console) return;
15749
+ cmd = createCopyCheckbox.node().checked ?
15750
+ '-filter true + name=labels -style label-text=' + quoteCommandValue(expr) :
15751
+ '-style label-text=' + quoteCommandValue(expr);
15752
+ runGuiEditCommand(gui, cmd, {
15753
+ title: 'Create labels',
15754
+ onSuccess: openLabelStyles
15755
+ });
14515
15756
  }
14516
15757
 
14517
- function hideColorPicker() {
14518
- colorPicker.hide();
15758
+ function openLabelStyles() {
15759
+ var active = gui.model.getActiveLayer();
15760
+ if (active && active.layer && internal.layerHasLabels(active.layer) &&
15761
+ gui.labelTool && gui.labelTool.open) {
15762
+ gui.labelTool.open(active.layer, active.dataset);
15763
+ } else {
15764
+ updateControls();
15765
+ }
15766
+ }
15767
+
15768
+ function createSimpleCircles() {
15769
+ runCommand('-style r=' + defaultCreatedCircleRadius +
15770
+ ' fill=' + quoteCommandValue(defaultCircleFill) +
15771
+ ' fill-opacity=1 stroke-opacity=1 stroke-width=0', 'Create circles');
15772
+ }
15773
+
15774
+ function applyCircleStyles() {
15775
+ var representation = getPointRepresentation();
15776
+ var radius = getCircleRadius();
15777
+ var fill = circleFillControl.input.node().value.trim();
15778
+ var fillOpacity = parseOpacityValue(circleFillOpacityInput.node().value);
15779
+ var stroke = circleStrokeControl.input.node().value.trim();
15780
+ var strokeOpacity = parseOpacityValue(circleStrokeOpacityInput.node().value);
15781
+ var strokeWidth = getCircleStrokeWidth();
15782
+ var args;
15783
+ if (!gui.console || !(representation == 'unstyled' || representation == 'circle') ||
15784
+ (radius !== null && !(radius >= 0)) || fillOpacity === null || strokeOpacity === null || strokeWidth === null) return;
15785
+ if (radius !== null && radius > 0 && !fill && !stroke && styleFieldIsUnset('fill') && styleFieldIsUnset('stroke')) {
15786
+ fill = defaultCircleFill;
15787
+ setCircleColor(circleFillControl, fill);
15788
+ }
15789
+ args = [
15790
+ 'fill-opacity=' + fillOpacity,
15791
+ 'stroke-opacity=' + strokeOpacity,
15792
+ 'stroke-width=' + strokeWidth
15793
+ ];
15794
+ if (radius !== null) args.unshift('r=' + radius);
15795
+ if (fill) args.push('fill=' + quoteCommandValue(fill));
15796
+ if (stroke) args.push('stroke=' + quoteCommandValue(stroke));
15797
+ runStyleCommand(args, 'Create circles');
14519
15798
  }
14520
15799
 
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();
15800
+ function runStyleCommand(args, title) {
15801
+ var ids = getTargetIds();
15802
+ var parts = ['-style'].concat(args);
15803
+ if (!targetLayer || ids.length === 0) return;
15804
+ addTargetOption(parts);
15805
+ if (ids.length < internal.getFeatureCount(targetLayer)) {
15806
+ parts.push('ids=' + ids.join(','));
14538
15807
  }
15808
+ runCommand(parts.join(' '), title);
14539
15809
  }
14540
15810
 
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
15811
+ function runCommand(cmd, title) {
15812
+ runGuiEditCommand(gui, cmd, {
15813
+ title: title,
15814
+ onSuccess: updateControls
14547
15815
  });
14548
15816
  }
14549
15817
 
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
- });
15818
+ function getCircleRadius() {
15819
+ var str = String(circleRadiusClickText.value()).trim();
15820
+ var radius = Number(str);
15821
+ if (str === '') return null;
15822
+ return isFinite(radius) && radius >= 0 ? radius : defaultCircleRadius;
14557
15823
  }
14558
15824
 
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
- };
15825
+ function getCircleRadiusForNudge() {
15826
+ var radius = getCircleRadius();
15827
+ return radius === null ? getMostCommonNumberValue('r', defaultCircleRadius) : radius;
14565
15828
  }
14566
15829
 
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();
15830
+ function setCircleRadius(value) {
15831
+ circleRadiusClickText.value(value === '' ? '' : formatNumberValue(value));
14575
15832
  }
14576
15833
 
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);
15834
+ function getCircleStrokeWidth() {
15835
+ var width = Number(circleStrokeWidthClickText.value());
15836
+ return isFinite(width) && width >= 0 ? width : 0;
14584
15837
  }
14585
15838
 
14586
- function drawColorPicker() {
14587
- if (!sbCanvas) return;
14588
- drawSaturationBrightnessCanvas();
14589
- drawHueCanvas();
15839
+ function setCircleStrokeWidth(value) {
15840
+ circleStrokeWidthClickText.value(formatNumberValue(value));
14590
15841
  }
14591
15842
 
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;
15843
+ function getNextStrokeWidth(value, direction) {
15844
+ var baseSteps = [0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4];
15845
+ var i;
15846
+ if (direction > 0) {
15847
+ for (i=0; i<baseSteps.length; i++) {
15848
+ if (value < baseSteps[i]) return baseSteps[i];
14608
15849
  }
15850
+ return Math.floor(value) + 1;
14609
15851
  }
14610
- ctx.putImageData(image, 0, 0);
14611
- positionMarker(sbMarker, pickerColor.s, 255 - pickerColor.b, pickerColor);
15852
+ for (i=baseSteps.length - 1; i>=0; i--) {
15853
+ if (value > baseSteps[i]) return baseSteps[i];
15854
+ }
15855
+ return baseSteps[0];
14612
15856
  }
14613
15857
 
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;
15858
+ function getNextCircleRadius(value, direction) {
15859
+ var baseSteps = [0, 0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
15860
+ var i;
15861
+ if (direction > 0) {
15862
+ for (i=0; i<baseSteps.length; i++) {
15863
+ if (value < baseSteps[i]) return baseSteps[i];
14626
15864
  }
15865
+ return Math.floor(value) + 1;
14627
15866
  }
14628
- ctx.putImageData(image, 0, 0);
14629
- positionMarker(hueMarker, pickerColor.h, 9, {h: pickerColor.h, s: 255, b: 255});
15867
+ for (i=baseSteps.length - 1; i>=0; i--) {
15868
+ if (value > baseSteps[i]) return baseSteps[i];
15869
+ }
15870
+ return baseSteps[0];
14630
15871
  }
14631
15872
 
14632
- function commitPickerColor() {
14633
- applyLabelColor(hsbToHex(pickerColor));
15873
+ function getPointRepresentation() {
15874
+ var lyr = targetLayer || getActiveLayer();
15875
+ var table = lyr && lyr.data;
15876
+ if (table && table.fieldExists('svg-symbol')) return 'svg-symbol';
15877
+ if (table && table.fieldExists('label-text')) return 'label';
15878
+ if (table && table.fieldExists('r')) return 'circle';
15879
+ return 'unstyled';
14634
15880
  }
14635
15881
 
14636
- function closePickerColor() {
14637
- hideColorPicker();
15882
+ function getCommonValue(field) {
15883
+ var table = targetLayer && targetLayer.data;
15884
+ var records = table && table.getRecords();
15885
+ var ids = getTargetIds();
15886
+ var value, val;
15887
+ if (!records || ids.length === 0) return '';
15888
+ for (var i=0; i<ids.length; i++) {
15889
+ val = records[ids[i]] && records[ids[i]][field];
15890
+ if (i === 0) {
15891
+ value = val;
15892
+ } else if (val != value) {
15893
+ return '';
15894
+ }
15895
+ }
15896
+ return value === undefined || value === null ? '' : value;
14638
15897
  }
14639
15898
 
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';
15899
+ function getMostCommonNumberValue(field, defaultValue) {
15900
+ var table = targetLayer && targetLayer.data;
15901
+ var records = table && table.getRecords();
15902
+ var ids = getTargetIds();
15903
+ var counts = {};
15904
+ var bestValue = null;
15905
+ var bestCount = 0;
15906
+ var val, key;
15907
+ if (!records || ids.length === 0) return defaultValue;
15908
+ for (var i=0; i<ids.length; i++) {
15909
+ val = Number(records[ids[i]] && records[ids[i]][field]);
15910
+ if (!isFinite(val)) continue;
15911
+ key = String(val);
15912
+ counts[key] = (counts[key] || 0) + 1;
15913
+ if (counts[key] > bestCount) {
15914
+ bestCount = counts[key];
15915
+ bestValue = val;
15916
+ }
15917
+ }
15918
+ return bestValue === null ? defaultValue : bestValue;
14643
15919
  }
14644
15920
 
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>');
15921
+ function styleFieldIsUnset(field) {
15922
+ var table = targetLayer && targetLayer.data;
15923
+ var records = table && table.getRecords();
15924
+ var ids = getTargetIds();
15925
+ var val;
15926
+ if (!records || ids.length === 0) return true;
15927
+ for (var i=0; i<ids.length; i++) {
15928
+ val = records[ids[i]] && records[ids[i]][field];
15929
+ if (val !== undefined && val !== null && val !== '') return false;
15930
+ }
15931
+ return true;
14647
15932
  }
14648
15933
 
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));
15934
+ function addTargetOption(parts) {
15935
+ if (getActiveLayer() != targetLayer) {
15936
+ parts.push('target=' + internal.formatOptionValue(internal.getLayerTargetId(gui.model, targetLayer)));
15937
+ }
14658
15938
  }
14659
15939
 
14660
- function hexToHsb(hex) {
14661
- var r = parseInt(hex.substr(1, 2), 16);
14662
- var g = parseInt(hex.substr(3, 2), 16);
14663
- var b = parseInt(hex.substr(5, 2), 16);
14664
- var max = Math.max(r, g, b);
14665
- var min = Math.min(r, g, b);
14666
- var d = max - min;
14667
- var h = 0;
14668
- if (d) {
14669
- if (max == r) h = ((g - b) / d) % 6;
14670
- else if (max == g) h = (b - r) / d + 2;
14671
- else h = (r - g) / d + 4;
14672
- h *= 60;
14673
- if (h < 0) h += 360;
14674
- }
14675
- return {
14676
- h: Math.round(h / 360 * 255),
14677
- s: Math.round(max === 0 ? 0 : d / max * 255),
14678
- b: max
14679
- };
15940
+ function getSelectionIds() {
15941
+ return hit ? hit.getSelectionIds() : [];
14680
15942
  }
14681
15943
 
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('');
15944
+ function getTargetIds() {
15945
+ var ids = getSelectionIds();
15946
+ if (!targetLayer) return [];
15947
+ return ids.length > 0 ? ids : getAllFeatureIds(targetLayer);
14687
15948
  }
14688
15949
 
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
- };
15950
+ function getAllFeatureIds(lyr) {
15951
+ var ids = [];
15952
+ for (var i=0, n=internal.getFeatureCount(lyr); i<n; i++) {
15953
+ ids.push(i);
15954
+ }
15955
+ return ids;
14706
15956
  }
14707
15957
 
14708
- function byteToPct(val) {
14709
- return Math.round(val / 255 * 100);
15958
+ function clearSelection() {
15959
+ if (hit) hit.clearSelection();
15960
+ updateControls();
14710
15961
  }
14711
15962
 
14712
- function pctToByte(val) {
14713
- return Math.round(clamp(val, 0, 100) / 100 * 255);
15963
+ function updateEditingStatus(count) {
15964
+ editingStatus.text(count > 0 ? 'Editing: ' + count + ' selected' : 'Editing: all');
15965
+ clearLink.classed('hidden', count === 0);
14714
15966
  }
14715
15967
 
14716
- function byteToDegrees(val) {
14717
- return Math.round(val / 255 * 360);
15968
+ function setCircleColor(control, color) {
15969
+ control.input.node().value = color;
15970
+ control.chit.css('background-color', isHexColor(color) ? color : 'transparent');
14718
15971
  }
14719
15972
 
14720
- function degreesToByte(val) {
14721
- return Math.round(clamp(val, 0, 360) / 360 * 255);
15973
+ function parseOpacityValue(str) {
15974
+ var pct = Number(String(str).replace('%', '').trim());
15975
+ if (!isFinite(pct)) return null;
15976
+ return Math.max(0, Math.min(100, pct)) / 100;
14722
15977
  }
14723
15978
 
14724
- function parseNumberField(str) {
14725
- return Number(String(str).replace(/[°%]/g, '').trim());
15979
+ function parsePositiveNumber(str) {
15980
+ if (String(str).trim() === '') return null;
15981
+ var val = Number(String(str).trim());
15982
+ return isFinite(val) && val >= 0 ? val : null;
14726
15983
  }
14727
15984
 
14728
- function clamp(val, min, max) {
14729
- return isFinite(val) ? Math.max(min, Math.min(max, val)) : min;
15985
+ function formatOpacityPct(val) {
15986
+ val = Number(val);
15987
+ return isFinite(val) ? Math.round(Math.max(0, Math.min(1, val)) * 100) + '%' : '';
14730
15988
  }
14731
15989
 
14732
- function applyStyleFields(fields, updateRecord) {
14733
- var table = getActiveTable();
14734
- var ids = getSelectionIds();
14735
- var lyr = getActiveLayer();
14736
- var hasNewFields;
14737
- if (!table || ids.length === 0) return;
14738
- hasNewFields = fields.some(function(field) {
14739
- return !table.fieldExists(field);
14740
- });
14741
- gui.dispatchEvent('data_preupdate', {ids: ids});
14742
- if (hasNewFields) {
14743
- table.captureSchemaBefore({operation: 'label-style', fields: fields});
14744
- } else {
14745
- table.captureFieldsBefore(fields, {operation: 'label-style'});
14746
- }
14747
- updateRecords(ids, table, updateRecord);
14748
- if (hasNewFields) {
14749
- table.markSchemaChanged({operation: 'label-style'});
14750
- } else {
14751
- table.markFieldsChanged(fields, {operation: 'label-style'});
14752
- }
14753
- gui.dispatchEvent('data_postupdate', {ids: ids});
14754
- gui.model.updated({style: true, same_table: true});
14755
- updatePendingStyleHistory(lyr, ids, fields);
14756
- setTimeout(updateSelectionDisplay, 0);
14757
- updateControls();
14758
- updateSelectionDisplay();
15990
+ function formatNumberValue(val) {
15991
+ val = Number(val);
15992
+ return isFinite(val) ? String(val) : '';
14759
15993
  }
14760
15994
 
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
- });
15995
+ function getFieldExpression(field) {
15996
+ return 'd[' + JSON.stringify(field) + ']';
14768
15997
  }
14769
15998
 
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
- });
15999
+ function getActiveLayer() {
16000
+ var active = gui.model.getActiveLayer();
16001
+ return active && active.layer;
14778
16002
  }
14779
16003
 
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
- });
16004
+ function modelSelectLayer(lyr, dataset) {
16005
+ if (lyr) lyr.hidden = false;
16006
+ gui.model.selectLayer(lyr, dataset);
14787
16007
  }
14788
16008
 
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');
16009
+ function modeMatchesActiveLayer(mode) {
16010
+ var lyr = getActiveLayer();
16011
+ return mode == 'point_style' && layerCanBeStyled(lyr);
16012
+ }
16013
+
16014
+ function layerCanBeStyled(lyr) {
16015
+ return !!(lyr && lyr.geometry_type == 'point');
14795
16016
  }
14796
16017
 
16018
+ function quoteCommandValue(str) {
16019
+ return "'" + String(str).replace(/'/g, "\\'") + "'";
16020
+ }
14797
16021
  }
14798
16022
 
14799
16023
  function Model(gui) {
@@ -15438,6 +16662,7 @@
15438
16662
  var targetLayer;
15439
16663
  var hitTest;
15440
16664
  var pinnedOn; // used in multi-edit mode (selection) for toggling pinning behavior
16665
+ var suppressChangeEvent = false;
15441
16666
 
15442
16667
  // event priority is higher than navigation, so stopping propagation disables
15443
16668
  // pan navigation
@@ -15514,7 +16739,8 @@
15514
16739
 
15515
16740
  function selectable() {
15516
16741
  var mode = interactionMode();
15517
- return mode == 'selection' || mode == 'label_style';
16742
+ return mode == 'selection' || mode == 'label_style' ||
16743
+ mode == 'point_style' || mode == 'line_style' || mode == 'polygon_style';
15518
16744
  }
15519
16745
 
15520
16746
  function pinnable() {
@@ -15532,7 +16758,8 @@
15532
16758
  var mode = interactionMode();
15533
16759
  // click used to pin popup and select features
15534
16760
  return mode == 'data' || mode == 'info' || mode == 'selection' ||
15535
- mode == 'label_style' || mode == 'rectangles' || mode == 'edit_points';
16761
+ mode == 'label_style' || mode == 'point_style' || mode == 'line_style' || mode == 'polygon_style' ||
16762
+ mode == 'rectangles' || mode == 'edit_points';
15536
16763
  }
15537
16764
 
15538
16765
  self.getHitId = function() {
@@ -15656,12 +16883,13 @@
15656
16883
  // make sure popup is unpinned and turned off when switching editing modes
15657
16884
  // (some modes do not support pinning)
15658
16885
  gui.on('interaction_mode_change', function(e) {
15659
- self.clearSelection();
16886
+ clearSelectionSilently();
15660
16887
  if (gui.interaction.modeUsesHitDetection(e.mode)) {
15661
16888
  turnOn(e.mode);
15662
16889
  } else {
15663
16890
  turnOff();
15664
16891
  }
16892
+ gui.dispatchEvent('map-needs-refresh');
15665
16893
  });
15666
16894
 
15667
16895
  gui.on('undo_redo_pre', function() {
@@ -15685,7 +16913,7 @@
15685
16913
 
15686
16914
  // TODO: move pinning to inspection control?
15687
16915
  if (clickable()) {
15688
- updateSelectionState(convertClickDataToSelectionData(hitTest(e)));
16916
+ updateSelectionState(convertClickDataToSelectionData(hitTest(e), e));
15689
16917
  }
15690
16918
 
15691
16919
  if (pinned && interactionMode() == 'edit_points') {
@@ -15727,7 +16955,7 @@
15727
16955
 
15728
16956
  // Translates feature hit data from a mouse click into feature selection data
15729
16957
  // hitData: hit data from a mouse click
15730
- function convertClickDataToSelectionData(hitData) {
16958
+ function convertClickDataToSelectionData(hitData, e) {
15731
16959
  // mergeCurrentState(hitData);
15732
16960
  // TOGGLE pinned state under some conditions
15733
16961
  var id = hitData.ids.length > 0 ? hitData.ids[0] : -1;
@@ -15744,13 +16972,32 @@
15744
16972
  }
15745
16973
  if (selectable()) {
15746
16974
  if (id > -1) {
15747
- selectionIds = toggleId(id, selectionIds);
16975
+ selectionIds = styleSelectionMode() ?
16976
+ selectStyleFeature(id, e) :
16977
+ toggleId(id, selectionIds);
15748
16978
  }
15749
16979
  hitData.ids = selectionIds;
15750
16980
  }
15751
16981
  return hitData;
15752
16982
  }
15753
16983
 
16984
+ function styleSelectionMode() {
16985
+ var mode = interactionMode();
16986
+ return mode == 'label_style' || mode == 'point_style' ||
16987
+ mode == 'line_style' || mode == 'polygon_style';
16988
+ }
16989
+
16990
+ function selectStyleFeature(id, e) {
16991
+ return eventUsesAdditiveSelection(e) ?
16992
+ toggleId(id, selectionIds) :
16993
+ [id];
16994
+ }
16995
+
16996
+ function eventUsesAdditiveSelection(e) {
16997
+ var original = e && e.originalEvent || e;
16998
+ return !!(original && original.shiftKey);
16999
+ }
17000
+
15754
17001
  function mergeSelectionModeHoverData(hitData) {
15755
17002
  if (hitData.ids.length === 0 || selectionIds.includes(hitData.ids[0])) {
15756
17003
  hitData.ids = selectionIds;
@@ -15806,11 +17053,17 @@
15806
17053
 
15807
17054
  storedData = newData;
15808
17055
  gui.container.findChild('.map-layers').classed('symbol-hit', nonEmpty);
15809
- if (active) {
17056
+ if (active && !suppressChangeEvent) {
15810
17057
  triggerChangeEvent();
15811
17058
  }
15812
17059
  }
15813
17060
 
17061
+ function clearSelectionSilently() {
17062
+ suppressChangeEvent = true;
17063
+ self.clearSelection();
17064
+ suppressChangeEvent = false;
17065
+ }
17066
+
15814
17067
  // check if an event is used in the current interaction mode
15815
17068
  function eventIsEnabled(type) {
15816
17069
  var mode = interactionMode();
@@ -16850,7 +18103,9 @@
16850
18103
 
16851
18104
  function useBoxZoom() {
16852
18105
  var mode = gui.getMode();
16853
- return !'selection_tool,box_tool,rectangle_tool,drawing_tool'.includes(mode);
18106
+ var interactionMode = gui.interaction && gui.interaction.getMode();
18107
+ return !'selection_tool,box_tool,rectangle_tool,drawing_tool'.includes(mode) &&
18108
+ !'label_style,point_style,line_style,polygon_style'.includes(interactionMode);
16854
18109
  }
16855
18110
 
16856
18111
  function getBoxData(e1, e2) {
@@ -17472,6 +18727,30 @@
17472
18727
  strokeWidth: 2.5
17473
18728
  }
17474
18729
  },
18730
+ styleSelectionStyles = {
18731
+ polygon: {
18732
+ fillColor: null,
18733
+ strokeColor: 'rgb(255, 198, 0)',
18734
+ strokeOpacity: 0.38,
18735
+ strokeWidth: 5,
18736
+ strokeOverlay: true,
18737
+ batchOverlay: true
18738
+ }, polyline: {
18739
+ fillColor: null,
18740
+ strokeColor: 'rgb(255, 198, 0)',
18741
+ strokeOpacity: 0.38,
18742
+ strokeWidth: 5,
18743
+ strokeOverlay: true,
18744
+ batchOverlay: true
18745
+ }, point: {
18746
+ fillColor: null,
18747
+ strokeColor: 'rgb(255, 198, 0)',
18748
+ strokeOpacity: 0.38,
18749
+ strokeWidth: 5,
18750
+ strokeOverlay: true,
18751
+ batchOverlay: true
18752
+ }
18753
+ },
17475
18754
  // currently not used -- selection hover is not styled
17476
18755
  selectionHoverStyles = {
17477
18756
  polygon: {
@@ -17522,11 +18801,22 @@
17522
18801
  return [lyr];
17523
18802
  }
17524
18803
  layers = [];
18804
+ if (styleOpts.interactionMode == 'line_style' || styleOpts.interactionMode == 'polygon_style' ||
18805
+ styleOpts.interactionMode == 'point_style') {
18806
+ ids = hitData.ids || [];
18807
+ if (ids.length > 0) {
18808
+ lyr = getOverlayLayer(activeLyr, ids);
18809
+ outlineStyle = getSelectionStyle(displayLyr.geometry_type, styleOpts);
18810
+ lyr.gui.style = getOverlayStyle(activeLyr, ids, outlineStyle);
18811
+ layers.push(lyr);
18812
+ }
18813
+ return layers;
18814
+ }
17525
18815
  // layer containing selected features, not including hover or pinned feature
17526
18816
  ids = utils$1.difference(hitData.ids || [], [hitData.id]);
17527
18817
  if (ids.length > 0) {
17528
18818
  lyr = getOverlayLayer(activeLyr, ids);
17529
- outlineStyle = selectionStyles[displayLyr.geometry_type];
18819
+ outlineStyle = getSelectionStyle(displayLyr.geometry_type, styleOpts);
17530
18820
  lyr.gui.style = getOverlayStyle(activeLyr, ids, outlineStyle);
17531
18821
  layers.push(lyr);
17532
18822
  }
@@ -17558,32 +18848,58 @@
17558
18848
  return;
17559
18849
  }
17560
18850
  var idx = ids[i];
18851
+ if (outlineStyle.strokeOverlay) {
18852
+ delete style.strokeWidth;
18853
+ delete style.strokeColor;
18854
+ }
17561
18855
  baseStyler(style, idx);
17562
18856
  if (geomType == 'point') {
17563
- if (style.radius > 0) {
17564
- style.radius += 0.8;
17565
- if (style.strokeWidth > 0) {
17566
- style.strokeColor = outlineStyle.dotColor;
18857
+ if (outlineStyle.strokeOverlay) {
18858
+ if (style.radius > 0) {
18859
+ style.radius += (style.strokeWidth || 0) / 2 + outlineStyle.strokeWidth / 2;
18860
+ }
18861
+ style.fillColor = null;
18862
+ style.strokeColor = outlineStyle.strokeColor;
18863
+ style.strokeWidth = outlineStyle.strokeWidth;
18864
+ } else {
18865
+ if (style.radius > 0) {
18866
+ style.radius += 0.8;
18867
+ if (style.strokeWidth > 0) {
18868
+ style.strokeColor = outlineStyle.dotColor;
18869
+ }
17567
18870
  }
18871
+ style.fillColor = outlineStyle.dotColor;
17568
18872
  }
17569
- style.fillColor = outlineStyle.dotColor;
17570
18873
  } else {
17571
18874
  style.strokeColor = outlineStyle.strokeColor;
17572
18875
  style.fillColor = outlineStyle.fillColor;
17573
- style.strokeWidth = Math.max(outlineStyle.strokeWidth, style.strokeWidth || 0);
18876
+ style.strokeWidth = outlineStyle.strokeOverlay ?
18877
+ (style.strokeWidth || 0) + outlineStyle.strokeWidth :
18878
+ Math.max(outlineStyle.strokeWidth, style.strokeWidth || 0);
17574
18879
  }
17575
18880
  style.opacity = 1;
17576
18881
  style.fillOpacity = 1;
17577
- style.strokeOpacity = 1;
18882
+ style.strokeOpacity = outlineStyle.strokeOpacity >= 0 ? outlineStyle.strokeOpacity : 1;
17578
18883
  };
17579
18884
  var style = Object.assign({}, baseStyle, {ids, overlay: true, type: 'styled', styler});
17580
- if (baseStyle.dotSize > 0) {
18885
+ if (outlineStyle.batchOverlay) {
18886
+ style.batchOverlay = true;
18887
+ }
18888
+ if (baseStyle.dotSize > 0 && outlineStyle.dotSize > 0) {
17581
18889
  // dot size must be a static property (not applied by styler function)
17582
18890
  style.dotSize = outlineStyle.dotSize;
17583
18891
  }
17584
18892
  return style;
17585
18893
  }
17586
18894
 
18895
+ function getSelectionStyle(geomType, styleOpts) {
18896
+ if (styleOpts.interactionMode == 'line_style' || styleOpts.interactionMode == 'polygon_style' ||
18897
+ styleOpts.interactionMode == 'point_style') {
18898
+ return styleSelectionStyles[geomType] || selectionStyles[geomType];
18899
+ }
18900
+ return selectionStyles[geomType];
18901
+ }
18902
+
17587
18903
  // style for vertex edit mode
17588
18904
  function getVertexStyle(o) {
17589
18905
  return {
@@ -19914,21 +21230,24 @@
19914
21230
  var draw = getShapePencil(arcs, _ext);
19915
21231
  var key, item, shp;
19916
21232
  var styler = style.styler || null;
21233
+ var drawStyle = styler ? utils$1.defaults({}, style) : style;
19917
21234
  for (var i=0; i<shapes.length; i++) {
19918
21235
  shp = shapes[i];
19919
21236
  if (!shp || filter && !filter(i)) continue;
19920
21237
  if (styler) {
19921
- styler(style, i);
21238
+ styler(drawStyle, i);
19922
21239
  }
19923
- if (style.overlay || style.opacity < 1 || style.fillOpacity < 1 || style.strokeOpacity < 1 || style.fillEffect) {
21240
+ if (!drawStyle.batchOverlay && (drawStyle.overlay ||
21241
+ drawStyle.opacity < 1 || drawStyle.fillOpacity < 1 ||
21242
+ drawStyle.strokeOpacity < 1 || drawStyle.fillEffect)) {
19924
21243
  // don't batch shapes with opacity, in case they overlap
19925
- drawPaths([shp], startPath, draw, style);
21244
+ drawPaths([shp], startPath, draw, drawStyle);
19926
21245
  continue;
19927
21246
  }
19928
- key = getStyleKey(style);
21247
+ key = getStyleKey(drawStyle);
19929
21248
  if (key in styleIndex === false) {
19930
21249
  styleIndex[key] = {
19931
- style: utils$1.defaults({}, style),
21250
+ style: utils$1.defaults({}, drawStyle),
19932
21251
  shapes: []
19933
21252
  };
19934
21253
  }
@@ -22294,6 +23613,8 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
22294
23613
  gui.interaction = new InteractionMode(gui);
22295
23614
  gui.editToolbar = new EditToolbar(gui);
22296
23615
  gui.labelTool = new LabelTool(gui);
23616
+ gui.layerStyleTool = new LayerStyleTool(gui);
23617
+ gui.pointStyleTool = new PointStyleTool(gui);
22297
23618
 
22298
23619
  gui.showProgressMessage = function(msg) {
22299
23620
  if (!gui.progressMessage) {