tangled
alpha
login
or
join now
smokesignal.events
/
smokesignal
51
fork
atom
The smokesignal.events web application
51
fork
atom
overview
issues
7
pulls
pipelines
refactor: tuning location page map
Nick Gerakines
2 months ago
fe5a7876
7479f16a
+98
-35
2 changed files
expand all
collapse all
unified
split
src
http
handle_location.rs
templates
en-us
location.common.html
+23
-5
src/http/handle_location.rs
···
27
27
middleware_auth::Auth,
28
28
middleware_i18n::Language,
29
29
},
30
30
-
search_index::SearchIndexManager,
30
30
+
search_index::{GeoCenter, SearchIndexManager},
31
31
select_template,
32
32
storage::{event::event_get, identity_profile::handles_by_did},
33
33
};
···
43
43
44
44
/// Search radius in kilometers for H3-based queries
45
45
/// Resolution 6 cells have ~36km edge length, so 50km covers neighbors well
46
46
-
const H3_SEARCH_RADIUS_KM: f64 = 50.0;
46
46
+
const H3_SEARCH_RADIUS_KM: f64 = 25.0;
47
47
+
48
48
+
/// H3 precision for geo aggregation (matches resolution 6)
49
49
+
const GEO_AGGREGATION_PRECISION: u8 = 6;
50
50
+
51
51
+
/// Search radius in miles for geo aggregation (covers cell + neighbors)
52
52
+
const GEO_AGGREGATION_RADIUS_MILES: f64 = 15.0;
47
53
48
54
/// The type of location being queried.
49
55
#[derive(Debug, Clone)]
···
187
193
};
188
194
189
195
// Search for events based on location type
190
190
-
let (event_uris, h3_info) = match &location_type {
196
196
+
let (event_uris, h3_info, geo_buckets) = match &location_type {
191
197
LocationType::Cid(cid) => {
192
198
let uris = search_manager
193
199
.search_events_by_location_with_time(cid, START_FROM_TIME, EVENT_LIMIT)
194
200
.await
195
201
.unwrap_or_default();
196
196
-
(uris, None)
202
202
+
(uris, None, Vec::new())
197
203
}
198
204
LocationType::H3(cell) => {
199
205
// Get H3 location info for the heatmap
···
232
238
.await
233
239
.unwrap_or_default();
234
240
235
235
-
(uris, Some(info))
241
241
+
// Get geo aggregation for heatmap display
242
242
+
let center = GeoCenter {
243
243
+
lat: info.center_lat,
244
244
+
lon: info.center_lon,
245
245
+
distance_miles: GEO_AGGREGATION_RADIUS_MILES,
246
246
+
};
247
247
+
let buckets = search_manager
248
248
+
.get_event_geo_aggregation(GEO_AGGREGATION_PRECISION, Some(center), true)
249
249
+
.await
250
250
+
.unwrap_or_default();
251
251
+
252
252
+
(uris, Some(info), buckets)
236
253
}
237
254
};
238
255
···
303
320
location_display => location_display,
304
321
is_h3_location => is_h3_location,
305
322
h3_info => h3_info,
323
323
+
geo_buckets => geo_buckets,
306
324
events => event_views,
307
325
event_count => event_views.len(),
308
326
}},
+75
-30
templates/en-us/location.common.html
···
30
30
<span class="icon">
31
31
<i class="fas fa-map-marker-alt"></i>
32
32
</span>
33
33
-
<span>Events at {{ location_display }}</span>
33
33
+
<span>Events</span>
34
34
</span>
35
35
</h1>
36
36
37
37
{% if is_h3_location and h3_info %}
38
38
-
<div class="box mb-5">
39
38
<h3 class="title is-6 mb-3">
40
39
<span class="icon-text">
41
40
<span class="icon">
···
45
44
</span>
46
45
</h3>
47
46
<div id="location-heatmap"></div>
48
48
-
</div>
49
47
{% endif %}
50
48
51
49
<div id="location-results">
52
50
{% if event_count > 0 %}
53
53
-
<p class="subtitle is-6 has-text-grey mb-4">
54
54
-
{{ event_count }} event{% if event_count != 1 %}s{% endif %} found
55
55
-
</p>
56
51
57
52
{% for event in events %}
58
53
<div style="padding: 1.25rem 0; {% if not loop.last %}border-bottom: 1px solid #f5f5f5;{% endif %}">
···
131
126
const centerLat = {{ h3_info.center_lat }};
132
127
const centerLon = {{ h3_info.center_lon }};
133
128
const centerCell = "{{ h3_info.cell_index }}";
134
134
-
const displayCells = {{ h3_info.display_cells | tojson }};
129
129
+
const geoBuckets = {{ geo_buckets | tojson }};
135
130
136
131
// Create map centered on the H3 cell
137
132
const map = L.map('location-heatmap', {
138
133
center: [centerLat, centerLon],
139
134
zoom: 9,
140
140
-
zoomControl: true,
141
141
-
dragging: true,
142
142
-
touchZoom: true,
135
135
+
zoomControl: false,
136
136
+
dragging: false,
137
137
+
touchZoom: false,
143
138
scrollWheelZoom: false,
144
144
-
doubleClickZoom: true,
139
139
+
doubleClickZoom: false,
145
140
boxZoom: false,
146
141
keyboard: false,
147
142
attributionControl: true
···
153
148
maxZoom: 19
154
149
}).addTo(map);
155
150
156
156
-
// Draw all cells
157
157
-
displayCells.forEach(cellIndex => {
158
158
-
try {
159
159
-
const boundary = h3.cellToBoundary(cellIndex);
160
160
-
const latLngs = boundary.map(coord => [coord[0], coord[1]]);
151
151
+
// Heatmap color scale from low (blue) to high (red)
152
152
+
function getHeatmapColor(value, min, max) {
153
153
+
if (max === min) return '#3273dc';
154
154
+
const ratio = (value - min) / (max - min);
155
155
+
// Blue (#3273dc) -> Purple (#8957e5) -> Orange (#f39c12) -> Red (#e74c3c)
156
156
+
if (ratio < 0.33) {
157
157
+
const t = ratio / 0.33;
158
158
+
return lerpColor('#3273dc', '#8957e5', t);
159
159
+
} else if (ratio < 0.66) {
160
160
+
const t = (ratio - 0.33) / 0.33;
161
161
+
return lerpColor('#8957e5', '#f39c12', t);
162
162
+
} else {
163
163
+
const t = (ratio - 0.66) / 0.34;
164
164
+
return lerpColor('#f39c12', '#e74c3c', t);
165
165
+
}
166
166
+
}
167
167
+
168
168
+
function lerpColor(a, b, t) {
169
169
+
const ah = parseInt(a.replace('#', ''), 16);
170
170
+
const bh = parseInt(b.replace('#', ''), 16);
171
171
+
const ar = ah >> 16, ag = (ah >> 8) & 0xff, ab = ah & 0xff;
172
172
+
const br = bh >> 16, bg = (bh >> 8) & 0xff, bb = bh & 0xff;
173
173
+
const rr = Math.round(ar + (br - ar) * t);
174
174
+
const rg = Math.round(ag + (bg - ag) * t);
175
175
+
const rb = Math.round(ab + (bb - ab) * t);
176
176
+
return '#' + ((1 << 24) + (rr << 16) + (rg << 8) + rb).toString(16).slice(1);
177
177
+
}
178
178
+
179
179
+
// Only draw hexes with events
180
180
+
if (geoBuckets && geoBuckets.length > 0) {
181
181
+
const counts = geoBuckets.map(b => b.doc_count);
182
182
+
const minCount = Math.min(...counts);
183
183
+
const maxCount = Math.max(...counts);
184
184
+
185
185
+
geoBuckets.forEach(bucket => {
186
186
+
try {
187
187
+
const cellIndex = bucket.key;
188
188
+
const count = bucket.doc_count;
189
189
+
const boundary = h3.cellToBoundary(cellIndex);
190
190
+
const latLngs = boundary.map(coord => [coord[0], coord[1]]);
161
191
162
162
-
const isCenter = cellIndex === centerCell;
163
163
-
L.polygon(latLngs, {
164
164
-
color: isCenter ? '#3273dc' : '#485fc7',
165
165
-
fillColor: isCenter ? '#3273dc' : '#485fc7',
166
166
-
fillOpacity: isCenter ? 0.4 : 0.2,
167
167
-
weight: isCenter ? 3 : 1,
168
168
-
className: isCenter ? 'h3-hex-center' : 'h3-hex'
169
169
-
}).addTo(map);
170
170
-
} catch (e) {
171
171
-
console.warn('Failed to draw hex:', cellIndex, e);
172
172
-
}
173
173
-
});
192
192
+
const isCenter = cellIndex === centerCell;
193
193
+
const color = getHeatmapColor(count, minCount, maxCount);
174
194
175
175
-
// Add a marker at the center
176
176
-
L.marker([centerLat, centerLon]).addTo(map);
195
195
+
const polygon = L.polygon(latLngs, {
196
196
+
color: isCenter ? '#1a1a1a' : color,
197
197
+
fillColor: color,
198
198
+
fillOpacity: 0.5,
199
199
+
weight: isCenter ? 3 : 2,
200
200
+
className: isCenter ? 'h3-hex-center' : 'h3-hex'
201
201
+
}).addTo(map);
202
202
+
} catch (e) {
203
203
+
console.warn('Failed to draw hex:', bucket.key, e);
204
204
+
}
205
205
+
});
206
206
+
207
207
+
// Fit bounds to show all hexes
208
208
+
const allCoords = geoBuckets.flatMap(bucket => {
209
209
+
try {
210
210
+
return h3.cellToBoundary(bucket.key).map(coord => [coord[0], coord[1]]);
211
211
+
} catch (e) {
212
212
+
return [];
213
213
+
}
214
214
+
});
215
215
+
if (allCoords.length > 0) {
216
216
+
map.fitBounds(allCoords, { padding: [10, 10] });
217
217
+
}
218
218
+
} else {
219
219
+
// No events - just show center marker
220
220
+
L.marker([centerLat, centerLon]).addTo(map);
221
221
+
}
177
222
})();
178
223
</script>
179
224
{% endif %}