Created
November 5, 2025 17:27
-
-
Save walkerke/4a10a8fbb1658493de4b8e8fe08d4968 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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