{
 "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": 1,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.665852Z",
     "iopub.status.busy": "2026-03-04T19:10:58.665659Z",
     "iopub.status.idle": "2026-03-04T19:10:58.717471Z",
     "shell.execute_reply": "2026-03-04T19:10:58.717089Z"
    }
   },
   "outputs": [],
   "source": [
    "from datetime import datetime, timedelta, timezone\n",
    "\n",
    "import numpy as np\n",
    "\n",
    "import timedatamodel as tdm\n",
    "\n",
    "base = datetime(2024, 1, 15, tzinfo=timezone.utc)\n",
    "timestamps = [base + timedelta(hours=i) for i in range(24)]\n",
    "rng = 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": 2,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.719053Z",
     "iopub.status.busy": "2026-03-04T19:10:58.718940Z",
     "iopub.status.idle": "2026-03-04T19:10:58.721085Z",
     "shell.execute_reply": "2026-03-04T19:10:58.720762Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Oslo:    GeoLocation(latitude=59.91, longitude=10.75)\n",
      "Bergen:  GeoLocation(latitude=60.39, longitude=5.32)\n",
      "Tromsø:  GeoLocation(latitude=69.65, longitude=18.96)\n"
     ]
    }
   ],
   "source": [
    "oslo = tdm.GeoLocation(latitude=59.91, longitude=10.75)\n",
    "bergen = tdm.GeoLocation(latitude=60.39, longitude=5.32)\n",
    "tromsoe = tdm.GeoLocation(latitude=69.65, longitude=18.96)\n",
    "\n",
    "print(f\"Oslo:    {oslo}\")\n",
    "print(f\"Bergen:  {bergen}\")\n",
    "print(f\"Tromsø:  {tromsoe}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.722153Z",
     "iopub.status.busy": "2026-03-04T19:10:58.722090Z",
     "iopub.status.idle": "2026-03-04T19:10:58.723782Z",
     "shell.execute_reply": "2026-03-04T19:10:58.723471Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Validation error: latitude must be between -90 and 90, got 200\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    bad = tdm.GeoLocation(latitude=200, longitude=0)\n",
    "except 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": 4,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.724752Z",
     "iopub.status.busy": "2026-03-04T19:10:58.724686Z",
     "iopub.status.idle": "2026-03-04T19:10:58.726599Z",
     "shell.execute_reply": "2026-03-04T19:10:58.726163Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Oslo → Bergen: 305.1 km  (189.6 mi)\n",
      "Oslo → Tromsø: 1148.4 km\n"
     ]
    }
   ],
   "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": 5,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.727555Z",
     "iopub.status.busy": "2026-03-04T19:10:58.727501Z",
     "iopub.status.idle": "2026-03-04T19:10:58.728992Z",
     "shell.execute_reply": "2026-03-04T19:10:58.728720Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Initial bearing Oslo → Bergen: 282.4°\n",
      "Geographic midpoint: GeoLocation(latitude=60.17777046667162, longitude=8.05483245837952)\n"
     ]
    }
   ],
   "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": 6,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.729907Z",
     "iopub.status.busy": "2026-03-04T19:10:58.729828Z",
     "iopub.status.idle": "2026-03-04T19:10:58.731407Z",
     "shell.execute_reply": "2026-03-04T19:10:58.731128Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "100 km east of Oslo: GeoLocation(latitude=59.89782201387188, longitude=12.54332661133784)\n",
      "Verify distance: 100.0 km\n"
     ]
    }
   ],
   "source": [
    "east_of_oslo = oslo.offset(100, 90)  # 100 km due east\n",
    "print(f\"100 km east of Oslo: {east_of_oslo}\")\n",
    "print(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": 7,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.732322Z",
     "iopub.status.busy": "2026-03-04T19:10:58.732272Z",
     "iopub.status.idle": "2026-03-04T19:10:58.735141Z",
     "shell.execute_reply": "2026-03-04T19:10:58.734822Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<style>\n",
       ".ts-repr { font-family: monospace; font-size: 13px; max-width: 640px; display: inline-grid; }\n",
       ".ts-repr .ts-header {\n",
       "  font-weight: bold; font-size: 14px;\n",
       "  padding: 6px 10px; border-bottom: 2px solid #4a4a4a;\n",
       "  background: #f0f0f0; color: #1a1a1a;\n",
       "}\n",
       ".ts-repr .ts-meta { padding: 6px 10px; background: #fafafa; overflow: hidden; min-width: 0; }\n",
       ".ts-repr .ts-meta table { border-collapse: collapse; width: 100%; table-layout: fixed; }\n",
       ".ts-repr .ts-meta td { padding: 1px 8px 1px 0; white-space: nowrap; }\n",
       ".ts-repr .ts-meta td:first-child { color: #475569; font-weight: 600; width: 90px; }\n",
       ".ts-repr .ts-meta td:last-child { color: #1a1a1a; overflow: hidden; text-overflow: ellipsis; }\n",
       ".ts-repr .ts-data { padding: 6px 10px; }\n",
       ".ts-repr .ts-data table {\n",
       "  border-collapse: collapse; text-align: right;\n",
       "}\n",
       ".ts-repr .ts-data th {\n",
       "  text-align: right; padding: 3px 10px; border-bottom: 1px solid #ccc;\n",
       "  color: #555; font-weight: 600;\n",
       "}\n",
       ".ts-repr .ts-data th.ts-idx { text-align: left; }\n",
       ".ts-repr .ts-data td { padding: 2px 10px; }\n",
       ".ts-repr .ts-data tr:hover { background: #f5f5f5; }\n",
       ".ts-repr .ts-data td:first-child { text-align: left; color: #1e293b; }\n",
       ".ts-repr .ts-data td.ts-idx { text-align: left; color: #1e293b; }\n",
       ".ts-repr .ts-ellipsis { text-align: center !important; color: #999; }\n",
       "@media (prefers-color-scheme: dark) {\n",
       "  .ts-repr .ts-header { background: #1e293b; color: #e2e8f0; border-color: #475569; }\n",
       "  .ts-repr .ts-meta { background: #0f172a; }\n",
       "  .ts-repr .ts-meta td:first-child { color: #94a3b8; }\n",
       "  .ts-repr .ts-meta td:last-child { color: #e2e8f0; }\n",
       "  .ts-repr .ts-data th { color: #94a3b8; border-color: #334155; }\n",
       "  .ts-repr .ts-data td { color: #e2e8f0; }\n",
       "  .ts-repr .ts-data td:first-child { color: #cbd5e1; }\n",
       "  .ts-repr .ts-data td.ts-idx { color: #cbd5e1; }\n",
       "  .ts-repr .ts-data tr:hover { background: #1e293b; }\n",
       "  .ts-repr .ts-ellipsis { color: #64748b; }\n",
       "}\n",
       "</style>\n",
       "<div class=\"ts-repr\">\n",
       "<div class=\"ts-header\">TimeSeriesList</div>\n",
       "<div class=\"ts-meta\"><table>\n",
       "<tr><td>Name</td><td>temperature</td></tr>\n",
       "<tr><td>Length</td><td>24</td></tr>\n",
       "<tr><td>Frequency</td><td>PT1H</td></tr>\n",
       "<tr><td>Timezone</td><td>Europe/Oslo (+00:00)</td></tr>\n",
       "<tr><td>Unit</td><td>°C</td></tr>\n",
       "<tr><td>Data type</td><td>OBSERVATION</td></tr>\n",
       "<tr><td>Location</td><td>59.91°N, 10.75°E</td></tr>\n",
       "<tr><td>Description</td><td>Hourly temperature at Oslo Blindern</td></tr>\n",
       "</table></div>\n",
       "<div class=\"ts-data\"><table>\n",
       "<tr><th class=\"ts-idx\">timestamp</th><th>temperature</th></tr>\n",
       "<tr><td>2024-01-15 00:00</td><td>5.60943</td></tr>\n",
       "<tr><td>2024-01-15 01:00</td><td>2.92003</td></tr>\n",
       "<tr><td>2024-01-15 02:00</td><td>6.5009</td></tr>\n",
       "<tr><td class=\"ts-ellipsis\">&hellip;</td><td class=\"ts-ellipsis\">&hellip;</td></tr>\n",
       "<tr><td>2024-01-15 21:00</td><td>3.63814</td></tr>\n",
       "<tr><td>2024-01-15 22:00</td><td>7.44508</td></tr>\n",
       "<tr><td>2024-01-15 23:00</td><td>4.69094</td></tr>\n",
       "</table></div>\n",
       "</div>"
      ],
      "text/plain": [
       "TimeSeriesList\n",
       "┌─────────────────────────────────────────────────────────┐\n",
       "│  Name:             temperature                          │\n",
       "│  Length:           24                                   │\n",
       "│  Frequency:        PT1H                                 │\n",
       "│  Timezone:         Europe/Oslo (+00:00)                 │\n",
       "│  Unit:             °C                                   │\n",
       "│  Data type:        OBSERVATION                          │\n",
       "│  Location:         59.91°N, 10.75°E                     │\n",
       "│  Description:      Hourly temperature at Oslo Blindern  │\n",
       "├─────────────────────────────────────────────────────────┤\n",
       "│  2024-01-15 00:00  5.60943                              │\n",
       "│  2024-01-15 01:00  2.92003                              │\n",
       "│  2024-01-15 02:00   6.5009                              │\n",
       "│  ...                   ...                              │\n",
       "│  2024-01-15 21:00  3.63814                              │\n",
       "│  2024-01-15 22:00  7.44508                              │\n",
       "│  2024-01-15 23:00  4.69094                              │\n",
       "└─────────────────────────────────────────────────────────┘"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "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",
    ")\n",
    "ts_oslo"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.735995Z",
     "iopub.status.busy": "2026-03-04T19:10:58.735943Z",
     "iopub.status.idle": "2026-03-04T19:10:58.737382Z",
     "shell.execute_reply": "2026-03-04T19:10:58.737128Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Location: GeoLocation(latitude=59.91, longitude=10.75)\n",
      "Lat: 59.91, Lon: 10.75\n"
     ]
    }
   ],
   "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": 9,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.738199Z",
     "iopub.status.busy": "2026-03-04T19:10:58.738145Z",
     "iopub.status.idle": "2026-03-04T19:10:58.740407Z",
     "shell.execute_reply": "2026-03-04T19:10:58.740078Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<style>\n",
       ".ts-repr { font-family: monospace; font-size: 13px; max-width: 640px; display: inline-grid; }\n",
       ".ts-repr .ts-header {\n",
       "  font-weight: bold; font-size: 14px;\n",
       "  padding: 6px 10px; border-bottom: 2px solid #4a4a4a;\n",
       "  background: #f0f0f0; color: #1a1a1a;\n",
       "}\n",
       ".ts-repr .ts-meta { padding: 6px 10px; background: #fafafa; overflow: hidden; min-width: 0; }\n",
       ".ts-repr .ts-meta table { border-collapse: collapse; width: 100%; table-layout: fixed; }\n",
       ".ts-repr .ts-meta td { padding: 1px 8px 1px 0; white-space: nowrap; }\n",
       ".ts-repr .ts-meta td:first-child { color: #475569; font-weight: 600; width: 90px; }\n",
       ".ts-repr .ts-meta td:last-child { color: #1a1a1a; overflow: hidden; text-overflow: ellipsis; }\n",
       ".ts-repr .ts-data { padding: 6px 10px; }\n",
       ".ts-repr .ts-data table {\n",
       "  border-collapse: collapse; text-align: right;\n",
       "}\n",
       ".ts-repr .ts-data th {\n",
       "  text-align: right; padding: 3px 10px; border-bottom: 1px solid #ccc;\n",
       "  color: #555; font-weight: 600;\n",
       "}\n",
       ".ts-repr .ts-data th.ts-idx { text-align: left; }\n",
       ".ts-repr .ts-data td { padding: 2px 10px; }\n",
       ".ts-repr .ts-data tr:hover { background: #f5f5f5; }\n",
       ".ts-repr .ts-data td:first-child { text-align: left; color: #1e293b; }\n",
       ".ts-repr .ts-data td.ts-idx { text-align: left; color: #1e293b; }\n",
       ".ts-repr .ts-ellipsis { text-align: center !important; color: #999; }\n",
       "@media (prefers-color-scheme: dark) {\n",
       "  .ts-repr .ts-header { background: #1e293b; color: #e2e8f0; border-color: #475569; }\n",
       "  .ts-repr .ts-meta { background: #0f172a; }\n",
       "  .ts-repr .ts-meta td:first-child { color: #94a3b8; }\n",
       "  .ts-repr .ts-meta td:last-child { color: #e2e8f0; }\n",
       "  .ts-repr .ts-data th { color: #94a3b8; border-color: #334155; }\n",
       "  .ts-repr .ts-data td { color: #e2e8f0; }\n",
       "  .ts-repr .ts-data td:first-child { color: #cbd5e1; }\n",
       "  .ts-repr .ts-data td.ts-idx { color: #cbd5e1; }\n",
       "  .ts-repr .ts-data tr:hover { background: #1e293b; }\n",
       "  .ts-repr .ts-ellipsis { color: #64748b; }\n",
       "}\n",
       "</style>\n",
       "<div class=\"ts-repr\">\n",
       "<div class=\"ts-header\">TimeSeriesTable</div>\n",
       "<div class=\"ts-meta\"><table>\n",
       "<tr><td>Name</td><td>unnamed</td></tr>\n",
       "<tr><td>Columns</td><td>Oslo, Bergen, Tromsø</td></tr>\n",
       "<tr><td>Length</td><td>24 × 3</td></tr>\n",
       "<tr><td>Frequency</td><td>PT1H</td></tr>\n",
       "<tr><td>Timezone</td><td>Europe/Oslo (+00:00)</td></tr>\n",
       "<tr><td>Unit</td><td>°C, °C, °C</td></tr>\n",
       "<tr><td>Data type</td><td>OBSERVATION, OBSERVATION, OBSERVATION</td></tr>\n",
       "<tr><td>Location</td><td>59.91°N, 10.75°E, 60.39°N, 5.32°E, 69.65°N, 18.96°E</td></tr>\n",
       "</table></div>\n",
       "<div class=\"ts-data\"><table>\n",
       "<tr><th class=\"ts-idx\">timestamp</th><th>Oslo</th><th>Bergen</th><th>Tromsø</th></tr>\n",
       "<tr><td>2024-01-15 00:00</td><td>4.14334</td><td>4.03674</td><td>-11.6778</td></tr>\n",
       "<tr><td>2024-01-15 01:00</td><td>4.29573</td><td>2.20274</td><td>-6.01136</td></tr>\n",
       "<tr><td>2024-01-15 02:00</td><td>6.06462</td><td>2.86736</td><td>-7.4303</td></tr>\n",
       "<tr><td class=\"ts-ellipsis\">&hellip;</td><td class=\"ts-ellipsis\">&hellip;</td><td class=\"ts-ellipsis\">&hellip;</td><td class=\"ts-ellipsis\">&hellip;</td></tr>\n",
       "<tr><td>2024-01-15 21:00</td><td>5.43738</td><td>1.42609</td><td>-8.31887</td></tr>\n",
       "<tr><td>2024-01-15 22:00</td><td>6.74286</td><td>-1.82706</td><td>-14.7493</td></tr>\n",
       "<tr><td>2024-01-15 23:00</td><td>5.44719</td><td>-1.39986</td><td>-13.7884</td></tr>\n",
       "</table></div>\n",
       "</div>"
      ],
      "text/plain": [
       "TimeSeriesTable\n",
       "┌─────────────────────────────────────────────────────────────────────────┐\n",
       "│  Name:             unnamed                                              │\n",
       "│  Columns:          Oslo, Bergen, Tromsø                                 │\n",
       "│  Length:           24 × 3                                               │\n",
       "│  Frequency:        PT1H                                                 │\n",
       "│  Timezone:         Europe/Oslo (+00:00)                                 │\n",
       "│  Unit:             °C, °C, °C                                           │\n",
       "│  Data type:        OBSERVATION, OBSERVATION, OBSERVATION                │\n",
       "│  Location:         59.91°N, 10.75°E, 60.39°N, 5.32°E, 69.65°N, 18.96°E  │\n",
       "├─────────────────────────────────────────────────────────────────────────┤\n",
       "│                       Oslo    Bergen    Tromsø                          │\n",
       "│  2024-01-15 00:00  4.14334   4.03674  -11.6778                          │\n",
       "│  2024-01-15 01:00  4.29573   2.20274  -6.01136                          │\n",
       "│  2024-01-15 02:00  6.06462   2.86736   -7.4303                          │\n",
       "│  ...                   ...       ...       ...                          │\n",
       "│  2024-01-15 21:00  5.43738   1.42609  -8.31887                          │\n",
       "│  2024-01-15 22:00  6.74286  -1.82706  -14.7493                          │\n",
       "│  2024-01-15 23:00  5.44719  -1.39986  -13.7884                          │\n",
       "└─────────────────────────────────────────────────────────────────────────┘"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "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",
    ")\n",
    "table"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.741186Z",
     "iopub.status.busy": "2026-03-04T19:10:58.741135Z",
     "iopub.status.idle": "2026-03-04T19:10:58.742642Z",
     "shell.execute_reply": "2026-03-04T19:10:58.742394Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Oslo      GeoLocation(latitude=59.91, longitude=10.75)\n",
      "Bergen    GeoLocation(latitude=60.39, longitude=5.32)\n",
      "Tromsø    GeoLocation(latitude=69.65, longitude=18.96)\n"
     ]
    }
   ],
   "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": 11,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.743493Z",
     "iopub.status.busy": "2026-03-04T19:10:58.743418Z",
     "iopub.status.idle": "2026-03-04T19:10:58.745157Z",
     "shell.execute_reply": "2026-03-04T19:10:58.744905Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Pairwise distances (km):\n",
      "  Oslo     ↔ Tromsø     1148.4 km\n",
      "  Bergen   ↔ Oslo        305.1 km\n",
      "  Bergen   ↔ Tromsø     1206.5 km\n"
     ]
    }
   ],
   "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": 12,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.746150Z",
     "iopub.status.busy": "2026-03-04T19:10:58.746100Z",
     "iopub.status.idle": "2026-03-04T19:10:58.753212Z",
     "shell.execute_reply": "2026-03-04T19:10:58.752877Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Area name: Southern Norway\n",
      "Bounds:    (4.5, 57.5, 12.5, 63.0)\n",
      "Centroid:  GeoLocation(latitude=60.25, longitude=8.5)\n"
     ]
    }
   ],
   "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}\")\n",
    "except 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": 13,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.754162Z",
     "iopub.status.busy": "2026-03-04T19:10:58.754099Z",
     "iopub.status.idle": "2026-03-04T19:10:58.755934Z",
     "shell.execute_reply": "2026-03-04T19:10:58.755564Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Area: Oslo 50km\n",
      "Bounds: (9.853172269304775, 59.46033919704064, 11.646827730695227, 60.35966080295936)\n",
      "Centroid: GeoLocation(latitude=59.91, longitude=10.75)\n"
     ]
    }
   ],
   "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}\")\n",
    "except 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": 14,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.756764Z",
     "iopub.status.busy": "2026-03-04T19:10:58.756711Z",
     "iopub.status.idle": "2026-03-04T19:10:58.758603Z",
     "shell.execute_reply": "2026-03-04T19:10:58.758268Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Oslo     in Southern Norway? True  (is_within: True)\n",
      "Bergen   in Southern Norway? True  (is_within: True)\n",
      "Tromsø   in Southern Norway? False  (is_within: False)\n"
     ]
    }
   ],
   "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": 15,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.759433Z",
     "iopub.status.busy": "2026-03-04T19:10:58.759384Z",
     "iopub.status.idle": "2026-03-04T19:10:58.761049Z",
     "shell.execute_reply": "2026-03-04T19:10:58.760779Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Southern Norway contains Oslo 50km region? True\n",
      "Oslo 50km region overlaps Southern Norway?  True\n"
     ]
    }
   ],
   "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": 16,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.761933Z",
     "iopub.status.busy": "2026-03-04T19:10:58.761854Z",
     "iopub.status.idle": "2026-03-04T19:10:58.763535Z",
     "shell.execute_reply": "2026-03-04T19:10:58.763204Z"
    }
   },
   "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\n",
    "except 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": 17,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.764430Z",
     "iopub.status.busy": "2026-03-04T19:10:58.764379Z",
     "iopub.status.idle": "2026-03-04T19:10:58.766094Z",
     "shell.execute_reply": "2026-03-04T19:10:58.765766Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Southern Norway → Oslo (contained):  0.0 km\n",
      "Southern Norway → Tromsø (outside):  1151.7 km\n"
     ]
    }
   ],
   "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": 18,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:10:58.766936Z",
     "iopub.status.busy": "2026-03-04T19:10:58.766892Z",
     "iopub.status.idle": "2026-03-04T19:10:58.768478Z",
     "shell.execute_reply": "2026-03-04T19:10:58.768178Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Name:     temperature\n",
      "Location: GeoLocation(latitude=59.91, longitude=10.75)\n",
      "Match:    True\n"
     ]
    }
   ],
   "source": [
    "json_str = ts_oslo.to_json()\n",
    "ts_back = tdm.TimeSeriesList.from_json(json_str, location=oslo)\n",
    "\n",
    "print(f\"Name:     {ts_back.name}\")\n",
    "print(f\"Location: {ts_back.location}\")\n",
    "print(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": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.14.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
