-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.js
More file actions
281 lines (243 loc) · 10.1 KB
/
Copy pathmain.js
File metadata and controls
281 lines (243 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
// 1. Initialize the Map with Bounds
var bounds = [[-85, -180], [-2, 176]];
// Zoom range notes, because this is easy to break:
// The tile layers below use tileSize 128 + zoomOffset 1, which is hand-rolled
// retina rendering (identical to what Leaflet's detectRetina does on a 2x
// display) and is why the map is pixel-sharp. The consequence is that the tile
// level actually requested is mapZoom + 1, capped by maxNativeZoom: 4.
// The pyramid only goes to level 5, so maxNativeZoom must stay 4 -- raise it
// and every tile 404s and the whole map goes background-grey.
// maxZoom is free to go higher (it just upscales level 5), but past 6 it is
// badly blurred: zoom 5 is a 2x upscale, 6 is 4x, the old 8 was 16x.
// minZoom is not a fixed number -- see updateMinZoom() below.
var map = L.map('map', {
attributionControl: false,
// No +/- control: the wheel, pinch and double-click all zoom already, and
// the buttons only sat on top of the painting.
zoomControl: false,
maxBounds: bounds,
maxBoundsViscosity: 1.0,
minZoom: 0,
maxZoom: 6
}).setView([-66.79, -2], 3);
// Zooming out past the point where the map spans the window leaves it floating
// in open background, so derive the floor from the window instead of hardcoding
// it. On a 1920x1080 screen this lands on 3, which is what it always was; on a
// smaller window it allows one more step out, on a larger one it allows one less.
// (For a stricter floor with no empty space above or below the map either, use
// map.getBoundsZoom(mapBounds, true) instead -- that gives 4 at 1920x1080.)
var mapBounds = L.latLngBounds(bounds);
// Whether we have ever managed to measure the container. Until we have, there
// is no floor to sit the map on -- and applying the *stale* minimum instead is
// exactly how the map ends up a postage stamp floating in open grey.
var floorApplied = false;
function updateMinZoom() {
var viewport = map.getSize();
var viewportWidth = viewport.x;
if (!viewportWidth || !viewport.y) { return; } // container not laid out yet; 'resize' will retry
// On a portrait phone, fitting the map only to the narrow edge leaves a
// small strip of map floating in a great deal of empty ocean. Fill both
// axes there instead: the map remains pannable horizontally, but it is a
// map first rather than a postage stamp. Wide screens retain the original
// overview-friendly, width-based floor.
var fillPortraitViewport = window.matchMedia('(max-width: 700px)').matches;
var floor = map.getMaxZoom();
for (var z = 0; z <= map.getMaxZoom(); z++) {
var nw = map.project(mapBounds.getNorthWest(), z);
var se = map.project(mapBounds.getSouthEast(), z);
var mapWidth = se.x - nw.x;
var mapHeight = se.y - nw.y;
if (mapWidth >= viewportWidth && (!fillPortraitViewport || mapHeight >= viewport.y)) {
floor = z;
break;
}
}
if (floor !== map.getMinZoom()) { map.setMinZoom(floor); }
// Leaflet only raises a below-minimum zoom through an *animated* setZoom,
// which needs a frame to land and silently does nothing if that frame
// never comes. Do it here, unanimated, so the map cannot be left below its
// own floor -- and do it on the first successful measurement, which is
// what puts the map on the floor at startup.
if (!floorApplied || map.getZoom() < floor) {
floorApplied = true;
map.setZoom(floor, { animate: false });
}
}
updateMinZoom();
map.on('resize', updateMinZoom);
// 2. Define the Layers
// --- BASE LAYERS ---
var standardMap = L.tileLayer('./tiles/{z}/{x}/{y}.webp', {
minZoom: 0,
maxZoom: 6,
tileSize: 128,
zoomOffset: 1,
detectRetina: false,
maxNativeZoom: 4,
tms: true,
noWrap: true,
bounds: bounds,
attribution: 'Virelia'
}).addTo(map);
var topoMap = L.tileLayer('./tiles-topo/{z}/{x}/{y}.webp', {
minZoom: 0,
maxZoom: 6,
tileSize: 128,
zoomOffset: 1,
detectRetina: false,
maxNativeZoom: 4,
tms: true,
noWrap: true,
bounds: bounds,
attribution: 'Virelia'
});
// --- OVERLAY LAYERS ---
var labelsMap = L.tileLayer('./tiles-labels/{z}/{x}/{y}.webp', {
minZoom: 0,
maxZoom: 6,
tileSize: 128,
zoomOffset: 1,
detectRetina: false,
maxNativeZoom: 4,
tms: true,
noWrap: true,
bounds: bounds,
zIndex: 10
}).addTo(map);
// D. Interactive Markers (Group Wrapper)
var markersLayer = L.layerGroup().addTo(map);
// 3. Add the Layer Control
var baseMaps = {
"Standard": standardMap,
"Topographic": topoMap
};
var overlayMaps = {
"Overlay": labelsMap,
"Pop-Ups": markersLayer
};
L.control.layers(baseMaps, overlayMaps, { position: 'topright', collapsed: false }).addTo(map);
// --- POINT TYPE REGISTRY ---
// To add a new pointer type: add an entry here, plus a matching `.type-<id>` rule in style.css.
// Edit mode's toolbar, the icon lookup, and the zoom-visibility rules all derive from this list.
var pointTypes = [
{ id: 'region', label: 'Region', size: 30, zoom: [0, 4] },
{ id: 'city', label: 'City', size: 22, zoom: [0, 6] },
{ id: 'town', label: 'Town', size: 16, zoom: [5, 6] },
{ id: 'poi', label: 'POI', size: 10, zoom: [5, 6] }
];
pointTypes.forEach(function(pt) {
var s = pt.size;
pt.icon = L.divIcon({
className: 'marker-pin type-' + pt.id,
iconSize: [s, s],
iconAnchor: [s / 2, s / 2]
});
});
function getPointType(type) {
var t = (type || 'poi').toLowerCase();
for (var i = 0; i < pointTypes.length; i++) {
if (pointTypes[i].id === t) return pointTypes[i];
}
return pointTypes[pointTypes.length - 1]; // fallback to last (POI)
}
function getIcon(type) {
return getPointType(type).icon;
}
// Storage Arrays
var markers = {}; // For sidebar links (lookup by ID)
var allMarkers = []; // For zoom logic (list of all objects)
for (let key in wikiData) {
let entry = wikiData[key];
if (entry.coords) {
let selectedIcon = getIcon(entry.type);
// Create marker BUT DO NOT ADD TO MAP YET.
// Leaflet makes every marker focusable (tabindex 0, role button), so
// without a name the map is 42 anonymous buttons to a keyboard or a
// screen reader. `title` is the tooltip; `alt` becomes the aria-label.
let label = entry.title + (entry.type ? ', ' + entry.type : '');
let marker = L.marker(entry.coords, {
icon: selectedIcon,
title: entry.title,
alt: label
});
// These are divIcons, and Leaflet only turns `alt` into markup for an
// <img>. The layer group tears markers down and rebuilds them on every
// zoom step, so the name is reapplied on each add rather than once.
marker.on('add', function () {
let el = marker.getElement();
if (el) el.setAttribute('aria-label', label);
});
// Store references
markers[key] = marker;
allMarkers.push({
id: key,
marker: marker,
type: entry.type ? entry.type.toLowerCase() : 'poi'
});
marker.on('click', function(e) {
if (window.editMode && window.editMode.active) {
L.DomEvent.stopPropagation(e);
window.editMode.loadEntry(key);
return;
}
// Clicking the pin you are already reading puts it away. The
// marker is the entry's switch, so it should turn off as well as
// on rather than re-opening what is open.
var sidebar = document.getElementById('sidebar');
if (sidebar.classList.contains('active') && sidebar.dataset.entry === key) {
closeSidebar();
return;
}
openEntry(key);
});
}
}
// --- CLICKING THE MAP ITSELF ---
// An open entry is dismissed by clicking away from it, which is what the rest
// of the page already does: the wiki, the book, the search panel and the
// lightbox all close on their surround.
//
// A drag is not a click. Leaflet does suppress the click that ends a pan, but
// the panel is easy to lose by accident and the guess is cheap to make
// ourselves: remember where the press started and let go of the idea if the
// pointer travelled.
var PRESS_SLOP = 5; // px of travel still counted as a click, not a drag
var pressedAt = null;
map.on('mousedown', function (e) { pressedAt = e.containerPoint; });
map.on('click', function (e) {
// Edit mode uses map clicks to place markers; leave it alone.
if (window.editMode && window.editMode.active) return;
if (pressedAt && e.containerPoint.distanceTo(pressedAt) > PRESS_SLOP) return;
var sidebar = document.getElementById('sidebar');
if (sidebar.classList.contains('active')) closeSidebar();
});
// --- VISIBILITY ENGINE (Semantic Zooming) ---
function updateVisibleMarkers() {
var currentZoom = map.getZoom();
// Clear current markers from the layer group
markersLayer.clearLayers();
// Visibility rules come from the pointTypes registry (zoom: [min, max])
allMarkers.forEach(function(item) {
var range = getPointType(item.type).zoom;
if (currentZoom >= range[0] && currentZoom <= range[1]) {
item.marker.addTo(markersLayer);
}
});
}
// Run once on load, and then every time zoom changes
updateVisibleMarkers();
map.on('zoomend', updateVisibleMarkers);
// --- SIDEBAR & LIGHTBOX LOGIC ---
// Rendering, cross-linking, history and map focus all live in wiki-runtime.js
// and regions.js. These two stay as the names the rest of the page (and the
// inline onclick in index.html) already call.
window.openEntry = function(key) {
window.WikiRuntime.open(key);
};
window.closeSidebar = function() {
window.WikiRuntime.close();
};
// Edit mode (loaded via edit.js when ?edit is present in the URL) hooks into:
// - map clicks (to drop new markers)
// - existing marker clicks (to load entries — see marker.on('click') above)
// - the pointTypes registry (to build its toolbar dynamically)