38980-vm/app-9w9pd00g5j41/GOOGLEMAP_ARCHITECTURE.md
2026-03-04 18:25:09 +00:00

610 lines
14 KiB
Markdown
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.

# GoogleMap Component - Final Architecture
## 📐 COMPONENT MİMARİSİ
### State Management
```typescript
// Map instance refs
const mapRef = useRef<HTMLDivElement>(null);
const mapInstanceRef = useRef<google.maps.Map | null>(null);
// Marker management
const markersRef = useRef<Map<string, google.maps.Marker>>(new Map());
const polylineRef = useRef<google.maps.Polyline | null>(null);
const infoWindowRef = useRef<google.maps.InfoWindow | null>(null);
// Center control
const hasCenteredRef = useRef(false); // ✅ Center sadece 1 kez
// Loading state
const [isScriptLoaded, setIsScriptLoaded] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
```
---
## 🔄 EFFECT LIFECYCLE
### Effect 1: Google Maps Script Loading
**Dependency:** `[]` (mount only)
**Sorumluluklar:**
- Google Maps script'ini yükle
- `isScriptLoaded` state'ini güncelle
- Error handling
```typescript
useEffect(() => {
loadGoogleMapsScript()
.then(() => setIsScriptLoaded(true))
.catch((error) => {
console.error('Google Maps yükleme hatası:', error);
setLoadError('Harita yüklenemedi. Lütfen sayfayı yenileyin.');
});
}, []);
```
---
### Effect 2: Map Initialization
**Dependency:** `[isScriptLoaded, places]`
**Sorumluluklar:**
- Map instance'ı oluştur (sadece 1 kez)
- Initial center hesapla (places varsa ilk place, yoksa default)
- InfoWindow oluştur
- hasCenteredRef'i set et
```typescript
useEffect(() => {
if (!mapRef.current || !isScriptLoaded || !window.google) return;
if (mapInstanceRef.current) return; // ✅ Zaten oluşturulmuş
try {
const initialCenter = places.length > 0
? { lat: places[0].lat, lng: places[0].lng }
: { lat: 38.9637, lng: 35.2433 };
const mapInstance = new google.maps.Map(mapRef.current, {
center: initialCenter,
zoom: 12,
// ... map options
});
mapInstanceRef.current = mapInstance;
infoWindowRef.current = new google.maps.InfoWindow();
hasCenteredRef.current = true;
} catch (error) {
console.error('Harita başlatma hatası:', error);
setLoadError('Harita oluşturulamadı.');
}
return () => {
// Cleanup: Sadece unmount'ta
markersRef.current.forEach(marker => marker.setMap(null));
markersRef.current.clear();
if (polylineRef.current) {
polylineRef.current.setMap(null);
}
};
}, [isScriptLoaded, places]);
```
---
### Effect 3: Marker Creation/Deletion
**Dependency:** `[places]` ⚠️ SADECE places
**Sorumluluklar:**
- Artık olmayan marker'ları sil
- Yeni marker'ları oluştur (zaten varsa ATLA)
- Event listener'ları ekle (click, hover)
- Auto-fit bounds (sadece ilk kez - hasCenteredRef)
**ÖNEMLİ:**
- ❌ onMarkerClick/onMarkerHover dependency DEĞİL
- ❌ hoveredPlaceId/selectedPlaceId/activeDayId dependency DEĞİL
- ✅ Marker recreation SADECE places değiştiğinde
```typescript
useEffect(() => {
if (!mapInstanceRef.current || !window.google) return;
const map = mapInstanceRef.current;
const currentPlaceIds = new Set(places.map(p => p.id));
// 1. Artık olmayan marker'ları sil
markersRef.current.forEach((marker, id) => {
if (!currentPlaceIds.has(id)) {
marker.setMap(null);
markersRef.current.delete(id);
}
});
// 2. Yeni marker'ları oluştur (zaten varsa ATLA)
places.forEach((place) => {
if (markersRef.current.has(place.id)) return; // ⚠️ ATLA
const label = `${(place.orderIndex || 0) + 1}`;
const marker = new google.maps.Marker({
position: { lat: place.lat, lng: place.lng },
map: map,
title: place.title,
label: { text: label, color: 'white', fontSize: '14px', fontWeight: 'bold' },
icon: createMarkerIcon(place.dayIndex || 0, 'default'),
zIndex: place.orderIndex || 0,
});
// Event listeners (closure içinde callback'leri yakala)
marker.addListener('click', () => {
if (onMarkerClick) onMarkerClick(place.id);
// ... info window, pan
});
marker.addListener('mouseover', () => {
if (onMarkerHover) onMarkerHover(place.id);
});
marker.addListener('mouseout', () => {
if (onMarkerHover) onMarkerHover(null);
});
markersRef.current.set(place.id, marker);
});
// 3. Auto-fit bounds (sadece ilk kez)
if (places.length > 0 && !hasCenteredRef.current) {
const bounds = new google.maps.LatLngBounds();
places.forEach(place => bounds.extend({ lat: place.lat, lng: place.lng }));
map.fitBounds(bounds);
const listener = google.maps.event.addListenerOnce(map, 'idle', () => {
const currentZoom = map.getZoom();
if (currentZoom && currentZoom > 15) {
map.setZoom(15);
}
});
hasCenteredRef.current = true;
}
return () => {
// Cleanup: Sadece unmount'ta
markersRef.current.forEach(marker => marker.setMap(null));
markersRef.current.clear();
};
}, [places]); // ⚠️ SADECE places
```
---
### Effect 4: Visual Updates (Icon, Visibility, Animation)
**Dependency:** `[hoveredPlaceId, selectedPlaceId, activeDayId, places]`
**Sorumluluklar:**
- Marker visibility güncelle (activeDayId)
- Marker icon güncelle (hover/select state)
- Marker zIndex güncelle
- Marker animation güncelle
- Marker label güncelle
**ÖNEMLİ:**
- ❌ Bu effect marker OLUŞTURMAZ
- ✅ Sadece mevcut marker'ları GÜNCELLER
- ✅ setIcon, setVisible, setZIndex, setAnimation, setLabel
```typescript
useEffect(() => {
if (!mapInstanceRef.current || !window.google) return;
markersRef.current.forEach((marker, id) => {
const place = places.find(p => p.id === id);
if (!place) return;
// 1. Visibility kontrolü
const isVisible = !activeDayId || place.dayId === activeDayId;
marker.setVisible(isVisible);
// 2. State belirleme
let state: 'default' | 'hover' | 'selected' = 'default';
if (id === selectedPlaceId) {
state = 'selected';
marker.setZIndex(1000);
marker.setAnimation(google.maps.Animation.BOUNCE);
setTimeout(() => marker.setAnimation(null), 2000);
} else if (id === hoveredPlaceId) {
state = 'hover';
marker.setZIndex(999);
marker.setAnimation(null);
} else {
marker.setZIndex(place.orderIndex || 0);
marker.setAnimation(null);
}
// 3. Icon güncelleme (sadece style - size/anchor sabit)
marker.setIcon(createMarkerIcon(place.dayIndex || 0, state));
// 4. Label güncelleme
const label = `${(place.orderIndex || 0) + 1}`;
marker.setLabel({
text: label,
color: 'white',
fontSize: state === 'default' ? '14px' : '16px',
fontWeight: 'bold'
});
});
}, [hoveredPlaceId, selectedPlaceId, activeDayId, places]);
```
---
### Effect 5: Polyline Update
**Dependency:** `[places, activeDayId, showPolyline]`
**Sorumluluklar:**
- Polyline oluştur/güncelle
- activeDayId'ye göre filtrele
- Gün renklerini uygula
```typescript
useEffect(() => {
if (!mapInstanceRef.current || !showPolyline) return;
const map = mapInstanceRef.current;
// Eski polyline'ı sil
if (polylineRef.current) {
polylineRef.current.setMap(null);
}
// activeDayId varsa sadece o günün place'lerini al
let filteredPlaces = places;
if (activeDayId) {
filteredPlaces = places.filter(p => p.dayId === activeDayId);
}
if (filteredPlaces.length < 2) return;
// Polyline path oluştur
const path = filteredPlaces.map(p => ({ lat: p.lat, lng: p.lng }));
// Polyline rengi (ilk place'in günü)
const firstPlace = filteredPlaces[0];
const dayColor = getDayColor(firstPlace.dayIndex || 0);
const polyline = new google.maps.Polyline({
path: path,
geodesic: true,
strokeColor: dayColor.stroke,
strokeOpacity: 0.8,
strokeWeight: 3,
map: map,
});
polylineRef.current = polyline;
}, [places, activeDayId, showPolyline]);
```
---
## 🎨 HELPER FUNCTIONS
### getDayColor
**Sorumluluk:** Gün index'ine göre renk döndür
```typescript
const getDayColor = (dayIndex: number): { fill: string; stroke: string } => {
const colors = [
{ fill: '#f97316', stroke: '#ea580c' }, // Turuncu (Gün 1)
{ fill: '#3b82f6', stroke: '#2563eb' }, // Mavi (Gün 2)
{ fill: '#10b981', stroke: '#059669' }, // Yeşil (Gün 3)
{ fill: '#8b5cf6', stroke: '#7c3aed' }, // Mor (Gün 4)
{ fill: '#ec4899', stroke: '#db2777' }, // Pembe (Gün 5)
{ fill: '#f59e0b', stroke: '#d97706' }, // Sarı (Gün 6)
{ fill: '#06b6d4', stroke: '#0891b2' }, // Cyan (Gün 7)
];
return colors[dayIndex % colors.length];
};
```
---
### createMarkerIcon
**Sorumluluk:** Marker icon oluştur (size/anchor SABİT)
```typescript
const createMarkerIcon = (
dayIndex: number,
state: 'default' | 'hover' | 'selected'
) => {
const scale = 20; // ⚠️ SABİT
const color = getDayColor(dayIndex);
const fillColor = state === 'default' ? color.fill : color.stroke;
return {
path: google.maps.SymbolPath.CIRCLE,
scale: scale, // ⚠️ SABİT
fillColor: fillColor,
fillOpacity: 1,
strokeColor: 'white',
strokeWeight: state === 'selected' ? 4 : 3,
anchor: new google.maps.Point(0, 0), // ⚠️ SABİT anchor
labelOrigin: new google.maps.Point(0, 0),
};
};
```
---
## 📊 DATA FLOW
### TripPlanner → GoogleMap
```typescript
// TripPlanner.tsx
const allPlaces = trip?.days?.flatMap((day: any, dayIndex: number) => {
return day.places?.map((place: any, orderIndex: number) => ({
id: place.id,
lat: place.position.lat,
lng: place.position.lng,
dayId: day.id,
dayIndex: dayIndex,
orderIndex: orderIndex,
title: place.name,
// ✅ Color YOK - GoogleMap içinde hesaplanacak
})) || [];
}) || [];
<GoogleMap
places={allPlaces}
hoveredPlaceId={hoveredPlaceId}
selectedPlaceId={selectedPlaceId}
activeDayId={activeDayId}
onMarkerClick={handleMarkerClick}
onMarkerHover={handleMarkerHover}
showPolyline={true}
/>
```
---
### GoogleMap Props
```typescript
interface PlaceData {
id: string;
lat: number;
lng: number;
dayId?: string;
dayIndex?: number;
orderIndex?: number;
title: string;
// ❌ color YOK
}
interface GoogleMapProps {
places?: PlaceData[];
// ❌ center YOK
// ❌ zoom YOK
className?: string;
hoveredPlaceId?: string | null;
selectedPlaceId?: string | null;
activeDayId?: string | null;
onMarkerClick?: (placeId: string) => void;
onMarkerHover?: (placeId: string | null) => void; // ❌ dayId YOK
showPolyline?: boolean;
}
```
---
## 🔄 INTERACTION FLOW
### 1. User Hovers Timeline Place
```
Timeline Place Hover
TripPlanner: setHoveredPlaceId(placeId)
GoogleMap: hoveredPlaceId prop değişir
Visual Update Effect tetiklenir
Marker: setIcon (hover state)
Marker: setZIndex (999)
Marker: setLabel (16px)
```
**Önemli:**
- ❌ activeDayId DEĞİŞMEZ
- ❌ Marker yeniden oluşturulmaz
- ✅ Sadece görsel güncelleme
---
### 2. User Clicks Timeline Place
```
Timeline Place Click
TripPlanner: setSelectedPlaceId(placeId)
GoogleMap: selectedPlaceId prop değişir
Visual Update Effect tetiklenir
Marker: setIcon (selected state)
Marker: setZIndex (1000)
Marker: setAnimation (BOUNCE)
Marker: setLabel (16px)
```
**Önemli:**
- ❌ Marker yeniden oluşturulmaz
- ✅ Sadece görsel güncelleme
- ✅ 2 saniye sonra animation durur
---
### 3. User Opens Day Accordion
```
Timeline Day Accordion Open
TripPlanner: setActiveDayId(dayId)
GoogleMap: activeDayId prop değişir
Visual Update Effect tetiklenir
Marker: setVisible (dayId === activeDayId)
```
**Önemli:**
- ❌ Marker silinmez
- ✅ Sadece gizlenir/gösterilir
- ✅ Smooth visibility toggle
---
### 4. User Hovers Map Marker
```
Map Marker Hover
Marker: mouseover event
onMarkerHover(placeId) callback
TripPlanner: setHoveredPlaceId(placeId)
GoogleMap: hoveredPlaceId prop değişir
Visual Update Effect tetiklenir
Marker: setIcon (hover state)
```
**Önemli:**
- ❌ activeDayId DEĞİŞMEZ
- ✅ Sadece hoveredPlaceId değişir
- ✅ Timeline'da place highlight olur
---
### 5. User Clicks Map Marker
```
Map Marker Click
Marker: click event
onMarkerClick(placeId) callback
TripPlanner: setSelectedPlaceId(placeId)
GoogleMap: selectedPlaceId prop değişir
Visual Update Effect tetiklenir
Marker: setIcon (selected state)
Marker: setAnimation (BOUNCE)
InfoWindow: open
Map: panTo marker
```
---
## ✅ BEST PRACTICES
### 1. Marker Lifecycle
**DO:**
- Marker'ları SADECE places değiştiğinde oluştur
- Mevcut marker'ları useRef içinde sakla
- Görsel güncellemeler için ayrı effect kullan
- Event listener'ları marker creation sırasında ekle
**DON'T:**
- Marker'ları hover/select'te yeniden oluşturma
- onMarkerClick/onMarkerHover'ı dependency'ye ekleme
- Her effect'te marker loop yapma
---
### 2. Center Management
**DO:**
- hasCenteredRef kullan
- Center'ı sadece 1 kez ayarla
- Kullanıcı zoom/pan'i koru
**DON'T:**
- Her places değişiminde fitBounds çağırma
- Kullanıcı zoom/pan'i resetleme
---
### 3. Visual Updates
**DO:**
- setIcon, setVisible, setZIndex, setAnimation kullan
- Marker size/anchor'ı sabit tut
- State'e göre sadece color değiştir
**DON'T:**
- Marker'ı yeniden oluşturma
- Size/anchor değiştirme (jitter)
- Gereksiz DOM manipulation
---
### 4. Performance
**DO:**
- Effect dependency'lerini minimize et
- Marker recreation'ı önle
- Memory leak'leri temizle
**DON'T:**
- Gereksiz effect tetikleme
- Marker'ları silmeden bırakma
- Çok fazla dependency ekleme
---
## 🎯 SONUÇ
GoogleMap component'i optimal marker lifecycle yönetimi ile:
**Performans:**
- Marker recreation: Minimal (sadece places değişiminde)
- Effect execution: Optimize edilmiş
- Memory kullanımı: Stabil
**Kullanıcı Deneyimi:**
- Smooth hover/select transitions
- Marker jitter YOK
- Map zoom/pan korunuyor
- Responsive interactions
**Kod Kalitesi:**
- Clean separation of concerns
- Predictable lifecycle
- Easy to maintain
- Well documented
**GoogleMap component production-ready!** 🎉