Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improves home page chapter map #1009

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 81 additions & 23 deletions frontend/src/components/ChapterMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,31 @@ import 'leaflet.markercluster/dist/MarkerCluster.Default.css'
import 'leaflet.markercluster'
import { GeoLocDataAlgolia, GeoLocDataGraphQL } from 'types/chapter'

const getDistance = (lat1: number, lng1: number, lat2: number, lng2: number) => {
const R = 6371
const dLat = ((lat2 - lat1) * Math.PI) / 180
const dLng = ((lng2 - lng1) * 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(dLng / 2) *
Math.sin(dLng / 2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return R * c
}

const ChapterMap = ({
geoLocData,
userLocation = null,
style,
}: {
geoLocData: GeoLocDataGraphQL[] | GeoLocDataAlgolia[]
userLocation?: { lat: number; lng: number } | null
style: React.CSSProperties
}) => {
const mapRef = useRef<L.Map | null>(null)
const markerClusterRef = useRef<L.MarkerClusterGroup | null>(null)

const normalizedData = useMemo(() => {
return geoLocData.map((chapter) => ({
Expand All @@ -24,48 +41,73 @@ const ChapterMap = ({
}))
}, [geoLocData])

//for reference: https://leafletjs.com/reference.html#map-example
const nearestChapters = useMemo(() => {
if (!userLocation) return normalizedData

return normalizedData
.map((chapter) => ({
...chapter,
distance: getDistance(userLocation.lat, userLocation.lng, chapter.lat, chapter.lng),
}))
.sort((a, b) => a.distance - b.distance)
.slice(0, 5)
}, [userLocation, normalizedData])

useEffect(() => {
// Initialize map if not created
if (!mapRef.current) {
mapRef.current = L.map('chapter-map', {
worldCopyJump: false, // Prevents the map from wrapping around the world
worldCopyJump: false,
maxBounds: [
[-90, -180], // Southwest corner of the map bounds (latitude, longitude)
[90, 180], // Northeast corner of the map bounds (latitude, longitude)
[-90, -180],
[90, 180],
],
maxBoundsViscosity: 1.0,
}).setView([20, 0], 2)

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
className: 'map-tiles',
}).addTo(mapRef.current)
}

const map = mapRef.current

// Remove previous markers
map.eachLayer((layer) => {
if (layer instanceof L.Marker || layer instanceof L.LayerGroup) {
map.removeLayer(layer)
}
})
// Remove existing marker cluster group
if (markerClusterRef.current) {
map.removeLayer(markerClusterRef.current)
}

// Create new marker cluster group
const markerClusterGroup = L.markerClusterGroup()
markerClusterRef.current = markerClusterGroup

const bounds: [number, number][] = []
normalizedData.forEach((chapter) => {

// Validate and filter out invalid coordinates
const validChapters = normalizedData.filter(
(chapter) =>
chapter.lat !== null &&
chapter.lng !== null &&
!isNaN(chapter.lat) &&
!isNaN(chapter.lng) &&
chapter.lat >= -90 &&
chapter.lat <= 90 &&
chapter.lng >= -180 &&
chapter.lng <= 180
)

// Create markers for all chapters
validChapters.forEach((chapter) => {
const markerIcon = new L.Icon({
iconAnchor: [12, 41], // Anchor point
iconAnchor: [12, 41],
iconRetinaUrl: '/img/marker-icon-2x.png',
iconSize: [25, 41], // Default size for Leaflet markers
iconSize: [25, 41],
iconUrl: '/img/marker-icon.png',
popupAnchor: [1, -34], // Popup position relative to marker
shadowSize: [41, 41], // Shadow size
popupAnchor: [1, -34],
shadowSize: [41, 41],
shadowUrl: '/img/marker-shadow.png',
})
const marker = L.marker([chapter.lat, chapter.lng], {
icon: markerIcon,
})
const marker = L.marker([chapter.lat, chapter.lng], { icon: markerIcon })
const popup = L.popup()
const popupContent = document.createElement('div')
popupContent.className = 'popup-content'
Expand All @@ -81,12 +123,28 @@ const ChapterMap = ({

map.addLayer(markerClusterGroup)

if (bounds.length > 0) {
map.fitBounds(bounds as L.LatLngBoundsExpression, { maxZoom: 10 })
// Determine map view
try {
if (userLocation && nearestChapters.length > 0) {
// Prioritize fitting bounds to nearest chapters
const nearestBounds = nearestChapters.map(
(chapter) => [chapter.lat, chapter.lng] as [number, number]
)
map.fitBounds(nearestBounds, {
maxZoom: 10, // Ensure not too zoomed in
padding: [50, 50], // Add some padding
})
} else if (bounds.length > 0) {
// Fallback to all chapters bounds
map.fitBounds(bounds)
}
} catch {
// Fallback to default view if bounds fitting fails
map.setView([20, 0], 2)
}
}, [normalizedData])
}, [normalizedData, nearestChapters, userLocation])

return <div id="chapter-map" className="rounded-lg dark:bg-[#212529]" style={style} />
return <div id="chapter-map" style={style} />
}

export default ChapterMap
41 changes: 33 additions & 8 deletions frontend/src/pages/Chapters.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fetchAlgoliaData } from 'api/fetchAlgoliaData'
import { useSearchPage } from 'hooks/useSearchPage'
import { useEffect, useState } from 'react'
import { useEffect, useState, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import { AlgoliaResponseType } from 'types/algolia'
import { ChapterTypeAlgolia } from 'types/chapter'
Expand All @@ -14,6 +14,8 @@ import SearchPageLayout from 'components/SearchPageLayout'

const ChaptersPage = () => {
const [geoLocData, setGeoLocData] = useState<ChapterTypeAlgolia[]>([])
const [userLocation, setUserLocation] = useState<{ lat: number; lng: number } | null>(null)

const {
items: chapters,
isLoaded,
Expand All @@ -27,6 +29,7 @@ const ChaptersPage = () => {
pageTitle: 'OWASP Chapters',
})

// Fetch chapter data and user location
useEffect(() => {
const fetchData = async () => {
const searchParams = {
Expand All @@ -43,15 +46,33 @@ const ChaptersPage = () => {
)
setGeoLocData(data.hits)
}

const fetchUserLocation = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
setUserLocation({
lat: position.coords.latitude,
lng: position.coords.longitude,
})
},
() => {
// Handle error (e.g., user denied access to location)
setUserLocation(null)
}
)
}
}

fetchData()
fetchUserLocation()
}, [])

const navigate = useNavigate()

const renderChapterCard = (chapter: ChapterTypeAlgolia) => {
const params: string[] = ['updated_at']
const filteredIcons = getFilteredIcons(chapter, params)
const filteredIcons = getFilteredIcons(chapter, ['updated_at'])
const formattedUrls = handleSocialUrls(chapter.related_urls)

const handleButtonClick = () => {
navigate(`/chapters/${chapter.key}`)
}
Expand All @@ -61,7 +82,6 @@ const ChaptersPage = () => {
icon: <FontAwesomeIconWrapper icon="fa-solid fa-right-to-bracket " />,
onclick: handleButtonClick,
}

return (
<Card
key={chapter.objectID}
Expand All @@ -76,6 +96,10 @@ const ChaptersPage = () => {
)
}

const mapData = useMemo(() => {
return searchQuery ? chapters : geoLocData
}, [searchQuery, chapters, geoLocData])

return (
<MetadataManager {...METADATA_CONFIG.chapters}>
<SearchPageLayout
Expand All @@ -89,13 +113,14 @@ const ChaptersPage = () => {
searchQuery={searchQuery}
totalPages={totalPages}
>
{chapters.length > 0 && (
{mapData.length > 0 && (
<ChapterMap
geoLocData={searchQuery ? chapters : geoLocData}
geoLocData={mapData}
userLocation={userLocation}
style={{ height: '400px', width: '100%', zIndex: '0' }}
/>
)}
{chapters && chapters.filter((chapter) => chapter.is_active).map(renderChapterCard)}
{chapters.filter((chapter) => chapter.is_active).map(renderChapterCard)}
</SearchPageLayout>
</MetadataManager>
)
Expand Down