2026-03-04 18:25:09 +00:00

285 lines
8.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from 'jsr:@supabase/supabase-js@2';
import { requireAuth, checkRateLimit } from '../_shared/auth.ts';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
interface Place {
tripPlaceId: string;
placeId: string;
name: string;
lat: number;
lng: number;
duration: string;
order_index: number;
is_time_fixed?: boolean; // Sabit saatli etkinlik mi?
fixed_start_time?: string; // Sabit başlangıç saati
fixed_end_time?: string; // Sabit bitiş saati
}
interface OptimizeRouteRequest {
dayId: string;
date: string;
places: Place[];
startLocation?: { lat: number; lng: number };
startLocationType?: 'balloon' | 'hotel' | 'place' | 'city_center'; // Başlangıç noktası tipi
travelMode?: string;
maxDayTime?: string;
}
interface OptimizedPlace {
tripPlaceId: string;
placeId: string;
name: string;
newOrderIndex: number;
oldOrderIndex: number;
}
interface OptimizationResult {
optimizedOrder: OptimizedPlace[];
distanceBeforeKm: number;
distanceAfterKm: number;
estimatedSavings: {
distanceKm: number;
timeMinutes: number;
};
explanation: string;
}
// Haversine formülü ile iki koordinat arasındaki mesafeyi hesapla (km)
function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371; // Dünya'nın yarıçapı (km)
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
// Toplam rota mesafesini hesapla
function calculateTotalDistance(places: Place[]): number {
let total = 0;
for (let i = 0; i < places.length - 1; i++) {
total += calculateDistance(
places[i].lat,
places[i].lng,
places[i + 1].lat,
places[i + 1].lng
);
}
return total;
}
// Nearest Neighbor algoritması ile rotayı optimize et
// ⚠️ Sabit saatli etkinlikler (balon uçuşu) optimizasyona dahil edilmez
function optimizeRoute(places: Place[], startLocation?: { lat: number; lng: number }): Place[] {
if (places.length <= 2) {
return places; // 2 veya daha az yer varsa optimize etmeye gerek yok
}
// Sabit saatli ve esnek etkinlikleri ayır
const fixedTimePlaces = places.filter(p => p.is_time_fixed);
const flexiblePlaces = places.filter(p => !p.is_time_fixed);
// Eğer sadece sabit saatli etkinlikler varsa, olduğu gibi döndür
if (flexiblePlaces.length === 0) {
return places;
}
// Esnek etkinlikleri optimize et
const unvisited = [...flexiblePlaces];
const optimized: Place[] = [];
// Başlangıç noktasını belirle
let current: { lat: number; lng: number };
// Eğer sabit saatli etkinlik varsa (örn: balon), onun konumundan başla
if (fixedTimePlaces.length > 0) {
const lastFixed = fixedTimePlaces[fixedTimePlaces.length - 1];
current = { lat: lastFixed.lat, lng: lastFixed.lng };
} else if (startLocation) {
current = startLocation;
} else {
// Başlangıç noktası yoksa ilk esnek yeri kullan
const first = unvisited.shift()!;
optimized.push(first);
current = { lat: first.lat, lng: first.lng };
}
// En yakın komşu algoritması (sadece esnek etkinlikler için)
while (unvisited.length > 0) {
let nearestIndex = 0;
let nearestDistance = Infinity;
// En yakın yeri bul
for (let i = 0; i < unvisited.length; i++) {
const distance = calculateDistance(
current.lat,
current.lng,
unvisited[i].lat,
unvisited[i].lng
);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestIndex = i;
}
}
// En yakın yeri ekle
const nearest = unvisited.splice(nearestIndex, 1)[0];
optimized.push(nearest);
current = { lat: nearest.lat, lng: nearest.lng };
}
// Sabit saatli etkinlikleri başa ekle, optimize edilmiş esnek etkinlikleri sonra ekle
return [...fixedTimePlaces, ...optimized];
}
// Optimizasyon açıklaması oluştur
function generateExplanation(original: Place[], optimized: Place[]): string {
const changes: string[] = [];
// Sabit saatli etkinlikleri kontrol et
const hasFixedTime = original.some(p => p.is_time_fixed);
if (hasFixedTime) {
changes.push('Sabit saatli etkinlikler (balon uçuşu) korundu');
}
for (let i = 0; i < optimized.length; i++) {
const newPlace = optimized[i];
const oldIndex = original.findIndex(p => p.tripPlaceId === newPlace.tripPlaceId);
if (oldIndex !== i && !newPlace.is_time_fixed) {
changes.push(`${newPlace.name} ${oldIndex + 1}. sıradan ${i + 1}. sıraya taşındı`);
}
}
if (changes.length === 0) {
return "Mevcut sıralama zaten optimal görünüyor.";
}
return changes.slice(0, 3).join(', ') + (changes.length > 3 ? ' ve diğer değişiklikler' : '') +
'. Bu değişiklikler geriye dönüşleri azaltarak daha verimli bir rota oluşturur.';
}
Deno.serve(async (req) => {
// CORS preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
// Auth check
const auth = await requireAuth(req);
if (auth.error) return auth.error;
// Rate limit check (20 AI suggestions per hour)
const supabaseService = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
const rateLimitResponse = await checkRateLimit(auth.userId, 'ai_suggest', supabaseService);
if (rateLimitResponse) return rateLimitResponse;
const { places, startLocation }: OptimizeRouteRequest = await req.json();
// Validasyon
if (!places || places.length < 2) {
return new Response(
JSON.stringify({
error: 'En az 2 yer gerekli',
optimizedOrder: [],
distanceBeforeKm: 0,
distanceAfterKm: 0,
estimatedSavings: { distanceKm: 0, timeMinutes: 0 },
explanation: 'Optimizasyon için yeterli yer yok.'
}),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
}
// Tüm yerlerin koordinatları var mı kontrol et
const missingCoords = places.filter(p => !p.lat || !p.lng);
if (missingCoords.length > 0) {
return new Response(
JSON.stringify({
error: 'Bazı yerlerin koordinatları eksik',
optimizedOrder: [],
distanceBeforeKm: 0,
distanceAfterKm: 0,
estimatedSavings: { distanceKm: 0, timeMinutes: 0 },
explanation: 'Tüm yerlerin koordinatları olmalı.'
}),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
}
// Orijinal toplam mesafe
const originalDistance = calculateTotalDistance(places);
// Rotayı optimize et
const optimizedPlaces = optimizeRoute(places, startLocation);
// Optimize edilmiş toplam mesafe
const optimizedDistance = calculateTotalDistance(optimizedPlaces);
// Tasarruf hesapla
const distanceSaved = Math.max(0, originalDistance - optimizedDistance);
const timeSaved = Math.round(distanceSaved / 50 * 60); // 50 km/h ortalama hız varsayımı
// Sonuç oluştur
const result: OptimizationResult = {
optimizedOrder: optimizedPlaces.map((place, index) => ({
tripPlaceId: place.tripPlaceId,
placeId: place.placeId,
name: place.name,
newOrderIndex: index,
oldOrderIndex: place.order_index,
})),
distanceBeforeKm: originalDistance,
distanceAfterKm: optimizedDistance,
estimatedSavings: {
distanceKm: distanceSaved,
timeMinutes: timeSaved,
},
explanation: generateExplanation(places, optimizedPlaces),
};
return new Response(
JSON.stringify(result),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
} catch (error) {
console.error('Optimize route error:', error);
return new Response(
JSON.stringify({
error: error.message,
optimizedOrder: [],
distanceBeforeKm: 0,
distanceAfterKm: 0,
estimatedSavings: { distanceKm: 0, timeMinutes: 0 },
explanation: 'Optimizasyon sırasında bir hata oluştu.'
}),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
}
});