Skip to content

Instantly share code, notes, and snippets.

@walkerke
Created November 5, 2025 17:27
Show Gist options
  • Select an option

  • Save walkerke/4a10a8fbb1658493de4b8e8fe08d4968 to your computer and use it in GitHub Desktop.

Select an option

Save walkerke/4a10a8fbb1658493de4b8e8fe08d4968 to your computer and use it in GitHub Desktop.
library(mapgl)
library(shiny)
library(geojsonsf)
ui <- fluidPage(
tags$head(
tags$style(HTML(
"
body { margin: 0; padding: 0; }
.container-fluid { padding: 0 !important; margin: 0 !important; }
#map { height: 100vh !important; width: 100vw !important; }
.restaurants-panel {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
max-height: 80vh;
overflow-y: auto;
}
.restaurants-panel h4 {
margin-top: 0;
margin-bottom: 15px;
font-size: 18px;
color: #d32f2f;
}
.restaurants-panel table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.restaurants-panel th {
background: #d32f2f;
color: white;
padding: 12px;
text-align: left;
font-size: 14px;
font-weight: 600;
position: sticky;
top: 0;
}
.restaurants-panel td {
padding: 10px 12px;
border-bottom: 1px solid #e0e0e0;
}
.restaurants-panel tr:hover {
background: #f5f5f5;
}
.instruction-message {
text-align: center;
padding: 20px;
color: #666;
font-size: 14px;
}
"
))
),
maplibreOutput("map", height = "100vh"),
absolutePanel(
top = 20,
left = 20,
width = 400,
class = "restaurants-panel",
uiOutput("restaurants_panel")
)
)
server <- function(input, output, session) {
# Store restaurants and buffer geojson
restaurants <- reactiveVal(NULL)
buffer_geojson <- reactiveVal(NULL)
query_attempts <- reactiveVal(0)
should_query <- reactiveVal(FALSE)
output$map <- renderMaplibre({
maplibre(
style = openfreemap_style("dark"),
zoom = 3
) |>
set_projection("globe") |>
add_pmtiles_source(
id = "places-source",
url = "https://overturemaps-tiles-us-west-2-beta.s3.amazonaws.com/2025-06-25/places.pmtiles"
) |>
add_circle_layer(
id = "places-layer",
source = "places-source",
source_layer = "place",
circle_color = "cyan",
circle_opacity = 0.5,
circle_radius = 4,
popup = concat(
"<strong>",
get_column("@name"),
"</strong><br>",
get_column("categories")
)
) |>
add_geolocate_control(
position = "top-right",
show_user_heading = TRUE
)
})
# When user geolocates
observeEvent(input$map_geolocate, {
coords <- input$map_geolocate$coords
lng <- coords$longitude
lat <- coords$latitude
# Fly to location
maplibre_proxy("map") |>
fly_to(center = c(lng, lat), zoom = 14)
# Add buffer using turf_buffer
maplibre_proxy("map") |>
turf_buffer(
coordinates = c(lng, lat),
radius = 0.5,
units = "miles",
source_id = "user_buffer",
input_id = "buffer_result"
) |>
add_fill_layer(
id = "buffer_fill",
source = "user_buffer",
fill_color = "red",
fill_opacity = 0.2
) |>
add_line_layer(
id = "buffer_outline",
source = "user_buffer",
line_color = "red",
line_width = 2
) |>
add_circle_layer(
id = "user_location",
source = list(
type = "geojson",
data = list(
type = "Point",
coordinates = c(lng, lat)
)
),
circle_color = "blue",
circle_radius = 8,
circle_stroke_color = "white",
circle_stroke_width = 2
)
})
# When buffer result is ready, style map and query restaurants
observeEvent(input$map_turf_buffer_result, {
buffer_geom <- input$map_turf_buffer_result$result$geometry
if (is.null(buffer_geom)) {
return()
}
buffer_geojson(buffer_geom)
# Apply styling to the map
maplibre_proxy("map") |>
set_paint_property(
"places-layer",
"circle-color",
list(
"case",
list(
"all",
list("==", list("distance", buffer_geom), 0),
list("in", "restaurant", list("get", "categories"))
),
"#ff6600",
"cyan"
)
) |>
set_paint_property(
"places-layer",
"circle-radius",
list(
"case",
list(
"all",
list("==", list("distance", buffer_geom), 0),
list("in", "restaurant", list("get", "categories"))
),
8,
4
)
) |>
set_paint_property(
"places-layer",
"circle-opacity",
list(
"case",
list(
"all",
list("==", list("distance", buffer_geom), 0),
list("in", "restaurant", list("get", "categories"))
),
1,
0.2
)
)
# Use a simple delayed query approach
buffer_geom_copy <- buffer_geom
# Try at 3 seconds
later::later(
function() {
print("Query attempt 1 (3s)")
query_rendered_features(
maplibre_proxy("map", session = session),
layer_id = "places-layer",
filter = list(
"all",
list("==", list("distance", buffer_geom_copy), 0),
list("in", "restaurant", list("get", "categories"))
),
callback = function(features) {
if (!is.null(features) && nrow(features) > 0) {
print(paste(
"Success at 3s! Found",
nrow(features),
"restaurants"
))
restaurants(features)
} else {
print("Attempt 1 - Features found: 0")
}
}
)
},
3
)
# Try again at 5 seconds if first didn't work
later::later(
function() {
if (!is.null(isolate(restaurants()))) {
return()
} # Already found
print("Query attempt 2 (5s)")
query_rendered_features(
maplibre_proxy("map", session = session),
layer_id = "places-layer",
filter = list(
"all",
list("==", list("distance", buffer_geom_copy), 0),
list("in", "restaurant", list("get", "categories"))
),
callback = function(features) {
if (!is.null(features) && nrow(features) > 0) {
print(paste(
"Success at 5s! Found",
nrow(features),
"restaurants"
))
restaurants(features)
} else {
print("Attempt 2 - Features found: 0")
}
}
)
},
5
)
# Final try at 7 seconds
later::later(
function() {
if (!is.null(isolate(restaurants()))) {
return()
} # Already found
print("Query attempt 3 (7s)")
query_rendered_features(
maplibre_proxy("map", session = session),
layer_id = "places-layer",
filter = list(
"all",
list("==", list("distance", buffer_geom_copy), 0),
list("in", "restaurant", list("get", "categories"))
),
callback = function(features) {
if (!is.null(features) && nrow(features) > 0) {
print(paste(
"Success at 7s! Found",
nrow(features),
"restaurants"
))
restaurants(features)
} else {
print("Attempt 3 - Features found: 0. Giving up.")
}
}
)
},
7
)
})
output$restaurants_panel <- renderUI({
places <- restaurants()
if (is.null(places)) {
return(
div(
class = "instruction-message",
h4(
"Find Restaurants Near You",
style = "color: #d32f2f; font-size: 18px;"
),
p(
"Click the geolocate button (top right) to find restaurants within 0.5 miles of your location."
)
)
)
}
# Debug: show column names
print(paste("Number of places found:", nrow(places)))
print(paste("Column names:", paste(names(places), collapse = ", ")))
if (nrow(places) == 0) {
return(
div(
class = "instruction-message",
h4("No Restaurants Found", style = "color: #d32f2f;"),
p(
"No restaurants found within 0.5 miles. Try moving to a different location."
)
)
)
}
# Extract names - R converts @name to X.name
names_list <- if ("X.name" %in% names(places)) {
places[["X.name"]]
} else if ("@name" %in% names(places)) {
places[["@name"]]
} else if ("name" %in% names(places)) {
places[["name"]]
} else {
rep("Unknown", nrow(places))
}
div(
h4(paste("Restaurants Found:", nrow(places))),
tags$div(
style = "max-height: 60vh; overflow-y: auto;",
tags$ul(
lapply(head(names_list, 50), function(name) {
tags$li(if (is.na(name) || is.null(name)) "Unknown" else name)
})
)
),
if (nrow(places) > 50) {
p(
paste("Showing first 50 of", nrow(places), "restaurants"),
style = "font-size: 12px; color: #666; margin-top: 10px;"
)
}
)
})
}
shinyApp(ui, server)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment