Skip to content
15 changes: 8 additions & 7 deletions components/src/maplibre/ArrowSource/ArrowSource.stories.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import AttributionControl from '../AttributionControl/AttributionControl.svelte';

import { SWRDataLabLight } from '../MapStyle';
import { tokens } from '../../DesignTokens';
import DesignTokens from '../../DesignTokens/DesignTokens.svelte';
import { tokens } from '../../DesignTokens';

const { Story } = defineMeta({
title: 'Maplibre/Source/ArrowSource',
Expand All @@ -22,9 +22,9 @@
showDebug={true}
style={SWRDataLabLight()}
initialLocation={{
lng: -84.783,
lat: 15.623,
zoom: 3.39,
lng: -78.404,
lat: 13.411,
zoom: 5.124,
pitch: 0
}}
>
Expand All @@ -39,7 +39,7 @@
c: [-81, 14.6]
},
{
width: 15,
width: 25,
a: [-71.1, 11.3],
b: [-75.783, 15.6],
c: [-75, 12.6]
Expand All @@ -60,18 +60,19 @@
0,
'transparent',
0.4,
tokens.shades.red.base
tokens.shades.violet.base
],
'line-width': ['get', 'width']
}}
/>

<VectorLayer
sourceId="arrows"
id="arrow-heads"
filter={['==', 'kind', 'arrow-head']}
type="fill"
paint={{
'fill-color': tokens.shades.red.base
'fill-color': tokens.shades.violet.base
}}
/>
</Map>
Expand Down
10 changes: 5 additions & 5 deletions components/src/maplibre/ArrowSource/ArrowSource.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<script lang="ts">
import { type GeoJSONSourceSpecification } from 'maplibre-gl';
import { onDestroy, onMount } from 'svelte';

import MapSource from '../Source';
import { getMapContext } from '../context.svelte.js';
import quadraticToPoints from './quadraticToPoints';
import { onDestroy } from 'svelte';

const { map } = $derived(getMapContext());
import type { V2 } from '../types';

type V2 = [number, number];
const { map } = $derived(getMapContext());

interface ArrowSourceProps {
id: string;
Expand All @@ -35,7 +35,7 @@
const ars: JsonArrow[] = arrows.map((a) => {
return {
width: a.width || 10,
points: quadraticToPoints(a.a, a.b, a.c, 10),
points: quadraticToPoints(a.a, a.b, a.c),
headScale: a.headScale
};
});
Expand Down Expand Up @@ -93,7 +93,7 @@
const onZoom = () => {
sourceSpec = { ...sourceSpec, data: arrowsToJson(ars) };
};
$effect(() => {
onMount(() => {
map?.on('zoom', onZoom);
});
onDestroy(() => {
Expand Down
87 changes: 70 additions & 17 deletions components/src/maplibre/ArrowSource/quadraticToPoints.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,80 @@
import type { V2 } from '../types';

/**
* Returns the euclidian distance between two points
*/
const distance = (a: V2, b: V2) => {
return Math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2);
};

/**
* Given a sorted array arr and a value, returns the index i of arr
* such that arr[i] < value && arr[i + 1] > value using binary search
*/
const getSortedIndex = (arr: number[], v: number) => {
let low = 0,
high = arr.length;
while (low < high) {
let mid = (low + high) >>> 1; // === Math.abs((low + high) * .5) but faster
if (arr[mid] < v) low = mid + 1;
else high = mid;
}
return low;
};

/**
Given a quadratic bezier defined by points a, b and c,
returns a series of n points on the curve
* Given a polyline, returns n evenly-spaced points on the polyline
*/
const interpolatePolyline = (points: V2[], n: number) => {
let res: V2[] = [],
totalLength: number = 0,
segments: number[] = [0];

for (let i = 0; i < points.length - 2; i++) {
totalLength += distance(points[i], points[i + 1]);
segments.push(totalLength);
}

for (let i = 0; i < n; i++) {
const d: number = totalLength * (i / n);
const si: number = getSortedIndex(segments, d);
const p0 = points[si];
const p1 = points[si + 1];

const sn = [p1[0] - p0[0], p1[1] - p0[1]];
const segmentFraction = (d - segments[si]) / distance(p0, p1);

res.push([p1[0] + sn[0] * segmentFraction, p1[1] + sn[1] * segmentFraction]);
}

The quadratic bezier is: B(t) = (1-t)[(1-t)P0 + tP1] + t[(1-t)P1+tP2]
Source: https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B%C3%A9zier_curves
return res;
};

Below is a naive implementation but good enough for now. For a better approach see:
https://bit-101.com/blog/posts/2024-09-29/evenly-placed-points-on-bezier-curves
/**
* Given a quadratic Bezier defined by points a, b and c
* returns a series of evenly-spaced points on the curve
*
* Sources:
* - https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B%C3%A9zier_curves
* - https://bit-101.com/blog/posts/2024-09-29/evenly-placed-points-on-bezier-curves
*/
const quadraticToPoints = (
a: [number, number],
b: [number, number],
c: [number, number],
n = 10
) => {
let points = [];

const quadraticToPoints = (a: V2, b: V2, c: V2, n = 25) => {
let points: V2[] = [];

// 1. Interpolate the quadratic bezier function to obtain an uneven polyline
for (let i = 0; i < n; i++) {
const t = i / n;
const x = (1 - t) * ((1 - t) * a[0] + t * c[0]) + t * ((1 - t) * c[0] + t * b[0]);
const y = (1 - t) * ((1 - t) * a[1] + t * c[1]) + t * ((1 - t) * c[1] + t * b[1]);
points.push([x, y]);
points.push([
(1 - t) * ((1 - t) * a[0] + t * c[0]) + t * ((1 - t) * c[0] + t * b[0]),
(1 - t) * ((1 - t) * a[1] + t * c[1]) + t * ((1 - t) * c[1] + t * b[1])
]);
}
return [...points, b] as [number, number][];

// 2. Interpolate the polyline from (1) to get evenly-spaced points
points = interpolatePolyline([...points, b], n);

return points;
};

export default quadraticToPoints;
3 changes: 3 additions & 0 deletions components/src/maplibre/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ export type Layer =
| LineLayerSpecification
| FillLayerSpecification
| FillExtrusionLayerSpecification;

export type GeocodingService = 'maptiler';
export type GeocodingCountry = 'de' | 'at';
export type GeocodingLanguage = 'de' | 'en';

export type V2 = [number, number];

export interface Location {
lat: number;
lng: number;
Expand Down