14 KiB
GoogleMap Marker - Hızlı Referans
🎯 7 KRİTİK KURAL
1. ✅ SVG MARKER KULLAN (SymbolPath DEĞİL)
// ❌ YANLIŞ - SymbolPath
return {
path: google.maps.SymbolPath.CIRCLE,
scale: 12,
// ...
};
// ✅ DOĞRU - SVG Data URL
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
anchor: new google.maps.Point(16, 16), // ⚠️ MERKEZ SABİT
labelOrigin: new google.maps.Point(16, 16),
};
Neden? SVG = pixel-perfect kontrol, sabit boyut/anchor, jitter sıfır
2. ❌ ASLA BOUNCE KULLANMA
// ❌ YANLIŞ
marker.setAnimation(google.maps.Animation.BOUNCE);
// ✅ DOĞRU
marker.setAnimation(null);
Neden? BOUNCE anchor'ı oynatır → jitter yaratır
3. ❌ hasCenteredRef MAP INIT'TE SET ETME
// ❌ YANLIŞ - Map init'te set etme
useEffect(() => {
// ... map oluştur
hasCenteredRef.current = true; // ❌
}, [isScriptLoaded, places]);
// ✅ DOĞRU - fitBounds sonrası set et
useEffect(() => {
if (places.length > 0 && !hasCenteredRef.current) {
map.fitBounds(bounds);
hasCenteredRef.current = true; // ✅
}
}, [places]);
Neden? Map init'te set edilirse fitBounds hiç çalışmaz
4. ❌ LABEL FONT-SIZE DEĞİŞTİRME
// ❌ YANLIŞ - Değişken font size
fontSize: state === 'default' ? '14px' : '16px'
// ✅ DOĞRU - Sabit font size
fontSize: '14px' // SABİT
Neden? Değişken font size labelOrigin'i yeniden hesaplatır → mikro-jitter
5. ❌ PLACES EFFECT'İNDE CLEANUP YAPMA
// ❌ YANLIŞ
useEffect(() => {
// ... marker creation
return () => {
markersRef.current.forEach(marker => marker.setMap(null));
markersRef.current.clear();
};
}, [places]);
// ✅ DOĞRU
useEffect(() => {
// ... marker creation
// Cleanup YOK - sadece selective deletion
}, [places]);
Neden? places değiştiğinde cleanup tüm marker'ları yok eder → recreation → jitter
6. ✅ POLYLINE SIRALAMA GÜVENLİ YAPMA
// ❌ YANLIŞ - undefined/null güvensiz
(a.orderIndex || 0) - (b.orderIndex || 0)
// ✅ DOĞRU - Number.isFinite check
const ordered = [...dayPlaces].sort((a, b) => {
const ai = Number.isFinite(a.orderIndex) ? a.orderIndex! : 999;
const bi = Number.isFinite(b.orderIndex) ? b.orderIndex! : 999;
return ai - bi;
});
Neden? undefined/null orderIndex'ler 0 olur → rota yanlış çizilir
7. ✅ POLYLINE DEPENDENCY OPTİMİZE ET
// ❌ YANLIŞ - places array referansı
}, [places, activeDayId, showPolyline]);
// ✅ DOĞRU - places.length kullan
}, [places.length, activeDayId, showPolyline]);
Neden? places referansı her değişimde effect tetiklenir → gereksiz recreation
🎨 SVG MARKER ICON
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
anchor: new google.maps.Point(16, 16), // ⚠️ MERKEZ SABİT
labelOrigin: new google.maps.Point(16, 16),
};
};
SABİT KALACAKLAR:
- ✅ width, height (32)
- ✅ cx, cy (16)
- ✅ r (12)
- ✅ scaledSize (32, 32)
- ✅ anchor (16, 16)
DEĞİŞEBİLECEKLER:
- ✅ fill (default: color.fill, hover/select: color.stroke)
- ✅ stroke-width (default/hover: 2, select: 3)
🛣️ PER-DAY POLYLINE
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,
strokeOpacity: 0.6,
strokeWeight: 4,
map,
});
polylinesRef.current.set(dayId, polyline);
});
}, [places, activeDayId, showPolyline]);
Özellikler:
- ✅ 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
📐 MARKER LIFECYCLE
Effect 1: Map Initialization (Unmount Cleanup)
useEffect(() => {
// Map oluştur
return () => {
// ✅ SADECE unmount'ta cleanup
markersRef.current.forEach(marker => marker.setMap(null));
markersRef.current.clear();
polylinesRef.current.forEach(polyline => polyline.setMap(null));
polylinesRef.current.clear();
};
}, [isScriptLoaded, places]);
Effect 2: Marker Creation (NO Cleanup)
useEffect(() => {
// 1. Selective deletion
markersRef.current.forEach((marker, id) => {
if (!currentPlaceIds.has(id)) {
marker.setMap(null);
markersRef.current.delete(id);
}
});
// 2. Marker creation (skip if exists, use SVG icon)
places.forEach((place) => {
if (markersRef.current.has(place.id)) return; // ⚠️ ATLA
const marker = new google.maps.Marker({
// ...
icon: createSvgMarkerIcon(place.dayIndex || 0, 'default'), // ✅ SVG
});
});
// ❌ CLEANUP YOK
}, [places]);
Effect 3: Visual Updates (NO Creation)
useEffect(() => {
markersRef.current.forEach((marker, id) => {
// 1. Visibility
marker.setVisible(isVisible);
// 2. State
if (id === selectedPlaceId) {
marker.setZIndex(1000);
marker.setAnimation(null); // ✅ BOUNCE YOK
}
// 3. Icon (SVG - sadece renk/stroke değişir)
marker.setIcon(createSvgMarkerIcon(dayIndex, state));
// 4. Label
marker.setLabel({ ... });
});
}, [hoveredPlaceId, selectedPlaceId, activeDayId, places]);
Effect 4: Per-Day Polyline
useEffect(() => {
// Eski polyline'ları temizle
polylinesRef.current.forEach(line => line.setMap(null));
polylinesRef.current.clear();
// Günlere göre grupla ve polyline oluştur
// (detay için yukarıdaki PER-DAY POLYLINE bölümüne bakın)
}, [places, activeDayId, showPolyline]);
🔄 STATE TRANSITIONS
Default → Hover
// SVG Değişenler:
fill: color.fill → color.stroke
stroke-width: 2 (aynı)
// Marker Değişenler:
zIndex: orderIndex → 999
label.fontSize: 14px → 16px
// Değişmeyenler:
scaledSize: (32, 32) (sabit)
anchor: (16, 16) (sabit)
animation: null (sabit)
Hover → Selected
// SVG Değişenler:
stroke-width: 2 → 3
// Marker Değişenler:
zIndex: 999 → 1000
// Değişmeyenler:
fill: color.stroke (aynı)
scaledSize: (32, 32) (sabit)
anchor: (16, 16) (sabit)
animation: null (sabit) ✅ BOUNCE YOK
Selected → Default
// SVG Değişenler:
fill: color.stroke → color.fill
stroke-width: 3 → 2
// Marker Değişenler:
zIndex: 1000 → orderIndex
label.fontSize: 16px → 14px
// Değişmeyenler:
scaledSize: (32, 32) (sabit)
anchor: (16, 16) (sabit)
animation: null (sabit)
✅ CHECKLIST
Yeni marker feature eklerken kontrol et:
- SVG marker kullandım (SymbolPath değil)
- BOUNCE animation kullanmadım
- hasCenteredRef map init'te set etmedim
- Label font-size sabit (14px)
- Places effect'inde cleanup yapmadım
- scaledSize (32, 32) sabit kaldı
- anchor (16, 16) sabit kaldı
- Sadece fill ve stroke-width değiştirdim
- Marker recreation yapmadım (has check)
- Selective deletion kullandım
- Visual update ayrı effect'te
- Per-day polyline kullandım (tek polyline değil)
- Polyline sıralama güvenli (Number.isFinite)
- Polyline dependency optimize (places.length)
- Polyline rengi getDayColor ile tutarlı
🚫 YASAKLAR
❌ ASLA YAPMA
-
SymbolPath kullanma
path: google.maps.SymbolPath.CIRCLE, // ❌ -
BOUNCE animation kullanma
marker.setAnimation(google.maps.Animation.BOUNCE); // ❌ -
hasCenteredRef map init'te set etme
useEffect(() => { hasCenteredRef.current = true; // ❌ }, [isScriptLoaded, places]); -
Label font-size değiştirme
fontSize: state === 'default' ? '14px' : '16px', // ❌ -
Places effect'inde cleanup yapma
useEffect(() => { return () => { /* cleanup */ }; // ❌ }, [places]); -
SVG boyutunu değiştirme
const size = state === 'hover' ? 36 : 32; // ❌ scaledSize: new google.maps.Size(size, size); // ❌ -
Anchor değiştirme
anchor: new google.maps.Point(16, state === 'hover' ? 18 : 16); // ❌ -
Her effect'te marker creation yapma
useEffect(() => { // Her effect'te yeni marker oluşturma ❌ }, [hoveredPlaceId]); -
Tek polyline kullanma (tüm günler için)
const polylineRef = useRef<google.maps.Polyline | null>(null); // ❌ -
Polyline sıralama güvensiz yapma
(a.orderIndex || 0) - (b.orderIndex || 0) // ❌ -
Polyline dependency'de places kullanma
}, [places, activeDayId, showPolyline]); // ❌
✅ YAPILACAKLAR
✅ HER ZAMAN YAP
-
SVG marker kullan
icon: createSvgMarkerIcon(dayIndex, state), // ✅ -
hasCenteredRef fitBounds sonrası set et
if (places.length > 0 && !hasCenteredRef.current) { map.fitBounds(bounds); hasCenteredRef.current = true; // ✅ } -
Label font-size sabit tut
fontSize: '14px', // ✅ SABİT -
Marker recreation check
if (markersRef.current.has(place.id)) return; // ✅ -
Selective deletion
if (!currentPlaceIds.has(id)) { marker.setMap(null); markersRef.current.delete(id); } -
Visual update için setIcon kullan
marker.setIcon(createSvgMarkerIcon(dayIndex, state)); // ✅ -
Animation null tut
marker.setAnimation(null); // ✅ -
Cleanup sadece unmount'ta
useEffect(() => { return () => { /* cleanup */ }; // ✅ }, [isScriptLoaded, places]); // Map init effect -
Per-day polyline kullan
const polylinesRef = useRef<Map<string, google.maps.Polyline>>(new Map()); // ✅ -
Polyline sıralama güvenli yap
const ordered = [...dayPlaces].sort((a, b) => { const ai = Number.isFinite(a.orderIndex) ? a.orderIndex! : 999; const bi = Number.isFinite(b.orderIndex) ? b.orderIndex! : 999; return ai - bi; }); // ✅ -
Polyline dependency optimize et
}, [places.length, activeDayId, showPolyline]); // ✅
🎯 ÖZET
11 Kritik Düzeltme:
GoogleMap Component (8 düzeltme):
- ✅ SVG marker kullanıldı → Pixel-perfect kontrol, sabit boyut/anchor
- ✅ BOUNCE kaldırıldı → Anchor oynatma YOK
- ✅ hasCenteredRef düzeltildi → fitBounds doğru çalışıyor
- ✅ Label font-size sabit → Mikro-jitter YOK
- ✅ Places cleanup kaldırıldı → Marker recreation YOK
- ✅ Per-day polyline eklendi → Her gün ayrı renkli rota
- ✅ Polyline sıralama güvenli → Rota her zaman doğru
- ✅ Polyline performance optimize → Gereksiz recreation YOK
TripPlanner Component (3 düzeltme): 9. ✅ allPlaces useMemo → Referans stabilitesi, 0 gereksiz işlem/saniye 10. ✅ orderIndex backend öncelikli → Drag/reorder sonrası marker sabit 11. ✅ activeDayId ilk gün otomatik → İlk yüklemede senkron visibility
Sonuç:
- ✅ Jitter tamamen yok
- ✅ Smooth transitions
- ✅ Profesyonel görünüm (Wanderlog/Layla hissi)
- ✅ Gün bazlı renkli rotalar
- ✅ Yüksek performans (0 gereksiz işlem/saniye)
- ✅ Stabil marker & polyline
- ✅ Senkron visibility
📚 DAHA FAZLA BİLGİ
- TripPlanner 3 kritik düzeltme:
TRIPPLANNER_CRITICAL_FIXES.md - GoogleMap 4 kritik düzeltme:
GOOGLEMAP_CRITICAL_FIXES.md - SVG marker detayları:
GOOGLEMAP_SVG_POLYLINE.md - Özet:
GOOGLEMAP_SUMMARY.md - Jitter düzeltmeleri:
GOOGLEMAP_JITTER_FIX.md - Component mimarisi:
GOOGLEMAP_ARCHITECTURE.md - Lifecycle düzeltmeleri:
GOOGLEMAP_LIFECYCLE_FIX.md