The Let's-Plot library allows to easily visualize geospatial features from GeoTools SimpleFeatureCollection
.
SimpleFeatureCollection
is a collection of SimpleFeature
-s. Each SimpleFeature
have a "geometry" attribute as well as optional "data" attributes.
The Let's-Plot library understands the following three geometry types:
These shapes can be plotted using various geometry layers, depending on the type of the shape:
geom_point, geom_text
with Points / Multi-Pointsgeom_path
with Lines / Multi-Linesgeom_polygon, geom_map
with Polygons / Multi-Polygonsgeom_rect
when used with Polygon shapes will display corresponding bounding boxesApart from SimpleFeatureCollection
the Lets-Plot library can also plot an individual Geometry
(org.locationtech.jts.geom) and a ReferencedEnvelope
(org.geotools.geometry.jts).
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').
Shapfiles used in this tutorial:
all are the copies of shapefiles distributed with the GeoPandas Python package.
%useLatestDescriptors
%use lets-plot
// Initialize Lets-Plot GeoTools extension.
%use lets-plot-gt(gt="[23,)")
LetsPlot.getInfo()
@file:DependsOn("org.geotools:gt-shapefile:[23,)")
@file:DependsOn("org.geotools:gt-cql:[23,)")
import org.geotools.data.shapefile.ShapefileDataStoreFactory
import org.geotools.data.simple.SimpleFeatureCollection
import java.net.URL
val factory = ShapefileDataStoreFactory()
val worldFeatures : SimpleFeatureCollection = with("naturalearth_lowres") {
val url = "https://raw.githubusercontent.com/JetBrains/lets-plot-kotlin/master/docs/examples/shp/${this}/${this}.shp"
factory.createDataStore(URL(url)).featureSource.features
}
// Convert Feature Collection to SpatialDataset.
// Use 10 decimals to encode floating point numbers (this is the default).
val world = worldFeatures.toSpatialDataset(10)
world["continent"]?.distinct()
val voidTheme = theme().axisLine_blank().axisText_blank().axisTicks_blank().axisTitle_blank()
// Use the parameter `map` in `geom_polygon` to display Polygons / Multi-Polygons
lets_plot() +
geom_polygon(map = world, fill = "white", color = "gray") +
ggsize(700, 400) +
voidTheme
geom_map() is very similar to geom_polygon() but it automatically applies the Mercator
projection and other defaults that are more suitable for displaying blank maps.
lets_plot() +
geom_map(map = world) +
ggsize(700, 400) +
voidTheme
// When applying Mercator projection to the world map, Antarctica becomes disproportionally large so
// in the future let's show only part of it above 85-th parallel south:
val worldLimits = coord_map(ylim = -70 to 85)
val cityFeatures : SimpleFeatureCollection = with("naturalearth_cities") {
val url = "https://raw.githubusercontent.com/JetBrains/lets-plot-kotlin/master/docs/examples/shp/${this}/${this}.shp"
factory.createDataStore(URL(url)).featureSource.features
}
// Use parameter `map` in `geom_point` to display Point shapes
val cities = cityFeatures.toSpatialDataset(10)
lets_plot() +
geom_map(map = world) +
geom_point(map = cities, color = "red") +
ggsize(800, 600) + voidTheme + worldLimits
The situation with geom_text is different because in order to display labels we have to specify mapping for the aesthetic "label".
Aesthetic mapping binds a variable in data (passed via data
parameter) with its representation on the screen.
Variables in a SpatialDataset passed via the map
parameter can not be used in the aesthetic mapping.
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.
In the next example we are going to show names of cities as labels on map.
Let's only show South American capitals because too many labels on the entire world map would quickly become not legible.
import org.geotools.filter.text.cql2.CQL
// Obtain bounding box of South America and use it to set the limits.
val southAm = worldFeatures.subCollection(
CQL.toFilter("continent = 'South America'")
)
val southAmBounds = southAm.bounds
// Let's use slightly expanded boundind box.
southAmBounds.expandBy(4.0)
// Define limits to use later with city markers and labels.
val southAmLimits = coord_map(
xlim = southAmBounds.minX to southAmBounds.maxX,
ylim = southAmBounds.minY to southAmBounds.maxY
)
lets_plot() +
geom_map(map = southAm.toSpatialDataset()) +
geom_rect(map = southAmBounds.toSpatialDataset(), alpha = 0, color = "#EFC623")
// Add `text` layer and use the `data` parameter to pass `cities` SpartialDataset.
// Also configure `tooltip` in the points layer to show the city name.
lets_plot() +
geom_map(map = southAm.toSpatialDataset(), fill="#e5f5e0") +
geom_point(data = cities, color = "red", size = 3, tooltips = layer_tooltips().line("@name")) +
geom_text(data = cities, vjust = 1, position = position_nudge(y = -.5)) { label = "name" } +
geom_rect(map = southAmBounds.toSpatialDataset(), alpha = 0, color="#EFC623", size=16) +
southAmLimits +
ggsize(450, 691) +
voidTheme