{"id":17138241,"url":"https://github.com/p-lr/mapcompose","last_synced_at":"2025-05-16T16:04:46.950Z","repository":{"id":39903410,"uuid":"359208603","full_name":"p-lr/MapCompose","owner":"p-lr","description":"A fast, memory efficient Jetpack Compose library to display tiled maps, with support for markers, paths, and rotation.","archived":false,"fork":false,"pushed_at":"2025-05-10T11:04:59.000Z","size":144628,"stargazers_count":246,"open_issues_count":7,"forks_count":21,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-05-16T16:04:01.559Z","etag":null,"topics":["android","android-library","compose","kotlin-library","map","mapview","tiles","tileview","tiling","zoom"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/p-lr.png","metadata":{"files":{"readme":"Readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2021-04-18T17:21:03.000Z","updated_at":"2025-05-16T06:58:13.000Z","dependencies_parsed_at":"2023-10-16T03:07:34.974Z","dependency_job_id":"6cbd8aee-4727-4a69-b140-26ccec3374f6","html_url":"https://github.com/p-lr/MapCompose","commit_stats":{"total_commits":430,"total_committers":5,"mean_commits":86.0,"dds":0.06744186046511624,"last_synced_commit":"01b8e38749081c8ae8fc6318bf7c2c44c1ada086"},"previous_names":[],"tags_count":79,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/p-lr%2FMapCompose","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/p-lr%2FMapCompose/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/p-lr%2FMapCompose/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/p-lr%2FMapCompose/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/p-lr","download_url":"https://codeload.github.com/p-lr/MapCompose/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254564121,"owners_count":22092121,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","android-library","compose","kotlin-library","map","mapview","tiles","tileview","tiling","zoom"],"created_at":"2024-10-14T20:09:08.076Z","updated_at":"2025-05-16T16:04:41.934Z","avatar_url":"https://github.com/p-lr.png","language":"Kotlin","readme":"[![Maven Central](https://img.shields.io/maven-central/v/ovh.plrapps/mapcompose)](https://central.sonatype.com/artifact/ovh.plrapps/mapcompose)\n[![GitHub License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0)\n[![](https://img.shields.io/badge/ComposeBOM-2025.02.00-brightgreen)](https://developer.android.com/jetpack/compose/bom/bom)\n\n🎉 News:\n- Memory footprint has been dramatically reduced on Android 10 and above, by leveraging [Hardware Bitmaps](https://bumptech.github.io/glide/doc/hardwarebitmaps.html).\n  Software rendering is however still required when there's more than one layer.\n- MapCompose Multiplatform is officially released: https://github.com/p-lr/MapComposeMP \\\n  Works on iOS, MacOS, Windows, Linux, and Android.\n- New path dash pattern api: ability to define a sequence made of dash, dot, and gap.\n- Paths rendering has been improved, and paths are automatically simplified depending on the scale to improve performance\n\n\n# MapCompose\n\nMapCompose is a fast, memory efficient Jetpack compose library to display tiled maps with minimal effort.\nIt shows the visible part of a tiled map with support of markers and paths, and various gestures\n(flinging, dragging, scaling, and rotating).\n\nAn example of setting up:\n\n```kotlin\n/* Inside your view-model */\nval tileStreamProvider = TileStreamProvider { row, col, zoomLvl -\u003e\n    FileInputStream(File(\"path/{$zoomLvl}/{$row}/{$col}.jpg\")) // or it can be a remote HTTP fetch\n}\n\nval state = MapState(4, 4096, 4096).apply {\n    addLayer(tileStreamProvider)\n    enableRotation()\n}\n\n/* Inside a composable */\n@Composable\nfun MapContainer(\n    modifier: Modifier = Modifier, viewModel: YourViewModel\n) {\n    MapUI(modifier, state = viewModel.state)\n}\n```\n\nThis project holds the source code of this library, plus a demo app - which is useful to get started.\nTo test the demo, just clone the repo and launch the demo app from Android Studio.\n\n## Clustering\n\nMarker clustering regroups markers of close proximity into clusters. The video below shows how it works.\n\nhttps://github.com/p-lr/MapCompose/assets/15638794/de48cb1b-396b-44d3-b47a-e3d719e8f38a\n\nThe sample below shows the relevant part of the code. We can still add regular markers (not managed by a clusterer), such as the red marker in the video.\nSee the [full code](demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/MarkersClusteringVM.kt).\n\n```kotlin\n/* Add clusterer */\nstate.addClusterer(\"default\") { ids -\u003e\n   { Cluster(size = ids.size) }\n}\n\n/* Add marker managed by the clusterer */\nstate.addMarker(\n    id = \"marker\",\n    x = 0.2,\n    y = 0.3,\n    renderingStrategy = RenderingStrategy.Clustering(\"default\"),\n) {\n    Marker()\n}\n```\n\nThere's an example in the demo app.\n\n\n## Installation\n\nAdd this to your module's build.gradle\n```groovy\nimplementation 'ovh.plrapps:mapcompose:2.16.1'\n```\n\nStarting with v.2.4.1, the library is using the \n[compose BOM](https://developer.android.com/jetpack/compose/bom/bom). The version of the BOM is\nspecified in the release notes. The demo app shows an example of how to use it.\n\n## Basics\n\nMapCompose is optimized to display maps that have several levels, like this:\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"doc/readme-files/pyramid.png\" width=\"400\"\u003e\n\u003c/p\u003e\n\nEach next level is twice bigger than the former, and provides more details. Overall, this looks like\n a pyramid. Another common name is \"deep-zoom\" map.\nThis library comes with a demo app featuring various use-cases such as using markers, paths,\nmap rotation, etc. All examples use the same map stored in the assets, which is a great example of\ndeep-zoom map.\n\nMapCompose can also be used with single level maps.\n\n### Usage\n\nWith Jetpack Compose, we have to change the way we think about views. In the previous `View`\nsystem, we had references on views and mutated their state directly. While that could be done right,\nthe state often ended-up scattered between views own state and application state. Sometimes, it was\ndifficult to predict how views were rendered because there were so many things to take into account.\n\nNow, the rendering is a function of a state. If that state changes, the \"view\" updates accordingly.\n\nIn a typical application, you create a `MapState` instance inside a `ViewModel` (or whatever\ncomponent which survives device rotation). Your `MapState` should then be passed to the `MapUI`\ncomposable. The code sample at the top of this readme shows an example. Then, whenever you need to\nupdate the map (add a marker, a path, change the scale, etc.), you invoke APIs on your `MapState`\ninstance. As its name suggests, `MapState` also _owns_ the state. Therefore, composables will always\nrender consistently - even after a device rotation.\n\nAll public APIs are located under the [api](mapcompose/src/main/java/ovh/plrapps/mapcompose/api) \npackage. The following sections provide details on the `MapState` class, and give examples of how to\nadd markers, callouts, and paths. All apis should be called from the main thread.\n\n### MapState\n\nThe `MapState` class expects three parameters for its construction:\n* `levelCount`: The number of levels of the map,\n* `fullWidth`: The width of the map at scale 1.0, which is the width of last level,\n* `fullHeight`: The height of the map at scale 1.0, which is the height of last level\n\n### Layers\n\nMapCompose supports layers - e.g it's possible to add several tile pyramids. Each level is made of\nthe superposition of tiles from all pyramids at the given level. For example, at the second level\n(starting from the lowest scale), tiles would look like the image below when three layers are added.\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"doc/readme-files/layer.png\" width=\"200\"\u003e\n\u003c/p\u003e\n\nYour implementation of the `TileStreamProvider` interface (see below) is what defines a tile\npyramid. It provides `InputStream`s of image files (png, jpg). MapCompose will request tiles using\nthe convention that the origin is at the top-left corner. For example, the tile requested with\n`row` = 0, and `col = 0` will be positioned at the top-left corner.\n\n```kotlin\nfun interface TileStreamProvider {\n    suspend fun getTileStream(row: Int, col: Int, zoomLvl: Int): InputStream?\n}\n```\n\nDepending on your configuration, your `TileStreamProvider` implementation might fetch local files,\nas well as performing remote HTTP requests - it's up to you. You don't have to worry about threading,\nMapCompose takes care of that (the main thread isn't blocked by `getTileStream` calls). However, in\ncase of HTTP requests, it's advised to create a `MapState` with a higher than default `workerCount`.\nThat optional parameter defines the size of the dedicated thread pool for fetching tiles, and defaults\nto the number of cores minus one. Typically, you would want to set `workerCount` to 16 when performing\nHTTP requests. Otherwise, you can safely leave it to its default.\n\nTo add a layer, use the `addLayer` on your `MapState` instance. There are others APIs for reordering,\nremoving, setting alpha - all dynamically.\n\n### Markers\n\nTo add a marker, use the [addMarker](https://github.com/p-lr/MapCompose/blob/982caf29ab5e86b58c56812735f60bfe405638ea/mapcompose/src/main/java/ovh/plrapps/mapcompose/api/MarkerApi.kt#L30)\nAPI, like so:\n\n```kotlin\n/* Add a marker at the center of the map */\nmapState.addMarker(\"id\", x = 0.5, y = 0.5) {\n    Icon(\n        painter = painterResource(id = R.drawable.map_marker),\n        contentDescription = null,\n        modifier = Modifier.size(50.dp),\n        tint = Color(0xCC2196F3)\n    )\n}\n```\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"doc/readme-files/marker.png\"\u003e\n\u003c/p\u003e\n\nA marker is a composable that you supply (in the example above, it's an `Icon`). It can be\nwhatever composable you like. A marker does not scale, but it's position updates as the map scales,\nso it's always attached to the original position. A marker has an anchor point defined - the point\nwhich is fixed relatively to the map. This anchor point is defined using relative offsets, which are\napplied to the width and height of the marker. For example, to have a marker centered horizontally \nand aligned at the bottom edge (like a typical map pin would do), you'd pass -0.5f and -1.0f as\nrelative offsets (left position is offset by half the width, and top is offset by the full height).\nIf necessary, an absolute offset expressed in pixels can be applied, in addition to the\nrelative offset.\n\nMarkers can be moved, removed, and be draggable. See the following APIs: [moveMarker](https://github.com/p-lr/MapCompose/blob/2fbf0967290ffe01d63a6c65a3022568ef48b9dd/mapcompose/src/main/java/ovh/plrapps/mapcompose/api/MarkerApi.kt#L72),\n[removeMarker](https://github.com/p-lr/MapCompose/blob/2fbf0967290ffe01d63a6c65a3022568ef48b9dd/mapcompose/src/main/java/ovh/plrapps/mapcompose/api/MarkerApi.kt#L61),\n[enableMarkerDrag](https://github.com/p-lr/MapCompose/blob/2fbf0967290ffe01d63a6c65a3022568ef48b9dd/mapcompose/src/main/java/ovh/plrapps/mapcompose/api/MarkerApi.kt#L89).\n\n### Callouts\n\nCallouts are typically message popups which are, like markers, attached to a specific position.\nHowever, they automatically dismiss on touch down. This default behavior can be changed. \nTo add a callout, use [addCallout](https://github.com/p-lr/MapCompose/blob/2fbf0967290ffe01d63a6c65a3022568ef48b9dd/mapcompose/src/main/java/ovh/plrapps/mapcompose/api/MarkerApi.kt#L220).\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"doc/readme-files/callout.png\"\u003e\n\u003c/p\u003e\n\nCallouts can be programmatically removed (if automatic dismiss was disabled).\n\n### Paths\n\nTo add a path, use the `addPath` api:\n\n```kotlin\nmapState.addPath(\"pathId\", color = Color(0xFF448AFF)) {\n  addPoints(points)\n}\n```\n\nThe demo app shows a complete example.\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"doc/readme-files/path.png\"\u003e\n\u003c/p\u003e\n\n## Animate state change\n\nIt's pretty common to programmatically animate the scroll and/or the scale, or even the rotation of\nthe map.\n\n*scroll and/or scale animation*\n\nWhen animating the scale, we generally do so while maintaining the center of the screen at\na specific position. Likewise, when animating the scroll position, we can do so with or without \nanimating the scale altogether, using [scrollTo](https://github.com/p-lr/MapCompose/blob/08c0f68f654c1ce27a295f3fb6c25e9cf4274de9/mapcompose/src/main/java/ovh/plrapps/mapcompose/api/LayoutApi.kt#L188)\nand [snapScrollTo](https://github.com/p-lr/MapCompose/blob/08c0f68f654c1ce27a295f3fb6c25e9cf4274de9/mapcompose/src/main/java/ovh/plrapps/mapcompose/api/LayoutApi.kt#L161).\n\n*rotation animation*\n\nFor animating the rotation while keeping the current scale and scroll, use the\n[rotateTo](https://github.com/p-lr/MapCompose/blob/08c0f68f654c1ce27a295f3fb6c25e9cf4274de9/mapcompose/src/main/java/ovh/plrapps/mapcompose/api/LayoutApi.kt#L149) API.\n\nBoth `scrollTo` and `rotateTo` are suspending functions. Therefore, you know exactly when\nan animation finishes, and you can easily chain animations inside a coroutine.\n\n```kotlin\n// Inside a ViewModel\nviewModelScope.launch {\n    mapState.scrollTo(0.8, 0.8, destScale = 2f)\n    mapState.rotateTo(180f, TweenSpec(2000, easing = FastOutSlowInEasing))\n}\n```\n\nFor a detailed example, see the \"AnimationDemo\".\n\n## Design changes and differences with MapView\n\n* In MapView, you had to define bounds before you could add markers. There's no such concept\nin MapCompose anymore. Now, coordinates are normalized. For example, (x=0.5, y=0.5) is a point located at\nthe center of the map. Normalized coordinates are easier to reason about, and application code can\nstill translate this coordinate system to a custom one.\n\n* In MapView, you had to build a configuration and use that configuration to create a `MapView`\ninstance. There's no such thing in MapCompose. Now, you create a `MapState` object with required\nparameters.\n\n* A lot of things which couldn't change after MapView configuration can now be changed dynamically\nin MapCompose. For example, the `zIndex` of a marker, or the minimum scale mode can be changed at\nruntime.\n\n## Difference with `1.x` version\n\n* There's now a way to set initial values for various properties such as scroll, scale, etc using\nthe `InitialValuesBuilder` in the `MapState` constructor. To produce similar behavior in 1.x, one\nhad to launch a coroutine right after `MapState` creation - which wasn't perfect since some\nundesired tile loading could happen between the initialization and the destination state.\n\n* Having a `TileStreamProvider` at `MapState` construction is no longer mandatory.\n`TileStreamProvider`s are now added using the `addLayer` api, which is completely dynamic.\n\n* While 1.x version had a non-suspending `TileStreamProvider`, 2.x greatly benefits from the new\nsuspend version. If you're using a library like Retrofit to perform remote http fetch (and suspend\ncalls), tile loading will be optimal since all layers are fetched concurrently. That was already the\ncase in 1.x, but not thanks to suspending calls.\n\n## Contributors\n\nMarcin (@Nohus) has contributed and fixed some issues. He also thoroughly tested the new layers \nfeature – which made `MapCompose` better.\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fp-lr%2Fmapcompose","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fp-lr%2Fmapcompose","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fp-lr%2Fmapcompose/lists"}