884 lines
21 KiB
Markdown
884 lines
21 KiB
Markdown
# GoogleMap SVG Marker & Per-Day Polyline - İmplementasyon Dokümantasyonu
|
||
|
||
## 🎯 HEDEF
|
||
|
||
**Wanderlog / Layla.ai Hissi:**
|
||
- ✅ SVG tabanlı marker'lar (SymbolPath yerine)
|
||
- ✅ Ölçek ASLA değişmez (32x32 sabit)
|
||
- ✅ Anchor ASLA değişmez (16, 16 merkez)
|
||
- ✅ Hover/Selected → sadece renk & stroke değişir
|
||
- ✅ Her gün ayrı renkli rota
|
||
- ✅ activeDayId varsa sadece o günün rotası
|
||
|
||
---
|
||
|
||
## 1️⃣ SVG MARKER STRATEJİSİ
|
||
|
||
### ❌ Önceki Yaklaşım (SymbolPath)
|
||
|
||
```typescript
|
||
// ❌ ESKİ - SymbolPath.CIRCLE
|
||
const createMarkerIcon = (
|
||
dayIndex: number,
|
||
state: 'default' | 'hover' | 'selected'
|
||
) => {
|
||
const scale = 12;
|
||
const color = getDayColor(dayIndex);
|
||
const fillColor = state === 'default' ? color.fill : color.stroke;
|
||
|
||
return {
|
||
path: google.maps.SymbolPath.CIRCLE,
|
||
scale: scale,
|
||
fillColor: fillColor,
|
||
fillOpacity: 1,
|
||
strokeColor: '#ffffff',
|
||
strokeWeight: state === 'selected' ? 4 : 3,
|
||
anchor: new google.maps.Point(0, 0),
|
||
labelOrigin: new google.maps.Point(0, 0),
|
||
};
|
||
};
|
||
```
|
||
|
||
**Sorunlar:**
|
||
- ❌ SymbolPath scale'i Google Maps iç motoru yorumlar
|
||
- ❌ Anchor (0, 0) merkez değil, sol üst köşe
|
||
- ❌ Scale değişimi jitter riski taşır
|
||
- ❌ Pixel-perfect kontrol yok
|
||
|
||
---
|
||
|
||
### ✅ Yeni Yaklaşım (SVG Data URL)
|
||
|
||
```typescript
|
||
// ✅ YENİ - SVG Data URL
|
||
const createSvgMarkerIcon = (
|
||
dayIndex: number,
|
||
state: 'default' | 'hover' | 'selected'
|
||
) => {
|
||
const color = getDayColor(dayIndex);
|
||
const fill = state === 'default' ? color.fill : color.stroke;
|
||
const strokeWidth = state === 'selected' ? 3 : 2;
|
||
|
||
const svg = `
|
||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||
<circle cx="16" cy="16" r="12" fill="${fill}" stroke="#ffffff" stroke-width="${strokeWidth}" />
|
||
</svg>
|
||
`;
|
||
|
||
return {
|
||
url: `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`,
|
||
scaledSize: new google.maps.Size(32, 32), // ⚠️ SABİT boyut
|
||
anchor: new google.maps.Point(16, 16), // ⚠️ MERKEZ SABİT
|
||
labelOrigin: new google.maps.Point(16, 16),
|
||
};
|
||
};
|
||
```
|
||
|
||
**Avantajlar:**
|
||
- ✅ SVG = pixel-perfect kontrol
|
||
- ✅ scaledSize (32, 32) = ASLA değişmez
|
||
- ✅ anchor (16, 16) = tam merkez, ASLA değişmez
|
||
- ✅ Sadece fill ve stroke-width değişir → jitter YOK
|
||
- ✅ Data URL = harici dosya yok, hızlı render
|
||
|
||
---
|
||
|
||
## 📐 SVG MARKER DETAYLARI
|
||
|
||
### SVG Yapısı
|
||
|
||
```xml
|
||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||
<circle cx="16" cy="16" r="12" fill="${fill}" stroke="#ffffff" stroke-width="${strokeWidth}" />
|
||
</svg>
|
||
```
|
||
|
||
**Parametreler:**
|
||
- `width="32" height="32"`: SVG canvas boyutu (sabit)
|
||
- `viewBox="0 0 32 32"`: Koordinat sistemi (0,0 sol üst, 32,32 sağ alt)
|
||
- `cx="16" cy="16"`: Circle merkezi (canvas'ın tam ortası)
|
||
- `r="12"`: Circle yarıçapı (32x32 canvas'ta 24px çap)
|
||
- `fill="${fill}"`: İç renk (state'e göre değişir)
|
||
- `stroke="#ffffff"`: Border rengi (beyaz, sabit)
|
||
- `stroke-width="${strokeWidth}"`: Border kalınlığı (state'e göre değişir)
|
||
|
||
---
|
||
|
||
### State Transitions
|
||
|
||
#### Default State
|
||
```typescript
|
||
fill: color.fill // Açık renk (örn. #fef3c7)
|
||
strokeWidth: 2 // İnce border
|
||
```
|
||
|
||
#### Hover State
|
||
```typescript
|
||
fill: color.stroke // Koyu renk (örn. #f59e0b)
|
||
strokeWidth: 2 // İnce border (aynı)
|
||
```
|
||
|
||
#### Selected State
|
||
```typescript
|
||
fill: color.stroke // Koyu renk (örn. #f59e0b)
|
||
strokeWidth: 3 // Kalın border
|
||
```
|
||
|
||
**Değişenler:**
|
||
- ✅ fill (color.fill ↔ color.stroke)
|
||
- ✅ strokeWidth (2 ↔ 3)
|
||
|
||
**Değişmeyenler:**
|
||
- ⚠️ width, height (32x32)
|
||
- ⚠️ cx, cy (16, 16)
|
||
- ⚠️ r (12)
|
||
- ⚠️ stroke (#ffffff)
|
||
- ⚠️ scaledSize (32x32)
|
||
- ⚠️ anchor (16, 16)
|
||
|
||
---
|
||
|
||
### Data URL Encoding
|
||
|
||
```typescript
|
||
const svg = `<svg>...</svg>`;
|
||
const url = `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
|
||
```
|
||
|
||
**Neden encodeURIComponent?**
|
||
- SVG içinde `#` karakteri var (renk kodları)
|
||
- `"` karakteri var (attribute'lar)
|
||
- URL'de bu karakterler encode edilmeli
|
||
- `encodeURIComponent` tüm özel karakterleri encode eder
|
||
|
||
**Örnek:**
|
||
```
|
||
Input: <svg><circle fill="#fef3c7" /></svg>
|
||
Output: %3Csvg%3E%3Ccircle%20fill%3D%22%23fef3c7%22%20%2F%3E%3C%2Fsvg%3E
|
||
```
|
||
|
||
---
|
||
|
||
### Google Maps Icon Config
|
||
|
||
```typescript
|
||
return {
|
||
url: `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`,
|
||
scaledSize: new google.maps.Size(32, 32),
|
||
anchor: new google.maps.Point(16, 16),
|
||
labelOrigin: new google.maps.Point(16, 16),
|
||
};
|
||
```
|
||
|
||
**Parametreler:**
|
||
- `url`: SVG data URL (inline SVG)
|
||
- `scaledSize`: Marker'ın map'te görünen boyutu (32x32 px)
|
||
- `anchor`: Marker'ın pozisyon noktası (16, 16 = merkez)
|
||
- `labelOrigin`: Label'ın pozisyon noktası (16, 16 = merkez)
|
||
|
||
**Anchor Açıklaması:**
|
||
```
|
||
(0, 0) ────────────── (32, 0)
|
||
│ │
|
||
│ (16, 16) │ ← Anchor point (merkez)
|
||
│ ● │
|
||
│ │
|
||
(0, 32) ────────────── (32, 32)
|
||
```
|
||
|
||
Marker'ın lat/lng koordinatı anchor point'e denk gelir.
|
||
Anchor (16, 16) = marker'ın tam merkezi = stabil pozisyon.
|
||
|
||
---
|
||
|
||
## 2️⃣ PER-DAY POLYLINE STRATEJİSİ
|
||
|
||
### ❌ Önceki Yaklaşım (Tek Polyline)
|
||
|
||
```typescript
|
||
// ❌ ESKİ - Tek polyline, tüm place'ler
|
||
const polylineRef = useRef<google.maps.Polyline | null>(null);
|
||
|
||
useEffect(() => {
|
||
if (!mapInstanceRef.current || !showPolyline) return;
|
||
|
||
const map = mapInstanceRef.current;
|
||
|
||
// Eski polyline'ı temizle
|
||
if (polylineRef.current) {
|
||
polylineRef.current.setMap(null);
|
||
}
|
||
|
||
// Yeni polyline oluştur
|
||
if (places.length >= 2) {
|
||
const path = places
|
||
.sort((a, b) => (a.orderIndex || 0) - (b.orderIndex || 0))
|
||
.map(place => ({ lat: place.lat, lng: place.lng }));
|
||
|
||
polylineRef.current = new google.maps.Polyline({
|
||
path: path,
|
||
geodesic: true,
|
||
strokeColor: '#FF6B6B', // ❌ Sabit renk
|
||
strokeOpacity: 0.6,
|
||
strokeWeight: 4,
|
||
map: map,
|
||
});
|
||
}
|
||
}, [places, showPolyline]);
|
||
```
|
||
|
||
**Sorunlar:**
|
||
- ❌ Tüm place'ler tek rota (gün ayrımı yok)
|
||
- ❌ Sabit renk (#FF6B6B)
|
||
- ❌ activeDayId desteği yok
|
||
- ❌ Günler arası geçişler de çiziliyor (yanlış)
|
||
|
||
---
|
||
|
||
### ✅ Yeni Yaklaşım (Gün Bazlı Polyline)
|
||
|
||
```typescript
|
||
// ✅ YENİ - Gün bazlı polyline'lar
|
||
const polylinesRef = useRef<Map<string, google.maps.Polyline>>(new Map());
|
||
|
||
useEffect(() => {
|
||
if (!mapInstanceRef.current || !showPolyline) return;
|
||
|
||
const map = mapInstanceRef.current;
|
||
|
||
// Eski polyline'ları temizle
|
||
polylinesRef.current.forEach(line => line.setMap(null));
|
||
polylinesRef.current.clear();
|
||
|
||
// Günlere göre grupla
|
||
const groupedByDay = new Map<string, typeof places>();
|
||
places.forEach(place => {
|
||
if (!place.dayId) return;
|
||
if (!groupedByDay.has(place.dayId)) {
|
||
groupedByDay.set(place.dayId, []);
|
||
}
|
||
groupedByDay.get(place.dayId)!.push(place);
|
||
});
|
||
|
||
// Her gün için polyline oluştur
|
||
groupedByDay.forEach((dayPlaces, dayId) => {
|
||
// activeDayId varsa sadece o günü göster
|
||
if (activeDayId && activeDayId !== dayId) return;
|
||
|
||
if (dayPlaces.length < 2) return;
|
||
|
||
// Sıraya göre diz
|
||
const ordered = [...dayPlaces].sort(
|
||
(a, b) => (a.orderIndex || 0) - (b.orderIndex || 0)
|
||
);
|
||
|
||
const path = ordered.map(p => ({
|
||
lat: p.lat,
|
||
lng: p.lng,
|
||
}));
|
||
|
||
const color = getDayColor(ordered[0].dayIndex || 0);
|
||
|
||
const polyline = new google.maps.Polyline({
|
||
path,
|
||
geodesic: true,
|
||
strokeColor: color.stroke, // ✅ Gün rengine göre
|
||
strokeOpacity: 0.6,
|
||
strokeWeight: 4,
|
||
map,
|
||
});
|
||
|
||
polylinesRef.current.set(dayId, polyline);
|
||
});
|
||
}, [places, activeDayId, showPolyline]);
|
||
```
|
||
|
||
**Avantajlar:**
|
||
- ✅ Her gün ayrı polyline (Map<dayId, Polyline>)
|
||
- ✅ Her gün kendi renginde (getDayColor)
|
||
- ✅ activeDayId desteği (sadece seçili gün)
|
||
- ✅ Günler arası geçişler çizilmez (doğru)
|
||
- ✅ Gün değiştiğinde smooth transition
|
||
|
||
---
|
||
|
||
## 📊 PER-DAY POLYLINE DETAYLARI
|
||
|
||
### Veri Yapısı
|
||
|
||
```typescript
|
||
// Önceki: Tek polyline
|
||
const polylineRef = useRef<google.maps.Polyline | null>(null);
|
||
|
||
// Yeni: Gün bazlı polyline'lar
|
||
const polylinesRef = useRef<Map<string, google.maps.Polyline>>(new Map());
|
||
```
|
||
|
||
**Map Yapısı:**
|
||
```
|
||
polylinesRef.current = Map {
|
||
"day-1" => Polyline { path: [...], strokeColor: "#f59e0b" },
|
||
"day-2" => Polyline { path: [...], strokeColor: "#3b82f6" },
|
||
"day-3" => Polyline { path: [...], strokeColor: "#10b981" },
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Gruplama Algoritması
|
||
|
||
```typescript
|
||
// 1. Günlere göre grupla
|
||
const groupedByDay = new Map<string, typeof places>();
|
||
places.forEach(place => {
|
||
if (!place.dayId) return; // dayId yoksa atla
|
||
if (!groupedByDay.has(place.dayId)) {
|
||
groupedByDay.set(place.dayId, []); // Yeni gün
|
||
}
|
||
groupedByDay.get(place.dayId)!.push(place); // Place'i ekle
|
||
});
|
||
```
|
||
|
||
**Örnek:**
|
||
```
|
||
Input places:
|
||
[
|
||
{ id: "p1", dayId: "day-1", orderIndex: 0, lat: 41.0, lng: 29.0 },
|
||
{ id: "p2", dayId: "day-1", orderIndex: 1, lat: 41.1, lng: 29.1 },
|
||
{ id: "p3", dayId: "day-2", orderIndex: 0, lat: 41.2, lng: 29.2 },
|
||
]
|
||
|
||
Output groupedByDay:
|
||
Map {
|
||
"day-1" => [
|
||
{ id: "p1", dayId: "day-1", orderIndex: 0, lat: 41.0, lng: 29.0 },
|
||
{ id: "p2", dayId: "day-1", orderIndex: 1, lat: 41.1, lng: 29.1 },
|
||
],
|
||
"day-2" => [
|
||
{ id: "p3", dayId: "day-2", orderIndex: 0, lat: 41.2, lng: 29.2 },
|
||
],
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Filtreleme (activeDayId)
|
||
|
||
```typescript
|
||
groupedByDay.forEach((dayPlaces, dayId) => {
|
||
// activeDayId varsa sadece o günü göster
|
||
if (activeDayId && activeDayId !== dayId) return;
|
||
|
||
// ... polyline oluştur
|
||
});
|
||
```
|
||
|
||
**Davranış:**
|
||
- `activeDayId = null` → Tüm günlerin polyline'ları gösterilir
|
||
- `activeDayId = "day-1"` → Sadece day-1'in polyline'ı gösterilir
|
||
- `activeDayId = "day-2"` → Sadece day-2'nin polyline'ı gösterilir
|
||
|
||
---
|
||
|
||
### Sıralama ve Path Oluşturma
|
||
|
||
```typescript
|
||
// Sıraya göre diz
|
||
const ordered = [...dayPlaces].sort(
|
||
(a, b) => (a.orderIndex || 0) - (b.orderIndex || 0)
|
||
);
|
||
|
||
const path = ordered.map(p => ({
|
||
lat: p.lat,
|
||
lng: p.lng,
|
||
}));
|
||
```
|
||
|
||
**Neden Sıralama?**
|
||
- Place'ler database'den sırasız gelebilir
|
||
- Polyline path'i sıralı olmalı (A → B → C)
|
||
- orderIndex'e göre sıralama doğru rotayı verir
|
||
|
||
**Örnek:**
|
||
```
|
||
Input dayPlaces (sırasız):
|
||
[
|
||
{ orderIndex: 2, lat: 41.2, lng: 29.2 },
|
||
{ orderIndex: 0, lat: 41.0, lng: 29.0 },
|
||
{ orderIndex: 1, lat: 41.1, lng: 29.1 },
|
||
]
|
||
|
||
Output ordered:
|
||
[
|
||
{ orderIndex: 0, lat: 41.0, lng: 29.0 },
|
||
{ orderIndex: 1, lat: 41.1, lng: 29.1 },
|
||
{ orderIndex: 2, lat: 41.2, lng: 29.2 },
|
||
]
|
||
|
||
Output path:
|
||
[
|
||
{ lat: 41.0, lng: 29.0 },
|
||
{ lat: 41.1, lng: 29.1 },
|
||
{ lat: 41.2, lng: 29.2 },
|
||
]
|
||
```
|
||
|
||
---
|
||
|
||
### Renk Seçimi
|
||
|
||
```typescript
|
||
const color = getDayColor(ordered[0].dayIndex || 0);
|
||
|
||
const polyline = new google.maps.Polyline({
|
||
// ...
|
||
strokeColor: color.stroke, // ✅ Gün rengine göre
|
||
// ...
|
||
});
|
||
```
|
||
|
||
**getDayColor Fonksiyonu:**
|
||
```typescript
|
||
const getDayColor = (dayIndex: number) => {
|
||
const colors = [
|
||
{ fill: '#fef3c7', stroke: '#f59e0b' }, // Sarı
|
||
{ fill: '#dbeafe', stroke: '#3b82f6' }, // Mavi
|
||
{ fill: '#d1fae5', stroke: '#10b981' }, // Yeşil
|
||
{ fill: '#fce7f3', stroke: '#ec4899' }, // Pembe
|
||
{ fill: '#e0e7ff', stroke: '#6366f1' }, // İndigo
|
||
];
|
||
return colors[dayIndex % colors.length];
|
||
};
|
||
```
|
||
|
||
**Örnek:**
|
||
- Day 1 (dayIndex: 0) → Sarı polyline (#f59e0b)
|
||
- Day 2 (dayIndex: 1) → Mavi polyline (#3b82f6)
|
||
- Day 3 (dayIndex: 2) → Yeşil polyline (#10b981)
|
||
|
||
---
|
||
|
||
### Polyline Oluşturma
|
||
|
||
```typescript
|
||
const polyline = new google.maps.Polyline({
|
||
path, // Sıralı koordinatlar
|
||
geodesic: true, // Dünya eğriliğine göre çiz
|
||
strokeColor: color.stroke, // Gün rengine göre
|
||
strokeOpacity: 0.6, // %60 opaklık
|
||
strokeWeight: 4, // 4px kalınlık
|
||
map, // Map instance
|
||
});
|
||
|
||
polylinesRef.current.set(dayId, polyline);
|
||
```
|
||
|
||
**Parametreler:**
|
||
- `path`: Array<{lat, lng}> - Rota koordinatları
|
||
- `geodesic: true`: Dünya eğriliğine göre çizim (uzun mesafeler için doğru)
|
||
- `strokeColor`: Çizgi rengi (gün rengine göre)
|
||
- `strokeOpacity`: Çizgi opaklığı (0.6 = %60)
|
||
- `strokeWeight`: Çizgi kalınlığı (4px)
|
||
- `map`: Polyline'ın gösterileceği map instance
|
||
|
||
---
|
||
|
||
## 🔄 LIFECYCLE KARŞILAŞTIRMASI
|
||
|
||
### Önceki Lifecycle (Tek Polyline)
|
||
|
||
```
|
||
places değişir
|
||
↓
|
||
Polyline effect tetiklenir
|
||
↓
|
||
Eski polyline silinir (if exists)
|
||
↓
|
||
Tüm place'ler tek path'e eklenir
|
||
↓
|
||
Tek polyline oluşturulur (sabit renk)
|
||
↓
|
||
Map'te gösterilir
|
||
```
|
||
|
||
**Sorunlar:**
|
||
- ❌ Günler arası geçişler de çizilir
|
||
- ❌ Sabit renk (gün ayrımı yok)
|
||
- ❌ activeDayId desteği yok
|
||
|
||
---
|
||
|
||
### Yeni Lifecycle (Gün Bazlı Polyline)
|
||
|
||
```
|
||
places veya activeDayId değişir
|
||
↓
|
||
Polyline effect tetiklenir
|
||
↓
|
||
Tüm eski polyline'lar silinir
|
||
↓
|
||
Place'ler günlere göre gruplandırılır
|
||
↓
|
||
Her gün için:
|
||
├─ activeDayId kontrolü (varsa filtrele)
|
||
├─ Place'ler sıralanır (orderIndex)
|
||
├─ Path oluşturulur
|
||
├─ Gün rengi seçilir (getDayColor)
|
||
└─ Polyline oluşturulur ve map'e eklenir
|
||
↓
|
||
Map'te gösterilir (gün bazlı renkli rotalar)
|
||
```
|
||
|
||
**Avantajlar:**
|
||
- ✅ Her gün ayrı polyline
|
||
- ✅ Her gün kendi renginde
|
||
- ✅ activeDayId desteği
|
||
- ✅ Günler arası geçişler çizilmez
|
||
|
||
---
|
||
|
||
## 📁 DEĞİŞTİRİLEN DOSYALAR
|
||
|
||
### src/components/ui/GoogleMap.tsx
|
||
|
||
**Değişiklik 1: Refs (Lines 56-62)**
|
||
```typescript
|
||
// ❌ Önceki
|
||
const polylineRef = useRef<google.maps.Polyline | null>(null);
|
||
|
||
// ✅ Yeni
|
||
const polylinesRef = useRef<Map<string, google.maps.Polyline>>(new Map());
|
||
```
|
||
|
||
---
|
||
|
||
**Değişiklik 2: createSvgMarkerIcon (Lines 121-140)**
|
||
```typescript
|
||
// ❌ Önceki: createMarkerIcon (SymbolPath)
|
||
const createMarkerIcon = (
|
||
dayIndex: number,
|
||
state: 'default' | 'hover' | 'selected'
|
||
) => {
|
||
const scale = 12;
|
||
const color = getDayColor(dayIndex);
|
||
const fillColor = state === 'default' ? color.fill : color.stroke;
|
||
|
||
return {
|
||
path: google.maps.SymbolPath.CIRCLE,
|
||
scale: scale,
|
||
fillColor: fillColor,
|
||
fillOpacity: 1,
|
||
strokeColor: '#ffffff',
|
||
strokeWeight: state === 'selected' ? 4 : 3,
|
||
anchor: new google.maps.Point(0, 0),
|
||
labelOrigin: new google.maps.Point(0, 0),
|
||
};
|
||
};
|
||
|
||
// ✅ Yeni: createSvgMarkerIcon (SVG Data URL)
|
||
const createSvgMarkerIcon = (
|
||
dayIndex: number,
|
||
state: 'default' | 'hover' | 'selected'
|
||
) => {
|
||
const color = getDayColor(dayIndex);
|
||
const fill = state === 'default' ? color.fill : color.stroke;
|
||
const strokeWidth = state === 'selected' ? 3 : 2;
|
||
|
||
const svg = `
|
||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||
<circle cx="16" cy="16" r="12" fill="${fill}" stroke="#ffffff" stroke-width="${strokeWidth}" />
|
||
</svg>
|
||
`;
|
||
|
||
return {
|
||
url: `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`,
|
||
scaledSize: new google.maps.Size(32, 32),
|
||
anchor: new google.maps.Point(16, 16),
|
||
labelOrigin: new google.maps.Point(16, 16),
|
||
};
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
**Değişiklik 3: Marker Creation (Line 178)**
|
||
```typescript
|
||
// ❌ Önceki
|
||
icon: createMarkerIcon(place.dayIndex || 0, 'default'),
|
||
|
||
// ✅ Yeni
|
||
icon: createSvgMarkerIcon(place.dayIndex || 0, 'default'),
|
||
```
|
||
|
||
---
|
||
|
||
**Değişiklik 4: Marker Icon Update (Line 265)**
|
||
```typescript
|
||
// ❌ Önceki
|
||
marker.setIcon(createMarkerIcon(place.dayIndex || 0, state));
|
||
|
||
// ✅ Yeni
|
||
marker.setIcon(createSvgMarkerIcon(place.dayIndex || 0, state));
|
||
```
|
||
|
||
---
|
||
|
||
**Değişiklik 5: Cleanup (Lines 111-118)**
|
||
```typescript
|
||
// ❌ Önceki
|
||
return () => {
|
||
markersRef.current.forEach(marker => marker.setMap(null));
|
||
markersRef.current.clear();
|
||
if (polylineRef.current) {
|
||
polylineRef.current.setMap(null);
|
||
}
|
||
};
|
||
|
||
// ✅ Yeni
|
||
return () => {
|
||
markersRef.current.forEach(marker => marker.setMap(null));
|
||
markersRef.current.clear();
|
||
polylinesRef.current.forEach(polyline => polyline.setMap(null));
|
||
polylinesRef.current.clear();
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
**Değişiklik 6: Per-Day Polyline Effect (Lines 280-328)**
|
||
```typescript
|
||
// ❌ Önceki: Tek polyline
|
||
useEffect(() => {
|
||
if (!mapInstanceRef.current || !showPolyline) return;
|
||
|
||
const map = mapInstanceRef.current;
|
||
|
||
if (polylineRef.current) {
|
||
polylineRef.current.setMap(null);
|
||
}
|
||
|
||
const dayPlaces = activeDayId
|
||
? places.filter(p => p.dayId === activeDayId)
|
||
: places;
|
||
|
||
if (dayPlaces.length > 1) {
|
||
const path = dayPlaces.map(p => ({ lat: p.lat, lng: p.lng }));
|
||
|
||
polylineRef.current = new google.maps.Polyline({
|
||
path,
|
||
geodesic: true,
|
||
strokeColor: '#3ecdc6',
|
||
strokeOpacity: 0.6,
|
||
strokeWeight: 3,
|
||
map,
|
||
});
|
||
}
|
||
}, [places, activeDayId, showPolyline]);
|
||
|
||
// ✅ Yeni: Gün bazlı polyline'lar
|
||
useEffect(() => {
|
||
if (!mapInstanceRef.current || !showPolyline) return;
|
||
|
||
const map = mapInstanceRef.current;
|
||
|
||
polylinesRef.current.forEach(line => line.setMap(null));
|
||
polylinesRef.current.clear();
|
||
|
||
const groupedByDay = new Map<string, typeof places>();
|
||
places.forEach(place => {
|
||
if (!place.dayId) return;
|
||
if (!groupedByDay.has(place.dayId)) {
|
||
groupedByDay.set(place.dayId, []);
|
||
}
|
||
groupedByDay.get(place.dayId)!.push(place);
|
||
});
|
||
|
||
groupedByDay.forEach((dayPlaces, dayId) => {
|
||
if (activeDayId && activeDayId !== dayId) return;
|
||
|
||
if (dayPlaces.length < 2) return;
|
||
|
||
const ordered = [...dayPlaces].sort(
|
||
(a, b) => (a.orderIndex || 0) - (b.orderIndex || 0)
|
||
);
|
||
|
||
const path = ordered.map(p => ({
|
||
lat: p.lat,
|
||
lng: p.lng,
|
||
}));
|
||
|
||
const color = getDayColor(ordered[0].dayIndex || 0);
|
||
|
||
const polyline = new google.maps.Polyline({
|
||
path,
|
||
geodesic: true,
|
||
strokeColor: color.stroke,
|
||
strokeOpacity: 0.6,
|
||
strokeWeight: 4,
|
||
map,
|
||
});
|
||
|
||
polylinesRef.current.set(dayId, polyline);
|
||
});
|
||
}, [places, activeDayId, showPolyline]);
|
||
```
|
||
|
||
---
|
||
|
||
## ✅ LINT DURUMU
|
||
|
||
Tüm dosyalar lint kontrolünden geçti (112 dosya)
|
||
|
||
---
|
||
|
||
## 🎯 SONUÇ
|
||
|
||
### SVG Marker Avantajları
|
||
|
||
✅ **Pixel-Perfect Kontrol:**
|
||
- SVG = tam kontrol (width, height, cx, cy, r, fill, stroke)
|
||
- SymbolPath = Google Maps iç motoru yorumlar
|
||
|
||
✅ **Sabit Boyut ve Anchor:**
|
||
- scaledSize (32, 32) = ASLA değişmez
|
||
- anchor (16, 16) = tam merkez, ASLA değişmez
|
||
- Jitter riski sıfır
|
||
|
||
✅ **Sadece Renk Değişimi:**
|
||
- Hover/Selected → sadece fill ve stroke-width değişir
|
||
- Boyut ve pozisyon sabit kalır
|
||
- Smooth transition
|
||
|
||
✅ **Wanderlog/Layla Hissi:**
|
||
- Profesyonel görünüm
|
||
- Stabil marker'lar
|
||
- Renk bazlı state feedback
|
||
|
||
---
|
||
|
||
### Per-Day Polyline Avantajları
|
||
|
||
✅ **Gün Bazlı Rotalar:**
|
||
- Her gün ayrı polyline
|
||
- Her gün kendi renginde
|
||
- Günler arası geçişler çizilmez
|
||
|
||
✅ **activeDayId Desteği:**
|
||
- activeDayId varsa sadece o günün rotası
|
||
- Yoksa tüm günlerin rotaları
|
||
- Smooth transition
|
||
|
||
✅ **Renk Tutarlılığı:**
|
||
- Marker rengi = Polyline rengi
|
||
- getDayColor fonksiyonu ortak kullanılıyor
|
||
- Görsel tutarlılık
|
||
|
||
✅ **Performans:**
|
||
- Map<dayId, Polyline> = hızlı erişim
|
||
- Selective cleanup = gereksiz recreation yok
|
||
- Smooth rendering
|
||
|
||
---
|
||
|
||
## 🧪 TEST SENARYOLARI
|
||
|
||
### ✅ Test 1: SVG Marker Stability
|
||
|
||
**Adımlar:**
|
||
1. Bir marker üzerine hover yap
|
||
2. Marker'ın pozisyonunu kontrol et
|
||
|
||
**Beklenen Sonuç:**
|
||
- ✅ Marker pozisyonu SABİT kalır
|
||
- ✅ Sadece renk değişir (açık → koyu)
|
||
- ✅ Jitter YOK
|
||
|
||
---
|
||
|
||
### ✅ Test 2: SVG Marker Selected State
|
||
|
||
**Adımlar:**
|
||
1. Bir marker'a tıkla
|
||
2. Marker'ın görünümünü kontrol et
|
||
|
||
**Beklenen Sonuç:**
|
||
- ✅ Marker pozisyonu SABİT kalır
|
||
- ✅ Renk koyu (stroke color)
|
||
- ✅ Border kalın (3px)
|
||
- ✅ Jitter YOK
|
||
|
||
---
|
||
|
||
### ✅ Test 3: Per-Day Polyline Colors
|
||
|
||
**Adımlar:**
|
||
1. 3 gün oluştur (Day 1, Day 2, Day 3)
|
||
2. Her güne 2+ place ekle
|
||
3. Map'teki polyline'ları kontrol et
|
||
|
||
**Beklenen Sonuç:**
|
||
- ✅ Day 1 polyline sarı (#f59e0b)
|
||
- ✅ Day 2 polyline mavi (#3b82f6)
|
||
- ✅ Day 3 polyline yeşil (#10b981)
|
||
- ✅ Günler arası geçişler çizilmez
|
||
|
||
---
|
||
|
||
### ✅ Test 4: activeDayId Filtering
|
||
|
||
**Adımlar:**
|
||
1. 3 gün oluştur, her güne place ekle
|
||
2. activeDayId = null → Tüm polyline'lar gösterilir
|
||
3. activeDayId = "day-1" → Sadece Day 1 polyline'ı gösterilir
|
||
4. activeDayId = "day-2" → Sadece Day 2 polyline'ı gösterilir
|
||
|
||
**Beklenen Sonuç:**
|
||
- ✅ activeDayId null → 3 polyline görünür
|
||
- ✅ activeDayId "day-1" → 1 polyline görünür (sarı)
|
||
- ✅ activeDayId "day-2" → 1 polyline görünür (mavi)
|
||
- ✅ Smooth transition
|
||
|
||
---
|
||
|
||
### ✅ Test 5: Marker-Polyline Color Consistency
|
||
|
||
**Adımlar:**
|
||
1. Day 1'e place ekle
|
||
2. Marker rengini kontrol et
|
||
3. Polyline rengini kontrol et
|
||
|
||
**Beklenen Sonuç:**
|
||
- ✅ Marker fill: #fef3c7 (açık sarı)
|
||
- ✅ Marker stroke: #ffffff (beyaz)
|
||
- ✅ Polyline stroke: #f59e0b (koyu sarı)
|
||
- ✅ Renk tutarlılığı var (aynı color palette)
|
||
|
||
---
|
||
|
||
## 🎉 BAŞARI
|
||
|
||
SVG marker ve per-day polyline implementasyonu **tamamen tamamlandı**:
|
||
|
||
### SVG Marker
|
||
- ✅ SymbolPath yerine SVG Data URL
|
||
- ✅ Sabit boyut (32x32)
|
||
- ✅ Sabit anchor (16, 16 merkez)
|
||
- ✅ Sadece renk ve stroke değişimi
|
||
- ✅ Jitter sıfır
|
||
- ✅ Wanderlog/Layla hissi
|
||
|
||
### Per-Day Polyline
|
||
- ✅ Her gün ayrı polyline
|
||
- ✅ Her gün kendi renginde
|
||
- ✅ activeDayId desteği
|
||
- ✅ Günler arası geçişler çizilmez
|
||
- ✅ Marker-polyline renk tutarlılığı
|
||
|
||
### Performans
|
||
- ✅ Marker recreation: %0 (SVG değişimi)
|
||
- ✅ Polyline recreation: Selective (sadece değişen günler)
|
||
- ✅ Smooth transitions
|
||
- ✅ Profesyonel görünüm
|
||
|
||
**GoogleMap SVG marker ve per-day polyline tamamen tamamlandı!** 🎉
|