All Projects → p-lr → MapCompose

p-lr / MapCompose

Licence: Apache-2.0 license
A fast, memory efficient Jetpack Compose library to display tiled maps, with support for markers, paths, and rotation.

Programming Languages

kotlin
9241 projects

Projects that are alternatives of or similar to MapCompose

Tileview
TileView is a subclass of android.view.ViewGroup that asynchronously displays, pans and zooms tile-based images. Plugins are available for features like markers, hotspots, and path drawing.
Stars: ✭ 1,447 (+1664.63%)
Mutual labels:  tiling, zoom, tileview
Brutile
BruTile is a .NET library to access tile services like those of OpenStreetMap, MapBox or GeodanMaps.
Stars: ✭ 203 (+147.56%)
Mutual labels:  map, tiling
the-subway-of-china
中国地铁图
Stars: ✭ 104 (+26.83%)
Mutual labels:  map, zoom
maptiles
Download, compose and display map tiles with R
Stars: ✭ 65 (-20.73%)
Mutual labels:  map, tiles
game-map-editor
game-map-editor
Stars: ✭ 17 (-79.27%)
Mutual labels:  map, tiles
Hajk
A modern, full-featured OpenLayers based map viewer and editor
Stars: ✭ 65 (-20.73%)
Mutual labels:  map
smsc
Flexible and scalable GSM Short Message Center (SMSC)
Stars: ✭ 23 (-71.95%)
Mutual labels:  map
korona.ws
🗺 Coronavirus interactive map of Poland
Stars: ✭ 74 (-9.76%)
Mutual labels:  map
sledge-formats
C# parsers and formats for Half-Life 1 and related engines.
Stars: ✭ 35 (-57.32%)
Mutual labels:  map
leaflet-layer-tree-plugin
No description or website provided.
Stars: ✭ 31 (-62.2%)
Mutual labels:  map
kmp-web-wizard
Wizard for Kotlin Multiplatform
Stars: ✭ 164 (+100%)
Mutual labels:  compose
vue-map-chart
VueJS map chart component
Stars: ✭ 27 (-67.07%)
Mutual labels:  map
coin-map-android
Easily find places to spend sats anywhere on the planet
Stars: ✭ 23 (-71.95%)
Mutual labels:  map
squaremap
squaremap is a minimalistic and lightweight world map viewer for Minecraft servers, using the vanilla map rendering style
Stars: ✭ 185 (+125.61%)
Mutual labels:  map
leaflet.minichart
Leaflet.minichart is a leaflet plugin for adding to a leaflet map small animated charts
Stars: ✭ 27 (-67.07%)
Mutual labels:  map
running-on-streetview
Virtual Running on Google Street View.
Stars: ✭ 20 (-75.61%)
Mutual labels:  map
neon
Provides Jetpack Compose support for different image loading libraries.
Stars: ✭ 13 (-84.15%)
Mutual labels:  compose
openfairdb
Open Fair DB is the CreativCommons Backend of Kartevonmorgen.org
Stars: ✭ 53 (-35.37%)
Mutual labels:  map
zoom-ci
Zoom-CI(简称Zoom),是一个轻量易安装的自动化部署工具,支持本地和远程两种项目部署类型。
Stars: ✭ 19 (-76.83%)
Mutual labels:  zoom
mxmaps
An R package for making maps of Mexico
Stars: ✭ 60 (-26.83%)
Mutual labels:  map

Maven Central GitHub License

🎉 News:

  • New gestures added (zoom fling, double tap to zoom, two fingers tap)
  • Marker clustering and lazy-loading. New examples added to the demo app
  • Performance of markers greatly improved

MapCompose

MapCompose is a fast, memory efficient Jetpack compose library to display tiled maps with minimal effort. It shows the visible part of a tiled map with support of markers and paths, and various gestures (flinging, dragging, scaling, and rotating).

An example of setting up:

/* Inside your view-model */
val tileStreamProvider =
    TileStreamProvider { row, col, zoomLvl ->
        FileInputStream(File("path/{$zoomLvl}/{$row}/{$col}.jpg")) // or it can be a remote HTTP fetch
    }

