Walkers

repository·main·Indexed 42 days ago

https://github.com/podusowski/walkers

A slippy maps widget for the egui Rust GUI framework providing raster and vector map rendering. It supports tile sources including OpenStreetMap, Mapbox, and offline PMTiles, as well as GeoJSON and KML layers via walkers_extras.

Tokens
15.5K
Snippets
59
Records
77
Agent score
54%

What's inside Walkers

  1. Use Hypermocker for async HTTP mocking

    main

    Hypermocker is an HTTP mocking library designed for async programming. It uses an 'anticipate' pattern to handle requests.

    By default, Hypermocker will panic if an unanticipated request is made to the server. To prevent this, use the anticipate function to register a specific path.

    Unlike traditional mock servers, an anticipated request does not respond immediately. Instead, the request remains in an active state, allowing you to perform other asynchronous tasks before manually triggering the response using request.respond().

    tokio_test::block_on(async {
        // Bind HTTP server to random, free port.
        let server = hypermocker::Server::bind().await;
    
        // Tell the server not to panic on such request.
        let mut request = server.anticipate("/api/article").await;
    
        // Do some work, while HTTP request remains in progress.
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    
        // Send the response.
        request.respond(b"hello").await;
    });
  2. Quick start with Walkers in egui

    main

    Walkers is a slippy maps widget for egui. To implement a map, you need to manage three main objects:

    1. Tiles: Handles downloading and caching images from a tile provider (e.g., HttpTiles).
    2. MapMemory: Maintains the internal state of the map widget.
    3. Map: The actual egui widget used in your ui loop.

    To initialize, use HttpTiles::new(source, egui_ctx) where source is a provider like OpenStreetMap. Use lon_lat(longitude, latitude) to set the initial position.

    use walkers::{HttpTiles, Map, MapMemory, Position, sources::OpenStreetMap, lon_lat};
    use egui::{Context, CentralPanel};
    use eframe::{App, Frame};
    
    struct MyApp {
        tiles: HttpTiles,
        map_memory: MapMemory,
    }
    
    impl MyApp {
        fn new(egui_ctx: Context) -> Self {
            Self {
                tiles: HttpTiles::new(OpenStreetMap, egui_ctx),
                map_memory: MapMemory::default(),
            }
        }
    }
    
    impl App for MyApp {
        fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut Frame) {
            ui.add(Map::new(
                Some(&mut self.tiles),
                &mut self.map_memory,
                lon_lat(17.03664, 51.09916)
            ));
        }
    }
  3. Anticipate and respond to HTTP requests with Hypermocker

    main

    To mock a specific endpoint without causing a panic, use server.anticipate(path). This returns a request object that holds the connection open. You can then call request.respond(body) to send the mocked response back to the client at a controlled point in your test execution.

    let mut request = server.anticipate("/api/article").await;
    // ... perform other async work ...
    request.respond(b"hello").await;
  4. Use offline maps and GeoJSON data

    main

    Walkers supports offline maps via the .pmtiles format and experimental support for local .geojson files.

    If you are using the repository's development environment with just, you can fetch sample data using:

    • PMTiles (Dolnośląskie region): just protomaps-dolnoslaskie
    • GeoJSON (Hiking trails): just overpass-trails-dolnoslaskie
  5. Core types and components in Walkers

    main

    The walkers library provides a set of core types for map rendering and tile management. Key components include:

    • Map: The primary map widget/instance.
    • Options: Configuration for the map.
    • Style: Defines the visual appearance of map layers.
    • Tiles and Tile: Represent the map data being rendered.
    • HttpTiles and LocalTiles: Providers for fetching tiles via HTTP or from local storage.
    • PmTiles: Support for PMTiles (when the pmtiles feature is enabled).
    • Position, lat_lon, and lon_lat: Utilities for handling geographic coordinates.
    • Plugin: An interface for extending map functionality.
  6. Configure KmlLayer rendering styles

    main

    The KmlLayer uses the Style object to determine how KML elements are rendered. It supports two primary layer types:

    • Layer::Line: Renders polygon boundaries (outer and inner holes) as closed lines with a black stroke.
    • Layer::Circle: Renders KML points as filled green circles with a black stroke.

    Unsupported style layers will trigger a warning at runtime.

  7. Manage location data with the Journal and Place types

    main

    The wanderers package provides a Journal system for tracking locations using a GeoJSON-based storage format.

    • Place: Represents a single location. It contains a position (using walkers::Position), a name (String), and preserves any additional GeoJSON properties in an unknown_properties field to ensure data is not lost during load-save cycles.
    • Journal: A collection of Place objects associated with a specific file path. It handles loading from and saving to the filesystem.

    The journal format is a GeoJSON FeatureCollection of Point features. Non-point features (like lines or polygons) are automatically skipped during loading.

    use wanderers::{Journal, Place};
    use walkers::{lon_lat, Position};
    
    // Create a new place
    let position = lon_lat(17.0321, 51.1101);
    let place = Place::new(position, "Rynek");
    
    // Initialize a journal at a specific path
    let journal = Journal {
        path: "my_journal.geojson".into(),
        places: vec![place],
    };
    
    // Save to disk
    journal.save().expect("Failed to save journal");
    
    // Load from disk
    let loaded_journal = Journal::load("my_journal.geojson").expect("Failed to load journal");
  8. Configure layer source matching with SourceLayer

    main

    The SourceLayer enum determines which layers of a vector tile a style layer draws from. It supports two modes:

    • One(String): Matches exactly one layer name.
    • Many(Vec<String>): Matches any layer name in the list.

    Special Behavior: An empty string or an empty list is treated as "all of them" (is_all() == true). This is useful for sparse overlay tiles where you want the layer to apply to every layer in the tile without scanning every name.

    // Matching a single layer
    let layer: SourceLayer = "roads".into();
    assert!(layer.matches("roads"));
    
    // Matching multiple layers
    let layers: SourceLayer = ["landcover", "landuse"].into();
    assert!(layers.matches("landcover"));
    
    // Matching everything (empty list/string)
    let all: SourceLayer = [].into();
    assert!(all.is_all());
  9. Configure Mapbox support

    main

    To enable Mapbox layers, you must provide a Mapbox access token via the MAPBOX_ACCESS_TOKEN environment variable before building your application.

    # Set the environment variable before building
    export MAPBOX_ACCESS_TOKEN=your_token_here
    # Then run your build/run command
  10. Understand the Center enum and map positioning modes

    main

    The Center enum manages how the map is positioned relative to a reference point (typically a GPS location, referred to as my_position). The map can exist in several states:

    • MyPosition: The default state. The map is centered exactly on the my_position provided to Map::new(). In this state, the map is 'attached' to the user's location.
    • Exact(AdjustedPosition): The map is centered at a specific, fixed position that is not necessarily my_position.
    • Moving { position, velocity, from_detached }: The map is currently being actively dragged by a user gesture.
    • Inertia { position, velocity }: The user has released the drag, but the map continues to move due to simulated inertia.
    • PulledToMyPosition(AdjustedPosition): A transitional state where the map is being automatically animated back toward my_position because the user released the drag while the map was still relatively close to the reference point.

    When a user drags the map, it becomes "detached" from my_position. It remains detached until the user or application explicitly changes the MapMemory::center_mode back to Center::MyPosition.

  11. Anticipate and respond to HTTP requests

    main

    To mock a specific endpoint, use Server::anticipate(url). This returns an AnticipatedRequest object. You must provide a response for every anticipated request, or the server will panic when dropped if unexpected requests occur.

    There are two main ways to handle an AnticipatedRequest:

    1. Provide a response: Use .respond(payload) or .respond_with_status(status) to define what the server should return when the request arrives.
    2. Wait for the request: Use .expect() to asynchronously wait until the actual HTTP request is received by the server. This is useful for verifying that a client actually made the call.
    // 1. Anticipate a request
    let mut anticipated = server.anticipate("/api/data").await;
    
    // 2. Define the response (e.g., with a body)
    anticipated.respond(b"{\"status\":\"ok\"}").await;
    
    // 3. (Optional) Wait for the request to actually happen in the client
    let request = anticipated.expect().await;
    println!("Received request to: {}", request.uri());