Skip to content

Commit a2a0ed3

Browse files
committed
Apply latest route engine fixes and code updates
1 parent 399b1ea commit a2a0ed3

4 files changed

Lines changed: 234 additions & 210 deletions

File tree

src/app/page.tsx

Lines changed: 23 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -406,16 +406,13 @@ function destinationPoint(lat: number, lng: number, bearingDeg: number, distance
406406

407407
const getSuggestion = async () => {
408408
setIsSuggesting(true);
409-
setApiKeyMissing(false);
410-
411409
try {
412-
// Use selected start point, or center from existing routes, or default to Stockholm
413410
let centerLat: number;
414411
let centerLon: number;
415412

416413
if (selectedStartPoint) {
417-
centerLat = selectedStartPoint[1];
418414
centerLon = selectedStartPoint[0];
415+
centerLat = selectedStartPoint[1];
419416
} else if (routes.length > 0) {
420417
const allCoords = routes.flatMap(r => r.coordinates || []);
421418
if (allCoords.length > 0 && Array.isArray(allCoords[0])) {
@@ -430,175 +427,33 @@ const getSuggestion = async () => {
430427
centerLon = 18.0686;
431428
}
432429

433-
const targetMeters = suggestDistance * 1000;
434-
const toleranceMeters = 1000; // ±1km tolerance
435-
436-
// Generate multiple candidates and pick the best one
437-
// Based on route-generator-pro algorithm
438-
const candidates: any[] = [];
439-
const roughRadiusMeters = Math.max(250, targetMeters / 4.4);
440-
441-
// Different shape patterns (waypoint angles)
442-
const shapes = [
443-
[0, 120, 240], // Triangle
444-
[0, 90, 180, 270], // Square
445-
[0, 72, 144, 216, 288], // Pentagon
446-
[0, 110, 230], // Triangle variant
447-
[0, 100, 200, 300], // Diamond
448-
[0, 60, 120, 180, 240, 300], // Hexagon
449-
[0, 130, 230], // Wide triangle
450-
[0, 80, 180, 280], // Narrow diamond
451-
[0, 150, 270], // Wide offset
452-
];
453-
454-
// Radius multipliers
455-
const radiusMults = [0.7, 0.8, 0.85, 0.9, 0.95, 1.0, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3];
456-
457-
// Bearing offsets
458-
const bearingStep = 15;
459-
460-
for (let bearing = 0; bearing < 360; bearing += bearingStep) {
461-
for (const mult of radiusMults) {
462-
for (const shape of shapes) {
463-
const radius = roughRadiusMeters * mult;
464-
const waypoints: [number, number][] = shape.map((offset) => {
465-
const [lng, lat] = destinationPoint(centerLat, centerLon, (bearing + offset) % 360, radius);
466-
return [lng, lat];
467-
});
468-
469-
candidates.push({
470-
waypoints,
471-
bearing,
472-
mult,
473-
shape: shape.join('-'),
474-
});
475-
476-
if (candidates.length >= 30) break; // Limit to 30 candidates
477-
}
478-
if (candidates.length >= 30) break;
479-
}
480-
if (candidates.length >= 30) break;
481-
}
482-
483-
// Shuffle candidates for variety
484-
for (let i = candidates.length - 1; i > 0; i--) {
485-
const j = Math.floor(Math.random() * (i + 1));
486-
[candidates[i], candidates[j]] = [candidates[j], candidates[i]];
487-
}
488-
489-
// Try each candidate - now using OpenRouteService API via internal route
490-
let bestRoute: any = null;
491-
let bestDistanceDiff = Infinity;
492-
493-
// Test candidates with OpenRouteService
494-
for (const candidate of candidates.slice(0, 30)) {
495-
const coordString = [
496-
`${centerLon},${centerLat}`,
497-
...candidate.waypoints.map((w: [number, number]) => `${w[0]},${w[1]}`),
498-
`${centerLon},${centerLat}`
499-
].join(';');
500-
501-
try {
502-
// Use OpenRouteService API
503-
const orsResponse = await fetch(
504-
`https://api.openrouteservice.org/v2/directions/foot-walking/geojson`,
505-
{
506-
method: 'POST',
507-
headers: {
508-
'Content-Type': 'application/json',
509-
},
510-
body: JSON.stringify({
511-
coordinates: coordString.split(';').map(c => c.split(','))
512-
}),
513-
signal: AbortSignal.timeout(8000)
514-
}
515-
);
516-
517-
if (!orsResponse.ok) continue;
518-
519-
const data = await orsResponse.json();
520-
if (data.features?.[0]?.properties?.summary?.distance) {
521-
const route = data.features[0];
522-
const distance = route.properties.summary.distance;
523-
const coords = route.geometry.coordinates.map((c: number[]) => [c[0], c[1]] as [number, number]);
524-
const distanceDiff = Math.abs(distance - targetMeters);
525-
526-
if (distanceDiff < bestDistanceDiff) {
527-
bestDistanceDiff = distanceDiff;
528-
bestRoute = { coords, distance };
529-
if (distanceDiff <= toleranceMeters) break;
530-
}
531-
}
532-
} catch (e) {
533-
continue;
534-
}
535-
}
536-
537-
if (!bestRoute) {
538-
// Fallback - use first candidate without routing
539-
const fallbackWaypoints = candidates[0]?.waypoints || [];
540-
const fallbackCoords: [number, number][] = [
541-
[centerLon, centerLat],
542-
...fallbackWaypoints,
543-
[centerLon, centerLat]
544-
];
545-
546-
setSuggestedRoute({
547-
coordinates: fallbackCoords,
548-
distance: targetMeters,
549-
elevationGain: Math.round(suggestDistance * 10),
550-
name: `Loop - ${suggestDistance}km`,
551-
isRoundTrip: true,
552-
startPoint: [centerLon, centerLat],
553-
familiarityScore: avoidFamiliar ? 0 : 100,
554-
});
555-
setIsSelectingStartPoint(false);
556-
return;
557-
}
558-
559-
const coords = bestRoute.coords;
430+
const response = await fetch('/api/routes/suggest', {
431+
method: 'POST',
432+
headers: { 'Content-Type': 'application/json' },
433+
body: JSON.stringify({
434+
distance: suggestDistance,
435+
avoidFamiliar,
436+
centerLat,
437+
centerLon,
438+
existingRoutes: routes.map((route) => ({ coordinates: route.coordinates || [] })),
439+
}),
440+
});
560441

561-
// Calculate familiarity - fixed bounds: new=0-20%, familiar=80-100%
562-
let familiarityScore = avoidFamiliar ? 0 : 100;
563-
564-
if (routes.length > 0) {
565-
const allExistingCoords = routes.flatMap((r: any) => r.coordinates || []);
566-
let overlapCount = 0;
567-
const sampleSize = Math.min(coords.length, 20);
568-
const step = Math.max(1, Math.floor(coords.length / sampleSize));
569-
570-
for (let i = 0; i < coords.length; i += step) {
571-
const [lon, lat] = coords[i];
572-
for (const existingCoord of allExistingCoords) {
573-
const existingLon = existingCoord[0];
574-
const existingLat = existingCoord[1];
575-
const dist = Math.sqrt(Math.pow(lon - existingLon, 2) + Math.pow(lat - existingLat, 2));
576-
if (dist < 0.005) {
577-
overlapCount++;
578-
break;
579-
}
580-
}
581-
}
582-
const actualOverlap = Math.round((overlapCount / sampleSize) * 100);
583-
if (avoidFamiliar) {
584-
familiarityScore = Math.min(20, actualOverlap);
585-
} else {
586-
familiarityScore = Math.max(80, actualOverlap);
587-
}
442+
const data = await response.json();
443+
if (!response.ok) {
444+
throw new Error(data?.error || 'Failed to generate route');
588445
}
589-
590-
const routeNames = ['Morning Loop', 'Evening Run', 'Park Circuit', 'Urban Loop', 'Nature Trail', 'City Route', 'Sunset Run', 'Quick Loop'];
591-
const actualDistanceKm = (bestRoute.distance / 1000).toFixed(1);
592-
446+
593447
setSuggestedRoute({
594-
coordinates: coords,
595-
distance: bestRoute.distance,
596-
elevationGain: Math.round(suggestDistance * 10),
597-
name: `${routeNames[Math.floor(Math.random() * routeNames.length)]} - ${actualDistanceKm}km`,
448+
coordinates: data.coordinates,
449+
distance: data.distance,
450+
elevationGain: data.elevationGain,
451+
name: data.name,
598452
isRoundTrip: true,
599-
startPoint: [centerLon, centerLat],
600-
familiarityScore,
453+
startPoint: data.startPoint,
454+
familiarityScore: data.familiarityScore,
601455
});
456+
setSelectedRoute(null);
602457
setIsSelectingStartPoint(false);
603458
} catch (error: any) {
604459
console.error('Suggestion error:', error);

src/engine/familiarHistory.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { LatLng } from "../types";
2+
import { haversineMeters, normalizeLoop, polylineDistanceMeters, simplifyByDistance } from "./utils/geo";
3+
4+
function cumulativeDistances(points: LatLng[]): number[] {
5+
const out = [0];
6+
for (let i = 1; i < points.length; i += 1) {
7+
out.push(out[out.length - 1] + haversineMeters(points[i - 1], points[i]));
8+
}
9+
return out;
10+
}
11+
12+
function sliceDistance(cumulative: number[], startIdx: number, endIdx: number): number {
13+
return cumulative[endIdx] - cumulative[startIdx];
14+
}
15+
16+
function nearestPointDistance(points: LatLng[], start: LatLng): number {
17+
let best = Number.POSITIVE_INFINITY;
18+
for (const point of points) {
19+
const d = haversineMeters(point, start);
20+
if (d < best) best = d;
21+
}
22+
return best;
23+
}
24+
25+
function rotateToNearestStart(points: LatLng[], start: LatLng): LatLng[] {
26+
if (points.length < 2) return points;
27+
28+
let bestIndex = 0;
29+
let bestDistance = Number.POSITIVE_INFINITY;
30+
for (let i = 0; i < points.length; i += 1) {
31+
const d = haversineMeters(points[i], start);
32+
if (d < bestDistance) {
33+
bestDistance = d;
34+
bestIndex = i;
35+
}
36+
}
37+
38+
if (bestIndex === 0) return points;
39+
const rotated = [...points.slice(bestIndex), ...points.slice(1, bestIndex + 1)];
40+
return normalizeLoop(rotated);
41+
}
42+
43+
function dedupeCandidates(candidates: LatLng[][]): LatLng[][] {
44+
const seen = new Set<string>();
45+
const out: LatLng[][] = [];
46+
47+
for (const candidate of candidates) {
48+
const key = candidate
49+
.filter((_, index) => index % Math.max(1, Math.floor(candidate.length / 24)) === 0)
50+
.map((point) => `${point.lat.toFixed(4)}:${point.lng.toFixed(4)}`)
51+
.join("|");
52+
53+
if (!seen.has(key)) {
54+
seen.add(key);
55+
out.push(candidate);
56+
}
57+
}
58+
59+
return out;
60+
}
61+
62+
export function findHistoricalLoopCandidates(params: {
63+
tracks: LatLng[][];
64+
start: LatLng;
65+
targetMeters: number;
66+
toleranceMeters: number;
67+
maxResults?: number;
68+
}): LatLng[][] {
69+
const maxResults = params.maxResults ?? 12;
70+
const nearStartMeters = 90;
71+
const searchTolerance = params.toleranceMeters * 1.15;
72+
const minLoopMeters = Math.max(800, params.targetMeters * 0.5);
73+
const candidates: { geometry: LatLng[]; distanceMeters: number; startProximity: number }[] = [];
74+
75+
for (const rawTrack of params.tracks) {
76+
const track = simplifyByDistance(rawTrack, 10);
77+
if (track.length < 12) continue;
78+
79+
const wholeTrackDistance = polylineDistanceMeters(track);
80+
const trackStartGap = haversineMeters(track[0], params.start);
81+
const trackEndGap = haversineMeters(track[track.length - 1], params.start);
82+
if (
83+
trackStartGap <= nearStartMeters &&
84+
trackEndGap <= nearStartMeters &&
85+
Math.abs(wholeTrackDistance - params.targetMeters) <= searchTolerance
86+
) {
87+
candidates.push({
88+
geometry: rotateToNearestStart(normalizeLoop(track), params.start),
89+
distanceMeters: wholeTrackDistance,
90+
startProximity: Math.min(trackStartGap, trackEndGap),
91+
});
92+
}
93+
94+
const cumulative = cumulativeDistances(track);
95+
const nearIndices: number[] = [];
96+
for (let i = 0; i < track.length; i += 1) {
97+
if (haversineMeters(track[i], params.start) <= nearStartMeters) nearIndices.push(i);
98+
}
99+
100+
for (let a = 0; a < nearIndices.length; a += 1) {
101+
const startIdx = nearIndices[a];
102+
for (let b = a + 1; b < nearIndices.length; b += 1) {
103+
const endIdx = nearIndices[b];
104+
const segmentDistance = sliceDistance(cumulative, startIdx, endIdx);
105+
if (segmentDistance < minLoopMeters) continue;
106+
if (segmentDistance > params.targetMeters + searchTolerance) break;
107+
108+
if (Math.abs(segmentDistance - params.targetMeters) > searchTolerance) continue;
109+
110+
const segment = normalizeLoop(track.slice(startIdx, endIdx + 1));
111+
const startProximity = nearestPointDistance([segment[0], segment[segment.length - 1]], params.start);
112+
candidates.push({ geometry: rotateToNearestStart(segment, params.start), distanceMeters: segmentDistance, startProximity });
113+
}
114+
}
115+
}
116+
117+
return dedupeCandidates(
118+
candidates
119+
.sort((a, b) => {
120+
const aScore = Math.abs(a.distanceMeters - params.targetMeters) + a.startProximity * 8;
121+
const bScore = Math.abs(b.distanceMeters - params.targetMeters) + b.startProximity * 8;
122+
return aScore - bScore;
123+
})
124+
.slice(0, maxResults * 3)
125+
.map((entry) => entry.geometry),
126+
).slice(0, maxResults);
127+
}

0 commit comments

Comments
 (0)