val state: MapState by mutableStateOf(
    MapState(4, 4096, 4096) {
        scroll(0.5, 0.5)
    }.apply {
        addLayer(tileStreamProvider)
        enableRotation()
    }
)

/* Inside a composable */
@Composable
fun MapContainer(
    modifier: Modifier = Modifier, viewModel: YourViewModel
) {
    MapUI(modifier, state = viewModel.state)
}

Inspired by MapView, every aspects of the library have been revisited. MapCompose brings the same level of performance as MapView, with a simplified API.

This project holds the source code of this library, plus a demo app (which is useful to get started). To test the demo, just clone the repo and launch the demo app from Android Studio Canary (for now).

Installation

Add this to your module's build.gradle

implementation 'ovh.plrapps:mapcompose:2.2.5'

Basics

MapCompose is optimized to display maps that have several levels, like this:

Each next level is twice bigger than the former, and provides more details. Overall, this looks like a pyramid. Another common name is "deep-zoom" map. This library comes with a demo app featuring various use-cases such as using markers, paths, map rotation, etc. All examples use the same map stored in the assets, which is a great example of deep-zoom map.

MapCompose can also be used with single level maps.

Usage

With Jetpack Compose, we have to change the way we think about views. In the previous View system, we had references on views and mutated their state directly. While that could be done right, the state often ended-up scattered between views own state and application state. Sometimes, it was difficult to predict how views were rendered because there were so many things to take into account.

Now, the rendering is function of a state. If that state changes, the "view" updates accordingly. The library exposes its API though MapState, which is the only public handle to mutate the state of the "view" (or in Compose terms, "composables"). As its name suggests, MapState also owns the state. Therefore, the composables will always render consistently, even after a device rotation.

In a typical application, you create a MapState instance inside a ViewModel (or whatever component which survives device rotation). Your MapState should then be passed to the MapUI composable. The code sample at the top of this readme shows an example. Then, whenever you need to update the map (add a marker, a path, change the scale, etc.), you invoke APIs on your MapState instance. All public APIs are located under the api package. The following sections provide details on the MapState class, and give examples of how to add markers, callouts, and paths.

MapState

The MapState class expects three parameters for its construction:

  • levelCount: The number of levels of the map,
  • fullWidth: The width of the map at scale 1.0, which is the width of last level,
  • fullHeight: The height of the map at scale 1.0, which is the height of last level

Layers

MapCompose supports layers though the ability to add several tile pyramids. Each level is made of the superposition of tiles from all pyramids at the given level. For example, at the second level (starting from the lowest scale), tiles would look like the image below when three layers are added.

Your implementation of the TileStreamProvider interface (see below) is what defines a tile pyramid. It provides InputStreams of image files (png, jpg). MapCompose will request tiles using the convention that the origin is at the top-left corner. For example, the tile requested with row = 0, and col = 0 will be positioned at the top-left corner.

fun interface TileStreamProvider {
    suspend fun getTileStream(row: Int, col: Int, zoomLvl: Int): InputStream?
}

