{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Using Lets-Plot with GeoTools to Create Maps\n", "\n", "\n", "The Let's-Plot library allows to easily visualize geospatial features from GeoTools `SimpleFeatureCollection`.\n", "\n", "`SimpleFeatureCollection` is a collection of `SimpleFeature`-s. Each `SimpleFeature` have a \"geometry\" attribute as well as optional \"data\" attributes.\n", "\n", "The Let's-Plot library understands the following three geometry types:\n", "\n", "- Points / Multi-Points\n", "- Lines / Multi-Lines\n", "- Polygons / Multi-Polygons\n", "\n", "These shapes can be plotted using various geometry layers, depending on the type of the shape:\n", "\n", "- `geomPoint, geom_text` with Points / Multi-Points\n", "- `geomPath` with Lines / Multi-Lines\n", "- `geomPolygon, geom_map` with Polygons / Multi-Polygons\n", "- `geomRect` when used with Polygon shapes will display corresponding bounding boxes\n", "\n", "Apart from `SimpleFeatureCollection` the Lets-Plot library can also plot an individual `Geometry` (org.locationtech.jts.geom) and a `ReferencedEnvelope` (org.geotools.geometry.jts).\n", "\n", "Before passing to a Lets-Plot geometry layer (via `map` or `data` parameters) any 'foreign' object must be converted to a Lets-Plot `SpatialDataset` object. This is done by the `toSpatialDataset()` extension method provided by Lets-Plot GeoTools extension (see the `%use lets-plot-gt` 'magic').\n", "\n", "\n", "Shapfiles used in this tutorial:\n", "- naturalearth_lowres.shp\n", "- naturalearth_cities.shp \n", "\n", "all are the copies of shapefiles distributed with the [GeoPandas](https://geopandas.org/) Python package." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%useLatestDescriptors\n", "%use lets-plot" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "// Initialize Lets-Plot GeoTools extension. \n", "%use lets-plot-gt" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Lets-Plot Kotlin API v.4.4.2. Frontend: Notebook with dynamically loaded JS. Lets-Plot JS v.4.0.0." ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "LetsPlot.getInfo()" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "@file:DependsOn(\"org.geotools:gt-shapefile:[23,)\")\n", "@file:DependsOn(\"org.geotools:gt-cql:[23,)\")" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "import org.geotools.data.shapefile.ShapefileDataStoreFactory\n", "import org.geotools.data.simple.SimpleFeatureCollection\n", "import java.net.URL\n", "\n", "val factory = ShapefileDataStoreFactory()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Polygon shapes - Naturalearth low-resolution world dataset." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "val worldFeatures : SimpleFeatureCollection = with(\"naturalearth_lowres\") {\n", " val url = \"https://raw.githubusercontent.com/JetBrains/lets-plot-kotlin/master/docs/examples/shp/${this}/${this}.shp\"\n", " factory.createDataStore(URL(url)).featureSource.features\n", "}\n", "\n", "// Convert Feature Collection to SpatialDataset.\n", "// Use 10 decimals to encode floating point numbers (this is the default).\n", "val world = worldFeatures.toSpatialDataset(10)\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[Oceania, Africa, North America, Asia, South America, Europe, Seven seas (open ocean), Antarctica]" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "world[\"continent\"]?.distinct()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### geomPolygon()" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "val voidTheme = theme(axis=\"blank\", panelGrid=\"blank\")\n", "\n", "// Use the parameter `map` in `geomPolygon()` to display Polygons / Multi-Polygons \n", "letsPlot() + \n", " geomPolygon(map = world, fill = \"white\", color = \"gray\") + \n", " ggsize(700, 400) + \n", " voidTheme" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### geomMap()\n", "\n", "*geom_map()* is very similar to *geomPolygon()* but it automatically applies the `Mercator` projection and other defaults that are more suitable for displaying blank maps. " ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "letsPlot() + \n", " geomMap(map = world) + \n", " ggsize(700, 400) + \n", " voidTheme" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "// When applying Mercator projection to the world map, Antarctica becomes disproportionally large so \n", "// in the future let's show only part of it above 85-th parallel south:\n", "val worldLimits = coordMap(ylim = -70 to 85)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Point shapes - Naturalearth world capitals dataset." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "val cityFeatures : SimpleFeatureCollection = with(\"naturalearth_cities\") {\n", " val url = \"https://raw.githubusercontent.com/JetBrains/lets-plot-kotlin/master/docs/examples/shp/${this}/${this}.shp\"\n", " factory.createDataStore(URL(url)).featureSource.features\n", "}\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### geomPoint()" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "// Use parameter `map` in `geomPoint()` to display Point shapes\n", "val cities = cityFeatures.toSpatialDataset(10)\n", "letsPlot() + \n", " geomMap(map = world) + \n", " geomPoint(map = cities, color = \"red\") +\n", " ggsize(800, 600) + voidTheme + worldLimits" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### geomText()\n", "\n", "The situation with *geomText()* is different because in order to display labels we have to specify mapping for the aesthetic \"label\".\n", "\n", "Aesthetic mapping binds a variable in *data* (passed via `data` parameter) with its representation on the screen.\n", "\n", "Variables in a SpatialDataset passed via the `map` parameter can not be used in the aesthetic mapping.\n", "\n", "Fortunately, such a SpatialDataset can as well be passed via the `data` parameter and Lets-Plot will undersand that its *geometries* should be mapped to the \"x\" and \"y\" aesthetic automatically.\n", "\n", "In the next example we are going to show names of cities as labels on map. \n", "\n", "Let's only show South American capitals because too many labels on the entire world map would quickly become not legible." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "import org.geotools.filter.text.cql2.CQL" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "// Obtain bounding box of South America and use it to set the limits.\n", "val southAm = worldFeatures.subCollection(\n", " CQL.toFilter(\"continent = 'South America'\")\n", ")\n", "val southAmBounds = southAm.bounds\n", "\n", "// Let's use slightly expanded boundind box.\n", "southAmBounds.expandBy(4.0)\n", "\n", "// Define limits to use later with city markers and labels. \n", "val southAmLimits = coordMap(\n", " xlim = southAmBounds.minX to southAmBounds.maxX,\n", " ylim = southAmBounds.minY to southAmBounds.maxY\n", ")\n", "\n", "\n", "letsPlot() + \n", " geomMap(map = southAm.toSpatialDataset()) +\n", " geomRect(map = southAmBounds.toSpatialDataset(), alpha = 0, color = \"#EFC623\")" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "// Add `text` layer and use the `data` parameter to pass `cities` SpartialDataset.\n", "// Also configure `tooltip` in the points layer to show the city name.\n", "letsPlot() + \n", " geomMap(map = southAm.toSpatialDataset(), fill=\"#e5f5e0\") +\n", " geomPoint(data = cities, color = \"red\", size = 3, tooltips = layerTooltips().line(\"@name\")) +\n", " geomText(data = cities, vjust = 1, position = positionNudge(y = -.5)) { label = \"name\" } +\n", " geomRect(map = southAmBounds.toSpatialDataset(), alpha = 0, fill = \"black\", color=\"#EFC623\", size=16) +\n", " southAmLimits +\n", " ggsize(450, 691) + \n", " voidTheme" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Choropleth\n", "\n", "As we saw earlier, *Lets-Plot* geom-layers accept *SpatialDataset* in their `data` parameter. \n", "\n", "This makes it easy to bind aesthetics with variables in *SpatialDataset*." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "// Create choropleth by mapping the `continent` variable to the `fill` aesthetic.\n", "letsPlot() + \n", " geomMap(data = world, color = \"white\") { fill = \"continent\" } + \n", " ggsize(900, 400) + voidTheme + worldLimits" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "// Create another choropleth by mapping the `GDP estimate` variable to the `fill` aesthetic.\n", "letsPlot() + \n", " geomMap(data = world, color = \"white\") { fill = \"gdp_md_est\" } + \n", " ggsize(800, 400) + voidTheme + worldLimits + labs(fill = \"GDP (mil$)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Joining `data` and `geometry` datasets\n", "\n", "In this example we will use both: the `data` and the `map` parameters.\n", "\n", "We will use the `data` parameter to pass \"average temperature per continent\" dataset to the geom layer.\n", "\n", "The continent geometries *SpatialDataset* is passed via the `map` parameter as before.\n", "\n", "For this to work it is also necessary to specify fields by which *Lets-Plot* will join `data` and `map` datasets. We will do that using the *mapJoin* parameter." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "// Average temperatures\n", "val climateData = mapOf(\n", " \"region\" to listOf(\"Europe\", \"Asia\", \"North America\", \"Africa\", \"Australia\", \"Oceania\"),\n", " \"avg_temp\" to listOf(8.6, 16.6, 11.7, 21.9, 14.9, 23.9)\n", ")" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "// Join `data` and `map` using the `mapJoin` parameter.\n", "// For the sake of the demo let's use `geom_rect` and customize the tooltip.\n", "letsPlot() + geomRect(data = climateData,\n", " map = world,\n", " mapJoin = \"region\" to \"continent\",\n", " color = \"white\",\n", " tooltips=layerTooltips().line(\"^fill C\\u00b0\")) { fill = \"avg_temp\" } + \n", " scaleFillGradient(low = \"light_blue\", high = \"dark_green\", name=\"Average t[C\\u00b0]\") + \n", " ggsize(800, 400) + voidTheme" ] } ], "metadata": { "kernelspec": { "display_name": "Kotlin", "language": "kotlin", "name": "kotlin" }, "language_info": { "codemirror_mode": "text/x-kotlin", "file_extension": ".kt", "mimetype": "text/x-kotlin", "name": "kotlin", "nbconvert_exporter": "", "pygments_lexer": "kotlin", "version": "1.8.20" } }, "nbformat": 4, "nbformat_minor": 4 }