leaflet.indoor adds indoor vector layers to a
leaflet map and keeps one floor visible per indoor data
set. It works with saved HTML widgets, R Markdown, Quarto, and Shiny
without a tile service.
Data structure
The primary input is an sf object. A column named
level is used by default. Numeric, character, and factor
floors are supported. A list-column assigns one feature to more than one
floor:
indoor_demo[c("name", "level")]
#> Simple feature collection with 10 features and 2 fields
#> Geometry type: GEOMETRY
#> Dimension: XY
#> Bounding box: xmin: 0 ymin: 0 xmax: 17 ymax: 23
#> CRS: NA
#> name level geometry
#> 1 Meeting room 0 POLYGON ((0 0, 8 0, 8 6, 0 ...
#> 2 Laboratory 0 POLYGON ((9 0, 17 0, 17 6, ...
#> 3 Main corridor 0 LINESTRING (0 7, 17 7)
#> 4 Stairwell 0, 1, 2 POLYGON ((7.5 6, 9.5 6, 9.5...
#> 5 Elevator 0, 1, 2 POINT (12 8)
#> 6 Reading room 1 POLYGON ((0 10, 8 10, 8 16,...
#> 7 Workshop 1 POLYGON ((9 10, 17 10, 17 1...
#> 8 Service suite 2 MULTIPOLYGON (((0 18, 6 18,...
#> 9 Emergency route 2 MULTILINESTRING ((0 17, 17 ...
#> 10 Environmental sensors 0 MULTIPOINT ((3 3), (14 3))The included data are synthetic. Rooms use polygons, the corridor and emergency route use line geometries, the elevator uses a point, and the sensor feature is a multipoint. The stairwell and elevator belong to floors 0, 1, and 2.
Missing or empty floors are errors by default. Use
missing_level = "drop" only when dropping incomplete
features is intentional.
A local Cartesian map
Local plans must use L.CRS.Simple, have no declared
sf CRS, and explicitly set crs = "simple".
Coordinates are passed through unchanged.
leaflet(
indoor_demo,
options = leafletOptions(
crs = leafletCRS(crsClass = "L.CRS.Simple"),
minZoom = -2
)
) |>
addIndoor(
level = "level",
initial_level = "0",
layerId = ~feature_id,
label = ~name,
popup = ~description,
style = list(fillColor = ~fill),
crs = "simple"
) |>
addIndoorControl(position = "bottomright")Floor order
Pure numeric levels are ordered numerically. Character levels use a
stable natural order, including basements such as B2 and
B1. Factor levels retain their observed factor order. For
domain-specific labels, provide every observed floor exactly once:
The canonical order runs from lowest to highest. The vertical control
displays the reverse order by default so higher floors appear above
lower floors. Set indoorControlOptions(descending = FALSE)
to display canonical order.
Styles, labels, and popups
indoorOptions() defines separate defaults for points,
lines, and polygons. The style list can contain constants,
vectors, or formulas. Its values override the geometry defaults feature
by feature.
Plain labels and popups are escaped. Mark up trusted content explicitly:
addIndoor(
label = ~name,
popup = htmltools::HTML("<strong>Trusted content</strong>"),
style = list(fillColor = ~fill, weight = 2)
)For paired room images and captions, see the
vignette("photo-carousels", package = "leaflet.indoor")
article.
Multiple indoor data sets
Data sets are independent when they use different
dataset_id values. Controls are independent when they use
different control_id values. Identifiers are scoped to one
map, so separate widgets never share an active floor.
map |>
addIndoor(data = rooms, dataset_id = "rooms") |>
addIndoorControl(dataset_id = "rooms", control_id = "rooms-control") |>
addIndoor(data = services, dataset_id = "services") |>
addIndoorControl(dataset_id = "services", control_id = "services-control")Geographic data
sf objects with a declared geographic or projected CRS
are transformed to EPSG:4326 for an ordinary Leaflet map. This example
intentionally omits online tiles so it builds without network
access.
leaflet(indoor_demo_geo) |>
addIndoor(
layerId = ~feature_id,
label = ~name,
popup = ~description,
style = list(fillColor = ~fill)
) |>
addIndoorControl()Shiny and proxies
Use setIndoorLevel() with a proxy after a map has
rendered:
leafletProxy("map") |>
setIndoorLevel("2")User changes arrive in input$map_indoor_level as a list
containing level, dataset_id, and
control_id. See
vignette("shiny", package = "leaflet.indoor") for a
complete application.
Troubleshooting
- A missing
levelcolumn is an error and names the missing column. - A local plan on a geographic map is rejected instead of being silently interpreted as longitude and latitude.
- A spatial CRS on an
L.CRS.Simplemap is rejected because no defensible spatial transformation exists. - An unknown floor passed to a regular map is an R error. On a Shiny proxy it is rejected in the browser and leaves the current floor unchanged.
Known limitations and roadmap
Version 0.1.0 supports two-dimensional GeoJSON-compatible vector
geometry. It does not provide rasters, three-dimensional floors, indoor
routing, feature editing, or automatic alignment of unrelated floor
plans. Future work will be driven by reproducible use cases and
compatibility with the Leaflet JavaScript version embedded by the R
leaflet package.