PageSourceSearch

https://hypercities.com/js/map-view.js?v=notes-navigation-1

js hypercities.com collected 2026-09-25 18:52:51 UTC 22,096 bytes, 478 lines download raw bytes

1import { asPolygon, containsCoordinate, tileDiagnostic, tileTemplate } from "./data.js";
2
3// HyperCities book-cover red, with a quieter shade for unselected extents.
4const RED = [166, 42, 38];
5const RED_BRIGHT = [218, 56, 51];
6const ESRI_WORLD_IMAGERY = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}";
7const GOOGLE_ROAD_MAP = "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}";
8const NOTE_ICON = `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="40" height="48" viewBox="0 0 40 48"><path d="M20 2c-10 0-18 8-18 18 0 13 18 26 18 26s18-13 18-26C38 10 30 2 20 2Z" fill="#da3833" stroke="#f5eee4" stroke-width="2"/><path d="M12 15h16M12 20h16M12 25h11" stroke="#fffaf0" stroke-width="2" stroke-linecap="round"/></svg>')}`;
9const NOTE_ICON_ACTIVE = `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="40" height="48" viewBox="0 0 40 48"><path d="M20 2c-10 0-18 8-18 18 0 13 18 26 18 26s18-13 18-26C38 10 30 2 20 2Z" fill="#fffaf0" stroke="#da3833" stroke-width="3"/><path d="M12 15h16M12 20h16M12 25h11" stroke="#a62a26" stroke-width="2.4" stroke-linecap="round"/></svg>')}`;
10const NOTE_ICON_PENDING = `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="40" height="48" viewBox="0 0 40 48"><path d="M20 2c-10 0-18 8-18 18 0 13 18 26 18 26s18-13 18-26C38 10 30 2 20 2Z" fill="#111212" stroke="#da3833" stroke-width="2"/><path d="M12 15h16M12 20h16M12 25h11" stroke="#fffaf0" stroke-width="2" stroke-linecap="round"/></svg>')}`;
11
12function grayscale(color, dimming = 0.45) {
13  if (typeof color !== "string") return null;
14  let red; let green; let blue; let alpha = null;
15  const hex = color.match(/^#([0-9a-f]{3,8})$/i);
16  const rgb = color.match(/^rgba?\(([^)]+)\)$/i);
17  if (hex) {
18    const value = hex[1].length <= 4
19      ? [...hex[1]].map((character) => character + character).join("")
20      : hex[1];
21    red = Number.parseInt(value.slice(0, 2), 16);
22    green = Number.parseInt(value.slice(2, 4), 16);
23    blue = Number.parseInt(value.slice(4, 6), 16);
24    alpha = value.length === 8 ? Number.parseInt(value.slice(6, 8), 16) / 255 : null;
25  } else if (rgb) {
26    const values = rgb[1].split(",").map((value) => value.trim());
27    [red, green, blue] = values.slice(0, 3).map(Number);
28    alpha = values[3] === undefined ? null : Number(values[3]);
29  } else return null;
30  if (![red, green, blue].every(Number.isFinite)) return null;
31  const lightness = Math.round((red * 0.2126 + green * 0.7152 + blue * 0.0722) * dimming);
32  return alpha === null ? `rgb(${lightness}, ${lightness}, ${lightness})` : `rgba(${lightness}, ${lightness}, ${lightness}, ${alpha})`;
33}
34
35export class MapView {
36  constructor({ maps, onCore, onDepth, onPreview, onFieldEncounter, onInteraction, onTileStatus, onBasemapChange, onRasterOpacityChange, onAnnotationPlace, onAnnotationSelect, onAnnotationViewMove }) {
37    Object.assign(this, { maps, onCore, onDepth, onPreview, onFieldEncounter, onInteraction, onTileStatus, onBasemapChange, onRasterOpacityChange, onAnnotationPlace, onAnnotationSelect, onAnnotationViewMove, hoverIds: new Set(), annotations: [] });
38    this.rasterLayerId = "historical-raster";
39    this.rasterSourceId = "historical-raster-source";
40    this.satelliteLayerId = "esri-world-imagery";
41    this.satelliteSourceId = "esri-world-imagery-source";
42    this.googleLayerId = "google-road-map";
43    this.googleSourceId = "google-road-map-source";
44    this.basemapMode = "dark";
45    this.rasterOpacity = 1;
46  }
47
48  async init() {
49    this.map = new maplibregl.Map({
50      container: "map",
51      style: "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json",
52      center: [10, 27], zoom: 1.7, minZoom: 1.25, attributionControl: false,
53      // Arrow keys belong to HyperCities: up/down move through the TimeWell
54      // and left/right follow the current conceptual path, never the basemap.
55      keyboard: false
56    });
57    await new Promise((resolve) => this.map.once("load", resolve));
58    this.neutraliseBasemap();
59    this.darkBasemapLayerIds = (this.map.getStyle().layers || []).map((layer) => layer.id);
60    this.installSatelliteBasemap();
61    this.installGoogleBasemap();
62    this.overlay = new deck.MapboxOverlay({ interleaved: true, layers: [] });
63    this.map.addControl(this.overlay);
64    this.map.on("mousemove", (event) => this.handleMove(event.lngLat));
65    this.map.on("click", (event) => { if (!this.annotationMode) this.handleClick(event.lngLat); });
66    // A note is placed on the visible raster, so use the canvas's own click
67    // coordinates while this mode is armed. It remains reliable when a deck.gl
68    // layer is present and avoids treating a map pan as a placement.
69    this.map.getCanvas().addEventListener("click", (event) => {
70      if (!this.annotationMode) return;
71      const bounds = this.map.getCanvas().getBoundingClientRect();
72      this.handleClick(this.map.unproject([event.clientX - bounds.left, event.clientY - bounds.top]));
73    }, true);
74    this.map.on("move", () => this.onAnnotationViewMove?.());
75    this.map.on("zoomstart", (event) => { this.visitorZooming = Boolean(event.originalEvent); });
76    this.map.on("zoomend", () => {
77      if (this.coreActive && this.visitorZooming) this.onInteraction?.("zoom-change");
78      this.visitorZooming = false;
79    });
80    this.map.on("error", (event) => this.handleMapError(event));
81    this.map.on("sourcedata", (event) => {
82      if (event.sourceId === this.rasterSourceId && event.isSourceLoaded && this.activeTile) {
83        this.onTileStatus({ ...this.activeTile, state: "loaded", message: `${this.activeTile.message} Current requested tiles loaded over HTTPS.` });
84      }
85    });
86    this.renderFootprints();
87    this.onBasemapChange?.({ mode: this.basemapMode });
88    this.onRasterOpacityChange?.({ opacity: this.rasterOpacity, hasHistorical: false });
89  }
90
91  installSatelliteBasemap() {
92    if (!this.map.getSource(this.satelliteSourceId)) {
93      this.map.addSource(this.satelliteSourceId, {
94        type: "raster",
95        tiles: [ESRI_WORLD_IMAGERY],
96        tileSize: 256,
97        maxzoom: 19,
98        attribution: "© Esri"
99      });
100    }
101    if (!this.map.getLayer(this.satelliteLayerId)) {
102      this.map.addLayer({
103        id: this.satelliteLayerId,
104        type: "raster",
105        source: this.satelliteSourceId,
106        layout: { visibility: "none" }
107      });
108    }
109  }
110
111  installGoogleBasemap() {
112    if (!this.map.getSource(this.googleSourceId)) {
113      this.map.addSource(this.googleSourceId, {
114        type: "raster",
115        tiles: [GOOGLE_ROAD_MAP],
116        tileSize: 256,
117        maxzoom: 21,
118        attribution: "Map data © Google"
119      });
120    }
121    if (!this.map.getLayer(this.googleLayerId)) {
122      this.map.addLayer({
123        id: this.googleLayerId,
124        type: "raster",
125        source: this.googleSourceId,
126        layout: { visibility: "none" }
127      });
128    }
129  }
130
131  setBasemap(mode) {
132    if (!this.map || !["dark", "satellite", "google"].includes(mode)) return;
133    const satellite = mode === "satellite";
134    const google = mode === "google";
135    this.darkBasemapLayerIds.forEach((id) => {
136      if (!this.map.getLayer(id)) return;
137      this.map.setLayoutProperty(id, "visibility", satellite || google ? "none" : "visible");
138    });
139    if (this.map.getLayer(this.satelliteLayerId)) {
140      this.map.setLayoutProperty(this.satelliteLayerId, "visibility", satellite ? "visible" : "none");
141    }
142    if (this.map.getLayer(this.googleLayerId)) {
143      this.map.setLayoutProperty(this.googleLayerId, "visibility", google ? "visible" : "none");
144    }
145    this.basemapMode = mode;
146    this.onBasemapChange?.({ mode });
147  }
148
149  setRasterOpacity(opacity) {
150    const next = Math.max(0, Math.min(1, Number(opacity)));
151    if (!Number.isFinite(next)) return;
152    this.rasterOpacity = next;
153    if (this.map?.getLayer(this.rasterLayerId)) {
154      this.map.setPaintProperty(this.rasterLayerId, "raster-opacity", next);
155    }
156    this.onRasterOpacityChange?.({ opacity: next, hasHistorical: Boolean(this.map?.getLayer(this.rasterLayerId)) });
157  }
158
159  neutraliseBasemap() {
160    const colorProperties = {
161      background: ["background-color"],
162      fill: ["fill-color", "fill-outline-color"],
163      line: ["line-color"],
164      circle: ["circle-color", "circle-stroke-color"],
165      symbol: ["text-color", "icon-color"]
166    };
167    for (const layer of this.map.getStyle().layers || []) {
168      // Carto defines the most visible country line as a zoom-stop expression,
169      // not a literal color. Handle that pair explicitly so it cannot bypass
170      // the generic colour neutralisation below.
171      if (layer.id === "boundary_country_outline" || layer.id === "boundary_country_inner") {
172        this.map.setPaintProperty(layer.id, "line-color", "rgb(48, 48, 48)");
173        this.map.setPaintProperty(layer.id, "line-opacity", layer.id === "boundary_country_inner" ? 0.34 : 0.16);
174        continue;
175      }
176      for (const property of colorProperties[layer.type] || []) {
177        // Country and administrative boundary lines are context, never a
178        // competing graphic system. Keep fills unchanged, but dim linework
179        // more strongly than the rest of the neutral basemap.
180        const neutral = grayscale(this.map.getPaintProperty(layer.id, property), layer.type === "line" ? 0.24 : 0.45);
181        if (neutral) this.map.setPaintProperty(layer.id, property, neutral);
182      }
183      if (layer.type === "raster") this.map.setPaintProperty(layer.id, "raster-saturation", -1);
184    }
185  }
186
187  handleMove(lngLat) {
188    if (this.coreActive) return;
189    const matches = this.maps.filter((map) => containsCoordinate(map, lngLat));
190    const ids = new Set(matches.map((map) => map.id));
191    const changed = ids.size !== this.hoverIds.size || [...ids].some((id) => !this.hoverIds.has(id));
192    if (changed) {
193      this.hoverIds = ids;
194      this.onDepth(matches, lngLat);
195      this.onFieldEncounter?.(matches, this.map.project(lngLat), lngLat);
196      this.renderFootprints();
197    }
198    this.onPreview(matches, lngLat);
199  }
200
201  handleClick(lngLat) {
202    if (this.coreActive && this.annotationMode) {
203      if (this.selectedMap) this.onAnnotationPlace?.(lngLat);
204      return;
205    }
206    if (!this.coreActive) this.onCore(this.maps.filter((map) => containsCoordinate(map, lngLat)), lngLat);
207  }
208
209  handleMapError(event) {
210    if (event.sourceId === this.rasterSourceId || event.error?.message?.includes(this.rasterSourceId)
210) {
211      this.onTileStatus({ state: "failed", message: "The browser could not load this raster source; the footprint remains available." });
212    }
213  }
214
215  forwardTimewellGesture(event) {
216    const canvas = this.map?.getCanvas();
217    if (!canvas) return;
218    const common = {
219      bubbles: true,
220      cancelable: true,
221      clientX: event.clientX,
222      clientY: event.clientY,
223      screenX: event.screenX,
224      screenY: event.screenY,
225      ctrlKey: event.ctrlKey,
226      shiftKey: event.shiftKey,
227      altKey: event.altKey,
228      metaKey: event.metaKey
229    };
230    if (event.type === "wheel") {
231      canvas.dispatchEvent(new WheelEvent("wheel", {
232        ...common,
233        deltaX: event.deltaX,
234        deltaY: event.deltaY,
235        deltaZ: event.deltaZ,
236        deltaMode: event.deltaMode
237      }));
238      return;
239    }
240    if (event.type === "click") {
241      canvas.dispatchEvent(new MouseEvent("click", common));
242      return;
243    }
244    const pointerInit = {
245      ...common,
246      button: event.button,
247      buttons: event.buttons,
248      pointerId: event.pointerId,
249      pointerType: event.pointerType,
250      isPrimary: event.isPrimary,
251      pressure: event.pressure
252    };
253    canvas.dispatchEvent(new PointerEvent(event.type, pointerInit));
254    // MapLibre's mouse gesture handler remains the reliable route on desktop;
255    // synthetic PointerEvents do not generate the browser's compatibility
256    // MouseEvents by themselves.
257    if (event.pointerType === "mouse") {
258      const mouseType = { pointerdown: "mousedown", pointermove: "mousemove", pointerup: "mouseup", pointercancel: "mouseup" }[event.type];
259      if (mouseType) canvas.dispatchEvent(new MouseEvent(mouseType, pointerInit));
260    }
261  }
262
263  renderFootprints() {
264    const layers = [new deck.PolygonLayer({
265      // Once a raster is selected, only its own footprint remains. The other
266      // archival extents return when the visitor surfaces to the map field.
267      id: "historical-map-extents", data: this.selectedMapId ? [this.selectedMap] : this.maps, pickable: true, stroked: true, filled: false,
268      getPolygon: (map) => asPolygon(map),
269      getLineColor: (map) => map.id === this.timewellHoverId
270          ? [...RED_BRIGHT, 230]
271        : map.id === this.selectedMapId
272          ? [241, 237, 228, 230]
273        : this.coreIds?.has(map.id)
274          ? [...RED_BRIGHT, 135]
275        : this.hoverIds.has(map.id)
276            ? [...RED_BRIGHT, 198]
277            : [...RED, 81],
278      getLineWidth: (map) => map.id === this.timewellHoverId ? 2.2 : map.id === this.selectedMapId ? 2.5 : this.coreIds?.has(map.id) ? 1.4 : 1,
279      lineWidthUnits: "pixels",
280      updateTriggers: { getLineColor: [[...this.hoverIds].join(","), this.selectedMapId, this.timewellHoverId, [...(this.coreIds || [])].join(",")] }
281    })];
282    if (this.coreCoordinate) {
283      layers.push(new deck.ScatterplotLayer({
284        id: "core-origin", data: [this.coreCoordinate], pickable: false, filled: true, stroked: true,
285        getPosition: (point) => [point.lng, point.lat], getRadius: 18, radiusUnits: "pixels",
286        getFillColor: [...RED, 50], getLineColor: [...RED_BRIGHT, 255], getLineWidth: 2,
287        parameters: { depthTest: false }
288      }));
289    }
290    if (this.annotationsVisible && this.annotations.length) {
291      const crowded = this.annotations.length >= 50;
292      const noteSize = crowded ? 18 : this.annotations.length >= 20 ? 21 : 26;
293      const active = this.annotations.find((annotation) => annotation.id === this.activeAnnotationId);
294      layers.push(new deck.IconLayer({
295        id: "map-annotations",
296        data: active ? this.annotations.filter((annotation) => annotation.id !== active.id) : this.annotations,
297        pickable: !this.annotationMode,
298        getPosition: (annotation) => [annotation.context.point.lng, annotation.context.point.lat],
299        getIcon: () => ({ url: NOTE_ICON, width: 40, height: 48, anchorX: 20, anchorY: 46 }),
300        getSize: (annotation) => annotation.id === this.hoverAnnotationId ? noteSize + 5 : noteSize,
301        getColor: () => active ? [255, 255, 255, 175] : [255, 255, 255, 255],
302        sizeUnits: "pixels",
303        onHover: (info) => {
304          const next = info.object?.id || null;
305          if (next === this.hoverAnnotationId) return;
306          this.hoverAnnotationId = next;
307          this.map.getCanvas().style.cursor = next ? "pointer" : "";
308          this.renderFootprints();
309        },
310        onClick: (info) => { if (info.object) this.onAnnotationSelect?.(info.object); },
311        updateTriggers: { getSize: [this.hoverAnnotationId, noteSize], getColor: [this.activeAnnotationId] }
312      }));
313      if (active) {
314        layers.push(new deck.ScatterplotLayer({
315          id: "active-annotation-halo", data: [active], pickable: false, filled: true, stroked: true,
316          getPosition: (annotation) => [annotation.context.point.lng, annotation.context.point.lat],
317          getRadius: noteSize + 15, radiusUnits: "pixels", getFillColor: [8, 9, 9, 95],
318          getLineColor: [255, 250, 240, 245], getLineWidth: 2.5, lineWidthUnits: "pixels"
319        }));
320        layers.push(new deck.IconLayer({
321          id: "active-map-annotation", data: [active], pickable: !this.annotationMode,
322          getPosition: (annotation) => [annotation.context.point.lng, annotation.context.point.lat],
323          getIcon: () => ({ url: NOTE_ICON_ACTIVE, width: 40, height: 48, anchorX: 20, anchorY: 46 }),
324          getSize: noteSize + 14, sizeUnits: "pixels",
325          onClick: (info) => { if (info.object) this.onAnnotationSelect?.(info.object); }
326        }));
327      }
328    }
329    if (this.pendingAnnotationPoint) {
330      layers.push(new deck.IconLayer({
331        id: "pending-map-annotation", data: [this.pendingAnnotationPoint], pickable: false,
332        getPosition: (point) => [point.lng, point.lat],
333        getIcon: () => ({ url: NOTE_ICON_PENDING, width: 40, height: 48, anchorX: 20, anchorY: 46 }),
334        getSize: 32, sizeUnits: "pixels"
335      }));
336    }
337    this.overlay.setProps({ layers });
338  }
339
340  enterCore(lngLat, maps = []) {
341    this.coreActive = true;
342    this.coreCoordinate = lngLat;
343    this.coreIds = new Set(maps.map((map) => map.id));
344    this.hoverIds = new Set();
345    this.map.getContainer().classList.add("field-cored");
346    this.renderFootprints();
347  }
348
349  highlightTimewellMap(map) {
350    const nextId = map?.id || null;
351    if (nextId === this.timewellHoverId) return;
352    this.timewellHoverId = nextId;
353    this.renderFootprints();
354  }
355
356  showRaster(map, { focus = true } = {}) {
357    this.removeRaster();
358    this.selectedMapId = map.id;
359    this.selectedMap = map;
360    this.renderFootprints();
361    const diagnostic = tileDiagnostic(map);
362    this.activeTile = diagnostic;
363    this.onTileStatus(diagnostic);
364    const template = tileTemplate(map);
365    if (!template) {
366      this.onRasterOpacityChange?.({ opacity: this.rasterOpacity, hasHistorical: false });
367      if (focus) this.focusMap(map);
368      return;
369    }
370    try {
371      this.map.addSource(this.rasterSourceId, {
372        type: "raster", tiles: [template], tileSize: 256,
373        minzoom: Math.max(0, map.minZoom), maxzoom: Math.max(map.minZoom || 0, map.maxZoom || 22), bounds: map.bbox
374      });
375      this.map.addLayer({ id: this.rasterLayerId, type: "raster", source: this.rasterSourceId, paint: { "raster-opacity": this.rasterOpacity } });
376      this.onRasterOpacityChange?.({ opacity: this.rasterOpacity, hasHistorical: true });
377      this.onTileStatus({ ...diagnostic, state: "loading", message: `${diagnostic.message} Browser is requesting secure raster tiles.` });
378    } catch (error) {
379      this.onTileStatus({ state: "failed", message: `Raster configuration failed: ${error.message}` });
380    }
381    // A selection remains a geographic movement even if its archival source
382    // fails. Respect the source minimum: below it, valid tiles stay invisible.
383    if (focus) this.focusMap(map);
384  }
385
386  focusMap(map, { point = null } = {}) {
387    const bounds = [[map.bbox[0], map.bbox[1]], [map.bbox[2], map.bbox[3]]];
388    const padding = this.focusPadding();
389    const camera = this.map.cameraForBounds(bounds, { padding, maxZoom: 19 });
390    if (!camera) return;
391    const minimum = map.tileBase ? Math.max(0, map.minZoom || 0) : 0;
392    const target = point && containsCoordinate(map, point)
393      ? [point.lng, point.lat]
394      : [(map.bbox[0] + map.bbox[2]) / 2, (map.bbox[1] + map.bbox[3]) / 2];
395    const options = { center: target, zoom: Math.max(camera.zoom, minimum), duration: 620 };
396    if (typeof padding !== "number") {
397      options.offset = [(padding.left - padding.right) / 2, (padding.top - padding.bottom) / 2];
398    }
399    this.map.easeTo(options);
400  }
401
402  focusPadding() {
403    if (!this.coreActive) return 70;
404    const { clientWidth, clientHeight } = this.map.getContainer();
405    if (window.matchMedia("(max-width: 780px)").matches) {
406      return { top: 70, right: 44, bottom: Math.round(clientHeight * 0.7) + 32, left: 44 };
407    }
408    const timeWellWidth = Math.min(Math.max(clientWidth * 0.48, 420), 680);
409    return { top: 70, right: Math.round(timeWellWidth) + 66, bottom: 70, left
409: 70 };
410  }
411
412  removeRaster() {
413    if (!this.map || !this.map.isStyleLoaded()) return;
414    if (this.map.getLayer(this.rasterLayerId)) this.map.removeLayer(this.rasterLayerId);
415    if (this.map.getSource(this.rasterSourceId)) this.map.removeSource(this.rasterSourceId);
416    this.activeTile = null;
417    this.onRasterOpacityChange?.({ opacity: this.rasterOpacity, hasHistorical: false });
418  }
419
420  leaveCore() {
421    this.removeRaster();
422    this.setAnnotationMode(false);
423    this.annotations = [];
424    this.pendingAnnotationPoint = null;
425    this.annotationsVisible = false;
426    this.activeAnnotationId = null;
427    this.coreActive = false;
428    this.coreCoordinate = null;
429    this.coreIds = null;
430    this.selectedMapId = null;
431    this.selectedMap = null;
432    this.timewellHoverId = null;
433    this.map.getContainer().classList.remove("field-cored");
434    this.renderFootprints();
435  }
436
437  setAnnotations(annotations = [], { visible = true, activeId = null } = {}) {
438    this.annotations = Array.isArray(annotations) ? annotations : [];
439    this.annotationsVisible = Boolean(visible);
440    this.activeAnnotationId = activeId || null;
441    this.hoverAnnotationId = null;
442    if (this.map) this.map.getCanvas().style.cursor = "";
443    this.renderFootprints();
444  }
445
446  setAnnotationVisibility(visible) {
447    this.annotationsVisible = Boolean(visible);
448    if (!visible) { this.hoverAnnotationId = null; this.map?.getCanvas().style.setProperty("cursor", ""); }
449    this.renderFootprints();
450  }
451
452  setAnnotationMode(active) {
453    this.annotationMode = Boolean(active);
454    if (active) { this.hoverAnnotationId = null; this.map?.getCanvas().style.setProperty("cursor", ""); }
455    this.map?.getContainer().classList.toggle("annotation-mode", this.annotationMode);
456  }
457
458  setPendingAnnotation(point = null) {
459    this.pendingAnnotationPoint = point;
460    this.renderFootprints();
461  }
462
463  projectPoint(point) {
464    return this.map?.project(point) || null;
465  }
466
467  focusAnnotation(point) {
468    if (!this.map || !point) return;
469    const width = this.map.getContainer().clientWidth;
470    this.map.easeTo({ center: [point.lng, point.lat], offset: [-Math.min(155, width * .16), 0], duration: 520 });
471  }
472
473  getAnnotationCamera() {
474    if (!this.map) return null;
475    const center = this.map.getCenter();
476    return { lng: center.lng, lat: center.lat, zoom: this.map.getZoom(), bearing: this.map.getBearing(), pitch: this.map.getPitch() };
477  }
478}

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.