Add POIs and journey times to map
This commit is contained in:
parent
7bfb1729bf
commit
500b9ef2aa
11 changed files with 914 additions and 177 deletions
|
|
@ -1,9 +1,5 @@
|
|||
"""POI (Points of Interest) API endpoint."""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["POLARS_UNKNOWN_EXTENSION_TYPE_BEHAVIOR"] = "load_as_storage"
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
|
@ -13,36 +9,190 @@ router = APIRouter()
|
|||
|
||||
DATA_FILE = Path("data_sources/uk_pois.parquet")
|
||||
|
||||
# Categories useful for property buyers
|
||||
POI_CATEGORIES = {
|
||||
"schools": [
|
||||
"elementary_school",
|
||||
"school",
|
||||
"high_school",
|
||||
"preschool",
|
||||
"college_university",
|
||||
"private_school",
|
||||
],
|
||||
"healthcare": [
|
||||
"doctor",
|
||||
"dentist",
|
||||
"pharmacy",
|
||||
"hospital",
|
||||
"public_health_clinic",
|
||||
],
|
||||
"transport": [
|
||||
"train_station",
|
||||
"bus_station",
|
||||
"metro_station",
|
||||
"light_rail_and_subway_stations",
|
||||
],
|
||||
"parks": ["park", "national_park", "dog_park"],
|
||||
"emergency": ["police_department", "fire_department"],
|
||||
"supermarkets": ["supermarket", "grocery_store", "convenience_store"],
|
||||
# Category groups with emoji and member categories
|
||||
POI_CATEGORY_GROUPS: dict[str, dict] = {
|
||||
"schools": {
|
||||
"emoji": "🏫",
|
||||
"label": "Schools",
|
||||
"categories": ["school", "preschool", "college_university", "library"],
|
||||
},
|
||||
"healthcare": {
|
||||
"emoji": "🏥",
|
||||
"label": "Healthcare",
|
||||
"categories": [
|
||||
"doctor",
|
||||
"dentist",
|
||||
"pharmacy",
|
||||
"hospital",
|
||||
"public_health_clinic",
|
||||
"veterinary",
|
||||
"nursing_home",
|
||||
"social_facility",
|
||||
],
|
||||
},
|
||||
"transport": {
|
||||
"emoji": "🚉",
|
||||
"label": "Transport",
|
||||
"categories": [
|
||||
"train_station",
|
||||
"bus_station",
|
||||
"bus_stop",
|
||||
"metro_station",
|
||||
"light_rail_station",
|
||||
"tram_stop",
|
||||
"ferry_terminal",
|
||||
"airport",
|
||||
],
|
||||
},
|
||||
"parks": {
|
||||
"emoji": "🌳",
|
||||
"label": "Parks & Leisure",
|
||||
"categories": [
|
||||
"park",
|
||||
"national_park",
|
||||
"nature_reserve",
|
||||
"dog_park",
|
||||
"playground",
|
||||
"garden",
|
||||
"sports_centre",
|
||||
"swimming_pool",
|
||||
"gym",
|
||||
"golf_course",
|
||||
"marina",
|
||||
],
|
||||
},
|
||||
"emergency": {
|
||||
"emoji": "🚨",
|
||||
"label": "Emergency",
|
||||
"categories": ["police_department", "fire_department"],
|
||||
},
|
||||
"supermarkets": {
|
||||
"emoji": "🛒",
|
||||
"label": "Supermarkets & Grocery",
|
||||
"categories": [
|
||||
"supermarket",
|
||||
"grocery_store",
|
||||
"convenience_store",
|
||||
"bakery",
|
||||
"butcher",
|
||||
"greengrocer",
|
||||
"deli",
|
||||
],
|
||||
},
|
||||
"shopping": {
|
||||
"emoji": "🛍️",
|
||||
"label": "Shopping",
|
||||
"categories": [
|
||||
"department_store",
|
||||
"clothing_store",
|
||||
"shoe_store",
|
||||
"electronics_store",
|
||||
"hardware_store",
|
||||
"furniture_store",
|
||||
"bookshop",
|
||||
"newsagent",
|
||||
"charity_shop",
|
||||
"shopping_centre",
|
||||
"optician",
|
||||
"off_licence",
|
||||
],
|
||||
},
|
||||
"food_drink": {
|
||||
"emoji": "🍽️",
|
||||
"label": "Food & Drink",
|
||||
"categories": [
|
||||
"restaurant",
|
||||
"cafe",
|
||||
"pub",
|
||||
"bar",
|
||||
"fast_food",
|
||||
"food_court",
|
||||
"ice_cream",
|
||||
"beer_garden",
|
||||
],
|
||||
},
|
||||
"personal_care": {
|
||||
"emoji": "💇",
|
||||
"label": "Personal Care",
|
||||
"categories": [
|
||||
"hairdresser",
|
||||
"beauty_salon",
|
||||
"laundry",
|
||||
"dry_cleaning",
|
||||
],
|
||||
},
|
||||
"finance": {
|
||||
"emoji": "🏦",
|
||||
"label": "Finance",
|
||||
"categories": ["bank", "atm", "bureau_de_change"],
|
||||
},
|
||||
"entertainment": {
|
||||
"emoji": "🎭",
|
||||
"label": "Entertainment & Culture",
|
||||
"categories": [
|
||||
"cinema",
|
||||
"theatre",
|
||||
"nightclub",
|
||||
"community_centre",
|
||||
"arts_centre",
|
||||
"museum",
|
||||
"gallery",
|
||||
"attraction",
|
||||
"zoo",
|
||||
"theme_park",
|
||||
"viewpoint",
|
||||
],
|
||||
},
|
||||
"accommodation": {
|
||||
"emoji": "🏨",
|
||||
"label": "Accommodation",
|
||||
"categories": [
|
||||
"hotel",
|
||||
"hostel",
|
||||
"guest_house",
|
||||
"campsite",
|
||||
"caravan_site",
|
||||
],
|
||||
},
|
||||
"religion": {
|
||||
"emoji": "🛐",
|
||||
"label": "Places of Worship",
|
||||
"categories": ["place_of_worship"],
|
||||
},
|
||||
"government": {
|
||||
"emoji": "🏛️",
|
||||
"label": "Government & Public",
|
||||
"categories": [
|
||||
"town_hall",
|
||||
"courthouse",
|
||||
"post_office",
|
||||
"prison",
|
||||
"public_toilets",
|
||||
],
|
||||
},
|
||||
"automotive": {
|
||||
"emoji": "⛽",
|
||||
"label": "Automotive",
|
||||
"categories": [
|
||||
"petrol_station",
|
||||
"ev_charging",
|
||||
"car_dealer",
|
||||
"car_repair",
|
||||
"parking",
|
||||
"bicycle_parking",
|
||||
],
|
||||
},
|
||||
"recycling": {
|
||||
"emoji": "♻️",
|
||||
"label": "Recycling & Waste",
|
||||
"categories": ["recycling", "waste_disposal"],
|
||||
},
|
||||
}
|
||||
|
||||
# Flatten for quick lookup
|
||||
ALL_CATEGORIES = {cat for cats in POI_CATEGORIES.values() for cat in cats}
|
||||
ALL_CATEGORIES = {
|
||||
cat for group in POI_CATEGORY_GROUPS.values() for cat in group["categories"]
|
||||
}
|
||||
|
||||
# Cache the dataframe
|
||||
_df_cache: pl.DataFrame | None = None
|
||||
|
|
@ -55,14 +205,9 @@ def get_df() -> pl.DataFrame | None:
|
|||
if not DATA_FILE.exists():
|
||||
return None
|
||||
df = pl.read_parquet(DATA_FILE)
|
||||
# Extract fields we need and filter to relevant categories
|
||||
_df_cache = df.select(
|
||||
pl.col("id"),
|
||||
pl.col("names").struct.field("primary").alias("name"),
|
||||
pl.col("categories").struct.field("primary").alias("category"),
|
||||
pl.col("bbox").struct.field("xmin").alias("lng"),
|
||||
pl.col("bbox").struct.field("ymin").alias("lat"),
|
||||
).filter(pl.col("category").is_in(ALL_CATEGORIES))
|
||||
_df_cache = df.select("id", "name", "category", "lat", "lng").filter(
|
||||
pl.col("category").is_in(ALL_CATEGORIES)
|
||||
)
|
||||
return _df_cache
|
||||
|
||||
|
||||
|
|
@ -83,23 +228,20 @@ async def get_pois(
|
|||
if df is None:
|
||||
return {"features": []}
|
||||
|
||||
# Parse bounds
|
||||
try:
|
||||
south, west, north, east = map(float, bounds.split(","))
|
||||
except ValueError:
|
||||
return {"features": []}
|
||||
|
||||
# Get categories to include
|
||||
requested_groups = [g.strip() for g in categories.split(",")]
|
||||
cats_to_include = set()
|
||||
for group in requested_groups:
|
||||
if group in POI_CATEGORIES:
|
||||
cats_to_include.update(POI_CATEGORIES[group])
|
||||
if group in POI_CATEGORY_GROUPS:
|
||||
cats_to_include.update(POI_CATEGORY_GROUPS[group]["categories"])
|
||||
|
||||
if not cats_to_include:
|
||||
return {"features": []}
|
||||
|
||||
# Filter by bounds and categories
|
||||
filtered = df.filter(
|
||||
(pl.col("lat") >= south)
|
||||
& (pl.col("lat") <= north)
|
||||
|
|
@ -108,7 +250,6 @@ async def get_pois(
|
|||
& (pl.col("category").is_in(cats_to_include))
|
||||
)
|
||||
|
||||
# Limit results to avoid overwhelming the frontend
|
||||
MAX_POIS = 5000
|
||||
if len(filtered) > MAX_POIS:
|
||||
filtered = filtered.sample(n=MAX_POIS, seed=42)
|
||||
|
|
@ -118,5 +259,10 @@ async def get_pois(
|
|||
|
||||
@router.get("/poi-categories")
|
||||
async def get_poi_categories() -> dict:
|
||||
"""Get available POI category groups."""
|
||||
return {"categories": list(POI_CATEGORIES.keys())}
|
||||
"""Get available POI category groups with emoji and labels."""
|
||||
return {
|
||||
"categories": {
|
||||
key: {"emoji": group["emoji"], "label": group["label"]}
|
||||
for key, group in POI_CATEGORY_GROUPS.items()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue