ixmaps-gl 0.2.15 → 0.2.16
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/README.md +13 -3
- package/ixmaps-gl.js +237 -156
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,6 +114,16 @@ ixmaps.map().theme("mytheme").show(); // also hide(), toggle(), remove(),
|
|
|
114
114
|
// replace(theme), markClass(n), unmarkClass(n)
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
Projection changes never need the project machinery: `map().setProjection("globe")` (or `"mercator"`)
|
|
118
|
+
swaps the MapLibre projection and rebuilds the projection-dependent layers — themes, data and the
|
|
119
|
+
current view are never touched (unlike `setProjectJSON`, which re-defines every theme to change
|
|
120
|
+
the projection and, without `"keepview"`, resets the view).
|
|
121
|
+
|
|
122
|
+
`CHART|USER` themes (a page's own `userdraw` chart function) render as live SVG in a pane over
|
|
123
|
+
the map, as flat renders them — vector-crisp at every zoom, labels as real text, following every
|
|
124
|
+
pan via transform-only updates (≈3ms for 8,000 charts). Picking/tooltips run on a transparent
|
|
125
|
+
deck layer at the same positions.
|
|
126
|
+
|
|
117
127
|
The historic global forms remain as compatibility wrappers and dispatch on arity: the canonical
|
|
118
128
|
theme-first `ixmaps.changeThemeStyle(themeId, "style:value", "set")` and `ixmaps.removeTheme(id)`
|
|
119
129
|
(htmlgui.js), the handle method `map().changeThemeStyle(theme, style, flag)`, and the older
|
|
@@ -233,7 +243,7 @@ of your own project files gl can render.
|
|
|
233
243
|
Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
|
|
234
244
|
|
|
235
245
|
```html
|
|
236
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
246
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.16/ixmaps-gl.js"></script>
|
|
237
247
|
```
|
|
238
248
|
|
|
239
249
|
or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
|
|
@@ -241,13 +251,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
|
|
|
241
251
|
browser script, which defines the global `ixmaps`):
|
|
242
252
|
|
|
243
253
|
```html
|
|
244
|
-
<script src="https://unpkg.com/ixmaps-gl@0.2.
|
|
254
|
+
<script src="https://unpkg.com/ixmaps-gl@0.2.16/ixmaps-gl.js"></script>
|
|
245
255
|
```
|
|
246
256
|
|
|
247
257
|
or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
|
|
248
258
|
|
|
249
259
|
```html
|
|
250
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
260
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.16/ixmaps-gl.js"></script>
|
|
251
261
|
<div id="map_div" style="position:absolute;inset:0;"></div>
|
|
252
262
|
|
|
253
263
|
<script>
|
package/ixmaps-gl.js
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
// ---------------------------------------------------------------
|
|
83
83
|
// this engine's release (= package.json "version", checked by
|
|
84
84
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
85
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
85
|
+
const IXMAPS_GL_VERSION = '0.2.16';
|
|
86
86
|
|
|
87
87
|
const LIB_URLS = {
|
|
88
88
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -1411,7 +1411,7 @@
|
|
|
1411
1411
|
const key = String(id), area = geometryArea(f.geometry);
|
|
1412
1412
|
if (!flatShapeWins(best.get(key), area)) continue;
|
|
1413
1413
|
const pos = flatShapeCenter(f.geometry);
|
|
1414
|
-
if (pos) best.set(key, { area, pos });
|
|
1414
|
+
if (pos) best.set(key, { area, pos, areaKm2: flatShapeAreaKm2(rt, f) });
|
|
1415
1415
|
}
|
|
1416
1416
|
}
|
|
1417
1417
|
rt._positionIndex = best;
|
|
@@ -1445,7 +1445,11 @@
|
|
|
1445
1445
|
const v = row[lookupField];
|
|
1446
1446
|
if (v == null || v === '') continue;
|
|
1447
1447
|
const hit = index.get(flatLookupKey(v, spec.style));
|
|
1448
|
-
|
|
1448
|
+
// the joined feature is a POINT — DENSITY (value per km² of the
|
|
1449
|
+
// joined shape, as flat) needs the shape's km² area carried along
|
|
1450
|
+
// (featureAreaKm2 prefers this marker), exactly as the choropleth
|
|
1451
|
+
// join stamps it
|
|
1452
|
+
if (hit) features.push({ type: 'Feature', geometry: { type: 'Point', coordinates: hit.pos }, properties: row, _flatAreaKm2: hit.areaKm2 });
|
|
1449
1453
|
}
|
|
1450
1454
|
}
|
|
1451
1455
|
return { type: 'FeatureCollection', features };
|
|
@@ -2010,16 +2014,57 @@
|
|
|
2010
2014
|
// the user chart's size unit: flat's normalX(15) = 300 is the chart's
|
|
2011
2015
|
// normal radius (maxSize), shown at gl's normal bubble radius
|
|
2012
2016
|
const USER_CHART_MAX_SIZE = 300;
|
|
2013
|
-
//
|
|
2014
|
-
|
|
2017
|
+
// ---- the pane USER charts render into: one per map, live over the canvas ----
|
|
2018
|
+
// flat draws its user charts as LIVE SVG over the map — vector-crisp at
|
|
2019
|
+
// every zoom, labels as real text. The first port here rasterized every
|
|
2020
|
+
// chart into one texture atlas instead, and a page with thousands of
|
|
2021
|
+
// charts got ~50 px per chart (the atlas area is fixed) — icons scaled
|
|
2022
|
+
// up several times over: blur, mushy gradients, unreadable labels. In
|
|
2023
|
+
// this pane each chart is a <g> mounted once; a map 'move' only updates
|
|
2024
|
+
// its transform (translate + scale), which the browser composites —
|
|
2025
|
+
// the same thing flat's own engine does over Leaflet, at the same item
|
|
2026
|
+
// counts. Keyed by the map's options object, so runtimes defined later
|
|
2027
|
+
// (loadProject, post-build map.layer) find it too.
|
|
2028
|
+
const _chartHosts = new WeakMap();
|
|
2029
|
+
function chartHostFor(mapOptions, map, el) {
|
|
2030
|
+
let host = _chartHosts.get(mapOptions);
|
|
2031
|
+
if (host) return host;
|
|
2032
|
+
const pane = document.createElementNS(SVG_NS, 'svg');
|
|
2033
|
+
pane.setAttribute('class', 'ix-user-charts');
|
|
2034
|
+
pane.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:3;';
|
|
2035
|
+
const defs = document.createElementNS(SVG_NS, 'defs');
|
|
2036
|
+
pane.appendChild(defs);
|
|
2037
|
+
el.appendChild(pane);
|
|
2038
|
+
host = { pane, defs, map, el, runtimes: new Set(), raf: 0 };
|
|
2039
|
+
host.requestUpdate = () => {
|
|
2040
|
+
if (host.raf) return;
|
|
2041
|
+
host.raf = global.requestAnimationFrame(() => { host.raf = 0; host.update(); });
|
|
2042
|
+
};
|
|
2043
|
+
// transform-only per move: positions follow the map live; the charts'
|
|
2044
|
+
// own markup is redrawn on the settled-refresh cadence (freezeOnPan)
|
|
2045
|
+
host.update = () => {
|
|
2046
|
+
const w = el.clientWidth, h = el.clientHeight, margin = 256;
|
|
2047
|
+
for (const rt of host.runtimes) {
|
|
2048
|
+
const items = rt._chartItems;
|
|
2049
|
+
if (!items) continue;
|
|
2050
|
+
for (const it of items) {
|
|
2051
|
+
const p = map.project(it.coords);
|
|
2052
|
+
const off = p.x < -margin || p.y < -margin || p.x > w + margin || p.y > h + margin;
|
|
2053
|
+
if (off) { if (it.el.style.display !== 'none') it.el.style.display = 'none'; continue; }
|
|
2054
|
+
if (it.el.style.display === 'none') it.el.style.display = '';
|
|
2055
|
+
it.el.setAttribute('transform', `translate(${p.x} ${p.y}) scale(${it.s}) translate(${-it.ox} ${-it.oy})`);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
map.on('move', host.requestUpdate);
|
|
2060
|
+
map.on('resize', host.requestUpdate);
|
|
2061
|
+
_chartHosts.set(mapOptions, host);
|
|
2062
|
+
return host;
|
|
2063
|
+
}
|
|
2015
2064
|
// ?ixgl-debug in the page URL: ixmaps-gl logs what it draws (console.info)
|
|
2016
2065
|
function glDebug() {
|
|
2017
2066
|
try { return new URLSearchParams(global.location.search).has('ixgl-debug'); } catch (e) { return false; }
|
|
2018
2067
|
}
|
|
2019
|
-
// the pixel area all of a layer's chart images may take together: deck.gl
|
|
2020
|
-
// packs them into one texture (1024 px wide), which must stay well below
|
|
2021
|
-
// the GPU's texture size limit — see _userChartIcon
|
|
2022
|
-
const USER_CHART_ATLAS_AREA = 1024 * 8192;
|
|
2023
2068
|
class LayerBuilder {
|
|
2024
2069
|
constructor(name) {
|
|
2025
2070
|
this.name = name;
|
|
@@ -2114,11 +2159,24 @@
|
|
|
2114
2159
|
define() { return this; }
|
|
2115
2160
|
}
|
|
2116
2161
|
|
|
2162
|
+
// flat-era pages keep the layer construct and CALL it like a function
|
|
2163
|
+
// when handing it back to the map later (censimenti's sidebar:
|
|
2164
|
+
// map().add(__theme(),'clear')) — the call returns the builder; pages also
|
|
2165
|
+
// chain builder methods right on the factory result, so the construct
|
|
2166
|
+
// delegates every one of them (chaining stays on the construct). Every
|
|
2167
|
+
// consumption point unwraps __ixLayer, so the object form keeps working.
|
|
2117
2168
|
function layer(name, configFn) {
|
|
2118
2169
|
const builder = new LayerBuilder(name);
|
|
2119
2170
|
if (configFn) configFn(builder);
|
|
2120
|
-
return builder;
|
|
2171
|
+
const construct = function () { return builder; };
|
|
2172
|
+
construct.__ixLayer = builder;
|
|
2173
|
+
for (const k of Object.getOwnPropertyNames(LayerBuilder.prototype)) {
|
|
2174
|
+
if (k === 'constructor') continue;
|
|
2175
|
+
construct[k] = function (...args) { const r = builder[k](...args); return r === builder ? construct : r; };
|
|
2176
|
+
}
|
|
2177
|
+
return construct;
|
|
2121
2178
|
}
|
|
2179
|
+
const unwrapLayer = x => (typeof x === 'function' && x.__ixLayer) ? x.__ixLayer : x;
|
|
2122
2180
|
|
|
2123
2181
|
// ---------------------------------------------------------------
|
|
2124
2182
|
// Binding aliases. Real ixmaps-flat accepts ~50 .binding() keys and maps
|
|
@@ -2875,8 +2933,20 @@
|
|
|
2875
2933
|
if (__hoverTooltipWatch || !el.parentElement) return;
|
|
2876
2934
|
const host = el.parentElement;
|
|
2877
2935
|
const attach = (tipEl) => {
|
|
2936
|
+
// deck nests its tooltip inside its widget root (.fill, z-index 2),
|
|
2937
|
+
// a stacking context under the USER-chart pane (z 3) — the tooltip's
|
|
2938
|
+
// own z-index can never cross that. Moved out to the map container,
|
|
2939
|
+
// a sibling of the pane, where z-index 7 puts it above everything;
|
|
2940
|
+
// same coordinate origin, deck keeps updating the same node.
|
|
2941
|
+
// Re-asserted on every style write (deck sets z-index 1 itself).
|
|
2942
|
+
const liftTooltip = () => {
|
|
2943
|
+
if (tipEl.parentNode !== el) el.appendChild(tipEl);
|
|
2944
|
+
if (tipEl.style.zIndex !== '7') tipEl.style.zIndex = 7;
|
|
2945
|
+
};
|
|
2946
|
+
liftTooltip();
|
|
2878
2947
|
let lastSetTransform = null;
|
|
2879
2948
|
const mo = new MutationObserver(() => {
|
|
2949
|
+
liftTooltip();
|
|
2880
2950
|
if (tipEl.style.display === 'none' || !tipEl.textContent.trim()) return;
|
|
2881
2951
|
const t = tipEl.style.transform;
|
|
2882
2952
|
if (!t || t === lastSetTransform) return;
|
|
@@ -2976,7 +3046,10 @@
|
|
|
2976
3046
|
// touches the tooltip DOM in the same tick.
|
|
2977
3047
|
function hideDefaultHoverTooltip() {
|
|
2978
3048
|
requestAnimationFrame(() => {
|
|
2979
|
-
|
|
3049
|
+
// the hover tooltip is reparented into el (see watchHoverTooltip) —
|
|
3050
|
+
// look there first, then where deck originally nested it
|
|
3051
|
+
const defaultTooltipEl = (el && el.querySelector('.deck-tooltip')) ||
|
|
3052
|
+
(el.parentElement && el.parentElement.querySelector('.deck-tooltip'));
|
|
2980
3053
|
if (defaultTooltipEl) defaultTooltipEl.style.display = 'none';
|
|
2981
3054
|
});
|
|
2982
3055
|
}
|
|
@@ -3085,6 +3158,33 @@
|
|
|
3085
3158
|
// calls: addLegendPanel(rt), setLegendOption(value), updateSubTheme(rt),
|
|
3086
3159
|
// layoutLegends().
|
|
3087
3160
|
// ---------------------------------------------------------------
|
|
3161
|
+
// The map-level `align` option positions the legend — BOTH the native
|
|
3162
|
+
// panel stack (createLegend) and a page legend (map.legend(html), the
|
|
3163
|
+
// legendEl built in build()): one helper, so they can never disagree.
|
|
3164
|
+
// gl's four corners, else flat's own values (htmlgui_flat.js 421-470):
|
|
3165
|
+
// "…left" → left (the part before "left", else 55px), 12px from the top;
|
|
3166
|
+
// "…right" → right (else 25px); "center" / "top" → top center.
|
|
3167
|
+
// Default "top-right" per explicit request (real engine's own fallback is
|
|
3168
|
+
// right-anchored too, just without an opinion on vertical placement).
|
|
3169
|
+
function legendAlignCssFor(mapOptions) {
|
|
3170
|
+
const ALIGN_CSS = {
|
|
3171
|
+
'top-left': 'left:10px;top:10px;',
|
|
3172
|
+
'top-right': 'right:10px;top:10px;',
|
|
3173
|
+
'bottom-left': 'left:10px;bottom:10px;',
|
|
3174
|
+
'bottom-right': 'right:10px;bottom:10px;'
|
|
3175
|
+
};
|
|
3176
|
+
const flatAlignCss = align => {
|
|
3177
|
+
const a = String(align || '');
|
|
3178
|
+
// flat hands these to jQuery .css(), which adds px to a unitless
|
|
3179
|
+
// number — a raw cssText needs it explicit
|
|
3180
|
+
const px = v => /^\d+(\.\d+)?$/.test(v) ? v + 'px' : v;
|
|
3181
|
+
if (/left/.test(a)) return 'left:' + px(a.split('left')[0] || '55px') + ';top:12px;';
|
|
3182
|
+
if (/right/.test(a)) return 'right:' + px(a.split('right')[0] || '25px') + ';top:10px;';
|
|
3183
|
+
if (a === 'center' || a === 'top') return 'left:50%;transform:translateX(-50%);top:10px;';
|
|
3184
|
+
return null;
|
|
3185
|
+
};
|
|
3186
|
+
return ALIGN_CSS[mapOptions.align] || flatAlignCss(mapOptions.align) || ALIGN_CSS['top-right'];
|
|
3187
|
+
}
|
|
3088
3188
|
function createLegend(ctx) {
|
|
3089
3189
|
const { builder, map, el, overlay, runtimes, engineApi, built, refresh, scheduleRefresh, notifyRedraw, quietRefresh } = ctx;
|
|
3090
3190
|
let addLegendPanel = () => {};
|
|
@@ -3128,6 +3228,10 @@
|
|
|
3128
3228
|
const legendOpt = builder.mapOptions.legend;
|
|
3129
3229
|
// let, not const: map.setLegend(value) switches them at runtime (see setLegendOption)
|
|
3130
3230
|
let legendOn = legendIsOn(legendOpt);
|
|
3231
|
+
// flat: a page legend (map.legend(html) → ixmaps.legend.url) REPLACES
|
|
3232
|
+
// the theme-generated legend — legend.js 2076 renders the layerlist
|
|
3233
|
+
// legend instead (useLayerListLegend) — so the native panels stay off
|
|
3234
|
+
if (builder._legendHtml) legendOn = false;
|
|
3131
3235
|
let legendFolded = legendOpt === 'closed';
|
|
3132
3236
|
{
|
|
3133
3237
|
// Map-level `align` option positions the legend panel — a map-
|
|
@@ -3146,24 +3250,7 @@
|
|
|
3146
3250
|
// shape choice earlier this session. Default "top-right" per
|
|
3147
3251
|
// explicit request (real engine's own fallback is right-anchored
|
|
3148
3252
|
// too, just without an opinion on vertical placement).
|
|
3149
|
-
const
|
|
3150
|
-
'top-left': 'left:10px;top:10px;',
|
|
3151
|
-
'top-right': 'right:10px;top:10px;',
|
|
3152
|
-
'bottom-left': 'left:10px;bottom:10px;',
|
|
3153
|
-
'bottom-right': 'right:10px;bottom:10px;'
|
|
3154
|
-
};
|
|
3155
|
-
// gl's four corners, else flat's own values (htmlgui_flat.js
|
|
3156
|
-
// 421-470): "…left" → left (the part before "left", else 55px), 12px
|
|
3157
|
-
// from the top; "…right" → right (else 25px); "center" / "top" → top
|
|
3158
|
-
// center
|
|
3159
|
-
const flatAlignCss = align => {
|
|
3160
|
-
const a = String(align || '');
|
|
3161
|
-
if (/left/.test(a)) return 'left:' + (a.split('left')[0] || '55px') + ';top:12px;';
|
|
3162
|
-
if (/right/.test(a)) return 'right:' + (a.split('right')[0] || '25px') + ';top:10px;';
|
|
3163
|
-
if (a === 'center' || a === 'top') return 'left:50%;transform:translateX(-50%);top:10px;';
|
|
3164
|
-
return null;
|
|
3165
|
-
};
|
|
3166
|
-
const legendAlignCss = ALIGN_CSS[builder.mapOptions.align] || flatAlignCss(builder.mapOptions.align) || ALIGN_CSS['top-right'];
|
|
3253
|
+
const legendAlignCss = legendAlignCssFor(builder.mapOptions);
|
|
3167
3254
|
// Real ixmaps-flat's SUB-THEME (MapTheme.createSubTheme,
|
|
3168
3255
|
// maptheme.js 14284-14292 / 15010-15160; map.Themes.enableSubThemes
|
|
3169
3256
|
// defaults to true): marking fields of a multi-field choropleth
|
|
@@ -3265,7 +3352,7 @@
|
|
|
3265
3352
|
setLegendOption = (opt) => {
|
|
3266
3353
|
builder.mapOptions.legend = opt;
|
|
3267
3354
|
legendFolded = opt === 'closed';
|
|
3268
|
-
legendOn = legendIsOn(opt);
|
|
3355
|
+
legendOn = builder._legendHtml ? false : legendIsOn(opt);
|
|
3269
3356
|
runtimes.forEach(rt => {
|
|
3270
3357
|
if (!legendOn) {
|
|
3271
3358
|
if (rt._legendPanel && rt._legendPanel.parentNode) rt._legendPanel.parentNode.removeChild(rt._legendPanel);
|
|
@@ -3873,7 +3960,7 @@
|
|
|
3873
3960
|
this._layerBuilders.push(lb);
|
|
3874
3961
|
return lb;
|
|
3875
3962
|
}
|
|
3876
|
-
this._layerBuilders.push(nameOrBuilder);
|
|
3963
|
+
this._layerBuilders.push(unwrapLayer(nameOrBuilder));
|
|
3877
3964
|
return this;
|
|
3878
3965
|
}
|
|
3879
3966
|
// .Layer() — alias of .layer()
|
|
@@ -4076,6 +4163,10 @@
|
|
|
4076
4163
|
// world once, at the cost of that zoom limit.
|
|
4077
4164
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
4078
4165
|
});
|
|
4166
|
+
// the live-SVG pane USER charts render into (see chartHostFor) —
|
|
4167
|
+
// registered here, keyed by this map's options object, so every
|
|
4168
|
+
// runtime (including later loadProject/map.layer ones) finds it
|
|
4169
|
+
chartHostFor(this._engineOptions, map, el);
|
|
4079
4170
|
// ixmaps.Map(id, {mapProjection}): flat's orthographic map is MapLibre's
|
|
4080
4171
|
// native globe here (same mapping as loadProject, applyProjectMap);
|
|
4081
4172
|
// "ortographic" is the spelling flat pages commonly carry. Any other
|
|
@@ -4199,7 +4290,9 @@
|
|
|
4199
4290
|
|
|
4200
4291
|
if (this._legendHtml) {
|
|
4201
4292
|
const legendEl = document.createElement('div');
|
|
4202
|
-
|
|
4293
|
+
// a page legend (map.legend(html)) is positioned by the map's
|
|
4294
|
+
// own align option — the same rule that places the native stack
|
|
4295
|
+
legendEl.style.cssText = 'position:absolute;' + legendAlignCssFor(this.mapOptions) + 'z-index:5;pointer-events:none';
|
|
4203
4296
|
legendEl.innerHTML = this._legendHtml;
|
|
4204
4297
|
// inside the map container, never the page's body (see the pinned
|
|
4205
4298
|
// tooltip mount) — styling body breaks page-positioned elements
|
|
@@ -4325,6 +4418,7 @@
|
|
|
4325
4418
|
// the layer name): add(theme, flag) — "replace" swaps a theme of the
|
|
4326
4419
|
// same id —, replace(id, theme, flag), remove(id)
|
|
4327
4420
|
add: (layerBuilder, flag) => {
|
|
4421
|
+
layerBuilder = unwrapLayer(layerBuilder);
|
|
4328
4422
|
_chainedLayers = _chainedLayers.then(() => {
|
|
4329
4423
|
if (/replace/i.test(flag || '')) {
|
|
4330
4424
|
const def = layerBuilder.definition();
|
|
@@ -4336,6 +4430,7 @@
|
|
|
4336
4430
|
return engineApi;
|
|
4337
4431
|
},
|
|
4338
4432
|
replace: (id, layerBuilder, flag) => {
|
|
4433
|
+
layerBuilder = unwrapLayer(layerBuilder);
|
|
4339
4434
|
_chainedLayers = _chainedLayers.then(() => { removeThemeById(id); return engineApi.defineLayer(layerBuilder); })
|
|
4340
4435
|
.catch(err => console.error('[ixmaps-gl] map.replace():', err));
|
|
4341
4436
|
return engineApi;
|
|
@@ -4367,6 +4462,23 @@
|
|
|
4367
4462
|
},
|
|
4368
4463
|
setMapTypeId: (id) => engineApi.setMapType(id),
|
|
4369
4464
|
mapType: (id) => engineApi.setMapType(id),
|
|
4465
|
+
// projection swap WITHOUT the project machinery: setProjectJSON
|
|
4466
|
+
// tears down and re-defines every theme to change the projection
|
|
4467
|
+
// (a projection toggle on a theme-heavy page paid a full data
|
|
4468
|
+
// re-aggregation — the power plants stage page). The projection
|
|
4469
|
+
// itself is one MapLibre call; the layers are projection-
|
|
4470
|
+
// dependent (globe far-hemisphere cull, chart placement) so they
|
|
4471
|
+
// are rebuilt — refreshLayers, once now and again when the new
|
|
4472
|
+
// projection has rendered — but THEMES and VIEW are never touched
|
|
4473
|
+
setProjection: (type) => {
|
|
4474
|
+
if (typeof map.setProjection !== 'function') { console.warn('[ixmaps-gl] map.setProjection not available on the loaded maplibre-gl build (needs >=5.0.1) — projection unchanged'); return engineApi; }
|
|
4475
|
+
const t = /globe/i.test(String(type || '')) ? 'globe' : 'mercator';
|
|
4476
|
+
try { map.setProjection({ type: t }); } catch (e) { console.warn('[ixmaps-gl] map.setProjection failed:', e); return engineApi; }
|
|
4477
|
+
refreshLayers();
|
|
4478
|
+
map.triggerRepaint();
|
|
4479
|
+
map.once('idle', refreshLayers);
|
|
4480
|
+
return engineApi;
|
|
4481
|
+
},
|
|
4370
4482
|
// flat's getMapTypeId(): the basemap type name in use
|
|
4371
4483
|
getMapTypeId: () => String(builder.mapOptions.mapType || ''),
|
|
4372
4484
|
// flat's attribution (htmlgui.js htmlgui_set/getAttributionString)
|
|
@@ -4515,7 +4627,7 @@
|
|
|
4515
4627
|
// exactly how this engine's own facet-filter/style setters below
|
|
4516
4628
|
// already push responsibility for "what should happen" to the
|
|
4517
4629
|
// caller rather than guessing.
|
|
4518
|
-
defineLayer: (layerBuilder) => defineFromDefinition(layerBuilder.definition()),
|
|
4630
|
+
defineLayer: (layerBuilder) => defineFromDefinition(unwrapLayer(layerBuilder).definition()),
|
|
4519
4631
|
// GL-PORT COMPAT: real ixmaps-flat's map handle, as a page gets it
|
|
4520
4632
|
// from `myMap.then(map => map.view(...).options(...).layer(a)
|
|
4521
4633
|
// .layer(b))` — the common multi-layer idiom — is chainable on the
|
|
@@ -4617,6 +4729,11 @@
|
|
|
4617
4729
|
if (rt._unlisten) rt._unlisten();
|
|
4618
4730
|
if (rt._cancelWait) rt._cancelWait();
|
|
4619
4731
|
clearTimeout(rt._dependentTimer);
|
|
4732
|
+
// a removed theme's live USER-chart SVG is not part of the deck
|
|
4733
|
+
// layers — leaving its groups mounted keeps the charts on the
|
|
4734
|
+
// map after map.remove()/replace() (censimenti's sidebar)
|
|
4735
|
+
if (rt._clearUserChartPane) rt._clearUserChartPane();
|
|
4736
|
+
if (rt._chartHost && rt._chartHost.runtimes) rt._chartHost.runtimes.delete(rt);
|
|
4620
4737
|
});
|
|
4621
4738
|
const removed = removedRuntimes.length > 0;
|
|
4622
4739
|
// stale-registry guard: _globalThemeRegistry is a bare name->
|
|
@@ -5336,7 +5453,7 @@
|
|
|
5336
5453
|
// semantics — layer, view, options, require, attribution, on — keep
|
|
5337
5454
|
// theirs and are not overridden.
|
|
5338
5455
|
for (const m of ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
5339
|
-
'refreshTheme', 'redrawTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'resize',
|
|
5456
|
+
'refreshTheme', 'redrawTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize',
|
|
5340
5457
|
'loadProject', 'project', 'setThemeVisible', 'setThemeTimeFrame', 'show', 'hide', 'getZoom', 'getThemeObj',
|
|
5341
5458
|
'getThemes', 'theme', 'setView']) {
|
|
5342
5459
|
if (!MapBuilder.prototype[m]) {
|
|
@@ -5412,6 +5529,9 @@
|
|
|
5412
5529
|
}
|
|
5413
5530
|
return promise;
|
|
5414
5531
|
};
|
|
5532
|
+
// flat's mapApi.setView(center, zoom) (htmlgui_flat.js 1327) — the
|
|
5533
|
+
// same live-or-build-time split as .view() above
|
|
5534
|
+
promise.setView = (...args) => promise.view(...args);
|
|
5415
5535
|
promise.options = (...args) => { builder.options(...args); return promise; };
|
|
5416
5536
|
promise.attribution = (...args) => { builder.attribution(...args); return promise; };
|
|
5417
5537
|
promise.legend = (...args) => { builder.legend(...args); return promise; };
|
|
@@ -9080,8 +9200,12 @@
|
|
|
9080
9200
|
buildDeckLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
|
|
9081
9201
|
this._lastZoom = zoom;
|
|
9082
9202
|
this._lastBbox = bbox;
|
|
9083
|
-
|
|
9084
|
-
|
|
9203
|
+
// the live USER-chart SVG pane is not part of the deck layers — an
|
|
9204
|
+
// early return here must also unmount its groups, or the pane keeps
|
|
9205
|
+
// showing the last drawing after the theme went out of scale (or was
|
|
9206
|
+
// hidden via setThemeVisible)
|
|
9207
|
+
if (this._hidden) { this._clearUserChartPane(); return []; }
|
|
9208
|
+
if (!this.flags.has('FEATURE') && !this.flags.has('FEATURES') && chartHiddenByScale(this.style, liveZoom)) { this._clearUserChartPane(); return []; }
|
|
9085
9209
|
// DOT is checked FIRST, matching the real engine's own precedence
|
|
9086
9210
|
// (its DOT branch is a loop-level fast-path that bypasses the whole
|
|
9087
9211
|
// drawChart/modifier machinery entirely — a layer flagged DOT never
|
|
@@ -9952,6 +10076,11 @@
|
|
|
9952
10076
|
}
|
|
9953
10077
|
// USER charts (see resolveUserChartFunction): one icon per chart, drawn
|
|
9954
10078
|
// by the page's function; null → no such function (drawn as bubbles)
|
|
10079
|
+
_clearUserChartPane() {
|
|
10080
|
+
if (this._chartPaneGroup) this._chartPaneGroup.remove();
|
|
10081
|
+
this._chartPaneGroup = null;
|
|
10082
|
+
this._chartItems = null;
|
|
10083
|
+
}
|
|
9955
10084
|
_buildUserChartLayers(combined, liveZoom, sizeValueOf) {
|
|
9956
10085
|
const name = String(this.style.userdraw || '');
|
|
9957
10086
|
const draw = resolveUserChartFunction(name);
|
|
@@ -9987,9 +10116,17 @@
|
|
|
9987
10116
|
// radius of it (see USER_CHART_MAX_SIZE)
|
|
9988
10117
|
const pxPerUnit = NORMAL_RADIUS_PX * objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1) / USER_CHART_MAX_SIZE;
|
|
9989
10118
|
const flag = theme.szFlag;
|
|
9990
|
-
const
|
|
9991
|
-
|
|
9992
|
-
|
|
10119
|
+
const chartHost = _chartHosts.get(this.mapOptions);
|
|
10120
|
+
if (!chartHost) {
|
|
10121
|
+
if (!this._userChartWarned) { this._userChartWarned = true; console.warn(`[ixmaps-gl] USER chart "${name}": no chart pane — drawn as bubbles`); }
|
|
10122
|
+
return null;
|
|
10123
|
+
}
|
|
10124
|
+
// this view's charts, mounted as live <g> elements in the pane (see
|
|
10125
|
+
// chartHostFor) — flat's own SVG rendering: no atlas, no raster, no
|
|
10126
|
+
// async image decode, the whole drawing is synchronous
|
|
10127
|
+
const items = [];
|
|
10128
|
+
const defsSeen = new Set();
|
|
10129
|
+
let html = '';
|
|
9993
10130
|
for (const d of combined) {
|
|
9994
10131
|
const value = valueOf(d);
|
|
9995
10132
|
// the item's class (range classes set it for groups too — a negative
|
|
@@ -9997,133 +10134,69 @@
|
|
|
9997
10134
|
const cat = d.properties.cat != null ? d.properties.cat : (d.properties.counts ? dominant(d.properties.counts) : 0);
|
|
9998
10135
|
const radiusPx = valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
9999
10136
|
const size = radiusPx / pxPerUnit;
|
|
10000
|
-
const
|
|
10001
|
-
if (!
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
this.
|
|
10137
|
+
const chart = this._userChartSvg(draw, name, theme, flag, d, value, cat, size, sizeValueOf(d));
|
|
10138
|
+
if (!chart) continue;
|
|
10139
|
+
html += chart.markup;
|
|
10140
|
+
// the chart's own defs (gradients) go to the pane once per id —
|
|
10141
|
+
// several charts share one (pinnacle's per-color gradient)
|
|
10142
|
+
for (const id of chart.ids) {
|
|
10143
|
+
if (defsSeen.has(id)) continue;
|
|
10144
|
+
defsSeen.add(id);
|
|
10145
|
+
const def = this._userChartGroup.ownerDocument.getElementById(id);
|
|
10146
|
+
if (def) chartHost.defs.appendChild(def.cloneNode(true));
|
|
10147
|
+
}
|
|
10148
|
+
items.push({ d, coords: d.geometry.coordinates, ox: chart.ox, oy: chart.oy, s: pxPerUnit,
|
|
10149
|
+
r: Math.max(chart.vw, chart.vh) * pxPerUnit / 2 });
|
|
10150
|
+
}
|
|
10151
|
+
if (glDebug()) console.info(`[ixmaps-gl debug] ${themeIdOf(this)}: zoom ${mapLibreToFlatZoom(liveZoom).toFixed(2)}, ${combined.length} charts, ${items.length} drawn${this._viewMoving ? ' (moving)' : ''}`);
|
|
10152
|
+
if (!items.length) {
|
|
10153
|
+
if (this._chartPaneGroup) this._chartPaneGroup.remove();
|
|
10154
|
+
this._chartPaneGroup = null;
|
|
10155
|
+
this._chartItems = null;
|
|
10156
|
+
return (this._userChartLastLayers = []);
|
|
10157
|
+
}
|
|
10158
|
+
if (this._chartPaneGroup) this._chartPaneGroup.remove();
|
|
10159
|
+
this._chartPaneGroup = document.createElementNS(SVG_NS, 'g');
|
|
10160
|
+
chartHost.pane.appendChild(this._chartPaneGroup);
|
|
10161
|
+
this._chartPaneGroup.innerHTML = html;
|
|
10162
|
+
const els = this._chartPaneGroup.children;
|
|
10163
|
+
for (let i = 0; i < items.length; i++) items[i].el = els[i];
|
|
10164
|
+
this._chartItems = items;
|
|
10165
|
+
chartHost.runtimes.add(this);
|
|
10166
|
+
chartHost.update();
|
|
10167
|
+
// picking: a transparent circle per chart — the VISIBLE chart is the
|
|
10168
|
+
// live SVG in the pane; this deck layer only serves hover, click and
|
|
10169
|
+
// the tooltips (pickedFeature unwraps the {d, icon} entry shape)
|
|
10170
|
+
this._userChartLastLayers = [new ScatterplotLayer({
|
|
10023
10171
|
id: `ix-bubbles-${this.name}`,
|
|
10024
|
-
data,
|
|
10025
|
-
|
|
10172
|
+
data: items.map(it => ({ d: it.d, icon: true, r: it.r })),
|
|
10173
|
+
pickable: true,
|
|
10026
10174
|
getPosition: e => e.d.geometry.coordinates,
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
billboard: true,
|
|
10031
|
-
// no mipmaps: the charts are drawn at their size on screen
|
|
10032
|
-
textureParameters: { minFilter: 'linear', magFilter: 'linear' },
|
|
10175
|
+
getRadius: e => Math.max(2, e.r),
|
|
10176
|
+
radiusUnits: 'pixels', radiusMinPixels: 1,
|
|
10177
|
+
stroked: false, filled: true, getFillColor: [0, 0, 0, 0],
|
|
10033
10178
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
10034
10179
|
})];
|
|
10035
10180
|
return this._userChartLastLayers;
|
|
10036
10181
|
}
|
|
10037
|
-
//
|
|
10038
|
-
//
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
const
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
if (seen.has(icon.id)) continue;
|
|
10047
|
-
seen.add(icon.id);
|
|
10048
|
-
if (x + icon.width + PAD > W) { x = 0; y += rowH + PAD; rowH = 0; }
|
|
10049
|
-
place.push([icon, x, y]);
|
|
10050
|
-
mapping[icon.id] = { x, y, width: icon.width, height: icon.height, anchorX: icon.anchorX, anchorY: icon.anchorY, mask: false };
|
|
10051
|
-
x += icon.width + PAD; rowH = Math.max(rowH, icon.height);
|
|
10052
|
-
}
|
|
10053
|
-
const canvas = document.createElement('canvas');
|
|
10054
|
-
canvas.width = W; canvas.height = Math.max(1, y + rowH);
|
|
10055
|
-
const ctx = canvas.getContext('2d');
|
|
10056
|
-
for (const [icon, px, py] of place) ctx.drawImage(icon.canvas, px, py);
|
|
10057
|
-
this._userChartAtlasCache = { sig, canvas, mapping };
|
|
10058
|
-
return this._userChartAtlasCache;
|
|
10059
|
-
}
|
|
10060
|
-
// user chart images become ready one by one: one redraw once they are
|
|
10061
|
-
_scheduleUserChartRedraw() {
|
|
10062
|
-
if (this._userChartRedrawTimer) return;
|
|
10063
|
-
this._userChartRedrawTimer = setTimeout(() => {
|
|
10064
|
-
this._userChartRedrawTimer = null;
|
|
10065
|
-
if (this._userChartWaiting && this._triggerRedraw) this._triggerRedraw();
|
|
10066
|
-
}, 30);
|
|
10067
|
-
}
|
|
10068
|
-
// The chart is drawn once (its SVG kept in _userChartSvgs) and rasterised
|
|
10069
|
-
// at its size on screen (× devicePixelRatio, in steps of USER_CHART_RASTER
|
|
10070
|
-
// px) onto a canvas (icon.canvas, once the SVG image has decoded): one
|
|
10071
|
-
// image per chart at a fixed 256 px made the texture of a few hundred
|
|
10072
|
-
// charts outgrow the GPU's texture size — WebGL errors, and deck.gl
|
|
10073
|
-
// frames that failed, leaving the map un-updated after a zoom.
|
|
10074
|
-
_userChartIcon(draw, name, theme, flag, d, value, cat, size, nSize, pxPerUnit, maxRasterPx = Infinity) {
|
|
10075
|
-
const base = `user|${name}|${cat}|${Math.round(value * 100) / 100}|${Math.round(size * 2) / 2}|${flag}`;
|
|
10182
|
+
// the chart's SVG, cached by its draw parameters — a failing draw
|
|
10183
|
+
// (undefined) is tried again on the next redraw (null: draws nothing)
|
|
10184
|
+
_userChartSvg(draw, name, theme, flag, d, value, cat, size, nSize) {
|
|
10185
|
+
// the title is part of the markup now (item.szTitle) — items sharing
|
|
10186
|
+
// value/size/class but not a title must not share a cache entry
|
|
10187
|
+
const raw = d.properties.raw || d.properties;
|
|
10188
|
+
const titleField = this.style.titlefield || this.binding.title;
|
|
10189
|
+
const title = titleField && raw && raw[titleField] != null ? String(raw[titleField]) : undefined;
|
|
10190
|
+
const base = `user|${name}|${cat}|${Math.round(value * 100) / 100}|${Math.round(size * 2) / 2}|${flag}|${title || ''}`;
|
|
10076
10191
|
if (!this._userChartSvgs) this._userChartSvgs = new Map();
|
|
10077
|
-
if (!this._userChartRasters) this._userChartRasters = new Map();
|
|
10078
10192
|
let chart = this._userChartSvgs.get(base);
|
|
10079
10193
|
if (chart === undefined) {
|
|
10080
10194
|
chart = this._drawUserChartSvg(draw, name, theme, flag, d, value, cat, size, nSize);
|
|
10081
|
-
if (chart === undefined) return null;
|
|
10195
|
+
if (chart === undefined) return null;
|
|
10082
10196
|
if (this._userChartSvgs.size >= ICON_CACHE_MAX) this._userChartSvgs.delete(this._userChartSvgs.keys().next().value);
|
|
10083
10197
|
this._userChartSvgs.set(base, chart);
|
|
10084
10198
|
}
|
|
10085
|
-
|
|
10086
|
-
const dpr = (global.devicePixelRatio || 1);
|
|
10087
|
-
const want = Math.max(chart.vw, chart.vh) * pxPerUnit * dpr;
|
|
10088
|
-
const fits = USER_CHART_RASTER.filter(r => r <= maxRasterPx);
|
|
10089
|
-
const steps = fits.length ? fits : USER_CHART_RASTER.slice(0, 1);
|
|
10090
|
-
const px = steps.find(r => r >= want) || steps[steps.length - 1];
|
|
10091
|
-
const key = base + '|' + px;
|
|
10092
|
-
const rasters = this._userChartRasters;
|
|
10093
|
-
if (rasters.has(key)) {
|
|
10094
|
-
// least recently used goes first
|
|
10095
|
-
const hit = rasters.get(key);
|
|
10096
|
-
rasters.delete(key); rasters.set(key, hit);
|
|
10097
|
-
return hit;
|
|
10098
|
-
}
|
|
10099
|
-
const s = px / Math.max(chart.vw, chart.vh);
|
|
10100
|
-
const w = Math.max(1, Math.round(chart.vw * s)), h = Math.max(1, Math.round(chart.vh * s));
|
|
10101
|
-
const icon = { canvas: null, width: w, height: h, anchorX: (chart.ox - chart.vx) * s, anchorY: (chart.oy - chart.vy) * s, id: key, unitHeight: chart.vh };
|
|
10102
|
-
// a chart whose image fails is left out (not waited for)
|
|
10103
|
-
const failed = e => {
|
|
10104
|
-
icon.failed = true;
|
|
10105
|
-
if (!this._userChartImageError) console.warn(`[ixmaps-gl] USER chart ${name}: chart image failed`, e);
|
|
10106
|
-
this._userChartImageError = e || true;
|
|
10107
|
-
this._scheduleUserChartRedraw();
|
|
10108
|
-
};
|
|
10109
|
-
// the SVG goes through a canvas: deck.gl blends an SVG image as decoded
|
|
10110
|
-
// (premultiplied), which darkened half-transparent fills (a
|
|
10111
|
-
// fadenegative arrow turned grey)
|
|
10112
|
-
const img = new Image();
|
|
10113
|
-
img.onerror = failed;
|
|
10114
|
-
img.onload = () => {
|
|
10115
|
-
try {
|
|
10116
|
-
const canvas = document.createElement('canvas');
|
|
10117
|
-
canvas.width = w; canvas.height = h;
|
|
10118
|
-
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
|
10119
|
-
icon.canvas = canvas;
|
|
10120
|
-
} catch (e) { failed(e); return; }
|
|
10121
|
-
this._scheduleUserChartRedraw();
|
|
10122
|
-
};
|
|
10123
|
-
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(chart.svg.replace('__W__', w).replace('__H__', h));
|
|
10124
|
-
if (rasters.size >= ICON_CACHE_MAX) rasters.delete(rasters.keys().next().value);
|
|
10125
|
-
rasters.set(key, icon);
|
|
10126
|
-
return icon;
|
|
10199
|
+
return chart || null;
|
|
10127
10200
|
}
|
|
10128
10201
|
// the page's chart function draws one chart into the hidden SVG: its
|
|
10129
10202
|
// markup and box (chart units), null when it draws nothing, undefined
|
|
@@ -10134,11 +10207,19 @@
|
|
|
10134
10207
|
const color = this._userChartColor(cat);
|
|
10135
10208
|
const raw = d.properties.raw || d.properties;
|
|
10136
10209
|
const label = this.binding.title && raw && raw[this.binding.title] != null ? String(raw[this.binding.title]) : undefined;
|
|
10210
|
+
// flat's chart title (style.titlefield, maptheme's item.szTitle) —
|
|
10211
|
+
// pinnacleChart draws it below the value text (chart.js 204-216)
|
|
10212
|
+
const titleField = this.style.titlefield || this.binding.title;
|
|
10213
|
+
const title = titleField && raw && raw[titleField] != null ? String(raw[titleField]) : undefined;
|
|
10137
10214
|
let chart = null;
|
|
10138
10215
|
try {
|
|
10139
10216
|
const ret = draw.call(global.ixmaps, document, {
|
|
10140
10217
|
target: g, theme, value, values: [value], size, maxSize: USER_CHART_MAX_SIZE, color, class: cat, flag,
|
|
10141
|
-
|
|
10218
|
+
// flat hands the chart the item's color CLASS object (maptheme's
|
|
10219
|
+
// ColorScheme entry) — pinnacleChart reads ccolor.lowColor for its
|
|
10220
|
+
// VALUES label fill (usercharts/d3/chart.js 199)
|
|
10221
|
+
ccolor: { szColor: color, lowColor: color, highColor: color },
|
|
10222
|
+
item: { szColor: color, szLabel: label, szTitle: title, nCount: d.properties.point_count || 1, nSize }, dbRecord: raw
|
|
10142
10223
|
});
|
|
10143
10224
|
const bb = ret ? g.getBBox() : null;
|
|
10144
10225
|
if (bb && bb.width > 0 && bb.height > 0) {
|
|
@@ -10150,7 +10231,7 @@
|
|
|
10150
10231
|
const vx = bb.x - pad, vy = bb.y - pad, vw = bb.width + 2 * pad, vh = bb.height + 2 * pad;
|
|
10151
10232
|
// the chart's origin (0,0 — or the point flat moves it to, ret) sits at the map position
|
|
10152
10233
|
const ox = (ret && ret.x) || 0, oy = (ret && ret.y) || 0;
|
|
10153
|
-
chart = { vx, vy, vw, vh, ox, oy,
|
|
10234
|
+
chart = { vx, vy, vw, vh, ox, oy, markup, ids: [...ids],
|
|
10154
10235
|
svg: `<svg xmlns="${SVG_NS}" width="__W__" height="__H__" viewBox="${vx} ${vy} ${vw} ${vh}"><defs>${defs}</defs>${markup}</svg>` };
|
|
10155
10236
|
}
|
|
10156
10237
|
} catch (e) {
|
|
@@ -11506,7 +11587,7 @@
|
|
|
11506
11587
|
// last built map; called while the map is still building, they run once
|
|
11507
11588
|
// it is ready.
|
|
11508
11589
|
const MAP_HANDLE_METHODS = ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
|
|
11509
|
-
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'setThemeTimeFrame', 'attribution'];
|
|
11590
|
+
'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'setProjection', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'setThemeTimeFrame', 'attribution'];
|
|
11510
11591
|
const _mapHandles = Object.create(null);
|
|
11511
11592
|
function makeMapHandle(szMap) {
|
|
11512
11593
|
const h = {};
|