Skip to content

Commit

Permalink
Merge pull request #120 from pedrolivaresanchez/feat/mapa-geolocaliza…
Browse files Browse the repository at this point in the history
…cion

Feat/mapa geolocalizacion
  • Loading branch information
robertobobby1 authored Nov 7, 2024
2 parents 62d3928 + c14281b commit 428ab89
Show file tree
Hide file tree
Showing 11 changed files with 282 additions and 4 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=

API_KEY=
77 changes: 77 additions & 0 deletions src/app/api/address/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { NextRequest } from 'next/server';

const mapsTranslationToDbTowns: { [key: string]: string } = {
Aldaya: 'Aldaia',
'Ribarroja de Turia': 'Riba-roja de Túria',
Benetuser: 'Benetusser',
Benetússer: 'Benetusser',
Benetúser: 'Benetusser',
Toris: 'Turís',
Picaña: 'Picanya',
'La Alcudia': "L'Alcúdia",
'Lugar Nuevo de la Corona': 'Llocnou de la Corona',
'Castellón de la Plana': 'Castelló de la Plana',
Alcudia: "L'Alcúdia",
Guadasuar: 'Guadassuar',
València: 'Valencia',
};

const GOOGLE_URL = `https://maps.googleapis.com/maps/api/geocode/json?key=${process.env.API_KEY}&latlng=`;

export type AddressAndTown = { address: string; town: string };

function normalizeData({ address, town }: AddressAndTown): AddressAndTown {
const normalizedTown = Object.keys(mapsTranslationToDbTowns).includes(town) ? mapsTranslationToDbTowns[town] : town;
const normalizedAddress = address.replace(town, normalizedTown);
return { address: normalizedAddress, town: normalizedTown };
}

function extractAddressAndTown(googleResponse: any) {
// for response refer to documentation: https://developers.google.com/maps/documentation/geocoding/requests-reverse-geocoding
// it returns many due to inaccuracies but they only differ from street number(normally) - we look for a good result - contains sublocality
let town = '';
let address = '';
for (const result of googleResponse['results']) {
for (const addressComponent of result['address_components']) {
let localityFound = false;

// max three, not really a performance issue
for (const type of addressComponent['types']) {
if (type === 'locality') {
localityFound = true;
town = addressComponent['long_name'];
break;
}
}

if (localityFound) {
address = result['formatted_address'];

return normalizeData({ address, town });
}
}
}

return { address, town };
}

export async function POST(request: NextRequest) {
const body = await request.json();
if (!body.latitude || !body.longitude) {
return Response.json({
error: 'Latitude and longitude are mandatory fields!',
});
}

try {
const response = await fetch(`${GOOGLE_URL}${body.latitude},${body.longitude}`);
const extractedData = extractAddressAndTown(await response.json());

return Response.json(extractedData);
} catch (exception) {
console.error(exception);
return Response.json({
error: 'An error occured calling google - check logs',
});
}
}
1 change: 0 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AlertCircle, Clock, Cross, Heart, MapPin, Navigation, Package, Thermometer, Users } from 'lucide-react';

// @ts-expect-error
import PhoneNumberDialog from '@/components/auth/PhoneNumberDialog';

import Image from 'next/image';
Expand Down
8 changes: 7 additions & 1 deletion src/components/AddressAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { MapPin } from 'lucide-react';

