Skip to content

Commit

Permalink
Merge pull request #156 from pixelspark/feature/speed-map
Browse files Browse the repository at this point in the history
Feature/speed map
  • Loading branch information
jovandeginste committed May 26, 2024
2 parents e5c5cb4 + ff03c2f commit 17f8f23
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 86 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ clean-dist:
rm -rf ./assets/dist/

build-dist: clean-dist
mkdir -p ./assets/dist/
mkdir -p ./assets/dist/images
cp -v ./node_modules/fullcalendar/index.global.min.js ./assets/dist/fullcalendar.min.js
cp -v ./node_modules/leaflet/dist/leaflet.css ./assets/dist/
cp -v ./node_modules/leaflet/dist/images/* ./assets/dist/images/
cp -v ./node_modules/leaflet/dist/leaflet.js ./assets/dist/
cp -v ./node_modules/sorttable/sorttable.js ./assets/dist/
cp -v ./node_modules/shareon/dist/shareon.iife.js ./assets/dist/
Expand Down
234 changes: 160 additions & 74 deletions assets/map.js
Original file line number Diff line number Diff line change
@@ -1,80 +1,159 @@
var map;
var hoverMarker;

// This script relies on HTML having a "points" and "center" variables.
function on_loaded() {
// Create map & tiles.
map = L.map("map", {
fadeAnimation: false,
}).setView(center, 15);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
className: "map-tiles",
}).addTo(map);
L.control.scale().addTo(map);

var group = new L.featureGroup();
var polyLineProperties = {
weight: 4,
interactive: false,
};

var prevPoint;
// Add points with tooltip to map.
points.forEach((pt) => {
p = [pt.lat, pt.lng];

if (prevPoint) {
group.addLayer(
L.circleMarker([pt.lat, pt.lng], {
opacity: 0,
fill: false,
radius: 4,
})
.addTo(map)
.bindTooltip(pt.title),
);

polyLineProperties["color"] = getColor(
(pt.elevation - minElevation) / (maxElevation - minElevation),
);
L.polyline([prevPoint, p], polyLineProperties).addTo(map);
}
/*
interface Point {
lat: number;
lng: number;
title: string;
elevation: number;
}
prevPoint = p;
});
interface Parameters {
elementID: string; // ID of the element to put the map in
center: [number, number]; // Lat, long coordinate to center the map to
points: Point[]; // Points of the route to show
minElevation: number;
maxElevation: number;
maxSpeed: number;
speedName: string; // Name for speed layer
elevationName: string; // Name of elevation layer
}
*/
let hoverMarker;

var last = points[points.length - 1];
group.addLayer(
L.circleMarker([last.lat, last.lng], {
color: "red",
radius: 8,
})
.addTo(map)
.bindTooltip(last.title),
);
function makeMap(params) {
document.addEventListener("DOMContentLoaded", () => {
// Create map
const map = L.map(params.elementID, {
fadeAnimation: false,
}).setView(params.center, 15);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
className: "map-tiles",
}).addTo(map);
L.control.scale().addTo(map);

var first = points[0];
group.addLayer(
L.circleMarker([first.lat, first.lng], {
color: "green",
radius: 8,
})
.addTo(map)
.bindTooltip(first.title),
);
const speeds = params.points
.filter((x) => x.speed !== null)
.map((x) => x.speed);

hoverMarker = L.circleMarker(first, {
color: "blue",
radius: 8,
});
const averageSpeed =
speeds.reduce((a, x) => {
return a + x;
}, 0) / speeds.length;
const stdevSpeed = Math.sqrt(
speeds.reduce((a, x) => a + Math.pow(x - averageSpeed, 2), 0) /
(speeds.length - 1),
);

// Add features to the map
const group = new L.featureGroup();
const polyLineProperties = {
weight: 4,
interactive: false,
};

let prevPoint;
// Add points with tooltip to map.
const MOVING_AVERAGE_LENGTH = 15;
const movingSpeeds = [];
const speedLayerGroup = new L.featureGroup();
const elevationLayerGroup = new L.featureGroup();

params.points.forEach((pt) => {
p = [pt.lat, pt.lng];

if (prevPoint) {
// Add invisible point to map to allow fitBounds to work
group.addLayer(
L.circleMarker([pt.lat, pt.lng], {
opacity: 0,
fill: false,
radius: 4,
})
.addTo(map)
.bindTooltip(pt.title),
);

// Elevation
polyLineProperties["color"] = getColor(
(pt.elevation - params.minElevation) /
(params.maxElevation - params.minElevation),
);
L.polyline([prevPoint, p], polyLineProperties).addTo(
elevationLayerGroup,
);

// Speed
if (pt.speed === null || pt.speed < 0.1) {
polyLineProperties["color"] = "rgb(0,0,0)"; // Pausing
} else {
if (movingSpeeds.length > MOVING_AVERAGE_LENGTH) {
movingSpeeds.shift();
}
movingSpeeds.push(pt.speed);
const movingAverageSpeed =
movingSpeeds.reduce((a, x) => a + x) / movingSpeeds.length;

hoverMarker.addTo(map); // Adding marker to the map
map.fitBounds(group.getBounds(), { animate: false });
const zScore =
((movingAverageSpeed || averageSpeed) - averageSpeed) / stdevSpeed; // -1...1 is within one standard deviation
polyLineProperties["color"] = getColor(0.5 + zScore / 2);
}
L.polyline([prevPoint, p], polyLineProperties).addTo(speedLayerGroup);
}

prevPoint = p;
});

elevationLayerGroup.addTo(map);
speedLayerGroup.addTo(map);

var last = params.points[params.points.length - 1];
group.addLayer(
L.circleMarker([last.lat, last.lng], {
color: "red",
fill: true,
fillColor: "red",
fillOpacity: 1,
radius: 6,
})
.addTo(map)
.bindTooltip(last.title),
);

var first = params.points[0];
group.addLayer(
L.circleMarker([first.lat, first.lng], {
color: "green",
fill: true,
fillColor: "green",
fillOpacity: 1,
radius: 6,
})
.addTo(map)
.bindTooltip(first.title),
);

if (!hoverMarker) {
hoverMarker = L.circleMarker(first, {
color: "blue",
radius: 8,
});
}

hoverMarker.addTo(map); // Adding marker to the map
const layerControl = L.control
.layers({
[params.elevationName]: elevationLayerGroup,
[params.speedName]: speedLayerGroup,
})
.addTo(map);
map.fitBounds(group.getBounds(), { animate: false });
});
}

