PageSourceSearch

https://www.duinrell.com/modules/map.js

js duinrell.com collected 2026-10-01 14:50:30 UTC 9,910 bytes, 303 lines download raw bytes

1// maps.js (ES6 module)
2
3import {lib} from './lib.js'; // must expose loadExt on lib
4
5let initialized = false;
6
7let readyPromiseResolve;
8export const ready = new Promise((resolve) => {
9    readyPromiseResolve = resolve;
10});
11
12/**
13 * Lazy loader based on IntersectionObserver + fallback
14 */
15function initLazyLoader() {
16    const callback = (entries) => {
17        if (initialized) return;
18        if (entries[0].isIntersecting) {
19            load();
20        }
21    };
22
23    if (window.IntersectionObserver) {
24        const observer = new IntersectionObserver(callback);
25        document.querySelectorAll('.module.module-map')
26            .forEach(map => observer.observe(map));
27    }
28
29    // fallback you already use
30    onViewport('.module.module-map', null, null, function (visible) {
31        if (visible && !initialized) {
32            load();
33        }
34    });
35}
36
37function load() {
38    if (initialized) return;
39    initialized = true;
40
41    lib.loadExt([
42        'https://unpkg.com/[email protected]/dist/leaflet.css',
43        'https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/leaflet.fullscreen.css'
44    ], () => {
45        lib.loadExt([
46            'https://unpkg.com/[email protected]/dist/leaflet.js',
47            'https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/Leaflet.fullscreen.min.js',
48            'https://unpkg.com/[email protected]/leaflet.responsive.popup.js',
49            'https://unpkg.com/[email protected]/leaflet.responsive.popup.css'
50        ], () => {
51            document.body.dispatchEvent(
52                new CustomEvent('maps:scripts_loaded', {
53                    bubbles: true,
54                    cancelable: true,
55                    detail: {init}
56                })
57            );
58            init();
59        });
60    });
61}
62
63function init_zoom(map) {
64    // kept intentionally empty (your commented logic removed)
65}
66
67export const Maps = {
68    create(node, map_src, mapWidth, mapHeight, minZoom, maxZoom) {
69        let width = 100;
70        let height = 100;
71
72        const map = new L.FreeMap(node, {
73            width: mapWidth,
74            height: mapHeight,
75            scrollWheelZoom: true,
76            crs: L.extend({}, L.CRS.Simple, {
77                zoom(scale) {
78                    return Math.log(scale / (node.clientWidth / width * 0.5)) / Math.LN2;
79                },
80                scale(zoom) {
81                    return Math.pow(2, zoom) * (node.clientWidth / width * 0.5);
82                },
83                transformation: new L.Transformation(1, 0, mapHeight / mapWidth, 0),
84            }),
85            zoomSnap: 0,
86            zoomControl: false,
87            minZoom,
88            maxZoom,
89            attributionControl: false
90        });
91
92        const lt = [0, 0];
93        const rb = [height, width];
94        const mapBounds = new L.LatLngBounds(lt, rb);
95
96        map.setView([height / 2, width / 2], 1);
97        map.setMaxBounds(map.getBounds());
98
99        const overlay = L.imageOverlay(map_src, mapBounds).addTo(map);
100        const wrapper = node.firstElementChild;
101
102        //let lastWidth = wrapper.clientWidth;
103
104        const ro = new ResizeObserver(() => {
105            const w = wrapper.clientWidth;
106            const h = Math.round(w * (mapHeight / mapWidth));
107            requestAnimationFrame(() => {
108                wrapper.style.height = h + 'px';
109                const center = map.getCenter();
110                const zoom = map.getZoom();
111
112                // overlay bounds stay consistent with normalized CRS
113                overlay.setBounds(L.latLngBounds([0, 0], [height, width]));
114
115                map.invalidateSize({animate: false});
116                map.setView(center, zoom, {animate: false});
117            });
118        });
119
120        ro.observe(wrapper);
121
122        L.control.zoom({position: 'bottomright'}).addTo(map);
123
124        const toggleFullscreen = map.toggleFullscreen;
125        map.toggleFullscreen = function (options) {
126            toggleFullscreen.call(map, options);
127            map.setView([height / 2, width / 2], 1.000001);
128        };
129
130        map.addControl(new L.Control.Fullscreen({
131            position: 'topright',
132            title: {
133                false: 'View Fullscreen',
134                true: 'Exit Fullscreen'
135            }
136        }));
137
138        node.addEventListener('keydown', function (e) {
139            if (e.key === 'Escape' && map._popup && map._popup.options.closeOnEscapeKey) {
140                e.preventDefault();
141                e.stopPropagation();
142                map.closePopup();
143            }
144        }, true);
145
146        map.markers = L.featureGroup().addTo(map);
147
148        init_zoom(map);
149        return map;
150    }
151};
152
153function init() {
154    // document.dispatchEvent(
155    //     new CustomEvent('MapLoaded', {bubbles: true, cancelable: true})
156    // );
157
158    // Grayscale tile layer
159    L.TileLayer.Grayscale = L.TileLayer.extend({
160        options: {
161            quotaRed: 21,
162            quotaGreen: 71,
163            quotaBlue: 8,
164            quotaDividerTune: 0,
165            quotaDivider() {
166                return this.quotaRed + this.quotaGreen + this.quotaBlue + this.quotaDividerTune;
167            }
168        },
169        initialize(url, options = {}) {
170            options.crossOrigin = true;
171            L.TileLayer.prototype.initialize.call(this, url, options);
172            this.on('tileload', e => this._makeGrayscale(e.tile));
173        },
174        _createTile() {
175            const tile = L.TileLayer.prototype._createTile.call(this);
176            tile.crossOrigin = 'Anonymous';
177            return tile;
178        },
179        _makeGrayscale(tile) {
180            if (tile.getAttribute('data-grayscaled')) return;
181
182            tile.crossOrigin = '';
183            const canvas = document.createElement('canvas');
184            canvas.width = tile.width;
185            canvas.height = tile.height;
186
187            const ctx = canvas.getContext('2d');
188            ctx.drawImage(tile, 0, 0);
189
190            const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
191            const data = imgData.data;
192
193            for (let i = 0; i < data.length; i += 4) {
194                const gray = (
195                    this.options.quotaRed * data[i] +
196                    this.options.quotaGreen * data[i + 1] +
197                    this.options.quotaBlue * data[i + 2]
198                ) / this.options.quotaDivider();
199
200                data[i] = data[i + 1] = data[i + 2] = gray;
201            }
202
203            ctx.putImageData(imgData, 0, 0);
204            tile.setAttribute('data-grayscaled', 'true');
205            tile.src = canvas.toDataURL();
206        }
207    });
208
209    L.tileLayer.grayscale = function (url, options) {
210        return new L.TileLayer.Grayscale(url, options);
211    };
212
213    // FreeMap
214    L.FreeMap = L.Map.extend({
215        initialize(element, options) {
216            element.style.position = 'relative';
217            element.style.overflow = 'hidden';
218            console.log(options);
219            const wrapper = document.createElement('div');
220            const container = document.createElement('div');
221            const popup_container = document.createElement('div');
222
223            const aspect = wrapper.appendChild(document.createElement('div'));
224            aspect.style.position = 'relative';
225            aspect.style.paddingBottom = Math.ceil(options.height / options.width * 100) + '%';
226
227            setTimeout(() => {
228                this.invalidateSize({animate: false});
229            }, 0);
230
231            element.appendChild(wrapper);
232            wrapper.appendChild(container);
233            wrapper.appendChild(popup_container);
234
235            Object.assign(popup_container.style, {
236                position: 'absolute',
237                left: 0,
238                top: 0,
239                zIndex: 1000,
240                overflow: 'visible'
241            });
242
243            L.DomUtil.addClass(
244                popup_container,
245                'leaflet-freepopup-pane leaflet-container'
246            );
247
248            Object.assign(container.style, {
249                position: 'absolute',
250                left: 0,
251                top: '0',
252                width: '100%',
253                height: '100%'
254            });
255
256            L.Map.prototype.initialize.call(this, container, options);
257        }
258    });
259
260    // FreePopup
261    L.FreePopup = L.ResponsivePopup.extend({
262        getEvents() {
263            const events = L.Popup.prototype.getEvents.call(this);
264            events.move = this._adjustPan;
265            return events;
266        },
267        onAdd(map) {
268            L.Popup.prototype.onAdd.call(this, map);
269            map.getContainer()
270                .parentElement
271                .querySelector('.leaflet-freepopup-pane')
272                .appendChild(this.getElement());
273        },
274        _adjustPan() {
275            const pos = this._map.latLngToContainerPoint(
276                this._source.getLatLng()
277            );
278            const anchor = this._getAnchor();
279            const newPos = pos.add(anchor);
280
281            L.DomUtil.setPosition(this._container, newPos);
282
283            if (!this._map.getBounds().contains(this._source.getLatLng())) {
284                L.DomUtil.addClass(this._container, 'hidden');
285            } else if (L.DomUtil.hasClass(this._container, 'hidden')) {
286                L.DomUtil.removeClass(this._container, 'hidden');
287            }
288        }
289    });
290
291    // document.dispatchEvent(
292    //     new CustomEvent('maps_ready', {
293    //         bubbles: true,
294    //         cancelable: false,
295    //         detail: {maps: Maps}
296    //     })
297    // );
298    //
299    readyPromiseResolve(Maps);
300}
301
302// start
303initLazyLoader();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.