From daa785e4ebda4287706df0abc52f10aec7b96b68 Mon Sep 17 00:00:00 2001 From: Hanka8 Date: Mon, 4 Nov 2024 18:09:20 +0100 Subject: [PATCH 1/4] Add marker, bird icon and radius to InteractiveMap --- src/assets/bird-ico.svg | 35 ++++++++++++++ src/components/Index.tsx | 5 +- src/components/InteractiveMap.tsx | 80 ++++++++++++++++++++++++++++--- src/constants.ts | 1 + src/index.css | 2 +- src/types.tsx | 3 ++ 6 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 src/assets/bird-ico.svg create mode 100644 src/constants.ts diff --git a/src/assets/bird-ico.svg b/src/assets/bird-ico.svg new file mode 100644 index 0000000..3396cd9 --- /dev/null +++ b/src/assets/bird-ico.svg @@ -0,0 +1,35 @@ + + + + diff --git a/src/components/Index.tsx b/src/components/Index.tsx index 1ebf8e9..d61a273 100644 --- a/src/components/Index.tsx +++ b/src/components/Index.tsx @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { useEffect, useState } from "react"; +import { radius } from "../constants"; import { getLocation } from "../functions/getLocation"; import BirdCard from "./BirdCard"; import InteractiveMap from "./InteractiveMap"; @@ -43,7 +44,7 @@ const Index: React.FC = () => { longitude, }) => { const response = await fetch( - `https://api.ebird.org/v2/data/obs/geo/recent?dist=15&back=3&lat=${Number(latitude)}&lng=${Number(longitude)}`, + `https://api.ebird.org/v2/data/obs/geo/recent?dist=${radius}&back=30&includeProvisional=true&lat=${Number(latitude)}&lng=${Number(longitude)}`, { headers: { "X-eBirdApiToken": import.meta.env.VITE_API_KEY_EBIRD, @@ -72,8 +73,8 @@ const Index: React.FC = () => { return (
-

Birds around you

+ {data && }
= ({ latitude, longitude }) => { +const InteractiveMap: React.FC = ({ + latitude, + longitude, + data, +}) => { const mapRef = useRef(null); - // create map when latitude and longitude are set useEffect(() => { if (!latitude || !longitude) return; @@ -26,17 +37,74 @@ const InteractiveMap: React.FC = ({ latitude, longitude }) }), ], view: new View({ - center: [0, 0], + center: coordinates, zoom: 12, }), }); - map.getView().setCenter(coordinates); + // Add main marker for the central location + const marker = new Feature({ + geometry: new Point(coordinates), + }); + + marker.setStyle( + new Style({ + image: new Icon({ + src: "https://openlayers.org/en/latest/examples/data/icon.png", + scale: 0.75, + }), + }) + ); + + const circleFeature = new Feature({ + geometry: new Circle(coordinates, radius * 1000), + }); + + circleFeature.setStyle( + new Style({ + stroke: new Stroke({ + color: "rgba(0, 128, 0, 0.8)", + width: 2, + }), + fill: new Fill({ + color: "rgba(0, 255, 0, 0.2)", + }), + }) + ); + + const vectorSource = new VectorSource({ + features: [marker, circleFeature], + }); + + data?.forEach((bird) => { + const birdCoordinates = fromLonLat([bird.lng, bird.lat]); + const birdMarker = new Feature({ + geometry: new Point(birdCoordinates), + }); + + birdMarker.setStyle( + new Style({ + image: new Icon({ + src: birdIcon, + scale: 0.75, + color: "rgba(255, 0, 0, 0.8)", + }), + }) + ); + + vectorSource.addFeature(birdMarker); + }); + + const markerLayer = new VectorLayer({ + source: vectorSource, + }); + + map.addLayer(markerLayer); return () => map.setTarget(undefined); - }, [latitude, longitude]); + }, [latitude, longitude, data]); - return
; + return
; }; export default InteractiveMap; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..439d407 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1 @@ +export const radius = 5; \ No newline at end of file diff --git a/src/index.css b/src/index.css index 2f1e94f..ce1b7f4 100644 --- a/src/index.css +++ b/src/index.css @@ -3,5 +3,5 @@ @tailwind utilities; .h-32rem { - height: 20rem; + height: 32rem; } \ No newline at end of file diff --git a/src/types.tsx b/src/types.tsx index 587f33e..bfa987f 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -21,6 +21,8 @@ export type Bird = { howMany: number; locName: string; obsDt: string; + lat: number; + lng: number; }; export type BirdCardProps = { @@ -91,4 +93,5 @@ export type ErrorProps = { export type InteractiveMapProps = { latitude: string; longitude: string; + data: Bird[] | undefined; }; \ No newline at end of file From e96d0e33dcda7e553ad2b67c82ebe00342ab4f6a Mon Sep 17 00:00:00 2001 From: Hanka8 Date: Mon, 4 Nov 2024 18:58:29 +0100 Subject: [PATCH 2/4] Add bird icon to map --- src/components/FormGeolocation.tsx | 2 +- src/components/Index.tsx | 2 +- src/components/InteractiveMap.tsx | 14 ++++++++++++-- src/types.tsx | 2 ++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/FormGeolocation.tsx b/src/components/FormGeolocation.tsx index 1ee772c..12459bf 100644 --- a/src/components/FormGeolocation.tsx +++ b/src/components/FormGeolocation.tsx @@ -96,7 +96,7 @@ const LocationForm: React.FC = ({ />
- Insert manually + {geolocationManually ? "Get your location" : "Set adress manually" }
diff --git a/src/components/Index.tsx b/src/components/Index.tsx index d61a273..b12d3ea 100644 --- a/src/components/Index.tsx +++ b/src/components/Index.tsx @@ -74,7 +74,7 @@ const Index: React.FC = () => { return (

Birds around you

- {data && } + {data && }
= ({ latitude, longitude, + setLatitude, + setLongitude, data, }) => { const mapRef = useRef(null); @@ -92,7 +95,7 @@ const InteractiveMap: React.FC = ({ }) ); - vectorSource.addFeature(birdMarker); + vectorSource.addFeature(birdMarker); }); const markerLayer = new VectorLayer({ @@ -101,8 +104,15 @@ const InteractiveMap: React.FC = ({ map.addLayer(markerLayer); + // Add click event listener to the map + map.on("singleclick", (event) => { + const clickedCoordinate = toLonLat(event.coordinate); // Convert to longitude/latitude + setLatitude(clickedCoordinate[1].toFixed(2)); + setLongitude(clickedCoordinate[0].toFixed(2)); + }); + return () => map.setTarget(undefined); - }, [latitude, longitude, data]); + }, [latitude, longitude, data, setLatitude, setLongitude]); return
; }; diff --git a/src/types.tsx b/src/types.tsx index bfa987f..a5e3c5a 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -93,5 +93,7 @@ export type ErrorProps = { export type InteractiveMapProps = { latitude: string; longitude: string; + setLatitude: (value: string) => void; + setLongitude: (value: string) => void; data: Bird[] | undefined; }; \ No newline at end of file From 79d08e785f49bcb1c54ee7b2ae347fc85e04e0e6 Mon Sep 17 00:00:00 2001 From: Hanka8 Date: Mon, 4 Nov 2024 19:19:27 +0100 Subject: [PATCH 3/4] Make the map modal a separate component Clicking on a map marker now opens a modal with the map and a close button --- src/components/InteractiveMap.tsx | 40 +++++++++++++++++++++++++++---- src/components/MapModal.tsx | 24 +++++++++++++++++++ src/types.tsx | 8 ++++++- 3 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 src/components/MapModal.tsx diff --git a/src/components/InteractiveMap.tsx b/src/components/InteractiveMap.tsx index 40118e4..b01c792 100644 --- a/src/components/InteractiveMap.tsx +++ b/src/components/InteractiveMap.tsx @@ -1,4 +1,5 @@ -import { useEffect, useRef } from "react"; +import { useState, useEffect, useRef } from "react"; +import MapModal from "./MapModal"; import Map from "ol/Map.js"; import OSM from "ol/source/OSM.js"; import TileLayer from "ol/layer/Tile.js"; @@ -22,6 +23,10 @@ const InteractiveMap: React.FC = ({ setLongitude, data, }) => { + + const [modalOpen, setModalOpen] = useState(false); + const [modalContent, setModalContent] = useState(""); + const mapRef = useRef(null); useEffect(() => { @@ -95,6 +100,7 @@ const InteractiveMap: React.FC = ({ }) ); + birdMarker.set("description", `Bird Name: ${bird.comName}`); vectorSource.addFeature(birdMarker); }); @@ -106,15 +112,39 @@ const InteractiveMap: React.FC = ({ // Add click event listener to the map map.on("singleclick", (event) => { - const clickedCoordinate = toLonLat(event.coordinate); // Convert to longitude/latitude - setLatitude(clickedCoordinate[1].toFixed(2)); - setLongitude(clickedCoordinate[0].toFixed(2)); + const clickedFeature = map.forEachFeatureAtPixel( + event.pixel, + (feature) => { + return feature; + } + ); + + if (clickedFeature) { + const description = clickedFeature.get("description"); + if (description) { + setModalContent(description); // Set the content for the modal + setModalOpen(true); // Open the modal + } + } else { + const clickedCoordinate = toLonLat(event.coordinate); // Convert to longitude/latitude + setLatitude(clickedCoordinate[1].toFixed(2)); + setLongitude(clickedCoordinate[0].toFixed(2)); + } }); return () => map.setTarget(undefined); }, [latitude, longitude, data, setLatitude, setLongitude]); - return
; + return ( +
+
+ setModalOpen(false)} + content={modalContent} + /> +
+ ); }; export default InteractiveMap; diff --git a/src/components/MapModal.tsx b/src/components/MapModal.tsx new file mode 100644 index 0000000..553dbb9 --- /dev/null +++ b/src/components/MapModal.tsx @@ -0,0 +1,24 @@ + + +import { MapModalProps } from "../types"; + +const MapModal: React.FC = ({ isOpen, onClose, content }) => { + if (!isOpen) return null; + + return ( +
+
+

Info

+

{content}

+ +
+
+ ); +}; + +export default MapModal; diff --git a/src/types.tsx b/src/types.tsx index a5e3c5a..ba3a8fd 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -96,4 +96,10 @@ export type InteractiveMapProps = { setLatitude: (value: string) => void; setLongitude: (value: string) => void; data: Bird[] | undefined; -}; \ No newline at end of file +}; + +export type MapModalProps = { + isOpen: boolean; + onClose: () => void; + content: string; +} \ No newline at end of file From c56a7bd3dd7f139557135e16eb5a4ee6fbd1a464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kudli=C4=8Dka?= <70945365+tkudlicka@users.noreply.github.com> Date: Wed, 6 Nov 2024 17:27:30 +0100 Subject: [PATCH 4/4] Update bird-ico.svg --- src/assets/bird-ico.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/bird-ico.svg b/src/assets/bird-ico.svg index 3396cd9..02cb38d 100644 --- a/src/assets/bird-ico.svg +++ b/src/assets/bird-ico.svg @@ -24,7 +24,7 @@ showgrid="false" />