{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Geographical Support\n",
    "\n",
    "Time series data often originates from physical locations — weather stations, wind farms, grid nodes.  \n",
    "TimeDataModel provides first-class geographical primitives so that **location and data travel together**.\n",
    "\n",
    "| Class | Purpose |\n",
    "| --- | --- |\n",
    "| `GeoLocation` | A validated (latitude, longitude) point |\n",
    "| `GeoArea` | A shapely `Polygon` region (optional `shapely` dependency) |\n",
    "\n",
    "Both can be attached to `TimeSeriesList` and `TimeSeriesTable` objects."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "from datetime import datetime, timedelta, timezone\n\nimport numpy as np\n\nimport timedatamodel as tdm\n\nbase = datetime(2024, 1, 15, tzinfo=timezone.utc)\ntimestamps = [base + timedelta(hours=i) for i in range(24)]\nrng = np.random.default_rng(42)"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## GeoLocation — point coordinates\n",
    "\n",
    "`GeoLocation` is a frozen dataclass that validates latitude ∈ [−90, 90] and longitude ∈ [−180, 180]."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "oslo = tdm.GeoLocation(latitude=59.91, longitude=10.75)\nbergen = tdm.GeoLocation(latitude=60.39, longitude=5.32)\ntromsoe = tdm.GeoLocation(latitude=69.65, longitude=18.96)\n\nprint(f\"Oslo:    {oslo}\")\nprint(f\"Bergen:  {bergen}\")\nprint(f\"Tromsø:  {tromsoe}\")"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "try:\n    bad = tdm.GeoLocation(latitude=200, longitude=0)\nexcept ValueError as e:\n    print(f\"Validation error: {e}\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Distance, bearing, and midpoint\n",
    "\n",
    "`GeoLocation` provides Haversine-based spatial methods — no external dependency required."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "d_km = oslo.distance_to(bergen)\n",
    "d_mi = oslo.distance_to(bergen, unit=\"mi\")\n",
    "print(f\"Oslo → Bergen: {d_km:.1f} km  ({d_mi:.1f} mi)\")\n",
    "\n",
    "d_tromsoe = oslo.distance_to(tromsoe)\n",
    "print(f\"Oslo → Tromsø: {d_tromsoe:.1f} km\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bearing = oslo.bearing_to(bergen)\n",
    "print(f\"Initial bearing Oslo → Bergen: {bearing:.1f}°\")\n",
    "\n",
    "mid = oslo.midpoint(bergen)\n",
    "print(f\"Geographic midpoint: {mid}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Offset — displace a point\n",
    "\n",
    "`offset(distance_km, bearing_deg)` produces a new `GeoLocation` displaced from the original."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "east_of_oslo = oslo.offset(100, 90)  # 100 km due east\nprint(f\"100 km east of Oslo: {east_of_oslo}\")\nprint(f\"Verify distance: {oslo.distance_to(east_of_oslo):.1f} km\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Attaching location to a TimeSeriesList\n",
    "\n",
    "Pass a `GeoLocation` via the `location` parameter. It shows up in the rich repr."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "ts_oslo = tdm.TimeSeriesList(\n    tdm.Frequency.PT1H,\n    timezone=\"Europe/Oslo\",\n    timestamps=timestamps,\n    values=(5.0 + rng.normal(0, 2, 24)).tolist(),\n    name=\"temperature\",\n    unit=\"°C\",\n    description=\"Hourly temperature at Oslo Blindern\",\n    data_type=tdm.DataType.OBSERVATION,\n    location=oslo,\n)\nts_oslo"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f\"Location: {ts_oslo.location}\")\n",
    "print(f\"Lat: {ts_oslo.location.latitude}, Lon: {ts_oslo.location.longitude}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Per-column locations in a TimeSeriesTable\n",
    "\n",
    "When columns represent different physical locations, attach one `GeoLocation` per column."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "table = tdm.TimeSeriesTable(\n    tdm.Frequency.PT1H,\n    timezone=\"Europe/Oslo\",\n    timestamps=timestamps,\n    values=np.column_stack([\n        5.0 + rng.normal(0, 2, 24),\n        2.0 + rng.normal(0, 3, 24),\n        -8.0 + rng.normal(0, 4, 24),\n    ]),\n    names=[\"Oslo\", \"Bergen\", \"Tromsø\"],\n    units=[\"°C\", \"°C\", \"°C\"],\n    locations=[oslo, bergen, tromsoe],\n    data_types=[tdm.DataType.OBSERVATION],\n)\ntable"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for i, name in enumerate(table.column_names):\n",
    "    loc = table.locations[i] if len(table.locations) > 1 else table.locations[0]\n",
    "    print(f\"{name:8s}  {loc}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Computing distances between stations"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stations = {\"Oslo\": oslo, \"Bergen\": bergen, \"Tromsø\": tromsoe}\n",
    "\n",
    "print(\"Pairwise distances (km):\")\n",
    "for a_name, a_loc in stations.items():\n",
    "    for b_name, b_loc in stations.items():\n",
    "        if a_name < b_name:\n",
    "            print(f\"  {a_name:8s} ↔ {b_name:8s}  {a_loc.distance_to(b_loc):7.1f} km\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## GeoArea — polygon regions\n",
    "\n",
    "`GeoArea` wraps a shapely `Polygon` for representing bidding zones, countries, or catchment areas.  \n",
    "It requires the optional `shapely` dependency:\n",
    "\n",
    "```bash\n",
    "pip install timedatamodel[geo]\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "try:\n    southern_norway = tdm.GeoArea.from_coordinates(\n        [\n            (57.5, 4.5),\n            (57.5, 12.5),\n            (63.0, 12.5),\n            (63.0, 4.5),\n            (57.5, 4.5),\n        ],\n        name=\"Southern Norway\",\n    )\n\n    print(f\"Area name: {southern_norway.name}\")\n    print(f\"Bounds:    {southern_norway.bounds}\")\n    print(f\"Centroid:  {southern_norway.centroid}\")\nexcept ImportError:\n    southern_norway = None\n    print(\"shapely not installed — skip this cell or: pip install timedatamodel[geo]\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Bounding box — quick rectangular areas\n",
    "\n",
    "`GeoArea.bounding_box(center, radius_km)` creates a rectangular area around a point."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "try:\n    oslo_region = tdm.GeoArea.bounding_box(oslo, radius_km=50, name=\"Oslo 50km\")\n    print(f\"Area: {oslo_region.name}\")\n    print(f\"Bounds: {oslo_region.bounds}\")\n    print(f\"Centroid: {oslo_region.centroid}\")\nexcept ImportError:\n    oslo_region = None\n    print(\"shapely not installed — skipping\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Spatial queries\n",
    "\n",
    "`GeoArea` supports `contains_point`, `contains_area`, and `overlaps`.  \n",
    "`GeoLocation` has a matching `is_within(area)` method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    if southern_norway is not None:\n",
    "        for name, loc in stations.items():\n",
    "            inside = southern_norway.contains_point(loc)\n",
    "            also = loc.is_within(southern_norway)\n",
    "            print(f\"{name:8s} in Southern Norway? {inside}  (is_within: {also})\")\n",
    "except ImportError:\n",
    "    print(\"shapely not installed — skipping\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    if southern_norway is not None and oslo_region is not None:\n",
    "        print(f\"Southern Norway contains Oslo 50km region? {southern_norway.contains_area(oslo_region)}\")\n",
    "        print(f\"Oslo 50km region overlaps Southern Norway?  {oslo_region.overlaps(southern_norway)}\")\n",
    "except ImportError:\n",
    "    print(\"shapely not installed — skipping\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Attaching a GeoArea to a TimeSeriesList\n",
    "\n",
    "Both `GeoLocation` and `GeoArea` are accepted by the `location` parameter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "try:\n    if southern_norway is not None:\n        ts_area = tdm.TimeSeriesList(\n            tdm.Frequency.PT1H,\n            timestamps=timestamps,\n            values=(120 + rng.normal(0, 15, 24)).tolist(),\n            name=\"wind_south\",\n            unit=\"MW\",\n            description=\"Aggregated wind production in southern Norway\",\n            location=southern_norway,\n        )\n        ts_area\nexcept ImportError:\n    print(\"shapely not installed — skipping\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Distance between areas and points\n",
    "\n",
    "`GeoArea.distance_to()` accepts both `GeoLocation` and `GeoArea`.  \n",
    "It returns 0.0 if the point is contained (or areas overlap), otherwise uses centroid-based Haversine."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    if southern_norway is not None:\n",
    "        print(f\"Southern Norway → Oslo (contained):  {southern_norway.distance_to(oslo):.1f} km\")\n",
    "        print(f\"Southern Norway → Tromsø (outside):  {southern_norway.distance_to(tromsoe):.1f} km\")\n",
    "except ImportError:\n",
    "    print(\"shapely not installed — skipping\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Location survives serialization\n",
    "\n",
    "`GeoLocation` round-trips through JSON.  \n",
    "(GeoArea requires re-attaching after deserialization.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "json_str = ts_oslo.to_json()\nts_back = tdm.TimeSeriesList.from_json(json_str, location=oslo)\n\nprint(f\"Name:     {ts_back.name}\")\nprint(f\"Location: {ts_back.location}\")\nprint(f\"Match:    {ts_back.location == oslo}\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "| Feature | API |\n",
    "| --- | --- |\n",
    "| Point coordinates | `GeoLocation(lat, lon)` — validated, frozen |\n",
    "| Distance | `loc.distance_to(other, unit=\"km\")` — Haversine |\n",
    "| Bearing | `loc.bearing_to(other)` — initial bearing in degrees |\n",
    "| Midpoint | `loc.midpoint(other)` — great-circle midpoint |\n",
    "| Offset | `loc.offset(distance_km, bearing_deg)` — displace a point |\n",
    "| Polygon regions | `GeoArea.from_coordinates(coords)` — requires shapely |\n",
    "| Quick box | `GeoArea.bounding_box(center, radius_km)` |\n",
    "| Spatial queries | `area.contains_point()`, `area.overlaps()`, `loc.is_within()` |\n",
    "| Attach to series | `TimeSeriesList(..., location=loc)`, `TimeSeriesTable(..., locations=[...])` |\n",
    "\n",
    "Next up: **nb_10** demonstrates hierarchical time series — organising series into tree structures with bottom-up aggregation."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}