Using Julia to plot OSM data
Plotting OSM data
There’s an awful lot of data on OpenStreetMap, and while the best place to look at it is on the map, it can also be downloaded and worked on offline. I wanted to do this with Julia, and actually made surprisingly quick progress.

See also OpenStreetMap
How to
One way of accessing the data is via the good people at https://download.geofabrik.de/. They make OSM data available in ESRI shapefile format, inter alia. (You can also talk directly to the OSM databases, but that’s for another project.) I started by downloading the data for Ireland at https://download.geofabrik.de/europe/ireland-and-northern-ireland.html. The resulting archive contains sets of files relating to a number of themes (waterways, roads, buildings, natural features), but I extracted the roads one, specifially the files gis_osm_roads_free_1.shp
and gis_osm_roads_free_1.dbf
. These are respectively a shapefile (polygons) and a file of attributes for each feature, in a standard GIS format.
Julia can cope with this data via the Shapefile.jl
library. Do import Pkg; Pkg.add("Shapefile")
and you’re good to go (it pulls in the required GeoInterface
library automatically, and what follows requires the Plots
library). Activate it all as follows, and read in the data
using Shapefile, GeoInterface, Plots
table = Shapefile.Table("gis_osm_roads_free_1.shp")
class = table.fclass
Objects and functions
Everything ends up in the table
object. The array table.geometry
contains the traces for all the objects in the file, and table.fclass
lists what each object is (here, the type of road or path, such as “motorway”, “trunk” or “cycleway”). Looking up table.fclass
is slow, so it’s better to copy its contents to an array once, and use that.
Each element of table.geometry
contains an array of latitude and longtitude pairs, extractable via GeoInterface.coordinates()
, so we could plot one like this:
trace = GeoInterface.coordinates(table.geometry[1])[1]
lon = [x[1] for x in trace]
lat = [x[2] for x in trace]
plot(lon, lat)
Plotting single paths is not interesting: there are 100s of 1000s of them. To many to plot, in fact, so we either need to select a rare type (e.g., motorways) and plot it for the whole island, or restrict ourselves to a fairly small bounding box (e.g., plot the paths for the city I live in, Limerick). If using a bounding box, we need to restrict ourselves to paths that have at least one point within it. This function does that (could be made more efficient for long paths, I’m sure):
function inbb(coords, minlat, maxlat, minlon, maxlon)
# add test that BBs don't overlap, for speed?
# if max(coord_lat)<minlat or min(coord_lat)>maxlat, or ditto for lon, skip loop.
result = false
for i in 1:size(coords)[1]
if (minlon <= coords[i][1] <= maxlon) & (minlat <= coords[i][2] <= maxlat)
result = true
break
end
end
return result
end
Another occasion for a function is the plotting code: if we’re going to plot multiple paths, we might as well tidy up the code. This function takes a path, its type (motorway, secondary, cycleway, etc.), the current plot identifier, and a bounding box. It then extracts the latitude and longitude arrays from the path, and adds them to the current plot (with different colour and linewidth according to path type). It only plots paths that enter within the bounding box (inbb()
) and uses xlim
and ylim
in the plots to cut off paths as they exit the bounding box.
function plotpoly(element, fclass, p, minlat, maxlat, minlon, maxlon)
trace = GeoInterface.coordinates(element)[1] # Is there always only one? In this data, yes.
lon = [x[1] for x in trace]
lat = [x[2] for x in trace]
if inbb(trace, minlat, maxlat, minlon, maxlon)
if (fclass == "service") | (fclass == "unclassified")
plot!(p, lon, lat, color="black", alpha=0.5, label=false, linewidth=0.5, xlim=(minlon, maxlon), ylim=(minlat, maxlat))
elseif fclass == "cycleway"
plot!(p, lon, lat, color="blue", alpha=0.5, label=false, linewidth=1, xlim=(minlon, maxlon), ylim=(minlat, maxlat))
else
plot!(p, lon, lat, color="darkred", alpha=0.5, label=false, linewidth=1, xlim=(minlon, maxlon), ylim=(minlat, maxlat))
end
end
end
Main code
Then we come to the main code: first, create a new plot, p
. Then iterate through all the table.geometry
entries, applying plotpoly()
to them. Finally, tidy up the plot: drop the axes and grid, and set the aspect ratio to approximately 1/cos(52°) to correct for that degrees of longitude represent much smaller distances than degrees of latitude, at this latitude.
p = plot(size=(1500,1000))
for i in 1:length(table.geometry)
plotpoly(table.geometry[i], class[i], p, 52.64, 52.68, -8.65,-8.565) #50, 56, -11, -5.0)
end
plot!(p, aspect_ratio = 1.62, axis=false, grid=false, title="Limerick City (OpenStreetMap path data)")
Postscript
While the Shapefile.jl
library is quite powerful (and general with respect to GIS), it is likely that it is more efficient to extract and plot OSM data with OSMMakie.jl.