21 KiB
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)
// ❌ 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)
// ✅ 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ı
<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
fill: color.fill // Açık renk (örn. #fef3c7)
strokeWidth: 2 // İnce border
Hover State
fill: color.stroke // Koyu renk (örn. #f59e0b)
strokeWidth: 2 // İnce border (aynı)
Selected State
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
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
encodeURIComponenttü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
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)
// ❌ 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)
// ✅ 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ı
// Ö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ı
// 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)
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österiliractiveDayId = "day-1"→ Sadece day-1'in polyline'ı gösteriliractiveDayId = "day-2"→ Sadece day-2'nin polyline'ı gösterilir
Sıralama ve Path Oluşturma
// 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
const color = getDayColor(ordered[0].dayIndex || 0);
const polyline = new google.maps.Polyline({
// ...
strokeColor: color.stroke, // ✅ Gün rengine göre
// ...
});
getDayColor Fonksiyonu:
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
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)
// ❌ Ö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)
// ❌ Ö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)
// ❌ Önceki
icon: createMarkerIcon(place.dayIndex || 0, 'default'),
// ✅ Yeni
icon: createSvgMarkerIcon(place.dayIndex || 0, 'default'),
Değişiklik 4: Marker Icon Update (Line 265)
// ❌ Önceki
marker.setIcon(createMarkerIcon(place.dayIndex || 0, state));
// ✅ Yeni
marker.setIcon(createSvgMarkerIcon(place.dayIndex || 0, state));
Değişiklik 5: Cleanup (Lines 111-118)
// ❌ Ö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)
// ❌ Ö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:
- Bir marker üzerine hover yap
- 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:
- Bir marker'a tıkla
- 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:
- 3 gün oluştur (Day 1, Day 2, Day 3)
- Her güne 2+ place ekle
- 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:
- 3 gün oluştur, her güne place ekle
- activeDayId = null → Tüm polyline'lar gösterilir
- activeDayId = "day-1" → Sadece Day 1 polyline'ı gösterilir
- 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:
- Day 1'e place ekle
- Marker rengini kontrol et
- 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ı! 🎉