summaryrefslogtreecommitdiff
path: root/web/static/ride-detail-map.js
blob: e8e7240ea97e392922a2719a138697e6a20b718f (plain) (blame)
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
import { clamp } from "./ride-detail-logic.js";

export class RideDetailMap {
  constructor({ leaflet, element, route, points, climbs, colors }) {
    this.leaflet = leaflet;
    this.points = points;
    this.colors = colors;
    this.map = leaflet.map(element);
    const tiles = leaflet.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
      maxZoom: 19,
      attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
    });
    tiles.addTo(this.map);
    const routeLayer = leaflet
      .geoJSON(route, {
        style: { color: colors.accent, weight: 4, opacity: 0.9 },
      })
      .addTo(this.map);
    const bounds = routeLayer.getBounds();
    if (bounds.isValid()) this.map.fitBounds(bounds, { padding: [24, 24], maxZoom: 15 });
    this.climbLayers = climbs.map((climb) => this.createClimbLayer(climb));
    this.routeCursor = leaflet
      .circleMarker([points[0].latitude, points[0].longitude], {
        color: colors.forest,
        fillColor: colors.accent,
        fillOpacity: 0,
        opacity: 0,
        radius: 7,
        weight: 3,
        interactive: false,
      })
      .addTo(this.map);
  }

  createClimbLayer(climb) {
    if (!this.isValidBounds(climb)) return null;
    const coordinates = this.points.slice(climb.startIndex, climb.endIndex + 1).map((point) => [point.latitude, point.longitude]);
    return this.leaflet
      .polyline(coordinates, {
        color: this.colors.climbRoute,
        weight: 7,
        opacity: 0.65,
        lineCap: "round",
        lineJoin: "round",
        interactive: false,
      })
      .addTo(this.map);
  }

  isValidBounds(bounds) {
    return (
      bounds &&
      Number.isInteger(bounds.startIndex) &&
      Number.isInteger(bounds.endIndex) &&
      bounds.startIndex >= 0 &&
      bounds.endIndex < this.points.length &&
      bounds.startIndex < bounds.endIndex
    );
  }

  updateClimbLayer(index, bounds, active) {
    const layer = this.climbLayers[index];
    if (!layer) return;
    if (!this.isValidBounds(bounds)) {
      layer.setLatLngs([]);
      return;
    }
    layer.setLatLngs(this.points.slice(bounds.startIndex, bounds.endIndex + 1).map((point) => [point.latitude, point.longitude]));
    layer.setStyle({ weight: active ? 9 : 7, opacity: active ? 1 : 0.65 });
  }

  nearestPointIndex(latitude, longitude) {
    let nearestIndex = 0;
    let nearestDistance = Infinity;
    for (let index = 0; index < this.points.length; index++) {
      const point = this.points[index];
      const distance = this.leaflet.latLng(point.latitude, point.longitude).distanceTo([latitude, longitude]);
      if (distance < nearestDistance) {
        nearestIndex = index;
        nearestDistance = distance;
      }
    }
    return nearestIndex;
  }

  showPoint(index) {
    const point = this.points[clamp(index, 0, this.points.length - 1)];
    this.routeCursor.setLatLng([point.latitude, point.longitude]);
    this.routeCursor.setStyle({ opacity: 1, fillOpacity: 0.9 });
  }

  clearPoint() {
    this.routeCursor.setStyle({ opacity: 0, fillOpacity: 0 });
  }

  zoomToClimb(bounds) {
    if (!this.isValidBounds(bounds)) return;
    const climbPoints = this.points.slice(bounds.startIndex, bounds.endIndex + 1);
    const mapBounds = this.leaflet.latLngBounds(climbPoints.map((point) => [point.latitude, point.longitude]));
    if (mapBounds.isValid()) this.map.fitBounds(mapBounds, { padding: [32, 32], maxZoom: 15 });
  }

  onClick(listener) {
    this.map.on("click", listener);
  }
}