Depending on your configuration, your TileStreamProvider implementation might fetch local files, as well as performing remote HTTP requests - it's up to you. You don't have to worry about threading, MapCompose takes care of that (the main thread isn't blocked by getTileStream calls). However, in case of HTTP requests, it's advised to create a MapState with a higher than default workerCount. That optional parameter defines the size of the dedicated thread pool for fetching tiles, and defaults to the number of cores minus one. Typically, you would want to set workerCount to 16 when performing HTTP requests. Otherwise, you can safely leave it to its default.

To add a layer, use the addLayer on your MapState instance. There are others APIs for reordering, removing, setting alpha - all dynamically.

Markers

To add a marker, use the addMarker API, like so:

/* Add a marker at the center of the map */
mapState.addMarker("id", x = 0.5, y = 0.5) {
    Icon(
        painter = painterResource(id = R.drawable.map_marker),
        contentDescription = null,
        modifier = Modifier.size(50.dp),
        tint = Color(0xCC2196F3)
    )
}

A marker is composable which you supply (in the example above, it's an Icon). It can be whatever composable you like. A marker does not scale, but it's position updates as the map scales, so it's always attached to the original position. A marker has an anchor point defined - the point which is fixed relatively to the map. This anchor point is defined using relative offsets, which are applied to the width and height of the marker. For example, to have a marker center horizontally to a point, and align at the bottom edge (like a typical map pin would do), you'd pass -0.5f and -1.0f (thus, left position is offset by half the width, and top is offset by the full height). If necessary, an absolute offset expressed in pixels can be applied, in addition to the relative offset.

Markers can be moved, removed, and be draggable. See the following APIs: moveMarker, removeMarker, enableMarkerDrag.

Callouts

Callouts are typically message popups which are, like markers, attached to a specific position. However, they automatically dismiss on touch down (this is the default behavior, which can be changed). To add a callout, use addCallout.

Callouts can be programmatically removed (if automatic dismiss was disabled).

Paths

To add a path, follow these three steps:

// 1. Get a PathDataBuilder
val builder: PathDataBuilder = mapState.makePathDataBuilder()

// 2. Build the path
for (point in points) {
    builder.addPoint(point.x, point.y)
}
val pathData = builder.build()

// 3. Use the API
mapState.addPath("pathName", pathData, color = Color(0xFF448AFF), width = 12.dp)

It's important to note that the only way to get a PathDataBuilder is by using the makePathDataBuilder function. Once you've built your PathData instance, you can use the use the addPath API.

State change listener

In order to get notified whenever the state (scale, scroll, rotation) changes, you can register a callback using setStateChangeListener API:

mapState.setStateChangeListener {
   println("scale: $scale, scroll: $scroll, rotation: $rotation")
}

To unregister, use removeStateChangeListener().

Animate state change

It's pretty common to programmatically animate the scroll and/or the scale, or even the rotation of the map.

scroll and/or scale animation

When animating the scale, we generally do so while maintaining the center of the screen at a specific position. When animating the scroll position, we can do so with or without animating the scale altogether, using scrollTo and snapScrollTo.

rotation animation

For animating the rotation while keeping the current scale and scroll, use the rotateTo API.

Both scrollTo and rotateTo are suspending functions. That means you know exactly when an animation finishes, and you can easily chain animations inside a coroutine.

// Inside a ViewModel
viewModelScope.launch {
    mapState.scrollTo(0.8, 0.8, destScale = 2f)
    mapState.rotateTo(180f, TweenSpec(2000, easing = FastOutSlowInEasing))
}

For a detailed example, see the "AnimationDemo".

Design changes and differences with MapView

  • In MapView, you had to define bounds before you could add markers. There's no more such concept in MapCompose. Now, coordinates are normalized. For example, (x=0.5, y=0.5) is a point located at the center of the map. Normalized coordinates are easier to reason about, and application code can still translate this coordinate system to a custom one.

  • In MapView, you had to build a configuration and use that configuration to create a MapView instance. There's no such thing in MapCompose. Now, you create a MapState object with required parameters.

  • A lot of things which couldn't change after MapView configuration can now be changed dynamically in MapCompose. For example, the zIndex of a marker, or the minimum scale mode can be changed at runtime.

Difference with 1.x version

  • There's now a way to set initial values for various properties such as scroll, scale, etc using the InitialValuesBuilder in the MapState constructor. To produce similar behavior in 1.x, one had to launch a coroutine right after MapState creation - which wasn't perfect since some undesired tile loading could happen between the initialization and the destination state.

  • Having a TileStreamProvider at MapState construction is no longer mandatory. TileStreamProviders are now added using the addLayer api, which is completely dynamic.

  • While 1.x version had a non-suspending TileStreamProvider, 2.x greatly benefits from the new suspend version. If you're using a library like Retrofit to perform remote http fetch (and suspend calls), tile loading will be optimal since all layers are fetched concurrently. That was already the case in 1.x, but not thanks to suspending calls.

Contributors

Marcin (@Nohus) has contributed and fixed some issues. He also thoroughly tested the new layers feature – which made MapCompose better.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].