{
 "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 `TimeSeries` and `TimeSeriesTable` objects."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:34.427996Z",
     "iopub.status.busy": "2026-03-01T13:36:34.427846Z",
     "iopub.status.idle": "2026-03-01T13:36:34.511451Z",
     "shell.execute_reply": "2026-03-01T13:36:34.511108Z"
    }
   },
   "outputs": [
    {
     "ename": "ModuleNotFoundError",
     "evalue": "No module named 'timedatamodel'",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mModuleNotFoundError\u001b[39m                       Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 5\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mdatetime\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m datetime, timedelta, timezone\n\u001b[32m      3\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mnumpy\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mnp\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m5\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mtimedatamodel\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mtdm\u001b[39;00m\n\u001b[32m      7\u001b[39m base = datetime(\u001b[32m2024\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m15\u001b[39m, tzinfo=timezone.utc)\n\u001b[32m      8\u001b[39m timestamps = [base + timedelta(hours=i) \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(\u001b[32m24\u001b[39m)]\n",
      "\u001b[31mModuleNotFoundError\u001b[39m: No module named 'timedatamodel'"
     ]
    }
   ],
   "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-01T13:36:34.512609Z",
     "iopub.status.busy": "2026-03-01T13:36:34.512534Z",
     "iopub.status.idle": "2026-03-01T13:36:34.519599Z",
     "shell.execute_reply": "2026-03-01T13:36:34.519231Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m oslo = \u001b[43mtdm\u001b[49m.GeoLocation(latitude=\u001b[32m59.91\u001b[39m, longitude=\u001b[32m10.75\u001b[39m)\n\u001b[32m      2\u001b[39m bergen = tdm.GeoLocation(latitude=\u001b[32m60.39\u001b[39m, longitude=\u001b[32m5.32\u001b[39m)\n\u001b[32m      3\u001b[39m tromsoe = tdm.GeoLocation(latitude=\u001b[32m69.65\u001b[39m, longitude=\u001b[32m18.96\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "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-01T13:36:34.520494Z",
     "iopub.status.busy": "2026-03-01T13:36:34.520432Z",
     "iopub.status.idle": "2026-03-01T13:36:34.526810Z",
     "shell.execute_reply": "2026-03-01T13:36:34.526499Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m     bad = \u001b[43mtdm\u001b[49m.GeoLocation(latitude=\u001b[32m200\u001b[39m, longitude=\u001b[32m0\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m      4\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mValidation error: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00me\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "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-01T13:36:34.527785Z",
     "iopub.status.busy": "2026-03-01T13:36:34.527721Z",
     "iopub.status.idle": "2026-03-01T13:36:34.534367Z",
     "shell.execute_reply": "2026-03-01T13:36:34.534076Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'oslo' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m d_km = \u001b[43moslo\u001b[49m.distance_to(bergen)\n\u001b[32m      2\u001b[39m d_mi = oslo.distance_to(bergen, unit=\u001b[33m\"\u001b[39m\u001b[33mmi\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mOslo → Bergen: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00md_km\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.1f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m km  (\u001b[39m\u001b[38;5;132;01m{\u001b[39;00md_mi\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.1f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m mi)\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'oslo' is not defined"
     ]
    }
   ],
   "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-01T13:36:34.535366Z",
     "iopub.status.busy": "2026-03-01T13:36:34.535313Z",
     "iopub.status.idle": "2026-03-01T13:36:34.541734Z",
     "shell.execute_reply": "2026-03-01T13:36:34.541347Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'oslo' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[5]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m bearing = \u001b[43moslo\u001b[49m.bearing_to(bergen)\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mInitial bearing Oslo → Bergen: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mbearing\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.1f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m°\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      4\u001b[39m mid = oslo.midpoint(bergen)\n",
      "\u001b[31mNameError\u001b[39m: name 'oslo' is not defined"
     ]
    }
   ],
   "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-01T13:36:34.542637Z",
     "iopub.status.busy": "2026-03-01T13:36:34.542573Z",
     "iopub.status.idle": "2026-03-01T13:36:34.548788Z",
     "shell.execute_reply": "2026-03-01T13:36:34.548530Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'oslo' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[6]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m east_of_oslo = \u001b[43moslo\u001b[49m.offset(\u001b[32m100\u001b[39m, \u001b[32m90\u001b[39m)  \u001b[38;5;66;03m# 100 km due east\u001b[39;00m\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m100 km east of Oslo: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00meast_of_oslo\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mVerify distance: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00moslo.distance_to(east_of_oslo)\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.1f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m km\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'oslo' is not defined"
     ]
    }
   ],
   "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 TimeSeries\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-01T13:36:34.549812Z",
     "iopub.status.busy": "2026-03-01T13:36:34.549755Z",
     "iopub.status.idle": "2026-03-01T13:36:34.556856Z",
     "shell.execute_reply": "2026-03-01T13:36:34.556529Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m ts_oslo = \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m      2\u001b[39m     tdm.Frequency.PT1H,\n\u001b[32m      3\u001b[39m     timezone=\u001b[33m\"\u001b[39m\u001b[33mEurope/Oslo\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      4\u001b[39m     timestamps=timestamps,\n\u001b[32m      5\u001b[39m     values=(\u001b[32m5.0\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m2\u001b[39m, \u001b[32m24\u001b[39m)).tolist(),\n\u001b[32m      6\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mtemperature\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      7\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33m°C\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      8\u001b[39m     description=\u001b[33m\"\u001b[39m\u001b[33mHourly temperature at Oslo Blindern\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      9\u001b[39m     data_type=tdm.DataType.MEASUREMENT,\n\u001b[32m     10\u001b[39m     location=oslo,\n\u001b[32m     11\u001b[39m )\n\u001b[32m     12\u001b[39m ts_oslo\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "ts_oslo = tdm.TimeSeries(\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.MEASUREMENT,\n",
    "    location=oslo,\n",
    ")\n",
    "ts_oslo"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:34.557947Z",
     "iopub.status.busy": "2026-03-01T13:36:34.557890Z",
     "iopub.status.idle": "2026-03-01T13:36:34.570114Z",
     "shell.execute_reply": "2026-03-01T13:36:34.569735Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'ts_oslo' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mLocation: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[43mts_oslo\u001b[49m.location\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mLat: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mts_oslo.location.latitude\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m, Lon: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mts_oslo.location.longitude\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'ts_oslo' is not defined"
     ]
    }
   ],
   "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-01T13:36:34.571214Z",
     "iopub.status.busy": "2026-03-01T13:36:34.571140Z",
     "iopub.status.idle": "2026-03-01T13:36:34.578853Z",
     "shell.execute_reply": "2026-03-01T13:36:34.578500Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[9]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m table = \u001b[43mtdm\u001b[49m.TimeSeriesTable(\n\u001b[32m      2\u001b[39m     tdm.Frequency.PT1H,\n\u001b[32m      3\u001b[39m     timezone=\u001b[33m\"\u001b[39m\u001b[33mEurope/Oslo\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      4\u001b[39m     timestamps=timestamps,\n\u001b[32m      5\u001b[39m     values=np.column_stack([\n\u001b[32m      6\u001b[39m         \u001b[32m5.0\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m2\u001b[39m, \u001b[32m24\u001b[39m),\n\u001b[32m      7\u001b[39m         \u001b[32m2.0\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m3\u001b[39m, \u001b[32m24\u001b[39m),\n\u001b[32m      8\u001b[39m         -\u001b[32m8.0\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m4\u001b[39m, \u001b[32m24\u001b[39m),\n\u001b[32m      9\u001b[39m     ]),\n\u001b[32m     10\u001b[39m     names=[\u001b[33m\"\u001b[39m\u001b[33mOslo\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mBergen\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mTromsø\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m     11\u001b[39m     units=[\u001b[33m\"\u001b[39m\u001b[33m°C\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33m°C\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33m°C\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m     12\u001b[39m     locations=[oslo, bergen, tromsoe],\n\u001b[32m     13\u001b[39m     data_types=[tdm.DataType.MEASUREMENT],\n\u001b[32m     14\u001b[39m )\n\u001b[32m     15\u001b[39m table\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "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.MEASUREMENT],\n",
    ")\n",
    "table"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:34.579869Z",
     "iopub.status.busy": "2026-03-01T13:36:34.579788Z",
     "iopub.status.idle": "2026-03-01T13:36:34.586540Z",
     "shell.execute_reply": "2026-03-01T13:36:34.586147Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'table' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[10]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m i, name \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28menumerate\u001b[39m(\u001b[43mtable\u001b[49m.column_names):\n\u001b[32m      2\u001b[39m     loc = table.locations[i] \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(table.locations) > \u001b[32m1\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m table.locations[\u001b[32m0\u001b[39m]\n\u001b[32m      3\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m8s\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mloc\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'table' is not defined"
     ]
    }
   ],
   "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-01T13:36:34.587488Z",
     "iopub.status.busy": "2026-03-01T13:36:34.587437Z",
     "iopub.status.idle": "2026-03-01T13:36:34.594403Z",
     "shell.execute_reply": "2026-03-01T13:36:34.594046Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'oslo' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[11]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m stations = {\u001b[33m\"\u001b[39m\u001b[33mOslo\u001b[39m\u001b[33m\"\u001b[39m: \u001b[43moslo\u001b[49m, \u001b[33m\"\u001b[39m\u001b[33mBergen\u001b[39m\u001b[33m\"\u001b[39m: bergen, \u001b[33m\"\u001b[39m\u001b[33mTromsø\u001b[39m\u001b[33m\"\u001b[39m: tromsoe}\n\u001b[32m      3\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33m\"\u001b[39m\u001b[33mPairwise distances (km):\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      4\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m a_name, a_loc \u001b[38;5;129;01min\u001b[39;00m stations.items():\n",
      "\u001b[31mNameError\u001b[39m: name 'oslo' is not defined"
     ]
    }
   ],
   "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-01T13:36:34.595319Z",
     "iopub.status.busy": "2026-03-01T13:36:34.595269Z",
     "iopub.status.idle": "2026-03-01T13:36:34.602776Z",
     "shell.execute_reply": "2026-03-01T13:36:34.602409Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[12]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m     southern_norway = \u001b[43mtdm\u001b[49m.GeoArea.from_coordinates(\n\u001b[32m      3\u001b[39m         [\n\u001b[32m      4\u001b[39m             (\u001b[32m57.5\u001b[39m, \u001b[32m4.5\u001b[39m),\n\u001b[32m      5\u001b[39m             (\u001b[32m57.5\u001b[39m, \u001b[32m12.5\u001b[39m),\n\u001b[32m      6\u001b[39m             (\u001b[32m63.0\u001b[39m, \u001b[32m12.5\u001b[39m),\n\u001b[32m      7\u001b[39m             (\u001b[32m63.0\u001b[39m, \u001b[32m4.5\u001b[39m),\n\u001b[32m      8\u001b[39m             (\u001b[32m57.5\u001b[39m, \u001b[32m4.5\u001b[39m),\n\u001b[32m      9\u001b[39m         ],\n\u001b[32m     10\u001b[39m         name=\u001b[33m\"\u001b[39m\u001b[33mSouthern Norway\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     11\u001b[39m     )\n\u001b[32m     13\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mArea name: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00msouthern_norway.name\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m     14\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mBounds:    \u001b[39m\u001b[38;5;132;01m{\u001b[39;00msouthern_norway.bounds\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "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-01T13:36:34.603751Z",
     "iopub.status.busy": "2026-03-01T13:36:34.603692Z",
     "iopub.status.idle": "2026-03-01T13:36:34.610707Z",
     "shell.execute_reply": "2026-03-01T13:36:34.610239Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[13]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m     oslo_region = \u001b[43mtdm\u001b[49m.GeoArea.bounding_box(oslo, radius_km=\u001b[32m50\u001b[39m, name=\u001b[33m\"\u001b[39m\u001b[33mOslo 50km\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mArea: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00moslo_region.name\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      4\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mBounds: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00moslo_region.bounds\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "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-01T13:36:34.611623Z",
     "iopub.status.busy": "2026-03-01T13:36:34.611561Z",
     "iopub.status.idle": "2026-03-01T13:36:34.618832Z",
     "shell.execute_reply": "2026-03-01T13:36:34.618468Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'southern_norway' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[14]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m     \u001b[38;5;28;01mif\u001b[39;00m \u001b[43msouthern_norway\u001b[49m \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m      3\u001b[39m         \u001b[38;5;28;01mfor\u001b[39;00m name, loc \u001b[38;5;129;01min\u001b[39;00m stations.items():\n\u001b[32m      4\u001b[39m             inside = southern_norway.contains_point(loc)\n",
      "\u001b[31mNameError\u001b[39m: name 'southern_norway' is not defined"
     ]
    }
   ],
   "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-01T13:36:34.619700Z",
     "iopub.status.busy": "2026-03-01T13:36:34.619648Z",
     "iopub.status.idle": "2026-03-01T13:36:34.626313Z",
     "shell.execute_reply": "2026-03-01T13:36:34.626007Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'southern_norway' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[15]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m     \u001b[38;5;28;01mif\u001b[39;00m \u001b[43msouthern_norway\u001b[49m \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mand\u001b[39;00m oslo_region \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m      3\u001b[39m         \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mSouthern Norway contains Oslo 50km region? \u001b[39m\u001b[38;5;132;01m{\u001b[39;00msouthern_norway.contains_area(oslo_region)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      4\u001b[39m         \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mOslo 50km region overlaps Southern Norway?  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00moslo_region.overlaps(southern_norway)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'southern_norway' is not defined"
     ]
    }
   ],
   "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 TimeSeries\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-01T13:36:34.627417Z",
     "iopub.status.busy": "2026-03-01T13:36:34.627363Z",
     "iopub.status.idle": "2026-03-01T13:36:34.635074Z",
     "shell.execute_reply": "2026-03-01T13:36:34.634750Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'southern_norway' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[16]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m     \u001b[38;5;28;01mif\u001b[39;00m \u001b[43msouthern_norway\u001b[49m \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m      3\u001b[39m         ts_area = tdm.TimeSeries(\n\u001b[32m      4\u001b[39m             tdm.Frequency.PT1H,\n\u001b[32m      5\u001b[39m             timestamps=timestamps,\n\u001b[32m   (...)\u001b[39m\u001b[32m     10\u001b[39m             location=southern_norway,\n\u001b[32m     11\u001b[39m         )\n\u001b[32m     12\u001b[39m         ts_area\n",
      "\u001b[31mNameError\u001b[39m: name 'southern_norway' is not defined"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    if southern_norway is not None:\n",
    "        ts_area = tdm.TimeSeries(\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-01T13:36:34.636129Z",
     "iopub.status.busy": "2026-03-01T13:36:34.636077Z",
     "iopub.status.idle": "2026-03-01T13:36:34.642832Z",
     "shell.execute_reply": "2026-03-01T13:36:34.642430Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'southern_norway' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[17]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m     \u001b[38;5;28;01mif\u001b[39;00m \u001b[43msouthern_norway\u001b[49m \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m      3\u001b[39m         \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mSouthern Norway → Oslo (contained):  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00msouthern_norway.distance_to(oslo)\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.1f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m km\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      4\u001b[39m         \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mSouthern Norway → Tromsø (outside):  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00msouthern_norway.distance_to(tromsoe)\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.1f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m km\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'southern_norway' is not defined"
     ]
    }
   ],
   "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-01T13:36:34.643729Z",
     "iopub.status.busy": "2026-03-01T13:36:34.643674Z",
     "iopub.status.idle": "2026-03-01T13:36:34.650191Z",
     "shell.execute_reply": "2026-03-01T13:36:34.649884Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'ts_oslo' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[18]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m json_str = \u001b[43mts_oslo\u001b[49m.to_json()\n\u001b[32m      2\u001b[39m ts_back = tdm.TimeSeries.from_json(json_str, location=oslo)\n\u001b[32m      4\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mName:     \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mts_back.name\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'ts_oslo' is not defined"
     ]
    }
   ],
   "source": [
    "json_str = ts_oslo.to_json()\n",
    "ts_back = tdm.TimeSeries.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 | `TimeSeries(..., 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
}