function set_marker(title, lat, lon) {
if (!hoverMarker) return;

if (title != null) {
hoverMarker.bindTooltip(title);
}
Expand All @@ -84,14 +163,21 @@ function set_marker(title, lat, lon) {
// Adding popup to the marker
hoverMarker.openTooltip();
}

function clear_marker() {
if (!hoverMarker) return;
hoverMarker.closeTooltip();
}

// Determine color for a value; value from 0 to 1
// Linearly interpolate between blue and green
function getColor(value) {
//value from 0 to 1
var hue = (240 + value * 120).toString(10);
return ["hsl(", hue, ",100%,50%)"].join("");
}
value = Math.max(0, Math.min(1, value)); // Clamp to 0...1

document.addEventListener("DOMContentLoaded", on_loaded);
const lowColor = [50, 50, 255];
const highColor = [50, 255, 50];
const color = [0, 1, 2].map((i) =>
Math.floor(value * (highColor[i] - lowColor[i]) + lowColor[i]),
);
return `rgb(${color.join(",")})`;
}
4 changes: 4 additions & 0 deletions assets/output.css
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,10 @@ table {
visibility: visible;
}

.invisible {
visibility: hidden;
}

.collapse {
visibility: collapse;
}
Expand Down
29 changes: 18 additions & 11 deletions views/workouts/workouts_show.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,26 @@ <h2 class="{{ IconFor .Type.String }}">
id="map"
class="border-2 border-black rounded-xl h-[300px] sm:h-[400px] md:h-[600px] print:w-full print:h-[600px]"
>
<script src="{{ RouteFor `assets` }}/map.js"></script>
<script>
let points = [
{{ with .Data.Details }}
{{ range .Points -}}
{ "lat": {{ .Lat }}, "lng": {{ .Lng }}, "speed": {{ .AverageSpeed }}, "elevation": {{ .ExtraMetrics.Get "Elevation" }}, "title": "{{ template `workout_point_title` . }}", },
{{ end }}
{{ end }}
];
let center = [{{ .Data.Center.Lat }}, {{ .Data.Center.Lng }}];
let minElevation = {{ .Data.MinElevation }};
let maxElevation = {{ .Data.MaxElevation }};
makeMap({
elementID: "map",
center: [{{ .Data.Center.Lat }}, {{ .Data.Center.Lng }}],
minElevation: {{ .Data.MinElevation }},
maxElevation: {{ .Data.MaxElevation }},
maxSpeed: {{ .Data.MaxSpeed }},
speedName: "{{ i18n "Average speed" }}",
elevationName: "{{ i18n "Elevation" }}",

points: [
{{ with .Data.Details }}
{{ range .Points -}}
{ "lat": {{ .Lat }}, "lng": {{ .Lng }}, "speed": {{ .AverageSpeed }}, "elevation": {{ .ExtraMetrics.Get "Elevation" }}, "title": "{{ template `workout_point_title` . }}", },
{{ end }}
{{ end }}
]
});
</script>
<script src="{{ RouteFor `assets` }}/map.js"></script>
</div>
{{ if and (not AppConfig.SocialsDisabled) (not
CurrentUser.Profile.SocialsDisabled) }} {{ template "workout_social"
Expand Down

0 comments on commit 17f8f23

Please sign in to comment.