316 lines
8.5 KiB
TypeScript
316 lines
8.5 KiB
TypeScript
// Example usage of Institute Geocoder functions
|
|
// This file demonstrates how to integrate the geocoding functions in your frontend
|
|
|
|
import { createClient } from '@supabase/supabase-js'
|
|
|
|
// Initialize Supabase client
|
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
|
|
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
|
|
|
// Types for institute data
|
|
interface Institute {
|
|
id: string
|
|
name: string
|
|
address: {
|
|
street?: string
|
|
town?: string
|
|
county?: string
|
|
postcode?: string
|
|
country?: string
|
|
}
|
|
geo_coordinates?: {
|
|
latitude: number
|
|
longitude: number
|
|
boundingbox: string[]
|
|
search_query: string
|
|
geocoded_at: string
|
|
}
|
|
}
|
|
|
|
interface GeocodingResult {
|
|
success: boolean
|
|
message: string
|
|
coordinates?: {
|
|
latitude: number
|
|
longitude: number
|
|
boundingbox: string[]
|
|
}
|
|
error?: string
|
|
}
|
|
|
|
// 1. Geocode a single institute
|
|
export async function geocodeInstitute(instituteId: string): Promise<GeocodingResult> {
|
|
try {
|
|
const { data, error } = await supabase.functions.invoke('institute-geocoder', {
|
|
body: { institute_id: instituteId }
|
|
})
|
|
|
|
if (error) {
|
|
throw new Error(error.message)
|
|
}
|
|
|
|
return data
|
|
} catch (error) {
|
|
console.error('Geocoding failed:', error)
|
|
return {
|
|
success: false,
|
|
message: 'Geocoding failed',
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Batch geocode multiple institutes
|
|
export async function batchGeocodeInstitutes(
|
|
limit: number = 10,
|
|
forceRefresh: boolean = false
|
|
): Promise<any> {
|
|
try {
|
|
const { data, error } = await supabase.functions.invoke('institute-geocoder/batch', {
|
|
body: {
|
|
limit,
|
|
force_refresh: forceRefresh
|
|
}
|
|
})
|
|
|
|
if (error) {
|
|
throw new Error(error.message)
|
|
}
|
|
|
|
return data
|
|
} catch (error) {
|
|
console.error('Batch geocoding failed:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// 3. Get institutes that need geocoding
|
|
export async function getInstitutesNeedingGeocoding(): Promise<Institute[]> {
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('institutes')
|
|
.select('id, name, address, geo_coordinates')
|
|
.or('geo_coordinates.is.null,geo_coordinates.eq.{}')
|
|
.not('import_id', 'is', null)
|
|
|
|
if (error) {
|
|
throw new Error(error.message)
|
|
}
|
|
|
|
return data || []
|
|
} catch (error) {
|
|
console.error('Failed to fetch institutes:', error)
|
|
return []
|
|
}
|
|
}
|
|
|
|
// 4. Display institute on a map (example with Leaflet)
|
|
export function displayInstituteOnMap(
|
|
institute: Institute,
|
|
mapElement: HTMLElement
|
|
): void {
|
|
if (!institute.geo_coordinates) {
|
|
console.warn('Institute has no coordinates:', institute.name)
|
|
return
|
|
}
|
|
|
|
// This is a placeholder - you'd need to implement actual map rendering
|
|
// For example, using Leaflet, Mapbox, or Google Maps
|
|
const { latitude, longitude } = institute.geo_coordinates
|
|
|
|
console.log(`Displaying ${institute.name} at ${latitude}, ${longitude}`)
|
|
|
|
// Example map implementation:
|
|
// const map = L.map(mapElement).setView([latitude, longitude], 13)
|
|
// L.marker([latitude, longitude]).addTo(map).bindPopup(institute.name)
|
|
}
|
|
|
|
// 5. React component example
|
|
export function InstituteGeocoder() {
|
|
const [institutes, setInstitutes] = useState<Institute[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [geocodingProgress, setGeocodingProgress] = useState(0)
|
|
|
|
// Load institutes that need geocoding
|
|
useEffect(() => {
|
|
loadInstitutes()
|
|
}, [])
|
|
|
|
async function loadInstitutes() {
|
|
const data = await getInstitutesNeedingGeocoding()
|
|
setInstitutes(data)
|
|
}
|
|
|
|
// Geocode all institutes
|
|
async function geocodeAllInstitutes() {
|
|
setLoading(true)
|
|
setGeocodingProgress(0)
|
|
|
|
try {
|
|
const result = await batchGeocodeInstitutes(institutes.length, false)
|
|
|
|
if (result.success) {
|
|
setGeocodingProgress(100)
|
|
// Reload institutes to show updated coordinates
|
|
await loadInstitutes()
|
|
}
|
|
} catch (error) {
|
|
console.error('Batch geocoding failed:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
// Geocode single institute
|
|
async function geocodeSingleInstitute(instituteId: string) {
|
|
try {
|
|
const result = await geocodeInstitute(instituteId)
|
|
if (result.success) {
|
|
// Reload institutes to show updated coordinates
|
|
await loadInstitutes()
|
|
}
|
|
} catch (error) {
|
|
console.error('Single geocoding failed:', error)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="institute-geocoder">
|
|
<h2>Institute Geocoding</h2>
|
|
|
|
<div className="controls">
|
|
<button
|
|
onClick={geocodeAllInstitutes}
|
|
disabled={loading || institutes.length === 0}
|
|
>
|
|
{loading ? 'Geocoding...' : `Geocode All (${institutes.length})`}
|
|
</button>
|
|
|
|
{loading && (
|
|
<div className="progress">
|
|
<div
|
|
className="progress-bar"
|
|
style={{ width: `${geocodingProgress}%` }}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="institutes-list">
|
|
{institutes.map(institute => (
|
|
<div key={institute.id} className="institute-item">
|
|
<h3>{institute.name}</h3>
|
|
<p>
|
|
{institute.address.street && `${institute.address.street}, `}
|
|
{institute.address.town && `${institute.address.town}, `}
|
|
{institute.address.county && `${institute.address.county}, `}
|
|
{institute.address.postcode}
|
|
</p>
|
|
|
|
{institute.geo_coordinates ? (
|
|
<div className="coordinates">
|
|
<span>📍 {institute.geo_coordinates.latitude}, {institute.geo_coordinates.longitude}</span>
|
|
<span>Geocoded: {new Date(institute.geo_coordinates.geocoded_at).toLocaleDateString()}</span>
|
|
</div>
|
|
) : (
|
|
<button
|
|
onClick={() => geocodeSingleInstitute(institute.id)}
|
|
disabled={loading}
|
|
>
|
|
Geocode
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 6. Utility functions for working with coordinates
|
|
export class CoordinateUtils {
|
|
// Calculate distance between two points (Haversine formula)
|
|
static calculateDistance(
|
|
lat1: number,
|
|
lon1: number,
|
|
lat2: number,
|
|
lon2: number
|
|
): number {
|
|
const R = 6371 // Earth's radius in kilometers
|
|
const dLat = this.toRadians(lat2 - lat1)
|
|
const dLon = this.toRadians(lon2 - lon1)
|
|
|
|
const a =
|
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
|
|
Math.sin(dLon / 2) * Math.sin(dLon / 2)
|
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
|
return R * c
|
|
}
|
|
|
|
// Convert degrees to radians
|
|
private static toRadians(degrees: number): number {
|
|
return degrees * (Math.PI / 180)
|
|
}
|
|
|
|
// Check if coordinates are within a bounding box
|
|
static isWithinBounds(
|
|
lat: number,
|
|
lon: number,
|
|
bounds: [number, number, number, number] // [minLat, maxLat, minLon, maxLon]
|
|
): boolean {
|
|
return lat >= bounds[0] && lat <= bounds[1] &&
|
|
lon >= bounds[2] && lon <= bounds[3]
|
|
}
|
|
|
|
// Format coordinates for display
|
|
static formatCoordinates(lat: number, lon: number): string {
|
|
const latDir = lat >= 0 ? 'N' : 'S'
|
|
const lonDir = lon >= 0 ? 'E' : 'W'
|
|
return `${Math.abs(lat).toFixed(6)}°${latDir}, ${Math.abs(lon).toFixed(6)}°${lonDir}`
|
|
}
|
|
}
|
|
|
|
// 7. Example of using coordinates in Neo4j queries
|
|
export const neo4jQueries = {
|
|
// Create institute node with location
|
|
createInstituteWithLocation: `
|
|
CREATE (i:Institute {
|
|
id: $institute_id,
|
|
name: $name,
|
|
location: point({latitude: $latitude, longitude: $longitude})
|
|
})
|
|
RETURN i
|
|
`,
|
|
|
|
// Find institutes within radius
|
|
findInstitutesWithinRadius: `
|
|
MATCH (i:Institute)
|
|
WHERE distance(i.location, point({latitude: $centerLat, longitude: $centerLon})) < $radiusMeters
|
|
RETURN i, distance(i.location, point({latitude: $centerLat, longitude: $centerLon})) as distance
|
|
ORDER BY distance
|
|
`,
|
|
|
|
// Find institutes in bounding box
|
|
findInstitutesInBounds: `
|
|
MATCH (i:Institute)
|
|
WHERE i.location.latitude >= $minLat
|
|
AND i.location.latitude <= $maxLat
|
|
AND i.location.longitude >= $minLon
|
|
AND i.location.longitude <= $maxLon
|
|
RETURN i
|
|
`
|
|
}
|
|
|
|
export default {
|
|
geocodeInstitute,
|
|
batchGeocodeInstitutes,
|
|
getInstitutesNeedingGeocoding,
|
|
displayInstituteOnMap,
|
|
InstituteGeocoder,
|
|
CoordinateUtils,
|
|
neo4jQueries
|
|
}
|