[
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor.xml\n/.idea/assetWizardSettings.xml\n.DS_Store\nbuild\n/captures\n.idea\n.externalNativeBuild\npublish-mavenCentral.sh\ncompile_and_install_demo_release.sh\n/publishErrors.txt\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Readme.md",
    "content": "[![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-2026.02.01-brightgreen)](https://developer.android.com/jetpack/compose/bom/bom)\n\n🎉 News:\n- `3.1.0` now supports infinite scroll (#119).\n- `3.0.0` is released. The library is now capable of handling much bigger maps, such as world-wide\nOpenStreetMap at zoom level 17 with a maximum scale of 8. See [Migrate from 2.x.x](#migrate-from-2xx).\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- MapCompose Multiplatform is officially released: https://github.com/p-lr/MapComposeMP \\\n  Works on iOS, MacOS, Windows, Linux, and Android.\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 ->\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 ->\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:3.2.7'\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<p align=\"center\">\n<img src=\"doc/readme-files/pyramid.png\" width=\"400\">\n</p>\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\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. \n\nAll apis should be invoked 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<p align=\"center\">\n<img src=\"doc/readme-files/layer.png\" width=\"200\">\n</p>\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<p align=\"center\">\n<img src=\"doc/readme-files/marker.png\">\n</p>\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<p align=\"center\">\n<img src=\"doc/readme-files/callout.png\">\n</p>\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<p align=\"center\">\n<img src=\"doc/readme-files/path.png\">\n</p>\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## Migrate from `2.x.x`\n\n* All apis taking the scale as parameter now require `Double` values instead of `Float`.\n\n* A few low-level apis taking the scroll in pixels now require `Double` values instead of `Float`.\n\n* The `addMarker` api now longer has the parameter `clipShape`.\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"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nbuildscript {\n    ext {\n        kotlin_version = \"2.2.21\"\n        coroutine_version = '1.10.2'\n    }\n    repositories {\n        google()\n        mavenCentral()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:9.2.0'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n"
  },
  {
    "path": "demo/.gitignore",
    "content": "/build"
  },
  {
    "path": "demo/build.gradle",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    id 'com.android.application'\n    id 'kotlin-android'\n    id \"org.jetbrains.kotlin.plugin.compose\" version \"$kotlin_version\"\n    id 'kotlin-parcelize'\n}\n\nandroid {\n    compileSdk = 36\n\n    defaultConfig {\n        applicationId \"ovh.plrapps.mapcompose.demo\"\n        minSdk = 23\n        targetSdk = 36\n        versionCode 1\n        versionName \"1.0\"\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        vectorDrawables {\n            useSupportLibrary = true\n        }\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_17\n        targetCompatibility JavaVersion.VERSION_17\n    }\n    buildFeatures {\n        compose = true\n    }\n    namespace = 'ovh.plrapps.mapcompose.demo'\n}\n\nkotlin {\n    compilerOptions {\n        freeCompilerArgs.addAll([\n                '-Xopt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi',\n                '-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi',\n        ])\n        jvmTarget = JvmTarget.JVM_17\n    }\n}\n\ndependencies {\n\n    implementation 'androidx.core:core-ktx:1.18.0'\n    implementation 'androidx.appcompat:appcompat:1.7.1'\n\n    // Compose - See https://developer.android.com/jetpack/compose/setup#bom-version-mapping\n    implementation platform('androidx.compose:compose-bom:2026.04.01')\n    implementation \"androidx.compose.ui:ui\"\n    implementation \"androidx.compose.material:material\"\n    implementation \"androidx.compose.material3:material3\"\n    implementation \"androidx.compose.ui:ui-tooling-preview\"\n    debugImplementation \"androidx.compose.ui:ui-tooling\"\n\n    implementation 'androidx.navigation:navigation-compose:2.9.8'\n    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0'\n    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.10.0'\n    implementation project(':mapcompose')\n    testImplementation 'junit:junit:4.13.2'\n}"
  },
  {
    "path": "demo/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "demo/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.MyApplication\">\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\"\n            android:windowSoftInputMode=\"adjustResize\"\n            android:theme=\"@style/Theme.MyApplication\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>"
  },
  {
    "path": "demo/src/main/assets/tracks/track1.txt",
    "content": "0.6044638908531056,0.23086354170969253\n0.6044638908531056,0.230781790986988\n0.6075361613235069,0.22873802292007173\n0.6100394928179091,0.23168104893644745\n0.6137944900595099,0.23225330399520489\n0.6170374422227098,0.2303730373736396\n0.6207355455667128,0.230945292432339\n0.624376755013113,0.23135404604574544\n0.628074858357116,0.23168104893644745\n0.6307488715443168,0.23429707206217923\n0.6302368264659158,0.23797585458272147\n0.6306919776467139,0.24165463710326368\n0.6304644020563174,0.24533341962380592\n0.6329677335507197,0.24811294419488875\n0.6345038687859178,0.25154647454737544\n0.6371778819731186,0.2539989962276982\n0.640762197521921,0.25514350634521316\n0.644460300865924,0.2553887585132687\n0.6474756774387223,0.25326323972364784\n0.6511168868851224,0.2524457324968349\n0.654473626843528,0.250974219488618\n0.6572614278259298,0.24852169780823716\n0.6569200644403274,0.2522004803287794\n0.6597078654227291,0.24974795864845659\n0.66238187860993,0.25236398177413033\n0.6607319555795311,0.2557157614039706\n0.6585699874707263,0.25874053814310893\n0.6622680908147293,0.2582500338069979\n0.665567936875532,0.25653326863078363\n0.6686971012435311,0.25457125128645564\n0.6675592232915333,0.25816828308435147\n0.6662506636467317,0.2617653148821892\n0.6641455894355297,0.2647900916213275\n0.665567936875532,0.2682236219738142\n0.66602308805633,0.2718206537716519\n0.6679574805747335,0.2750089319561412\n0.6685833134483302,0.2786877144766834\n0.6696642975027303,0.2822847462745211\n0.6711435388403354,0.28563652590436134\n0.6748416421843334,0.28604527951770975\n0.6752967933651366,0.28972406203825196\n0.6733624008467333,0.2929123402227413\n0.671314220533134,0.2959371169618215\n0.6712573266355312,0.2996976502050683\n0.6736468703347327,0.3025589254987975\n0.6751830055699358,0.30591070512863777\n0.6756950506483369,0.3096712383718265\n0.6719969473043338,0.30983473981717746\n0.6691522524243343,0.31220551077485376\n0.6700625547859305,0.31580254257274953\n0.672964143563533,0.3181733135304258\n0.6755812628531361,0.32070758593345317\n0.6778001248595338,0.32373236267253336\n0.6796207295827364,0.3270023915797272\n0.6781983821427341,0.3304359219322139\n0.6746709604915347,0.3292096610920525\n0.6726796740755335,0.33231618855383727\n0.6712004327379333,0.335749718906324\n0.6747847482867356,0.33673072757842987\n0.6725658862803328,0.3396737535948636\n0.6764346713171344,0.3397555043175682\n0.6800189868659366,0.3412270173257851\n0.6838308780051404,0.3409817651577296\n0.6817258037939384,0.34400654189686786\n0.678767321118738,0.3461320606864887\n0.6774587614739365,0.34964734176167994\n0.6754674750579353,0.3527538692234647\n0.6733624008467333,0.35577864596254494\n0.6696642975027303,0.3556151445171939\n0.6663075575443296,0.35398013006362605\n0.6626663480979295,0.3545523851223836\n0.6598785471155277,0.35700490680276437\n0.6576027912115271,0.3598661820964936\n0.6592527142419259,0.3632179617263339\n0.658683775265927,0.3668967442468762\n0.6565787010547249,0.3699215209859564\n0.655554610897928,0.37351855278379403\n0.6580010484947273,0.37629807735487686\n0.6572614278259298,0.37989510915277264\n0.6594233959347295,0.38300163661455744\n0.6608457433747269,0.3863534162443396\n0.6636335443571286,0.3888876886473669\n0.6673316477011316,0.3892146915381269\n0.6706314937619344,0.3908497059916948\n0.6719969473043338,0.3942832363441815\n0.6756950506483369,0.3942832363441815\n0.6789948967091346,0.3959182507977494\n0.6813844404083361,0.39877952609147865\n0.685082543752339,0.3984525232007767\n0.6886668593011412,0.39755326525131723\n0.6923649626451444,0.39722626236061526\n0.6948114002419437,0.4000875376543445\n0.6977129890195461,0.4023765578893744\n0.7014110923635442,0.40286706222542734\n0.7051091957075472,0.4029488129481319\n0.7060194980691484,0.39935178115023606\n0.7067022248403481,0.39567299862969385\n0.7091486624371475,0.39297522478131564\n0.7122778268051516,0.39093145671434126\n0.7137570681427517,0.38757967708455904\n0.7152932033779499,0.38422789745471875\n0.718934412824355,0.38406439600936776\n0.7225756222707551,0.3834921409506103\n0.7259892561267536,0.38193887721968894\n0.7286632693139545,0.3794046048166616\n0.7302562984467555,0.37605282518687944\n0.7325889482483592,0.37319154989309206\n0.7357750065139611,0.3712295325488223\n0.7380507624179619,0.36836825725509303\n0.7416919718643619,0.3691857644818479\n0.7451624996179633,0.3705755267673603\n0.7484054517811631,0.37221054122098624\n0.7518190856371667,0.373682054229145\n0.7555171889811698,0.37417255856525605\n0.7592152923251677,0.37458131217866253\n0.7625720322835734,0.3730280484477411\n0.7644495309043738,0.3698397702632518\n0.7672373318867706,0.36738724858292904\n0.7697406633811728,0.36460772401184627\n0.7730974033395734,0.3629727095582784\n0.7764541432979791,0.36150119655006147\n0.7798677771539776,0.36002968354184456\n0.7827124720339772,0.3577406633068728\n0.7851589096307816,0.35487938801308544\n0.783907243883578,0.35144585766059877\n0.7822004269555812,0.34817582875346303\n0.7803798222323787,0.3449057998463273\n0.7783316419187796,0.341881023107189\n0.7762265677075776,0.33877449564540424\n0.7741783873939734,0.335749718906324\n0.7717319497971741,0.3330519450579457\n0.7695130877907762,0.3300271683188074\n0.7670666501939719,0.32724764374772464\n0.764392637006771,0.3247133713446973\n0.7624582444883726,0.3215250931602661\n0.7598411251987696,0.3189090700345343\n0.7562568096499673,0.3178463106397239\n0.7529000696915666,0.31629304690880244\n0.7503398442995666,0.31367702378307066\n0.747438255521964,0.3114697542707453\n0.744252197256362,0.30958948764918\n0.7420902291475622,0.30656471091004167\n0.7419764413523613,0.3028859283894995\n0.7447642423347631,0.30043340670917673\n0.7473813616243662,0.2978173835834449\n0.7497140114259647,0.2948743575670111\n0.7525587063059643,0.29258533733203934\n0.7551758255955674,0.289887563483603\n0.7576222631923667,0.28710803891257825\n0.7600118068915682,0.28424676361879087\n0.7617755177171729,0.2809767347116552\n0.7649046820851719,0.2789329666446808\n0.7687734671219736,0.2781154594179259\n0.7686027854291749,0.27427317545203267\n0.769114830507576,0.27059439293149046\n0.7724715704659767,0.26912287992327355\n0.7756576287315786,0.26658860752024627\n0.777592021249977,0.2633185786131106\n0.7804936100275794,0.26094780765543424\n0.7827693659315802,0.25767777874824044\n0.7836227743955786,0.2539172455050518\n0.7827124720339772,0.24999321081645404\n0.7837934560883772,0.24623267757326536\n0.7843055011667782,0.24255389505272312\n0.7848744401427822,0.23879336180953445\n0.7841917133715774,0.23503282856628768\n0.7835089866003777,0.23127229532309904\n0.7831676232147804,0.2275935128025568\n0.7868657265587834,0.22685775629844837\n0.786524363173181,0.22317897377790613\n0.7868657265587834,0.21941844053465936\n0.7886294373843831,0.21614841162752363\n0.7906776176979822,0.21304188416573885\n0.7914172383667848,0.20936310164519664\n0.793408524782786,0.2062565741834119\n0.7952860234035866,0.20306829599898066\n0.7980738243859831,0.20053402359595338\n0.8004633680851847,0.19767274830216605\n0.8024546545011859,0.19448447011773484\n0.8055269249715871,0.1924407020507605\n0.8079733625683915,0.18933417458897572\n0.8110456330387926,0.18729040652200138\n0.8143454790995904,0.1856553920684335\n0.8181004763411963,0.18557364134572896\n0.8216278979923956,0.18688165290859488\n0.8252122135411978,0.18573714279113804\n0.8261225159027992,0.18214011099324226\n0.8293085741683961,0.18025984437161888\n0.8330066775123991,0.17976934003556594\n0.8363065235732018,0.18148610521183833\n0.837785764910802,0.1848378848416205\n0.8392650062484022,0.18827141519416532\n0.8415407621524029,0.18532838917773156\n0.8419390194356031,0.18156785593448477\n0.8430200034900031,0.17797082413664708\n0.8456371227796061,0.18066859798502535\n0.8466612129364032,0.18426562978292113\n0.8483111359668071,0.18091385015308087\n0.8485387115572087,0.17723506763253866\n0.8482542420692042,0.17347453438934998\n0.8463767434484036,0.17028625620486068\n0.8471732580148043,0.166689224407023\n0.8480835603764055,0.16301044188648076\n0.8480266664788076,0.15933165936593852\n0.8483680298644051,0.15557112612269178\n0.8453526532916068,0.15344560733307094\n0.848880074942806,0.15213759577020505\n0.8485387115572087,0.1484588132496628\n0.848424923762008,0.14469828000647417\n0.8492783322260062,0.1411012482085784\n0.8455802288820032,0.14101949748593193\n0.8419959133332061,0.14191875543539134\n0.8385253855796047,0.1431450162755527\n0.8357944784948007,0.140528993149879\n0.832494632433998,0.1388122279736066\n0.8287965290900001,0.1386487265282556\n0.827146606059596,0.1420005061580959\n0.8247001684627968,0.14469828000647417\n0.8210589590163967,0.14551578723322905\n0.8179866885459955,0.1475595553002034\n0.8142316913043947,0.14764130602290793\n0.8105904818579894,0.14829531180431182\n0.8068923785139915,0.1487858161403648\n0.8033649568627871,0.15001207698058425\n0.7996668535187842,0.1502573291485817\n0.7996099596211863,0.146496795905393\n0.7962532196627857,0.14813181035896086\n0.798187612181184,0.1449435321744716\n0.7949446600179841,0.14322676699825726\n0.7943757210419852,0.1394662337550105\n0.7913603444691819,0.14167350326733585\n0.7888001190771817,0.1443712771157722\n0.7852158035283795,0.14518878434252708\n0.7814608062867786,0.14543403651058262\n0.7785023236115782,0.14764130602290793\n0.7748611141651781,0.1484588132496628\n0.771788843694777,0.15066608276198817\n0.7682614220435726,0.15172884215679858\n0.7646202125971725,0.15270985082896252\n0.7609790031507725,0.1535273580557755\n0.7576791570899696,0.15508062178663884\n0.758589459451571,0.15148358998880113\n0.7557447645715664,0.1539361116691239\n0.7525587063059643,0.15573462756804274\n0.7499415870163664,0.1584324014164791\n0.7485761334739669,0.1618659317689658\n0.7472675738291653,0.16538121284415708\n0.7465848470579657,0.16897824464199476\n0.7449349240275618,0.1722482735491305\n0.7412937145811617,0.17322928222129444\n0.7376525051347615,0.1739650387254029\n0.7339544017907585,0.17445554306145583\n0.7312803886035576,0.17698981546448314\n0.7288339510067582,0.17976934003556594\n0.7254772110483576,0.18148610521183833\n0.7223480466803535,0.18352987327881268\n0.7193326701075553,0.18581889351378447\n0.7162035057395512,0.1877809108580543\n0.7137570681427517,0.19072393687448808\n0.7115382061363489,0.19366696289092186\n0.708352147870747,0.19562898023519168\n0.7055074529907474,0.19808150191557253\n0.7022076069299447,0.19979826709184492\n0.6994766998451458,0.19726399468881764\n0.6974285195315417,0.19423921794967933\n0.6937873100851415,0.19366696289092186\n0.6913408724883422,0.19080568759719263\n0.6884392837107397,0.19317645855486892\n0.6851394376499369,0.1948114730084368\n0.6822947427699373,0.1924407020507605\n0.6786535333235372,0.1916231948239475\n0.6749554299795343,0.19178669626929848\n0.6715417961235357,0.1933399600002199\n0.6692091463219321,0.19620123529394914\n0.6671040721107301,0.19930776275573392\n0.6644300589235291,0.2018420351588193\n0.6634059687667322,0.20543906695665698\n0.6613008945555301,0.20846384369573723\n0.6582286240851289,0.2105893624854161\n0.6548718841267283,0.21214262621627944\n0.6512306746803231,0.2127148812750369\n0.6475325713363251,0.21345063777914536\n0.6441189374803216,0.2148404000646577\n0.6405346219315193,0.21598491018217264\n0.6371778819731186,0.21761992463574054\n0.6337642481171152,0.21917318836666197\n0.6304644020563174,0.22088995354287627\n0.6272783437907155,0.2229337216098506\n0.6244336489107158,0.22530449256752694\n0.6215889540307112,0.22783876497055422\n0.6189149408435103,0.2303730373736396\n0.6157857764755112,0.23110879387774805"
  },
  {
    "path": "demo/src/main/assets/tracks/track2.txt",
    "content": "0.5442701471922805,0.22415998245007007\n0.5441563593970797,0.22415998245007007\n0.5443839349874813,0.22407823172736555\n0.5443839349874813,0.223996481004661\n0.5442132532946826,0.22391473028201458\n0.5443839349874813,0.22407823172736555\n0.5443270410898784,0.22424173317271653\n0.5441563593970797,0.22415998245007007\n0.5443270410898784,0.2244052346180675\n0.5441563593970797,0.22456873606341848\n0.5442132532946826,0.22456873606341848\n0.5442132532946826,0.22448698534077202\n0.544042571601879,0.2243234838954211\n0.543530526523478,0.2244052346180675\n0.5431322692402777,0.2243234838954211\n0.542961587547479,0.22407823172736555\n0.5429046936498813,0.223996481004661\n0.5420512851858778,0.2226884694418532\n0.5419943912882799,0.22260671871914864\n0.5415392401074818,0.2222797158284467\n0.5409703011314778,0.22170746076968925\n0.5409134072338799,0.2216257100469847\n0.5401737865650773,0.2212169564336363\n0.5398893170770779,0.2212169564336363\n0.5400599987698765,0.22129870715628275\n0.5400599987698765,0.2213804578789873\n0.5401737865650773,0.2212169564336363\n0.5399462109746808,0.22088995354287627\n0.5396617414866762,0.22064470137487885\n0.539263484203481,0.2205629506521743\n0.5392065903058781,0.22048119992952786\n0.5388083330226778,0.22015419703876782\n0.5378980306610767,0.2199089448707704\n0.5377842428658759,0.21982719414806587\n0.5364756832210794,0.2193366898120129\n0.5364187893234764,0.2193366898120129\n0.5352809113714786,0.21884618547595996\n0.5352809113714786,0.21892793619860643\n0.5351102296786748,0.21876443475325547\n0.535053335781077,0.21860093330790448\n0.5349395479858762,0.21770167535844506\n0.5349395479858762,0.21761992463574054\n0.5347688662930775,0.21721117102239212\n0.534541290702676,0.21688416813163208\n0.5342568212146765,0.2163119130728746\n0.5342568212146765,0.21623016235022818\n0.5336878822386776,0.21492215078736224\n0.5336309883410747,0.2148404000646577\n0.5333465188530752,0.21451339717395576\n0.5328913676722771,0.21353238850184988\n0.5328344737746742,0.21345063777914536\n0.5327775798770763,0.21320538561108984\n0.5324931103890769,0.2128783827203879\n0.5324931103890769,0.21263313055239044\n0.5323224286962782,0.21255137982968594\n0.5321517470034745,0.21214262621627944\n0.5320948531058766,0.212060875493633\n0.5317534897202741,0.21157037115752197\n0.5313552324370739,0.21116161754417356\n0.5311845507442753,0.21132511898952455\n0.5311845507442753,0.21132511898952455\n0.530843187358673,0.21132511898952455\n0.5307293995634772,0.211406869712171\n0.5303880361778749,0.212060875493633\n0.530331142280277,0.21214262621627944\n0.5301035666898753,0.2128783827203879\n0.5296484155090723,0.2133688870564989\n0.5295915216114744,0.21345063777914536\n0.5293070521234748,0.21385939139255183\n0.5290225826354754,0.21418639428325378\n0.5288519009426768,0.2144316464513093\n0.5287950070450739,0.2145951478966603\n0.528738113147476,0.21467689861930675\n0.5289087948402746,0.21492215078736224\n0.5288519009426768,0.21508565223271323\n0.5289087948402746,0.21533090440076874\n0.5286243253522752,0.21582140873682168\n0.5286243253522752,0.21598491018217264\n0.5286243253522752,0.21623016235022818\n0.5285105375570743,0.2167206666862811\n0.5281122802738741,0.21721117102239212\n0.5280553863762762,0.21729292174503856\n0.5279415985810755,0.21761992463574054\n0.5275433412978752,0.21851918258519992\n0.5274864474002723,0.21860093330790448\n0.5268606145266755,0.21950019125736392\n0.5267468267314747,0.21982719414806587\n0.5266899328338718,0.2199089448707704\n0.526576145038671,0.22015419703876782\n0.5262347816530737,0.2205629506521743\n0.526064099960275,0.2209717042655808\n0.5259503121650742,0.2210534549882853\n0.526007206062672,0.2212169564336363\n0.5259503121650742,0.2212169564336363\n0.5257796304722705,0.22170746076968925\n0.5254951609842711,0.2218709622150402\n0.5251537975986738,0.22195271293774474\n0.5249262220082722,0.22219796510574216\n0.5248693281106743,0.22219796510574216\n0.5244710708274741,0.22260671871914864\n0.5243003891346704,0.22260671871914864\n0.52390213185147,0.2229337216098506\n0.5237883440562744,0.2229337216098506\n0.5227073600018693,0.2237512288366636\n0.5225935722066736,0.22383297955931003\n0.5221953149234734,0.223996481004661\n0.5220246332306696,0.223996481004661\n0.5212850125618721,0.22424173317271653\n0.5211712247666712,0.22424173317271653\n0.5208867552786718,0.2243234838954211\n0.520204028507472,0.22407823172736555\n0.5198057712242719,0.22415998245007007\n0.519748877326669,0.22415998245007007\n0.5193506200434688,0.22424173317271653\n0.5188954688626706,0.22407823172736555\n0.5184403176818725,0.22407823172736555\n0.5183834237842696,0.22407823172736555\n0.5178144848082706,0.22424173317271653\n0.5170179702418701,0.22407823172736555\n0.5169041824466694,0.22415998245007007\n0.5166766068562677,0.2243234838954211\n0.5154249411090691,0.2243234838954211\n0.5153111533138683,0.22424173317271653\n0.5142301692594684,0.2244052346180675\n0.513945699771469,0.2243234838954211\n0.5138319119762681,0.2243234838954211\n0.5131491852050685,0.22407823172736555\n0.5125233523314666,0.22407823172736555\n0.5124664584338688,0.22407823172736555\n0.5122388828434672,0.22415998245007007\n0.5114423682770667,0.22407823172736555\n0.5112147926866651,0.22415998245007007\n0.5111010048914694,0.22424173317271653\n0.5101907025298681,0.2243234838954211\n0.5098493391442657,0.2244052346180675\n0.5097924452466678,0.22448698534077202\n0.5090528245778652,0.22424173317271653\n0.5085407794994641,0.2244052346180675\n0.5084838856018662,0.2244052346180675\n0.5074597954450643,0.22456873606341848\n0.5074029015474664,0.22465048678612304\n0.5073460076498635,0.22473223750876944\n0.5071753259570648,0.224977489676825\n0.507061538161864,0.22489573895417853\n0.5067770686738645,0.22448698534077202\n0.5063219174930664,0.2244052346180675\n0.5062081296978657,0.2244052346180675\n0.5047857822578633,0.22415998245007007\n0.5047288883602654,0.22407823172736555\n0.5043306310770651,0.22391473028201458\n0.5039323737938649,0.2244052346180675\n0.5039323737938649,0.22415998245007007\n0.5041030554866636,0.22407823172736555\n0.5042168432818643,0.22383297955931003\n0.5041599493842664,0.22391473028201458\n0.5042168432818643,0.22383297955931003\n0.5042737371794622,0.22407823172736555\n0.5042168432818643,0.22415998245007007\n0.504444418872266,0.22415998245007007\n0.504387524974663,0.2243234838954211\n0.5042168432818643,0.22424173317271653\n0.5037616921010662,0.22424173317271653\n0.5037047982034633,0.2243234838954211\n0.5033065409202631,0.22456873606341848\n0.5028513897394649,0.22522274184488048\n0.502794495841862,0.22530449256752694\n0.5019410873778637,0.22587674762628437\n0.5018272995826629,0.22620375051698632\n0.5018272995826629,0.22636725196233734\n0.5016566178898643,0.22653075340774637\n0.5013152545042618,0.22669425485309738\n0.5010307850162624,0.22702125774379933\n0.5008601033234638,0.22718475918915032\n0.5008601033234638,0.2274300113572058\n0.5008032094258609,0.22751176207985227\n0.4997222253714609,0.22816576786131426\n0.4996084375762601,0.22824751858396072\n0.4993808619858635,0.22832926930666525\n0.4992101802930598,0.22832926930666525\n0.4989257108050604,0.22800226641596327\n0.49829987793146363,0.22816576786131426\n0.49824298403386064,0.22824751858396072\n0.4979585145458612,0.22832926930666525\n0.4979016206482633,0.22824751858396072\n0.49773093895545967,0.22783876497055422\n0.4976171511602639,0.22775701424790776\n0.49744646946746013,0.2275935128025568\n0.4972188938770637,0.22767526352520323\n0.4971619999794607,0.22767526352520323\n0.4968775304914613,0.22783876497055422\n0.4965361671058589,0.22792051569325877\n0.4961948037202616,0.22816576786131426\n0.4957396525394584,0.22824751858396072\n0.49562586474426273,0.22832926930666525\n0.4942604112018582,0.2284927707520162\n0.4942035173042604,0.22841102002931168\n0.4936914722258593,0.22832926930666525\n0.49289495765945884,0.2284927707520162\n0.4927811698642581,0.2284927707520162\n0.4913019285266578,0.22832926930666525\n0.49124503462905994,0.22832926930666525\n0.49056230785786026,0.22857452147466267\n0.4900502627794592,0.22881977364271816\n0.4899933688818563,0.22890152436542271\n0.48891238482745636,0.22988253303752856\n0.48885549092985847,0.22988253303752856\n0.4886279153394568,0.2301277852055841\n0.48817276415865873,0.23127229532309904\n0.48811587026105585,0.23127229532309904\n0.4878882946706593,0.23192630110450294\n0.4878882946706593,0.23225330399520489\n0.4877176129778556,0.2326620576086114\n0.4876038251826548,0.23274380833131592\n0.4872624617970575,0.2330708112220179\n0.4872055678994596,0.23339781411271984\n0.48669352282105854,0.23380656772612632\n0.48663662892345566,0.23380656772612632\n0.4864090533330591,0.23397006917147728\n0.4856125387666586,0.23413357061682827\n0.48538496317625707,0.23429707206217923\n0.48527117538105624,0.23429707206217923\n0.484872918097856,0.23437882278488378\n0.4845315547122536,0.2347875763982322\n0.4841332974290534,0.23495107784364128\n0.48407640353145553,0.23503282856628768\n0.4837350401458582,0.23535983145698963\n0.48322299506745714,0.23560508362504518\n0.4829385255794577,0.23593208651574712\n0.48282473778425683,0.23593208651574712\n0.4818575415250527,0.23601383723845165\n0.4815161781394554,0.23625908940644907\n0.4814023903442545,0.23634084012915363\n0.4807196635730549,0.23683134446520654\n0.48083345136825567,0.23691309518791107\n0.4807765574706527,0.23707659663326205\n0.480605875777854,0.23724009807861302\n0.48054898188025624,0.23732184880131757\n0.47958178562105197,0.23756710096931496\n0.4791835283378517,0.2378123531373705\n0.4791266344402539,0.2378123531373705\n0.47861458936185286,0.238057605305426\n0.4781594381810548,0.23838460819612795\n0.47787496869305524,0.23838460819612795\n0.47781807479545235,0.2384663589187744\n0.4770784541266547,0.2386298603641254\n0.47645262125305293,0.23838460819612795\n0.4763957273554551,0.23830285747342342\n0.4756561066866524,0.23838460819612795\n0.47514406160825146,0.23871161108682992\n0.47514406160825146,0.23871161108682992\n0.47520095550585434,0.23895686325488544\n0.4748595921202519,0.23903861397753187\n0.4741768653490523,0.23961086903628934\n0.4740630775538514,0.2396926197589939\n0.4736079263730533,0.23985612120434485\n0.4725838362162512,0.24001962264969584\n0.47252694231865344,0.24001962264969584\n0.472185578933051,0.23985612120434485\n0.47207179113785025,0.23993787192699131\n0.47201489724025236,0.2401013733723423\n0.47207179113785025,0.24051012698574875\n0.4722993667282518,0.24051012698574875\n0.47241315452345256,0.24051012698574875\n0.47286830570425076,0.2407553791538043\n0.47326656298745096,0.2414093849352082\n0.47326656298745096,0.24157288638055918\n0.4733803507826517,0.2424721443300186\n0.47349413857785255,0.24296264866612963\n0.47349413857785255,0.24296264866612963\n0.47349413857785255,0.24361665444753353\n0.4733803507826517,0.24378015589288451\n0.47286830570425076,0.24402540806094003\n0.47286830570425076,0.24410715878364456\n0.47252694231865344,0.24467941384234393\n0.47241315452345256,0.24541517034645238\n0.4723562606258497,0.2454969210691569\n0.4719011094450516,0.2459056746825634\n0.4717873216498507,0.24606917612791437\n0.47173042775225293,0.24639617901861632\n0.4715028521618513,0.24647792974132085\n0.4715028521618513,0.2465596804639673\n0.4717873216498507,0.2468049326320228\n0.4715028521618513,0.24705018480002025\n0.47121838267385185,0.2472136862454293\n0.47099080708345026,0.2473771876907803\n0.4709339131858524,0.24745893841342675\n0.47070633759545083,0.24754068913613125\n0.47019429251704975,0.24754068913613125\n0.4700236108242511,0.24762243985877772\n0.46973914133625166,0.24794944274953776\n0.4696822474386488,0.2480311934721842\n0.4695684596434479,0.2482764456402397\n0.46934088405305147,0.24852169780823716\n0.4692270962578506,0.2488487006989972\n0.4690564145650519,0.24901220214434816\n0.46899952066744904,0.24925745431234558\n0.46899952066744904,0.2493392050350501\n0.46865815728185173,0.24966620792575206\n0.4683167938962493,0.24974795864845659\n0.46820300610104854,0.24982970937110305\n0.4680323244082499,0.2502384629845096\n0.4680323244082499,0.2503202137072141\n0.46797543051064694,0.2505654658752115\n0.4676340671250496,0.25130122237931996\n0.46752027932984885,0.25154647454737544\n0.46752027932984885,0.25154647454737544\n0.4672927037394473,0.25170997599272643\n0.46672376476344835,0.25154647454737544\n0.4660979318898465,0.25146472382467094\n0.4659841440946507,0.25146472382467094\n0.46507384173304944,0.250974219488618\n0.46478937224505,0.250974219488618\n0.46467558444984924,0.250974219488618\n0.46404975157624745,0.2508924687659134\n0.4634239187026456,0.25146472382467094\n0.4633670248050477,0.25154647454737544\n0.46319634311224905,0.25170997599272643\n0.4630825553170483,0.25236398177413033\n0.4629118736242445,0.25252748321953944\n0.462684298033848,0.2524457324968349\n0.462684298033848,0.25236398177413033\n0.462684298033848,0.2522004803287794\n0.46274119193144586,0.2519552281607819\n0.46285497972664663,0.2518734774380774\n0.4630256614194453,0.251791726715431\n0.46331013090744483,0.2521187296061329\n0.46325323700984694,0.2519552281607819\n0.4633670248050477,0.25170997599272643\n0.4633670248050477,0.2516282252700219\n0.4634808126002485,0.2508924687659134\n0.4638221759858458,0.2502384629845096\n0.4638790698834488,0.250156712261805\n0.4641635393714482,0.24974795864845659\n0.4648462661426479,0.24942095575775464\n0.46467558444984924,0.24925745431234558\n0.4646186905522463,0.24917570358969915\n0.4642204332690461,0.24917570358969915\n0.4643342210642469,0.24925745431234558\n0.4645617966546484,0.24893045142164363\n0.46501694783544656,0.24876694997629265\n0.46507384173304944,0.24876694997629265\n0.465301417323446,0.24868519925364618\n0.4654152051186468,0.24852169780823716\n0.46547209901624975,0.24835819636288614\n0.4655858868114505,0.2481946949175352\n0.4656996746066463,0.2478676920268332\n0.46581346240184707,0.24762243985877772\n0.46587035629944995,0.24762243985877772\n0.4659841440946507,0.24713193552272478\n0.4659841440946507,0.24696843407737382\n0.4659841440946507,0.2467231819093183\n0.46621171968504727,0.24623267757326536\n0.46626861358265026,0.2461509268505608\n0.4663824013778459,0.24566042251450787\n0.4663824013778459,0.24533341962380592\n0.46678065866104623,0.24492466601039942\n0.46678065866104623,0.24492466601039942\n0.46695134035385,0.24467941384234393\n0.46706512814905077,0.24435241095164198\n0.4673495976370502,0.24410715878364456\n0.46746338543225097,0.24386190661558904\n0.46752027932984885,0.24378015589288451\n0.4674064915346481,0.24361665444753353\n0.4673495976370502,0.24345315300218254\n0.4664392952754489,0.24386190661558904\n0.4663824013778459,0.24386190661558904\n0.46552899291384764,0.24410715878364456\n0.465301417323446,0.2444341616743465\n0.46507384173304944,0.24459766311969747\n0.46501694783544656,0.24467941384234393\n0.4649031600402458,0.24508816745575043\n0.4643342210642469,0.2459056746825634\n0.4642204332690461,0.2459056746825634\n0.4637652820882479,0.24606917612791437\n0.4633670248050477,0.2461509268505608\n0.46285497972664663,0.24582392395985886\n0.46279808582904874,0.24582392395985886\n0.4623429346482456,0.24566042251450787\n0.4620015712626483,0.24516991817845496\n0.46177399567224675,0.24508816745575043\n0.4616033139794481,0.24541517034645238\n0.46148952618424727,0.2454969210691569\n0.46103437500344413,0.24582392395985886\n0.46097748110584624,0.24623267757326536\n0.4606361177202438,0.2465596804639673\n0.46052232992504816,0.24664143118667187\n0.4606361177202438,0.24688668335466926\n0.46057922382264593,0.24705018480002025\n0.4607499055154446,0.24745893841342675\n0.4604085421298473,0.24794944274953776\n0.4603516482322444,0.2480311934721842\n0.4599533909490441,0.24811294419488875\n0.4595551336658439,0.24835819636288614\n0.4593844519730452,0.2488487006989972\n0.45932755807544734,0.2488487006989972\n0.45915687638264363,0.2490939528669946\n0.45824657402104746,0.24966620792575206\n0.45824657402104746,0.24974795864845659\n0.4575638472498426,0.2503202137072141\n0.4572793777618432,0.250647216597916\n0.4572224838642453,0.2507289673205625\n0.456881120478643,0.2513829731020245\n0.45671043878584433,0.25154647454737544\n0.45665354488824644,0.2519552281607819\n0.45665354488824644,0.25203697888342835\n0.45636907540024196,0.2524457324968349\n0.45642596929784485,0.2530997382782388\n0.4563121815026441,0.25334499044629427\n0.4562552876050462,0.2534267411689988\n0.4560277120146446,0.25350849189164526\n0.4557432425266451,0.2539172455050518\n0.45511740965304337,0.2539989962276982\n0.45511740965304337,0.2539989962276982\n0.4548898340626417,0.2539172455050518\n0.45471915236984306,0.2539172455050518\n0.4546053645746423,0.2539989962276982\n0.4540364255986433,0.2541624976731073\n0.4537519561106439,0.2540807469504027\n0.453695062213041,0.2540807469504027\n0.45346748662264447,0.2540807469504027\n0.45329680492984076,0.25457125128645564\n0.452670972056244,0.2548165034545112\n0.452614078158641,0.2548165034545112\n0.4519882452850443,0.25514350634521316\n0.45170377579704485,0.2554705092359151\n0.4514762002066432,0.2554705092359151\n0.4514193063090403,0.2553887585132687\n0.4509641551282421,0.2554705092359151\n0.45045211004984115,0.2557975121266751\n0.4502814283570425,0.2557975121266751\n0.45016764056184166,0.2557975121266751\n0.44976938327864147,0.2559610135720261\n0.4492573382002404,0.2557157614039706\n0.4488590809170402,0.2557157614039706\n0.4488021870194423,0.2557975121266751\n0.44811946024824256,0.2559610135720261\n0.4482901419410412,0.25604276429467254\n0.44868839922424153,0.2558792628493216\n0.44891597481464307,0.2558792628493216\n0.44897286871224096,0.2558792628493216\n0.44937112599544116,0.2554705092359151\n0.4497124893810436,0.2547347527318067\n0.44982627717623935,0.2547347527318067\n0.45011074666424383,0.25448950056380926\n0.4507365795378406,0.25375374405970075\n0.45079347343544346,0.2536719933369963\n0.4514193063090403,0.2526092339421858\n0.4514762002066432,0.2524457324968349\n0.4515330941042411,0.2519552281607819\n0.45164688189944185,0.251791726715431\n0.4518175635922405,0.2516282252700219\n0.4522158208754408,0.25146472382467094\n0.4523296086706416,0.25130122237931996\n0.4523865025682445,0.25121947165667347\n0.45250029036344025,0.250974219488618\n0.4523296086706416,0.250647216597916\n0.45244339646584236,0.2502384629845096\n0.4523865025682445,0.25007496153915854\n0.45244339646584236,0.24982970937110305\n0.45244339646584236,0.24974795864845659\n0.45215892697784293,0.24925745431234558\n0.45204513918264216,0.24868519925364618\n0.45204513918264216,0.24835819636288614\n0.4519882452850443,0.2482764456402397\n0.45204513918264216,0.2480311934721842\n0.4519313513874413,0.24770419058148224\n0.4519882452850443,0.2473771876907803\n0.4519882452850443,0.2472136862454293\n0.45210203308024505,0.24696843407737382\n0.45215892697784293,0.24688668335466926\n0.4522727147730437,0.24664143118667187\n0.452614078158641,0.24623267757326536\n0.45295544154424344,0.24574217323721242\n0.45301233544184133,0.24566042251450787\n0.45341059272504153,0.24557867179186146\n0.4535243805202423,0.2454969210691569\n0.4536381683154431,0.24508816745575043\n0.4536381683154431,0.24467941384234393\n0.45358127441784524,0.24459766311969747\n0.4535243805202423,0.24435241095164198\n0.45329680492984076,0.2431261501114806\n0.45329680492984076,0.2431261501114806\n0.45284165374904267,0.24271739649807408\n0.452670972056244,0.24198163999396566\n0.452670972056244,0.24189988927131922\n0.452614078158641,0.24157288638055918\n0.452670972056244,0.24083712987645073\n0.4527847598514448,0.24067362843109974\n0.4527278659538418,0.24042837626310234\n0.4527278659538418,0.24042837626310234\n0.4527847598514448,0.23993787192699131\n0.45295544154424344,0.2396926197589939\n0.4530692293394442,0.2391203647002364\n0.4531261232370421,0.23903861397753187\n0.4531261232370421,0.23895686325488544\n0.4530692293394442,0.23879336180953445\n0.4531261232370421,0.2386298603641254\n0.4530692293394442,0.23830285747342342\n0.45323991103224287,0.23789410386001697\n0.45323991103224287,0.23756710096931496\n0.45323991103224287,0.23748535024666856\n0.453183017134645,0.2371583473559666\n0.45329680492984076,0.23699484591055753\n0.4535243805202423,0.2367495937425601\n0.4536381683154431,0.23609558796109809\n0.4536381683154431,0.23609558796109809\n0.4536381683154431,0.23576858507039614\n0.4535243805202423,0.2356868343477497\n0.45329680492984076,0.2356868343477497\n0.45301233544184133,0.23593208651574712\n0.45284165374904267,0.23601383723845165\n0.45284165374904267,0.23625908940644907\n0.4527847598514448,0.23634084012915363\n0.452614078158641,0.23650434157450456\n0.4522158208754408,0.23666784301985558\n0.45170377579704485,0.23732184880131757\n0.45164688189944185,0.237403599523964\n0.45130551851384454,0.23773060241466595\n0.451077942923443,0.23813935602807246\n0.451077942923443,0.23830285747342342\n0.45102104902584006,0.2384663589187744\n0.45090726123064434,0.23854810964147896\n0.45085036733304135,0.23854810964147896\n0.4506227917426398,0.23871161108682992\n0.45045211004984115,0.2391203647002364\n0.4497124893810436,0.2391203647002364\n0.4495987015858428,0.2391203647002364\n0.4492573382002404,0.23944736759093835\n0.44902976260983885,0.2401013733723423\n0.4488021870194423,0.2403466255403978\n0.4488021870194423,0.2403466255403978\n0.44817635414584045,0.24100063132185978\n0.44811946024824256,0.24116413276721077\n0.44794877855543885,0.24116413276721077\n0.44783499076024313,0.2414093849352082\n0.44783499076024313,0.24149113565791275\n0.4477780968626402,0.24165463710326368\n0.44760741516984154,0.2418181385486147\n0.4470953700914405,0.24206339071667018\n0.44681090060344103,0.24239039360737213\n0.44675400670583815,0.2424721443300186\n0.4465833250130395,0.24271739649807408\n0.4461281738322413,0.24296264866612963\n0.4460712799346384,0.24328965155683158\n0.4460712799346384,0.24361665444753353\n0.4460712799346384,0.24361665444753353\n0.4460143860370405,0.24410715878364456\n0.44590059824183975,0.2444341616743465\n0.44544544706104167,0.24476116456504846\n0.44538855316343867,0.244842915287753\n0.4453316592658408,0.24508816745575043\n0.44504718977784136,0.24533341962380592\n0.444705826392239,0.24541517034645238\n0.4443075691090388,0.24533341962380592\n0.444193781313838,0.24533341962380592\n0.44322658505463886,0.24516991817845496\n0.4430559033618402,0.24525166890110142\n0.443112797259438,0.2454969210691569\n0.44316969115704097,0.2454969210691569\n0.44316969115704097,0.24533341962380592\n0.4426576460786399,0.24500641673310397\n0.4420318132050381,0.24492466601039942\n0.4419749193074402,0.24492466601039942\n0.4411215108434368,0.24476116456504846\n0.4407801474578395,0.24500641673310397\n0.44055257186743785,0.24508816745575043\n0.44049567796984007,0.24508816745575043\n0.44009742068663976,0.24492466601039942\n0.4391871183250385,0.24533341962380592\n0.43913022442743554,0.24541517034645238\n0.4388457549394361,0.24541517034645238\n0.43873196714423535,0.24557867179186146\n0.43782166478263906,0.24566042251450787\n0.4377078769874383,0.24566042251450787\n0.43736651360183587,0.24582392395985886\n0.43702515021623856,0.2459056746825634\n0.4367406807282341,0.24606917612791437\n0.4364562112402346,0.2461509268505608\n0.4363993173426367,0.24623267757326536\n0.4360579539570344,0.24631442829591182\n0.4350907576978352,0.24631442829591182\n0.4349769699026344,0.24631442829591182\n0.4345787126194342,0.24647792974132085\n0.43361151636023504,0.2468049326320228\n0.43355462246263715,0.2468049326320228\n0.43292878958903536,0.2468049326320228\n0.43213227502263485,0.24705018480002025\n0.43207538112503185,0.24705018480002025\n0.43133576045623434,0.2473771876907803\n0.43122197266103357,0.24754068913613125\n0.4311650787634357,0.24770419058148224\n0.4309943970706319,0.2478676920268332\n0.43093750317303403,0.24794944274953776\n0.43053924588983383,0.2480311934721842\n0.43042545809463306,0.2481946949175352\n0.4302547764018344,0.2482764456402397\n0.4301409886066336,0.24852169780823716\n0.42979962522103127,0.24868519925364618\n0.4297427313234334,0.24876694997629265\n0.4296289435282326,0.24893045142164363\n0.4292875801426353,0.24917570358969915\n0.428889322859435,0.24917570358969915\n0.42854795947383256,0.2495027064804011\n0.4284910655762348,0.2495027064804011\n0.4280928082930345,0.24966620792575206\n0.4277514449074321,0.24991146009380757\n0.42758076321463345,0.24991146009380757\n0.42752386931703057,0.2504019644298605\n0.4274100815218348,0.25048371515256507\n0.4272393998290311,0.25048371515256507\n0.4264997791602335,0.2504019644298605\n0.42610152187703326,0.2502384629845096\n0.42598773408183244,0.250156712261805\n0.4258170523890338,0.250156712261805\n0.42564637069623007,0.24999321081645404\n0.4252481134130298,0.24991146009380757\n0.4247360683346288,0.24999321081645404\n0.4246791744370309,0.24999321081645404\n0.4242240232562328,0.24991146009380757\n0.4240533415634291,0.24974795864845659\n0.4237119781778318,0.24974795864845659\n0.42337061479222937,0.24958445720310563\n0.42331372089463154,0.24958445720310563\n0.42337061479222937,0.24974795864845659\n0.4231999330994307,0.24982970937110305\n0.4229154636114313,0.2495027064804011\n0.4223465246354324,0.2493392050350501\n0.42228963073782944,0.24925745431234558\n0.42166379786422764,0.2490939528669946\n0.42115175278583167,0.24876694997629265\n0.42092417719543007,0.24876694997629265\n0.4208672832978322,0.24876694997629265\n0.42046902601463193,0.2488487006989972\n0.41955872365303065,0.24860344853094166\n0.41955872365303065,0.24860344853094166\n0.41933114806262906,0.24868519925364618\n0.4189897846770267,0.24876694997629265\n0.41870531518902726,0.24852169780823716\n0.4185346334962286,0.24852169780823716\n0.4182501640082291,0.24868519925364618\n0.4181932701106262,0.24868519925364618\n0.41694160436342753,0.24835819636288614\n0.4168847104658297,0.24835819636288614\n0.4164295592850265,0.2480311934721842\n0.41625887759222785,0.24811294419488875\n0.4161450897970271,0.2482764456402397\n0.4160313020018263,0.24868519925364618\n0.4160313020018263,0.24868519925364618\n0.4157468325138268,0.24942095575775464\n0.41557615082102817,0.24999321081645404\n0.41557615082102817,0.25007496153915854\n0.41557615082102817,0.2503202137072141\n0.4154054691282295,0.2504019644298605\n0.41534857523062657,0.25105597021132253\n0.4152347874354258,0.25130122237931996\n0.4152347874354258,0.2513829731020245\n0.4147227423570247,0.2519552281607819\n0.4144382728690253,0.2524457324968349\n0.4144382728690253,0.2526092339421858\n0.4143813789714274,0.25301798755559235\n0.41421069727862875,0.2530997382782388\n0.4135848644050269,0.25350849189164526\n0.41352797050742907,0.25350849189164526\n0.413015925429028,0.25383549478234724\n0.41284524373622433,0.2539989962276982\n0.41273145594102856,0.25383549478234724\n0.4122763047602254,0.25383549478234724\n0.41221941086262753,0.2539172455050518\n0.41199183527222594,0.25383549478234724\n0.41187804747702517,0.2536719933369963\n0.41165047188662357,0.25350849189164526\n0.41096774511542383,0.25350849189164526\n0.41096774511542383,0.25350849189164526\n0.4097729732658231,0.25334499044629427\n0.4097160793682253,0.2530997382782388\n0.4096591854706274,0.25301798755559235\n0.40994365495862684,0.2524457324968349\n0.41028501834422415,0.2522004803287794\n0.4103988061394249,0.2518734774380774\n0.41045570003702786,0.2518734774380774\n0.41074016952502734,0.25154647454737544\n0.4107970634226252,0.250810718043267\n0.41096774511542383,0.2505654658752115\n0.41096774511542383,0.25048371515256507\n0.4111384268082276,0.24999321081645404\n0.41119532070582543,0.2493392050350501\n0.41130910850102625,0.24917570358969915\n0.4113660023986241,0.2490939528669946\n0.41153668409142785,0.24893045142164363\n0.4115935779890257,0.24876694997629265\n0.4114797901938249,0.2484399470855907\n0.41165047188662357,0.2477859413041287\n0.41165047188662357,0.24770419058148224\n0.41153668409142785,0.24754068913613125\n0.4114797901938249,0.2472136862454293\n0.41153668409142785,0.2465596804639673\n0.41153668409142785,0.24631442829591182\n0.4114797901938249,0.24623267757326536\n0.4117073657842265,0.24557867179186146\n0.4120487291698238,0.24508816745575043\n0.41221941086262753,0.24492466601039942\n0.41221941086262753,0.24492466601039942\n0.4123900925554262,0.24467941384234393\n0.412503880350627,0.24427066022899552\n0.4123900925554262,0.24410715878364456\n0.4126176681458278,0.24386190661558904\n0.412503880350627,0.24410715878364456\n0.4124469864530241,0.24410715878364456\n0.4120487291698238,0.24451591239699297\n0.4117073657842265,0.24467941384234393\n0.41130910850102625,0.24492466601039942\n0.4112522146034233,0.24500641673310397\n0.41102463901302677,0.24516991817845496\n0.4106832756274244,0.24525166890110142\n0.4100005488562247,0.24574217323721242\n0.40994365495862684,0.24574217323721242\n0.4086919892114232,0.24647792974132085\n0.4086350953138253,0.24647792974132085\n0.40857820141622236,0.24631442829591182\n0.4086350953138253,0.2461509268505608\n0.4089195648018248,0.24574217323721242\n0.40903335259702556,0.24533341962380592\n0.40903335259702556,0.24533341962380592\n0.40903335259702556,0.24508816745575043\n0.4092040342898242,0.24459766311969747\n0.4092040342898242,0.24435241095164198\n0.40931782208502504,0.24394365733823548\n0.40937471598262287,0.24386190661558904\n0.40954539767542664,0.24369840517023808\n0.4097729732658231,0.24296264866612963\n0.4097729732658231,0.24255389505272312\n0.4097729732658231,0.2424721443300186\n0.40982986716342606,0.24222689216202117\n0.41022812444662626,0.24206339071667018\n0.41045570003702786,0.24157288638055918\n0.41022812444662626,0.24157288638055918\n0.4101712305490234,0.24165463710326368\n0.4097160793682253,0.24206339071667018\n0.4092040342898242,0.24230864288466764\n0.4088626709042269,0.24230864288466764\n0.40880577700662396,0.24222689216202117\n0.4080661563378264,0.24255389505272312\n0.4075541112594253,0.24296264866612963\n0.4075541112594253,0.2430443993887761\n0.4072696417714259,0.24328965155683158\n0.40709896007862223,0.24337140227953613\n0.4068713844882257,0.24328965155683158\n0.40675759669302486,0.24345315300218254\n0.40653002110262326,0.24345315300218254\n0.40647312720502543,0.24361665444753353\n0.405619718741022,0.244188909506291\n0.40550593094582116,0.24435241095164198\n0.4051076736626209,0.24451591239699297\n0.4048232041746215,0.24476116456504846\n0.40459562858422493,0.24516991817845496\n0.40453873468662205,0.24525166890110142\n0.40431115909622045,0.24541517034645238\n0.4042542651986226,0.24566042251450787\n0.404026689608221,0.24598742540520985\n0.4035715384274229,0.24639617901861632\n0.4035715384274229,0.24639617901861632\n0.40317328114422263,0.24688668335466926\n0.40294570555382103,0.24688668335466926\n0.4028888116562232,0.24664143118667187\n0.40300259945142397,0.24639617901861632\n0.40305949334902186,0.24631442829591182\n0.40328706893942345,0.24623267757326536\n0.4034577506322221,0.2459056746825634\n0.4034008567346242,0.24525166890110142\n0.40351464452982505,0.24500641673310397\n0.40351464452982505,0.24500641673310397\n0.40351464452982505,0.24467941384234393\n0.4034577506322221,0.24492466601039942\n0.4034008567346242,0.24476116456504846\n0.4034008567346242,0.244188909506291\n0.40317328114422263,0.24435241095164198\n0.40317328114422263,0.2444341616743465\n0.40305949334902186,0.24467941384234393\n0.4026612360658216,0.24500641673310397\n0.40226297878262135,0.24557867179186146\n0.40220608488502346,0.24557867179186146\n0.40163714590901956,0.2459056746825634\n0.40135267642102007,0.24639617901861632\n0.40106820693302064,0.24647792974132085\n0.40101131303542276,0.2465596804639673\n0.40049926795702173,0.24688668335466926\n0.4003285862642231,0.2468049326320228\n0.4006699496498204,0.2465596804639673\n0.40049926795702173,0.24631442829591182\n0.40049926795702173,0.24623267757326536\n0.40055616185461956,0.24582392395985886\n0.4006699496498204,0.24598742540520985\n0.40084063134261905,0.24606917612791437\n0.40078373744502116,0.2459056746825634\n0.40078373744502116,0.2454969210691569\n0.40078373744502116,0.24541517034645238\n0.40101131303542276,0.24476116456504846\n0.4011251008306235,0.24402540806094003\n0.4011251008306235,0.24394365733823548\n0.4012957825234222,0.24345315300218254\n0.4011819947282214,0.2431261501114806\n0.40101131303542276,0.2431261501114806\n0.40072684354742333,0.24345315300218254\n0.4006699496498204,0.24353490372488706\n0.3995320716978225,0.24451591239699297\n0.3994751778002196,0.24451591239699297\n0.3989631327218186,0.24492466601039942\n0.3983941937458197,0.24500641673310397\n0.3981666181554181,0.24492466601039942\n0.3980528303602224,0.24492466601039942\n0.3976545730770221,0.24508816745575043\n0.3974838913842184,0.24516991817845496\n0.3974269974866205,0.24541517034645238\n0.39725631579382187,0.2454969210691569\n0.3968580585106216,0.2454969210691569\n0.3968011646130187,0.24557867179186146\n0.39628911953461765,0.24631442829591182\n0.3961753317394219,0.24664143118667187\n0.3958908622514174,0.2467231819093183\n0.3958339683538195,0.2468049326320228\n0.3953219232754185,0.24696843407737382\n0.3952081354802177,0.2465596804639673\n0.3953219232754185,0.2461509268505608\n0.3953219232754185,0.2459056746825634\n0.3953219232754185,0.24582392395985886\n0.39560639276341797,0.24541517034645238\n0.39628911953461765,0.24492466601039942\n0.3964029073298184,0.24476116456504846\n0.3964029073298184,0.24476116456504846\n0.3964029073298184,0.24467941384234393\n0.396118437841819,0.24459766311969747\n0.39600465004661817,0.24467941384234393\n0.3958339683538195,0.24467941384234393\n0.3955494988658201,0.24492466601039942\n0.3952081354802177,0.24525166890110142\n0.3952081354802177,0.24533341962380592\n0.39480987819701746,0.24606917612791437\n0.39446851481142015,0.2461509268505608\n0.3943547270162193,0.24639617901861632\n0.3942978331186164,0.24647792974132085\n0.39344442465461804,0.24754068913613125\n0.39338753075702015,0.24762243985877772\n0.3933306368594172,0.24811294419488875\n0.3931030612690207,0.2484399470855907\n0.39287548567861913,0.24893045142164363\n0.39281859178101625,0.24901220214434816\n0.39247722839541893,0.24925745431234558\n0.39224965280501733,0.24974795864845659\n0.39190828941942,0.25007496153915854\n0.39190828941942,0.250156712261805\n0.39151003213621977,0.2505654658752115\n0.39111177485301946,0.25130122237931996\n0.39111177485301946,0.2513829731020245\n0.3909979870578187,0.2516282252700219\n0.3907704114674171,0.2518734774380774\n0.3907704114674171,0.2521187296061329\n0.3905428358770155,0.25236398177413033\n0.3904290480818147,0.2526909846648904\n0.3904290480818147,0.2526909846648904\n0.3900876846962174,0.25326323972364784\n0.3900307907986145,0.25301798755559235\n0.3901445785938153,0.2529362368328878\n0.3901445785938153,0.2526909846648904\n0.3900307907986145,0.25252748321953944\n0.3900307907986145,0.2524457324968349\n0.389803215208218,0.2522822310514839\n0.3894618518226156,0.2526909846648904\n0.38894980674421453,0.2530997382782388\n0.38889291284661665,0.2530997382782388\n0.3884946555634164,0.2534267411689988\n0.3882670799730148,0.2534267411689988\n0.3884946555634164,0.2530997382782388\n0.3884946555634164,0.2529362368328878\n0.3884946555634164,0.25285448611024136\n0.38843776166581856,0.2526092339421858\n0.3885515494610143,0.2524457324968349\n0.38843776166581856,0.2521187296061329\n0.3885515494610143,0.251791726715431\n0.3883808677682156,0.2516282252700219\n0.38832397387061773,0.25170997599272643\n0.38832397387061773,0.251791726715431\n0.388153292177814,0.2522004803287794\n0.3879257165874175,0.25252748321953944\n0.3879257165874175,0.2526909846648904\n0.38775503489461377,0.25285448611024136\n0.3875274593042172,0.25285448611024136\n0.3875274593042172,0.25285448611024136\n0.38747056540661434,0.2526909846648904\n0.38764124709941805,0.25252748321953944\n0.38764124709941805,0.25236398177413033\n0.3875843532018151,0.2522004803287794\n0.38764124709941805,0.2519552281607819\n0.3875843532018151,0.251791726715431\n0.38775503489461377,0.25154647454737544\n0.38775503489461377,0.25146472382467094\n0.3879257165874175,0.25121947165667347\n0.3879257165874175,0.250647216597916\n0.3882670799730148,0.2504019644298605\n0.3882670799730148,0.25007496153915854\n0.3882670799730148,0.25007496153915854\n0.38821018607541696,0.24982970937110305\n0.38786882268981454,0.24999321081645404\n0.38747056540661434,0.24999321081645404\n0.38747056540661434,0.24958445720310563\n0.38747056540661434,0.24958445720310563\n0.3872998837138157,0.24893045142164363\n0.3870723081234141,0.2488487006989972\n0.38690162643061543,0.2488487006989972\n0.3869585203282133,0.24925745431234558\n0.3869585203282133,0.2493392050350501\n0.38678783863541466,0.24974795864845659\n0.38656026304501306,0.25007496153915854\n0.3865033691474152,0.2502384629845096\n0.3863326874546165,0.250156712261805\n0.3863326874546165,0.24991146009380757\n0.3863326874546165,0.24982970937110305\n0.38621889965941575,0.2495027064804011\n0.3862757935570136,0.2493392050350501\n0.3861620057618128,0.24876694997629265\n0.3861620057618128,0.24835819636288614\n0.3861620057618128,0.2482764456402397\n0.3861051118642149,0.2480311934721842\n0.38599132406901415,0.2478676920268332\n0.3858775362738133,0.24794944274953776\n0.3858206423762155,0.2484399470855907\n0.38570685458101467,0.24852169780823716\n0.3854792789906131,0.24852169780823716\n0.38542238509301524,0.24860344853094166\n0.38513791560501576,0.24917570358969915\n0.3852517034002166,0.24974795864845659\n0.3850810217074128,0.24991146009380757\n0.385024127809815,0.24991146009380757\n0.385024127809815,0.24999321081645404\n0.3847965522194134,0.24999321081645404\n0.3846827644242126,0.24982970937110305\n0.38462587052661473,0.24958445720310563\n0.38485344611701633,0.24966620792575206\n0.38485344611701633,0.2495027064804011\n0.38485344611701633,0.24925745431234558\n0.3846827644242126,0.2490939528669946\n0.3846827644242126,0.24901220214434816\n0.38462587052661473,0.2484399470855907\n0.3846827644242126,0.2480311934721842\n0.38451208273141396,0.2477859413041287\n0.3844551888338161,0.2477859413041287\n0.3843982949362132,0.2477859413041287\n0.3841707193458166,0.24917570358969915\n0.3841138254482137,0.24925745431234558\n0.3838293559602143,0.24982970937110305\n0.38371556816501345,0.24991146009380757\n0.3835448864722148,0.24982970937110305\n0.3836017803698127,0.24942095575775464\n0.3836586742674156,0.2493392050350501\n0.38348799257461186,0.24917570358969915\n0.3828621597010151,0.24958445720310563\n0.3827483719058143,0.24942095575775464\n0.3828621597010151,0.24925745431234558\n0.3828621597010151,0.24917570358969915\n0.3829759474962159,0.2488487006989972\n0.3829759474962159,0.2484399470855907\n0.3828052658034122,0.2484399470855907\n0.38246390241781486,0.24876694997629265\n0.3824070085202119,0.24876694997629265\n0.3821794329298154,0.24917570358969915\n0.3821794329298154,0.24901220214434816\n0.3824070085202119,0.24868519925364618\n0.3827483719058143,0.2484399470855907\n0.3827483719058143,0.2484399470855907\n0.38257769021301563,0.2484399470855907\n0.38246390241781486,0.24860344853094166\n0.3817811756466151,0.24893045142164363\n0.3816104939538114,0.2490939528669946\n0.3815536000562135,0.2490939528669946\n0.38143981226101276,0.2495027064804011\n0.38143981226101276,0.24991146009380757\n0.3815536000562135,0.24974795864845659\n0.3816104939538114,0.2493392050350501\n0.3816104939538114,0.24925745431234558\n0.38143981226101276,0.2490939528669946\n0.3812691305682141,0.2490939528669946\n0.38109844887541544,0.24917570358969915\n0.38104155497781256,0.2493392050350501\n0.3808708732850139,0.2495027064804011\n0.3808708732850139,0.2490939528669946\n0.3808708732850139,0.24901220214434816\n0.3808708732850139,0.24860344853094166\n0.38081397938741096,0.24835819636288614\n0.38018814651381416,0.24893045142164363\n0.3801312526162112,0.24893045142164363\n0.38001746482101045,0.24917570358969915\n0.37996057092341257,0.2495027064804011\n0.3798467831282118,0.24982970937110305\n0.3796761014354131,0.24999321081645404\n0.37939163194741365,0.25007496153915854\n0.3793347380498107,0.25007496153915854\n0.37927784415221283,0.24999321081645404\n0.37944852584501154,0.24982970937110305\n0.3795623136402123,0.24893045142164363\n0.3796761014354131,0.24876694997629265\n0.3796761014354131,0.24868519925364618\n0.3797898892306139,0.2484399470855907\n0.37996057092341257,0.2484399470855907\n0.38018814651381416,0.24811294419488875\n0.3807001915922152,0.2477859413041287\n0.3807001915922152,0.2477859413041287\n0.38092776718261173,0.24762243985877772\n0.38104155497781256,0.24745893841342675\n0.38115534277301333,0.24729543696807577\n0.381326024465812,0.24647792974132085\n0.381326024465812,0.24647792974132085\n0.38149670615861564,0.24582392395985886\n0.38172428174901224,0.2454969210691569\n0.381838069544213,0.24500641673310397\n0.381838069544213,0.24492466601039942\n0.3818949634418159,0.24467941384234393\n0.38166738785141435,0.24459766311969747\n0.381326024465812,0.24508816745575043\n0.38115534277301333,0.24516991817845496\n0.3812122366706112,0.24533341962380592\n0.3812122366706112,0.24533341962380592\n0.38104155497781256,0.24541517034645238\n0.38092776718261173,0.24582392395985886\n0.3806432976946123,0.24606917612791437\n0.3805295098994115,0.24623267757326536\n0.3803588282066128,0.24639617901861632\n0.38030193430901493,0.24631442829591182\n0.38024504041141205,0.24606917612791437\n0.3805864037970144,0.24582392395985886\n0.3806432976946123,0.24566042251450787\n0.3806432976946123,0.2454969210691569\n0.3805295098994115,0.24533341962380592\n0.3803588282066128,0.24525166890110142\n0.3803588282066128,0.24525166890110142\n0.37996057092341257,0.2454969210691569\n0.37996057092341257,0.24582392395985886\n0.3799036770258147,0.24598742540520985\n0.37944852584501154,0.24623267757326536\n0.37939163194741365,0.24623267757326536\n0.3788795868690126,0.24647792974132085\n0.3787089051762139,0.24688668335466926\n0.3783675417906116,0.24705018480002025\n0.3783106478930137,0.24713193552272478\n0.3780830723026121,0.24713193552272478\n0.37768481501941187,0.24713193552272478\n0.37694519435060925,0.24745893841342675\n0.37688830045301136,0.24754068913613125\n0.37660383096501193,0.24762243985877772\n0.3764331492722133,0.24754068913613125\n0.37631936147701245,0.24729543696807577\n0.3770020882482122,0.24696843407737382\n0.37705898214581,0.24688668335466926\n0.3773434516338095,0.2465596804639673\n0.37711587604341296,0.2465596804639673\n0.37694519435060925,0.2467231819093183\n0.37660383096501193,0.2468049326320228\n0.37637625537461034,0.24688668335466926\n0.37631936147701245,0.24696843407737382\n0.3758073163986114,0.24713193552272478\n0.3750676957298088,0.24754068913613125\n0.37501080183221097,0.24754068913613125\n0.37523837742261257,0.24696843407737382\n0.3754090591154112,0.24664143118667187\n0.3754090591154112,0.24647792974132085\n0.37518148352500963,0.24664143118667187\n0.37518148352500963,0.24664143118667187\n0.3747832262418094,0.24705018480002025\n0.3743849689586091,0.24729543696807577\n0.37410049947060964,0.24762243985877772\n0.37410049947060964,0.24762243985877772\n0.3740436055730118,0.24729543696807577\n0.37410049947060964,0.24713193552272478\n0.37455565065141283,0.24639617901861632\n0.3746125445490107,0.24631442829591182\n0.3747832262418094,0.24606917612791437\n0.37466943844660855,0.2459056746825634\n0.3741573933682126,0.24606917612791437\n0.373929817777811,0.2461509268505608\n0.3738160299826102,0.24639617901861632\n0.3738160299826102,0.24639617901861632\n0.3735884543922086,0.24647792974132085\n0.37313330321141047,0.24696843407737382\n0.3725643642354116,0.2472136862454293\n0.3725074703378087,0.24729543696807577\n0.3722798947474071,0.24729543696807577\n0.37222300084980925,0.24713193552272478\n0.3722798947474071,0.24688668335466926\n0.37279193982580816,0.24664143118667187\n0.37290572762100893,0.24647792974132085\n0.37284883372341104,0.24639617901861632\n0.3726212581330095,0.24631442829591182\n0.3724505764402108,0.24639617901861632\n0.3723936825426079,0.2465596804639673\n0.371824743566609,0.2467231819093183\n0.37193853136180977,0.24647792974132085\n0.37199542525940765,0.24647792974132085\n0.37233678864501,0.24623267757326536\n0.3725643642354116,0.2459056746825634\n0.3730764093138076,0.2454969210691569\n0.3730764093138076,0.2454969210691569\n0.37364534828981155,0.24492466601039942\n0.3737022421874094,0.24459766311969747\n0.3737022421874094,0.24435241095164198\n0.3737022421874094,0.24427066022899552\n0.3737591360850123,0.24378015589288451\n0.37330398490420913,0.24435241095164198\n0.3730195154162097,0.24451591239699297\n0.37290572762100893,0.24451591239699297\n0.37233678864501,0.24467941384234393\n0.3718816374642119,0.24500641673310397\n0.37165406187381034,0.24500641673310397\n0.3715971679762074,0.24500641673310397\n0.371824743566609,0.24476116456504846\n0.37193853136180977,0.24459766311969747\n0.37199542525940765,0.24402540806094003\n0.3717678496690111,0.24402540806094003\n0.37171095577140817,0.24402540806094003\n0.37165406187381034,0.24427066022899552\n0.37136959238581085,0.24451591239699297\n0.37136959238581085,0.24435241095164198\n0.37171095577140817,0.24386190661558904\n0.37171095577140817,0.24378015589288451\n0.3720523191570106,0.24361665444753353\n0.3723936825426079,0.2431261501114806\n0.3724505764402108,0.24279914722077864\n0.3724505764402108,0.24271739649807408\n0.3726212581330095,0.2424721443300186\n0.3729626215186118,0.2424721443300186\n0.37336087880181207,0.24230864288466764\n0.37341777269940996,0.24206339071667018\n0.3734746665970078,0.24189988927131922\n0.37341777269940996,0.2418181385486147\n0.37319019710900836,0.24165463710326368\n0.37267815203060733,0.24189988927131922\n0.3721092130546084,0.24198163999396566\n0.3720523191570106,0.24206339071667018\n0.3717678496690111,0.24239039360737213\n0.37136959238581085,0.2430443993887761\n0.37114201679540926,0.2430443993887761\n0.37114201679540926,0.24296264866612963\n0.37114201679540926,0.24288089794342507\n0.37136959238581085,0.24214514143931665\n0.37142648628340874,0.24149113565791275\n0.37142648628340874,0.24149113565791275\n0.37165406187381034,0.2412458834898572\n0.37216610695221136,0.24116413276721077\n0.3721092130546084,0.24100063132185978\n0.37233678864501,0.24067362843109974\n0.3723936825426079,0.2405918777084533\n0.3726212581330095,0.2397743704816403\n0.3725643642354116,0.2391203647002364\n0.3725074703378087,0.23903861397753187\n0.3725074703378087,0.23879336180953445\n0.37267815203060733,0.2384663589187744\n0.3727350459282103,0.23797585458272147\n0.3726212581330095,0.23773060241466595\n0.3725643642354116,0.23773060241466595\n0.3725074703378087,0.23773060241466595\n0.371824743566609,0.23879336180953445\n0.3717678496690111,0.2388751125321809\n0.3710282290002085,0.2392838661455874\n0.3708006534098069,0.2396926197589939\n0.3709713351026106,0.23985612120434485\n0.3709713351026106,0.23985612120434485\n0.3705161839218074,0.2401831240950468\n0.36994724494580855,0.2401831240950468\n0.36971966935540695,0.2403466255403978\n0.36966277545780907,0.24042837626310234\n0.3695489876626083,0.2405918777084533\n0.3692645181746088,0.24132763421256173\n0.36915073037940804,0.2414093849352082\n0.36892315478900645,0.2412458834898572\n0.3689800486866094,0.24116413276721077\n0.36920762427700593,0.24083712987645073\n0.3694920937650104,0.23993787192699131\n0.3695489876626083,0.23985612120434485\n0.36971966935540695,0.2391203647002364\n0.3696058815602062,0.2388751125321809\n0.36971966935540695,0.2384663589187744\n0.36977656325300984,0.23838460819612795\n0.36994724494580855,0.23756710096931496\n0.36977656325300984,0.23756710096931496\n0.36966277545780907,0.23797585458272147\n0.36966277545780907,0.238057605305426\n0.36937830596980964,0.23830285747342342\n0.36863868530100696,0.23895686325488544\n0.36858179140340913,0.23903861397753187\n0.36829732191540965,0.2392838661455874\n0.3677852768370086,0.2401013733723423\n0.3677283829394057,0.2401831240950468\n0.36761459514420997,0.2403466255403978\n0.36744391345140626,0.24051012698574875\n0.3672732317586076,0.24051012698574875\n0.3671025500658089,0.24042837626310234\n0.3673301256562055,0.2401013733723423\n0.36744391345140626,0.2397743704816403\n0.36750080734900914,0.2396926197589939\n0.36761459514420997,0.23895686325488544\n0.367557701246607,0.23838460819612795\n0.367557701246607,0.23830285747342342\n0.367557701246607,0.23764885169201952\n0.36784217073460646,0.23683134446520654\n0.36784217073460646,0.23683134446520654\n0.36784217073460646,0.23650434157450456\n0.36812664022260594,0.23625908940644907\n0.36829732191540965,0.23576858507039614\n0.3682404280178067,0.23552333290234062\n0.3682404280178067,0.2354415821796942\n0.3684111097106105,0.23470582567558573\n0.3682404280178067,0.23454232423023477\n0.36806974632500805,0.23470582567558573\n0.3684111097106105,0.23495107784364128\n0.3684111097106105,0.23495107784364128\n0.3678990646322094,0.23552333290234062\n0.36761459514420997,0.23576858507039614\n0.36738701955380837,0.23585033579310066\n0.3672732317586076,0.23585033579310066\n0.367045656168206,0.23593208651574712\n0.36687497447540734,0.23650434157450456\n0.36664739888500575,0.23650434157450456\n0.36641982329460926,0.23609558796109809\n0.36641982329460926,0.23609558796109809\n0.36630603549940843,0.2356868343477497\n0.36619224770420766,0.23552333290234062\n0.36619224770420766,0.23527808073434323\n0.36607845990900684,0.23511457928899224\n0.36573709652340447,0.2354415821796942\n0.36573709652340447,0.2354415821796942\n0.36562330872820875,0.23585033579310066\n0.36528194534260633,0.23683134446520654\n0.36528194534260633,0.23691309518791107\n0.36516815754740556,0.23756710096931496\n0.3649974758546069,0.23789410386001697\n0.36482679416180824,0.23797585458272147\n0.36482679416180824,0.23773060241466595\n0.3648836880594061,0.23764885169201952\n0.3648836880594061,0.237403599523964\n0.3647699002642053,0.23724009807861302\n0.36459921857140665,0.23732184880131757\n0.36459921857140665,0.23773060241466595\n0.36437164298100505,0.23813935602807246\n0.36437164298100505,0.238221106750777\n0.36437164298100505,0.2384663589187744\n0.364428536878608,0.23903861397753187\n0.36431474908340716,0.23920211542288286\n0.3641440673906085,0.2391203647002364\n0.36397338569780485,0.2388751125321809\n0.36397338569780485,0.23879336180953445\n0.363859597902604,0.23854810964147896\n0.363859597902604,0.23838460819612795\n0.3637458101074083,0.237403599523964\n0.3638027040050062,0.237403599523964\n0.3637458101074083,0.23707659663326205\n0.3638027040050062,0.23691309518791107\n0.36363202231220754,0.23699484591055753\n0.3635182345170067,0.23707659663326205\n0.36340444672180594,0.23748535024666856\n0.3632906589266051,0.23764885169201952\n0.3632337650290073,0.23773060241466595\n0.36283550774580703,0.238221106750777\n0.3627217199506062,0.23895686325488544\n0.3627217199506062,0.23903861397753187\n0.36260793215540543,0.23985612120434485\n0.36238035656500384,0.24042837626310234\n0.36232346266740595,0.24051012698574875\n0.3619252053842057,0.24108238204450624\n0.3618114175890049,0.24132763421256173\n0.3616407358962062,0.2414093849352082\n0.36152694810100544,0.24116413276721077\n0.36147005420340755,0.24116413276721077\n0.3616976297938041,0.2403466255403978\n0.36158384199860333,0.2396926197589939\n0.36152694810100544,0.2396926197589939\n0.36141316030580467,0.23944736759093835\n0.36152694810100544,0.2391203647002364\n0.36141316030580467,0.23879336180953445\n0.361242478613006,0.2386298603641254\n0.36107179692020736,0.2388751125321809\n0.36107179692020736,0.2388751125321809\n0.36107179692020736,0.2396926197589939\n0.3609580091250066,0.23985612120434485\n0.36101490302260447,0.24026487481775136\n0.3609580091250066,0.2403466255403978\n0.360730433534605,0.24083712987645073\n0.36055975184180633,0.24100063132185978\n0.3603890701490026,0.24083712987645073\n0.3602183884562039,0.24051012698574875\n0.3601614945586061,0.24042837626310234\n0.36010460066100314,0.23936561686823385\n0.35999081286580237,0.23903861397753187\n0.35999081286580237,0.23895686325488544\n0.3599339189682045,0.23871161108682992\n0.359649449480205,0.23797585458272147\n0.3595925555826021,0.23756710096931496\n0.3595925555826021,0.23748535024666856\n0.359649449480205,0.2367495937425601\n0.35982013117300365,0.23609558796109809\n0.3598770250706066,0.23601383723845165\n0.36004770676340525,0.23560508362504518\n0.35982013117300365,0.23552333290234062\n0.3597063433778029,0.23593208651574712\n0.35936497999220557,0.2361773386838026\n0.35936497999220557,0.23625908940644907\n0.35902361660660315,0.23634084012915363\n0.3587960410162016,0.2367495937425601\n0.3583408898354035,0.23724009807861302\n0.35828399593780563,0.23732184880131757\n0.35805642034740404,0.23764885169201952\n0.35771505696180167,0.2378123531373705\n0.3576581630642038,0.23797585458272147\n0.357544375269003,0.23838460819612795\n0.357544375269003,0.23838460819612795\n0.3574305874738022,0.2386298603641254\n0.35737369357620435,0.23895686325488544\n0.3573167996786014,0.2391203647002364\n0.3573167996786014,0.2395291183136429\n0.35725990578100353,0.2397743704816403\n0.3572030118834057,0.2397743704816403\n0.3569754362930041,0.23944736759093835\n0.3566909668050046,0.23895686325488544\n0.3565202851122009,0.23936561686823385\n0.356463391214603,0.23936561686823385\n0.3564064973170052,0.23961086903628934\n0.3564064973170052,0.23993787192699131\n0.3562358156242014,0.2401013733723423\n0.3561789217266036,0.23985612120434485\n0.3561789217266036,0.2396926197589939\n0.35612202782900065,0.23944736759093835\n0.35606513393140277,0.23944736759093835\n0.3558375583410012,0.23936561686823385\n0.35566687664820257,0.2396926197589939\n0.3550410437746007,0.2401831240950468\n0.3549841498770029,0.24026487481775136\n0.35481346818420423,0.2403466255403978\n0.35452899869620474,0.24067362843109974\n0.35435831700340104,0.24067362843109974\n0.3539600597202008,0.2407553791538043\n0.35373248412980424,0.2407553791538043\n0.35361869633460347,0.2407553791538043\n0.35282218176820296,0.24067362843109974\n0.35282218176820296,0.24051012698574875\n0.3531066512562024,0.2401831240950468\n0.3531635451538003,0.2401831240950468\n0.3531635451538003,0.23993787192699131\n0.3529928634610016,0.2397743704816403\n0.35196877330419957,0.23993787192699131\n0.3519118794066017,0.23993787192699131\n0.35117225873779906,0.2395291183136429\n0.35111536484020117,0.23920211542288286\n0.3509446831474025,0.23903861397753187\n0.3508877892497996,0.23903861397753187\n0.3507171075570009,0.2386298603641254\n0.3507740014545988,0.2384663589187744\n0.35100157704500035,0.2384663589187744\n0.3515705160209993,0.238221106750777\n0.3515705160209993,0.23813935602807246\n0.3517980916114009,0.23797585458272147\n0.3523670305873998,0.23813935602807246\n0.35265150007539925,0.23797585458272147\n0.35282218176820296,0.2378123531373705\n0.35282218176820296,0.23789410386001697\n0.35293596956340373,0.237403599523964\n0.3529928634610016,0.23764885169201952\n0.3531066512562024,0.23756710096931496\n0.3530497573585995,0.23748535024666856\n0.35293596956340373,0.23789410386001697\n0.35259460617780136,0.23789410386001697\n0.35219634889460116,0.2378123531373705\n0.35202566720180245,0.23756710096931496\n0.35202566720180245,0.23756710096931496\n0.35185498550899874,0.23724009807861302\n0.351741197713803,0.23707659663326205\n0.3515136221234014,0.23707659663326205\n0.3513998343282006,0.23691309518791107\n0.35128604653299983,0.23650434157450456\n0.35122915263540194,0.23642259085185816\n0.35066021365940303,0.23503282856628768\n0.3506033197618001,0.23495107784364128\n0.3502619563762028,0.23446057350753022\n0.3500343807858012,0.23364306628077533\n0.3500343807858012,0.2335613155580708\n0.3502619563762028,0.23331606339007338\n0.35111536484020117,0.23388831844877275\n0.35122915263540194,0.23413357061682827\n0.35122915263540194,0.23413357061682827\n0.35134294043060277,0.23405181989412374\n0.3513998343282006,0.23380656772612632\n0.35117225873779906,0.23347956483542437\n0.35117225873779906,0.23282555905396235\n0.35117225873779906,0.23274380833131592\n0.35111536484020117,0.2324985561632604\n0.35128604653299983,0.23233505471790944\n0.35122915263540194,0.23159929821380099\n0.35117225873779906,0.23135404604574544\n0.35111536484020117,0.23127229532309904\n0.3509446831474025,0.23086354170969253\n0.3502619563762028,0.23020953592823054\n0.35020506247859984,0.23020953592823054\n0.34935165401460155,0.2301277852055841\n0.3492947601169986,0.22996428376023312\n0.34889650283379836,0.2297190315921776\n0.3488396089362005,0.2297190315921776\n0.34827066996020156,0.22922852725612466\n0.3482137760625986,0.22898327508806915\n0.3481568821650008,0.22857452147466267\n0.3481568821650008,0.22857452147466267\n0.34832756385779945,0.2284927707520162\n0.3484982455505981,0.2280840171386097\n0.3486120333457989,0.22792051569325877\n0.34901029062899913,0.22792051569325877\n0.34912407842419996,0.22775701424790776\n0.34906718452660207,0.22775701424790776\n0.34901029062899913,0.22775701424790776\n0.3486120333457989,0.22775701424790776\n0.34827066996020156,0.22751176207985227\n0.3478724126770013,0.22702125774379933\n0.34781551877939837,0.22693950702109478\n0.3476448370865997,0.22677600557574382\n0.34724657980339946,0.22669425485309738\n0.34679142862260137,0.22644900268504187\n0.34656385303219983,0.22628550123969088\n0.34656385303219983,0.22628550123969088\n0.3465069591345969,0.22612199979433992\n0.3463931713394012,0.22612199979433992\n0.3462224896465974,0.22620375051698632\n0.345938020158598,0.22661250413039283\n0.3457104445681964,0.22677600557574382\n0.3456535506705985,0.22669425485309738\n0.34536908118259907,0.22628550123969088\n0.34536908118259907,0.2260402490716354\n0.3455397628753977,0.2257132461809334\n0.34559665677300067,0.2254679940128779\n0.34559665677300067,0.22538624329023146\n0.345938020158598,0.22407823172736555\n0.345938020158598,0.223996481004661\n0.3461655957489996,0.22358772739131264\n0.34627938354420035,0.2225249679964441\n0.34627938354420035,0.22244321727379768\n0.3465069591345969,0.22211621438309573\n0.34684832252019926,0.22195271293774474\n0.3471327920081987,0.22195271293774474\n0.34730347370099734,0.22211621438309573\n0.3473603675986003,0.22219796510574216\n0.34753104929139894,0.22236146655109315\n0.3478724126770013,0.2222797158284467\n0.34804309436979997,0.22236146655109315\n0.34855513944820105,0.22277022016449965\n0.3486120333457989,0.22277022016449965\n0.34957922960499804,0.22317897377790613\n0.34974991129780175,0.22342422594596165\n0.34974991129780175,0.22342422594596165\n0.3502619563762028,0.22342422594596165\n0.3509446831474025,0.2237512288366636\n0.35100157704500035,0.2237512288366636\n0.35185498550899874,0.2237512288366636\n0.35185498550899874,0.22358772739131264\n0.3515136221234014,0.22326072450055257\n0.3514567282257985,0.22326072450055257\n0.35122915263540194,0.2229337216098506\n0.35117225873779906,0.2222797158284467\n0.35111536484020117,0.2220344636603912\n0.3510584709426033,0.22195271293774474\n0.3508308953522017,0.22195271293774474\n0.3507740014545988,0.22178921149239375\n0.3507171075570009,0.2216257100469847\n0.3507171075570009,0.2213804578789873\n0.35054642586420226,0.22129870715628275\n0.35054642586420226,0.22088995354287627\n0.35054642586420226,0.2208082028202298\n0.3507171075570009,0.22023594776147234\n0.35066021365940303,0.21950019125736392\n0.3506033197618001,0.21941844053465936\n0.35066021365940303,0.218028678249147\n0.35066021365940303,0.21794692752644249\n0.3507171075570009,0.21753817391309407\n0.3509446831474025,0.21729292174503856\n0.3508877892497996,0.2167206666862811\n0.3509446831474025,0.2167206666862811\n0.35100157704500035,0.21663891596363463\n0.35100157704500035,0.2160666609048772\n0.3508877892497996,0.2156579072914707\n0.35043263806900143,0.2156579072914707\n0.35037574417139855,0.21557615656876614\n0.35031885027380066,0.21557615656876614\n0.35043263806900143,0.21541265512341518\n0.3507740014545988,0.2152491536780642\n0.3509446831474025,0.21516740295541775\n0.35128604653299983,0.21516740295541775\n0.3515136221234014,0.2150039015100668\n0.3515136221234014,0.21492215078736224\n0.3517980916114009,0.21467689861930675\n0.35231013668980193,0.2144316464513093\n0.3524239244850027,0.21418639428325378\n0.35265150007539925,0.21402289283790285\n0.35265150007539925,0.21402289283790285\n0.35293596956340373,0.21361413922449635\n0.35293596956340373,0.2133688870564989\n0.3530497573585995,0.2129601334430924\n0.3530497573585995,0.2127148812750369\n0.3530497573585995,0.21255137982968594\n0.35322043905140316,0.21214262621627944\n0.353334226846604,0.21197912477092845\n0.35350490853940264,0.21197912477092845\n0.3536755902322013,0.21214262621627944\n0.35361869633460347,0.21255137982968594\n0.35361869633460347,0.21263313055239044\n0.35361869633460347,0.2128783827203879\n0.3537893780274021,0.2133688870564989\n0.3540169536178037,0.21345063777914536\n0.3542445292082002,0.21304188416573885\n0.35430142310580315,0.21304188416573885\n0.35452899869620474,0.2128783827203879\n0.35509793767220366,0.2128783827203879\n0.3552686193650023,0.21304188416573885\n0.35566687664820257,0.2131236348884434\n0.35572377054580046,0.2131236348884434\n0.356463391214603,0.21345063777914536\n0.35691854239540116,0.21361413922449635\n0.3569754362930041,0.21361413922449635\n0.3584546776306043,0.21418639428325378\n0.35856846542580506,0.21418639428325378\n0.3589667227090053,0.21418639428325378\n0.359649449480205,0.2139411421151983\n0.3598770250706066,0.21402289283790285\n0.3599339189682045,0.21402289283790285\n0.36055975184180633,0.21418639428325378\n0.3612993725106039,0.2137776406698473\n0.3613562664082068,0.21369588994720085\n0.36175452369140704,0.21345063777914536\n0.3619252053842057,0.21353238850184988\n0.36260793215540543,0.21320538561108984\n0.36266482605300326,0.21320538561108984\n0.3627786138482041,0.2131236348884434\n0.3627217199506062,0.2133688870564989\n0.36289240164340486,0.2133688870564989\n0.3630630833362035,0.2133688870564989\n0.36340444672180594,0.21304188416573885\n0.36363202231220754,0.21279663199774146\n0.36368891620980537,0.21279663199774146\n0.3649974758546069,0.21189737404828202\n0.36505436975220473,0.2118156233255775\n0.3652250514450085,0.21157037115752197\n0.36539573313780715,0.21157037115752197\n0.3656802026258066,0.21173387260287296\n0.3659077782162082,0.2118156233255775\n0.3662491416018055,0.2114886204348755\n0.3663629293970063,0.2114886204348755\n0.3664767171922071,0.21116161754417356\n0.36693186837300523,0.2105893624854161\n0.3669887622706081,0.2103441103173606\n0.367045656168206,0.21026235959465606\n0.367045656168206,0.21018060887200962\n0.3669887622706081,0.21009885814930507\n0.36664739888500575,0.20944485236790117\n0.3665905049874079,0.20969010453595668\n0.3667611866802066,0.2099353567039541\n0.36681808057780946,0.21001710742665863\n0.3669887622706081,0.21009885814930507\n0.3672163378610097,0.21009885814930507\n0.3681835341202089,0.20969010453595668\n0.36829732191540965,0.20960835381325216\n0.3690369425842072,0.20936310164519664\n0.36915073037940804,0.20919960019984568\n0.3693214120722067,0.20919960019984568\n0.3695489876626083,0.20919960019984568\n0.3696058815602062,0.20919960019984568\n0.36989035104821066,0.20919960019984568\n0.3700610327410093,0.2086273451410882\n0.3701179266386072,0.20887259730914373\n0.3704592900242095,0.20854559441844175\n0.3705161839218074,0.20854559441844175\n0.3708575473074098,0.20838209297309077\n0.37091444120500766,0.20821859152773978\n0.3708575473074098,0.20805509008238882\n0.37108512289781137,0.20772808719162877\n0.3713126984882079,0.20756458574627779\n0.37142648628340874,0.20756458574627779\n0.3720523191570106,0.20699233068752032\n0.3724505764402108,0.20682882924216933\n0.3725643642354116,0.20666532779681837\n0.3726212581330095,0.20666532779681837\n0.3723936825426079,0.20666532779681837\n0.3722798947474071,0.2067470785195229\n0.3729626215186118,0.20601132201541444\n0.3730195154162097,0.20601132201541444\n0.3732470910066113,0.20584782057006348\n0.3734746665970078,0.20552081767930344\n0.3734746665970078,0.20535731623395245\n0.3737591360850123,0.2050303133432505\n0.3738160299826102,0.204785061175195\n0.37387292388020804,0.20470331045254855\n0.3740436055730118,0.20429455683914205\n0.3744987567538099,0.2038040525030891\n0.3747263323442115,0.20339529888968264\n0.3747832262418094,0.20339529888968264\n0.37512458962741174,0.20315004672162712\n0.3752952713202104,0.20282304383092517\n0.37575042250100854,0.20241429021751867\n0.3758073163986114,0.2023325394948722\n0.3758642102962093,0.20192378588146576\n0.37569352860341065,0.20151503226805925\n0.37512458962741174,0.2016785337134102\n0.37512458962741174,0.2016785337134102\n0.3743849689586091,0.20176028443611477\n0.37398671167540887,0.2016785337134102\n0.3737591360850123,0.20176028443611477\n0.3737022421874094,0.20176028443611477\n0.3734746665970078,0.20192378588146576\n0.3729626215186118,0.2018420351588193\n0.37290572762100893,0.2016785337134102\n0.3729626215186118,0.20151503226805925\n0.3729626215186118,0.20135153082270826\n0.3729626215186118,0.20126978010006183\n0.3730195154162097,0.20086102648665533\n0.37313330321141047,0.20045227287324888\n0.3730764093138076,0.20004351925984237\n0.3730764093138076,0.19979826709184492\n0.37313330321141047,0.19971651636914042\n0.3735884543922086,0.19849025552897903\n0.3735884543922086,0.19840850480627448\n0.3737022421874094,0.19816325263827708\n0.3735884543922086,0.197999751192868\n0.3740436055730118,0.19742749613416863\n0.37410049947060964,0.1971822439661131\n0.37410049947060964,0.1971004932434086\n0.37444186285621206,0.19595598312595175\n0.37455565065141283,0.19571073095789623\n0.3746125445490107,0.19562898023519168\n0.37518148352500963,0.19432096867238385\n0.37523837742261257,0.19423921794967933\n0.3752952713202104,0.19391221505897738\n0.37546595301300906,0.19391221505897738\n0.37575042250100854,0.19358521216827543\n0.37609178588661085,0.19317645855486892\n0.37614867978420874,0.1930947078321644\n0.37631936147701245,0.1929312063868134\n0.3764900431698111,0.19260420349611146\n0.3764331492722133,0.191868446992003\n0.3764900431698111,0.19178669626929848\n0.37683140655541353,0.19154144410130106\n0.37683140655541353,0.19105093976519003\n0.3767745126578106,0.19056043542913711\n0.3767745126578106,0.19047868470649065\n0.37688830045301136,0.19006993109308418\n0.37688830045301136,0.1897429282023822\n0.37683140655541353,0.1894976760343267\n0.37694519435060925,0.18900717169827377\n0.37694519435060925,0.18892542097556925\n0.37694519435060925,0.18827141519416532\n0.37683140655541353,0.18810791374881433\n0.37683140655541353,0.18794441230340528\n0.37705898214581,0.18761740941270333\n0.37711587604341296,0.18753565869005687\n0.37745723942901027,0.18622764712719098\n0.37745723942901027,0.18614589640448642\n0.37768481501941187,0.18581889351378447\n0.37768481501941187,0.18557364134572896\n0.37774170891700976,0.18581889351378447\n0.37774170891700976,0.18516488773238055\n0.37774170891700976,0.18508313700967605\n0.37796928450741135,0.1843473805055676\n0.3780261784050143,0.18377512544681013\n0.3780830723026121,0.18369337472416367\n0.3783106478930137,0.18336637183340362\n0.3788795868690126,0.1831211196654062\n0.37922095025460995,0.18279411677470425\n0.3793347380498107,0.18271236605199973\n0.37944852584501154,0.1824671138839442\n0.37996057092341257,0.1818948588252448\n0.3801312526162112,0.18148610521183833\n0.38018814651381416,0.18132260376648734\n0.3807001915922152,0.18042334581702793\n0.3807570854898131,0.18001459220362143\n0.3807570854898131,0.17993284148091693\n0.38092776718261173,0.17952408786751042\n0.38092776718261173,0.17870658064075554\n0.38092776718261173,0.178624829918051\n0.3808708732850139,0.17854307919540455\n0.38092776718261173,0.17837957775005356\n0.381326024465812,0.17829782702734906\n0.38149670615861564,0.17813432558199807\n0.3818949634418159,0.17813432558199807\n0.38166738785141435,0.17797082413664708\n0.38166738785141435,0.17788907341394256\n0.38115534277301333,0.1764993111284302\n0.38115534277301333,0.17641756040572568\n0.38098466108021467,0.17617230823772823\n0.38109844887541544,0.17600880679237726\n0.3808708732850139,0.17568180390161722\n0.3808708732850139,0.17551830245626623\n0.3808708732850139,0.17510954884291785\n0.3808708732850139,0.17502779812021332\n0.38092776718261173,0.1747825459521578\n0.38143981226101276,0.17470079522951135\n0.38115534277301333,0.17453729378416036\n0.3808708732850139,0.17453729378416036\n0.38081397938741096,0.17453729378416036\n0.3804726160018136,0.17421029089340032\n0.3804157221042107,0.17404678944804935\n0.3805295098994115,0.17388328800269837\n0.3807001915922152,0.1739650387254029\n0.38104155497781256,0.1737197865573474\n0.38104155497781256,0.17363803583470094\n0.3812691305682141,0.17339278366664546\n0.38138291836341487,0.17347453438934998\n0.38149670615861564,0.1733110329439409\n0.38138291836341487,0.1724117749944815\n0.38138291836341487,0.1724117749944815\n0.3812691305682141,0.17167601849037303\n0.38115534277301333,0.17151251704502207\n0.3812122366706112,0.17077676054091362\n0.3812122366706112,0.17069500981826716\n0.38115534277301333,0.17020450548215615\n0.38138291836341487,0.1698775025914542\n0.3815536000562135,0.16979575186880774\n0.3817811756466151,0.1698775025914542\n0.38200875123701167,0.16979575186880774\n0.3820656451346146,0.16971400114610322\n0.3822932207250162,0.16963225042345675\n0.3827483719058143,0.1693052475326967\n0.3827483719058143,0.16914174608734575\n0.3828621597010151,0.16889649391934833\n0.3830897352914116,0.16873299247393925\n0.38320352308661243,0.16873299247393925\n0.3833173108818132,0.1685694910285883\n0.3835448864722148,0.16816073741523987\n0.3836017803698127,0.16799723596983082\n0.3836017803698127,0.1675884823564824\n0.3836017803698127,0.16750673163377788\n0.3836017803698127,0.1671797287430759\n0.3837724620626164,0.16693447657502042\n0.3836586742674156,0.16677097512966943\n0.3837724620626164,0.16660747368431844\n0.3838862498578121,0.16644397223896745\n0.3838862498578121,0.16619872007091196\n0.3838862498578121,0.16611696934826553\n0.3841707193458166,0.16578996645756355\n0.3841707193458166,0.1656264650121545\n0.3840569315506158,0.16513596067610156\n0.3837724620626164,0.16497245923075057\n0.38371556816501345,0.16497245923075057\n0.3836017803698127,0.16472720706269506\n0.38371556816501345,0.16464545634004862\n0.3835448864722148,0.16407320128129116\n0.38348799257461186,0.16366444766788468\n0.38348799257461186,0.16358269694523822\n0.38337420477941614,0.1633374447771827\n0.383431098677014,0.16309219260912722\n0.3838862498578121,0.1626834389957207\n0.38400003765301294,0.16227468538237233\n0.38400003765301294,0.16219293465966778\n0.38394314375541505,0.1617841810462613\n0.3838293559602143,0.1616206796009103\n0.3837724620626164,0.16113017526485737\n0.3836017803698127,0.16080317237415542\n0.3836017803698127,0.1607214216514509\n0.38326041698421537,0.15998566514734244\n0.3828621597010151,0.1596586622566405\n0.3827483719058143,0.1596586622566405\n0.3824070085202119,0.15933165936593852\n0.38223632682741326,0.1584324014164791\n0.38223632682741326,0.15835065069377458\n0.3821794329298154,0.15761489418966612\n0.3822932207250162,0.15696088840826222\n0.38235011462261403,0.1568791376855577\n0.38257769021301563,0.15679738696291123\n0.3827483719058143,0.15679738696291123\n0.38291905359861295,0.15696088840826222\n0.38303284139381377,0.1573696420216687\n0.38337420477941614,0.15761489418966612\n0.383431098677014,0.15761489418966612\n0.3836586742674156,0.1577783956350171\n0.3838293559602143,0.15769664491237068\n0.3838862498578121,0.1573696420216687\n0.3838293559602143,0.15720614057625965\n0.38371556816501345,0.15671563624020668\n0.38371556816501345,0.15663388551756025\n0.3836586742674156,0.15540762467734082\n0.3838862498578121,0.15532587395469435\n0.38394314375541505,0.15532587395469435\n0.38485344611701633,0.15597987973609825\n0.3849672339122171,0.15622513190415377\n0.3849672339122171,0.1563068826268002\n0.38513791560501576,0.15655213479485572\n0.3852517034002166,0.15638863334950476\n0.3853085972978144,0.15606163045880278\n0.385536172888216,0.15606163045880278\n0.3858775362738133,0.1563068826268002\n0.38593443017141627,0.15638863334950476\n0.38656026304501306,0.15704263913090866\n0.38678783863541466,0.15745139274431513\n0.38684473253301754,0.1575331434670197\n0.3870723081234141,0.15810539852571906\n0.3871860959186149,0.1584324014164791\n0.3875274593042172,0.15875940430718105\n0.3875843532018151,0.15875940430718105\n0.3878119287922167,0.15892290575253204\n0.3886653372562151,0.1596586622566405\n0.388722231153818,0.1596586622566405\n0.3895756396178164,0.16047616948345347\n0.38968942741301715,0.16063967092880443\n0.38974632131061504,0.1607214216514509\n0.39025836638901606,0.16129367671020836\n0.3904290480818147,0.1613754274329129\n0.390827305365015,0.16129367671020836\n0.39094109316021575,0.16129367671020836\n0.39116866875061734,0.16129367671020836\n0.39139624434101894,0.16145717815555932\n0.3915669260338176,0.16145717815555932\n0.3918513955218171,0.16129367671020836\n0.39202207721461574,0.16129367671020836\n0.39202207721461574,0.16113017526485737\n0.39190828941942,0.16104842454215285\n0.3918513955218171,0.16104842454215285\n0.3914531382386168,0.1607214216514509\n0.39116866875061734,0.16023091731539796\n0.3909979870578187,0.16023091731539796\n0.3907704114674171,0.16014916659269343\n0.3907135175698192,0.16006741587004697\n0.3905428358770155,0.15982216370199148\n0.39059972977461843,0.15957691153393594\n0.39031526028661895,0.1588411550298275\n0.39031526028661895,0.15875940430718105\n0.3901445785938153,0.15835065069377458\n0.3900876846962174,0.1579418970803681\n0.3901445785938153,0.1577783956350171\n0.39037215418421684,0.1577783956350171\n0.39059972977461843,0.1580236478030726\n0.3906566236722163,0.1580236478030726\n0.3909979870578187,0.15835065069377458\n0.3915669260338176,0.1584324014164791\n0.39196518331701785,0.15859590286183006\n0.39202207721461574,0.15859590286183006\n0.39270480398582047,0.15875940430718105\n0.39276169788341836,0.15851415213912556\n0.39224965280501733,0.15810539852571906\n0.39224965280501733,0.15810539852571906\n0.3920789711122187,0.15786014635772164\n0.39219275890741945,0.15769664491237068\n0.3923634406002181,0.15761489418966612\n0.39270480398582047,0.1579418970803681\n0.39315995516661856,0.15810539852571906\n0.39321684906421644,0.15810539852571906\n0.3932737429618194,0.15810539852571906\n0.3933306368594172,0.1579418970803681\n0.39315995516661856,0.15769664491237068\n0.3931030612690207,0.1571243898536132\n0.3933306368594172,0.1571243898536132\n0.39338753075702015,0.15720614057625965\n0.3938426819378183,0.1575331434670197\n0.39395646973301907,0.1571243898536132\n0.3938995758354161,0.15696088840826222\n0.39395646973301907,0.15671563624020668\n0.39395646973301907,0.15663388551756025\n0.39401336363061695,0.15622513190415377\n0.39395646973301907,0.15606163045880278\n0.3936151063474167,0.15532587395469435\n0.3936151063474167,0.15532587395469435\n0.39321684906421644,0.1545901174505859\n0.39287548567861913,0.15434486528253039\n0.392932379576217,0.1541813638371794\n0.392932379576217,0.1540996131144749\n0.392932379576217,0.1541813638371794\n0.39287548567861913,0.1540996131144749\n0.3926479100882176,0.15385436094647748\n0.39253412229301676,0.15344560733307094\n0.39224965280501733,0.15287335227431348\n0.39219275890741945,0.15287335227431348\n0.3921358650098165,0.152464598660907\n0.39202207721461574,0.15189234360214954\n0.3917945016242192,0.15148358998880113\n0.3917945016242192,0.1514018392660966\n0.3915669260338176,0.15099308565269012\n0.39151003213621977,0.15042083059393266\n0.3916807138290184,0.15042083059393266\n0.39196518331701785,0.15042083059393266\n0.39202207721461574,0.15050258131663719\n0.39276169788341836,0.1507478334846927\n0.39281859178101625,0.15091133493004366\n0.39281859178101625,0.15066608276198817\n0.39270480398582047,0.15042083059393266\n0.39270480398582047,0.15042083059393266\n0.39247722839541893,0.14976682481252873\n0.392420334497816,0.14960332336717774\n0.3923634406002181,0.14919456975377127\n0.3923065467026203,0.14894931758577384\n0.39224965280501733,0.14894931758577384\n0.3917945016242192,0.1482135610816654\n0.3916807138290184,0.14764130602290793\n0.3916807138290184,0.1475595553002034\n0.3915669260338176,0.14739605385485244\n0.3915669260338176,0.14723255240950145\n0.39139624434101894,0.1469055495187995\n0.39139624434101894,0.14674204807344854\n0.3915669260338176,0.14657854662803946\n0.39173760772661625,0.14682379879609497\n0.39173760772661625,0.14682379879609497\n0.392932379576217,0.14764130602290793\n0.3929892734738199,0.1475595553002034\n0.3930461673714178,0.1475595553002034\n0.39281859178101625,0.14723255240950145\n0.39276169788341836,0.14666029735074398\n0.39287548567861913,0.14625154373733748\n0.39281859178101625,0.14616979301469105\n0.39276169788341836,0.14592454084663553\n0.39270480398582047,0.14551578723322905\n0.39281859178101625,0.14469828000647417\n0.39281859178101625,0.1446165292837696\n0.39281859178101625,0.14420777567036314\n0.3929892734738199,0.14379902205701472\n0.3933306368594172,0.14322676699825726\n0.3933306368594172,0.14322676699825726\n0.3935582124498188,0.14306326555290627\n0.3936151063474167,0.14281801338485078\n0.3937857880402204,0.14273626266214623\n0.39418404532342066,0.14208225688074233\n0.39424093922101855,0.14208225688074233\n0.39446851481142015,0.14175525399004038\n0.39446851481142015,0.14191875543539134\n0.3942978331186164,0.1420005061580959\n0.39424093922101855,0.1426545119394998\n0.39418404532342066,0.14273626266214623\n0.3943547270162193,0.1431450162755527\n0.3944116209138172,0.1437172713343102\n0.3946391965042188,0.1441260249477167\n0.3946391965042188,0.14420777567036314\n0.39469609040181663,0.14445302783841862\n0.39480987819701746,0.1446165292837696\n0.39498055988982117,0.14486178145182513\n0.3951512415826198,0.1449435321744716\n0.3953219232754185,0.1447800307291206\n0.3953788171730214,0.14453477856112318\n0.3953219232754185,0.14445302783841862\n0.3951512415826198,0.14404427422501215\n0.3952081354802177,0.14379902205701472\n0.3953219232754185,0.14355376988895924\n0.3954357110706193,0.1431450162755527\n0.3954357110706193,0.14306326555290627\n0.39572018055861874,0.14298151483020174\n0.396118437841819,0.1424910104941488\n0.39645980122742136,0.14298151483020174\n0.39645980122742136,0.14298151483020174\n0.39668737681781785,0.1431450162755527\n0.39691495240821945,0.1431450162755527\n0.39697184630581733,0.14355376988895924\n0.39702874020342027,0.1437172713343102\n0.39719942189621893,0.14388077277966116\n0.3974269974866205,0.14388077277966116\n0.3974838913842184,0.14388077277966116\n0.3981666181554181,0.14404427422501215\n0.39873555713142206,0.14404427422501215\n0.3989062388242207,0.14428952639306766\n0.3989062388242207,0.1443712771157722\n0.3989631327218186,0.14445302783841862\n0.39919070831222014,0.14445302783841862\n0.39919070831222014,0.1447800307291206\n0.3990200266194215,0.14486178145182513\n0.39856487543861835,0.14453477856112318\n0.39850798154102046,0.14445302783841862\n0.3981097242578202,0.1441260249477167\n0.39793904256502155,0.1441260249477167\n0.39776836087221784,0.14420777567036314\n0.3975407852818213,0.1441260249477167\n0.3973132096914197,0.14379902205701472\n0.39725631579382187,0.1437172713343102\n0.39719942189621893,0.14347201916625468\n0.39668737681781785,0.14322676699825726\n0.39663048292022,0.14306326555290627\n0.39668737681781785,0.14281801338485078\n0.39663048292022,0.14257276121679527\n0.39657358902262213,0.1424910104941488\n0.3964029073298184,0.14232750904879785\n0.3964029073298184,0.14191875543539134\n0.39634601343222053,0.14175525399004038\n0.39645980122742136,0.1411012482085784\n0.39645980122742136,0.1411012482085784\n0.3965166951250192,0.14085599604058097\n0.39645980122742136,0.14044724242717446\n0.39657358902262213,0.1401202395364725\n0.39691495240821945,0.13979323664577056\n0.39691495240821945,0.13979323664577056\n0.39719942189621893,0.13938448303236406\n0.3976545730770221,0.13897572941895758\n0.3976545730770221,0.13873047725090204\n0.3975407852818213,0.13848522508290464\n0.3975407852818213,0.13840347436020012\n0.3973701035890176,0.13840347436020012\n0.3970856341010181,0.13889397869625303\n0.39657358902262213,0.13922098158701307\n0.39645980122742136,0.13922098158701307\n0.39572018055861874,0.1393027323096595\n0.3950943476850169,0.13938448303236406\n0.39503745378741906,0.1394662337550105\n0.3945823026066209,0.139874987368417\n0.3943547270162193,0.13954798447771505\n0.3942978331186164,0.1393027323096595\n0.3942978331186164,0.1390574801416621\n0.3942978331186164,0.1390574801416621\n0.3943547270162193,0.1386487265282556\n0.39424093922101855,0.13848522508290464\n0.39424093922101855,0.13832172363755368\n0.394525408709018,0.1377494685787962\n0.394525408709018,0.1377494685787962\n0.39498055988982117,0.1377494685787962\n0.39526502937782065,0.1373407149653897\n0.3954357110706193,0.1369319613519832\n0.3954357110706193,0.1369319613519832\n0.3957770744562216,0.13660495846128126\n0.3957770744562216,0.13635970629322575\n0.39572018055861874,0.13578745123446828\n0.3959477561490203,0.1356239497891173\n0.3959477561490203,0.1356239497891173\n0.39623222563701976,0.1352151961757689\n0.39634601343222053,0.1348064425623624\n0.3968011646130187,0.13439768894895593\n0.3968011646130187,0.13439768894895593\n0.39719942189621893,0.13374368316755203\n0.3974269974866205,0.13317142810879457\n0.3974269974866205,0.13308967738609\n0.3975407852818213,0.13292617594073902\n0.3974269974866205,0.1326809237726835\n0.39776836087221784,0.13219041943663057\n0.39771146697461995,0.13169991510057766\n0.39776836087221784,0.1316181643778731\n0.39799593646261944,0.1311276600418202\n0.3981666181554181,0.13039190353771174\n0.39822351205302103,0.13031015281500719\n0.39822351205302103,0.1300649006470098\n0.3981666181554181,0.12916564269755038\n0.39799593646261944,0.1290021412521994\n0.39793904256502155,0.12892039052949483\n0.3976545730770221,0.12802113258003545\n0.3974269974866205,0.12769412968933347\n0.3973701035890176,0.12769412968933347\n0.39691495240821945,0.12753062824398248\n0.3968580585106216,0.12736712679857343\n0.39657358902262213,0.12663137029446497\n0.39657358902262213,0.12654961957181854\n0.39645980122742136,0.12630436740376302\n0.39634601343222053,0.12614086595841206\n0.39623222563701976,0.12556861089965457\n0.39628911953461765,0.12532335873165715\n0.39628911953461765,0.12524160800895262\n0.39634601343222053,0.1245876022275487\n0.39628911953461765,0.12417884861414222\n0.39634601343222053,0.1239335964460867\n0.39634601343222053,0.1239335964460867\n0.39657358902262213,0.12319783994197826\n0.39691495240821945,0.12270733560592534\n0.39697184630581733,0.1226255848832208\n0.39714252799862104,0.12238033271522336\n0.39776836087221784,0.12164457621111494\n0.3978252547698208,0.1215628254884104\n0.3981097242578202,0.12123582259770845\n0.3982804059506189,0.12099057042965293\n0.39856487543861835,0.12082706898430196\n0.39873555713142206,0.12058181681630452\n0.39879245102901995,0.12050006609359999\n0.3989062388242207,0.12033656464824902\n0.39919070831222014,0.11968255886678701\n0.39941828390262174,0.11935555597608506\n0.39941828390262174,0.11927380525343861\n0.3995889655954204,0.1190285530853831\n0.3995320716978225,0.11927380525343861\n0.3993613900050188,0.11911030380808764\n0.39941828390262174,0.11894680236267857\n0.39919070831222014,0.11861979947197661\n0.39913381441462226,0.11861979947197661\n0.39884934492662283,0.11829279658127466\n0.39862176933622123,0.11829279658127466\n0.39822351205302103,0.11804754441321914\n0.3980528303602224,0.11821104585857013\n0.3981097242578202,0.11845629802662563\n0.3981097242578202,0.11845629802662563\n0.39799593646261944,0.11829279658127466\n0.3976545730770221,0.11804754441321914\n0.3973701035890176,0.1177205415225172\n0.39719942189621893,0.1177205415225172\n0.39702874020342027,0.11747528935451976\n0.39697184630581733,0.11747528935451976\n0.39691495240821945,0.11755704007716622\n0.39691495240821945,0.11698478501840874\n0.39668737681781785,0.11665778212770678\n0.3965166951250192,0.11673953285041132\n0.3965166951250192,0.11698478501840874\n0.39645980122742136,0.11706653574111328\n0.3958908622514174,0.11739353863181523\n0.3957770744562216,0.11731178790911069\n0.3958908622514174,0.11698478501840874\n0.3958908622514174,0.11673953285041132\n0.3958908622514174,0.11665778212770678\n0.3958339683538195,0.1162490285143003\n0.3958908622514174,0.11608552706894933\n0.3958339683538195,0.11584027490089381\n0.3957770744562216,0.11526801984219442\n0.3958339683538195,0.1151862691194899\n0.3961753317394219,0.11445051261538144\n0.3958339683538195,0.11428701117003047\n0.39566328666102085,0.11445051261538144\n0.39566328666102085,0.11445051261538144\n0.39572018055861874,0.11428701117003047\n0.39566328666102085,0.11404175900197497\n0.39526502937782065,0.1141235097246795\n0.3952081354802177,0.11387825755662398\n0.3953788171730214,0.11379650683397755\n0.39549260496821714,0.11355125466592203\n0.39549260496821714,0.11355125466592203\n0.39572018055861874,0.11330600249786651\n0.39572018055861874,0.11306075032986909\n0.3960615439442211,0.11232499382576065\n0.3960615439442211,0.11232499382576065\n0.39645980122742136,0.11158923732165221\n0.39657358902262213,0.11093523154019021\n0.39657358902262213,0.11085348081754376\n0.39663048292022,0.1102812257587863\n0.39657358902262213,0.11011772431343532\n0.3965166951250192,0.10954546925467784\n0.3965166951250192,0.10946371853197331\n0.39645980122742136,0.10913671564127135\n0.3965166951250192,0.10864621130521843\n0.39657358902262213,0.1081557069691074\n0.3965166951250192,0.10807395624646095\n0.3965166951250192,0.10791045480110997\n0.39657358902262213,0.10750170118770348\n0.39634601343222053,0.10717469829700153\n0.39645980122742136,0.10676594468359504\n0.3965166951250192,0.1066841939608905\n0.3967442707154208,0.10635719107018855\n0.3970856341010181,0.10619368962483756\n0.3973132096914197,0.10553968384343367\n0.3973132096914197,0.10553968384343367\n0.3976545730770221,0.1049674287846762\n0.3976545730770221,0.10423167228056776\n0.3976545730770221,0.10423167228056776\n0.3976545730770221,0.10365941722181028\n0.39776836087221784,0.10382291866716126\n0.39771146697461995,0.10325066360840379\n0.39771146697461995,0.10316891288575734\n0.3976545730770221,0.10276015927235085\n0.39771146697461995,0.10259665782699989\n0.39793904256502155,0.10251490710429535\n0.3981097242578202,0.10186090132289144\n0.3981097242578202,0.10177915060018691\n0.3981097242578202,0.1013703969868385\n0.3983372998482218,0.10145214770948494\n0.3983941937458197,0.10120689554142943\n0.39879245102901995,0.1007163912053765\n0.39879245102901995,0.1007163912053765\n0.3981666181554181,0.10063464048273005\n0.3982804059506189,0.100471139037321\n0.39850798154102046,0.100471139037321\n0.39850798154102046,0.10030763759197\n0.39873555713142206,0.10030763759197\n0.39873555713142206,0.10038938831467455\n0.3989631327218186,0.10030763759197\n0.3990200266194215,0.10014413614661903\n0.39913381441462226,0.09998063470126806\n0.39919070831222014,0.09957188108786157\n0.3994751778002196,0.0992448781971596\n0.3994751778002196,0.0992448781971596\n0.39964585949302334,0.09908137675180864\n0.39964585949302334,0.09875437386110666\n0.3998734350834199,0.09875437386110666\n0.4000441167762236,0.09891787530645765\n0.40021479846902225,0.09883612458375313\n0.4003285862642231,0.0985908724157557\n0.4003285862642231,0.0985908724157557\n0.40049926795702173,0.09826386952499566\n0.40084063134261905,0.09777336518894272\n0.4012388886258193,0.0975281130209453\n0.4012957825234222,0.0975281130209453\n0.40135267642102007,0.09744636229824077\n0.401409570318623,0.09728286085288979\n0.4012388886258193,0.09720111013018526\n0.4012388886258193,0.09695585796218784\n0.40135267642102007,0.09679235651683685\n0.40169403980662244,0.0966288550714278\n0.40192161539702403,0.09671060579413232\n0.40197850929462187,0.09671060579413232\n0.40220608488502346,0.09654710434878135\n0.4023198726802243,0.09638360290343036\n0.4023198726802243,0.09605660001272841\n0.40249055437302295,0.0954025942312664\n0.40249055437302295,0.09532084350861997\n0.4025474482706208,0.09507559134056445\n0.4027750238610224,0.09474858844986249\n0.4027750238610224,0.09442158555910245\n0.40300259945142397,0.09409458266840048\n0.40305949334902186,0.09409458266840048\n0.40328706893942345,0.09393108122304952\n0.40351464452982505,0.09344057688699658\n0.40351464452982505,0.0932770754416456\n0.4036853262226237,0.09311357399629462\n0.40385600791542237,0.09295007255094365\n0.4039129018130202,0.09286832182823912\n0.4042542651986226,0.09245956821483262\n0.4050507797650231,0.09213256532413067\n0.4051076736626209,0.09205081460142614\n0.40539214315062544,0.09196906387877968\n0.4065869150002262,0.09213256532413067\n0.4066438088978241,0.09221431604683519\n0.4069282783858235,0.09221431604683519\n0.4072696417714259,0.09213256532413067\n0.4072696417714259,0.09188731315607515\n0.40738342956662166,0.09172381171072418\n0.40772479295222397,0.0915603102653732\n0.4078385807474248,0.0915603102653732\n0.40829373192822294,0.09131505809731769\n0.4083506258258258,0.0911515566519667\n0.4086919892114232,0.09090630448396929\n0.40880577700662396,0.09074280303861831\n0.4088626709042269,0.09066105231591379\n0.4094316098802258,0.09041580014785826\n0.4097160793682253,0.09008879725715631\n0.4100574427538276,0.08976179436645436\n0.4101143366514255,0.08976179436645436\n0.4107970634226252,0.0894347914757524\n0.4112522146034233,0.08902603786234592\n0.4112522146034233,0.08894428713964138\n0.41165047188662357,0.08853553352629298\n0.412503880350627,0.08837203208094199\n0.41256077424822485,0.08829028135823747\n0.41278834983862644,0.08820853063553294\n0.4132435010194246,0.08820853063553294\n0.41335728881462536,0.08804502919018195\n0.41352797050742907,0.0879632784675355\n0.4136986522002277,0.08804502919018195\n0.4138693338930264,0.08788152774483098\n0.4138693338930264,0.08788152774483098\n0.4136986522002277,0.08771802629948\n0.4138124399954285,0.08755452485412901\n0.41375554609782556,0.08739102340877804\n0.4144382728690253,0.08698226979537155\n0.4144382728690253,0.08698226979537155\n0.41432448507382447,0.08673701762731603\n0.41449516676662823,0.0866552669046696\n0.4148934240498285,0.0866552669046696\n0.4154054691282295,0.08632826401396765\n0.4154623630258274,0.08632826401396765\n0.4159744081042284,0.08600126112326567\n0.416714028773026,0.08583775967785662\n0.4167709226706289,0.08583775967785662\n0.41716917995382913,0.0855925075098592\n0.4177381189298281,0.08493850172839719\n0.4180225884178275,0.08485675100575074\n0.41807948231543046,0.08485675100575074\n0.4189328907794288,0.08485675100575074\n0.41910357247222746,0.08477500028304621\n0.4193880419602269,0.08436624666963971\n0.4194449358578298,0.08436624666963971\n0.42001387483382874,0.0841209945016423\n0.42035523821943116,0.08395749305629133\n0.42081038940022925,0.08403924377893776\n0.4208672832978322,0.08403924377893776\n0.42137932837622816,0.0838757423335868\n0.4217206917618305,0.08354873944288482\n0.4221189490450308,0.0834669887201803\n0.42228963073782944,0.08330348727482932\n0.42228963073782944,0.08330348727482932\n0.4224603124306281,0.08313998582947835\n0.42285856971382835,0.08305823510683188\n0.42302925140663206,0.08297648438412736\n0.42365508428022886,0.08281298293877638\n0.42359819038263097,0.08281298293877638\n0.4240533415634291,0.08273123221607186\n0.4242240232562328,0.08232247860272346\n0.4243947049490315,0.08224072788001892\n0.4247929622322317,0.08215897715731438\n0.4247929622322317,0.08215897715731438\n0.4253619012082306,0.08199547571196342\n0.42530500731063275,0.081750223543966\n0.425703264593833,0.08134146993055949\n0.425703264593833,0.08125971920785496\n0.42587394628663167,0.08134146993055949\n0.4260446279794303,0.08142322065326403\n0.4264428852626306,0.08134146993055949\n0.42706871813623243,0.08134146993055949\n0.4271825059314332,0.08134146993055949\n0.42860485337143045,0.08117796848520853\n0.42860485337143045,0.08109621776250399\n0.428889322859435,0.08109621776250399\n0.4287755350642342,0.08125971920785496\n0.42934447404023307,0.08109621776250399\n0.4292306862450323,0.08093271631715301\n0.42900311065463076,0.08085096559450657\n0.42900311065463076,0.08076921487180203\n0.4284341716786318,0.08076921487180203\n0.4277514449074321,0.08044221198110008\n0.42763765711223134,0.08044221198110008\n0.4272393998290311,0.08036046125839555\n0.4276945510098342,0.08019695981304456\n0.4280928082930345,0.0802787105357491\n0.42814970219063236,0.08036046125839555\n0.42814970219063236,0.08052396270374652\n0.428320383883431,0.08060571342645105\n0.4288324289618321,0.08068746414915559\n0.4287186411666312,0.08052396270374652\n0.42894621675703287,0.08052396270374652\n0.42900311065463076,0.08060571342645105\n0.4292306862450323,0.08076921487180203\n0.4296289435282326,0.08076921487180203\n0.4299703069138349,0.08093271631715301\n0.4300272008114328,0.08076921487180203\n0.43019788250423147,0.08068746414915559\n0.43019788250423147,0.08068746414915559\n0.43042545809463306,0.08060571342645105\n0.4309943970706319,0.08085096559450657\n0.4311650787634357,0.08068746414915559\n0.4311650787634357,0.0802787105357491\n0.4311650787634357,0.0802787105357491\n0.43173401773943454,0.08011520909039813\n0.4319615933298362,0.08003345836769359\n0.43207538112503185,0.08019695981304456\n0.4319615933298362,0.08036046125839555\n0.4319615933298362,0.08052396270374652\n0.4320184872274341,0.08060571342645105\n0.43213227502263485,0.08060571342645105\n0.4323029567154335,0.08076921487180203\n0.4332132590770348,0.08085096559450657\n0.4334408346674364,0.08101446703985754\n0.4334977285650342,0.08101446703985754\n0.4337821980530337,0.08109621776250399\n0.4340666675410331,0.08134146993055949\n0.4345218187218363,0.08199547571196342\n0.4345218187218363,0.08199547571196342\n0.4346356065170371,0.08215897715731438\n0.43480628820983575,0.08232247860272346\n0.4350338638002374,0.08264948149342541\n0.43554590887863337,0.08305823510683188\n0.43560280277623625,0.08313998582947835\n0.43594416616183357,0.08363049016553128\n0.43628552954743594,0.0838757423335868\n0.43656999903543536,0.08420274522428875\n0.43662689293303836,0.08420274522428875\n0.436797574625837,0.08436624666963971\n0.43702515021623856,0.08436624666963971\n0.43713893801143433,0.08452974811504879\n0.4372527258066351,0.08502025245110173\n0.43736651360183587,0.08526550461915723\n0.43736651360183587,0.08534725534180368\n0.4375940891922375,0.08534725534180368\n0.4381630281682364,0.08485675100575074\n0.43833370986103504,0.08493850172839719\n0.43839060375863803,0.08518375389645269\n0.43839060375863803,0.08518375389645269\n0.4385043915538388,0.08567425823250563\n0.4384474976562359,0.08583775967785662\n0.4385043915538388,0.08624651329126311\n0.43839060375863803,0.0866552669046696\n0.43839060375863803,0.08673701762731603\n0.4384474976562359,0.08698226979537155\n0.43878886104183823,0.08706402051807607\n0.43913022442743554,0.08681876835002057\n0.43930090612023925,0.08690051907272511\n0.4394715878130379,0.0866552669046696\n0.4394715878130379,0.08657351618196506\n0.43964226950583657,0.08641001473661408\n0.4398698450962382,0.08641001473661408\n0.4398129511986352,0.08714577124072254\n0.4398698450962382,0.08739102340877804\n0.4398698450962382,0.08739102340877804\n0.4398698450962382,0.08763627557683355\n0.439926738993836,0.08779977702218453\n0.439983632891439,0.08812677991288648\n0.4403249962770363,0.08861728424893942\n0.4403818901746392,0.08869903497164394\n0.44066635966263873,0.08869903497164394\n0.4404387840722371,0.08820853063553294\n0.4404387840722371,0.0879632784675355\n0.44055257186743785,0.08755452485412901\n0.44055257186743785,0.08755452485412901\n0.4408370413554374,0.08690051907272511\n0.4408370413554374,0.08649176545931861\n0.4407801474578395,0.08608301184591213\n0.4407801474578395,0.08608301184591213\n0.4408370413554374,0.08591951040056114\n0.4410646169458389,0.08583775967785662\n0.44095082915063816,0.08575600895521017\n0.44100772304823604,0.08551075678715467\n0.4411215108434368,0.08518375389645269\n0.4412921925362405,0.08493850172839719\n0.4412921925362405,0.08493850172839719\n0.441519768126637,0.08452974811504879\n0.4411784047410397,0.08379399161094034\n0.4411784047410397,0.08354873944288482\n0.4411215108434368,0.0834669887201803\n0.44095082915063816,0.08313998582947835\n0.44095082915063816,0.08297648438412736\n0.4410646169458389,0.08281298293877638\n0.4411784047410397,0.08215897715731438\n0.4412352986386376,0.08215897715731438\n0.441519768126637,0.081586722098615\n0.44157666202423995,0.08101446703985754\n0.441519768126637,0.08076921487180203\n0.441519768126637,0.08076921487180203\n0.4417473437170386,0.08068746414915559\n0.4419180254098373,0.08125971920785496\n0.44214560100023886,0.08166847282126145\n0.44214560100023886,0.08191372498931696\n0.44214560100023886,0.08199547571196342\n0.4422593887954397,0.0824042293253699\n0.44288522166903643,0.08313998582947835\n0.4429421155666393,0.08322173655218287\n0.44322658505463886,0.08354873944288482\n0.4434541606450404,0.0834669887201803\n0.4435110545426383,0.08371224088823581\n0.4433403728498396,0.08379399161094034\n0.44322658505463886,0.08395749305629133\n0.44316969115704097,0.08420274522428875\n0.44322658505463886,0.08420274522428875\n0.44362484233783905,0.08477500028304621\n0.4437386301330398,0.08469324956039978\n0.4437386301330398,0.08420274522428875\n0.4437386301330398,0.0841209945016423\n0.4437955240306377,0.08354873944288482\n0.44368173623543694,0.08273123221607186\n0.44368173623543694,0.08264948149342541\n0.44362484233783905,0.08166847282126145\n0.44368173623543694,0.08150497137591048\n0.4439093118258385,0.081586722098615\n0.4439662057234415,0.08166847282126145\n0.4445920385970382,0.0824042293253699\n0.44476272028984193,0.08289473366142283\n0.44476272028984193,0.08297648438412736\n0.4449334019826406,0.08338523799753386\n0.44504718977784136,0.0834669887201803\n0.44510408367543924,0.08363049016553128\n0.44521787147064,0.08379399161094034\n0.4451609775730421,0.08420274522428875\n0.44521787147064,0.08428449594699328\n0.4452747653682379,0.08452974811504879\n0.4451609775730421,0.08469324956039978\n0.4451609775730421,0.08485675100575074\n0.4452747653682379,0.0855925075098592\n0.4452747653682379,0.08567425823250563\n0.4453316592658408,0.08649176545931861\n0.44538855316343867,0.08673701762731603\n0.44544544706104167,0.08690051907272511\n0.4456161287538403,0.08706402051807607\n0.4456730226514382,0.08698226979537155\n0.4456730226514382,0.08690051907272511\n0.4456730226514382,0.08641001473661408\n0.4460143860370405,0.08575600895521017\n0.4460712799346384,0.08551075678715467\n0.4461281738322413,0.0854290060645082\n0.44641264332024083,0.08510200317374816\n0.4466402189106424,0.08502025245110173\n0.4469815822962397,0.08444799739234425\n0.4469815822962397,0.08436624666963971\n0.4469815822962397,0.0841209945016423\n0.4470384761938426,0.08395749305629133\n0.4470384761938426,0.0834669887201803\n0.4469815822962397,0.08322173655218287\n0.4470384761938426,0.08297648438412736\n0.4470384761938426,0.08297648438412736\n0.4472660517842392,0.08264948149342541\n0.4473229456818421,0.08232247860272346\n0.44755052127223865,0.081750223543966\n0.44760741516984154,0.08166847282126145\n0.44755052127223865,0.08109621776250399\n0.44692468839864186,0.08060571342645105\n0.4468677945010389,0.08052396270374652\n0.4466402189106424,0.0802787105357491\n0.4464695372178387,0.07986995692234261\n0.4465264311154416,0.07970645547699162\n0.4464695372178387,0.07954295403164066\n0.4465264311154416,0.07905244969552963\n0.4465833250130395,0.07897069897288318\n0.4466402189106424,0.07880719752753221\n0.44669711280824026,0.07839844391412572\n0.44681090060344103,0.07823494246877474\n0.44692468839864186,0.07766268741001728\n0.44692468839864186,0.07758093668737082\n0.4470384761938426,0.07717218307396434\n0.4469815822962397,0.07692693090590882\n0.44720915788664134,0.07692693090590882\n0.44755052127223865,0.07643642656985589\n0.44760741516984154,0.07635467584715136\n0.447891884657841,0.07586417151109842\n0.4482901419410412,0.07545541789769195\n0.448403929736242,0.07529191645234096\n0.4484608236338399,0.07521016572963643\n0.44868839922424153,0.07463791067093704\n0.4488590809170402,0.0739021541668286\n0.4488590809170402,0.07382040344412406\n0.44897286871224096,0.07275764404931367\n0.44908665650744173,0.07234889043590717\n0.4491435504050396,0.07226713971326074\n0.4492004443026425,0.07210363826785166\n0.44902976260983885,0.07202188754520522\n0.44902976260983885,0.07267589332660913\n0.44908665650744173,0.07308464694001562\n0.44908665650744173,0.07316639766272015\n0.4491435504050396,0.07357515127606856\n0.44902976260983885,0.07414740633482603\n0.44902976260983885,0.0745561599482325\n0.44902976260983885,0.0745561599482325\n0.4492573382002404,0.07463791067093704\n0.4493142320978434,0.07529191645234096\n0.4492573382002404,0.07545541789769195\n0.44942801989303904,0.07570067006574745\n0.44942801989303904,0.07570067006574745\n0.4496555954834407,0.07570067006574745\n0.44988317107384224,0.07651817729250233\n0.45011074666424383,0.07643642656985589\n0.45022453445943955,0.07668167873785331\n0.4502814283570425,0.07676342946055784\n0.45045211004984115,0.07692693090590882\n0.4506227917426398,0.07741743524196176\n0.4509641551282421,0.07798969030071923\n0.45102104902584006,0.07798969030071923\n0.45119173071864377,0.07823494246877474\n0.4513624124114424,0.07880719752753221\n0.45170377579704485,0.07937945258628967\n0.4517606696946426,0.07946120330893612\n0.4523296086706416,0.08036046125839555\n0.45244339646584236,0.08076921487180203\n0.45244339646584236,0.08085096559450657\n0.45250029036344025,0.08093271631715301\n0.45289854764664045,0.08101446703985754\n0.4530692293394442,0.08117796848520853\n0.453183017134645,0.08134146993055949\n0.45329680492984076,0.08199547571196342\n0.45335369882744364,0.08199547571196342\n0.45341059272504153,0.08248598004807443\n0.4535243805202423,0.08281298293877638\n0.4535243805202423,0.0834669887201803\n0.45358127441784524,0.0834669887201803\n0.4536381683154431,0.08371224088823581\n0.4537519561106439,0.08428449594699328\n0.4536381683154431,0.08444799739234425\n0.4537519561106439,0.08493850172839719\n0.4538088500082418,0.08493850172839719\n0.45386574390584467,0.08518375389645269\n0.4537519561106439,0.08551075678715467\n0.4537519561106439,0.08567425823250563\n0.4540933194962412,0.08616476256861666\n0.4541502133938441,0.08616476256861666\n0.45392263780344255,0.08616476256861666\n0.45392263780344255,0.08632826401396765\n0.454207107291442,0.08657351618196506\n0.454207107291442,0.08690051907272511\n0.45437778898424575,0.08714577124072254\n0.45437778898424575,0.08714577124072254\n0.45432089508664275,0.08739102340877804\n0.4546622584722452,0.08755452485412901\n0.45477604626744594,0.0879632784675355\n0.45483294016504383,0.08812677991288648\n0.45471915236984306,0.08829028135823747\n0.45477604626744594,0.08837203208094199\n0.45477604626744594,0.08837203208094199\n0.4549467279602446,0.08918953930769688\n0.4550036218578425,0.08935304075304787\n0.45483294016504383,0.08976179436645436\n0.45483294016504383,0.0898435450891589\n0.45477604626744594,0.08992529581180533\n0.4549467279602446,0.08992529581180533\n0.4550036218578425,0.09033404942521181\n0.45517430355064625,0.09057930159320925\n0.45523119744824414,0.09106980592932026\n0.45523119744824414,0.09106980592932026\n0.4554018791410428,0.09090630448396929\n0.4554018791410428,0.09074280303861831\n0.45551566693624357,0.09106980592932026\n0.45551566693624357,0.09139680882002223\n0.4557432425266451,0.09139680882002223\n0.455800136424243,0.09139680882002223\n0.45585703032184594,0.09139680882002223\n0.45562945473144434,0.09139680882002223\n0.455800136424243,0.09180556243342872\n0.4559139242194438,0.09196906387877968\n0.4560846059122425,0.09229606676948166\n0.4560846059122425,0.09245956821483262\n0.4560277120146446,0.09254131893753716\n0.45597081811704165,0.09278657110553458\n0.4560846059122425,0.09295007255094365\n0.45614149980984536,0.09311357399629462\n0.4562552876050462,0.0930318232735901\n0.45648286319544273,0.09319532471894107\n0.45642596929784485,0.09352232760964302\n0.45642596929784485,0.09360407833234756\n0.45642596929784485,0.09393108122304952\n0.4560846059122425,0.09417633339110502\n0.4562552876050462,0.09409458266840048\n0.45642596929784485,0.09417633339110502\n0.45642596929784485,0.09458508700451151\n0.45648286319544273,0.09466683772715795\n0.4565966509906435,0.09499384061785993\n0.4565966509906435,0.09532084350861997\n0.45676733268344216,0.09564784639932192\n0.4568242265810451,0.09597484929002387\n0.4568242265810451,0.09605660001272841\n0.456881120478643,0.09630185218072582\n0.45705180217144664,0.0964653536260768\n0.45705180217144664,0.0966288550714278\n0.45745005945464695,0.09679235651683685\n0.4576776350450435,0.09720111013018526\n0.4576776350450435,0.09728286085288979\n0.4577345289426464,0.0975281130209453\n0.45790521063544504,0.09769161446629628\n0.4576776350450435,0.09777336518894272\n0.4576207411474456,0.09793686663429368\n0.4580758923282437,0.09834562024770019\n0.4581327862258467,0.09842737097040472\n0.45830346791864535,0.09908137675180864\n0.45870172520184554,0.09981713325591707\n0.45875861909944343,0.09981713325591707\n0.45892930079224714,0.10006238542397258\n0.45870172520184554,0.09998063470126806\n0.458986194689845,0.10022588686932357\n0.4593844519730452,0.1007163912053765\n0.4594413458706431,0.10079814192808104\n0.45966892146104465,0.1013703969868385\n0.46006717874424496,0.10194265204553787\n0.46012407264184785,0.10194265204553787\n0.4602378604370436,0.10235140565894436\n0.4602378604370436,0.10276015927235085\n0.4604085421298473,0.1028419099949973\n0.46052232992504816,0.10325066360840379\n0.46052232992504816,0.10325066360840379\n0.46057922382264593,0.10300541144040637\n0.4606361177202438,0.1028419099949973\n0.4606361177202438,0.10259665782699989\n0.4607499055154446,0.1024331563816489\n0.461091268901047,0.1030871621630528\n0.461091268901047,0.10316891288575734\n0.4611481627986449,0.10341416505375478\n0.461091268901047,0.10398642011251225\n0.46097748110584624,0.10365941722181028\n0.46097748110584624,0.10390466938986578\n0.46097748110584624,0.10390466938986578\n0.4612050566962478,0.10406817083521677\n0.46126195059384567,0.10382291866716126\n0.4616033139794481,0.10365941722181028\n0.46171710177464886,0.10374116794451482\n0.461660207877046,0.10414992155786322\n0.461660207877046,0.10423167228056776\n0.46171710177464886,0.10472217661662069\n0.46183088956984464,0.10488567806197166\n0.4618877834674475,0.10570318528878464\n0.4618877834674475,0.10578493601143109\n0.4620584651602462,0.1064389417928931\n0.4622860407506478,0.107092947574297\n0.4622860407506478,0.10717469829700153\n0.4622860407506478,0.10741995046499896\n0.4621153590578491,0.10741995046499896\n0.4620584651602462,0.10782870407840545\n0.46222914685304484,0.1081557069691074\n0.46245672244344643,0.10807395624646095\n0.46245672244344643,0.10807395624646095\n0.46279808582904874,0.10799220552375642\n0.4630825553170483,0.10799220552375642\n0.46365149429304714,0.10766520263305446\n0.4637652820882479,0.10766520263305446\n0.46410664547384534,0.10766520263305446\n0.46444800885944765,0.10782870407840545\n0.46450490275704553,0.10807395624646095\n0.46450490275704553,0.10823745769181194\n0.46439111496184976,0.10848270985986744\n0.4643342210642469,0.10856446058251389\n0.464277327166649,0.10889146347321585\n0.4643342210642469,0.1090549649186249\n0.46496005393784867,0.10946371853197331\n0.46507384173304944,0.10954546925467784\n0.4654152051186468,0.11003597359073078\n0.465358311221049,0.11044472720413727\n0.4655858868114505,0.11085348081754376\n0.4655858868114505,0.11085348081754376\n0.4657565685042492,0.11109873298554118\n0.4660979318898465,0.11068997937219277\n0.4664392952754489,0.11060822864948824\n0.4666099769682476,0.11052647792678372\n0.46666687086585046,0.11044472720413727\n0.46672376476344835,0.11036297648143273\n0.46695134035385,0.11077173009483923\n0.4673495976370502,0.11077173009483923\n0.4676909610226475,0.11077173009483923\n0.4678047488178483,0.11060822864948824\n0.4678047488178483,0.11060822864948824\n0.4679185366130491,0.11068997937219277\n0.46774785492025045,0.11085348081754376\n0.46757717322744674,0.11077173009483923\n0.4683736877938472,0.11085348081754376\n0.4683736877938472,0.11093523154019021\n0.46860126338424873,0.11109873298554118\n0.46848747558904796,0.11118048370824571\n0.46945467184825224,0.11134398515359668\n0.4695115657458501,0.11142573587630122\n0.4700236108242511,0.11150748659894767\n0.4703649742098484,0.11167098804429865\n0.47099080708345026,0.11158923732165221\n0.47099080708345026,0.11158923732165221\n0.4719011094450516,0.1119979909350006\n0.4722993667282518,0.1119979909350006\n0.4723562606258497,0.1119979909350006\n0.4725838362162512,0.11207974165770514\n0.4734372446802496,0.11281549816181358\n0.47349413857785255,0.11289724888451812\n0.4741768653490523,0.113714756111273\n0.47429065314425306,0.11396000827932852\n0.47423375924665007,0.11396000827932852\n0.47423375924665007,0.11404175900197497\n0.47429065314425306,0.11420526044732594\n0.4746320165298504,0.11436876189267692\n0.47508716771065357,0.11469576478343696\n0.47520095550585434,0.11494101695143438\n0.47520095550585434,0.11502276767413892\n0.47548542499385377,0.11616727779165385\n0.4754285310962509,0.11641252995965128\n0.4754285310962509,0.1164942806823558\n0.4756561066866524,0.11731178790911069\n0.4757130005842503,0.1179657936905727\n0.47576989448185325,0.11804754441321914\n0.4758267883794511,0.11878330091732758\n0.475371637198653,0.11935555597608506\n0.4753147433010501,0.11943730669878959\n0.47514406160825146,0.12025481392554448\n0.4753147433010501,0.12082706898430196\n0.4753147433010501,0.1209088197070065\n0.47554231889145165,0.12107232115235747\n0.475883682277054,0.12066356753895097\n0.47605436396985273,0.12058181681630452\n0.47645262125305293,0.12066356753895097\n0.47656640904825376,0.12066356753895097\n0.47696466633145396,0.12082706898430196\n0.47736292361465427,0.12172632693376137\n0.47741981751225204,0.12172632693376137\n0.477988756488251,0.12238033271522336\n0.4785008015666521,0.12270733560592534\n0.47861458936185286,0.12270733560592534\n0.479069740542656,0.12278908632857177\n0.4794111039282533,0.12303433849662729\n0.47969557341625274,0.1232795906646828\n0.47975246731385573,0.12344309211003378\n0.47969557341625274,0.12352484283268021\n0.47963867951865485,0.12360659355538475\n0.4795248917234541,0.12385184572344025\n0.47969557341625274,0.12385184572344025\n0.4803214062898546,0.12344309211003378\n0.480605875777854,0.12336134138732924\n0.4807196635730549,0.12336134138732924\n0.4810610269586522,0.12344309211003378\n0.4815161781394554,0.12377009500073573\n0.4819144354226556,0.12377009500073573\n0.4820282232178564,0.12377009500073573\n0.4829385255794577,0.12352484283268021\n0.483279888965055,0.12352484283268021\n0.4833936767602558,0.12352484283268021\n0.48441776691705785,0.12377009500073573\n0.484872918097856,0.12377009500073573\n0.48498670589305676,0.12368834427808928\n0.48527117538105624,0.12368834427808928\n0.48532806927865413,0.12360659355538475"
  },
  {
    "path": "demo/src/main/assets/tracks/track3.txt",
    "content": "0.4961379098226587,0.3391014985361062\n0.5002342704498619,0.33877449564540424\n0.5047288883602654,0.3378752376959448\n0.5054116151314652,0.3382839913093513\n0.5058098724146654,0.33836574203199776\n0.5066632808786637,0.3382839913093513\n0.5074029015474664,0.33779348697329836\n0.5076873710354658,0.33730298263718733\n0.5078011588306666,0.3364037246877279\n0.5073460076498635,0.3355044667382685\n0.5066632808786637,0.33501396240221554\n0.5060943419026648,0.33485046095686455\n0.5054685090290629,0.33493221167951104\n0.5033065409202631,0.3343599566207536\n0.5023393446610639,0.3343599566207536\n0.4983567718290615,0.335586217460973\n0.49545518305145897,0.3361584725197305\n0.49391904781626084,0.3364037246877279\n0.48703488620665586,0.3365672261330789\n0.4856694326642565,0.3364037246877279\n0.4846453425074544,0.3359949710743214\n0.48401950963385765,0.33542271601562207\n0.48316610116985426,0.3343599566207536\n0.4816299659346562,0.3318256842177263\n0.48049208798265325,0.3321526871084282\n0.4741199714514493,0.3299454175961029\n0.4700236108242511,0.3280651509745376\n0.4664392952754489,0.3261848843529142\n0.465301417323446,0.3255308785715103\n0.46285497972664663,0.32373236267253336\n0.46217225295544695,0.32356886122718237\n0.4618877834674475,0.32324185833648045\n0.461091268901047,0.32307835689112946\n0.4588155129970463,0.32291485544577847\n0.45779142284024427,0.322506101832372\n0.45710869606904453,0.32201559749631903\n0.45295544154424344,0.3178463106397239\n0.4448196141874398,0.32626663507561876\n0.4381630281682364,0.3321526871084282\n0.4331563651794369,0.3368942290238389\n0.43247363840823216,0.33779348697329836\n0.43218916892023274,0.33861099420005325\n0.4327012139986337,0.341554020216487\n0.43218916892023274,0.34179927238454255\n0.4322460628178356,0.3431890346700549\n0.4320184872274341,0.34417004334221885\n0.43173401773943454,0.3449057998463273\n0.4296289435282326,0.3471948200812991\n0.4217775856594284,0.35422538223168154\n0.4180225884178275,0.35782241402951925\n0.4078385807474248,0.36656974135611614\n0.4060748699218252,0.368204755809684\n0.39964585949302334,0.37261929483433465\n0.3858206423762155,0.38193887721968894\n0.38212253903221244,0.3847184017907717\n0.3819518573394138,0.3845549003454207\n0.38138291836341487,0.3845549003454207\n0.38115534277301333,0.3846366510681253\n0.38098466108021467,0.3850454046814737\n0.38098466108021467,0.3852906568495292\n0.37944852584501154,0.38602641335363763\n0.36835421581300754,0.3923212189999116\n0.354415210901004,0.40098679560380396\n0.35350490853940264,0.4017225521079124\n0.35231013668980193,0.4030305636707783\n0.3510584709426033,0.40499258101504815\n0.3483844577553973,0.4113691373840267\n0.34724657980339946,0.413576406896294\n0.34559665677300067,0.4159471778540284\n0.34332090086899997,0.4181544473662956\n0.34030552429619665,0.42036171687862095\n0.33353515048179244,0.42453100373527425\n0.32636651938419303,0.4304170557680837\n0.32374940009458997,0.4321338209443561\n0.31117584872498577,0.43859212803598113\n0.3077622148689822,0.44071764682560194\n0.30747774538098277,0.44047239465760446\n0.30668123081458226,0.4403906439348999\n0.3062260796337842,0.44055414538025095\n0.3059985040433826,0.44088114827095287\n0.3058278223505839,0.44145340332971034\n0.30332449085618163,0.44202565838846786\n0.2987160851505773,0.44267966416987176\n0.296269647553778,0.44251616272452077\n0.2939369977521794,0.4421074091111724\n0.28386677787697245,0.4391643830947386\n0.2817617036657704,0.4391643830947386\n0.2809651890993699,0.43924613381738503\n0.2796566294545734,0.4396548874307915\n0.2773808735505727,0.44071764682560194\n0.27556026882737017,0.44137165260706396\n0.27191905938097005,0.4421891598338188\n0.267196865880165,0.44292491633792724\n0.2641245954097638,0.4433336699513337\n0.25615944974576393,0.4427614148925763\n0.253485436558563,0.44292491633792724\n0.25018559049776024,0.44374242356474025\n0.24785294069615657,0.44472343223684613\n0.24625991156335558,0.44586794235436106\n0.24415483735215862,0.4478299596986309\n0.24227733873135818,0.4491379712614968\n0.24005847672495534,0.4503642321016581\n0.23766893302575387,0.45126349005117566\n0.23641726727855525,0.45159049294187764\n0.22776939484335063,0.4523262494459861\n0.22372992811375028,0.452816753782039\n0.2167888726065474,0.4532255073954455\n0.21166842182254206,0.45420651606755136\n0.21155463402734126,0.45404301462220037\n0.21109948284654315,0.45371601173149845\n0.21058743776814212,0.4538795131768494\n0.21030296828014267,0.4542882667902559\n0.21035986217774053,0.45461526968095783\n0.21070122556334292,0.45502402329436437\n0.21041675607534346,0.4553510261850663\n0.2062066076529394,0.4574765449747452\n0.19812767419373872,0.46238158833544873\n0.19602259998253668,0.4640166027890167\n0.19351926848813442,0.4663056230239884\n0.1927227539217339,0.46728663169615237\n0.18794366652333097,0.4739084402331284\n0.18396109369133354,0.48044849804739986\n0.18310768522733012,0.4800397444339934\n0.1825387462513312,0.4800397444339934\n0.18020609644973265,0.4808572516608064\n0.17940958188333214,0.48077550093810184\n0.1788406429073282,0.48036674732475343\n0.17861306731693166,0.4794674893752359\n0.178954430702529,0.47873173287112747\n0.18117329270893182,0.47644271263615573\n0.18231117066092964,0.47488944890523427\n0.18242495845613044,0.47448069529188586\n0.18242495845613044,0.4739084402331284\n0.18219738286572884,0.47358143734242647\n0.17935268798572926,0.4707201620486391\n0.17832859782892715,0.4701479069898816\n0.17639420531052885,0.4696574026538287\n0.175768372436927,0.47039315915793717\n0.17480117617772786,0.4713741678301011\n0.17406155550892524,0.472518677947558\n0.17400466161132735,0.47292743156096445\n0.17406155550892524,0.4739901909557749\n0.17457360058732627,0.4755434546866963\n0.17457360058732627,0.47627921119080474\n0.17434602499692975,0.4770149676949132\n0.17411844940652815,0.4773419705856151\n0.1721840568881248,0.4788952343165365\n0.16825837795372522,0.4815112574422103\n0.16450338071212436,0.4842907820132931\n0.1602932322897203,0.4862527993575629\n0.15750543130731856,0.48739730947507787\n0.1538642218609185,0.4883783181472418\n0.1521574049329167,0.4883783181472418\n0.15107642087851678,0.4881330659791863\n0.14692316635371563,0.48617104863491645\n0.14618354568491806,0.48592579646686096\n0.14544392501611542,0.4858440457442145\n0.14430604706411762,0.48633455008026744\n0.14413536537131388,0.486579802248323\n0.1433388508049134,0.48633455008026744\n0.14214407895531267,0.4864163008029139\n0.1385597634065155,0.4872338080297269\n0.13793393053291367,0.4871520573070223\n0.13628400750250974,0.4864980515256184\n0.13213075297770863,0.4841272805679421\n0.12405181951850794,0.4778324749216681\n0.11944341381290363,0.47382668951042384\n0.11711076401130507,0.4712106663847501\n0.11540394708330329,0.46843114181366735\n0.11437985692650121,0.46589686941064\n0.11341266066730205,0.46156408110863584\n0.11250235830570077,0.4568225391932832\n0.11170584373930029,0.4542882667902559\n0.11090932917289978,0.45257150161398346\n0.10954387563050036,0.4503642321016581\n0.10652849905770212,0.446685449581174\n0.10436653094889717,0.44259791344722527\n0.10294418350889989,0.44055414538025095\n0.10066842760489919,0.4385103773132766\n0.09429631107369525,0.43417758901133047\n0.0931015392240945,0.43295132817116905\n0.09179297957929297,0.4300083021547353\n0.0910533589104954,0.4287820413145158\n0.08837934572329446,0.42493975734862255\n0.08798108844009422,0.4242040008445142\n0.08763972505449184,0.42224198350024433\n0.0865587410000919,0.42019821543326996\n0.08581912033128927,0.4180726966436492\n0.08559154474089273,0.4177456937529472\n0.08388472781289097,0.4162741807447303\n0.08104003293289136,0.4133311547282965\n0.07927632210728666,0.4112873866613222\n0.07677299061288943,0.40867136353559036\n0.07609026384168469,0.40801735775418646\n0.073302462859288,0.4061370911325631\n0.06442701483368177,0.3985342739234812\n0.06146853215848138,0.39534599573899193\n0.05714459594088159,0.38929644226077337\n0.05572224850087926,0.3868439205804506\n0.05287755362087966,0.3813666221609895\n0.049691495355277684,0.37580757301882395\n0.04741573945127699,0.3709025296581203\n0.04622096760167625,0.36877701086844145\n0.04468483236647314,0.3667332428014671\n0.040133320558471755,0.3621552023314654\n0.0383696097328721,0.3601931849871956\n0.03751620126887374,0.3590486748696806\n0.03637832331687087,0.35700490680276437\n0.03535423316006879,0.3545523851223836\n0.03484218808166775,0.3527538692234647\n0.034557718593668296,0.35095535332454586\n0.034443930798467505,0.349156837425627\n0.034614612491271214,0.34629556213183965\n0.035183551467270126,0.3435977882834614\n0.036890368395271905,0.33771173625059386\n0.037231731780869234,0.33607672179702597\n0.03762998906406948,0.33329719722594314\n0.0376868829616724,0.3317439334950798\n0.03745930737127082,0.32904615964664347\n0.036890368395271905,0.3264301365209697\n0.035923172136072754,0.3239776148405889\n0.034330143003271765,0.3210345888241551\n0.028640753243267512,0.31261426438826023\n0.026706360724869198,0.30991649053988196\n0.025397801080067665,0.30819972536360957\n0.022723787892866727,0.30542020079258486\n0.018798108958462127,0.3019049197173936\n0.017375761518464858,0.3010056617679342\n0.011458796168059018,0.2977356328607404\n0.011117432782461696,0.297326879247392\n0.01094675108965798,0.29675462418863446\n0.01094675108965798,0.2955283633484731\n0.011174326680059563,0.2951196097350666\n0.011857053451259265,0.2946291053989556\n0.012198416836861643,0.2944656039536046\n0.01265356801765976,0.2946291053989556\n0.013620764276858915,0.2952831111804176\n0.015498262897664414,0.29691812563398545\n0.016408565259260644,0.29740862997003836\n0.0177740188016651,0.2978173835834449\n0.01868432116326134,0.29838963864220236\n0.02340651466406643,0.302313673330742\n0.024203029230466924,0.30321293128025956\n0.025625376670464196,0.3059924558512842\n0.02625120954406603,0.30623770801933975\n0.02727529970086811,0.30607420657398876\n0.027730450881666226,0.30542020079258486\n0.027787344779269148,0.30476619501112284\n0.027673556984068357,0.30435744139771637\n0.0273890874960689,0.3040304385070144\n0.024203029230466924,0.3019049197173936\n0.01936704793446609,0.2978173835834449\n0.018456745572864804,0.29724512852468743\n0.01657924695206436,0.29642762129793254\n0.016010307976060397,0.2958553662391751\n0.01572583848806094,0.29520136045771306\n0.015384475102463619,0.2936480967268497\n0.015498262897664414,0.29242183588668835\n0.015782732385663866,0.2916860793825799\n0.01663614084966223,0.2904598185423604\n0.01726197372326406,0.289887563483603\n0.01822916998246322,0.28956056059290103\n0.01931015403686317,0.28947880987025454\n0.026820148520064938,0.2906233199877114\n0.02744598139366677,0.29054156926506497\n0.028185602062469395,0.290378067819714\n0.028754541038468306,0.29005106492895394\n0.02920969221926642,0.28956056059290103\n0.029664843400069592,0.2888248040887926\n0.029949312888069044,0.2875985432486312\n0.029778631195270383,0.28130373760235716\n0.028868328833669098,0.2781154594179259\n0.02744598139366677,0.27435492617467916\n0.02522711938726395,0.26642510607489533\n0.02465818041126504,0.2630733264450551\n0.024259923128064794,0.2616018134368382\n0.022837575688067522,0.2583317845297024\n0.02221174281446569,0.25628801646272803\n0.021244546555266534,0.25236398177413033\n0.021016970964864948,0.24982970937110305\n0.021073864862462818,0.24794944274953776\n0.021642803838466777,0.24353490372488706\n0.01891189675366292,0.2156579072914707\n0.017944700494463763,0.21173387260287296\n0.01822916998246322,0.2116521218802265\n0.018627427265663465,0.21107986682146906\n0.018627427265663465,0.21075286393076706\n0.018513639470462674,0.2104258610400651\n0.018001594392061636,0.21009885814930507\n0.0177740188016651,0.21009885814930507\n0.0177740188016651,0.20887259730914373\n0.017944700494463763,0.20715583213287128\n0.014644854433660995,0.1739650387254029\n0.014019021560059162,0.17126726487702462\n0.012369098529660304,0.16922349681005028\n0.010889857192060107,0.16799723596983082\n0.003948801684857246,0.1641549520039376\n0.0014454701904549755,0.16227468538237233\n-0.0015130124847454185,0.1609666738195064\n-0.004585282955146604,0.16006741587004697\n-0.03957502997916037,0.1573696420216687\n-0.04355760281116284,0.15761489418966612\n-0.04606093430556512,0.15867765358447655\n-0.057041456542368314,0.16366444766788468\n-0.05823622839196906,0.16399145055858663\n-0.060113727012769506,0.16390969983594017\n-0.06182054394077128,0.16341919549982914\n-0.06716857031517318,0.1607214216514509\n-0.06819266047197017,0.16006741587004697\n-0.0709804614543719,0.15875940430718105\n-0.07337000515357339,0.15851415213912556\n-0.07644227562397457,0.15875940430718105\n-0.07951454609437575,0.1581871492484236\n-0.0821885592815767,0.1573696420216687\n-0.08412295179998008,0.15704263913090866\n-0.08742279786078283,0.1571243898536132\n-0.08856067581278065,0.15679738696291123\n-0.08958476596958273,0.1563068826268002\n-0.09077953781918346,0.15532587395469435\n-0.09191741577118129,0.15450836672788137\n-0.09305529372318416,0.1539361116691239\n-0.09533104962718486,0.15336385661036644\n-0.09595688250078163,0.15303685371966447\n-0.09692407875998585,0.15221934649290958\n-0.0986877895855855,0.1500938277032307\n-0.09993945533278409,0.1479683089136099\n-0.10102043938718408,0.1464150451826885\n-0.10312551359838609,0.1446165292837696\n-0.10392202816478657,0.1428997641075553\n-0.10426339155038897,0.14036549170446994\n-0.10471854273118708,0.13897572941895758\n-0.1054581633999897,0.13807647146949814\n-0.10631157186398807,0.13742246568809424\n-0.1088717972559882,0.13619620484787476\n-0.10961141792479082,0.1352151961757689\n-0.11063550808158787,0.13276267449538803\n-0.11126134095518969,0.13210866871398413\n-0.11188717382879151,0.1317816658232241\n-0.11461808091359033,0.1316181643778731\n-0.11547148937759374,0.13120941076446663\n-0.11934027441439543,0.12916564269755038\n-0.12019368287839377,0.12802113258003545\n-0.12116087913759294,0.1256503616223591\n-0.12150224252319533,0.12254383416057436\n-0.1222418631919929,0.12180807765646591\n-0.12320905945119712,0.12148107476576395\n-0.12747610177119903,0.12148107476576395\n-0.12884155531359842,0.1211540718750039\n-0.1341895816880003,0.11927380525343861\n-0.13572571692319838,0.11829279658127466\n-0.13709117046560282,0.11714828646375972\n-0.13828594231520355,0.11502276767413892\n-0.14135821278560476,0.10840095913716291\n-0.14477184664160325,0.1043134230032142\n-0.14727517813600552,0.1007163912053765\n-0.14852684388320414,0.09957188108786157\n-0.14926646455200676,0.0992448781971596\n-0.151257750968008,0.0989996260291041\n-0.1520542655344085,0.09916312747451317\n-0.15683335293281145,0.10104339409607846\n-0.1579143369872114,0.10177915060018691\n-0.16115728915041125,0.1049674287846762\n-0.16257963659041358,0.1062754403475421\n-0.16639152772961233,0.10782870407840545\n-0.16838281414561357,0.10840095913716291\n-0.17043099445921267,0.10930021708662234\n-0.17373084052001547,0.11036297648143273\n-0.17794098894241953,0.11207974165770514\n-0.18175288008161827,0.1124067445484071\n-0.18357348480482086,0.11175273876700319\n-0.1860768162992231,0.10897321419592038\n-0.18738537594401958,0.107746953355759\n-0.18840946610082165,0.10750170118770348\n-0.18988870743842187,0.10758345191040802\n-0.19091279759522395,0.10733819974235251\n-0.19261961452322574,0.10611193890219113\n-0.19347302298722407,0.10570318528878464\n-0.19432643145122247,0.10562143456608011\n-0.19569188499362689,0.10594843745684016\n-0.1966021873552231,0.1066841939608905\n-0.20121059306082748,0.11085348081754376\n-0.20439665132642942,0.11355125466592203\n-0.20684308892322875,0.11543152128754541\n-0.20832233026082897,0.11641252995965128\n-0.21116702514083363,0.11739353863181523\n-0.21184975191203334,0.1179657936905727\n-0.21236179699043436,0.11870155019468115\n-0.21264626647843382,0.1200913124801935\n-0.21213422140003277,0.1243423500594932\n-0.21219111529763066,0.12646786884911398\n-0.21241869088803225,0.1281846340253864\n-0.21440997730403347,0.13210866871398413\n-0.21497891628003238,0.1328444252180926\n-0.2154909613584334,0.133253178831441\n-0.21605990033443231,0.1334984309994965\n-0.21714088438883225,0.1334984309994965\n-0.21953042808803377,0.13317142810879457\n-0.22066830604003665,0.13366193244484748\n-0.2212941389136334,0.13423418750360494\n-0.22169239619683365,0.13488819328500884\n-0.22260269855843492,0.13856697580555108\n-0.22305784973923812,0.1394662337550105\n-0.2237974704080357,0.14003848881376796\n-0.2248215605648378,0.14044724242717446\n-0.22544739343843959,0.140528993149879\n-0.226812846980839,0.1401202395364725\n-0.22846277001123785,0.13856697580555108\n-0.23107988930084092,0.13652320773857674\n-0.23335564520484162,0.1345611903943069\n-0.23420905366883996,0.133907184612903\n-0.23517624992803912,0.1334984309994965\n-0.23591587059684174,0.13333492955414553\n-0.2398415495312413,0.13415243678090039\n-0.24115010917604282,0.13415243678090039\n-0.24627055996004818,0.13341668027679196\n-0.24854631586404888,0.13259917305003707\n-0.24939972432804722,0.13251742232733255\n-0.2519599497200473,0.13308967738609\n-0.254804644600052,0.1334984309994965\n-0.2556580530640504,0.13374368316755203\n-0.2586734296368537,0.13635970629322575\n-0.2596406258960528,0.13701371207468777\n-0.2604940343600512,0.13742246568809424\n-0.26225774518565087,0.13742246568809424\n-0.26584206073445305,0.13717721352003873\n-0.2668092569936522,0.1373407149653897\n-0.26771955935525354,0.1377494685787962\n-0.26828849833125745,0.1381582221921446\n-0.26891433120485425,0.13979323664577056\n-0.2699953152592542,0.14142825109933843\n-0.2723279650608578,0.1431450162755527\n-0.27420546368165827,0.14420777567036314\n-0.2747175087600593,0.14469828000647417\n-0.2738641002960559,0.14469828000647417\n-0.27289690403685674,0.14404427422501215\n-0.2713038749040557,0.14339026844360825\n-0.268004028843253,0.14224575832609332\n-0.26453350108965157,0.14069249459522998\n-0.26322494144485503,0.14036549170446994\n-0.2620301695952543,0.14044724242717446\n-0.26055092825765414,0.1407742453178764\n-0.25702350660644974,0.14208225688074233\n-0.2561132042448485,0.14232750904879785\n-0.25491843239524775,0.1420005061580959\n-0.2529271459792465,0.1411012482085784\n-0.2524151009008505,0.14101949748593193\n-0.25201684361765025,0.1411829989312829\n-0.2517323741296508,0.14175525399004038\n-0.2524151009008505,0.1441260249477167\n-0.253211615467251,0.14616979301469105\n-0.2543494934192488,0.14854056397236737\n-0.25554426526884955,0.15001207698058425\n-0.2564545676304509,0.15058433203934174\n-0.25759244558244865,0.15082958420733916\n-0.25946994420324915,0.15082958420733916\n-0.26453350108965157,0.1503390798712862\n-0.2653300156560521,0.1503390798712862\n-0.26612653022245253,0.15082958420733916\n-0.26629721191525624,0.15156534071144762\n-0.2662403180176533,0.15230109721555604\n-0.26584206073445305,0.15287335227431348\n-0.2644197132944558,0.1536908595011265\n-0.26003888317925306,0.15589812901345182\n-0.2595837319984499,0.15663388551756025\n-0.25946994420324915,0.15745139274431513\n-0.2598113075888515,0.1581871492484236\n-0.26066471605284985,0.15908640719788303\n-0.264305925499255,0.16104842454215285\n-0.2662403180176533,0.16153892887826388\n-0.26715062037925463,0.16145717815555932\n-0.26823160443365457,0.1612119259875619\n-0.26971084577125476,0.1612119259875619\n-0.2703366786448566,0.1613754274329129\n-0.27073493592805686,0.1618659317689658\n-0.2707918298256547,0.16235643610501876\n-0.27073493592805686,0.16309219260912722\n-0.27039357254245444,0.16341919549982914\n-0.26971084577125476,0.16374619839058918\n-0.266979938686456,0.16423670272664215\n-0.26453350108965157,0.1654629635668035\n-0.26385077431845183,0.16570821573485903\n-0.26299736585445344,0.16570821573485903\n-0.26038024656485037,0.1652177113988061\n-0.25918547471524966,0.1652177113988061\n-0.25713729440165056,0.1650542099534551\n-0.2562838859376522,0.1650542099534551\n-0.2538374483408478,0.16578996645756355\n-0.2519599497200473,0.16644397223896745\n-0.253211615467251,0.16767023307912884\n-0.2545770690096504,0.16824248813788634\n-0.25486153849764986,0.1685694910285883\n-0.2564545676304509,0.17216652282648406\n-0.25719418829924845,0.17322928222129444\n-0.2652162278608563,0.185246638455027\n-0.2703366786448566,0.19235895132805594\n-0.2705642542352582,0.19358521216827543\n-0.2704504664400574,0.19432096867238385\n-0.2714176626992565,0.19464797156308583\n-0.27238485895845566,0.19546547878984072\n-0.2990680969328672,0.23437882278488378\n-0.3051557439760717,0.24296264866612963\n-0.30595225854247216,0.24386190661558904\n-0.3061229402352708,0.24353490372488706\n-0.3063505158256724,0.24337140227953613\n-0.3066918792112697,0.24345315300218254\n-0.30709013649447,0.24369840517023808\n-0.30726081818727374,0.244188909506291\n-0.30726081818727374,0.244842915287753\n-0.30703324259687215,0.24516991817845496\n-0.30674877310887266,0.24525166890110142\n-0.3168189929840746,0.2674061147470012\n-0.33513882801128086,0.30689171380074365\n-0.33536640360168246,0.30836322680896056\n-0.3357077669872849,0.3090172325904226\n-0.33616291816808297,0.30926248475847806\n-0.3367887510416848,0.30983473981717746\n-0.33690253883688565,0.3106522470439904\n-0.3366180693488811,0.3114697542707453\n-0.33701632663208136,0.31261426438826023\n-0.3374145839152816,0.31343177161507324\n-0.33934897643368495,0.316374797631507\n-0.34105579336168673,0.3195630758159382\n-0.3439573821392842,0.32561262929415674\n-0.345664199067286,0.32945491326004994\n-0.34737101599528775,0.33288844361253667\n-0.35823775043689016,0.35692315608005987\n-0.3650650181488973,0.3723740426663372\n-0.3654063815344946,0.37482656434666\n-0.3651219120464952,0.37736083674968723\n-0.36478054866089277,0.37776959036309377\n-0.3646098669680941,0.3787505990352577\n-0.3647236547632949,0.381121369992934\n-0.36495123035369653,0.3825928830011509\n-0.3681941825168964,0.38300163661455744\n-0.36876312149289525,0.38300163661455744\n-0.37331463330089665,0.38063086565688103\n-0.37564728310250023,0.3799768598754191\n-0.3822469752241007,0.3776060889177427\n-0.3827590203025018,0.3771973353043363\n-0.38292970199530046,0.37646157880022785\n-0.38332795927850066,0.37613457590952587\n-0.3846934128209052,0.37621632663223037\n-0.3851485640017033,0.37646157880022785\n-0.3854330334897027,0.3767885816909879\n-0.3849778823089046,0.37801484253114925\n-0.38520545789930116,0.37948635553936616\n-0.38571750297770213,0.3800586105981236\n-0.38787947108650206,0.3808761178248785\n-0.3891311368337057,0.38185712649704245\n-0.39009833309290487,0.38300163661455744\n-0.39248787679210634,0.38520890612688274\n-0.39328439135850685,0.3861081640763422\n-0.39334128525610473,0.3864351669670441\n-0.3931706035633061,0.387252674193799\n-0.39248787679210634,0.38749792636185454\n-0.391634468328108,0.387252674193799\n-0.38901734903850493,0.3852906568495292\n-0.38833462226730525,0.38512715540417825\n-0.38787947108650206,0.38512715540417825\n-0.38748121380330686,0.38537240757223373\n-0.38708295652010666,0.38586291190828664\n-0.38713985041770443,0.3865986684123951\n-0.387424319905704,0.3871709234711525\n-0.3882208344721045,0.38823368286596294\n-0.3894725002193031,0.3891329408154224\n-0.39675491911210836,0.3925664711679091\n-0.39721007029290645,0.39297522478131564\n-0.39760832757610665,0.3939562334534796\n-0.3976652214737096,0.39510074357093644\n-0.39726696419050933,0.3961635029658049\n-0.3966980252145105,0.39689925946991333\n-0.3957877228529091,0.3984525232007767\n-0.3885621978577068,0.4036028187295358\n-0.3837831104593038,0.40523783318310364\n-0.38179182404330264,0.4062188418552676\n-0.3814504606577002,0.40679109691402504\n-0.381564248452901,0.40760860414078\n-0.38264523250730104,0.4093253693170524\n-0.3837831104593038,0.41014287654380727\n-0.3850916701041054,0.41071513160256473\n-0.3864571236465048,0.41046987943450924\n-0.38787947108650206,0.4098976243758098\n-0.3891311368337057,0.40957062148504975\n-0.389529394116906,0.4096523722077543\n-0.38987075750250333,0.4099793750984563\n-0.3899845452977041,0.41038812871186275\n-0.3899845452977041,0.4107968823252693\n-0.3896431819121068,0.4112056359386177\n-0.3876518954961055,0.41202314316543065\n-0.38662780533930347,0.41259539822418806\n-0.38355553486890226,0.4166011836354323\n-0.3779799329040988,0.4201164647106236\n-0.37684205495210105,0.4210974733827294\n-0.37604554038570054,0.42158797771884043\n-0.37496455633130055,0.42183322988683786\n-0.3718922858608994,0.4213427255507849\n-0.37064062011369575,0.42150622699613594\n-0.3681941825168964,0.42224198350024433\n-0.36546327543209756,0.4226507371136508\n-0.36495123035369653,0.4229777400043528\n-0.3647236547632949,0.42346824434040575\n-0.36478054866089277,0.42395874867651673"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/MainActivity.kt",
    "content": "package ovh.plrapps.mapcompose.demo\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.tooling.preview.Preview\nimport ovh.plrapps.mapcompose.demo.ui.MapComposeDemoApp\nimport ovh.plrapps.mapcompose.demo.ui.theme.MapComposeTheme\n\nclass MainActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n        setContent {\n            MapComposeTheme {\n                MapComposeDemoApp()\n            }\n        }\n    }\n}\n\n@Preview(showBackground = true)\n@Composable\nfun DefaultPreview() {\n    MapComposeTheme {\n        MapComposeDemoApp()\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/providers/TileStreamProviderFactory.kt",
    "content": "package ovh.plrapps.mapcompose.demo.providers\n\nimport android.content.Context\nimport ovh.plrapps.mapcompose.core.TileStreamProvider\n\nfun makeTileStreamProvider(appContext: Context) =\n    TileStreamProvider { row, col, zoomLvl ->\n        try {\n            appContext.assets?.open(\"tiles/mont_blanc_layered/$zoomLvl/$row/$col.jpg\")\n        } catch (e: Exception) {\n            null\n        }\n    }\n"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/MapComposeDemoApp.kt",
    "content": "package ovh.plrapps.mapcompose.demo.ui\n\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.rememberNavController\nimport ovh.plrapps.mapcompose.demo.ui.screens.AddingMarkerDemo\nimport ovh.plrapps.mapcompose.demo.ui.screens.AnimationDemo\nimport ovh.plrapps.mapcompose.demo.ui.screens.CalloutDemo\nimport ovh.plrapps.mapcompose.demo.ui.screens.CenteringOnMarkerDemo\nimport ovh.plrapps.mapcompose.demo.ui.screens.CustomDrawDemo\nimport ovh.plrapps.mapcompose.demo.ui.screens.Home\nimport ovh.plrapps.mapcompose.demo.ui.screens.HttpTilesDemo\nimport ovh.plrapps.mapcompose.demo.ui.screens.InfiniteScrollDemo\nimport ovh.plrapps.mapcompose.demo.ui.screens.LayersDemoSimple\nimport ovh.plrapps.mapcompose.demo.ui.screens.MapDemoSimple\nimport ovh.plrapps.mapcompose.demo.ui.screens.MarkersClusteringDemo\nimport ovh.plrapps.mapcompose.demo.ui.screens.MarkersLazyLoadingDemo\nimport ovh.plrapps.mapcompose.demo.ui.screens.OsmDemo\nimport ovh.plrapps.mapcompose.demo.ui.screens.PathsDemo\nimport ovh.plrapps.mapcompose.demo.ui.screens.RotationDemo\nimport ovh.plrapps.mapcompose.demo.ui.screens.VisibleAreaPaddingDemo\nimport ovh.plrapps.mapcompose.demo.ui.theme.MapComposeTheme\n\n@Composable\nfun MapComposeDemoApp() {\n    val navController = rememberNavController()\n\n    MapComposeTheme {\n        NavHost(navController, startDestination = HOME) {\n            composable(HOME) {\n                Home(demoListState = rememberLazyListState()) {\n                    navController.navigate(it.name)\n                }\n            }\n            composable(MainDestinations.MAP_ALONE.name) {\n                MapDemoSimple()\n            }\n            composable(MainDestinations.LAYERS_DEMO.name) {\n                LayersDemoSimple()\n            }\n            composable(MainDestinations.MAP_WITH_ROTATION_CONTROLS.name) {\n                RotationDemo()\n            }\n            composable(MainDestinations.ADDING_MARKERS.name) {\n                AddingMarkerDemo()\n            }\n            composable(MainDestinations.CENTERING_ON_MARKER.name) {\n                CenteringOnMarkerDemo()\n            }\n            composable(MainDestinations.PATHS.name) {\n                PathsDemo()\n            }\n            composable(MainDestinations.CUSTOM_DRAW.name) {\n                CustomDrawDemo()\n            }\n            composable(MainDestinations.CALLOUT_DEMO.name) {\n                CalloutDemo()\n            }\n            composable(MainDestinations.ANIMATION_DEMO.name) {\n                AnimationDemo()\n            }\n            composable(MainDestinations.INFINITE_SCROLL.name) {\n                InfiniteScrollDemo()\n            }\n            composable(MainDestinations.OSM_DEMO.name) {\n                OsmDemo()\n            }\n            composable(MainDestinations.HTTP_TILES_DEMO.name) {\n                HttpTilesDemo()\n            }\n            composable(MainDestinations.VISIBLE_AREA_PADDING.name) {\n                VisibleAreaPaddingDemo()\n            }\n            composable(MainDestinations.MARKERS_CLUSTERING.name) {\n                MarkersClusteringDemo()\n            }\n            composable(MainDestinations.MARKERS_LAZY_LOADING.name) {\n                MarkersLazyLoadingDemo()\n            }\n        }\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/NavGraph.kt",
    "content": "package ovh.plrapps.mapcompose.demo.ui\n\nconst val HOME = \"home\"\n\nenum class MainDestinations(val title: String) {\n    MAP_ALONE(\"Simple map\"),\n    LAYERS_DEMO(\"Layers demo\"),\n    MAP_WITH_ROTATION_CONTROLS(\"Map with rotation controls\"),\n    ADDING_MARKERS(\"Adding markers\"),\n    CENTERING_ON_MARKER(\"Centering on marker\"),\n    PATHS(\"Map with paths\"),\n    CUSTOM_DRAW(\"Map with custom drawings\"),\n    CALLOUT_DEMO(\"Callout (tap markers)\"),\n    ANIMATION_DEMO(\"Animation demo\"),\n    INFINITE_SCROLL(\"Infinite scroll demo\"),\n    OSM_DEMO(\"Open Street Map demo\"),\n    HTTP_TILES_DEMO(\"Remote HTTP tiles\"),\n    VISIBLE_AREA_PADDING(\"Visible area padding\"),\n    MARKERS_CLUSTERING(\"Markers clustering\"),\n    MARKERS_LAZY_LOADING(\"Markers lazy loading\")\n}\n"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/AddingMarkerDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.AddingMarkerVM\nimport ovh.plrapps.mapcompose.ui.MapUI\n\n@Composable\nfun AddingMarkerDemo(\n    modifier: Modifier = Modifier,\n    viewModel: AddingMarkerVM = viewModel(),\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.ADDING_MARKERS.title) },\n            )\n        }\n    ) { padding ->\n        Column(\n            modifier\n                .padding(padding)\n                .fillMaxSize()) {\n            MapUI(\n                modifier.weight(2f),\n                state = viewModel.state\n            )\n            Row(verticalAlignment = Alignment.CenterVertically) {\n                Button(onClick = {\n                    viewModel.addMarker()\n                }, Modifier.padding(8.dp)) {\n                    Text(text = \"Add marker\")\n                }\n\n                Text(\"Drag markers with finger\")\n            }\n        }\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/AnimationDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.AnimationDemoVM\nimport ovh.plrapps.mapcompose.ui.MapUI\n\n\n@Composable\nfun AnimationDemo(\n    viewModel: AnimationDemoVM = viewModel(),\n    onRestart: () -> Unit = viewModel::startAnimation\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.ANIMATION_DEMO.title) },\n            )\n        }\n    ) { padding ->\n        Column(\n            Modifier\n                .padding(padding)\n                .fillMaxSize()) {\n            MapUI(\n                Modifier.weight(1f),\n                state = viewModel.state\n            )\n            Button(onClick = {\n                onRestart()\n            }, Modifier.padding(8.dp)) {\n                Text(text = \"Start\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/CalloutDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.CalloutVM\nimport ovh.plrapps.mapcompose.ui.MapUI\n\n@Composable\nfun CalloutDemo(\n    viewModel: CalloutVM = viewModel()\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.CALLOUT_DEMO.title) },\n            )\n        }\n    ) { padding ->\n        Column(\n            Modifier\n                .padding(padding)\n                .fillMaxSize()) {\n            MapUI(state = viewModel.state)\n        }\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/CenteringOnMarkerDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.CenteringOnMarkerVM\nimport ovh.plrapps.mapcompose.ui.MapUI\n\n@Composable\nfun CenteringOnMarkerDemo(\n    modifier: Modifier = Modifier,\n    viewModel: CenteringOnMarkerVM = viewModel(),\n    onCenter: () -> Unit = viewModel::onCenter\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.CENTERING_ON_MARKER.title) },\n            )\n        }\n    ) { padding ->\n        Column(\n            modifier\n                .padding(padding)\n                .fillMaxSize()) {\n            MapUI(\n                modifier.weight(1f),\n                state = viewModel.state\n            )\n            Button(onClick = onCenter, Modifier.padding(8.dp)) {\n                Text(text = \"Center on marker\")\n            }\n        }\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/CustomDraw.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.api.DefaultCanvas\nimport ovh.plrapps.mapcompose.api.fullSize\nimport ovh.plrapps.mapcompose.api.scale\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.utils.pxToDp\nimport ovh.plrapps.mapcompose.demo.viewmodels.CustomDrawVM\nimport ovh.plrapps.mapcompose.ui.MapUI\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport kotlin.math.log10\nimport kotlin.math.pow\n\n/**\n * This demo shows how to embed custom drawings inside [MapUI].\n */\n@Composable\nfun CustomDrawDemo(viewModel: CustomDrawVM = viewModel()) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.CUSTOM_DRAW.title) },\n            )\n        }\n    ) { padding ->\n        CustomDraw(Modifier.padding(padding), viewModel)\n    }\n}\n\n/**\n * This demo shows how to embed custom drawings inside [MapUI].\n */\n@Composable\nprivate fun CustomDraw(\n    modifier: Modifier = Modifier, viewModel: CustomDrawVM\n) {\n    Box(modifier) {\n        MapUI(state = viewModel.state) {\n            Square(\n                modifier = Modifier,\n                mapState = viewModel.state,\n                position = Offset(\n                    viewModel.state.fullSize.width / 2f - 300f,\n                    viewModel.state.fullSize.height / 2f - 300f\n                ),\n                color = Color(0xff5c6bc0),\n                isScaling = true\n            )\n            Square(\n                modifier = Modifier,\n                mapState = viewModel.state,\n                position = Offset(\n                    viewModel.state.fullSize.width / 2f,\n                    viewModel.state.fullSize.height / 2f\n                ),\n                color = Color(0xff087f23),\n                isScaling = false\n            )\n            Line(\n                modifier = Modifier,\n                mapState = viewModel.state,\n                color = Color(0xAAF44336),\n                p1 = with(viewModel) {\n                    Offset(\n                        (p1x * state.fullSize.width).toFloat(),\n                        (p1y * state.fullSize.height).toFloat()\n                    )\n                },\n                p2 = with(viewModel) {\n                    Offset(\n                        (p2x * state.fullSize.width).toFloat(),\n                        (p2y * state.fullSize.height).toFloat()\n                    )\n                }\n            )\n        }\n        ScaleIndicator(\n            controller = viewModel.scaleIndicatorController,\n            lineColor = Color.Black\n        )\n    }\n}\n\n@Composable\nfun ScaleIndicator(\n    controller: ScaleIndicatorController,\n    lineColor: Color\n) {\n    Box(Modifier.height(50.dp)) {\n        Canvas(\n            modifier = Modifier\n                .alpha(0.8f)\n                .padding(5.dp)\n                .size(pxToDp(controller.widthPx).dp, 15.dp)\n        ) {\n            val width = controller.widthPx * controller.widthRatio\n            val height = size.height\n            drawLine(lineColor, Offset(0f, height / 2), Offset(width, height / 2), 2.dp.toPx())\n            drawLine(\n                lineColor,\n                Offset(0f, 0f),\n                Offset(0f, height),\n                2.dp.toPx(),\n                cap = StrokeCap.Round\n            )\n            drawLine(\n                lineColor,\n                Offset(width, 0f),\n                Offset(width, height),\n                2.dp.toPx(),\n                cap = StrokeCap.Round\n            )\n        }\n        Text(\n            text = controller.scaleText,\n            color = Color.White,\n            modifier = Modifier\n                .padding(start = 16.dp, top = 20.dp)\n                .background(color = Color(0x885D4037), shape = RoundedCornerShape(4.dp))\n                .padding(start = 5.dp, end = 5.dp)\n        )\n    }\n}\n\nclass ScaleIndicatorController(val widthPx: Int, initScale: Double) {\n    var widthRatio by mutableFloatStateOf(0f)\n    var scaleText by mutableStateOf(\"\")\n\n    private var snapScale: Double = initScale\n    private var snapWidthRatio = 0f\n\n    init {\n        snapToNewValue(initScale)\n    }\n\n    fun onScaleChanged(scale: Double) {\n        val ratio = (scale / snapScale).toFloat()\n        if (widthRatio * ratio in 0.5f..1f) {\n            widthRatio = snapWidthRatio * ratio\n        } else {\n            snapToNewValue(scale)\n        }\n    }\n\n    private fun snapToNewValue(scale: Double) {\n        val distance = distanceForPx(widthPx, scale)\n        val snap = computeSnapValue(distance) ?: return\n        snapScale = scale\n        widthRatio = snap.toFloat() / distance\n        snapWidthRatio = widthRatio\n        scaleText = formatDistance(snap)\n    }\n\n    /**\n     * Computes the distance in meters, given a size in pixels.\n     */\n    private fun distanceForPx(nPx: Int, scale: Double): Int {\n        // TODO: This a simplified calculation\n        return (widthPx * 5 / scale).toInt()\n    }\n\n    private fun formatDistance(d: Int): String {\n        return \"$d m\"\n    }\n\n    /**\n     * A snap value is an entire multiple of power of 10, which is lower than [input].\n     * The first digit of a snap value is either 1, 2, 3, or 5.\n     * For example: 835 -> 500, 480 -> 300, 270 -> 200, 114 -> 100\n     * The snap value is always greater than half of [input].\n     */\n    private fun computeSnapValue(input: Int): Int? {\n        if (input <= 1) return null\n\n        // Lowest entire power of 10\n        val power = (log10(input.toDouble())).toInt()\n\n        val power10 = 10.0.pow(power)\n        val mostSignificantDigit = (input / power10).toInt()\n\n        return when {\n            mostSignificantDigit >= 5 -> 5 * power10\n            mostSignificantDigit >= 3 -> 3 * power10\n            mostSignificantDigit >= 2 -> 2 * power10\n            else -> power10\n        }.toInt()\n    }\n}\n\n\n/**\n * Here, we define a square with various inputs such as [position], [color], and [isScaling].\n * Our custom composable is based on [DefaultCanvas], which is provided by the MapCompose library.\n * Since [DefaultCanvas] moves, scales, and rotates with the map, so does our custom square composable.\n */\n@Composable\nfun Square(\n    modifier: Modifier,\n    mapState: MapState,\n    position: Offset,\n    color: Color,\n    isScaling: Boolean\n) {\n    DefaultCanvas(\n        modifier = modifier,\n        mapState = mapState\n    ) {\n        val side = if (isScaling) 300f else (300f / mapState.scale).toFloat()\n        drawRect(\n            color,\n            topLeft = position,\n            size = Size(side, side)\n        )\n    }\n}\n\n@Composable\nfun Line(\n    modifier: Modifier,\n    mapState: MapState,\n    color: Color,\n    p1: Offset,\n    p2: Offset\n) {\n    DefaultCanvas(\n        modifier = modifier,\n        mapState = mapState\n    ) {\n        drawLine(color, start = p1, end = p2, strokeWidth = (8.0 / mapState.scale).toFloat())\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/Home.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport ovh.plrapps.mapcompose.demo.R\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\n\n@Composable\nfun Home(demoListState: LazyListState, onDemoSelected: (dest: MainDestinations) -> Unit) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(stringResource(R.string.app_name)) },\n            )\n        }\n    ) { padding ->\n        LazyColumn(\n            Modifier.padding(padding).fillMaxWidth(),\n            state = demoListState,\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            MainDestinations.entries.map { dest ->\n                item {\n                    Button(\n                        onClick = { onDemoSelected.invoke(dest) }\n                    ) {\n                        Text(text = dest.title)\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/HttpTilesDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.HttpTilesVM\nimport ovh.plrapps.mapcompose.ui.MapUI\n\n@Composable\nfun HttpTilesDemo(\n    modifier: Modifier = Modifier, viewModel: HttpTilesVM = viewModel()\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.HTTP_TILES_DEMO.title) },\n            )\n        }\n    ) { padding ->\n        MapUI(\n            modifier.padding(padding),\n            state = viewModel.state\n        )\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/InfiniteScrollDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.InfiniteScrollVM\nimport ovh.plrapps.mapcompose.ui.MapUI\n\n\n@Composable\nfun InfiniteScrollDemo(\n    modifier: Modifier = Modifier, viewModel: InfiniteScrollVM = viewModel()\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.INFINITE_SCROLL.title) },\n            )\n        }\n    ) { padding ->\n        MapUI(modifier.padding(padding), state = viewModel.state)\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/LayersDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.LayersVM\nimport ovh.plrapps.mapcompose.ui.MapUI\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\n@Composable\nfun LayersDemoSimple(viewModel: LayersVM = viewModel()) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.LAYERS_DEMO.title) },\n            )\n        }\n    ) { padding ->\n        LayersDemoScreen(\n            Modifier.padding(padding),\n            viewModel.state,\n            onSlopesOpacity = viewModel::setSlopesOpacity,\n            onRoadOpacity = viewModel::setRoadOpacity\n        )\n    }\n}\n\n@Composable\nfun LayersDemoScreen(\n    modifier: Modifier = Modifier,\n    mapState: MapState,\n    onSlopesOpacity: (Float) -> Unit,\n    onRoadOpacity: (Float) -> Unit\n) {\n    var slopesSliderValue by remember {\n        mutableFloatStateOf(0.6f)\n    }\n\n    var roadSliderValue by remember {\n        mutableFloatStateOf(1f)\n    }\n\n    Column(modifier) {\n        MapUI(Modifier.weight(1f), state = mapState)\n        LayerSlider(\n            name = \"Slopes\",\n            value = slopesSliderValue,\n            onValueChange = {\n                slopesSliderValue = it\n                onSlopesOpacity(it)\n            }\n        )\n        LayerSlider(\n            name = \"Roads\",\n            value = roadSliderValue,\n            onValueChange = {\n                roadSliderValue = it\n                onRoadOpacity(it)\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun LayerSlider(name: String, value: Float, onValueChange: (Float) -> Unit) {\n    Row(\n        Modifier\n            .height(50.dp)\n            .padding(horizontal = 16.dp)) {\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            Text(text = name, Modifier.padding(horizontal = 16.dp))\n            Slider(\n                value = value,\n                onValueChange = onValueChange\n            )\n        }\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/MarkersClusteringDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.MarkersClusteringVM\nimport ovh.plrapps.mapcompose.ui.MapUI\n\n@Composable\nfun MarkersClusteringDemo(\n    modifier: Modifier = Modifier,\n    viewModel: MarkersClusteringVM = viewModel()\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.MARKERS_CLUSTERING.title) },\n            )\n        }\n    ) { padding ->\n        MapUI(modifier.padding(padding), state = viewModel.state)\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/MarkersLazyLoadingDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.MarkersLazyLoadingVM\nimport ovh.plrapps.mapcompose.ui.MapUI\n\n@Composable\nfun MarkersLazyLoadingDemo(\n    modifier: Modifier = Modifier,\n    viewModel: MarkersLazyLoadingVM = viewModel()\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.MARKERS_LAZY_LOADING.title) },\n            )\n        }\n    ) { padding ->\n        MapUI(modifier.padding(padding), state = viewModel.state)\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/OsmDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.OsmVM\nimport ovh.plrapps.mapcompose.ui.MapUI\n\n@Composable\nfun OsmDemo(\n    modifier: Modifier = Modifier, viewModel: OsmVM = viewModel()\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.OSM_DEMO.title) },\n            )\n        }\n    ) { padding ->\n        MapUI(\n            modifier.padding(padding),\n            state = viewModel.state\n        )\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/PathsDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.PathsVM\nimport ovh.plrapps.mapcompose.ui.MapUI\n\n@Composable\nfun PathsDemo(\n    modifier: Modifier = Modifier, viewModel: PathsVM = viewModel()\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.PATHS.title) },\n            )\n        }\n    ) { padding ->\n        MapUI(\n            modifier.padding(padding),\n            state = viewModel.state\n        )\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/RotationDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.api.rotation\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.RotationVM\nimport ovh.plrapps.mapcompose.ui.MapUI\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\n@Composable\nfun RotationDemo(modifier: Modifier = Modifier, viewModel: RotationVM = viewModel()) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.MAP_WITH_ROTATION_CONTROLS.title) },\n            )\n        }\n    ) { padding ->\n        RotationScreen(\n            modifier.padding(padding),\n            mapState = viewModel.state,\n            onRotate = viewModel::onRotate\n        )\n    }\n}\n\n@Composable\nprivate fun RotationScreen(\n    modifier: Modifier = Modifier,\n    mapState: MapState,\n    onRotate: () -> Unit\n) {\n    val sliderValue = mapState.rotation / 360f\n\n    Column(modifier.fillMaxSize()) {\n        MapUI(\n            Modifier.weight(1f),\n            state = mapState\n        )\n        Row {\n            Button(onClick = onRotate, Modifier.padding(8.dp)) {\n                Text(text = \"Rotate 90°\")\n            }\n            Slider(\n                value = sliderValue,\n                valueRange = 0f..0.9999f,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 16.dp),\n                onValueChange = { v -> mapState.rotation = v * 360f })\n        }\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/SimpleDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.SimpleDemoVM\nimport ovh.plrapps.mapcompose.ui.MapUI\n\n\n@Composable\nfun MapDemoSimple(\n    modifier: Modifier = Modifier, viewModel: SimpleDemoVM = viewModel()\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.MAP_ALONE.title) },\n            )\n        }\n    ) { padding ->\n        MapUI(modifier.padding(padding), state = viewModel.state)\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/screens/VisibleAreaPaddingDemo.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.demo.ui.screens\n\nimport androidx.compose.animation.expandHorizontally\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.shrinkHorizontally\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.api.centerOnMarker\nimport ovh.plrapps.mapcompose.api.setVisibleAreaPadding\nimport ovh.plrapps.mapcompose.demo.ui.MainDestinations\nimport ovh.plrapps.mapcompose.demo.viewmodels.VisibleAreaPaddingVM\nimport ovh.plrapps.mapcompose.ui.MapUI\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\n@Composable\nfun VisibleAreaPaddingDemo(\n    viewModel: VisibleAreaPaddingVM = viewModel()\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(MainDestinations.VISIBLE_AREA_PADDING.title) },\n            )\n        }\n    ) { padding ->\n        VisibleAreaPaddingScreen(Modifier.padding(padding), viewModel.state)\n    }\n}\n\n@Composable\nprivate fun VisibleAreaPaddingScreen(\n    modifier: Modifier,\n    mapState: MapState\n) {\n    val obstructionSize = 100.dp\n    val obstructionColor = Color(0xA0000000)\n    var leftObstructionEnabled by remember { mutableStateOf(true) }\n    var rightObstructionEnabled by remember { mutableStateOf(false) }\n    var topObstructionEnabled by remember { mutableStateOf(false) }\n    var bottomObstructionEnabled by remember { mutableStateOf(false) }\n    Column(\n        modifier = modifier,\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .clickable {\n                    topObstructionEnabled = !topObstructionEnabled\n                },\n            horizontalArrangement = Arrangement.Center,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Switch(topObstructionEnabled, onCheckedChange = null)\n            Text(\n                \"Top   \", // Same width as \"Bottom\"\n                modifier = Modifier.padding(start = 4.dp),\n                fontFamily = FontFamily.Monospace\n            )\n        }\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .clickable {\n                    topObstructionEnabled = !topObstructionEnabled\n                },\n            horizontalArrangement = Arrangement.SpaceAround\n        ) {\n            Row(\n                modifier = Modifier.clickable {\n                    leftObstructionEnabled = !leftObstructionEnabled\n                },\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Switch(leftObstructionEnabled, onCheckedChange = null)\n                Text(\n                    \"Left\",\n                    modifier = Modifier.padding(start = 4.dp),\n                    fontFamily = FontFamily.Monospace\n                )\n            }\n            Row(\n                modifier = Modifier.clickable {\n                    rightObstructionEnabled = !rightObstructionEnabled\n                },\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Switch(rightObstructionEnabled, onCheckedChange = null)\n                Text(\n                    \"Right\",\n                    modifier = Modifier.padding(start = 4.dp),\n                    fontFamily = FontFamily.Monospace\n                )\n            }\n        }\n\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .clickable {\n                    bottomObstructionEnabled = !bottomObstructionEnabled\n                },\n            horizontalArrangement = Arrangement.Center,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Switch(bottomObstructionEnabled, onCheckedChange = null)\n            Text(\n                \"Bottom\",\n                modifier = Modifier.padding(start = 4.dp),\n                fontFamily = FontFamily.Monospace\n            )\n        }\n        Spacer(Modifier.height(8.dp))\n        Box {\n            MapUI(\n                state = mapState\n            )\n            androidx.compose.animation.AnimatedVisibility(\n                visible = leftObstructionEnabled,\n                enter = expandHorizontally(),\n                exit = shrinkHorizontally(),\n                modifier = Modifier.align(Alignment.CenterStart)\n            ) {\n                Surface(\n                    color = obstructionColor,\n                    modifier = Modifier\n                        .fillMaxHeight()\n                        .width(obstructionSize)\n                ) {}\n            }\n            androidx.compose.animation.AnimatedVisibility(\n                visible = rightObstructionEnabled,\n                enter = expandHorizontally(expandFrom = Alignment.Start),\n                exit = shrinkHorizontally(),\n                modifier = Modifier.align(Alignment.CenterEnd)\n            ) {\n                Surface(\n                    color = obstructionColor,\n                    modifier = Modifier\n                        .fillMaxHeight()\n                        .width(obstructionSize)\n                ) {}\n            }\n            androidx.compose.animation.AnimatedVisibility(\n                visible = topObstructionEnabled,\n                enter = expandVertically(),\n                exit = shrinkVertically(),\n                modifier = Modifier.align(Alignment.TopCenter)\n            ) {\n                Surface(\n                    color = obstructionColor,\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(obstructionSize)\n                ) {}\n            }\n            androidx.compose.animation.AnimatedVisibility(\n                visible = bottomObstructionEnabled,\n                enter = expandVertically(),\n                exit = shrinkVertically(),\n                modifier = Modifier.align(Alignment.BottomCenter)\n            ) {\n                Surface(\n                    color = obstructionColor,\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(obstructionSize)\n                ) {}\n            }\n        }\n    }\n\n    LaunchedEffect(\n        leftObstructionEnabled,\n        rightObstructionEnabled,\n        topObstructionEnabled,\n        bottomObstructionEnabled\n    ) {\n        mapState.setVisibleAreaPadding(\n            left = if (leftObstructionEnabled) obstructionSize else 0.dp,\n            right = if (rightObstructionEnabled) obstructionSize else 0.dp,\n            top = if (topObstructionEnabled) obstructionSize else 0.dp,\n            bottom = if (bottomObstructionEnabled) obstructionSize else 0.dp\n        )\n        mapState.centerOnMarker(\"m0\")\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/theme/Color.kt",
    "content": "package ovh.plrapps.mapcompose.demo.ui.theme\n\nimport androidx.compose.ui.graphics.Color\n\nval primaryLight = Color(0xFF415F91)\nval onPrimaryLight = Color(0xFFFFFFFF)\nval primaryContainerLight = Color(0xFFD6E3FF)\nval onPrimaryContainerLight = Color(0xFF001B3E)\nval secondaryLight = Color(0xFF565F71)\nval onSecondaryLight = Color(0xFFFFFFFF)\nval secondaryContainerLight = Color(0xFFDAE2F9)\nval onSecondaryContainerLight = Color(0xFF131C2B)\nval tertiaryLight = Color(0xFF705575)\nval onTertiaryLight = Color(0xFFFFFFFF)\nval tertiaryContainerLight = Color(0xFFFAD8FD)\nval onTertiaryContainerLight = Color(0xFF28132E)\nval errorLight = Color(0xFFBA1A1A)\nval onErrorLight = Color(0xFFFFFFFF)\nval errorContainerLight = Color(0xFFFFDAD6)\nval onErrorContainerLight = Color(0xFF410002)\nval backgroundLight = Color(0xFFF9F9FF)\nval onBackgroundLight = Color(0xFF191C20)\nval surfaceLight = Color(0xFFF9F9FF)\nval onSurfaceLight = Color(0xFF191C20)\nval surfaceVariantLight = Color(0xFFE0E2EC)\nval onSurfaceVariantLight = Color(0xFF44474E)\nval outlineLight = Color(0xFF74777F)\nval outlineVariantLight = Color(0xFFC4C6D0)\nval scrimLight = Color(0xFF000000)\nval inverseSurfaceLight = Color(0xFF2E3036)\nval inverseOnSurfaceLight = Color(0xFFF0F0F7)\nval inversePrimaryLight = Color(0xFFAAC7FF)\nval surfaceDimLight = Color(0xFFD9D9E0)\nval surfaceBrightLight = Color(0xFFF9F9FF)\nval surfaceContainerLowestLight = Color(0xFFFFFFFF)\nval surfaceContainerLowLight = Color(0xFFF3F3FA)\nval surfaceContainerLight = Color(0xFFEDEDF4)\nval surfaceContainerHighLight = Color(0xFFE7E8EE)\nval surfaceContainerHighestLight = Color(0xFFE2E2E9)\n\nval primaryLightMediumContrast = Color(0xFF234373)\nval onPrimaryLightMediumContrast = Color(0xFFFFFFFF)\nval primaryContainerLightMediumContrast = Color(0xFF5875A8)\nval onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)\nval secondaryLightMediumContrast = Color(0xFF3A4354)\nval onSecondaryLightMediumContrast = Color(0xFFFFFFFF)\nval secondaryContainerLightMediumContrast = Color(0xFF6C7588)\nval onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)\nval tertiaryLightMediumContrast = Color(0xFF523A58)\nval onTertiaryLightMediumContrast = Color(0xFFFFFFFF)\nval tertiaryContainerLightMediumContrast = Color(0xFF876B8C)\nval onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)\nval errorLightMediumContrast = Color(0xFF8C0009)\nval onErrorLightMediumContrast = Color(0xFFFFFFFF)\nval errorContainerLightMediumContrast = Color(0xFFDA342E)\nval onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)\nval backgroundLightMediumContrast = Color(0xFFF9F9FF)\nval onBackgroundLightMediumContrast = Color(0xFF191C20)\nval surfaceLightMediumContrast = Color(0xFFF9F9FF)\nval onSurfaceLightMediumContrast = Color(0xFF191C20)\nval surfaceVariantLightMediumContrast = Color(0xFFE0E2EC)\nval onSurfaceVariantLightMediumContrast = Color(0xFF40434A)\nval outlineLightMediumContrast = Color(0xFF5C5F67)\nval outlineVariantLightMediumContrast = Color(0xFF787A83)\nval scrimLightMediumContrast = Color(0xFF000000)\nval inverseSurfaceLightMediumContrast = Color(0xFF2E3036)\nval inverseOnSurfaceLightMediumContrast = Color(0xFFF0F0F7)\nval inversePrimaryLightMediumContrast = Color(0xFFAAC7FF)\nval surfaceDimLightMediumContrast = Color(0xFFD9D9E0)\nval surfaceBrightLightMediumContrast = Color(0xFFF9F9FF)\nval surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)\nval surfaceContainerLowLightMediumContrast = Color(0xFFF3F3FA)\nval surfaceContainerLightMediumContrast = Color(0xFFEDEDF4)\nval surfaceContainerHighLightMediumContrast = Color(0xFFE7E8EE)\nval surfaceContainerHighestLightMediumContrast = Color(0xFFE2E2E9)\n\nval primaryLightHighContrast = Color(0xFF00214A)\nval onPrimaryLightHighContrast = Color(0xFFFFFFFF)\nval primaryContainerLightHighContrast = Color(0xFF234373)\nval onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)\nval secondaryLightHighContrast = Color(0xFF192232)\nval onSecondaryLightHighContrast = Color(0xFFFFFFFF)\nval secondaryContainerLightHighContrast = Color(0xFF3A4354)\nval onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)\nval tertiaryLightHighContrast = Color(0xFF301A35)\nval onTertiaryLightHighContrast = Color(0xFFFFFFFF)\nval tertiaryContainerLightHighContrast = Color(0xFF523A58)\nval onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)\nval errorLightHighContrast = Color(0xFF4E0002)\nval onErrorLightHighContrast = Color(0xFFFFFFFF)\nval errorContainerLightHighContrast = Color(0xFF8C0009)\nval onErrorContainerLightHighContrast = Color(0xFFFFFFFF)\nval backgroundLightHighContrast = Color(0xFFF9F9FF)\nval onBackgroundLightHighContrast = Color(0xFF191C20)\nval surfaceLightHighContrast = Color(0xFFF9F9FF)\nval onSurfaceLightHighContrast = Color(0xFF000000)\nval surfaceVariantLightHighContrast = Color(0xFFE0E2EC)\nval onSurfaceVariantLightHighContrast = Color(0xFF21242B)\nval outlineLightHighContrast = Color(0xFF40434A)\nval outlineVariantLightHighContrast = Color(0xFF40434A)\nval scrimLightHighContrast = Color(0xFF000000)\nval inverseSurfaceLightHighContrast = Color(0xFF2E3036)\nval inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)\nval inversePrimaryLightHighContrast = Color(0xFFE5ECFF)\nval surfaceDimLightHighContrast = Color(0xFFD9D9E0)\nval surfaceBrightLightHighContrast = Color(0xFFF9F9FF)\nval surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)\nval surfaceContainerLowLightHighContrast = Color(0xFFF3F3FA)\nval surfaceContainerLightHighContrast = Color(0xFFEDEDF4)\nval surfaceContainerHighLightHighContrast = Color(0xFFE7E8EE)\nval surfaceContainerHighestLightHighContrast = Color(0xFFE2E2E9)\n\nval primaryDark = Color(0xFFAAC7FF)\nval onPrimaryDark = Color(0xFF0A305F)\nval primaryContainerDark = Color(0xFF284777)\nval onPrimaryContainerDark = Color(0xFFD6E3FF)\nval secondaryDark = Color(0xFFBEC6DC)\nval onSecondaryDark = Color(0xFF283141)\nval secondaryContainerDark = Color(0xFF3E4759)\nval onSecondaryContainerDark = Color(0xFFDAE2F9)\nval tertiaryDark = Color(0xFFDDBCE0)\nval onTertiaryDark = Color(0xFF3F2844)\nval tertiaryContainerDark = Color(0xFF573E5C)\nval onTertiaryContainerDark = Color(0xFFFAD8FD)\nval errorDark = Color(0xFFFFB4AB)\nval onErrorDark = Color(0xFF690005)\nval errorContainerDark = Color(0xFF93000A)\nval onErrorContainerDark = Color(0xFFFFDAD6)\nval backgroundDark = Color(0xFF111318)\nval onBackgroundDark = Color(0xFFE2E2E9)\nval surfaceDark = Color(0xFF111318)\nval onSurfaceDark = Color(0xFFE2E2E9)\nval surfaceVariantDark = Color(0xFF44474E)\nval onSurfaceVariantDark = Color(0xFFC4C6D0)\nval outlineDark = Color(0xFF8E9099)\nval outlineVariantDark = Color(0xFF44474E)\nval scrimDark = Color(0xFF000000)\nval inverseSurfaceDark = Color(0xFFE2E2E9)\nval inverseOnSurfaceDark = Color(0xFF2E3036)\nval inversePrimaryDark = Color(0xFF415F91)\nval surfaceDimDark = Color(0xFF111318)\nval surfaceBrightDark = Color(0xFF37393E)\nval surfaceContainerLowestDark = Color(0xFF0C0E13)\nval surfaceContainerLowDark = Color(0xFF191C20)\nval surfaceContainerDark = Color(0xFF1D2024)\nval surfaceContainerHighDark = Color(0xFF282A2F)\nval surfaceContainerHighestDark = Color(0xFF33353A)\n\nval primaryDarkMediumContrast = Color(0xFFB1CBFF)\nval onPrimaryDarkMediumContrast = Color(0xFF001634)\nval primaryContainerDarkMediumContrast = Color(0xFF7491C7)\nval onPrimaryContainerDarkMediumContrast = Color(0xFF000000)\nval secondaryDarkMediumContrast = Color(0xFFC2CBE0)\nval onSecondaryDarkMediumContrast = Color(0xFF0D1626)\nval secondaryContainerDarkMediumContrast = Color(0xFF8891A5)\nval onSecondaryContainerDarkMediumContrast = Color(0xFF000000)\nval tertiaryDarkMediumContrast = Color(0xFFE1C0E5)\nval onTertiaryDarkMediumContrast = Color(0xFF230E29)\nval tertiaryContainerDarkMediumContrast = Color(0xFFA487A9)\nval onTertiaryContainerDarkMediumContrast = Color(0xFF000000)\nval errorDarkMediumContrast = Color(0xFFFFBAB1)\nval onErrorDarkMediumContrast = Color(0xFF370001)\nval errorContainerDarkMediumContrast = Color(0xFFFF5449)\nval onErrorContainerDarkMediumContrast = Color(0xFF000000)\nval backgroundDarkMediumContrast = Color(0xFF111318)\nval onBackgroundDarkMediumContrast = Color(0xFFE2E2E9)\nval surfaceDarkMediumContrast = Color(0xFF111318)\nval onSurfaceDarkMediumContrast = Color(0xFFFBFAFF)\nval surfaceVariantDarkMediumContrast = Color(0xFF44474E)\nval onSurfaceVariantDarkMediumContrast = Color(0xFFC8CAD4)\nval outlineDarkMediumContrast = Color(0xFFA0A3AC)\nval outlineVariantDarkMediumContrast = Color(0xFF80838C)\nval scrimDarkMediumContrast = Color(0xFF000000)\nval inverseSurfaceDarkMediumContrast = Color(0xFFE2E2E9)\nval inverseOnSurfaceDarkMediumContrast = Color(0xFF282A2F)\nval inversePrimaryDarkMediumContrast = Color(0xFF294878)\nval surfaceDimDarkMediumContrast = Color(0xFF111318)\nval surfaceBrightDarkMediumContrast = Color(0xFF37393E)\nval surfaceContainerLowestDarkMediumContrast = Color(0xFF0C0E13)\nval surfaceContainerLowDarkMediumContrast = Color(0xFF191C20)\nval surfaceContainerDarkMediumContrast = Color(0xFF1D2024)\nval surfaceContainerHighDarkMediumContrast = Color(0xFF282A2F)\nval surfaceContainerHighestDarkMediumContrast = Color(0xFF33353A)\n\nval primaryDarkHighContrast = Color(0xFFFBFAFF)\nval onPrimaryDarkHighContrast = Color(0xFF000000)\nval primaryContainerDarkHighContrast = Color(0xFFB1CBFF)\nval onPrimaryContainerDarkHighContrast = Color(0xFF000000)\nval secondaryDarkHighContrast = Color(0xFFFBFAFF)\nval onSecondaryDarkHighContrast = Color(0xFF000000)\nval secondaryContainerDarkHighContrast = Color(0xFFC2CBE0)\nval onSecondaryContainerDarkHighContrast = Color(0xFF000000)\nval tertiaryDarkHighContrast = Color(0xFFFFF9FA)\nval onTertiaryDarkHighContrast = Color(0xFF000000)\nval tertiaryContainerDarkHighContrast = Color(0xFFE1C0E5)\nval onTertiaryContainerDarkHighContrast = Color(0xFF000000)\nval errorDarkHighContrast = Color(0xFFFFF9F9)\nval onErrorDarkHighContrast = Color(0xFF000000)\nval errorContainerDarkHighContrast = Color(0xFFFFBAB1)\nval onErrorContainerDarkHighContrast = Color(0xFF000000)\nval backgroundDarkHighContrast = Color(0xFF111318)\nval onBackgroundDarkHighContrast = Color(0xFFE2E2E9)\nval surfaceDarkHighContrast = Color(0xFF111318)\nval onSurfaceDarkHighContrast = Color(0xFFFFFFFF)\nval surfaceVariantDarkHighContrast = Color(0xFF44474E)\nval onSurfaceVariantDarkHighContrast = Color(0xFFFBFAFF)\nval outlineDarkHighContrast = Color(0xFFC8CAD4)\nval outlineVariantDarkHighContrast = Color(0xFFC8CAD4)\nval scrimDarkHighContrast = Color(0xFF000000)\nval inverseSurfaceDarkHighContrast = Color(0xFFE2E2E9)\nval inverseOnSurfaceDarkHighContrast = Color(0xFF000000)\nval inversePrimaryDarkHighContrast = Color(0xFF002959)\nval surfaceDimDarkHighContrast = Color(0xFF111318)\nval surfaceBrightDarkHighContrast = Color(0xFF37393E)\nval surfaceContainerLowestDarkHighContrast = Color(0xFF0C0E13)\nval surfaceContainerLowDarkHighContrast = Color(0xFF191C20)\nval surfaceContainerDarkHighContrast = Color(0xFF1D2024)\nval surfaceContainerHighDarkHighContrast = Color(0xFF282A2F)\nval surfaceContainerHighestDarkHighContrast = Color(0xFF33353A)\n\n\n\n\n\n\n\n"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/theme/Theme.kt",
    "content": "package ovh.plrapps.mapcompose.demo.ui.theme\n\nimport android.os.Build\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\n\nprivate val lightScheme = lightColorScheme(\n    primary = primaryLight,\n    onPrimary = onPrimaryLight,\n    primaryContainer = primaryContainerLight,\n    onPrimaryContainer = onPrimaryContainerLight,\n    secondary = secondaryLight,\n    onSecondary = onSecondaryLight,\n    secondaryContainer = secondaryContainerLight,\n    onSecondaryContainer = onSecondaryContainerLight,\n    tertiary = tertiaryLight,\n    onTertiary = onTertiaryLight,\n    tertiaryContainer = tertiaryContainerLight,\n    onTertiaryContainer = onTertiaryContainerLight,\n    error = errorLight,\n    onError = onErrorLight,\n    errorContainer = errorContainerLight,\n    onErrorContainer = onErrorContainerLight,\n    background = backgroundLight,\n    onBackground = onBackgroundLight,\n    surface = surfaceLight,\n    onSurface = onSurfaceLight,\n    surfaceVariant = surfaceVariantLight,\n    onSurfaceVariant = onSurfaceVariantLight,\n    outline = outlineLight,\n    outlineVariant = outlineVariantLight,\n    scrim = scrimLight,\n    inverseSurface = inverseSurfaceLight,\n    inverseOnSurface = inverseOnSurfaceLight,\n    inversePrimary = inversePrimaryLight,\n    surfaceDim = surfaceDimLight,\n    surfaceBright = surfaceBrightLight,\n    surfaceContainerLowest = surfaceContainerLowestLight,\n    surfaceContainerLow = surfaceContainerLowLight,\n    surfaceContainer = surfaceContainerLight,\n    surfaceContainerHigh = surfaceContainerHighLight,\n    surfaceContainerHighest = surfaceContainerHighestLight,\n)\n\nprivate val darkScheme = darkColorScheme(\n    primary = primaryDark,\n    onPrimary = onPrimaryDark,\n    primaryContainer = primaryContainerDark,\n    onPrimaryContainer = onPrimaryContainerDark,\n    secondary = secondaryDark,\n    onSecondary = onSecondaryDark,\n    secondaryContainer = secondaryContainerDark,\n    onSecondaryContainer = onSecondaryContainerDark,\n    tertiary = tertiaryDark,\n    onTertiary = onTertiaryDark,\n    tertiaryContainer = tertiaryContainerDark,\n    onTertiaryContainer = onTertiaryContainerDark,\n    error = errorDark,\n    onError = onErrorDark,\n    errorContainer = errorContainerDark,\n    onErrorContainer = onErrorContainerDark,\n    background = backgroundDark,\n    onBackground = onBackgroundDark,\n    surface = surfaceDark,\n    onSurface = onSurfaceDark,\n    surfaceVariant = surfaceVariantDark,\n    onSurfaceVariant = onSurfaceVariantDark,\n    outline = outlineDark,\n    outlineVariant = outlineVariantDark,\n    scrim = scrimDark,\n    inverseSurface = inverseSurfaceDark,\n    inverseOnSurface = inverseOnSurfaceDark,\n    inversePrimary = inversePrimaryDark,\n    surfaceDim = surfaceDimDark,\n    surfaceBright = surfaceBrightDark,\n    surfaceContainerLowest = surfaceContainerLowestDark,\n    surfaceContainerLow = surfaceContainerLowDark,\n    surfaceContainer = surfaceContainerDark,\n    surfaceContainerHigh = surfaceContainerHighDark,\n    surfaceContainerHighest = surfaceContainerHighestDark,\n)\n\nprivate val mediumContrastLightColorScheme = lightColorScheme(\n    primary = primaryLightMediumContrast,\n    onPrimary = onPrimaryLightMediumContrast,\n    primaryContainer = primaryContainerLightMediumContrast,\n    onPrimaryContainer = onPrimaryContainerLightMediumContrast,\n    secondary = secondaryLightMediumContrast,\n    onSecondary = onSecondaryLightMediumContrast,\n    secondaryContainer = secondaryContainerLightMediumContrast,\n    onSecondaryContainer = onSecondaryContainerLightMediumContrast,\n    tertiary = tertiaryLightMediumContrast,\n    onTertiary = onTertiaryLightMediumContrast,\n    tertiaryContainer = tertiaryContainerLightMediumContrast,\n    onTertiaryContainer = onTertiaryContainerLightMediumContrast,\n    error = errorLightMediumContrast,\n    onError = onErrorLightMediumContrast,\n    errorContainer = errorContainerLightMediumContrast,\n    onErrorContainer = onErrorContainerLightMediumContrast,\n    background = backgroundLightMediumContrast,\n    onBackground = onBackgroundLightMediumContrast,\n    surface = surfaceLightMediumContrast,\n    onSurface = onSurfaceLightMediumContrast,\n    surfaceVariant = surfaceVariantLightMediumContrast,\n    onSurfaceVariant = onSurfaceVariantLightMediumContrast,\n    outline = outlineLightMediumContrast,\n    outlineVariant = outlineVariantLightMediumContrast,\n    scrim = scrimLightMediumContrast,\n    inverseSurface = inverseSurfaceLightMediumContrast,\n    inverseOnSurface = inverseOnSurfaceLightMediumContrast,\n    inversePrimary = inversePrimaryLightMediumContrast,\n    surfaceDim = surfaceDimLightMediumContrast,\n    surfaceBright = surfaceBrightLightMediumContrast,\n    surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,\n    surfaceContainerLow = surfaceContainerLowLightMediumContrast,\n    surfaceContainer = surfaceContainerLightMediumContrast,\n    surfaceContainerHigh = surfaceContainerHighLightMediumContrast,\n    surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,\n)\n\nprivate val highContrastLightColorScheme = lightColorScheme(\n    primary = primaryLightHighContrast,\n    onPrimary = onPrimaryLightHighContrast,\n    primaryContainer = primaryContainerLightHighContrast,\n    onPrimaryContainer = onPrimaryContainerLightHighContrast,\n    secondary = secondaryLightHighContrast,\n    onSecondary = onSecondaryLightHighContrast,\n    secondaryContainer = secondaryContainerLightHighContrast,\n    onSecondaryContainer = onSecondaryContainerLightHighContrast,\n    tertiary = tertiaryLightHighContrast,\n    onTertiary = onTertiaryLightHighContrast,\n    tertiaryContainer = tertiaryContainerLightHighContrast,\n    onTertiaryContainer = onTertiaryContainerLightHighContrast,\n    error = errorLightHighContrast,\n    onError = onErrorLightHighContrast,\n    errorContainer = errorContainerLightHighContrast,\n    onErrorContainer = onErrorContainerLightHighContrast,\n    background = backgroundLightHighContrast,\n    onBackground = onBackgroundLightHighContrast,\n    surface = surfaceLightHighContrast,\n    onSurface = onSurfaceLightHighContrast,\n    surfaceVariant = surfaceVariantLightHighContrast,\n    onSurfaceVariant = onSurfaceVariantLightHighContrast,\n    outline = outlineLightHighContrast,\n    outlineVariant = outlineVariantLightHighContrast,\n    scrim = scrimLightHighContrast,\n    inverseSurface = inverseSurfaceLightHighContrast,\n    inverseOnSurface = inverseOnSurfaceLightHighContrast,\n    inversePrimary = inversePrimaryLightHighContrast,\n    surfaceDim = surfaceDimLightHighContrast,\n    surfaceBright = surfaceBrightLightHighContrast,\n    surfaceContainerLowest = surfaceContainerLowestLightHighContrast,\n    surfaceContainerLow = surfaceContainerLowLightHighContrast,\n    surfaceContainer = surfaceContainerLightHighContrast,\n    surfaceContainerHigh = surfaceContainerHighLightHighContrast,\n    surfaceContainerHighest = surfaceContainerHighestLightHighContrast,\n)\n\nprivate val mediumContrastDarkColorScheme = darkColorScheme(\n    primary = primaryDarkMediumContrast,\n    onPrimary = onPrimaryDarkMediumContrast,\n    primaryContainer = primaryContainerDarkMediumContrast,\n    onPrimaryContainer = onPrimaryContainerDarkMediumContrast,\n    secondary = secondaryDarkMediumContrast,\n    onSecondary = onSecondaryDarkMediumContrast,\n    secondaryContainer = secondaryContainerDarkMediumContrast,\n    onSecondaryContainer = onSecondaryContainerDarkMediumContrast,\n    tertiary = tertiaryDarkMediumContrast,\n    onTertiary = onTertiaryDarkMediumContrast,\n    tertiaryContainer = tertiaryContainerDarkMediumContrast,\n    onTertiaryContainer = onTertiaryContainerDarkMediumContrast,\n    error = errorDarkMediumContrast,\n    onError = onErrorDarkMediumContrast,\n    errorContainer = errorContainerDarkMediumContrast,\n    onErrorContainer = onErrorContainerDarkMediumContrast,\n    background = backgroundDarkMediumContrast,\n    onBackground = onBackgroundDarkMediumContrast,\n    surface = surfaceDarkMediumContrast,\n    onSurface = onSurfaceDarkMediumContrast,\n    surfaceVariant = surfaceVariantDarkMediumContrast,\n    onSurfaceVariant = onSurfaceVariantDarkMediumContrast,\n    outline = outlineDarkMediumContrast,\n    outlineVariant = outlineVariantDarkMediumContrast,\n    scrim = scrimDarkMediumContrast,\n    inverseSurface = inverseSurfaceDarkMediumContrast,\n    inverseOnSurface = inverseOnSurfaceDarkMediumContrast,\n    inversePrimary = inversePrimaryDarkMediumContrast,\n    surfaceDim = surfaceDimDarkMediumContrast,\n    surfaceBright = surfaceBrightDarkMediumContrast,\n    surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,\n    surfaceContainerLow = surfaceContainerLowDarkMediumContrast,\n    surfaceContainer = surfaceContainerDarkMediumContrast,\n    surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,\n    surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,\n)\n\nprivate val highContrastDarkColorScheme = darkColorScheme(\n    primary = primaryDarkHighContrast,\n    onPrimary = onPrimaryDarkHighContrast,\n    primaryContainer = primaryContainerDarkHighContrast,\n    onPrimaryContainer = onPrimaryContainerDarkHighContrast,\n    secondary = secondaryDarkHighContrast,\n    onSecondary = onSecondaryDarkHighContrast,\n    secondaryContainer = secondaryContainerDarkHighContrast,\n    onSecondaryContainer = onSecondaryContainerDarkHighContrast,\n    tertiary = tertiaryDarkHighContrast,\n    onTertiary = onTertiaryDarkHighContrast,\n    tertiaryContainer = tertiaryContainerDarkHighContrast,\n    onTertiaryContainer = onTertiaryContainerDarkHighContrast,\n    error = errorDarkHighContrast,\n    onError = onErrorDarkHighContrast,\n    errorContainer = errorContainerDarkHighContrast,\n    onErrorContainer = onErrorContainerDarkHighContrast,\n    background = backgroundDarkHighContrast,\n    onBackground = onBackgroundDarkHighContrast,\n    surface = surfaceDarkHighContrast,\n    onSurface = onSurfaceDarkHighContrast,\n    surfaceVariant = surfaceVariantDarkHighContrast,\n    onSurfaceVariant = onSurfaceVariantDarkHighContrast,\n    outline = outlineDarkHighContrast,\n    outlineVariant = outlineVariantDarkHighContrast,\n    scrim = scrimDarkHighContrast,\n    inverseSurface = inverseSurfaceDarkHighContrast,\n    inverseOnSurface = inverseOnSurfaceDarkHighContrast,\n    inversePrimary = inversePrimaryDarkHighContrast,\n    surfaceDim = surfaceDimDarkHighContrast,\n    surfaceBright = surfaceBrightDarkHighContrast,\n    surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,\n    surfaceContainerLow = surfaceContainerLowDarkHighContrast,\n    surfaceContainer = surfaceContainerDarkHighContrast,\n    surfaceContainerHigh = surfaceContainerHighDarkHighContrast,\n    surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,\n)\n\n@Immutable\ndata class ColorFamily(\n    val color: Color,\n    val onColor: Color,\n    val colorContainer: Color,\n    val onColorContainer: Color\n)\n\nval unspecified_scheme = ColorFamily(\n    Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified\n)\n\n@Composable\nfun MapComposeTheme(\n    darkTheme: Boolean = isSystemInDarkTheme(),\n    // Dynamic color is available on Android 12+\n    dynamicColor: Boolean = true,\n    content: @Composable() () -> Unit\n) {\n  val colorScheme = when {\n      dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {\n          val context = LocalContext.current\n          if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)\n      }\n      \n      darkTheme -> darkScheme\n      else -> lightScheme\n  }\n\n  MaterialTheme(\n    colorScheme = colorScheme,\n    typography = AppTypography,\n    content = content\n  )\n}\n\n"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/theme/Type.kt",
    "content": "package ovh.plrapps.mapcompose.demo.ui.theme\n\nimport androidx.compose.material3.Typography\n\nval AppTypography = Typography()\n"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/widgets/Callout.kt",
    "content": "package ovh.plrapps.mapcompose.demo.ui.widgets\n\nimport android.animation.TimeInterpolator\nimport android.view.animation.OvershootInterpolator\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.Easing\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.TransformOrigin\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport java.math.RoundingMode\nimport java.text.DecimalFormat\n\n/**\n * A callout which animates its entry with an overshoot scaling interpolator.\n */\n@Composable\nfun Callout(\n    x: Double, y: Double,\n    title: String,\n    shouldAnimate: Boolean,\n    onAnimationDone: () -> Unit\n) {\n    var animVal by remember { mutableStateOf(if (shouldAnimate) 0f else 1f) }\n    LaunchedEffect(true) {\n        if (shouldAnimate) {\n            Animatable(0f).animateTo(\n                targetValue = 1f,\n                animationSpec = tween(250, easing = overshootEasing)\n            ) {\n                animVal = value\n            }\n            onAnimationDone()\n        }\n    }\n    Surface(\n        Modifier\n            .alpha(animVal)\n            .padding(10.dp)\n            .graphicsLayer {\n                scaleX = animVal\n                scaleY = animVal\n                transformOrigin = TransformOrigin(0.5f, 1f)\n            },\n        shape = RoundedCornerShape(5.dp),\n        shadowElevation = 10.dp\n    ) {\n        Column(Modifier.padding(16.dp)) {\n            Text(\n                text = title,\n                modifier = Modifier.align(alignment = Alignment.CenterHorizontally),\n                fontSize = 14.sp,\n                textAlign = TextAlign.Center,\n                fontWeight = FontWeight.Bold,\n            )\n            Text(\n                text = \"position ${df.format(x)} , ${df.format(y)}\",\n                modifier = Modifier\n                    .align(alignment = Alignment.CenterHorizontally)\n                    .padding(top = 4.dp),\n                fontSize = 12.sp,\n                textAlign = TextAlign.Center,\n            )\n        }\n    }\n}\n\nprivate val df = DecimalFormat(\"#.##\").apply {\n    roundingMode = RoundingMode.CEILING\n}\n\nprivate val overshootEasing = OvershootInterpolator(1.2f).toEasing()\n\nprivate fun TimeInterpolator.toEasing() = Easing { x ->\n    getInterpolation(x)\n}\n"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/ui/widgets/Marker.kt",
    "content": "package ovh.plrapps.mapcompose.demo.ui.widgets\n\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Icon\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport ovh.plrapps.mapcompose.demo.R\n\n@Composable\nfun Marker() = Icon(\n    painter = painterResource(id = R.drawable.map_marker),\n    contentDescription = null,\n    modifier = Modifier.size(50.dp),\n    tint = Color(0xCC2196F3)\n)"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/utils/Metrics.kt",
    "content": "package ovh.plrapps.mapcompose.demo.utils\n\nimport android.content.res.Resources\n\n/**\n * Convert px to dp\n */\nfun pxToDp(px: Int): Int {\n    return (px / Resources.getSystem().displayMetrics.density).toInt()\n}\n\n/**\n * Convert dp to px\n */\nfun dpToPx(dp: Int): Int {\n    return (dp * Resources.getSystem().displayMetrics.density).toInt()\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/utils/Random.kt",
    "content": "package ovh.plrapps.mapcompose.demo.utils\n\nimport kotlin.random.Random.Default.nextDouble\n\nfun randomDouble(center: Double, radius: Double) : Double {\n    return nextDouble(from = center - radius, until = center + radius)\n}\n"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/utils/WebMercator.kt",
    "content": "package ovh.plrapps.mapcompose.demo.utils\n\nimport kotlin.math.PI\nimport kotlin.math.atan\nimport kotlin.math.exp\nimport kotlin.math.floor\nimport kotlin.math.ln\nimport kotlin.math.tan\n\n/**\n * Given a [latitude] and [longitude], get the corresponding relative coordinates (values between\n * 0.0 and 1.0).\n * It is assumed that the map uses the Web Mercator projection.\n */\nfun latLonToNormalized(latitude: Double, longitude: Double): Pair<Double, Double> {\n    val latRad = latitude * PI / 180.0\n    val lngRad = longitude * PI / 180.0\n\n    // Web Mercator projected coordinates\n    val X = earthRadius * lngRad\n    val Y = earthRadius * ln(tan((PI / 4.0) + (latRad / 2.0)))\n\n    // Relative coordinates for MapCompose\n    val piR = earthRadius * PI\n    val normalizedX = (X + piR) / (2.0 * piR)\n    val normalizedY = (piR - Y) / (2.0 * piR)\n\n    return Pair(normalizedX, normalizedY)\n}\n\n/**\n * Given relative coordinates in a world map (a square of size 2.0 * piR), get the corresponding\n * latitude and longitude.\n * It is assumed that the map uses the Web Mercator projection.\n */\nfun normalizedToLatLon(normalizedX: Double, normalizedY: Double): Pair<Double, Double> {\n    val piR = earthRadius * PI\n    val mercatorX = normalizedX * (2.0 * piR) - piR\n    val mercatorY = piR - normalizedY * (2.0 * piR)\n\n    val num = mercatorX / earthRadius\n    val num2 = num * 180.0 / PI\n    val num3 = floor((num2 + 180) / 360.0f)\n\n    val lng = num2 - (num3 * 360)\n    val num4 = PI / 2 - (2.0 * atan(exp(-mercatorY / earthRadius)))\n    val lat = num4 * 180.0 / PI\n    return Pair(lat, lng)\n}\n\nprivate const val earthRadius = 6_378_137.0 // in meters"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/AddingMarkerVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport android.app.Application\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.Icon\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.AndroidViewModel\nimport ovh.plrapps.mapcompose.api.*\nimport ovh.plrapps.mapcompose.demo.R\nimport ovh.plrapps.mapcompose.demo.providers.makeTileStreamProvider\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\nclass AddingMarkerVM(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider = makeTileStreamProvider(application.applicationContext)\n\n    private var markerCount = 0\n\n    val state = MapState(4, 8192, 8192) {\n        scale(0.0) // zoom-out to minimum scale\n    }.apply {\n        addLayer(tileStreamProvider)\n        onMarkerMove { id, x, y, _, _ ->\n            println(\"move $id $x $y\")\n        }\n        onMarkerClick { id, x, y ->\n            println(\"marker click $id $x $y\")\n        }\n        onMarkerLongPress { id, x, y ->\n            println(\"on marker long press $id $x $y\")\n        }\n        onTap { x, y ->\n            println(\"on tap $x $y\")\n        }\n        onLongPress { x, y ->\n            println(\"on long press $x $y\")\n        }\n        enableRotation()\n        setScrollOffsetRatio(0.5f, 0.5f)\n    }\n\n\n    fun addMarker() {\n        state.addMarker(\"marker$markerCount\", 0.5, 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        state.enableMarkerDrag(\"marker$markerCount\")\n        markerCount++\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/AnimationDemoVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport android.app.Application\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.SnapSpec\nimport androidx.compose.animation.core.TweenSpec\nimport androidx.compose.ui.geometry.Offset\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.addMarker\nimport ovh.plrapps.mapcompose.api.enableRotation\nimport ovh.plrapps.mapcompose.api.onTouchDown\nimport ovh.plrapps.mapcompose.api.rotateTo\nimport ovh.plrapps.mapcompose.api.scrollTo\nimport ovh.plrapps.mapcompose.api.shouldLoopScale\nimport ovh.plrapps.mapcompose.demo.providers.makeTileStreamProvider\nimport ovh.plrapps.mapcompose.demo.ui.widgets.Marker\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\n/**\n * This demo shows how animations can be chained one after another.\n * Since animations APIs are suspending functions, this is easy to do.\n */\nclass AnimationDemoVM(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider = makeTileStreamProvider(application.applicationContext)\n    private var job: Job? = null\n    private val spec = TweenSpec<Float>(2000, easing = FastOutSlowInEasing)\n\n    val state = MapState(4, 8192, 8192).apply {\n        addLayer(tileStreamProvider)\n        shouldLoopScale = true\n        enableRotation()\n        addMarker(\"m0\", 0.5, 0.5) { Marker() }\n        addMarker(\"m1\", 0.78, 0.78) { Marker() }\n        addMarker(\"m2\", 0.79, 0.79) { Marker() }\n        addMarker(\"m3\", 0.785, 0.72) { Marker() }\n        onTouchDown {\n            job?.cancel()\n        }\n        viewModelScope.launch {\n            scrollTo(0.5, 0.5, 2.0, SnapSpec())\n        }\n    }\n\n    fun startAnimation() {\n        /* Cancel ongoing animation */\n        job?.cancel()\n\n        /* Start a new one */\n        with(state) {\n            job = viewModelScope.launch {\n                scrollTo(0.0, 0.0, 2.0, spec, screenOffset = Offset.Zero)\n                scrollTo(0.8, 0.8, 2.0, spec)\n                rotateTo(180f, spec)\n                scrollTo(0.5, 0.5, 0.5, spec)\n                scrollTo(0.5, 0.5, 2.0, TweenSpec(800, easing = FastOutSlowInEasing))\n                rotateTo(0f, TweenSpec(1000, easing = FastOutSlowInEasing))\n            }\n        }\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/CalloutVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport android.app.Application\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Icon\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.AndroidViewModel\nimport ovh.plrapps.mapcompose.api.addCallout\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.addMarker\nimport ovh.plrapps.mapcompose.api.onCalloutClick\nimport ovh.plrapps.mapcompose.api.onMarkerClick\nimport ovh.plrapps.mapcompose.api.removeCallout\nimport ovh.plrapps.mapcompose.api.scale\nimport ovh.plrapps.mapcompose.demo.R\nimport ovh.plrapps.mapcompose.demo.providers.makeTileStreamProvider\nimport ovh.plrapps.mapcompose.demo.ui.widgets.Callout\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\nclass CalloutVM(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider = makeTileStreamProvider(application.applicationContext)\n\n    /* Define the markers data (id and position) */\n    private val markers = listOf(\n        MarkerInfo(\"Callout #1\", 0.45, 0.6),\n        MarkerInfo(\"Callout #2\", 0.24, 0.1),\n        MarkerInfo(\"Callout #3\", 0.25, 0.18),\n        MarkerInfo(TAP_TO_DISMISS_ID, 0.4, 0.3)\n    )\n\n    val state = MapState(4, 8192, 8192).apply {\n        addLayer(tileStreamProvider)\n\n        /* Add all markers */\n        for (marker in markers) {\n            addMarker(marker.id, marker.x, marker.y) {\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        scale = 0.0\n\n        /**\n         * On marker click, add a callout. If the id is [TAP_TO_DISMISS_ID], set auto-dismiss\n         * to false. For this particular id, we programmatically remove the callout on tap.\n         */\n        onMarkerClick { id, x, y ->\n            var shouldAnimate by mutableStateOf(true)\n            addCallout(\n                id, x, y,\n                absoluteOffset = DpOffset(0.dp, (-50).dp),\n                autoDismiss = id != TAP_TO_DISMISS_ID,\n                clickable = id == TAP_TO_DISMISS_ID\n            ) {\n                Callout(x, y, title = id, shouldAnimate) {\n                    shouldAnimate = false\n                }\n            }\n        }\n\n        /**\n         * Register a click listener on callouts. We don't need to remove the other callouts\n         * because they automatically dismiss on touch down.\n         */\n        onCalloutClick { id, _, _ ->\n            if (id == TAP_TO_DISMISS_ID) removeCallout(TAP_TO_DISMISS_ID)\n        }\n    }\n\n}\n\nprivate data class MarkerInfo(val id: String, val x: Double, val y: Double)\n\nprivate const val TAP_TO_DISMISS_ID = \"Tap me to dismiss\""
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/CenteringOnMarkerVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport android.app.Application\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.Icon\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.launch\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.addMarker\nimport ovh.plrapps.mapcompose.api.centerOnMarker\nimport ovh.plrapps.mapcompose.api.enableRotation\nimport ovh.plrapps.mapcompose.demo.R\nimport ovh.plrapps.mapcompose.demo.providers.makeTileStreamProvider\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\nclass CenteringOnMarkerVM(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider = makeTileStreamProvider(application.applicationContext)\n\n    val state = MapState(4, 8192, 8192) {\n        rotation(45f)\n    }.apply {\n        addLayer(tileStreamProvider)\n        addMarker(\"parking\", 0.2457938, 0.3746023) {\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        enableRotation()\n    }\n\n    fun onCenter() {\n        viewModelScope.launch {\n            state.centerOnMarker(\"parking\", destScale = 1.0, destAngle = 0f)\n        }\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/CustomDrawVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport android.app.Application\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.launch\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.addMarker\nimport ovh.plrapps.mapcompose.api.enableMarkerDrag\nimport ovh.plrapps.mapcompose.api.enableRotation\nimport ovh.plrapps.mapcompose.api.moveMarker\nimport ovh.plrapps.mapcompose.api.scale\nimport ovh.plrapps.mapcompose.api.scrollTo\nimport ovh.plrapps.mapcompose.api.setStateChangeListener\nimport ovh.plrapps.mapcompose.api.shouldLoopScale\nimport ovh.plrapps.mapcompose.demo.providers.makeTileStreamProvider\nimport ovh.plrapps.mapcompose.demo.ui.screens.ScaleIndicatorController\nimport ovh.plrapps.mapcompose.ui.MapUI\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\n/**\n * In this example, we're adding two markers with custom drag interceptors which update [p1x], [p1y],\n * [p2x], and [p2y] states. In turn, when those state change, the line joining the two markers updates.\n * The line is added as a custom view inside [MapUI] composable.\n */\nclass CustomDrawVM(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider = makeTileStreamProvider(application.applicationContext)\n\n    var p1x by mutableStateOf(0.6)\n    var p1y by mutableStateOf(0.6)\n    var p2x by mutableStateOf(0.4)\n    var p2y by mutableStateOf(0.4)\n\n    val state = MapState(4, 8192, 8192).apply {\n        addLayer(tileStreamProvider)\n        shouldLoopScale = true\n        enableRotation()\n        viewModelScope.launch {\n            scrollTo(0.5, 0.5, 1.1)\n        }\n    }\n\n    val scaleIndicatorController = ScaleIndicatorController(450, state.scale)\n\n    init {\n        state.addMarker(\"m1\", p1x, p1y, Offset(-0.5f, -0.5f)) {\n            Box(\n                modifier = Modifier\n                    .size(50.dp)\n                    .clip(CircleShape)\n                    .background(Color(0xAAF44336))\n            )\n        }\n        state.addMarker(\"m2\", p2x, p2y, Offset(-0.5f, -0.5f)) {\n            Box(\n                modifier = Modifier\n                    .size(50.dp)\n                    .clip(CircleShape)\n                    .background(Color(0xAAF44336))\n            )\n        }\n        state.enableMarkerDrag(\"m1\") { id, x, y, dx, dy, _, _ ->\n            p1x = x + dx\n            p1y = y + dy\n            state.moveMarker(id, p1x, p1y)\n        }\n        state.enableMarkerDrag(\"m2\") { id, x, y, dx, dy, _, _ ->\n            p2x = x + dx\n            p2y = y + dy\n            state.moveMarker(id, p2x, p2y)\n        }\n        state.setStateChangeListener {\n            scaleIndicatorController.onScaleChanged(scale)\n        }\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/HttpTilesVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport androidx.lifecycle.ViewModel\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.scale\nimport ovh.plrapps.mapcompose.api.shouldLoopScale\nimport ovh.plrapps.mapcompose.core.TileStreamProvider\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport java.io.BufferedInputStream\nimport java.net.HttpURLConnection\nimport java.net.URL\n\n/**\n * Shows how MapCompose behaves with remote HTTP tiles.\n */\nclass HttpTilesVM : ViewModel() {\n    private val tileStreamProvider = makeTileStreamProvider()\n\n    val state = MapState(\n        levelCount = 4,\n        fullWidth = 8192,\n        fullHeight = 8192,\n        workerCount = 16  // Notice how we increase the worker count when performing HTTP requests\n    ).apply {\n        addLayer(tileStreamProvider)\n        scale = 0.0\n        shouldLoopScale = true\n    }\n}\n\n/**\n * A [TileStreamProvider] which performs HTTP requests.\n */\nprivate fun makeTileStreamProvider() =\n    TileStreamProvider { row, col, zoomLvl ->\n        try {\n            val url =\n                URL(\"https://raw.githubusercontent.com/p-lr/MapCompose/master/demo/src/main/assets/tiles/mont_blanc_layered/$zoomLvl/$row/$col.jpg\")\n            val connection = url.openConnection() as HttpURLConnection\n            connection.doInput = true\n            connection.connect()\n            BufferedInputStream(connection.inputStream)\n        } catch (e: Exception) {\n            e.printStackTrace()\n            null\n        }\n    }"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/InfiniteScrollVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.lifecycle.AndroidViewModel\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.enableRotation\nimport ovh.plrapps.mapcompose.api.shouldLoopScale\nimport ovh.plrapps.mapcompose.core.TileStreamProvider\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\nclass InfiniteScrollVM(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider = makeWorldTileStreamProvider(application.applicationContext)\n\n    val state = MapState(5, 8192, 8192) {\n        scale(0.1)\n        infiniteScrollX(true)\n    }.apply {\n        addLayer(tileStreamProvider)\n        shouldLoopScale = true\n        enableRotation()\n    }\n\n}\n\nprivate fun makeWorldTileStreamProvider(appContext: Context) =\n    TileStreamProvider { row, col, zoomLvl ->\n        try {\n            appContext.assets?.open(\"tiles/world/$zoomLvl/$row/$col.jpg\")\n        } catch (e: Exception) {\n            null\n        }\n    }"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/LayersVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.launch\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.enableRotation\nimport ovh.plrapps.mapcompose.api.scrollTo\nimport ovh.plrapps.mapcompose.api.setLayerOpacity\nimport ovh.plrapps.mapcompose.api.shouldLoopScale\nimport ovh.plrapps.mapcompose.core.TileStreamProvider\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\nclass LayersVM(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider =\n        makeTileStreamProvider(application.applicationContext, imageExt = \".jpg\")\n    private val slopesLayerProvider =\n        makeTileStreamProvider(application.applicationContext, \"ign-slopes-\", imageExt = \".png\")\n    private val roadLayerProvider =\n        makeTileStreamProvider(application.applicationContext, \"ign-road-\", imageExt = \".png\")\n\n    private var slopesId: String? = null\n    private var roadId: String? = null\n\n    val state = MapState(4, 8192, 8192).apply {\n        shouldLoopScale = true\n        enableRotation()\n        viewModelScope.launch {\n            scrollTo(0.4, 0.4, 1.0)\n        }\n\n        addLayer(tileStreamProvider)\n        slopesId = addLayer(slopesLayerProvider, initialOpacity = 0.6f)\n        roadId = addLayer(roadLayerProvider, initialOpacity = 1f)\n    }\n\n    private fun makeTileStreamProvider(appContext: Context, layer: String = \"\", imageExt: String) =\n        TileStreamProvider { row, col, zoomLvl ->\n            try {\n                appContext.assets?.open(\"tiles/mont_blanc_layered/$zoomLvl/$row/$layer$col$imageExt\")\n            } catch (e: Exception) {\n                null\n            }\n        }\n\n    fun setSlopesOpacity(opacity: Float) {\n        slopesId?.also { id ->\n            state.setLayerOpacity(id, opacity)\n        }\n\n    }\n\n    fun setRoadOpacity(opacity: Float) {\n        roadId?.also { id ->\n            state.setLayerOpacity(id, opacity)\n        }\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/MarkersClusteringVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport android.app.Application\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.AndroidViewModel\nimport ovh.plrapps.mapcompose.api.addClusterer\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.addMarker\nimport ovh.plrapps.mapcompose.demo.R\nimport ovh.plrapps.mapcompose.demo.providers.makeTileStreamProvider\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport ovh.plrapps.mapcompose.ui.state.markers.model.RenderingStrategy\n\n\n/**\n * Shows how to define and use a marker clusterer.\n */\nclass MarkersClusteringVM(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider = makeTileStreamProvider(application.applicationContext)\n\n    val state = MapState(4, 8192, 8192) {\n        scale(0.2)\n        maxScale(8.0)\n        scroll(0.5, 0.5)\n    }.apply {\n        addLayer(tileStreamProvider)\n    }\n\n    init {\n        /* Add a marker clusterer to manage markers. In this example, we use \"default\" for the id */\n        state.addClusterer(\"default\") { ids ->\n            { Cluster(size = ids.size) }\n        }\n\n        /* Add some markers to the map, using the same clusterer id we just defined (if a marker\n         * is added without any clusterer, it won't be managed by any clusterer)*/\n        listOf(\n            0.5 to 0.5,\n            0.51 to 0.5,\n            0.5 to 0.54,\n            0.51 to 0.54,\n            0.6 to 0.52,\n            0.48 to 0.35,\n            0.48 to 0.355,\n            0.485 to 0.35,\n            0.52 to 0.35,\n            0.515 to 0.36,\n            0.515 to 0.355,\n        ).forEachIndexed { i, pair ->\n            state.addMarker(\n                id = \"marker-$i\",\n                x = pair.first,\n                y = pair.second,\n                renderingStrategy = RenderingStrategy.Clustering(\"default\"),\n            ) {\n                Marker()\n            }\n        }\n\n        /* We can still add regular markers */\n        state.addMarker(\n            \"marker-regular\", 0.52, 0.36,\n            clickable = false\n        ) {\n            Icon(\n                painter = painterResource(id = R.drawable.map_marker),\n                contentDescription = null,\n                modifier = Modifier.size(50.dp),\n                tint = Color(0xEEF44336)\n            )\n        }\n    }\n\n    @Composable\n    private fun Marker() {\n        Icon(\n            painter = painterResource(id = R.drawable.map_marker),\n            contentDescription = null,\n            modifier = Modifier.size(50.dp),\n            tint = Color(0xEE2196F3)\n        )\n    }\n\n    @Composable\n    private fun Cluster(size: Int) {\n        /* Here we can customize the cluster style */\n        Box(\n            modifier = Modifier\n                .background(\n                    Color(0x992196F3),\n                    shape = CircleShape\n                )\n                .size(50.dp),\n            contentAlignment = Alignment.Center\n        ) {\n            Text(text = size.toString(), color = Color.White)\n        }\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/MarkersLazyLoadingVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport android.app.Application\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Icon\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.AndroidViewModel\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.addLazyLoader\nimport ovh.plrapps.mapcompose.api.addMarker\nimport ovh.plrapps.mapcompose.api.onMarkerClick\nimport ovh.plrapps.mapcompose.api.shouldLoopScale\nimport ovh.plrapps.mapcompose.demo.R\nimport ovh.plrapps.mapcompose.ui.layout.Forced\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport ovh.plrapps.mapcompose.ui.state.markers.model.RenderingStrategy\nimport kotlin.random.Random\n\n/**\n * Shows how to define and use a marker lazy-loader.\n */\nclass MarkersLazyLoadingVM(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider =\n        ovh.plrapps.mapcompose.demo.providers.makeTileStreamProvider(application.applicationContext)\n\n    val state = MapState(4, 8192, 8192) {\n        minimumScaleMode(Forced(1.0))\n        scale(1.0)\n        maxScale(4.0)\n        scroll(0.5, 0.5)\n    }.apply {\n        addLayer(tileStreamProvider)\n        shouldLoopScale = true\n    }\n\n    init {\n        /* Add a marker lazy loader. In this example, we use \"default\" for the id */\n        state.addLazyLoader(\"default\")\n\n        repeat(200) { i ->\n            val x = Random.nextDouble()\n            val y = Random.nextDouble()\n\n            /* Notice how we set the rendering strategy to lazy loading with the same id */\n            state.addMarker(\n                \"marker-$i\", x, y,\n                renderingStrategy = RenderingStrategy.LazyLoading(lazyLoaderId = \"default\")\n            ) {\n                Icon(\n                    painter = painterResource(id = R.drawable.map_marker),\n                    contentDescription = null,\n                    modifier = Modifier.size(50.dp),\n                    tint = Color(0xEE2196F3)\n                )\n            }\n        }\n\n        /* We can still add regular markers */\n        state.addMarker(\n            \"marker-regular\", 0.5, 0.5,\n        ) {\n            Icon(\n                painter = painterResource(id = R.drawable.map_marker),\n                contentDescription = null,\n                modifier = Modifier.size(50.dp),\n                tint = Color(0xEEF44336)\n            )\n        }\n\n        state.onMarkerClick { id, x, y ->\n            println(\"marker click $id $x $y\")\n        }\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/OsmVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Icon\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.ViewModel\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.addMarker\nimport ovh.plrapps.mapcompose.core.TileStreamProvider\nimport ovh.plrapps.mapcompose.demo.R\nimport ovh.plrapps.mapcompose.demo.utils.latLonToNormalized\nimport ovh.plrapps.mapcompose.ui.layout.Forced\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport java.io.BufferedInputStream\nimport java.net.HttpURLConnection\nimport java.net.URL\nimport kotlin.math.pow\n\n/**\n * Shows how to use WMTS tile servers with MapCompose, such as Open Street Map.\n */\nclass OsmVM : ViewModel() {\n    private val tileStreamProvider = makeTileStreamProvider()\n\n    private val maxLevel = 16\n    private val minLevel = 12\n    private val mapSize = mapSizeAtLevel(maxLevel, tileSize = 256)\n    private val paris = latLonToNormalized(48.856667, 2.351667) // Paris\n    private val x = paris.first\n    private val y = paris.second\n\n    val state = MapState(levelCount = maxLevel + 1, mapSize, mapSize, workerCount = 16) {\n        minimumScaleMode(Forced(1 / 2.0.pow(maxLevel - minLevel)))\n        scroll(x, y)\n        scale(0.0) // to zoom out initially\n    }.apply {\n        addLayer(tileStreamProvider)\n        addMarker(\"id\", x, y) {\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\n/**\n * A [TileStreamProvider] which performs HTTP requests.\n */\nprivate fun makeTileStreamProvider() =\n    TileStreamProvider { row, col, zoomLvl ->\n        try {\n            val url = URL(\"https://tile.openstreetmap.org/$zoomLvl/$col/$row.png\")\n            val connection = url.openConnection() as HttpURLConnection\n            // OSM requires a user-agent\n            connection.setRequestProperty(\"User-Agent\", \"Chrome/120.0.0.0 Safari/537.36\")\n            connection.doInput = true\n            connection.connect()\n            BufferedInputStream(connection.inputStream)\n        } catch (e: Exception) {\n            e.printStackTrace()\n            null\n        }\n    }\n\n/**\n * wmts level are 0 based.\n * At level 0, the map corresponds to just one tile.\n */\nprivate fun mapSizeAtLevel(wmtsLevel: Int, tileSize: Int): Int {\n    return tileSize * 2.0.pow(wmtsLevel).toInt()\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/PathsVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport android.app.Application\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.launch\nimport ovh.plrapps.mapcompose.api.addCallout\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.addPath\nimport ovh.plrapps.mapcompose.api.enableRotation\nimport ovh.plrapps.mapcompose.api.onPathClick\nimport ovh.plrapps.mapcompose.api.onPathLongPress\nimport ovh.plrapps.mapcompose.api.scrollTo\nimport ovh.plrapps.mapcompose.api.shouldLoopScale\nimport ovh.plrapps.mapcompose.demo.providers.makeTileStreamProvider\nimport ovh.plrapps.mapcompose.demo.ui.widgets.Callout\nimport ovh.plrapps.mapcompose.ui.paths.PathDataBuilder\nimport ovh.plrapps.mapcompose.ui.paths.model.PatternItem\nimport ovh.plrapps.mapcompose.ui.paths.model.PatternItem.Dash\nimport ovh.plrapps.mapcompose.ui.paths.model.PatternItem.Gap\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\n/**\n * In this sample, we add \"tracks\" to the map. The tracks are rendered as paths using MapCompose.\n */\nclass PathsVM(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider = makeTileStreamProvider(application.applicationContext)\n\n    val state = MapState(4, 8192, 8192).apply {\n        addLayer(tileStreamProvider)\n        shouldLoopScale = true\n        enableRotation()\n\n        /**\n         * Demonstrates path click.\n         */\n        onPathClick { id, x, y ->\n            var shouldAnimate by mutableStateOf(true)\n            addCallout(\n                id, x, y,\n                absoluteOffset = DpOffset(0.dp, (-10).dp),\n            ) {\n                Callout(x, y, title = \"Click on $id\", shouldAnimate) {\n                    shouldAnimate = false\n                }\n            }\n        }\n\n        /**\n         * Demonstrates path long-press.\n         */\n        onPathLongPress { id, x, y ->\n            var shouldAnimate by mutableStateOf(true)\n            addCallout(\n                id, x, y,\n                absoluteOffset = DpOffset(0.dp, (-10).dp),\n            ) {\n                Callout(x, y, title = \"Long-press on $id\", shouldAnimate) {\n                    shouldAnimate = false\n                }\n            }\n        }\n\n        viewModelScope.launch {\n            scrollTo(0.72, 0.3)\n        }\n    }\n\n\n    init {\n        /* Add tracks */\n        addTrack(\"track1\", Color(0xFF448AFF))\n        addTrack(\"track2\", Color(0xFFFFFF00))\n        addTrack(\"track3\", pattern = listOf(Dash(8.dp), Gap(4.dp)))\n\n        // filled polygon\n        with(state) {\n            addPath(\n                id = \"filled polygon\",\n                color = Color.Green,\n                fillColor = Color.Green.copy(alpha = .6f),\n            ) {\n                // Pentagon\n                addPoint(0.2009, 0.17878)\n                addPoint(0.08909, 0.2151)\n                addPoint(0.01999, 0.12)\n                addPoint(0.08909, 0.02489)\n                addPoint(0.2009, 0.06122)\n            }\n        }\n    }\n\n    /**\n     * In this sample, we retrieve track points from text files in the assets.\n     * To add a path, use the [addPath] api. From inside the builder block, you can add individual\n     * points or a list of points.\n     * Here, since we're getting points from a sequence, we add them on the fly using [PathDataBuilder.addPoint].\n     */\n    private fun addTrack(\n        trackName: String,\n        color: Color? = null,\n        pattern: List<PatternItem>? = null,\n        clickable: Boolean = true\n    ) {\n        with(state) {\n            val lines = getApplication<Application>().applicationContext.assets?.open(\n                \"tracks/$trackName.txt\"\n            )?.bufferedReader()?.lineSequence()\n                ?: return@with\n\n            addPath(\n                id = trackName, color = color, clickable = clickable, pattern = pattern, offset = 1\n            ) {\n                for (line in lines) {\n                    val values = line.split(',').map(String::toDouble)\n                    addPoint(values[0], values[1])\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/RotationVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport android.app.Application\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.launch\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.enableRotation\nimport ovh.plrapps.mapcompose.api.rotateTo\nimport ovh.plrapps.mapcompose.api.rotation\nimport ovh.plrapps.mapcompose.api.scale\nimport ovh.plrapps.mapcompose.api.scroll\nimport ovh.plrapps.mapcompose.api.setScrollOffsetRatio\nimport ovh.plrapps.mapcompose.api.setStateChangeListener\nimport ovh.plrapps.mapcompose.demo.providers.makeTileStreamProvider\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\nclass RotationVM(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider = makeTileStreamProvider(application.applicationContext)\n\n    val state = MapState(4, 8192, 8192).apply {\n        addLayer(tileStreamProvider)\n        enableRotation()\n        setScrollOffsetRatio(0.3f, 0.3f)\n        scale = 0.0\n\n        /* Not useful here, just showing how this API works */\n        setStateChangeListener {\n            println(\"scale: $scale, scroll: $scroll, rotation: $rotation\")\n        }\n    }\n\n    fun onRotate() {\n        viewModelScope.launch {\n            state.rotateTo(state.rotation + 90f)\n        }\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/SimpleDemoVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport android.app.Application\nimport androidx.lifecycle.AndroidViewModel\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.enableRotation\nimport ovh.plrapps.mapcompose.api.shouldLoopScale\nimport ovh.plrapps.mapcompose.demo.providers.makeTileStreamProvider\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\nclass SimpleDemoVM(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider = makeTileStreamProvider(application.applicationContext)\n\n    val state = MapState(4, 8448, 8448) {\n        scale(1.2)\n    }.apply {\n        addLayer(tileStreamProvider)\n        shouldLoopScale = true\n        enableRotation()\n    }\n}"
  },
  {
    "path": "demo/src/main/java/ovh/plrapps/mapcompose/demo/viewmodels/VisibleAreaPaddingVM.kt",
    "content": "package ovh.plrapps.mapcompose.demo.viewmodels\n\nimport android.app.Application\nimport androidx.lifecycle.AndroidViewModel\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.addMarker\nimport ovh.plrapps.mapcompose.api.enableRotation\nimport ovh.plrapps.mapcompose.demo.providers.makeTileStreamProvider\nimport ovh.plrapps.mapcompose.demo.ui.widgets.Marker\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\nclass VisibleAreaPaddingVM(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider = makeTileStreamProvider(application.applicationContext)\n\n    val state = MapState(4, 8192, 8192) {\n        scale(1.2)\n    }.apply {\n        enableRotation()\n        addLayer(tileStreamProvider)\n        addMarker(\"m0\", 0.5, 0.5) { Marker() }\n    }\n}\n"
  },
  {
    "path": "demo/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#3DDC84\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "demo/src/main/res/drawable/map_marker.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:height=\"24dp\"\n        android:width=\"24dp\"\n        android:viewportWidth=\"24\"\n        android:viewportHeight=\"24\">\n    <path android:fillColor=\"#2196f3\" android:pathData=\"M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z\" />\n</vector>"
  },
  {
    "path": "demo/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path android:pathData=\"M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"85.84757\"\n                android:endY=\"92.4963\"\n                android:startX=\"42.9492\"\n                android:startY=\"49.59793\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\" />\n</vector>"
  },
  {
    "path": "demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "demo/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"purple_200\">#FFBB86FC</color>\n    <color name=\"purple_500\">#FF6200EE</color>\n    <color name=\"purple_700\">#FF3700B3</color>\n    <color name=\"teal_200\">#FF03DAC5</color>\n    <color name=\"teal_700\">#FF018786</color>\n    <color name=\"black\">#FF000000</color>\n    <color name=\"white\">#FFFFFFFF</color>\n</resources>"
  },
  {
    "path": "demo/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">MapCompose Demo</string>\n</resources>"
  },
  {
    "path": "demo/src/main/res/values/themes.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"Theme.MyApplication\" parent=\"android:Theme.Material.Light.NoActionBar\" />\n</resources>"
  },
  {
    "path": "demo/src/test/java/ovh/plrapps/mapcompose/demo/utils/WebMercatorTest.kt",
    "content": "package ovh.plrapps.mapcompose.demo.utils\n\nimport org.junit.Assert.assertEquals\nimport org.junit.Test\n\n\nclass WebMercatorTest {\n    @Test\n    fun latLonToNormalizedTest() {\n        val lat = 48.856667\n        val lon = 2.351667\n        val parisNormalized = latLonToNormalized(lat, lon)\n\n        val parisLonLat = normalizedToLatLon(parisNormalized.first, parisNormalized.second)\n\n        assertEquals(lat, parisLonLat.first, 0.00001)\n        assertEquals(lon, parisLonLat.second, 0.00001)\n    }\n}"
  },
  {
    "path": "doc/mapcompose/MapCompose.drawio",
    "content": "<mxfile modified=\"2021-06-28T06:43:05.033Z\" host=\"app.diagrams.net\" agent=\"5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36\" etag=\"EQ9tXuhvoO4qK45lQe5w\" version=\"14.8.1\" type=\"google\"><diagram name=\"Page-1\" id=\"b33cb84f-bf7a-9ccf-f142-242d16432e5d\">7V1rc5s4F/41/hgPkpAQH3Pt7jt76Uy7k+1HbGSbKQEvJo7TX/+KizC62BYO2E7TdKYxAoQ5z7nrHGWEbp82n7JgufgzDVk8gk64GaG7EYTA9zz+qxh5rUccD1Uj8ywK67HtwJfoBxMX1qPPUchW0oV5msZ5tJQHp2mSsGkujQVZlr7Il83SWH7qMpgzbeDLNIjF6Bhvxx+jMF/U44D42xO/sWi+qB9OIalOTILp93mWPif1E0cQzcqf6vRTIOaqX3W1CML0pTWE7kfoNkvTvPr0tLllcUFeQThxX/4qvu0I3Szyp5gfAP6xPP2w42ZgczN/uYwleftxu+Z7fly+/ID0Ifj99Z//km9f4Q2dXcFqlnUQP4uHQBLz+W5mKZ+2/Xjy33MqTlytSj645hcAvNxsT/JP8+L3n8Hyn9/FVPw7VbNV57T3KiFgYf1eL4soZ1+WwbQ4+8K5Vn7tWRTHt2mcZuW9KAwYnU35+CrP0u+sdYZMKZvMjISqSbtmWc42raGacJ9Y+sTy7JVfUp+lfg2lkBJaH7+0GE7AvWjxGqnHgprL583UW6T4hxqsDsCh4YD7kgc5E7NNsi1sA6DZBzZUxgY5WMfGPSU2dCBsvkYxuw2SdbCSILIHxDkNIIqwYOfcwgJdDZGbKH8Klp+5pTJxukJB/uK5TKaMcbCCSXlBQdVlGiV5+bXxzQjf8ZHgOU8rQMsbgjiaJ/xzzGbFVAU1I27EruvhPC1wWHFYomT+tTi4u3JrvqhtLnD7gccHMjwe1OHxDejAwdAhBnlREGBJeF04C/xoGgerVTSV8ai0vzD/0EA5TrDs9d8CLu4w1Iff6rvLg7tNjWV19Fofdbc3bBPlrQfxo29iZv55+5jiQDxllQdZrr9hOfwQxQelc5U+Z1O2h8igFgE+4Zzlh5UXCyW3S+emFrdgA7eIsYzFQR6tZRfOxEL1Ez4XgtRiVk9mVgIULqxevL6r7f0oEwFEnDEggLM24t+OCiMhjAbyxxRRz/GJ57nAR0h+TEU27TElvzdEeYMIeIdFoJMaV7k/DFaLxib3oEQAwDIwrkHJY6MaoXAoPaLb3dJipnHMAw8upm/zO/vXxAC5iuti61YiPBANgcl3Uch2gFAynzYxk6NpaYM3T8ofmdrQbZnPrKKBon2TNGE9GUcKZH2DPR0RAyCqWuoPEN110Y1jHPNom53FuSPIQCATy9IeKAQDHDggDCFzAZpNOHVM8etHYldB/YZd4ZnZFX9wQPClAWLhWfzUgAiLJgDxzg2I/8EB8S4MEGGsPiwgqg05OyDggwOi2pCzA/LLy5IB8Q1h2kkBMWX/PxIgqoScGxDXIkorVhqX9i/frJjWWeZRe83RmE1wqDuWs1pYTNWmC2pWTE+S3XUttHm31FYPC4D7QbTmQ6JT1xT6DkZbZJGtsU4OyNkwNd2lUde7vyb3ziDUbdMTnJJVLdTqOyTnpmHNsWFl9KQENmUHpJXRaUOR7eIn8n3HKdLL6npoK7OrLuF9Z1nCjEt7dmum1jAHq2VV1TKLNgXaKu6ysprRKZtObfhhQrGLzfzQPc8MlSVy19cZgQ6UszMyArYwCmewly4xeNontpciYXT59hJ3ZsRz20v8jhR8B+qeS53jPtcmLoecl2MvsXlF8ws3OYW+WwRJaeQUkuvVKiIum3IaMU5ArfzkKQrDeBdIsrZpI2OycFUBJbqZZ0EY8eeZzvVh04DjqNGOq8Pl+6eEy5Ta7Vj4BZabyk0x1H61MbdzYy6DFXqBG6hwu64e9JucmMHgJn1HtruV3DFCpqLTewkIVmpzXIP87bDvg0GiW6TrA6Ufx+CCCPJRaMaFUgPt+yC36sRjH1nxP4JDEVuP5h7TjEdexauWhZXvn+hYKXdwHXpmolsUSw4eObmiKqWp8PV1yXdOqot7L6AbKmpqALRmQh6lEughj7gQU4yFJ9v2csCYez8ehhQDj1tKeErKv6McZAfat+V7jAh2gM+dM9chVGTeTkNdi2qBd0jdpjhjTJDrU8enlJszoOcHzkl74d5clqo1eL2nVbXeu1nQaQD8SVStqAN4D8qgA+0vRdzfUXKwO2dfuKq1yCROn7N1Q9Fu/UAmDLb9QKNWN1CrOWhHP9C2u8cvo46mv2eMPDja1+Ozay1o99pPcftnlkWcyEWu5m7vetCFdOwAhP2xw/kNkvIzlM0XZzMPUoAxQpwZRYKzczePC+jYAS4mCDjE87FcU4gB/wp8mADAWdqlSrfEjmYezlDBa+uyuqdvz5uqjY51bedWEqopt3LRQ6uQp0fdv3LCO5wll+rR+kkzwp4erf8CawuWmsAnBn/rpHBZZBF6NkOq84sZDY0uAoUTVBbdNYar6Vy1M1w7MTvYEiryupdiYUrl7wAMAeU6ntsRmYuwM4bE91wAkMf1v6L8OzSMumPPI8T3K2smN4ZhxP0l6CBKuVeFgTuYiVHsZ10eMKyJsUk6bDm/ruu0ZnswkrqxHQcrjAzhIR+sOFIdI+GYAckpEy3YVi6ZhfRZNmpz8tRHbxM9cmHOHXYKz8r3iOdyKSSOkpzgoudAxH9TzrlY7VK1Fj3MRY9g7rxBzy+epgi464/FE7iFF8XZJ2rVphbpjzcKxxGBwpE2oQlm5ECG0L1SYwpL+pWLg9sZXJxcEG+83VuAuIpccIOF3db+AsfKBeDyJR6BRQzfDnpaJ8FpxUKP4JvFQH0nq06d9xrbQ5/AhwddUCDwyQMyCVQ6nBvbLMPuqSH0TLkUbzeTvW33oT3LsvsytT8dElhYp7MhoUd/DRJ/fSgkDP05p0XCFNh1rMyipi25xuPx8bVYxvaoNn710JDRfx9wE2VHqcIN1EuBTFuwDRbJU5tV6TftKSVFMVdFGINk34vAQ1tL7Q1jnLHX3j5KRDVmj0xKSvS4Q+JBP6wm8+Fdpchl+WsOUjZBO3pfKUcpyyLqLnZ9pQKA8pUpkPb7HCYVQPVUQLVzHh97iLnsdNVybbbbxaiXru+AL7v3nqPvgXPSSlRhXg3mLYzWRutW4HRVE7owb+W+hJp5s+nOYqT4t/ve1TJIxNj9psh5z4tKZsFGJHgqUEsmq2XrJk6G9n2muSYsf2GMHzpPQVT8yhcZC8KCvknx/4twtaYpZ4k84qYU3pYg5Qt+XCijzTKOplFp/l+T6SJLk+gHVytpcuBraKa+GS7JfaQDUNQ4lj+6aDTjgy4EyB5h88x6V8o/yp0r70hfQqQkVD1k2rbV0HlFetj7bDlJ/maf/P9h/xpErwGk0fWPK5tulgMuQyut6WIlrdksOh/lDUgJGrg/q7nPG7BIJh3vDRDbtKboHLoUdwAqpexYrVC3XzWA8kwuHsgdaDZcbPIO/boDRhGx2N3roIiYd0etxKNTynFX0hMeynp2FwlJoo6XD9F/+M42YQWOqwV4x3rLu/aG7ls81K/s0hOIRw8bGe/aprgwKG8xIMOuih0vErYBJLp0kfB7EwnXTiT6YlqLmgdbnX4mJrsUnoBajyJGxy71aN1e6kw9csWG/Lter8P14+fbT/P8j8e//7q+6rbK2cHS709k6Tk3I8TH6Rpoq2vOyUVU8esQOlKxUGWHbm03j4E5CF4KB+00qQfsqT3n2flz5+Iov6iXkHlKjaFteUqfylXZ82iu4ofbP6VUXb79k1Xo/v8=</diagram></mxfile>"
  },
  {
    "path": "gradle/gradle-daemon-jvm.properties",
    "content": "#This file is generated by updateDaemonJvm\ntoolchainUrl.FREE_BSD.AARCH64=https\\://api.foojay.io/disco/v3.0/ids/29ee363f71d060405f729a8f1b7f7aef/redirect\ntoolchainUrl.FREE_BSD.X86_64=https\\://api.foojay.io/disco/v3.0/ids/67a0fee3c4236b6397dcbe8575ca2011/redirect\ntoolchainUrl.LINUX.AARCH64=https\\://api.foojay.io/disco/v3.0/ids/29ee363f71d060405f729a8f1b7f7aef/redirect\ntoolchainUrl.LINUX.X86_64=https\\://api.foojay.io/disco/v3.0/ids/67a0fee3c4236b6397dcbe8575ca2011/redirect\ntoolchainUrl.MAC_OS.AARCH64=https\\://api.foojay.io/disco/v3.0/ids/0b98aec810298c2c1d7fdac5dac37910/redirect\ntoolchainUrl.MAC_OS.X86_64=https\\://api.foojay.io/disco/v3.0/ids/658299a896470fbb3103ba3a430ee227/redirect\ntoolchainUrl.UNIX.AARCH64=https\\://api.foojay.io/disco/v3.0/ids/29ee363f71d060405f729a8f1b7f7aef/redirect\ntoolchainUrl.UNIX.X86_64=https\\://api.foojay.io/disco/v3.0/ids/67a0fee3c4236b6397dcbe8575ca2011/redirect\ntoolchainUrl.WINDOWS.AARCH64=https\\://api.foojay.io/disco/v3.0/ids/23adb857f3cb3cbe28750bc7faa7abc0/redirect\ntoolchainUrl.WINDOWS.X86_64=https\\://api.foojay.io/disco/v3.0/ids/932015f6361ccaead0c6d9b8717ed96e/redirect\ntoolchainVendor=JETBRAINS\ntoolchainVersion=21\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Wed Apr 21 20:01:30 CEST 2021\ndistributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.4.1-bin.zip\ndistributionPath=wrapper/dists\nzipStorePath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx8096M -Dfile.encoding=UTF-8\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app\"s APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Automatically convert third-party libraries to use AndroidX\nandroid.enableJetifier=false\n# Kotlin code style for this project: \"official\" or \"obsolete\":\nkotlin.code.style=official\nandroid.nonTransitiveRClass=false\nandroid.nonFinalResIds=false\nandroid.defaults.buildfeatures.resvalues=true\nandroid.sdk.defaultTargetSdkToCompileSdkIfUnset=false\nandroid.enableAppCompileTimeRClass=false\nandroid.usesSdkInManifest.disallowed=false\nandroid.uniquePackageNames=false\nandroid.dependency.useConstraints=true\nandroid.r8.strictFullModeForKeepRules=false\nandroid.r8.optimizedResourceShrinking=false\nandroid.builtInKotlin=false\nandroid.newDsl=false"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windows variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "mapcompose/build.gradle",
    "content": "import com.vanniktech.maven.publish.AndroidSingleVariantLibrary\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    id 'com.android.library'\n    id 'kotlin-android'\n    id \"org.jetbrains.kotlin.plugin.compose\" version \"$kotlin_version\"\n    id \"com.vanniktech.maven.publish\" version \"0.36.0\"\n}\n\nandroid {\n    compileSdk = 36\n\n    defaultConfig {\n        minSdk = 21\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        vectorDrawables {\n            useSupportLibrary = true\n        }\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n    buildFeatures {\n        compose = true\n        buildConfig = true\n    }\n\n    namespace = 'ovh.plrapps.mapcompose'\n    lint {\n        targetSdk 36\n    }\n    testOptions {\n        targetSdk 36\n    }\n}\n\nkotlin {\n    compilerOptions {\n        freeCompilerArgs.addAll([\n                '-Xopt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi',\n                '-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi',\n        ])\n        jvmTarget = JvmTarget.JVM_17\n    }\n}\n\ndependencies {\n    // Compose - See https://developer.android.com/jetpack/compose/setup#bom-version-mapping\n    api platform('androidx.compose:compose-bom:2026.04.01')\n    api \"androidx.compose.foundation:foundation\"\n    implementation \"androidx.compose.ui:ui-tooling-preview\"\n    debugImplementation \"androidx.compose.ui:ui-tooling\"\n    implementation \"androidx.compose.ui:ui-util\"\n    implementation \"androidx.compose.ui:ui-unit\"\n\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version\"\n    testImplementation 'junit:junit:4.13.2'\n    testImplementation \"org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutine_version\"\n    testImplementation 'org.robolectric:robolectric:4.16'\n    androidTestImplementation 'androidx.test.ext:junit:1.3.0'\n    androidTestImplementation \"androidx.compose.ui:ui-test-junit4\"\n}\n\next.'signing.keyId' = System.getenv('signingKeyId')\next.'signing.password' = System.getenv('signingPwd')\next.'signing.secretKeyRingFile' = System.getenv('signingKeyFile')\n\nmavenPublishing {\n    configure(new AndroidSingleVariantLibrary(\"release\", true, true))\n\n    coordinates(GROUP, ARTIFACT_ID, VERSION_NAME)\n    pom {\n        name = POM_NAME\n        description = POM_DESCRIPTION\n        url = POM_URL\n        scm {\n            url = POM_SCM_URL\n            connection = POM_SCM_CONNECTION\n            developerConnection = POM_SCM_DEV_CONNECTION\n        }\n\n        licenses {\n            license {\n                name = POM_LICENCE_NAME\n                url = POM_LICENCE_URL\n                distribution = POM_LICENCE_DIST\n            }\n        }\n\n        developers {\n            developer {\n                id = POM_DEVELOPER_ID\n                name = POM_DEVELOPER_NAME\n                url = POM_DEVELOPER_URL\n            }\n        }\n    }\n\n    publishToMavenCentral(false)\n    signAllPublications()\n}\n"
  },
  {
    "path": "mapcompose/gradle.properties",
    "content": "GROUP=ovh.plrapps\nVERSION_NAME=3.2.7\nARTIFACT_ID=mapcompose\n\nPOM_NAME=MapCompose\nPOM_ARTIFACT_ID=library\nPOM_DESCRIPTION=A Jetpack Compose Android library to display tiled maps, with support for markers, paths, and rotation\nPOM_PACKAGING=aar\nPOM_INCEPTION_YEAR=2021\n\nPOM_URL=https://github.com/p-lr/MapCompose\nPOM_SCM_URL=https://github.com/p-lr/MapCompose\nPOM_SCM_CONNECTION=scm:git@github.com:p-lr/MapCompose.git\nPOM_SCM_DEV_CONNECTION=scm:git@github.com:p-lr/MapCompose.git\n\nPOM_LICENCE_NAME=The Apache Software License, Version 2.0\nPOM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt\nPOM_LICENCE_DIST=repo\n\nPOM_DEVELOPER_ID=p-lr\nPOM_DEVELOPER_NAME=Pierre Laurence\nPOM_DEVELOPER_URL=https://github.com/p-lr/"
  },
  {
    "path": "mapcompose/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" />\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/api/Annotation.kt",
    "content": "package ovh.plrapps.mapcompose.api\n\n@RequiresOptIn(message = \"This API is experimental. It is likely to change before becoming stable.\")\n@Retention(AnnotationRetention.BINARY)\n@Target(\n    AnnotationTarget.CLASS,\n    AnnotationTarget.FUNCTION,\n    AnnotationTarget.PROPERTY,\n    AnnotationTarget.PROPERTY_GETTER,\n)\nannotation class ExperimentalClusteringApi"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/api/ApiDefaults.kt",
    "content": "package ovh.plrapps.mapcompose.api\n\ninternal const val maxAnimationsRetries = 6\ninternal const val animationsRetriesInterval = 10L"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/api/Common.kt",
    "content": "package ovh.plrapps.mapcompose.api\n\nimport androidx.compose.ui.unit.IntOffset\nimport ovh.plrapps.mapcompose.ui.state.VisibleAreaPadding\nimport ovh.plrapps.mapcompose.utils.AngleDegree\nimport ovh.plrapps.mapcompose.utils.rotateX\nimport ovh.plrapps.mapcompose.utils.rotateY\nimport ovh.plrapps.mapcompose.utils.toRad\n\n/**\n * When scrolling to a given position, the viewport needs to be offset by taking into account the\n * [VisibleAreaPadding]. This is needed for apis when scrolling is involved.\n */\ninternal fun VisibleAreaPadding.getOffsetForScroll(rotation: AngleDegree): IntOffset {\n    val angle = -rotation.toRad()\n    val offsetX = rotateX((left - right) / 2.0, (top - bottom) / 2.0, angle)\n    val offsetY = rotateY((left - right) / 2.0, (top - bottom) / 2.0, angle)\n    return IntOffset(offsetX.toInt(), offsetY.toInt())\n}\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/api/DefaultCanvas.kt",
    "content": "package ovh.plrapps.mapcompose.api\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.graphics.drawscope.scale\nimport androidx.compose.ui.graphics.drawscope.withTransform\nimport ovh.plrapps.mapcompose.ui.MapUI\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\n/**\n * A custom canvas which moves, scales, and rotates along with the map (exactly like some internal\n * components of [MapUI]).\n * It's an example which can be used in a custom composable (as it takes a [drawBlock] as input).\n *\n * This implementation only uses the public API. Therefore, different implementations are possible.\n * However, this implementation should fit most needs for custom drawings inside [MapUI].\n */\n@Composable\nfun DefaultCanvas(\n    modifier: Modifier,\n    mapState: MapState,\n    drawBlock: DrawScope.() -> Unit\n) {\n    Canvas(\n        modifier = modifier\n    ) {\n        withTransform({\n            /* Geometric transformations seem to be applied in reversed order of declaration */\n            translate(left = -mapState.scroll.x.toFloat(), top = -mapState.scroll.y.toFloat())\n            rotate(\n                degrees = mapState.rotation,\n                pivot = Offset(\n                    x = (mapState.centroidX * mapState.fullSize.width * mapState.scale).toFloat(),\n                    y = (mapState.centroidY.toFloat() * mapState.fullSize.height * mapState.scale).toFloat()\n                )\n            )\n            scale(scale = mapState.scale.toFloat(), Offset.Zero)\n        }, drawBlock)\n    }\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/api/GesturesApi.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage ovh.plrapps.mapcompose.api\n\nimport androidx.compose.ui.platform.ViewConfiguration\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\n\n/**\n * Enable rotation by user gestures.\n */\nfun MapState.enableRotation() {\n    zoomPanRotateState.isRotationEnabled = true\n}\n\n/**\n * Enable scrolling by user gestures. This is enabled by default.\n */\nfun MapState.enableScrolling() {\n    zoomPanRotateState.isScrollingEnabled = true\n}\n\n/**\n * Enable zooming by user gestures. This is enabled by default.\n */\nfun MapState.enableZooming() {\n    zoomPanRotateState.isZoomingEnabled = true\n}\n\n/**\n * Discard rotation gestures. The map can still be programmatically rotated using APIs such as\n * [rotateTo] or [rotation].\n */\nfun MapState.disableRotation() {\n    zoomPanRotateState.isRotationEnabled = false\n}\n\n/**\n * Discard scrolling gestures. The map can still be programmatically scrolled using APIs such as\n * [scrollTo] or [snapScrollTo].\n */\nfun MapState.disableScrolling() {\n    zoomPanRotateState.isScrollingEnabled = false\n}\n\n/**\n * Discard zooming gestures. The map can still be programmatically zoomed using [scale].\n */\nfun MapState.disableZooming() {\n    zoomPanRotateState.isZoomingEnabled = false\n}\n\n/**\n * Disable gesture detection. The map view can still be transformed programmatically.\n */\nfun MapState.disableGestures() {\n    with (zoomPanRotateState) {\n        isRotationEnabled = false\n        isScrollingEnabled = false\n        isZoomingEnabled = false\n    }\n}\n\n/**\n * Enables fling scale animation after a pinch to zoom gesture. Enabled by default.\n */\nfun MapState.enableFlingZoom() {\n    zoomPanRotateState.isFlingZoomEnabled = true\n}\n\n/**\n * Disables fling scale animation after a pinch to zoom gesture.\n */\nfun MapState.disableFlingZoom() {\n    zoomPanRotateState.isFlingZoomEnabled = false\n}\n\n/**\n * Registers a tap callback for tap gestures. The callback is invoked with the relative coordinates\n * of the tapped point on the map.\n * Note: the tap gesture is detected only after the [ViewConfiguration.doubleTapMinTimeMillis] has\n * passed, because the layout's gesture detector also detects double-tap gestures.\n */\nfun MapState.onTap(tapCb: (x: Double, y: Double) -> Unit) {\n    this.tapCb = tapCb\n}\n\n/**\n * Registers a callback for long press gestures. The callback is invoked with the relative coordinates\n * of the pressed point on the map.\n */\nfun MapState.onLongPress(longPressCb: (x: Double, y: Double) -> Unit) {\n    this.longPressCb = longPressCb\n}\n\n/**\n * Registers a callback for touch down event.\n */\nfun MapState.onTouchDown(cb: () -> Unit) {\n    touchDownCb = cb\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/api/LayerApi.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage ovh.plrapps.mapcompose.api\n\nimport ovh.plrapps.mapcompose.core.*\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport java.util.*\n\n\n/**\n * Add a layer. By default, the layer is added on top of the layer stack (see [AboveAll]).\n * Optionally, the layer can be added at the bottom of the stack, or above / below an existing layer.\n *\n * Note that [initialOpacity] is taken into account _only_ if the layer being added isn't the lowest\n * one, or the only one. However, if later on another layer is added below this layer, the\n * [initialOpacity] will be taken into account.\n *\n * @return The id of the created layer\n */\nfun MapState.addLayer(\n    tileStreamProvider: TileStreamProvider,\n    initialOpacity: Float = 1f,\n    placement: LayerPlacement = AboveAll\n): String {\n    val layers = tileCanvasState.layerFlow.value.toMutableList()\n    val id = makeLayerId()\n    val layer = Layer(id, tileStreamProvider, initialOpacity)\n\n    val newLayers = when (placement) {\n        AboveAll -> {\n            layers + layer\n        }\n        is AboveLayer -> {\n            val existingLayerIndex = layers.indexOfFirst { it.id == placement.layerId }\n            if (existingLayerIndex != -1 && existingLayerIndex < layers.lastIndex) {\n                layers.add(existingLayerIndex + 1, layer)\n            }\n            layers\n        }\n        BelowAll -> {\n            layers.add(0, layer)\n            layers\n        }\n        is BelowLayer -> {\n            val existingLayerIndex = layers.indexOfFirst { it.id == placement.layerId }\n            if (existingLayerIndex != -1) {\n                layers.add(existingLayerIndex, layer)\n            }\n            layers\n        }\n    }\n\n    setLayers(newLayers)\n\n    return id\n}\n\n/**\n * Replaces a layer. If the layer doesn't exist, no layer is added.\n *\n * @return The id of the added layer, or null if [layerId] doesn't match with any existing layer\n */\nfun MapState.replaceLayer(\n    layerId: String,\n    tileStreamProvider: TileStreamProvider,\n    initialOpacity: Float = 1f\n): String? {\n    val layers = tileCanvasState.layerFlow.value.toMutableList()\n\n    val index = layers.indexOfFirst {\n        it.id == layerId\n    }\n\n    val id = makeLayerId()\n\n    return if (index != -1) {\n        layers[index] = Layer(id, tileStreamProvider, initialOpacity)\n        setLayers(layers)\n        id\n    } else null\n}\n\n/**\n * Moves a layer up in the layer stack, making it drawn on top of the layer which was previously\n * above it.\n */\nfun MapState.moveLayerUp(layerId: String) {\n    val layers = tileCanvasState.layerFlow.value.toMutableList()\n\n    val index = layers.indexOfFirst {\n        it.id == layerId\n    }\n\n    if (index < layers.lastIndex) {\n        Collections.swap(layers, index + 1, index)\n        setLayers(layers)\n    }\n}\n\n/**\n * Moves a layer down in the layer stack, making it drawn below the layer which was previously\n * below it.\n */\nfun MapState.moveLayerDown(layerId: String) {\n    val layers = tileCanvasState.layerFlow.value.toMutableList()\n\n    val index = layers.indexOfFirst {\n        it.id == layerId\n    }\n\n    if (index > 0) {\n        Collections.swap(layers, index - 1, index)\n        setLayers(layers)\n    }\n}\n\n/**\n * Remove the top layer from the stack.\n */\nfun MapState.removeLastLayer() {\n    val layers = tileCanvasState.layerFlow.value.toMutableList()\n    val remainingLayers = layers.subList(0, layers.size - 1)\n    setLayers(remainingLayers)\n}\n\n/**\n * Remove the top [n] layers from the stack.\n * @param n The number of layers to remove.\n */\nfun MapState.removeLastLayers(n: Int) {\n    val layers = tileCanvasState.layerFlow.value.toMutableList()\n    val remainingLayers = layers.subList(0, (layers.size - n).coerceAtLeast(0))\n    setLayers(remainingLayers)\n}\n\n/**\n * Reorder layers in the order of the provided list of ids. Layers listed first will be drawn before\n * subsequent layers (so the later will be above).\n * Existing layers not included in the provided list will be removed\n */\nfun MapState.reorderLayers(layerIds: List<String>) {\n    val layerForId = tileCanvasState.layerFlow.value.associateBy { it.id }\n    val layers = layerIds.mapNotNull { layerForId[it] }\n\n    setLayers(layers)\n}\n\n/**\n * Remove all layers.\n */\nfun MapState.removeAllLayers() {\n    setLayers(emptyList())\n}\n\n/**\n * Remove some layers.\n */\nfun MapState.removeLayers(layerIds: List<String>) {\n    val remainingLayers = tileCanvasState.layerFlow.value.filterNot {\n        it.id in layerIds\n    }\n    setLayers(remainingLayers)\n}\n\n/**\n * Remove a layer.\n */\nfun MapState.removeLayer(layerId: String) {\n    val remainingLayers = tileCanvasState.layerFlow.value.filterNot {\n        it.id == layerId\n    }\n    setLayers(remainingLayers)\n}\n\n/**\n * Dynamically update the opacity of a layer. If the layer is the lowest one or the only one, the\n * new opacity won't have effect until a layer is added below it.\n */\nfun MapState.setLayerOpacity(layerId: String, opacity: Float) {\n    val newLayers = tileCanvasState.layerFlow.value.map {\n        if (it.id == layerId) {\n            it.copy(alpha = opacity.coerceIn(0f..1f))\n        } else it\n    }\n    setLayers(newLayers)\n}\n\n/**\n * Define the list of layers using a builder.\n *\n * @return The list of layer ids, in the order of addition.\n */\nfun MapState.buildLayers(builder: LayersBuilder.() -> Unit): List<String> {\n    val builderInternal = LayersBuilderInternal()\n    builderInternal.apply(builder)\n    setLayers(builderInternal.layers)\n\n    return builderInternal.layers.map { it.id }\n}\n\ninterface LayersBuilder {\n    fun addLayer(tileStreamProvider: TileStreamProvider, initialOpacity: Float = 1f)\n}\n\n/**\n * Utility function to automatically refresh tiles after a change of layers.\n */\nprivate fun MapState.setLayers(layers: List<Layer>) {\n    tileCanvasState.setLayers(layers)\n    renderVisibleTilesThrottled()\n}\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/api/LayoutApi.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage ovh.plrapps.mapcompose.api\n\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.SpringSpec\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport ovh.plrapps.mapcompose.ui.layout.Fill\nimport ovh.plrapps.mapcompose.ui.layout.Fit\nimport ovh.plrapps.mapcompose.ui.layout.Forced\nimport ovh.plrapps.mapcompose.ui.layout.MinimumScaleMode\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport ovh.plrapps.mapcompose.ui.state.VisibleAreaPadding\nimport ovh.plrapps.mapcompose.ui.state.ZoomPanRotateState\nimport ovh.plrapps.mapcompose.utils.*\nimport kotlin.math.min\nimport kotlin.math.roundToInt\n\n/**\n * The scale of the map. By convention, the scale at full dimension is 1.0.\n */\nvar MapState.scale: Double\n    get() = zoomPanRotateState.scale\n    set(value) {\n        zoomPanRotateState.setScale(value)\n    }\n\n/**\n * The [rotation] property is the angle (in decimal degrees) of rotation,\n * using the center of the view as the pivot point.\n */\nvar MapState.rotation: AngleDegree\n    get() = zoomPanRotateState.rotation\n    set(value) {\n        zoomPanRotateState.setRotation(value)\n    }\n\n/**\n * Get the current [scroll] - the position of the top-left corner of the visible viewport.\n * This is a low-level concept (returned value is in scaled pixels).\n */\nval MapState.scroll: Scroll\n    get() = Scroll(zoomPanRotateState.scrollX, zoomPanRotateState.scrollY)\n\n\n/**\n * Set the [scroll] - the position of the top-left corner of the visible viewport. This is a\n * suspending call because it's required to wait the first composition. Otherwise, it's invoked\n * immediately.\n * This is a low-level concept (input value is expected to be in scaled pixels). To scroll to a\n * known position, prefer the [snapScrollTo] API.\n */\nsuspend fun MapState.setScroll(scrollX: Double, scrollY: Double) {\n    with(zoomPanRotateState) {\n        awaitLayout()\n\n        setScroll(scrollX, scrollY)\n    }\n}\n\nfun MapState.referentialSnapshotFlow(): Flow<ReferentialSnapshot> = snapshotFlow {\n    ReferentialSnapshot(zoomPanRotateState.scale, scroll, zoomPanRotateState.rotation)\n}\n\ndata class ReferentialSnapshot(val scale: Double, val scroll: Scroll, val rotation: AngleDegree)\n\n/**\n * Get notified whenever the state ([scale] and/or [scroll] and/or [rotation]) changes.\n *\n * @param cb An extension function with [MapState] as receiver type\n */\nfun MapState.setStateChangeListener(cb: MapState.() -> Unit) {\n    stateChangeListener = cb\n}\n\n/**\n * Removes the state change listener.\n */\nfun MapState.removeStateChangeListener() {\n    stateChangeListener = null\n}\n\n/**\n * On double-tap, and if the scale is already at its maximum value, circle-back to the minimum scale.\n */\nvar MapState.shouldLoopScale\n    get() = zoomPanRotateState.shouldLoopScale\n    set(value) {\n        zoomPanRotateState.shouldLoopScale = value\n    }\n\n/**\n * Sets the padding of the visible area of the map viewport in [Dp], for the purpose of camera moves.\n * For example, if you have some UI obscuring the map on the left, you can set the appropriate\n * left padding. Then, when you use the scrollTo methods, the map will take that into account, by\n * centering on the visible portion of the viewport.\n */\nfun MapState.setVisibleAreaPadding(\n    left: Dp = 0.dp,\n    right: Dp = 0.dp,\n    top: Dp = 0.dp,\n    bottom: Dp = 0.dp\n) {\n    setVisibleAreaPadding(\n        left = dpToPx(left.value).roundToInt(),\n        right = dpToPx(right.value).roundToInt(),\n        top = dpToPx(top.value).roundToInt(),\n        bottom = dpToPx(bottom.value).roundToInt()\n    )\n}\n\n/**\n * Variant of [MapState.setVisibleAreaPadding] using ratios. This is a suspending call because it\n * awaits for the first layout.\n *\n * @param leftRatio The left padding will be equal to this ratio multiplied by the layout width.\n * @param rightRatio The right padding will be equal to this ratio multiplied by the layout width.\n * @param topRatio The top padding will be equal to this ratio multiplied by the layout height.\n * @param bottomRatio The bottom padding will be equal to this ratio multiplied by the layout height.\n */\nsuspend fun MapState.setVisibleAreaPadding(\n    leftRatio: Float = 0f,\n    rightRatio: Float = 0f,\n    topRatio: Float = 0f,\n    bottomRatio: Float = 0f\n) {\n    with(zoomPanRotateState) {\n        awaitLayout()\n        val layoutSize = zoomPanRotateState.layoutSize\n        setVisibleAreaPadding(\n            left = (leftRatio * layoutSize.width).roundToInt(),\n            right = (rightRatio * layoutSize.width).roundToInt(),\n            top = (topRatio * layoutSize.height).roundToInt(),\n            bottom = (bottomRatio * layoutSize.height).roundToInt()\n        )\n    }\n}\n\n/**\n * Variant of [MapState.setVisibleAreaPadding] using pixels.\n */\nfun MapState.setVisibleAreaPadding(left: Int = 0, right: Int = 0, top: Int = 0, bottom: Int = 0) {\n    zoomPanRotateState.visibleAreaPadding = VisibleAreaPadding(left, top, right, bottom)\n}\n\n/**\n * Set the minimum scale mode. See [MinimumScaleMode].\n * The minimum scale can be manually defined using [Forced], or can be inferred using [Fill], or\n * [Fit] (the default).\n * Note: When enabling map rotation, it's advised to use the [Fill] mode.\n */\nvar MapState.minimumScaleMode: MinimumScaleMode\n    get() = zoomPanRotateState.minimumScaleMode\n    set(value) {\n        zoomPanRotateState.minimumScaleMode = value\n    }\n\n/**\n * Get the current minimum scale. The minimum scale changes on [minimumScaleMode] change.\n * Do note that the initial value is always 0.0. However, the value is updated after the first layout\n * pass. To observe minimum scale changes, use [MapState.minScaleSnapshotFlow] api.\n */\nval MapState.minScale: Double\n    get() = zoomPanRotateState.minScale\n\n/**\n * Get the minimum scale changes.\n */\nfun MapState.minScaleSnapshotFlow(): Flow<Double> {\n    return snapshotFlow {\n        zoomPanRotateState.minScale\n    }\n}\n\n/**\n * The default maximum scale is 2.0.\n * When changed, and if the current scale is greater than the new [maxScale], the current scale is\n * changed to be equal to [maxScale].\n */\nvar MapState.maxScale: Double\n    get() = zoomPanRotateState.maxScale\n    set(value) {\n        zoomPanRotateState.maxScale = value\n    }\n\n/**\n * The scroll offset ratio allows to scroll past the default scroll limits. They are expressed in\n * percent of the layout dimensions.\n * Setting a scroll offset ratio is useful when rotation is enabled, so that edges of the map are\n * reachable.\n * The recommended value to try it out is 0.5f\n * Values must be in [0f..1f] range, or an [IllegalArgumentException] is thrown.\n *\n * This parameter has no effect in x dimension when infinite scroll is enabled.\n *\n * @param xRatio The horizontal scroll offset ratio. The scroll offset will be equal to this ratio\n * multiplied by the layout width.\n * @param yRatio The vertical scroll offset ratio. The scroll offset will be equal to this ratio\n * multiplied by the layout height.\n */\nfun MapState.setScrollOffsetRatio(xRatio: Float, yRatio: Float) {\n    zoomPanRotateState.scrollOffsetRatio = Offset(xRatio, yRatio)\n}\n\n/**\n * Rotates to the specified [angle] in decimal degrees, animating the rotation.\n */\nsuspend fun MapState.rotateTo(\n    angle: AngleDegree,\n    animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow)\n) {\n    withRetry(maxAnimationsRetries, animationsRetriesInterval) {\n        zoomPanRotateState.smoothRotateTo(angle, animationSpec)\n    }\n}\n\n/**\n * Get the layout dimensions in pixels.\n * Note that layout dimension may change during the lifetime of the application. The returned value\n * is a read-only snapshot.\n */\nsuspend fun MapState.getLayoutSize(): IntSize {\n    return with(zoomPanRotateState) {\n        awaitLayout()\n        layoutSize\n    }\n}\n\n/**\n * Get the layout dimensions in pixels, as a [Flow].\n * This api is useful to observe layout changes.\n */\nsuspend fun MapState.getLayoutSizeFlow(): Flow<IntSize> {\n    return with(zoomPanRotateState) {\n        awaitLayout()\n        snapshotFlow {\n            layoutSize\n        }\n    }\n}\n\n/**\n * Scrolls to a position. Defaults to centering on the provided scroll destination.\n *\n * @param x The normalized X position on the map, in range [0..1]\n * @param y The normalized Y position on the map, in range [0..1]\n * @param screenOffset Offset of the screen relatively to its dimension. Default is\n * Offset(-0.5f, -0.5f), so moving the screen by half the width left and by half the height top,\n * effectively centering on the scroll destination.\n */\nsuspend fun MapState.snapScrollTo(\n    x: Double,\n    y: Double,\n    screenOffset: Offset = Offset(-0.5f, -0.5f)\n) {\n    with(zoomPanRotateState) {\n        awaitLayout()\n        val offsetX = screenOffset.x * layoutSize.width\n        val offsetY = screenOffset.y * layoutSize.height\n\n        val paddingOffset = visibleAreaPadding.getOffsetForScroll(rotation)\n        val destScrollX = x * fullWidth * scale + offsetX - paddingOffset.x\n        val destScrollY = y * fullHeight * scale + offsetY - paddingOffset.y\n\n        setScroll(destScrollX, destScrollY)\n    }\n}\n\n/**\n * Scrolls to a position, animating the scroll and the scale. Defaults to centering on the provided\n * scroll destination.\n *\n * @param x The normalized X position on the map, in range [0..1]\n * @param y The normalized Y position on the map, in range [0..1]\n * @param destScale The destination scale. The default value is the current scale.\n * @param animationSpec The [AnimationSpec]. Default is [SpringSpec] with low stiffness.\n * @param screenOffset Offset of the screen relatively to its dimension. Default is\n * Offset(-0.5f, -0.5f), so moving the screen by half the width left and by half the height top,\n * effectively centering on the scroll destination.\n */\nsuspend fun MapState.scrollTo(\n    x: Double,\n    y: Double,\n    destScale: Double = scale,\n    animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow),\n    screenOffset: Offset = Offset(-0.5f, -0.5f)\n) {\n    with(zoomPanRotateState) {\n        awaitLayout()\n        val offsetX = screenOffset.x * layoutSize.width\n        val offsetY = screenOffset.y * layoutSize.height\n\n        val effectiveDstScale = constrainScale(destScale)\n\n        val paddingOffset = visibleAreaPadding.getOffsetForScroll(rotation)\n        val destScrollX = x * fullWidth * effectiveDstScale + offsetX - paddingOffset.x\n        val destScrollY = y * fullHeight * effectiveDstScale + offsetY - paddingOffset.y\n\n        withRetry(maxAnimationsRetries, animationsRetriesInterval) {\n            smoothScrollScaleRotate(\n                destScrollX,\n                destScrollY,\n                effectiveDstScale,\n                animationSpec\n            )\n        }\n    }\n}\n\n/**\n * Scrolls to an area. The target position will be centered on the area, scaled in as much as\n * possible while still keeping the area plus the provided padding completely in view.\n *\n * @param area The [BoundingBox] of the target area to scroll to.\n * @param padding Padding around the area defined as a fraction of the viewport.\n */\nsuspend fun MapState.snapScrollTo(\n    area: BoundingBox,\n    padding: Offset = Offset(0f, 0f)\n) {\n    with(zoomPanRotateState) {\n        awaitLayout()\n        val (center, scale) = calculateScrollTo(area, padding)\n        setScale(scale)\n        snapScrollTo(center.x, center.y)\n    }\n}\n\n/**\n * Scrolls to an area, animating the scroll and the scale. The target position will be centered\n * on the area, scaled in as much as possible while still keeping the area plus the provided\n * padding completely in view.\n *\n * @param area The [BoundingBox] of the target area to scroll to.\n * @param padding Padding around the area defined as a fraction of the viewport.\n * @param animationSpec The [AnimationSpec]. Default is [SpringSpec] with low stiffness.\n */\nsuspend fun MapState.scrollTo(\n    area: BoundingBox,\n    padding: Offset = Offset(0f, 0f),\n    animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow),\n) {\n    with(zoomPanRotateState) {\n        awaitLayout()\n        val (center, scale) = calculateScrollTo(area, padding)\n        scrollTo(center.x, center.y, scale, animationSpec)\n    }\n}\n\n/**\n * Calculates the target scroll position and scale that will be centered on the given [area] and\n * scaled in as much as possible while keeping the [area] plus [padding] in view.\n *\n * @return The scroll position and scale, as a [Pair].\n */\nprivate fun ZoomPanRotateState.calculateScrollTo(\n    area: BoundingBox,\n    padding: Offset\n): Pair<Point, Double> {\n    val centerX = (area.xLeft + area.xRight) / 2\n    val centerY = (area.yTop + area.yBottom) / 2\n\n    val xAxisScale = fullHeight / fullWidth.toDouble()\n    val normalizedArea = area.scaleAxis(1 / xAxisScale)\n    val rotatedNormalizedArea =\n        normalizedArea.rotate(Point(centerX / xAxisScale, centerY), -rotation.toRad())\n    val rotatedArea = rotatedNormalizedArea.scaleAxis(xAxisScale)\n\n    val areaWidth = fullWidth * (rotatedArea.xRight - rotatedArea.xLeft)\n    val availableViewportWidth = (layoutSize.width - visibleAreaPadding.left - visibleAreaPadding.right) * (1 - padding.x)\n    val horizontalScale = availableViewportWidth / areaWidth\n\n    val areaHeight = fullHeight * (rotatedArea.yBottom - rotatedArea.yTop)\n    val availableViewportHeight = (layoutSize.height - visibleAreaPadding.top - visibleAreaPadding.bottom) * (1 - padding.y)\n    val verticalScale = availableViewportHeight / areaHeight\n\n    val targetScale = min(horizontalScale, verticalScale)\n    val effectiveTargetScale = constrainScale(targetScale)\n\n    return Point(centerX, centerY) to effectiveTargetScale\n}\n\n/**\n * The [centroidX] is the x coordinate of the center of the current viewport (which is also the\n * origin of rotation transformation). It changes with the scroll and the scale.\n * This is a low-level concept, and is only useful when defining custom views.\n * The value is a relative coordinate (in [0.0 .. 1.0] range).\n */\nval MapState.centroidX: Double\n    get() = zoomPanRotateState.centroidX\n\n/**\n * The [centroidY] is the y coordinate of the center of the current viewport (which is also the\n * origin of rotation transformation). It changes with the scroll and the scale.\n * This is a low-level concept, and is only useful when defining custom views.\n * The value is a relative coordinate (in [0.0 .. 1.0] range).\n */\nval MapState.centroidY: Double\n    get() = zoomPanRotateState.centroidY\n\n/**\n * Get the flow of centroid points. A centroid point contains the normalized coordinates of the\n * center of the map.\n * Useful for asynchronous processing using flow operators. Like every snapshot flow, it should be\n * collected from the main thread.\n *\n * Example:\n * ```\n * mapState.centroidSnapshotFlow().map { point ->\n *   withContext(Dispatchers.Default) {\n *     // some heavy computing\n *   }\n * }.launchIn(scope)  // scope is using Dispatchers.Main\n * ```\n */\nfun MapState.centroidSnapshotFlow(): Flow<Point> {\n    return snapshotFlow {\n        Point(zoomPanRotateState.centroidX, zoomPanRotateState.centroidY)\n    }\n}\n\n/**\n * A convenience property. It corresponds to the size used when creating the [MapState].\n */\nval MapState.fullSize: IntSize\n    get() = IntSize(zoomPanRotateState.fullWidth, zoomPanRotateState.fullHeight)\n\n/**\n * Returns the level, an entire value belonging to [0 ; levelCount - 1], where `levelCount` is the\n * count of levels passed at [MapState] constructor.\n */\nfun MapState.getLevelAtScale(scale: Double): Int {\n    return visibleTilesResolver.getLevel(scale)\n}\n\n/**\n * Stops all currently running animations. If other animations are scheduled to run (inside running\n * coroutines), you might have to cancel those coroutines as well.\n */\nsuspend fun MapState.stopAnimations() {\n    zoomPanRotateState.stopAnimations()\n}\n\n/**\n * Returns the visible area expressed in normalized coordinates. This does not account for rotation.\n * When the map isn't rotated, the obtained [BoundingBox] represents the same area as the one\n * obtained with the [visibleArea] API.\n */\nsuspend fun MapState.visibleBoundingBox(): BoundingBox {\n    return with(zoomPanRotateState) {\n        awaitLayout()\n\n        BoundingBox(\n            xLeft = centroidX - layoutSize.width / (2 * fullWidth * scale),\n            yTop = centroidY - layoutSize.height / (2 * fullHeight * scale),\n            xRight = centroidX + layoutSize.width / (2 * fullWidth * scale),\n            yBottom = centroidY + layoutSize.height / (2 * fullHeight * scale)\n        )\n    }\n}\n\ndata class BoundingBox(val xLeft: Double, val yTop: Double, val xRight: Double, val yBottom: Double)\n\n/**\n * Returns the visible area expressed in normalized coordinates. This **does** account for rotation.\n *\n * @return The [VisibleArea], as follows:\n * ```\n *    p1         p2\n *      ---------\n *      |       |\n *      |       |\n *      ---------\n *    p4         p3\n * ```\n */\nsuspend fun MapState.visibleArea(padding: IntOffset = IntOffset.Zero): VisibleArea {\n    return with(zoomPanRotateState) {\n        awaitLayout()\n\n        val xLeft = centroidX - (layoutSize.width + padding.x * 2) / (2 * fullWidth * scale)\n        val yTop = centroidY - (layoutSize.height + padding.y * 2) / (2 * fullHeight * scale)\n        val xRight = centroidX + (layoutSize.width + padding.x * 2) / (2 * fullWidth * scale)\n        val yBottom = centroidY + (layoutSize.height + padding.y * 2) / (2 * fullHeight * scale)\n\n        val xAxisScale = fullHeight / fullWidth.toDouble()\n        val scaledCenterX = centroidX / xAxisScale\n\n        val p1x = rotateCenteredX(\n            xLeft / xAxisScale, yTop, scaledCenterX, centroidY, -rotation.toRad()\n        ) * xAxisScale\n        val p1y = rotateCenteredY(\n            xLeft / xAxisScale, yTop, scaledCenterX, centroidY, -rotation.toRad()\n        )\n\n        val p2x = rotateCenteredX(\n            xRight / xAxisScale, yTop, scaledCenterX, centroidY, -rotation.toRad()\n        ) * xAxisScale\n        val p2y = rotateCenteredY(\n            xRight / xAxisScale, yTop, scaledCenterX, centroidY, -rotation.toRad()\n        )\n\n        val p3x = rotateCenteredX(\n            xRight / xAxisScale, yBottom, scaledCenterX, centroidY, -rotation.toRad()\n        ) * xAxisScale\n        val p3y = rotateCenteredY(\n            xRight / xAxisScale, yBottom, scaledCenterX, centroidY, -rotation.toRad()\n        )\n\n        val p4x = rotateCenteredX(\n            xLeft / xAxisScale, yBottom, scaledCenterX, centroidY, -rotation.toRad()\n        ) * xAxisScale\n        val p4y = rotateCenteredY(\n            xLeft / xAxisScale, yBottom, scaledCenterX, centroidY, -rotation.toRad()\n        )\n\n        visibleAreaMutex.withLock {\n            val area = visibleArea\n            if (area == null) {\n                visibleArea = VisibleArea(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y)\n            } else {\n                area._p1x = p1x\n                area._p1y = p1y\n                area._p2x = p2x\n                area._p2y = p2y\n                area._p3x = p3x\n                area._p3y = p3y\n                area._p4x = p4x\n                area._p4y = p4y\n            }\n            visibleArea as VisibleArea\n        }\n    }\n}\n\n/**\n * Returns the visible area expressed in normalized coordinates. This *does* account for rotation.\n */\nsuspend fun MapState.visibleAreaFlow(\n    padding: IntOffset = IntOffset.Zero,\n    throttleMillis: Long = 500\n): Flow<VisibleArea> {\n    return snapshotFlow {\n        centroidX.hashCode() + centroidY.hashCode() + scale.hashCode()\n    }.throttle(throttleMillis).map {\n        visibleArea(padding)\n    }\n}\n\n/**\n * ```\n *    p1         p2\n *      ---------\n *      |       |\n *      |       |\n *      ---------\n *    p4         p3\n * ```\n */\ndata class VisibleArea(\n    internal var _p1x: Double,\n    internal var _p1y: Double,\n    internal var _p2x: Double,\n    internal var _p2y: Double,\n    internal var _p3x: Double,\n    internal var _p3y: Double,\n    internal var _p4x: Double,\n    internal var _p4y: Double,\n) {\n    val p1x: Double\n        get() = _p1x\n    val p1y: Double\n        get() = _p1y\n    val p2x: Double\n        get() = _p2x\n    val p2y: Double\n        get() = _p2y\n    val p3x: Double\n        get() = _p3x\n    val p3y: Double\n        get() = _p3y\n    val p4x: Double\n        get() = _p4x\n    val p4y: Double\n        get() = _p4y\n}\n\n/* Internally, we're working on a single VisibleArea instance, and we must ensure mutual exclusion\n * when creating the instance. */\ninternal val visibleAreaMutex = Mutex()\ninternal var visibleArea: VisibleArea? = null\n\n/**\n * Returns a flow which emits [MapState] whenever the viewport changes.\n * It's a flow equivalent of [setStateChangeListener].\n */\nfun MapState.viewportChangeFlow(): Flow<MapState> {\n    return snapshotFlow {\n        centroidX.hashCode() + centroidY.hashCode() + scale.hashCode() + rotation.hashCode()\n    }.map { this }\n}\n\n/**\n * The [MapState] is considered idle when its [centroidX] and [centroidY] haven't changed for at\n * least [thresholdMillis] which is 400ms by default.\n */\nfun MapState.idleStateFlow(thresholdMillis: Long = 400): StateFlow<Boolean> {\n    val stateFlow = MutableStateFlow(false)\n\n    scope.launch {\n        snapshotFlow {\n            \"$centroidX,$centroidY\"\n        }.map {\n            stateFlow.value = false\n        }.collectLatest {\n            delay(thresholdMillis)\n            stateFlow.value = true\n        }\n    }\n\n    return stateFlow.asStateFlow()\n}\n\n\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/api/MarkerApi.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage ovh.plrapps.mapcompose.api\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.SpringSpec\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.State\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.lerp\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.lerp\nimport kotlinx.coroutines.flow.Flow\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport ovh.plrapps.mapcompose.ui.state.markers.DragEndListener\nimport ovh.plrapps.mapcompose.ui.state.markers.DragInterceptor\nimport ovh.plrapps.mapcompose.ui.state.markers.DragStartListener\nimport ovh.plrapps.mapcompose.ui.state.markers.model.MarkerData\nimport ovh.plrapps.mapcompose.ui.state.markers.model.RenderingStrategy\nimport ovh.plrapps.mapcompose.utils.AngleDegree\nimport ovh.plrapps.mapcompose.utils.rotateX\nimport ovh.plrapps.mapcompose.utils.rotateY\nimport ovh.plrapps.mapcompose.utils.toRad\nimport ovh.plrapps.mapcompose.utils.withRetry\n\n/**\n * Add a marker to the given position.\n *\n * @param id The id of the marker\n * @param x The normalized X position on the map, in range [0..1]\n * @param y The normalized Y position on the map, in range [0..1]\n * @param relativeOffset The x-axis and y-axis positions of the marker will be respectively offset by\n * the width of the marker multiplied by the x value of the offset, and the height of the marker\n * multiplied by the y value of the offset.\n * @param absoluteOffset The x-axis and y-axis positions of a marker will be respectively offset by\n * the x and y [Dp] values of the offset.\n * @param zIndex A marker with larger zIndex will be drawn on top of all markers with smaller zIndex.\n * When markers have the same zIndex, the original order in which the parent placed the marker is used.\n * @param clickable Controls whether the marker is clickable. Default is true. If a click listener\n * is registered using [onMarkerClick], that listener will be invoked for that marker if [clickable]\n * is true.\n * @param isConstrainedInBounds By default, a marker cannot be positioned or moved outside of the\n * map bounds.\n * @param clickableAreaScale The clickable area, which defaults to the bounds of the provided\n * composable, can be expanded or shrunk. For example, using Offset(1.2f, 1f), the clickable area\n * will be expanded by 20% on the X axis relatively to the center.\n * @param clickableAreaCenterOffset The center of the clickable area will be offset by the width of\n * the marker multiplied by the x value of the offset, and the height of the marker multiplied by\n * the y value of the offset.\n * @param renderingStrategy By default, markers are eagerly laid-out, e.g they are laid-out\n * even when not visible. There are two alternative rendering strategies:\n * - [RenderingStrategy.LazyLoading]: removes all non-visible markers, dynamically.\n * - [RenderingStrategy.Clustering]: in addition to lazy loading, clusterize markers when they are\n * close to each other.\n */\nfun MapState.addMarker(\n    id: String,\n    x: Double,\n    y: Double,\n    relativeOffset: Offset = Offset(-0.5f, -1f),\n    absoluteOffset: DpOffset = DpOffset.Zero,\n    zIndex: Float = 0f,\n    clickable: Boolean = true,\n    isConstrainedInBounds: Boolean = true,\n    clickableAreaScale: Offset = Offset(1f, 1f),\n    clickableAreaCenterOffset: Offset = Offset(0f, 0f),\n    renderingStrategy: RenderingStrategy = RenderingStrategy.Default,\n    c: @Composable () -> Unit\n) {\n    markerState.addMarker(\n        id,\n        x,\n        y,\n        relativeOffset,\n        absoluteOffset,\n        zIndex,\n        clickable,\n        isConstrainedInBounds,\n        clickableAreaScale,\n        clickableAreaCenterOffset,\n        renderingStrategy,\n        c\n    )\n}\n\n/**\n * Add a clusterer which will clusterize all markers added with the same clusterer id.\n * The default behavior on cluster click is a zoom-in to reveal the content of the clicked\n * cluster. This can be changed using [clusterClickBehavior].\n * The style of a cluster is user-defined using [clusterFactory].\n *\n * @param id The id of the clusterer.\n * @param clusteringThreshold When the distance between two markers goes below that threshold, a\n * cluster is formed. Defaults to 50 dp. There's one exception: when the scale reaches max scale,\n * in which case clustering is disabled.\n * @param clusterClickBehavior Defines the behavior when a cluster is clicked.\n * @param scaleThreshold Defines the scale above which the clusterer is disabled. Defaults to\n * [ClusterScaleThreshold.MaxScale] which corresponds to [MapState.maxScale].\n * @param clusterFactory Compose code for a cluster. Receives the list of marker ids which are fused\n * to form the cluster.\n */\nfun MapState.addClusterer(\n    id: String,\n    clusteringThreshold: Dp = 50.dp,\n    clusterClickBehavior: ClusterClickBehavior = Default,\n    scaleThreshold: ClusterScaleThreshold = ClusterScaleThreshold.MaxScale,\n    clusterFactory: (ids: List<String>) -> (@Composable () -> Unit)\n) {\n    markerState.addClusterer(\n        mapState = this,\n        id = id,\n        clusteringThreshold = clusteringThreshold,\n        clusterClickBehavior = clusterClickBehavior.toInternal(),\n        scaleThreshold = scaleThreshold,\n        clusterFactory = clusterFactory\n    )\n}\n\n/**\n * Set a list of marker id to not clusterize by the clusterer which has the given [id].\n * This is useful to call this api right at the beginning of a marker drag. Otherwise, the clusterer\n * might clusterize the marker during the drag gesture which would cause the gesture to be interrupted\n * and the `onDragEnd` callback (if set) wouldn't be invoked.\n * When this api is invoked, the relevant clusterer re-processes its managed markers.\n *\n * @param id The id of the clusterer\n * @param markersToExempt The set of marker ids to not clusterize.\n */\nfun MapState.setClustererExemptList(\n    id: String,\n    markersToExempt: Set<String>\n) {\n    markerState.setClusteredExemptList(id, markersToExempt)\n}\n\n/**\n * Add a lazy loader for markers. The lazy loader removes markers as they go out of the visible area\n * (and adds markers which are visible).\n *\n * @param id The id for the lazy loader\n * @param padding Padding added to the visible area, in dp. Defaults to 0.\n */\nfun MapState.addLazyLoader(\n    id: String,\n    padding: Dp = 0.dp\n) {\n    markerState.addLazyLoader(this, id, padding)\n}\n\n/**\n * Remove a clusterer.\n * By default, also removes all markers managed by this clusterer.\n */\nfun MapState.removeClusterer(\n    id: String,\n    removeManagedMarkers: Boolean = true\n) {\n    markerState.removeClusterer(id, removeManagedMarkers)\n}\n\n/**\n * Remove a lazy loader.\n * By default, also removes all markers managed by this lazy loader.\n */\nfun MapState.removeLazyLoader(\n    id: String,\n    removeManagedMarkers: Boolean = true\n) {\n    markerState.removeLazyLoader(id, removeManagedMarkers)\n}\n\n/**\n * Check whether a marker was already added or not.\n */\nfun MapState.hasMarker(id: String): Boolean {\n    return markerState.hasMarker(id)\n}\n\n/**\n * Get info on a marker, if the marker is already added.\n *\n * @return Available [MarkerInfo] if the marker was already added, `null` otherwise.\n */\nfun MapState.getMarkerInfo(id: String): MarkerInfo? {\n    return markerState.getMarker(id)?.let {\n        MarkerInfo(it.id, it.x, it.y, it.relativeOffset, it.absoluteOffset, it.zIndex)\n    }\n}\n\n/**\n * Updates the [zIndex] for an existing marker.\n *\n * @param id The id of the marker\n * @param zIndex A marker with larger zIndex will be drawn on top of all markers with smaller zIndex.\n * When markers have the same zIndex, the original order in which the parent placed the marker is used.\n */\nfun MapState.updateMarkerZ(\n    id: String,\n    zIndex: Float\n) {\n    markerState.getMarker(id)?.zIndex = zIndex\n}\n\n/**\n * Updates the clickable property of an existing marker.\n *\n * @param id The id of the marker\n * @param clickable Controls whether the marker is clickable.\n */\nfun MapState.updateMarkerClickable(\n    id: String,\n    clickable: Boolean\n) {\n    markerState.getMarker(id)?.isClickable = clickable\n}\n\n/**\n * Updates the offsets of a marker.\n * @param relativeOffset The x-axis and y-axis positions of the marker will be respectively offset by\n * the width of the marker multiplied by the x value of the offset, and the height of the marker\n * multiplied by the y value of the offset. If null, does not updates the current value.\n * @param absoluteOffset The x-axis and y-axis positions of a marker will be respectively offset by\n * the x and y [Dp] values of the offset. If null, does not updates the current value.\n * @param animationSpec The [AnimationSpec]. Default is [SpringSpec] with low stiffness. When null,\n * no animation is used.\n */\nsuspend fun MapState.updateMarkerOffset(\n    id: String,\n    relativeOffset: Offset? = null,\n    absoluteOffset: DpOffset? = null,\n    animationSpec: AnimationSpec<Float>? = SpringSpec(stiffness = Spring.StiffnessLow)\n) {\n    updateOffset(\n        markerData = markerState.getMarker(id) ?: return,\n        relativeOffset = relativeOffset,\n        absoluteOffset = absoluteOffset,\n        animationSpec = animationSpec\n    )\n}\n\n/**\n * Updates the constrained in bounds state of the marker.\n *\n * @param id The id of the marker\n * @param constrainedInBounds Controls whether the marker is constrained inside map bounds\n */\nfun MapState.updateMarkerConstrained(\n    id: String,\n    constrainedInBounds: Boolean\n) {\n    val markerData = markerState.getMarker(id) ?: return\n    markerData.isConstrainedInBounds = constrainedInBounds\n\n    /* If constrained, immediately move the marker to its constrained position */\n    if (constrainedInBounds) {\n        markerState.moveMarkerTo(id, markerData.x, markerData.y)\n    }\n}\n\n/**\n * Updates the clickable area of the marker.\n */\nfun MapState.updateClickableArea(\n    id: String,\n    clickableAreaScale: Offset? = null,\n    clickableAreaCenterOffset: Offset? = null\n) {\n    val markerData = markerState.getMarker(id) ?: return\n    if (clickableAreaScale != null) {\n        markerData.clickableAreaScale = clickableAreaScale\n    }\n    if (clickableAreaCenterOffset != null) {\n        markerData.clickableAreaCenterOffset = clickableAreaCenterOffset\n    }\n}\n\n/**\n * Update a marker visibility.\n *\n * @param id The id of the marker\n */\nfun MapState.updateMarkerVisibility(id: String, visible: Boolean) {\n    val markerData = markerState.getMarker(id) ?: return\n    markerData.isVisible = visible\n}\n\n/**\n * Remove a marker.\n *\n * @param id The id of the marker\n */\nfun MapState.removeMarker(id: String): Boolean {\n    return markerState.removeMarker(id)\n}\n\n/**\n * Remove all markers.\n */\nfun MapState.removeAllMarkers() {\n    markerState.removeAllMarkers()\n}\n\n/**\n * Move marker to the given position.\n *\n * @param id The id of the marker\n * @param x The normalized X position on the map, in range [0..1]\n * @param y The normalized Y position on the map, in range [0..1]\n */\nfun MapState.moveMarker(id: String, x: Double, y: Double) {\n    markerState.moveMarkerTo(id, x, y)\n}\n\n/**\n * Enable drag gestures on a marker.\n *\n * @param id The id of the marker\n * @param onDragStart (Optional) To get notified when a drag gesture starts.\n * @param onDragEnd (Optional) To get notified when a drag gesture ends.\n * @param dragInterceptor (Optional) Useful to constrain drag movements along a path. When this\n * parameter is set, you're responsible for invoking [moveMarker] with appropriate values (using\n * your own custom logic).\n * See [DragInterceptor].\n */\nfun MapState.enableMarkerDrag(\n    id: String,\n    onDragStart: DragStartListener? = null,\n    onDragEnd: DragEndListener? = null,\n    dragInterceptor: DragInterceptor? = null\n) {\n    markerState.setDraggable(id, true)\n    val markerData = markerState.getMarker(id)\n    if (onDragStart != null) {\n        markerData?.dragStartListener = onDragStart\n    }\n    if (onDragEnd != null) {\n        markerData?.dragEndListener = onDragEnd\n    }\n    if (dragInterceptor != null) {\n        markerData?.dragInterceptor = dragInterceptor\n    }\n}\n\n/**\n * Disable drag gestures on a marker.\n *\n * @param id The id of the marker\n */\nfun MapState.disableMarkerDrag(id: String) {\n    markerState.setDraggable(id, false)\n}\n\n/**\n * Register a callback which will be invoked for every marker move (API move and user drag).\n */\nfun MapState.onMarkerMove(\n    cb: (id: String, x: Double, y: Double, dx: Double, dy: Double) -> Unit\n) {\n    markerState.markerMoveCb = cb\n}\n\n/**\n * Register a callback which will be invoked when a marker is tapped.\n * Beware that this click listener will only be invoked if the marker is clickable, and when the\n * click gesture isn't already consumed by some other composable (like a button).\n */\nfun MapState.onMarkerClick(cb: (id: String, x: Double, y: Double) -> Unit) {\n    markerState.markerClickCb = cb\n}\n\n/**\n * Register a callback which will be invoked when a marker is long-pressed.\n * Beware that the provided callback will only be invoked if the marker is clickable, and when the\n * gesture isn't already consumed by some other composable (like a button).\n */\nfun MapState.onMarkerLongPress(cb: (id: String, x: Double, y: Double) -> Unit) {\n    markerState.markerLongPressCb = cb\n}\n\n/**\n * Sometimes, some components need to observe marker position changes. However, the [MapState] owns\n * the [State] of each marker position. To avoid duplicating state and have the [MapState] as single\n * source of truth, this API creates an observable [State] of marker positions.\n * Note that this api only accounts for regular markers (e.g not managed by a clusterer).\n */\nfun MapState.markerDerivedState(): State<List<MarkerDataSnapshot>> {\n    return derivedStateOf {\n        markerState.getRenderedMarkers().map {\n            MarkerDataSnapshot(it.id, it.x, it.y)\n        }\n    }\n}\n\n/**\n * Similar to [markerDerivedState], but useful for asynchronous processing, using flow operators.\n * Like every snapshot flow, it should be collected from the main thread.\n * Note that this api only accounts for regular markers (e.g not managed by a clusterer).\n */\nfun MapState.markerSnapshotFlow(): Flow<List<MarkerDataSnapshot>> {\n    return snapshotFlow {\n        markerState.getRenderedMarkers().map {\n            MarkerDataSnapshot(it.id, it.x, it.y)\n        }\n    }\n}\n\ndata class MarkerDataSnapshot(val id: String, val x: Double, val y: Double)\n\n/**\n * Register a callback which will be invoked when a callout is tapped.\n * Beware that this click listener will only be invoked if the callout is clickable, and when the\n * click gesture isn't already consumed by some other composable (like a button).\n */\nfun MapState.onCalloutClick(cb: (id: String, x: Double, y: Double) -> Unit) {\n    markerRenderState.calloutClickCb = cb\n}\n\n/**\n * Move a marker, given a displacement in pixels. This is typically useful when programmatically\n * simulating a drag gesture.\n * This API is internally used when enabling drag gestures on a marker using [enableMarkerDrag].\n *\n * @param id The id of the marker\n * @param deltaPx The displacement amount in pixels\n */\nfun MapState.moveMarkerBy(id: String, deltaPx: Offset) {\n    val angle = -zoomPanRotateState.rotation.toRad()\n    val dx = rotateX(deltaPx.x.toDouble(), deltaPx.y.toDouble(), angle)\n    val dy = rotateY(deltaPx.x.toDouble(), deltaPx.y.toDouble(), angle)\n    markerState.moveMarkerBy(\n        id,\n        dx / (zoomPanRotateState.fullWidth * zoomPanRotateState.scale),\n        dy / (zoomPanRotateState.fullHeight * zoomPanRotateState.scale)\n    )\n}\n\n/**\n * Center on a marker, animating the scroll.\n *\n * @param id The id of the marker\n * @param animationSpec The [AnimationSpec]. Default is [SpringSpec] with low stiffness.\n */\nsuspend fun MapState.centerOnMarker(\n    id: String,\n    animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow)\n) {\n    with(zoomPanRotateState) {\n        markerState.getMarker(id)?.also {\n            awaitLayout()\n            val paddingOffset = visibleAreaPadding.getOffsetForScroll(rotation)\n            val destScrollX = it.x * fullWidth * scale - layoutSize.width / 2 - paddingOffset.x\n            val destScrollY = it.y * fullHeight * scale - layoutSize.height / 2 - paddingOffset.y\n\n            withRetry(maxAnimationsRetries, animationsRetriesInterval) {\n                smoothScrollTo(destScrollX, destScrollY, animationSpec)\n            }\n        }\n    }\n}\n\n/**\n * Center on a marker, animating the scroll position and the scale.\n *\n * @param id The id of the marker\n * @param destScale The destination scale\n * @param animationSpec The [AnimationSpec]. Default is [SpringSpec] with low stiffness.\n */\nsuspend fun MapState.centerOnMarker(\n    id: String,\n    destScale: Double,\n    animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow)\n) {\n    with(zoomPanRotateState) {\n        markerState.getMarker(id)?.also {\n            awaitLayout()\n            val destScaleCst = constrainScale(destScale)\n            val paddingOffset = visibleAreaPadding.getOffsetForScroll(rotation)\n            val destScrollX = it.x * fullWidth * destScaleCst - layoutSize.width / 2 - paddingOffset.x\n            val destScrollY = it.y * fullHeight * destScaleCst - layoutSize.height / 2 - paddingOffset.y\n\n            withRetry(maxAnimationsRetries, animationsRetriesInterval) {\n                smoothScrollScaleRotate(\n                    destScrollX,\n                    destScrollY,\n                    destScale,\n                    animationSpec\n                )\n            }\n        }\n    }\n}\n\n/**\n * Center on a marker, animating the scroll position, the scale, and the rotation.\n *\n * @param id The id of the marker\n * @param destScale The destination scale\n * @param destAngle The destination angle in decimal degrees\n * @param animationSpec The [AnimationSpec]. Default is [SpringSpec] with low stiffness.\n */\nsuspend fun MapState.centerOnMarker(\n    id: String,\n    destScale: Double,\n    destAngle: AngleDegree,\n    animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow)\n) {\n    with(zoomPanRotateState) {\n        markerState.getMarker(id)?.also {\n            awaitLayout()\n            val destScaleCst = constrainScale(destScale)\n            val paddingOffset = visibleAreaPadding.getOffsetForScroll(rotation)\n            val destScrollX = it.x * fullWidth * destScaleCst - layoutSize.width / 2 - paddingOffset.x\n            val destScrollY = it.y * fullHeight * destScaleCst - layoutSize.height / 2 - paddingOffset.y\n\n            withRetry(maxAnimationsRetries, animationsRetriesInterval) {\n                smoothScrollScaleRotate(\n                    destScrollX,\n                    destScrollY,\n                    destScale,\n                    destAngle,\n                    animationSpec\n                )\n            }\n        }\n    }\n}\n\n/**\n * Add a callout to the given position.\n *\n * @param id The id of the callout\n * @param x The normalized X position on the map, in range [0..1]\n * @param y The normalized Y position on the map, in range [0..1]\n * @param relativeOffset The x-axis and y-axis positions of the callout will be respectively offset by\n * the width of the marker multiplied by the x value of the offset, and the height of the marker\n * multiplied by the y value of the offset.\n * @param absoluteOffset The x-axis and y-axis positions of a callout will be respectively offset by\n * the x and y [Dp] values of the offset.\n * @param zIndex A callout with larger zIndex will be drawn on top of all callouts with smaller zIndex.\n * When callouts have the same zIndex, the original order in which the parent placed the callout is used.\n * @param autoDismiss Whether the callout should be dismissed on touch down. Default is true. If set\n * to false, the callout can be programmatically dismissed with [removeCallout].\n * @param clickable Controls whether the callout is clickable. Default is false. If a click listener\n * is registered using [onMarkerClick], that listener will only be invoked for that marker if\n * [clickable] is true.\n * @param isConstrainedInBounds By default, a callout cannot be positioned outside of the map\n * bounds.\n */\nfun MapState.addCallout(\n    id: String,\n    x: Double,\n    y: Double,\n    relativeOffset: Offset = Offset(-0.5f, -1f),\n    absoluteOffset: DpOffset = DpOffset.Zero,\n    zIndex: Float = 0f,\n    autoDismiss: Boolean = true,\n    clickable: Boolean = false,\n    isConstrainedInBounds: Boolean = true,\n    c: @Composable () -> Unit\n) {\n    markerRenderState.addCallout(\n        id,\n        x,\n        y,\n        relativeOffset,\n        absoluteOffset,\n        zIndex,\n        autoDismiss,\n        clickable,\n        isConstrainedInBounds,\n        c\n    )\n}\n\n/**\n * Check whether a callout was already added or not.\n */\nfun MapState.hasCallout(id: String): Boolean {\n    return markerRenderState.hasCallout(id)\n}\n\n/**\n * Updates the clickable property of an existing callout.\n *\n * @param id The id of the marker\n * @param clickable Controls whether the callout is clickable.\n */\nfun MapState.updateCalloutClickable(\n    id: String,\n    clickable: Boolean\n) {\n    markerRenderState.callouts[id]?.markerData?.isClickable = clickable\n}\n\n/**\n * Moves a callout.\n *\n * @param id The id of the callout.\n * @param x The normalized X position on the map, in range [0..1]\n * @param y The normalized Y position on the map, in range [0..1]\n */\nfun MapState.moveCallout(id: String, x: Double, y: Double) {\n    markerRenderState.moveCallout(id, x, y)\n}\n\n/**\n * Removes a callout.\n *\n * @param id The id of the callout.\n */\nfun MapState.removeCallout(id: String): Boolean {\n    return markerRenderState.removeCallout(id)\n}\n\n/**\n * Updates the offsets of a callout.\n * @param relativeOffset The x-axis and y-axis positions of the callout will be respectively offset by\n * the width of the callout multiplied by the x value of the offset, and the height of the callout\n * multiplied by the y value of the offset. If null, does not updates the current value.\n * @param absoluteOffset The x-axis and y-axis positions of a callout will be respectively offset by\n * the x and y [Dp] values of the offset. If null, does not updates the current value.\n * @param animationSpec The [AnimationSpec]. Default is [SpringSpec] with low stiffness. When null,\n * no animation is used.\n */\nsuspend fun MapState.updateCalloutOffset(\n    id: String,\n    relativeOffset: Offset? = null,\n    absoluteOffset: DpOffset? = null,\n    animationSpec: AnimationSpec<Float>? = SpringSpec(stiffness = Spring.StiffnessLow)\n) {\n    updateOffset(\n        markerData = markerRenderState.callouts[id]?.markerData ?: return,\n        relativeOffset = relativeOffset,\n        absoluteOffset = absoluteOffset,\n        animationSpec = animationSpec\n    )\n}\n\nprivate suspend fun MapState.updateOffset(\n    markerData: MarkerData,\n    relativeOffset: Offset?,\n    absoluteOffset: DpOffset?,\n    animationSpec: AnimationSpec<Float>?\n) {\n    if (animationSpec != null) {\n        with(zoomPanRotateState) {\n            awaitLayout()\n            invokeAndCheckSuccess {\n                Animatable(0f).animateTo(1f, animationSpec) {\n                    if (relativeOffset != null) {\n                        markerData.relativeOffset = lerp(\n                            markerData.relativeOffset,\n                            relativeOffset,\n                            value\n                        )\n                    }\n                    if (absoluteOffset != null) {\n                        markerData.absoluteOffset = lerp(\n                            markerData.absoluteOffset,\n                            absoluteOffset,\n                            value\n                        )\n                    }\n                }\n            }\n        }\n    } else {\n        if (relativeOffset != null) {\n            markerData.relativeOffset = relativeOffset\n        }\n        if (absoluteOffset != null) {\n            markerData.absoluteOffset = absoluteOffset\n        }\n    }\n}\n\n/**\n * Public data on a marker.\n */\ndata class MarkerInfo(\n    val id: String, val x: Double,\n    val y: Double,\n    val relativeOffset: Offset,\n    val absoluteOffset: DpOffset,\n    val zIndex: Float\n)"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/api/Model.kt",
    "content": "package ovh.plrapps.mapcompose.api\n\nimport ovh.plrapps.mapcompose.core.Layer\nimport ovh.plrapps.mapcompose.core.TileStreamProvider\nimport ovh.plrapps.mapcompose.core.makeLayerId\nimport ovh.plrapps.mapcompose.ui.state.markers.model.ClusterClickBehavior as ClusterClickBehaviorInternal\nimport ovh.plrapps.mapcompose.ui.state.markers.model.Custom as CustomInternal\nimport ovh.plrapps.mapcompose.ui.state.markers.model.Default as DefaultInternal\nimport ovh.plrapps.mapcompose.ui.state.markers.model.None as NoneInternal\n\ndata class Scroll(val x: Double, val y: Double)\n\nsealed interface ClusterClickBehavior\n\n/**\n * Zoom-in to reveal a subset or all markers of the cluster.\n */\ndata object Default : ClusterClickBehavior\n\n/**\n * When a cluster is clicked, the provided [onClick] callback is invoked.\n * the optional parameter [withDefaultBehavior] signifies if the [Default] callback behavior should be applied too\n */\ndata class Custom(val withDefaultBehavior: Boolean = false, val onClick: (ClusterData) -> Unit) : ClusterClickBehavior\n\n/**\n * Cluster related data.\n * @param x, y The coordinates of the cluster's barycenter\n * @param markers The list of markers contained by the cluster\n */\ndata class ClusterData(val x: Double, val y: Double, val markers: List<MarkerDataSnapshot>)\n\n/**\n * Clusters aren't clickable\n */\ndata object None : ClusterClickBehavior\n\n/**\n * Convert public api type to internal type.\n */\ninternal fun ClusterClickBehavior.toInternal(): ClusterClickBehaviorInternal {\n    return when (this) {\n        is Custom -> CustomInternal(\n            onClick = {\n                this.onClick(\n                    ClusterData(\n                        it.x,\n                        it.y,\n                        it.markers.map { markerData ->\n                            MarkerDataSnapshot(markerData.id, markerData.x, markerData.y)\n                        }\n                    )\n                )\n            },\n            withDefaultBehavior = this.withDefaultBehavior\n\n        )\n        Default -> DefaultInternal\n        None -> NoneInternal\n    }\n}\n\nsealed interface ClusterScaleThreshold {\n    data object MaxScale : ClusterScaleThreshold\n    data class FixedScale(val scale: Double) : ClusterScaleThreshold\n}\n\ninternal class LayersBuilderInternal : LayersBuilder {\n    internal val layers = mutableListOf<Layer>()\n    override fun addLayer(tileStreamProvider: TileStreamProvider, initialOpacity: Float) {\n        val id = makeLayerId()\n        val layer = Layer(id, tileStreamProvider, initialOpacity)\n        layers.add(layer)\n    }\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/api/PathApi.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage ovh.plrapps.mapcompose.api\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.Dp\nimport ovh.plrapps.mapcompose.ui.gestures.model.HitType\nimport ovh.plrapps.mapcompose.ui.paths.PathData\nimport ovh.plrapps.mapcompose.ui.paths.PathDataBuilder\nimport ovh.plrapps.mapcompose.ui.paths.model.Cap\nimport ovh.plrapps.mapcompose.ui.paths.model.PatternItem\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\n/**\n * Adds a path, optionally setting some properties.\n *\n * @param id The unique identifier of the path\n * @param pathData Obtained from [PathDataBuilder.build]\n * @param width The width of the path, in [Dp]. Defaults to 4.dp\n * @param color The color of the path. Defaults to Color(0xFF448AFF)\n * @param fillColor If set - the path will be filled and the area drawn in this color.\n * @param offset The number of points to skip from the beginning of the path. Defaults to 0.\n * @param count The number of points to draw after [offset]. Defaults to the number of points added\n * to built [pathData].\n * @param cap The cap style at the start and end of the path. Defaults to [Cap.Round].\n * @param simplify By default, the path is simplified depending on the scale to improve performance.\n * Higher values increase the simplification effect, while a value of 0f effectively disables path\n * simplification. Sensible values a typically in the range [0.5f..2f]. Default value is 1f.\n * @param clickable Controls whether the path is clickable. Default is false. If a click listener\n * is registered using [onPathClick], that listener will be invoked for that marker if [clickable]\n * is true.\n * @param zIndex A path with larger zIndex will be drawn on top of paths with smaller zIndex.\n * When paths have the same zIndex, the more recently added path is drawn on top of the others.\n * @param pattern The dash pattern. By default, no dash effect is applied.\n */\nfun MapState.addPath(\n    id: String,\n    pathData: PathData,\n    width: Dp? = null,\n    color: Color? = null,\n    fillColor: Color? = null,\n    offset: Int? = null,\n    count: Int? = null,\n    cap: Cap = Cap.Round,\n    simplify: Float? = null,\n    clickable: Boolean = false,\n    zIndex: Float = 0f,\n    pattern: List<PatternItem>? = null\n) {\n    pathState.addPath(id, pathData, width, color, fillColor, offset, count, cap, simplify, clickable, zIndex, pattern)\n}\n\n/**\n * Adds a path, optionally setting some properties.\n *\n * @param id The unique identifier of the path\n * @param width The width of the path, in [Dp]. Defaults to 4.dp\n * @param color The color of the path. Defaults to Color(0xFF448AFF)\n * @param offset The number of points to skip from the beginning of the path. Defaults to 0.\n * @param count The number of points to draw after [offset]. Defaults to the number of points\n * provided inside the [builder] block.\n * @param cap The cap style at the start and end of the path. Defaults to [Cap.Round].\n * @param simplify By default, the path is simplified depending on the scale to improve performance.\n * Higher values increase the simplification effect, while a value of 0f effectively disables path\n * simplification. Sensible values a typically in the range [0.5f..2f]. Default value is 1f.\n * @param clickable Controls whether the path is clickable. Default is false. If a click listener\n * is registered using [onPathClick], that listener will be invoked for that marker if [clickable]\n * is true.\n * @param zIndex A path with larger zIndex will be drawn on top of paths with smaller zIndex.\n * When paths have the same zIndex, the more recently added path is drawn on top of the others.\n * @param pattern The dash pattern. By default, no dash effect is applied.\n * @param builder The builder block from with to add individual points or list of points.\n *\n * @return The [PathData] which can be used for adding other paths.\n */\nfun MapState.addPath(\n    id: String,\n    width: Dp? = null,\n    color: Color? = null,\n    fillColor: Color? = null,\n    offset: Int? = null,\n    count: Int? = null,\n    cap: Cap = Cap.Round,\n    simplify: Float? = null,\n    clickable: Boolean = false,\n    zIndex: Float = 0f,\n    pattern: List<PatternItem>? = null,\n    builder: (PathDataBuilder).() -> Unit\n): PathData? {\n    val pathData = makePathDataBuilder().apply { builder() }.build() ?: return null\n    pathState.addPath(id, pathData, width, color, fillColor, offset, count, cap, simplify, clickable, zIndex, pattern)\n    return pathData\n}\n\n/**\n * Updates some properties of a previously added path (using [addPath]).\n *\n * @param id The unique identifier of the path\n * @param pathData The points of the path. The [PathDataBuilder] which was originally used to create\n * the path can be used to build new [PathData] instances with additional points.\n * @param width The width of the path, in [Dp]\n * @param color The color of the path\n * @param offset The number of points to skip from the beginning of the path\n * @param count The number of points to draw after [offset]\n * @param cap The cap style at the start and end of the path\n * @param simplify By default, the path is simplified depending on the scale to improve performance.\n * Higher values increase the simplification effect, while a value of 0f effectively disables path\n * simplification. Sensible values a typically in the range [0.5f..2f]. Default value is 1f.\n * @param zIndex A path with larger zIndex will be drawn on top of paths with smaller zIndex.\n * When paths have the same zIndex, the more recently added path is drawn on top of the others.\n * @param clickable Controls whether the path is clickable.\n * @param pattern The dash pattern. By default, no dash effect is applied.\n */\nfun MapState.updatePath(\n    id: String,\n    pathData: PathData? = null,\n    visible: Boolean? = null,\n    width: Dp? = null,\n    color: Color? = null,\n    fillColor: Color? = null,\n    offset: Int? = null,\n    count: Int? = null,\n    cap: Cap? = null,\n    simplify: Float? = null,\n    clickable: Boolean? = null,\n    zIndex: Float? = null,\n    pattern: List<PatternItem>? = null\n) {\n    pathState.updatePath(id, pathData, visible, width, color, fillColor, offset, count, cap, simplify, clickable, zIndex, pattern)\n}\n\n/**\n * Removes a path.\n *\n * @param id The id of the path\n */\nfun MapState.removePath(id: String) {\n    pathState.removePath(id)\n}\n\n/**\n * Removes all paths.\n */\nfun MapState.removeAllPaths() {\n    pathState.removeAllPaths()\n}\n\n/**\n * Remove paths given a predicate which operates on path id.\n */\nfun MapState.removePaths(predicate: (id: String) -> Boolean) {\n    pathState.removePaths(predicate)\n}\n\n/**\n * Check whether a path was already added or not.\n *\n * @param id The id of the path\n */\nfun MapState.hasPath(id: String): Boolean {\n    return pathState.hasPath(id)\n}\n\n/**\n * Get a new instance of [PathDataBuilder].\n * Adding a path is done using [addPath], which requires a [PathData] instance. A [PathData]\n * instance can only be built using a [PathDataBuilder].\n */\nfun MapState.makePathDataBuilder(): PathDataBuilder {\n    return PathDataBuilder(zoomPanRotateState.fullWidth, zoomPanRotateState.fullHeight)\n}\n\n/**\n * Register a callback which will be invoked when a path is tapped.\n * Beware that this click listener will only be invoked if at least one path is clickable, and when\n * the click gesture isn't already consumed by some other composable (like a button), or a marker.\n * When several paths hover each other, the [cb] is invoked for the path with the highest z-index\n * and which is the last drawn.\n */\nfun MapState.onPathClick(cb: (id: String, x: Double, y: Double) -> Unit) {\n    pathState.pathClickCb = cb\n}\n\n/**\n * Register a callback which will be invoked when a path is long-clicked.\n * Beware that the provided callback will only be invoked if at least one path is clickable, and when\n * the gesture isn't already consumed by some other composable (like a button), or a marker.\n * When several paths hover each other, the [cb] is invoked for the path with the highest z-index.\n */\nfun MapState.onPathLongPress(cb: (id: String, x: Double, y: Double) -> Unit) {\n    pathState.pathLongPressCb = cb\n}\n\n/**\n * Register a callback which will be invoked when one or more paths are tapped.\n * /!\\ This api takes precedence over the [onPathClick] and [onPathLongPress] apis. For example,\n * when [onPathHitTraversal] is set, the callback registered with [onPathClick] isn't invoked.\n * Beware that this click listener will only be invoked if at least one path is clickable, and when\n * the click gesture isn't already consumed by some other composable (like a button), or a marker.\n * When several paths hover each other, the [cb] is invoked for all paths, regardless of their\n * z-index.\n *\n * To unregister the callback, set it to null.\n */\nfun MapState.onPathHitTraversal(cb: ((ids: List<String>, x: Double, y: Double, hitType: HitType) -> Unit)?) {\n    pathState.pathHitTraversalCb = cb\n}\n\n/**\n * When application code lost reference on a [PathData], this api can be useful to retrieve the\n * [PathData] instance.\n * A typical use case is to draw a new path on top or underneath the path with id [id].\n */\nfun MapState.getPathData(id: String): PathData? {\n    return pathState.pathState[id]?.pathData\n}\n\n/**\n * Loops on all paths and snapshots each path properties.\n * Useful to loop and update paths depending on their properties.\n */\nfun MapState.allPaths(block: MapState.(properties: PathProperties) -> Unit) {\n    pathState.pathState.values.forEach { drawablePathState ->\n        val properties = PathProperties(\n            id = drawablePathState.id,\n            visible = drawablePathState.visible,\n            width = drawablePathState.width,\n            color = drawablePathState.color,\n            offset = drawablePathState.offsetAndCount.x,\n            count = drawablePathState.offsetAndCount.y,\n            cap = drawablePathState.cap,\n            simplify = drawablePathState.simplify,\n            clickable = drawablePathState.isClickable,\n            zIndex = drawablePathState.zIndex\n        )\n        block(properties)\n    }\n}\n\n/**\n * Checks if a circle centered on ([x], [y]) with a radius of [rangePx] at scale 1 intersects the\n * path with id = [id].\n */\nfun MapState.isPathWithinRange(id: String, rangePx: Int, x: Double, y: Double): Boolean {\n    return pathState.isPathWithinRange(id, rangePx, x, y)\n}\n\n/**\n * Removes the dash effect of a path.\n */\nfun MapState.removePathPattern(id: String) {\n    pathState.pathState[id]?.apply {\n        pattern = null\n    }\n}\n\ndata class PathProperties(\n    val id: String, val visible: Boolean, val width: Dp, val color: Color, val offset: Int,\n    val count: Int, val cap: Cap, val simplify: Float, val clickable: Boolean,\n    val zIndex: Float\n)\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/api/RenderApi.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage ovh.plrapps.mapcompose.api\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ColorFilter\nimport kotlinx.coroutines.launch\nimport ovh.plrapps.mapcompose.core.ColorFilterProvider\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\n/**\n * Reloads all tiles.\n */\nfun MapState.reloadTiles() {\n    scope.launch {\n        tileCanvasState.forgetTiles()\n        renderVisibleTilesThrottled()\n    }\n}\n\n/**\n * Controls the fade-in effect of tiles. Provided speed should be in the range [0.01f, 1.0f].\n * Values below 0.04f aren't recommended (can cause blinks), the default is 0.07f.\n * A [speed] of 1f effectively disables the fade-in effect.\n */\nfun MapState.setFadeInSpeed(speed: Float) {\n    scope.launch {\n        tileCanvasState.alphaTick = speed\n    }\n}\n\n/**\n * Disables the fade-in effect of tiles.\n */\nfun MapState.disableFadeIn() {\n    scope.launch {\n        tileCanvasState.alphaTick = 1f\n    }\n}\n\n/**\n * Applies a [ColorFilter] for each tile. A different [ColorFilter] can be applied depending on the\n * coordinate of tiles.\n * This change triggers a re-composition (effects are immediately visible).\n */\nfun MapState.setColorFilterProvider(provider: ColorFilterProvider) {\n    tileCanvasState.colorFilterProvider = provider\n}\n\n/**\n * Sets the background color visible before tiles are loaded or when the canvas outside of the\n * map area is in view.\n */\nfun MapState.setMapBackground(color: Color) {\n    mapBackground = color\n}\n\n/**\n * Controls whether Bitmap filtering is enabled when drawing tiles. This is enabled by default.\n * Disabling it is useful to achieve nearest-neighbor scaling, for cases when the art style of the\n * displayed image benefits from it.\n * @see [android.graphics.Paint.setFilterBitmap]\n */\nfun MapState.setBitmapFilteringEnabled(enabled: Boolean) {\n    setBitmapFilteringEnabled { enabled }\n}\n\n/**\n * A version of [setBitmapFilteringEnabled] which allows for dynamic control of bitmap filtering\n * depending on the current [MapState].\n */\nfun MapState.setBitmapFilteringEnabled(predicate: (state: MapState) -> Boolean) {\n    isFilteringBitmap = { predicate(this) }\n}\n\n/**\n * Virtually increase the size of the screen by a padding in pixel amount.\n * With the appropriate value, this can be used to produce a seamless tile loading effect.\n *\n * @param padding in pixels\n */\nfun MapState.setPreloadingPadding(padding: Int) {\n    scope.launch {\n        preloadingPadding = padding.coerceAtLeast(0)\n        renderVisibleTilesThrottled()\n    }\n}\n\n/**\n * The magnifying factor alters the level at which tiles are picked for a given scale. By default,\n * the level immediately higher (in index) is picked, to avoid sub-sampling. This corresponds to a\n * magnifying factor of 0. The value 1 will result in picking the current level at a given scale,\n * which will be at a relative scale between 1.0 and 2.0\n */\nfun MapState.setMagnifyingFactor(factor: Int) {\n    scope.launch {\n        visibleTilesResolver.magnifyingFactor = factor\n        renderVisibleTilesThrottled()\n    }\n}\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/api/UtilsApi.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage ovh.plrapps.mapcompose.api\n\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport ovh.plrapps.mapcompose.utils.*\n\n/**\n * Given a [point] with known normalized coordinates, rotate it by [angleDegree] around the current\n * centroid.\n */\nsuspend fun MapState.rotatePoint(point: Point, angleDegree: AngleDegree): Point {\n    return with(zoomPanRotateState) {\n        awaitLayout()\n\n        val xAxisScale = fullHeight / fullWidth.toDouble()\n        val scaledCenterX = centroidX / xAxisScale\n\n        val xR = rotateCenteredX(\n            point.x / xAxisScale, point.y, scaledCenterX, centroidY, angleDegree.toRad()\n        ) * xAxisScale\n        val yR = rotateCenteredY(\n            point.x / xAxisScale, point.y, scaledCenterX, centroidY, angleDegree.toRad()\n        )\n\n        Point(xR, yR)\n    }\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/core/ColorFilterProvider.kt",
    "content": "package ovh.plrapps.mapcompose.core\n\nimport androidx.compose.ui.graphics.ColorFilter\n\n\n/**\n * Provides a [ColorFilter] for a tile coordinate.\n */\nfun interface ColorFilterProvider {\n    /* Must not be a blocking call - should return immediately */\n    fun getColorFilter(row: Int, col: Int, zoomLvl: Int): ColorFilter?\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/core/Debounce.kt",
    "content": "package ovh.plrapps.mapcompose.core\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.FlowPreview\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.channels.SendChannel\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.launch\n\n/**\n * So long as the returned [SendChannel] receives [T] elements, the provided [block] function isn't\n * executed until a time-span of [timeoutMillis] elapses.\n * When [block] is executed, it's provided with the last [T] value sent to the channel.\n */\n@OptIn(FlowPreview::class)\nfun <T> CoroutineScope.debounce(\n    timeoutMillis: Long,\n    block: suspend (T) -> Unit\n): SendChannel<T> {\n    val channel = Channel<T>(capacity = Channel.CONFLATED)\n    val flow = channel.receiveAsFlow().debounce(timeoutMillis)\n    launch {\n        flow.collect {\n            block(it)\n        }\n    }\n\n    return channel\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/core/GestureConfiguration.kt",
    "content": "package ovh.plrapps.mapcompose.core\n\nimport android.view.ViewConfiguration\n\n/**\n * Configuration of various gestures.\n * Scroll fling friction is controlled by [ViewConfiguration.getScrollFriction].\n */\nclass GestureConfiguration {\n    /**\n     * The friction multiplier of the zoom fling, indicating how quickly the animation should stop.\n     * This should be greater than 0, with a default value of 1.5f. Minimum allowed value is 0.5f.\n     */\n    var flingZoomFriction: Float = 1.5f\n        set(value) {\n            field = value.coerceAtLeast(0.5f)\n        }\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/core/Layer.kt",
    "content": "package ovh.plrapps.mapcompose.core\n\nimport java.util.*\n\ninternal data class Layer(\n    val id: String,\n    val tileStreamProvider: TileStreamProvider,\n    val alpha: Float = 1f\n)\n\nsealed interface LayerPlacement\ndata object AboveAll : LayerPlacement\ndata object BelowAll : LayerPlacement\ndata class AboveLayer(val layerId: String) : LayerPlacement\ndata class BelowLayer(val layerId: String) : LayerPlacement\n\ninternal fun makeLayerId(): String = UUID.randomUUID().toString()\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/core/Throttle.kt",
    "content": "package ovh.plrapps.mapcompose.core\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.channels.SendChannel\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\n\n/**\n * Limit the rate at which a [block] is called.\n * The [block] execution is triggered upon reception of [Unit] from the returned [SendChannel].\n *\n * @param wait The time in ms between each [block] call.\n *\n * @author P.Laurence\n */\nfun CoroutineScope.throttle(wait: Long, block: suspend () -> Unit): SendChannel<Unit> {\n    val channel = Channel<Unit>(capacity = Channel.CONFLATED)\n    val flow = channel.receiveAsFlow()\n\n    launch {\n        flow.collect {\n            block()\n            delay(wait)\n        }\n    }\n    return channel\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/core/Tile.kt",
    "content": "package ovh.plrapps.mapcompose.core\n\nimport android.graphics.Bitmap\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport kotlin.time.TimeSource\n\n/**\n * A [Tile] is defined by its coordinates in the \"pyramid\". A [Tile] is sub-sampled when the\n * scale becomes lower than the scale of the lowest level. To reflect that, there is [subSample]\n * property which is a positive integer (can be 0). When [subSample] equals 0, the [bitmap] of the\n * tile is full scale. When [subSample] equals 1, the [bitmap] is sub-sampled and its size is half\n * the original bitmap (the one at the lowest level), and so on.\n */\ninternal data class Tile(\n    val zoom: Int,\n    val row: Int,\n    val col: Int,\n    val subSample: Int,\n    val layerIds: List<String>,\n    val opacities: List<Float>\n) {\n    @Volatile\n    var bitmap: Bitmap? = null   // write on main-thread only\n\n    var alpha: Float by mutableFloatStateOf(0f)\n\n    @Volatile\n    var overlaps: Tile? = null\n\n    @Volatile\n    var markedForSweep = false   // write on main-thread only\n\n    var phases: IntRange? by mutableStateOf(null)\n\n    @Volatile\n    var timeMark: TimeSource.Monotonic.ValueTimeMark? = null\n}\n\ninternal data class TileSpec(val zoom: Int, val row: Int, val col: Int, val subSample: Int = 0)\n\ninternal fun Tile.spaceKey(): SpaceKey {\n    return \"row=$row,col=$col,zoom=$zoom\"\n}\n\ninternal typealias SpaceKey = String\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/core/TileCollector.kt",
    "content": "package ovh.plrapps.mapcompose.core\n\nimport android.graphics.Bitmap\nimport android.graphics.Bitmap.Config\nimport android.graphics.Bitmap.createBitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.Canvas\nimport android.graphics.Paint\nimport android.os.Build\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Runnable\nimport kotlinx.coroutines.asCoroutineDispatcher\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.channels.ReceiveChannel\nimport kotlinx.coroutines.channels.SendChannel\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.selects.select\nimport java.io.InputStream\nimport java.util.concurrent.LinkedBlockingQueue\nimport java.util.concurrent.SynchronousQueue\nimport java.util.concurrent.ThreadPoolExecutor\nimport java.util.concurrent.TimeUnit\nimport kotlin.math.pow\n\n\n/**\n * The engine of MapCompose. The view-model uses two channels to communicate with the [TileCollector]:\n * * one to send [TileSpec]s (a [SendChannel])\n * * one to receive [TileSpec]s (a [ReceiveChannel])\n *\n * The [TileCollector] encapsulates all the complexity that transforms a [TileSpec] into a [Tile].\n * ```\n *                                              _____________________________________________________________________\n *                                             |                           TileCollector             ____________    |\n *                                  tiles      |                                                    |  ________  |   |\n *              ---------------- [*********] <----------------------------------------------------- | | worker | |   |\n *             |                               |                                                    |  --------  |   |\n *             ↓                               |                                                    |  ________  |   |\n *  _____________________                      |                                   tileSpecs        | | worker | |   |\n * | TileCanvasViewModel |                     |    _____________________  <---- [**********] <---- |  --------  |   |\n *  ---------------------  ----> [*********] ----> | tileCollectorKernel |                          |  ________  |   |\n *                                tileSpecs    |    ---------------------  ----> [**********] ----> | | worker | |   |\n *                                             |                                   tileSpecs        |  --------  |   |\n *                                             |                                                    |____________|   |\n *                                             |                                                      worker pool    |\n *                                             |                                                                     |\n *                                              ---------------------------------------------------------------------\n * ```\n * This architecture is an example of Communicating Sequential Processes (CSP).\n *\n * @author p-lr on 22/06/19\n */\ninternal class TileCollector(\n    private val workerCount: Int,\n    private val optimizeForLowEndDevices: Boolean,\n    private val tileSize: Int\n) {\n    @Volatile\n    var isIdle: Boolean = true\n\n    /**\n     * Sets up the tile collector machinery. The architecture is inspired from\n     * [Kotlin Conf 2018](https://www.youtube.com/watch?v=a3agLJQ6vt8).\n     * It support back-pressure, and avoids deadlock in CSP taking into account recommendations of\n     * this [article](https://medium.com/@elizarov/deadlocks-in-non-hierarchical-csp-e5910d137cc),\n     * which is from the same author.\n     *\n     * @param [tileSpecs] channel of [TileSpec], which capacity should be [Channel.RENDEZVOUS].\n     * @param [tilesOutput] channel of [Tile], which should be set as [Channel.RENDEZVOUS].\n     */\n    suspend fun collectTiles(\n        tileSpecs: ReceiveChannel<TileSpec>,\n        tilesOutput: SendChannel<Tile>,\n        layers: List<Layer>,\n    ) = coroutineScope {\n        val tilesToDownload = Channel<TileSpec>(capacity = Channel.RENDEZVOUS)\n        val tilesDownloadedFromWorker = Channel<TileSpec>(capacity = 1)\n\n        repeat(workerCount) {\n            worker(\n                tilesToDownload = tilesToDownload,\n                tilesDownloaded = tilesDownloadedFromWorker,\n                tilesOutput = tilesOutput,\n                layers = layers\n            )\n        }\n        tileCollectorKernel(tileSpecs, tilesToDownload, tilesDownloadedFromWorker)\n    }\n\n    private fun CoroutineScope.worker(\n        tilesToDownload: ReceiveChannel<TileSpec>,\n        tilesDownloaded: SendChannel<TileSpec>,\n        tilesOutput: SendChannel<Tile>,\n        layers: List<Layer>,\n    ) = launch(dispatcher) {\n\n        val layerIds = layers.map { it.id }\n        val canUseHardwareBitmaps = canUseHardwareBitmaps()\n\n        /**\n         * This config is for the software canvas, which is used in two situations:\n         * 1. There's more than one layer. We use a software canvas before copying the result either\n         *    on a hardware bitmap (if we can use hardware bitmaps), or to another software bitmap.\n         * 2. There's exactly one layer. Then, [Config.RGB_565] is suitable when we're optimizing\n         *    for low-end devices. This config won't be used if we can use hardware bitmaps.\n         */\n        val config = if (layers.size == 1 && optimizeForLowEndDevices) {\n            Config.RGB_565\n        } else {\n            Config.ARGB_8888\n        }\n\n        val bitmapLoadingOptionsForLayer = layerIds.associateWith {\n            BitmapFactory.Options().apply {\n                inPreferredConfig = config\n            }\n        }\n\n        /* If we can't use hardware bitmaps or we have two or more layers, we need to work with\n         * a software canvas */\n        val shouldUseSoftwareCanvas = layers.size > 1 || !canUseHardwareBitmaps\n\n        val bitmapForLayer = if (shouldUseSoftwareCanvas) {\n            layerIds.associateWith {\n                createBitmap(tileSize, tileSize, config)\n            }\n        } else emptyMap()\n\n        val canvas = Canvas()\n        val paint = Paint(Paint.FILTER_BITMAP_FLAG)\n\n        fun getBitmap(\n            subSamplingRatio: Int,\n            layer: Layer,\n            inputStream: InputStream,\n        ): BitmapForLayer {\n            val bitmapLoadingOptions =\n                bitmapLoadingOptionsForLayer[layer.id] ?: return BitmapForLayer(null, layer)\n\n            bitmapLoadingOptions.inSampleSize = subSamplingRatio\n            if (shouldUseSoftwareCanvas) {\n                bitmapLoadingOptions.inMutable = true\n                bitmapLoadingOptions.inBitmap = bitmapForLayer[layer.id]\n            } else {\n                bitmapLoadingOptions.inPreferredConfig = Config.HARDWARE\n            }\n\n            return inputStream.use {\n                val bitmap = runCatching {\n                    BitmapFactory.decodeStream(inputStream, null, bitmapLoadingOptions)\n                }.getOrNull()\n                BitmapForLayer(bitmap, layer)\n            }\n        }\n\n        for (spec in tilesToDownload) {\n            if (layers.isEmpty()) {\n                tilesDownloaded.send(spec)\n                continue\n            }\n\n            val subSamplingRatio = 2.0.pow(spec.subSample).toInt()\n            val bitmapForLayers = layers.mapIndexed { index, layer ->\n                async {\n                    val i = layer.tileStreamProvider.getTileStream(spec.row, spec.col, spec.zoom)\n                    if (i != null) {\n                        getBitmap(\n                            subSamplingRatio = subSamplingRatio,\n                            layer = layer,\n                            inputStream = i\n                        )\n                    } else BitmapForLayer(null, layer)\n                }\n            }.awaitAll()\n\n            val primaryLayerBitmap = bitmapForLayers.firstOrNull()?.bitmap ?: run {\n                tilesDownloaded.send(spec)\n                /* When the decoding failed or if there's nothing to decode, then send back the Tile\n                 * just as in normal processing, so that the actor which submits tiles specs to the\n                 * collector knows that this tile has been processed and does not immediately\n                 * re-sends the same spec. */\n                tilesOutput.send(\n                    Tile(\n                        spec.zoom,\n                        spec.row,\n                        spec.col,\n                        spec.subSample,\n                        layerIds,\n                        layers.map { it.alpha }\n                    )\n                )\n                null\n            } ?: continue // If the decoding of the first layer failed, skip the rest\n\n            if (layers.size > 1) {\n                canvas.setBitmap(primaryLayerBitmap)\n\n                for (result in bitmapForLayers.drop(1)) {\n                    paint.alpha = (255f * result.layer.alpha).toInt()\n                    if (result.bitmap == null) continue\n                    canvas.drawBitmap(result.bitmap, 0f, 0f, paint)\n                }\n            }\n\n            val resultBitmap = if (canUseHardwareBitmaps) {\n                if (layers.size > 1) {\n                    primaryLayerBitmap.copy(Config.HARDWARE, false)\n                } else primaryLayerBitmap\n            } else {\n                primaryLayerBitmap.copy(config, false)\n            }\n\n            val tile = Tile(\n                spec.zoom,\n                spec.row,\n                spec.col,\n                spec.subSample,\n                layerIds,\n                layers.map { it.alpha }\n            ).apply {\n                this.bitmap = resultBitmap\n            }\n            tilesOutput.send(tile)\n            tilesDownloaded.send(spec)\n        }\n    }\n\n    private fun CoroutineScope.tileCollectorKernel(\n        tileSpecs: ReceiveChannel<TileSpec>,\n        tilesToDownload: SendChannel<TileSpec>,\n        tilesDownloadedFromWorker: ReceiveChannel<TileSpec>,\n    ) = launch(Dispatchers.Default) {\n\n        val specsBeingProcessed = mutableListOf<TileSpec>()\n\n        while (true) {\n            select<Unit> {\n                tilesDownloadedFromWorker.onReceive {\n                    specsBeingProcessed.remove(it)\n                    isIdle = specsBeingProcessed.isEmpty()\n                }\n                tileSpecs.onReceive {\n                    if (it !in specsBeingProcessed) {\n                        /* Add it to the list of specs being processed */\n                        specsBeingProcessed.add(it)\n                        isIdle = false\n\n                        /* Now download the tile */\n                        tilesToDownload.send(it)\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Attempts to stop all actively executing tasks, halts the processing of waiting tasks.\n     */\n    fun shutdownNow() {\n        executor.shutdownNow()\n    }\n\n    /**\n     * On Android O+, ART has a more efficient GC and HARDWARE Bitmaps are supported, making\n     * Bitmap re-use much less important.\n     * However:\n     * - a framework issue pre Q requires to wait until GL context is initialized. Otherwise,\n     * allocating a hardware Bitmap can cause a native crash.\n     * - Allocating a hardware Bitmap involves the creation of a file descriptor. Android O, as well\n     * as some P devices, have a maximum of 1024 file descriptors. Android Q+ devices have a much\n     * higher limit of fd.\n     *\n     * To avoid all those issues entirely, we enable HARDWARE Bitmaps on Android Q and above.\n     * We don't monitor the file descriptor count because in practice, MapCompose creates a few\n     * hundreds of them and they seem to be efficiently recycled.\n     */\n    private fun canUseHardwareBitmaps(): Boolean {\n        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q\n    }\n\n    /**\n     * When using a [LinkedBlockingQueue], the core pool size mustn't be 0, or the active thread\n     * count won't be greater than 1. Previous versions used a [SynchronousQueue], which could have\n     * a core pool size of 0 and a growing count of active threads. However, a [Runnable] could be\n     * rejected when no thread were available. Starting from kotlinx.coroutines 1.4.0, this cause\n     * the associated coroutine to be cancelled. By using a [LinkedBlockingQueue], we avoid rejections.\n     */\n    private val executor = ThreadPoolExecutor(\n        workerCount, workerCount,\n        60L, TimeUnit.SECONDS, LinkedBlockingQueue()\n    ).apply {\n        allowCoreThreadTimeOut(true)\n    }\n    private val dispatcher = executor.asCoroutineDispatcher()\n}\n\nprivate data class BitmapForLayer(val bitmap: Bitmap?, val layer: Layer)"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/core/TileStreamProvider.kt",
    "content": "package ovh.plrapps.mapcompose.core\n\nimport java.io.InputStream\n\n/**\n * Defines how tiles should be fetched. It must be supplied as part of the configuration of\n * MapCompose.\n *\n * The [getTileStream] method implementation may suspend, but it isn't required (e.g, it isn't\n * required to switch context using withContext(Dispatcher.IO) { .. }) as MapCompose does that\n * already. The [getTileStream] method is declared using the suspend modifier, as it is sometimes\n * useful to provide an implementation which suspends.\n *\n * MapCompose leverages bitmap pooling to reduce the pressure on the garbage collector. However,\n * there's no tile caching by default - this is an implementation detail of the supplied\n * [TileStreamProvider].\n *\n * If [getTileStream] returns null, the tile won't be rendered.\n * The library does not handle exceptions thrown from [getTileStream]. Such errors are treated as\n * unrecoverable failures.\n */\nfun interface TileStreamProvider {\n    suspend fun getTileStream(row: Int, col: Int, zoomLvl: Int): InputStream?\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/core/Viewport.kt",
    "content": "package ovh.plrapps.mapcompose.core\n\nimport ovh.plrapps.mapcompose.utils.AngleRad\n\n\n/**\n * Denotes an area on the screen. Values are in pixels.\n */\ndata class Viewport(\n    var left: Int = 0, var top: Int = 0, var right: Int = 0, var bottom: Int = 0,\n    var angleRad: AngleRad = 0f\n)"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/core/VisibleTilesResolver.kt",
    "content": "package ovh.plrapps.mapcompose.core\n\nimport ovh.plrapps.mapcompose.utils.rotateX\nimport ovh.plrapps.mapcompose.utils.rotateY\nimport kotlin.math.*\nimport kotlin.time.TimeSource\n\n/**\n * Resolves the visible tiles.\n * This class isn't thread-safe, and public methods should be invoked from the same thread to ensure\n * consistency.\n *\n * @param levelCount Number of levels\n * @param fullWidth Width of the map at scale 1.0\n * @param fullHeight Height of the map at scale 1.0\n * @param magnifyingFactor Alters the level at which tiles are picked for a given scale. By default,\n * the level immediately higher (in index) is picked, to avoid sub-sampling. This corresponds to a\n * [magnifyingFactor] of 0. The value 1 will result in picking the current level at a given scale,\n * which will be at a relative scale between 1.0 and 2.0\n * @param scaleProvider Since the component which invokes [getVisibleTiles] isn't likely to be the\n * component which owns the scale state, we provide it here as a loosely coupled reference.\n *\n * @author p-lr on 25/05/2019\n */\ninternal class VisibleTilesResolver(\n    private val levelCount: Int,\n    private val fullWidth: Int,\n    private val fullHeight: Int,\n    private val tileSize: Int = 256,\n    var magnifyingFactor: Int = 0,\n    private val infiniteScrollX: Boolean = false,\n    private val scaleProvider: ScaleProvider,\n) {\n\n    /**\n     * Last level is at scale 1.0, others are at scale 1.0 / power_of_2\n     */\n    private val scaleForLevel: Map<Int, Double> = (0 until levelCount).associateWith {\n        (1.0 / 2.0.pow((levelCount - it - 1)))\n    }\n\n    /**\n     * Get the scale for a given [level] (also called zoom).\n     * @return the scale or null if no such level was configured.\n     */\n    fun getScaleForLevel(level: Int): Double? {\n        return scaleForLevel[level]\n    }\n\n    fun getColCountForLevel(level: Int): Int? {\n        val scale = scaleForLevel[level] ?: return null\n        return max(0.0, ceil(fullWidth * scale / tileSize) - 1).toInt() + 1\n    }\n\n    /**\n     * Returns the level, an entire value belonging to [0 ; [levelCount] - 1]\n     */\n    internal fun getLevel(scale: Double, magnifyingFactor: Int = 0): Int {\n        /* This value can be negative */\n        val partialLevel = levelCount - 1 - magnifyingFactor +\n                ln(scale) / ln(2.0)\n\n        /* The level can't be greater than levelCount - 1.0 */\n        val capedLevel = min(partialLevel, levelCount - 1.0)\n\n        /* The level can't be lower than 0 */\n        return ceil(max(capedLevel, 0.0)).toInt()\n    }\n\n    /**\n     * Get the [VisibleTiles], given the visible area in pixels.\n     *\n     * @param viewport The [Viewport] which represents the visible area. Its values depend on the\n     * scale.\n     */\n    fun getVisibleTiles(viewport: Viewport): VisibleTiles {\n        val scale = scaleProvider.getScale()\n        val level = getLevel(scale, magnifyingFactor)\n        val scaleAtLevel = scaleForLevel[level] ?: throw AssertionError()\n        val relativeScale = scale / scaleAtLevel\n\n        /* At the current level, row and col index have maximum values */\n        val maxCol = max(0.0, ceil(fullWidth * scaleAtLevel / tileSize) - 1).toInt()\n        val maxRow = max(0.0, ceil(fullHeight * scaleAtLevel / tileSize) - 1).toInt()\n\n        fun Int.lowerThan(limit: Int): Int {\n            return if (this <= limit) this else limit\n        }\n\n        val scaledTileSize = tileSize.toDouble() * relativeScale\n\n        fun makeVisibleTiles(left: Int, top: Int, right: Int, bottom: Int): VisibleTiles {\n            val colLeft = floor(left / scaledTileSize).toInt().lowerThan(maxCol).coerceAtLeast(0)\n            val rowTop = floor(top / scaledTileSize).toInt().lowerThan(maxRow).coerceAtLeast(0)\n            val colRight = (ceil(right / scaledTileSize).toInt() - 1).lowerThan(maxCol)\n            val rowBottom = (ceil(bottom / scaledTileSize).toInt() - 1).lowerThan(maxRow)\n\n            val tileMatrix = (rowTop..rowBottom).associateWith {\n                colLeft..colRight\n            }\n\n            val visibleWindow = if (infiniteScrollX) {\n                val colCnt = maxCol + 1\n\n                val overflowLeft = if (left < 0) {\n                    val leftOverflow = floor(left / scaledTileSize)\n\n                    val phaseForColLeft = buildMap {\n                        for (c in leftOverflow.toInt()..<0) {\n                            val remainder = c + (abs(c) / colCnt) * colCnt\n                            val col = if (remainder < 0) {\n                                colCnt + remainder\n                            } else 0\n                            val phase = floor(c.toDouble() / colCnt).toInt()\n                            if (phase < 0 && phase < (get(col) ?: 0)) {\n                                put(col, phase)\n                            }\n                        }\n                    }\n\n                    val c = (abs(leftOverflow) - 1).toInt()\n                    val colLeftL = (maxCol - c).coerceAtLeast(0)\n\n                    val tileMatrixL = (rowTop..rowBottom).associateWith {\n                        colLeftL..maxCol\n                    }\n\n                    Overflow(tileMatrixL, phaseForColLeft)\n                } else null\n\n                val rightOverflow = ceil(right / scaledTileSize) - 1\n                val overflowRight = if (rightOverflow > maxCol) {\n                    val phaseForColRight = buildMap {\n                        for (c in 0..<(rightOverflow - maxCol).toInt()) {\n                            val col = c - (c / colCnt) * colCnt\n                            val phase = floor(c.toDouble() / colCnt).toInt() + 1\n                            if (phase > 0 && phase > (get(col) ?: 0)) {\n                                put(col, phase)\n                            }\n                        }\n                    }\n\n                    val c = ((rightOverflow - maxCol).toInt() - 1).coerceAtLeast(0)\n                    val colRightR = c.coerceAtMost(maxCol)\n\n                    val tileMatrixR = (rowTop..rowBottom).associateWith {\n                        0..colRightR\n                    }\n\n                    Overflow(tileMatrixR, phaseForColRight)\n                } else null\n\n                VisibleWindow.InfiniteScrollX(tileMatrix, overflowLeft, overflowRight, TimeSource.Monotonic.markNow())\n            } else {\n                VisibleWindow.BoundsConstrained(tileMatrix)\n            }\n\n            return VisibleTiles(level, visibleWindow, getSubSample(scale))\n        }\n\n        return if (viewport.angleRad == 0f) {\n            makeVisibleTiles(viewport.left, viewport.top, viewport.right, viewport.bottom)\n        } else {\n            val xTopLeft = viewport.left\n            val yTopLeft = viewport.top\n\n            val xTopRight = viewport.right\n            val yTopRight = viewport.top\n\n            val xBotLeft = viewport.left\n            val yBotLeft = viewport.bottom\n\n            val xBotRight = viewport.right\n            val yBotRight = viewport.bottom\n\n            val xCenter = (viewport.right + viewport.left).toDouble() / 2\n            val yCenter = (viewport.bottom + viewport.top).toDouble() / 2\n\n            val xTopLeftRot =\n                rotateX(xTopLeft - xCenter, yTopLeft - yCenter, viewport.angleRad) + xCenter\n            val yTopLeftRot =\n                rotateY(xTopLeft - xCenter, yTopLeft - yCenter, viewport.angleRad) + yCenter\n            var xLeftMost = xTopLeftRot\n            var yTopMost = yTopLeftRot\n            var xRightMost = xTopLeftRot\n            var yBotMost = yTopLeftRot\n\n            val xTopRightRot =\n                rotateX(xTopRight - xCenter, yTopRight - yCenter, viewport.angleRad) + xCenter\n            val yTopRightRot =\n                rotateY(xTopRight - xCenter, yTopRight - yCenter, viewport.angleRad) + yCenter\n            xLeftMost = xLeftMost.coerceAtMost(xTopRightRot)\n            yTopMost = yTopMost.coerceAtMost(yTopRightRot)\n            xRightMost = xRightMost.coerceAtLeast(xTopRightRot)\n            yBotMost = yBotMost.coerceAtLeast(yTopRightRot)\n\n            val xBotLeftRot =\n                rotateX(xBotLeft - xCenter, yBotLeft - yCenter, viewport.angleRad) + xCenter\n            val yBotLeftRot =\n                rotateY(xBotLeft - xCenter, yBotLeft - yCenter, viewport.angleRad) + yCenter\n            xLeftMost = xLeftMost.coerceAtMost(xBotLeftRot)\n            yTopMost = yTopMost.coerceAtMost(yBotLeftRot)\n            xRightMost = xRightMost.coerceAtLeast(xBotLeftRot)\n            yBotMost = yBotMost.coerceAtLeast(yBotLeftRot)\n\n            val xBotRightRot =\n                rotateX(xBotRight - xCenter, yBotRight - yCenter, viewport.angleRad) + xCenter\n            val yBotRightRot =\n                rotateY(xBotRight - xCenter, yBotRight - yCenter, viewport.angleRad) + yCenter\n            xLeftMost = xLeftMost.coerceAtMost(xBotRightRot)\n            yTopMost = yTopMost.coerceAtMost(yBotRightRot)\n            xRightMost = xRightMost.coerceAtLeast(xBotRightRot)\n            yBotMost = yBotMost.coerceAtLeast(yBotRightRot)\n\n            makeVisibleTiles(\n                xLeftMost.toInt(),\n                yTopMost.toInt(),\n                xRightMost.toInt(),\n                yBotMost.toInt()\n            )\n        }\n    }\n\n    // internal for test purposes\n    internal fun getSubSample(scale: Double): Int {\n        return if (scale < (scaleForLevel[0] ?: Double.MIN_VALUE)) {\n            ceil(ln((scaleForLevel[0] ?: error(\"\")).toDouble() / scale) / ln(2.0)).toInt()\n        } else {\n            0\n        }\n    }\n\n    fun interface ScaleProvider {\n        fun getScale(): Double\n    }\n}\n\n/**\n * Properties container for the computed visible tiles.\n * @param level 0-based level index\n * @param visibleWindow contains information about which tiles are currently visible\n * @param subSample the current sub-sample factor. If the current scale of the [VisibleTilesResolver]\n * is lower than the scale of the minimum level, [subSample] is greater than 0. Otherwise, [subSample]\n * equals 0.\n */\ninternal data class VisibleTiles(\n    val level: Int,\n    val visibleWindow: VisibleWindow,\n    val subSample: Int = 0\n)\n\ninternal typealias Row = Int\ninternal typealias Col = Int\ninternal typealias ColRange = IntRange\n\n/* Contains all (row, col) indexes, grouped by rows*/\ninternal typealias TileMatrix = Map<Row, ColRange>\n\ninternal sealed interface VisibleWindow {\n    data class BoundsConstrained(val tileMatrix: TileMatrix): VisibleWindow\n    data class InfiniteScrollX(\n        val tileMatrix: TileMatrix,\n        val leftOverflow: Overflow?,\n        val rightOverflow: Overflow?,\n        val timeMark: TimeSource.Monotonic.ValueTimeMark\n    ): VisibleWindow\n}\n\n/**\n * Contains information about which tiles should be repeated on one side and how.\n * For example, if `phase[3]` returns -2, it means the tile of column index 3 should be repeated 2\n * times on the left. If `phase[0]` returns 1, it means the tile of column index 0 should be drawn a\n * single time on the right.\n * A phase should always be different than 0.\n */\ninternal data class Overflow(val tileMatrix: TileMatrix, val phase: Map<Col, Int>)"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/MapUI.kt",
    "content": "package ovh.plrapps.mapcompose.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.key\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.zIndex\nimport ovh.plrapps.mapcompose.ui.layout.ZoomPanRotate\nimport ovh.plrapps.mapcompose.ui.markers.MarkerComposer\nimport ovh.plrapps.mapcompose.ui.paths.PathComposer\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport ovh.plrapps.mapcompose.ui.view.TileCanvas\n\n@Composable\nfun MapUI(\n    modifier: Modifier = Modifier,\n    state: MapState,\n    content: @Composable () -> Unit = {}\n) {\n    val zoomPRState = state.zoomPanRotateState\n    val markerState = state.markerRenderState\n    val pathState = state.pathState\n\n    key(state) {\n        ZoomPanRotate(\n            modifier = modifier\n                .clipToBounds()\n                .background(state.mapBackground),\n            gestureListener = zoomPRState,\n            layoutSizeChangeListener = zoomPRState,\n        ) {\n            TileCanvas(\n                modifier = Modifier,\n                zoomPRState = zoomPRState,\n                visibleTilesResolver = state.visibleTilesResolver,\n                tileSize = state.tileSize,\n                alphaTick = state.tileCanvasState.alphaTick,\n                colorFilterProvider = state.tileCanvasState.colorFilterProvider,\n                tilesToRender = state.tileCanvasState.tilesToRender,\n                isFilteringBitmap = state.isFilteringBitmap,\n            )\n\n            MarkerComposer(\n                modifier = Modifier.zIndex(1f),\n                zoomPRState = zoomPRState,\n                markerRenderState = markerState,\n                mapState = state\n            )\n\n            PathComposer(\n                modifier = Modifier,\n                zoomPRState = zoomPRState,\n                pathState = pathState\n            )\n\n            content()\n        }\n    }\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/gestures/GestureDetector.kt",
    "content": "package ovh.plrapps.mapcompose.ui.gestures\n\nimport androidx.compose.foundation.gestures.*\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.PointerEvent\nimport androidx.compose.ui.input.pointer.PointerInputScope\nimport androidx.compose.ui.input.pointer.positionChanged\nimport androidx.compose.ui.input.pointer.util.VelocityTracker\nimport androidx.compose.ui.input.pointer.util.VelocityTracker1D\nimport androidx.compose.ui.unit.Velocity\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastAll\nimport androidx.compose.ui.util.fastAny\nimport androidx.compose.ui.util.fastForEach\nimport kotlin.math.PI\nimport kotlin.math.abs\nimport kotlin.math.pow\n\n/**\n * A modified version of [detectTransformGestures] from the framework, which adds fling and\n * two-fingers tap support.\n */\ninternal suspend fun PointerInputScope.detectTransformGestures(\n    panZoomLock: Boolean = false,\n    onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit,\n    onTouchDown: () -> Unit,\n    onTwoFingersTap: (centroid: Offset) -> Unit,\n    onFling: (velocity: Velocity) -> Unit,\n    onFlingZoom: (centroid: Offset, velocity: Float) -> Unit\n) {\n    val flingVelocityThreshold = 200.dp.toPx().pow(2)\n    val flingVelocityMaxRange = -8000f..8000f\n\n    val flingZoomThreshold = 1f\n    val flingZoomVelocityFactor = 400  // lower value for faster fling\n\n    val twoFingersReleaseTolerance = 150 // in ms\n\n    awaitEachGesture {\n        var rotation = 0f\n        var zoom = 1f\n        var pan = Offset.Zero\n        var pastTouchSlop = false\n        val touchSlop = viewConfiguration.touchSlop\n        var lockedToPanZoom = false\n\n        awaitFirstDown(requireUnconsumed = false)\n        onTouchDown()\n        val panVelocityTracker = VelocityTracker()\n        val zoomVelocityTracker = VelocityTracker1D(isDataDifferential = false)\n        var canceled: Boolean\n        var centroidTwoFingers = Offset.Unspecified\n        var lastTwoFingersDown = 0L\n        var lastTime = 0L\n        do {\n            val event = awaitPointerEvent()\n            canceled = event.changes.fastAny { it.isConsumed }\n            if (!canceled) {\n                val zoomChange = event.calculateZoom()\n                val rotationChange = event.calculateRotation()\n                val panChange = event.calculatePan()\n                pan += panChange\n                zoom *= zoomChange\n                rotation += rotationChange\n\n                if (!pastTouchSlop) {\n                    val centroidSize = event.calculateCentroidSize(useCurrent = false)\n                    val zoomMotion = abs(1 - zoom) * centroidSize\n                    val rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f)\n                    val panMotion = pan.getDistance()\n\n                    if (zoomMotion > touchSlop ||\n                        rotationMotion > touchSlop ||\n                        panMotion > touchSlop\n                    ) {\n                        pastTouchSlop = true\n                        lockedToPanZoom = panZoomLock && rotationMotion < touchSlop\n                    }\n                }\n\n                if (pastTouchSlop) {\n                    val uptime =\n                        event.changes.maxByOrNull { it.uptimeMillis }?.uptimeMillis ?: 0L\n                    lastTime = uptime\n                    panVelocityTracker.addPosition(uptime, pan)\n\n                    /* For the fling velocity, only take into account the centroid size when the\n                     * two fingers are down */\n                    if (event.changes.size == 2 && event.changes.fastAll { it.pressed }) {\n                        val size = event.calculateCentroidSize(useCurrent = true)\n                        zoomVelocityTracker.addDataPoint(uptime, size)\n                        lastTwoFingersDown = uptime\n                    }\n\n                    val centroid = event.calculateCentroid(useCurrent = false)\n                    val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange\n                    if (effectiveRotation != 0f ||\n                        zoomChange != 1f ||\n                        panChange != Offset.Zero\n                    ) {\n                        onGesture(centroid, panChange, zoomChange, effectiveRotation)\n                    }\n                    event.changes.fastForEach {\n                        if (it.positionChanged()) {\n                            it.consume()\n                        }\n                    }\n                }\n\n                /* When releasing from two fingers tap, only one of the two pointers is pressed.\n                 * Note that this only detects the release of the two fingers. */\n                if (event.changes.size == 2\n                    && event.changes.fastAny { it.pressed }\n                    && event.changes.fastAny { !it.pressed }\n                ) {\n                    centroidTwoFingers = event.calculateCentroidIgnorePressed()\n                    event.changes.forEach { it.consume() }\n                }\n            }\n        } while (!canceled && event.changes.fastAny { it.pressed })\n\n        // If changes where consumed in another gesture, no need to go further.\n        if (canceled) {\n            return@awaitEachGesture\n        }\n\n        // If there where some zooming involved, there might be some zoom fling.\n        // Then, no need to go further since we'll next check for two-fingers tap and fling.\n        if (zoom != 1f && pastTouchSlop) {\n            val velocity = runCatching {\n                zoomVelocityTracker.calculateVelocity()\n            }.getOrDefault(0f)\n\n            if (abs(velocity) > flingZoomThreshold\n                && centroidTwoFingers != Offset.Unspecified\n                // Tolerate a slight delay between the release of the first and second finger\n                && (lastTime - lastTwoFingersDown) < twoFingersReleaseTolerance\n            ) {\n                onFlingZoom(centroidTwoFingers, velocity / flingZoomVelocityFactor)\n            }\n\n            return@awaitEachGesture\n        }\n\n        // In addition to not zooming, if there where no pan or the fingers didn't move enough\n        // to trigger a zoom or pan, it might be a two fingers tap.\n        if (pan == Offset.Zero || !pastTouchSlop) {\n            if (centroidTwoFingers != Offset.Unspecified) {\n                onTwoFingersTap(centroidTwoFingers)\n            }\n        } else {\n            // No zoom with pan: it might be a fling\n            val velocity = runCatching {\n                panVelocityTracker.calculateVelocity()\n            }.getOrDefault(Velocity.Zero)\n            val velocitySquared = velocity.x.pow(2) + velocity.y.pow(2)\n            val velocityCapped = Velocity(\n                velocity.x.coerceIn(flingVelocityMaxRange),\n                velocity.y.coerceIn(flingVelocityMaxRange)\n            )\n\n            if (velocitySquared > flingVelocityThreshold) {\n                onFling(velocityCapped)\n            }\n        }\n    }\n}\n\n/**\n * Returns the centroid when releasing two fingers. One of the changes isn't pressed while the other\n * one is still pressed.\n */\nprivate fun PointerEvent.calculateCentroidIgnorePressed(): Offset {\n    var centroid = Offset.Zero\n    var centroidWeight = 0\n\n    changes.fastForEach { change ->\n        val position = change.position\n        centroid += position\n        centroidWeight++\n    }\n    return if (centroidWeight == 0) {\n        Offset.Unspecified\n    } else {\n        centroid / centroidWeight.toFloat()\n    }\n}\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/gestures/TapGestureDetector.kt",
    "content": "package ovh.plrapps.mapcompose.ui.gestures\n\nimport androidx.compose.foundation.gestures.GestureCancellationException\nimport androidx.compose.foundation.gestures.PressGestureScope\nimport androidx.compose.foundation.gestures.awaitEachGesture\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.foundation.gestures.calculatePan\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.gestures.waitForUpOrCancellation\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.AwaitPointerEventScope\nimport androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.input.pointer.PointerInputScope\nimport androidx.compose.ui.input.pointer.positionChanged\nimport androidx.compose.ui.input.pointer.util.VelocityTracker\nimport androidx.compose.ui.platform.ViewConfiguration\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.Velocity\nimport androidx.compose.ui.util.fastAny\nimport androidx.compose.ui.util.fastForEach\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlin.math.abs\n\nprivate val NoPressGesture: suspend PressGestureScope.(Offset) -> Unit = { }\n\n/**\n * A modified version of [detectTapGestures] from the framework, with the following differences:\n * - can take [shouldConsumeTap] callback which is invoked to check whether a tap should be consumed.\n * - can take [shouldConsumeLongPress] callback which is invoked to check whether a long-press should\n * be consumed.\n * When [shouldConsumeTap] returns true, [onTap] isn't invoked and the gesture ends there without\n * waiting for [ViewConfiguration.doubleTapMinTimeMillis].\n * When a long-press gesture is detected, [shouldConsumeLongPress] is invoked, and [onLongPress] is\n * invoked only when the long-press isn't consumed.\n * - takes a [onDoubleTapZoom] callback for one finger zooming by double tapping but not releasing\n * on the second tap, and then sliding the finger up to zoom out, or down to zoom in.\n * Consequently, this gesture detector doesn't try to detect a long-press after the\n * second tap, and a double-tap can no-longer timeout.\n */\ninternal suspend fun PointerInputScope.detectTapGestures(\n    onDoubleTap: ((Offset) -> Unit)? = null,\n    onDoubleTapZoom: (centroid: Offset, zoom: Float) -> Unit,\n    onDoubleTapZoomFling: (centroid: Offset, velocity: Float) -> Unit,\n    onLongPress: ((Offset) -> Unit)? = null,\n    onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,\n    onTap: ((Offset) -> Unit)? = null,\n    shouldConsumeTap: ((Offset) -> Boolean)? = null,\n    shouldConsumeLongPress: ((Offset) -> Boolean) ? = null\n) = coroutineScope {\n    // special signal to indicate to the sending side that it shouldn't intercept and consume\n    // cancel/up events as we're only require down events\n    val pressScope = PressGestureScopeImpl(this@detectTapGestures)\n\n    val flingZoomThreshold = 1f\n    val flingZoomVelocityFactor = 400  // lower value for faster fling\n\n    awaitEachGesture {\n        val down = awaitFirstDown()\n        down.consume()\n        launch {\n            pressScope.reset()\n        }\n        if (onPress !== NoPressGesture) launch {\n            pressScope.onPress(down.position)\n        }\n        val longPressTimeout = onLongPress?.let {\n            viewConfiguration.longPressTimeoutMillis\n        } ?: (Long.MAX_VALUE / 2)\n        var upOrCancel: PointerInputChange? = null\n        try {\n            // wait for first tap up or long press\n            upOrCancel = withTimeout(longPressTimeout) {\n                waitForUpOrCancellation()\n            }\n            if (upOrCancel == null) {\n                launch {\n                    pressScope.cancel() // tap-up was canceled\n                }\n            } else {\n                upOrCancel.consume()\n                launch {\n                    pressScope.release()\n                }\n            }\n        } catch (_: PointerEventTimeoutCancellationException) {\n            val longPressConsumed = shouldConsumeLongPress?.invoke(down.position) ?: false\n            if (!longPressConsumed) {\n                onLongPress?.invoke(down.position)\n            }\n            consumeUntilUp()\n            pressScope.release()\n        }\n\n        if (upOrCancel != null) {\n            // tap was successful.\n            val tapConsumed = shouldConsumeTap?.invoke(upOrCancel.position) ?: false\n            if (tapConsumed) return@awaitEachGesture\n\n            if (onDoubleTap == null) {\n                onTap?.invoke(upOrCancel.position) // no need to check for double-tap.\n            } else {\n                // check for second tap\n                val secondDown = awaitSecondDown(upOrCancel)\n\n                if (secondDown == null) { // no valid second tap started\n                    onTap?.invoke(upOrCancel.position)\n                } else {\n                    // Second tap down detected\n                    launch {\n                        pressScope.reset()\n                    }\n                    if (onPress !== NoPressGesture) {\n                        launch { pressScope.onPress(secondDown.position) }\n                    }\n\n                    // Now, either double-tap or zoom gesture. This is where we deviate\n                    // from the framework : no timeout to detect long-press.\n                    val secondUp = waitForUpOrCancellation()\n                    if (secondUp != null) {\n                        secondUp.consume()\n                        launch {\n                            pressScope.release()\n                        }\n                        onDoubleTap(secondUp.position)\n                    } else {\n                        val zoomVelocityTracker = VelocityTracker()\n                        var pan = Offset.Zero\n                        do {\n                            val event = awaitPointerEvent()\n                            val canceled = event.changes.fastAny { it.isConsumed }\n                            if (!canceled) {\n                                val panChange = event.calculatePan()\n                                pan += panChange\n                                val zoom = (size.height + panChange.y * density) / size.height\n                                val uptime = event.changes.maxByOrNull { it.uptimeMillis }?.uptimeMillis ?: 0L\n                                zoomVelocityTracker.addPosition(uptime, pan)\n                                onDoubleTapZoom(secondDown.position, zoom)\n\n                                event.changes.fastForEach {\n                                    if (it.positionChanged()) {\n                                        it.consume()\n                                    }\n                                }\n                            }\n                        } while (!canceled && event.changes.fastAny { it.pressed })\n\n                        launch {\n                            pressScope.cancel()\n                        }\n\n                        /* Depending on the velocity, we might trigger a fling */\n                        zoomVelocityTracker.calculateVelocity()\n                        val velocity = runCatching {\n                            zoomVelocityTracker.calculateVelocity()\n                        }.getOrDefault(Velocity.Zero).y\n\n                        if (abs(velocity) > flingZoomThreshold) {\n                            onDoubleTapZoomFling(\n                                secondDown.position,\n                                velocity / flingZoomVelocityFactor\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n/**\n * Consumes all pointer events until nothing is pressed and then returns. This method assumes\n * that something is currently pressed.\n */\nprivate suspend fun AwaitPointerEventScope.consumeUntilUp() {\n    do {\n        val event = awaitPointerEvent()\n        event.changes.fastForEach { it.consume() }\n    } while (event.changes.fastAny { it.pressed })\n}\n\n/**\n * Waits for [ViewConfiguration.doubleTapTimeoutMillis] for a second press event. If a\n * second press event is received before the time out, it is returned or `null` is returned\n * if no second press is received.\n */\nprivate suspend fun AwaitPointerEventScope.awaitSecondDown(\n    firstUp: PointerInputChange\n): PointerInputChange? = withTimeoutOrNull(viewConfiguration.doubleTapTimeoutMillis) {\n    val minUptime = firstUp.uptimeMillis + viewConfiguration.doubleTapMinTimeMillis\n    var change: PointerInputChange\n    // The second tap doesn't count if it happens before DoubleTapMinTime of the first tap\n    do {\n        change = awaitFirstDown()\n    } while (change.uptimeMillis < minUptime)\n    change\n}\n\n/**\n * [detectTapGestures]'s implementation of [PressGestureScope].\n */\nprivate class PressGestureScopeImpl(\n    density: Density\n) : PressGestureScope, Density by density {\n    private var isReleased = false\n    private var isCanceled = false\n    private val mutex = Mutex(locked = false)\n\n    /**\n     * Called when a gesture has been canceled.\n     */\n    fun cancel() {\n        isCanceled = true\n        mutex.unlock()\n    }\n\n    /**\n     * Called when all pointers are up.\n     */\n    fun release() {\n        isReleased = true\n        mutex.unlock()\n    }\n\n    /**\n     * Called when a new gesture has started.\n     */\n    suspend fun reset() {\n        mutex.lock()\n        isReleased = false\n        isCanceled = false\n    }\n\n    override suspend fun awaitRelease() {\n        if (!tryAwaitRelease()) {\n            throw GestureCancellationException(\"The press gesture was canceled.\")\n        }\n    }\n\n    override suspend fun tryAwaitRelease(): Boolean {\n        if (!isReleased && !isCanceled) {\n            mutex.lock()\n        }\n        return isReleased\n    }\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/gestures/model/HitType.kt",
    "content": "package ovh.plrapps.mapcompose.ui.gestures.model\n\nenum class HitType {\n    Click, LongPress\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/layout/MinimumScaleMode.kt",
    "content": "package ovh.plrapps.mapcompose.ui.layout\n\nsealed class MinimumScaleMode\n\n/**\n * Limit the minimum scale to no less than what would be required to fit inside the container.\n * This is the default mode.\n */\ndata object Fit : MinimumScaleMode()\n\n/**\n * Limit the minimum scale to no less than what would be required to fill the container.\n */\ndata object Fill : MinimumScaleMode()\n\n/**\n * Force a specific minimum scale.\n */\ndata class Forced(val scale: Double) : MinimumScaleMode()"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/layout/Rendering.kt",
    "content": "package ovh.plrapps.mapcompose.ui.layout\n\n/* We assume no device has a screen wider than this */\nconst val grid = 65536"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/layout/ZoomPanRotate.kt",
    "content": "package ovh.plrapps.mapcompose.ui.layout\n\nimport androidx.compose.animation.core.DecayAnimationSpec\nimport androidx.compose.animation.rememberSplineBasedDecay\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.Velocity\nimport kotlinx.coroutines.CoroutineScope\nimport ovh.plrapps.mapcompose.ui.gestures.detectTransformGestures\nimport ovh.plrapps.mapcompose.ui.gestures.detectTapGestures\n\n@Composable\ninternal fun ZoomPanRotate(\n    modifier: Modifier = Modifier,\n    gestureListener: GestureListener,\n    layoutSizeChangeListener: LayoutSizeChangeListener,\n    content: @Composable () -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val flingSpec = rememberSplineBasedDecay<Offset>()\n\n    Layout(\n        content = content,\n        modifier\n            .pointerInput(gestureListener.isListeningForGestures()) {\n                if (!gestureListener.isListeningForGestures()) return@pointerInput\n                detectTransformGestures(\n                    onGesture = { centroid, pan, gestureZoom, gestureRotate ->\n                        gestureListener.onRotationDelta(gestureRotate)\n                        gestureListener.onScaleRatio(gestureZoom.toDouble(), centroid)\n                        gestureListener.onScrollDelta(pan)\n                    },\n                    onTouchDown = gestureListener::onTouchDown,\n                    onTwoFingersTap = gestureListener::onTwoFingersTap,\n                    onFling = { velocity -> gestureListener.onFling(flingSpec, velocity) },\n                    onFlingZoom = { centroid, velocity ->\n                        gestureListener.onFlingZoom(velocity, centroid)\n                    }\n                )\n            }\n            .pointerInput(gestureListener.isListeningForGestures()) {\n                if (!gestureListener.isListeningForGestures()) return@pointerInput\n                detectTapGestures(\n                    onTap = { offset -> gestureListener.onTap(offset) },\n                    onDoubleTap = { offset -> gestureListener.onDoubleTap(offset) },\n                    onDoubleTapZoom = { centroid, zoom ->\n                        gestureListener.onScaleRatio(zoom.toDouble(), centroid)\n                    },\n                    onDoubleTapZoomFling = { centroid, velocity ->\n                        gestureListener.onFlingZoom(velocity, centroid)\n                    },\n                    onPress = { gestureListener.onPress() },\n                    onLongPress = { offset -> gestureListener.onLongPress(offset) },\n                    shouldConsumeTap = { offset -> gestureListener.shouldConsumeTapGesture(offset) },\n                    shouldConsumeLongPress = { offset ->\n                        gestureListener.shouldConsumeLongPress(offset)\n                    }\n                )\n            }\n            .onSizeChanged {\n                layoutSizeChangeListener.onSizeChanged(scope, it)\n            }\n            .fillMaxSize(),\n    ) { measurables, constraints ->\n        val placeables = measurables.map { measurable ->\n            // Measure each children\n            measurable.measure(constraints)\n        }\n\n        // Set the size of the layout as big as it can\n        layout(constraints.maxWidth, constraints.maxHeight) {\n            // Place children in the parent layout\n            placeables.forEach { placeable ->\n                placeable.place(x = 0, y = 0)\n            }\n        }\n    }\n}\n\ninternal interface GestureListener {\n    fun onScaleRatio(scaleRatio: Double, centroid: Offset)\n    fun onRotationDelta(rotationDelta: Float)\n    fun onScrollDelta(scrollDelta: Offset)\n    fun onFling(flingSpec: DecayAnimationSpec<Offset>, velocity: Velocity)\n    fun onFlingZoom(velocity: Float, centroid: Offset)\n    fun onTouchDown()\n    fun onPress()\n    fun onTap(focalPt: Offset)\n    fun onDoubleTap(focalPt: Offset)\n    fun onTwoFingersTap(focalPt: Offset)\n    fun onLongPress(focalPt: Offset)\n    fun isListeningForGestures(): Boolean\n    fun shouldConsumeTapGesture(focalPt: Offset): Boolean\n    fun shouldConsumeLongPress(focalPt: Offset): Boolean\n}\n\ninternal interface LayoutSizeChangeListener {\n    fun onSizeChanged(composableScope: CoroutineScope, size: IntSize)\n}\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/markers/Clusterer.kt",
    "content": "package ovh.plrapps.mapcompose.ui.markers\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.IntSize\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport ovh.plrapps.mapcompose.api.BoundingBox\nimport ovh.plrapps.mapcompose.api.ClusterScaleThreshold\nimport ovh.plrapps.mapcompose.api.MarkerDataSnapshot\nimport ovh.plrapps.mapcompose.api.VisibleArea\nimport ovh.plrapps.mapcompose.api.fullSize\nimport ovh.plrapps.mapcompose.api.maxScale\nimport ovh.plrapps.mapcompose.api.referentialSnapshotFlow\nimport ovh.plrapps.mapcompose.api.scrollTo\nimport ovh.plrapps.mapcompose.api.visibleArea\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport ovh.plrapps.mapcompose.ui.state.markers.MarkerRenderState\nimport ovh.plrapps.mapcompose.ui.state.markers.model.ClusterClickBehavior\nimport ovh.plrapps.mapcompose.ui.state.markers.model.ClusterInfo\nimport ovh.plrapps.mapcompose.ui.state.markers.model.Custom\nimport ovh.plrapps.mapcompose.ui.state.markers.model.Default\nimport ovh.plrapps.mapcompose.ui.state.markers.model.MarkerData\nimport ovh.plrapps.mapcompose.ui.state.markers.model.MarkerType\nimport ovh.plrapps.mapcompose.ui.state.markers.model.None\nimport ovh.plrapps.mapcompose.ui.state.markers.model.RenderingStrategy\nimport ovh.plrapps.mapcompose.utils.contains\nimport ovh.plrapps.mapcompose.utils.dpToPx\nimport ovh.plrapps.mapcompose.utils.map\nimport ovh.plrapps.mapcompose.utils.throttle\nimport java.util.UUID\nimport kotlin.math.abs\nimport kotlin.math.absoluteValue\nimport kotlin.math.ceil\nimport kotlin.math.ln\nimport kotlin.math.pow\nimport kotlin.math.sqrt\n\ninternal class Clusterer(\n    val id: String,\n    clusteringThreshold: Dp,\n    private val mapState: MapState,\n    private val markerRenderState: MarkerRenderState,\n    markersDataFlow: MutableStateFlow<List<MarkerData>>,\n    private val clusterClickBehavior: ClusterClickBehavior,\n    private val scaleThreshold: ClusterScaleThreshold,\n    private val clusterFactory: (ids: List<String>) -> (@Composable () -> Unit)\n) {\n    private val scope = CoroutineScope(\n        mapState.scope.coroutineContext + SupervisorJob(mapState.scope.coroutineContext[Job])\n    )\n\n    /* Create a derived state flow from the original unique source of truth */\n    private val markers = markersDataFlow.map(scope) {\n        it.filter { markerData ->\n            (markerData.renderingStrategy is RenderingStrategy.Clustering) &&\n                    markerData.renderingStrategy.clustererId == id\n        }.map { markerData ->\n            Marker(markerData)\n        }\n    }\n\n    internal val exemptionSet = MutableStateFlow<Set<String>>(setOf())\n\n    private val referentialSnapshotFlow = mapState.referentialSnapshotFlow()\n    private val markersSnapshotFlow = snapshotFlow {\n        markerRenderState.getClusteredMarkers().map {\n            MarkerDataSnapshot(it.id, it.x, it.y)\n        }\n    }\n    private val clusterIdPrefix = \"#cluster#-$id\"\n    private val epsilon = dpToPx(clusteringThreshold.value)\n\n    init {\n        scope.launch {\n            // react on base data change\n            markers.throttle(100).collectLatest { markers ->\n                // react on marker move\n                markersSnapshotFlow.throttle(300).collectLatest {\n                    // react on scale and scroll change\n                    referentialSnapshotFlow.throttle(500).collectLatest {\n                        val scale = it.scale\n                        val padding = dpToPx(100f).toInt()\n                        val visibleArea = mapState.visibleArea(IntOffset(padding, padding))\n\n                        /* Get the list of rendered clusterer managed (by this clusterer) markers */\n                        val markersOnMap =\n                            markerRenderState.getClusteredMarkers().filter { markerData ->\n                                (markerData.renderingStrategy is RenderingStrategy.Clustering) &&\n                                        markerData.renderingStrategy.clustererId == id\n                            }\n\n                        exemptionSet.collectLatest { exemptionSet ->\n                            withContext(Dispatchers.Default) {\n                                clusterize(\n                                    scale,\n                                    visibleArea,\n                                    markers,\n                                    markersOnMap,\n                                    exemptionSet,\n                                    epsilon\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    fun onPlaceableClick(clusterData: MarkerData) {\n        if (clusterData.type !is MarkerType.Cluster) return\n        val markersData = clusterData.type.markersData\n        when (clusterClickBehavior) {\n            is Custom -> {\n                clusterClickBehavior.onClick(\n                    ClusterInfo(clusterData.x, clusterData.y, markersData)\n                )\n                if (clusterClickBehavior.withDefaultBehavior) {\n                    defaultClusterClickListener(markersData)\n                }\n            }\n\n            Default -> {\n                defaultClusterClickListener(markersData)\n            }\n\n            None -> {\n            }\n        }\n    }\n\n    /**\n     * The user might want to cancel a clusterer while keeping managed markers. For example,\n     * removing a clusterer and adding it back with the same id but with a different cluster style.\n     * This allows for replacing a clusterer without any visual blinks.\n     */\n    fun cancel(removeManaged: Boolean) {\n        scope.cancel()\n        if (removeManaged) {\n            markerRenderState.removeAllClusterManagedMarkers(id)\n        }\n    }\n\n    private suspend fun clusterize(\n        scale: Double,\n        visibleArea: VisibleArea,\n        markers: List<Marker>,\n        markersOnMap: List<MarkerData>,\n        exemptionSet: Set<String>,\n        epsilon: Float\n    ) = coroutineScope {\n        val visibleMarkers = markers.filter { marker ->\n            visibleArea.contains(marker.x, marker.y) && marker.id !in exemptionSet\n        }\n        val exempted = markers.filter { marker ->\n            marker.id in exemptionSet\n        }\n\n        /* Disable clustering if scale is greater than the threshold */\n        val maxScale = when (scaleThreshold) {\n            is ClusterScaleThreshold.FixedScale -> scaleThreshold.scale\n            ClusterScaleThreshold.MaxScale -> mapState.maxScale\n        }\n        val result = if (scale < maxScale) {\n            val densitySearchPass = processMarkers(markers, visibleMarkers, scale, epsilon)\n            mergeClosest(densitySearchPass, epsilon, scale)\n        } else {\n            ClusteringResult(markers = visibleMarkers)\n        }\n\n        withContext(Dispatchers.Main) {\n            render(markersOnMap, result.clusters, result.markers + exempted)\n        }\n    }\n\n    private fun render(\n        markersOnMap: List<MarkerData>,\n        clusters: List<Cluster>,\n        markers: List<Marker>\n    ) {\n        val clustersById = clusters.associateByTo(mutableMapOf()) { it.id }\n        val markersById = markers.associateByTo(mutableMapOf()) { it.uuid }\n\n        val clusterIds = mutableListOf<String>()\n        val markerIds = mutableListOf<UUID>()\n\n        markersOnMap.forEach { markerData ->\n            if (markerData.id.startsWith(clusterIdPrefix)) {\n                clusterIds.add(markerData.id)\n                val inMemory = clustersById[markerData.id]\n                if (inMemory == null) {\n                    markerRenderState.removeClustererManagedMarker(markerData.id)\n                } else {\n                    if (inMemory.x != markerData.x || inMemory.y != markerData.y) {\n                        mapState.markerState.moveMarkerTo(markerData, inMemory.x, inMemory.y)\n                    }\n                }\n            } else { // then it must be a marker\n                if (shouldProcessMarker(markerData)) {\n                    markerIds.add(markerData.uuid)\n                    val inMemory = markersById[markerData.uuid]\n                    if (inMemory == null) {\n                        markerRenderState.removeClustererManagedMarker(markerData.id)\n                    } else {\n                        if (inMemory.x != markerData.x || inMemory.y != markerData.y) {\n                            mapState.markerState.moveMarkerTo(markerData, inMemory.x, inMemory.y)\n                        }\n                    }\n                }\n            }\n        }\n\n        clustersById.entries.forEach {\n            if (it.key !in clusterIds) {\n                it.value.addToMap()\n            }\n        }\n        markersById.entries.forEach {\n            if (it.key !in markerIds) {\n                it.value.addToMap()\n            }\n        }\n    }\n\n    private fun processMarkers(\n        markers: List<Marker>, visibleMarkers: List<Marker>, scale: Double, epsilon: Float\n    ): ClusteringResult {\n        val snapScale = getSnapScale(scale)\n        val mesh = Mesh(epsilon, snapScale, mapState.fullSize)\n        visibleMarkers.forEach { marker ->\n            mesh.add(marker)\n        }\n\n        return findNewClustersByDensity(markers, mesh, scale, epsilon)\n    }\n\n    private fun findNewClustersByDensity(\n        markers: List<Marker>,\n        mesh: Mesh,\n        scale: Double,\n        epsilon: Float,\n    ): ClusteringResult {\n        /* Compute density for each window */\n        mesh.gridMap.keys.forEach { key ->\n            val neighbors = mesh.getNeighbors(key)\n            val window = mesh.gridMap[key]\n            if (window != null) {\n                window.density = window.markers.size + neighbors.sumOf { it.markers.size }\n            }\n        }\n\n        val entriesSorted = mesh.gridMap.entries.sortedByDescending {\n            it.value.density\n        }\n\n        val clusterList = mutableListOf<Cluster>()\n        val markerList = mutableListOf<Marker>()\n\n        val markerAssigned = markers.associateTo(mutableMapOf()) {\n            it.uuid to false\n        }\n\n        for (e in entriesSorted) {\n            val neighbors = mesh.getNeighbors(e.key)\n            val neighborsMarkers = neighbors.flatMap {\n                it.markers\n            }\n            val startBary = getBarycenter(e.value.markers) ?: break\n\n            val mergedMarkers = (e.value.markers + neighborsMarkers).filter { marker ->\n                distance(startBary, marker, scale) < epsilon && (markerAssigned[marker.uuid]\n                    ?: false).not()\n            }.onEach {\n                markerAssigned[it.uuid] = true\n            }\n\n            if (mergedMarkers.size == 1) {\n                markerList.add(mergedMarkers.first())\n                continue\n            }\n\n            val cluster = mergedMarkers.toCluster()\n\n            if (cluster.markers.isNotEmpty()) {\n                clusterList.add(cluster)\n            }\n        }\n\n        return ClusteringResult(clusterList, markerList)\n    }\n\n    private tailrec fun mergeClosest(\n        result: ClusteringResult,\n        epsilon: Float,\n        scale: Double\n    ): ClusteringResult {\n        fun findInVicinity(cluster: Cluster): Placeable? {\n            val closeEnoughMarker = result.markers.firstOrNull {\n                distance(cluster.x, cluster.y, it.x, it.y, scale) < epsilon\n            }\n            return closeEnoughMarker ?: result.clusters.firstOrNull { otherCluster ->\n                distance(otherCluster.x, otherCluster.y, cluster.x, cluster.y, scale) < epsilon\n                        && otherCluster != cluster\n            }\n        }\n\n        for (cluster in result.clusters) {\n            val inVicinity = findInVicinity(cluster)\n            if (inVicinity != null) {\n                return when (inVicinity) {\n                    is Cluster -> {\n                        val fusedCluster = fuseClusters(cluster, inVicinity)\n                        val newClusterList = result.clusters.filter {\n                            it != cluster && it != inVicinity\n                        } + fusedCluster\n                        mergeClosest(\n                            ClusteringResult(newClusterList, result.markers),\n                            epsilon,\n                            scale\n                        )\n                    }\n\n                    is Marker -> {\n                        val fusedCluster = cluster.addMarker(inVicinity)\n                        val newClusterList = result.clusters.filter {\n                            it != cluster\n                        } + fusedCluster\n                        val newMarkerList = result.markers.filter {\n                            it != inVicinity\n                        }\n                        mergeClosest(\n                            ClusteringResult(newClusterList, newMarkerList),\n                            epsilon,\n                            scale\n                        )\n                    }\n                }\n            }\n        }\n\n        return result\n    }\n\n    private fun getBarycenter(markers: List<Marker>): Barycenter? {\n        if (markers.isEmpty()) return null\n        return Barycenter(\n            x = markers.sumOf { it.x } / markers.size,\n            y = markers.sumOf { it.y } / markers.size,\n            weight = markers.size\n        )\n    }\n\n    private fun distance(b: Barycenter, marker: Marker, scale: Double): Double {\n        return distance(b.x, b.y, marker.x, marker.y, scale)\n    }\n\n    private fun distance(x1: Double, y1: Double, x2: Double, y2: Double, scale: Double): Double {\n        return sqrt(\n            (abs(x1 - x2) * mapState.fullSize.width * scale).pow(2) +\n                    (abs(y1 - y2) * mapState.fullSize.height * scale).pow(2),\n        )\n    }\n\n    private fun fuseClusters(cluster1: Cluster, cluster2: Cluster): Cluster {\n        val newMarkers = cluster1.markers + cluster2.markers\n        return newMarkers.toCluster()\n    }\n\n    private fun Cluster.addMarker(marker: Marker): Cluster {\n        val newMarkers = markers + marker\n        return newMarkers.toCluster()\n    }\n\n    private fun List<Marker>.toCluster(): Cluster {\n        return Cluster(\n            clusterIdPrefix = clusterIdPrefix,\n            x = sumOf { it.x } / size,\n            y = sumOf { it.y } / size,\n            markers = this\n        )\n    }\n\n    private fun Cluster.addToMap() {\n        val markersData = markers.map { it.markerData }\n        val markerData = makeClusterMarkerData(id, x, y, markersData) {\n            clusterFactory(markers.map { it.id })()\n        }\n        markerRenderState.addClustererManagedMarker(markerData)\n    }\n\n    private fun defaultClusterClickListener(markers: List<MarkerData>) {\n        if (markers.isEmpty()) return\n\n        /* Compute the bounding box */\n        var minX: Double = Double.MAX_VALUE\n        var maxX: Double = Double.MIN_VALUE\n        var minY: Double = Double.MAX_VALUE\n        var maxY: Double = Double.MIN_VALUE\n        markers.forEach {\n            minX = if (it.x < minX) it.x else minX\n            maxX = if (it.x > maxX) it.x else maxX\n            minY = if (it.y < minY) it.y else minY\n            maxY = if (it.y > maxY) it.y else maxY\n        }\n        val bb = BoundingBox(minX, minY, maxX, maxY)\n\n        scope.launch {\n            mapState.scrollTo(bb, padding = Offset(0.2f, 0.2f))\n        }\n    }\n\n    private fun shouldProcessMarker(markerData: MarkerData): Boolean {\n        return (markerData.renderingStrategy is RenderingStrategy.Clustering) &&\n                markerData.renderingStrategy.clustererId == id\n    }\n\n    private fun getSnapScale(scale: Double): Double = 2.0.pow(ceil(ln(scale) / ln(2.0)))\n\n    private fun Marker.addToMap() {\n        markerRenderState.addClustererManagedMarker(markerData)\n    }\n\n    private fun makeClusterMarkerData(\n        id: String,\n        x: Double,\n        y: Double,\n        markersData: List<MarkerData>,\n        c: @Composable () -> Unit\n    ): MarkerData {\n        return MarkerData(\n            id, x, y,\n            relativeOffset = Offset(-0.5f, -0.5f),\n            absoluteOffset = DpOffset.Zero,\n            zIndex = markersData.maxOfOrNull { it.zIndex } ?: 0f,\n            isConstrainedInBounds = true,\n            clickableAreaScale = Offset(1f, 1f),\n            clickableAreaCenterOffset = Offset(0f, 0f),\n            clickable = true,\n            renderingStrategy = RenderingStrategy.Clustering(this@Clusterer.id),\n            type = MarkerType.Cluster(clustererId = this@Clusterer.id, markersData),\n            c = c\n        )\n    }\n\n    private data class Barycenter(val x: Double, val y: Double, val weight: Int)\n\n    private data class ClusteringResult(\n        val clusters: List<Cluster> = emptyList(),\n        val markers: List<Marker> = emptyList()\n    )\n}\n\nprivate class Mesh(\n    private val meshSize: Float,\n    private val scale: Double,\n    private val fullSize: IntSize,\n) {\n    val gridMap = mutableMapOf<Key, MarkerWindow>()\n    val markers = mutableListOf<Marker>()\n\n    private fun getKey(marker: Marker, meshSize: Float, scale: Double): Key {\n        val relativeWidth = marker.x * fullSize.width * scale\n        val relativeHeight = marker.y * fullSize.height * scale\n\n        return Key(\n            row = (relativeWidth / meshSize).toInt(),\n            col = (relativeHeight / meshSize).toInt()\n        )\n    }\n\n    fun add(marker: Marker) {\n        val key = getKey(marker, meshSize, scale)\n        val window = gridMap[key]\n        if (window == null) {\n            gridMap[key] = MarkerWindow(mutableListOf(marker))\n        } else {\n            window.markers.add(marker)\n        }\n        markers.add(marker)\n    }\n\n    fun getNeighbors(key: Key): List<MarkerWindow> {\n        val neighborKeys = listOf(\n            Key(key.row - 1, key.col - 1),\n            Key(key.row - 1, key.col),\n            Key(key.row - 1, key.col + 1),\n            Key(key.row, key.col - 1),\n            Key(key.row, key.col + 1),\n            Key(key.row + 1, key.col - 1),\n            Key(key.row + 1, key.col),\n            Key(key.row + 1, key.col + 1),\n        )\n\n        return neighborKeys.mapNotNull {\n            gridMap[it]\n        }\n    }\n}\n\nprivate data class MarkerWindow(\n    val markers: MutableList<Marker>,\n    var density: Int = 0\n)\n\nprivate data class Key(val row: Int, val col: Int)\n\nprivate sealed interface Placeable\n\n/**\n * A marker can belong to one and only one cluster.\n */\nprivate data class Marker(\n    val markerData: MarkerData,\n) : Placeable {\n    val uuid: UUID\n        get() = markerData.uuid\n    val id: String\n        get() = markerData.id\n    val x: Double\n        get() = markerData.x\n    val y: Double\n        get() = markerData.y\n}\n\nprivate data class Cluster(\n    val clusterIdPrefix: String,\n    val x: Double,\n    val y: Double,\n    val markers: List<Marker>\n) : Placeable {\n    val id = buildString {\n        append(clusterIdPrefix)\n\n        /* Now we produce a hash based on the ids of the markers, and this hash *does not* depend\n         * on the order of the markers. */\n        val hashes = LongArray(markers.size)\n        for (i in markers.indices) {\n            hashes[i] = markers[i].id.hashCode().toLong()\n        }\n\n        hashes.sort()\n\n        var h = 0L\n        for (value in hashes) {\n            h = 31 * h + value\n        }\n\n        append(h.absoluteValue.toString(16))\n    }\n}\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/markers/LazyLoader.kt",
    "content": "package ovh.plrapps.mapcompose.ui.markers\n\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.IntOffset\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.collectLatest\nimport ovh.plrapps.mapcompose.api.referentialSnapshotFlow\nimport ovh.plrapps.mapcompose.api.visibleArea\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport ovh.plrapps.mapcompose.ui.state.markers.MarkerRenderState\nimport ovh.plrapps.mapcompose.ui.state.markers.model.MarkerData\nimport ovh.plrapps.mapcompose.ui.state.markers.model.RenderingStrategy\nimport ovh.plrapps.mapcompose.utils.contains\nimport ovh.plrapps.mapcompose.utils.dpToPx\nimport ovh.plrapps.mapcompose.utils.map\nimport ovh.plrapps.mapcompose.utils.throttle\nimport java.util.*\n\ninternal class LazyLoader(\n    private val id: String,\n    private val mapState: MapState,\n    private val markerRenderState: MarkerRenderState,\n    markersDataFlow: MutableStateFlow<List<MarkerData>>,\n    private val padding: Dp,\n    scope: CoroutineScope\n) {\n    private val referentialSnapshotFlow = mapState.referentialSnapshotFlow()\n    private val job: Job\n\n    /* Create a derived state flow from the original unique source of truth */\n    private val markers = markersDataFlow.map(scope) {\n        it.filter { markerData ->\n            (markerData.renderingStrategy is RenderingStrategy.LazyLoading)\n                    && markerData.renderingStrategy.lazyLoaderId == id\n        }\n    }\n\n    init {\n        job = scope.launch {\n            markers.throttle(100).collectLatest {\n                referentialSnapshotFlow.throttle(100).collectLatest {\n                    val padding = dpToPx(padding.value).toInt()\n                    val visibleArea = mapState.visibleArea(IntOffset(padding, padding))\n\n                    /* Get the list of lazy loaded markers */\n                    val markersOnMap =\n                        markerRenderState.getLazyLoadedMarkers().filter { markerData ->\n                            (markerData.renderingStrategy is RenderingStrategy.LazyLoading)\n                                    && markerData.renderingStrategy.lazyLoaderId == id\n                        }\n\n                    val visibleMarkers = withContext(Dispatchers.Default) {\n                        markers.value.filter { dataSnapshot ->\n                            visibleArea.contains(dataSnapshot.x, dataSnapshot.y)\n                        }\n                    }\n\n                    render(markersOnMap, visibleMarkers)\n                }\n            }\n        }\n    }\n\n    private fun render(\n        markersOnMap: List<MarkerData>,\n        markers: List<MarkerData>\n    ) {\n        val markersById = markers.associateByTo(mutableMapOf()) { it.uuid }\n        val markerIds = mutableListOf<UUID>()\n\n        markersOnMap.forEach { markerData ->\n            markerIds.add(markerData.uuid)\n            val inMemory = markersById[markerData.uuid]\n            if (inMemory == null) {\n                markerRenderState.removeLazyLoadedMarker(markerData.id)\n            } else {\n                if (inMemory.x != markerData.x || inMemory.y != markerData.y) {\n                    mapState.markerState.moveMarkerTo(markerData, inMemory.x, inMemory.y)\n                }\n            }\n        }\n\n        markersById.entries.forEach {\n            if (it.key !in markerIds) {\n                markerRenderState.addLazyLoadedMarker(it.value)\n            }\n        }\n    }\n\n    fun cancel(removeManaged: Boolean) {\n        job.cancel()\n        if (removeManaged) {\n            markerRenderState.removeAllLazyLoadedMarkers(id)\n        }\n    }\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/markers/MarkerComposer.kt",
    "content": "package ovh.plrapps.mapcompose.ui.markers\n\nimport androidx.compose.foundation.gestures.detectDragGestures\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.key\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.layoutId\nimport ovh.plrapps.mapcompose.api.moveMarkerBy\nimport ovh.plrapps.mapcompose.ui.state.markers.model.MarkerData\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport ovh.plrapps.mapcompose.ui.state.markers.MarkerRenderState\nimport ovh.plrapps.mapcompose.ui.state.ZoomPanRotateState\nimport ovh.plrapps.mapcompose.utils.rotateX\nimport ovh.plrapps.mapcompose.utils.rotateY\nimport ovh.plrapps.mapcompose.utils.toRad\n\n@Composable\ninternal fun MarkerComposer(\n    modifier: Modifier,\n    zoomPRState: ZoomPanRotateState,\n    markerRenderState: MarkerRenderState,\n    mapState: MapState\n) {\n    MarkerLayout(\n        modifier = modifier,\n        zoomPRState = zoomPRState,\n    ) {\n        for (data in markerRenderState.markers.value) {\n            /* Optimize re-compositions */\n            key(data.uuid) {\n                Box(\n                    Modifier\n                        .layoutId(data)\n                        .then(\n                            if (data.isDraggable) {\n                                Modifier.pointerInput(Unit) {\n                                    detectDragGestures(\n                                        onDragStart = {\n                                            val listener = data.dragStartListener\n                                            if (listener != null) {\n                                                invokeDragStartListener(data, zoomPRState, it)\n                                            }\n                                        },\n                                        onDragEnd = {\n                                            data.dragEndListener?.onDragEnd(data.id, data.x, data.y)\n                                        }\n                                    ) { change, dragAmount ->\n                                        change.consume()\n                                        val interceptor = data.dragInterceptor\n                                        if (interceptor != null) {\n                                            invokeDragInterceptor(\n                                                data,\n                                                zoomPRState,\n                                                dragAmount,\n                                                change.position\n                                            )\n                                        } else {\n                                            mapState.moveMarkerBy(data.id, dragAmount)\n                                        }\n                                    }\n                                }\n                            } else Modifier\n                        )\n                ) {\n                    data.c()\n                }\n            }\n        }\n        for (data in markerRenderState.callouts.values) {\n            /* Optimize re-compositions */\n            key(data.markerData.uuid) {\n                Box(\n                    Modifier\n                        .layoutId(data.markerData)\n                        .then(\n                            if (data.markerData.isClickable) {\n                                /**\n                                 * As of 2022/04, using Modifier.clickable causes a huge performance\n                                 * drop when the number of callouts exceeds a few dozens.\n                                 * Using pointerInput, we loose the ripple effect.\n                                 */\n                                Modifier.pointerInput(Unit) {\n                                    detectTapGestures(\n                                        onTap = {\n                                            markerRenderState.onCalloutClick(data.markerData)\n                                        }\n                                    )\n                                }\n                            } else Modifier\n                        )\n                ) {\n                    data.markerData.c()\n                }\n            }\n        }\n    }\n}\n\nprivate fun invokeDragStartListener(\n    data: MarkerData,\n    zoomPRState: ZoomPanRotateState,\n    position: Offset\n) {\n    /* Compute the pointer offset */\n    val origin = Offset(- data.measuredWidth * data.relativeOffset.x, - data.measuredHeight * data.relativeOffset.y)\n    val pointerOffset = position - origin\n    val angle = -zoomPRState.rotation.toRad()\n    val pointerOffsetRotated = Offset(\n        rotateX(pointerOffset.x.toDouble(), pointerOffset.y.toDouble(), angle).toFloat(),\n        rotateY(pointerOffset.x.toDouble(), pointerOffset.y.toDouble(), angle).toFloat()\n    )\n\n    val px = data.x + pointerOffsetRotated.x.toDouble() / (zoomPRState.fullWidth * zoomPRState.scale)\n    val py = data.y + pointerOffsetRotated.y.toDouble() / (zoomPRState.fullHeight * zoomPRState.scale)\n\n    data.dragStartListener?.onDragStart(\n        id = data.id,\n        x = data.x,\n        y = data.y,\n        px = if (data.isConstrainedInBounds) px.coerceIn(0.0, 1.0) else px,\n        py = if (data.isConstrainedInBounds) py.coerceIn(0.0, 1.0) else py\n    )\n}\n\nprivate fun invokeDragInterceptor(\n    data: MarkerData,\n    zoomPRState: ZoomPanRotateState,\n    deltaPx: Offset,\n    position: Offset\n) {\n    /* Compute the displacement */\n    val angle = -zoomPRState.rotation.toRad()\n    val dx = rotateX(deltaPx.x.toDouble(), deltaPx.y.toDouble(), angle)\n    val dy = rotateY(deltaPx.x.toDouble(), deltaPx.y.toDouble(), angle)\n\n    val deltaX = dx / (zoomPRState.fullWidth * zoomPRState.scale)\n    val deltaY = dy / (zoomPRState.fullHeight * zoomPRState.scale)\n\n    /* Compute the pointer offset */\n    val origin = Offset(- data.measuredWidth * data.relativeOffset.x, - data.measuredHeight * data.relativeOffset.y)\n    val pointerOffset = position - origin\n    val pointerOffsetRotated = Offset(\n        rotateX(pointerOffset.x.toDouble(), pointerOffset.y.toDouble(), angle).toFloat(),\n        rotateY(pointerOffset.x.toDouble(), pointerOffset.y.toDouble(), angle).toFloat()\n    )\n\n    val px = data.x + pointerOffsetRotated.x.toDouble() / (zoomPRState.fullWidth * zoomPRState.scale)\n    val py = data.y + pointerOffsetRotated.y.toDouble() / (zoomPRState.fullHeight * zoomPRState.scale)\n\n    data.dragInterceptor?.onMove(\n        id = data.id,\n        x = data.x,\n        y = data.y,\n        dx = deltaX,\n        dy = deltaY,\n        px = if (data.isConstrainedInBounds) px.coerceIn(0.0, 1.0) else px,\n        py = if (data.isConstrainedInBounds) py.coerceIn(0.0, 1.0) else py\n    )\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/markers/MarkerLayout.kt",
    "content": "package ovh.plrapps.mapcompose.ui.markers\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.layout.layoutId\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.IntOffset\nimport ovh.plrapps.mapcompose.ui.layout.grid\nimport ovh.plrapps.mapcompose.ui.state.ZoomPanRotateState\nimport ovh.plrapps.mapcompose.ui.state.markers.model.MarkerData\nimport ovh.plrapps.mapcompose.utils.rotateCenteredX\nimport ovh.plrapps.mapcompose.utils.rotateCenteredY\nimport ovh.plrapps.mapcompose.utils.toRad\nimport kotlin.math.ceil\n\n@Composable\ninternal fun MarkerLayout(\n    modifier: Modifier,\n    zoomPRState: ZoomPanRotateState,\n    content: @Composable () -> Unit\n) {\n    /* Scroll values may not be represented accurately using floats (a float has 7 significant\n     * decimal digits, so any number above ~10M isn't represented accurately).\n     * Since the translate function of the Canvas works with floats, we perform a change of\n     * referential so that we only need to translate the canvas by an amount which can be\n     * precisely represented as a float. */\n    val origin by remember {\n        derivedStateOf {\n            IntOffset(\n                ((ceil(zoomPRState.scrollX / grid) * grid)).toInt(),\n                ((ceil(zoomPRState.scrollY / grid) * grid)).toInt()\n            )\n        }\n    }\n\n    val density = LocalDensity.current\n    Layout(\n        content = content,\n        modifier\n            .graphicsLayer {\n                translationX = (-zoomPRState.scrollX + origin.x).toFloat()\n                translationY = (-zoomPRState.scrollY + origin.y).toFloat()\n            }\n            .background(Color.Transparent)\n            .fillMaxSize()\n    ) { measurables, constraints ->\n        val placeableCst = constraints.copy(minHeight = 0, minWidth = 0)\n\n        layout(constraints.maxWidth, constraints.maxHeight) {\n            for (measurable in measurables) {\n                val data = measurable.layoutId as? MarkerData ?: continue\n\n                /* Don't layout markers which are way out of display bounds, as it can can cause\n                 * jitter in marker rendering. */\n                if (data.isOutOfDisplay()) continue\n\n                val placeable = measurable.measure(placeableCst)\n                data.measuredWidth = placeable.measuredWidth\n                data.measuredHeight = placeable.measuredHeight\n\n                val widthOffset =\n                    placeable.measuredWidth * data.relativeOffset.x + with(density) { data.absoluteOffset.x.toPx() }\n                val heightOffset =\n                    placeable.measuredHeight * data.relativeOffset.y + with(density) { data.absoluteOffset.y.toPx() }\n\n                if (zoomPRState.rotation == 0f) {\n                    val x = data.x * zoomPRState.fullWidth * zoomPRState.scale + widthOffset\n                    val y = data.y * zoomPRState.fullHeight * zoomPRState.scale + heightOffset\n                    /* It's important to always update data even when visibility is set to false, so\n                     * click handling works on updated data (a non-visible marker might be clickable) */\n                    data.xPlacement = x\n                    data.yPlacement = y\n\n                    if (data.isVisible) {\n                        placeable.place((x - origin.x).toInt(), (y - origin.y).toInt(), zIndex = data.zIndex)\n                    }\n                } else {\n                    with(zoomPRState) {\n                        val angleRad = rotation.toRad()\n                        val xFullPx = data.x * fullWidth * scale\n                        val yFullPx = data.y * fullHeight * scale\n                        val centerX = centroidX * fullWidth * scale\n                        val centerY = centroidY * fullHeight * scale\n\n                        val x = rotateCenteredX(\n                            xFullPx,\n                            yFullPx,\n                            centerX,\n                            centerY,\n                            angleRad\n                        ) + widthOffset\n\n                        val y = rotateCenteredY(\n                            xFullPx,\n                            yFullPx,\n                            centerX,\n                            centerY,\n                            angleRad\n                        ) + heightOffset\n\n                        /* It's important to always update data even when visibility is set to false,\n                         * so click handling works on updated data (a non-visible marker might be\n                         * clickable) */\n                        data.xPlacement = x\n                        data.yPlacement = y\n\n                        if (data.isVisible) {\n                            placeable.place(\n                                (x - origin.x).toInt(),\n                                (y - origin.y).toInt(),\n                                zIndex = data.zIndex\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\nprivate fun MarkerData.isOutOfDisplay() = x < -1.0 || x > 2.0 || y < -1.0 || y > 2.0"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/paths/PathComposer.kt",
    "content": "package ovh.plrapps.mapcompose.ui.paths\n\nimport android.graphics.DashPathEffect\nimport android.graphics.Paint\nimport android.graphics.Path\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.produceState\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.drawIntoCanvas\nimport androidx.compose.ui.graphics.drawscope.scale\nimport androidx.compose.ui.graphics.drawscope.withTransform\nimport androidx.compose.ui.graphics.nativeCanvas\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.withContext\nimport ovh.plrapps.mapcompose.ui.layout.grid\nimport ovh.plrapps.mapcompose.ui.paths.model.Cap\nimport ovh.plrapps.mapcompose.ui.paths.model.PatternItem\nimport ovh.plrapps.mapcompose.ui.state.DrawablePathState\nimport ovh.plrapps.mapcompose.ui.state.PathState\nimport ovh.plrapps.mapcompose.ui.state.ZoomPanRotateState\nimport ovh.plrapps.mapcompose.utils.Point\nimport kotlin.math.abs\nimport kotlin.math.ceil\n\n@Composable\ninternal fun PathComposer(\n    modifier: Modifier,\n    zoomPRState: ZoomPanRotateState,\n    pathState: PathState\n) {\n    var drawOrder = 0\n    for (path in pathState.pathState.values.sortedBy { it.zIndex }) {\n        key(path.id) {\n            path.drawOrder.update { drawOrder++ }\n            PathCanvas(modifier, zoomPRState, path)\n        }\n    }\n}\n\n@Composable\ninternal fun PathCanvas(\n    modifier: Modifier,\n    zoomPRState: ZoomPanRotateState,\n    drawablePathState: DrawablePathState\n) {\n    val offsetAndCount = drawablePathState.offsetAndCount\n    val pathData = drawablePathState.pathData\n\n    /* Scroll values may not be represented accurately using floats (a float has 7 significant\n     * decimal digits, so any number above ~10M isn't represented accurately).\n     * Since the translate function of the Canvas works with floats, we perform a change of\n     * referential so that we only need to translate the canvas by an amount which can be\n     * precisely represented as a float.\n     * For paths, we also need to be mindful not to change the referential too often. */\n    val origin by produceState(\n        initialValue = IntOffset.Zero,\n        key1 = zoomPRState.scale,\n        key2 = zoomPRState.scrollX,\n        key3 = zoomPRState.scrollY\n    ) {\n        val scale = zoomPRState.scale\n\n        val formerX0 = value.x\n        val formerY0 = value.y\n        val x0 = ((ceil(zoomPRState.scrollX / grid) * grid) / scale).toInt()\n        val y0 = ((ceil(zoomPRState.scrollY / grid) * grid) / scale).toInt()\n\n        val shouldUpdate = (abs(x0 - formerX0) * scale > grid) ||\n                (abs(y0 - formerY0) * scale > grid)\n\n        if (shouldUpdate) {\n            value = IntOffset(x0, y0)\n        }\n    }\n\n    /* When epsilon changes, a new path is generated. */\n    val epsilon by remember {\n        derivedStateOf {\n            val scale = zoomPRState.scale\n            val simplify = drawablePathState.simplify\n            if (simplify == 0f) {\n                0.0\n            } else {\n                simplify / scale\n            }\n        }\n    }\n\n    val pathWithOrigin by produceState<PathWithOrigin?>(\n        /* Only affects the very first value.\n         * During the computation of a new value, the state holds the last computed value. */\n        initialValue = null,\n        keys = arrayOf(\n            pathData,\n            offsetAndCount,\n            epsilon,\n            origin,\n            drawablePathState.simplify\n        )\n    ) {\n        val x0 = origin.x\n        val y0 = origin.y\n        val ep = epsilon\n        value = withContext(Dispatchers.Default) {\n            generatePath(\n                pathData = pathData,\n                offset = offsetAndCount.x,\n                count = offsetAndCount.y,\n                epsilon = ep,\n                x0 = x0,\n                y0 = y0,\n                onNewDecimatedPath = { drawablePathState.currentDecimatedPath.value = it }\n            )\n        }\n    }\n\n    val path = pathWithOrigin ?: return\n\n    val widthPx = with(LocalDensity.current) {\n        drawablePathState.width.toPx()\n    }\n\n    val density = LocalDensity.current\n    val dashPathEffect = remember(drawablePathState.pattern, widthPx, zoomPRState.scale, density) {\n        drawablePathState.pattern?.let {\n            makePathEffect(it, strokeWidthPx = widthPx, scale = zoomPRState.scale.toFloat(), density)\n        }\n    }\n\n    val paint = remember(\n        dashPathEffect,\n        drawablePathState.color,\n        drawablePathState.cap,\n        widthPx,\n        zoomPRState.scale\n    ) {\n        Paint().apply {\n            style = Paint.Style.STROKE\n            strokeJoin = Paint.Join.ROUND\n            this.color = drawablePathState.color.toArgb()\n            strokeCap = when (drawablePathState.cap) {\n                Cap.Butt -> Paint.Cap.BUTT\n                Cap.Round -> Paint.Cap.ROUND\n                Cap.Square -> Paint.Cap.SQUARE\n            }\n            pathEffect = dashPathEffect\n            strokeWidth = (widthPx / zoomPRState.scale).toFloat()\n        }\n    }\n\n    val fillPaint = remember(\n        drawablePathState.fillColor,\n    ) {\n        Paint().apply {\n            style = Paint.Style.FILL\n            this.color = drawablePathState.fillColor?.toArgb() ?: Color.Transparent.toArgb()\n        }\n    }\n\n    Canvas(\n        modifier = modifier\n            .fillMaxSize()\n            .background(Color.Transparent)\n    ) {\n        withTransform({\n            /* Geometric transformations seem to be applied in reversed order of declaration */\n            rotate(\n                degrees = zoomPRState.rotation,\n                pivot = Offset(\n                    x = (zoomPRState.pivotX).toFloat(),\n                    y = (zoomPRState.pivotY).toFloat()\n                )\n            )\n            translate(\n                left = (-zoomPRState.scrollX + path.origin.x * zoomPRState.scale).toFloat(),\n                top = (-zoomPRState.scrollY + path.origin.y * zoomPRState.scale).toFloat()\n            )\n            scale(scale = zoomPRState.scale.toFloat(), Offset.Zero)\n        }) {\n            with(drawablePathState) {\n                if (visible) {\n                    drawIntoCanvas {\n                        if (drawablePathState.fillColor != null) {\n                            it.nativeCanvas.drawPath(path.path, fillPaint)\n                        }\n                        it.nativeCanvas.drawPath(path.path, paint)\n                    }\n                }\n            }\n        }\n    }\n}\n\n/**\n * Once an instance of [PathData] is created, [data] shall not have structural modifications for\n * subList to work (see [List.subList] doc). */\nclass PathData internal constructor(\n    internal val data: List<Point>,\n    internal val boundingBox: Pair<Point, Point>     // topLeft, bottomRight\n) {\n    val size: Int\n        get() = data.size\n}\n\n@Suppress(\"unused\")\nclass PathDataBuilder internal constructor(\n    private val fullWidth: Int,\n    private val fullHeight: Int\n) {\n    private val points = mutableListOf<Point>()\n    private var xMin: Double? = null\n    private var xMax: Double? = null\n    private var yMin: Double? = null\n    private var yMax: Double? = null\n\n    /**\n     * Add a point to the path. Values are relative coordinates (in range [0f..1f]).\n     */\n    @Synchronized\n    fun addPoint(x: Double, y: Double) = apply {\n        points.add(createPoint(x, y))\n    }\n\n    /**\n     * Add points to the path. Values are relative coordinates (in range [0f..1f]).\n     */\n    @Synchronized\n    fun addPoints(points: List<Pair<Double, Double>>) = apply {\n        this.points += points.map { (x, y) -> createPoint(x, y) }\n    }\n\n    private fun createPoint(x: Double, y: Double): Point {\n        return Point(x * fullWidth, y * fullHeight).also {\n            updateBoundingBox(it.x, it.y)\n        }\n    }\n\n    private fun updateBoundingBox(x: Double, y: Double) {\n        xMin = xMin?.coerceAtMost(x) ?: x\n        xMax = xMax?.coerceAtLeast(x) ?: x\n        yMin = yMin?.coerceAtMost(y) ?: y\n        yMax = yMax?.coerceAtLeast(y) ?: y\n    }\n\n    @Synchronized\n    fun build(): PathData? {\n        /* If there is only one point, the path has no sense */\n        if (points.size < 2) return null\n\n        val _xMin = xMin\n        val _xMax = xMax\n        val _yMin = yMin\n        val _yMax = yMax\n\n        val bb = if (_xMin != null && _xMax != null && _yMin != null && _yMax != null) {\n            Pair(Point(_xMin, _yMin), Point(_xMax, _yMax))\n        } else return null\n\n        /**\n         * Make a defensive copy (see PathData doc). We don't want structural modifications to\n         * [points] to be visible from the [PathData] instance. */\n        return PathData(points.toList(), bb)\n    }\n}\n\nprivate fun generatePath(\n    pathData: PathData,\n    offset: Int,\n    count: Int,\n    epsilon: Double,\n    x0: Int,\n    y0: Int,\n    onNewDecimatedPath: (decimatedPath: List<Point>) -> Unit\n): PathWithOrigin {\n    val p = Path()\n    val subList = pathData.data.subList(offset, offset + count)\n    val toRender = if (epsilon > 0f) {\n        runCatching {\n            val out = mutableListOf<Point>()\n            ramerDouglasPeucker(subList, epsilon, out)\n            onNewDecimatedPath(out)\n            out\n        }.getOrElse {\n            subList\n        }\n    } else subList\n\n    for ((i, point) in toRender.withIndex()) {\n        if (i == 0) {\n            p.moveTo((point.x - x0).toFloat(), (point.y - y0).toFloat())\n        } else {\n            p.lineTo((point.x - x0).toFloat(), (point.y - y0).toFloat())\n        }\n    }\n    return PathWithOrigin(p, IntOffset(x0, y0))\n}\n\ninternal fun makePathEffect(\n    pattern: List<PatternItem>,\n    strokeWidthPx: Float,\n    scale: Float,\n    density: Density\n): DashPathEffect? {\n    val data = makeIntervals(pattern, strokeWidthPx, scale, density) ?: return null\n    return DashPathEffect(data.intervals, data.phase)\n}\n\ninternal fun concatGap(pattern: List<PatternItem>): List<PatternItem> {\n    return buildList {\n        var gap = 0.dp\n        for (item in pattern) {\n            if (item is PatternItem.Gap) {\n                gap += item.length\n            } else {\n                if (gap.value > 0f) {\n                    add(PatternItem.Gap(gap))\n                }\n                gap = 0.dp\n                add(item)\n            }\n        }\n        if (gap.value > 0f) {\n            add(PatternItem.Gap(gap))\n        }\n    }\n}\n\ninternal fun makeIntervals(\n    pattern: List<PatternItem>,\n    strokeWidthPx: Float,\n    scale: Float,\n    density: Density\n): DashPathEffectData? {\n    if (pattern.isEmpty()) return null\n\n    // First, concat gaps\n    val concat = concatGap(pattern)\n\n    var phase = 0f\n    val firstItem = concat.firstOrNull() ?: return null\n    val trimmed = if (firstItem is PatternItem.Gap) {\n        phase = with(density) { firstItem.length.toPx() }\n        /* If first item is a gap, remember it as phase and move it to then end of the pattern and\n         * re-concat since the original last item may also be a gap. */\n        concatGap(concat.subList(1, concat.size) + firstItem)\n    } else {\n        concat\n    }\n\n    // If the pattern only contained a gap, ignore the pattern\n    if (trimmed.isEmpty()) return null\n\n    fun MutableList<Float>.addOffInterval(prev: PatternItem) {\n        if (prev is PatternItem.Gap) {\n            add((strokeWidthPx + with(density) { prev.length.toPx() }) / scale)\n        } else {\n            add(strokeWidthPx / scale)\n        }\n    }\n\n    val intervals: FloatArray = buildList {\n        var previousItem: PatternItem? = null\n        // At this stage, trimmed starts either with a Dot or a Dash\n        for (item in trimmed) {\n            val toAdd = when (item) {\n                is PatternItem.Dash -> with(density) { item.length.toPx() } / scale\n                PatternItem.Dot -> 1f\n                is PatternItem.Gap -> null\n            }\n\n            if (toAdd != null) {\n                /* If previous item isn't null, then we're adding a value at an odd index */\n                previousItem?.also { prev ->\n                    addOffInterval(prev)\n                }\n                add(toAdd)\n            }\n            previousItem = item\n        }\n\n        previousItem?.also { prev ->\n            addOffInterval(prev)\n        }\n    }.toFloatArray()\n\n    return DashPathEffectData(intervals, phase)\n}\n\nprivate data class PathWithOrigin(val path: Path, val origin: IntOffset)\n\ninternal class DashPathEffectData(val intervals: FloatArray, val phase: Float)"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/paths/RamerDouglaPeucker.kt",
    "content": "package ovh.plrapps.mapcompose.ui.paths\n\nimport ovh.plrapps.mapcompose.utils.Point\nimport kotlin.math.hypot\n\n\ninternal fun ramerDouglasPeucker(pointList: List<Point>, epsilon: Double, out: MutableList<Point>) {\n    if (pointList.size < 2) throw IllegalArgumentException(\"Not enough points to simplify\")\n\n    // Find the point with the maximum distance from line between start and end\n    var dmax = 0.0\n    var index = 0\n    val end = pointList.size - 1\n    for (i in 1 until end) {\n        val d = perpendicularDistance(pointList[i], pointList[0], pointList[end])\n        if (d > dmax) { index = i; dmax = d }\n    }\n\n    // If max distance is greater than epsilon, recursively simplify\n    if (dmax > epsilon) {\n        val recResults1 = mutableListOf<Point>()\n        val recResults2 = mutableListOf<Point>()\n        val firstLine = pointList.take(index + 1)\n        val lastLine  = pointList.drop(index)\n        ramerDouglasPeucker(firstLine, epsilon, recResults1)\n        ramerDouglasPeucker(lastLine, epsilon, recResults2)\n\n        // build the result list\n        out.addAll(recResults1.take(recResults1.size - 1))\n        out.addAll(recResults2)\n        if (out.size < 2) throw RuntimeException(\"Problem assembling output\")\n    }\n    else {\n        // Just return start and end points\n        out.clear()\n        out.add(pointList.first())\n        out.add(pointList.last())\n    }\n}\n\nprivate fun perpendicularDistance(pt: Point, lineStart: Point, lineEnd: Point): Double {\n    var dx = lineEnd.x - lineStart.x\n    var dy = lineEnd.y - lineStart.y\n\n    // Normalize\n    val mag = hypot(dx, dy)\n    if (mag > 0.0) { dx /= mag; dy /= mag }\n    val pvx = pt.x - lineStart.x\n    val pvy = pt.y - lineStart.y\n\n    // Get dot product (project pv onto normalized direction)\n    val pvdot = dx * pvx + dy * pvy\n\n    // Scale line direction vector and substract it from pv\n    val ax = pvx - pvdot * dx\n    val ay = pvy - pvdot * dy\n\n    return hypot(ax, ay)\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/paths/model/Cap.kt",
    "content": "package ovh.plrapps.mapcompose.ui.paths.model\n\nenum class Cap  {\n    /** The stroke ends with the path, and does not project beyond it. */\n    Butt,\n\n    /**\n     * The stroke projects out as a semicircle, with the center at the end of the path.\n     */\n    Round,\n\n    /**\n     * The stroke projects out as a square, with the center at the end of the path.\n     */\n    Square\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/paths/model/PatternItem.kt",
    "content": "package ovh.plrapps.mapcompose.ui.paths.model\n\nimport androidx.compose.ui.unit.Dp\n\nsealed interface PatternItem {\n    data class Dash(val length: Dp): PatternItem\n    data object Dot: PatternItem\n    data class Gap(val length: Dp): PatternItem\n}\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/state/MapState.kt",
    "content": "package ovh.plrapps.mapcompose.ui.state\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.cancel\nimport ovh.plrapps.mapcompose.core.GestureConfiguration\nimport ovh.plrapps.mapcompose.core.Viewport\nimport ovh.plrapps.mapcompose.core.VisibleTilesResolver\nimport ovh.plrapps.mapcompose.core.throttle\nimport ovh.plrapps.mapcompose.ui.gestures.model.HitType\nimport ovh.plrapps.mapcompose.ui.layout.Fit\nimport ovh.plrapps.mapcompose.ui.layout.MinimumScaleMode\nimport ovh.plrapps.mapcompose.ui.state.markers.MarkerRenderState\nimport ovh.plrapps.mapcompose.ui.state.markers.MarkerState\nimport ovh.plrapps.mapcompose.utils.AngleDegree\nimport ovh.plrapps.mapcompose.utils.toRad\n\n/**\n * The state of the map. All public APIs are extensions functions or extension properties of this\n * class.\n *\n * @param levelCount The number of levels in the pyramid.\n * @param fullWidth The width in pixels of the map at scale 1f.\n * @param fullHeight The height in pixels of the map at scale 1f.\n * @param tileSize The size in pixels of tiles, which are expected to be squared. Defaults to 256.\n * @param workerCount The thread count used to fetch tiles. Defaults to the number of cores minus\n * one, which works well for tiles in the file system or in a local database. However, that number\n * should be increased to 16 or more for remote tiles (HTTP requests).\n * @param initialValuesBuilder A builder for [InitialValues] which are applied during [MapState]\n * initialization. Note that the provided lambda should not start any coroutines.\n */\nclass MapState(\n    levelCount: Int,\n    fullWidth: Int,\n    fullHeight: Int,\n    tileSize: Int = 256,\n    workerCount: Int = Runtime.getRuntime().availableProcessors() - 1,\n    initialValuesBuilder: InitialValues.() -> Unit = {}\n) : ZoomPanRotateStateListener {\n    private val initialValues = InitialValues().apply(initialValuesBuilder)\n    internal val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)\n    internal val zoomPanRotateState = ZoomPanRotateState(\n        fullWidth = fullWidth,\n        fullHeight = fullHeight,\n        stateChangeListener = this,\n        minimumScaleMode = initialValues.minimumScaleMode,\n        maxScale = initialValues.maxScale,\n        scale = initialValues.scale,\n        rotation = initialValues.rotation,\n        gestureConfiguration = initialValues.gestureConfiguration,\n        infiniteScrollX = initialValues.infiniteScrollX\n    )\n    internal val markerRenderState = MarkerRenderState()\n    internal val markerState = MarkerState(scope, markerRenderState)\n    internal val pathState = PathState(fullWidth, fullHeight)\n    internal val visibleTilesResolver =\n        VisibleTilesResolver(\n            levelCount = levelCount,\n            fullWidth = fullWidth,\n            fullHeight = fullHeight,\n            tileSize = tileSize,\n            magnifyingFactor = initialValues.magnifyingFactor,\n            infiniteScrollX = initialValues.infiniteScrollX\n        ) {\n            zoomPanRotateState.scale\n        }\n    internal val tileCanvasState = TileCanvasState(\n        scope,\n        tileSize,\n        visibleTilesResolver,\n        workerCount,\n        initialValues.highFidelityColors\n    )\n\n    private val throttledTask = scope.throttle(wait = 18) {\n        renderVisibleTiles()\n    }\n    private val viewport = Viewport()\n    internal var preloadingPadding: Int = initialValues.preloadingPadding\n    internal val tileSize by mutableIntStateOf(tileSize)\n    internal var stateChangeListener: (MapState.() -> Unit)? = null\n    internal var touchDownCb: (() -> Unit)? = null\n    internal var tapCb: LayoutTapCb? = null\n    internal var longPressCb: LayoutTapCb? = null\n    internal var mapBackground by mutableStateOf(Color.Transparent)\n    internal var isFilteringBitmap: () -> Boolean by mutableStateOf(\n        { initialValues.isFilteringBitmap(this) }\n    )\n    private var consumeLateInitialValues: () -> Unit = {\n        consumeLateInitialValues = {}\n        applyLateInitialValues(initialValues)\n    }\n\n    /**\n     * Cancels all internal tasks.\n     * After this call, this [MapState] is unusable.\n     */\n    @Suppress(\"unused\")\n    fun shutdown() {\n        scope.cancel()\n        tileCanvasState.shutdown()\n        pathState.removeAllPaths()\n        markerState.removeAllMarkers()\n    }\n\n    override fun onStateChanged() {\n        consumeLateInitialValues()\n\n        renderVisibleTilesThrottled()\n        stateChangeListener?.invoke(this)\n    }\n\n    override fun onTouchDown() {\n        touchDownCb?.invoke()\n    }\n\n    override fun onPress() {\n        markerRenderState.removeAllAutoDismissCallouts()\n    }\n\n    override fun onLongPress(x: Double, y: Double) {\n        longPressCb?.invoke(x, y)\n    }\n\n    override fun onTap(x: Double, y: Double) {\n        tapCb?.invoke(x, y)\n    }\n\n    override fun detectsTap(): Boolean = tapCb != null\n\n    override fun detectsLongPress(): Boolean = longPressCb != null\n\n    override fun interceptsTap(x: Double, y: Double, xPx: Int, yPx: Int): Boolean {\n        val markerHandled = markerState.onHit(xPx, yPx, hitType = HitType.Click)\n        val pathHandled = if (!markerHandled) {\n            pathState.onHit(x, y, zoomPanRotateState.scale, hitType = HitType.Click)\n        } else false\n\n        return markerHandled || pathHandled\n    }\n\n    override fun interceptsLongPress(x: Double, y: Double, xPx: Int, yPx: Int): Boolean {\n        val markerHandled = markerState.onHit(xPx, yPx, hitType = HitType.LongPress)\n        val pathHandled = if (!markerHandled) {\n            pathState.onHit(x, y, zoomPanRotateState.scale, hitType = HitType.LongPress)\n        } else false\n\n        return markerHandled || pathHandled\n    }\n\n    internal fun renderVisibleTilesThrottled() {\n        throttledTask.trySend(Unit)\n    }\n\n    private suspend fun renderVisibleTiles() {\n        val viewport = updateViewport()\n        tileCanvasState.setViewport(viewport)\n    }\n\n    private fun updateViewport(): Viewport {\n        val padding = preloadingPadding\n        return viewport.apply {\n            left = zoomPanRotateState.scrollX.toInt() - padding\n            top = zoomPanRotateState.scrollY.toInt() - padding\n            right = left + zoomPanRotateState.layoutSize.width + padding * 2\n            bottom = top + zoomPanRotateState.layoutSize.height + padding * 2\n            angleRad = zoomPanRotateState.rotation.toRad()\n        }\n    }\n\n    /**\n     * Apply \"late\" initial values - e.g, those which depend on the layout size.\n     * For the moment, the scroll is the only one.\n     */\n    private fun applyLateInitialValues(initialValues: InitialValues) {\n        with(zoomPanRotateState) {\n            val offsetX = initialValues.screenOffset.x * layoutSize.width\n            val offsetY = initialValues.screenOffset.y * layoutSize.height\n\n            val destScrollX = initialValues.x * fullWidth * scale + offsetX\n            val destScrollY = initialValues.y * fullHeight * scale + offsetY\n\n            setScroll(destScrollX, destScrollY)\n        }\n    }\n}\n\n/**\n * Builder for initial values.\n * Changes made after the `MapState` instance creation take precedence over initial values.\n * In the following example, the init scale will be 4.0 since the max scale is later set to 4.0.\n *\n * ```\n * MapState(4, 4096, 4096,\n *   initialValues = InitialValues().scale(8.0)\n * ).apply {\n *   addLayer(tileStreamProvider)\n *   maxScale = 4.0\n * }\n * ```\n */\n@Suppress(\"unused\")\nclass InitialValues internal constructor() {\n    internal var x = 0.5\n    internal var y = 0.5\n    internal var screenOffset: Offset = Offset(-0.5f, -0.5f)\n    internal var scale: Double = 1.0\n    internal var minimumScaleMode: MinimumScaleMode = Fit\n    internal var maxScale: Double = 2.0\n    internal var rotation: AngleDegree = 0f\n    internal var magnifyingFactor = 0\n    internal var infiniteScrollX = false\n    internal var highFidelityColors: Boolean = true\n    internal var preloadingPadding: Int = 0\n    internal var isFilteringBitmap: (MapState) -> Boolean = { true }\n    internal var gestureConfiguration: GestureConfiguration = GestureConfiguration()\n\n    /**\n     * Init the scroll position. Defaults to centering on the provided scroll destination.\n     *\n     * @param x The normalized X position on the map, in range [0..1]\n     * @param y The normalized Y position on the map, in range [0..1]\n     * @param screenOffset Offset of the screen relatively to its dimension. Default is\n     * Offset(-0.5f, -0.5f), so moving the screen by half the width left and by half the height top,\n     * effectively centering on the scroll destination.\n     */\n    fun scroll(x: Double, y: Double, screenOffset: Offset = Offset(-0.5f, -0.5f)) = apply {\n        this.screenOffset = screenOffset\n        this.x = x\n        this.y = y\n    }\n\n    /**\n     * Set the initial scale. Defaults to 1.0.\n     */\n    fun scale(scale: Double) = apply {\n        this.scale = scale\n    }\n\n    /**\n     * Set the [MinimumScaleMode]. Defaults to [Fit].\n     */\n    fun minimumScaleMode(minimumScaleMode: MinimumScaleMode) = apply {\n        this.minimumScaleMode = minimumScaleMode\n    }\n\n    /**\n     * Set the maximum allowed scale. Defaults to 2.0.\n     */\n    fun maxScale(maxScale: Double) = apply {\n        this.maxScale = maxScale\n    }\n\n    /**\n     * Set the initial rotation. Defaults to 0° (no rotation).\n     */\n    fun rotation(rotation: AngleDegree) = apply {\n        this.rotation = rotation\n    }\n\n    /**\n     * Alters the level at which tiles are picked for a given scale. By default, the level\n     * immediately higher (in index) is picked, to avoid sub-sampling. This corresponds to a\n     * [magnifyingFactor] of 0. The value 1 will result in picking the current level at a given\n     * scale, which will be at a relative scale between 1.0 and 2.0\n     */\n    fun magnifyingFactor(magnifyingFactor: Int) = apply {\n        this.magnifyingFactor = magnifyingFactor.coerceAtLeast(0)\n    }\n\n    /**\n     * On API level 29 and above, HARDWARE bitmaps are used and this api is irrelevant.\n     * On API 28 and below, by default bitmaps are loaded using ARGB_8888, which is best suited for\n     * most usages.\n     * However, if you're only loading images without alpha channel and high fidelity color isn't\n     * a requirement, RGB_565 can be used instead for less memory usage (by setting this to false).\n     * Beware, however, that some types of images can't be loaded using RGB_565 (such as PNGs with\n     * alpha channel). Unless you know what you're doing, let this parameter be true.\n     */\n    fun highFidelityColors(enabled: Boolean) = apply {\n        this.highFidelityColors = enabled\n    }\n\n    /**\n     * By default, only visible tiles are loaded. By adding a preloadingPadding additional tiles\n     * will be loaded, which can be used to produce a seamless tile loading effect.\n     *\n     * @param padding in pixels\n     */\n    fun preloadingPadding(padding: Int) = apply {\n        this.preloadingPadding = padding.coerceAtLeast(0)\n    }\n\n    /**\n     * Controls whether Bitmap filtering is enabled when drawing tiles. This is enabled by default.\n     * Disabling it is useful to achieve nearest-neighbor scaling, for cases when the art style of\n     * the displayed image benefits from it.\n     * @see [android.graphics.Paint.setFilterBitmap]\n     */\n    fun bitmapFilteringEnabled(enabled: Boolean) = apply {\n        bitmapFilteringEnabled { enabled }\n    }\n\n    /**\n     * A version of [bitmapFilteringEnabled] which allows for dynamic control of bitmap filtering\n     * depending on the current [MapState].\n     */\n    fun bitmapFilteringEnabled(predicate: (state: MapState) -> Boolean) = apply {\n        isFilteringBitmap = predicate\n    }\n\n    /**\n     * Customize gestures.\n     */\n    fun configureGestures(gestureConfigurationBlock: GestureConfiguration.() -> Unit) {\n        this.gestureConfiguration.gestureConfigurationBlock()\n    }\n\n    /**\n     * Enable infinite scroll on x-axis. When enabled, the scroll offset ratio in x dimension has\n     * no effect.\n     */\n    fun infiniteScrollX(enabled: Boolean) {\n        infiniteScrollX = enabled\n    }\n}\n\ninternal typealias LayoutTapCb = (x: Double, y: Double) -> Unit"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/state/PathState.kt",
    "content": "package ovh.plrapps.mapcompose.ui.state\n\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateMapOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport ovh.plrapps.mapcompose.ui.gestures.model.HitType\nimport ovh.plrapps.mapcompose.ui.paths.PathData\nimport ovh.plrapps.mapcompose.ui.paths.model.Cap\nimport ovh.plrapps.mapcompose.ui.paths.model.PatternItem\nimport ovh.plrapps.mapcompose.utils.Point\nimport ovh.plrapps.mapcompose.utils.dpToPx\nimport ovh.plrapps.mapcompose.utils.getDistance\nimport ovh.plrapps.mapcompose.utils.getDistanceFromBox\nimport ovh.plrapps.mapcompose.utils.getNearestPoint\nimport ovh.plrapps.mapcompose.utils.isInsideBox\n\ninternal class PathState(\n    val fullWidth: Int,\n    val fullHeight: Int\n) {\n    val pathState = mutableStateMapOf<String, DrawablePathState>()\n\n    var pathClickCb: PathClickCb? = null\n    var pathHitTraversalCb: PathHitTraversalCb? = null\n    var pathLongPressCb: PathClickCb? = null\n\n    private val hasClickable = derivedStateOf {\n        pathState.values.any {\n            it.isClickable\n        }\n    }\n\n    fun addPath(\n        id: String,\n        path: PathData,\n        width: Dp?,\n        color: Color?,\n        fillColor: Color?,\n        offset: Int?,\n        count: Int?,\n        cap: Cap,\n        simplify: Float?,\n        clickable: Boolean,\n        zIndex: Float,\n        pattern: List<PatternItem>?\n    ) {\n        if (hasPath(id)) return\n        pathState[id] = DrawablePathState(id, path, width, color,fillColor, offset, count, cap, simplify, clickable, zIndex, pattern)\n    }\n\n    fun removePath(id: String): Boolean {\n        return pathState.remove(id) != null\n    }\n\n    fun removeAllPaths() {\n        pathState.clear()\n    }\n\n    fun removePaths(predicate: (String) -> Boolean) {\n        val iter = pathState.iterator()\n        for ((id, _) in iter) {\n            if (predicate(id)) iter.remove()\n        }\n    }\n\n    fun updatePath(\n        id: String,\n        pathData: PathData? = null,\n        visible: Boolean? = null,\n        width: Dp? = null,\n        color: Color? = null,\n        fillColor: Color? = null,\n        offset: Int? = null,\n        count: Int? = null,\n        cap: Cap? = null,\n        simplify: Float? = null,\n        clickable: Boolean? = null,\n        zIndex: Float? = null,\n        pattern: List<PatternItem>? = null\n    ) {\n        pathState[id]?.apply {\n            val path = this\n            pathData?.also {\n                path.pathData = it\n                resetOffsetAndCount()\n            }\n            visible?.also { path.visible = it }\n            width?.also { path.width = it }\n            color?.also { path.color = it }\n            fillColor?.also { path.fillColor = it }\n            cap?.also { path.cap = it }\n            simplify?.also { path.simplify = it.coerceAtLeast(0f) }\n            if (offset != null || count != null) {\n                offsetAndCount = coerceOffsetAndCount(offset, count)\n            }\n            clickable?.also { path.isClickable = it }\n            zIndex?.also { path.zIndex = it }\n            pattern?.also { path.pattern = it }\n        }\n    }\n\n    fun hasPath(id: String): Boolean {\n        return pathState.keys.contains(id)\n    }\n\n    /**\n     * [x], [y] are the relative coordinates of the tap.\n     */\n    fun onHit(x: Double, y: Double, scale: Double, hitType: HitType): Boolean {\n        if (!hasClickable.value) return false\n\n        /* Compute pixel coordinates, at scale 1 because path coordinates (see below) are at scale 1 */\n        val xPx = x * fullWidth\n        val yPx = y * fullHeight\n\n        val radius = dpToPx(12f)\n        val threshold = radius / scale\n\n        val traversalClickIds = mutableListOf<String>()\n        var traversalClickPosition: Point? = null\n        val candidates = pathState.entries\n            .filter { it.value.isClickable }\n            /* Sort by descending draw order and not just z-index, because z-index is an application\n             * concept and for two paths with the same z-index, the draw order is undetermined.\n             * The draw order is a low level information, unknown to the application. */\n            .sortedByDescending { it.value.drawOrder.value }\n\n        for ((id, pathState) in candidates) {\n\n            val bb = pathState.pathData.boundingBox\n            val (topLeft, bottomRight) = bb\n            val (xMin, yMin) = topLeft\n            val (xMax, yMax) = bottomRight\n\n            /* Don't compute the nearest point for a point outside of the bounding box and with a\n             * distance to the bounding box greater than the threshold */\n            if (!isInsideBox(xPx, yPx, xMin, xMax, yMin, yMax) && getDistanceFromBox(xPx, yPx, xMin, xMax, yMin, yMax) > threshold) {\n                continue\n            }\n\n            var d = Double.MAX_VALUE\n            var nearestP1: Point? = null\n            var nearestP2: Point? = null\n\n            val points = pathState.currentDecimatedPath.value ?: pathState.pathData.data\n            for (i in points.indices) {\n                if (i + 1 == points.size) break\n                val p1 = points[i]\n                val p2 = points[i + 1]\n                val dist = getDistance(xPx, yPx, p1.x, p1.y, p2.x, p2.y)\n                if (dist < threshold && dist < d) {\n                    d = dist\n                    nearestP1 = p1\n                    nearestP2 = p2\n                }\n            }\n\n            if (nearestP1 != null && nearestP2 != null) {\n                val nearest =\n                    getNearestPoint(xPx, yPx, nearestP1.x, nearestP1.y, nearestP2.x, nearestP2.y)\n                val xOnPath = nearest.x / fullWidth\n                val yOnPath = nearest.y / fullHeight\n\n                if (pathHitTraversalCb == null) {\n                    when (hitType) {\n                        HitType.Click -> pathClickCb?.invoke(id, xOnPath, yOnPath)\n                        HitType.LongPress -> pathLongPressCb?.invoke(id, xOnPath, yOnPath)\n                    }\n                    return true\n                } else {\n                    traversalClickIds.add(id)\n                    if (traversalClickPosition == null) {\n                        traversalClickPosition = Point(xOnPath, yOnPath)\n                    }\n                }\n            }\n        }\n\n        return if (pathHitTraversalCb == null) {\n            false\n        } else {\n            if (traversalClickIds.isNotEmpty()) {\n                val pos = traversalClickPosition\n                if (pos != null) {  // should always be true\n                    pathHitTraversalCb?.invoke(traversalClickIds, pos.x, pos.y, hitType)\n                }\n                true\n            } else false\n        }\n    }\n\n    /**\n     * The scale doesn't matter as all computations are done at scale 1.\n     */\n    fun isPathWithinRange(id: String, rangePx: Int, x: Double, y: Double): Boolean {\n        val drawablePathState = pathState[id] ?: return false\n\n        /* Compute pixel coordinates, at scale 1 because path coordinates (see below) are at scale 1 */\n        val xPx = x * fullWidth\n        val yPx = y * fullHeight\n\n        for (i in 0 until drawablePathState.pathData.data.size) {\n            if (i + 1 == drawablePathState.pathData.data.size) break\n            val p1 = drawablePathState.pathData.data[i]\n            val p2 = drawablePathState.pathData.data[i + 1]\n            val dist = getDistance(xPx, yPx, p1.x, p1.y, p2.x, p2.y)\n            if (dist < rangePx) {\n                return true\n            }\n        }\n\n        return false\n    }\n}\n\ninternal class DrawablePathState(\n    val id: String,\n    pathData: PathData,\n    width: Dp?,\n    color: Color?,\n    fillColor: Color?,\n    offset: Int?,\n    count: Int?,\n    cap: Cap,\n    simplify: Float?,\n    clickable: Boolean,\n    zIndex: Float,\n    pattern: List<PatternItem>?\n) {\n    /* Using a StateFlow mainly for its thread-safety (value is written off ui thread, and we do\n     * not want/need to switch to the main thread while updating this value) */\n    val currentDecimatedPath = MutableStateFlow<List<Point>?>(null)\n    var pathData by mutableStateOf(pathData)\n    var visible by mutableStateOf(true)\n    var width: Dp by mutableStateOf(width ?: 4.dp)\n    var color: Color by mutableStateOf(color ?: Color(0xFF448AFF))\n    var fillColor: Color? by mutableStateOf(fillColor)\n    var cap: Cap by mutableStateOf(cap)\n    var isClickable: Boolean by mutableStateOf(clickable)\n    var zIndex: Float by mutableFloatStateOf(zIndex)\n    var pattern: List<PatternItem>? by mutableStateOf(pattern)\n\n    /**\n     * The \"count\" is the number of values in [pathData] to process, after skipping \"offset\" of them.\n     */\n    var offsetAndCount: IntOffset by mutableStateOf(initializeOffsetAndCount(offset, count))\n    var simplify: Float by mutableFloatStateOf(simplify?.coerceAtLeast(0f) ?: 1f)\n\n    val drawOrder = MutableStateFlow(0)\n\n    fun resetOffsetAndCount() {\n        offsetAndCount = IntOffset(0, pathData.data.size)\n    }\n\n    private fun initializeOffsetAndCount(offset: Int?, cnt: Int?): IntOffset {\n        val ofst = offset?.coerceIn(0, pathData.data.size) ?: 0\n        val count = (cnt ?: pathData.data.size).coerceIn(\n            0, (pathData.data.size - ofst)\n        )\n        return IntOffset(ofst, count)\n    }\n\n    /**\n     * Ensure that \"count\" + \"offset\" shouldn't exceed the path length.\n     */\n    fun coerceOffsetAndCount(offset: Int?, cnt: Int?): IntOffset {\n        val ofst = (offset ?: offsetAndCount.x).coerceIn(0, pathData.data.size)\n        val count = (cnt ?: offsetAndCount.y).coerceIn(0, (pathData.data.size - ofst))\n        return IntOffset(ofst, count)\n    }\n\n    override fun hashCode(): Int {\n        var hash = id.hashCode()\n        hash += 31 * pathData.data.size\n\n        return hash\n    }\n\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (javaClass != other?.javaClass) return false\n\n        other as DrawablePathState\n        if (id != other.id) return false\n        if (pathData.data.size != other.pathData.data.size) return false\n\n        return true\n    }\n}\n\ninternal typealias PathClickCb = (id: String, x: Double, y: Double) -> Unit\ninternal typealias PathHitTraversalCb = (ids: List<String>, x: Double, y: Double, hitType: HitType) -> Unit"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/state/TileCanvasState.kt",
    "content": "package ovh.plrapps.mapcompose.ui.state\n\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.channels.ReceiveChannel\nimport kotlinx.coroutines.flow.*\nimport ovh.plrapps.mapcompose.core.*\nimport java.util.concurrent.Executors\nimport kotlin.math.pow\nimport kotlin.time.TimeSource\n\n/**\n * This class contains all the logic related to [Tile] management.\n * It defers [Tile] loading to the [TileCollector].\n * All internal data manipulation are thread-confined to a single background thread. This is\n * guarantied by the [scope] and its custom dispatcher.\n * Ultimately, it exposes the list of tiles to render ([tilesToRender]) which is backed by a\n * [MutableState]. A composable using [tilesToRender] will be automatically recomposed when this\n * list changes.\n *\n * @author P.Laurence on 04/06/2019\n */\ninternal class TileCanvasState(\n    parentScope: CoroutineScope, tileSize: Int,\n    private val visibleTilesResolver: VisibleTilesResolver,\n    workerCount: Int, highFidelityColors: Boolean\n) {\n\n    /* This view-model uses a background thread for its computations */\n    private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()\n    private val scope = CoroutineScope(\n        parentScope.coroutineContext + singleThreadDispatcher\n    )\n    internal var tilesToRender: List<Tile> by mutableStateOf(listOf())\n    private var tilesCollectedBySpace: Map<SpaceKey, Tile> = mapOf()\n\n    private val _layerFlow = MutableStateFlow<List<Layer>>(listOf())\n    internal val layerFlow = _layerFlow.asStateFlow()\n\n    private val visibleTileLocationsChannel = Channel<TileSpec>(capacity = Channel.RENDEZVOUS)\n    private val tilesOutput = Channel<Tile>(capacity = Channel.RENDEZVOUS)\n    private val visibleStateFlow = MutableStateFlow<VisibleState?>(null)\n    internal var alphaTick = 0.07f\n        set(value) {\n            field = value.coerceIn(0.01f, 1f)\n        }\n    internal var colorFilterProvider: ColorFilterProvider? by mutableStateOf(null)\n    private val recycleChannel = Channel<Tile>(Channel.UNLIMITED)\n\n    /**\n     * So long as this debounced channel is offered a message, the lambda isn't called.\n     */\n    private val idleDebounced = scope.debounce<Unit>(400) {\n        visibleStateFlow.value?.also { (visibleTiles, layerIds, opacities) ->\n            evictTiles(visibleTiles, layerIds, opacities, aggressiveAttempt = true)\n            renderTiles(visibleTiles, layerIds, opacities)\n        }\n    }\n\n    private val renderTask = scope.throttle(wait = 34) {\n        /* Evict, then render */\n        val (lastVisible, ids, opacities) = visibleStateFlow.value ?: return@throttle\n        evictTiles(lastVisible, ids, opacities)\n        renderTiles(lastVisible, ids, opacities)\n    }\n\n    private fun renderTiles(\n        visibleTiles: VisibleTiles,\n        layerIds: List<String>,\n        opacities: List<Float>\n    ) {\n        /* Right before sending tiles to the view, reorder them so that tiles from current level are\n         * above others. */\n        val tilesToRenderCopy = tilesCollected.sortedBy {\n            /* As a side effect of sorting tiles, also set tile phases */\n            if (visibleTiles.visibleWindow is VisibleWindow.InfiniteScrollX) {\n                setTilePhases(it, visibleTiles.visibleWindow, visibleTiles.level, visibleTiles.visibleWindow.timeMark)\n            }\n\n            val priority =\n                if (it.zoom == visibleTiles.level && it.subSample == visibleTiles.subSample) 100 else 0\n            priority + if (layerIds == it.layerIds && opacities == it.opacities) 1 else 0\n        }\n\n        tilesToRender = tilesToRenderCopy\n    }\n\n    private val tilesCollected = mutableSetOf<Tile>()\n\n    private val tileCollector: TileCollector\n\n    init {\n        /* Collect visible tiles and send specs to the TileCollector */\n        scope.launch {\n            collectNewTiles()\n        }\n\n        /* Launch the TileCollector */\n        tileCollector = TileCollector(\n            workerCount = workerCount.coerceAtLeast(1),\n            optimizeForLowEndDevices = !highFidelityColors,\n            tileSize = tileSize\n        )\n        scope.launch {\n            _layerFlow.collectLatest { layers ->\n                tileCollector.collectTiles(\n                    tileSpecs = visibleTileLocationsChannel,\n                    tilesOutput = tilesOutput,\n                    layers = layers\n                )\n            }\n        }\n\n        /* Launch a coroutine to consume the produced tiles */\n        scope.launch {\n            consumeTiles(tilesOutput)\n        }\n\n        /* This is very important to null a tile's bitmap on the main thread because this ensures\n         * that on the next composition the bitmap won't be accessed.\n         * In the future, if the Compose framework does multi-threaded rendering, another technique\n         * will have to be used. Or, consider not using Bitmap.recycle() at all since it seems\n         * not necessary for hardware bitmaps. */\n        scope.launch(Dispatchers.Main) {\n            for (t in recycleChannel) {\n                val b = t.bitmap\n                t.bitmap = null\n                b?.recycle()\n            }\n        }\n    }\n\n    fun setLayers(layers: List<Layer>) {\n        _layerFlow.value = layers\n    }\n\n    /**\n     * Forgets visible state and previously collected tiles.\n     * To clear the canvas, call [forgetTiles], then [renderThrottled].\n     */\n    suspend fun forgetTiles() {\n        scope.launch {\n            visibleStateFlow.value = null\n            tilesCollected.clear()\n        }.join()\n    }\n\n    fun shutdown() {\n        singleThreadDispatcher.close()\n        tileCollector.shutdownNow()\n    }\n\n    suspend fun setViewport(viewport: Viewport) {\n        /* Thread-confine the tileResolver to the main thread */\n        val visibleTiles = withContext(Dispatchers.Main) {\n            visibleTilesResolver.getVisibleTiles(viewport)\n        }\n\n        withContext(scope.coroutineContext) {\n            setVisibleTiles(visibleTiles)\n        }\n    }\n\n    private fun setVisibleTiles(visibleTiles: VisibleTiles) {\n        /* Feed the tile processing machinery */\n        val layerIds = _layerFlow.value.map { it.id }\n        val opacities = _layerFlow.value.map { it.alpha }\n        val visibleTilesForLayers = VisibleState(visibleTiles, layerIds, opacities)\n        visibleStateFlow.value = visibleTilesForLayers\n\n        renderThrottled()\n    }\n\n    /**\n     * Consumes incoming visible tiles from [visibleStateFlow] and sends [TileSpec] instances to the\n     * [TileCollector].\n     *\n     * Leverage built-in back pressure, as this function will suspend when the tile collector is busy\n     * to the point it can't handshake the [visibleTileLocationsChannel] channel.\n     *\n     * Using [Flow.collectLatest], we cancel any ongoing previous tile list processing. It's\n     * particularly useful when the [TileCollector] is too slow, so when a new [VisibleTiles] element\n     * is received from [visibleStateFlow], no new [TileSpec] elements from the previous [VisibleTiles]\n     * element are sent to the [TileCollector]. When the [TileCollector] is ready to resume processing,\n     * the latest [VisibleTiles] element is processed right away.\n     */\n    private suspend fun collectNewTiles() {\n        visibleStateFlow.collectLatest { visibleState ->\n            if (visibleState != null) {\n                when (visibleState.visibleTiles.visibleWindow) {\n                    is VisibleWindow.BoundsConstrained -> {\n                        sendSpecsForTileMatrix(\n                            visibleState,\n                            visibleState.visibleTiles.visibleWindow.tileMatrix\n                        )\n                    }\n\n                    is VisibleWindow.InfiniteScrollX -> {\n                        sendSpecsForTileMatrix(\n                            visibleState,\n                            visibleState.visibleTiles.visibleWindow.tileMatrix\n                        )\n                        val leftMatrix = visibleState.visibleTiles.visibleWindow.leftOverflow?.tileMatrix\n                        if (leftMatrix != null) {\n                            sendSpecsForTileMatrix(\n                                visibleState,\n                                leftMatrix\n                            )\n                        }\n                        val rightMatrix = visibleState.visibleTiles.visibleWindow.rightOverflow?.tileMatrix\n                        if (rightMatrix != null) {\n                            sendSpecsForTileMatrix(\n                                visibleState,\n                                rightMatrix\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    private suspend fun sendSpecsForTileMatrix(\n        visibleState: VisibleState,\n        tileMatrix: TileMatrix\n    ) {\n        val visibleTiles = visibleState.visibleTiles\n        for (e in tileMatrix) {\n            val row = e.key\n            val colRange = e.value\n            for (col in colRange) {\n                val tile = Tile(\n                    zoom = visibleTiles.level,\n                    row = row,\n                    col = col,\n                    subSample = visibleTiles.subSample,\n                    layerIds = visibleState.layerIds,\n                    opacities = visibleState.opacities\n                )\n                val alreadyProcessed = tilesCollected.contains(tile)\n\n                /* Only emit specs which haven't already been processed by the collector\n                 * Doing this now results in less object allocations than filtering the flow\n                 * afterwards */\n                if (!alreadyProcessed) {\n                    visibleTileLocationsChannel.send(\n                        TileSpec(\n                            visibleTiles.level,\n                            row,\n                            col,\n                            visibleTiles.subSample\n                        )\n                    )\n                }\n            }\n        }\n    }\n\n    /**\n     * For each [Tile] received, add it to the list of collected tiles if it's visible. Otherwise,\n     * recycle the tile.\n     */\n    private suspend fun consumeTiles(tileChannel: ReceiveChannel<Tile>) {\n        for (tile in tileChannel) {\n            val (lastVisible, layerIds, opacities) = visibleStateFlow.value ?: continue\n\n            if (\n                lastVisible.contains(tile)\n                && !tilesCollected.contains(tile)\n                && tile.layerIds == layerIds\n                && tile.opacities == opacities\n            ) {\n                val tileWithSameSpace = tilesCollectedBySpace[tile.spaceKey()]\n                if (tileWithSameSpace != null && (tileWithSameSpace.layerIds != tile.layerIds || tileWithSameSpace.opacities != tile.opacities)) {\n                    tile.overlaps = tileWithSameSpace\n                    /* A tile already occupies the same space, so we don't need any fade-in */\n                    tile.alpha = 1f\n                } else {\n                    tile.prepare()\n                }\n                tilesCollected.add(tile)\n                renderThrottled()\n            } else {\n                tile.recycle()\n            }\n            fullEvictionDebounced()\n        }\n    }\n\n    private fun fullEvictionDebounced() {\n        idleDebounced.trySend(Unit)\n    }\n\n    /**\n     * The the alpha needs to be set to [alphaTick], to produce a fade-in effect. If [alphaTick] is\n     * 1f, the alpha won't be updated and there won't be any fade-in effect.\n     */\n    private fun Tile.prepare() {\n        alpha = alphaTick\n    }\n\n    private fun VisibleTiles.contains(tile: Tile): Boolean {\n        if (level != tile.zoom) return false\n        return when (visibleWindow) {\n            is VisibleWindow.BoundsConstrained -> {\n                val colRange = visibleWindow.tileMatrix[tile.row] ?: return false\n                subSample == tile.subSample && tile.col in colRange\n            }\n\n            is VisibleWindow.InfiniteScrollX -> {\n                if (subSample != tile.subSample) return false\n                visibleWindow.tileMatrix[tile.row]?.let { range ->\n                    tile.col in range\n                } == true ||\n                        visibleWindow.leftOverflow?.tileMatrix?.get(tile.row)?.let { range ->\n                            tile.col in range\n                        } == true ||\n                        visibleWindow.rightOverflow?.tileMatrix?.get(tile.row)?.let { range ->\n                            tile.col in range\n                        } == true\n            }\n        }\n    }\n\n    private fun VisibleTiles.intersects(tile: Tile): Boolean {\n        fun checkIntersection(tileMatrix: TileMatrix, tile: Tile): Boolean {\n            return if (level == tile.zoom) {\n                val colRange = tileMatrix[tile.row] ?: return false\n                tile.col in colRange\n            } else {\n                val curMinRow = tileMatrix.keys.minOrNull() ?: return false\n                val curMaxRow = tileMatrix.keys.maxOrNull() ?: return false\n                val curMinCol = tileMatrix.entries.firstOrNull()?.value?.first ?: return false\n                val curMaxCol = tileMatrix.entries.firstOrNull()?.value?.last ?: return false\n\n                if (tile.zoom > level) { // User is zooming out\n                    val dLevel = tile.zoom - level\n                    val minRowAtLvl = curMinRow.minAtGreaterLevel(dLevel)\n                    val maxRowAtLvl = curMaxRow.maxAtGreaterLevel(dLevel)\n\n                    val minColAtLvl = curMinCol.minAtGreaterLevel(dLevel)\n                    val maxColAtLvl = curMaxCol.maxAtGreaterLevel(dLevel)\n                    return tile.row in minRowAtLvl..maxRowAtLvl && tile.col in minColAtLvl..maxColAtLvl\n                } else { // User is zooming in\n                    val dLevel = level - tile.zoom\n                    val minRowAtLvl = tile.row.minAtGreaterLevel(dLevel)\n                    val maxRowAtLvl = tile.row.maxAtGreaterLevel(dLevel)\n\n                    val minColAtLvl = tile.col.minAtGreaterLevel(dLevel)\n                    val maxColAtLvl = tile.col.maxAtGreaterLevel(dLevel)\n                    return curMinCol <= maxColAtLvl && minColAtLvl <= curMaxCol && curMinRow <= maxRowAtLvl &&\n                            minRowAtLvl <= curMaxRow\n                }\n            }\n        }\n\n        return when (visibleWindow) {\n            is VisibleWindow.BoundsConstrained -> checkIntersection(visibleWindow.tileMatrix, tile)\n            is VisibleWindow.InfiniteScrollX -> {\n                val mainIntersect = checkIntersection(visibleWindow.tileMatrix, tile)\n\n                mainIntersect || (visibleWindow.leftOverflow != null && checkIntersection(\n                    visibleWindow.leftOverflow.tileMatrix,\n                    tile\n                )) || (visibleWindow.rightOverflow != null && checkIntersection(\n                    visibleWindow.rightOverflow.tileMatrix,\n                    tile\n                ))\n            }\n        }\n    }\n\n    private fun updateTileCollectedBySpace() {\n        tilesCollectedBySpace = tilesCollected.associateBy {\n            it.spaceKey()\n        }\n    }\n\n    /**\n     * Each time we get a new [VisibleTiles], remove all [Tile] from [tilesCollected] which aren't\n     * visible or that aren't needed anymore and put their bitmap into the pool.\n     */\n    private fun evictTiles(\n        visibleTiles: VisibleTiles,\n        layerIds: List<String>,\n        opacities: List<Float>,\n        aggressiveAttempt: Boolean = false\n    ) {\n        val currentLevel = visibleTiles.level\n        val currentSubSample = visibleTiles.subSample\n\n        /* Always perform partial eviction */\n        partialEviction(visibleTiles, layerIds, opacities)\n\n        /* Only perform aggressive eviction when tile collector is idle */\n        if (aggressiveAttempt && tileCollector.isIdle) {\n            aggressiveEviction(currentLevel, currentSubSample, layerIds, opacities)\n        }\n\n        /* Now that tileCollected is cleaned up, update an internal data structure */\n        updateTileCollectedBySpace()\n    }\n\n    /**\n     * Evict:\n     * * tiles of levels different than the current one, that aren't visible,\n     * * tiles that aren't visible at current level, and tiles from current level which aren't made\n     * of current layers\n     */\n    private fun partialEviction(\n        visibleTiles: VisibleTiles,\n        layerIds: List<String>,\n        opacities: List<Float>\n    ) {\n        val currentLevel = visibleTiles.level\n        val currentSubSample = visibleTiles.subSample\n        val addedSet = mutableSetOf<SpaceKey>()\n\n        val iterator = tilesCollected.iterator()\n        while (iterator.hasNext()) {\n            val tile = iterator.next()\n\n            if (layerIds == tile.layerIds && opacities == tile.opacities) {\n                val spaceHash = tile.spaceKey()\n                addedSet.add(spaceHash)\n            }\n\n            if (layerIds.isEmpty() || tile.zoom != currentLevel && !visibleTiles.intersects(tile)) {\n                iterator.remove()\n                tile.recycle()\n                continue\n            }\n\n            if (\n                tile.zoom == currentLevel\n                && tile.subSample == currentSubSample\n                && (!visibleTiles.contains(tile) || tile.markedForSweep)\n            ) {\n                iterator.remove()\n                tile.recycle()\n            }\n        }\n\n        /* Now that we know all tiles with the latest layerIds and opacities, forget the other\n         * tiles which occupy the same space. Don't recycle the associated bitmaps because some of\n         * the latest tiles haven't been drawn yet. So we rely on garbage collection for these\n         * bitmaps. */\n        val secondPass = tilesCollected.iterator()\n        while (secondPass.hasNext()) {\n            val tile = secondPass.next()\n            if (layerIds != tile.layerIds || opacities != tile.opacities) {\n                val spaceHash = tile.spaceKey()\n                if (addedSet.contains(spaceHash)) {\n                    secondPass.remove()\n                }\n            }\n        }\n    }\n\n    /**\n     * Removes tiles of other levels, even if they are visible (although they should be drawn beneath\n     * currently visible tiles).\n     * Only triggered after the [idleDebounced] fires.\n     */\n    private fun aggressiveEviction(\n        currentLevel: Int,\n        currentSubSample: Int,\n        layerIds: List<String>,\n        opacities: List<Float>\n    ) {\n        val iterator = tilesCollected.iterator()\n        while (iterator.hasNext()) {\n            val tile = iterator.next()\n\n            /* Remove tiles at the same level but from other layers */\n            if (\n                tile.zoom == currentLevel\n                && tile.subSample == currentSubSample\n                && (tile.layerIds != layerIds || tile.opacities != opacities)\n            ) {\n                iterator.remove()\n                tile.recycle()\n            }\n\n            /* Remove other tiles at different level and sub-sample */\n            if ((tile.zoom != currentLevel && tile.subSample == 0)\n                || (tile.zoom == 0 && tile.subSample != currentSubSample)\n            ) {\n                iterator.remove()\n                tile.recycle()\n            }\n        }\n    }\n\n    /**\n     * Post a new value to the observable. The view should update its UI.\n     */\n    private fun renderThrottled() {\n        renderTask.trySend(Unit)\n    }\n\n    /**\n     * After a [Tile] is no longer visible, depending on the bitmap mutability:\n     * - If the Bitmap is mutable, put it into the pool for later use.\n     * - If the bitmap isn't mutable, we don't use bitmap pooling. That means the associated graphic\n     * memory can be reclaimed asap.\n     * The Compose framework draws tiles on the main thread and checks whether or not [Tile.bitmap]\n     * is null. So, prior to calling recycle() we set [Tile.bitmap] to null on the main thread. This\n     * is done inside the coroutine which consumes [recycleChannel].\n     */\n    private fun Tile.recycle() {\n        val b = bitmap ?: return\n        if (!b.isMutable) {\n            recycleChannel.trySend(this)\n        }\n        alpha = 0f\n    }\n\n    private fun Int.minAtGreaterLevel(n: Int): Int {\n        return this * 2.0.pow(n).toInt()\n    }\n\n    private fun Int.maxAtGreaterLevel(n: Int): Int {\n        return (this + 1) * 2.0.pow(n).toInt() - 1\n    }\n\n    private fun setTilePhases(tile: Tile, visibleWindow: VisibleWindow.InfiniteScrollX, level: Int, timeMark: TimeSource.Monotonic.ValueTimeMark) {\n        if (tile.zoom != level) return\n\n        val left = visibleWindow.leftOverflow?.phase?.get(tile.col)\n        val right = visibleWindow.rightOverflow?.phase?.get(tile.col)\n        val inCenter = tile.col in (visibleWindow.tileMatrix[tile.row] ?: IntRange.EMPTY)\n        tile.phases = if (left != null || right != null) {\n             IntRange(\n                start = left ?: (if (inCenter) 0 else 1),\n                endInclusive = right ?: (if (inCenter) 0 else -1)\n            )\n        } else null\n        tile.timeMark = timeMark\n    }\n\n    private data class VisibleState(\n        val visibleTiles: VisibleTiles,\n        val layerIds: List<String>,\n        val opacities: List<Float>\n    )\n}\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/state/ZoomPanRotateState.kt",
    "content": "package ovh.plrapps.mapcompose.ui.state\n\nimport androidx.compose.animation.core.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.Velocity\nimport kotlinx.coroutines.*\nimport ovh.plrapps.mapcompose.core.GestureConfiguration\nimport ovh.plrapps.mapcompose.ui.layout.*\nimport ovh.plrapps.mapcompose.utils.*\nimport kotlin.coroutines.Continuation\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.suspendCoroutine\nimport kotlin.math.*\nimport kotlin.time.TimeSource\n\ninternal class ZoomPanRotateState(\n    val fullWidth: Int,\n    val fullHeight: Int,\n    private val stateChangeListener: ZoomPanRotateStateListener,\n    minimumScaleMode: MinimumScaleMode,\n    maxScale: Double,\n    scale: Double,\n    rotation: AngleDegree,\n    gestureConfiguration: GestureConfiguration,\n    val infiniteScrollX: Boolean,\n) : GestureListener, LayoutSizeChangeListener {\n    private var scope: CoroutineScope? = null\n    private var onLayoutContinuations = mutableListOf<Continuation<Unit>>()\n\n    /**\n     * Suspends until the view is laid out. To do that, we use the [scope] as flag.\n     *\n     * _Contract_:\n     * On layout change, [scope] and [layoutSize] are initialized, and queued continuations\n     * are resumed.\n     */\n    internal suspend fun awaitLayout() {\n        if (scope != null) return\n        suspendCoroutine {\n            onLayoutContinuations.add(it)\n        }\n    }\n\n    internal var minimumScaleMode: MinimumScaleMode = minimumScaleMode\n        set(value) {\n            field = value\n            recalculateMinScale()\n        }\n\n    private val areGesturesEnabled by derivedStateOf { isRotationEnabled || isScrollingEnabled || isZoomingEnabled }\n    internal var isRotationEnabled by mutableStateOf(false)\n    internal var isScrollingEnabled by mutableStateOf(true)\n    internal var isZoomingEnabled by mutableStateOf(true)\n    internal var isFlingZoomEnabled by mutableStateOf(true)\n\n    /* Single source of truth. Don't mutate directly, use appropriate setScale(), setRotation(), etc. */\n    internal var scale by mutableDoubleStateOf(scale)\n    internal var rotation: AngleDegree by mutableFloatStateOf(rotation)\n    internal var scrollX by mutableDoubleStateOf(0.0)\n    internal var scrollY by mutableDoubleStateOf(0.0)\n\n    internal var pivotX: Double by mutableDoubleStateOf(0.0)\n    internal var pivotY: Double by mutableDoubleStateOf(0.0)\n\n    internal var centroidX: Double by mutableDoubleStateOf(0.0)\n    internal var centroidY: Double by mutableDoubleStateOf(0.0)\n\n    internal var layoutSize by mutableStateOf(IntSize.Zero)\n\n    internal var visibleAreaPadding = VisibleAreaPadding(0, 0, 0, 0)\n\n    internal var minScale by mutableDoubleStateOf(0.0)   // should only be changed through MinimumScaleMode\n\n    var maxScale = maxScale\n        set(value) {\n            field = value\n            setScale(scale)\n        }\n\n    internal var shouldLoopScale by mutableStateOf(false)\n\n    internal var scrollOffsetRatio = Offset(0f, 0f)\n        set(value) {\n            if (value.x in 0f..1f && value.y in 0f..1f) {\n                field = value\n                /* Update the scroll to constrain it */\n                setScroll(\n                    scrollX = scrollX,\n                    scrollY = scrollY\n                )\n            } else throw IllegalArgumentException(\"The offset ratio should have values in 0f..1f range\")\n        }\n\n    internal val rolloverX = mutableStateOf<RolloverData?>(null)\n\n    // For user gestures animations\n    private val userFloatAnimatable = Animatable(0f)\n    private val userAnimatable: Animatable<Offset, AnimationVector2D> =\n        Animatable(Offset.Zero, Offset.VectorConverter)\n\n    // For api-based animations\n    private val apiAnimatable = Animatable(0f)\n\n    private val doubleTapSpec =\n        TweenSpec<Float>(durationMillis = 300, easing = LinearOutSlowInEasing)\n    private val flingZoomSpec =\n        FloatExponentialDecaySpec(\n            frictionMultiplier = gestureConfiguration.flingZoomFriction\n        ).generateDecayAnimationSpec<Float>()\n\n    @Suppress(\"unused\")\n    fun setScale(scale: Double, notify: Boolean = true) {\n        this.scale = constrainScale(scale)\n        updateCentroid()\n        if (notify) notifyStateChanged()\n    }\n\n    @Suppress(\"unused\")\n    fun setScroll(scrollX: Double, scrollY: Double) {\n        this.scrollX = constrainScrollX(scrollX)\n        this.scrollY = constrainScrollY(scrollY)\n        updateCentroid()\n        notifyStateChanged()\n    }\n\n    @Suppress(\"unused\")\n    fun setRotation(angle: AngleDegree, notify: Boolean = true) {\n        this.rotation = angle.modulo()\n        updateCentroid()\n        if (notify) notifyStateChanged()\n    }\n\n    /**\n     * Scales the layout with animated scale, without maintaining scroll position.\n     *\n     * @param scale The final scale value the layout should animate to.\n     * @param animationSpec The [AnimationSpec] the animation should use.\n     */\n    @Suppress(\"unused\")\n    suspend fun smoothScaleTo(\n        scale: Double,\n        animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow)\n    ): Boolean {\n        return invokeAndCheckSuccess {\n            val currScale = this@ZoomPanRotateState.scale\n            if (currScale > 0) {\n                apiAnimatable.snapTo(0f)\n                apiAnimatable.animateTo(1f, animationSpec) {\n                    setScale(lerp(currScale, scale, value.toDouble()))\n                }\n            }\n        }\n    }\n\n    suspend fun smoothRotateTo(\n        angle: AngleDegree,\n        animationSpec: AnimationSpec<Float>\n    ): Boolean {\n        /* We don't have to stop scrolling animation while doing that */\n        return invokeAndCheckSuccess {\n            val currRotation = this@ZoomPanRotateState.rotation\n            var targetAngle = (angle % 360)\n            if (abs(targetAngle - currRotation) > 180) {\n                targetAngle += if (targetAngle > currRotation) -360 else 360\n            }\n            apiAnimatable.snapTo(0f)\n            apiAnimatable.animateTo(1f, animationSpec) {\n                setRotation(lerp(currRotation, targetAngle, value))\n            }\n        }\n    }\n\n    /**\n     * Animates the scroll to the destination value.\n     *\n     * @return `true` if the operation completed without being cancelled.\n     */\n    suspend fun smoothScrollTo(\n        destScrollX: Double,\n        destScrollY: Double,\n        animationSpec: AnimationSpec<Float>\n    ): Boolean {\n        val startScrollX = this.scrollX\n        val startScrollY = this.scrollY\n\n        return invokeAndCheckSuccess {\n            userAnimatable.stop()\n            apiAnimatable.snapTo(0f)\n            apiAnimatable.animateTo(1f, animationSpec) {\n                setScroll(\n                    scrollX = lerp(startScrollX, destScrollX, value.toDouble()),\n                    scrollY = lerp(startScrollY, destScrollY, value.toDouble())\n                )\n            }\n        }\n    }\n\n    /**\n     * Animates the scroll and the scale together with the supplied destination values.\n     *\n     * @param destScrollX Horizontal scroll of the destination point.\n     * @param destScrollY Vertical scroll of the destination point.\n     * @param destScale The final scale value the layout should animate to.\n     * @param animationSpec The [AnimationSpec] the animation should use.\n     */\n    suspend fun smoothScrollScaleRotate(\n        destScrollX: Double,\n        destScrollY: Double,\n        destScale: Double,\n        animationSpec: AnimationSpec<Float>\n    ): Boolean {\n        val startScrollX = this.scrollX\n        val startScrollY = this.scrollY\n        val startScale = this.scale\n\n        return invokeAndCheckSuccess {\n            userAnimatable.stop()\n            apiAnimatable.snapTo(0f)\n            apiAnimatable.animateTo(1f, animationSpec) {\n                setScale(lerp(startScale, destScale, value.toDouble()))\n                setScroll(\n                    scrollX = lerp(startScrollX, destScrollX, value.toDouble()),\n                    scrollY = lerp(startScrollY, destScrollY, value.toDouble())\n                )\n            }\n        }\n    }\n\n    /**\n     * Animates the scroll, the scale, and the rotation together with the supplied destination values.\n     *\n     * @param destScrollX Horizontal scroll of the destination point.\n     * @param destScrollY Vertical scroll of the destination point.\n     * @param destScale The final scale value the layout should animate to.\n     * @param destAngle The final angle in decimal degrees the layout should animate to.\n     * @param animationSpec The [AnimationSpec] the animation should use.\n     */\n    suspend fun smoothScrollScaleRotate(\n        destScrollX: Double,\n        destScrollY: Double,\n        destScale: Double,\n        destAngle: AngleDegree,\n        animationSpec: AnimationSpec<Float>\n    ): Boolean {\n        val startScrollX = this.scrollX\n        val startScrollY = this.scrollY\n        val startScale = this.scale\n\n        val currRotation = this@ZoomPanRotateState.rotation\n        var targetAngle = (destAngle % 360)\n        if (abs(targetAngle - currRotation) > 180) {\n            targetAngle += if (targetAngle > currRotation) -360 else 360\n        }\n\n        return invokeAndCheckSuccess {\n            userAnimatable.stop()\n            apiAnimatable.snapTo(0f)\n            apiAnimatable.animateTo(1f, animationSpec) {\n                setScale(lerp(startScale, destScale, value.toDouble()))\n                setScroll(\n                    scrollX = lerp(startScrollX, destScrollX, value.toDouble()),\n                    scrollY = lerp(startScrollY, destScrollY, value.toDouble())\n                )\n                setRotation(lerp(currRotation, targetAngle, value))\n            }\n        }\n    }\n\n    /**\n     * Animates the layout to the scale provided, while maintaining position determined by the\n     * the provided focal point.\n     *\n     * @param focusX The horizontal focal point to maintain, relative to the layout.\n     * @param focusY The vertical focal point to maintain, relative to the layout.\n     * @param destScale The final scale value the layout should animate to.\n     * @param animationSpec The [AnimationSpec] the animation should use.\n     */\n    private suspend fun smoothScaleWithFocalPoint(\n        focusX: Float,\n        focusY: Float,\n        destScale: Double,\n        animationSpec: AnimationSpec<Float>\n    ): Boolean {\n        val destScaleCst = constrainScale(destScale)\n        val startScale = scale\n        if (startScale == destScale) return true\n        val startScrollX = scrollX\n        val startScrollY = scrollY\n        val destScrollX = getScrollAtOffsetAndScale(startScrollX, focusX, destScaleCst / startScale)\n        val destScrollY = getScrollAtOffsetAndScale(startScrollY, focusY, destScaleCst / startScale)\n\n        return smoothScrollScaleRotate(destScrollX, destScrollY, destScale, animationSpec)\n    }\n\n    /**\n     * Invokes [block] in the scope of the composition and return whether the operation completed\n     * without being cancelled.\n     */\n    internal suspend fun invokeAndCheckSuccess(block: suspend () -> Unit): Boolean {\n        var success = true\n        scope?.launch {\n            block()\n        }?.also {\n            it.invokeOnCompletion { t ->\n                if (t != null) success = false\n            }\n        }?.join()\n\n        return success\n    }\n\n    suspend fun stopAnimations() {\n        apiAnimatable.stop()\n        userAnimatable.stop()\n        userFloatAnimatable.stop()\n    }\n\n    override fun onScaleRatio(scaleRatio: Double, centroid: Offset) {\n        if (!isZoomingEnabled) return\n\n        val formerScale = scale\n        setScale(scale * scaleRatio)\n\n        /* Pinch and zoom magic */\n        val effectiveScaleRatio = scale / formerScale\n        val angleRad = -rotation.toRad()\n        val centroidRotated = rotateFocalPoint(centroid, angleRad)\n        setScroll(\n            scrollX = getScrollAtOffsetAndScale(scrollX, centroidRotated.x, effectiveScaleRatio),\n            scrollY = getScrollAtOffsetAndScale(scrollY, centroidRotated.y, effectiveScaleRatio)\n        )\n    }\n\n    private fun getScrollAtOffsetAndScale(scroll: Double, offSet: Float, scaleRatio: Double): Double {\n        return (scroll + offSet) * scaleRatio - offSet\n    }\n\n    /**\n     * Rotates a focal point around the center of the layout.\n     */\n    private fun rotateFocalPoint(point: Offset, angleRad: AngleRad): Offset {\n        val x = if (angleRad == 0f) point.x else {\n            layoutSize.height / 2 * sin(angleRad) + layoutSize.width / 2 * (1 - cos(angleRad)) +\n                    point.x * cos(angleRad) - point.y * sin(angleRad)\n        }\n\n        val y = if (angleRad == 0f) point.y else {\n            layoutSize.height / 2 * (1 - cos(angleRad)) - layoutSize.width / 2 * sin(angleRad) +\n                    point.x * sin(angleRad) + point.y * cos(angleRad)\n        }\n        return Offset(x, y)\n    }\n\n    override fun onRotationDelta(rotationDelta: Float) {\n        if (!isRotationEnabled) return\n\n        setRotation(rotation + rotationDelta)\n    }\n\n    override fun onScrollDelta(scrollDelta: Offset) {\n        if (!isScrollingEnabled) return\n\n        var scrollX = scrollX\n        var scrollY = scrollY\n\n        val rotRad = -rotation.toRad()\n        scrollX -= if (rotRad == 0f) scrollDelta.x else {\n            scrollDelta.x * cos(rotRad) - scrollDelta.y * sin(rotRad)\n        }\n        scrollY -= if (rotRad == 0f) scrollDelta.y else {\n            scrollDelta.x * sin(rotRad) + scrollDelta.y * cos(rotRad)\n        }\n        setScroll(scrollX, scrollY)\n    }\n\n    override fun onFling(flingSpec: DecayAnimationSpec<Offset>, velocity: Velocity) {\n        if (!isScrollingEnabled) return\n\n        val rotRad = -rotation.toRad()\n        val velocityX = if (rotRad == 0f) velocity.x else {\n            velocity.x * cos(rotRad) - velocity.y * sin(rotRad)\n        }\n        val velocityY = if (rotRad == 0f) velocity.y else {\n            velocity.x * sin(rotRad) + velocity.y * cos(rotRad)\n        }\n\n        scope?.launch {\n            userAnimatable.snapTo(Offset.Zero)\n            val initialScrollX = scrollX\n            val initialScrollY = scrollY\n            userAnimatable.animateDecay(\n                initialVelocity = -Offset(velocityX, velocityY),\n                animationSpec = flingSpec,\n            ) {\n                setScroll(\n                    scrollX = initialScrollX + value.x,\n                    scrollY = initialScrollY + value.y\n                )\n            }\n        }\n    }\n\n    override fun onFlingZoom(velocity: Float, centroid: Offset) {\n        if (!isZoomingEnabled || !isFlingZoomEnabled) return\n\n        scope?.launch {\n            userFloatAnimatable.snapTo(0f)\n            var previous = 0f\n            userFloatAnimatable.animateDecay(\n                initialVelocity = velocity,\n                animationSpec = flingZoomSpec,\n            ) {\n                /* Since scale = 2.pow(z - maxLevel)  , where z is the zoom level\n                 * taking the derivative: d_scale = ln(2) * scale * d_z */\n                val newScale = scale + ln(2.0) * scale * (value - previous)\n                onScaleRatio(newScale / scale, centroid)\n                previous = value\n            }\n        }\n    }\n\n    override fun onTouchDown() {\n        if (!areGesturesEnabled) return\n\n        scope?.launch {\n            stopAnimations()\n        }\n        stateChangeListener.onTouchDown()\n    }\n\n    override fun onPress() {\n        stateChangeListener.onPress()\n    }\n\n    override fun onTap(focalPt: Offset) {\n        if (!stateChangeListener.detectsTap()) return\n        offsetToRelative(focalPt) { x, y ->\n            stateChangeListener.onTap(x, y)\n        }\n    }\n\n    override fun onLongPress(focalPt: Offset) {\n        if (!stateChangeListener.detectsLongPress()) return\n        offsetToRelative(focalPt) { x, y ->\n            stateChangeListener.onLongPress(x, y)\n        }\n    }\n\n    private fun <T> offsetToRelative(focalPt: Offset, block: (Double, Double) -> T): T {\n        val angleRad = -rotation.toRad()\n        val focalPtRotated = rotateFocalPoint(focalPt, angleRad)\n        val x = (scrollX + focalPtRotated.x) / (scale * fullWidth)\n        val y = (scrollY + focalPtRotated.y) / (scale * fullHeight)\n        return block(x, y)\n    }\n\n    private fun <T> relativeToMarkerLayoutCoords(x: Double, y: Double, block: (Int, Int) -> T): T {\n        val xFullPx = x * fullWidth * scale\n        val yFullPx = y * fullHeight * scale\n        val centerX = centroidX * fullWidth * scale\n        val centerY = centroidY * fullHeight * scale\n\n        val angleRad = rotation.toRad()\n        val xPx = (rotateCenteredX(\n            xFullPx,\n            yFullPx,\n            centerX,\n            centerY,\n            angleRad\n        )).toInt()\n\n        val yPx = (rotateCenteredY(\n            xFullPx,\n            yFullPx,\n            centerX,\n            centerY,\n            angleRad\n        )).toInt()\n\n        return block(xPx, yPx)\n    }\n\n    override fun onDoubleTap(focalPt: Offset) {\n        if (!isZoomingEnabled) return\n\n        val destScale = (\n                2.0.pow(floor(ln((scale * 2)) / ln(2.0)))\n                ).let {\n                if (shouldLoopScale && it > maxScale) minScale else it\n            }\n\n        val angleRad = -rotation.toRad()\n        val focalPtRotated = rotateFocalPoint(focalPt, angleRad)\n\n        scope?.launch {\n            smoothScaleWithFocalPoint(\n                focalPtRotated.x,\n                focalPtRotated.y,\n                destScale,\n                doubleTapSpec\n            )\n        }\n    }\n\n    override fun onTwoFingersTap(focalPt: Offset) {\n        if (!isZoomingEnabled) return\n\n        val destScale = 2.0.pow(floor(ln((scale / 2)) / ln(2.0)))\n\n        val angleRad = -rotation.toRad()\n        val focalPtRotated = rotateFocalPoint(focalPt, angleRad)\n\n        scope?.launch {\n            smoothScaleWithFocalPoint(\n                focalPtRotated.x,\n                focalPtRotated.y,\n                destScale,\n                doubleTapSpec\n            )\n        }\n    }\n\n    override fun isListeningForGestures(): Boolean = areGesturesEnabled\n\n    override fun shouldConsumeTapGesture(focalPt: Offset): Boolean {\n        return offsetToRelative(focalPt) { x, y ->\n            relativeToMarkerLayoutCoords(x, y) { xPx, yPx ->\n                stateChangeListener.interceptsTap(x, y, xPx, yPx)\n            }\n        }\n    }\n\n    override fun shouldConsumeLongPress(focalPt: Offset): Boolean {\n        return offsetToRelative(focalPt) { x, y ->\n            relativeToMarkerLayoutCoords(x, y) { xPx, yPx ->\n                stateChangeListener.interceptsLongPress(x, y, xPx, yPx)\n            }\n        }\n    }\n\n    override fun onSizeChanged(composableScope: CoroutineScope, size: IntSize) {\n        scope = composableScope\n\n        /* When the size changes, typically on device rotation, the scroll needs to be adapted so\n         * that we keep the same location at the center of the screen. Don't do that when layout\n         * hasn't been done yet. */\n        var newScrollX: Double? = null\n        var newScrollY: Double? = null\n        if (layoutSize != IntSize.Zero) {\n            newScrollX = scrollX + (layoutSize.width - size.width) / 2\n            newScrollY = scrollY + (layoutSize.height - size.height) / 2\n        }\n\n        layoutSize = size\n        recalculateMinScale()\n        if (newScrollX != null && newScrollY != null) {\n            setScroll(newScrollX, newScrollY)\n        }\n\n        /* Layout was done at least once, resume continuations */\n        for (ct in onLayoutContinuations) {\n            ct.resume(Unit)\n        }\n        onLayoutContinuations.clear()\n    }\n\n    private fun constrainScrollX(scrollX: Double): Double {\n        val angle = rotation.toRad()\n\n        val layoutDimension =\n            polarRadius(layoutSize.width.toFloat(), layoutSize.height.toFloat(), angle)\n        val bias = (layoutDimension - layoutSize.width) / 2\n\n        return if (infiniteScrollX) {\n            val left = bias\n            val right = bias + fullWidth * scale - layoutDimension\n            val constrained = when {\n                scrollX < (left - layoutDimension) -> {\n                    val delta = left - layoutDimension - scrollX\n                    val window = right - left + layoutDimension\n                    val ratio = (delta / window).toInt()\n                    right - (delta - ratio * window)\n                }\n                scrollX > (right + layoutDimension) -> {\n                    val delta = scrollX - right - layoutDimension\n                    val window = right - left + layoutDimension\n                    val ratio = (delta / window).toInt()\n                    left + (delta - ratio * window)\n                }\n                else -> scrollX\n            }\n\n            /* Also update the rollover */\n            val newRollover = when {\n                abs(left - layoutDimension - constrained) < rolloverThreshold -> Rollover.Backward\n                abs(right + layoutDimension - constrained) < rolloverThreshold -> Rollover.Forward\n                else -> null\n            }\n            val current = rolloverX.value\n\n            rolloverX.value = if (current == null) {\n                RolloverData(\n                    current = newRollover ?: Rollover.None(TimeSource.Monotonic.markNow())\n                )\n            } else {\n                when (current.current) {\n                    Rollover.Backward, Rollover.Forward -> {\n                        if (newRollover == null) {\n                            current.copy(\n                                current = Rollover.None(TimeSource.Monotonic.markNow()),\n                                previous = current.current\n                            )\n                        } else current\n                    }\n                    is Rollover.None -> {\n                        if (newRollover == null) {\n                            current\n                        } else {\n                            current.copy(\n                                current = newRollover,\n                                previous = current.current\n                            )\n                        }\n                    }\n                }\n            }\n\n            constrained\n        } else {\n            if (fullWidth * scale < layoutDimension) {\n                val offset = scrollOffsetRatio.x * fullWidth * scale\n                scrollX.coerceIn(fullWidth * scale - layoutDimension - offset + bias, offset + bias)\n            } else {\n                val offset = scrollOffsetRatio.x * layoutDimension\n                scrollX.coerceIn(\n                    (-offset + bias).toDouble(),\n                    offset + bias + fullWidth * scale - layoutDimension\n                )\n            }\n        }\n    }\n\n    private fun constrainScrollY(scrollY: Double): Double {\n        val angle = rotation.toRad()\n\n        val layoutDimension =\n            polarRadius(layoutSize.height.toFloat(), layoutSize.width.toFloat(), angle)\n        val bias = (layoutDimension - layoutSize.height) / 2\n\n        return if (fullHeight * scale < layoutDimension) {\n            val offset = scrollOffsetRatio.y * fullHeight * scale\n            scrollY.coerceIn(fullHeight * scale - layoutDimension - offset + bias, offset + bias)\n        } else {\n            val offset = scrollOffsetRatio.y * layoutDimension\n            scrollY.coerceIn(\n                (-offset + bias).toDouble(),\n                offset + bias + fullHeight * scale - layoutDimension\n            )\n        }\n    }\n\n    internal fun constrainScale(scale: Double): Double {\n        return scale.coerceIn(max(minScale, Double.MIN_VALUE), maxScale.coerceAtLeast(minScale))\n    }\n\n    private fun updateCentroid() {\n        pivotX = layoutSize.width.toDouble() / 2\n        pivotY = layoutSize.height.toDouble() / 2\n\n        centroidX = (scrollX + pivotX) / (fullWidth * scale)\n        centroidY = (scrollY + pivotY) / (fullHeight * scale)\n    }\n\n    private fun recalculateMinScale() {\n        val minScaleX = layoutSize.width.toDouble() / fullWidth\n        val minScaleY = layoutSize.height.toDouble() / fullHeight\n        val mode = minimumScaleMode\n        minScale = when (mode) {\n            Fit -> min(minScaleX, minScaleY)\n            Fill -> max(minScaleX, minScaleY)\n            is Forced -> mode.scale\n        }\n        setScale(scale)\n    }\n\n    private fun notifyStateChanged() {\n        if (layoutSize != IntSize.Zero) {\n            stateChangeListener.onStateChanged()\n        }\n    }\n\n    private fun polarRadius(a: Float, b: Float, angle: AngleRad): Float {\n        return a * b / sqrt((a * sin(angle)).pow(2) + (b * cos(angle)).pow(2))\n    }\n\n    private val rolloverThreshold = 200.0\n}\n\ninternal sealed interface Rollover {\n    data object Forward : Rollover\n    data object Backward : Rollover\n    data class None(val timeMark: TimeSource.Monotonic.ValueTimeMark) : Rollover\n}\n\ninternal data class RolloverData(val current: Rollover, val previous: Rollover? = null)\n\n/**\n * The padding to apply when some UI is obscuring the map on it's borders.\n */\ninternal data class VisibleAreaPadding(val left: Int, val top: Int, val right: Int, val bottom: Int)\n\ninterface ZoomPanRotateStateListener {\n    fun onStateChanged()\n    fun onTouchDown()\n    fun onPress()\n    fun onLongPress(x: Double, y: Double)\n    fun onTap(x: Double, y: Double)\n    fun detectsTap(): Boolean\n    fun detectsLongPress(): Boolean\n    fun interceptsTap(x: Double, y: Double, xPx: Int, yPx: Int): Boolean\n    fun interceptsLongPress(x: Double, y: Double, xPx: Int, yPx: Int): Boolean\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/state/markers/MarkerRenderState.kt",
    "content": "package ovh.plrapps.mapcompose.ui.state.markers\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateMapOf\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.unit.DpOffset\nimport ovh.plrapps.mapcompose.ui.state.markers.model.MarkerData\nimport ovh.plrapps.mapcompose.ui.state.markers.model.MarkerType\nimport ovh.plrapps.mapcompose.ui.state.markers.model.RenderingStrategy\nimport ovh.plrapps.mapcompose.utils.removeFirst\nimport kotlin.math.pow\n\ninternal class MarkerRenderState {\n    internal val markers = derivedStateOf {\n        regularMarkers + lazyLoadedMarkers + clustererManagedMarkers\n    }\n\n    private val hasClickable = derivedStateOf {\n        markers.value.any {\n            it.isClickable\n        }\n    }\n\n    private val regularMarkers = mutableStateListOf<MarkerData>()\n    private val lazyLoadedMarkers = mutableStateListOf<MarkerData>()\n    private val clustererManagedMarkers = mutableStateListOf<MarkerData>()\n\n    internal val callouts = mutableStateMapOf<String, CalloutData>()\n    internal var calloutClickCb: MarkerHitCb? = null\n\n    fun getRegularMarkers(): List<MarkerData> {\n        return regularMarkers\n    }\n\n    fun addRegularMarkers(markerDataList: List<MarkerData>) {\n        regularMarkers += markerDataList\n    }\n\n    fun removeRegularMarkers(markerDataList: List<MarkerData>) {\n        regularMarkers -= markerDataList\n    }\n\n    fun getClusteredMarkers(): List<MarkerData> {\n        return clustererManagedMarkers\n    }\n\n    fun addClustererManagedMarker(markerData: MarkerData) {\n        clustererManagedMarkers.add(markerData)\n    }\n\n    fun removeClustererManagedMarker(id: String): Boolean {\n        return clustererManagedMarkers.removeFirst { it.id == id }\n    }\n\n    fun removeAllClusterManagedMarkers(clusteredId: String) {\n        clustererManagedMarkers.removeAll { markerData ->\n            (markerData.renderingStrategy is RenderingStrategy.Clustering)\n                    && markerData.renderingStrategy.clustererId == clusteredId\n        }\n    }\n\n    fun getLazyLoadedMarkers(): List<MarkerData> {\n        return lazyLoadedMarkers\n    }\n\n    fun addLazyLoadedMarker(markerData: MarkerData) {\n        lazyLoadedMarkers.add(markerData)\n    }\n\n    fun removeLazyLoadedMarker(id: String): Boolean {\n        return lazyLoadedMarkers.removeFirst { it.id == id }\n    }\n\n    fun removeAllLazyLoadedMarkers(lazyLoaderId: String) {\n        lazyLoadedMarkers.removeAll { markerData ->\n            (markerData.renderingStrategy is RenderingStrategy.LazyLoading)\n                    && markerData.renderingStrategy.lazyLoaderId == lazyLoaderId\n        }\n    }\n\n    fun addCallout(\n        id: String, x: Double, y: Double, relativeOffset: Offset, absoluteOffset: DpOffset,\n        zIndex: Float, autoDismiss: Boolean, clickable: Boolean, isConstrainedInBounds: Boolean,\n        c: @Composable () -> Unit\n    ) {\n        val markerData =\n            MarkerData(\n                id = id,\n                x = x,\n                y = y,\n                relativeOffset = relativeOffset,\n                absoluteOffset = absoluteOffset,\n                zIndex = zIndex,\n                clickable = clickable,\n                isConstrainedInBounds = isConstrainedInBounds,\n                clickableAreaScale = Offset(1f, 1f),\n                clickableAreaCenterOffset = Offset(0f, 0f),\n                renderingStrategy = RenderingStrategy.Default,\n                type = MarkerType.Callout,\n                c = c\n            )\n        callouts[id] = CalloutData(markerData, autoDismiss)\n    }\n\n    fun hasCallout(id: String): Boolean = callouts.containsKey(id)\n\n    fun moveCallout(id: String, x: Double, y: Double) {\n        callouts[id]?.markerData?.also {\n            it.x = if (it.isConstrainedInBounds) x.coerceIn(0.0, 1.0) else x\n            it.y = if (it.isConstrainedInBounds) y.coerceIn(0.0, 1.0) else y\n        }\n    }\n\n    fun removeCallout(id: String): Boolean {\n        return callouts.remove(id) != null\n    }\n\n    fun removeAllAutoDismissCallouts() {\n        if (callouts.isEmpty()) return\n        val it = callouts.iterator()\n        while (it.hasNext()) {\n            if (it.next().value.autoDismiss) it.remove()\n        }\n    }\n\n    /**\n     * Get the nearest marker which contains the click position and has the highest z-index.\n     */\n    fun getMarkerForHit(xPx: Int, yPx: Int): MarkerData? {\n        if (!hasClickable.value) return null\n        val candidates = markers.value.filter { markerData ->\n            markerData.isClickable && markerData.contains(xPx, yPx)\n        }\n        val highestZ = candidates.maxByOrNull { it.zIndex }?.zIndex ?: return null\n\n        return candidates.filter {\n            it.zIndex == highestZ\n        }.minWithOrNull { markerData1, markerData2 ->\n            if (squareDistance(markerData1, xPx, yPx) > squareDistance(markerData2, xPx, yPx)) 1 else -1\n        }\n    }\n\n    private fun squareDistance(markerData: MarkerData, x: Int, y: Int): Double {\n        val (cx, cy) = markerData.getCenter() ?: return Double.MAX_VALUE\n        return (cx - x).pow(2) + (cy - y).pow(2)\n    }\n\n    internal fun onCalloutClick(data: MarkerData) {\n        calloutClickCb?.invoke(data.id, data.x, data.y)\n    }\n}\n\ninternal data class CalloutData(val markerData: MarkerData, val autoDismiss: Boolean)\n\ninternal typealias MarkerMoveCb = (id: String, x: Double, y: Double, dx: Double, dy: Double) -> Unit\ninternal typealias MarkerHitCb = (id: String, x: Double, y: Double) -> Unit\n\nfun interface DragInterceptor {\n    /**\n     * The default behavior (e.g without a drag interceptor) updates the marker coordinates like so:\n     * * x: [x] + [dx]\n     * * y: [y] + [dy]\n     *\n     * @param id: The id of the marker\n     * @param x, y: The current normalized coordinates of the marker\n     * @param dx, dy: The virtual displacement expressed in relative coordinates (not in pixels) that would\n     * have been applied if there were no drag interceptor\n     * @param px, py: The current normalized coordinates of the pointer. If the marker's\n     * \"isConstrainedInBounds\" property is set to true, these coordinates are coerced in 0.0..1.0\n     */\n    fun onMove(\n        id: String,\n        x: Double,\n        y: Double,\n        dx: Double,\n        dy: Double,\n        px: Double,\n        py: Double\n    )\n}\n\nfun interface DragStartListener {\n    /**\n     * @param id: The id of the marker\n     * @param x, y: The normalized coordinates of the marker, before the drag starts.\n     * @param px, py: The current normalized coordinates of the pointer. If the marker's\n     * \"isConstrainedInBounds\" property is set to true, these coordinates are coerced in 0.0..1.0\n     */\n    fun onDragStart(id: String, x: Double, y: Double, px: Double, py: Double)\n}\n\nfun interface DragEndListener {\n    /**\n     * @param id: The id of the marker\n     * @param x, y: The normalized coordinates of the marker when the drag ends.\n     */\n    fun onDragEnd(id: String, x: Double, y: Double)\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/state/markers/MarkerState.kt",
    "content": "package ovh.plrapps.mapcompose.ui.state.markers\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.DpOffset\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.launch\nimport ovh.plrapps.mapcompose.api.ClusterScaleThreshold\nimport ovh.plrapps.mapcompose.ui.markers.Clusterer\nimport ovh.plrapps.mapcompose.ui.markers.LazyLoader\nimport ovh.plrapps.mapcompose.ui.gestures.model.HitType\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport ovh.plrapps.mapcompose.ui.state.markers.model.ClusterClickBehavior\nimport ovh.plrapps.mapcompose.ui.state.markers.model.MarkerData\nimport ovh.plrapps.mapcompose.ui.state.markers.model.MarkerType\nimport ovh.plrapps.mapcompose.ui.state.markers.model.RenderingStrategy\n\ninternal class MarkerState(\n    scope: CoroutineScope,\n    private val markerRenderState: MarkerRenderState\n) {\n    private val markers = MutableStateFlow<List<MarkerData>>(emptyList())\n    internal var markerClickCb: MarkerHitCb? = null\n    internal var markerLongPressCb: MarkerHitCb? = null\n    internal var markerMoveCb: MarkerMoveCb? = null\n\n    private val clusterersById = mutableMapOf<String, Clusterer>()\n    private val lazyLoaderById = mutableMapOf<String, LazyLoader>()\n\n    init {\n        scope.launch {\n            renderRegularMarkers()\n        }\n    }\n\n    fun hasMarker(id: String): Boolean = markers.value.any { it.id == id }\n\n    fun getMarker(id: String): MarkerData? {\n        return markers.value.firstOrNull { it.id == id }\n    }\n\n    fun getRenderedMarkers(): List<MarkerData> = markerRenderState.markers.value\n\n    fun addMarker(\n        id: String, x: Double, y: Double,\n        relativeOffset: Offset,\n        absoluteOffset: DpOffset,\n        zIndex: Float,\n        clickable: Boolean,\n        isConstrainedInBounds: Boolean,\n        clickableAreaScale: Offset,\n        clickableAreaCenterOffset: Offset,\n        renderingStrategy: RenderingStrategy,\n        c: @Composable () -> Unit\n    ) {\n        if (hasMarker(id)) return\n        markers.value += MarkerData(\n            id = id,\n            x = x,\n            y = y,\n            relativeOffset = relativeOffset,\n            absoluteOffset = absoluteOffset,\n            zIndex = zIndex,\n            clickable = clickable,\n            isConstrainedInBounds = isConstrainedInBounds,\n            clickableAreaScale = clickableAreaScale,\n            clickableAreaCenterOffset = clickableAreaCenterOffset,\n            renderingStrategy = renderingStrategy,\n            type = MarkerType.Marker,\n            c = c\n        )\n    }\n\n    fun removeMarker(id: String): Boolean {\n        return getMarker(id)?.let {\n            markers.value = markers.value - it\n            true\n        } ?: false\n    }\n\n    fun removeAllMarkers() {\n        markers.value = emptyList()\n    }\n\n    /**\n     * Move a marker by the provided delta (normalized) coordinates.\n     */\n    fun moveMarkerBy(id: String, deltaX: Double, deltaY: Double) {\n        getMarker(id)?.apply {\n            x = (x + deltaX).let {\n                if (isConstrainedInBounds) it.coerceIn(0.0, 1.0) else it\n            }\n            y = (y + deltaY).let {\n                if (isConstrainedInBounds) it.coerceIn(0.0, 1.0) else it\n            }\n        }.also {\n            if (it != null) onMarkerMove(it, deltaX, deltaY)\n        }\n    }\n\n    fun moveMarkerTo(id: String, x: Double, y: Double) {\n        val marker = getMarker(id) ?: return\n        moveMarkerTo(marker, x, y)\n    }\n\n    fun moveMarkerTo(markerData: MarkerData, x: Double, y: Double) {\n        with(markerData) {\n            val prevX = x\n            val prevY = y\n            this.x = if (isConstrainedInBounds) x.coerceIn(0.0, 1.0) else x\n            this.y = if (isConstrainedInBounds) y.coerceIn(0.0, 1.0) else y\n            onMarkerMove(this, this.x - prevX, this.y - prevY)\n        }\n    }\n\n    /**\n     * If set, drag gestures will be handled for the marker identifiable by the [id].\n     */\n    fun setDraggable(id: String, draggable: Boolean) {\n        getMarker(id)?.isDraggable = draggable\n    }\n\n    private fun onMarkerMove(data: MarkerData, dx: Double, dy: Double) {\n        markerMoveCb?.invoke(data.id, data.x, data.y, dx, dy)\n    }\n\n    fun addClusterer(\n        mapState: MapState,\n        id: String,\n        clusteringThreshold: Dp,\n        clusterClickBehavior: ClusterClickBehavior,\n        scaleThreshold: ClusterScaleThreshold,\n        clusterFactory: (ids: List<String>) -> (@Composable () -> Unit)\n    ) {\n        val clusterer = Clusterer(\n            id = id,\n            clusteringThreshold = clusteringThreshold,\n            mapState = mapState,\n            markerRenderState = markerRenderState,\n            markersDataFlow = markers,\n            clusterClickBehavior = clusterClickBehavior,\n            scaleThreshold = scaleThreshold,\n            clusterFactory = clusterFactory\n        )\n        clusterersById[id] = clusterer\n    }\n\n    fun setClusteredExemptList(id: String, markersToExempt: Set<String>) {\n        clusterersById[id]?.apply {\n            exemptionSet.value = markersToExempt\n        }\n    }\n\n    fun addLazyLoader(\n        mapState: MapState,\n        id: String,\n        padding: Dp\n    ) {\n        val lazyLoader = LazyLoader(\n            id, mapState, markerRenderState, markers, padding, mapState.scope\n        )\n        lazyLoaderById[id] = lazyLoader\n    }\n\n    fun removeClusterer(id: String, removeManaged: Boolean) {\n        clusterersById[id]?.apply {\n            cancel(removeManaged = removeManaged)\n        }\n        clusterersById.remove(id)\n\n        if (removeManaged) {\n            removeAll {\n                (it.renderingStrategy is RenderingStrategy.Clustering) &&\n                        (it.renderingStrategy.clustererId == id)\n            }\n        }\n    }\n\n    fun removeLazyLoader(id: String, removeManaged: Boolean) {\n        lazyLoaderById[id]?.apply {\n            cancel(removeManaged = removeManaged)\n        }\n\n        if (removeManaged) {\n            removeAll {\n                (it.renderingStrategy is RenderingStrategy.LazyLoading) &&\n                        (it.renderingStrategy.lazyLoaderId == id)\n            }\n        }\n    }\n\n    fun onHit(x: Int, y: Int, hitType: HitType): Boolean {\n        return markerRenderState.getMarkerForHit(x, y)?.also { markerData ->\n            /* If it's a cluster, run the corresponding click behavior. */\n            if (markerData.type is MarkerType.Cluster && hitType == HitType.Click) {\n                val clusterer = clusterersById[markerData.type.clustererId]\n                clusterer?.onPlaceableClick(markerData)\n            } else {\n                /* It's not a cluster. Invoke user callback, if any. */\n                when (hitType) {\n                    HitType.Click -> markerClickCb?.invoke(markerData.id, markerData.x, markerData.y)\n                    HitType.LongPress -> markerLongPressCb?.invoke(markerData.id, markerData.x, markerData.y)\n                }\n            }\n        } != null\n    }\n\n    private fun removeAll(predicate: (MarkerData) -> Boolean) {\n        markers.value = markers.value.filterNot {\n            predicate(it)\n        }\n    }\n\n    private suspend fun renderRegularMarkers() {\n        markers.collect {\n            val regular = it.filter { markerData ->\n                markerData.renderingStrategy is RenderingStrategy.Default\n            }\n            val rendered = markerRenderState.getRegularMarkers()\n            val toAdd = regular - rendered\n            val toRemove = rendered - regular\n\n            markerRenderState.addRegularMarkers(toAdd)\n            markerRenderState.removeRegularMarkers(toRemove)\n        }\n    }\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/state/markers/model/ClusterClickBehavior.kt",
    "content": "package ovh.plrapps.mapcompose.ui.state.markers.model\n\ninternal sealed interface ClusterClickBehavior\ninternal data object Default : ClusterClickBehavior\ninternal data class Custom(\n    val withDefaultBehavior: Boolean = false,\n    val onClick: (ClusterInfo) -> Unit\n) : ClusterClickBehavior\n\ninternal data object None : ClusterClickBehavior\n\ninternal data class ClusterInfo(val x: Double, val y: Double, val markers: List<MarkerData>)"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/state/markers/model/MarkerData.kt",
    "content": "package ovh.plrapps.mapcompose.ui.state.markers.model\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableDoubleStateOf\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.unit.DpOffset\nimport ovh.plrapps.mapcompose.ui.state.markers.DragEndListener\nimport ovh.plrapps.mapcompose.ui.state.markers.DragInterceptor\nimport ovh.plrapps.mapcompose.ui.state.markers.DragStartListener\nimport ovh.plrapps.mapcompose.utils.Point\nimport java.util.*\n\ninternal class MarkerData(\n    val id: String,\n    x: Double, y: Double,\n    relativeOffset: Offset,\n    absoluteOffset: DpOffset,\n    zIndex: Float,\n    clickable: Boolean,\n    isConstrainedInBounds: Boolean,\n    clickableAreaScale: Offset,\n    clickableAreaCenterOffset: Offset,\n    val renderingStrategy: RenderingStrategy,\n    val type: MarkerType,\n    val c: @Composable () -> Unit\n) {\n    var x: Double by mutableDoubleStateOf(x)\n    var y: Double by mutableDoubleStateOf(y)\n    var relativeOffset by mutableStateOf(relativeOffset)\n    var absoluteOffset by mutableStateOf(absoluteOffset)\n    var isDraggable by mutableStateOf(false)\n    var dragStartListener: DragStartListener? by mutableStateOf(null)\n    var dragEndListener: DragEndListener? by mutableStateOf(null)\n    var dragInterceptor: DragInterceptor? by mutableStateOf(null)\n    var isClickable: Boolean by mutableStateOf(clickable)\n    var clickableAreaScale by mutableStateOf(clickableAreaScale)\n    var clickableAreaCenterOffset by mutableStateOf(clickableAreaCenterOffset)\n    var isVisible: Boolean by mutableStateOf(true)\n    var zIndex: Float by mutableFloatStateOf(zIndex)\n    var isConstrainedInBounds by mutableStateOf(isConstrainedInBounds)\n\n    var measuredWidth = 0\n    var measuredHeight = 0\n    var xPlacement: Double? = null\n    var yPlacement: Double? = null\n    val uuid: UUID = UUID.randomUUID()\n\n    fun contains(x: Int, y: Int): Boolean {\n        val (centerX, centerY) = getCenter() ?: return false\n\n        val deltaX = measuredWidth * clickableAreaScale.x / 2\n        val deltaY = measuredHeight * clickableAreaScale.y / 2\n\n        return (x >= centerX - deltaX && x <= centerX + deltaX\n                && y >= centerY - deltaY && y <= centerY + deltaY)\n    }\n\n    fun getCenter(): Point? {\n        val xPos = xPlacement ?: return null\n        val yPos = yPlacement ?: return null\n\n        val centerX = xPos + measuredWidth / 2 + measuredWidth * clickableAreaCenterOffset.x\n        val centerY = yPos + measuredHeight / 2 + measuredHeight * clickableAreaCenterOffset.y\n        return Point(centerX, centerY)\n    }\n\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (javaClass != other?.javaClass) return false\n\n        other as MarkerData\n\n        return (id == other.id && x == other.x && y == other.y && uuid == other.uuid)\n    }\n\n    override fun hashCode(): Int {\n        var result = id.hashCode()\n        result = 31 * result + x.hashCode()\n        result = 31 * result + y.hashCode()\n        return result\n    }\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/state/markers/model/MarkerType.kt",
    "content": "package ovh.plrapps.mapcompose.ui.state.markers.model\n\ninternal sealed interface MarkerType {\n    data object Marker : MarkerType\n    data object Callout : MarkerType\n    data class Cluster(val clustererId: String, val markersData: List<MarkerData>) : MarkerType\n}\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/state/markers/model/RenderingStrategy.kt",
    "content": "package ovh.plrapps.mapcompose.ui.state.markers.model\n\n\nsealed interface RenderingStrategy {\n    data object Default : RenderingStrategy\n    data class Clustering(val clustererId: String) : RenderingStrategy\n    data class LazyLoading(val lazyLoaderId: String) : RenderingStrategy\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/ui/view/TileCanvas.kt",
    "content": "package ovh.plrapps.mapcompose.ui.view\n\nimport android.graphics.Paint\nimport android.graphics.Rect\nimport android.graphics.Bitmap\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.asAndroidColorFilter\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.graphics.drawscope.drawIntoCanvas\nimport androidx.compose.ui.graphics.drawscope.scale\nimport androidx.compose.ui.graphics.drawscope.withTransform\nimport androidx.compose.ui.graphics.nativeCanvas\nimport ovh.plrapps.mapcompose.core.ColorFilterProvider\nimport ovh.plrapps.mapcompose.core.Tile\nimport ovh.plrapps.mapcompose.core.VisibleTilesResolver\nimport ovh.plrapps.mapcompose.ui.layout.grid\nimport ovh.plrapps.mapcompose.ui.state.Rollover\nimport ovh.plrapps.mapcompose.ui.state.RolloverData\nimport ovh.plrapps.mapcompose.ui.state.ZoomPanRotateState\nimport kotlin.math.ceil\nimport kotlin.time.TimeSource\n\n@Composable\ninternal fun TileCanvas(\n    modifier: Modifier,\n    zoomPRState: ZoomPanRotateState,\n    visibleTilesResolver: VisibleTilesResolver,\n    tileSize: Int,\n    alphaTick: Float,\n    colorFilterProvider: ColorFilterProvider?,\n    tilesToRender: List<Tile>,\n    isFilteringBitmap: () -> Boolean,\n) {\n    val dest = remember { Rect() }\n    val paint: Paint = remember {\n        Paint().apply {\n            isAntiAlias = false\n        }\n    }\n\n    Canvas(\n        modifier = modifier\n            .fillMaxSize()\n    ) {\n        /* Scroll values may not be represented accurately using floats (a float has 7 significant\n         * decimal digits, so any number above ~10M isn't represented accurately).\n         * Since the translate function of the Canvas works with floats, we perform a change of\n         * referential so that we only need to translate the canvas by an amount which can be\n         * precisely represented as a float. */\n        val x0 = ((ceil(zoomPRState.scrollX / grid) * grid) / zoomPRState.scale).toInt()\n        val y0 = ((ceil(zoomPRState.scrollY / grid) * grid) / zoomPRState.scale).toInt()\n\n        withTransform({\n            /* Geometric transformations seem to be applied in reversed order of declaration */\n            rotate(\n                degrees = zoomPRState.rotation,\n                pivot = Offset(\n                    x = zoomPRState.pivotX.toFloat(),\n                    y = zoomPRState.pivotY.toFloat()\n                )\n            )\n            translate(\n                left = (-zoomPRState.scrollX + x0 * zoomPRState.scale).toFloat(),\n                top = (-zoomPRState.scrollY + y0 * zoomPRState.scale).toFloat()\n            )\n            scale(scale = zoomPRState.scale.toFloat(), Offset.Zero)\n        }) {\n            paint.isFilterBitmap = isFilteringBitmap()\n            val rolloverX = zoomPRState.rolloverX.value\n\n            for (tile in tilesToRender) {\n                if (tile.markedForSweep) continue\n                val bitmap = tile.bitmap ?: continue\n                val scaleForLevel = visibleTilesResolver.getScaleForLevel(tile.zoom)\n                    ?: continue\n                val tileScaled = (tileSize / scaleForLevel).toInt()\n                val phases = tile.phases.applyRolloverX(rolloverX, tile.timeMark)\n\n                if (phases == null) {\n                    drawTile(\n                        tile = tile,\n                        tileScaled = tileScaled,\n                        phi = 0,\n                        x0 = x0,\n                        y0 = y0,\n                        dest = dest,\n                        colorFilterProvider = colorFilterProvider,\n                        paint = paint,\n                        bitmap = bitmap,\n                    )\n                } else {\n                    val colCount = visibleTilesResolver.getColCountForLevel(tile.zoom) ?: continue\n                    for (i in phases) {\n                        drawTile(\n                            tile = tile,\n                            tileScaled = tileScaled,\n                            phi = i * colCount,\n                            x0 = x0,\n                            y0 = y0,\n                            dest = dest,\n                            colorFilterProvider = colorFilterProvider,\n                            paint = paint,\n                            bitmap = bitmap,\n                        )\n                    }\n                }\n\n                /* If a tile isn't fully opaque, increase its alpha state by the alpha tick */\n                if (tile.alpha < 1f) {\n                    tile.alpha = (tile.alpha + alphaTick).coerceAtMost(1f)\n                } else {\n                    tile.overlaps?.markedForSweep = true\n                    tile.overlaps = null\n                }\n            }\n        }\n    }\n}\n\nprivate fun DrawScope.drawTile(\n    tile: Tile,\n    tileScaled: Int,\n    phi: Int,\n    x0: Int,\n    y0: Int,\n    dest: Rect,\n    colorFilterProvider: ColorFilterProvider?,\n    paint: Paint,\n    bitmap: Bitmap,\n) {\n    val l = tile.col * tileScaled + phi * tileScaled\n    val t = tile.row * tileScaled\n    val r = l + tileScaled\n    val b = t + tileScaled\n    /* The change of referential is done by offsetting coordinates by (x0, y0) */\n    dest.set(l - x0, t - y0, r - x0, b - y0)\n\n    val colorFilter = colorFilterProvider?.getColorFilter(tile.row, tile.col, tile.zoom)\n\n    paint.alpha = (tile.alpha * 255).toInt()\n    paint.colorFilter = colorFilter?.asAndroidColorFilter()\n\n    drawIntoCanvas {\n        it.nativeCanvas.drawBitmap(bitmap, null, dest, paint)\n    }\n}\n\nprivate fun IntRange?.applyRolloverX(rolloverData: RolloverData?, timeMark: TimeSource.Monotonic.ValueTimeMark?): IntRange? {\n    return if (rolloverData == null || timeMark == null) {\n        this\n    } else {\n        val rollover = getAppliedRollover(rolloverData, timeMark) ?: return this\n        if (this == null) {\n            when (rollover) {\n                Rollover.Forward -> -1..0\n                Rollover.Backward -> 0..1\n                is Rollover.None -> null\n            }\n        } else {\n            when (rollover) {\n                Rollover.Forward -> IntRange(first - 1, last)\n                Rollover.Backward -> IntRange(first, last + 1)\n                is Rollover.None -> this\n            }\n        }\n    }\n}\n\n/**\n * Apply [Rollover.None] only when the tile originates from a snapshot made _after_ the rollover.\n * Otherwise, when the tile originates from a snapshot made _before_ the rollover, the tile's phases\n * should be applied either [Rollover.Forward] or [Rollover.Backward] (depending on the direction\n * of the scroll).\n */\nprivate fun getAppliedRollover(rolloverData: RolloverData, timeMark: TimeSource.Monotonic.ValueTimeMark): Rollover? {\n    return if (rolloverData.current is Rollover.None) {\n        if (timeMark > rolloverData.current.timeMark) {\n            rolloverData.current\n        } else {\n            rolloverData.previous\n        }\n    } else {\n        rolloverData.current\n    }\n}\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/utils/AnimUtils.kt",
    "content": "package ovh.plrapps.mapcompose.utils\n\n/**\n * Calculates a number between two numbers at a specific increment.\n */\nfun lerp(a: Float, b: Float, t: Float): Float {\n    return a + (b - a) * t\n}\n\nfun lerp(a: Double, b: Double, t: Double): Double {\n    return a + (b - a) * t\n}\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/utils/ApiUtils.kt",
    "content": "package ovh.plrapps.mapcompose.utils\n\nimport kotlinx.coroutines.delay\n\ninternal suspend fun withRetry(maxRetry: Int, intervalMs: Long, block: suspend () -> Boolean) {\n    var cnt = 0\n    var res = block()\n    while (!res && cnt < maxRetry) {\n        delay(intervalMs)\n        res = block()\n        cnt++\n    }\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/utils/BoundingBoxUtils.kt",
    "content": "package ovh.plrapps.mapcompose.utils\n\nimport ovh.plrapps.mapcompose.api.BoundingBox\n\ninternal fun BoundingBox.scaleAxis(xAxisMultiplier: Double): BoundingBox {\n    return BoundingBox(xLeft * xAxisMultiplier, yTop, xRight * xAxisMultiplier, yBottom)\n}\n\ninternal fun BoundingBox.rotate(center: Point, angle: AngleRad): BoundingBox {\n    val topLeft = Point(xLeft, yTop)\n    val topRight = Point(xRight, yTop)\n    val bottomLeft = Point(xLeft, yBottom)\n    val bottomRight = Point(xRight, yBottom)\n\n    val points = listOf(topLeft, topRight, bottomLeft, bottomRight)\n    val rotatedPoints = rotateCentered(points, center, angle)\n\n    val left = rotatedPoints.minOf { it.x }\n    val top = rotatedPoints.minOf { it.y }\n    val right = rotatedPoints.maxOf { it.x }\n    val bottom = rotatedPoints.maxOf { it.y }\n\n    return BoundingBox(left, top, right, bottom)\n}\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/utils/Collections.kt",
    "content": "package ovh.plrapps.mapcompose.utils\n\nfun <T> MutableCollection<T>.removeFirst(predicate: (T) -> Boolean): Boolean {\n    var removed = false\n    val it = iterator()\n    while (it.hasNext()) {\n        if (predicate(it.next())) {\n            it.remove()\n            removed = true\n            break\n        }\n    }\n    return removed\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/utils/Dp.kt",
    "content": "package ovh.plrapps.mapcompose.utils\n\nimport android.content.res.Resources\n\nfun dpToPx(dp: Float): Float = dp * Resources.getSystem().displayMetrics.density"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/utils/Flow.kt",
    "content": "package ovh.plrapps.mapcompose.utils\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport ovh.plrapps.mapcompose.utils.map\n\nfun <T> Flow<T>.throttle(wait: Long) = channelFlow {\n    val channel = Channel<T>(capacity = Channel.CONFLATED)\n    coroutineScope {\n        launch {\n            collect {\n                channel.send(it)\n            }\n        }\n        launch {\n            for (e in channel) {\n                send(e)\n                delay(wait)\n            }\n        }\n    }\n}\n\nfun <T, M> StateFlow<T>.map(\n    coroutineScope : CoroutineScope,\n    mapper : (value : T) -> M\n) : StateFlow<M> = map { mapper(it) }.stateIn(\n    coroutineScope,\n    SharingStarted.Eagerly,\n    mapper(value)\n)"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/utils/Geometry.kt",
    "content": "package ovh.plrapps.mapcompose.utils\n\nimport kotlin.math.hypot\n\ninternal fun getDistance(x: Double, y: Double, x1: Double, y1: Double, x2: Double, y2: Double): Double {\n    val a = x - x1\n    val b = y - y1\n    val c = x2 - x1\n    val d = y2 - y1\n\n    val lenSq = c * c + d * d\n    val param = if (lenSq != 0.0) {\n        val dot = a * c + b * d\n        dot / lenSq\n    } else {\n        -1.0\n    }\n\n    val (xx, yy) = when {\n        param < 0.0 -> x1 to y1\n        param > 1.0 -> x2 to y2\n        else -> x1 + param * c to y1 + param * d\n    }\n\n    val dx = x - xx\n    val dy = y - yy\n    return hypot(dx, dy)\n}\n\ninternal fun getNearestPoint(\n    x: Double,\n    y: Double,\n    x1: Double,\n    y1: Double,\n    x2: Double,\n    y2: Double\n): Point {\n    val a = x - x1\n    val b = y - y1\n    val c = x2 - x1\n    val d = y2 - y1\n\n    val lenSq = c * c + d * d\n    val param = if (lenSq != 0.0) {\n        val dot = a * c + b * d\n        dot / lenSq\n    } else {\n        -1.0\n    }\n\n    val (xx, yy) = when {\n        param < 0.0 -> x1 to y1\n        param > 1.0 -> x2 to y2\n        else -> x1 + param * c to y1 + param * d\n    }\n\n    return Point(xx, yy)\n}\n\ninternal fun isInsideBox(x: Double, y: Double, xMin: Double, xMax: Double, yMin: Double, yMax: Double): Boolean {\n    return x in xMin..xMax && y in yMin..yMax\n}\n\ninternal fun getDistanceFromBox(x: Double, y: Double, xMin: Double, xMax: Double, yMin: Double, yMax: Double): Double {\n    return when {\n        x < xMin -> getDistance(x, y, xMin, yMin, xMin, yMax)\n        x > xMax -> getDistance(x, y, xMax, yMin, xMax, yMax)\n        y < yMin -> getDistance(x, y, xMin, yMin, xMax, yMin)\n        y > yMax -> getDistance(x, y, xMin, yMax, xMax, yMax)\n        else -> 0.0 // inside\n    }\n}"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/utils/Point.kt",
    "content": "package ovh.plrapps.mapcompose.utils\n\ndata class Point(val x: Double, val y: Double)\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/utils/RotationUtils.kt",
    "content": "package ovh.plrapps.mapcompose.utils\n\nimport kotlin.math.cos\nimport kotlin.math.sin\n\ntypealias AngleDegree = Float\ntypealias AngleRad = Float\n\nfun AngleDegree.toRad(): AngleRad = this * 0.017453292519943295f  // this * PI / 180.0\n\n/**\n * Constrain the angle to have values between 0f and 360f.\n */\nfun AngleDegree.modulo(): AngleDegree {\n    val mod = this % 360f\n    return if (mod < 0) {\n        mod + 360f\n    } else mod\n}\n\nfun rotateX(x: Double, y: Double, angleRad: AngleRad): Double {\n    return x * cos(angleRad) - y * sin(angleRad)\n}\n\nfun rotateY(x: Double, y: Double, angleRad: AngleRad): Double {\n    return x * sin(angleRad) + y * cos(angleRad)\n}\n\nfun rotateCentered(points: List<Point>, center: Point, angleRad: AngleRad): List<Point> {\n    return points.map { rotateCentered(it, center, angleRad) }\n}\n\nfun rotateCentered(point: Point, center: Point, angleRad: AngleRad): Point {\n    return Point(rotateCenteredX(point, center, angleRad), rotateCenteredY(point, center, angleRad))\n}\n\nfun rotateCenteredX(point: Point, center: Point, angleRad: AngleRad): Double {\n    return rotateCenteredX(point.x, point.y, center.x, center.y, angleRad)\n}\n\nfun rotateCenteredY(point: Point, center: Point, angleRad: AngleRad): Double {\n    return rotateCenteredY(point.x, point.y, center.x, center.y, angleRad)\n}\n\nfun rotateCenteredX(x: Double, y: Double, centerX: Double, centerY: Double, angleRad: AngleRad): Double {\n    return centerX + (x - centerX) * cos(angleRad) - (y - centerY) * sin(angleRad)\n}\n\nfun rotateCenteredY(x: Double, y: Double, centerX: Double, centerY: Double, angleRad: AngleRad): Double {\n    return centerY + (x - centerX) * sin(angleRad) + (y - centerY) * cos(angleRad)\n}\n"
  },
  {
    "path": "mapcompose/src/main/java/ovh/plrapps/mapcompose/utils/VisibleAreaUtils.kt",
    "content": "package ovh.plrapps.mapcompose.utils\n\nimport ovh.plrapps.mapcompose.api.VisibleArea\nimport kotlin.math.abs\nimport kotlin.math.max\nimport kotlin.math.min\n\nfun VisibleArea.contains(x: Double, y: Double): Boolean {\n    val fullArea =\n        triangleArea(p1x, p1y, p2x, p2y, p3x, p3y) + triangleArea(p1x, p1y, p4x, p4y, p3x, p3y)\n    val t1 = triangleArea(x, y, p1x, p1y, p2x, p2y)\n    val t2 = triangleArea(x, y, p2x, p2y, p3x, p3y)\n    val t3 = triangleArea(x, y, p3x, p3y, p4x, p4y)\n    val t4 = triangleArea(x, y, p1x, p1y, p4x, p4y)\n\n    return abs(fullArea - (t1 + t2 + t3 + t4)) < 1E-8\n}\n\nprivate fun triangleArea(\n    x1: Double,\n    y1: Double,\n    x2: Double,\n    y2: Double,\n    x3: Double,\n    y3: Double\n): Double {\n    return abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2.0)\n}\n\nfun VisibleArea.intersects(other: VisibleArea): Boolean {\n    if (\n        other.contains(p1x, p1y) ||\n        other.contains(p2x, p2y) ||\n        other.contains(p3x, p3y) ||\n        other.contains(p4x, p4y)\n    ) return true\n\n    if (\n        contains(other.p1x, other.p1y) ||\n        contains(other.p2x, other.p2y) ||\n        contains(other.p3x, other.p3y) ||\n        contains(other.p4x, other.p4y)\n    ) return true\n\n    if (\n        segmentsIntersect(p1x, p1y, p3x, p3y, other.p1x, other.p1y, other.p3x, other.p3y)\n    ) return true\n\n    return false\n}\n\n/**\n * Checks whether the two segments [p1;p2] and [p3;p4] intersect.\n */\nprivate fun segmentsIntersect(\n    p1x: Double,\n    p1y: Double,\n    p2x: Double,\n    p2y: Double,\n    p3x: Double,\n    p3y: Double,\n    p4x: Double,\n    p4y: Double\n): Boolean {\n    val o1 = orientation(p1x, p1y, p2x, p2y, p3x, p3y)\n    val o2 = orientation(p1x, p1y, p2x, p2y, p4x, p4y)\n    val o3 = orientation(p3x, p3y, p4x, p4y, p1x, p1y)\n    val o4 = orientation(p3x, p3y, p4x, p4y, p2x, p2y)\n\n    // General case\n    if (o1 != o2 && o3 != o4) return true\n\n    // p1, q1 and p2 are collinear and p2 lies on segment [p1;q1]\n    if (o1 == 0 && onSegment(p1x, p1y, p3x, p3y, p2x, p2y)) return true\n\n    // p1, q1 and q2 are collinear and q2 lies on segment [p1;q1]\n    if (o2 == 0 && onSegment(p1x, p1y, p4x, p4y, p2x, p2y)) return true\n\n    // p2, q2 and p1 are collinear and p1 lies on segment [p2;q2]\n    if (o3 == 0 && onSegment(p3x, p3y, p1x, p1y, p4x, p4y)) return true\n\n    // p2, q2 and q1 are collinear and q1 lies on segment [p2;q2]\n    if (o4 == 0 && onSegment(p3x, p3y, p2x, p2y, p4x, p4y)) return true\n\n    return false\n}\n\n/**\n * Given three collinear points p1, p2, p3, check if point p2 lies on line segment [p1;p2]]\n */\nprivate fun onSegment(\n    p1x: Double,\n    p1y: Double,\n    p2x: Double,\n    p2y: Double,\n    p3x: Double,\n    p3y: Double\n): Boolean {\n    return p2x <= max(p1x, p3x) && p2x >= min(p1x, p3x) && p2y <= max(p1y, p3y) && p2y >= min(p1y, p3y)\n}\n\n/**\n * Get the orientation of ordered triplet (p1, p2, p3).\n * @returns 0 when p1, p2 and p3 are collinear\n *          1 when Clockwise\n *          2 when Counterclockwise\n */\nprivate fun orientation(\n    p1x: Double,\n    p1y: Double,\n    p2x: Double,\n    p2y: Double,\n    p3x: Double,\n    p3y: Double\n): Int {\n    val p = (p2y - p1y) * (p3x - p2x) - (p2x - p1x) * (p3y - p2y)\n    return if (p == 0.0) 0 else if (p > 0) 1 else 2\n}"
  },
  {
    "path": "mapcompose/src/test/java/ovh/plrapps/mapcompose/core/TileCollectorTest.kt",
    "content": "package ovh.plrapps.mapcompose.core\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.os.Build\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.channels.ReceiveChannel\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Assert.assertEquals\nimport org.junit.Assert.assertNotNull\nimport org.junit.Assert.assertTrue\nimport org.junit.Assert.fail\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.robolectric.RobolectricTestRunner\nimport org.robolectric.annotation.Config\nimport java.io.File\nimport java.io.FileInputStream\n\n/**\n * Test the [TileCollector.collectTiles] engine. The following assertions are tested:\n * * If [TileSpec]s are send to the input channel, corresponding [Tile]s are received from the\n * output channel (from the [TileCollector.collectTiles] point of view).\n * * The [Bitmap] of the [Tile]s produced should be consistent with the output of the flow\n */\n@RunWith(RobolectricTestRunner::class)\n@Config(sdk = [Build.VERSION_CODES.P])\nclass TileCollectorTest {\n\n    private val tileSize = 256\n\n    companion object {\n        private var assetsDir: File? = null\n\n        init {\n            try {\n                val mapviewDirURL = TileCollectorTest::class.java.classLoader!!.getResource(\"tiles\")\n                assetsDir = File(mapviewDirURL.toURI())\n            } catch (e: Exception) {\n                println(\"No tiles directory found.\")\n            }\n\n        }\n    }\n\n    @Test\n    fun fullTest() = runTest {\n        assertNotNull(assetsDir)\n        val imageFile = File(assetsDir, \"10.jpg\")\n        assertTrue(imageFile.exists())\n\n        /* Setup the channels */\n        val visibleTileLocationsChannel = Channel<TileSpec>(capacity = Channel.RENDEZVOUS)\n        val tilesOutput = Channel<Tile>(capacity = Channel.RENDEZVOUS)\n\n        val tileStreamProvider = TileStreamProvider { _, _, _ -> FileInputStream(imageFile) }\n\n        val bitmapReference = try {\n            val inputStream = FileInputStream(imageFile)\n            BitmapFactory.decodeStream(inputStream, null, null)\n        } catch (e: Exception) {\n            fail()\n            error(\"Could not decode image\")\n        }\n\n\n        val layers = listOf(\n            Layer(\"default\", tileStreamProvider)\n        )\n\n        /* Start collecting tiles */\n        val tileCollector = TileCollector(1, optimizeForLowEndDevices = false, tileSize)\n        val tileCollectorJob = launch {\n            tileCollector.collectTiles(visibleTileLocationsChannel, tilesOutput, layers)\n        }\n\n        fun CoroutineScope.consumeTiles(tileChannel: ReceiveChannel<Tile>) = launch {\n            var receivedTiles = 0\n            for (tile in tileChannel) {\n                println(\"received tile ${tile.zoom}-${tile.row}-${tile.col}\")\n                assertTrue(tile.bitmap?.sameAs(bitmapReference) ?: false)\n                receivedTiles += 1\n\n                if (tile.zoom == 6 && tile.row == 6 && tile.col == 6) {\n                    println(\"received poison pill\")\n                    assertEquals(7, receivedTiles)\n                    cancel()\n                    tileCollectorJob.cancel()\n                }\n            }\n        }\n\n        /* Start consuming tiles */\n        consumeTiles(tilesOutput)\n\n        launch {\n            val locations1 = listOf(\n                TileSpec(0, 0, 0),\n                TileSpec(0, 1, 1),\n                TileSpec(0, 2, 1)\n            )\n            for (spec in locations1) {\n                visibleTileLocationsChannel.send(spec)\n            }\n\n            val locations2 = listOf(\n                TileSpec(1, 0, 0),\n                TileSpec(1, 1, 1),\n                TileSpec(1, 2, 1),\n                TileSpec(6, 6, 6),  // poison pill\n            )\n\n            for (spec in locations2) {\n                visibleTileLocationsChannel.send(spec)\n            }\n        }\n        Unit\n    }\n}"
  },
  {
    "path": "mapcompose/src/test/java/ovh/plrapps/mapcompose/core/VisibleTilesResolverTest.kt",
    "content": "package ovh.plrapps.mapcompose.core\n\nimport org.junit.Assert.assertEquals\nimport org.junit.Assert.assertTrue\nimport org.junit.Test\nimport ovh.plrapps.mapcompose.core.VisibleTilesResolver.*\nimport kotlin.math.pow\n\nclass VisibleTilesResolverTest {\n    private var scale = 1.0\n\n    private val scaleProvider = ScaleProvider { scale }\n\n    @Test\n    fun levelTest() {\n        val resolver = VisibleTilesResolver(\n            levelCount = 8,\n            fullWidth = 1000,\n            fullHeight = 800,\n            tileSize = 256,\n            magnifyingFactor = 0,\n            infiniteScrollX = false,\n            scaleProvider = scaleProvider\n        )\n\n        assertEquals(7, resolver.getLevel(1.0))\n        assertEquals(7, resolver.getLevel(0.7))\n        assertEquals(6, resolver.getLevel(0.5))\n        assertEquals(6, resolver.getLevel(0.26))\n        assertEquals(5, resolver.getLevel(0.15))\n        assertEquals(0, resolver.getLevel(0.0078))\n        assertEquals(1, resolver.getLevel(0.008))\n\n        /* Outside of bounds test */\n        assertEquals(0, resolver.getLevel(0.0030))\n        assertEquals(7, resolver.getLevel(1.0))\n    }\n\n    @Test\n    fun subSampleTest() {\n        val resolver = VisibleTilesResolver(\n            levelCount = 8,\n            fullWidth = 1000,\n            fullHeight = 800,\n            tileSize = 256,\n            magnifyingFactor = 0,\n            infiniteScrollX = false,\n            scaleProvider = scaleProvider\n        )\n\n        assertEquals(0, resolver.getSubSample(0.008))\n        assertEquals(1, resolver.getSubSample(0.0078)) // 0.0078 is the scale of level 0\n\n        /* Outside of bounds: subsample should be at least 1 */\n        assertEquals(1, resolver.getSubSample(1.0 / 2.0.pow(7.5)))\n        assertEquals(2, resolver.getSubSample(1.0 / 2.0.pow(9)))\n        assertEquals(3, resolver.getSubSample(1.0 / 2.0.pow(10)))\n    }\n\n    @Test\n    fun infiniteScrollXTest() {\n        val resolver = VisibleTilesResolver(\n            levelCount = 3,\n            fullWidth = 1024,\n            fullHeight = 800,\n            tileSize = 256,\n            magnifyingFactor = 0,\n            infiniteScrollX = true,\n            scaleProvider = scaleProvider\n        )\n        scale = 1.0\n        var viewport = Viewport(-256, 0, 512, 768)\n\n        var visibleTiles = resolver.getVisibleTiles(viewport)\n        var visibleWindow = visibleTiles.visibleWindow as VisibleWindow.InfiniteScrollX\n        val tileMatrix = visibleWindow.tileMatrix\n        with(tileMatrix.toTileRange()) {\n            assertEquals(2, visibleTiles.level)\n            assertEquals(0, colLeft)\n            assertEquals(1, colRight)\n            assertEquals(0, rowTop)\n            assertEquals(2, rowBottom)\n        }\n\n        var overflowLeft = visibleWindow.leftOverflow?.tileMatrix?.toTileRange()\n        with(overflowLeft!!) {\n            assertEquals(2, visibleTiles.level)\n            assertEquals(3, colLeft)\n            assertEquals(3, colRight)\n            assertEquals(0, rowTop)\n            assertEquals(2, rowBottom)\n        }\n\n        var phaseLeft = visibleWindow.leftOverflow?.phase\n        assertEquals(mapOf(3 to -1), phaseLeft)\n\n        var overflowRight = visibleWindow.rightOverflow?.tileMatrix?.toTileRange()\n        assertTrue(overflowRight == null)\n\n        /* ------------------------------------------------------------------ */\n\n        viewport = Viewport(-513, 0, 512, 768)\n\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        visibleWindow = visibleTiles.visibleWindow as VisibleWindow.InfiniteScrollX\n        overflowLeft = visibleWindow.leftOverflow?.tileMatrix?.toTileRange()\n        with(overflowLeft!!) {\n            assertEquals(2, visibleTiles.level)\n            assertEquals(1, colLeft)\n            assertEquals(3, colRight)\n            assertEquals(0, rowTop)\n            assertEquals(2, rowBottom)\n        }\n\n        phaseLeft = visibleWindow.leftOverflow?.phase\n        assertEquals(mapOf(3 to -1, 2 to -1, 1 to -1), phaseLeft)\n\n        overflowRight = visibleWindow.rightOverflow?.tileMatrix?.toTileRange()\n        assertTrue(overflowRight == null)\n\n        /* ------------------------------------------------------------------ */\n\n        viewport = Viewport(-1024, 0, 512, 768)\n\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        visibleWindow = visibleTiles.visibleWindow as VisibleWindow.InfiniteScrollX\n        overflowLeft = visibleWindow.leftOverflow?.tileMatrix?.toTileRange()\n        with(overflowLeft!!) {\n            assertEquals(2, visibleTiles.level)\n            assertEquals(0, colLeft)\n            assertEquals(3, colRight)\n            assertEquals(0, rowTop)\n            assertEquals(2, rowBottom)\n        }\n\n        phaseLeft = visibleWindow.leftOverflow?.phase\n        assertEquals(mapOf(3 to -1, 2 to -1, 1 to -1, 0 to -1), phaseLeft)\n\n        overflowRight = visibleWindow.rightOverflow?.tileMatrix?.toTileRange()\n        assertTrue(overflowRight == null)\n\n        /* ------------------------------------------------------------------ */\n\n        viewport = Viewport(-1792, 0, 512, 768)\n\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        visibleWindow = visibleTiles.visibleWindow as VisibleWindow.InfiniteScrollX\n        overflowLeft = visibleWindow.leftOverflow?.tileMatrix?.toTileRange()\n        with(overflowLeft!!) {\n            assertEquals(2, visibleTiles.level)\n            assertEquals(0, colLeft)\n            assertEquals(3, colRight)\n            assertEquals(0, rowTop)\n            assertEquals(2, rowBottom)\n        }\n\n        phaseLeft = visibleWindow.leftOverflow?.phase\n        assertEquals(mapOf(3 to -2, 2 to -2, 1 to -2, 0 to -1), phaseLeft)\n\n        overflowRight = visibleWindow.rightOverflow?.tileMatrix?.toTileRange()\n        assertTrue(overflowRight == null)\n\n        /* ------------------------------------------------------------------ */\n\n        viewport = Viewport(-2049, 0, 512, 768)\n\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        visibleWindow = visibleTiles.visibleWindow as VisibleWindow.InfiniteScrollX\n        overflowLeft = visibleWindow.leftOverflow?.tileMatrix?.toTileRange()\n        with(overflowLeft!!) {\n            assertEquals(2, visibleTiles.level)\n            assertEquals(0, colLeft)\n            assertEquals(3, colRight)\n            assertEquals(0, rowTop)\n            assertEquals(2, rowBottom)\n        }\n\n        phaseLeft = visibleWindow.leftOverflow?.phase\n        assertEquals(mapOf(3 to -3, 2 to -2, 1 to -2, 0 to -2), phaseLeft)\n\n        overflowRight = visibleWindow.rightOverflow?.tileMatrix?.toTileRange()\n        assertTrue(overflowRight == null)\n\n        /* ------------------------------------------------------------------ */\n\n        viewport = Viewport(0, 0, 1024 + 512, 768)\n\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        visibleWindow = visibleTiles.visibleWindow as VisibleWindow.InfiniteScrollX\n        assertTrue(visibleWindow.leftOverflow == null)\n\n        overflowRight = visibleWindow.rightOverflow?.tileMatrix?.toTileRange()\n        with(overflowRight!!) {\n            assertEquals(2, visibleTiles.level)\n            assertEquals(0, colLeft)\n            assertEquals(1, colRight)\n            assertEquals(0, rowTop)\n            assertEquals(2, rowBottom)\n        }\n\n        var phaseRight = visibleWindow.rightOverflow?.phase\n        assertEquals(mapOf(0 to 1, 1 to 1), phaseRight)\n\n        /* ------------------------------------------------------------------ */\n\n        viewport = Viewport(0, 0, 1024 + 1025, 768)\n\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        visibleWindow = visibleTiles.visibleWindow as VisibleWindow.InfiniteScrollX\n        assertTrue(visibleWindow.leftOverflow == null)\n\n        overflowRight = visibleWindow.rightOverflow?.tileMatrix?.toTileRange()\n        with(overflowRight!!) {\n            assertEquals(2, visibleTiles.level)\n            assertEquals(0, colLeft)\n            assertEquals(3, colRight)\n            assertEquals(0, rowTop)\n            assertEquals(2, rowBottom)\n        }\n\n        phaseRight = visibleWindow.rightOverflow?.phase\n        assertEquals(mapOf(0 to 2, 1 to 1, 2 to 1, 3 to 1), phaseRight)\n\n        /* ------------------------------------------------------------------ */\n\n        scale = 0.5\n        viewport = Viewport(-256, 0, 512, 768)\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        visibleWindow = visibleTiles.visibleWindow as VisibleWindow.InfiniteScrollX\n        overflowLeft = visibleWindow.leftOverflow?.tileMatrix?.toTileRange()\n        with(overflowLeft!!) {\n            assertEquals(1, visibleTiles.level)\n            assertEquals(1, colLeft)\n            assertEquals(1, colRight)\n            assertEquals(0, rowTop)\n            assertEquals(1, rowBottom)\n        }\n\n        phaseLeft = visibleWindow.leftOverflow?.phase\n        assertEquals(mapOf(1 to -1), phaseLeft)\n\n        overflowRight = visibleWindow.rightOverflow?.tileMatrix?.toTileRange()\n        assertTrue(overflowRight == null)\n    }\n\n    @Test\n    fun viewportTestSimple() {\n        val resolver = VisibleTilesResolver(\n            levelCount = 3,\n            fullWidth = 1000,\n            fullHeight = 800,\n            tileSize = 256,\n            magnifyingFactor = 0,\n            infiniteScrollX = false,\n            scaleProvider = scaleProvider\n        )\n        var viewport = Viewport(0, 0, 700, 512)\n\n        var visibleTiles = resolver.getVisibleTiles(viewport)\n        var tileMatrix = (visibleTiles.visibleWindow as VisibleWindow.BoundsConstrained).tileMatrix\n        with(tileMatrix.toTileRange()) {\n            assertEquals(2, visibleTiles.level)\n            assertEquals(0, colLeft)\n            assertEquals(0, rowTop)\n            assertEquals(2, colRight)\n            assertEquals(1, rowBottom)\n        }\n\n\n        scale = 0.5\n        viewport = Viewport(0, 0, 512, 512)\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        tileMatrix = (visibleTiles.visibleWindow as VisibleWindow.BoundsConstrained).tileMatrix\n        with(tileMatrix.toTileRange()) {\n            assertEquals(1, visibleTiles.level)\n            assertEquals(0, colLeft)\n            assertEquals(0, rowTop)\n            assertEquals(1, colRight)\n            assertEquals(1, rowBottom)\n        }\n\n\n        scale = 1.0\n        val resolver2 = VisibleTilesResolver(\n            levelCount = 5,\n            fullWidth = 8192,\n            fullHeight = 8192,\n            tileSize = 256,\n            magnifyingFactor = 0,\n            infiniteScrollX = false,\n            scaleProvider = scaleProvider\n        )\n        val viewport2 = Viewport(0, 0, 8192, 8192)\n        visibleTiles = resolver2.getVisibleTiles(viewport2)\n        tileMatrix = (visibleTiles.visibleWindow as VisibleWindow.BoundsConstrained).tileMatrix\n        with(tileMatrix.toTileRange()) {\n            assertEquals(4, visibleTiles.level)\n            assertEquals(0, colLeft)\n            assertEquals(0, rowTop)\n            assertEquals(31, colRight)\n            assertEquals(31, rowBottom)\n        }\n    }\n\n    @Test\n    fun viewportTestAdvanced() {\n        // 6-level map.\n        // 256 * 2⁶ = 16384\n        scale = 1.0\n        val resolver = VisibleTilesResolver(\n            levelCount = 6,\n            fullWidth = 16400,\n            fullHeight = 8000,\n            tileSize = 256,\n            magnifyingFactor = 0,\n            infiniteScrollX = false,\n            scaleProvider = scaleProvider\n        )\n        var viewport = Viewport(0, 0, 1080, 1380)\n        var visibleTiles = resolver.getVisibleTiles(viewport)\n        var tileMatrix = (visibleTiles.visibleWindow as VisibleWindow.BoundsConstrained).tileMatrix\n        with(tileMatrix.toTileRange()) {\n            assertEquals(5, visibleTiles.level)\n            assertEquals(0, colLeft)\n            assertEquals(0, rowTop)\n            assertEquals(4, colRight)\n            assertEquals(5, rowBottom)\n        }\n\n        viewport = Viewport(4753, 6222, 4753 + 1080, 6222 + 1380)\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        tileMatrix = (visibleTiles.visibleWindow as VisibleWindow.BoundsConstrained).tileMatrix\n        with(tileMatrix.toTileRange()) {\n            assertEquals(5, visibleTiles.level)\n            assertEquals(18, colLeft)\n            assertEquals(24, rowTop)\n            assertEquals(22, colRight)\n            assertEquals(29, rowBottom)\n        }\n\n        viewport = Viewport(3720, 1543, 3720 + 1080, 1543 + 1380)\n        scale = 0.5\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        tileMatrix = (visibleTiles.visibleWindow as VisibleWindow.BoundsConstrained).tileMatrix\n        with(tileMatrix.toTileRange()) {\n            assertEquals(4, visibleTiles.level)\n            assertEquals(14, colLeft)\n            assertEquals(6, rowTop)\n            assertEquals(18, colRight)\n            assertEquals(11, rowBottom)\n        }\n\n        viewport = Viewport(3720, 1543, 3720 + 1080, 1543 + 1380)\n        scale = 0.71\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        tileMatrix = (visibleTiles.visibleWindow as VisibleWindow.BoundsConstrained).tileMatrix\n        with(tileMatrix.toTileRange()) {\n            assertEquals(5, visibleTiles.level)\n            assertEquals(20, colLeft)\n            assertEquals(8, rowTop)\n            assertEquals(26, colRight)\n            assertEquals(16, rowBottom)\n        }\n\n        viewport = Viewport(1643, 427, 1643 + 1080, 427 + 1380)\n        scale = 0.43\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        tileMatrix = (visibleTiles.visibleWindow as VisibleWindow.BoundsConstrained).tileMatrix\n        with(tileMatrix.toTileRange()) {\n            assertEquals(4, visibleTiles.level)\n            assertEquals(7, colLeft)\n            assertEquals(1, rowTop)\n            assertEquals(12, colRight)\n            assertEquals(8, rowBottom)\n        }\n    }\n\n    @Test\n    fun viewportMagnifyingTest() {\n        // 6-level map.\n        // 256 * 2⁶ = 16384\n        var resolver = VisibleTilesResolver(\n            levelCount = 6,\n            fullWidth = 16400,\n            fullHeight = 8000,\n            tileSize = 256, magnifyingFactor = 1,\n            infiniteScrollX = false,\n            scaleProvider = scaleProvider\n        )\n        scale = 0.37\n        var viewport = Viewport(3720, 1543, 3720 + 1080, 1543 + 1380)\n        var visibleTiles = resolver.getVisibleTiles(viewport)\n        var tileMatrix = (visibleTiles.visibleWindow as VisibleWindow.BoundsConstrained).tileMatrix\n        with(tileMatrix.toTileRange()) {\n            assertEquals(3, visibleTiles.level)\n            assertEquals(9, colLeft)\n            assertEquals(4, rowTop)\n            assertEquals(12, colRight)\n            assertEquals(7, rowBottom)\n        }\n\n        // magnify even further, with an abnormally big viewport\n        resolver = VisibleTilesResolver(\n            levelCount = 6,\n            fullWidth = 16400,\n            fullHeight = 8000,\n            tileSize = 256, magnifyingFactor = 2,\n            infiniteScrollX = false,\n            scaleProvider = scaleProvider\n        )\n        viewport = Viewport(250, 123, 250 + 1080, 123 + 1380)\n        scale = 0.37\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        tileMatrix = (visibleTiles.visibleWindow as VisibleWindow.BoundsConstrained).tileMatrix\n        with(tileMatrix.toTileRange()) {\n            assertEquals(2, visibleTiles.level)\n            assertEquals(0, colLeft)\n            assertEquals(0, rowTop)\n            assertEquals(1, colRight)\n            assertEquals(1, rowBottom)\n        }\n\n        // (un)magnify\n        resolver = VisibleTilesResolver(\n            levelCount = 6,\n            fullWidth = 16400,\n            fullHeight = 8000,\n            tileSize = 256, magnifyingFactor = -1,\n            infiniteScrollX = false,\n            scaleProvider = scaleProvider\n        )\n        viewport = Viewport(3720, 1543, 3720 + 1080, 1543 + 1380)\n        scale = 0.37\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        tileMatrix = (visibleTiles.visibleWindow as VisibleWindow.BoundsConstrained).tileMatrix\n        with(tileMatrix.toTileRange()) {\n            assertEquals(5, visibleTiles.level)\n            assertEquals(39, colLeft)\n            assertEquals(16, rowTop)\n            assertEquals(50, colRight)\n            assertEquals(30, rowBottom)\n        }\n\n        // Try to (un)magnify beyond available level: this shouldn't change anything\n        resolver = VisibleTilesResolver(\n            levelCount = 6,\n            fullWidth = 16400,\n            fullHeight = 8000,\n            tileSize = 256, magnifyingFactor = -2,\n            infiniteScrollX = false,\n            scaleProvider = scaleProvider\n        )\n        viewport = Viewport(3720, 1543, 3720 + 1080, 1543 + 1380)\n        scale = 0.37\n        visibleTiles = resolver.getVisibleTiles(viewport)\n        tileMatrix = (visibleTiles.visibleWindow as VisibleWindow.BoundsConstrained).tileMatrix\n        with(tileMatrix.toTileRange()) {\n            assertEquals(5, visibleTiles.level)\n            assertEquals(39, colLeft)\n            assertEquals(16, rowTop)\n            assertEquals(50, colRight)\n            assertEquals(30, rowBottom)\n        }\n    }\n}\n\nprivate data class TileRange(val colLeft: Int, val rowTop: Int, val colRight: Int, val rowBottom: Int)\n\n/**\n * If the tile matrix represents a rectangle, then is can be represented by a [TileRange].\n * It only makes sense when the angle of rotation is 0 modulo pi/2\n */\nprivate fun TileMatrix.toTileRange(): TileRange {\n    val rowTop = keys.minOrNull()!!\n    val rowBottom = keys.maxOrNull()!!\n    val colRange = getValue(rowTop)\n    val colLeft = colRange.first\n    val colRight = colRange.last\n    return TileRange(colLeft, rowTop, colRight, rowBottom)\n}"
  },
  {
    "path": "mapcompose/src/test/java/ovh/plrapps/mapcompose/state/TileCanvasStateTest.kt",
    "content": "@file:OptIn(ExperimentalCoroutinesApi::class)\n\npackage ovh.plrapps.mapcompose.state\n\nimport android.os.Build\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.test.UnconfinedTestDispatcher\nimport kotlinx.coroutines.test.runTest\nimport kotlinx.coroutines.test.setMain\nimport org.junit.Assert.assertEquals\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport org.robolectric.RobolectricTestRunner\nimport org.robolectric.annotation.Config\nimport ovh.plrapps.mapcompose.core.Layer\nimport ovh.plrapps.mapcompose.core.TileStreamProvider\nimport ovh.plrapps.mapcompose.core.Viewport\nimport ovh.plrapps.mapcompose.core.VisibleTilesResolver\nimport ovh.plrapps.mapcompose.ui.state.TileCanvasState\n\n@RunWith(RobolectricTestRunner::class)\n@Config(sdk = [Build.VERSION_CODES.P])\nclass TileCanvasStateTest {\n\n    /**\n     * This test checks that the correct list of tiles is sent for rendering when `infiniteScrollX`\n     * is set to `true`.\n     */\n    @Test\n    fun infiniteScrollTest() = runTest {\n        val testDispatcher = UnconfinedTestDispatcher(testScheduler)\n        Dispatchers.setMain(testDispatcher)\n\n        val scaleProvider = object : VisibleTilesResolver.ScaleProvider {\n            override fun getScale(): Double {\n                return 1.0\n            }\n        }\n        val tileCanvasState = TileCanvasState(\n            parentScope = backgroundScope,\n            visibleTilesResolver = VisibleTilesResolver(\n                levelCount = 4,\n                fullWidth = 1024,\n                fullHeight = 1024,\n                infiniteScrollX = true,\n                scaleProvider = scaleProvider\n            ),\n            workerCount = 4,\n            tileSize = 256,\n            highFidelityColors = true\n        )\n\n        tileCanvasState.setLayers(\n            listOf(\n                Layer(\n                    id = \"id\",\n                    tileStreamProvider = TileStreamProvider { _, _, _ ->\n                        return@TileStreamProvider null // actual bitmaps don't matter in this test\n                    }\n                )\n            )\n        )\n\n\n        launch(Dispatchers.Default) {\n            /* Overflow on the left */\n            tileCanvasState.setViewport(Viewport(-1024, 0, 256, 512))\n            delay(500)  // wait for tile production\n\n            assertEquals(8, tileCanvasState.tilesToRender.size)\n            var tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 0 && it.col == 0 }\n            assertEquals(-1..0, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 1 && it.col == 0 }\n            assertEquals(-1..0, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 0 && it.col == 1 }\n            assertEquals(-1..-1, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 1 && it.col == 1 }\n            assertEquals(-1..-1, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 0 && it.col == 2 }\n            assertEquals(-1..-1, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 1 && it.col == 3 }\n            assertEquals(-1..-1, tile?.phases)\n\n            /* Overflow on the right */\n            tileCanvasState.setViewport(Viewport(768, 0, 1024 + 1024 + 1024, 512))\n            delay(500)  // wait for tile production\n\n            assertEquals(8, tileCanvasState.tilesToRender.size)\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 0 && it.col == 3 }\n            assertEquals(0..2, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 1 && it.col == 3 }\n            assertEquals(0..2, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 0 && it.col == 1 }\n            assertEquals(1..2, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 1 && it.col == 1 }\n            assertEquals(1..2, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 0 && it.col == 2 }\n            assertEquals(1..2, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 1 && it.col == 3 }\n            assertEquals(0..2, tile?.phases)\n\n            /* Overflow on both left and right */\n            tileCanvasState.setViewport(Viewport(-1024, 0, 1024 + 1024 + 1024, 512))\n            delay(500)  // wait for tile production\n\n            assertEquals(8, tileCanvasState.tilesToRender.size)\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 0 && it.col == 3 }\n            assertEquals(-1..2, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 1 && it.col == 3 }\n            assertEquals(-1..2, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 0 && it.col == 1 }\n            assertEquals(-1..2, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 1 && it.col == 1 }\n            assertEquals(-1..2, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 0 && it.col == 2 }\n            assertEquals(-1..2, tile?.phases)\n\n            tile = tileCanvasState.tilesToRender.firstOrNull { it.row == 1 && it.col == 3 }\n            assertEquals(-1..2, tile?.phases)\n        }\n    }\n}"
  },
  {
    "path": "mapcompose/src/test/java/ovh/plrapps/mapcompose/ui/paths/PathComposerTest.kt",
    "content": "package ovh.plrapps.mapcompose.ui.paths\n\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.dp\nimport junit.framework.TestCase.assertEquals\nimport org.junit.Test\nimport ovh.plrapps.mapcompose.ui.paths.model.PatternItem.*\n\nclass PathComposerTest {\n    private val density = object : Density {\n        override val density: Float = 1f\n        override val fontScale: Float = 1f\n    }\n\n    @Test\n    fun patternTest1() {\n        val pattern = listOf(Dot, Gap(15.dp))\n        val pathEffectData = makeIntervals(pattern, 10f, 1f, density)\n        assertEquals(listOf(1f, 25f), pathEffectData?.intervals?.toList())\n        assertEquals(0f, pathEffectData?.phase)\n    }\n\n    @Test\n    fun patternTest2() {\n        val pattern = listOf(Dot, Gap(15.dp), Dash(3.dp), Gap(15.dp))\n        val pathEffectData = makeIntervals(pattern, 10f, 1f, density)\n        assertEquals(listOf(1f, 25f, 3f, 25f), pathEffectData?.intervals?.toList())\n        assertEquals(0f, pathEffectData?.phase)\n    }\n\n    @Test\n    fun patternTest3() {\n        val pattern = listOf(Dot, Dot, Gap(15.dp), Dash(3.dp))\n        val pathEffectData = makeIntervals(pattern, 10f, 1f, density)\n        assertEquals(listOf(1f, 10f, 1f, 25f, 3f, 10f), pathEffectData?.intervals?.toList())\n        assertEquals(0f, pathEffectData?.phase)\n    }\n\n    @Test\n    fun `pattern with leading gap`() {\n        val pattern = listOf(Gap(25.dp), Dot, Dash(7.dp))\n        val pathEffectData = makeIntervals(pattern, 10f, 1f, density)\n        assertEquals(listOf(1f, 10f, 7f, 35f), pathEffectData?.intervals?.toList())\n        assertEquals(25f, pathEffectData?.phase)\n    }\n\n    @Test\n    fun `pattern with leading and trailing gap`() {\n        val pattern = listOf(Gap(25.dp), Dot, Gap(15.dp))\n        val pathEffectData = makeIntervals(pattern, 10f, 1f, density)\n        assertEquals(listOf(1f, 50f), pathEffectData?.intervals?.toList())\n        assertEquals(25f, pathEffectData?.phase)\n    }\n\n    @Test\n    fun `consecutive gaps should be concatenated`() {\n        val pattern = listOf(Dot, Gap(15.dp), Gap(4.dp), Dash(6.dp), Gap(9.dp))\n        val pathEffectData = makeIntervals(pattern, 10f, 1f, density)\n        assertEquals(listOf(1f, 29f, 6f, 19f), pathEffectData?.intervals?.toList())\n        assertEquals(0f, pathEffectData?.phase)\n    }\n}"
  },
  {
    "path": "mapcompose/src/test/java/ovh/plrapps/mapcompose/utils/GeometryTest.kt",
    "content": "package ovh.plrapps.mapcompose.utils\n\nimport junit.framework.TestCase.assertEquals\nimport org.junit.Test\n\nclass GeometryTest {\n    @Test\n    fun getDistanceTest() {\n        val d = getDistance(1.0, 0.0, 0.0, 0.0, 0.0, 0.0)\n        assertEquals(1.0, d)\n\n        val d2 = getDistance(1.0, 0.0, 0.0, 0.0, 2.0, 0.0)\n        assertEquals(0.0, d2)\n\n        val d3 = getDistance(1.0, 1.0, 0.0, 0.0, 2.0, 0.0)\n        assertEquals(1.0, d3)\n\n        val d4 = getDistance(-1.0, 0.0, 0.0, 0.0, 2.0, 0.0)\n        assertEquals(1.0, d4)\n\n        val d5 = getDistance(3.0, 0.0, 0.0, 0.0, 2.0, 0.0)\n        assertEquals(1.0, d5)\n    }\n}"
  },
  {
    "path": "mapcompose/src/test/java/ovh/plrapps/mapcompose/utils/VisibleAreaUtilsTest.kt",
    "content": "package ovh.plrapps.mapcompose.utils\n\nimport junit.framework.TestCase.assertFalse\nimport junit.framework.TestCase.assertTrue\nimport org.junit.Test\nimport ovh.plrapps.mapcompose.api.VisibleArea\n\nclass VisibleAreaUtilsTest {\n    @Test\n    fun testVisibleAreaIntersect() {\n        val area1 = VisibleArea(\n            _p1x = 0.45080566406223405,\n            _p1y = 0.3736979166669377,\n            _p2x = 0.5572740261482944,\n            _p2y = 0.3736979166669377,\n            _p3x = 0.5572740261482944,\n            _p3y = 0.5914016629881235,\n            _p4x = 0.45080566406223405,\n            _p4y = 0.5914016629881235\n        )\n        val area2 = VisibleArea(\n            _p1x = 0.45167756735567244,\n            _p1y = 0.3721153620806136,\n            _p2x = 0.5485091051388183,\n            _p2y = 0.3721153620806136,\n            _p3x = 0.5485091051388183,\n            _p3y = 0.6169437501755156,\n            _p4x = 0.45167756735567244,\n            _p4y = 0.6169437501755156\n        )\n        assertTrue(area1.intersects(area2))\n\n        val area3 = area1.copy(\n            _p1x = area1._p1x + 0.1,\n            _p2x = area1._p2x + 0.1,\n            _p3x = area1._p3x + 0.1,\n            _p4x = area1._p4x + 0.1\n        )\n        assertTrue(area3.intersects(area1))\n\n        val area4 = area1.copy(\n            _p1x = area1._p1x + 0.1,\n            _p1y = area1._p1y + 0.1,\n            _p2x = area1._p2x + 0.1,\n            _p2y = area1._p2y + 0.1,\n            _p3x = area1._p3x + 0.1,\n            _p3y = area1._p3y + 0.1,\n            _p4x = area1._p4x + 0.1,\n            _p4y = area1._p4y + 0.1\n        )\n        assertTrue(area4.intersects(area1))\n\n        val area5 = area1.copy(\n            _p1x = area1._p1x + 0.1,\n            _p1y = area1._p1y + 0.1,\n            _p2x = area1._p2x - 0.1,\n            _p2y = area1._p2y + 0.1,\n            _p3x = area1._p3x - 0.1,\n            _p3y = area1._p3y - 0.1,\n            _p4x = area1._p4x + 0.1,\n            _p4y = area1._p4y - 0.1\n        )\n        assertTrue(area5.intersects(area1))\n\n        val area6 = area1.copy(\n            _p1x = area1._p1x + 1,\n            _p1y = area1._p1y + 1,\n            _p2x = area1._p2x + 1,\n            _p2y = area1._p2y + 1,\n            _p3x = area1._p3x + 1,\n            _p3y = area1._p3y + 1,\n            _p4x = area1._p4x + 1,\n            _p4y = area1._p4y + 1\n        )\n        assertFalse(area6.intersects(area1))\n    }\n}"
  },
  {
    "path": "settings.gradle",
    "content": "plugins {\n    id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'\n}\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n        maven { url = uri(\"https://dl.bintray.com/kotlin/kotlin-eap\") }\n    }\n}\nrootProject.name = \"MapCompose\"\ninclude ':mapcompose', ':demo', ':testapp'\n"
  },
  {
    "path": "testapp/.gitignore",
    "content": "/build"
  },
  {
    "path": "testapp/build.gradle",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    id 'com.android.application'\n    id 'kotlin-android'\n    id \"org.jetbrains.kotlin.plugin.compose\" version \"$kotlin_version\"\n}\n\nandroid {\n    compileSdk = 36\n\n    defaultConfig {\n        applicationId \"ovh.plrapps.mapcompose.testapp\"\n        minSdk = 23\n        targetSdk = 36\n        versionCode 1\n        versionName \"1.0\"\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        vectorDrawables {\n            useSupportLibrary = true\n        }\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n    buildFeatures {\n        compose = true\n    }\n    packagingOptions {\n        resources {\n            excludes += '/META-INF/{AL2.0,LGPL2.1}'\n        }\n    }\n    namespace = 'ovh.plrapps.mapcompose.testapp'\n}\n\nkotlin {\n    compilerOptions {\n        jvmTarget = JvmTarget.JVM_17\n    }\n}\n\ndependencies {\n\n    implementation 'androidx.core:core-ktx:1.18.0'\n    implementation 'androidx.appcompat:appcompat:1.7.1'\n\n    // Compose - See https://developer.android.com/jetpack/compose/setup#bom-version-mapping\n    implementation platform('androidx.compose:compose-bom:2026.04.01')\n    implementation \"androidx.compose.ui:ui\"\n    implementation \"androidx.compose.material:material\"\n    implementation \"androidx.compose.material3:material3\"\n    implementation \"androidx.compose.ui:ui-tooling-preview\"\n    debugImplementation \"androidx.compose.ui:ui-tooling\"\n\n    implementation 'androidx.navigation:navigation-compose:2.9.8'\n    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.10.0'\n    implementation 'androidx.activity:activity-compose:1.13.0'\n    implementation project(':mapcompose')\n    testImplementation 'junit:junit:4.13.2'\n}"
  },
  {
    "path": "testapp/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "testapp/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.MyApplication\">\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\"\n            android:windowSoftInputMode=\"adjustResize\"\n            android:theme=\"@style/Theme.MyApplication\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>"
  },
  {
    "path": "testapp/src/main/java/ovh/plrapps/mapcompose/testapp/MainActivity.kt",
    "content": "package ovh.plrapps.mapcompose.testapp\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport ovh.plrapps.mapcompose.testapp.core.ui.MapComposeTestApp\nimport ovh.plrapps.mapcompose.testapp.core.ui.theme.MapComposeTheme\n\nclass MainActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n        setContent {\n            MapComposeTheme {\n                MapComposeTestApp()\n            }\n        }\n    }\n}"
  },
  {
    "path": "testapp/src/main/java/ovh/plrapps/mapcompose/testapp/core/ui/MapComposeTestApp.kt",
    "content": "package ovh.plrapps.mapcompose.testapp.core.ui\n\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.rememberNavController\nimport ovh.plrapps.mapcompose.testapp.core.ui.nav.HOME\nimport ovh.plrapps.mapcompose.testapp.core.ui.nav.NavDestinations\nimport ovh.plrapps.mapcompose.testapp.core.ui.theme.MapComposeTheme\nimport ovh.plrapps.mapcompose.testapp.features.clustering.MarkerClusteringUi\nimport ovh.plrapps.mapcompose.testapp.features.home.Home\nimport ovh.plrapps.mapcompose.testapp.features.layerswitch.LayerSwitchTest\n\n@Composable\nfun MapComposeTestApp() {\n    val navController = rememberNavController()\n\n    MapComposeTheme {\n        NavHost(navController, startDestination = HOME) {\n            composable(HOME) {\n                Home(demoListState = rememberLazyListState()) {\n                    navController.navigate(it.name)\n                }\n            }\n            composable(NavDestinations.LAYERS_SWITCH.name) {\n                LayerSwitchTest()\n            }\n            composable(NavDestinations.CLUSTERING.name) {\n                MarkerClusteringUi()\n            }\n        }\n    }\n}"
  },
  {
    "path": "testapp/src/main/java/ovh/plrapps/mapcompose/testapp/core/ui/nav/NavDestinations.kt",
    "content": "package ovh.plrapps.mapcompose.testapp.core.ui.nav\n\nimport androidx.annotation.StringRes\nimport ovh.plrapps.mapcompose.testapp.R\n\nconst val HOME = \"home\"\n\nenum class NavDestinations(@StringRes val title: Int) {\n    LAYERS_SWITCH(R.string.layers_switch_test),\n    CLUSTERING(R.string.clustering_test)\n}"
  },
  {
    "path": "testapp/src/main/java/ovh/plrapps/mapcompose/testapp/core/ui/theme/Color.kt",
    "content": "package ovh.plrapps.mapcompose.testapp.core.ui.theme\n\nimport androidx.compose.ui.graphics.Color\n\nval primaryLight = Color(0xFF415F91)\nval onPrimaryLight = Color(0xFFFFFFFF)\nval primaryContainerLight = Color(0xFFD6E3FF)\nval onPrimaryContainerLight = Color(0xFF001B3E)\nval secondaryLight = Color(0xFF565F71)\nval onSecondaryLight = Color(0xFFFFFFFF)\nval secondaryContainerLight = Color(0xFFDAE2F9)\nval onSecondaryContainerLight = Color(0xFF131C2B)\nval tertiaryLight = Color(0xFF705575)\nval onTertiaryLight = Color(0xFFFFFFFF)\nval tertiaryContainerLight = Color(0xFFFAD8FD)\nval onTertiaryContainerLight = Color(0xFF28132E)\nval errorLight = Color(0xFFBA1A1A)\nval onErrorLight = Color(0xFFFFFFFF)\nval errorContainerLight = Color(0xFFFFDAD6)\nval onErrorContainerLight = Color(0xFF410002)\nval backgroundLight = Color(0xFFF9F9FF)\nval onBackgroundLight = Color(0xFF191C20)\nval surfaceLight = Color(0xFFF9F9FF)\nval onSurfaceLight = Color(0xFF191C20)\nval surfaceVariantLight = Color(0xFFE0E2EC)\nval onSurfaceVariantLight = Color(0xFF44474E)\nval outlineLight = Color(0xFF74777F)\nval outlineVariantLight = Color(0xFFC4C6D0)\nval scrimLight = Color(0xFF000000)\nval inverseSurfaceLight = Color(0xFF2E3036)\nval inverseOnSurfaceLight = Color(0xFFF0F0F7)\nval inversePrimaryLight = Color(0xFFAAC7FF)\nval surfaceDimLight = Color(0xFFD9D9E0)\nval surfaceBrightLight = Color(0xFFF9F9FF)\nval surfaceContainerLowestLight = Color(0xFFFFFFFF)\nval surfaceContainerLowLight = Color(0xFFF3F3FA)\nval surfaceContainerLight = Color(0xFFEDEDF4)\nval surfaceContainerHighLight = Color(0xFFE7E8EE)\nval surfaceContainerHighestLight = Color(0xFFE2E2E9)\n\nval primaryLightMediumContrast = Color(0xFF234373)\nval onPrimaryLightMediumContrast = Color(0xFFFFFFFF)\nval primaryContainerLightMediumContrast = Color(0xFF5875A8)\nval onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)\nval secondaryLightMediumContrast = Color(0xFF3A4354)\nval onSecondaryLightMediumContrast = Color(0xFFFFFFFF)\nval secondaryContainerLightMediumContrast = Color(0xFF6C7588)\nval onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)\nval tertiaryLightMediumContrast = Color(0xFF523A58)\nval onTertiaryLightMediumContrast = Color(0xFFFFFFFF)\nval tertiaryContainerLightMediumContrast = Color(0xFF876B8C)\nval onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)\nval errorLightMediumContrast = Color(0xFF8C0009)\nval onErrorLightMediumContrast = Color(0xFFFFFFFF)\nval errorContainerLightMediumContrast = Color(0xFFDA342E)\nval onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)\nval backgroundLightMediumContrast = Color(0xFFF9F9FF)\nval onBackgroundLightMediumContrast = Color(0xFF191C20)\nval surfaceLightMediumContrast = Color(0xFFF9F9FF)\nval onSurfaceLightMediumContrast = Color(0xFF191C20)\nval surfaceVariantLightMediumContrast = Color(0xFFE0E2EC)\nval onSurfaceVariantLightMediumContrast = Color(0xFF40434A)\nval outlineLightMediumContrast = Color(0xFF5C5F67)\nval outlineVariantLightMediumContrast = Color(0xFF787A83)\nval scrimLightMediumContrast = Color(0xFF000000)\nval inverseSurfaceLightMediumContrast = Color(0xFF2E3036)\nval inverseOnSurfaceLightMediumContrast = Color(0xFFF0F0F7)\nval inversePrimaryLightMediumContrast = Color(0xFFAAC7FF)\nval surfaceDimLightMediumContrast = Color(0xFFD9D9E0)\nval surfaceBrightLightMediumContrast = Color(0xFFF9F9FF)\nval surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)\nval surfaceContainerLowLightMediumContrast = Color(0xFFF3F3FA)\nval surfaceContainerLightMediumContrast = Color(0xFFEDEDF4)\nval surfaceContainerHighLightMediumContrast = Color(0xFFE7E8EE)\nval surfaceContainerHighestLightMediumContrast = Color(0xFFE2E2E9)\n\nval primaryLightHighContrast = Color(0xFF00214A)\nval onPrimaryLightHighContrast = Color(0xFFFFFFFF)\nval primaryContainerLightHighContrast = Color(0xFF234373)\nval onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)\nval secondaryLightHighContrast = Color(0xFF192232)\nval onSecondaryLightHighContrast = Color(0xFFFFFFFF)\nval secondaryContainerLightHighContrast = Color(0xFF3A4354)\nval onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)\nval tertiaryLightHighContrast = Color(0xFF301A35)\nval onTertiaryLightHighContrast = Color(0xFFFFFFFF)\nval tertiaryContainerLightHighContrast = Color(0xFF523A58)\nval onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)\nval errorLightHighContrast = Color(0xFF4E0002)\nval onErrorLightHighContrast = Color(0xFFFFFFFF)\nval errorContainerLightHighContrast = Color(0xFF8C0009)\nval onErrorContainerLightHighContrast = Color(0xFFFFFFFF)\nval backgroundLightHighContrast = Color(0xFFF9F9FF)\nval onBackgroundLightHighContrast = Color(0xFF191C20)\nval surfaceLightHighContrast = Color(0xFFF9F9FF)\nval onSurfaceLightHighContrast = Color(0xFF000000)\nval surfaceVariantLightHighContrast = Color(0xFFE0E2EC)\nval onSurfaceVariantLightHighContrast = Color(0xFF21242B)\nval outlineLightHighContrast = Color(0xFF40434A)\nval outlineVariantLightHighContrast = Color(0xFF40434A)\nval scrimLightHighContrast = Color(0xFF000000)\nval inverseSurfaceLightHighContrast = Color(0xFF2E3036)\nval inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)\nval inversePrimaryLightHighContrast = Color(0xFFE5ECFF)\nval surfaceDimLightHighContrast = Color(0xFFD9D9E0)\nval surfaceBrightLightHighContrast = Color(0xFFF9F9FF)\nval surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)\nval surfaceContainerLowLightHighContrast = Color(0xFFF3F3FA)\nval surfaceContainerLightHighContrast = Color(0xFFEDEDF4)\nval surfaceContainerHighLightHighContrast = Color(0xFFE7E8EE)\nval surfaceContainerHighestLightHighContrast = Color(0xFFE2E2E9)\n\nval primaryDark = Color(0xFFAAC7FF)\nval onPrimaryDark = Color(0xFF0A305F)\nval primaryContainerDark = Color(0xFF284777)\nval onPrimaryContainerDark = Color(0xFFD6E3FF)\nval secondaryDark = Color(0xFFBEC6DC)\nval onSecondaryDark = Color(0xFF283141)\nval secondaryContainerDark = Color(0xFF3E4759)\nval onSecondaryContainerDark = Color(0xFFDAE2F9)\nval tertiaryDark = Color(0xFFDDBCE0)\nval onTertiaryDark = Color(0xFF3F2844)\nval tertiaryContainerDark = Color(0xFF573E5C)\nval onTertiaryContainerDark = Color(0xFFFAD8FD)\nval errorDark = Color(0xFFFFB4AB)\nval onErrorDark = Color(0xFF690005)\nval errorContainerDark = Color(0xFF93000A)\nval onErrorContainerDark = Color(0xFFFFDAD6)\nval backgroundDark = Color(0xFF111318)\nval onBackgroundDark = Color(0xFFE2E2E9)\nval surfaceDark = Color(0xFF111318)\nval onSurfaceDark = Color(0xFFE2E2E9)\nval surfaceVariantDark = Color(0xFF44474E)\nval onSurfaceVariantDark = Color(0xFFC4C6D0)\nval outlineDark = Color(0xFF8E9099)\nval outlineVariantDark = Color(0xFF44474E)\nval scrimDark = Color(0xFF000000)\nval inverseSurfaceDark = Color(0xFFE2E2E9)\nval inverseOnSurfaceDark = Color(0xFF2E3036)\nval inversePrimaryDark = Color(0xFF415F91)\nval surfaceDimDark = Color(0xFF111318)\nval surfaceBrightDark = Color(0xFF37393E)\nval surfaceContainerLowestDark = Color(0xFF0C0E13)\nval surfaceContainerLowDark = Color(0xFF191C20)\nval surfaceContainerDark = Color(0xFF1D2024)\nval surfaceContainerHighDark = Color(0xFF282A2F)\nval surfaceContainerHighestDark = Color(0xFF33353A)\n\nval primaryDarkMediumContrast = Color(0xFFB1CBFF)\nval onPrimaryDarkMediumContrast = Color(0xFF001634)\nval primaryContainerDarkMediumContrast = Color(0xFF7491C7)\nval onPrimaryContainerDarkMediumContrast = Color(0xFF000000)\nval secondaryDarkMediumContrast = Color(0xFFC2CBE0)\nval onSecondaryDarkMediumContrast = Color(0xFF0D1626)\nval secondaryContainerDarkMediumContrast = Color(0xFF8891A5)\nval onSecondaryContainerDarkMediumContrast = Color(0xFF000000)\nval tertiaryDarkMediumContrast = Color(0xFFE1C0E5)\nval onTertiaryDarkMediumContrast = Color(0xFF230E29)\nval tertiaryContainerDarkMediumContrast = Color(0xFFA487A9)\nval onTertiaryContainerDarkMediumContrast = Color(0xFF000000)\nval errorDarkMediumContrast = Color(0xFFFFBAB1)\nval onErrorDarkMediumContrast = Color(0xFF370001)\nval errorContainerDarkMediumContrast = Color(0xFFFF5449)\nval onErrorContainerDarkMediumContrast = Color(0xFF000000)\nval backgroundDarkMediumContrast = Color(0xFF111318)\nval onBackgroundDarkMediumContrast = Color(0xFFE2E2E9)\nval surfaceDarkMediumContrast = Color(0xFF111318)\nval onSurfaceDarkMediumContrast = Color(0xFFFBFAFF)\nval surfaceVariantDarkMediumContrast = Color(0xFF44474E)\nval onSurfaceVariantDarkMediumContrast = Color(0xFFC8CAD4)\nval outlineDarkMediumContrast = Color(0xFFA0A3AC)\nval outlineVariantDarkMediumContrast = Color(0xFF80838C)\nval scrimDarkMediumContrast = Color(0xFF000000)\nval inverseSurfaceDarkMediumContrast = Color(0xFFE2E2E9)\nval inverseOnSurfaceDarkMediumContrast = Color(0xFF282A2F)\nval inversePrimaryDarkMediumContrast = Color(0xFF294878)\nval surfaceDimDarkMediumContrast = Color(0xFF111318)\nval surfaceBrightDarkMediumContrast = Color(0xFF37393E)\nval surfaceContainerLowestDarkMediumContrast = Color(0xFF0C0E13)\nval surfaceContainerLowDarkMediumContrast = Color(0xFF191C20)\nval surfaceContainerDarkMediumContrast = Color(0xFF1D2024)\nval surfaceContainerHighDarkMediumContrast = Color(0xFF282A2F)\nval surfaceContainerHighestDarkMediumContrast = Color(0xFF33353A)\n\nval primaryDarkHighContrast = Color(0xFFFBFAFF)\nval onPrimaryDarkHighContrast = Color(0xFF000000)\nval primaryContainerDarkHighContrast = Color(0xFFB1CBFF)\nval onPrimaryContainerDarkHighContrast = Color(0xFF000000)\nval secondaryDarkHighContrast = Color(0xFFFBFAFF)\nval onSecondaryDarkHighContrast = Color(0xFF000000)\nval secondaryContainerDarkHighContrast = Color(0xFFC2CBE0)\nval onSecondaryContainerDarkHighContrast = Color(0xFF000000)\nval tertiaryDarkHighContrast = Color(0xFFFFF9FA)\nval onTertiaryDarkHighContrast = Color(0xFF000000)\nval tertiaryContainerDarkHighContrast = Color(0xFFE1C0E5)\nval onTertiaryContainerDarkHighContrast = Color(0xFF000000)\nval errorDarkHighContrast = Color(0xFFFFF9F9)\nval onErrorDarkHighContrast = Color(0xFF000000)\nval errorContainerDarkHighContrast = Color(0xFFFFBAB1)\nval onErrorContainerDarkHighContrast = Color(0xFF000000)\nval backgroundDarkHighContrast = Color(0xFF111318)\nval onBackgroundDarkHighContrast = Color(0xFFE2E2E9)\nval surfaceDarkHighContrast = Color(0xFF111318)\nval onSurfaceDarkHighContrast = Color(0xFFFFFFFF)\nval surfaceVariantDarkHighContrast = Color(0xFF44474E)\nval onSurfaceVariantDarkHighContrast = Color(0xFFFBFAFF)\nval outlineDarkHighContrast = Color(0xFFC8CAD4)\nval outlineVariantDarkHighContrast = Color(0xFFC8CAD4)\nval scrimDarkHighContrast = Color(0xFF000000)\nval inverseSurfaceDarkHighContrast = Color(0xFFE2E2E9)\nval inverseOnSurfaceDarkHighContrast = Color(0xFF000000)\nval inversePrimaryDarkHighContrast = Color(0xFF002959)\nval surfaceDimDarkHighContrast = Color(0xFF111318)\nval surfaceBrightDarkHighContrast = Color(0xFF37393E)\nval surfaceContainerLowestDarkHighContrast = Color(0xFF0C0E13)\nval surfaceContainerLowDarkHighContrast = Color(0xFF191C20)\nval surfaceContainerDarkHighContrast = Color(0xFF1D2024)\nval surfaceContainerHighDarkHighContrast = Color(0xFF282A2F)\nval surfaceContainerHighestDarkHighContrast = Color(0xFF33353A)\n\n\n\n\n\n\n\n"
  },
  {
    "path": "testapp/src/main/java/ovh/plrapps/mapcompose/testapp/core/ui/theme/Theme.kt",
    "content": "package ovh.plrapps.mapcompose.testapp.core.ui.theme\n\nimport android.os.Build\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\n\nprivate val lightScheme = lightColorScheme(\n    primary = primaryLight,\n    onPrimary = onPrimaryLight,\n    primaryContainer = primaryContainerLight,\n    onPrimaryContainer = onPrimaryContainerLight,\n    secondary = secondaryLight,\n    onSecondary = onSecondaryLight,\n    secondaryContainer = secondaryContainerLight,\n    onSecondaryContainer = onSecondaryContainerLight,\n    tertiary = tertiaryLight,\n    onTertiary = onTertiaryLight,\n    tertiaryContainer = tertiaryContainerLight,\n    onTertiaryContainer = onTertiaryContainerLight,\n    error = errorLight,\n    onError = onErrorLight,\n    errorContainer = errorContainerLight,\n    onErrorContainer = onErrorContainerLight,\n    background = backgroundLight,\n    onBackground = onBackgroundLight,\n    surface = surfaceLight,\n    onSurface = onSurfaceLight,\n    surfaceVariant = surfaceVariantLight,\n    onSurfaceVariant = onSurfaceVariantLight,\n    outline = outlineLight,\n    outlineVariant = outlineVariantLight,\n    scrim = scrimLight,\n    inverseSurface = inverseSurfaceLight,\n    inverseOnSurface = inverseOnSurfaceLight,\n    inversePrimary = inversePrimaryLight,\n    surfaceDim = surfaceDimLight,\n    surfaceBright = surfaceBrightLight,\n    surfaceContainerLowest = surfaceContainerLowestLight,\n    surfaceContainerLow = surfaceContainerLowLight,\n    surfaceContainer = surfaceContainerLight,\n    surfaceContainerHigh = surfaceContainerHighLight,\n    surfaceContainerHighest = surfaceContainerHighestLight,\n)\n\nprivate val darkScheme = darkColorScheme(\n    primary = primaryDark,\n    onPrimary = onPrimaryDark,\n    primaryContainer = primaryContainerDark,\n    onPrimaryContainer = onPrimaryContainerDark,\n    secondary = secondaryDark,\n    onSecondary = onSecondaryDark,\n    secondaryContainer = secondaryContainerDark,\n    onSecondaryContainer = onSecondaryContainerDark,\n    tertiary = tertiaryDark,\n    onTertiary = onTertiaryDark,\n    tertiaryContainer = tertiaryContainerDark,\n    onTertiaryContainer = onTertiaryContainerDark,\n    error = errorDark,\n    onError = onErrorDark,\n    errorContainer = errorContainerDark,\n    onErrorContainer = onErrorContainerDark,\n    background = backgroundDark,\n    onBackground = onBackgroundDark,\n    surface = surfaceDark,\n    onSurface = onSurfaceDark,\n    surfaceVariant = surfaceVariantDark,\n    onSurfaceVariant = onSurfaceVariantDark,\n    outline = outlineDark,\n    outlineVariant = outlineVariantDark,\n    scrim = scrimDark,\n    inverseSurface = inverseSurfaceDark,\n    inverseOnSurface = inverseOnSurfaceDark,\n    inversePrimary = inversePrimaryDark,\n    surfaceDim = surfaceDimDark,\n    surfaceBright = surfaceBrightDark,\n    surfaceContainerLowest = surfaceContainerLowestDark,\n    surfaceContainerLow = surfaceContainerLowDark,\n    surfaceContainer = surfaceContainerDark,\n    surfaceContainerHigh = surfaceContainerHighDark,\n    surfaceContainerHighest = surfaceContainerHighestDark,\n)\n\nprivate val mediumContrastLightColorScheme = lightColorScheme(\n    primary = primaryLightMediumContrast,\n    onPrimary = onPrimaryLightMediumContrast,\n    primaryContainer = primaryContainerLightMediumContrast,\n    onPrimaryContainer = onPrimaryContainerLightMediumContrast,\n    secondary = secondaryLightMediumContrast,\n    onSecondary = onSecondaryLightMediumContrast,\n    secondaryContainer = secondaryContainerLightMediumContrast,\n    onSecondaryContainer = onSecondaryContainerLightMediumContrast,\n    tertiary = tertiaryLightMediumContrast,\n    onTertiary = onTertiaryLightMediumContrast,\n    tertiaryContainer = tertiaryContainerLightMediumContrast,\n    onTertiaryContainer = onTertiaryContainerLightMediumContrast,\n    error = errorLightMediumContrast,\n    onError = onErrorLightMediumContrast,\n    errorContainer = errorContainerLightMediumContrast,\n    onErrorContainer = onErrorContainerLightMediumContrast,\n    background = backgroundLightMediumContrast,\n    onBackground = onBackgroundLightMediumContrast,\n    surface = surfaceLightMediumContrast,\n    onSurface = onSurfaceLightMediumContrast,\n    surfaceVariant = surfaceVariantLightMediumContrast,\n    onSurfaceVariant = onSurfaceVariantLightMediumContrast,\n    outline = outlineLightMediumContrast,\n    outlineVariant = outlineVariantLightMediumContrast,\n    scrim = scrimLightMediumContrast,\n    inverseSurface = inverseSurfaceLightMediumContrast,\n    inverseOnSurface = inverseOnSurfaceLightMediumContrast,\n    inversePrimary = inversePrimaryLightMediumContrast,\n    surfaceDim = surfaceDimLightMediumContrast,\n    surfaceBright = surfaceBrightLightMediumContrast,\n    surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,\n    surfaceContainerLow = surfaceContainerLowLightMediumContrast,\n    surfaceContainer = surfaceContainerLightMediumContrast,\n    surfaceContainerHigh = surfaceContainerHighLightMediumContrast,\n    surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,\n)\n\nprivate val highContrastLightColorScheme = lightColorScheme(\n    primary = primaryLightHighContrast,\n    onPrimary = onPrimaryLightHighContrast,\n    primaryContainer = primaryContainerLightHighContrast,\n    onPrimaryContainer = onPrimaryContainerLightHighContrast,\n    secondary = secondaryLightHighContrast,\n    onSecondary = onSecondaryLightHighContrast,\n    secondaryContainer = secondaryContainerLightHighContrast,\n    onSecondaryContainer = onSecondaryContainerLightHighContrast,\n    tertiary = tertiaryLightHighContrast,\n    onTertiary = onTertiaryLightHighContrast,\n    tertiaryContainer = tertiaryContainerLightHighContrast,\n    onTertiaryContainer = onTertiaryContainerLightHighContrast,\n    error = errorLightHighContrast,\n    onError = onErrorLightHighContrast,\n    errorContainer = errorContainerLightHighContrast,\n    onErrorContainer = onErrorContainerLightHighContrast,\n    background = backgroundLightHighContrast,\n    onBackground = onBackgroundLightHighContrast,\n    surface = surfaceLightHighContrast,\n    onSurface = onSurfaceLightHighContrast,\n    surfaceVariant = surfaceVariantLightHighContrast,\n    onSurfaceVariant = onSurfaceVariantLightHighContrast,\n    outline = outlineLightHighContrast,\n    outlineVariant = outlineVariantLightHighContrast,\n    scrim = scrimLightHighContrast,\n    inverseSurface = inverseSurfaceLightHighContrast,\n    inverseOnSurface = inverseOnSurfaceLightHighContrast,\n    inversePrimary = inversePrimaryLightHighContrast,\n    surfaceDim = surfaceDimLightHighContrast,\n    surfaceBright = surfaceBrightLightHighContrast,\n    surfaceContainerLowest = surfaceContainerLowestLightHighContrast,\n    surfaceContainerLow = surfaceContainerLowLightHighContrast,\n    surfaceContainer = surfaceContainerLightHighContrast,\n    surfaceContainerHigh = surfaceContainerHighLightHighContrast,\n    surfaceContainerHighest = surfaceContainerHighestLightHighContrast,\n)\n\nprivate val mediumContrastDarkColorScheme = darkColorScheme(\n    primary = primaryDarkMediumContrast,\n    onPrimary = onPrimaryDarkMediumContrast,\n    primaryContainer = primaryContainerDarkMediumContrast,\n    onPrimaryContainer = onPrimaryContainerDarkMediumContrast,\n    secondary = secondaryDarkMediumContrast,\n    onSecondary = onSecondaryDarkMediumContrast,\n    secondaryContainer = secondaryContainerDarkMediumContrast,\n    onSecondaryContainer = onSecondaryContainerDarkMediumContrast,\n    tertiary = tertiaryDarkMediumContrast,\n    onTertiary = onTertiaryDarkMediumContrast,\n    tertiaryContainer = tertiaryContainerDarkMediumContrast,\n    onTertiaryContainer = onTertiaryContainerDarkMediumContrast,\n    error = errorDarkMediumContrast,\n    onError = onErrorDarkMediumContrast,\n    errorContainer = errorContainerDarkMediumContrast,\n    onErrorContainer = onErrorContainerDarkMediumContrast,\n    background = backgroundDarkMediumContrast,\n    onBackground = onBackgroundDarkMediumContrast,\n    surface = surfaceDarkMediumContrast,\n    onSurface = onSurfaceDarkMediumContrast,\n    surfaceVariant = surfaceVariantDarkMediumContrast,\n    onSurfaceVariant = onSurfaceVariantDarkMediumContrast,\n    outline = outlineDarkMediumContrast,\n    outlineVariant = outlineVariantDarkMediumContrast,\n    scrim = scrimDarkMediumContrast,\n    inverseSurface = inverseSurfaceDarkMediumContrast,\n    inverseOnSurface = inverseOnSurfaceDarkMediumContrast,\n    inversePrimary = inversePrimaryDarkMediumContrast,\n    surfaceDim = surfaceDimDarkMediumContrast,\n    surfaceBright = surfaceBrightDarkMediumContrast,\n    surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,\n    surfaceContainerLow = surfaceContainerLowDarkMediumContrast,\n    surfaceContainer = surfaceContainerDarkMediumContrast,\n    surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,\n    surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,\n)\n\nprivate val highContrastDarkColorScheme = darkColorScheme(\n    primary = primaryDarkHighContrast,\n    onPrimary = onPrimaryDarkHighContrast,\n    primaryContainer = primaryContainerDarkHighContrast,\n    onPrimaryContainer = onPrimaryContainerDarkHighContrast,\n    secondary = secondaryDarkHighContrast,\n    onSecondary = onSecondaryDarkHighContrast,\n    secondaryContainer = secondaryContainerDarkHighContrast,\n    onSecondaryContainer = onSecondaryContainerDarkHighContrast,\n    tertiary = tertiaryDarkHighContrast,\n    onTertiary = onTertiaryDarkHighContrast,\n    tertiaryContainer = tertiaryContainerDarkHighContrast,\n    onTertiaryContainer = onTertiaryContainerDarkHighContrast,\n    error = errorDarkHighContrast,\n    onError = onErrorDarkHighContrast,\n    errorContainer = errorContainerDarkHighContrast,\n    onErrorContainer = onErrorContainerDarkHighContrast,\n    background = backgroundDarkHighContrast,\n    onBackground = onBackgroundDarkHighContrast,\n    surface = surfaceDarkHighContrast,\n    onSurface = onSurfaceDarkHighContrast,\n    surfaceVariant = surfaceVariantDarkHighContrast,\n    onSurfaceVariant = onSurfaceVariantDarkHighContrast,\n    outline = outlineDarkHighContrast,\n    outlineVariant = outlineVariantDarkHighContrast,\n    scrim = scrimDarkHighContrast,\n    inverseSurface = inverseSurfaceDarkHighContrast,\n    inverseOnSurface = inverseOnSurfaceDarkHighContrast,\n    inversePrimary = inversePrimaryDarkHighContrast,\n    surfaceDim = surfaceDimDarkHighContrast,\n    surfaceBright = surfaceBrightDarkHighContrast,\n    surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,\n    surfaceContainerLow = surfaceContainerLowDarkHighContrast,\n    surfaceContainer = surfaceContainerDarkHighContrast,\n    surfaceContainerHigh = surfaceContainerHighDarkHighContrast,\n    surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,\n)\n\n@Immutable\ndata class ColorFamily(\n    val color: Color,\n    val onColor: Color,\n    val colorContainer: Color,\n    val onColorContainer: Color\n)\n\nval unspecified_scheme = ColorFamily(\n    Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified\n)\n\n@Composable\nfun MapComposeTheme(\n    darkTheme: Boolean = isSystemInDarkTheme(),\n    // Dynamic color is available on Android 12+\n    dynamicColor: Boolean = true,\n    content: @Composable() () -> Unit\n) {\n  val colorScheme = when {\n      dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {\n          val context = LocalContext.current\n          if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)\n      }\n      \n      darkTheme -> darkScheme\n      else -> lightScheme\n  }\n\n  MaterialTheme(\n    colorScheme = colorScheme,\n    typography = AppTypography,\n    content = content\n  )\n}\n\n"
  },
  {
    "path": "testapp/src/main/java/ovh/plrapps/mapcompose/testapp/core/ui/theme/Type.kt",
    "content": "package ovh.plrapps.mapcompose.testapp.core.ui.theme\n\nimport androidx.compose.material3.Typography\n\nval AppTypography = Typography()\n"
  },
  {
    "path": "testapp/src/main/java/ovh/plrapps/mapcompose/testapp/features/clustering/MarkerClusteringUi.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.testapp.features.clustering\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.testapp.core.ui.nav.NavDestinations\nimport ovh.plrapps.mapcompose.ui.MapUI\n\n@Composable\nfun MarkerClusteringUi(\n    viewModel: MarkersClusteringViewModel = viewModel()\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(stringResource(NavDestinations.CLUSTERING.title)) },\n            )\n        }\n    ) { padding ->\n        MapUI(Modifier.padding(padding), state = viewModel.state)\n    }\n}"
  },
  {
    "path": "testapp/src/main/java/ovh/plrapps/mapcompose/testapp/features/clustering/MarkersClusteringViewModel.kt",
    "content": "package ovh.plrapps.mapcompose.testapp.features.clustering\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.AndroidViewModel\nimport ovh.plrapps.mapcompose.api.addClusterer\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.addMarker\nimport ovh.plrapps.mapcompose.api.enableRotation\nimport ovh.plrapps.mapcompose.api.onMarkerClick\nimport ovh.plrapps.mapcompose.api.shouldLoopScale\nimport ovh.plrapps.mapcompose.core.TileStreamProvider\nimport ovh.plrapps.mapcompose.testapp.R\nimport ovh.plrapps.mapcompose.testapp.utils.randomDouble\nimport ovh.plrapps.mapcompose.ui.state.MapState\nimport ovh.plrapps.mapcompose.ui.state.markers.model.RenderingStrategy\nimport kotlin.random.Random.Default.nextDouble\n\n/**\n * In this sample, an experimental clustering algorithm is used to display 400 markers.\n * The lazy loading technique (removing a marker/cluster when it's not visible) is also used for\n * performance reasons.\n */\nclass MarkersClusteringViewModel(application: Application) : AndroidViewModel(application) {\n    private val tileStreamProvider = makeTileStreamProvider(application.applicationContext)\n\n    private fun makeTileStreamProvider(appContext: Context): TileStreamProvider {\n        return TileStreamProvider { row, col, level ->\n            runCatching {\n                appContext.assets?.open(\"tiles/mont_blanc/$level/$row/$col.jpg\")\n            }.getOrNull()\n        }\n    }\n\n    val state: MapState = MapState(4, 4096, 4096) {\n        scale(0.81)\n        maxScale(8.0)\n    }.apply {\n        addLayer(tileStreamProvider)\n        enableRotation()\n        shouldLoopScale = true\n        onMarkerClick { id, x, y ->\n            println(\"on marker click $id $x $y\")\n        }\n    }\n\n    init {\n        state.addClusterer(\"default\") { n ->\n            {\n                /* Here we can customize the cluster style */\n                Box(\n                    modifier = Modifier\n                        .background(\n                            Color(0x992196F3),\n                            shape = CircleShape\n                        )\n                        .size(50.dp),\n                    contentAlignment = Alignment.Center\n                ) {\n                    Text(text = n.size.toString(), color = Color.White)\n                }\n            }\n        }\n\n        repeat(40) { i ->\n            val cx = nextDouble()\n            val cy = nextDouble()\n            repeat(10) { j ->\n                val x = randomDouble(cx, 0.03).coerceAtLeast(0.0)\n                val y = randomDouble(cy, 0.03).coerceAtLeast(0.0)\n\n                /* Notice how we set the cluster which we previously added */\n                state.addMarker(\n                    \"marker-$i-$j\", x, y,\n                    renderingStrategy = RenderingStrategy.Clustering(\"default\")\n                ) {\n                    Icon(\n                        painter = painterResource(id = R.drawable.map_marker),\n                        contentDescription = null,\n                        modifier = Modifier.size(50.dp),\n                        tint = Color(0xEE2196F3)\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "testapp/src/main/java/ovh/plrapps/mapcompose/testapp/features/home/Home.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.testapp.features.home\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport ovh.plrapps.mapcompose.testapp.R\nimport ovh.plrapps.mapcompose.testapp.core.ui.nav.NavDestinations\n\n@Composable\nfun Home(demoListState: LazyListState, onDemoSelected: (dest: NavDestinations) -> Unit) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(stringResource(R.string.app_name)) },\n            )\n        }\n    ) { padding ->\n        LazyColumn(\n            Modifier.padding(padding),\n            state = demoListState\n        ) {\n            NavDestinations.values().map { dest ->\n                item {\n                    Text(\n                        text = stringResource(dest.title),\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .clickable { onDemoSelected.invoke(dest) }\n                            .padding(16.dp),\n                        textAlign = TextAlign.Center\n                    )\n                    HorizontalDivider(thickness = 1.dp)\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "testapp/src/main/java/ovh/plrapps/mapcompose/testapp/features/layerswitch/LayerSwitchTest.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage ovh.plrapps.mapcompose.testapp.features.layerswitch\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport ovh.plrapps.mapcompose.testapp.core.ui.nav.NavDestinations\nimport ovh.plrapps.mapcompose.ui.MapUI\n\n@Composable\nfun LayerSwitchTest(viewModel: LayerSwitchViewModel = viewModel()) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(stringResource(NavDestinations.LAYERS_SWITCH.title)) },\n            )\n        }\n    ) { padding ->\n        MapUI(Modifier.padding(padding), state = viewModel.state)\n    }\n}"
  },
  {
    "path": "testapp/src/main/java/ovh/plrapps/mapcompose/testapp/features/layerswitch/LayerSwitchViewModel.kt",
    "content": "package ovh.plrapps.mapcompose.testapp.features.layerswitch\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport ovh.plrapps.mapcompose.api.addLayer\nimport ovh.plrapps.mapcompose.api.enableRotation\nimport ovh.plrapps.mapcompose.api.replaceLayer\nimport ovh.plrapps.mapcompose.api.scrollTo\nimport ovh.plrapps.mapcompose.api.shouldLoopScale\nimport ovh.plrapps.mapcompose.core.TileStreamProvider\nimport ovh.plrapps.mapcompose.ui.state.MapState\n\nclass LayerSwitchViewModel(application: Application) : AndroidViewModel(application) {\n    private val appContext: Context by lazy {\n        getApplication<Application>().applicationContext\n    }\n\n    private var type = 0\n    private val tileStreamProvider = makeTileStreamProvider(appContext, type)\n    private var currentLayerId: String? = null\n\n    val state: MapState = MapState(4, 4096, 4096, workerCount = 64).apply {\n        shouldLoopScale = true\n        enableRotation()\n        viewModelScope.launch {\n            scrollTo(0.5, 0.5, 1.0)\n        }\n        currentLayerId = addLayer(tileStreamProvider)\n    }\n\n    init {\n        viewModelScope.launch {\n            while (true) {\n                delay(2000)\n                changeMapType()\n                delay(200)\n                changeMapType()\n            }\n        }\n    }\n\n    private fun changeMapType() {\n        type = ((0..2) - type).random()\n        val tileStreamProvider = makeTileStreamProvider(appContext, type)\n        currentLayerId?.also { id ->\n            currentLayerId = state.replaceLayer(id, tileStreamProvider)\n        }\n    }\n\n    private fun makeTileStreamProvider(appContext: Context, type: Int): TileStreamProvider {\n        /* Pay attention to how type is captured and immutable in the context of the TileStreamProvider */\n        return TileStreamProvider { row, col, _ ->\n            runCatching {\n                Thread.sleep((100L..200L).random())\n                appContext.assets?.open(\"tiles/test/tile_${type}_${col}_$row.png\")\n            }.getOrNull()\n        }\n    }\n}"
  },
  {
    "path": "testapp/src/main/java/ovh/plrapps/mapcompose/testapp/utils/Random.kt",
    "content": "package ovh.plrapps.mapcompose.testapp.utils\n\nimport kotlin.random.Random.Default.nextDouble\n\nfun randomDouble(center: Double, radius: Double) : Double {\n    return nextDouble(from = center - radius, until = center + radius)\n}\n"
  },
  {
    "path": "testapp/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#3DDC84\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "testapp/src/main/res/drawable/map_marker.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:height=\"24dp\"\n        android:width=\"24dp\"\n        android:viewportWidth=\"24\"\n        android:viewportHeight=\"24\">\n    <path android:fillColor=\"#2196f3\" android:pathData=\"M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z\" />\n</vector>"
  },
  {
    "path": "testapp/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path android:pathData=\"M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"85.84757\"\n                android:endY=\"92.4963\"\n                android:startX=\"42.9492\"\n                android:startY=\"49.59793\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\" />\n</vector>"
  },
  {
    "path": "testapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "testapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "testapp/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"purple_200\">#FFBB86FC</color>\n    <color name=\"purple_500\">#FF6200EE</color>\n    <color name=\"purple_700\">#FF3700B3</color>\n    <color name=\"teal_200\">#FF03DAC5</color>\n    <color name=\"teal_700\">#FF018786</color>\n    <color name=\"black\">#FF000000</color>\n    <color name=\"white\">#FFFFFFFF</color>\n</resources>"
  },
  {
    "path": "testapp/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">MapCompose Test App</string>\n    <string name=\"layers_switch_test\">Layers switch stress test</string>\n    <string name=\"clustering_test\">Clustering stress test</string>\n</resources>"
  },
  {
    "path": "testapp/src/main/res/values/themes.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"Theme.MyApplication\" parent=\"android:Theme.Material.Light.NoActionBar\" />\n</resources>"
  },
  {
    "path": "testapp/src/test/java/ovh/plrapps/mapcompose/testapp/ExampleUnitTest.kt",
    "content": "package ovh.plrapps.mapcompose.testapp\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}"
  }
]