Skip to content

Commit 8855ec8

Browse files
authored
Merge pull request #61 from UTSC-CSCC01-Software-Engineering-I/updates
clustering of user points and graphs work
2 parents 7e9eff6 + 204fbb8 commit 8855ec8

File tree

2 files changed

+190
-18
lines changed

2 files changed

+190
-18
lines changed

frontend/src/components/MapComponent.jsx

Lines changed: 168 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,98 @@ export default function MapComponent() {
185185

186186
// Update the addMarkers function
187187
function addMarkers(items) {
188-
items.forEach((item, i) => {
188+
// First, separate user points from API points
189+
const apiPoints = [];
190+
const userPoints = [];
191+
192+
items.forEach(item => {
193+
if (item.isUserPoint || (!item.siteName && !item.Label)) {
194+
userPoints.push(item);
195+
} else {
196+
apiPoints.push(item);
197+
}
198+
});
199+
200+
// Helper function to calculate distance between two points in kilometers (Haversine formula)
201+
function calculateDistance(lat1, lon1, lat2, lon2) {
202+
const R = 6371; // Radius of the earth in km
203+
const dLat = (lat2 - lat1) * Math.PI / 180;
204+
const dLon = (lon2 - lon1) * Math.PI / 180;
205+
const a =
206+
Math.sin(dLat/2) * Math.sin(dLat/2) +
207+
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
208+
Math.sin(dLon/2) * Math.sin(dLon/2);
209+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
210+
return R * c; // Distance in km
211+
}
212+
213+
// Group user points that are within 1km of each other
214+
const groupedUserPoints = [];
215+
216+
userPoints.forEach(point => {
217+
const lon = point.lng || point.lon || point.Longitude;
218+
const lat = point.lat || point.Latitude;
219+
const timestamp = new Date(point.timestamp || point.createdAt || point.created_at).getTime();
220+
221+
// Find if this point belongs to any existing group
222+
let foundGroup = false;
223+
224+
for (const group of groupedUserPoints) {
225+
const distance = calculateDistance(lat, lon, group.lat, group.lon);
226+
227+
if (distance <= 1) { // Within 1km
228+
foundGroup = true;
229+
// Add this point to the group's historical points collection
230+
if (!group.historicalPoints) {
231+
group.historicalPoints = [];
232+
}
233+
234+
// Add this point to the historical collection
235+
group.historicalPoints.push({
236+
temp: point.temp || point.Result,
237+
timestamp: point.timestamp || point.createdAt || point.created_at
238+
});
239+
240+
// Update the main display with the most recent point
241+
const groupTimestamp = new Date(group.timestamp || group.createdAt || group.created_at).getTime();
242+
243+
if (!isNaN(timestamp) && !isNaN(groupTimestamp) && timestamp > groupTimestamp) {
244+
// Update the group with this point's data but keep the group's position
245+
group.temp = point.temp || point.Result;
246+
group.timestamp = point.timestamp || point.createdAt || point.created_at;
247+
}
248+
249+
// Increase the count regardless
250+
group.pointCount = (group.pointCount || 1) + 1;
251+
break;
252+
}
253+
}
254+
255+
// If no matching group was found, create a new one
256+
if (!foundGroup) {
257+
groupedUserPoints.push({
258+
...point,
259+
pointCount: 1,
260+
historicalPoints: [{
261+
temp: point.temp || point.Result,
262+
timestamp: point.timestamp || point.createdAt || point.created_at
263+
}]
264+
});
265+
}
266+
});
267+
268+
// Add API points (these don't get grouped)
269+
apiPoints.forEach((item, i) => {
270+
addMarkerForItem(item, i);
271+
});
272+
273+
// Add the grouped user points
274+
groupedUserPoints.forEach((item, i) => {
275+
addMarkerForItem(item, i, true);
276+
});
277+
278+
// Function to add a marker for a single item
279+
function addMarkerForItem(item, i, isGrouped = false) {
189280
const lon = item.lng || item.lon || item.Longitude;
190281
const lat = item.lat || item.Latitude;
191282
const t = item.temp || item.Result;
@@ -210,23 +301,34 @@ export default function MapComponent() {
210301
const staleFilter = isStale
211302
? 'backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);'
212303
: '';
304+
305+
// If this is a grouped user point, modify the marker appearance
306+
const groupLabel = isGrouped && item.pointCount > 1
307+
? `<span class="group-count">${item.pointCount}</span>`
308+
: '';
309+
310+
const groupStyle = isGrouped && item.pointCount > 1
311+
? 'border: 2px solid #fff; transform: scale(1.1);'
312+
: '';
213313

214314
const icon = L.divIcon({
215315
className: 'custom-temp-marker',
216316
html: `
217317
<div
218-
class="temp-label accessible-marker${isStale ? ' stale' : ''}"
318+
class="temp-label accessible-marker${isStale ? ' stale' : ''}${isGrouped && item.pointCount > 1 ? ' grouped' : ''}"
219319
style="
220320
background-color: ${bgColor};
221321
${outlineStyle}
222322
${staleFilter}
323+
${groupStyle}
223324
"
224325
role="button"
225326
tabindex="0"
226-
aria-label="Water temperature ${formattedTemp} at ${name}. ${isStale ? 'Data older than 2 days.' : `Category: ${tempCategory}. Press Enter or Space to view details.`}"
327+
aria-label="Water temperature ${formattedTemp} at ${name}. ${isGrouped && item.pointCount > 1 ? `Group of ${item.pointCount} user points. ` : ''}${isStale ? 'Data older than 2 days.' : `Category: ${tempCategory}. Press Enter or Space to view details.`}"
227328
data-temp-category="${tempCategory.toLowerCase().replace(/ /g,'-')}"
228329
>
229330
<span class="temp-value" style="${valueColor}">${formattedTemp}</span>
331+
${groupLabel}
230332
</div>
231333
`,
232334
iconSize: [50, 40],
@@ -235,6 +337,12 @@ export default function MapComponent() {
235337

236338
const marker = L.marker([lat, lon], { icon }).addTo(mapInstanceRef.current);
237339

340+
// Set group information for popup display
341+
if (isGrouped && item.pointCount > 1) {
342+
marker.isGrouped = true;
343+
marker.pointCount = item.pointCount;
344+
}
345+
238346
// bump this marker to the top on hover
239347
marker.on('mouseover', () => {
240348
marker.setZIndexOffset(1000);
@@ -266,36 +374,67 @@ export default function MapComponent() {
266374
marker.on('click', async () => {
267375
// add prefix for grey (stale) points
268376
const labelPrefix = isStale ? '<strong>OLD:</strong> ' : '';
377+
378+
// add prefix for grouped points
379+
const groupPrefix = isGrouped && item.pointCount > 1
380+
? `<strong>GROUP:</strong> ${item.pointCount} points within 1km • `
381+
: '';
269382

270-
const historicalData = await fetchHistoricalData(name);
383+
let historicalData = [];
384+
385+
// For grouped user points, use the collected historical points
386+
if (isGrouped && item.historicalPoints && item.historicalPoints.length > 0) {
387+
historicalData = item.historicalPoints;
388+
} else {
389+
// For non-grouped points, fetch data from API as usual
390+
historicalData = await fetchHistoricalData(name);
391+
}
392+
271393
const popupOffset = [17, -32];
272394

273395
// no historical data
274396
if (historicalData.length === 0) {
397+
// Ensure popup is rebinding so it can reopen
398+
marker.closePopup();
399+
marker.unbindPopup();
275400
marker.bindPopup(`
276401
<div role="dialog" aria-labelledby="popup-title-${i}">
277-
<h3 id="popup-title-${i}">${labelPrefix}${name}</h3>
402+
<h3 id="popup-title-${i}">${labelPrefix}${groupPrefix}${name}</h3>
278403
<p>No historical data available</p>
279-
<p>Current temperature: ${formattedTemp} (${tempCategory})</p>
404+
${
405+
isStale
406+
? `<p>Last updated at: ${new Date(rawTime).toLocaleString()}</p>`
407+
: `<p>Current temperature: ${formattedTemp} (${tempCategory})</p>`
408+
}
409+
${isGrouped && item.pointCount > 1 ? '<p>This is a group of multiple user points within 1km radius. The most recent temperature is shown.</p>' : ''}
280410
</div>
281411
`, {
282412
offset: popupOffset,
283413
className: 'custom-popup'
284-
}).openPopup();
414+
});
415+
marker.openPopup();
285416
return;
286417
}
287418

288419
if (historicalData.length < 2) {
289-
marker.bindPopup(
290-
`<strong>${name}</strong><br/>Not enough data to generate a graph`,
291-
{
292-
offset: popupOffset,
293-
className: 'custom-popup'
294-
}
295-
).openPopup();
420+
// Rebind popup to guarantee it opens on every click
421+
marker.closePopup();
422+
marker.unbindPopup();
423+
marker.bindPopup(`
424+
<div role="dialog" aria-labelledby="popup-title-${i}">
425+
<h3 id="popup-title-${i}">${labelPrefix}${groupPrefix}${name}</h3>
426+
<p>Not enough data to generate a graph.</p>
427+
<p>Last updated at: ${new Date(rawTime).toLocaleString()}</p>
428+
</div>
429+
`, {
430+
offset: popupOffset,
431+
className: 'custom-popup'
432+
});
433+
marker.openPopup();
296434
return;
297435
}
298436

437+
// Sort the historical data by timestamp
299438
const sortedData = historicalData.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
300439

301440
// Declare currentUnit BEFORE using it
@@ -389,7 +528,9 @@ export default function MapComponent() {
389528
plugins: {
390529
title: {
391530
display: true,
392-
text: `Historical Data for ${name}`,
531+
text: isGrouped && item.pointCount > 1
532+
? `Historical Data for Group (${item.pointCount} points within 1km)`
533+
: `Historical Data for ${name}`,
393534
font: {
394535
size: isSmallMobile ? 14 : (isMobile ? 16 : 18),
395536
weight: 'bold'
@@ -417,10 +558,19 @@ export default function MapComponent() {
417558
},
418559
afterBody: function(context) {
419560
const dataIndex = context[0].dataIndex;
561+
let info = [];
562+
563+
// Add gap information
420564
if (dataIndex > 0 && timeDifferences[dataIndex - 1]) {
421-
return [`Gap from previous: ${timeDifferences[dataIndex - 1]}`];
565+
info.push(`Gap from previous: ${timeDifferences[dataIndex - 1]}`);
566+
}
567+
568+
// For grouped points, add additional information
569+
if (isGrouped && item.pointCount > 1) {
570+
info.push(`From group of ${item.pointCount} user points within 1km`);
422571
}
423-
return [];
572+
573+
return info;
424574
}
425575
}
426576
}
@@ -575,7 +725,7 @@ export default function MapComponent() {
575725
});
576726

577727
markersRef.current.push({ marker, tempC: t, name, lat, lon });
578-
});
728+
}
579729
}
580730

581731
// Function to add water temperature markers

frontend/src/styles/MapView.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,25 @@
285285
.temp-label.stale {
286286
border-left: none !important;
287287
}
288+
289+
/* Grouped user points styling */
290+
.temp-label.grouped {
291+
position: relative;
292+
border: 2px solid white !important;
293+
box-shadow: 0 0 0 1px rgba(0,0,0,0.3), 0 4px 12px rgba(0,0,0,0.4) !important;
294+
}
295+
296+
.temp-label .group-count {
297+
display: none !important;
298+
}
299+
300+
/* Dark mode adjustments */
301+
@media (prefers-color-scheme: dark) {
302+
.temp-label.grouped {
303+
border: 2px solid rgba(255,255,255,0.8) !important;
304+
}
305+
306+
.temp-label .group-count {
307+
border: 1px solid rgba(0,0,0,0.8);
308+
}
309+
}

0 commit comments

Comments
 (0)