{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import param\n",
    "import pandas as pd\n",
    "import panel as pn\n",
    "import numpy as np\n",
    "\n",
    "from panel.reactive import ReactiveHTML\n",
    "\n",
    "CSS = ['https://unpkg.com/leaflet@1.7.1/dist/leaflet.css']\n",
    "\n",
    "JS = {\n",
    "    'leaflet': 'https://unpkg.com/leaflet@1.7.1/dist/leaflet.js',\n",
    "    'heat': 'https://cdn.jsdelivr.net/npm/leaflet.heat@0.2.0/dist/leaflet-heat.min.js'\n",
    "}\n",
    "\n",
    "pn.extension(css_files=CSS, js_files=JS)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class LeafletHeatMap(ReactiveHTML):\n",
    "    \n",
    "    attribution = param.String(doc=\"Tile source attribution.\")\n",
    "    \n",
    "    blur = param.Integer(default=18, bounds=(5, 50), doc=\"Amount of blur to apply to heatmap\")\n",
    "    \n",
    "    center = param.XYCoordinates(default=(0, 0), doc=\"The center of the map.\")\n",
    "    \n",
    "    data = param.DataFrame(doc=\"The heatmap data to plot, should have 'x', 'y' and 'value' columns.\")\n",
    "    \n",
    "    tile_url = param.String(doc=\"Tile source URL with {x}, {y} and {z} parameter\")\n",
    "    \n",
    "    min_alpha = param.Number(default=0.2, bounds=(0, 1), doc=\"Minimum alpha of the heatmap\")\n",
    "    \n",
    "    radius = param.Integer(default=25, bounds=(5, 50), doc=\"The radius of heatmap values on the map\")\n",
    "    \n",
    "    x = param.String(default='longitude', doc=\"Column in the data with longitude coordinates\")\n",
    "    \n",
    "    y = param.String(default='latitude', doc=\"Column in the data with latitude coordinates\")\n",
    "    \n",
    "    value = param.String(doc=\"Column in the data with the data values\")\n",
    "    \n",
    "    zoom = param.Integer(default=13, bounds=(0, 21), doc=\"The plots zoom-level\")\n",
    "\n",
    "    _template = \"\"\"\n",
    "    <div id=\"map\" style=\"width: 100%; height: 100%; position: absolute;\"></div>\n",
    "    \"\"\"\n",
    "    \n",
    "    _scripts = {\n",
    "        'get_data': \"\"\"\n",
    "            const records = []\n",
    "            for (let i=0; i<data.data.index.length; i++)\n",
    "                records.push([data.data[data.y][i], data.data[data.x][i], data.data[data.value][i]])\n",
    "            return records\n",
    "        \"\"\",\n",
    "        'render': \"\"\"\n",
    "            state.map = L.map(map).setView(data.center, data.zoom);\n",
    "            state.map.on('zoom', (e) => { data.zoom = state.map.getZoom() })            \n",
    "            state.tileLayer = L.tileLayer(data.tile_url, {\n",
    "                attribution: data.attribution,\n",
    "                maxZoom: 21,\n",
    "                tileSize: 512,\n",
    "                zoomOffset: -1,\n",
    "            }).addTo(state.map);\n",
    "        \"\"\",\n",
    "        'after_layout': \"\"\"\n",
    "           state.map.invalidateSize()\n",
    "           state.heatLayer = L.heatLayer(self.get_data(), {blur: data.blur, radius: data.radius, max: 10, minOpacity: data.min_alpha}).addTo(state.map)\n",
    "        \"\"\",\n",
    "        'radius': \"state.heatLayer.setOptions({radius: data.radius})\",\n",
    "        'blur': \"state.heatLayer.setOptions({blur: data.blur})\",\n",
    "        'min_alpha': \"state.heatLayer.setOptions({minOpacity: data.min_alpha})\",\n",
    "        'zoom': \"state.map.setZoom(data.zoom)\",\n",
    "        'data': 'state.heatLayer.setLatLngs(self.get_data())'\n",
    "    }\n",
    "    \n",
    "    __css__ = CSS\n",
    "    \n",
    "    __javascript__ = list(JS.values())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Example"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "url=\"http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.csv\"\n",
    "earthquakes = pd.read_csv(url)\n",
    "\n",
    "heatmap = LeafletHeatMap(\n",
    "    attribution='Map data &copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors',\n",
    "    data=earthquakes[['longitude', 'latitude', 'mag']],\n",
    "    min_height=500,\n",
    "    tile_url='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.jpg',\n",
    "    radius=30,\n",
    "    sizing_mode='stretch_both',\n",
    "    value='mag',\n",
    "    zoom=2,\n",
    ")\n",
    "\n",
    "description=pn.pane.Markdown(f'## Earthquakes between {earthquakes.time.min()} and {earthquakes.time.max()}\\n\\n[Data Source]({url})', sizing_mode=\"stretch_width\")\n",
    "\n",
    "pn.Column(\n",
    "    description,\n",
    "    pn.Row(\n",
    "        heatmap.controls(['blur', 'min_alpha', 'radius', 'zoom']),\n",
    "        heatmap,\n",
    "        sizing_mode='stretch_both'\n",
    "    ),\n",
    "    sizing_mode='stretch_both'\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## App\n",
    "\n",
    "Let's wrap it into nice template that can be served via `panel serve LeafletHeatMap.ipynb`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pn.template.FastListTemplate(\n",
    "    site=\"Panel\", title=\"Leaflet Heat Map\", \n",
    "    sidebar=[heatmap.controls(['blur', 'min_alpha', 'radius', 'zoom'])], \n",
    "    main=[description, pn.Row(heatmap, sizing_mode=\"stretch_both\")]\n",
    ").servable();"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
