@superrouteca/smap 1.0.1 → 1.0.5

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.js CHANGED
@@ -1,6 +1,7 @@
1
- /*! sMap Web SDK 1.0.1. Bundles maplibre-gl 6.16.0 (BSD-3-Clause), see vendor-licenses.txt. */
1
+ /*! sMap Web SDK 1.0.5. Bundles maplibre-gl 6.16.0 (BSD-3-Clause), see vendor-licenses.txt. */
2
2
  (function () {
3
3
  function smapCreateEngine() {
4
+ var SMAP_BASE_URL = (typeof document !== 'undefined' && document.baseURI) || (typeof location !== 'undefined' && location.href) || 'about:blank';
4
5
  var smapEngineExports = (() => {
5
6
  var __defProp = Object.defineProperty;
6
7
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -4596,7 +4597,7 @@ Use an identity property function instead: \`{ "type": "identity", "property": $
4596
4597
  }
4597
4598
  }
4598
4599
  function el() {
4599
- let e66 = "";
4600
+ let e66 = SMAP_BASE_URL;
4600
4601
  if (!/^https?:/.test(e66)) return ``;
4601
4602
  let t2 = e66.endsWith(`-dev.mjs`) ? `maplibre-gl-worker-dev.mjs` : `maplibre-gl-worker.mjs`, n2 = new URL(`./${t2}`, e66);
4602
4603
  return n2.searchParams.set(`v`, l), n2.href;
@@ -4619,7 +4620,7 @@ Use an identity property function instead: \`{ "type": "identity", "property": $
4619
4620
  return URL.createObjectURL(r2);
4620
4621
  }
4621
4622
  function il(e66) {
4622
- let t2 = new Blob([`import ${JSON.stringify(new URL(e66, "").href)}`], { type: `text/javascript` });
4623
+ let t2 = new Blob([`import ${JSON.stringify(new URL(e66, SMAP_BASE_URL).href)}`], { type: `text/javascript` });
4623
4624
  return URL.createObjectURL(t2);
4624
4625
  }
4625
4626
  async function al() {
@@ -26862,14 +26863,14 @@ function smapCreateSdk(engine) {
26862
26863
  */
26863
26864
  (function (root, factory) {
26864
26865
  if (typeof module === 'object' && module.exports) {
26865
- module.exports = factory(root.maplibregl || require('maplibre-gl'));
26866
+ module.exports = factory(root.maplibregl !== undefined ? root.maplibregl : require('maplibre-gl'));
26866
26867
  } else {
26867
26868
  root.sMap = factory(root.maplibregl);
26868
26869
  }
26869
26870
  })(typeof self !== 'undefined' ? self : this, function (engine) {
26870
26871
  'use strict';
26871
26872
 
26872
- var VERSION = '1.0.1';
26873
+ var VERSION = '1.0.5';
26873
26874
  var FLAVORS = ['light', 'white', 'grayscale', 'dark', 'black'];
26874
26875
  var BASEMAP_SOURCE = 'basemap';
26875
26876
  var BUILDINGS_LAYER = 'smap-buildings-3d';
@@ -26885,10 +26886,16 @@ function smapCreateSdk(engine) {
26885
26886
  } catch (e) { return 0; }
26886
26887
  }
26887
26888
 
26889
+ var GEOMETRY_TYPES = ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection'];
26890
+
26888
26891
  function toFeatureCollection(data) {
26889
26892
  if (!data) { return { type: 'FeatureCollection', features: [] }; }
26890
26893
  if (data.type === 'FeatureCollection') { return data; }
26891
26894
  if (data.type === 'Feature') { return { type: 'FeatureCollection', features: [data] }; }
26895
+ // A bare geometry (a LineString from a directions answer, a Polygon, …) is GeoJSON too: one feature without properties.
26896
+ if (GEOMETRY_TYPES.indexOf(data.type) !== -1) {
26897
+ return { type: 'FeatureCollection', features: [{ type: 'Feature', properties: {}, geometry: data }] };
26898
+ }
26892
26899
  if (Array.isArray(data)) {
26893
26900
  var features = new Array(data.length);
26894
26901
  for (var i = 0; i < data.length; i++) {
@@ -26962,6 +26969,203 @@ function smapCreateSdk(engine) {
26962
26969
  };
26963
26970
  }
26964
26971
 
26972
+ // ------------------------------------------------------------------ popups
26973
+
26974
+ var POPUP_OPTIONS = ['className', 'maxWidth', 'minWidth', 'offset', 'anchor', 'closeButton', 'closeOnClick', 'focusAfterOpen', 'autoPan'];
26975
+ // Shown while a popup's content is on its way: three pulsing dots, no text.
26976
+ var POPUP_LOADING = '<div class="smap-popup-loading" role="status"><span></span><span></span><span></span></div>';
26977
+ var HTML_ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
26978
+
26979
+ function escapeHtml(value) {
26980
+ return String(value).replace(/[&<>"']/g, function (c) { return HTML_ESCAPES[c]; });
26981
+ }
26982
+
26983
+ /**
26984
+ * A popup function from a template: every {name} is replaced by the feature's property of that name, HTML-escaped
26985
+ * (empty when the feature has none). The template itself is trusted HTML. Compiled once; nothing runs until a click.
26986
+ * popupTemplate('<b>{name}</b><br>{address}')
26987
+ */
26988
+ function popupTemplate(template) {
26989
+ // Even entries are the template's own text, odd entries property names.
26990
+ var parts = String(template).split(/\{([^{}]+)\}/);
26991
+ for (var i = 1; i < parts.length; i += 2) { parts[i] = parts[i].trim(); }
26992
+ return function (properties) {
26993
+ var out = parts[0];
26994
+ for (var j = 1; j < parts.length; j += 2) {
26995
+ var v = properties ? properties[parts[j]] : undefined;
26996
+ out += (v === undefined || v === null ? '' : escapeHtml(v)) + parts[j + 1];
26997
+ }
26998
+ return out;
26999
+ };
27000
+ }
27001
+
27002
+ function isThenable(v) { return !!v && (typeof v === 'object' || typeof v === 'function') && typeof v.then === 'function'; }
27003
+
27004
+ function cssSize(v) { return typeof v === 'number' ? v + 'px' : v; }
27005
+
27006
+ // ------------------------------------------------------------------ drawn images (pins, numbers, badges, arrows)
27007
+
27008
+ // A style value: a constant, {property: 'name'} (the feature's property, `default` when it has none), or an engine
27009
+ // expression (an array).
27010
+ function valueExpr(v, fallback) {
27011
+ if (v === undefined || v === null) { return fallback; }
27012
+ if (Array.isArray(v)) { return v; }
27013
+ if (typeof v === 'object' && v.property) {
27014
+ return v['default'] !== undefined ? ['coalesce', ['get', v.property], v['default']] : ['get', v.property];
27015
+ }
27016
+ return v;
27017
+ }
27018
+
27019
+ // Image ids carry what the image shows; the map draws each one the first time a feature needs it (see _drawImage).
27020
+ var IMAGE_PREFIX = 'smap-img|';
27021
+ var SQRT1_2 = Math.SQRT1_2;
27022
+
27023
+ // The pin of Superroute's route map: a square with three rounded corners turned 45 degrees, its sharp corner the tip.
27024
+ // tipToHead: from the position up to the centre of the round part (where the number goes); badge: the badge's centre
27025
+ // from the position (right of the round part, as the page's badge sits on the turned pin's corner).
27026
+ function markerGeometry(shape, size) {
27027
+ var k = size / 32;
27028
+ if (shape === 'circle') {
27029
+ return { k: k, tipToHead: 0, badge: [size * 0.36, -size * 0.36] };
27030
+ }
27031
+ var half = size * SQRT1_2; // the centre of the round part, above the tip
27032
+ return { k: k, tipToHead: half, badge: [14.1 * k, -half] };
27033
+ }
27034
+
27035
+ // A rounded rectangle path whose corners are ellipses: radii [[rx, ry] top-left, top-right, bottom-right, bottom-left].
27036
+ function roundedPath(ctx, x, y, w, h, r) {
27037
+ ctx.beginPath();
27038
+ ctx.moveTo(x + r[0][0], y);
27039
+ ctx.lineTo(x + w - r[1][0], y);
27040
+ 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); }
27041
+ ctx.lineTo(x + w, y + h - r[2][1]);
27042
+ 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); }
27043
+ ctx.lineTo(x + r[3][0], y + h);
27044
+ 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); }
27045
+ ctx.lineTo(x, y + r[0][1]);
27046
+ 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); }
27047
+ ctx.closePath();
27048
+ }
27049
+
27050
+ var PIN_PAD = 6; // room for the shadow around a pin, in pixels
27051
+
27052
+ // Each draw function gets a 2D context already scaled to the pixel ratio and returns nothing; sizes are in pixels.
27053
+ var IMAGE_KINDS = {
27054
+ // pin|shape|fill|border|borderWidth|size
27055
+ pin: function (args) {
27056
+ var shape = args[0], fill = args[1], border = args[2], bw = Number(args[3]), size = Number(args[4]);
27057
+ var g = markerGeometry(shape, size);
27058
+ var reach = shape === 'circle' ? size / 2 : g.tipToHead; // farthest the shape goes from its centre
27059
+ var w = Math.ceil(2 * reach + 2 * PIN_PAD), h = w;
27060
+ return {
27061
+ width: w, height: h,
27062
+ draw: function (ctx, ratio) {
27063
+ var c = w / 2;
27064
+ ctx.save();
27065
+ ctx.translate(c, c);
27066
+ var s = size / 2;
27067
+ var outer, inner;
27068
+ if (shape === 'circle') {
27069
+ outer = function () { ctx.beginPath(); ctx.arc(0, 0, s, 0, Math.PI * 2); };
27070
+ inner = function () { ctx.beginPath(); ctx.arc(0, 0, Math.max(0, s - bw), 0, Math.PI * 2); };
27071
+ ctx.shadowOffsetY = 3 * ratio;
27072
+ } else {
27073
+ ctx.rotate(Math.PI / 4);
27074
+ outer = function () { roundedPath(ctx, -s, -s, size, size, [[s, s], [s, size], [0, 0], [size, s]]); };
27075
+ var t = Math.max(0, s - bw), r2 = Math.max(0, size - bw);
27076
+ inner = function () { roundedPath(ctx, -s + bw, -s + bw, size - 2 * bw, size - 2 * bw, [[t, t], [t, r2], [0, 0], [r2, t]]); };
27077
+ // The page's box-shadow (0 3px) turns with the pin.
27078
+ ctx.shadowOffsetX = -3 * SQRT1_2 * ratio;
27079
+ ctx.shadowOffsetY = 3 * SQRT1_2 * ratio;
27080
+ }
27081
+ ctx.shadowColor = 'rgba(0,0,0,0.3)';
27082
+ ctx.shadowBlur = 8 * ratio;
27083
+ outer();
27084
+ ctx.fillStyle = border;
27085
+ ctx.fill();
27086
+ ctx.shadowColor = 'transparent';
27087
+ inner();
27088
+ ctx.fillStyle = fill;
27089
+ ctx.fill();
27090
+ ctx.restore();
27091
+ },
27092
+ };
27093
+ },
27094
+ // num|color|size|text (bold, with the route map's text shadow)
27095
+ num: function (args, measure) {
27096
+ var color = args[0], size = Number(args[1]), text = args.slice(2).join('|');
27097
+ var px = 14 * size / 32;
27098
+ var font = 'bold ' + px + 'px monospace';
27099
+ var w = Math.ceil(measure(font, text) + 6), h = Math.ceil(px * 1.4 + 4);
27100
+ return {
27101
+ width: w, height: h,
27102
+ draw: function (ctx, ratio) {
27103
+ ctx.font = font;
27104
+ ctx.textAlign = 'center';
27105
+ ctx.textBaseline = 'middle';
27106
+ ctx.shadowColor = 'rgba(0,0,0,0.5)';
27107
+ ctx.shadowOffsetX = ratio;
27108
+ ctx.shadowOffsetY = ratio;
27109
+ ctx.shadowBlur = 2 * ratio;
27110
+ ctx.fillStyle = color;
27111
+ ctx.fillText(text, w / 2, h / 2);
27112
+ },
27113
+ };
27114
+ },
27115
+ // badge|text (red circle, white border, at least 18 px inside the border)
27116
+ badge: function (args, measure) {
27117
+ var text = args.join('|');
27118
+ var font = 'bold 10px sans-serif';
27119
+ var inner = Math.max(18, Math.ceil(measure(font, text) + 4));
27120
+ var w = inner + 4 + 6, h = 18 + 4 + 6;
27121
+ return {
27122
+ width: w, height: h,
27123
+ draw: function (ctx, ratio) {
27124
+ var bw = inner + 4, bh = 22, x = 3, y = 2;
27125
+ var r = bh / 2;
27126
+ ctx.shadowColor = 'rgba(0,0,0,0.4)';
27127
+ ctx.shadowOffsetY = 2 * ratio;
27128
+ ctx.shadowBlur = 4 * ratio;
27129
+ roundedPath(ctx, x, y, bw, bh, [[r, r], [r, r], [r, r], [r, r]]);
27130
+ ctx.fillStyle = '#ffffff';
27131
+ ctx.fill();
27132
+ ctx.shadowColor = 'transparent';
27133
+ 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]]);
27134
+ ctx.fillStyle = '#e74c3c';
27135
+ ctx.fill();
27136
+ ctx.font = font;
27137
+ ctx.textAlign = 'center';
27138
+ ctx.textBaseline = 'middle';
27139
+ ctx.fillStyle = '#ffffff';
27140
+ ctx.fillText(text, x + bw / 2, y + bh / 2 + 0.5);
27141
+ },
27142
+ };
27143
+ },
27144
+ // arrow|size|color (an open arrowhead pointing along the line, like the route map's)
27145
+ arrow: function (args) {
27146
+ var size = Number(args[0]), color = args.slice(1).join('|');
27147
+ var w = Math.ceil(size + 4), h = w;
27148
+ return {
27149
+ width: w, height: h,
27150
+ draw: function (ctx) {
27151
+ var len = size, back = len * Math.cos(Math.PI / 6), half = len * Math.sin(Math.PI / 6);
27152
+ var tipX = w / 2 + back / 2, cy = h / 2;
27153
+ ctx.strokeStyle = color;
27154
+ ctx.lineWidth = 2;
27155
+ ctx.lineCap = 'round';
27156
+ ctx.lineJoin = 'round';
27157
+ ctx.beginPath();
27158
+ ctx.moveTo(tipX - back, cy - half);
27159
+ ctx.lineTo(tipX, cy);
27160
+ ctx.lineTo(tipX - back, cy + half);
27161
+ ctx.stroke();
27162
+ },
27163
+ };
27164
+ },
27165
+ };
27166
+
27167
+ function drawnImageId(kind, parts) { return ['concat', IMAGE_PREFIX + kind].concat(parts.reduce(function (a, p) { return a.concat(['|', p]); }, [])); }
27168
+
26965
27169
  var NODE_TTL = 3600 * 1000;
26966
27170
 
26967
27171
  function timed(url, ms) {
@@ -27042,6 +27246,9 @@ function smapCreateSdk(engine) {
27042
27246
  * @param {boolean} [options.nearest=true] read from the nearest sMap node (asked of baseUrl, remembered for an hour)
27043
27247
  * @param {string[]} [options.nodes] node URLs known in advance, used while baseUrl is down
27044
27248
  * @param {'mercator'|'globe'} [options.projection]
27249
+ * @param {Object} [options.popup] default popup options of every layer and openPopup (className, maxWidth,
27250
+ * minWidth, offset, anchor, closeButton, closeOnClick, focusAfterOpen)
27251
+ * @param {string|Node} [options.popupLoading] shown while a popup's content is a pending promise (default: dots)
27045
27252
  * Every other option (center, zoom, pitch, bearing, minZoom, maxZoom, maxBounds, hash, …) goes to the engine.
27046
27253
  */
27047
27254
  function SMap(options) {
@@ -27061,6 +27268,12 @@ function smapCreateSdk(engine) {
27061
27268
  this._buildings = false;
27062
27269
  this._handlers = {};
27063
27270
  this._refreshTimer = null;
27271
+ this._popupDefaults = options.popup || {};
27272
+ this._popupLoading = options.popupLoading;
27273
+ this._autoZoom = options.autoZoom !== false;
27274
+ this._wasd = options.keyboard3D;
27275
+ this._popup = null;
27276
+ this._popupSeq = 0;
27064
27277
  // True between a style's load and the next style change. The engine's own isStyleLoaded() turns false while any
27065
27278
  // source is loading data, which would leave layers added meanwhile waiting for a load that never comes.
27066
27279
  this._styleReady = false;
@@ -27068,7 +27281,7 @@ function smapCreateSdk(engine) {
27068
27281
  var self = this;
27069
27282
  var engineOptions = {};
27070
27283
  for (var k in options) {
27071
- if (Object.prototype.hasOwnProperty.call(options, k) && ['token', 'getToken', 'apiKey', 'style', 'language', 'baseUrl', 'projection', 'nearest', 'nodes'].indexOf(k) < 0) {
27284
+ if (Object.prototype.hasOwnProperty.call(options, k) && ['token', 'getToken', 'apiKey', 'style', 'language', 'baseUrl', 'projection', 'nearest', 'nodes', 'popup', 'popupLoading', 'autoZoom', 'keyboard3D'].indexOf(k) < 0) {
27072
27285
  engineOptions[k] = options[k];
27073
27286
  }
27074
27287
  }
@@ -27101,6 +27314,14 @@ function smapCreateSdk(engine) {
27101
27314
  badge._place();
27102
27315
  }
27103
27316
  self.engine.on('style.load', function () { self._styleReady = true; self._restore(); });
27317
+ self._keyboard3D();
27318
+ // One click handler for every layer's popup: the engine's hit test over those layers, the top feature opens.
27319
+ self.engine.on('click', function (e) { self._popupClick(e); });
27320
+ self._styleClass();
27321
+ // Pins, numbers, badges and arrows are drawn the first time a feature needs one (and again after a style change).
27322
+ if (self.engine.setMissingStyleImageResolver) {
27323
+ self.engine.setMissingStyleImageResolver(function (id) { self._drawImage(id); });
27324
+ }
27104
27325
  return new Promise(function (resolve) { self.engine.once('load', function () { resolve(self); }); });
27105
27326
  });
27106
27327
  if (this._token && this._getToken) { this._scheduleRefresh(); }
@@ -27136,10 +27357,242 @@ function smapCreateSdk(engine) {
27136
27357
  if (FLAVORS.indexOf(style) >= 0) { this._flavor = style; this._customStyle = null; } else { this._customStyle = style; }
27137
27358
  this._styleReady = false;
27138
27359
  this.engine.setStyle(this._styleUrl());
27360
+ this._styleClass();
27361
+ return this;
27362
+ };
27363
+
27364
+ // The map container carries smap-style-<scheme>, so the stylesheet can follow the scheme (dark popups).
27365
+ SMap.prototype._styleClass = function () {
27366
+ var c = this.engine && this.engine.getContainer && this.engine.getContainer();
27367
+ if (!c || !c.classList) { return; }
27368
+ for (var i = 0; i < FLAVORS.length; i++) { c.classList.remove('smap-style-' + FLAVORS[i]); }
27369
+ if (!this._customStyle) { c.classList.add('smap-style-' + this._flavor); }
27370
+ };
27371
+
27372
+ // Draws an image the SDK named (IMAGE_PREFIX + kind|args) and adds it to the map; other names are left alone.
27373
+ SMap.prototype._drawImage = function (id) {
27374
+ if (typeof id !== 'string' || id.indexOf(IMAGE_PREFIX) !== 0) { return; }
27375
+ var m = this.engine;
27376
+ if (m.hasImage(id)) { return; }
27377
+ var parts = id.slice(IMAGE_PREFIX.length).split('|');
27378
+ var kind = IMAGE_KINDS[parts[0]];
27379
+ if (!kind) { return; }
27380
+ var doc = m.getContainer().ownerDocument;
27381
+ var win = doc.defaultView || {};
27382
+ var ratio = Math.min(3, Math.max(1, Math.ceil(win.devicePixelRatio || 1)));
27383
+ var canvas = doc.createElement('canvas');
27384
+ var ctx = canvas.getContext('2d');
27385
+ var measure = function (font, text) { ctx.font = font; return ctx.measureText(text).width; };
27386
+ var image = kind(parts.slice(1), measure);
27387
+ canvas.width = Math.ceil(image.width * ratio);
27388
+ canvas.height = Math.ceil(image.height * ratio);
27389
+ ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
27390
+ image.draw(ctx, ratio);
27391
+ m.addImage(id, ctx.getImageData(0, 0, canvas.width, canvas.height), { pixelRatio: ratio });
27392
+ this._imagesDrawn = (this._imagesDrawn || 0) + 1;
27393
+ };
27394
+
27395
+ /**
27396
+ * Opens the map's popup at a position, closing the one open before (there is one popup per map).
27397
+ * @param {Array|Object} lngLat [lng, lat] or {lng, lat}
27398
+ * @param {string|Node|Promise} content trusted HTML (inserted as is, never sanitized), a DOM node, or a promise
27399
+ * of either (popupLoading shows meanwhile; a rejection closes the popup
27400
+ * and is reported as the map's 'error' event)
27401
+ * @param {Object} [options] popup options over the map's `popup` defaults, and `loading`
27402
+ */
27403
+ SMap.prototype.openPopup = function (lngLat, content, options) {
27404
+ var self = this;
27405
+ if (!this.engine) { this.ready.then(function () { self.openPopup(lngLat, content, options); }); return this; }
27406
+ this.closePopup();
27407
+ if (!content) { return this; }
27408
+ var seq = ++this._popupSeq;
27409
+ var o = Object.assign({}, this._popupDefaults, options || {});
27410
+ var engineOptions = { offset: 12, className: 'smap-popup' + (o.className ? ' ' + o.className : '') };
27411
+ for (var i = 0; i < POPUP_OPTIONS.length; i++) {
27412
+ var k = POPUP_OPTIONS[i];
27413
+ if (k !== 'className' && k !== 'minWidth' && k !== 'autoPan' && o[k] !== undefined) { engineOptions[k] = k === 'maxWidth' ? cssSize(o[k]) : o[k]; }
27414
+ }
27415
+ var popup = new engine.Popup(engineOptions).setLngLat(lngLat);
27416
+ this._popup = popup;
27417
+ var pending = isThenable(content);
27418
+ var loading = o.loading !== undefined ? o.loading : (this._popupLoading !== undefined ? this._popupLoading : POPUP_LOADING);
27419
+ this._popupContent(popup, pending ? loading : content);
27420
+ popup.addTo(this.engine);
27421
+ var el = popup.getElement && popup.getElement();
27422
+ if (el && o.minWidth !== undefined) { el.style.minWidth = cssSize(o.minWidth); }
27423
+ if (o.autoPan !== false) {
27424
+ // After the engine has placed it (next frame), so its box is final.
27425
+ var raf = (typeof requestAnimationFrame === 'function') ? requestAnimationFrame : function (fn) { return setTimeout(fn, 16); };
27426
+ raf(function () { if (self._popup === popup) { self._panIntoView(popup); } });
27427
+ }
27428
+ if (pending) {
27429
+ content.then(function (answer) {
27430
+ // A later popup, or a close, makes this answer stale.
27431
+ if (seq !== self._popupSeq || self._popup !== popup) { return; }
27432
+ if (answer) { self._popupContent(popup, answer); } else { self.closePopup(); }
27433
+ }, function (error) {
27434
+ if (seq !== self._popupSeq || self._popup !== popup) { return; }
27435
+ self.closePopup();
27436
+ self._reportError(error);
27437
+ });
27438
+ }
27439
+ return this;
27440
+ };
27441
+
27442
+ /** Pans the map so the popup is fully inside it, with a small margin (Leaflet's autoPan). */
27443
+ SMap.prototype._panIntoView = function (popup) {
27444
+ var m = this.engine, el = popup.getElement && popup.getElement();
27445
+ if (!el || !m.getContainer || !m.panBy || !el.getBoundingClientRect) { return; }
27446
+ var box = el.getBoundingClientRect(), frame = m.getContainer().getBoundingClientRect(), pad = 10, dx = 0, dy = 0;
27447
+ 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; }
27448
+ 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; }
27449
+ if (dx || dy) { m.panBy([dx, dy], { duration: 250 }); }
27450
+ };
27451
+
27452
+ /** Closes the map's popup, if one is open. */
27453
+ SMap.prototype.closePopup = function () {
27454
+ var popup = this._popup;
27455
+ this._popup = null;
27456
+ this._popupSeq++;
27457
+ if (popup) { popup.remove(); }
27139
27458
  return this;
27140
27459
  };
27141
27460
 
27461
+ SMap.prototype._popupContent = function (popup, content) {
27462
+ if (typeof content === 'string') { popup.setHTML(content); } else { popup.setDOMContent(content); }
27463
+ };
27464
+
27465
+ SMap.prototype._reportError = function (error) {
27466
+ var m = this.engine;
27467
+ if (m && engine.ErrorEvent && m.fire) { m.fire(new engine.ErrorEvent(error)); } else if (typeof console !== 'undefined') { console.error(error); }
27468
+ };
27469
+
27470
+ // A click on the map: the top feature of the layers with a popup, if any, opens its layer's popup.
27471
+ SMap.prototype._popupClick = function (e) {
27472
+ var m = this.engine;
27473
+ var ids = [];
27474
+ var byEngineLayer = {};
27475
+ for (var i = 0; i < this._order.length; i++) {
27476
+ var def = this._layers[this._order[i]];
27477
+ if (def.o.popup && def.popupLayer && !def.hidden && m.getLayer(def.popupLayer)) {
27478
+ ids.push(def.popupLayer);
27479
+ byEngineLayer[def.popupLayer] = def;
27480
+ if (def.hoverLayer && m.getLayer(def.hoverLayer)) { ids.push(def.hoverLayer); byEngineLayer[def.hoverLayer] = def; }
27481
+ }
27482
+ }
27483
+ if (!ids.length) { return; }
27484
+ var hits = m.queryRenderedFeatures(e.point, { layers: ids });
27485
+ if (!hits || !hits.length) { return; }
27486
+ var f = hits[0];
27487
+ var hit = byEngineLayer[f.layer && f.layer.id];
27488
+ if (!hit) { return; }
27489
+ // A tight pile of pins: zoom in around the click so they come apart, or, when they still overlap close up (or
27490
+ // share one spot), offer a chooser. A layer turns this off with marker.overlap === false.
27491
+ if (this._autoZoom && hit.o.marker && hit.o.marker.overlap !== false && e.point && m.project) {
27492
+ var ov = hit.o.marker.overlap || {}, near = ov.radius || 12, threshold = ov.threshold || 4;
27493
+ var box = [[e.point.x - near, e.point.y - near], [e.point.x + near, e.point.y + near]];
27494
+ var seen = {}, pile = [];
27495
+ (m.queryRenderedFeatures(box, { layers: ids }) || []).forEach(function (x) {
27496
+ var d = byEngineLayer[x.layer && x.layer.id];
27497
+ if (d !== hit || !x.geometry || x.geometry.type !== 'Point') { return; }
27498
+ var key = JSON.stringify(x.geometry.coordinates) + JSON.stringify(x.properties || {});
27499
+ if (!seen[key]) { seen[key] = true; pile.push(x); }
27500
+ });
27501
+ if (pile.length >= 2) {
27502
+ var spots = {};
27503
+ pile.forEach(function (x) { spots[x.geometry.coordinates.map(function (c) { return c.toFixed(6); }).join(',')] = true; });
27504
+ var oneSpot = Object.keys(spots).length === 1;
27505
+ var closeUp = m.getZoom() >= (ov.maxZoom || 17);
27506
+ if (pile.length >= threshold && !oneSpot && !closeUp) {
27507
+ m.easeTo({ center: e.lngLat, zoom: Math.min(m.getZoom() + (ov.zoomBy || 2), ov.maxZoom || 17), duration: 400 });
27508
+ return;
27509
+ }
27510
+ if (pile.length >= threshold || oneSpot) { this._chooser(hit, pile, e.lngLat); return; }
27511
+ }
27512
+ }
27513
+ this._openFeaturePopup(hit, f, e.lngLat);
27514
+ };
27515
+
27516
+ /** A small list of the pins under a click; choosing one opens its popup. */
27517
+ SMap.prototype._chooser = function (hit, pile, lngLat) {
27518
+ var self = this, mk = hit.o.marker, doc = this.engine.getContainer().ownerDocument;
27519
+ var list = doc.createElement('div');
27520
+ list.className = 'smap-chooser';
27521
+ var esc = function (v) { return String(v == null ? '' : v).replace(/[&<>"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]; }); };
27522
+ var plainValue = function (v, p) {
27523
+ if (v && typeof v === 'object' && !Array.isArray(v) && v.property) { return p[v.property] !== undefined ? p[v.property] : v.default; }
27524
+ return Array.isArray(v) ? undefined : v;
27525
+ };
27526
+ pile.slice(0, 50).forEach(function (x, i) {
27527
+ var p = x.properties || {};
27528
+ var label = mk.overlap && typeof mk.overlap.label === 'function' ? mk.overlap.label(p, x) : null;
27529
+ var number = plainValue(mk.text, p), fill = plainValue(mk.fill, p) || '#ffffff', border = plainValue(mk.border, p) || hit.o.color || '#4f46e5';
27530
+ var item = doc.createElement('button');
27531
+ item.type = 'button';
27532
+ item.className = 'smap-chooser-item';
27533
+ item.innerHTML = '<span class="smap-chooser-pin" style="background:' + esc(fill) + ';border-color:' + esc(border) + '">' + esc(number == null ? '' : number) + '</span>'
27534
+ + '<span class="smap-chooser-label">' + esc(label != null ? label : (number != null ? number : i + 1)) + '</span>';
27535
+ item.addEventListener('click', function (ev) { ev.stopPropagation(); self._openFeaturePopup(hit, x, x.geometry.coordinates); });
27536
+ list.appendChild(item);
27537
+ });
27538
+ this._unpin();
27539
+ this.openPopup(lngLat, list, { className: 'smap-chooser-popup', anchor: 'bottom', offset: 12 });
27540
+ };
27541
+
27542
+ /** Opens a feature's own popup (the layer's popup function), above a pin when the layer draws pins. */
27543
+ SMap.prototype._openFeaturePopup = function (hit, f, lngLat) {
27544
+ var m = this.engine;
27545
+ // Points open at their own position; lines and areas where they were clicked.
27546
+ var at = hit.kind === 'points' && f.geometry && f.geometry.type === 'Point' ? f.geometry.coordinates : lngLat;
27547
+ var content;
27548
+ try { content = hit.o.popup(f.properties || {}, f); } catch (error) { this._reportError(error); return; }
27549
+ var popupOptions = Object.assign({}, hit.o.popupOptions || {});
27550
+ if (hit.o.marker && popupOptions.offset === undefined && this._popupDefaults.offset === undefined) {
27551
+ // Just above the pin as it is drawn now (its scale may follow the zoom).
27552
+ var mk = hit.o.marker, size = mk.size || 32, k = scaleAt(mk.scale, m.getZoom ? m.getZoom() : 0);
27553
+ popupOptions.offset = mk.shape === 'circle' ? Math.round((size / 2) * k + 4) : Math.round((markerGeometry('pin', size).tipToHead + size / 2) * k + 2);
27554
+ }
27555
+ // Above the pin, always (as on Superroute's route map); the map pans to make room instead of flipping it below.
27556
+ if (hit.o.marker && popupOptions.anchor === undefined && this._popupDefaults.anchor === undefined) { popupOptions.anchor = 'bottom'; }
27557
+ if (hit.o.popupLoading !== undefined) { popupOptions.loading = hit.o.popupLoading; }
27558
+ this._unpin();
27559
+ this.openPopup(at, content, popupOptions);
27560
+ // The clicked pin stays in front while its popup is open (the route map's rule).
27561
+ if (hit.hoverSource && this._popup) {
27562
+ var self = this, popup = this._popup;
27563
+ this._setHover(null, hit, f, 'popup');
27564
+ hit.pinned = popup;
27565
+ this._pinnedDef = hit;
27566
+ if (popup.on) { popup.on('close', function () { if (hit.pinned === popup) { self._unpin(); } }); }
27567
+ }
27568
+ };
27569
+
27142
27570
  /** Switches the label language; your layers are kept. */
27571
+ /**
27572
+ * A marker scale at a zoom: a number, or the linear / step zoom expressions the SDK documents
27573
+ * (['interpolate', ['linear'], ['zoom'], z, v, …], ['step', ['zoom'], v, z, v, …]); anything else counts as 1.
27574
+ */
27575
+ function scaleAt(scale, zoom) {
27576
+ if (scale === undefined || scale === null) { return 1; }
27577
+ if (typeof scale === 'number') { return scale; }
27578
+ if (!Array.isArray(scale)) { return 1; }
27579
+ var isZoom = function (x) { return Array.isArray(x) && x[0] === 'zoom'; };
27580
+ if (scale[0] === 'interpolate' && isZoom(scale[2])) {
27581
+ var stops = scale.slice(3);
27582
+ if (zoom <= stops[0]) { return stops[1]; }
27583
+ for (var i = 2; i < stops.length; i += 2) {
27584
+ 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]); }
27585
+ }
27586
+ return stops[stops.length - 1];
27587
+ }
27588
+ if (scale[0] === 'step' && isZoom(scale[1])) {
27589
+ var value = scale[2];
27590
+ for (var j = 3; j < scale.length; j += 2) { if (zoom >= scale[j]) { value = scale[j + 1]; } }
27591
+ return value;
27592
+ }
27593
+ return 1;
27594
+ }
27595
+
27143
27596
  SMap.prototype.setLanguage = function (language) {
27144
27597
  this._language = language;
27145
27598
  if (!this._customStyle) { this._styleReady = false; this.engine.setStyle(this._styleUrl()); }
@@ -27154,6 +27607,33 @@ function smapCreateSdk(engine) {
27154
27607
  };
27155
27608
 
27156
27609
  /** Shows buildings in 3D (and tilts the map when it is flat). */
27610
+ /**
27611
+ * Auto Zoom: a click on pins piled up zooms the map in around it, or lists them close up (see marker.overlap).
27612
+ * On by default (`autoZoom: false` in the options turns it off).
27613
+ */
27614
+ SMap.prototype.setAutoZoom = function (on) { this._autoZoom = !!on; return this; };
27615
+ SMap.prototype.autoZoom = function () { return this._autoZoom; };
27616
+
27617
+ /**
27618
+ * In 3D (3D buildings on, or the map tilted) W / S tilt the view and A / D turn it; Shift takes bigger steps. Only
27619
+ * while the map has the keyboard focus, so typing elsewhere on the page is never taken.
27620
+ */
27621
+ SMap.prototype._keyboard3D = function () {
27622
+ var self = this, m = this.engine;
27623
+ var el = m.getCanvasContainer ? m.getCanvasContainer() : (m.getContainer ? m.getContainer() : null);
27624
+ if (!el || !el.addEventListener || this._wasd === false) { return; }
27625
+ el.addEventListener('keydown', function (e) {
27626
+ if (e.altKey || e.ctrlKey || e.metaKey) { return; }
27627
+ if (!self._buildings && m.getPitch() < 1) { return; }
27628
+ var big = e.shiftKey, key = (e.key || '').toLowerCase();
27629
+ var pitch = m.getPitch(), bearing = m.getBearing();
27630
+ if (key === 'w') { pitch = Math.min(85, pitch + (big ? 15 : 5)); } else if (key === 's') { pitch = Math.max(0, pitch - (big ? 15 : 5)); }
27631
+ else if (key === 'a') { bearing -= big ? 45 : 15; } else if (key === 'd') { bearing += big ? 45 : 15; } else { return; }
27632
+ e.preventDefault();
27633
+ m.easeTo({ pitch: pitch, bearing: bearing, duration: 200 });
27634
+ });
27635
+ };
27636
+
27157
27637
  SMap.prototype.setBuildings3D = function (on, options) {
27158
27638
  this._buildings = !!on;
27159
27639
  this._buildingOptions = options || this._buildingOptions || {};
@@ -27192,8 +27672,9 @@ function smapCreateSdk(engine) {
27192
27672
  * @param {Object|Array} data GeoJSON, or an array of {lng, lat, …properties}
27193
27673
  * @param {Object} [o]
27194
27674
  * cluster: true | {radius, maxZoom}; color; radius; strokeColor; icon (image URL); iconSize; label (property name);
27195
- * minzoom; maxzoom; zIndex (higher draws on top); onClick(feature, event); popup(properties) → trusted HTML
27196
- * (inserted as is, never sanitized: escape feature data) or a DOM node
27675
+ * minzoom; maxzoom; zIndex (higher draws on top); onClick(feature, event);
27676
+ * popup(properties, feature) → trusted HTML (inserted as is, never sanitized: escape feature data, or use
27677
+ * sMap.popupTemplate), a DOM node, or a promise of either; popupOptions (over the map's `popup`); popupLoading
27197
27678
  */
27198
27679
  SMap.prototype.addPoints = function (id, data, o) {
27199
27680
  o = o || {};
@@ -27221,6 +27702,7 @@ function smapCreateSdk(engine) {
27221
27702
  var def = this._layers[id];
27222
27703
  if (!def) { throw new Error('sMap: no layer ' + id); }
27223
27704
  def.data = toFeatureCollection(data);
27705
+ if (def.hovered) { this._setHover(id, def, null); }
27224
27706
  var src = this.engine.getSource('smap-' + id);
27225
27707
  if (src) { src.setData(def.data); }
27226
27708
  return this;
@@ -27299,7 +27781,9 @@ function smapCreateSdk(engine) {
27299
27781
  if (!m) { return; }
27300
27782
  def.engineLayers.forEach(function (l) { if (m.getLayer(l)) { m.removeLayer(l); } });
27301
27783
  if (m.getSource('smap-' + id)) { m.removeSource('smap-' + id); }
27302
- (this._handlers[id] || []).forEach(function (h) { m.off(h[0], h[1], h[2]); });
27784
+ if (m.getSource('smap-' + id + '-hover')) { m.removeSource('smap-' + id + '-hover'); }
27785
+ def.hoverLayer = null; def.hovered = null;
27786
+ (this._handlers[id] || []).forEach(function (h) { if (h[1]) { m.off(h[0], h[1], h[2]); } else { m.off(h[0], h[2]); } });
27303
27787
  this._handlers[id] = [];
27304
27788
  def.engineLayers = [];
27305
27789
  };
@@ -27332,7 +27816,10 @@ function smapCreateSdk(engine) {
27332
27816
  layer.source = src;
27333
27817
  // The engine refuses a filter key whose value is undefined (unclustered points have none).
27334
27818
  if (layer.filter === undefined) { delete layer.filter; }
27335
- for (var k in range) { layer[k] = range[k]; }
27819
+ if (layer.minzoom === undefined) { delete layer.minzoom; }
27820
+ if (layer.paint === undefined) { delete layer.paint; }
27821
+ // A layer's own minzoom (a marker's textMinzoom / badgeMinzoom) never shows it before the layer's own range.
27822
+ for (var k in range) { layer[k] = k === 'minzoom' && layer.minzoom !== undefined ? Math.max(layer.minzoom, range.minzoom) : range[k]; }
27336
27823
  layer.layout = Object.assign({ visibility: visibility }, layer.layout || {});
27337
27824
  m.addLayer(layer, before);
27338
27825
  def.engineLayers.push(layer.id);
@@ -27362,7 +27849,57 @@ function smapCreateSdk(engine) {
27362
27849
  });
27363
27850
  }
27364
27851
  var only = o.cluster ? ['!', ['has', 'point_count']] : undefined;
27365
- if (o.icon) {
27852
+ var both = function (extra) { return only ? ['all', only, extra] : extra; };
27853
+ if (o.marker) {
27854
+ // Pins like Superroute's route map, drawn on the GPU: one image per colour pair (and per number, per badge),
27855
+ // made the first time a feature shows it. Numbers and badges are their own layers above the pins.
27856
+ var mk = o.marker;
27857
+ var shape = mk.shape === 'circle' ? 'circle' : 'pin';
27858
+ var size = mk.size || 32;
27859
+ var bw = mk.borderWidth !== undefined ? mk.borderWidth : 3;
27860
+ var g = markerGeometry(shape, size);
27861
+ var str = function (v, d) { return ['to-string', valueExpr(v, d)]; };
27862
+ // One scale for the pin, its number and its badge (a number, or an engine expression such as one of the zoom),
27863
+ // so the three stay together at every size: the offsets are in icon units and scale with them.
27864
+ var scale = mk.scale !== undefined ? mk.scale : 1;
27865
+ // Opacity of the pin, its number and its badge together (the route map draws settled stops at .72).
27866
+ var markerPaint = mk.opacity !== undefined ? { 'icon-opacity': valueExpr(mk.opacity, 1) } : undefined;
27867
+ main = src + '-points';
27868
+ var markerSpecs = [];
27869
+ var addMarker = function (layer) { markerSpecs.push(JSON.parse(JSON.stringify(layer))); add(layer); };
27870
+ addMarker({
27871
+ id: main, type: 'symbol', filter: only, paint: markerPaint,
27872
+ layout: {
27873
+ 'icon-image': drawnImageId('pin', [shape, str(mk.fill, '#ffffff'), str(mk.border, color), String(bw), String(size)]),
27874
+ 'icon-anchor': shape === 'circle' ? 'center' : 'bottom',
27875
+ // The image has room for the shadow below the tip: the tip, not the image's edge, is at the position.
27876
+ 'icon-offset': [0, shape === 'circle' ? 0 : PIN_PAD], 'icon-size': scale,
27877
+ 'icon-allow-overlap': true, 'icon-ignore-placement': true,
27878
+ },
27879
+ });
27880
+ if (mk.text !== undefined) {
27881
+ var text = str(mk.text, '');
27882
+ addMarker({
27883
+ id: src + '-numbers', type: 'symbol', filter: both(['!=', text, '']), minzoom: mk.textMinzoom, paint: markerPaint,
27884
+ layout: {
27885
+ 'icon-image': drawnImageId('num', [str(mk.textColor, '#ffffff'), String(size), text]),
27886
+ 'icon-offset': [0, -g.tipToHead], 'icon-size': scale, 'icon-allow-overlap': true, 'icon-ignore-placement': true,
27887
+ },
27888
+ });
27889
+ }
27890
+ if (mk.badge !== undefined) {
27891
+ var badge = ['to-number', valueExpr(mk.badge, 0), 0];
27892
+ addMarker({
27893
+ id: src + '-badges', type: 'symbol', filter: both(['>', badge, 1]), minzoom: mk.badgeMinzoom, paint: markerPaint,
27894
+ layout: {
27895
+ 'icon-image': drawnImageId('badge', [['to-string', badge]]),
27896
+ 'icon-offset': g.badge, 'icon-size': scale,
27897
+ 'icon-allow-overlap': true, 'icon-ignore-placement': true,
27898
+ },
27899
+ });
27900
+ }
27901
+ if (mk.hoverRaise !== false) { this._hoverRaise(id, def, src, markerSpecs, main); }
27902
+ } else if (o.icon) {
27366
27903
  var imageId = src + '-icon';
27367
27904
  main = src + '-points';
27368
27905
  var placeIcon = function () {
@@ -27396,9 +27933,35 @@ function smapCreateSdk(engine) {
27396
27933
  }
27397
27934
  } else if (def.kind === 'lines') {
27398
27935
  main = src + '-lines';
27399
- var paint = { 'line-color': color, 'line-width': o.width || 3, 'line-opacity': o.opacity !== undefined ? o.opacity : 1 };
27936
+ var lineColor = valueExpr(o.color, '#4f46e5');
27937
+ var round = { 'line-cap': 'round', 'line-join': 'round' };
27938
+ // Bottom to top: casing, shadow, the line, its arrows (as on Superroute's route map).
27939
+ if (o.casing) {
27940
+ var cs = o.casing === true ? {} : o.casing;
27941
+ add({ id: src + '-casing', type: 'line', layout: round, paint: {
27942
+ 'line-color': valueExpr(cs.color, '#ffffff'), 'line-width': cs.width || 9, 'line-opacity': cs.opacity !== undefined ? cs.opacity : 1 } });
27943
+ }
27944
+ if (o.shadow) {
27945
+ var sh = o.shadow === true ? {} : o.shadow;
27946
+ add({ id: src + '-shadow', type: 'line', layout: round, paint: {
27947
+ 'line-color': valueExpr(sh.color, '#000000'), 'line-width': sh.width || 7, 'line-opacity': sh.opacity !== undefined ? sh.opacity : 0.3 } });
27948
+ }
27949
+ var paint = { 'line-color': lineColor, 'line-width': o.width || 3, 'line-opacity': o.opacity !== undefined ? o.opacity : 1 };
27400
27950
  if (o.dash) { paint['line-dasharray'] = o.dash; }
27401
- add({ id: main, type: 'line', layout: { 'line-cap': 'round', 'line-join': 'round' }, paint: paint });
27951
+ add({ id: main, type: 'line', layout: round, paint: paint });
27952
+ if (o.arrows) {
27953
+ var ar = o.arrows === true ? {} : o.arrows;
27954
+ var arrowSize = ar.size || 14;
27955
+ add({
27956
+ id: src + '-arrows', type: 'symbol',
27957
+ layout: {
27958
+ 'symbol-placement': 'line', 'symbol-spacing': ar.spacing || 80,
27959
+ 'icon-image': drawnImageId('arrow', [String(arrowSize), ['to-string', valueExpr(ar.color, lineColor)]]),
27960
+ 'icon-rotation-alignment': 'map', 'icon-allow-overlap': true, 'icon-ignore-placement': true,
27961
+ },
27962
+ paint: { 'icon-opacity': ar.opacity !== undefined ? ar.opacity : 0.8 },
27963
+ });
27964
+ }
27402
27965
  } else {
27403
27966
  if (o.extrude) {
27404
27967
  main = src + '-extrusion';
@@ -27418,32 +27981,74 @@ function smapCreateSdk(engine) {
27418
27981
  add({ id: src + '-outline', type: 'line', paint: { 'line-color': o.outline || o.fill || color, 'line-width': o.outlineWidth || 1.5 } });
27419
27982
  }
27420
27983
  }
27984
+ // The popup is opened by the map's one click handler (_popupClick), from this layer's own hit test.
27985
+ def.popupLayer = o.popup ? main : null;
27421
27986
  if (main && (o.popup || o.onClick)) {
27422
- this._listen(id, 'click', main, function (e) {
27423
- var f = e.features[0];
27424
- if (o.onClick) { o.onClick(f, e); }
27425
- if (o.popup) {
27426
- // A string is inserted as HTML as it is (never sanitized): it must be trusted, with feature data escaped.
27427
- // A DOM node is inserted as it is.
27428
- var content = o.popup(f.properties, f);
27429
- if (content) {
27430
- var popup = new engine.Popup({ offset: 12 }).setLngLat(e.lngLat);
27431
- if (typeof content === 'string') { popup.setHTML(content); } else { popup.setDOMContent(content); }
27432
- popup.addTo(m);
27433
- }
27434
- }
27435
- });
27987
+ if (o.onClick) {
27988
+ this._listen(id, 'click', main, function (e) { o.onClick(e.features[0], e); });
27989
+ }
27436
27990
  this._listen(id, 'mouseenter', main, function () { m.getCanvas().style.cursor = 'pointer'; });
27437
27991
  this._listen(id, 'mouseleave', main, function () { m.getCanvas().style.cursor = ''; });
27438
27992
  }
27439
27993
  };
27440
27994
 
27995
+ /**
27996
+ * Overlapping pins: the one under the pointer comes to the front, as on Superroute's route map, so it can be read
27997
+ * and clicked. A copy of that one feature is drawn in its own source above everything else; nothing is done per
27998
+ * feature, only for the feature under the pointer.
27999
+ */
28000
+ SMap.prototype._hoverRaise = function (id, def, src, specs, main) {
28001
+ var m = this.engine, self = this;
28002
+ var hoverSrc = src + '-hover';
28003
+ m.addSource(hoverSrc, { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
28004
+ var top = null;
28005
+ specs.forEach(function (spec) {
28006
+ var layer = JSON.parse(JSON.stringify(spec));
28007
+ layer.id = spec.id.replace(src + '-', src + '-hover-');
28008
+ layer.source = hoverSrc;
28009
+ if (layer.filter === undefined) { delete layer.filter; }
28010
+ if (layer.paint === undefined) { delete layer.paint; }
28011
+ if (layer.minzoom === undefined) { delete layer.minzoom; }
28012
+ m.addLayer(layer);
28013
+ def.engineLayers.push(layer.id);
28014
+ if (!top) { top = layer.id; }
28015
+ });
28016
+ def.hoverLayer = top;
28017
+ def.hoverSource = hoverSrc;
28018
+ var keyOf = function (f) { return JSON.stringify(f.geometry && f.geometry.coordinates) + JSON.stringify(f.properties || {}); };
28019
+ this._listen(id, 'mousemove', null, function (e) {
28020
+ if (def.hidden || !m.getLayer(main) || def.pinned) { return; }
28021
+ // Still over the raised pin: keep it. Otherwise raise the top pin under the pointer, if any.
28022
+ if (def.hovered && m.getLayer(top) && m.queryRenderedFeatures(e.point, { layers: [top] }).length) { return; }
28023
+ var hits = m.queryRenderedFeatures(e.point, { layers: [main] });
28024
+ var f = hits && hits[0];
28025
+ if (!f) { if (def.hovered) { self._setHover(id, def, null); } return; }
28026
+ if (def.hovered && def.hovered.key === keyOf(f)) { return; }
28027
+ self._setHover(id, def, f, keyOf(f));
28028
+ });
28029
+ this._listen(id, 'mouseout', null, function () { if (def.hovered && !def.pinned) { self._setHover(id, def, null); } });
28030
+ };
28031
+
28032
+ SMap.prototype._unpin = function () {
28033
+ var def = this._pinnedDef;
28034
+ this._pinnedDef = null;
28035
+ if (def) { def.pinned = null; this._setHover(null, def, null); }
28036
+ };
28037
+
28038
+ SMap.prototype._setHover = function (id, def, f, key) {
28039
+ var source = def.hoverSource && this.engine.getSource(def.hoverSource);
28040
+ def.hovered = f ? { key: key } : null;
28041
+ if (!source) { return; }
28042
+ // A plain copy: the engine's own feature objects cannot be sent to its worker.
28043
+ source.setData({ type: 'FeatureCollection', features: f ? [JSON.parse(JSON.stringify({ type: 'Feature', properties: f.properties || {}, geometry: f.geometry }))] : [] });
28044
+ };
28045
+
27441
28046
  SMap.prototype._listen = function (id, type, layer, fn) {
27442
- this.engine.on(type, layer, fn);
28047
+ if (layer) { this.engine.on(type, layer, fn); } else { this.engine.on(type, fn); }
27443
28048
  (this._handlers[id] = this._handlers[id] || []).push([type, layer, fn]);
27444
28049
  };
27445
28050
 
27446
- return { Map: SMap, version: VERSION, styles: FLAVORS.slice(), engine: engine };
28051
+ return { Map: SMap, version: VERSION, styles: FLAVORS.slice(), engine: engine, popupTemplate: popupTemplate };
27447
28052
  });
27448
28053
 
27449
28054
  return module.exports;