@superrouteca/smap 1.0.2 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/smap.cjs CHANGED
@@ -1,9 +1,10 @@
1
1
  /* Generated by js/build.mjs from js/src (see SOURCE.json). Do not edit. */
2
2
  'use strict';
3
- /*! sMap Web SDK 1.0.2. Bundles maplibre-gl 6.16.0 (BSD-3-Clause), see vendor-licenses.txt. */
3
+ /*! sMap Web SDK 1.0.6. Bundles maplibre-gl 6.16.0 (BSD-3-Clause), see vendor-licenses.txt. */
4
4
 
5
5
  var smapCore = null;
6
6
  function smapCreateEngine() {
7
+ var SMAP_BASE_URL = (typeof document !== 'undefined' && document.baseURI) || (typeof location !== 'undefined' && location.href) || 'about:blank';
7
8
  var smapEngineExports = (() => {
8
9
  var __defProp = Object.defineProperty;
9
10
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -4599,7 +4600,7 @@ Use an identity property function instead: \`{ "type": "identity", "property": $
4599
4600
  }
4600
4601
  }
4601
4602
  function el() {
4602
- let e66 = "";
4603
+ let e66 = SMAP_BASE_URL;
4603
4604
  if (!/^https?:/.test(e66)) return ``;
4604
4605
  let t2 = e66.endsWith(`-dev.mjs`) ? `maplibre-gl-worker-dev.mjs` : `maplibre-gl-worker.mjs`, n2 = new URL(`./${t2}`, e66);
4605
4606
  return n2.searchParams.set(`v`, l), n2.href;
@@ -4622,7 +4623,7 @@ Use an identity property function instead: \`{ "type": "identity", "property": $
4622
4623
  return URL.createObjectURL(r2);
4623
4624
  }
4624
4625
  function il(e66) {
4625
- let t2 = new Blob([`import ${JSON.stringify(new URL(e66, "").href)}`], { type: `text/javascript` });
4626
+ let t2 = new Blob([`import ${JSON.stringify(new URL(e66, SMAP_BASE_URL).href)}`], { type: `text/javascript` });
4626
4627
  return URL.createObjectURL(t2);
4627
4628
  }
4628
4629
  async function al() {
@@ -26865,14 +26866,14 @@ function smapCreateSdk(engine) {
26865
26866
  */
26866
26867
  (function (root, factory) {
26867
26868
  if (typeof module === 'object' && module.exports) {
26868
- module.exports = factory(root.maplibregl || require('maplibre-gl'));
26869
+ module.exports = factory(root.maplibregl !== undefined ? root.maplibregl : require('maplibre-gl'));
26869
26870
  } else {
26870
26871
  root.sMap = factory(root.maplibregl);
26871
26872
  }
26872
26873
  })(typeof self !== 'undefined' ? self : this, function (engine) {
26873
26874
  'use strict';
26874
26875
 
26875
- var VERSION = '1.0.2';
26876
+ var VERSION = '1.0.6';
26876
26877
  var FLAVORS = ['light', 'white', 'grayscale', 'dark', 'black'];
26877
26878
  var BASEMAP_SOURCE = 'basemap';
26878
26879
  var BUILDINGS_LAYER = 'smap-buildings-3d';
@@ -26971,6 +26972,230 @@ function smapCreateSdk(engine) {
26971
26972
  };
26972
26973
  }
26973
26974
 
26975
+ // ------------------------------------------------------------------ popups
26976
+
26977
+ var POPUP_OPTIONS = ['className', 'maxWidth', 'minWidth', 'offset', 'anchor', 'closeButton', 'closeOnClick', 'focusAfterOpen', 'autoPan'];
26978
+ // Shown while a popup's content is on its way: three pulsing dots, no text.
26979
+ var POPUP_LOADING = '<div class="smap-popup-loading" role="status"><span></span><span></span><span></span></div>';
26980
+ var HTML_ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
26981
+
26982
+ function escapeHtml(value) {
26983
+ return String(value).replace(/[&<>"']/g, function (c) { return HTML_ESCAPES[c]; });
26984
+ }
26985
+
26986
+ /**
26987
+ * A popup function from a template: every {name} is replaced by the feature's property of that name, HTML-escaped
26988
+ * (empty when the feature has none). The template itself is trusted HTML. Compiled once; nothing runs until a click.
26989
+ * popupTemplate('<b>{name}</b><br>{address}')
26990
+ */
26991
+ function popupTemplate(template) {
26992
+ // Even entries are the template's own text, odd entries property names.
26993
+ var parts = String(template).split(/\{([^{}]+)\}/);
26994
+ for (var i = 1; i < parts.length; i += 2) { parts[i] = parts[i].trim(); }
26995
+ return function (properties) {
26996
+ var out = parts[0];
26997
+ for (var j = 1; j < parts.length; j += 2) {
26998
+ var v = properties ? properties[parts[j]] : undefined;
26999
+ out += (v === undefined || v === null ? '' : escapeHtml(v)) + parts[j + 1];
27000
+ }
27001
+ return out;
27002
+ };
27003
+ }
27004
+
27005
+ function isThenable(v) { return !!v && (typeof v === 'object' || typeof v === 'function') && typeof v.then === 'function'; }
27006
+
27007
+ function cssSize(v) { return typeof v === 'number' ? v + 'px' : v; }
27008
+
27009
+ // ------------------------------------------------------------------ drawn images (pins, numbers, badges, arrows)
27010
+
27011
+ // A style value: a constant, {property: 'name'} (the feature's property, `default` when it has none), or an engine
27012
+ // expression (an array).
27013
+ function valueExpr(v, fallback) {
27014
+ if (v === undefined || v === null) { return fallback; }
27015
+ if (Array.isArray(v)) { return v; }
27016
+ if (typeof v === 'object' && v.property) {
27017
+ return v['default'] !== undefined ? ['coalesce', ['get', v.property], v['default']] : ['get', v.property];
27018
+ }
27019
+ return v;
27020
+ }
27021
+
27022
+ // Image ids carry what the image shows; the map draws each one the first time a feature needs it (see _drawImage).
27023
+ var IMAGE_PREFIX = 'smap-img|';
27024
+ var SQRT1_2 = Math.SQRT1_2;
27025
+
27026
+ // The pin of Superroute's route map: a square with three rounded corners turned 45 degrees, its sharp corner the tip.
27027
+ // tipToHead: from the position up to the centre of the round part (where the number goes); badge: the badge's centre
27028
+ // from the position (right of the round part, as the page's badge sits on the turned pin's corner).
27029
+ function markerGeometry(shape, size) {
27030
+ var k = size / 32;
27031
+ if (shape === 'circle' || shape === 'vehicle') {
27032
+ return { k: k, tipToHead: 0, badge: [size * 0.36, -size * 0.36] };
27033
+ }
27034
+ var half = size * SQRT1_2; // the centre of the round part, above the tip
27035
+ return { k: k, tipToHead: half, badge: [14.1 * k, -half] };
27036
+ }
27037
+
27038
+ // A rounded rectangle path whose corners are ellipses: radii [[rx, ry] top-left, top-right, bottom-right, bottom-left].
27039
+ function roundedPath(ctx, x, y, w, h, r) {
27040
+ ctx.beginPath();
27041
+ ctx.moveTo(x + r[0][0], y);
27042
+ ctx.lineTo(x + w - r[1][0], y);
27043
+ if (r[1][0] && r[1][1]) { ctx.ellipse(x + w - r[1][0], y + r[1][1], r[1][0], r[1][1], 0, -Math.PI / 2, 0); }
27044
+ ctx.lineTo(x + w, y + h - r[2][1]);
27045
+ if (r[2][0] && r[2][1]) { ctx.ellipse(x + w - r[2][0], y + h - r[2][1], r[2][0], r[2][1], 0, 0, Math.PI / 2); }
27046
+ ctx.lineTo(x + r[3][0], y + h);
27047
+ if (r[3][0] && r[3][1]) { ctx.ellipse(x + r[3][0], y + h - r[3][1], r[3][0], r[3][1], 0, Math.PI / 2, Math.PI); }
27048
+ ctx.lineTo(x, y + r[0][1]);
27049
+ if (r[0][0] && r[0][1]) { ctx.ellipse(x + r[0][0], y + r[0][1], r[0][0], r[0][1], 0, Math.PI, Math.PI * 1.5); }
27050
+ ctx.closePath();
27051
+ }
27052
+
27053
+ var PIN_PAD = 6; // room for the shadow around a pin, in pixels
27054
+
27055
+ // Each draw function gets a 2D context already scaled to the pixel ratio and returns nothing; sizes are in pixels.
27056
+ var IMAGE_KINDS = {
27057
+ // pin|shape|fill|border|borderWidth|size (shape pin, circle, or vehicle: the route map's live position, a white
27058
+ // circle with a truck in the border colour)
27059
+ pin: function (args) {
27060
+ var shape = args[0], fill = args[1], border = args[2], bw = Number(args[3]), size = Number(args[4]);
27061
+ var g = markerGeometry(shape, size);
27062
+ var reach = shape === 'pin' ? g.tipToHead : size / 2; // farthest the shape goes from its centre
27063
+ var w = Math.ceil(2 * reach + 2 * PIN_PAD), h = w;
27064
+ return {
27065
+ width: w, height: h,
27066
+ draw: function (ctx, ratio) {
27067
+ var c = w / 2;
27068
+ if (shape === 'vehicle') { drawVehicle(ctx, ratio, c, size, border); return; }
27069
+ ctx.save();
27070
+ ctx.translate(c, c);
27071
+ var s = size / 2;
27072
+ var outer, inner;
27073
+ if (shape === 'circle') {
27074
+ outer = function () { ctx.beginPath(); ctx.arc(0, 0, s, 0, Math.PI * 2); };
27075
+ inner = function () { ctx.beginPath(); ctx.arc(0, 0, Math.max(0, s - bw), 0, Math.PI * 2); };
27076
+ ctx.shadowOffsetY = 3 * ratio;
27077
+ } else {
27078
+ ctx.rotate(Math.PI / 4);
27079
+ outer = function () { roundedPath(ctx, -s, -s, size, size, [[s, s], [s, size], [0, 0], [size, s]]); };
27080
+ var t = Math.max(0, s - bw), r2 = Math.max(0, size - bw);
27081
+ inner = function () { roundedPath(ctx, -s + bw, -s + bw, size - 2 * bw, size - 2 * bw, [[t, t], [t, r2], [0, 0], [r2, t]]); };
27082
+ // The page's box-shadow (0 3px) turns with the pin.
27083
+ ctx.shadowOffsetX = -3 * SQRT1_2 * ratio;
27084
+ ctx.shadowOffsetY = 3 * SQRT1_2 * ratio;
27085
+ }
27086
+ ctx.shadowColor = 'rgba(0,0,0,0.3)';
27087
+ ctx.shadowBlur = 8 * ratio;
27088
+ outer();
27089
+ ctx.fillStyle = border;
27090
+ ctx.fill();
27091
+ ctx.shadowColor = 'transparent';
27092
+ inner();
27093
+ ctx.fillStyle = fill;
27094
+ ctx.fill();
27095
+ ctx.restore();
27096
+ },
27097
+ };
27098
+ },
27099
+ // pinnum|shape|fill|border|borderWidth|size|textColor|text: a pin with its number drawn in, so a pin above covers a
27100
+ // pin below completely (used close up, where few pins are in view; further out the number is its own layer).
27101
+ pinnum: function (args, measure) {
27102
+ var pin = IMAGE_KINDS.pin(args.slice(0, 5));
27103
+ var size = Number(args[4]), color = args[5], text = args.slice(6).join('|');
27104
+ var px = 14 * size / 32, font = 'bold ' + px + 'px monospace';
27105
+ return {
27106
+ width: pin.width, height: pin.height,
27107
+ draw: function (ctx, ratio) {
27108
+ pin.draw(ctx, ratio);
27109
+ if (text === '') { return; }
27110
+ ctx.save();
27111
+ ctx.font = font;
27112
+ ctx.textAlign = 'center';
27113
+ ctx.textBaseline = 'middle';
27114
+ ctx.shadowColor = 'rgba(0,0,0,0.5)';
27115
+ ctx.shadowOffsetX = ratio;
27116
+ ctx.shadowOffsetY = ratio;
27117
+ ctx.shadowBlur = 2 * ratio;
27118
+ ctx.fillStyle = color;
27119
+ ctx.fillText(text, pin.width / 2, pin.height / 2 + 0.5);
27120
+ ctx.restore();
27121
+ },
27122
+ };
27123
+ },
27124
+ // num|color|size|text (bold, with the route map's text shadow)
27125
+ num: function (args, measure) {
27126
+ var color = args[0], size = Number(args[1]), text = args.slice(2).join('|');
27127
+ var px = 14 * size / 32;
27128
+ var font = 'bold ' + px + 'px monospace';
27129
+ var w = Math.ceil(measure(font, text) + 6), h = Math.ceil(px * 1.4 + 4);
27130
+ return {
27131
+ width: w, height: h,
27132
+ draw: function (ctx, ratio) {
27133
+ ctx.font = font;
27134
+ ctx.textAlign = 'center';
27135
+ ctx.textBaseline = 'middle';
27136
+ ctx.shadowColor = 'rgba(0,0,0,0.5)';
27137
+ ctx.shadowOffsetX = ratio;
27138
+ ctx.shadowOffsetY = ratio;
27139
+ ctx.shadowBlur = 2 * ratio;
27140
+ ctx.fillStyle = color;
27141
+ ctx.fillText(text, w / 2, h / 2);
27142
+ },
27143
+ };
27144
+ },
27145
+ // badge|text (red circle, white border, at least 18 px inside the border)
27146
+ badge: function (args, measure) {
27147
+ var text = args.join('|');
27148
+ var font = 'bold 10px sans-serif';
27149
+ var inner = Math.max(18, Math.ceil(measure(font, text) + 4));
27150
+ var w = inner + 4 + 6, h = 18 + 4 + 6;
27151
+ return {
27152
+ width: w, height: h,
27153
+ draw: function (ctx, ratio) {
27154
+ var bw = inner + 4, bh = 22, x = 3, y = 2;
27155
+ var r = bh / 2;
27156
+ ctx.shadowColor = 'rgba(0,0,0,0.4)';
27157
+ ctx.shadowOffsetY = 2 * ratio;
27158
+ ctx.shadowBlur = 4 * ratio;
27159
+ roundedPath(ctx, x, y, bw, bh, [[r, r], [r, r], [r, r], [r, r]]);
27160
+ ctx.fillStyle = '#ffffff';
27161
+ ctx.fill();
27162
+ ctx.shadowColor = 'transparent';
27163
+ roundedPath(ctx, x + 2, y + 2, bw - 4, bh - 4, [[r - 2, r - 2], [r - 2, r - 2], [r - 2, r - 2], [r - 2, r - 2]]);
27164
+ ctx.fillStyle = '#e74c3c';
27165
+ ctx.fill();
27166
+ ctx.font = font;
27167
+ ctx.textAlign = 'center';
27168
+ ctx.textBaseline = 'middle';
27169
+ ctx.fillStyle = '#ffffff';
27170
+ ctx.fillText(text, x + bw / 2, y + bh / 2 + 0.5);
27171
+ },
27172
+ };
27173
+ },
27174
+ // arrow|size|color (an open arrowhead pointing along the line, like the route map's)
27175
+ arrow: function (args) {
27176
+ var size = Number(args[0]), color = args.slice(1).join('|');
27177
+ var w = Math.ceil(size + 4), h = w;
27178
+ return {
27179
+ width: w, height: h,
27180
+ draw: function (ctx) {
27181
+ var len = size, back = len * Math.cos(Math.PI / 6), half = len * Math.sin(Math.PI / 6);
27182
+ var tipX = w / 2 + back / 2, cy = h / 2;
27183
+ ctx.strokeStyle = color;
27184
+ ctx.lineWidth = 2;
27185
+ ctx.lineCap = 'round';
27186
+ ctx.lineJoin = 'round';
27187
+ ctx.beginPath();
27188
+ ctx.moveTo(tipX - back, cy - half);
27189
+ ctx.lineTo(tipX, cy);
27190
+ ctx.lineTo(tipX - back, cy + half);
27191
+ ctx.stroke();
27192
+ },
27193
+ };
27194
+ },
27195
+ };
27196
+
27197
+ function drawnImageId(kind, parts) { return ['concat', IMAGE_PREFIX + kind].concat(parts.reduce(function (a, p) { return a.concat(['|', p]); }, [])); }
27198
+
26974
27199
  var NODE_TTL = 3600 * 1000;
26975
27200
 
26976
27201
  function timed(url, ms) {
@@ -27051,6 +27276,9 @@ function smapCreateSdk(engine) {
27051
27276
  * @param {boolean} [options.nearest=true] read from the nearest sMap node (asked of baseUrl, remembered for an hour)
27052
27277
  * @param {string[]} [options.nodes] node URLs known in advance, used while baseUrl is down
27053
27278
  * @param {'mercator'|'globe'} [options.projection]
27279
+ * @param {Object} [options.popup] default popup options of every layer and openPopup (className, maxWidth,
27280
+ * minWidth, offset, anchor, closeButton, closeOnClick, focusAfterOpen)
27281
+ * @param {string|Node} [options.popupLoading] shown while a popup's content is a pending promise (default: dots)
27054
27282
  * Every other option (center, zoom, pitch, bearing, minZoom, maxZoom, maxBounds, hash, …) goes to the engine.
27055
27283
  */
27056
27284
  function SMap(options) {
@@ -27070,6 +27298,12 @@ function smapCreateSdk(engine) {
27070
27298
  this._buildings = false;
27071
27299
  this._handlers = {};
27072
27300
  this._refreshTimer = null;
27301
+ this._popupDefaults = options.popup || {};
27302
+ this._popupLoading = options.popupLoading;
27303
+ this._autoZoom = options.autoZoom !== false;
27304
+ this._wasd = options.keyboard3D;
27305
+ this._popup = null;
27306
+ this._popupSeq = 0;
27073
27307
  // True between a style's load and the next style change. The engine's own isStyleLoaded() turns false while any
27074
27308
  // source is loading data, which would leave layers added meanwhile waiting for a load that never comes.
27075
27309
  this._styleReady = false;
@@ -27077,7 +27311,7 @@ function smapCreateSdk(engine) {
27077
27311
  var self = this;
27078
27312
  var engineOptions = {};
27079
27313
  for (var k in options) {
27080
- if (Object.prototype.hasOwnProperty.call(options, k) && ['token', 'getToken', 'apiKey', 'style', 'language', 'baseUrl', 'projection', 'nearest', 'nodes'].indexOf(k) < 0) {
27314
+ if (Object.prototype.hasOwnProperty.call(options, k) && ['token', 'getToken', 'apiKey', 'style', 'language', 'baseUrl', 'projection', 'nearest', 'nodes', 'popup', 'popupLoading', 'autoZoom', 'keyboard3D'].indexOf(k) < 0) {
27081
27315
  engineOptions[k] = options[k];
27082
27316
  }
27083
27317
  }
@@ -27110,6 +27344,14 @@ function smapCreateSdk(engine) {
27110
27344
  badge._place();
27111
27345
  }
27112
27346
  self.engine.on('style.load', function () { self._styleReady = true; self._restore(); });
27347
+ self._keyboard3D();
27348
+ // One click handler for every layer's popup: the engine's hit test over those layers, the top feature opens.
27349
+ self.engine.on('click', function (e) { self._popupClick(e); });
27350
+ self._styleClass();
27351
+ // Pins, numbers, badges and arrows are drawn the first time a feature needs one (and again after a style change).
27352
+ if (self.engine.setMissingStyleImageResolver) {
27353
+ self.engine.setMissingStyleImageResolver(function (id) { self._drawImage(id); });
27354
+ }
27113
27355
  return new Promise(function (resolve) { self.engine.once('load', function () { resolve(self); }); });
27114
27356
  });
27115
27357
  if (this._token && this._getToken) { this._scheduleRefresh(); }
@@ -27145,10 +27387,263 @@ function smapCreateSdk(engine) {
27145
27387
  if (FLAVORS.indexOf(style) >= 0) { this._flavor = style; this._customStyle = null; } else { this._customStyle = style; }
27146
27388
  this._styleReady = false;
27147
27389
  this.engine.setStyle(this._styleUrl());
27390
+ this._styleClass();
27391
+ return this;
27392
+ };
27393
+
27394
+ // The map container carries smap-style-<scheme>, so the stylesheet can follow the scheme (dark popups).
27395
+ SMap.prototype._styleClass = function () {
27396
+ var c = this.engine && this.engine.getContainer && this.engine.getContainer();
27397
+ if (!c || !c.classList) { return; }
27398
+ for (var i = 0; i < FLAVORS.length; i++) { c.classList.remove('smap-style-' + FLAVORS[i]); }
27399
+ if (!this._customStyle) { c.classList.add('smap-style-' + this._flavor); }
27400
+ };
27401
+
27402
+ // Draws an image the SDK named (IMAGE_PREFIX + kind|args) and adds it to the map; other names are left alone.
27403
+ // The live-position marker of the route map: a white circle (85%) with a truck in the driver colour.
27404
+ function drawVehicle(ctx, ratio, c, size, color) {
27405
+ var r = size / 2, k = size / 24;
27406
+ ctx.save();
27407
+ ctx.translate(c, c);
27408
+ ctx.shadowColor = 'rgba(0,0,0,0.5)';
27409
+ ctx.shadowBlur = 2 * ratio;
27410
+ ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI * 2);
27411
+ ctx.fillStyle = 'rgba(255,255,255,0.85)'; ctx.fill();
27412
+ ctx.shadowColor = 'transparent';
27413
+ ctx.scale(k, k);
27414
+ ctx.fillStyle = color;
27415
+ // cargo box, cab and two wheels, about 14 x 10 px on a 24 px marker
27416
+ ctx.fillRect(-7, -5, 8.5, 7);
27417
+ roundedPath(ctx, 2.2, -2.6, 4.8, 4.6, [[1.4, 1.4], [0.6, 0.6], [0, 0], [0, 0]]); ctx.fill();
27418
+ ctx.beginPath(); ctx.arc(-4.2, 3.4, 1.9, 0, Math.PI * 2); ctx.arc(4.1, 3.4, 1.9, 0, Math.PI * 2); ctx.fill();
27419
+ ctx.fillStyle = 'rgba(255,255,255,0.85)';
27420
+ ctx.beginPath(); ctx.arc(-4.2, 3.4, 0.7, 0, Math.PI * 2); ctx.arc(4.1, 3.4, 0.7, 0, Math.PI * 2); ctx.fill();
27421
+ ctx.restore();
27422
+ }
27423
+
27424
+ SMap.prototype._drawImage = function (id) {
27425
+ if (typeof id !== 'string' || id.indexOf(IMAGE_PREFIX) !== 0) { return; }
27426
+ var m = this.engine;
27427
+ if (m.hasImage(id)) { return; }
27428
+ var parts = id.slice(IMAGE_PREFIX.length).split('|');
27429
+ var kind = IMAGE_KINDS[parts[0]];
27430
+ if (!kind) { return; }
27431
+ var doc = m.getContainer().ownerDocument;
27432
+ var win = doc.defaultView || {};
27433
+ var ratio = Math.min(3, Math.max(1, Math.ceil(win.devicePixelRatio || 1)));
27434
+ var canvas = doc.createElement('canvas');
27435
+ var ctx = canvas.getContext('2d');
27436
+ var measure = function (font, text) { ctx.font = font; return ctx.measureText(text).width; };
27437
+ var image = kind(parts.slice(1), measure);
27438
+ canvas.width = Math.ceil(image.width * ratio);
27439
+ canvas.height = Math.ceil(image.height * ratio);
27440
+ ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
27441
+ image.draw(ctx, ratio);
27442
+ m.addImage(id, ctx.getImageData(0, 0, canvas.width, canvas.height), { pixelRatio: ratio });
27443
+ this._imagesDrawn = (this._imagesDrawn || 0) + 1;
27444
+ };
27445
+
27446
+ /**
27447
+ * Opens the map's popup at a position, closing the one open before (there is one popup per map).
27448
+ * @param {Array|Object} lngLat [lng, lat] or {lng, lat}
27449
+ * @param {string|Node|Promise} content trusted HTML (inserted as is, never sanitized), a DOM node, or a promise
27450
+ * of either (popupLoading shows meanwhile; a rejection closes the popup
27451
+ * and is reported as the map's 'error' event)
27452
+ * @param {Object} [options] popup options over the map's `popup` defaults, and `loading`
27453
+ */
27454
+ SMap.prototype.openPopup = function (lngLat, content, options) {
27455
+ var self = this;
27456
+ if (!this.engine) { this.ready.then(function () { self.openPopup(lngLat, content, options); }); return this; }
27457
+ this.closePopup();
27458
+ if (!content) { return this; }
27459
+ var seq = ++this._popupSeq;
27460
+ var o = Object.assign({}, this._popupDefaults, options || {});
27461
+ var engineOptions = { offset: 12, className: 'smap-popup' + (o.className ? ' ' + o.className : '') };
27462
+ for (var i = 0; i < POPUP_OPTIONS.length; i++) {
27463
+ var k = POPUP_OPTIONS[i];
27464
+ if (k !== 'className' && k !== 'minWidth' && k !== 'autoPan' && o[k] !== undefined) { engineOptions[k] = k === 'maxWidth' ? cssSize(o[k]) : o[k]; }
27465
+ }
27466
+ var popup = new engine.Popup(engineOptions).setLngLat(lngLat);
27467
+ this._popup = popup;
27468
+ var pending = isThenable(content);
27469
+ var loading = o.loading !== undefined ? o.loading : (this._popupLoading !== undefined ? this._popupLoading : POPUP_LOADING);
27470
+ this._popupContent(popup, pending ? loading : content);
27471
+ popup.addTo(this.engine);
27472
+ var el = popup.getElement && popup.getElement();
27473
+ if (el && o.minWidth !== undefined) { el.style.minWidth = cssSize(o.minWidth); }
27474
+ if (o.autoPan !== false) {
27475
+ // After the engine has placed it (next frame), so its box is final.
27476
+ var raf = (typeof requestAnimationFrame === 'function') ? requestAnimationFrame : function (fn) { return setTimeout(fn, 16); };
27477
+ raf(function () { if (self._popup === popup) { self._panIntoView(popup); } });
27478
+ }
27479
+ if (pending) {
27480
+ content.then(function (answer) {
27481
+ // A later popup, or a close, makes this answer stale.
27482
+ if (seq !== self._popupSeq || self._popup !== popup) { return; }
27483
+ if (answer) { self._popupContent(popup, answer); } else { self.closePopup(); }
27484
+ }, function (error) {
27485
+ if (seq !== self._popupSeq || self._popup !== popup) { return; }
27486
+ self.closePopup();
27487
+ self._reportError(error);
27488
+ });
27489
+ }
27490
+ return this;
27491
+ };
27492
+
27493
+ /** Pans the map so the popup is fully inside it, with a small margin (Leaflet's autoPan). */
27494
+ SMap.prototype._panIntoView = function (popup) {
27495
+ var m = this.engine, el = popup.getElement && popup.getElement();
27496
+ if (!el || !m.getContainer || !m.panBy || !el.getBoundingClientRect) { return; }
27497
+ var box = el.getBoundingClientRect(), frame = m.getContainer().getBoundingClientRect(), pad = 10, dx = 0, dy = 0;
27498
+ if (box.top < frame.top + pad) { dy = box.top - frame.top - pad; } else if (box.bottom > frame.bottom - pad) { dy = box.bottom - frame.bottom + pad; }
27499
+ if (box.left < frame.left + pad) { dx = box.left - frame.left - pad; } else if (box.right > frame.right - pad) { dx = box.right - frame.right + pad; }
27500
+ if (dx || dy) { m.panBy([dx, dy], { duration: 250 }); }
27501
+ };
27502
+
27503
+ /** Closes the map's popup, if one is open. */
27504
+ SMap.prototype.closePopup = function () {
27505
+ var popup = this._popup;
27506
+ this._popup = null;
27507
+ this._popupSeq++;
27508
+ if (popup) { popup.remove(); }
27148
27509
  return this;
27149
27510
  };
27150
27511
 
27512
+ SMap.prototype._popupContent = function (popup, content) {
27513
+ if (typeof content === 'string') { popup.setHTML(content); } else { popup.setDOMContent(content); }
27514
+ };
27515
+
27516
+ SMap.prototype._reportError = function (error) {
27517
+ var m = this.engine;
27518
+ if (m && engine.ErrorEvent && m.fire) { m.fire(new engine.ErrorEvent(error)); } else if (typeof console !== 'undefined') { console.error(error); }
27519
+ };
27520
+
27521
+ // A click on the map: the top feature of the layers with a popup, if any, opens its layer's popup.
27522
+ SMap.prototype._popupClick = function (e) {
27523
+ var m = this.engine;
27524
+ var ids = [];
27525
+ var byEngineLayer = {};
27526
+ for (var i = 0; i < this._order.length; i++) {
27527
+ var def = this._layers[this._order[i]];
27528
+ if (def.o.popup && def.popupLayer && !def.hidden && m.getLayer(def.popupLayer)) {
27529
+ ids.push(def.popupLayer);
27530
+ byEngineLayer[def.popupLayer] = def;
27531
+ if (def.hoverLayer && m.getLayer(def.hoverLayer)) { ids.push(def.hoverLayer); byEngineLayer[def.hoverLayer] = def; }
27532
+ }
27533
+ }
27534
+ if (!ids.length) { return; }
27535
+ var hits = m.queryRenderedFeatures(e.point, { layers: ids });
27536
+ if (!hits || !hits.length) { return; }
27537
+ var f = hits[0];
27538
+ var hit = byEngineLayer[f.layer && f.layer.id];
27539
+ if (!hit) { return; }
27540
+ // A tight pile of pins: zoom in around the click so they come apart, or, when they still overlap close up (or
27541
+ // share one spot), offer a chooser. A layer turns this off with marker.overlap === false.
27542
+ if (this._autoZoom && hit.o.marker && hit.o.marker.overlap !== false && e.point && m.project) {
27543
+ var ov = hit.o.marker.overlap || {}, near = ov.radius || 12, threshold = ov.threshold || 4;
27544
+ var box = [[e.point.x - near, e.point.y - near], [e.point.x + near, e.point.y + near]];
27545
+ var seen = {}, pile = [];
27546
+ (m.queryRenderedFeatures(box, { layers: ids }) || []).forEach(function (x) {
27547
+ var d = byEngineLayer[x.layer && x.layer.id];
27548
+ if (d !== hit || !x.geometry || x.geometry.type !== 'Point') { return; }
27549
+ var key = JSON.stringify(x.geometry.coordinates) + JSON.stringify(x.properties || {});
27550
+ if (!seen[key]) { seen[key] = true; pile.push(x); }
27551
+ });
27552
+ if (pile.length >= 2) {
27553
+ var spots = {};
27554
+ pile.forEach(function (x) { spots[x.geometry.coordinates.map(function (c) { return c.toFixed(6); }).join(',')] = true; });
27555
+ var oneSpot = Object.keys(spots).length === 1;
27556
+ var closeUp = m.getZoom() >= (ov.maxZoom || 17);
27557
+ if (pile.length >= threshold && !oneSpot && !closeUp) {
27558
+ m.easeTo({ center: e.lngLat, zoom: Math.min(m.getZoom() + (ov.zoomBy || 2), ov.maxZoom || 17), duration: 400 });
27559
+ return;
27560
+ }
27561
+ if (pile.length >= threshold || oneSpot) { this._chooser(hit, pile, e.lngLat); return; }
27562
+ }
27563
+ }
27564
+ this._openFeaturePopup(hit, f, e.lngLat);
27565
+ };
27566
+
27567
+ /** A small list of the pins under a click; choosing one opens its popup. */
27568
+ SMap.prototype._chooser = function (hit, pile, lngLat) {
27569
+ var self = this, mk = hit.o.marker, doc = this.engine.getContainer().ownerDocument;
27570
+ var list = doc.createElement('div');
27571
+ list.className = 'smap-chooser';
27572
+ var esc = function (v) { return String(v == null ? '' : v).replace(/[&<>"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]; }); };
27573
+ var plainValue = function (v, p) {
27574
+ if (v && typeof v === 'object' && !Array.isArray(v) && v.property) { return p[v.property] !== undefined ? p[v.property] : v.default; }
27575
+ return Array.isArray(v) ? undefined : v;
27576
+ };
27577
+ pile.slice(0, 50).forEach(function (x, i) {
27578
+ var p = x.properties || {};
27579
+ var label = mk.overlap && typeof mk.overlap.label === 'function' ? mk.overlap.label(p, x) : null;
27580
+ var number = plainValue(mk.text, p), fill = plainValue(mk.fill, p) || '#ffffff', border = plainValue(mk.border, p) || hit.o.color || '#4f46e5';
27581
+ var item = doc.createElement('button');
27582
+ item.type = 'button';
27583
+ item.className = 'smap-chooser-item';
27584
+ item.innerHTML = '<span class="smap-chooser-pin" style="background:' + esc(fill) + ';border-color:' + esc(border) + '">' + esc(number == null ? '' : number) + '</span>'
27585
+ + '<span class="smap-chooser-label">' + esc(label != null ? label : (number != null ? number : i + 1)) + '</span>';
27586
+ item.addEventListener('click', function (ev) { ev.stopPropagation(); self._openFeaturePopup(hit, x, x.geometry.coordinates); });
27587
+ list.appendChild(item);
27588
+ });
27589
+ this._unpin();
27590
+ this.openPopup(lngLat, list, { className: 'smap-chooser-popup', anchor: 'bottom', offset: 12 });
27591
+ };
27592
+
27593
+ /** Opens a feature's own popup (the layer's popup function), above a pin when the layer draws pins. */
27594
+ SMap.prototype._openFeaturePopup = function (hit, f, lngLat) {
27595
+ var m = this.engine;
27596
+ // Points open at their own position; lines and areas where they were clicked.
27597
+ var at = hit.kind === 'points' && f.geometry && f.geometry.type === 'Point' ? f.geometry.coordinates : lngLat;
27598
+ var content;
27599
+ try { content = hit.o.popup(f.properties || {}, f); } catch (error) { this._reportError(error); return; }
27600
+ var popupOptions = Object.assign({}, hit.o.popupOptions || {});
27601
+ if (hit.o.marker && popupOptions.offset === undefined && this._popupDefaults.offset === undefined) {
27602
+ // Just above the pin as it is drawn now (its scale may follow the zoom).
27603
+ var mk = hit.o.marker, size = mk.size || 32, k = scaleAt(mk.scale, m.getZoom ? m.getZoom() : 0);
27604
+ popupOptions.offset = mk.shape === 'circle' || mk.shape === 'vehicle' ? Math.round((size / 2) * k + 4) : Math.round((markerGeometry('pin', size).tipToHead + size / 2) * k + 2);
27605
+ }
27606
+ // Above the pin, always (as on Superroute's route map); the map pans to make room instead of flipping it below.
27607
+ if (hit.o.marker && popupOptions.anchor === undefined && this._popupDefaults.anchor === undefined) { popupOptions.anchor = 'bottom'; }
27608
+ if (hit.o.popupLoading !== undefined) { popupOptions.loading = hit.o.popupLoading; }
27609
+ this._unpin();
27610
+ this.openPopup(at, content, popupOptions);
27611
+ // The clicked pin stays in front while its popup is open (the route map's rule).
27612
+ if (hit.hoverSource && this._popup) {
27613
+ var self = this, popup = this._popup;
27614
+ this._setHover(null, hit, f, 'popup');
27615
+ hit.pinned = popup;
27616
+ this._pinnedDef = hit;
27617
+ if (popup.on) { popup.on('close', function () { if (hit.pinned === popup) { self._unpin(); } }); }
27618
+ }
27619
+ };
27620
+
27151
27621
  /** Switches the label language; your layers are kept. */
27622
+ /**
27623
+ * A marker scale at a zoom: a number, or the linear / step zoom expressions the SDK documents
27624
+ * (['interpolate', ['linear'], ['zoom'], z, v, …], ['step', ['zoom'], v, z, v, …]); anything else counts as 1.
27625
+ */
27626
+ function scaleAt(scale, zoom) {
27627
+ if (scale === undefined || scale === null) { return 1; }
27628
+ if (typeof scale === 'number') { return scale; }
27629
+ if (!Array.isArray(scale)) { return 1; }
27630
+ var isZoom = function (x) { return Array.isArray(x) && x[0] === 'zoom'; };
27631
+ if (scale[0] === 'interpolate' && isZoom(scale[2])) {
27632
+ var stops = scale.slice(3);
27633
+ if (zoom <= stops[0]) { return stops[1]; }
27634
+ for (var i = 2; i < stops.length; i += 2) {
27635
+ if (zoom <= stops[i]) { var t = (zoom - stops[i - 2]) / (stops[i] - stops[i - 2]); return stops[i - 1] + t * (stops[i + 1] - stops[i - 1]); }
27636
+ }
27637
+ return stops[stops.length - 1];
27638
+ }
27639
+ if (scale[0] === 'step' && isZoom(scale[1])) {
27640
+ var value = scale[2];
27641
+ for (var j = 3; j < scale.length; j += 2) { if (zoom >= scale[j]) { value = scale[j + 1]; } }
27642
+ return value;
27643
+ }
27644
+ return 1;
27645
+ }
27646
+
27152
27647
  SMap.prototype.setLanguage = function (language) {
27153
27648
  this._language = language;
27154
27649
  if (!this._customStyle) { this._styleReady = false; this.engine.setStyle(this._styleUrl()); }
@@ -27163,6 +27658,33 @@ function smapCreateSdk(engine) {
27163
27658
  };
27164
27659
 
27165
27660
  /** Shows buildings in 3D (and tilts the map when it is flat). */
27661
+ /**
27662
+ * Auto Zoom: a click on pins piled up zooms the map in around it, or lists them close up (see marker.overlap).
27663
+ * On by default (`autoZoom: false` in the options turns it off).
27664
+ */
27665
+ SMap.prototype.setAutoZoom = function (on) { this._autoZoom = !!on; return this; };
27666
+ SMap.prototype.autoZoom = function () { return this._autoZoom; };
27667
+
27668
+ /**
27669
+ * In 3D (3D buildings on, or the map tilted) W / S tilt the view and A / D turn it; Shift takes bigger steps. Only
27670
+ * while the map has the keyboard focus, so typing elsewhere on the page is never taken.
27671
+ */
27672
+ SMap.prototype._keyboard3D = function () {
27673
+ var self = this, m = this.engine;
27674
+ var el = m.getCanvasContainer ? m.getCanvasContainer() : (m.getContainer ? m.getContainer() : null);
27675
+ if (!el || !el.addEventListener || this._wasd === false) { return; }
27676
+ el.addEventListener('keydown', function (e) {
27677
+ if (e.altKey || e.ctrlKey || e.metaKey) { return; }
27678
+ if (!self._buildings && m.getPitch() < 1) { return; }
27679
+ var big = e.shiftKey, key = (e.key || '').toLowerCase();
27680
+ var pitch = m.getPitch(), bearing = m.getBearing();
27681
+ if (key === 'w') { pitch = Math.min(85, pitch + (big ? 15 : 5)); } else if (key === 's') { pitch = Math.max(0, pitch - (big ? 15 : 5)); }
27682
+ else if (key === 'a') { bearing -= big ? 45 : 15; } else if (key === 'd') { bearing += big ? 45 : 15; } else { return; }
27683
+ e.preventDefault();
27684
+ m.easeTo({ pitch: pitch, bearing: bearing, duration: 200 });
27685
+ });
27686
+ };
27687
+
27166
27688
  SMap.prototype.setBuildings3D = function (on, options) {
27167
27689
  this._buildings = !!on;
27168
27690
  this._buildingOptions = options || this._buildingOptions || {};
@@ -27201,8 +27723,9 @@ function smapCreateSdk(engine) {
27201
27723
  * @param {Object|Array} data GeoJSON, or an array of {lng, lat, …properties}
27202
27724
  * @param {Object} [o]
27203
27725
  * cluster: true | {radius, maxZoom}; color; radius; strokeColor; icon (image URL); iconSize; label (property name);
27204
- * minzoom; maxzoom; zIndex (higher draws on top); onClick(feature, event); popup(properties) → trusted HTML
27205
- * (inserted as is, never sanitized: escape feature data) or a DOM node
27726
+ * minzoom; maxzoom; zIndex (higher draws on top); onClick(feature, event);
27727
+ * popup(properties, feature) → trusted HTML (inserted as is, never sanitized: escape feature data, or use
27728
+ * sMap.popupTemplate), a DOM node, or a promise of either; popupOptions (over the map's `popup`); popupLoading
27206
27729
  */
27207
27730
  SMap.prototype.addPoints = function (id, data, o) {
27208
27731
  o = o || {};
@@ -27230,6 +27753,7 @@ function smapCreateSdk(engine) {
27230
27753
  var def = this._layers[id];
27231
27754
  if (!def) { throw new Error('sMap: no layer ' + id); }
27232
27755
  def.data = toFeatureCollection(data);
27756
+ if (def.hovered) { this._setHover(id, def, null); }
27233
27757
  var src = this.engine.getSource('smap-' + id);
27234
27758
  if (src) { src.setData(def.data); }
27235
27759
  return this;
@@ -27308,7 +27832,9 @@ function smapCreateSdk(engine) {
27308
27832
  if (!m) { return; }
27309
27833
  def.engineLayers.forEach(function (l) { if (m.getLayer(l)) { m.removeLayer(l); } });
27310
27834
  if (m.getSource('smap-' + id)) { m.removeSource('smap-' + id); }
27311
- (this._handlers[id] || []).forEach(function (h) { m.off(h[0], h[1], h[2]); });
27835
+ if (m.getSource('smap-' + id + '-hover')) { m.removeSource('smap-' + id + '-hover'); }
27836
+ def.hoverLayer = null; def.hovered = null;
27837
+ (this._handlers[id] || []).forEach(function (h) { if (h[1]) { m.off(h[0], h[1], h[2]); } else { m.off(h[0], h[2]); } });
27312
27838
  this._handlers[id] = [];
27313
27839
  def.engineLayers = [];
27314
27840
  };
@@ -27341,7 +27867,11 @@ function smapCreateSdk(engine) {
27341
27867
  layer.source = src;
27342
27868
  // The engine refuses a filter key whose value is undefined (unclustered points have none).
27343
27869
  if (layer.filter === undefined) { delete layer.filter; }
27344
- for (var k in range) { layer[k] = range[k]; }
27870
+ if (layer.minzoom === undefined) { delete layer.minzoom; }
27871
+ if (layer.maxzoom === undefined) { delete layer.maxzoom; }
27872
+ if (layer.paint === undefined) { delete layer.paint; }
27873
+ // A layer's own minzoom (a marker's textMinzoom / badgeMinzoom) never shows it before the layer's own range.
27874
+ for (var k in range) { layer[k] = k === 'minzoom' && layer.minzoom !== undefined ? Math.max(layer.minzoom, range.minzoom) : range[k]; }
27345
27875
  layer.layout = Object.assign({ visibility: visibility }, layer.layout || {});
27346
27876
  m.addLayer(layer, before);
27347
27877
  def.engineLayers.push(layer.id);
@@ -27371,7 +27901,64 @@ function smapCreateSdk(engine) {
27371
27901
  });
27372
27902
  }
27373
27903
  var only = o.cluster ? ['!', ['has', 'point_count']] : undefined;
27374
- if (o.icon) {
27904
+ var both = function (extra) { return only ? ['all', only, extra] : extra; };
27905
+ if (o.marker) {
27906
+ // Pins like Superroute's route map, drawn on the GPU: one image per colour pair (and per number, per badge),
27907
+ // made the first time a feature shows it. Numbers and badges are their own layers above the pins.
27908
+ var mk = o.marker;
27909
+ var shape = mk.shape === 'circle' || mk.shape === 'vehicle' ? mk.shape : 'pin';
27910
+ var size = mk.size || 32;
27911
+ var bw = mk.borderWidth !== undefined ? mk.borderWidth : 3;
27912
+ var g = markerGeometry(shape, size);
27913
+ var str = function (v, d) { return ['to-string', valueExpr(v, d)]; };
27914
+ // One scale for the pin, its number and its badge (a number, or an engine expression such as one of the zoom),
27915
+ // so the three stay together at every size: the offsets are in icon units and scale with them.
27916
+ var scale = mk.scale !== undefined ? mk.scale : 1;
27917
+ // Opacity of the pin, its number and its badge together (the route map draws settled stops at .72).
27918
+ var markerPaint = mk.opacity !== undefined ? { 'icon-opacity': valueExpr(mk.opacity, 1) } : undefined;
27919
+ main = src + '-points';
27920
+ // From bakeZoom on (default 13) a pin's number is drawn into its own image, so an upper pin covers a lower
27921
+ // one completely. Further out, where thousands of pins can be in view, the numbers stay one layer above the
27922
+ // pins (an image per colour pair instead of per pin and number). bakeZoom: false keeps them separate.
27923
+ var plainPin = drawnImageId('pin', [shape, str(mk.fill, '#ffffff'), str(mk.border, color), String(bw), String(size)]);
27924
+ var bakeZoom = mk.text !== undefined && mk.bakeZoom !== false ? (mk.bakeZoom !== undefined ? mk.bakeZoom : 13) : null;
27925
+ var pinImage = bakeZoom === null ? plainPin : ['step', ['zoom'], plainPin, bakeZoom,
27926
+ drawnImageId('pinnum', [shape, str(mk.fill, '#ffffff'), str(mk.border, color), String(bw), String(size), str(mk.textColor, '#ffffff'), str(mk.text, '')])];
27927
+ var markerSpecs = [];
27928
+ var addMarker = function (layer) { markerSpecs.push(JSON.parse(JSON.stringify(layer))); add(layer); };
27929
+ addMarker({
27930
+ id: main, type: 'symbol', filter: only, paint: markerPaint,
27931
+ layout: {
27932
+ 'icon-image': pinImage,
27933
+ 'icon-anchor': shape === 'pin' ? 'bottom' : 'center',
27934
+ // The image has room for the shadow below the tip: the tip, not the image's edge, is at the position.
27935
+ 'icon-offset': [0, shape === 'pin' ? PIN_PAD : 0], 'icon-size': scale,
27936
+ 'icon-allow-overlap': true, 'icon-ignore-placement': true,
27937
+ },
27938
+ });
27939
+ if (mk.text !== undefined) {
27940
+ var text = str(mk.text, '');
27941
+ addMarker({
27942
+ id: src + '-numbers', type: 'symbol', filter: both(['!=', text, '']), minzoom: mk.textMinzoom, maxzoom: bakeZoom === null ? undefined : bakeZoom, paint: markerPaint,
27943
+ layout: {
27944
+ 'icon-image': drawnImageId('num', [str(mk.textColor, '#ffffff'), String(size), text]),
27945
+ 'icon-offset': [0, -g.tipToHead], 'icon-size': scale, 'icon-allow-overlap': true, 'icon-ignore-placement': true,
27946
+ },
27947
+ });
27948
+ }
27949
+ if (mk.badge !== undefined) {
27950
+ var badge = ['to-number', valueExpr(mk.badge, 0), 0];
27951
+ addMarker({
27952
+ id: src + '-badges', type: 'symbol', filter: both(['>', badge, 1]), minzoom: mk.badgeMinzoom, paint: markerPaint,
27953
+ layout: {
27954
+ 'icon-image': drawnImageId('badge', [['to-string', badge]]),
27955
+ 'icon-offset': g.badge, 'icon-size': scale,
27956
+ 'icon-allow-overlap': true, 'icon-ignore-placement': true,
27957
+ },
27958
+ });
27959
+ }
27960
+ if (mk.hoverRaise !== false) { this._hoverRaise(id, def, src, markerSpecs, main); }
27961
+ } else if (o.icon) {
27375
27962
  var imageId = src + '-icon';
27376
27963
  main = src + '-points';
27377
27964
  var placeIcon = function () {
@@ -27399,15 +27986,43 @@ function smapCreateSdk(engine) {
27399
27986
  if (o.label) {
27400
27987
  add({
27401
27988
  id: src + '-labels', type: 'symbol', filter: only,
27402
- layout: { 'text-field': ['to-string', ['get', o.label]], 'text-font': ['Noto Sans Regular'], 'text-size': 12, 'text-offset': [0, 1.2], 'text-anchor': 'top', 'text-optional': true },
27989
+ layout: o.labelPosition === 'right'
27990
+ ? { 'text-field': ['to-string', ['get', o.label]], 'text-font': ['Noto Sans Regular'], 'text-size': 11, 'text-offset': [1.3, 0], 'text-anchor': 'left', 'text-optional': true, 'text-max-width': 12 }
27991
+ : { 'text-field': ['to-string', ['get', o.label]], 'text-font': ['Noto Sans Regular'], 'text-size': 12, 'text-offset': [0, 1.2], 'text-anchor': 'top', 'text-optional': true },
27403
27992
  paint: { 'text-color': '#1f2937', 'text-halo-color': '#ffffff', 'text-halo-width': 1.4 },
27404
27993
  });
27405
27994
  }
27406
27995
  } else if (def.kind === 'lines') {
27407
27996
  main = src + '-lines';
27408
- var paint = { 'line-color': color, 'line-width': o.width || 3, 'line-opacity': o.opacity !== undefined ? o.opacity : 1 };
27997
+ var lineColor = valueExpr(o.color, '#4f46e5');
27998
+ var round = { 'line-cap': 'round', 'line-join': 'round' };
27999
+ // Bottom to top: casing, shadow, the line, its arrows (as on Superroute's route map).
28000
+ if (o.casing) {
28001
+ var cs = o.casing === true ? {} : o.casing;
28002
+ add({ id: src + '-casing', type: 'line', layout: round, paint: {
28003
+ 'line-color': valueExpr(cs.color, '#ffffff'), 'line-width': cs.width || 9, 'line-opacity': cs.opacity !== undefined ? cs.opacity : 1 } });
28004
+ }
28005
+ if (o.shadow) {
28006
+ var sh = o.shadow === true ? {} : o.shadow;
28007
+ add({ id: src + '-shadow', type: 'line', layout: round, paint: {
28008
+ 'line-color': valueExpr(sh.color, '#000000'), 'line-width': sh.width || 7, 'line-opacity': sh.opacity !== undefined ? sh.opacity : 0.3 } });
28009
+ }
28010
+ var paint = { 'line-color': lineColor, 'line-width': o.width || 3, 'line-opacity': o.opacity !== undefined ? o.opacity : 1 };
27409
28011
  if (o.dash) { paint['line-dasharray'] = o.dash; }
27410
- add({ id: main, type: 'line', layout: { 'line-cap': 'round', 'line-join': 'round' }, paint: paint });
28012
+ add({ id: main, type: 'line', layout: round, paint: paint });
28013
+ if (o.arrows) {
28014
+ var ar = o.arrows === true ? {} : o.arrows;
28015
+ var arrowSize = ar.size || 14;
28016
+ add({
28017
+ id: src + '-arrows', type: 'symbol',
28018
+ layout: {
28019
+ 'symbol-placement': 'line', 'symbol-spacing': ar.spacing || 80,
28020
+ 'icon-image': drawnImageId('arrow', [String(arrowSize), ['to-string', valueExpr(ar.color, lineColor)]]),
28021
+ 'icon-rotation-alignment': 'map', 'icon-allow-overlap': true, 'icon-ignore-placement': true,
28022
+ },
28023
+ paint: { 'icon-opacity': ar.opacity !== undefined ? ar.opacity : 0.8 },
28024
+ });
28025
+ }
27411
28026
  } else {
27412
28027
  if (o.extrude) {
27413
28028
  main = src + '-extrusion';
@@ -27427,32 +28042,75 @@ function smapCreateSdk(engine) {
27427
28042
  add({ id: src + '-outline', type: 'line', paint: { 'line-color': o.outline || o.fill || color, 'line-width': o.outlineWidth || 1.5 } });
27428
28043
  }
27429
28044
  }
28045
+ // The popup is opened by the map's one click handler (_popupClick), from this layer's own hit test.
28046
+ def.popupLayer = o.popup ? main : null;
27430
28047
  if (main && (o.popup || o.onClick)) {
27431
- this._listen(id, 'click', main, function (e) {
27432
- var f = e.features[0];
27433
- if (o.onClick) { o.onClick(f, e); }
27434
- if (o.popup) {
27435
- // A string is inserted as HTML as it is (never sanitized): it must be trusted, with feature data escaped.
27436
- // A DOM node is inserted as it is.
27437
- var content = o.popup(f.properties, f);
27438
- if (content) {
27439
- var popup = new engine.Popup({ offset: 12 }).setLngLat(e.lngLat);
27440
- if (typeof content === 'string') { popup.setHTML(content); } else { popup.setDOMContent(content); }
27441
- popup.addTo(m);
27442
- }
27443
- }
27444
- });
28048
+ if (o.onClick) {
28049
+ this._listen(id, 'click', main, function (e) { o.onClick(e.features[0], e); });
28050
+ }
27445
28051
  this._listen(id, 'mouseenter', main, function () { m.getCanvas().style.cursor = 'pointer'; });
27446
28052
  this._listen(id, 'mouseleave', main, function () { m.getCanvas().style.cursor = ''; });
27447
28053
  }
27448
28054
  };
27449
28055
 
28056
+ /**
28057
+ * Overlapping pins: the one under the pointer comes to the front, as on Superroute's route map, so it can be read
28058
+ * and clicked. A copy of that one feature is drawn in its own source above everything else; nothing is done per
28059
+ * feature, only for the feature under the pointer.
28060
+ */
28061
+ SMap.prototype._hoverRaise = function (id, def, src, specs, main) {
28062
+ var m = this.engine, self = this;
28063
+ var hoverSrc = src + '-hover';
28064
+ m.addSource(hoverSrc, { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
28065
+ var top = null;
28066
+ specs.forEach(function (spec) {
28067
+ var layer = JSON.parse(JSON.stringify(spec));
28068
+ layer.id = spec.id.replace(src + '-', src + '-hover-');
28069
+ layer.source = hoverSrc;
28070
+ if (layer.filter === undefined) { delete layer.filter; }
28071
+ if (layer.paint === undefined) { delete layer.paint; }
28072
+ if (layer.minzoom === undefined) { delete layer.minzoom; }
28073
+ if (layer.maxzoom === undefined) { delete layer.maxzoom; }
28074
+ m.addLayer(layer);
28075
+ def.engineLayers.push(layer.id);
28076
+ if (!top) { top = layer.id; }
28077
+ });
28078
+ def.hoverLayer = top;
28079
+ def.hoverSource = hoverSrc;
28080
+ var keyOf = function (f) { return JSON.stringify(f.geometry && f.geometry.coordinates) + JSON.stringify(f.properties || {}); };
28081
+ this._listen(id, 'mousemove', null, function (e) {
28082
+ if (def.hidden || !m.getLayer(main) || def.pinned) { return; }
28083
+ // Still over the raised pin: keep it. Otherwise raise the top pin under the pointer, if any.
28084
+ if (def.hovered && m.getLayer(top) && m.queryRenderedFeatures(e.point, { layers: [top] }).length) { return; }
28085
+ var hits = m.queryRenderedFeatures(e.point, { layers: [main] });
28086
+ var f = hits && hits[0];
28087
+ if (!f) { if (def.hovered) { self._setHover(id, def, null); } return; }
28088
+ if (def.hovered && def.hovered.key === keyOf(f)) { return; }
28089
+ self._setHover(id, def, f, keyOf(f));
28090
+ });
28091
+ this._listen(id, 'mouseout', null, function () { if (def.hovered && !def.pinned) { self._setHover(id, def, null); } });
28092
+ };
28093
+
28094
+ SMap.prototype._unpin = function () {
28095
+ var def = this._pinnedDef;
28096
+ this._pinnedDef = null;
28097
+ if (def) { def.pinned = null; this._setHover(null, def, null); }
28098
+ };
28099
+
28100
+ SMap.prototype._setHover = function (id, def, f, key) {
28101
+ var source = def.hoverSource && this.engine.getSource(def.hoverSource);
28102
+ def.hovered = f ? { key: key } : null;
28103
+ if (!source) { return; }
28104
+ // A plain copy: the engine's own feature objects cannot be sent to its worker.
28105
+ source.setData({ type: 'FeatureCollection', features: f ? [JSON.parse(JSON.stringify({ type: 'Feature', properties: f.properties || {}, geometry: f.geometry }))] : [] });
28106
+ };
28107
+
27450
28108
  SMap.prototype._listen = function (id, type, layer, fn) {
27451
- this.engine.on(type, layer, fn);
28109
+ if (layer) { this.engine.on(type, layer, fn); } else { this.engine.on(type, fn); }
27452
28110
  (this._handlers[id] = this._handlers[id] || []).push([type, layer, fn]);
27453
28111
  };
27454
28112
 
27455
- return { Map: SMap, version: VERSION, styles: FLAVORS.slice(), engine: engine };
28113
+ return { Map: SMap, version: VERSION, styles: FLAVORS.slice(), engine: engine, popupTemplate: popupTemplate };
27456
28114
  });
27457
28115
 
27458
28116
  return module.exports;
@@ -27472,13 +28130,20 @@ function SMapMap(options) {
27472
28130
  SMapMap.prototype = core.Map.prototype;
27473
28131
  return new core.Map(options);
27474
28132
  }
27475
- var smapVersion = "1.0.2";
28133
+ var smapVersion = "1.0.6";
27476
28134
  var smapStyles = ['light', 'white', 'grayscale', 'dark', 'black'];
27477
28135
  /** The bundled rendering engine, for anything the SDK does not wrap (loaded on the first call). */
27478
28136
  function smapGetEngine() { return smapLoad().engine; }
27479
- var smapDefault = { Map: SMapMap, version: smapVersion, styles: smapStyles, getEngine: smapGetEngine };
28137
+ // Templates need no engine: before the first map they come from an SDK made without one.
28138
+ var smapNoEngine = null;
28139
+ function smapPopupTemplate(template) {
28140
+ if (!smapCore && !smapNoEngine) { smapNoEngine = smapCreateSdk(null); }
28141
+ return (smapCore || smapNoEngine).popupTemplate(template);
28142
+ }
28143
+ var smapDefault = { Map: SMapMap, version: smapVersion, styles: smapStyles, getEngine: smapGetEngine, popupTemplate: smapPopupTemplate };
27480
28144
  exports.Map = SMapMap;
27481
28145
  exports.version = smapVersion;
27482
28146
  exports.styles = smapStyles;
27483
28147
  exports.getEngine = smapGetEngine;
28148
+ exports.popupTemplate = smapPopupTemplate;
27484
28149
  exports.default = smapDefault;