export default function AddressAutocomplete({ onSelect, placeholder = 'Buscar dirección...', initialValue = '' }) {
export default function AddressAutocomplete({
onSelect,
placeholder = 'Buscar dirección...',
initialValue = '',
required = false,
}) {
const [query, setQuery] = useState(initialValue);
const [suggestions, setSuggestions] = useState([]);
const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -150,6 +155,7 @@ export default function AddressAutocomplete({ onSelect, placeholder = 'Buscar di
<div className="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg max-h-60 overflow-auto">
{suggestions.map((suggestion, index) => (
<button
required={required}
key={index}
onClick={() => handleSelect(suggestion)}
className="w-full text-left px-4 py-2 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
Expand Down
50 changes: 50 additions & 0 deletions src/components/AddressMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client';

import GeoLocationMap, { LngLat } from '@/components/map/GeolocationMap';
import { useState } from 'react';

export type AddressDescriptopr = { address: string; town: string; coordinates: LngLat };
export type AddressAndTownCallback = (addressAndTown: AddressDescriptopr) => void;
export type AddressMapProps = {
onNewAddressCallback: AddressAndTownCallback;
};

export default function AddressMap({ onNewAddressCallback }: AddressMapProps) {
const [address, setAddress] = useState('');
const [town, setTown] = useState('');

const onNewPosition = async (lngLat: LngLat) => {
if (address !== '') {
return;
}

const response = await fetch('/api/address', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
longitude: lngLat.lng,
latitude: lngLat.lat,
}),
}).then((res) => res.json());

setAddress(response.address);
setTown(response.town);
if (typeof onNewAddressCallback === 'function') {
onNewAddressCallback({ address: response.address, town: response.town, coordinates: lngLat });
}
};

return (
<div className="space-y-2">
<GeoLocationMap onNewPositionCallback={onNewPosition} />
{/* Address */}
<input
disabled
type="text"
value={address}
onChange={(e) => setAddress(e.target.value)}
className="w-full p-2 border rounded focus:ring-2 focus:ring-green-500 focus:border-green-500"
/>
</div>
);
}
99 changes: 99 additions & 0 deletions src/components/map/GeolocationMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useEffect, useRef } from 'react';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';

const PAIPORTA_LAT_LNG: [number, number] = [-0.41667, 39.42333];

export type LngLat = { lng: number; lat: number };
export type GeoLocationMapProps = {
onNewPositionCallback: (lngLat: LngLat) => void;
zoom?: number;
};

export default function GeoLocationMap({ onNewPositionCallback, zoom = 13 }: GeoLocationMapProps) {
const mapContainerRef = useRef<HTMLDivElement | null>(null);
const mapRef = useRef<maplibregl.Map | null>(null);

// geolocate control
const geolocateControl = new maplibregl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true,
},
trackUserLocation: true,
showAccuracyCircle: true,
});

useEffect(() => {
if (!mapRef.current) {
if (!mapContainerRef.current) {
return;
}

mapRef.current = new maplibregl.Map({
container: mapContainerRef.current,
style: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
center: PAIPORTA_LAT_LNG,
zoom: zoom,
});

mapRef.current.on('moveend', () => {
if (!mapRef.current) {
return;
}

const center = mapRef.current.getCenter();
if (typeof onNewPositionCallback === 'function') {
onNewPositionCallback(center);
}
});

mapRef.current.on('move', () => {
if (!mapRef.current) {
return;
}

const center = mapRef.current.getCenter();
marker.setLngLat(center);
});

geolocateControl.on('geolocate', (e) => {
if (!mapRef.current) {
return;
}

const userLocation: [number, number] = [e.coords.longitude, e.coords.latitude];
// Center the map on the user's location
mapRef.current.flyTo({
center: userLocation,
zoom,
essential: true,
});

if (typeof onNewPositionCallback === 'function') {
onNewPositionCallback({ lng: userLocation[0], lat: userLocation[1] });
}
});

const marker = new maplibregl.Marker({
color: '#ef4444', //text-red-500
draggable: false,
})
.setLngLat(mapRef.current.getCenter())
.addTo(mapRef.current);

mapRef.current.addControl(new maplibregl.NavigationControl(), 'top-right');
// Add the geolocate control
mapRef.current.addControl(geolocateControl);

// add to js queue so that the control is correctly added, then trigger the location detection
setTimeout(() => geolocateControl.trigger(), 100);
}

return () => {
mapRef.current?.remove();
mapRef.current = null;
};
}, [zoom, onNewPositionCallback]);

return <div ref={mapContainerRef} className="aspect-video w-full" />;
}
20 changes: 20 additions & 0 deletions src/lib/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ export const helpRequestService = {
},
};

export const townService = {
async getByName(townName: string) {
return await supabase.from('towns').select('id').eq('name', townName);
},
async create(townName: string) {
return await supabase.from('towns').insert({ name: townName }).select('id');
},
async createIfNotExists(townName: string) {
const response = await this.getByName(townName);
if (response.error) return response;

// new town should be created
if (response.data.length === 0) {
return await townService.create(townName);
}

return response;
},
};

export const missingPersonService = {
async create(data: any) {
const { data: result, error } = await supabase.from('missing_persons').insert([data]).select();
Expand Down
14 changes: 14 additions & 0 deletions supabase/migrations/20241106194708_fix_towns_names.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
UPDATE towns
SET name = 'Castelló de la Plana'
WHERE name = 'Castelló';

UPDATE help_requests
SET town_id = (
SELECT id FROM towns WHERE name = 'L''Alcúdia'
)
WHERE town_id = (
SELECT id FROM towns WHERE name = 'Alcudia'
);

DELETE FROM towns
WHERE name = 'Alcudia'
8 changes: 8 additions & 0 deletions supabase/migrations/20241106201910_insert_policy_towns.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
create policy "Enable insert public"
on "public"."towns"
as permissive
for insert
to public
with check (true);


5 changes: 4 additions & 1 deletion supabase/seed.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ INSERT INTO "public"."towns" ("id", "created_at", "name", "people_helping", "hel
('10', '2024-11-02 17:52:17.645308+00', 'Chiva', '0', '0'),
('11', '2024-11-02 17:52:17.645308+00', 'Gestalgar', '0', '0'),
('12', '2024-11-02 17:52:17.645308+00', 'Guadassuar', '0', '0'),
('13', '2024-11-02 17:52:17.645308+00', 'L''Alcúdia', '0', '0'),
('14', '2024-11-02 17:52:17.645308+00', 'Manuel', '0', '0'),
('15', '2024-11-02 17:52:17.645308+00', 'Massanassa', '0', '0'),
('16', '2024-11-02 17:52:17.645308+00', 'Montserrat', '0', '0'),
Expand All @@ -30,4 +31,6 @@ INSERT INTO "public"."towns" ("id", "created_at", "name", "people_helping", "hel
('30', '2024-11-03 09:05:24.008619+00', 'Benimaclet', '0', '0'),
('31', '2024-11-03 09:21:27.852067+00', 'Godelleta', '0', '0'),
('32', '2024-11-05 10:38:38.669406+00', 'Llocnou de la Corona', '0', '0'),
('33', '2024-11-05 10:38:52.085488+00', 'Beniparrell', '0', '0');
('33', '2024-11-05 10:38:52.085488+00', 'Beniparrell', '0', '0');

SELECT setval(pg_get_serial_sequence('towns', 'id'), (SELECT MAX(id) FROM towns) + 1);
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"strict": true,
"skipLibCheck": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"allowJs": true,
"checkJs": false,
"noEmit": true,
"incremental": true,
Expand Down

0 comments on commit 428ab89

Please sign in to comment.