Skip to content

Commit

Permalink
109 marker and line design (#110)
Browse files Browse the repository at this point in the history
* [Task] #109, start polyline

* [Task] #94, marker

* [Task] #109, gradient color polyline color based on speed

* [Task] #109 linter fixes
  • Loading branch information
Type-Style authored Aug 21, 2024
1 parent f14c9e3 commit 842d0c8
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 15 deletions.
5 changes: 5 additions & 0 deletions httpdocs/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ Neutral: #131211
--text: color-mix(in oklch, var(--neutral) 20%, black);
--textOnColor: var(--neutral);
--semiBg: #ffffffbb;
--contrastText: white;
--contrastBackground: black;

--baseFontWeightModifier: 50;

Expand All @@ -148,6 +150,9 @@ Neutral: #131211
--text: color-mix(in oklch, var(--neutral) 20%, white);
--semiBg: #00000077;

--contrastText: black;
--contrastBackground: white;

--baseFontWeightModifier: -50;
}
}
Expand Down
4 changes: 4 additions & 0 deletions httpdocs/images/marker.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 45 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@tsconfig/node20": "^20.1.4",
"@types/bcrypt": "^5.0.2",
"@types/compression": "^1.7.5",
"@types/culori": "^2.1.1",
"@types/express": "^4.17.21",
"@types/hpp": "^0.2.5",
"@types/jest": "^29.5.11",
Expand Down Expand Up @@ -67,10 +68,12 @@
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.16.5",
"@mui/material": "^5.15.20",
"@types/leaflet-rotatedmarker": "^0.2.5",
"axios": "^1.7.4",
"bcrypt": "^5.1.1",
"chalk": "^4.1.2",
"compression": "^1.7.4",
"culori": "^4.0.1",
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"express": "^4.19.2",
Expand All @@ -82,6 +85,8 @@
"jsonwebtoken": "^9.0.2",
"leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility": "^0.1.2",
"leaflet-polycolor": "^2.0.5",
"leaflet-rotatedmarker": "^0.2.0",
"module-alias": "^2.2.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
117 changes: 105 additions & 12 deletions src/client/components/Map.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,72 @@
import React, { useEffect } from 'react'
import { MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet'
import { MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet'
import leafletPolycolor from 'leaflet-polycolor';
import { formatRgb, toGamut, parse, Oklch } from 'culori';
import L, { LatLngExpression } from 'leaflet';
import 'leaflet-rotatedmarker';
import 'leaflet/dist/leaflet.css';
import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css'; // Re-uses images from ~leaflet package
// import L from 'leaflet';
import 'leaflet-defaulticon-compatibility';
import "../css/map.css";
leafletPolycolor(L);


// Used to recenter the map to new coordinates
const MapRecenter = ({ lat, lon, zoom }: { lat: number, lon: number, zoom: number }) => {
const map = useMap();

useEffect(() => {
// Fly to that coordinates and set new zoom level
map.flyTo([lat, lon], zoom);
}, [lat, lon]);
return null;
};

const MultiColorPolyline = ({ cleanEntries }: { cleanEntries: Models.IEntry[] }) => {
const map = useMap();
const useRelativeColors = true; // Change candidate; Use color in range to maximum speed, like from 0 to max, rather than fixed range

function calculateHue(baseHue, maxSpeed, currentSpeed) {
// range of currentSpeed and maxSpeed transfered to range from 0 to 360
const hueOffset = (currentSpeed / maxSpeed) * 360;
// add baseHue to the hueOffset and overflow at 360
const hue = (baseHue + hueOffset) % 360;

return hue;
}

useEffect(() => {
if (map) {
let maxSpeed = 0;

if (useRelativeColors) {
maxSpeed = cleanEntries.reduce((maxSpeed, entry) => {
// compare the current entry's GPS speed with the maxSpeed found so far
return Math.max(maxSpeed, entry.speed.gps);
}, cleanEntries[0].speed.gps);
maxSpeed *= 3.6; // convert M/S to KM/h
}

const colorsArray = cleanEntries.map((entry) => {
const startColor = parse('oklch(62.8% 0.2577 29.23)') as Oklch; // red
const currentSpeed = entry.speed.gps * 3.6; // convert to km/h

startColor.h = calculateHue(startColor.h, maxSpeed, currentSpeed);
startColor.l = currentSpeed > maxSpeed * 0.8 ? startColor.l = currentSpeed / maxSpeed : startColor.l;

const rgbInGamut = toGamut('rgb', 'oklch', null)(startColor); // map OKLCH to the RGB gamut
const colorRgb = formatRgb(rgbInGamut); // format the result as an RGB string

return colorRgb;
});

const polylineArray: LatLngExpression[] = cleanEntries.map((entry) => ([entry.lat, entry.lon]));

L.polycolor(polylineArray, {
colors: colorsArray,
weight: 5
}).addTo(map);
}
}, [map]);

return null;
};

function Map({ entries }: { entries: Models.IEntry[] }) {
Expand All @@ -28,25 +78,68 @@ function Map({ entries }: { entries: Models.IEntry[] }) {
const cleanEntries = entries.filter((entry) => !entry.ignore);


// Function to create custom icon with dynamic className
function createCustomIcon(entry: Models.IEntry) {
let className = "";
let iconSize = 15;
if (entry.index == 0) {
className = "start"
}
if (entry == lastEntry) {
className = "end"
}

if (className) {
iconSize = 22;
}

return L.divIcon({
html: `
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<title>Marker Arrow</title>
<polygon fill="var(--contrastText, currentColor)" points="50,0 100,100 0,100" />
</svg>`,
shadowUrl: null,
shadowSize: null,
shadowAnchor: null,
iconSize: [iconSize, iconSize],
iconAnchor: [iconSize / 2, iconSize / 2],
popupAnchor: [0, 0],
className: `customMarkerIcon ${className}`,
});
}

return (
<MapContainer className="mapContainer" center={[lastEntry.lat, lastEntry.lon]} zoom={13} scrollWheelZoom={false}>
<MapContainer className="mapContainer" center={[lastEntry.lat, lastEntry.lon]} zoom={13} preferCanvas={true}>
<MapRecenter lat={lastEntry.lat} lon={lastEntry.lon} zoom={13} />
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{cleanEntries.map((entry) => {
console.log(entry.index);
return (
<Marker key={entry.index} position={[entry.lat, entry.lon]}>
<Popup>
<pre>{JSON.stringify(entry, null, 2)}</pre>
</Popup>
</Marker>
<div key={entry.time.created}>
<Marker
key={entry.index}
position={[entry.lat, entry.lon]}
icon={createCustomIcon(entry)}
rotationAngle={entry.heading}
rotationOrigin="center"
>
<Popup>
<pre>{JSON.stringify(entry, null, 2)}</pre>
</Popup>
</Marker>

<MultiColorPolyline cleanEntries={cleanEntries} />
</div>
)
})}





</MapContainer>
)
}
Expand Down
37 changes: 37 additions & 0 deletions src/client/css/map.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.mapContainer {
height: 100%;
}


.leaflet-popup-content {
font-size: 1.2rem;
min-width: min-content;
}

.leaflet-overlay-pane canvas { /* polyline */
filter: drop-shadow(0px 0px 3px var(--neutral));
}


.customMarkerIcon {

&.start, &.end {
display: flex;
place-content: center;
border: 2px solid var(--contrastBackground);
outline: 3px solid var(--contrastBackground);
outline-offset: 3px;
border-radius: 50%;
background: var(--contrastBackground);

svg {
height: 80%;
}
}

&.start {
outline: none;
}


}
2 changes: 1 addition & 1 deletion src/client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"jsx": "react",
"esModuleInterop": true
},
"include": ["**/*.tsx", "**/*.ts", "types.d.ts", "../../types.d.ts"]
"include": ["**/*.tsx", "**/*.ts", "types.d.ts", "../../types.d.ts", "types_polyline.d.ts"]
}
14 changes: 14 additions & 0 deletions src/client/types_polyline.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Polyline plugin for native Leaflet
import * as L from 'leaflet';

declare module 'leaflet' {
namespace Polycolor {
interface Options {
colors: Array<string | null>;
weight?: number;
}
}

// Declare the actual polycolor function under the L namespace
function polycolor(latlngs: L.LatLngExpression[], options: Polycolor.Options): L.Polyline;
}

0 comments on commit 842d0c8

Please sign in to comment.