PageSourceSearch

https://www.survivalinternational.de/assets/controllers/numiko/mapbox_globe_controller-0965106f.js

js survivalinternational.de collected 2026-09-24 22:29:56 UTC 6,688 bytes, 200 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2import mapboxgl from "mapbox-gl"
3import supportsInteractionObserver from "utils/supports_interaction_observer"
4
5// Connects to data-controller="numiko--mapbox-globe"
6export default class extends Controller {
7  static targets = ['globeWrapper']
8  static values = { token: String, style: String }
9
10  initialize() {
11    this.map = null;
12  }
13
14  connect () {
15    if (supportsInteractionObserver()) {
16      this.#setupObserver();
17    } else {
18      this.#loadMapbox();
19    }
20  }
21
22  disconnect () {
23    if (this.intersectionObserver) {
24      this.intersectionObserver.disconnect();
25    }
26    this.map?.remove();
27  }
28
29  #setupObserver () {
30    this.intersectionObserver = new window.IntersectionObserver(([entry]) => {
31      if (entry.isIntersecting) {
32        this.#loadMapbox();
33        return;
34      }
35    }, { root: null, threshold: 0.1 })
36
37    this.intersectionObserver.observe(this.element);
38  }
39
40  #loadMapbox () {
41    const globeContainerElem = this.globeWrapperTarget;
42    let allowSpin = true;
43    /* Should make file .js.erb and interpolate via ENV key */
44    mapboxgl.accessToken = this.tokenValue;
45    this.map = new mapboxgl.Map({
46      container: 'mapbox-globe',
47      style: this.styleValue,
48      center: [0, 10],
49      projection: { name: 'globe' },
50      zoom: 1.2,
51      minZoom: 1.2,
52      scrollZoom: false,
53      dragRotate: false,
54      renderWorldCopies: false
55    });
56    let nav = new mapboxgl.NavigationControl();
57    this.map.addControl(nav, 'top-right');
58    this.map.on('load', () => {
59      this.map.setFog({});
60      this.spinGlobe(this.map, allowSpin);
61      this.map.addSource('pointsOnGlobe', {
62        type: 'geojson',
63        data: {
64          "type":"FeatureCollection",
65          "features": JSON.parse(globeContainerElem.dataset.mapboxGlobePoints).map((p
65oint) => JSON.parse(point))
66        },
67        cluster: true,
68        clusterMaxZoom: 14,
69        clusterRadius: 50
70      });
71      this.map.on('moveend', () => {
72        this.spinGlobe(this.map, allowSpin);
73      });
74      this.map.on('click', (e) => {
75        allowSpin = false;
76        this.stopSpin(this.map);
77      })
78      this.map.addLayer({
79        id: 'clusters',
80        type: 'circle',
81        source: 'pointsOnGlobe',
82        filter: ['has', 'point_count'],
83        paint: {
84          'circle-color': ['step', ['get', 'point_count'], '#B33D26', 100, '#B33D26', 750, '#B33D26'],
85          'circle-radius': ['step', ['get', 'point_count'], 20, 10, 30, 20, 40, 30, 60]
86        }
87      });
88      this.map.addLayer({
89        id: 'cluster-count',
90        type: 'symbol',
91        source: 'pointsOnGlobe',
92        filter: ['has', 'point_count'],
93        layout: {
94          'text-field': '{point_count_abbreviated}',
95          'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
96          'text-size': 14
97        },
98        paint: {
99          'text-color': '#ffffff'
100        }
101      });
102      this.map.addLayer({
103        id: 'unclustered-point',
104        type: 'circle',
105        source: 'pointsOnGlobe',
106        filter: ['!', ['has', 'point_count']],
107        paint: {
108          'circle-color': '#B33D26',
109          'circle-radius': 8,
110          'circle-stroke-width': 1,
111          'circle-stroke-color': '#fff'
112        }
113      });
114      this.map.on('click', 'clusters', (e) => {
115        var clusterId, features;
116        features = this.map.queryRenderedFeatures(e.point, {
117          layers: ['clusters']
118        });
119        clusterId = features[0].properties.cluster_id;
120        this.map.getSource('pointsOnGlobe').getClusterExpansionZoom(clusterId, function(err, zoom) {
121          if (err) {
122            return;
123          }
124          this.map.easeTo({
125            center: features[0].geometry.coordinates,
126            zoom: zoom + 1
127          });
128        });
129      });
130      this.map.on('mouseenter', 'clusters', () => {
131        this.map.getCanvas().style.cursor = 'pointer';
132      });
133      this.map.on('mouseleave', 'clusters', () => {
134        this.map.getCanvas().style.cursor = '';
135      });
136      this.map.on('click', 'unclustered-point', (e) => {
137        var coordinates, linktext, popupHtml, properties;
138        coordinates = e.features[0].geometry.coordinates.slice();
139        properties = e.features[0].properties;
140        popupHtml = `<h3>${properties.title}</h3>`;
141        if (properties.subhead) { popupHtml += `<h5>${properties.subhead}</h5>`; }
142        if (properties.imageSrc) {
143          popupHtml += `<img class="mapbox--globe-popup" src="${properties.imageSrc}" />`;
144        } else if (properties.vimeoId) {
145          popupHtml += `<div class="mapbox--globe-popup"><iframe class="mapbox--globe-popup" src="https://player.vimeo.com/video/${properties.vimeoId}?color=B33D26" allowfullscreen></iframe></div>`;
146        }
147        if (properties.description) {
148          popupHtml += `<p>${properties.description}</p>`;
149        }
150        if (properties.link) {
151          linktext = 'Find out more';
152          if (properties.linktext) {
153            linktext = properties.linktext;
154          }
155          popupHtml += `<a class="btn btn-primary" href="${properties.link}">${linktext}</a>`;
156        }
157        while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
158          coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
159        }
160        (new mapboxgl.Popup).setLngLat(coordinates).setHTML(popupHtml).addTo(this.map);
161      });
162      this.map.on('mouseenter', 'unclustered-point', () => {
163        this.map.getCanvas().style.cursor = 'pointer';
164      });
165      this.map.on('mouseleave', 'unclustered-point', () => {
166        this.map.getCanvas().style.cursor = '';
167      });
168    });
169  }
170
171  spinGlobe(map, allowSpin) {
172    if (allowSpin) {
173      // Rotate at intermediate speeds between zoom levels 3 and 5.
174      const slowSpinZoom = 3;
175      // At low zooms, complete a revolution every two minutes.
176      const secondsPerRevolution = 120;
177      // Above zoom level 5, do not rotate.
178      const maxSpinZoom = 5;
179      const zoom = this.map.getZoom();
180      if (zoom < maxSpinZoom) {
181        let distancePerSecond = 360 / secondsPerRevolution;
182        if (zoom > slowSpinZoom) {
183            // Slow spinning at higher zooms
184            const zoomDif =
185                (maxSpinZoom - zoom) / (maxSpinZoom - slowSpinZoom);
186            distancePerSecond *= zoomDif;
187        }
188        const center = this.map.getCenter();
189        center.lng -= distancePerSecond;
190        // Smoothly animate the map over one second.
191        // When this animation is complete, it calls a 'moveend' event.
192        this.map.easeTo({ center, duration: 1000, easing: (n) => n });
193      }
194    }
195  }
196
197  stopSpin(map) {
198    map.stop();
199  }
200}

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.