Skip to content

Commit 0c10d46

Browse files
committed
feat: heat map style for clusters, custom pins
1 parent 2b7ba11 commit 0c10d46

File tree

9 files changed

+276
-28
lines changed

9 files changed

+276
-28
lines changed

cmd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func main() {
5757

5858
webauthn.Register(app)
5959

60-
app.Cron().MustAdd("hourly_delete_expired_events", "* * * * *", func() {
60+
app.Cron().MustAdd("delete_expired_events_cron", "* * * * *", func() {
6161
_, err := app.DB().Delete("events", dbx.NewExp("end < {:now}", dbx.Params{"now": time.Now().Format("2006-01-02 15:04:05")})).Execute()
6262
if err != nil {
6363
app.Logger().Error("failed to delete expired events", "error", err)

ui/src/lib/components/MapView.svelte

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,45 @@
1414
import DateRangeSelector from '$lib/components/DateRangeSelector.svelte';
1515
// @ts-ignore
1616
import type { Feature, Geometry } from 'geojson';
17+
import { pinSVGs } from '$lib/components/pins/svg';
1718
18-
// Subscribe to pins store
1919
const pins = $derived($pinsStore);
2020
let map = $state<MaplibreMap | undefined>(undefined);
21-
let sidebarCollapsed = $state(false);
21+
let sidebarCollapsed = $state<boolean>(false);
2222
let geoJsonData = $state(eventsToGeoJSON([]));
2323
24-
// Function to update pins based on current map bounds
24+
function loadPinImages() {
25+
if (!map) return;
26+
27+
Object.entries(pinSVGs).forEach(([name, svg]) => {
28+
const img = new Image();
29+
img.onload = () => {
30+
if (map && !map.hasImage(`pin-${name}`)) {
31+
map.addImage(`pin-${name}`, img);
32+
console.log(`Image pin-${name} chargée`);
33+
}
34+
};
35+
36+
const blob = new Blob([svg as string], { type: 'image/svg+xml' });
37+
const url = URL.createObjectURL(blob);
38+
img.src = url;
39+
});
40+
}
41+
2542
async function updatePins() {
2643
if (!map) return;
2744
const events = await pinsStore.loadPins(map.getBounds());
2845
geoJsonData = eventsToGeoJSON(events);
2946
}
3047
31-
// Effect to add map event listeners when map is available
3248
$effect(() => {
3349
if (!map) return;
3450
3551
map.on('moveend', updatePins);
52+
map.on('load', loadPinImages);
3653
3754
updatePins();
55+
loadPinImages();
3856
3957
// Add geolocate control to the map
4058
map.addControl(
@@ -51,12 +69,22 @@
5169
5270
return () => {
5371
map?.off('moveend', updatePins);
72+
map?.off('load', loadPinImages);
5473
};
5574
});
5675
57-
// Update GeoJSON data when pins change
76+
let prevPinsLength = 0;
77+
let prevPinsString = '';
78+
5879
$effect(() => {
59-
geoJsonData = eventsToGeoJSON(pins);
80+
const currentPinsString = JSON.stringify(pins.map((p: any) => p.id));
81+
82+
if (pins.length !== prevPinsLength || currentPinsString !== prevPinsString) {
83+
prevPinsLength = pins.length;
84+
prevPinsString = currentPinsString;
85+
86+
geoJsonData = eventsToGeoJSON(pins);
87+
}
6088
});
6189
</script>
6290

@@ -82,25 +110,37 @@
82110
id="events"
83111
data={geoJsonData}
84112
cluster={{
85-
radius: 40,
86-
maxZoom: 14
113+
radius: 100,
114+
maxZoom: 13
87115
}}
88116
>
89117
<CircleLayer
90118
id="cluster_circles"
91119
applyToClusters
92120
cursor="pointer"
93121
paint={{
94-
'circle-color': '#2196f3',
122+
'circle-color': [
123+
'interpolate',
124+
['linear'],
125+
['get', 'point_count'],
126+
1, 'rgba(255, 240, 50, 0.95)',
127+
5, 'rgba(255, 150, 0, 0.95)',
128+
15, 'rgba(255, 0, 50, 0.95)',
129+
30, 'rgba(200, 0, 100, 0.95)',
130+
50, 'rgba(100, 0, 150, 0.95)'
131+
],
95132
'circle-radius': [
96-
'step',
133+
'interpolate',
134+
['linear'],
97135
['get', 'point_count'],
98-
20, // Size for small clusters
99-
20, // Threshold
100-
30, // Size for medium clusters
101-
50, // Threshold
102-
40 // Size for large clusters
103-
] as any,
136+
1, 35,
137+
5, 45,
138+
15, 55,
139+
30, 65,
140+
50, 75
141+
],
142+
'circle-blur': 1.5,
143+
'circle-opacity': 0.8,
104144
}}
105145
>
106146
</CircleLayer>
@@ -116,30 +156,36 @@
116156
'text-font': ['Open Sans Bold']
117157
}}
118158
paint={{
119-
'text-color': '#ffffff'
159+
'text-color': '#222222'
120160
}}
121161
/>
122162

123-
<CircleLayer
124-
id="events_circle"
163+
<SymbolLayer
164+
id="event_points"
125165
applyToClusters={false}
126166
hoverCursor="pointer"
127-
paint={{
128-
'circle-color': [
167+
layout={{
168+
'icon-image': [
129169
'match',
130170
['get', 'kind'],
131-
'movie', 'rgb(94, 37, 207)',
132-
'#2196f3' // default color
133-
] as any,
134-
'circle-radius': 8,
171+
'movie', 'pin-movie',
172+
'concert', 'pin-concert',
173+
'festival', 'pin-festival',
174+
'theater', 'pin-theater',
175+
'party', 'pin-party',
176+
'pin-default'
177+
],
178+
'icon-size': 1.0,
179+
'icon-allow-overlap': true,
180+
'icon-anchor': 'bottom'
135181
}}
136182
>
137183
<Popup openOn="click">
138184
{#snippet children({ data }: { data: Feature<Geometry, EventWithCoordinates> | undefined })}
139185
<EventPopup feature={data ?? undefined} />
140186
{/snippet}
141187
</Popup>
142-
</CircleLayer>
188+
</SymbolLayer>
143189
</GeoJSON>
144190
</MapLibre>
145191

@@ -165,7 +211,6 @@
165211
right: 10px;
166212
}
167213
168-
/* Style des popups */
169214
:global(.maplibregl-popup-content) {
170215
background: transparent !important;
171216
padding: 0 !important;
@@ -192,4 +237,8 @@
192237
border-left-color: rgb(240, 240, 245) !important;
193238
border-right-color: transparent !important;
194239
}
240+
241+
:global(.maplibregl-marker) {
242+
background: none !important;
243+
}
195244
</style>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* SVG pour le pin concert avec un symbole musical simple
3+
*/
4+
export function createConcertPinSVG(color: string): string {
5+
return `<svg width="40" height="50" viewBox="0 0 40 50" xmlns="http://www.w3.org/2000/svg">
6+
<defs>
7+
<filter id="shadow-concert" x="-20%" y="-20%" width="140%" height="140%">
8+
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.3" />
9+
</filter>
10+
</defs>
11+
12+
<!-- Main pin shape -->
13+
<path d="M20 0C9 0 0 9 0 20C0 31 20 50 20 50C20 50 40 31 40 20C40 9 31 0 20 0Z"
14+
fill="${color}" filter="url(#shadow-concert)" />
15+
16+
<!-- Background -->
17+
<circle cx="20" cy="20" r="14" fill="white" opacity="0.9" />
18+
19+
<!-- Symbole musical simple et grand -->
20+
<text x="20" y="26" font-family="Arial" font-size="24" font-weight="bold" text-anchor="middle" fill="${color}">♫</text>
21+
</svg>`;
22+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* SVG pour le pin par défaut
3+
*/
4+
export function createDefaultPinSVG(color: string): string {
5+
return `<svg width="40" height="50" viewBox="0 0 40 50" xmlns="http://www.w3.org/2000/svg">
6+
<defs>
7+
<filter id="shadow-default" x="-20%" y="-20%" width="140%" height="140%">
8+
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.3" />
9+
</filter>
10+
</defs>
11+
12+
<!-- Main pin shape -->
13+
<path d="M20 0C9 0 0 9 0 20C0 31 20 50 20 50C20 50 40 31 40 20C40 9 31 0 20 0Z"
14+
fill="${color}" filter="url(#shadow-default)" />
15+
16+
<!-- Inner circle -->
17+
<circle cx="20" cy="20" r="10" fill="white" opacity="0.7" />
18+
</svg>`;
19+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* SVG pour le pin festival avec des drapeaux/fanions
3+
*/
4+
export function createFestivalPinSVG(color: string): string {
5+
return `<svg width="40" height="50" viewBox="0 0 40 50" xmlns="http://www.w3.org/2000/svg">
6+
<defs>
7+
<filter id="shadow-festival" x="-20%" y="-20%" width="140%" height="140%">
8+
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.3" />
9+
</filter>
10+
</defs>
11+
12+
<!-- Main pin shape -->
13+
<path d="M20 0C9 0 0 9 0 20C0 31 20 50 20 50C20 50 40 31 40 20C40 9 31 0 20 0Z"
14+
fill="${color}" filter="url(#shadow-festival)" />
15+
16+
<!-- Festival flags background -->
17+
<circle cx="20" cy="20" r="14" fill="white" opacity="0.9" />
18+
19+
<!-- Festival flags -->
20+
<path d="M12,10 L12,30 L14,30 L14,10 Z" fill="${color}" />
21+
<path d="M14,12 L24,15 L14,18 Z" fill="${color}" />
22+
<path d="M14,21 L24,24 L14,27 Z" fill="${color}" />
23+
24+
<path d="M28,10 L28,30 L26,30 L26,10 Z" fill="${color}" />
25+
<path d="M26,12 L16,15 L26,18 Z" fill="${color}" />
26+
<path d="M26,21 L16,24 L26,27 Z" fill="${color}" />
27+
</svg>`;
28+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* SVG pour le pin cinéma avec une bobine de film
3+
*/
4+
export function createMoviePinSVG(color: string): string {
5+
return `<svg width="40" height="50" viewBox="0 0 40 50" xmlns="http://www.w3.org/2000/svg">
6+
<defs>
7+
<filter id="shadow-movie" x="-20%" y="-20%" width="140%" height="140%">
8+
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.3" />
9+
</filter>
10+
</defs>
11+
12+
<!-- Main pin shape -->
13+
<path d="M20 0C9 0 0 9 0 20C0 31 20 50 20 50C20 50 40 31 40 20C40 9 31 0 20 0Z"
14+
fill="${color}" filter="url(#shadow-movie)" />
15+
16+
<!-- Film reel icon -->
17+
<circle cx="20" cy="20" r="14" fill="white" opacity="0.9" />
18+
<circle cx="20" cy="20" r="12" fill="${color}" opacity="0.9" />
19+
<circle cx="20" cy="20" r="4" fill="white" />
20+
21+
<!-- Film perforations -->
22+
<circle cx="12" cy="12" r="2" fill="white" />
23+
<circle cx="28" cy="12" r="2" fill="white" />
24+
<circle cx="12" cy="28" r="2" fill="white" />
25+
<circle cx="28" cy="28" r="2" fill="white" />
26+
<circle cx="12" cy="20" r="2" fill="white" />
27+
<circle cx="28" cy="20" r="2" fill="white" />
28+
</svg>`;
29+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* SVG pour le pin soirée avec des lumières/disco
3+
*/
4+
export function createPartyPinSVG(color: string): string {
5+
return `<svg width="40" height="50" viewBox="0 0 40 50" xmlns="http://www.w3.org/2000/svg">
6+
<defs>
7+
<filter id="shadow-party" x="-20%" y="-20%" width="140%" height="140%">
8+
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.3" />
9+
</filter>
10+
</defs>
11+
12+
<!-- Main pin shape -->
13+
<path d="M20 0C9 0 0 9 0 20C0 31 20 50 20 50C20 50 40 31 40 20C40 9 31 0 20 0Z"
14+
fill="${color}" filter="url(#shadow-party)" />
15+
16+
<!-- Party icon background -->
17+
<circle cx="20" cy="20" r="14" fill="white" opacity="0.9" />
18+
19+
<!-- Disco ball -->
20+
<circle cx="20" cy="20" r="10" fill="${color}" />
21+
22+
<!-- Light reflections -->
23+
<path d="M20,10 L20,14" stroke="white" stroke-width="1.5" />
24+
<path d="M20,26 L20,30" stroke="white" stroke-width="1.5" />
25+
<path d="M10,20 L14,20" stroke="white" stroke-width="1.5" />
26+
<path d="M26,20 L30,20" stroke="white" stroke-width="1.5" />
27+
<path d="M13,13 L16,16" stroke="white" stroke-width="1.5" />
28+
<path d="M27,13 L24,16" stroke="white" stroke-width="1.5" />
29+
<path d="M13,27 L16,24" stroke="white" stroke-width="1.5" />
30+
<path d="M27,27 L24,24" stroke="white" stroke-width="1.5" />
31+
</svg>`;
32+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* SVG pour le pin théâtre avec des masques de comédie/tragédie
3+
*/
4+
export function createTheaterPinSVG(color: string): string {
5+
return `<svg width="40" height="50" viewBox="0 0 40 50" xmlns="http://www.w3.org/2000/svg">
6+
<defs>
7+
<filter id="shadow-theater" x="-20%" y="-20%" width="140%" height="140%">
8+
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.3" />
9+
</filter>
10+
</defs>
11+
12+
<!-- Main pin shape -->
13+
<path d="M20 0C9 0 0 9 0 20C0 31 20 50 20 50C20 50 40 31 40 20C40 9 31 0 20 0Z"
14+
fill="${color}" filter="url(#shadow-theater)" />
15+
16+
<!-- Theater masks background -->
17+
<circle cx="20" cy="20" r="14" fill="white" opacity="0.9" />
18+
19+
<!-- Comedy mask (left) -->
20+
<path d="M13,15 Q10,18 12,24 Q15,22 17,19 Z" fill="${color}" />
21+
<circle cx="13" cy="18" r="1.2" fill="white" />
22+
23+
<!-- Tragedy mask (right) -->
24+
<path d="M27,15 Q30,18 28,24 Q25,22 23,19 Z" fill="${color}" />
25+
<circle cx="27" cy="18" r="1.2" fill="white" />
26+
27+
<!-- Mask connecting line -->
28+
<path d="M17,19 Q20,25 23,19" fill="none" stroke="${color}" stroke-width="1.5" />
29+
</svg>`;
30+
}

0 commit comments

Comments
 (0)