diff --git a/app/build.gradle b/app/build.gradle index f18ecfb..fa2ec3e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,146 +1,152 @@ apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-android' apply plugin: 'com.android.application' android { compileSdk 34 namespace "it.reyboz.bustorino" defaultConfig { applicationId "it.reyboz.bustorino" minSdkVersion 21 targetSdkVersion 34 buildToolsVersion = '34.0.0' versionCode 62 versionName "2.3.1" vectorDrawables.useSupportLibrary = true multiDexEnabled true javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/assets/schemas/".toString()] } } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } testOptions { unitTests.returnDefaultValues = true } sourceSets { androidTest.assets.srcDirs += files("$projectDir/assets/schemas/".toString()) } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-dev" } gitpull{ applicationIdSuffix ".gitdev" versionNameSuffix "-gitdev" } } repositories { mavenCentral() mavenLocal() } dependencies { //new libraries } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlin { jvmToolchain 17 } lint { abortOnError false } androidResources { generateLocaleConfig true } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation 'androidx.legacy:legacy-support-v4:1.0.0' // Guava implementation for DBUpdateWorker implementation 'com.google.guava:guava:29.0-android' implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.activity:activity:$activity_version" implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation "androidx.preference:preference:$preference_version" implementation "androidx.work:work-runtime:$work_version" implementation "androidx.work:work-runtime-ktx:$work_version" implementation "com.google.android.material:material:1.11.0" implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" implementation 'org.jsoup:jsoup:1.15.3' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' implementation 'com.android.volley:volley:1.2.1' - implementation 'org.osmdroid:osmdroid-android:6.1.10' + implementation 'org.osmdroid:osmdroid-android:6.1.18' + //maplibre + implementation 'org.maplibre.gl:android-sdk:11.8.6' + implementation 'org.maplibre.gl:android-sdk-turf:6.0.1' + + implementation 'org.maplibre.gl:android-plugin-annotation-v9:3.0.2' + // remember to enable maven repo jitpack.io when wanting to use osmbonuspack //implementation 'com.github.MKergall:osmbonuspack:6.9.0' // ACRA implementation "ch.acra:acra-mail:$acra_version" implementation "ch.acra:acra-dialog:$acra_version" // google transit realtime implementation 'com.google.protobuf:protobuf-java:3.19.6' // mqtt library implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' implementation 'com.github.fabmazz:paho.mqtt.android:v1.0.0' // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" // LiveData implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // Lifecycles only (without ViewModel or LiveData) implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" // Legacy implementation 'androidx.legacy:legacy-support-v4:1.0.0' // Room components implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" //multidex - we need this to build the app implementation "androidx.multidex:multidex:$multidex_version" implementation 'de.siegmar:fastcsv:2.2.2' testImplementation 'junit:junit:4.12' implementation 'junit:junit:4.12' implementation "androidx.test.ext:junit:1.1.5" implementation "androidx.test:core:$androidXTestVersion" implementation "androidx.test:runner:$androidXTestVersion" implementation "androidx.room:room-testing:$room_version" androidTestImplementation "androidx.test.ext:junit:1.1.5" androidTestImplementation "androidx.test:core:$androidXTestVersion" androidTestImplementation "androidx.test:runner:$androidXTestVersion" androidTestImplementation "androidx.test:rules:$androidXTestVersion" androidTestImplementation "androidx.room:room-testing:$room_version" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b8d192a..ee45ded 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,144 +1,146 @@ - - + + + + \ No newline at end of file diff --git a/app/src/main/assets/map_style_good.json b/app/src/main/assets/map_style_good.json new file mode 100644 index 0000000..f2b5836 --- /dev/null +++ b/app/src/main/assets/map_style_good.json @@ -0,0 +1,3147 @@ +{ + "version": 8, + "metadata": {"maputnik:renderer": "mlgljs"}, + "sources": { + "ne2_shaded": { + "maxzoom": 6, + "tileSize": 256, + "tiles": [ + "https://tiles.openfreemap.org/natural_earth/ne2sr/{z}/{x}/{y}.png" + ], + "type": "raster" + }, + "openmaptiles": { + "type": "vector", + "url": "https://tiles.openfreemap.org/planet" + } + }, + "sprite": "https://tiles.openfreemap.org/sprites/ofm_f384/ofm", + "glyphs": "https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": {"background-color": "#f8f4f0"} + }, + { + "id": "landcover-glacier", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", ["get", "subclass"], "glacier"], + "paint": { + "fill-color": "#fff", + "fill-opacity": ["interpolate", ["linear"], ["zoom"], 0, 0.9, 10, 0.3] + } + }, + { + "id": "landuse-residential", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": [ + "match", + ["get", "class"], + ["neighbourhood", "residential"], + true, + false + ], + "paint": { + "fill-color": [ + "interpolate", + ["linear"], + ["zoom"], + 12, + "hsla(30,19%,90%,0.4)", + 16, + "hsla(30,19%,90%,0.2)" + ] + } + }, + { + "id": "landuse-suburb", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "maxzoom": 10, + "filter": ["==", ["get", "class"], "suburb"], + "paint": { + "fill-color": [ + "interpolate", + ["linear"], + ["zoom"], + 8, + "hsla(30,19%,90%,0.4)", + 10, + "hsla(30,19%,90%,0.0)" + ] + } + }, + { + "id": "landuse-commercial", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": [ + "all", + ["match", ["geometry-type"], ["MultiPolygon", "Polygon"], true, false], + ["==", ["get", "class"], "commercial"] + ], + "paint": {"fill-color": "hsla(0,60%,87%,0.23)"} + }, + { + "id": "landuse-industrial", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": [ + "all", + ["match", ["geometry-type"], ["MultiPolygon", "Polygon"], true, false], + [ + "match", + ["get", "class"], + ["dam", "garages", "industrial"], + true, + false + ] + ], + "paint": {"fill-color": "hsla(49,100%,88%,0.34)"} + }, + { + "id": "landuse-cemetery", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": ["==", ["get", "class"], "cemetery"], + "paint": {"fill-color": "#e0e4dd"} + }, + { + "id": "landuse-hospital", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": ["==", ["get", "class"], "hospital"], + "paint": {"fill-color": "#fde"} + }, + { + "id": "landuse-school", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": ["==", ["get", "class"], "school"], + "paint": {"fill-color": "#f0e8f8"} + }, + { + "id": "landuse-railway", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": ["==", ["get", "class"], "railway"], + "paint": {"fill-color": "hsla(30,19%,90%,0.4)"} + }, + { + "id": "park", + "type": "fill", + "source": "openmaptiles", + "source-layer": "park", + "filter": [ + "match", + ["geometry-type"], + ["MultiPolygon", "Polygon"], + true, + false + ], + "paint": { + "fill-color": "#d8e8c8", + "fill-opacity": [ + "interpolate", + ["exponential", 1.8], + ["zoom"], + 9, + 0.5, + 12, + 0.2 + ] + } + }, + { + "id": "landcover-wood", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", ["get", "class"], "wood"], + "paint": { + "fill-antialias": ["step", ["zoom"], false, 9, true], + "fill-color": "#6a4", + "fill-opacity": 0.1, + "fill-outline-color": "hsla(0,0%,0%,0.03)" + } + }, + { + "id": "landcover-grass", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", ["get", "class"], "grass"], + "paint": {"fill-color": "#d8e8c8", "fill-opacity": 1} + }, + { + "id": "landcover-grass-park", + "type": "fill", + "source": "openmaptiles", + "source-layer": "park", + "filter": ["==", ["get", "class"], "public_park"], + "paint": {"fill-color": "#d8e8c8", "fill-opacity": 0.8} + }, + { + "id": "waterway_tunnel", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "minzoom": 14, + "filter": [ + "all", + ["match", ["get", "class"], ["canal", "river", "stream"], true, false], + ["==", ["get", "brunnel"], "tunnel"] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [2, 4], + "line-width": [ + "interpolate", + ["exponential", 1.3], + ["zoom"], + 13, + 0.5, + 20, + 6 + ] + } + }, + { + "id": "waterway-other", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["match", ["get", "class"], ["canal", "river", "stream"], false, true], + ["==", ["get", "intermittent"], 0] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-width": [ + "interpolate", + ["exponential", 1.3], + ["zoom"], + 13, + 0.5, + 20, + 2 + ] + } + }, + { + "id": "waterway-other-intermittent", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["match", ["get", "class"], ["canal", "river", "stream"], false, true], + ["==", ["get", "intermittent"], 1] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [4, 3], + "line-width": [ + "interpolate", + ["exponential", 1.3], + ["zoom"], + 13, + 0.5, + 20, + 2 + ] + } + }, + { + "id": "waterway-stream-canal", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["match", ["get", "class"], ["canal", "stream"], true, false], + ["!=", ["get", "brunnel"], "tunnel"], + ["==", ["get", "intermittent"], 0] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-width": [ + "interpolate", + ["exponential", 1.3], + ["zoom"], + 13, + 0.5, + 20, + 6 + ] + } + }, + { + "id": "waterway-stream-canal-intermittent", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["match", ["get", "class"], ["canal", "stream"], true, false], + ["!=", ["get", "brunnel"], "tunnel"], + ["==", ["get", "intermittent"], 1] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [4, 3], + "line-width": [ + "interpolate", + ["exponential", 1.3], + ["zoom"], + 13, + 0.5, + 20, + 6 + ] + } + }, + { + "id": "waterway-river", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["==", ["get", "class"], "river"], + ["!=", ["get", "brunnel"], "tunnel"], + ["!=", ["get", "intermittent"], 1] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 10, + 0.8, + 20, + 6 + ] + } + }, + { + "id": "waterway-river-intermittent", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["==", ["get", "class"], "river"], + ["!=", ["get", "brunnel"], "tunnel"], + ["==", ["get", "intermittent"], 1] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [3, 2.5], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 10, + 0.8, + 20, + 6 + ] + } + }, + { + "id": "water", + "type": "fill", + "source": "openmaptiles", + "source-layer": "water", + "filter": [ + "all", + ["!=", ["get", "intermittent"], 1], + ["!=", ["get", "brunnel"], "tunnel"] + ], + "paint": {"fill-color": "#AECFE2"} + }, + { + "id": "water-intermittent", + "type": "fill", + "source": "openmaptiles", + "source-layer": "water", + "filter": ["==", ["get", "intermittent"], 1], + "paint": {"fill-color": "hsl(210,67%,85%)", "fill-opacity": 0.7} + }, + { + "id": "landcover-ice-shelf", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", ["get", "subclass"], "ice_shelf"], + "paint": { + "fill-color": "#fff", + "fill-opacity": ["interpolate", ["linear"], ["zoom"], 0, 0.9, 10, 0.3] + } + }, + { + "id": "landcover-sand", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", ["get", "class"], "sand"], + "paint": {"fill-color": "rgba(245, 238, 188, 1)", "fill-opacity": 1} + }, + { + "id": "building", + "type": "fill", + "source": "openmaptiles", + "source-layer": "building", + "paint": { + "fill-antialias": true, + "fill-color": [ + "interpolate", + ["linear"], + ["zoom"], + 15.5, + "#f2eae2", + 16, + "#dfdbd7" + ] + } + }, + { + "id": "building-top", + "type": "fill", + "source": "openmaptiles", + "source-layer": "building", + "paint": { + "fill-color": "#f2eae2", + "fill-opacity": ["interpolate", ["linear"], ["zoom"], 13, 0, 16, 1], + "fill-outline-color": "#dfdbd7", + "fill-translate": [ + "interpolate", + ["linear"], + ["zoom"], + 14, + ["literal", [0, 0]], + 16, + ["literal", [-2, -2]] + ] + } + }, + { + "id": "tunnel-service-track-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["service", "track"], true, false] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-dasharray": [0.5, 0.25], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15, + 1, + 16, + 4, + 20, + 11 + ] + } + }, + { + "id": "tunnel-motorway-link-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "motorway"], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "rgba(200, 147, 102, 1)", + "line-dasharray": [0.5, 0.25], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 1, + 13, + 3, + 14, + 4, + 20, + 15 + ] + } + }, + { + "id": "tunnel-minor-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "minor"] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-dasharray": [0.5, 0.25], + "line-opacity": ["interpolate", ["linear"], ["zoom"], 12, 0, 12.5, 1], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 0.5, + 13, + 1, + 14, + 4, + 20, + 15 + ] + } + }, + { + "id": "tunnel-link-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-dasharray": [0.5, 0.25], + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 1, + 13, + 3, + 14, + 4, + 20, + 15 + ] + } + }, + { + "id": "tunnel-secondary-tertiary-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["secondary", "tertiary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-dasharray": [0.5, 0.25], + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 8, + 1.5, + 20, + 17 + ] + } + }, + { + "id": "tunnel-trunk-primary-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["primary", "trunk"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 5, + 0.4, + 6, + 0.6, + 7, + 1.5, + 20, + 22 + ] + } + }, + { + "id": "tunnel-motorway-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "motorway"], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-dasharray": [0.5, 0.25], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 5, + 0.4, + 6, + 0.6, + 7, + 1.5, + 20, + 22 + ] + } + }, + { + "id": "tunnel-path", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "path"] + ], + "paint": { + "line-color": "#cba", + "line-dasharray": [1.5, 0.75], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15, + 1.2, + 20, + 4 + ] + } + }, + { + "id": "tunnel-motorway-link", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "motorway"], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "rgba(244, 209, 158, 1)", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12.5, + 0, + 13, + 1.5, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "tunnel-service-track", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["service", "track"], true, false] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15.5, + 0, + 16, + 2, + 20, + 7.5 + ] + } + }, + { + "id": "tunnel-link", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff4c6", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12.5, + 0, + 13, + 1.5, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "tunnel-minor", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "minor"] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 13.5, + 0, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "tunnel-secondary-tertiary", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["secondary", "tertiary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff4c6", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 10 + ] + } + }, + { + "id": "tunnel-trunk-primary", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["primary", "trunk"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff4c6", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "tunnel-motorway", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "motorway"], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#ffdaa6", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "tunnel-railway", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "rail"] + ], + "paint": { + "line-color": "#bbb", + "line-dasharray": [2, 2], + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14, + 0.4, + 15, + 0.75, + 20, + 2 + ] + } + }, + { + "id": "ferry", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": ["match", ["get", "class"], ["ferry"], true, false], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "rgba(108, 159, 182, 1)", + "line-dasharray": [2, 2], + "line-width": 1.1 + } + }, + { + "id": "aeroway-taxiway-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 12, + "filter": ["match", ["get", "class"], ["taxiway"], true, false], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgba(153, 153, 153, 1)", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.5], + ["zoom"], + 11, + 2, + 17, + 12 + ] + } + }, + { + "id": "aeroway-runway-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 12, + "filter": ["match", ["get", "class"], ["runway"], true, false], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgba(153, 153, 153, 1)", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.5], + ["zoom"], + 11, + 5, + 17, + 55 + ] + } + }, + { + "id": "aeroway-area", + "type": "fill", + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 4, + "filter": [ + "all", + ["match", ["geometry-type"], ["MultiPolygon", "Polygon"], true, false], + ["match", ["get", "class"], ["runway", "taxiway"], true, false] + ], + "paint": { + "fill-color": "rgba(255, 255, 255, 1)", + "fill-opacity": ["interpolate", ["linear"], ["zoom"], 13, 0, 14, 1] + } + }, + { + "id": "aeroway-taxiway", + "type": "line", + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 4, + "filter": [ + "all", + ["match", ["get", "class"], ["taxiway"], true, false], + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgba(255, 255, 255, 1)", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 11, 0, 12, 1], + "line-width": [ + "interpolate", + ["exponential", 1.5], + ["zoom"], + 11, + 1, + 17, + 10 + ] + } + }, + { + "id": "aeroway-runway", + "type": "line", + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 4, + "filter": [ + "all", + ["match", ["get", "class"], ["runway"], true, false], + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgba(255, 255, 255, 1)", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 11, 0, 12, 1], + "line-width": [ + "interpolate", + ["exponential", 1.5], + ["zoom"], + 11, + 4, + 17, + 50 + ] + } + }, + { + "id": "road_area_pier", + "type": "fill", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["match", ["geometry-type"], ["MultiPolygon", "Polygon"], true, false], + ["==", ["get", "class"], "pier"] + ], + "paint": {"fill-antialias": true, "fill-color": "#f8f4f0"} + }, + { + "id": "road_pier", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "class"], ["pier"], true, false] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#f8f4f0", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15, + 1, + 17, + 4 + ] + } + }, + { + "id": "highway-area", + "type": "fill", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["match", ["geometry-type"], ["MultiPolygon", "Polygon"], true, false], + ["match", ["get", "class"], ["pier"], false, true] + ], + "paint": { + "fill-antialias": false, + "fill-color": "hsla(0,0%,89%,0.56)", + "fill-opacity": 0.9, + "fill-outline-color": "#cfcdca" + } + }, + { + "id": "highway-motorway-link-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "motorway"], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 1, + 13, + 3, + 14, + 4, + 20, + 15 + ] + } + }, + { + "id": "highway-link-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 1, + 13, + 3, + 14, + 4, + 20, + 15 + ] + } + }, + { + "id": "highway-minor-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["!=", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["minor", "service", "track"], true, false] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 12, 0, 12.5, 1], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 0.5, + 13, + 1, + 14, + 4, + 20, + 15 + ] + } + }, + { + "id": "highway-secondary-tertiary-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["match", ["get", "class"], ["secondary", "tertiary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 8, + 1.5, + 20, + 17 + ] + } + }, + { + "id": "highway-primary-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 5, + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["match", ["get", "class"], ["primary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 7, 0, 8, 1], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 7, + 0, + 8, + 0.6, + 9, + 1.5, + 20, + 22 + ] + } + }, + { + "id": "highway-trunk-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 5, + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["match", ["get", "class"], ["trunk"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 5, 0, 6, 1], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 5, + 0, + 6, + 0.6, + 7, + 1.5, + 20, + 22 + ] + } + }, + { + "id": "highway-motorway-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 4, + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "motorway"], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 4, 0, 5, 1], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 4, + 0, + 5, + 0.4, + 6, + 0.6, + 7, + 1.5, + 20, + 22 + ] + } + }, + { + "id": "highway-path", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "path"] + ], + "paint": { + "line-color": "#cba", + "line-dasharray": [1.5, 0.75], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15, + 1.2, + 20, + 4 + ] + } + }, + { + "id": "highway-motorway-link", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 12, + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "motorway"], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fc8", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12.5, + 0, + 13, + 1.5, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "highway-link", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12.5, + 0, + 13, + 1.5, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "highway-minor", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["!=", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["minor", "service", "track"], true, false] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fff", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 13.5, + 0, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "highway-secondary-tertiary", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["match", ["get", "class"], ["secondary", "tertiary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 8, + 0.5, + 20, + 13 + ] + } + }, + { + "id": "highway-primary", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["match", ["get", "class"], ["primary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 8.5, + 0, + 9, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "highway-trunk", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["match", ["get", "class"], ["trunk"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "highway-motorway", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 5, + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "motorway"], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fc8", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "railway-transit", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "class"], "transit"], + ["match", ["get", "brunnel"], ["tunnel"], false, true] + ], + "paint": { + "line-color": "hsla(0,0%,73%,0.77)", + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14, + 0.4, + 20, + 1 + ] + } + }, + { + "id": "railway-transit-hatching", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "class"], "transit"], + ["match", ["get", "brunnel"], ["tunnel"], false, true] + ], + "paint": { + "line-color": "hsla(0,0%,73%,0.68)", + "line-dasharray": [0.2, 8], + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14.5, + 0, + 15, + 2, + 20, + 6 + ] + } + }, + { + "id": "railway-service", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "class"], "rail"], + ["has", "service"] + ], + "paint": { + "line-color": "hsla(0,0%,73%,0.77)", + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14, + 0.4, + 20, + 1 + ] + } + }, + { + "id": "railway-service-hatching", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "class"], "rail"], + ["has", "service"] + ], + "paint": { + "line-color": "hsla(0,0%,73%,0.68)", + "line-dasharray": [0.2, 8], + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14.5, + 0, + 15, + 2, + 20, + 6 + ] + } + }, + { + "id": "railway", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["!", ["has", "service"]], + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "rail"] + ], + "paint": { + "line-color": "#bbb", + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14, + 0.4, + 15, + 0.75, + 20, + 2 + ] + } + }, + { + "id": "railway-hatching", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["!", ["has", "service"]], + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "rail"] + ], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14.5, + 0, + 15, + 3, + 20, + 8 + ] + } + }, + { + "id": "bridge-motorway-link-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "motorway"], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 1, + 13, + 3, + 14, + 4, + 20, + 19 + ] + } + }, + { + "id": "bridge-link-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 1, + 13, + 3, + 14, + 4, + 20, + 19 + ] + } + }, + { + "id": "bridge-secondary-tertiary-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["match", ["get", "class"], ["secondary", "tertiary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 5, + 0.4, + 7, + 0.6, + 8, + 1.5, + 20, + 21 + ] + } + }, + { + "id": "bridge-trunk-primary-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["match", ["get", "class"], ["primary", "trunk"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "hsl(28,76%,67%)", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 5, + 0.4, + 6, + 0.6, + 7, + 1.5, + 20, + 26 + ] + } + }, + { + "id": "bridge-motorway-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "motorway"], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 5, + 0.4, + 6, + 0.6, + 7, + 1.5, + 20, + 26 + ] + } + }, + { + "id": "bridge-minor-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "brunnel"], "bridge"], + ["match", ["get", "class"], ["minor", "service", "track"], true, false] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 12, 0, 12.5, 1], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 0.5, + 13, + 1, + 14, + 6, + 20, + 24 + ] + } + }, + { + "id": "bridge-path-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "path"] + ], + "paint": { + "line-color": "#f8f4f0", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15, + 1.2, + 20, + 18 + ] + } + }, + { + "id": "bridge-path", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "path"] + ], + "paint": { + "line-color": "#cba", + "line-dasharray": [1.5, 0.75], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15, + 1.2, + 20, + 4 + ] + } + }, + { + "id": "bridge-motorway-link", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "motorway"], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fc8", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12.5, + 0, + 13, + 1.5, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "bridge-link", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12.5, + 0, + 13, + 1.5, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "bridge-minor", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "brunnel"], "bridge"], + ["match", ["get", "class"], ["minor", "service", "track"], true, false] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fff", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 13.5, + 0, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "bridge-secondary-tertiary", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["match", ["get", "class"], ["secondary", "tertiary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 8, + 0.5, + 20, + 13 + ] + } + }, + { + "id": "bridge-trunk-primary", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["match", ["get", "class"], ["primary", "trunk"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "bridge-motorway", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "motorway"], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fc8", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "bridge-railway", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "rail"] + ], + "paint": { + "line-color": "#bbb", + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14, + 0.4, + 15, + 0.75, + 20, + 2 + ] + } + }, + { + "id": "bridge-railway-hatching", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "rail"] + ], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14.5, + 0, + 15, + 3, + 20, + 8 + ] + } + }, + { + "id": "cablecar", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": ["==", ["get", "subclass"], "cable_car"], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "hsl(0,0%,70%)", + "line-width": ["interpolate", ["linear"], ["zoom"], 11, 1, 19, 2.5] + } + }, + { + "id": "cablecar-dash", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": ["==", ["get", "subclass"], "cable_car"], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "hsl(0,0%,70%)", + "line-dasharray": [2, 3], + "line-width": ["interpolate", ["linear"], ["zoom"], 11, 3, 19, 5.5] + } + }, + { + "id": "boundary_3", + "type": "line", + "source": "openmaptiles", + "source-layer": "boundary", + "minzoom": 5, + "filter": [ + "all", + [">=", ["get", "admin_level"], 3], + ["<=", ["get", "admin_level"], 6], + ["!=", ["get", "maritime"], 1], + ["!=", ["get", "disputed"], 1], + ["!", ["has", "claimed_by"]] + ], + "paint": { + "line-color": "hsl(0,0%,70%)", + "line-dasharray": [1, 1], + "line-width": ["interpolate", ["linear", 1], ["zoom"], 7, 1, 11, 2] + } + }, + { + "id": "boundary_2", + "type": "line", + "source": "openmaptiles", + "source-layer": "boundary", + "filter": [ + "all", + ["==", ["get", "admin_level"], 2], + ["!=", ["get", "maritime"], 1], + ["!=", ["get", "disputed"], 1], + ["!", ["has", "claimed_by"]] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "hsl(248,7%,66%)", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 0, 0.4, 4, 1], + "line-width": ["interpolate", ["linear"], ["zoom"], 3, 1, 5, 1.2, 12, 3] + } + }, + { + "id": "boundary_disputed", + "type": "line", + "source": "openmaptiles", + "source-layer": "boundary", + "filter": [ + "all", + ["!=", ["get", "maritime"], 1], + ["==", ["get", "disputed"], 1] + ], + "paint": { + "line-color": "hsl(248,7%,66%)", + "line-dasharray": [1, 2], + "line-width": ["interpolate", ["linear"], ["zoom"], 3, 1, 5, 1.2, 12, 3] + } + }, + { + "id": "road_oneway", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + ["==", ["get", "oneway"], 1], + [ + "match", + ["get", "class"], + [ + "minor", + "motorway", + "primary", + "secondary", + "service", + "tertiary", + "trunk" + ], + true, + false + ] + ], + "layout": { + "icon-image": "oneway", + "icon-padding": 2, + "icon-rotate": 90, + "icon-rotation-alignment": "map", + "icon-size": ["interpolate", ["linear"], ["zoom"], 15, 0.5, 19, 1], + "symbol-placement": "line", + "symbol-spacing": 75 + }, + "paint": {"icon-opacity": 0.5} + }, + { + "id": "road_oneway_opposite", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + ["==", ["get", "oneway"], -1], + [ + "match", + ["get", "class"], + [ + "minor", + "motorway", + "primary", + "secondary", + "service", + "tertiary", + "trunk" + ], + true, + false + ] + ], + "layout": { + "icon-image": "oneway", + "icon-padding": 2, + "icon-rotate": -90, + "icon-rotation-alignment": "map", + "icon-size": ["interpolate", ["linear"], ["zoom"], 15, 0.5, 19, 1], + "symbol-placement": "line", + "symbol-spacing": 75 + }, + "paint": {"icon-opacity": 0.5} + }, + { + "id": "waterway_line_label", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "waterway", + "minzoom": 10, + "filter": [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 350, + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], " ", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.2, + "text-max-width": 5, + "text-size": 14 + }, + "paint": { + "text-color": "#74aee9", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1.5 + } + }, + { + "id": "water_name_point_label", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "water_name", + "filter": [ + "match", + ["geometry-type"], + ["MultiPoint", "Point"], + true, + false + ], + "layout": { + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.2, + "text-max-width": 5, + "text-size": ["interpolate", ["linear"], ["zoom"], 0, 10, 8, 14] + }, + "paint": { + "text-color": "#495e91", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1.5 + } + }, + { + "id": "water_name_line_label", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "water_name", + "filter": [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 350, + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], " ", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.2, + "text-max-width": 5, + "text-size": 14 + }, + "paint": { + "text-color": "#495e91", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1.5 + } + }, + { + "id": "poi_hos", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "poi", + "minzoom": 15, + "filter": [ + "all", + ["==", "$type", "Point"], + ["has", "name"], + ["==", "class", "railway"], + ["==", "subclass", "station"] + ], + "layout": { + "icon-image": [ + "match", + ["get", "subclass"], + ["florist", "furniture"], + ["get", "subclass"], + ["get", "class"] + ], + "text-anchor": "top", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Italic"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-size": 12, + "visibility": "none" + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "poi_r1", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "poi", + "minzoom": 15, + "filter": [ + "all", + ["match", ["geometry-type"], ["MultiPoint", "Point"], true, false], + [">=", ["get", "rank"], 1], + ["<=", ["get", "rank"], 6], + ["!=", ["get", "subclass"], "bus_stop"], + ["!=", ["get", "subclass"], "tram_stop"] + ], + "layout": { + "icon-image": [ + "match", + ["get", "subclass"], + ["florist", "furniture"], + ["get", "subclass"], + ["get", "class"] + ], + "text-anchor": "top", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Italic"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-size": 12, + "visibility": "visible" + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "highway-name-path", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 15.5, + "filter": ["==", ["get", "class"], "path"], + "layout": { + "symbol-placement": "line", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], " ", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "map", + "text-size": ["interpolate", ["linear"], ["zoom"], 13, 12, 14, 13] + }, + "paint": { + "text-color": "hsl(30,23%,62%)", + "text-halo-color": "#f8f4f0", + "text-halo-width": 0.5 + } + }, + { + "id": "highway-name-minor", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 15, + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "class"], ["minor", "service", "track"], true, false] + ], + "layout": { + "symbol-placement": "line", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], " ", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "map", + "text-size": ["interpolate", ["linear"], ["zoom"], 13, 12, 14, 13] + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-width": 1 + } + }, + { + "id": "highway-name-major", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 12.2, + "filter": [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + "layout": { + "symbol-placement": "line", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], " ", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "map", + "text-size": ["interpolate", ["linear"], ["zoom"], 13, 12, 14, 13] + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-width": 1 + } + }, + { + "id": "highway-shield-non-us", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 8, + "filter": [ + "all", + ["<=", ["get", "ref_length"], 6], + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + [ + "match", + ["get", "network"], + ["us-highway", "us-interstate", "us-state"], + false, + true + ] + ], + "layout": { + "icon-image": ["concat", "road_", ["get", "ref_length"]], + "icon-rotation-alignment": "viewport", + "icon-size": 1, + "symbol-placement": ["step", ["zoom"], "point", 11, "line"], + "symbol-spacing": 200, + "text-field": ["to-string", ["get", "ref"]], + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "viewport", + "text-size": 10 + } + }, + { + "id": "highway-shield-us-interstate", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 7, + "filter": [ + "all", + ["<=", ["get", "ref_length"], 6], + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "network"], ["us-interstate"], true, false] + ], + "layout": { + "icon-image": [ + "concat", + ["get", "network"], + "_", + ["get", "ref_length"] + ], + "icon-rotation-alignment": "viewport", + "icon-size": 1, + "symbol-placement": ["step", ["zoom"], "point", 7, "line", 8, "line"], + "symbol-spacing": 200, + "text-field": ["to-string", ["get", "ref"]], + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "viewport", + "text-size": 10 + } + }, + { + "id": "road_shield_us", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 9, + "filter": [ + "all", + ["<=", ["get", "ref_length"], 6], + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "network"], ["us-highway", "us-state"], true, false] + ], + "layout": { + "icon-image": [ + "concat", + ["get", "network"], + "_", + ["get", "ref_length"] + ], + "icon-rotation-alignment": "viewport", + "icon-size": 1, + "symbol-placement": ["step", ["zoom"], "point", 11, "line"], + "symbol-spacing": 200, + "text-field": ["to-string", ["get", "ref"]], + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "viewport", + "text-size": 10 + } + }, + { + "id": "airport", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "aerodrome_label", + "minzoom": 10, + "filter": ["all", ["has", "iata"]], + "layout": { + "icon-image": "airport_11", + "icon-size": 1, + "text-anchor": "top", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-optional": true, + "text-padding": 2, + "text-size": 12 + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "label_other", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 8, + "filter": [ + "match", + ["get", "class"], + ["city", "continent", "country", "state", "town", "village"], + false, + true + ], + "layout": { + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.1, + "text-max-width": 9, + "text-size": ["interpolate", ["linear"], ["zoom"], 8, 9, 12, 10], + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#333", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_village", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 9, + "filter": ["==", ["get", "class"], "village"], + "layout": { + "icon-allow-overlap": true, + "icon-image": ["step", ["zoom"], "circle_11_black", 10, ""], + "icon-optional": false, + "icon-size": 0.2, + "text-anchor": "bottom", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-max-width": 8, + "text-size": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 7, + 10, + 11, + 12 + ] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_town", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 6, + "filter": ["==", ["get", "class"], "town"], + "layout": { + "icon-allow-overlap": true, + "icon-image": ["step", ["zoom"], "circle_11_black", 10, ""], + "icon-optional": false, + "icon-size": 0.2, + "text-anchor": "bottom", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-max-width": 8, + "text-size": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 7, + 12, + 11, + 14 + ] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_state", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 5, + "maxzoom": 8, + "filter": ["==", ["get", "class"], "state"], + "layout": { + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.2, + "text-max-width": 9, + "text-size": ["interpolate", ["linear"], ["zoom"], 5, 10, 8, 14], + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#333", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_city", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 3, + "filter": [ + "all", + ["==", ["get", "class"], "city"], + ["!=", ["get", "capital"], 2] + ], + "layout": { + "icon-allow-overlap": true, + "icon-image": ["step", ["zoom"], "circle_11_black", 9, ""], + "icon-optional": false, + "icon-size": 0.4, + "text-anchor": "bottom", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-max-width": 8, + "text-offset": [0, -0.1], + "text-size": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 4, + 11, + 7, + 13, + 11, + 18 + ] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_city_capital", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 3, + "filter": [ + "all", + ["==", ["get", "class"], "city"], + ["==", ["get", "capital"], 2] + ], + "layout": { + "icon-allow-overlap": true, + "icon-image": ["step", ["zoom"], "circle_11_black", 9, ""], + "icon-optional": false, + "icon-size": 0.5, + "text-anchor": "bottom", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Bold"], + "text-max-width": 8, + "text-offset": [0, -0.2], + "text-size": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 4, + 12, + 7, + 14, + 11, + 20 + ] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_country_3", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 2, + "maxzoom": 9, + "filter": [ + "all", + ["==", ["get", "class"], "country"], + [">=", ["get", "rank"], 3] + ], + "layout": { + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Bold"], + "text-max-width": 6.25, + "text-size": ["interpolate", ["linear"], ["zoom"], 3, 9, 7, 17] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_country_2", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 9, + "filter": [ + "all", + ["==", ["get", "class"], "country"], + ["==", ["get", "rank"], 2] + ], + "layout": { + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Bold"], + "text-max-width": 6.25, + "text-size": ["interpolate", ["linear"], ["zoom"], 2, 9, 5, 17] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_country_1", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 9, + "filter": [ + "all", + ["==", ["get", "class"], "country"], + ["==", ["get", "rank"], 1] + ], + "layout": { + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Bold"], + "text-max-width": 6.25, + "text-size": ["interpolate", ["linear"], ["zoom"], 1, 9, 4, 17] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + } + ], + "id": "94huyrm" +} diff --git a/app/src/main/assets/map_style_good_noshops.json b/app/src/main/assets/map_style_good_noshops.json new file mode 100644 index 0000000..c2de1bc --- /dev/null +++ b/app/src/main/assets/map_style_good_noshops.json @@ -0,0 +1,3107 @@ +{ + "version": 8, + "metadata": {"maputnik:renderer": "mlgljs"}, + "sources": { + "ne2_shaded": { + "maxzoom": 6, + "tileSize": 256, + "tiles": [ + "https://tiles.openfreemap.org/natural_earth/ne2sr/{z}/{x}/{y}.png" + ], + "type": "raster" + }, + "openmaptiles": { + "type": "vector", + "url": "https://tiles.openfreemap.org/planet" + } + }, + "sprite": "https://tiles.openfreemap.org/sprites/ofm_f384/ofm", + "glyphs": "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": {"background-color": "#f8f4f0"} + }, + { + "id": "landcover-glacier", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", ["get", "subclass"], "glacier"], + "paint": { + "fill-color": "#fff", + "fill-opacity": ["interpolate", ["linear"], ["zoom"], 0, 0.9, 10, 0.3] + } + }, + { + "id": "landuse-residential", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": [ + "match", + ["get", "class"], + ["neighbourhood", "residential"], + true, + false + ], + "paint": { + "fill-color": [ + "interpolate", + ["linear"], + ["zoom"], + 12, + "hsla(30,19%,90%,0.4)", + 16, + "hsla(30,19%,90%,0.2)" + ] + } + }, + { + "id": "landuse-suburb", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "maxzoom": 10, + "filter": ["==", ["get", "class"], "suburb"], + "paint": { + "fill-color": [ + "interpolate", + ["linear"], + ["zoom"], + 8, + "hsla(30,19%,90%,0.4)", + 10, + "hsla(30,19%,90%,0.0)" + ] + } + }, + { + "id": "landuse-commercial", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": [ + "all", + ["match", ["geometry-type"], ["MultiPolygon", "Polygon"], true, false], + ["==", ["get", "class"], "commercial"] + ], + "paint": {"fill-color": "hsla(0,60%,87%,0.23)"} + }, + { + "id": "landuse-industrial", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": [ + "all", + ["match", ["geometry-type"], ["MultiPolygon", "Polygon"], true, false], + [ + "match", + ["get", "class"], + ["dam", "garages", "industrial"], + true, + false + ] + ], + "paint": {"fill-color": "hsla(49,100%,88%,0.34)"} + }, + { + "id": "landuse-cemetery", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": ["==", ["get", "class"], "cemetery"], + "paint": {"fill-color": "#e0e4dd"} + }, + { + "id": "landuse-hospital", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": ["==", ["get", "class"], "hospital"], + "paint": {"fill-color": "#fde"} + }, + { + "id": "landuse-school", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": ["==", ["get", "class"], "school"], + "paint": {"fill-color": "#f0e8f8"} + }, + { + "id": "landuse-railway", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": ["==", ["get", "class"], "railway"], + "paint": {"fill-color": "hsla(30,19%,90%,0.4)"} + }, + { + "id": "park", + "type": "fill", + "source": "openmaptiles", + "source-layer": "park", + "filter": [ + "match", + ["geometry-type"], + ["MultiPolygon", "Polygon"], + true, + false + ], + "paint": { + "fill-color": "#d8e8c8", + "fill-opacity": [ + "interpolate", + ["exponential", 1.8], + ["zoom"], + 9, + 0.5, + 12, + 0.2 + ] + } + }, + { + "id": "landcover-wood", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", ["get", "class"], "wood"], + "paint": { + "fill-antialias": ["step", ["zoom"], false, 9, true], + "fill-color": "#6a4", + "fill-opacity": 0.1, + "fill-outline-color": "hsla(0,0%,0%,0.03)" + } + }, + { + "id": "landcover-grass", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", ["get", "class"], "grass"], + "paint": {"fill-color": "#d8e8c8", "fill-opacity": 1} + }, + { + "id": "landcover-grass-park", + "type": "fill", + "source": "openmaptiles", + "source-layer": "park", + "filter": ["==", ["get", "class"], "public_park"], + "paint": {"fill-color": "#d8e8c8", "fill-opacity": 0.8} + }, + { + "id": "waterway_tunnel", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "minzoom": 14, + "filter": [ + "all", + ["match", ["get", "class"], ["canal", "river", "stream"], true, false], + ["==", ["get", "brunnel"], "tunnel"] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [2, 4], + "line-width": [ + "interpolate", + ["exponential", 1.3], + ["zoom"], + 13, + 0.5, + 20, + 6 + ] + } + }, + { + "id": "waterway-other", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["match", ["get", "class"], ["canal", "river", "stream"], false, true], + ["==", ["get", "intermittent"], 0] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-width": [ + "interpolate", + ["exponential", 1.3], + ["zoom"], + 13, + 0.5, + 20, + 2 + ] + } + }, + { + "id": "waterway-other-intermittent", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["match", ["get", "class"], ["canal", "river", "stream"], false, true], + ["==", ["get", "intermittent"], 1] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [4, 3], + "line-width": [ + "interpolate", + ["exponential", 1.3], + ["zoom"], + 13, + 0.5, + 20, + 2 + ] + } + }, + { + "id": "waterway-stream-canal", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["match", ["get", "class"], ["canal", "stream"], true, false], + ["!=", ["get", "brunnel"], "tunnel"], + ["==", ["get", "intermittent"], 0] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-width": [ + "interpolate", + ["exponential", 1.3], + ["zoom"], + 13, + 0.5, + 20, + 6 + ] + } + }, + { + "id": "waterway-stream-canal-intermittent", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["match", ["get", "class"], ["canal", "stream"], true, false], + ["!=", ["get", "brunnel"], "tunnel"], + ["==", ["get", "intermittent"], 1] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [4, 3], + "line-width": [ + "interpolate", + ["exponential", 1.3], + ["zoom"], + 13, + 0.5, + 20, + 6 + ] + } + }, + { + "id": "waterway-river", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["==", ["get", "class"], "river"], + ["!=", ["get", "brunnel"], "tunnel"], + ["!=", ["get", "intermittent"], 1] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 10, + 0.8, + 20, + 6 + ] + } + }, + { + "id": "waterway-river-intermittent", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["==", ["get", "class"], "river"], + ["!=", ["get", "brunnel"], "tunnel"], + ["==", ["get", "intermittent"], 1] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [3, 2.5], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 10, + 0.8, + 20, + 6 + ] + } + }, + { + "id": "water", + "type": "fill", + "source": "openmaptiles", + "source-layer": "water", + "filter": [ + "all", + ["!=", ["get", "intermittent"], 1], + ["!=", ["get", "brunnel"], "tunnel"] + ], + "paint": {"fill-color": "#AECFE2"} + }, + { + "id": "water-intermittent", + "type": "fill", + "source": "openmaptiles", + "source-layer": "water", + "filter": ["==", ["get", "intermittent"], 1], + "paint": {"fill-color": "hsl(210,67%,85%)", "fill-opacity": 0.7} + }, + { + "id": "landcover-ice-shelf", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", ["get", "subclass"], "ice_shelf"], + "paint": { + "fill-color": "#fff", + "fill-opacity": ["interpolate", ["linear"], ["zoom"], 0, 0.9, 10, 0.3] + } + }, + { + "id": "landcover-sand", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", ["get", "class"], "sand"], + "paint": {"fill-color": "rgba(245, 238, 188, 1)", "fill-opacity": 1} + }, + { + "id": "building", + "type": "fill", + "source": "openmaptiles", + "source-layer": "building", + "paint": { + "fill-antialias": true, + "fill-color": [ + "interpolate", + ["linear"], + ["zoom"], + 15.5, + "#f2eae2", + 16, + "#dfdbd7" + ] + } + }, + { + "id": "building-top", + "type": "fill", + "source": "openmaptiles", + "source-layer": "building", + "paint": { + "fill-color": "#f2eae2", + "fill-opacity": ["interpolate", ["linear"], ["zoom"], 13, 0, 16, 1], + "fill-outline-color": "#dfdbd7", + "fill-translate": [ + "interpolate", + ["linear"], + ["zoom"], + 14, + ["literal", [0, 0]], + 16, + ["literal", [-2, -2]] + ] + } + }, + { + "id": "tunnel-service-track-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["service", "track"], true, false] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-dasharray": [0.5, 0.25], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15, + 1, + 16, + 4, + 20, + 11 + ] + } + }, + { + "id": "tunnel-motorway-link-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "motorway"], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "rgba(200, 147, 102, 1)", + "line-dasharray": [0.5, 0.25], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 1, + 13, + 3, + 14, + 4, + 20, + 15 + ] + } + }, + { + "id": "tunnel-minor-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "minor"] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-dasharray": [0.5, 0.25], + "line-opacity": ["interpolate", ["linear"], ["zoom"], 12, 0, 12.5, 1], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 0.5, + 13, + 1, + 14, + 4, + 20, + 15 + ] + } + }, + { + "id": "tunnel-link-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-dasharray": [0.5, 0.25], + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 1, + 13, + 3, + 14, + 4, + 20, + 15 + ] + } + }, + { + "id": "tunnel-secondary-tertiary-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["secondary", "tertiary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-dasharray": [0.5, 0.25], + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 8, + 1.5, + 20, + 17 + ] + } + }, + { + "id": "tunnel-trunk-primary-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["primary", "trunk"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 5, + 0.4, + 6, + 0.6, + 7, + 1.5, + 20, + 22 + ] + } + }, + { + "id": "tunnel-motorway-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "motorway"], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-dasharray": [0.5, 0.25], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 5, + 0.4, + 6, + 0.6, + 7, + 1.5, + 20, + 22 + ] + } + }, + { + "id": "tunnel-path", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "path"] + ], + "paint": { + "line-color": "#cba", + "line-dasharray": [1.5, 0.75], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15, + 1.2, + 20, + 4 + ] + } + }, + { + "id": "tunnel-motorway-link", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "motorway"], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "rgba(244, 209, 158, 1)", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12.5, + 0, + 13, + 1.5, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "tunnel-service-track", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["service", "track"], true, false] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15.5, + 0, + 16, + 2, + 20, + 7.5 + ] + } + }, + { + "id": "tunnel-link", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff4c6", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12.5, + 0, + 13, + 1.5, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "tunnel-minor", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "minor"] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 13.5, + 0, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "tunnel-secondary-tertiary", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["secondary", "tertiary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff4c6", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 10 + ] + } + }, + { + "id": "tunnel-trunk-primary", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["primary", "trunk"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff4c6", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "tunnel-motorway", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "motorway"], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#ffdaa6", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "tunnel-railway", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "tunnel"], + ["==", ["get", "class"], "rail"] + ], + "paint": { + "line-color": "#bbb", + "line-dasharray": [2, 2], + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14, + 0.4, + 15, + 0.75, + 20, + 2 + ] + } + }, + { + "id": "ferry", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": ["match", ["get", "class"], ["ferry"], true, false], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "rgba(108, 159, 182, 1)", + "line-dasharray": [2, 2], + "line-width": 1.1 + } + }, + { + "id": "aeroway-taxiway-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 12, + "filter": ["match", ["get", "class"], ["taxiway"], true, false], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgba(153, 153, 153, 1)", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.5], + ["zoom"], + 11, + 2, + 17, + 12 + ] + } + }, + { + "id": "aeroway-runway-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 12, + "filter": ["match", ["get", "class"], ["runway"], true, false], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgba(153, 153, 153, 1)", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.5], + ["zoom"], + 11, + 5, + 17, + 55 + ] + } + }, + { + "id": "aeroway-area", + "type": "fill", + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 4, + "filter": [ + "all", + ["match", ["geometry-type"], ["MultiPolygon", "Polygon"], true, false], + ["match", ["get", "class"], ["runway", "taxiway"], true, false] + ], + "paint": { + "fill-color": "rgba(255, 255, 255, 1)", + "fill-opacity": ["interpolate", ["linear"], ["zoom"], 13, 0, 14, 1] + } + }, + { + "id": "aeroway-taxiway", + "type": "line", + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 4, + "filter": [ + "all", + ["match", ["get", "class"], ["taxiway"], true, false], + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgba(255, 255, 255, 1)", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 11, 0, 12, 1], + "line-width": [ + "interpolate", + ["exponential", 1.5], + ["zoom"], + 11, + 1, + 17, + 10 + ] + } + }, + { + "id": "aeroway-runway", + "type": "line", + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 4, + "filter": [ + "all", + ["match", ["get", "class"], ["runway"], true, false], + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgba(255, 255, 255, 1)", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 11, 0, 12, 1], + "line-width": [ + "interpolate", + ["exponential", 1.5], + ["zoom"], + 11, + 4, + 17, + 50 + ] + } + }, + { + "id": "road_area_pier", + "type": "fill", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["match", ["geometry-type"], ["MultiPolygon", "Polygon"], true, false], + ["==", ["get", "class"], "pier"] + ], + "paint": {"fill-antialias": true, "fill-color": "#f8f4f0"} + }, + { + "id": "road_pier", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "class"], ["pier"], true, false] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#f8f4f0", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15, + 1, + 17, + 4 + ] + } + }, + { + "id": "highway-area", + "type": "fill", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["match", ["geometry-type"], ["MultiPolygon", "Polygon"], true, false], + ["match", ["get", "class"], ["pier"], false, true] + ], + "paint": { + "fill-antialias": false, + "fill-color": "hsla(0,0%,89%,0.56)", + "fill-opacity": 0.9, + "fill-outline-color": "#cfcdca" + } + }, + { + "id": "highway-motorway-link-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "motorway"], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 1, + 13, + 3, + 14, + 4, + 20, + 15 + ] + } + }, + { + "id": "highway-link-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 1, + 13, + 3, + 14, + 4, + 20, + 15 + ] + } + }, + { + "id": "highway-minor-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["!=", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["minor", "service", "track"], true, false] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 12, 0, 12.5, 1], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 0.5, + 13, + 1, + 14, + 4, + 20, + 15 + ] + } + }, + { + "id": "highway-secondary-tertiary-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["match", ["get", "class"], ["secondary", "tertiary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 8, + 1.5, + 20, + 17 + ] + } + }, + { + "id": "highway-primary-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 5, + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["match", ["get", "class"], ["primary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 7, 0, 8, 1], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 7, + 0, + 8, + 0.6, + 9, + 1.5, + 20, + 22 + ] + } + }, + { + "id": "highway-trunk-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 5, + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["match", ["get", "class"], ["trunk"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 5, 0, 6, 1], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 5, + 0, + 6, + 0.6, + 7, + 1.5, + 20, + 22 + ] + } + }, + { + "id": "highway-motorway-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 4, + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "motorway"], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 4, 0, 5, 1], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 4, + 0, + 5, + 0.4, + 6, + 0.6, + 7, + 1.5, + 20, + 22 + ] + } + }, + { + "id": "highway-path", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "path"] + ], + "paint": { + "line-color": "#cba", + "line-dasharray": [1.5, 0.75], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15, + 1.2, + 20, + 4 + ] + } + }, + { + "id": "highway-motorway-link", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 12, + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "motorway"], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fc8", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12.5, + 0, + 13, + 1.5, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "highway-link", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12.5, + 0, + 13, + 1.5, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "highway-minor", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["!=", ["get", "brunnel"], "tunnel"], + ["match", ["get", "class"], ["minor", "service", "track"], true, false] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fff", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 13.5, + 0, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "highway-secondary-tertiary", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["match", ["get", "class"], ["secondary", "tertiary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 8, + 0.5, + 20, + 13 + ] + } + }, + { + "id": "highway-primary", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["match", ["get", "class"], ["primary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 8.5, + 0, + 9, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "highway-trunk", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["match", ["get", "class"], ["trunk"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "highway-motorway", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 5, + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "motorway"], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fc8", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "railway-transit", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "class"], "transit"], + ["match", ["get", "brunnel"], ["tunnel"], false, true] + ], + "paint": { + "line-color": "hsla(0,0%,73%,0.77)", + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14, + 0.4, + 20, + 1 + ] + } + }, + { + "id": "railway-transit-hatching", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "class"], "transit"], + ["match", ["get", "brunnel"], ["tunnel"], false, true] + ], + "paint": { + "line-color": "hsla(0,0%,73%,0.68)", + "line-dasharray": [0.2, 8], + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14.5, + 0, + 15, + 2, + 20, + 6 + ] + } + }, + { + "id": "railway-service", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "class"], "rail"], + ["has", "service"] + ], + "paint": { + "line-color": "hsla(0,0%,73%,0.77)", + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14, + 0.4, + 20, + 1 + ] + } + }, + { + "id": "railway-service-hatching", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "class"], "rail"], + ["has", "service"] + ], + "paint": { + "line-color": "hsla(0,0%,73%,0.68)", + "line-dasharray": [0.2, 8], + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14.5, + 0, + 15, + 2, + 20, + 6 + ] + } + }, + { + "id": "railway", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["!", ["has", "service"]], + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "rail"] + ], + "paint": { + "line-color": "#bbb", + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14, + 0.4, + 15, + 0.75, + 20, + 2 + ] + } + }, + { + "id": "railway-hatching", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["!", ["has", "service"]], + ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], + ["==", ["get", "class"], "rail"] + ], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14.5, + 0, + 15, + 3, + 20, + 8 + ] + } + }, + { + "id": "bridge-motorway-link-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "motorway"], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 1, + 13, + 3, + 14, + 4, + 20, + 19 + ] + } + }, + { + "id": "bridge-link-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 1, + 13, + 3, + 14, + 4, + 20, + 19 + ] + } + }, + { + "id": "bridge-secondary-tertiary-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["match", ["get", "class"], ["secondary", "tertiary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 5, + 0.4, + 7, + 0.6, + 8, + 1.5, + 20, + 21 + ] + } + }, + { + "id": "bridge-trunk-primary-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["match", ["get", "class"], ["primary", "trunk"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "hsl(28,76%,67%)", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 5, + 0.4, + 6, + 0.6, + 7, + 1.5, + 20, + 26 + ] + } + }, + { + "id": "bridge-motorway-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "motorway"], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 5, + 0.4, + 6, + 0.6, + 7, + 1.5, + 20, + 26 + ] + } + }, + { + "id": "bridge-minor-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "brunnel"], "bridge"], + ["match", ["get", "class"], ["minor", "service", "track"], true, false] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 12, 0, 12.5, 1], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12, + 0.5, + 13, + 1, + 14, + 6, + 20, + 24 + ] + } + }, + { + "id": "bridge-path-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "path"] + ], + "paint": { + "line-color": "#f8f4f0", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15, + 1.2, + 20, + 18 + ] + } + }, + { + "id": "bridge-path", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "path"] + ], + "paint": { + "line-color": "#cba", + "line-dasharray": [1.5, 0.75], + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 15, + 1.2, + 20, + 4 + ] + } + }, + { + "id": "bridge-motorway-link", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "motorway"], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fc8", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12.5, + 0, + 13, + 1.5, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "bridge-link", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + ["==", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 12.5, + 0, + 13, + 1.5, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "bridge-minor", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["==", ["get", "brunnel"], "bridge"], + ["match", ["get", "class"], ["minor", "service", "track"], true, false] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fff", + "line-opacity": 1, + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 13.5, + 0, + 14, + 2.5, + 20, + 11.5 + ] + } + }, + { + "id": "bridge-secondary-tertiary", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["match", ["get", "class"], ["secondary", "tertiary"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 8, + 0.5, + 20, + 13 + ] + } + }, + { + "id": "bridge-trunk-primary", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["match", ["get", "class"], ["primary", "trunk"], true, false], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "bridge-motorway", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "motorway"], + ["!=", ["get", "ramp"], 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fc8", + "line-width": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 6.5, + 0, + 7, + 0.5, + 20, + 18 + ] + } + }, + { + "id": "bridge-railway", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "rail"] + ], + "paint": { + "line-color": "#bbb", + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14, + 0.4, + 15, + 0.75, + 20, + 2 + ] + } + }, + { + "id": "bridge-railway-hatching", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", ["get", "brunnel"], "bridge"], + ["==", ["get", "class"], "rail"] + ], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": [ + "interpolate", + ["exponential", 1.4], + ["zoom"], + 14.5, + 0, + 15, + 3, + 20, + 8 + ] + } + }, + { + "id": "cablecar", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": ["==", ["get", "subclass"], "cable_car"], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "hsl(0,0%,70%)", + "line-width": ["interpolate", ["linear"], ["zoom"], 11, 1, 19, 2.5] + } + }, + { + "id": "cablecar-dash", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": ["==", ["get", "subclass"], "cable_car"], + "layout": {"line-cap": "round"}, + "paint": { + "line-color": "hsl(0,0%,70%)", + "line-dasharray": [2, 3], + "line-width": ["interpolate", ["linear"], ["zoom"], 11, 3, 19, 5.5] + } + }, + { + "id": "boundary_3", + "type": "line", + "source": "openmaptiles", + "source-layer": "boundary", + "minzoom": 5, + "filter": [ + "all", + [">=", ["get", "admin_level"], 3], + ["<=", ["get", "admin_level"], 6], + ["!=", ["get", "maritime"], 1], + ["!=", ["get", "disputed"], 1], + ["!", ["has", "claimed_by"]] + ], + "paint": { + "line-color": "hsl(0,0%,70%)", + "line-dasharray": [1, 1], + "line-width": ["interpolate", ["linear", 1], ["zoom"], 7, 1, 11, 2] + } + }, + { + "id": "boundary_2", + "type": "line", + "source": "openmaptiles", + "source-layer": "boundary", + "filter": [ + "all", + ["==", ["get", "admin_level"], 2], + ["!=", ["get", "maritime"], 1], + ["!=", ["get", "disputed"], 1], + ["!", ["has", "claimed_by"]] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "hsl(248,7%,66%)", + "line-opacity": ["interpolate", ["linear"], ["zoom"], 0, 0.4, 4, 1], + "line-width": ["interpolate", ["linear"], ["zoom"], 3, 1, 5, 1.2, 12, 3] + } + }, + { + "id": "boundary_disputed", + "type": "line", + "source": "openmaptiles", + "source-layer": "boundary", + "filter": [ + "all", + ["!=", ["get", "maritime"], 1], + ["==", ["get", "disputed"], 1] + ], + "paint": { + "line-color": "hsl(248,7%,66%)", + "line-dasharray": [1, 2], + "line-width": ["interpolate", ["linear"], ["zoom"], 3, 1, 5, 1.2, 12, 3] + } + }, + { + "id": "road_oneway", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + ["==", ["get", "oneway"], 1], + [ + "match", + ["get", "class"], + [ + "minor", + "motorway", + "primary", + "secondary", + "service", + "tertiary", + "trunk" + ], + true, + false + ] + ], + "layout": { + "icon-image": "oneway", + "icon-padding": 2, + "icon-rotate": 90, + "icon-rotation-alignment": "map", + "icon-size": ["interpolate", ["linear"], ["zoom"], 15, 0.5, 19, 1], + "symbol-placement": "line", + "symbol-spacing": 75 + }, + "paint": {"icon-opacity": 0.5} + }, + { + "id": "road_oneway_opposite", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + ["==", ["get", "oneway"], -1], + [ + "match", + ["get", "class"], + [ + "minor", + "motorway", + "primary", + "secondary", + "service", + "tertiary", + "trunk" + ], + true, + false + ] + ], + "layout": { + "icon-image": "oneway", + "icon-padding": 2, + "icon-rotate": -90, + "icon-rotation-alignment": "map", + "icon-size": ["interpolate", ["linear"], ["zoom"], 15, 0.5, 19, 1], + "symbol-placement": "line", + "symbol-spacing": 75 + }, + "paint": {"icon-opacity": 0.5} + }, + { + "id": "waterway_line_label", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "waterway", + "minzoom": 10, + "filter": [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 350, + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], " ", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.2, + "text-max-width": 5, + "text-size": 14 + }, + "paint": { + "text-color": "#74aee9", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1.5 + } + }, + { + "id": "water_name_point_label", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "water_name", + "filter": [ + "match", + ["geometry-type"], + ["MultiPoint", "Point"], + true, + false + ], + "layout": { + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.2, + "text-max-width": 5, + "text-size": ["interpolate", ["linear"], ["zoom"], 0, 10, 8, 14] + }, + "paint": { + "text-color": "#495e91", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1.5 + } + }, + { + "id": "water_name_line_label", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "water_name", + "filter": [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 350, + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], " ", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.2, + "text-max-width": 5, + "text-size": 14 + }, + "paint": { + "text-color": "#495e91", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1.5 + } + }, + { + "id": "poi_r1", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "poi", + "minzoom": 15, + "filter": [ + "all", + ["match", ["geometry-type"], ["MultiPoint", "Point"], true, false], + [">=", ["get", "rank"], 1], + ["<=", ["get", "rank"], 6], + ["!=", ["get", "subclass"], "bus_stop"], + ["!=", ["get", "subclass"], "tram_stop"], + ["!=", ["get", "class"], "shop"] + ], + "layout": { + "icon-image": [ + "match", + ["get", "subclass"], + ["florist", "furniture"], + ["get", "subclass"], + ["get", "class"] + ], + "text-anchor": "top", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Italic"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-size": 12, + "visibility": "visible" + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "highway-name-path", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 15.5, + "filter": ["==", ["get", "class"], "path"], + "layout": { + "symbol-placement": "line", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], " ", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "map", + "text-size": ["interpolate", ["linear"], ["zoom"], 13, 12, 14, 13] + }, + "paint": { + "text-color": "hsl(30,23%,62%)", + "text-halo-color": "#f8f4f0", + "text-halo-width": 0.5 + } + }, + { + "id": "highway-name-minor", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 15, + "filter": [ + "all", + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "class"], ["minor", "service", "track"], true, false] + ], + "layout": { + "symbol-placement": "line", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], " ", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "map", + "text-size": ["interpolate", ["linear"], ["zoom"], 13, 12, 14, 13] + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-width": 1 + } + }, + { + "id": "highway-name-major", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 12.2, + "filter": [ + "match", + ["get", "class"], + ["primary", "secondary", "tertiary", "trunk"], + true, + false + ], + "layout": { + "symbol-placement": "line", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], " ", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "map", + "text-size": ["interpolate", ["linear"], ["zoom"], 13, 12, 14, 13] + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-width": 1 + } + }, + { + "id": "highway-shield-non-us", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 8, + "filter": [ + "all", + ["<=", ["get", "ref_length"], 6], + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + [ + "match", + ["get", "network"], + ["us-highway", "us-interstate", "us-state"], + false, + true + ] + ], + "layout": { + "icon-image": ["concat", "road_", ["get", "ref_length"]], + "icon-rotation-alignment": "viewport", + "icon-size": 1, + "symbol-placement": ["step", ["zoom"], "point", 11, "line"], + "symbol-spacing": 200, + "text-field": ["to-string", ["get", "ref"]], + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "viewport", + "text-size": 10 + } + }, + { + "id": "highway-shield-us-interstate", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 7, + "filter": [ + "all", + ["<=", ["get", "ref_length"], 6], + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "network"], ["us-interstate"], true, false] + ], + "layout": { + "icon-image": [ + "concat", + ["get", "network"], + "_", + ["get", "ref_length"] + ], + "icon-rotation-alignment": "viewport", + "icon-size": 1, + "symbol-placement": ["step", ["zoom"], "point", 7, "line", 8, "line"], + "symbol-spacing": 200, + "text-field": ["to-string", ["get", "ref"]], + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "viewport", + "text-size": 10 + } + }, + { + "id": "road_shield_us", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 9, + "filter": [ + "all", + ["<=", ["get", "ref_length"], 6], + [ + "match", + ["geometry-type"], + ["LineString", "MultiLineString"], + true, + false + ], + ["match", ["get", "network"], ["us-highway", "us-state"], true, false] + ], + "layout": { + "icon-image": [ + "concat", + ["get", "network"], + "_", + ["get", "ref_length"] + ], + "icon-rotation-alignment": "viewport", + "icon-size": 1, + "symbol-placement": ["step", ["zoom"], "point", 11, "line"], + "symbol-spacing": 200, + "text-field": ["to-string", ["get", "ref"]], + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "viewport", + "text-size": 10 + } + }, + { + "id": "airport", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "aerodrome_label", + "minzoom": 10, + "filter": ["all", ["has", "iata"]], + "layout": { + "icon-image": "airport_11", + "icon-size": 1, + "text-anchor": "top", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-optional": true, + "text-padding": 2, + "text-size": 12 + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "label_other", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 8, + "filter": [ + "match", + ["get", "class"], + ["city", "continent", "country", "state", "town", "village"], + false, + true + ], + "layout": { + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.1, + "text-max-width": 9, + "text-size": ["interpolate", ["linear"], ["zoom"], 8, 9, 12, 10], + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#333", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_village", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 9, + "filter": ["==", ["get", "class"], "village"], + "layout": { + "icon-allow-overlap": true, + "icon-image": ["step", ["zoom"], "circle_11_black", 10, ""], + "icon-optional": false, + "icon-size": 0.2, + "text-anchor": "bottom", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-max-width": 8, + "text-size": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 7, + 10, + 11, + 12 + ] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_town", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 6, + "filter": ["==", ["get", "class"], "town"], + "layout": { + "icon-allow-overlap": true, + "icon-image": ["step", ["zoom"], "circle_11_black", 10, ""], + "icon-optional": false, + "icon-size": 0.2, + "text-anchor": "bottom", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-max-width": 8, + "text-size": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 7, + 12, + 11, + 14 + ] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_state", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 5, + "maxzoom": 8, + "filter": ["==", ["get", "class"], "state"], + "layout": { + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.2, + "text-max-width": 9, + "text-size": ["interpolate", ["linear"], ["zoom"], 5, 10, 8, 14], + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#333", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_city", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 3, + "filter": [ + "all", + ["==", ["get", "class"], "city"], + ["!=", ["get", "capital"], 2] + ], + "layout": { + "icon-allow-overlap": true, + "icon-image": ["step", ["zoom"], "circle_11_black", 9, ""], + "icon-optional": false, + "icon-size": 0.4, + "text-anchor": "bottom", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Regular"], + "text-max-width": 8, + "text-offset": [0, -0.1], + "text-size": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 4, + 11, + 7, + 13, + 11, + 18 + ] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_city_capital", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 3, + "filter": [ + "all", + ["==", ["get", "class"], "city"], + ["==", ["get", "capital"], 2] + ], + "layout": { + "icon-allow-overlap": true, + "icon-image": ["step", ["zoom"], "circle_11_black", 9, ""], + "icon-optional": false, + "icon-size": 0.5, + "text-anchor": "bottom", + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Bold"], + "text-max-width": 8, + "text-offset": [0, -0.2], + "text-size": [ + "interpolate", + ["exponential", 1.2], + ["zoom"], + 4, + 12, + 7, + 14, + 11, + 20 + ] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_country_3", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "minzoom": 2, + "maxzoom": 9, + "filter": [ + "all", + ["==", ["get", "class"], "country"], + [">=", ["get", "rank"], 3] + ], + "layout": { + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Bold"], + "text-max-width": 6.25, + "text-size": ["interpolate", ["linear"], ["zoom"], 3, 9, 7, 17] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_country_2", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 9, + "filter": [ + "all", + ["==", ["get", "class"], "country"], + ["==", ["get", "rank"], 2] + ], + "layout": { + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Bold"], + "text-max-width": 6.25, + "text-size": ["interpolate", ["linear"], ["zoom"], 2, 9, 5, 17] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "label_country_1", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 9, + "filter": [ + "all", + ["==", ["get", "class"], "country"], + ["==", ["get", "rank"], 1] + ], + "layout": { + "text-field": [ + "case", + ["has", "name:nonlatin"], + ["concat", ["get", "name:latin"], "\n", ["get", "name:nonlatin"]], + ["coalesce", ["get", "name_en"], ["get", "name"]] + ], + "text-font": ["Noto Sans Bold"], + "text-max-width": 6.25, + "text-size": ["interpolate", ["linear"], ["zoom"], 1, 9, 4, 17] + }, + "paint": { + "text-color": "#000", + "text-halo-blur": 1, + "text-halo-color": "#fff", + "text-halo-width": 1 + } + } + ], + "id": "94huyrm" +} \ No newline at end of file diff --git a/app/src/main/assets/openstreetmap_raster.json b/app/src/main/assets/openstreetmap_raster.json new file mode 100644 index 0000000..04ebdce --- /dev/null +++ b/app/src/main/assets/openstreetmap_raster.json @@ -0,0 +1,21 @@ +{ + "version": 8, + "sources": { + "osm": { + "type": "raster", + "tiles": ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png","https://b.tile.openstreetmap.org/{z}/{x}/{y}.png"], + "tileSize": 256, + "attribution": "© OpenStreetMap Contributors", + "maxzoom": 19 + } + }, + "glyphs": "https://tiles.versatiles.org/assets/glyphs/{fontstack}/{range}.pbf", + + "layers": [ + { + "id": "osm-raster", + "type": "raster", + "source": "osm" + } + ] +} diff --git a/app/src/main/assets/versatiles_colorful_light.json b/app/src/main/assets/versatiles_colorful_light.json new file mode 100644 index 0000000..36f38f2 --- /dev/null +++ b/app/src/main/assets/versatiles_colorful_light.json @@ -0,0 +1,5956 @@ +{ + "version": 8, + "name": "versatiles-colorful", + "metadata": { + "license": "https://creativecommons.org/publicdomain/zero/1.0/", + "maputnik:renderer": "mlgljs" + }, + "sources": { + "versatiles-shortbread": { + "attribution": "© OpenStreetMap contributors", + "tiles": ["https://tiles.versatiles.org/tiles/osm/{z}/{x}/{y}"], + "type": "vector", + "scheme": "xyz", + "bounds": [-180, -85.0511287798066, 180, 85.0511287798066], + "minzoom": 0, + "maxzoom": 14 + } + }, + "sprite": [ + { + "id": "basics", + "url": "https://tiles.versatiles.org/assets/sprites/basics/sprites" + } + ], + "glyphs": "https://tiles.versatiles.org/assets/glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": {"background-color": "rgb(249,244,238)"} + }, + { + "id": "water-ocean", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "ocean", + "paint": {"fill-color": "rgb(190,221,243)"} + }, + { + "id": "land-glacier", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "water_polygons", + "filter": ["all", ["==", "kind", "glacier"]], + "paint": {"fill-color": "rgb(255,255,255)"} + }, + { + "id": "land-commercial", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": ["all", ["in", "kind", "commercial", "retail"]], + "paint": { + "fill-color": "rgba(247,222,237,0.251)", + "fill-opacity": {"stops": [[10, 0], [11, 1]]} + } + }, + { + "id": "land-industrial", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": ["all", ["in", "kind", "industrial", "quarry", "railway"]], + "paint": { + "fill-color": "rgba(255,244,194,0.333)", + "fill-opacity": {"stops": [[10, 0], [11, 1]]} + } + }, + { + "id": "land-residential", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": ["all", ["in", "kind", "garages", "residential"]], + "paint": { + "fill-color": "rgba(234,230,225,0.2)", + "fill-opacity": {"stops": [[10, 0], [11, 1]]} + } + }, + { + "id": "land-agriculture", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": [ + "all", + [ + "in", + "kind", + "brownfield", + "farmland", + "farmyard", + "greenfield", + "greenhouse_horticulture", + "orchard", + "plant_nursery", + "vineyard" + ] + ], + "paint": { + "fill-color": "rgb(240,231,209)", + "fill-opacity": {"stops": [[10, 0], [11, 1]]} + } + }, + { + "id": "land-waste", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": ["all", ["in", "kind", "landfill"]], + "paint": { + "fill-color": "rgb(219,214,189)", + "fill-opacity": {"stops": [[10, 0], [11, 1]]} + } + }, + { + "id": "land-park", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": [ + "all", + ["in", "kind", "park", "village_green", "recreation_ground"] + ], + "paint": { + "fill-color": "rgb(217,217,165)", + "fill-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "land-garden", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": ["all", ["in", "kind", "allotments", "garden"]], + "paint": { + "fill-color": "rgb(217,217,165)", + "fill-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "land-burial", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": ["all", ["in", "kind", "cemetery", "grave_yard"]], + "paint": { + "fill-color": "rgb(221,219,202)", + "fill-opacity": {"stops": [[13, 0], [14, 1]]} + } + }, + { + "id": "land-leisure", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": [ + "all", + ["in", "kind", "miniature_golf", "playground", "golf_course"] + ], + "paint": {"fill-color": "rgb(231,237,222)"} + }, + { + "id": "land-rock", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": ["all", ["in", "kind", "bare_rock", "scree", "shingle"]], + "paint": {"fill-color": "rgb(224,228,229)"} + }, + { + "id": "land-forest", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": ["all", ["in", "kind", "forest"]], + "paint": { + "fill-color": "rgb(102,170,68)", + "fill-opacity": {"stops": [[7, 0], [8, 0.1]]} + } + }, + { + "id": "land-grass", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": [ + "all", + ["in", "kind", "grass", "grassland", "meadow", "wet_meadow"] + ], + "paint": { + "fill-color": "rgb(216,232,200)", + "fill-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "land-vegetation", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": ["all", ["in", "kind", "heath", "scrub"]], + "paint": { + "fill-color": "rgb(217,217,165)", + "fill-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "land-sand", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": ["all", ["in", "kind", "beach", "sand"]], + "paint": {"fill-color": "rgb(250,250,237)"} + }, + { + "id": "land-wetland", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "land", + "filter": ["all", ["in", "kind", "bog", "marsh", "string_bog", "swamp"]], + "paint": {"fill-color": "rgb(211,230,219)"} + }, + { + "id": "water-river", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "water_lines", + "filter": [ + "all", + ["in", "kind", "river"], + ["!=", "tunnel", true], + ["!=", "bridge", true] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgb(190,221,243)", + "line-width": { + "stops": [[9, 0], [10, 3], [15, 5], [17, 9], [18, 20], [20, 60]] + } + } + }, + { + "id": "water-canal", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "water_lines", + "filter": [ + "all", + ["in", "kind", "canal"], + ["!=", "tunnel", true], + ["!=", "bridge", true] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgb(190,221,243)", + "line-width": { + "stops": [[9, 0], [10, 2], [15, 4], [17, 8], [18, 17], [20, 50]] + } + } + }, + { + "id": "water-stream", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "water_lines", + "filter": [ + "all", + ["in", "kind", "stream"], + ["!=", "tunnel", true], + ["!=", "bridge", true] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgb(190,221,243)", + "line-width": { + "stops": [[13, 0], [14, 1], [15, 2], [17, 6], [18, 12], [20, 30]] + } + } + }, + { + "id": "water-ditch", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "water_lines", + "filter": [ + "all", + ["in", "kind", "ditch"], + ["!=", "tunnel", true], + ["!=", "bridge", true] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgb(190,221,243)", + "line-width": {"stops": [[14, 0], [15, 1], [17, 4], [18, 8], [20, 20]]} + } + }, + { + "id": "water-area", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "water_polygons", + "filter": ["==", "kind", "water"], + "paint": { + "fill-color": "rgb(190,221,243)", + "fill-opacity": {"stops": [[4, 0], [6, 1]]} + } + }, + { + "id": "water-area-river", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "water_polygons", + "filter": ["==", "kind", "river"], + "paint": { + "fill-color": "rgb(190,221,243)", + "fill-opacity": {"stops": [[4, 0], [6, 1]]} + } + }, + { + "id": "water-area-small", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "water_polygons", + "filter": ["in", "kind", "reservoir", "basin", "dock"], + "paint": { + "fill-color": "rgb(190,221,243)", + "fill-opacity": {"stops": [[4, 0], [6, 1]]} + } + }, + { + "id": "water-dam-area", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "dam_polygons", + "filter": ["==", "kind", "dam"], + "paint": { + "fill-color": "rgb(249,244,238)", + "fill-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "water-dam", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "dam_lines", + "filter": ["==", "kind", "dam"], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": {"line-color": "rgb(190,221,243)"} + }, + { + "id": "water-pier-area", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "pier_polygons", + "filter": ["in", "kind", "pier", "breakwater", "groyne"], + "paint": { + "fill-color": "rgb(249,244,238)", + "fill-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "water-pier", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "pier_lines", + "filter": ["in", "kind", "pier", "breakwater", "groyne"], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": {"line-color": "rgb(249,244,238)"} + }, + { + "id": "site-dangerarea", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "sites", + "filter": ["in", "kind", "danger_area"], + "paint": { + "fill-color": "rgb(255,0,0)", + "fill-outline-color": "rgb(255,0,0)", + "fill-opacity": 0.3, + "fill-pattern": "basics:pattern-warning" + } + }, + { + "id": "site-university", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "sites", + "filter": ["in", "kind", "university"], + "paint": {"fill-color": "rgb(255,255,128)", "fill-opacity": 0.1} + }, + { + "id": "site-college", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "sites", + "filter": ["in", "kind", "college"], + "paint": {"fill-color": "rgb(255,255,128)", "fill-opacity": 0.1} + }, + { + "id": "site-school", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "sites", + "filter": ["in", "kind", "school"], + "paint": {"fill-color": "rgb(255,255,128)", "fill-opacity": 0.1} + }, + { + "id": "site-hospital", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "sites", + "filter": ["in", "kind", "hospital"], + "paint": {"fill-color": "rgb(255,102,102)", "fill-opacity": 0.1} + }, + { + "id": "site-prison", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "sites", + "filter": ["in", "kind", "prison"], + "paint": { + "fill-color": "rgb(253,242,252)", + "fill-pattern": "basics:pattern-striped", + "fill-opacity": 0.1 + } + }, + { + "id": "site-parking", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "sites", + "filter": ["in", "kind", "parking"], + "paint": {"fill-color": "rgb(235,232,230)"} + }, + { + "id": "site-bicycleparking", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "sites", + "filter": ["in", "kind", "bicycle_parking"], + "paint": {"fill-color": "rgb(235,232,230)"} + }, + { + "id": "site-construction", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "sites", + "filter": ["in", "kind", "construction"], + "paint": { + "fill-color": "rgb(169,169,169)", + "fill-pattern": "basics:pattern-hatched_thin", + "fill-opacity": 0.1 + } + }, + { + "id": "airport-area", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "street_polygons", + "filter": ["in", "kind", "runway", "taxiway"], + "paint": {"fill-color": "rgb(255,255,255)", "fill-opacity": 0.5} + }, + { + "id": "airport-taxiway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["==", "kind", "taxiway"], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "rgb(207,205,202)", + "line-width": { + "stops": [[13, 0], [14, 2], [15, 10], [16, 14], [18, 20], [20, 40]] + } + } + }, + { + "id": "airport-runway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["==", "kind", "runway"], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "rgb(207,205,202)", + "line-width": { + "stops": [ + [11, 0], + [12, 6], + [13, 9], + [14, 16], + [15, 24], + [16, 40], + [17, 100], + [18, 160], + [20, 300] + ] + } + } + }, + { + "id": "airport-taxiway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["==", "kind", "taxiway"], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[13, 0], [14, 1], [15, 8], [16, 12], [18, 18], [20, 36]] + }, + "line-opacity": {"stops": [[13, 0], [14, 1]]} + } + }, + { + "id": "airport-runway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["==", "kind", "runway"], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [ + [11, 0], + [12, 5], + [13, 8], + [14, 14], + [15, 22], + [16, 38], + [17, 98], + [18, 158], + [20, 298] + ] + }, + "line-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "building:outline", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "buildings", + "paint": { + "fill-color": "rgb(223,219,215)", + "fill-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "building", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "buildings", + "paint": { + "fill-color": "rgb(242,234,226)", + "fill-opacity": {"stops": [[14, 0], [15, 1]]}, + "fill-translate": [-2, -2] + } + }, + { + "id": "tunnel-street-pedestrian-zone", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "street_polygons", + "filter": ["all", ["==", "tunnel", true], ["==", "kind", "pedestrian"]], + "paint": { + "fill-color": "rgb(247,247,247)", + "fill-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-way-footway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "tunnel", true], ["in", "kind", "footway"]], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 5], [18, 7], [19, 12], [20, 22]] + }, + "line-color": "hsl(288,13%,86%)" + } + }, + { + "id": "tunnel-way-steps:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "tunnel", true], ["in", "kind", "steps"]], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 5], [18, 7], [19, 12], [20, 22]] + }, + "line-color": "hsl(288,13%,86%)" + } + }, + { + "id": "tunnel-way-path:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "tunnel", true], ["in", "kind", "path"]], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 5], [18, 7], [19, 12], [20, 22]] + }, + "line-color": "hsl(288,13%,86%)" + } + }, + { + "id": "tunnel-way-cycleway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "tunnel", true], ["in", "kind", "cycleway"]], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 5], [18, 7], [19, 12], [20, 22]] + }, + "line-color": "hsl(203,11%,87%)" + } + }, + { + "id": "tunnel-street-track:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "track"], ["==", "tunnel", true]], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(222,222,222)", + "line-width": { + "stops": [[14, 2], [16, 4], [18, 18], [19, 48], [20, 96]] + }, + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "tunnel-street-pedestrian:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "pedestrian"], ["==", "tunnel", true]], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(222,222,222)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-service:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "service"], ["==", "tunnel", true]], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(221,220,218)", + "line-width": { + "stops": [[14, 1], [16, 3], [18, 12], [19, 32], [20, 48]] + }, + "line-opacity": {"stops": [[15, 0], [16, 1]]} + } + }, + { + "id": "tunnel-street-livingstreet:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "living_street"], + ["==", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(222,222,222)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-residential:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "residential"], ["==", "tunnel", true]], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(222,222,222)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-unclassified:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "unclassified"], ["==", "tunnel", true]], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(222,222,222)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-tertiary-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "tertiary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(222,222,222)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-secondary-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "secondary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(234,176,126)", + "line-dasharray": [1, 0.3], + "line-width": {"stops": [[12, 2], [14, 3], [16, 7], [18, 14], [20, 40]]} + } + }, + { + "id": "tunnel-street-primary-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "primary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(234,176,126)", + "line-dasharray": [1, 0.3], + "line-width": {"stops": [[12, 2], [14, 3], [16, 7], [18, 14], [20, 40]]} + } + }, + { + "id": "tunnel-street-trunk-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "trunk"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(234,176,126)", + "line-dasharray": [1, 0.3], + "line-width": {"stops": [[12, 2], [14, 3], [16, 7], [18, 14], [20, 40]]} + } + }, + { + "id": "tunnel-street-motorway-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 12, + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "motorway"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(234,176,126)", + "line-dasharray": [1, 0.3], + "line-width": {"stops": [[12, 2], [14, 3], [16, 7], [18, 14], [20, 40]]} + } + }, + { + "id": "tunnel-street-tertiary:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "tertiary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(222,222,222)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-secondary:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "secondary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(234,176,126)", + "line-dasharray": [1, 0.3], + "line-width": { + "stops": [[11, 2], [14, 5], [16, 8], [18, 30], [19, 68], [20, 138]] + }, + "line-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "tunnel-street-primary:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "primary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(234,176,126)", + "line-dasharray": [1, 0.3], + "line-width": { + "stops": [ + [8, 0], + [9, 1], + [10, 4], + [14, 6], + [16, 12], + [18, 36], + [19, 74], + [20, 144] + ] + } + } + }, + { + "id": "tunnel-street-trunk:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "trunk"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(234,176,126)", + "line-dasharray": [1, 0.3], + "line-width": { + "stops": [ + [7, 0], + [8, 2], + [10, 4], + [14, 6], + [16, 12], + [18, 36], + [19, 74], + [20, 144] + ] + } + } + }, + { + "id": "tunnel-street-motorway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "motorway"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(234,176,126)", + "line-dasharray": [1, 0.3], + "line-width": { + "stops": [ + [5, 0], + [6, 2], + [10, 5], + [14, 5], + [16, 14], + [18, 38], + [19, 84], + [20, 168] + ] + } + } + }, + { + "id": "tunnel-way-footway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "tunnel", true], ["in", "kind", "footway"]], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 4], [18, 6], [19, 10], [20, 20]] + }, + "line-color": "hsl(288,33%,94%)", + "line-dasharray": [1, 0.2] + } + }, + { + "id": "tunnel-way-steps", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "tunnel", true], ["in", "kind", "steps"]], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 4], [18, 6], [19, 10], [20, 20]] + }, + "line-color": "hsl(288,33%,94%)", + "line-dasharray": [1, 0.2] + } + }, + { + "id": "tunnel-way-path", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "tunnel", true], ["in", "kind", "path"]], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 4], [18, 6], [19, 10], [20, 20]] + }, + "line-color": "hsl(288,33%,94%)", + "line-dasharray": [1, 0.2] + } + }, + { + "id": "tunnel-way-cycleway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "tunnel", true], ["in", "kind", "cycleway"]], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 4], [18, 6], [19, 10], [20, 20]] + }, + "line-color": "hsl(203,30%,95%)", + "line-dasharray": [1, 0.2] + } + }, + { + "id": "tunnel-street-track", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "track"], ["==", "tunnel", true]], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(247,247,247)", + "line-width": { + "stops": [[14, 1], [16, 3], [18, 16], [19, 44], [20, 88]] + }, + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "tunnel-street-pedestrian", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "pedestrian"], ["==", "tunnel", true]], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(247,247,247)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-service", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "service"], ["==", "tunnel", true]], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(247,247,247)", + "line-width": { + "stops": [[14, 1], [16, 2], [18, 10], [19, 28], [20, 40]] + }, + "line-opacity": {"stops": [[15, 0], [16, 1]]} + } + }, + { + "id": "tunnel-street-livingstreet", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "living_street"], + ["==", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(247,247,247)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-residential", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "residential"], ["==", "tunnel", true]], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(247,247,247)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-unclassified", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "unclassified"], ["==", "tunnel", true]], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(247,247,247)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-track-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "track"], + ["==", "bicycle", "designated"], + ["==", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": {"line-color": "rgb(247,247,247)"} + }, + { + "id": "tunnel-street-pedestrian-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "pedestrian"], + ["==", "bicycle", "designated"], + ["==", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(239,249,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-service-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "service"], + ["==", "bicycle", "designated"], + ["==", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": {"line-color": "rgb(247,247,247)"} + }, + { + "id": "tunnel-street-livingstreet-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "living_street"], + ["==", "bicycle", "designated"], + ["==", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(239,249,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-residential-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "residential"], + ["==", "bicycle", "designated"], + ["==", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(239,249,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-unclassified-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "unclassified"], + ["==", "bicycle", "designated"], + ["==", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(239,249,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-tertiary-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "tertiary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(247,247,247)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-secondary-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "secondary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,240,179)", + "line-width": {"stops": [[12, 1], [14, 2], [16, 5], [18, 12], [20, 38]]} + } + }, + { + "id": "tunnel-street-primary-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "primary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,240,179)", + "line-width": {"stops": [[12, 1], [14, 2], [16, 5], [18, 12], [20, 38]]} + } + }, + { + "id": "tunnel-street-trunk-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "trunk"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,240,179)", + "line-width": {"stops": [[12, 1], [14, 2], [16, 5], [18, 12], [20, 38]]} + } + }, + { + "id": "tunnel-street-motorway-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 12, + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "motorway"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,209,148)", + "line-width": {"stops": [[12, 1], [14, 2], [16, 5], [18, 12], [20, 38]]} + } + }, + { + "id": "tunnel-street-tertiary", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "tertiary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(247,247,247)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "tunnel-street-secondary", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "secondary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,240,179)", + "line-width": { + "stops": [[11, 1], [14, 4], [16, 6], [18, 28], [19, 64], [20, 130]] + }, + "line-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "tunnel-street-primary", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "primary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,240,179)", + "line-width": { + "stops": [ + [8, 0], + [9, 2], + [10, 3], + [14, 5], + [16, 10], + [18, 34], + [19, 70], + [20, 140] + ] + }, + "line-opacity": {"stops": [[8, 0], [9, 1]]} + } + }, + { + "id": "tunnel-street-trunk", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "trunk"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,240,179)", + "line-width": { + "stops": [ + [7, 0], + [8, 1], + [10, 3], + [14, 5], + [16, 10], + [18, 34], + [19, 70], + [20, 140] + ] + }, + "line-opacity": {"stops": [[7, 0], [8, 1]]} + } + }, + { + "id": "tunnel-street-motorway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "tunnel", true], + ["in", "kind", "motorway"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,209,148)", + "line-width": { + "stops": [ + [5, 0], + [6, 1], + [10, 4], + [14, 4], + [16, 12], + [18, 36], + [19, 80], + [20, 160] + ] + }, + "line-opacity": {"stops": [[5, 0], [6, 1]]} + } + }, + { + "id": "tunnel-transport-tram:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "tram"], + ["!has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[15, 0], [16, 5], [18, 7], [20, 20]]}, + "line-dasharray": [0.1, 0.5] + } + }, + { + "id": "tunnel-transport-narrowgauge:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "narrow_gauge"], + ["!has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[15, 0], [16, 5], [18, 7], [20, 20]]}, + "line-dasharray": [0.1, 0.5] + } + }, + { + "id": "tunnel-transport-subway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["in", "kind", "subway"], + ["!has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-color": "rgb(166,184,199)", + "line-width": { + "stops": [ + [11, 0], + [12, 1], + [15, 3], + [16, 3], + [18, 6], + [19, 8], + [20, 10] + ] + }, + "line-opacity": {"stops": [[11, 0], [12, 0.5]]} + } + }, + { + "id": "tunnel-transport-lightrail:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 8, + "filter": [ + "all", + ["in", "kind", "light_rail"], + ["!has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[8, 1], [13, 1], [15, 1], [20, 14]]}, + "line-opacity": {"stops": [[11, 0], [12, 0.5]]} + } + }, + { + "id": "tunnel-transport-lightrail-service:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "light_rail"], + ["has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[14, 0], [15, 1], [16, 1], [20, 14]]} + } + }, + { + "id": "tunnel-transport-rail:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 8, + "filter": [ + "all", + ["in", "kind", "rail"], + ["!has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[8, 1], [13, 1], [15, 1], [20, 14]]}, + "line-opacity": {"stops": [[8, 0], [9, 0.3]]} + } + }, + { + "id": "tunnel-transport-rail-service:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "rail"], + ["has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[14, 0], [15, 1], [16, 1], [20, 14]]} + } + }, + { + "id": "tunnel-transport-monorail:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["in", "kind", "monorail"], ["==", "tunnel", true]], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[15, 0], [16, 5], [18, 7], [20, 20]]}, + "line-dasharray": [0.1, 0.5] + } + }, + { + "id": "tunnel-transport-funicular:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["in", "kind", "funicular"], ["==", "tunnel", true]], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[15, 0], [16, 5], [18, 7], [20, 20]]}, + "line-dasharray": [0.1, 0.5] + } + }, + { + "id": "tunnel-transport-tram", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["in", "kind", "tram"], + ["!has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-width": {"stops": [[13, 0], [16, 1], [17, 2], [18, 3], [20, 5]]}, + "line-color": "rgb(177,187,196)" + } + }, + { + "id": "tunnel-transport-narrowgauge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["in", "kind", "narrow_gauge"], + ["!has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-width": {"stops": [[13, 0], [16, 1], [17, 2], [18, 3], [20, 5]]}, + "line-color": "rgb(177,187,196)" + } + }, + { + "id": "tunnel-transport-subway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["in", "kind", "subway"], + ["!has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-color": "rgb(188,202,213)", + "line-width": { + "stops": [ + [11, 0], + [12, 1], + [15, 2], + [16, 2], + [18, 5], + [19, 6], + [20, 8] + ] + }, + "line-dasharray": [2, 2], + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "tunnel-transport-lightrail", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "light_rail"], + ["!has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-color": "rgb(197,204,211)", + "line-width": {"stops": [[14, 0], [15, 1], [20, 10]]}, + "line-dasharray": [2, 2], + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "tunnel-transport-lightrail-service", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "light_rail"], + ["has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-color": "rgb(197,204,211)", + "line-width": {"stops": [[15, 0], [16, 1], [20, 10]]}, + "line-dasharray": [2, 2] + } + }, + { + "id": "tunnel-transport-rail", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "rail"], + ["!has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-color": "rgb(197,204,211)", + "line-width": {"stops": [[14, 0], [15, 1], [20, 10]]}, + "line-dasharray": [2, 2], + "line-opacity": {"stops": [[14, 0], [15, 0.3]]} + } + }, + { + "id": "tunnel-transport-rail-service", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "rail"], + ["has", "service"], + ["==", "tunnel", true] + ], + "paint": { + "line-color": "rgb(197,204,211)", + "line-width": {"stops": [[15, 0], [16, 1], [20, 10]]}, + "line-dasharray": [2, 2] + } + }, + { + "id": "tunnel-transport-monorail", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": ["all", ["in", "kind", "monorail"], ["==", "tunnel", true]], + "paint": { + "line-width": {"stops": [[13, 0], [16, 1], [17, 2], [18, 3], [20, 5]]}, + "line-color": "rgb(177,187,196)" + } + }, + { + "id": "tunnel-transport-funicular", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": ["all", ["in", "kind", "funicular"], ["==", "tunnel", true]], + "paint": { + "line-width": {"stops": [[13, 0], [16, 1], [17, 2], [18, 3], [20, 5]]}, + "line-color": "rgb(177,187,196)" + } + }, + { + "id": "bridge", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "bridges", + "paint": { + "fill-color": "rgb(244,239,233)", + "fill-antialias": true, + "fill-opacity": 0.8 + } + }, + { + "id": "street-pedestrian-zone", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "street_polygons", + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["==", "kind", "pedestrian"] + ], + "paint": { + "fill-color": "rgba(251,235,255,0.25)", + "fill-opacity": {"stops": [[12, 0], [13, 1], [14, 0], [15, 1]]} + } + }, + { + "id": "way-footway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "footway"] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 5], [18, 7], [19, 12], [20, 22]] + }, + "line-color": "rgb(226,212,230)" + } + }, + { + "id": "way-steps:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "steps"] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 5], [18, 7], [19, 12], [20, 22]] + }, + "line-color": "rgb(226,212,230)" + } + }, + { + "id": "way-path:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "path"] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 5], [18, 7], [19, 12], [20, 22]] + }, + "line-color": "rgb(226,212,230)" + } + }, + { + "id": "way-cycleway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "cycleway"] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 5], [18, 7], [19, 12], [20, 22]] + }, + "line-color": "rgb(215,224,230)" + } + }, + { + "id": "street-track:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "track"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(207,205,202)", + "line-width": { + "stops": [[14, 2], [16, 4], [18, 18], [19, 48], [20, 96]] + }, + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "street-pedestrian:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "pedestrian"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(207,205,202)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-service:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(221,220,218)", + "line-width": { + "stops": [[14, 1], [16, 3], [18, 12], [19, 32], [20, 48]] + }, + "line-opacity": {"stops": [[15, 0], [16, 1]]} + } + }, + { + "id": "street-livingstreet:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "living_street"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(207,205,202)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-residential:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "residential"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(207,205,202)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-unclassified:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "unclassified"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(207,205,202)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-tertiary-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "tertiary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(207,205,202)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-secondary-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "secondary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": {"stops": [[12, 2], [14, 3], [16, 7], [18, 14], [20, 40]]} + } + }, + { + "id": "street-primary-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "primary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": {"stops": [[12, 2], [14, 3], [16, 7], [18, 14], [20, 40]]} + } + }, + { + "id": "street-trunk-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "trunk"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": {"stops": [[12, 2], [14, 3], [16, 7], [18, 14], [20, 40]]} + } + }, + { + "id": "street-motorway-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 12, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "motorway"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": {"stops": [[12, 2], [14, 3], [16, 7], [18, 14], [20, 40]]} + } + }, + { + "id": "street-tertiary:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "tertiary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(207,205,202)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-secondary:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "secondary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": { + "stops": [[11, 2], [14, 5], [16, 8], [18, 30], [19, 68], [20, 138]] + }, + "line-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "street-primary:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "primary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": { + "stops": [ + [8, 0], + [9, 1], + [10, 4], + [14, 6], + [16, 12], + [18, 36], + [19, 74], + [20, 144] + ] + } + } + }, + { + "id": "street-trunk:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "trunk"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": { + "stops": [ + [7, 0], + [8, 2], + [10, 4], + [14, 6], + [16, 12], + [18, 36], + [19, 74], + [20, 144] + ] + } + } + }, + { + "id": "street-motorway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "motorway"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": { + "stops": [ + [5, 0], + [6, 2], + [10, 5], + [14, 5], + [16, 14], + [18, 38], + [19, 84], + [20, 168] + ] + } + } + }, + { + "id": "way-footway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "footway"] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 4], [18, 6], [19, 10], [20, 20]] + }, + "line-color": "rgb(251,235,255)" + } + }, + { + "id": "way-steps", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "steps"] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 4], [18, 6], [19, 10], [20, 20]] + }, + "line-color": "rgb(251,235,255)" + } + }, + { + "id": "way-path", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "path"] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 4], [18, 6], [19, 10], [20, 20]] + }, + "line-color": "rgb(251,235,255)" + } + }, + { + "id": "way-cycleway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "cycleway"] + ], + "layout": {"line-cap": "round"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 4], [18, 6], [19, 10], [20, 20]] + }, + "line-color": "rgb(239,249,255)" + } + }, + { + "id": "street-track", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "track"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[14, 1], [16, 3], [18, 16], [19, 44], [20, 88]] + }, + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "street-pedestrian", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "pedestrian"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(251,235,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 0], [14, 1]]} + } + }, + { + "id": "street-service", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(247,247,247)", + "line-width": { + "stops": [[14, 1], [16, 2], [18, 10], [19, 28], [20, 40]] + }, + "line-opacity": {"stops": [[15, 0], [16, 1]]} + } + }, + { + "id": "street-livingstreet", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "living_street"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-residential", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "residential"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-unclassified", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "unclassified"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-track-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "track"], + ["==", "bicycle", "designated"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": {"line-color": "rgb(255,255,255)"} + }, + { + "id": "street-pedestrian-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "pedestrian"], + ["==", "bicycle", "designated"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(239,249,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-service-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "service"], + ["==", "bicycle", "designated"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": {"line-color": "rgb(255,255,255)"} + }, + { + "id": "street-livingstreet-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "living_street"], + ["==", "bicycle", "designated"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(239,249,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-residential-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "residential"], + ["==", "bicycle", "designated"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(239,249,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-unclassified-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "unclassified"], + ["==", "bicycle", "designated"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(239,249,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-tertiary-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "tertiary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-secondary-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "secondary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,238,170)", + "line-width": {"stops": [[12, 1], [14, 2], [16, 5], [18, 12], [20, 38]]} + } + }, + { + "id": "street-primary-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "primary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,238,170)", + "line-width": {"stops": [[12, 1], [14, 2], [16, 5], [18, 12], [20, 38]]} + } + }, + { + "id": "street-trunk-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "trunk"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,238,170)", + "line-width": {"stops": [[12, 1], [14, 2], [16, 5], [18, 12], [20, 38]]} + } + }, + { + "id": "street-motorway-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 12, + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "motorway"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,204,136)", + "line-width": {"stops": [[12, 1], [14, 2], [16, 5], [18, 12], [20, 38]]} + } + }, + { + "id": "street-tertiary", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "tertiary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "street-secondary", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "secondary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,238,170)", + "line-width": { + "stops": [[11, 1], [14, 4], [16, 6], [18, 28], [19, 64], [20, 130]] + }, + "line-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "street-primary", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "primary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,238,170)", + "line-width": { + "stops": [ + [8, 0], + [9, 2], + [10, 3], + [14, 5], + [16, 10], + [18, 34], + [19, 70], + [20, 140] + ] + }, + "line-opacity": {"stops": [[8, 0], [9, 1]]} + } + }, + { + "id": "street-trunk", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "trunk"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,238,170)", + "line-width": { + "stops": [ + [7, 0], + [8, 1], + [10, 3], + [14, 5], + [16, 10], + [18, 34], + [19, 70], + [20, 140] + ] + }, + "line-opacity": {"stops": [[7, 0], [8, 1]]} + } + }, + { + "id": "street-motorway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["!=", "bridge", true], + ["!=", "tunnel", true], + ["in", "kind", "motorway"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(255,204,136)", + "line-width": { + "stops": [ + [5, 0], + [6, 1], + [10, 4], + [14, 4], + [16, 12], + [18, 36], + [19, 80], + [20, 160] + ] + }, + "line-opacity": {"stops": [[5, 0], [6, 1]]} + } + }, + { + "id": "transport-tram:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "tram"], + ["!has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[15, 0], [16, 5], [18, 7], [20, 20]]}, + "line-dasharray": [0.1, 0.5] + } + }, + { + "id": "transport-narrowgauge:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "narrow_gauge"], + ["!has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[15, 0], [16, 5], [18, 7], [20, 20]]}, + "line-dasharray": [0.1, 0.5] + } + }, + { + "id": "transport-subway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["in", "kind", "subway"], + ["!has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(166,184,199)", + "line-width": { + "stops": [ + [11, 0], + [12, 1], + [15, 3], + [16, 3], + [18, 6], + [19, 8], + [20, 10] + ] + }, + "line-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "transport-lightrail:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 8, + "filter": [ + "all", + ["in", "kind", "light_rail"], + ["!has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[8, 1], [13, 1], [15, 1], [20, 14]]}, + "line-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "transport-lightrail-service:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "light_rail"], + ["has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[14, 0], [15, 1], [16, 1], [20, 14]]} + } + }, + { + "id": "transport-rail:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 8, + "filter": [ + "all", + ["in", "kind", "rail"], + ["!has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[8, 1], [13, 1], [15, 1], [20, 14]]}, + "line-opacity": {"stops": [[8, 0], [9, 1]]} + } + }, + { + "id": "transport-rail-service:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "rail"], + ["has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[14, 0], [15, 1], [16, 1], [20, 14]]} + } + }, + { + "id": "transport-monorail:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "monorail"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[15, 0], [16, 5], [18, 7], [20, 20]]}, + "line-dasharray": [0.1, 0.5] + } + }, + { + "id": "transport-funicular:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "funicular"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[15, 0], [16, 5], [18, 7], [20, 20]]}, + "line-dasharray": [0.1, 0.5] + } + }, + { + "id": "transport-tram", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["in", "kind", "tram"], + ["!has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-width": {"stops": [[13, 0], [16, 1], [17, 2], [18, 3], [20, 5]]}, + "line-color": "rgb(177,187,196)" + } + }, + { + "id": "transport-narrowgauge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["in", "kind", "narrow_gauge"], + ["!has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-width": {"stops": [[13, 0], [16, 1], [17, 2], [18, 3], [20, 5]]}, + "line-color": "rgb(177,187,196)" + } + }, + { + "id": "transport-subway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["in", "kind", "subway"], + ["!has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(188,202,213)", + "line-width": { + "stops": [ + [11, 0], + [12, 1], + [15, 2], + [16, 2], + [18, 5], + [19, 6], + [20, 8] + ] + }, + "line-dasharray": [2, 2], + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "transport-lightrail", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "light_rail"], + ["!has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(197,204,211)", + "line-width": {"stops": [[14, 0], [15, 1], [20, 10]]}, + "line-dasharray": [2, 2], + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "transport-lightrail-service", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "light_rail"], + ["has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(197,204,211)", + "line-width": {"stops": [[15, 0], [16, 1], [20, 10]]}, + "line-dasharray": [2, 2] + } + }, + { + "id": "transport-rail", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "rail"], + ["!has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(197,204,211)", + "line-width": {"stops": [[14, 0], [15, 1], [20, 10]]}, + "line-dasharray": [2, 2], + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "transport-rail-service", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "rail"], + ["has", "service"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-color": "rgb(197,204,211)", + "line-width": {"stops": [[15, 0], [16, 1], [20, 10]]}, + "line-dasharray": [2, 2] + } + }, + { + "id": "transport-monorail", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["in", "kind", "monorail"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-width": {"stops": [[13, 0], [16, 1], [17, 2], [18, 3], [20, 5]]}, + "line-color": "rgb(177,187,196)" + } + }, + { + "id": "transport-funicular", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["in", "kind", "funicular"], + ["!=", "bridge", true], + ["!=", "tunnel", true] + ], + "paint": { + "line-width": {"stops": [[13, 0], [16, 1], [17, 2], [18, 3], [20, 5]]}, + "line-color": "rgb(177,187,196)" + } + }, + { + "id": "transport-ferry", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "ferries", + "minzoom": 10, + "paint": { + "line-color": "rgb(171,199,219)", + "line-width": {"stops": [[10, 1], [13, 2], [14, 3], [16, 4], [17, 6]]}, + "line-opacity": {"stops": [[10, 0], [11, 1]]}, + "line-dasharray": [1, 1] + } + }, + { + "id": "bridge-way-footway:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "bridge", true], ["in", "kind", "footway"]], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": 0.5, + "line-width": { + "stops": [[15, 0], [16, 7], [18, 10], [19, 17], [20, 31]] + } + } + }, + { + "id": "bridge-way-steps:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "bridge", true], ["in", "kind", "steps"]], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": 0.5, + "line-width": { + "stops": [[15, 0], [16, 7], [18, 10], [19, 17], [20, 31]] + } + } + }, + { + "id": "bridge-way-path:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "bridge", true], ["in", "kind", "path"]], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": 0.5, + "line-width": { + "stops": [[15, 0], [16, 7], [18, 10], [19, 17], [20, 31]] + } + } + }, + { + "id": "bridge-way-cycleway:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "bridge", true], ["in", "kind", "cycleway"]], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": 0.5, + "line-width": { + "stops": [[15, 0], [16, 7], [18, 10], [19, 17], [20, 31]] + } + } + }, + { + "id": "bridge-street-track:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "track"], ["==", "bridge", true]], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": {"stops": [[14, 0], [15, 1]]}, + "line-width": { + "stops": [[14, 3], [16, 6], [18, 25], [19, 67], [20, 134]] + } + } + }, + { + "id": "bridge-street-pedestrian:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "pedestrian"], ["==", "bridge", true]], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": {"stops": [[12, 0], [13, 1]]}, + "line-width": { + "stops": [[12, 3], [14, 4], [16, 8], [18, 36], [19, 90], [20, 179]] + } + } + }, + { + "id": "bridge-street-service:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "service"], ["==", "bridge", true]], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": {"stops": [[14, 0], [15, 1]]}, + "line-width": { + "stops": [[14, 3], [16, 6], [18, 25], [19, 67], [20, 134]] + } + } + }, + { + "id": "bridge-street-livingstreet:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "living_street"], + ["==", "bridge", true] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": {"stops": [[12, 0], [13, 1]]}, + "line-width": { + "stops": [[12, 3], [14, 4], [16, 8], [18, 36], [19, 90], [20, 179]] + } + } + }, + { + "id": "bridge-street-residential:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "residential"], ["==", "bridge", true]], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": {"stops": [[12, 0], [13, 1]]}, + "line-width": { + "stops": [[12, 3], [14, 4], [16, 8], [18, 36], [19, 90], [20, 179]] + } + } + }, + { + "id": "bridge-street-unclassified:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "unclassified"], ["==", "bridge", true]], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": {"stops": [[12, 0], [13, 1]]}, + "line-width": { + "stops": [[12, 3], [14, 4], [16, 8], [18, 36], [19, 90], [20, 179]] + } + } + }, + { + "id": "bridge-street-tertiary-link:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "tertiary"], + ["==", "link", true] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": {"stops": [[12, 0], [13, 1]]}, + "line-width": { + "stops": [[12, 3], [14, 4], [16, 8], [18, 36], [19, 90], [20, 179]] + } + } + }, + { + "id": "bridge-street-secondary-link:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "secondary"], + ["==", "link", true] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": 0.5, + "line-width": { + "stops": [[12, 3], [14, 4], [16, 10], [18, 20], [20, 56]] + } + } + }, + { + "id": "bridge-street-primary-link:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "primary"], + ["==", "link", true] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": 0.5, + "line-width": { + "stops": [[12, 3], [14, 4], [16, 10], [18, 20], [20, 56]] + } + } + }, + { + "id": "bridge-street-trunk-link:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "trunk"], + ["==", "link", true] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": 0.5, + "line-width": { + "stops": [[12, 3], [14, 4], [16, 10], [18, 20], [20, 56]] + } + } + }, + { + "id": "bridge-street-motorway-link:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 12, + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "motorway"], + ["==", "link", true] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": 0.5, + "line-width": { + "stops": [[12, 3], [14, 4], [16, 10], [18, 20], [20, 56]] + } + } + }, + { + "id": "bridge-street-tertiary:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "tertiary"], + ["!=", "link", true] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": {"stops": [[12, 0], [13, 1]]}, + "line-width": { + "stops": [[12, 3], [14, 4], [16, 8], [18, 36], [19, 90], [20, 179]] + } + } + }, + { + "id": "bridge-street-secondary:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "secondary"], + ["!=", "link", true] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": {"stops": [[11, 0], [12, 1]]}, + "line-width": { + "stops": [[11, 3], [14, 7], [16, 11], [18, 42], [19, 95], [20, 193]] + } + } + }, + { + "id": "bridge-street-primary:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "primary"], + ["!=", "link", true] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": 0.5, + "line-width": { + "stops": [ + [8, 0], + [9, 1], + [10, 6], + [14, 8], + [16, 17], + [18, 50], + [19, 104], + [20, 202] + ] + } + } + }, + { + "id": "bridge-street-trunk:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "trunk"], + ["!=", "link", true] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": 0.5, + "line-width": { + "stops": [ + [7, 0], + [8, 3], + [10, 6], + [14, 8], + [16, 17], + [18, 50], + [19, 104], + [20, 202] + ] + } + } + }, + { + "id": "bridge-street-motorway:bridge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "motorway"], + ["!=", "link", true] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "rgb(244,239,233)", + "line-opacity": 0.5, + "line-width": { + "stops": [ + [5, 0], + [6, 3], + [10, 7], + [14, 7], + [16, 20], + [18, 53], + [19, 118], + [20, 235] + ] + } + } + }, + { + "id": "bridge-street-pedestrian-zone", + "type": "fill", + "source": "versatiles-shortbread", + "source-layer": "street_polygons", + "filter": ["all", ["==", "bridge", true], ["==", "kind", "pedestrian"]], + "paint": { + "fill-color": "rgb(255,255,255)", + "fill-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-way-footway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "bridge", true], ["in", "kind", "footway"]], + "layout": {"line-cap": "butt"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 5], [18, 7], [19, 12], [20, 22]] + }, + "line-color": "rgb(226,212,230)" + } + }, + { + "id": "bridge-way-steps:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "bridge", true], ["in", "kind", "steps"]], + "layout": {"line-cap": "butt"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 5], [18, 7], [19, 12], [20, 22]] + }, + "line-color": "rgb(226,212,230)" + } + }, + { + "id": "bridge-way-path:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "bridge", true], ["in", "kind", "path"]], + "layout": {"line-cap": "butt"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 5], [18, 7], [19, 12], [20, 22]] + }, + "line-color": "rgb(226,212,230)" + } + }, + { + "id": "bridge-way-cycleway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "bridge", true], ["in", "kind", "cycleway"]], + "layout": {"line-cap": "butt"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 5], [18, 7], [19, 12], [20, 22]] + }, + "line-color": "rgb(215,224,230)" + } + }, + { + "id": "bridge-street-track:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "track"], ["==", "bridge", true]], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(217,217,217)", + "line-width": { + "stops": [[14, 2], [16, 4], [18, 18], [19, 48], [20, 96]] + }, + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "bridge-street-pedestrian:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "pedestrian"], ["==", "bridge", true]], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(217,217,217)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-service:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "service"], ["==", "bridge", true]], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(221,220,218)", + "line-width": { + "stops": [[14, 1], [16, 3], [18, 12], [19, 32], [20, 48]] + }, + "line-opacity": {"stops": [[15, 0], [16, 1]]} + } + }, + { + "id": "bridge-street-livingstreet:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "living_street"], + ["==", "bridge", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(217,217,217)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-residential:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "residential"], ["==", "bridge", true]], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(217,217,217)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-unclassified:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "unclassified"], ["==", "bridge", true]], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(217,217,217)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-tertiary-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "tertiary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(217,217,217)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-secondary-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "secondary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": {"stops": [[12, 2], [14, 3], [16, 7], [18, 14], [20, 40]]} + } + }, + { + "id": "bridge-street-primary-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "primary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": {"stops": [[12, 2], [14, 3], [16, 7], [18, 14], [20, 40]]} + } + }, + { + "id": "bridge-street-trunk-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "trunk"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": {"stops": [[12, 2], [14, 3], [16, 7], [18, 14], [20, 40]]} + } + }, + { + "id": "bridge-street-motorway-link:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 12, + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "motorway"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": {"stops": [[12, 2], [14, 3], [16, 7], [18, 14], [20, 40]]} + } + }, + { + "id": "bridge-street-tertiary:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "tertiary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(217,217,217)", + "line-width": { + "stops": [[12, 2], [14, 3], [16, 6], [18, 26], [19, 64], [20, 128]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-secondary:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "secondary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": { + "stops": [[11, 2], [14, 5], [16, 8], [18, 30], [19, 68], [20, 138]] + }, + "line-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "bridge-street-primary:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "primary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": { + "stops": [ + [8, 0], + [9, 1], + [10, 4], + [14, 6], + [16, 12], + [18, 36], + [19, 74], + [20, 144] + ] + } + } + }, + { + "id": "bridge-street-trunk:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "trunk"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": { + "stops": [ + [7, 0], + [8, 2], + [10, 4], + [14, 6], + [16, 12], + [18, 36], + [19, 74], + [20, 144] + ] + } + } + }, + { + "id": "bridge-street-motorway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "motorway"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(233,172,119)", + "line-width": { + "stops": [ + [5, 0], + [6, 2], + [10, 5], + [14, 5], + [16, 14], + [18, 38], + [19, 84], + [20, 168] + ] + } + } + }, + { + "id": "bridge-way-footway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "bridge", true], ["in", "kind", "footway"]], + "layout": {"line-cap": "butt"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 4], [18, 6], [19, 10], [20, 20]] + }, + "line-color": "rgb(251,235,255)" + } + }, + { + "id": "bridge-way-steps", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "bridge", true], ["in", "kind", "steps"]], + "layout": {"line-cap": "butt"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 4], [18, 6], [19, 10], [20, 20]] + }, + "line-color": "rgb(251,235,255)" + } + }, + { + "id": "bridge-way-path", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "bridge", true], ["in", "kind", "path"]], + "layout": {"line-cap": "butt"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 4], [18, 6], [19, 10], [20, 20]] + }, + "line-color": "rgb(251,235,255)" + } + }, + { + "id": "bridge-way-cycleway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["==", "bridge", true], ["in", "kind", "cycleway"]], + "layout": {"line-cap": "butt"}, + "paint": { + "line-width": { + "stops": [[15, 0], [16, 4], [18, 6], [19, 10], [20, 20]] + }, + "line-color": "rgb(239,249,255)" + } + }, + { + "id": "bridge-street-track", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "track"], ["==", "bridge", true]], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[14, 1], [16, 3], [18, 16], [19, 44], [20, 88]] + }, + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "bridge-street-pedestrian", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "pedestrian"], ["==", "bridge", true]], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-service", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "service"], ["==", "bridge", true]], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(247,247,247)", + "line-width": { + "stops": [[14, 1], [16, 2], [18, 10], [19, 28], [20, 40]] + }, + "line-opacity": {"stops": [[15, 0], [16, 1]]} + } + }, + { + "id": "bridge-street-livingstreet", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "living_street"], + ["==", "bridge", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-residential", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "residential"], ["==", "bridge", true]], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-unclassified", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": ["all", ["==", "kind", "unclassified"], ["==", "bridge", true]], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-track-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "track"], + ["==", "bicycle", "designated"], + ["==", "bridge", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": {"line-color": "rgb(255,255,255)"} + }, + { + "id": "bridge-street-pedestrian-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "pedestrian"], + ["==", "bicycle", "designated"], + ["==", "bridge", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(239,249,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-service-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "service"], + ["==", "bicycle", "designated"], + ["==", "bridge", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": {"line-color": "rgb(255,255,255)"} + }, + { + "id": "bridge-street-livingstreet-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "living_street"], + ["==", "bicycle", "designated"], + ["==", "bridge", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(239,249,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-residential-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "residential"], + ["==", "bicycle", "designated"], + ["==", "bridge", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(239,249,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-unclassified-bicycle", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "kind", "unclassified"], + ["==", "bicycle", "designated"], + ["==", "bridge", true] + ], + "layout": {"line-join": "round", "line-cap": "round"}, + "paint": { + "line-color": "rgb(239,249,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-tertiary-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "tertiary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-secondary-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "secondary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,238,170)", + "line-width": {"stops": [[12, 1], [14, 2], [16, 5], [18, 12], [20, 38]]} + } + }, + { + "id": "bridge-street-primary-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "primary"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,238,170)", + "line-width": {"stops": [[12, 1], [14, 2], [16, 5], [18, 12], [20, 38]]} + } + }, + { + "id": "bridge-street-trunk-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "trunk"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,238,170)", + "line-width": {"stops": [[12, 1], [14, 2], [16, 5], [18, 12], [20, 38]]} + } + }, + { + "id": "bridge-street-motorway-link", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 12, + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "motorway"], + ["==", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,204,136)", + "line-width": {"stops": [[12, 1], [14, 2], [16, 5], [18, 12], [20, 38]]} + } + }, + { + "id": "bridge-street-tertiary", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "tertiary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,255,255)", + "line-width": { + "stops": [[12, 1], [14, 2], [16, 5], [18, 24], [19, 60], [20, 120]] + }, + "line-opacity": {"stops": [[12, 0], [13, 1]]} + } + }, + { + "id": "bridge-street-secondary", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "secondary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,238,170)", + "line-width": { + "stops": [[11, 1], [14, 4], [16, 6], [18, 28], [19, 64], [20, 130]] + }, + "line-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "bridge-street-primary", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "primary"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,238,170)", + "line-width": { + "stops": [ + [8, 0], + [9, 2], + [10, 3], + [14, 5], + [16, 10], + [18, 34], + [19, 70], + [20, 140] + ] + }, + "line-opacity": {"stops": [[8, 0], [9, 1]]} + } + }, + { + "id": "bridge-street-trunk", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "trunk"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,238,170)", + "line-width": { + "stops": [ + [7, 0], + [8, 1], + [10, 3], + [14, 5], + [16, 10], + [18, 34], + [19, 70], + [20, 140] + ] + }, + "line-opacity": {"stops": [[7, 0], [8, 1]]} + } + }, + { + "id": "bridge-street-motorway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["==", "bridge", true], + ["in", "kind", "motorway"], + ["!=", "link", true] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "rgb(255,204,136)", + "line-width": { + "stops": [ + [5, 0], + [6, 1], + [10, 4], + [14, 4], + [16, 12], + [18, 36], + [19, 80], + [20, 160] + ] + }, + "line-opacity": {"stops": [[5, 0], [6, 1]]} + } + }, + { + "id": "bridge-transport-tram:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "tram"], + ["!has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[15, 0], [16, 5], [18, 7], [20, 20]]}, + "line-dasharray": [0.1, 0.5] + } + }, + { + "id": "bridge-transport-narrowgauge:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "narrow_gauge"], + ["!has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[15, 0], [16, 5], [18, 7], [20, 20]]}, + "line-dasharray": [0.1, 0.5] + } + }, + { + "id": "bridge-transport-subway:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["in", "kind", "subway"], + ["!has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-color": "rgb(166,184,199)", + "line-width": { + "stops": [ + [11, 0], + [12, 1], + [15, 3], + [16, 3], + [18, 6], + [19, 8], + [20, 10] + ] + }, + "line-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "bridge-transport-lightrail:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 8, + "filter": [ + "all", + ["in", "kind", "light_rail"], + ["!has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[8, 1], [13, 1], [15, 1], [20, 14]]}, + "line-opacity": {"stops": [[11, 0], [12, 1]]} + } + }, + { + "id": "bridge-transport-lightrail-service:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "light_rail"], + ["has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[14, 0], [15, 1], [16, 1], [20, 14]]} + } + }, + { + "id": "bridge-transport-rail:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 8, + "filter": [ + "all", + ["in", "kind", "rail"], + ["!has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[8, 1], [13, 1], [15, 1], [20, 14]]}, + "line-opacity": {"stops": [[8, 0], [9, 1]]} + } + }, + { + "id": "bridge-transport-rail-service:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "rail"], + ["has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[14, 0], [15, 1], [16, 1], [20, 14]]} + } + }, + { + "id": "bridge-transport-monorail:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["in", "kind", "monorail"], ["==", "bridge", true]], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[15, 0], [16, 5], [18, 7], [20, 20]]}, + "line-dasharray": [0.1, 0.5] + } + }, + { + "id": "bridge-transport-funicular:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": ["all", ["in", "kind", "funicular"], ["==", "bridge", true]], + "paint": { + "line-color": "rgb(177,187,196)", + "line-width": {"stops": [[15, 0], [16, 5], [18, 7], [20, 20]]}, + "line-dasharray": [0.1, 0.5] + } + }, + { + "id": "bridge-transport-tram", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["in", "kind", "tram"], + ["!has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-width": {"stops": [[13, 0], [16, 1], [17, 2], [18, 3], [20, 5]]}, + "line-color": "rgb(177,187,196)" + } + }, + { + "id": "bridge-transport-narrowgauge", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": [ + "all", + ["in", "kind", "narrow_gauge"], + ["!has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-width": {"stops": [[13, 0], [16, 1], [17, 2], [18, 3], [20, 5]]}, + "line-color": "rgb(177,187,196)" + } + }, + { + "id": "bridge-transport-subway", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "filter": [ + "all", + ["in", "kind", "subway"], + ["!has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-color": "rgb(188,202,213)", + "line-width": { + "stops": [ + [11, 0], + [12, 1], + [15, 2], + [16, 2], + [18, 5], + [19, 6], + [20, 8] + ] + }, + "line-dasharray": [2, 2], + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "bridge-transport-lightrail", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "light_rail"], + ["!has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-color": "rgb(197,204,211)", + "line-width": {"stops": [[14, 0], [15, 1], [20, 10]]}, + "line-dasharray": [2, 2], + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "bridge-transport-lightrail-service", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "light_rail"], + ["has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-color": "rgb(197,204,211)", + "line-width": {"stops": [[15, 0], [16, 1], [20, 10]]}, + "line-dasharray": [2, 2] + } + }, + { + "id": "bridge-transport-rail", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "rail"], + ["!has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-color": "rgb(197,204,211)", + "line-width": {"stops": [[14, 0], [15, 1], [20, 10]]}, + "line-dasharray": [2, 2], + "line-opacity": {"stops": [[14, 0], [15, 1]]} + } + }, + { + "id": "bridge-transport-rail-service", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 15, + "filter": [ + "all", + ["in", "kind", "rail"], + ["has", "service"], + ["==", "bridge", true] + ], + "paint": { + "line-color": "rgb(197,204,211)", + "line-width": {"stops": [[15, 0], [16, 1], [20, 10]]}, + "line-dasharray": [2, 2] + } + }, + { + "id": "bridge-transport-monorail", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": ["all", ["in", "kind", "monorail"], ["==", "bridge", true]], + "paint": { + "line-width": {"stops": [[13, 0], [16, 1], [17, 2], [18, 3], [20, 5]]}, + "line-color": "rgb(177,187,196)" + } + }, + { + "id": "bridge-transport-funicular", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 13, + "filter": ["all", ["in", "kind", "funicular"], ["==", "bridge", true]], + "paint": { + "line-width": {"stops": [[13, 0], [16, 1], [17, 2], [18, 3], [20, 5]]}, + "line-color": "rgb(177,187,196)" + } + }, + { + "id": "poi-amenity", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "pois", + "minzoom": 16, + "filter": ["to-boolean", ["get", "amenity"]], + "layout": { + "icon-size": {"stops": [[16, 0.5], [19, 0.5], [20, 1]]}, + "symbol-placement": "point", + "icon-optional": true, + "text-font": ["noto_sans_regular"], + "icon-image": [ + "match", + ["get", "amenity"], + "arts_centre", + "basics:icon-art_gallery", + "atm", + "basics:icon-atm", + "bank", + "basics:icon-bank", + "bar", + "basics:icon-bar", + "bench", + "basics:icon-bench", + "bicycle_rental", + "basics:icon-bicycle_share", + "biergarten", + "basics:icon-beergarden", + "cafe", + "basics:icon-cafe", + "car_rental", + "basics:icon-car_rental", + "car_sharing", + "basics:icon-car_rental", + "car_wash", + "basics:icon-car_wash", + "cinema", + "basics:icon-cinema", + "college", + "basics:icon-college", + "community_centre", + "basics:icon-community", + "dentist", + "basics:icon-dentist", + "doctors", + "basics:icon-doctor", + "dog_park", + "basics:icon-dog_park", + "drinking_water", + "basics:icon-drinking_water", + "embassy", + "basics:icon-embassy", + "fast_food", + "basics:icon-fast_food", + "fire_station", + "basics:icon-fire_station", + "fountain", + "basics:icon-fountain", + "grave_yard", + "basics:icon-cemetery", + "hospital", + "basics:icon-hospital", + "hunting_stand", + "basics:icon-huntingstand", + "library", + "basics:icon-library", + "marketplace", + "basics:icon-marketplace", + "nightclub", + "basics:icon-nightclub", + "nursing_home", + "basics:icon-nursinghome", + "pharmacy", + "basics:icon-pharmacy", + "place_of_worship", + "basics:icon-place_of_worship", + "playground", + "basics:icon-playground", + "police", + "basics:icon-police", + "post_box", + "basics:icon-postbox", + "post_office", + "basics:icon-post", + "prison", + "basics:icon-prison", + "pub", + "basics:icon-beer", + "recycling", + "basics:icon-recycling", + "restaurant", + "basics:icon-restaurant", + "school", + "basics:icon-school", + "shelter", + "basics:icon-shelter", + "telephone", + "basics:icon-telephone", + "theatre", + "basics:icon-theatre", + "toilets", + "basics:icon-toilet", + "townhall", + "basics:icon-town_hall", + "vending_machine", + "basics:icon-vendingmachine", + "veterinary", + "basics:icon-veterinary", + "waste_basket", + "basics:icon-waste_basket", + "" + ] + }, + "paint": { + "icon-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "text-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "icon-color": "rgb(85,85,85)", + "text-color": "rgb(85,85,85)" + } + }, + { + "id": "poi-leisure", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "pois", + "minzoom": 16, + "filter": ["to-boolean", ["get", "leisure"]], + "layout": { + "icon-size": {"stops": [[16, 0.5], [19, 0.5], [20, 1]]}, + "symbol-placement": "point", + "icon-optional": true, + "text-font": ["noto_sans_regular"], + "icon-image": [ + "match", + ["get", "leisure"], + "golf_course", + "basics:icon-golf", + "ice_rink", + "basics:icon-icerink", + "pitch", + "basics:icon-pitch", + "stadium", + "basics:icon-stadium", + "swimming_pool", + "basics:icon-swimming", + "water_park", + "basics:icon-waterpark", + "basics:icon-sports" + ] + }, + "paint": { + "icon-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "text-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "icon-color": "rgb(85,85,85)", + "text-color": "rgb(85,85,85)" + } + }, + { + "id": "poi-tourism", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "pois", + "minzoom": 16, + "filter": ["to-boolean", ["get", "tourism"]], + "layout": { + "icon-size": {"stops": [[16, 0.5], [19, 0.5], [20, 1]]}, + "symbol-placement": "point", + "icon-optional": true, + "text-font": ["noto_sans_regular"], + "icon-image": [ + "match", + ["get", "tourism"], + "chalet", + "basics:icon-chalet", + "information", + "basics:transport-information", + "picnic_site", + "basics:icon-picnic_site", + "viewpoint", + "basics:icon-viewpoint", + "zoo", + "basics:icon-zoo", + "" + ] + }, + "paint": { + "icon-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "text-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "icon-color": "rgb(85,85,85)", + "text-color": "rgb(85,85,85)" + } + }, + { + "id": "poi-shop", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "pois", + "minzoom": 16, + "filter": ["to-boolean", ["get", "shop"]], + "layout": { + "icon-size": {"stops": [[16, 0.5], [19, 0.5], [20, 1]]}, + "symbol-placement": "point", + "icon-optional": true, + "text-font": ["noto_sans_regular"], + "icon-image": [ + "match", + ["get", "shop"], + "alcohol", + "basics:icon-alcohol_shop", + "bakery", + "basics:icon-bakery", + "beauty", + "basics:icon-beauty", + "beverages", + "basics:icon-beverages", + "books", + "basics:icon-books", + "butcher", + "basics:icon-butcher", + "chemist", + "basics:icon-chemist", + "clothes", + "basics:icon-clothes", + "doityourself", + "basics:icon-doityourself", + "dry_cleaning", + "basics:icon-drycleaning", + "florist", + "basics:icon-florist", + "furniture", + "basics:icon-furniture", + "garden_centre", + "basics:icon-garden_centre", + "general", + "basics:icon-shop", + "gift", + "basics:icon-gift", + "greengrocer", + "basics:icon-greengrocer", + "hairdresser", + "basics:icon-hairdresser", + "hardware", + "basics:icon-hardware", + "jewelry", + "basics:icon-jewelry_store", + "kiosk", + "basics:icon-kiosk", + "laundry", + "basics:icon-laundry", + "newsagent", + "basics:icon-newsagent", + "optican", + "basics:icon-optician", + "outdoor", + "basics:icon-outdoor", + "shoes", + "basics:icon-shoes", + "sports", + "basics:icon-sports", + "stationery", + "basics:icon-stationery", + "toys", + "basics:icon-toys", + "travel_agency", + "basics:icon-travel_agent", + "video", + "basics:icon-video", + "basics:icon-shop" + ] + }, + "paint": { + "icon-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "text-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "icon-color": "rgb(85,85,85)", + "text-color": "rgb(85,85,85)" + } + }, + { + "id": "poi-man_made", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "pois", + "minzoom": 16, + "filter": ["to-boolean", ["get", "man_made"]], + "layout": { + "icon-size": {"stops": [[16, 0.5], [19, 0.5], [20, 1]]}, + "symbol-placement": "point", + "icon-optional": true, + "text-font": ["noto_sans_regular"], + "icon-image": [ + "match", + ["get", "man_made"], + "lighthouse", + "basics:icon-lighthouse", + "surveillance", + "basics:icon-surveillance", + "tower", + "basics:icon-observation_tower", + "watermill", + "basics:icon-watermill", + "windmill", + "basics:icon-windmill", + "" + ] + }, + "paint": { + "icon-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "text-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "icon-color": "rgb(85,85,85)", + "text-color": "rgb(85,85,85)" + } + }, + { + "id": "poi-historic", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "pois", + "minzoom": 16, + "filter": ["to-boolean", ["get", "historic"]], + "layout": { + "icon-size": {"stops": [[16, 0.5], [19, 0.5], [20, 1]]}, + "symbol-placement": "point", + "icon-optional": true, + "text-font": ["noto_sans_regular"], + "icon-image": [ + "match", + ["get", "historic"], + "artwork", + "basics:icon-artwork", + "castle", + "basics:icon-castle", + "monument", + "basics:icon-monument", + "wayside_shrine", + "basics:icon-shrine", + "basics:icon-historic" + ] + }, + "paint": { + "icon-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "text-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "icon-color": "rgb(85,85,85)", + "text-color": "rgb(85,85,85)" + } + }, + { + "id": "poi-emergency", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "pois", + "minzoom": 16, + "filter": ["to-boolean", ["get", "emergency"]], + "layout": { + "icon-size": {"stops": [[16, 0.5], [19, 0.5], [20, 1]]}, + "symbol-placement": "point", + "icon-optional": true, + "text-font": ["noto_sans_regular"], + "icon-image": [ + "match", + ["get", "emergency"], + "defibrillator", + "basics:icon-defibrillator", + "fire_hydrant", + "basics:icon-hydrant", + "phone", + "basics:icon-emergency_phone", + "" + ] + }, + "paint": { + "icon-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "text-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "icon-color": "rgb(85,85,85)", + "text-color": "rgb(85,85,85)" + } + }, + { + "id": "poi-highway", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "pois", + "minzoom": 16, + "filter": ["to-boolean", ["get", "highway"]], + "layout": { + "icon-size": {"stops": [[16, 0.5], [19, 0.5], [20, 1]]}, + "symbol-placement": "point", + "icon-optional": true, + "text-font": ["noto_sans_regular"] + }, + "paint": { + "icon-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "text-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "icon-color": "rgb(85,85,85)", + "text-color": "rgb(85,85,85)" + } + }, + { + "id": "poi-office", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "pois", + "minzoom": 16, + "filter": ["to-boolean", ["get", "office"]], + "layout": { + "icon-size": {"stops": [[16, 0.5], [19, 0.5], [20, 1]]}, + "symbol-placement": "point", + "icon-optional": true, + "text-font": ["noto_sans_regular"] + }, + "paint": { + "icon-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "text-opacity": {"stops": [[16, 0], [17, 0.4]]}, + "icon-color": "rgb(85,85,85)", + "text-color": "rgb(85,85,85)" + } + }, + { + "id": "boundary-country:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "boundaries", + "filter": [ + "all", + ["==", "admin_level", 2], + ["!=", "maritime", true], + ["!=", "disputed", true], + ["!=", "coastline", true] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgb(249,245,239)", + "line-blur": 1, + "line-width": {"stops": [[2, 0], [3, 2], [10, 8]]}, + "line-opacity": 0.75 + } + }, + { + "id": "boundary-country-disputed:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "boundaries", + "filter": [ + "all", + ["==", "admin_level", 2], + ["==", "disputed", true], + ["!=", "maritime", true], + ["!=", "coastline", true] + ], + "paint": { + "line-width": {"stops": [[2, 0], [3, 2], [10, 8]]}, + "line-opacity": 0.75, + "line-color": "rgb(249,245,239)" + } + }, + { + "id": "boundary-state:outline", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "boundaries", + "filter": [ + "all", + ["==", "admin_level", 4], + ["!=", "maritime", true], + ["!=", "disputed", true], + ["!=", "coastline", true] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgb(250,245,240)", + "line-blur": 1, + "line-width": {"stops": [[7, 0], [8, 2], [10, 4]]}, + "line-opacity": 0.75 + } + }, + { + "id": "boundary-country", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "boundaries", + "filter": [ + "all", + ["==", "admin_level", 2], + ["!=", "maritime", true], + ["!=", "disputed", true], + ["!=", "coastline", true] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgb(166,166,200)", + "line-width": {"stops": [[2, 0], [3, 1], [10, 4]]} + } + }, + { + "id": "boundary-country-disputed", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "boundaries", + "filter": [ + "all", + ["==", "admin_level", 2], + ["==", "disputed", true], + ["!=", "maritime", true], + ["!=", "coastline", true] + ], + "layout": {"line-cap": "square"}, + "paint": { + "line-width": {"stops": [[2, 0], [3, 1], [10, 4]]}, + "line-color": "rgb(190,188,207)", + "line-dasharray": [2, 1] + } + }, + { + "id": "boundary-state", + "type": "line", + "source": "versatiles-shortbread", + "source-layer": "boundaries", + "filter": [ + "all", + ["==", "admin_level", 4], + ["!=", "maritime", true], + ["!=", "disputed", true], + ["!=", "coastline", true] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "rgb(166,166,200)", + "line-width": {"stops": [[7, 0], [8, 1], [10, 2]]} + } + }, + { + "id": "label-address-housenumber", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "addresses", + "minzoom": 17, + "filter": ["has", "housenumber"], + "layout": { + "text-field": "{housenumber}", + "text-font": ["noto_sans_regular"], + "symbol-placement": "point", + "text-anchor": "center", + "text-size": {"stops": [[17, 8], [19, 10]]} + }, + "paint": { + "text-halo-color": "rgb(243,235,227)", + "text-halo-width": 2, + "text-halo-blur": 1, + "icon-color": "rgb(169,164,158)", + "text-color": "rgb(169,164,158)" + } + }, + { + "id": "label-motorway-shield", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "street_labels", + "minzoom": 14, + "filter": ["==", "kind", "motorway"], + "layout": { + "text-field": "{ref}", + "text-font": ["noto_sans_bold"], + "symbol-placement": "line", + "text-anchor": "center", + "text-size": {"stops": [[14, 10], [18, 12], [20, 16]]} + }, + "paint": { + "icon-color": "rgb(255,255,255)", + "text-color": "rgb(255,255,255)", + "text-halo-color": "rgb(255,204,136)", + "text-halo-width": 0.1, + "text-halo-blur": 1 + } + }, + { + "id": "label-street-pedestrian", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "street_labels", + "minzoom": 12, + "filter": ["==", "kind", "pedestrian"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "symbol-placement": "line", + "text-anchor": "center", + "text-size": {"stops": [[12, 10], [15, 13]]} + }, + "paint": { + "icon-color": "rgb(51,51,68)", + "text-color": "rgb(51,51,68)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-street-livingstreet", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "street_labels", + "minzoom": 12, + "filter": ["==", "kind", "living_street"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "symbol-placement": "line", + "text-anchor": "center", + "text-size": {"stops": [[12, 10], [15, 13]]} + }, + "paint": { + "icon-color": "rgb(51,51,68)", + "text-color": "rgb(51,51,68)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-street-residential", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "street_labels", + "minzoom": 12, + "filter": ["==", "kind", "residential"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "symbol-placement": "line", + "text-anchor": "center", + "text-size": {"stops": [[12, 10], [15, 13]]} + }, + "paint": { + "icon-color": "rgb(51,51,68)", + "text-color": "rgb(51,51,68)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-street-unclassified", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "street_labels", + "minzoom": 12, + "filter": ["==", "kind", "unclassified"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "symbol-placement": "line", + "text-anchor": "center", + "text-size": {"stops": [[12, 10], [15, 13]]} + }, + "paint": { + "icon-color": "rgb(51,51,68)", + "text-color": "rgb(51,51,68)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-street-tertiary", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "street_labels", + "minzoom": 12, + "filter": ["==", "kind", "tertiary"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "symbol-placement": "line", + "text-anchor": "center", + "text-size": {"stops": [[12, 10], [15, 13]]} + }, + "paint": { + "icon-color": "rgb(51,51,68)", + "text-color": "rgb(51,51,68)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-street-secondary", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "street_labels", + "minzoom": 12, + "filter": ["==", "kind", "secondary"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "symbol-placement": "line", + "text-anchor": "center", + "text-size": {"stops": [[12, 10], [15, 13]]} + }, + "paint": { + "icon-color": "rgb(51,51,68)", + "text-color": "rgb(51,51,68)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-street-primary", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "street_labels", + "minzoom": 12, + "filter": ["==", "kind", "primary"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "symbol-placement": "line", + "text-anchor": "center", + "text-size": {"stops": [[12, 10], [15, 13]]} + }, + "paint": { + "icon-color": "rgb(51,51,68)", + "text-color": "rgb(51,51,68)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-street-trunk", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "street_labels", + "minzoom": 12, + "filter": ["==", "kind", "trunk"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "symbol-placement": "line", + "text-anchor": "center", + "text-size": {"stops": [[12, 10], [15, 13]]} + }, + "paint": { + "icon-color": "rgb(51,51,68)", + "text-color": "rgb(51,51,68)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-place-neighbourhood", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "place_labels", + "minzoom": 14, + "filter": ["==", "kind", "neighbourhood"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "text-size": {"stops": [[14, 12]]}, + "text-transform": "uppercase" + }, + "paint": { + "icon-color": "rgb(40,67,73)", + "text-color": "rgb(40,67,73)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-place-quarter", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "place_labels", + "minzoom": 13, + "filter": ["==", "kind", "quarter"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "text-size": {"stops": [[13, 13]]}, + "text-transform": "uppercase" + }, + "paint": { + "icon-color": "rgb(40,62,73)", + "text-color": "rgb(40,62,73)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-place-suburb", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "place_labels", + "minzoom": 11, + "filter": ["==", "kind", "suburb"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "text-size": {"stops": [[11, 11], [13, 14]]}, + "text-transform": "uppercase" + }, + "paint": { + "icon-color": "rgb(40,57,73)", + "text-color": "rgb(40,57,73)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-place-hamlet", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "place_labels", + "minzoom": 13, + "filter": ["==", "kind", "hamlet"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "text-size": {"stops": [[10, 11], [12, 14]]} + }, + "paint": { + "icon-color": "rgb(40,48,73)", + "text-color": "rgb(40,48,73)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-place-village", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "place_labels", + "minzoom": 11, + "filter": ["==", "kind", "village"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "text-size": {"stops": [[9, 11], [12, 14]]} + }, + "paint": { + "icon-color": "rgb(40,48,73)", + "text-color": "rgb(40,48,73)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-place-town", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "place_labels", + "minzoom": 9, + "filter": ["==", "kind", "town"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "text-size": {"stops": [[8, 11], [12, 14]]} + }, + "paint": { + "icon-color": "rgb(40,48,73)", + "text-color": "rgb(40,48,73)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-boundary-state", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "boundary_labels", + "minzoom": 5, + "filter": ["in", "admin_level", 4, "4"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "text-transform": "uppercase", + "text-anchor": "top", + "text-offset": [0, 0.2], + "text-padding": 0, + "text-optional": true, + "text-size": {"stops": [[5, 8], [8, 12]]} + }, + "paint": { + "icon-color": "rgb(61,61,77)", + "text-color": "rgb(61,61,77)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-place-city", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "place_labels", + "minzoom": 7, + "filter": ["==", "kind", "city"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "text-size": {"stops": [[7, 11], [10, 14]]} + }, + "paint": { + "icon-color": "rgb(40,48,73)", + "text-color": "rgb(40,48,73)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-place-statecapital", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "place_labels", + "minzoom": 6, + "filter": ["==", "kind", "state_capital"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "text-size": {"stops": [[6, 11], [10, 15]]} + }, + "paint": { + "icon-color": "rgb(40,48,73)", + "text-color": "rgb(40,48,73)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-place-capital", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "place_labels", + "minzoom": 5, + "filter": ["==", "kind", "capital"], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "text-size": {"stops": [[5, 12], [10, 16]]} + }, + "paint": { + "icon-color": "rgb(40,48,73)", + "text-color": "rgb(40,48,73)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-boundary-country-small", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "boundary_labels", + "minzoom": 4, + "filter": [ + "all", + ["in", "admin_level", 2, "2"], + ["<=", "way_area", 10000000] + ], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "text-transform": "uppercase", + "text-anchor": "top", + "text-offset": [0, 0.2], + "text-padding": 0, + "text-optional": true, + "text-size": {"stops": [[4, 8], [5, 11]]} + }, + "paint": { + "icon-color": "rgb(51,51,68)", + "text-color": "rgb(51,51,68)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-boundary-country-medium", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "boundary_labels", + "minzoom": 3, + "filter": [ + "all", + ["in", "admin_level", 2, "2"], + ["<", "way_area", 90000000], + [">", "way_area", 10000000] + ], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "text-transform": "uppercase", + "text-anchor": "top", + "text-offset": [0, 0.2], + "text-padding": 0, + "text-optional": true, + "text-size": {"stops": [[3, 8], [5, 12]]} + }, + "paint": { + "icon-color": "rgb(51,51,68)", + "text-color": "rgb(51,51,68)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "label-boundary-country-large", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "boundary_labels", + "minzoom": 2, + "filter": [ + "all", + ["in", "admin_level", 2, "2"], + [">=", "way_area", 90000000] + ], + "layout": { + "text-field": "{name}", + "text-font": ["noto_sans_regular"], + "text-transform": "uppercase", + "text-anchor": "top", + "text-offset": [0, 0.2], + "text-padding": 0, + "text-optional": true, + "text-size": {"stops": [[2, 8], [5, 13]]} + }, + "paint": { + "icon-color": "rgb(51,51,68)", + "text-color": "rgb(51,51,68)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "marking-oneway", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 16, + "filter": [ + "all", + ["==", "oneway", true], + [ + "in", + "kind", + "trunk", + "primary", + "secondary", + "tertiary", + "unclassified", + "residential", + "living_street" + ] + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 175, + "icon-rotate": 90, + "icon-rotation-alignment": "map", + "icon-padding": 5, + "symbol-avoid-edges": true, + "icon-image": "basics:marking-arrow", + "text-font": ["noto_sans_regular"] + }, + "paint": { + "icon-opacity": {"stops": [[16, 0], [17, 0.4], [20, 0.4]]}, + "text-opacity": {"stops": [[16, 0], [17, 0.4], [20, 0.4]]} + } + }, + { + "id": "marking-oneway-reverse", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "streets", + "minzoom": 16, + "filter": [ + "all", + ["==", "oneway_reverse", true], + [ + "in", + "kind", + "trunk", + "primary", + "secondary", + "tertiary", + "unclassified", + "residential", + "living_street" + ] + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 75, + "icon-rotate": -90, + "icon-rotation-alignment": "map", + "icon-padding": 5, + "symbol-avoid-edges": true, + "icon-image": "basics:marking-arrow", + "text-font": ["noto_sans_regular"] + }, + "paint": { + "icon-opacity": {"stops": [[16, 0], [17, 0.4], [20, 0.4]]}, + "text-opacity": {"stops": [[16, 0], [17, 0.4], [20, 0.4]]} + } + }, + { + "id": "symbol-transit-subway", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "public_transport", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "station", "halt"], + ["==", "station", "subway"] + ], + "layout": { + "text-field": "{name}", + "icon-size": {"stops": [[14, 0.5], [16, 1]]}, + "symbol-placement": "point", + "icon-keep-upright": true, + "text-font": ["noto_sans_regular"], + "text-size": 10, + "icon-anchor": "bottom", + "text-anchor": "top", + "icon-image": "basics:icon-rail_metro", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "icon-color": "rgb(102,98,106)", + "text-color": "rgb(102,98,106)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "symbol-transit-lightrail", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "public_transport", + "minzoom": 14, + "filter": [ + "all", + ["in", "kind", "station", "halt"], + ["==", "station", "light_rail"] + ], + "layout": { + "text-field": "{name}", + "icon-size": {"stops": [[14, 0.5], [16, 1]]}, + "symbol-placement": "point", + "icon-keep-upright": true, + "text-font": ["noto_sans_regular"], + "text-size": 10, + "icon-anchor": "bottom", + "text-anchor": "top", + "icon-image": "basics:icon-rail_light", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "icon-color": "rgb(102,98,106)", + "text-color": "rgb(102,98,106)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "symbol-transit-station", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "public_transport", + "minzoom": 13, + "filter": [ + "all", + ["in", "kind", "station", "halt"], + ["!in", "station", "light_rail", "subway"] + ], + "layout": { + "text-field": "{name}", + "icon-size": {"stops": [[13, 0.5], [15, 1]]}, + "symbol-placement": "point", + "icon-keep-upright": true, + "text-font": ["noto_sans_regular"], + "text-size": 10, + "icon-anchor": "bottom", + "text-anchor": "top", + "icon-image": "basics:icon-rail" + }, + "paint": { + "icon-opacity": 0.7, + "icon-color": "rgb(102,98,106)", + "text-color": "rgb(102,98,106)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "symbol-transit-airfield", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "public_transport", + "minzoom": 13, + "filter": ["all", ["==", "kind", "aerodrome"], ["!has", "iata"]], + "layout": { + "text-field": "{name}", + "icon-size": {"stops": [[13, 0.5], [15, 1]]}, + "symbol-placement": "point", + "icon-keep-upright": true, + "text-font": ["noto_sans_regular"], + "text-size": 10, + "icon-anchor": "bottom", + "text-anchor": "top", + "icon-image": "basics:icon-airfield" + }, + "paint": { + "icon-opacity": 0.7, + "icon-color": "rgb(102,98,106)", + "text-color": "rgb(102,98,106)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + }, + { + "id": "symbol-transit-airport", + "type": "symbol", + "source": "versatiles-shortbread", + "source-layer": "public_transport", + "minzoom": 12, + "filter": ["all", ["==", "kind", "aerodrome"], ["has", "iata"]], + "layout": { + "text-field": "{name}", + "icon-size": {"stops": [[12, 0.5], [14, 1]]}, + "symbol-placement": "point", + "icon-keep-upright": true, + "text-font": ["noto_sans_regular"], + "text-size": 10, + "icon-anchor": "bottom", + "text-anchor": "top", + "icon-image": "basics:icon-airport" + }, + "paint": { + "icon-opacity": 0.7, + "icon-color": "rgb(102,98,106)", + "text-color": "rgb(102,98,106)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2, + "text-halo-blur": 1 + } + } + ], + "id": "vootuu7" +} diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java index b2a91d0..b32915d 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityExperiments.java @@ -1,93 +1,94 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino; import android.os.Bundle; import android.util.Log; +import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.fragment.app.FragmentTransaction; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.GeneralActivity; public class ActivityExperiments extends GeneralActivity implements CommonFragmentListener { final static String DEBUG_TAG = "ExperimentsActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_container_fragment); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(false); actionBar.setIcon(R.drawable.ic_launcher); } if (savedInstanceState==null) { getSupportFragmentManager().beginTransaction() .setReorderingAllowed(true) /* .add(R.id.fragment_container_view, LinesDetailFragment.class, LinesDetailFragment.Companion.makeArgs("gtt:4U")) */ //.add(R.id.fragment_container_view, LinesGridShowingFragment.class, null) //.add(R.id.fragment_container_view, IntroFragment.class, IntroFragment.makeArguments(0)) //.commit(); //.add(R.id.fragment_container_view, LinesDetailFragment.class, // LinesDetailFragment.Companion.makeArgs("gtt:4U")) - .add(R.id.fragment_container_view, BackupImportFragment.class, null) + .add(R.id.fragment_container_view, MapLibreFragment.class, null) .commit(); } } @Override public void showFloatingActionButton(boolean yes) { Log.d(DEBUG_TAG, "Asked to show the action button"); } @Override public void readyGUIfor(FragmentKind fragmentType) { Log.d(DEBUG_TAG, "Asked to prepare the GUI for fragmentType "+fragmentType); } @Override public void requestArrivalsForStopID(String ID) { } @Override public void showMapCenteredOnStop(Stop stop) { } @Override - public void showLineOnMap(String routeGtfsId){ + public void showLineOnMap(String routeGtfsId, @Nullable String stopIDFrom){ readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.fragment_container_view, LinesDetailFragment.class, - LinesDetailFragment.Companion.makeArgs(routeGtfsId)); + LinesDetailFragment.Companion.makeArgs(routeGtfsId, stopIDFrom)); tr.addToBackStack("LineonMap-"+routeGtfsId); tr.commit(); } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java index 7a9f1fb..8d7bb5a 100644 --- a/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java +++ b/app/src/main/java/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,799 +1,809 @@ /* BusTO - Arrival times for Turin public transport. Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.FrameLayout; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.widget.Toolbar; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.preference.PreferenceManager; import androidx.work.WorkInfo; import androidx.work.WorkManager; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.data.PreferencesHolder; import it.reyboz.bustorino.data.gtfs.GtfsDatabase; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.GeneralActivity; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.backend.utils.openIceweasel; public class ActivityPrincipal extends GeneralActivity implements FragmentListenerMain { private DrawerLayout mDrawer; private NavigationView mNavView; private ActionBarDrawerToggle drawerToggle; private final static String DEBUG_TAG="BusTO Act Principal"; private final static String TAG_FAVORITES="favorites_frag"; private Snackbar snackbar; private boolean showingMainFragmentFromOther = false; private boolean onCreateComplete = false; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(DEBUG_TAG, "onCreate, savedInstanceState is: "+savedInstanceState); setContentView(R.layout.activity_principal); boolean showingArrivalsFromIntent = false; Toolbar mToolbar = findViewById(R.id.default_toolbar); setSupportActionBar(mToolbar); if (getSupportActionBar()!=null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); else Log.w(DEBUG_TAG, "NO ACTION BAR"); mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener(this)); mDrawer = findViewById(R.id.drawer_layout); drawerToggle = setupDrawerToggle(mToolbar); // Setup toggle to display hamburger icon with nice animation drawerToggle.setDrawerIndicatorEnabled(true); drawerToggle.syncState(); mDrawer.addDrawerListener(drawerToggle); mDrawer.addDrawerListener(new DrawerLayout.DrawerListener() { @Override public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { } @Override public void onDrawerOpened(@NonNull View drawerView) { hideKeyboard(); } @Override public void onDrawerClosed(@NonNull View drawerView) { } @Override public void onDrawerStateChanged(int newState) { } }); mNavView = findViewById(R.id.nvView); setupDrawerContent(mNavView); /*View header = mNavView.getHeaderView(0); */ //mNavView.getMenu().findItem(R.id.versionFooter). /// LEGACY CODE //---------------------------- START INTENT CHECK QUEUE ------------------------------------ // Intercept calls from URL intent boolean tryedFromIntent = false; String busStopID = null; Uri data = getIntent().getData(); if (data != null) { busStopID = getBusStopIDFromUri(data); Log.d(DEBUG_TAG, "Opening Intent: busStopID: "+busStopID); tryedFromIntent = true; } // Intercept calls from other activities if (!tryedFromIntent) { Bundle b = getIntent().getExtras(); if (b != null) { busStopID = b.getString("bus-stop-ID"); /* * I'm not very sure if you are coming from an Intent. * Some launchers work in strange ways. */ tryedFromIntent = busStopID != null; } } //---------------------------- END INTENT CHECK QUEUE -------------------------------------- if (busStopID == null) { // Show keyboard if can't start from intent // JUST DON'T // showKeyboard(); // You haven't obtained anything... from an intent? if (tryedFromIntent) { // This shows a luser warning Toast.makeText(getApplicationContext(), R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show(); } } else { // If you are here an intent has worked successfully //setBusStopSearchByIDEditText(busStopID); //Log.d(DEBUG_TAG, "Requesting arrivals for stop "+busStopID+" from intent"); requestArrivalsForStopID(busStopID); //this shows the fragment, too showingArrivalsFromIntent = true; } //database check GtfsDatabase gtfsDB = GtfsDatabase.Companion.getGtfsDatabase(this); final int db_version = gtfsDB.getOpenHelper().getReadableDatabase().getVersion(); boolean dataUpdateRequested = false; final SharedPreferences theShPr = getMainSharedPreferences(); final int old_version = PreferencesHolder.getGtfsDBVersion(theShPr); Log.d(DEBUG_TAG, "GTFS Database: old version is "+old_version+ ", new version is "+db_version); if (old_version < db_version){ //decide update conditions in the future if(old_version < 2 && db_version >= 2) { dataUpdateRequested = true; DatabaseUpdate.requestDBUpdateWithWork(this, true, true); } PreferencesHolder.setGtfsDBVersion(theShPr, db_version); } //Try (hopefully) database update if(!dataUpdateRequested) DatabaseUpdate.requestDBUpdateWithWork(this, false, false); /* Watch for database update */ final WorkManager workManager = WorkManager.getInstance(this); workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG) .observe(this, workInfoList -> { // If there are no matching work info, do nothing if (workInfoList == null || workInfoList.isEmpty()) { return; } Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList); boolean showProgress = false; for (WorkInfo workInfo : workInfoList) { if (workInfo.getState() == WorkInfo.State.RUNNING) { showProgress = true; break; } } if (showProgress) { createDefaultSnackbar(); } else { if(snackbar!=null) { snackbar.dismiss(); snackbar = null; } } }); // show the main fragment Fragment f = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); Log.d(DEBUG_TAG, "OnCreate the fragment is "+f); String vl = PreferenceManager.getDefaultSharedPreferences(this).getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, ""); //if (vl.length() == 0 || vl.equals("arrivals")) { // showMainFragment(); Log.d(DEBUG_TAG, "The default screen to open is: "+vl); if (showingArrivalsFromIntent){ //do nothing but exclude a case }else if (savedInstanceState==null) { //we are not restarting the activity from nothing if (vl.equals("map")) { requestMapFragment(false); } else if (vl.equals("favorites")) { checkAndShowFavoritesFragment(getSupportFragmentManager(), false); } else if (vl.equals("lines")) { showLinesFragment(getSupportFragmentManager(), false, null); } else { showMainFragment(false); } } onCreateComplete = true; //last but not least, set the good default values - manageDefaultValuesForSettings(); + setDefaultSettingsValuesWhenMissing(); //check if first run activity (IntroActivity) has been started once or not boolean hasIntroRun = theShPr.getBoolean(PreferencesHolder.PREF_INTRO_ACTIVITY_RUN,false); if(!hasIntroRun){ startIntroductionActivity(); } } private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) { // NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it // and will not render the hamburger icon without it. return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close); } /** * Setup drawer actions * @param navigationView the navigation view on which to set the callbacks */ private void setupDrawerContent(NavigationView navigationView) { navigationView.setNavigationItemSelectedListener( menuItem -> { if (menuItem.getItemId() == R.id.drawer_action_settings) { Log.d("MAINBusTO", "Pressed button preferences"); closeDrawerIfOpen(); startActivity(new Intent(ActivityPrincipal.this, ActivitySettings.class)); return true; } else if(menuItem.getItemId() == R.id.nav_favorites_item){ closeDrawerIfOpen(); //get Fragment checkAndShowFavoritesFragment(getSupportFragmentManager(), true); return true; } else if(menuItem.getItemId() == R.id.nav_arrivals){ closeDrawerIfOpen(); showMainFragment(true); return true; } else if(menuItem.getItemId() == R.id.nav_map_item){ closeDrawerIfOpen(); requestMapFragment(true); return true; } else if (menuItem.getItemId() == R.id.nav_lines_item) { closeDrawerIfOpen(); showLinesFragment(getSupportFragmentManager(), true,null); return true; } else if(menuItem.getItemId() == R.id.drawer_action_info) { closeDrawerIfOpen(); startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; } //selectDrawerItem(menuItem); Log.d(DEBUG_TAG, "pressed item "+menuItem); return true; }); } private void closeDrawerIfOpen(){ if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); } // `onPostCreate` called when activity start-up is complete after `onStart()` // NOTE 1: Make sure to override the method with only a single `Bundle` argument // Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method. // There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon. @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sync the toggle state after onRestoreInstanceState has occurred. drawerToggle.syncState(); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); // Pass any configuration change to the drawer toggles drawerToggle.onConfigurationChanged(newConfig); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.principal_menu, menu); MenuItem experimentsMenuItem = menu.findItem(R.id.action_experiments); SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false); experimentsMenuItem.setVisible(exper_On); return super.onCreateOptionsMenu(menu); } //requesting permissions @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode==STORAGE_PERMISSION_REQ){ final String storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE; if (permissionDoneRunnables.containsKey(storagePerm)) { Runnable toRun = permissionDoneRunnables.get(storagePerm); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storagePerm); } if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions)); if (permissionDoneRunnables.containsKey(storagePerm)) { Runnable toRun = permissionDoneRunnables.get(storagePerm); if (toRun != null) toRun.run(); permissionDoneRunnables.remove(storagePerm); } } else { //permission denied showToastMessage(R.string.permission_storage_maps_msg, false); } } } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int[] cases = {R.id.nav_arrivals, R.id.nav_favorites_item}; Log.d(DEBUG_TAG, "Item pressed"); if (item.getItemId() == android.R.id.home) { mDrawer.openDrawer(GravityCompat.START); return true; } if (drawerToggle.onOptionsItemSelected(item)) { return true; } return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { boolean foundFragment = false; Fragment shownFrag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (mDrawer.isDrawerOpen(GravityCompat.START)) mDrawer.closeDrawer(GravityCompat.START); else if(shownFrag != null && shownFrag.isVisible() && shownFrag.getChildFragmentManager().getBackStackEntryCount() > 0){ //if we have been asked to show a stop from another fragment, we should go back even in the main if(shownFrag instanceof MainScreenFragment){ //we have to stop the arrivals reload ((MainScreenFragment) shownFrag).cancelReloadArrivalsIfNeeded(); } shownFrag.getChildFragmentManager().popBackStack(); if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){ getSupportFragmentManager().popBackStack(); Log.d(DEBUG_TAG, "Popping main back stack also"); } } else if (getSupportFragmentManager().getBackStackEntryCount() > 0) { getSupportFragmentManager().popBackStack(); Log.d(DEBUG_TAG, "Popping main frame backstack for fragments"); } else super.onBackPressed(); } /** * Create and show the SnackBar with the message */ private void createDefaultSnackbar() { View baseView = null; boolean showSnackbar = true; final Fragment frag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame); if (frag instanceof ScreenBaseFragment){ baseView = ((ScreenBaseFragment) frag).getBaseViewForSnackBar(); showSnackbar = ((ScreenBaseFragment) frag).showSnackbarOnDBUpdate(); } if (baseView == null) baseView = findViewById(R.id.mainActContentFrame); //if (baseView == null) Log.e(DEBUG_TAG, "baseView null for default snackbar, probably exploding now"); if (baseView !=null && showSnackbar) { this.snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE); if (frag instanceof ScreenBaseFragment){ ((ScreenBaseFragment) frag).setSnackbarPropertiesBeforeShowing(this.snackbar); } this.snackbar.show(); } else{ Log.e(DEBUG_TAG, "Asked to show the snackbar but the baseView is null"); } } /** * Show the fragment by adding it to the backstack * @param fraMan the fragmentManager * @param fragment the fragment */ private static void showMainFragment(FragmentManager fraMan, MainScreenFragment fragment, boolean addToBackStack){ FragmentTransaction ft = fraMan.beginTransaction() .replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG) .setReorderingAllowed(false) /*.setCustomAnimations( R.anim.slide_in, // enter R.anim.fade_out, // exit R.anim.fade_in, // popEnter R.anim.slide_out // popExit )*/ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); if (addToBackStack) ft.addToBackStack(null); ft.commit(); } /** * Show the fragment by adding it to the backstack * @param fraMan the fragmentManager * @param arguments args for the fragment */ private static void createShowMainFragment(FragmentManager fraMan,@Nullable Bundle arguments, boolean addToBackStack){ FragmentTransaction ft = fraMan.beginTransaction() .replace(R.id.mainActContentFrame, MainScreenFragment.class, arguments, MainScreenFragment.FRAGMENT_TAG) .setReorderingAllowed(false) /*.setCustomAnimations( R.anim.slide_in, // enter R.anim.fade_out, // exit R.anim.fade_in, // popEnter R.anim.slide_out // popExit )*/ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); if (addToBackStack) ft.addToBackStack(null); ft.commit(); } private void requestMapFragment(final boolean allowReturn){ // starting from Android 11, we don't need to have the STORAGE permission anymore for the map cache - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){ + /*if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){ //nothing to do Log.d(DEBUG_TAG, "Build codes allow the showing of the map"); createAndShowMapFragment(null, allowReturn); return; } final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE; int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ); Log.d(DEBUG_TAG, "Permission for storage: "+result); switch (result) { case PERMISSION_OK: createAndShowMapFragment(null, allowReturn); break; case PERMISSION_ASKING: permissionDoneRunnables.put(permission, () -> createAndShowMapFragment(null, allowReturn)); break; case PERMISSION_NEG_CANNOT_ASK: String storage_perm = getString(R.string.storage_permission); String text = getString(R.string.too_many_permission_asks, storage_perm); Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show(); } + + */ + //The permissions are handled in the MapLibreFragment instead + createAndShowMapFragment(null, allowReturn); } private static void checkAndShowFavoritesFragment(FragmentManager fragmentManager, boolean addToBackStack){ FragmentTransaction ft = fragmentManager.beginTransaction(); Fragment fragment = fragmentManager.findFragmentByTag(TAG_FAVORITES); if(fragment!=null){ ft.replace(R.id.mainActContentFrame, fragment, TAG_FAVORITES); }else{ //use new method ft.replace(R.id.mainActContentFrame,FavoritesFragment.class,null,TAG_FAVORITES); } if (addToBackStack) ft.addToBackStack("favorites_main"); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .setReorderingAllowed(false); ft.commit(); } private static void showLinesFragment(@NonNull FragmentManager fragmentManager, boolean addToBackStack, @Nullable Bundle fragArgs){ FragmentTransaction ft = fragmentManager.beginTransaction(); Fragment f = fragmentManager.findFragmentByTag(LinesGridShowingFragment.FRAGMENT_TAG); if(f!=null){ ft.replace(R.id.mainActContentFrame, f, LinesGridShowingFragment.FRAGMENT_TAG); }else{ //use new method ft.replace(R.id.mainActContentFrame,LinesGridShowingFragment.class,fragArgs, LinesGridShowingFragment.FRAGMENT_TAG); } if (addToBackStack) ft.addToBackStack("linesGrid"); ft.setReorderingAllowed(true) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit(); } private void showMainFragment(boolean addToBackStack){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); final MainScreenFragment mainScreenFragment; if (fragment==null | !(fragment instanceof MainScreenFragment)){ createShowMainFragment(fraMan, null, addToBackStack); } else if(!fragment.isVisible()){ mainScreenFragment = (MainScreenFragment) fragment; showMainFragment(fraMan, mainScreenFragment, addToBackStack); Log.d(DEBUG_TAG, "Found the main fragment"); } else{ mainScreenFragment = (MainScreenFragment) fragment; } //return mainScreenFragment; } @Nullable private MainScreenFragment getMainFragmentIfVisible(){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); if (fragment!= null && fragment.isVisible()) return (MainScreenFragment) fragment; else return null; } @Override public void showFloatingActionButton(boolean yes) { //TODO } /* public void setDrawerSelectedItem(String fragmentTag){ switch (fragmentTag){ case MainScreenFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_arrivals); break; case MapFragment.FRAGMENT_TAG: break; case FavoritesFragment.FRAGMENT_TAG: mNavView.setCheckedItem(R.id.nav_favorites_item); break; } }*/ @Override public void readyGUIfor(FragmentKind fragmentType) { MainScreenFragment mainFragmentIfVisible = getMainFragmentIfVisible(); if (mainFragmentIfVisible!=null){ mainFragmentIfVisible.readyGUIfor(fragmentType); } int titleResId; switch (fragmentType){ case MAP: mNavView.setCheckedItem(R.id.nav_map_item); titleResId = R.string.map; break; case FAVORITES: mNavView.setCheckedItem(R.id.nav_favorites_item); titleResId = R.string.nav_favorites_text; break; case ARRIVALS: titleResId = R.string.nav_arrivals_text; mNavView.setCheckedItem(R.id.nav_arrivals); break; case STOPS: titleResId = R.string.stop_search_view_title; mNavView.setCheckedItem(R.id.nav_arrivals); break; case MAIN_SCREEN_FRAGMENT: case NEARBY_STOPS: case NEARBY_ARRIVALS: titleResId=R.string.app_name_full; mNavView.setCheckedItem(R.id.nav_arrivals); break; case LINES: titleResId=R.string.lines; mNavView.setCheckedItem(R.id.nav_lines_item); break; default: titleResId = 0; } if(getSupportActionBar()!=null && titleResId!=0) getSupportActionBar().setTitle(titleResId); } @Override public void requestArrivalsForStopID(String ID) { //register if the request came from the main fragment or not MainScreenFragment probableFragment = getMainFragmentIfVisible(); showingMainFragmentFromOther = (probableFragment==null); if (showingMainFragmentFromOther){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); Log.d(DEBUG_TAG, "Requested main fragment, not visible. Search by TAG returned: "+fragment); if(fragment!=null){ //the fragment is there but not shown probableFragment = (MainScreenFragment) fragment; // set the flag probableFragment.setSuppressArrivalsReload(true); showMainFragment(fraMan, probableFragment, true); probableFragment.requestArrivalsForStopID(ID); } else { // we have no fragment final Bundle args = new Bundle(); args.putString(MainScreenFragment.PENDING_STOP_SEARCH, ID); //if onCreate is complete, then we are not asking for the first showing fragment boolean addtobackstack = onCreateComplete; createShowMainFragment(fraMan, args ,addtobackstack); } } else { //the MainScreeFragment is shown, nothing to do probableFragment.requestArrivalsForStopID(ID); } mNavView.setCheckedItem(R.id.nav_arrivals); } @Override - public void showLineOnMap(String routeGtfsId){ + public void showLineOnMap(String routeGtfsId, @Nullable String stopIDFrom){ readyGUIfor(FragmentKind.LINES); FragmentTransaction tr = getSupportFragmentManager().beginTransaction(); tr.replace(R.id.mainActContentFrame, LinesDetailFragment.class, - LinesDetailFragment.Companion.makeArgs(routeGtfsId)); + LinesDetailFragment.Companion.makeArgs(routeGtfsId, stopIDFrom)); tr.addToBackStack("LineonMap-"+routeGtfsId); tr.commit(); } @Override public void toggleSpinner(boolean state) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.toggleSpinner(state); } } @Override public void enableRefreshLayout(boolean yes) { MainScreenFragment probableFragment = getMainFragmentIfVisible(); if (probableFragment!=null){ probableFragment.enableRefreshLayout(yes); } } @Override public void showMapCenteredOnStop(Stop stop) { createAndShowMapFragment(stop, true); } //Map Fragment stuff void createAndShowMapFragment(@Nullable Stop stop, boolean addToBackStack){ - FragmentManager fm = getSupportFragmentManager(); - FragmentTransaction ft = fm.beginTransaction(); - MapFragment fragment = stop == null? MapFragment.getInstance(): MapFragment.getInstance(stop); - ft.replace(R.id.mainActContentFrame, fragment, MapFragment.FRAGMENT_TAG); + final FragmentManager fm = getSupportFragmentManager(); + final FragmentTransaction ft = fm.beginTransaction(); + final MapLibreFragment fragment = MapLibreFragment.Companion.newInstance(stop); + ft.replace(R.id.mainActContentFrame, fragment, MapFragmentKt.FRAGMENT_TAG); if (addToBackStack) ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } void startIntroductionActivity(){ Intent intent = new Intent(ActivityPrincipal.this, ActivityIntro.class); intent.putExtra(ActivityIntro.RESTART_MAIN, false); startActivity(intent); } class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{ private final Context activityContext; public ToolbarItemClickListener(Context activityContext) { this.activityContext = activityContext; } @Override public boolean onMenuItemClick(MenuItem item) { final int id = item.getItemId(); if(id == R.id.action_about){ startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class)); return true; } else if (id == R.id.action_hack) { openIceweasel(getString(R.string.hack_url), activityContext); return true; } else if (id == R.id.action_source){ openIceweasel("https://gitpull.it/source/libre-busto/", activityContext); return true; } else if (id == R.id.action_licence){ openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext); return true; } else if (id == R.id.action_experiments) { startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class)); return true; } else if (id == R.id.action_tutorial) { startIntroductionActivity(); return true; } return false; } } /** * Adjust setting to match the default ones */ - private void manageDefaultValuesForSettings(){ + private void setDefaultSettingsValuesWhenMissing(){ SharedPreferences mainSharedPref = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor editor = mainSharedPref.edit(); //Main fragment to show String screen = mainSharedPref.getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, ""); boolean edit = false; if (screen.isEmpty()){ editor.putString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, "arrivals"); edit=true; } //Fetchers final Set setSelected = mainSharedPref.getStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, new HashSet<>()); if (setSelected.isEmpty()){ String[] defaultVals = getResources().getStringArray(R.array.arrivals_sources_values_default); editor.putStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, utils.convertArrayToSet(defaultVals)); edit=true; } //Live bus positions final String keySourcePositions=getString(R.string.pref_positions_source); final String positionsSource = mainSharedPref.getString(keySourcePositions, ""); if(positionsSource.isEmpty()){ String[] defaultVals = getResources().getStringArray(R.array.positions_source_values); editor.putString(keySourcePositions, defaultVals[0]); edit=true; } + //Map style + final String mapStylePref = mainSharedPref.getString(SettingsFragment.LIBREMAP_STYLE_PREF_KEY, ""); + if(mapStylePref.isEmpty()){ + final String[] defaultVals = getResources().getStringArray(R.array.map_style_pref_values); + editor.putString(SettingsFragment.LIBREMAP_STYLE_PREF_KEY, defaultVals[0]); + edit=true; + } if (edit){ editor.commit(); } } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/LivePositionTripPattern.kt b/app/src/main/java/it/reyboz/bustorino/backend/LivePositionTripPattern.kt new file mode 100644 index 0000000..3c4c051 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/backend/LivePositionTripPattern.kt @@ -0,0 +1,9 @@ +package it.reyboz.bustorino.backend + +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate +import it.reyboz.bustorino.data.gtfs.MatoPattern + +data class LivePositionTripPattern( + var posUpdate: LivePositionUpdate, + var pattern: MatoPattern? +) diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Stop.java b/app/src/main/java/it/reyboz/bustorino/backend/Stop.java index a2da413..63aaee7 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/Stop.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Stop.java @@ -1,308 +1,360 @@ /* BusTO (backend components) Copyright (C) 2016 Ludovico Pavesi This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend; import android.location.Location; +import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import it.reyboz.bustorino.util.LinesNameSorter; import org.osmdroid.api.IGeoPoint; import java.net.URLEncoder; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; public class Stop implements Comparable { // remove "final" in case you need to set these from outside the parser\scrapers\fetchers public final @NonNull String ID; private @Nullable String name; private @Nullable String username; public @Nullable String location; public @Nullable Route.Type type; private @Nullable List routesThatStopHere; private final @Nullable Double lat; private final @Nullable Double lon; // leave this non-final private @Nullable String routesThatStopHereString = null; private @Nullable String absurdGTTPlaceName = null; // public @Nullable String gtfsID = null; /** * Hey, look, method overloading! */ public Stop(final @Nullable String name, final @NonNull String ID, @Nullable final String location, @Nullable final Route.Type type, @Nullable final List routesThatStopHere) { this.ID = ID; this.name = name; this.username = null; this.location = (location != null && location.length() == 0) ? null : location; this.type = type; this.routesThatStopHere = routesThatStopHere; this.lat = null; this.lon = null; } /** * Hey, look, method overloading! */ public Stop(final @NonNull String ID) { this.ID = ID; this.name = null; this.username = null; this.location = null; this.type = null; this.routesThatStopHere = null; this.lat = null; this.lon = null; } /** * Constructor that sets EVERYTHING. */ public Stop(@NonNull String ID, @Nullable String name, @Nullable String userName, @Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere, @Nullable Double lat, @Nullable Double lon, @Nullable String gtfsID) { this.ID = ID; this.name = name; this.username = userName; this.location = location; this.type = type; this.routesThatStopHere = routesThatStopHere; this.lat = lat; this.lon = lon; this.gtfsID = gtfsID; } public @Nullable String routesThatStopHereToString() { // M E M O I Z A T I O N if(this.routesThatStopHereString != null) { return this.routesThatStopHereString; } // no string yet? build it! return buildRoutesString(); } @Nullable public String getAbsurdGTTPlaceName() { return absurdGTTPlaceName; } public void setAbsurdGTTPlaceName(@NonNull String absurdGTTPlaceName) { this.absurdGTTPlaceName = absurdGTTPlaceName; } public void setRoutesThatStopHere(@Nullable List routesThatStopHere) { this.routesThatStopHere = routesThatStopHere; } protected void setRoutesThatStopHereString(String routesStopping){ this.routesThatStopHereString = routesStopping; } + public int getNumRoutesStopping(){ + if(this.routesThatStopHere == null) { + return 0; + } else { + return this.routesThatStopHere.size(); + } + } + @Nullable protected List getRoutesThatStopHere(){ return routesThatStopHere; } protected @Nullable String buildRoutesString() { // no routes => no string if(this.routesThatStopHere == null || this.routesThatStopHere.size() == 0) { return null; } StringBuilder sb = new StringBuilder(); Collections.sort(routesThatStopHere,new LinesNameSorter()); int i, lenMinusOne = routesThatStopHere.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(routesThatStopHere.get(i)).append(", "); } // last one: sb.append(routesThatStopHere.get(i)); this.routesThatStopHereString = sb.toString(); return this.routesThatStopHereString; } @Override public int compareTo(@NonNull Stop other) { int res; int thisAsInt = networkTools.failsafeParseInt(this.ID); int otherAsInt = networkTools.failsafeParseInt(other.ID); // numeric stop IDs if(thisAsInt != 0 && otherAsInt != 0) { return thisAsInt - otherAsInt; } else { // non-numeric res = this.ID.compareTo(other.ID); if (res != 0) { return res; } } // try with name, then if(this.name != null && other.name != null) { res = this.name.compareTo(other.name); } // and give up return res; } /** * Sets a name. * * @param name stop name as string (not null) */ public final void setStopName(@NonNull String name) { this.name = name; } /** * Sets user name. Empty string is converted to null. * * @param name a string of non-zero length, or null */ public final void setStopUserName(@Nullable String name) { if(name == null) { this.username = null; } else if(name.length() == 0) { this.username = null; } else { this.username = name; } } /** * Returns stop name or username (if set).
* - empty string means "already searched everywhere, can't find it"
* - null means "didn't search, yet. Maybe you should try."
* - string means "here's the name.", obviously.
* * @return string if known, null if still unknown */ public final @Nullable String getStopDisplayName() { if(this.username == null) { return this.name; } else { return this.username; } } /** * Same as getStopDisplayName, only returns default name.
* I'd use an @see tag, but Android Studio is incapable of understanding that getStopDefaultName * refers to the method exactly above this one and not some arcane and esoteric unknown symbol. */ public final @Nullable String getStopDefaultName() { return this.name; } /** * Same as getStopDisplayName, only returns user name.
* Also, never an empty string. */ public final @Nullable String getStopUserName() { return this.username; } /** * Gets username and name from other stop if they exist, sets itself accordingly. * * @param other another Stop * @return did we actually set/change anything? */ public final boolean mergeNameFrom(Stop other) { boolean ret = false; if(other.name != null) { if(this.name == null || !this.name.equals(other.name)) { this.name = other.name; ret = true; } } if(other.username != null) { if(this.username == null || !this.username.equals(other.username)) { this.username = other.username; ret = true; } } return ret; } public final @Nullable String getGeoURL() { if(this.lat == null || this.lon == null) { return null; } // Android documentation suggests US for machine readable output (use dot as decimal separator) return String.format(Locale.US, "geo:%f,%f", this.lat, this.lon); } public final @Nullable String getGeoURLWithAddress() { String url = getGeoURL(); if(url == null) { return null; } if(this.location != null) { try { String addThis = "?q=".concat(URLEncoder.encode(this.location, "utf-8")); return url.concat(addThis); } catch (Exception ignored) {} } return url; } + @Override + public String toString(){ + return "id:"+ID+" { name: "+this.name+", lines: "+this.routesThatStopHereToString()+"}"; + } + @Nullable public Double getLatitude() { return lat; } @Nullable public Double getLongitude() { return lon; } public Double getDistanceFromLocation(IGeoPoint loc){ return getDistanceFromLocation(loc.getLatitude(), loc.getLongitude()); } public Double getDistanceFromLocation(double latitude, double longitude){ if(this.lat!=null && this.lon !=null) return utils.measuredistanceBetween(this.lat,this.lon,latitude, longitude); else return Double.POSITIVE_INFINITY; - }} + } + + public Bundle toBundle(Bundle bundle) { + //Bundle bundle = new Bundle(); + if(bundle==null) return null; + bundle.putString("ID", ID); + bundle.putString("name", name); + bundle.putString("username", username); + bundle.putString("location", location); + bundle.putString("type", (type != null) ? type.name() : null); + bundle.putStringArrayList("routesThatStopHere", (routesThatStopHere != null) ? new ArrayList<>(routesThatStopHere) : null); + if (lat != null) bundle.putDouble("lat", lat); + if (lon != null) bundle.putDouble("lon", lon); + if (gtfsID !=null) bundle.putString("gtfsID", gtfsID); + return bundle; + } + + public Bundle toBundle(){ + return toBundle(new Bundle()); + } + + @Nullable + public static Stop fromBundle(Bundle bundle) { + String ID = bundle.getString("ID"); + if (ID == null) return null; //throw new IllegalArgumentException("ID cannot be null"); + String name = bundle.getString("name"); + String username = bundle.getString("username"); + String location = bundle.getString("location"); + String typeStr = bundle.getString("type"); + Route.Type type = (typeStr != null) ? Route.Type.valueOf(typeStr) : null; + List routesThatStopHere = bundle.getStringArrayList("routesThatStopHere"); + Double lat = bundle.containsKey("lat") ? bundle.getDouble("lat") : null; + Double lon = bundle.containsKey("lon") ? bundle.getDouble("lon") : null; + String gtfsId = bundle.getString("gtfsID"); + + return new Stop(ID, name, username, location, type, routesThatStopHere, lat, lon, gtfsId); + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt index a604eff..86f30e5 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/LivePositionUpdate.kt @@ -1,65 +1,75 @@ /* BusTO - Backend components Copyright (C) 2023 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.backend.gtfs -import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship import com.google.transit.realtime.GtfsRealtime.VehiclePosition -import com.google.transit.realtime.GtfsRealtime.VehiclePosition.OccupancyStatus data class LivePositionUpdate( val tripID: String, //tripID WITHOUT THE "gtt:" prefix val startTime: String?, val startDate: String?, val routeID: String, val vehicle: String, - val latitude: Double, - val longitude: Double, - val bearing: Float?, - + var latitude: Double, + var longitude: Double, + var bearing: Float?, + //the timestamp IN SECONDS val timestamp: Long, val nextStop: String?, /*val vehicleInfo: VehicleInfo, val occupancyStatus: OccupancyStatus?, val scheduleRelationship: ScheduleRelationship? */ + //var tripInfo: TripAndPatternWithStops?, ){ constructor(position: VehiclePosition) : this( position.trip.tripId, position.trip.startTime, position.trip.startDate, position.trip.routeId, position.vehicle.label, position.position.latitude.toDouble(), position.position.longitude.toDouble(), position.position.bearing, position.timestamp, null ) /*data class VehicleInfo( val id: String, val label:String ) */ + /*fun withNewPositionAndBearing(latitude: Double, longitude: Double, bearing: Float) = + LivePositionUpdate(this.tripID, this.startTime, this.startTime, + this.routeID, this.vehicle, latitude, longitude, bearing, + this.timestamp,this.nextStop) + + fun withNewPosition(latitude: Double, longitude: Double) = + LivePositionUpdate(this.tripID, this.startTime, this.startTime, + this.routeID, this.vehicle, latitude, longitude, this.bearing, + this.timestamp,this.nextStop) + + */ } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/PolylineParser.java b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/PolylineParser.java index cac9905..4bdc54f 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/gtfs/PolylineParser.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/gtfs/PolylineParser.java @@ -1,48 +1,48 @@ package it.reyboz.bustorino.backend.gtfs; -import org.osmdroid.util.GeoPoint; +import org.maplibre.android.geometry.LatLng; import java.util.ArrayList; public abstract class PolylineParser { /** * Decode a Google polyline * Thanks to https://stackoverflow.com/questions/9341020/how-to-decode-googles-polyline-algorithm * @param encodedPolyline the encoded polyline in a string * @param initial_capacity for the list * @return the list of points correspoding to the polyline */ - public static ArrayList decodePolyline(String encodedPolyline, int initial_capacity) { - ArrayList points = new ArrayList<>(initial_capacity); + public static ArrayList decodePolyline(String encodedPolyline, int initial_capacity) { + ArrayList points = new ArrayList<>(initial_capacity); int truck = 0; int carriage_q = 0; int longit=0, latit=0; boolean is_lat=true; for (int x = 0, xx = encodedPolyline.length(); x < xx; ++x) { int i = encodedPolyline.charAt(x); i -= 63; int _5_bits = i << (32 - 5) >>> (32 - 5); truck |= _5_bits << carriage_q; carriage_q += 5; boolean is_last = (i & (1 << 5)) == 0; if (is_last) { boolean is_negative = (truck & 1) == 1; truck >>>= 1; if (is_negative) { truck = ~truck; } if (is_lat){ latit += truck; is_lat = false; } else{ longit += truck; - points.add(new GeoPoint((double)latit/1e5,(double)longit/1e5)); + points.add(new LatLng((double)latit/1e5,(double)longit/1e5)); is_lat=true; } carriage_q = 0; truck = 0; } } return points; } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt index 7c8dc8e..b6182fd 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt @@ -1,407 +1,407 @@ package it.reyboz.bustorino.backend.mato import android.app.Notification import android.app.NotificationManager import android.content.Context import android.os.Build import android.util.Log import androidx.lifecycle.LifecycleOwner import info.mqtt.android.service.Ack import info.mqtt.android.service.MqttAndroidClient import info.mqtt.android.service.QoS import it.reyboz.bustorino.backend.Notifications import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import org.eclipse.paho.client.mqttv3.* import org.json.JSONArray import org.json.JSONException import java.lang.ref.WeakReference import java.util.* typealias PositionsMap = HashMap > class MQTTMatoClient(): MqttCallbackExtended{ private var isStarted = false private var subscribedToAll = false private var client: MqttAndroidClient? = null //private var clientID = "" private val respondersMap = HashMap>>() private val currentPositions = PositionsMap() private lateinit var lifecycle: LifecycleOwner //TODO: remove class reference to context (always require context in all methods) private var context: Context?= null private var connectionTrials = 0 private var notification: Notification? = null //private lateinit var notification: Notification private fun connect(context: Context, iMqttActionListener: IMqttActionListener?){ val clientID = "mqtt-explorer-${getRandomString(8)}"//"mqttjs_${getRandomString(8)}" //notification = Notifications.makeMQTTServiceNotification(context) client = MqttAndroidClient(context,SERVER_ADDR,clientID,Ack.AUTO_ACK) // WE DO NOT WANT A FOREGROUND SERVICE -> it's only more mayhem // (and the positions need to be downloaded only when the app is shown) // update, 2024-04: Google Play doesn't understand our needs, so we put back the notification // and add a video of it working as Google wants /*if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ //we need a notification Notifications.createLivePositionsChannel(context) val notific = Notifications.makeMQTTServiceNotification(context) client!!.setForegroundService(notific) notification=notific }*/ val options = MqttConnectOptions() //options.sslProperties = options.isCleanSession = true val headersPars = Properties() headersPars.setProperty("Origin","https://mato.muoversiatorino.it") headersPars.setProperty("Host","mapi.5t.torino.it") options.customWebSocketHeaders = headersPars Log.d(DEBUG_TAG,"client name: $clientID") //actually connect - client!!.connect(options,null, iMqttActionListener) isStarted = true + client!!.connect(options,null, iMqttActionListener) client!!.setCallback(this) if (this.context ==null) this.context = context.applicationContext } override fun connectComplete(reconnect: Boolean, serverURI: String?) { Log.d(DEBUG_TAG, "Connected to server, reconnect: $reconnect") Log.d(DEBUG_TAG, "Have listeners: $respondersMap") } private fun connectTopic(topic: String){ if(context==null){ Log.e(DEBUG_TAG, "Trying to connect but context is null") return } connectionTrials += 1 connect(context!!, object : IMqttActionListener{ override fun onSuccess(asyncActionToken: IMqttToken?) { val disconnectedBufferOptions = DisconnectedBufferOptions() disconnectedBufferOptions.isBufferEnabled = true disconnectedBufferOptions.bufferSize = 100 disconnectedBufferOptions.isPersistBuffer = false disconnectedBufferOptions.isDeleteOldestMessages = false client!!.setBufferOpts(disconnectedBufferOptions) client!!.subscribe(topic, QoS.AtMostOnce.value) isStarted = true } override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { Log.e(DEBUG_TAG, "FAILED To connect to the server",exception) if (connectionTrials < 10) { Log.d(DEBUG_TAG, "Reconnecting") connectTopic(topic) } else { //reset connection trials connectionTrials = 0 } } }) } fun startAndSubscribe(lineId: String, responder: MQTTMatoListener, context: Context): Boolean{ //start the client, and then subscribe to the topic val topic = mapTopic(lineId) this.context = context.applicationContext synchronized(this) { if(!isStarted){ connectTopic(topic) //wait for connection } else { client!!.subscribe(topic, QoS.AtMostOnce.value) } } synchronized(this){ if (!respondersMap.contains(lineId)) respondersMap[lineId] = ArrayList() respondersMap[lineId]!!.add(WeakReference(responder)) Log.d(DEBUG_TAG, "Add MQTT Listener for line $lineId, topic $topic") } return true } fun stopMatoRequests(responder: MQTTMatoListener){ var removed = false for ((line,v)in respondersMap.entries){ var done = false for (el in v){ if (el.get()==null){ v.remove(el) } else if(el.get() == responder){ v.remove(el) done = true } if (done) break } if(done) Log.d(DEBUG_TAG, "Removed one listener for line $line, listeners: $v") //if (done) break if (v.isEmpty()){ //actually unsubscribe - client!!.unsubscribe( mapTopic(line)) + client?.unsubscribe( mapTopic(line)) } removed = done || removed } // check responders map, remove lines that have no responders for(line in respondersMap.keys){ if(respondersMap[line]?.isEmpty() == true){ respondersMap.remove(line) } } Log.d(DEBUG_TAG, "Removed: $removed, respondersMap: $respondersMap") } fun getPositions(): PositionsMap{ return currentPositions } /** * Cancel the notification */ fun removeNotification(context: Context){ val notifManager = context.applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notifManager.cancel(MQTT_NOTIFICATION_ID) } private fun sendUpdateToResponders(responders: ArrayList>): Int{ //var sent = false var count = 0 for (wrD in responders) { if (wrD.get() == null) { Log.d(DEBUG_TAG, "Removing weak reference") responders.remove(wrD) } else { wrD.get()!!.onUpdateReceived(currentPositions) //sent = true count++ } } return count } override fun connectionLost(cause: Throwable?) { var doReconnect = false for ((line,elms) in respondersMap.entries){ if(!elms.isEmpty()){ doReconnect = true break } } if (!doReconnect){ Log.d(DEBUG_TAG, "Disconnected, but no responders to give the positions, avoid reconnecting") //finish here return } Log.w(DEBUG_TAG, "Lost connection in MQTT Mato Client") synchronized(this){ // isStarted = false //var i = 0 // while(i < 20 && !isStarted) { connect(context!!, object: IMqttActionListener{ override fun onSuccess(asyncActionToken: IMqttToken?) { //relisten to messages for ((line,elms) in respondersMap.entries){ val topic = mapTopic(line) if(elms.isEmpty()) respondersMap.remove(line) else { client!!.subscribe(topic, QoS.AtMostOnce.value, null, null) Log.d(DEBUG_TAG, "Resubscribed with topic $topic") } } Log.d(DEBUG_TAG, "Reconnected to MQTT Mato Client") } override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { Log.w(DEBUG_TAG, "Failed to reconnect to MQTT server") } }) } } override fun messageArrived(topic: String?, message: MqttMessage?) { if (topic==null || message==null) return //Log.d(DEBUG_TAG,"Arrived message on topic $topic, ${String(message.payload)}") parseMessageAndAddToList(topic, message) //GlobalScope.launch { } } private fun parseMessageAndAddToList(topic: String, message: MqttMessage){ val vals = topic.split("/") val lineId = vals[1] val vehicleId = vals[2] val timestamp = (System.currentTimeMillis() / 1000 ) as Long val messString = String(message.payload) try { val jsonList = JSONArray(messString) /*val posUpdate = MQTTPositionUpdate(lineId+"U", vehicleId, jsonList.getDouble(0), jsonList.getDouble(1), if(jsonList.get(2).equals(null)) null else jsonList.getInt(2), if(jsonList.get(3).equals(null)) null else jsonList.getInt(3), if(jsonList.get(4).equals(null)) null else jsonList.getString(4)+"U", if(jsonList.get(5).equals(null)) null else jsonList.getInt(5), if(jsonList.get(6).equals(null)) null else jsonList.getInt(6), //full ) */ if(jsonList.get(4)==null){ Log.d(DEBUG_TAG, "We have null tripId: line $lineId veh $vehicleId: $jsonList") return } val posUpdate = LivePositionUpdate( jsonList.getString(4)+"U", null, null, lineId+"U", vehicleId, jsonList.getDouble(0), //latitude jsonList.getDouble(1), //longitude if(jsonList.get(2).equals(null)) null else jsonList.getInt(2).toFloat(), //"heading" (same as bearing?) timestamp, if(jsonList.get(6).equals(null)) null else jsonList.getInt(6).toString() //nextStop ) //add update var valid = false if(!currentPositions.contains(lineId)) currentPositions[lineId] = HashMap() currentPositions[lineId]?.let{ it[vehicleId] = posUpdate valid = true } //sending //Log.d(DEBUG_TAG, "Parsed update on topic $topic, line $lineId, responders $respondersMap") var cc = 0 if (LINES_ALL in respondersMap.keys) { val count = sendUpdateToResponders(respondersMap[LINES_ALL]!!) cc +=count } if(lineId in respondersMap.keys){ cc += sendUpdateToResponders(respondersMap[lineId]!!) } //Log.d(DEBUG_TAG, "Sent to $cc responders, have $respondersMap") if(cc==0){ Log.w(DEBUG_TAG, "We have received an update but apparently there is no one to send it") var emptyResp = true for(en in respondersMap.values){ if(!en.isEmpty()){ emptyResp=false break } } //try unsubscribing to all if(emptyResp) { Log.d(DEBUG_TAG, "Unsubscribe all") client!!.unsubscribe(LINES_ALL) } } //Log.d(DEBUG_TAG, "We have update on line $lineId, vehicle $vehicleId") } catch (e: JSONException){ Log.w(DEBUG_TAG,"Cannot decipher message on topic $topic, line $lineId, veh $vehicleId (bad JSON)") } catch (e: Exception){ Log.e(DEBUG_TAG, "Exception occurred", e) } } override fun deliveryComplete(token: IMqttDeliveryToken?) { //NOT USED (we're not sending any messages) } /*/** * Stop the service forever. Client has not to be used again!! */ fun closeClientForever(){ client.disconnect() client.close() }*/ fun disconnect(){ client?.disconnect() } companion object{ const val SERVER_ADDR="wss://mapi.5t.torino.it:443/scre" const val LINES_ALL="ALL" private const val DEBUG_TAG="BusTO-MatoMQTT" //this has to match the value in MQTT library (MQTTAndroidClient) const val MQTT_NOTIFICATION_ID: Int = 77 @JvmStatic fun mapTopic(lineId: String): String{ return if(lineId== LINES_ALL || lineId == "#") "#" else{ "/${lineId}/#" } } fun getRandomString(length: Int) : String { val allowedChars = ('a'..'f') + ('0'..'9') return (1..length) .map { allowedChars.random() } .joinToString("") } fun interface MQTTMatoListener{ //positionsMap is a dict with line -> vehicle -> Update fun onUpdateReceived(posUpdates: PositionsMap) } } } data class MQTTPositionUpdate( val lineId: String, val vehicleId: String, val latitude: Double, val longitude: Double, val heading: Int?, val speed: Int?, val tripId: String?, val direct: Int?, val nextStop: Int?, //val full: Int? ) \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java b/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java index 82c832e..c08a194 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java +++ b/app/src/main/java/it/reyboz/bustorino/data/PreferencesHolder.java @@ -1,94 +1,108 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data; import android.content.Context; import android.content.SharedPreferences; +import androidx.annotation.NonNull; import it.reyboz.bustorino.R; import static android.content.Context.MODE_PRIVATE; import androidx.preference.PreferenceManager; +import it.reyboz.bustorino.fragments.SettingsFragment; +import it.reyboz.bustorino.map.MapLibreUtils; import java.util.HashSet; import java.util.Set; /** * Static class for commonly used SharedPreference operations */ public abstract class PreferencesHolder { public static final String PREF_GTFS_DB_VERSION = "gtfs_db_version"; public static final String PREF_INTRO_ACTIVITY_RUN ="pref_intro_activity_run"; public static final String DB_GTT_VERSION_KEY = "NextGenDB.GTTVersion"; public static final String DB_LAST_UPDATE_KEY = "NextGenDB.LastDBUpdate"; public static final String PREF_FAVORITE_LINES = "pref_favorite_lines"; public static final Set KEYS_MERGE_SET = Set.of(PREF_FAVORITE_LINES); public static final Set IGNORE_KEYS_LOAD_MAIN = Set.of(PREF_GTFS_DB_VERSION, PREF_INTRO_ACTIVITY_RUN, DB_GTT_VERSION_KEY, DB_LAST_UPDATE_KEY); public static SharedPreferences getMainSharedPreferences(Context context){ return context.getSharedPreferences(context.getString(R.string.mainSharedPreferences), MODE_PRIVATE); } public static SharedPreferences getAppPreferences(Context con){ return PreferenceManager.getDefaultSharedPreferences(con); } public static int getGtfsDBVersion(SharedPreferences pref){ return pref.getInt(PREF_GTFS_DB_VERSION,-1); } public static void setGtfsDBVersion(SharedPreferences pref,int version){ SharedPreferences.Editor ed = pref.edit(); ed.putInt(PREF_GTFS_DB_VERSION,version); ed.apply(); } /** * Check if the introduction activity has been run at least one * @param con the context needed * @return true if it has been run */ public static boolean hasIntroFinishedOneShot(Context con){ final SharedPreferences pref = getMainSharedPreferences(con); return pref.getBoolean(PREF_INTRO_ACTIVITY_RUN, false); } public static boolean addOrRemoveLineToFavorites(Context con, String gtfsLineId, boolean addToFavorites){ final SharedPreferences pref = getMainSharedPreferences(con); final HashSet favorites = new HashSet<>(pref.getStringSet(PREF_FAVORITE_LINES, new HashSet<>())); boolean modified = true; if(addToFavorites) favorites.add(gtfsLineId); else if(favorites.contains(gtfsLineId)) favorites.remove(gtfsLineId); else modified = false; // we are not changing anything if(modified) { final SharedPreferences.Editor editor = pref.edit(); editor.putStringSet(PREF_FAVORITE_LINES, favorites); editor.apply(); } return modified; } - public static HashSet getFavoritesLinesGtfsIDs(Context con){ + public static HashSet getFavoritesLinesGtfsIDs(@NonNull Context con){ final SharedPreferences pref = getMainSharedPreferences(con); return new HashSet<>(pref.getStringSet(PREF_FAVORITE_LINES, new HashSet<>())); } + + public static String getMapLibreStyleFile(Context con){ + final SharedPreferences pref = getAppPreferences(con); + final String mapStyle_val = pref.getString(SettingsFragment.LIBREMAP_STYLE_PREF_KEY, ""); + return switch (mapStyle_val) { + //MUST MATCH IN keys.xml -> map_style_pref_values + case "versatiles_c" -> MapLibreUtils.STYLE_VERSATILES_COLORFUL_JSON; + case "osm_legacy" -> MapLibreUtils.STYLE_OSM_RASTER; + default -> MapLibreUtils.getDefaultStyleJson(); + }; + } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java index f7b88cf..f574581 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java @@ -1,710 +1,710 @@ /* BusTO - Fragments components Copyright (C) 2018 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.widget.*; import androidx.annotation.Nullable; import androidx.annotation.NonNull; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.Collections; import java.util.List; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.PalinaAdapter; import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter; import it.reyboz.bustorino.backend.ArrivalsFetcher; import it.reyboz.bustorino.backend.DBStatusManager; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTNormalizer; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Passaggio; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.data.UserDB; import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; import it.reyboz.bustorino.util.LinesNameSorter; import static it.reyboz.bustorino.fragments.ScreenBaseFragment.setOption; public class ArrivalsFragment extends ResultBaseFragment implements LoaderManager.LoaderCallbacks { private static final String OPTION_SHOW_LEGEND = "show_legend"; private final static String KEY_STOP_ID = "stopid"; private final static String KEY_STOP_NAME = "stopname"; private final static String DEBUG_TAG_ALL = "BUSTOArrivalsFragment"; private String DEBUG_TAG = DEBUG_TAG_ALL; private final static int loaderFavId = 2; private final static int loaderStopId = 1; static final String STOP_TITLE = "messageExtra"; private final static String SOURCES_TEXT="sources_textview_message"; private @Nullable String stopID,stopName; private DBStatusManager prefs; private DBStatusManager.OnDBUpdateStatusChangeListener listener; private boolean justCreated = false; private Palina lastUpdatedPalina = null; private boolean needUpdateOnAttach = false; private boolean fetchersChangeRequestPending = false; private boolean stopIsInFavorites = false; //Views protected ImageButton addToFavorites; protected TextView timesSourceTextView; protected TextView messageTextView; protected RecyclerView arrivalsRecyclerView; private PalinaAdapter mListAdapter = null; private TextView howDoesItWorkTextView; private Button hideHintButton; //private NestedScrollView theScrollView; protected RecyclerView noArrivalsRecyclerView; private RouteOnlyLineAdapter noArrivalsAdapter; private TextView noArrivalsTitleView; private GridLayoutManager layoutManager; //private View canaryEndView; private List fetchers = null; //new ArrayList<>(Arrays.asList(utils.getDefaultArrivalsFetchers())); private boolean reloadOnResume = true; private final PalinaAdapter.PalinaClickListener palinaClickListener = new PalinaAdapter.PalinaClickListener() { @Override public void showRouteFullDirection(Route route) { String routeName; Log.d(DEBUG_TAG, "Make toast for line "+route.getName()); routeName = FiveTNormalizer.routeInternalToDisplay(route.getName()); if (routeName == null) { routeName = route.getDisplayCode(); } if(getContext()==null) Log.e(DEBUG_TAG, "Touched on a route but Context is null"); else if (route.destinazione == null || route.destinazione.length() == 0) { Toast.makeText(getContext(), getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), getString(R.string.route_towards_destination, routeName, route.destinazione), Toast.LENGTH_SHORT).show(); } } @Override public void requestShowingRoute(Route route) { Log.d(DEBUG_TAG, "Need to show line for route:\ngtfsID "+route.getGtfsId()+ " name "+route.getName()); if(route.getGtfsId()!=null){ - mListener.showLineOnMap(route.getGtfsId()); + mListener.showLineOnMap(route.getGtfsId(), stopID); } else { String gtfsID = FiveTNormalizer.getGtfsRouteID(route); Log.d(DEBUG_TAG, "GtfsID for route is: " + gtfsID); - mListener.showLineOnMap(gtfsID); + mListener.showLineOnMap(gtfsID, stopID); } } }; public static ArrivalsFragment newInstance(String stopID){ return newInstance(stopID, null); } public static ArrivalsFragment newInstance(@NonNull String stopID, @Nullable String stopName){ ArrivalsFragment fragment = new ArrivalsFragment(); Bundle args = new Bundle(); args.putString(KEY_STOP_ID,stopID); //parameter for ResultListFragmentrequestArrivalsForStopID //args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS); if (stopName != null){ args.putString(KEY_STOP_NAME,stopName); } fragment.setArguments(args); return fragment; } public static String getFragmentTag(Palina p) { return "palina_"+p.ID; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); stopID = getArguments().getString(KEY_STOP_ID); DEBUG_TAG = DEBUG_TAG_ALL+" "+stopID; //this might really be null stopName = getArguments().getString(KEY_STOP_NAME); final ArrivalsFragment arrivalsFragment = this; listener = new DBStatusManager.OnDBUpdateStatusChangeListener() { @Override public void onDBStatusChanged(boolean updating) { if(!updating){ getLoaderManager().restartLoader(loaderFavId,getArguments(),arrivalsFragment); } else { final LoaderManager lm = getLoaderManager(); lm.destroyLoader(loaderFavId); lm.destroyLoader(loaderStopId); } } @Override public boolean defaultStatusValue() { return true; } }; prefs = new DBStatusManager(getContext().getApplicationContext(),listener); justCreated = true; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_arrivals, container, false); messageTextView = root.findViewById(R.id.messageTextView); addToFavorites = root.findViewById(R.id.addToFavorites); // "How does it work part" howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView); hideHintButton = root.findViewById(R.id.hideHintButton); hideHintButton.setOnClickListener(this::onHideHint); //theScrollView = root.findViewById(R.id.arrivalsScrollView); // recyclerview holding the arrival times arrivalsRecyclerView = root.findViewById(R.id.arrivalsRecyclerView); final LinearLayoutManager manager = new LinearLayoutManager(getContext()); arrivalsRecyclerView.setLayoutManager(manager); final DividerItemDecoration mDividerItemDecoration = new DividerItemDecoration(arrivalsRecyclerView.getContext(), manager.getOrientation()); arrivalsRecyclerView.addItemDecoration(mDividerItemDecoration); timesSourceTextView = root.findViewById(R.id.timesSourceTextView); timesSourceTextView.setOnLongClickListener(view -> { if(!fetchersChangeRequestPending){ rotateFetchers(); //Show we are changing provider timesSourceTextView.setText(R.string.arrival_source_changing); mListener.requestArrivalsForStopID(stopID); fetchersChangeRequestPending = true; return true; } return false; }); timesSourceTextView.setOnClickListener(view -> { Toast.makeText(getContext(), R.string.change_arrivals_source_message, Toast.LENGTH_SHORT) .show(); }); //Button addToFavorites.setClickable(true); addToFavorites.setOnClickListener(v -> { // add/remove the stop in the favorites toggleLastStopToFavorites(); }); String displayName = getArguments().getString(STOP_TITLE); if(displayName!=null) setTextViewMessage(String.format( getString(R.string.passages), displayName)); String probablemessage = getArguments().getString(MESSAGE_TEXT_VIEW); if (probablemessage != null) { //Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage); messageTextView.setText(probablemessage); messageTextView.setVisibility(View.VISIBLE); } //no arrivals stuff noArrivalsRecyclerView = root.findViewById(R.id.noArrivalsRecyclerView); layoutManager = new GridLayoutManager(getContext(),60); layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { return 12; } }); noArrivalsRecyclerView.setLayoutManager(layoutManager); noArrivalsTitleView = root.findViewById(R.id.noArrivalsMessageTextView); //canaryEndView = root.findViewById(R.id.canaryEndView); /*String sourcesTextViewData = getArguments().getString(SOURCES_TEXT); if (sourcesTextViewData!=null){ timesSourceTextView.setText(sourcesTextViewData); }*/ //need to do this when we recreate the fragment but we haven't updated the arrival times if (lastUpdatedPalina!=null) showArrivalsSources(lastUpdatedPalina); return root; } @Override public void onResume() { super.onResume(); LoaderManager loaderManager = getLoaderManager(); Log.d(DEBUG_TAG, "OnResume, justCreated "+justCreated+", lastUpdatedPalina is: "+lastUpdatedPalina); /*if(needUpdateOnAttach){ updateFragmentData(null); needUpdateOnAttach=false; }*/ /*if(lastUpdatedPalina!=null){ updateFragmentData(null); showArrivalsSources(lastUpdatedPalina); }*/ mListener.readyGUIfor(FragmentKind.ARRIVALS); if (mListAdapter!=null) resetListAdapter(mListAdapter); if(noArrivalsAdapter!=null){ noArrivalsRecyclerView.setAdapter(noArrivalsAdapter); } if(stopID!=null){ if(!justCreated){ fetchers = utils.getDefaultArrivalsFetchers(getContext()); adjustFetchersToSource(); if (reloadOnResume) mListener.requestArrivalsForStopID(stopID); } else justCreated = false; //start the loader if(prefs.isDBUpdating(true)){ prefs.registerListener(); } else { Log.d(DEBUG_TAG, "Restarting loader for stop"); loaderManager.restartLoader(loaderFavId, getArguments(), this); } updateMessage(); } if (ScreenBaseFragment.getOption(requireContext(),OPTION_SHOW_LEGEND, true)) { showHints(); } } @Override public void onStart() { super.onStart(); if (needUpdateOnAttach){ updateFragmentData(null); needUpdateOnAttach = false; } } @Override public void onPause() { if(listener!=null) prefs.unregisterListener(); super.onPause(); LoaderManager loaderManager = getLoaderManager(); Log.d(DEBUG_TAG, "onPause, have running loaders: "+loaderManager.hasRunningLoaders()); loaderManager.destroyLoader(loaderFavId); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); //get fetchers fetchers = utils.getDefaultArrivalsFetchers(context); } @Nullable public String getStopID() { return stopID; } public boolean reloadsOnResume() { return reloadOnResume; } public void setReloadOnResume(boolean reloadOnResume) { this.reloadOnResume = reloadOnResume; } // HINT "HOW TO USE" private void showHints() { howDoesItWorkTextView.setVisibility(View.VISIBLE); hideHintButton.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void hideHints() { howDoesItWorkTextView.setVisibility(View.GONE); hideHintButton.setVisibility(View.GONE); //actionHelpMenuItem.setVisible(true); } public void onHideHint(View v) { hideHints(); setOption(requireContext(),OPTION_SHOW_LEGEND, false); } /** * Give the fetchers * @return the list of the fetchers */ public ArrayList getCurrentFetchers(){ return new ArrayList<>(this.fetchers); } public ArrivalsFetcher[] getCurrentFetchersAsArray(){ ArrivalsFetcher[] arr = new ArrivalsFetcher[fetchers.size()]; fetchers.toArray(arr); return arr; } private void rotateFetchers(){ Log.d(DEBUG_TAG, "Rotating fetchers, before: "+fetchers); Collections.rotate(fetchers, -1); Log.d(DEBUG_TAG, "Rotating fetchers, afterwards: "+fetchers); } /** * Update the UI with the new data * @param p the full Palina */ public void updateFragmentData(@Nullable Palina p){ if (p!=null) lastUpdatedPalina = p; if (!isAdded()){ //defer update at next show if (p==null) Log.w(DEBUG_TAG, "Asked to update the data, but we're not attached and the data is null"); else needUpdateOnAttach = true; } else { final PalinaAdapter adapter = new PalinaAdapter(getContext(), lastUpdatedPalina, palinaClickListener, true); showArrivalsSources(lastUpdatedPalina); resetListAdapter(adapter); final ArrayList routesWithNoPassages = lastUpdatedPalina.getRoutesNamesWithNoPassages(); if(routesWithNoPassages.isEmpty()){ //hide the views if there are no empty routes noArrivalsRecyclerView.setVisibility(View.GONE); noArrivalsTitleView.setVisibility(View.GONE); }else{ Collections.sort(routesWithNoPassages, new LinesNameSorter()); noArrivalsAdapter = new RouteOnlyLineAdapter(routesWithNoPassages, null); if(noArrivalsRecyclerView!=null){ noArrivalsRecyclerView.setAdapter(noArrivalsAdapter); noArrivalsRecyclerView.setVisibility(View.VISIBLE); noArrivalsTitleView.setVisibility(View.VISIBLE); } } //canaryEndView.setVisibility(View.VISIBLE); //check if canaryEndView is visible //boolean isCanaryVisibile = ViewUtils.Companion.isViewPartiallyVisibleInScroll(canaryEndView, theScrollView); //Log.d(DEBUG_TAG, "Canary view fully visibile: "+isCanaryVisibile); } } /** * Set the message of the arrival times source * @param p Palina with the arrival times */ protected void showArrivalsSources(Palina p){ final Passaggio.Source source = p.getPassaggiSourceIfAny(); if (source == null){ Log.e(DEBUG_TAG, "NULL SOURCE"); return; } String source_txt; switch (source){ case GTTJSON: source_txt = getString(R.string.gttjsonfetcher); break; case FiveTAPI: source_txt = getString(R.string.fivetapifetcher); break; case FiveTScraper: source_txt = getString(R.string.fivetscraper); break; case MatoAPI: source_txt = getString(R.string.source_mato); break; case UNDETERMINED: //Don't show the view source_txt = getString(R.string.undetermined_source); break; default: throw new IllegalStateException("Unexpected value: " + source); } // final boolean updatedFetchers = adjustFetchersToSource(source); if(!updatedFetchers) Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work"); final String base_message = getString(R.string.times_source_fmt, source_txt); timesSourceTextView.setText(base_message); timesSourceTextView.setVisibility(View.VISIBLE); if (p.getTotalNumberOfPassages() > 0) { timesSourceTextView.setVisibility(View.VISIBLE); } else { timesSourceTextView.setVisibility(View.INVISIBLE); } fetchersChangeRequestPending = false; } protected boolean adjustFetchersToSource(Passaggio.Source source){ if (source == null) return false; int count = 0; if (source!= Passaggio.Source.UNDETERMINED) while (source != fetchers.get(0).getSourceForFetcher() && count < 200){ //we need to update the fetcher that is requested rotateFetchers(); count++; } return count < 200; } protected boolean adjustFetchersToSource(){ if (lastUpdatedPalina == null) return false; final Passaggio.Source source = lastUpdatedPalina.getPassaggiSourceIfAny(); return adjustFetchersToSource(source); } /** * Update the message in the fragment * * It may eventually change the "Add to Favorite" icon */ private void updateMessage(){ String message = null; if (stopName != null && stopID != null && !stopName.isEmpty()) { message = (stopID.concat(" - ").concat(stopName)); } else if(stopID!=null) { message = stopID; } else { Log.e("ArrivalsFragm"+getTag(),"NO ID FOR THIS FRAGMENT - something went horribly wrong"); } if(message!=null) { setTextViewMessage(getString(R.string.passages,message)); } // whatever is the case, update the star icon //updateStarIconFromLastBusStop(); } @NonNull @Override public Loader onCreateLoader(int id, Bundle args) { if(args.getString(KEY_STOP_ID)==null) return null; final String stopID = args.getString(KEY_STOP_ID); final Uri.Builder builder = AppDataProvider.getUriBuilderToComplete(); CursorLoader cl; switch (id){ case loaderFavId: builder.appendPath("favorites").appendPath(stopID); cl = new CursorLoader(getContext(),builder.build(),UserDB.getFavoritesColumnNamesAsArray,null,null,null); break; case loaderStopId: builder.appendPath("stop").appendPath(stopID); cl = new CursorLoader(getContext(),builder.build(),new String[]{NextGenDB.Contract.StopsTable.COL_NAME}, null,null,null); break; default: return null; } cl.setUpdateThrottle(500); return cl; } @Override public void onLoadFinished(Loader loader, Cursor data) { switch (loader.getId()){ case loaderFavId: final int colUserName = data.getColumnIndex(UserDB.getFavoritesColumnNamesAsArray[1]); if(data.getCount()>0){ // IT'S IN FAVORITES data.moveToFirst(); final String probableName = data.getString(colUserName); stopIsInFavorites = true; if (probableName != null && !probableName.isEmpty()) stopName = probableName; //set the stop //update the message in the textview updateMessage(); } else { stopIsInFavorites =false; } updateStarIcon(); if(stopName == null){ //stop is not inside the favorites and wasn't provided Log.d("ArrivalsFragment"+getTag(),"Stop wasn't in the favorites and has no name, looking in the DB"); getLoaderManager().restartLoader(loaderStopId,getArguments(),this); } break; case loaderStopId: if(data.getCount()>0){ data.moveToFirst(); int index = data.getColumnIndex( NextGenDB.Contract.StopsTable.COL_NAME ); if (index == -1){ Log.e(DEBUG_TAG, "Index is -1, column not present. App may explode now..."); } stopName = data.getString(index); updateMessage(); } else { Log.w("ArrivalsFragment"+getTag(),"Stop is not inside the database... CLOISTER BELL"); } } } @Override public void onLoaderReset(Loader loader) { //NOTHING TO DO } protected void resetListAdapter(PalinaAdapter adapter) { mListAdapter = adapter; if (arrivalsRecyclerView != null) { arrivalsRecyclerView.setAdapter(adapter); arrivalsRecyclerView.setVisibility(View.VISIBLE); } } /** * Set the message textView * @param message the whole message to write in the textView */ public void setTextViewMessage(String message) { messageTextView.setText(message); messageTextView.setVisibility(View.VISIBLE); } public void toggleLastStopToFavorites() { Stop stop = lastUpdatedPalina; if (stop != null) { // toggle the status in background new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.TOGGLE, v->updateStarIconFromLastBusStop(v)).execute(stop); } else { // this case have no sense, but just immediately update the favorite icon updateStarIconFromLastBusStop(true); } } /** * Update the star "Add to favorite" icon */ public void updateStarIconFromLastBusStop(Boolean toggleDone) { if (stopIsInFavorites) stopIsInFavorites = !toggleDone; else stopIsInFavorites = toggleDone; updateStarIcon(); // check if there is a last Stop /* if (stopID == null) { addToFavorites.setVisibility(View.INVISIBLE); } else { // filled or outline? if (isStopInFavorites(stopID)) { addToFavorites.setImageResource(R.drawable.ic_star_filled); } else { addToFavorites.setImageResource(R.drawable.ic_star_outline); } addToFavorites.setVisibility(View.VISIBLE); } */ } /** * Update the star icon according to `stopIsInFavorites` */ public void updateStarIcon() { // no favorites no party! // check if there is a last Stop if (stopID == null) { addToFavorites.setVisibility(View.INVISIBLE); } else { // filled or outline? if (stopIsInFavorites) { addToFavorites.setImageResource(R.drawable.ic_star_filled); } else { addToFavorites.setImageResource(R.drawable.ic_star_outline); } addToFavorites.setVisibility(View.VISIBLE); } } @Override public void onDestroyView() { arrivalsRecyclerView = null; if(getArguments()!=null) { getArguments().putString(SOURCES_TEXT, timesSourceTextView.getText().toString()); getArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.getText().toString()); } super.onDestroyView(); } public boolean isFragmentForTheSameStop(Palina p) { if (getTag() != null) return getTag().equals(getFragmentTag(p)); else return false; } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java b/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java index 03e6831..7c4f935 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/CommonFragmentListener.java @@ -1,45 +1,47 @@ package it.reyboz.bustorino.fragments; import android.location.Location; import android.view.View; import it.reyboz.bustorino.backend.Stop; +import javax.annotation.Nullable; + public interface CommonFragmentListener { /** * Tell the activity that we need to disable/enable its floatingActionButton * @param yes or no */ void showFloatingActionButton(boolean yes); /** * Sends the message to the activity to adapt the GUI * to the fragment that has been attached * @param fragmentType the type of fragment attached */ void readyGUIfor(FragmentKind fragmentType); /** * Houston, we need another fragment! * * @param ID the Stop ID */ void requestArrivalsForStopID(String ID); /** * Method to call when we want to hide the keyboard */ void hideKeyboard(); /** * We want to open the map on the specified stop * @param stop needs to have location data (latitude, longitude) */ void showMapCenteredOnStop(Stop stop); /** * We want to show the line in detail for route * @param routeGtfsId the route gtfsID (eg, "gtt:10U") */ - void showLineOnMap(String routeGtfsId); + void showLineOnMap(String routeGtfsId,@Nullable String fromStopID); } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt new file mode 100644 index 0000000..4104d0f --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/GeneralMapLibreFragment.kt @@ -0,0 +1,171 @@ +package it.reyboz.bustorino.fragments + +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.preference.PreferenceManager +import com.google.gson.JsonObject +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.data.PreferencesHolder +import org.maplibre.android.MapLibre +import org.maplibre.android.camera.CameraPosition +import org.maplibre.android.geometry.LatLng +import org.maplibre.android.maps.MapLibreMap +import org.maplibre.android.maps.MapView +import org.maplibre.android.maps.OnMapReadyCallback +import org.maplibre.android.maps.Style +import org.maplibre.android.style.sources.GeoJsonSource +import org.maplibre.geojson.Feature +import org.maplibre.geojson.Point + +abstract class GeneralMapLibreFragment: ScreenBaseFragment(), OnMapReadyCallback { + protected var map: MapLibreMap? = null + protected var shownStopInBottomSheet : Stop? = null + protected var savedMapStateOnPause : Bundle? = null + + // Declare a variable for MapView + protected lateinit var mapView: MapView + protected lateinit var mapStyle: Style + protected lateinit var stopsSource: GeoJsonSource + protected lateinit var busesSource: GeoJsonSource + protected lateinit var selectedStopSource: GeoJsonSource + + protected lateinit var sharedPreferences: SharedPreferences + + private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener(){ pref, key -> + /*when(key){ + SettingsFragment.LIBREMAP_STYLE_PREF_KEY -> reloadMap() + } + + */ + if(key == SettingsFragment.LIBREMAP_STYLE_PREF_KEY){ + Log.d(DEBUG_TAG,"ASKING RELOAD OF MAP") + + reloadMap() + } + } + + private var lastMapStyle ="" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + //sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + lastMapStyle = PreferencesHolder.getMapLibreStyleFile(requireContext()) + + //init map + MapLibre.getInstance(requireContext()) + + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + lastMapStyle = PreferencesHolder.getMapLibreStyleFile(requireContext()) + Log.d(DEBUG_TAG, "onCreateView lastMapStyle: $lastMapStyle") + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onResume() { + super.onResume() + val newMapStyle = PreferencesHolder.getMapLibreStyleFile(requireContext()) + Log.d(DEBUG_TAG, "onResume newMapStyle: $newMapStyle, lastMapStyle: $lastMapStyle") + if(newMapStyle!=lastMapStyle){ + reloadMap() + } + } + + override fun onLowMemory() { + super.onLowMemory() + mapView.onLowMemory() + } + + + protected fun reloadMap(){ + /*map?.let { + Log.d("GeneralMapFragment", "RELOADING MAP") + //save map state + savedMapStateOnPause = saveMapStateInBundle() + + onMapDestroy() + //Destroy and recreate MAP + mapView.onDestroy() + mapView.onCreate(null) + mapView.getMapAsync(this) + } + + */ + //TODO figure out how to switch map safely + } + + abstract fun openStopInBottomSheet(stop: Stop) + + //For extra stuff to do when the map is destroyed + abstract fun onMapDestroy() + + protected fun restoreMapStateFromBundle(bundle: Bundle){ + val nullDouble = -10_000.0 + val latCenter = bundle.getDouble("center_map_lat", -10.0) + val lonCenter = bundle.getDouble("center_map_lon",-10.0) + val zoom = bundle.getDouble("map_zoom", -10.0) + val bearing = bundle.getDouble("map_bearing", nullDouble) + val tilt = bundle.getDouble("map_tilt", nullDouble) + if(lonCenter>=0 &&latCenter>=0) map?.let { + val newPos = CameraPosition.Builder().target(LatLng(latCenter,lonCenter)) + if(zoom>0) newPos.zoom(zoom) + if(bearing!=nullDouble) newPos.bearing(bearing) + if(tilt != nullDouble) newPos.tilt(tilt) + it.cameraPosition=newPos.build() + + } + val mStop = bundle.getBundle("shown_stop")?.let { + Stop.fromBundle(it) + } + mStop?.let { openStopInBottomSheet(it) } + } + + protected fun saveMapStateBeforePause(bundle: Bundle){ + map?.let { + val newBbox = it.projection.visibleRegion.latLngBounds + + + val cp = it.cameraPosition + bundle.putDouble("center_map_lat", newBbox.center.latitude) + bundle.putDouble("center_map_lon", newBbox.center.longitude) + it.cameraPosition.zoom.let { z-> bundle.putDouble("map_zoom",z) } + bundle.putDouble("map_bearing",cp.bearing) + bundle.putDouble("map_tilt", cp.tilt) + } + shownStopInBottomSheet?.let { + bundle.putBundle("shown_stop", it.toBundle()) + } + } + + protected fun saveMapStateInBundle(): Bundle { + val b = Bundle() + saveMapStateBeforePause(b) + return b + } + + protected fun stopToGeoJsonFeature(s: Stop): Feature{ + return Feature.fromGeometry( + Point.fromLngLat(s.longitude!!, s.latitude!!), + JsonObject().apply { + addProperty("id", s.ID) + addProperty("name", s.stopDefaultName) + //addProperty("routes", s.routesThatStopHereToString()) // Add routes array to JSON object + } + ) + } + + companion object{ + private const val DEBUG_TAG="GeneralMapLibreFragment" + + const val BUSES_SOURCE_ID = "buses-source" + const val BUSES_LAYER_ID = "buses-layer" + + const val SEL_STOP_SOURCE="selected-stop-source" + const val SEL_STOP_LAYER = "selected-stop-layer" + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt index ae9d5d9..4497770 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -1,907 +1,1567 @@ /* BusTO - Fragments components Copyright (C) 2023 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments + +import android.Manifest import android.animation.ObjectAnimator +import android.animation.ValueAnimator import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences -import android.graphics.Paint +import android.content.res.ColorStateList +import android.location.Location import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.animation.LinearInterpolator import android.widget.* +import androidx.activity.result.ActivityResultCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.content.res.AppCompatResources +import androidx.cardview.widget.CardView import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat +import androidx.core.view.ViewCompat import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.gson.JsonObject import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.NameCapitalize import it.reyboz.bustorino.adapters.StopAdapterListener import it.reyboz.bustorino.adapters.StopRecyclerAdapter import it.reyboz.bustorino.backend.FiveTNormalizer +import it.reyboz.bustorino.backend.LivePositionTripPattern import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.backend.gtfs.GtfsUtils import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.gtfs.PolylineParser import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.MatoTripsDownloadWorker import it.reyboz.bustorino.data.PreferencesHolder -import it.reyboz.bustorino.data.gtfs.MatoPattern import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import it.reyboz.bustorino.map.* -import it.reyboz.bustorino.map.CustomInfoWindow.TouchResponder import it.reyboz.bustorino.middleware.LocationUtils import it.reyboz.bustorino.util.Permissions +import it.reyboz.bustorino.util.ViewUtils import it.reyboz.bustorino.viewmodels.LinesViewModel import it.reyboz.bustorino.viewmodels.LivePositionsViewModel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.osmdroid.config.Configuration -import org.osmdroid.tileprovider.tilesource.TileSourceFactory -import org.osmdroid.util.BoundingBox -import org.osmdroid.util.GeoPoint -import org.osmdroid.views.MapView -import org.osmdroid.views.overlay.FolderOverlay -import org.osmdroid.views.overlay.Marker -import org.osmdroid.views.overlay.Polyline -import org.osmdroid.views.overlay.advancedpolyline.MonochromaticPaintList - - -class LinesDetailFragment() : ScreenBaseFragment() { +import kotlinx.coroutines.Runnable +import org.maplibre.android.camera.CameraPosition +import org.maplibre.android.camera.CameraUpdateFactory +import org.maplibre.android.geometry.LatLng +import org.maplibre.android.geometry.LatLngBounds +import org.maplibre.android.location.LocationComponent +import org.maplibre.android.location.LocationComponentOptions +import org.maplibre.android.maps.MapLibreMap +import org.maplibre.android.maps.Style +import org.maplibre.android.plugins.annotation.Symbol +import org.maplibre.android.plugins.annotation.SymbolManager +import org.maplibre.android.plugins.annotation.SymbolOptions +import org.maplibre.android.style.expressions.Expression +import org.maplibre.android.style.layers.LineLayer +import org.maplibre.android.style.layers.Property +import org.maplibre.android.style.layers.Property.ICON_ANCHOR_CENTER +import org.maplibre.android.style.layers.Property.ICON_ROTATION_ALIGNMENT_MAP +import org.maplibre.android.style.layers.PropertyFactory +import org.maplibre.android.style.layers.SymbolLayer +import org.maplibre.android.style.sources.GeoJsonSource +import org.maplibre.geojson.Feature +import org.maplibre.geojson.FeatureCollection +import org.maplibre.geojson.LineString +import org.maplibre.geojson.Point +import java.util.concurrent.atomic.AtomicBoolean + + +class LinesDetailFragment() : GeneralMapLibreFragment() { private var lineID = "" private lateinit var patternsSpinner: Spinner private var patternsAdapter: ArrayAdapter? = null + //Bottom sheet behavior + private lateinit var bottomSheetBehavior: BottomSheetBehavior + private var bottomLayout: RelativeLayout? = null + private lateinit var stopTitleTextView: TextView + private lateinit var stopNumberTextView: TextView + private lateinit var linesPassingTextView: TextView + private lateinit var arrivalsCard: CardView + private lateinit var directionsCard: CardView + private lateinit var bottomrightImage: ImageView + + //private var isBottomSheetShowing = false + private var shouldMapLocationBeReactivated = true + + private var toRunWhenMapReady : Runnable? = null + private var mapInitialized = AtomicBoolean(false) + //private var patternsSpinnerState: Parcelable? = null private lateinit var currentPatterns: List - private lateinit var map: MapView - private var viewingPattern: MatoPatternWithStops? = null + //private lateinit var map: MapView + private var patternShown: MatoPatternWithStops? = null private val viewModel: LinesViewModel by viewModels() private val mapViewModel: MapViewModel by viewModels() private var firstInit = true private var pausedFragment = false private lateinit var switchButton: ImageButton private var favoritesButton: ImageButton? = null private var locationIcon: ImageButton? = null private var isLineInFavorite = false private var appContext: Context? = null private var isLocationPermissionOK = false private val lineSharedPrefMonitor = SharedPreferences.OnSharedPreferenceChangeListener { pref, keychanged -> if(keychanged!=PreferencesHolder.PREF_FAVORITE_LINES || lineID.isEmpty()) return@OnSharedPreferenceChangeListener val newFavorites = pref.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) newFavorites?.let {favorites-> isLineInFavorite = favorites.contains(lineID) //if the button has been intialized, change the icon accordingly favoritesButton?.let { button-> //avoid crashes if fragment not attached if(context==null) return@let if(isLineInFavorite) { button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null)) appContext?.let { Toast.makeText(it,R.string.favorites_line_add,Toast.LENGTH_SHORT).show()} } else { button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_outline, null)) appContext?.let {Toast.makeText(it,R.string.favorites_line_remove,Toast.LENGTH_SHORT).show()} } } } } private lateinit var stopsRecyclerView: RecyclerView private lateinit var descripTextView: TextView + private var stopIDFromToShow: String? = null //adapter for recyclerView private val stopAdapterListener= object : StopAdapterListener { override fun onTappedStop(stop: Stop?) { if(viewModel.shouldShowMessage) { Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show() viewModel.shouldShowMessage=false } stop?.let { fragmentListener.requestArrivalsForStopID(it.ID) } if(stop == null){ Log.e(DEBUG_TAG,"Passed wrong stop") } if(fragmentListener == null){ Log.e(DEBUG_TAG, "Fragment listener is null") } } override fun onLongPressOnStop(stop: Stop?): Boolean { TODO("Not yet implemented") } } + private val patternsSorter = Comparator{ p1: MatoPatternWithStops, p2: MatoPatternWithStops -> + if(p1.pattern.directionId != p2.pattern.directionId) + return@Comparator p1.pattern.directionId - p2.pattern.directionId + else + return@Comparator -1*(p1.stopsIndices.size - p2.stopsIndices.size) + } - private var polyline: Polyline? = null + //map data + //style and sources are in GeneralMapLibreFragment + private lateinit var locationComponent: LocationComponent + private lateinit var polylineSource: GeoJsonSource + private lateinit var polyArrowSource: GeoJsonSource + + private var savedCameraPosition: CameraPosition? = null + private var vehShowing = "" + + private var stopsLayerStarted = false + private var lastStopsSizeShown = 0 + private var lastUpdateTime:Long = -2 + + //BUS POSITIONS + private val updatesByVehDict = HashMap(5) + private val animatorsByVeh = HashMap() + + private var lastLocation : Location? = null + private var enablingPositionFromClick = false + + private var polyline: LineString? = null + + private val showUserPositionRequestLauncher = + registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions(), + ActivityResultCallback { result -> + if (result == null) { + Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?") + } else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION] + && java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) { + // We can use the position, restart location overlay + if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null) + return@ActivityResultCallback ///@registerForActivityResult + setMapUserLocationEnabled(true, true, enablingPositionFromClick) + } else Log.w(DEBUG_TAG, "No location permission") + }) //private var stopPosList = ArrayList() - private lateinit var stopsOverlay: FolderOverlay - private lateinit var locationOverlay: LocationOverlay - private val locationOverlayResponder = object : LocationOverlay.OverlayCallbacks{ - override fun onDisableFollowMyLocation() { - Log.d(DEBUG_TAG, "Follow location disabled") - } - - override fun onEnableFollowMyLocation() { - Log.d(DEBUG_TAG, "Follow location enabled") - } - } - //location request responder - private val locationRequestResLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()){ res -> - //onActivityResult(res: map) - if(res[Permissions.LOCATION_PERMISSIONS[0]] ==true || res[Permissions.LOCATION_PERMISSIONS[1]] ==true) - locationIcon?.let { onPositionIconButtonClick(it) } - else{ - context?.let { Toast.makeText(it,R.string.location_permission_not_granted, Toast.LENGTH_SHORT).show() } - } - } //fragment actions private lateinit var fragmentListener: CommonFragmentListener - private val stopTouchResponder = TouchResponder { stopID, stopName -> - Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: $stopID") - fragmentListener.requestArrivalsForStopID(stopID) - } - private var showOnTopOfLine = true + private var showOnTopOfLine = false private var recyclerInitDone = false private var useMQTTPositions = true + + //position of live markers - private val busPositionMarkersByTrip = HashMap() - private var busPositionsOverlay = FolderOverlay() private val tripMarkersAnimators = HashMap() private val liveBusViewModel: LivePositionsViewModel by viewModels() + + //extra items to use the LibreMap + private lateinit var symbolManager : SymbolManager + private var stopActiveSymbol: Symbol? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val args = requireArguments() + lineID = args.getString(LINEID_KEY,"") + stopIDFromToShow = args.getString(STOPID_FROM_KEY) + } + @SuppressLint("SetTextI18n") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { + //reset statuses + //isBottomSheetShowing = false + //stopsLayerStarted = false + lastStopsSizeShown = 0 + mapInitialized.set(false) + val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false) - lineID = requireArguments().getString(LINEID_KEY, "") + //lineID = requireArguments().getString(LINEID_KEY, "") + arguments?.let { + lineID = it.getString(LINEID_KEY, "") + } switchButton = rootView.findViewById(R.id.switchImageButton) locationIcon = rootView.findViewById(R.id.locationEnableIcon) favoritesButton = rootView.findViewById(R.id.favoritesButton) stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) descripTextView = rootView.findViewById(R.id.lineDescripTextView) descripTextView.visibility = View.INVISIBLE + //map stuff + mapView = rootView.findViewById(R.id.lineMap) + mapView.getMapAsync(this) + + + //init bottom sheet + val bottomSheet = rootView.findViewById(R.id.bottom_sheet) + bottomLayout = bottomSheet + stopTitleTextView = bottomSheet.findViewById(R.id.stopTitleTextView) + stopNumberTextView = bottomSheet.findViewById(R.id.stopNumberTextView) + linesPassingTextView = bottomSheet.findViewById(R.id.linesPassingTextView) + arrivalsCard = bottomSheet.findViewById(R.id.arrivalsCardButton) + directionsCard = bottomSheet.findViewById(R.id.directionsCardButton) + bottomrightImage = bottomSheet.findViewById(R.id.rightmostImageView) + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) + + // Setup close button + rootView.findViewById(R.id.btnClose).setOnClickListener { + hideStopBottomSheet() + } + val titleTextView = rootView.findViewById(R.id.titleTextView) titleTextView.text = getString(R.string.line)+" "+FiveTNormalizer.fixShortNameForDisplay( GtfsUtils.getLineNameFromGtfsID(lineID), true) favoritesButton?.isClickable = true favoritesButton?.setOnClickListener { if(lineID.isNotEmpty()) PreferencesHolder.addOrRemoveLineToFavorites(requireContext(),lineID,!isLineInFavorite) } val preferences = PreferencesHolder.getMainSharedPreferences(requireContext()) val favorites = preferences.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) if(favorites!=null && favorites.contains(lineID)){ favoritesButton?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null)) isLineInFavorite = true } appContext = requireContext().applicationContext preferences.registerOnSharedPreferenceChangeListener(lineSharedPrefMonitor) patternsSpinner = rootView.findViewById(R.id.patternsSpinner) patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter - initializeMap(rootView) initializeRecyclerView() switchButton.setOnClickListener{ - if(map.visibility == View.VISIBLE){ - map.visibility = View.GONE - stopsRecyclerView.visibility = View.VISIBLE - locationIcon?.visibility = View.GONE - - viewModel.setMapShowing(false) - liveBusViewModel.stopMatoUpdates() - //map.overlayManager.remove(busPositionsOverlay) - - switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_map_white_30)) + if(mapView.visibility == View.VISIBLE){ + hideMapAndShowStopList() } else{ - stopsRecyclerView.visibility = View.GONE - map.visibility = View.VISIBLE - locationIcon?.visibility = View.VISIBLE - viewModel.setMapShowing(true) - - //map.overlayManager.add(busPositionsOverlay) - //map. - if(useMQTTPositions) - liveBusViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) - else - liveBusViewModel.requestGTFSUpdates() - - switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30)) + hideStopListAndShowMap() } } locationIcon?.let {view -> if(!LocationUtils.isLocationEnabled(requireContext()) || !Permissions.anyLocationPermissionsGranted(requireContext())) setLocationIconEnabled(false) //set click Listener view.setOnClickListener(this::onPositionIconButtonClick) } //set - - + //INITIALIZE VIEW MODELS viewModel.setRouteIDQuery(lineID) + liveBusViewModel.setGtfsLineToFilterPos(lineID, null) val keySourcePositions = getString(R.string.pref_positions_source) useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions, "mqtt").contentEquals("mqtt") viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ patterns -> savePatternsToShow(patterns) } /* - We have the pattern and the stops here, time to display them */ viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner) { stops -> - if(map.visibility ==View.VISIBLE) - showPatternWithStopsOnMap(stops) + if(mapView.visibility ==View.VISIBLE) + patternShown?.let{ + // We have the pattern and the stops here, time to display them + displayPatternWithStopsOnMap(it,stops, true) + } ?:{ + Log.w(DEBUG_TAG, "The viewingPattern is null!") + } else{ if(stopsRecyclerView.visibility==View.VISIBLE) - showStopsAsList(stops) + showStopsInRecyclerView(stops) } } viewModel.gtfsRoute.observe(viewLifecycleOwner){route-> if(route == null){ //need to close the fragment activity?.supportFragmentManager?.popBackStack() return@observe } descripTextView.text = route.longName descripTextView.visibility = View.VISIBLE } - if(pausedFragment && viewModel.selectedPatternLiveData.value!=null){ - val patt = viewModel.selectedPatternLiveData.value!! - Log.d(DEBUG_TAG, "Recreating views on resume, setting pattern: ${patt.pattern.code}") - showPattern(patt) - pausedFragment = false - } + /* + + */ Log.d(DEBUG_TAG,"Data ${viewModel.stopsForPatternLiveData.value}") //listeners patternsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { - val patternWithStops = currentPatterns.get(position) - //viewModel.setPatternToDisplay(patternWithStops) - setPatternAndReqStops(patternWithStops) + val currentShownPattern = patternShown?.pattern + val patternWithStops = currentPatterns[position] - Log.d(DEBUG_TAG, "item Selected, cleaning bus markers") - if(map?.visibility == View.VISIBLE) { - busPositionsOverlay.closeAllInfoWindows() - busPositionsOverlay.items.clear() - busPositionMarkersByTrip.clear() + Log.d(DEBUG_TAG, "request stops for pattern ${patternWithStops.pattern.code}") + setPatternAndReqStops(patternWithStops) - stopAnimations() - tripMarkersAnimators.clear() - liveBusViewModel.retriggerPositionUpdate() + if(mapView.visibility == View.VISIBLE) { + //Clear buses if we are changing direction + currentShownPattern?.let { patt -> + if(patt.directionId != patternWithStops.pattern.directionId){ + stopAnimations() + updatesByVehDict.clear() + updatePositionsIcons(true) + liveBusViewModel.retriggerPositionUpdate() + } + } } + liveBusViewModel.setGtfsLineToFilterPos(lineID, patternWithStops.pattern) + } override fun onNothingSelected(p0: AdapterView<*>?) { } } + Log.d(DEBUG_TAG, "Views created!") + return rootView + } - //live bus positions - liveBusViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner){ - if(map.visibility == View.GONE || viewingPattern ==null){ - //DO NOTHING - return@observe + // ------------- UI switch stuff --------- + + private fun hideMapAndShowStopList(){ + mapView.visibility = View.GONE + stopsRecyclerView.visibility = View.VISIBLE + locationIcon?.visibility = View.GONE + + viewModel.setMapShowing(false) + if(useMQTTPositions) liveBusViewModel.stopMatoUpdates() + //map.overlayManager.remove(busPositionsOverlay) + + switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_map_white_30)) + + hideStopBottomSheet() + + if(locationComponent.isLocationComponentEnabled){ + locationComponent.isLocationComponentEnabled = false + shouldMapLocationBeReactivated = true + } else + shouldMapLocationBeReactivated = false + } + + private fun hideStopListAndShowMap(){ + stopsRecyclerView.visibility = View.GONE + mapView.visibility = View.VISIBLE + locationIcon?.visibility = View.VISIBLE + viewModel.setMapShowing(true) + + //map.overlayManager.add(busPositionsOverlay) + //map. + if(useMQTTPositions) + liveBusViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) + else + liveBusViewModel.requestGTFSUpdates() + + switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30)) + + if(shouldMapLocationBeReactivated && Permissions.bothLocationPermissionsGranted(requireContext())){ + locationComponent.isLocationComponentEnabled = true + } + } + + private fun setLocationIconEnabled(setTrue: Boolean){ + if(setTrue) + locationIcon?.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red)) + else + locationIcon?.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey)) + } + + /** + * Handles logic of enabling the user location on the map + */ + @SuppressLint("MissingPermission") + private fun setMapUserLocationEnabled(enabled: Boolean, assumePermissions: Boolean, fromClick: Boolean) { + if (enabled) { + val permissionOk = assumePermissions || Permissions.bothLocationPermissionsGranted(requireContext()) + + if (permissionOk) { + Log.d(DEBUG_TAG, "Permission OK, starting location component, assumed: $assumePermissions") + locationComponent.isLocationComponentEnabled = true + //locationComponent.cameraMode = CameraMode.TRACKING //CameraMode.TRACKING + + setLocationIconEnabled(true) + if (fromClick) Toast.makeText(context, R.string.location_enabled, Toast.LENGTH_SHORT).show() + } else { + if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { + //TODO: show dialog for permission rationale + Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show() + } + Log.d(DEBUG_TAG, "Requesting permission to show user location") + enablingPositionFromClick = fromClick + showUserPositionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS) } - val filtdLineID = GtfsUtils.stripGtfsPrefix(lineID) - //filter buses with direction, show those only with the same direction - val outmap = HashMap>() - val currentPattern = viewingPattern!!.pattern - val numUpds = it.entries.size - Log.d(DEBUG_TAG, "Got $numUpds updates, current pattern is: ${currentPattern.name}, directionID: ${currentPattern.directionId}") - val patternsDirections = HashMap() - for((tripId, pair) in it.entries){ - //remove trips with wrong line ideas - if(pair.first.routeID!=filtdLineID) - continue + } else{ + locationComponent.isLocationComponentEnabled = false + setLocationIconEnabled(false) + if (fromClick) { + Toast.makeText(requireContext(), R.string.location_disabled, Toast.LENGTH_SHORT).show() + //TODO: Cancel the request for the enablement of the position if needed + } + } + + } + + /** + * Switch position icon from activ + */ + private fun onPositionIconButtonClick(view: View){ + if(locationComponent.isLocationComponentEnabled) setMapUserLocationEnabled(false, false, true) + else{ + setMapUserLocationEnabled(true, false, true) + } + } + + // ------------- Map Code ------------------------- + /** + * This method sets up the map and the layers + */ + override fun onMapReady(mapReady: MapLibreMap) { + this.map = mapReady + + val context = requireContext() + val mjson = Styles.getJsonStyleFromAsset(context, PreferencesHolder.getMapLibreStyleFile(context)) //ViewUtils.loadJsonFromAsset(requireContext(),"map_style_good.json") + + activity?.run { + val builder = Style.Builder().fromJson(mjson!!) + + mapReady.setStyle(builder) { style -> + + mapStyle = style + //setupLayers(style) + + // Start observing data + initMapUserLocation(style, mapReady, requireContext()) + + //if(!stopsLayerStarted) + initStopsPolyLineLayers(style, FeatureCollection.fromFeatures(ArrayList()), null, null) + /*if(!stopsLayerStarted) { + Log.d(DEBUG_TAG, "Stop layer is not started yet") + initStopsPolyLineLayers(style, FeatureCollection.fromFeatures(ArrayList()), null) + } + */ + setupBusLayer(style) + + symbolManager = SymbolManager(mapView,mapReady,style) + symbolManager.iconAllowOverlap = true + symbolManager.textAllowOverlap = false + + symbolManager.addClickListener{ _ -> + if (stopActiveSymbol!=null){ + hideStopBottomSheet() + + return@addClickListener true + } else + return@addClickListener false + } + + mapViewModel.stopShowing?.let { + openStopInBottomSheet(it) + } + mapViewModel.stopShowing = null + toRunWhenMapReady?.run() + toRunWhenMapReady = null + mapInitialized.set(true) + + if(patternShown!=null){ + viewModel.stopsForPatternLiveData.value?.let { + Log.d(DEBUG_TAG, "Show stops from the cache") + displayPatternWithStopsOnMap(patternShown!!, it, true) + } + } - if(pair.second!=null && pair.second?.pattern !=null){ - val dir = pair.second?.pattern?.directionId - if(dir !=null && dir == currentPattern.directionId){ - outmap[tripId] = pair + } + + mapReady.addOnMapClickListener { point -> + val screenPoint = mapReady.projection.toScreenLocation(point) + val features = mapReady.queryRenderedFeatures(screenPoint, STOPS_LAYER_ID) + val busNearby = mapReady.queryRenderedFeatures(screenPoint, BUSES_LAYER_ID) + if (features.isNotEmpty()) { + val feature = features[0] + val id = feature.getStringProperty("id") + val name = feature.getStringProperty("name") + //Toast.makeText(requireContext(), "Clicked on $name ($id)", Toast.LENGTH_SHORT).show() + val stop = viewModel.getStopByID(id) + stop?.let { + if (isBottomSheetShowing() || vehShowing.isNotEmpty()){ + hideStopBottomSheet() + } + openStopInBottomSheet(it) + + //move camera + if(it.latitude!=null && it.longitude!=null) + mapReady.animateCamera(CameraUpdateFactory.newLatLng(LatLng(it.latitude!!,it.longitude!!)),750) + } + return@addOnMapClickListener true + } else if (busNearby.isNotEmpty()){ + val feature = busNearby[0] + val vehid = feature.getStringProperty("veh") + val route = feature.getStringProperty("line") + if(isBottomSheetShowing()) + hideStopBottomSheet() + //if(context!=null){ + // Toast.makeText(context, "Veh $vehid on route ${route.slice(0..route.length-2)}", Toast.LENGTH_SHORT).show() + //} + showVehicleTripInBottomSheet(vehid) + updatesByVehDict[vehid]?.let { + //if (it.posUpdate.latitude != null && it.longitude != null) + mapReady.animateCamera( + CameraUpdateFactory.newLatLng(LatLng(it.posUpdate.latitude, it.posUpdate.longitude)), + 750 + ) } - patternsDirections.set(tripId,if (dir!=null) dir else -10) - } else{ - outmap[tripId] = pair - //Log.d(DEBUG_TAG, "No pattern for tripID: $tripId") - patternsDirections[tripId] = -10 + + return@addOnMapClickListener true } + false + } + + // we start requesting the bus positions now + observeBusPositionUpdates() + + } + /*savedMapStateOnPause?.let{ + restoreMapStateFromBundle(it) + pendingLocationActivation = false + Log.d(DEBUG_TAG, "Restored map state from the saved bundle") + } + + */ + + val zoom = 12.0 + val latlngTarget = LatLng(MapLibreFragment.DEFAULT_CENTER_LAT, MapLibreFragment.DEFAULT_CENTER_LON) + + mapReady.cameraPosition = savedCameraPosition ?:CameraPosition.Builder().target(latlngTarget).zoom(zoom).build() + + savedCameraPosition = null + + if(shouldMapLocationBeReactivated) setMapUserLocationEnabled(true, false, false) + } + + private fun observeBusPositionUpdates(){ + //live bus positions + liveBusViewModel.filteredLocationUpdates.observe(viewLifecycleOwner){ pair -> + //Log.d(DEBUG_TAG, "Received ${updates.size} updates for the positions") + val updates = pair.first + val vehiclesNotOnCorrectDir = pair.second + if(mapView.visibility == View.GONE || patternShown ==null){ + //DO NOTHING + Log.w(DEBUG_TAG, "not doing anything because map is not visible") + return@observe } - Log.d(DEBUG_TAG, " Filtered updates are ${outmap.keys.size}") // Original updates directs: $patternsDirections\n - updateBusPositionsInMap(outmap) + //remove vehicles not on this direction + removeVehiclesData(vehiclesNotOnCorrectDir) + updateBusPositionsInMap(updates) //if not using MQTT positions if(!useMQTTPositions){ liveBusViewModel.requestDelayedGTFSUpdates(2000) } } //download missing tripIDs liveBusViewModel.tripsGtfsIDsToQuery.observe(viewLifecycleOwner){ //gtfsPosViewModel.downloadTripsFromMato(dat); MatoTripsDownloadWorker.requestMatoTripsDownload( it, requireContext().applicationContext, "BusTO-MatoTripDownload" ) } - - - return rootView } - private fun setLocationIconEnabled(setTrue: Boolean){ - if(setTrue) - locationIcon?.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red)) - else - locationIcon?.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey)) + private fun isBottomSheetShowing(): Boolean{ + return bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED } /** - * Switch position icon from activ + * Initialize the map location, but do not enable the component */ - private fun onPositionIconButtonClick(view: View){ - if(locationOverlay.isMyLocationEnabled){ - //switch off - locationOverlay.disableMyLocation() - //set image on respective button - setLocationIconEnabled(false) - if(context!=null) { - if (LocationUtils.isLocationEnabled(context)) { - //show message - Toast.makeText(context, R.string.location_disabled, Toast.LENGTH_SHORT).show() - } + @SuppressLint("MissingPermission") + private fun initMapUserLocation(style: Style, map: MapLibreMap, context: Context){ + locationComponent = map.locationComponent + val locationComponentOptions = + LocationComponentOptions.builder(context) + .pulseEnabled(false) + .build() + val locationComponentActivationOptions = + MapLibreUtils.buildLocationComponentActivationOptions(style, locationComponentOptions, context) + locationComponent.activateLocationComponent(locationComponentActivationOptions) + locationComponent.isLocationComponentEnabled = false + + lastLocation?.let { + if (it.accuracy < 200) + locationComponent.forceLocationUpdate(it) + } + } + /** + * Update the bottom sheet with the stop information + */ + override fun openStopInBottomSheet(stop: Stop){ + bottomLayout?.let { + + //lay.findViewById(R.id.stopTitleTextView).text ="${stop.ID} - ${stop.stopDefaultName}" + val stopName = stop.stopUserName ?: stop.stopDefaultName + stopTitleTextView.text = stopName//stop.stopDefaultName + stopNumberTextView.text = stop.ID + stopTitleTextView.visibility = View.VISIBLE + + val string_show = if (stop.numRoutesStopping==0) "" + else if (stop.numRoutesStopping <= 1) + requireContext().getString(R.string.line_fill, stop.routesThatStopHereToString()) + else requireContext().getString(R.string.lines_fill, stop.routesThatStopHereToString()) + linesPassingTextView.text = string_show + + //SET ON CLICK LISTENER + arrivalsCard.setOnClickListener{ + fragmentListener?.requestArrivalsForStopID(stop.ID) } - } else{ - //switch on - locationOverlay.enableMyLocation() - if(context!=null) { - if(!Permissions.anyLocationPermissionsGranted(context)) { - locationRequestResLauncher.launch(Permissions.LOCATION_PERMISSIONS) - Toast.makeText(context, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show() - } - else if (LocationUtils.isLocationEnabled(context)) { - //set image on button - setLocationIconEnabled(true) - //show message - Toast.makeText(context, R.string.location_enabled, Toast.LENGTH_SHORT).show() - } else{ - Toast.makeText(context, R.string.map_location_disabled_device, Toast.LENGTH_SHORT).show() - } + + arrivalsCard.visibility = View.VISIBLE + + directionsCard.setOnClickListener { + ViewUtils.openStopInOutsideApp(stop, context) } + context?.let { + val colorIcon = ViewUtils.getColorFromTheme(it, android.R.attr.colorAccent)//ResourcesCompat.getColor(resources,R.attr.colorAccent,activity?.theme) + ViewCompat.setBackgroundTintList(directionsCard, ColorStateList.valueOf(colorIcon)) + } + + bottomrightImage.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.navigation_right, activity?.theme)) + + } + //add stop marker + if (stop.latitude!=null && stop.longitude!=null) { + stopActiveSymbol = symbolManager.create( + SymbolOptions() + .withLatLng(LatLng(stop.latitude!!, stop.longitude!!)) + .withIconImage(STOP_ACTIVE_IMG) + .withIconAnchor(ICON_ANCHOR_CENTER) + + ) + } + Log.d(DEBUG_TAG, "Shown stop $stop in bottom sheet") + shownStopInBottomSheet = stop + + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + //isBottomSheetShowing = true } + // Hide the bottom sheet and remove extra symbol + private fun hideStopBottomSheet(){ + if (stopActiveSymbol!=null){ + symbolManager.delete(stopActiveSymbol) + stopActiveSymbol = null + } + bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + //isBottomSheetShowing = false - private fun initializeMap(rootView : View){ - val ctx = requireContext().applicationContext - Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)) + //reset states + shownStopInBottomSheet = null + vehShowing = "" - map = rootView.findViewById(R.id.lineMap) - map.let { - it.setTileSource(TileSourceFactory.MAPNIK) + } - locationOverlay = LocationOverlay.createLocationOverlay(true, it, requireContext(), locationOverlayResponder) - locationOverlay.disableFollowLocation() + private fun showVehicleTripInBottomSheet(veh: String){ + val data = updatesByVehDict[veh] + if(data==null) { + Log.w(DEBUG_TAG,"Asked to show vehicle $veh, but it's not present in the updates") + return + } - stopsOverlay = FolderOverlay() - busPositionsOverlay = FolderOverlay() + bottomLayout?.let { + val lineName = FiveTNormalizer.fixShortNameForDisplay( + GtfsUtils.getLineNameFromGtfsID(data.posUpdate.routeID), true) + stopNumberTextView.text = requireContext().getString(R.string.line_fill, lineName) + val pat = data.pattern + if (pat!=null){ + stopTitleTextView.text = pat.headsign + stopTitleTextView.visibility = View.VISIBLE + Log.d(DEBUG_TAG, "Showing headsign ${pat.headsign} for vehicle $veh") + } else { + //stopTitleTextView.text = "NN" + stopTitleTextView.visibility = View.GONE + } + linesPassingTextView.text = data.posUpdate.vehicle + } + arrivalsCard.visibility=View.GONE + bottomrightImage.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_magnifying_glass, activity?.theme)) + directionsCard.setOnClickListener { + data.pattern?.let { + + if(patternShown?.pattern?.code == it.code){ + context?.let { c->Toast.makeText(c, R.string.showing_same_direction, Toast.LENGTH_SHORT).show() } + }else + showPatternWithCode(it.code) + } //TODO + // ?: { + // context?.let { ctx -> Toast.makeText(ctx,"") } + //} + } + //set color + val colorBlue = ResourcesCompat.getColor(resources,R.color.blue_620,activity?.theme) + ViewCompat.setBackgroundTintList(directionsCard, ColorStateList.valueOf(colorBlue)) + + vehShowing = veh + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + //isBottomSheetShowing = true + Log.d(DEBUG_TAG, "Shown vehicle $veh in bottom layout") + } - //map.setTilesScaledToDpi(true); - //map.setTilesScaledToDpi(true); - it.setFlingEnabled(true) - it.setUseDataConnection(true) + // ------- MAP LAYERS INITIALIZE ---- + /** + * Initialize the map layers for the stops + */ + private fun initStopsPolyLineLayers(style: Style, stopFeatures:FeatureCollection, lineFeature: Feature?, arrowFeatures: FeatureCollection?){ + + Log.d(DEBUG_TAG, "INIT STOPS CALLED") + stopsSource = GeoJsonSource(STOPS_SOURCE_ID) + style.addSource(stopsSource) + //val context = requireContext() + val stopIcon = ResourcesCompat.getDrawable(resources,R.drawable.ball, activity?.theme)!! + + val imgStop = ResourcesCompat.getDrawable(resources,R.drawable.bus_stop_new, activity?.theme)!! + val polyIconArrow = ResourcesCompat.getDrawable(resources, R.drawable.arrow_up_box_fill, activity?.theme)!! + //set the image tint + //DrawableCompat.setTint(imgBus,ContextCompat.getColor(context,R.color.line_drawn_poly)) + + // add icon + style.addImage(STOP_IMAGE_ID,stopIcon) + style.addImage(POLY_ARROW, polyIconArrow) + style.addImage(STOP_ACTIVE_IMG, ResourcesCompat.getDrawable(resources, R.drawable.bus_stop_new_highlight, activity?.theme)!!) + // Stops layer + val stopsLayer = SymbolLayer(STOPS_LAYER_ID, STOPS_SOURCE_ID) + stopsLayer.withProperties( + PropertyFactory.iconImage(STOP_IMAGE_ID), + PropertyFactory.iconAllowOverlap(true), + PropertyFactory.iconIgnorePlacement(true) + ) + + polylineSource = GeoJsonSource(POLYLINE_SOURCE) //lineFeature?.let { GeoJsonSource(POLYLINE_SOURCE, it) } ?: GeoJsonSource(POLYLINE_SOURCE) + style.addSource(polylineSource) + + val color=ContextCompat.getColor(requireContext(),R.color.line_drawn_poly) + //paint.style = Paint.Style.FILL_AND_STROKE + //paint.strokeJoin = Paint.Join.ROUND + //paint.strokeCap = Paint.Cap.ROUND + + val lineLayer = LineLayer(POLYLINE_LAYER, POLYLINE_SOURCE).withProperties( + PropertyFactory.lineColor(color), + PropertyFactory.lineWidth(5.0f), //originally 13f + PropertyFactory.lineOpacity(1.0f), + PropertyFactory.lineJoin(Property.LINE_JOIN_ROUND), + PropertyFactory.lineCap(Property.LINE_CAP_ROUND) + + ) + polyArrowSource = GeoJsonSource(POLY_ARROWS_SOURCE, arrowFeatures) + style.addSource(polyArrowSource) + val arrowsLayer = SymbolLayer(POLY_ARROWS_LAYER, POLY_ARROWS_SOURCE).withProperties( + PropertyFactory.iconImage(POLY_ARROW), + PropertyFactory.iconRotate(Expression.get("bearing")), + PropertyFactory.iconRotationAlignment(ICON_ROTATION_ALIGNMENT_MAP) + ) + + val layers = style.layers + val lastLayers = layers.filter { l-> l.id.contains("city") } + //Log.d(DEBUG_TAG,"Layers:\n ${style.layers.map { l -> l.id }}") + Log.d(DEBUG_TAG, "City layers: ${lastLayers.map { l-> l.id }}") + if(lastLayers.isNotEmpty()) + style.addLayerAbove(lineLayer,lastLayers[0].id) + else + style.addLayerBelow(lineLayer,"label_country_1") + style.addLayerAbove(stopsLayer, POLYLINE_LAYER) + style.addLayerAbove(arrowsLayer, POLYLINE_LAYER) - // add ability to zoom with 2 fingers - it.setMultiTouchControls(true) - it.minZoomLevel = 12.0 + stopsLayerStarted = true + } - //map controller setup - val mapController = it.controller - var zoom = 12.0 - var centerMap = GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) - if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { - Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ - " zoom ${mapViewModel.currentZoom.value}") - zoom = mapViewModel.currentZoom.value!! - centerMap = GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!) - /*viewLifecycleOwner.lifecycleScope.launch { - delay(100) - Log.d(DEBUG_TAG, "zooming back to point") - controller.animateTo(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!), - mapViewModel.currentZoom.value!!,null,null) - //controller.setCenter(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!)) - //controller.setZoom(mapViewModel.currentZoom.value!!) - */ - } - mapController.setZoom(zoom) - mapController.setCenter(centerMap) - Log.d(DEBUG_TAG, "Initializing map, first init $firstInit") - //map.invalidate() - it.overlayManager.add(stopsOverlay) - it.overlayManager.add(locationOverlay) - it.overlayManager.add(busPositionsOverlay) + /** + * Setup the Map Layers + */ + private fun setupBusLayer(style: Style) { + // Buses source + busesSource = GeoJsonSource(BUSES_SOURCE_ID) + style.addSource(busesSource) + style.addImage("bus_symbol",ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon, activity?.theme)!!) + + // Buses layer + val busesLayer = SymbolLayer(BUSES_LAYER_ID, BUSES_SOURCE_ID).apply { + withProperties( + PropertyFactory.iconImage("bus_symbol"), + //PropertyFactory.iconSize(1.2f), + PropertyFactory.iconAllowOverlap(true), + PropertyFactory.iconIgnorePlacement(true), + PropertyFactory.iconRotate(Expression.get("bearing")), + PropertyFactory.iconRotationAlignment(ICON_ROTATION_ALIGNMENT_MAP) - zoomToCurrentPattern() - firstInit = false + ) } - + style.addLayerAbove(busesLayer, STOPS_LAYER_ID) } override fun onAttach(context: Context) { super.onAttach(context) if(context is CommonFragmentListener){ fragmentListener = context } else throw RuntimeException("$context must implement CommonFragmentListener") } private fun stopAnimations(){ - for(anim in tripMarkersAnimators.values){ + for(anim in animatorsByVeh.values){ anim.cancel() } } + /** + * Save the loaded pattern data, without the stops! + */ private fun savePatternsToShow(patterns: List){ - val patternsSorter = Comparator{ p1: MatoPatternWithStops, p2: MatoPatternWithStops -> - if(p1.pattern.directionId != p2.pattern.directionId) - return@Comparator p1.pattern.directionId - p2.pattern.directionId - else - return@Comparator -1*(p1.stopsIndices.size - p2.stopsIndices.size) - } currentPatterns = patterns.sortedWith(patternsSorter) patternsAdapter?.let { it.clear() it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) it.notifyDataSetChanged() } - viewingPattern?.let { + // if we are loading from a stop, find it + val patternToShow = stopIDFromToShow?.let { sID -> + val stopGtfsID = "gtt:$sID" + var p: MatoPatternWithStops? = null + var pLength = 0 + for(patt in currentPatterns){ + for(pstop in patt.stopsIndices){ + if(pstop.stopGtfsId == stopGtfsID){ + //found + if (patt.stopsIndices.size>pLength){ + p = patt + pLength = patt.stopsIndices.size + } + //break here, we have determined this pattern has the stop we're looking for + break + } + } + } + p + } + if(stopIDFromToShow!=null){ + if(patternToShow==null) + Log.w(DEBUG_TAG, "We had to show the pattern from stop $stopIDFromToShow, but we didn't find it") + else + Log.d(DEBUG_TAG, "Requesting to show pattern from stop $stopIDFromToShow, found pattern ${patternToShow.pattern.code}") + } + //unset the stopID to show + if(patternToShow!=null) { + + //showPattern(patternToShow) + patternShown = patternToShow + stopIDFromToShow = null + } + patternShown?.let { showPattern(it) } } /** * Called when the position of the spinner is updated */ private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") viewModel.selectedPatternLiveData.value = patternWithStops viewModel.currentPatternStops.value = patternWithStops.stopsIndices.sortedBy { i-> i.order } - viewingPattern = patternWithStops + patternShown = patternWithStops viewModel.requestStopsForPatternWithStops(patternWithStops) } private fun showPattern(patternWs: MatoPatternWithStops){ - Log.d(DEBUG_TAG, "Finding pattern to show: ${patternWs.pattern.code}") + //Log.d(DEBUG_TAG, "Finding pattern to show: ${patternWs.pattern.code}") var pos = -2 val code = patternWs.pattern.code.trim() - for(k in currentPatterns.indices){ - if(currentPatterns[k].pattern.code.trim() == code){ + for (k in currentPatterns.indices) { + if (currentPatterns[k].pattern.code.trim() == code) { pos = k break } } - Log.d(DEBUG_TAG, "Found pattern $code in position: $pos") - if(pos>=0) + Log.d(DEBUG_TAG, "Requesting stops fro pattern $code in position: $pos") + if (pos !=-2) patternsSpinner.setSelection(pos) - //set pattern - setPatternAndReqStops(patternWs) + else + Log.e(DEBUG_TAG, "Pattern with code $code not found!!") + //request pattern stops from DB + //setPatternAndReqStops(patternWs) } private fun zoomToCurrentPattern(){ - var pointsList: List - if(viewingPattern==null) { - Log.e(DEBUG_TAG, "asked to zoom to pattern but current viewing pattern is null") - if(polyline!=null) - pointsList = polyline!!.actualPoints - else { - Log.d(DEBUG_TAG, "The polyline is null") - return - } - }else{ - val pattern = viewingPattern!!.pattern - - pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) - } - - var maxLat = -4000.0 - var minLat = -4000.0 - var minLong = -4000.0 - var maxLong = -4000.0 - for (p in pointsList){ - // get max latitude - if(maxLat == -4000.0) - maxLat = p.latitude - else if (maxLat < p.latitude) maxLat = p.latitude - // find min latitude - if (minLat == -4000.0) - minLat = p.latitude - else if (minLat > p.latitude) minLat = p.latitude - if(maxLong == -4000.0 || maxLong < p.longitude ) - maxLong = p.longitude - if (minLong == -4000.0 || minLong > p.longitude) - minLong = p.longitude - } - - val del = 0.008 - //map.controller.c - Log.d(DEBUG_TAG, "Setting limits of bounding box of line: $minLat -> $maxLat, $minLong -> $maxLong") - map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), false) - } - - private fun showPatternWithStopsOnMap(stops: List){ - Log.d(DEBUG_TAG, "Got the stops: ${stops.map { s->s.gtfsID }}}") - if(viewingPattern==null || map == null) return - - val pattern = viewingPattern!!.pattern + if(polyline==null) return + val NULL_VALUE = -4000.0 + var maxLat = NULL_VALUE + var minLat = NULL_VALUE + var minLong = NULL_VALUE + var maxLong = NULL_VALUE + + polyline?.let { + for(p in it.coordinates()){ + val lat = p.latitude() + val lon = p.longitude() + // get max latitude + if(maxLat == NULL_VALUE) + maxLat =lat + else if (maxLat < lat) maxLat = lat + // find min latitude + if (minLat ==NULL_VALUE) + minLat = lat + else if (minLat > lat) minLat = lat + if(maxLong == NULL_VALUE || maxLong < lon ) + maxLong = lon + if (minLong == NULL_VALUE || minLong > lon) + minLong = lon + } + val padding = 50 // Pixel di padding intorno ai limiti + + Log.d(DEBUG_TAG, "Setting limits of bounding box of line: $minLat -> $maxLat, $minLong -> $maxLong") + val bbox = LatLngBounds.from(maxLat,maxLong, minLat, minLong) + //map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), false) + map?.animateCamera(CameraUpdateFactory.newLatLngBounds(bbox, padding)) + } + + + } + private fun displayPatternWithStopsOnMap(patternWs: MatoPatternWithStops, stopsToSort: List, zoomToPattern: Boolean){ + if(!mapInitialized.get()){ + //set the runnable and do nothing else + Log.d(DEBUG_TAG, "Delaying pattern display to when map is Ready: ${patternWs.pattern.code}") + toRunWhenMapReady = Runnable { + displayPatternWithStopsOnMap(patternWs, stopsToSort, zoomToPattern) + } + return + } + + Log.d(DEBUG_TAG, "Got the stops: ${stopsToSort.map { s->s.gtfsID }}}") + patternShown = patternWs + //Problem: stops are not sorted + val stopOrderD = patternWs.stopsIndices.withIndex().associate{it.value.stopGtfsId to it.index} + val stopsSorted = stopsToSort.sortedBy { s-> stopOrderD[s.gtfsID] } + + val pattern = patternWs.pattern val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) - var maxLat = -4000.0 - var minLat = -4000.0 - var minLong = -4000.0 - var maxLong = -4000.0 - for (p in pointsList){ - // get max latitude - if(maxLat == -4000.0) - maxLat = p.latitude - else if (maxLat < p.latitude) maxLat = p.latitude - // find min latitude - if (minLat == -4000.0) - minLat = p.latitude - else if (minLat > p.latitude) minLat = p.latitude - if(maxLong == -4000.0 || maxLong < p.longitude ) - maxLong = p.longitude - if (minLong == -4000.0 || minLong > p.longitude) - minLong = p.longitude - } - //val polyLine=Polyline(map) - //polyLine.setPoints(pointsList) - //save points - if(map.overlayManager.contains(polyline)){ - map.overlayManager.remove(polyline) - } - polyline = Polyline(map, false) - polyline!!.setPoints(pointsList) - //polyline.color = ContextCompat.getColor(context!!,R.color.brown_vd) - polyline!!.infoWindow = null - val paint = Paint() - paint.color = ContextCompat.getColor(requireContext(),R.color.line_drawn_poly) - paint.isAntiAlias = true - paint.strokeWidth = 13f - - paint.style = Paint.Style.FILL_AND_STROKE - paint.strokeJoin = Paint.Join.ROUND - paint.strokeCap = Paint.Cap.ROUND - polyline!!.outlinePaintLists.add(MonochromaticPaintList(paint)) - - map.overlayManager.add(0,polyline!!) - - stopsOverlay.closeAllInfoWindows() - stopsOverlay.items.clear() - val stopIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ball) + val pointsToShow = pointsList.map { Point.fromLngLat(it.longitude, it.latitude) } + Log.d(DEBUG_TAG, "The polyline has ${pointsToShow.size} points to display") + polyline = LineString.fromLngLats(pointsToShow) + val lineFeature = Feature.fromGeometry(polyline) + //Log.d(DEBUG_TAG, "Polyline in JSON is: ${lineFeature.toJson()}") + + // --- STOPS--- + val features = ArrayList() + for (s in stopsSorted){ + if (s.latitude!=null && s.longitude!=null) { + val loc = if (showOnTopOfLine) findOptimalPosition(s, pointsList) + else LatLng(s.latitude!!, s.longitude!!) + features.add( + Feature.fromGeometry( + Point.fromLngLat(loc.longitude, loc.latitude), + JsonObject().apply { + addProperty("id", s.ID) + addProperty("name", s.stopDefaultName) + //addProperty("routes", s.routesThatStopHereToString()) // Add routes array to JSON object + } + ) + ) + } + } + // -- ARROWS -- + //val splitPolyline = MapLibreUtils.splitPolyWhenDistanceTooBig(pointsList, 200.0) + val arrowFeatures = ArrayList() + val pointsIndexToShowIcon = MapLibreUtils.findPointsToPutDirectionMarkers(pointsList, stopsSorted, 750.0) + + for (idx in pointsIndexToShowIcon){ + val pnow = pointsList[idx] + val otherp = if(idx>1) pointsList[idx-1] else pointsList[idx+1] + val bearing = if (idx>1) MapLibreUtils.getBearing(pointsList[idx-1], pnow) else MapLibreUtils.getBearing(pnow, pointsList[idx+1]) + + arrowFeatures.add(Feature.fromGeometry( + Point.fromLngLat((pnow.longitude+otherp.longitude)/2, (pnow.latitude+otherp.latitude)/2 ), //average + JsonObject().apply { + addProperty("bearing", bearing) + } + )) + } + Log.d(DEBUG_TAG,"Have put ${features.size} stops to display") + + // if the layer is already started, substitute the stops inside, otherwise start it + if (stopsLayerStarted) { + stopsSource.setGeoJson(FeatureCollection.fromFeatures(features)) + polylineSource.setGeoJson(lineFeature) + polyArrowSource.setGeoJson(FeatureCollection.fromFeatures(arrowFeatures)) + lastStopsSizeShown = features.size + } else + map?.let { + Log.d(DEBUG_TAG, "Map stop layer is not started yet, init layer") + initStopsPolyLineLayers(mapStyle, FeatureCollection.fromFeatures(features),lineFeature, FeatureCollection.fromFeatures(arrowFeatures)) + Log.d(DEBUG_TAG,"Started stops layer on map") + lastStopsSizeShown = features.size + stopsLayerStarted = true + } ?:{ + Log.e(DEBUG_TAG, "Stops layer is not started!!") + } + /* OLD CODE for(s in stops){ - val gp = if (showOnTopOfLine) - findOptimalPosition(s,pointsList) - else GeoPoint(s.latitude!!,s.longitude!!) + val gp = val marker = MarkerUtils.makeMarker( gp, s.ID, s.stopDefaultName, s.routesThatStopHereToString(), map,stopTouchResponder, stopIcon, R.layout.linedetail_stop_infowindow, R.color.line_drawn_poly ) marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) stopsOverlay.add(marker) } + */ //POINTS LIST IS NOT IN ORDER ANY MORE //if(!map.overlayManager.contains(stopsOverlay)){ // map.overlayManager.add(stopsOverlay) //} - polyline!!.setOnClickListener(Polyline.OnClickListener { polyline, mapView, eventPos -> - Log.d(DEBUG_TAG, "clicked") - true - }) - - //map.controller.zoomToB//#animateTo(pointsList[0]) - val del = 0.008 - map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), true) + if(zoomToPattern) zoomToCurrentPattern() //map.invalidate() } private fun initializeRecyclerView(){ val llManager = LinearLayoutManager(context) llManager.orientation = LinearLayoutManager.VERTICAL stopsRecyclerView.layoutManager = llManager } - private fun showStopsAsList(stops: List){ + private fun showStopsInRecyclerView(stops: List){ Log.d(DEBUG_TAG, "Setting stops from: "+viewModel.currentPatternStops.value) val orderBy = viewModel.currentPatternStops.value!!.withIndex().associate{it.value.stopGtfsId to it.index} val stopsSorted = stops.sortedBy { s -> orderBy[s.gtfsID] } val numStops = stopsSorted.size Log.d(DEBUG_TAG, "RecyclerView adapter is: ${stopsRecyclerView.adapter}") val setNewAdapter = true if(setNewAdapter){ stopsRecyclerView.adapter = StopRecyclerAdapter( stopsSorted, stopAdapterListener, StopRecyclerAdapter.Use.LINES, NameCapitalize.FIRST ) } - - } - /** - * Remove bus marker from overlay associated with tripID + * This method fixes the display of the pattern, to be used when clicking on a bus */ - private fun removeBusMarker(tripID: String){ - if(!busPositionMarkersByTrip.containsKey(tripID)){ - Log.e(DEBUG_TAG, "Asked to remove veh with tripID $tripID but it's supposedly not shown") - return - } - val marker = busPositionMarkersByTrip[tripID] - busPositionsOverlay.remove(marker) - busPositionMarkersByTrip.remove(tripID) - - val animator = tripMarkersAnimators[tripID] - animator?.let{ - it.cancel() - tripMarkersAnimators.remove(tripID) - } - - } - - private fun showPatternWithStop(patternId: String){ + private fun showPatternWithCode(patternId: String){ //var index = 0 Log.d(DEBUG_TAG, "Showing pattern with code $patternId ") for (i in currentPatterns.indices){ val pattStop = currentPatterns[i] if(pattStop.pattern.code == patternId){ Log.d(DEBUG_TAG, "Pattern found in position $i") //setPatternAndReqStops(pattStop) patternsSpinner.setSelection(i) break } } } + private fun removeVehiclesData(vehs: List){ + for(v in vehs){ + if (updatesByVehDict.contains(v)) + updatesByVehDict.remove(v) + } + } + /** - * draw the position of the buses in the map. Copied from MapFragment + * Update function for the bus positions + * Takes the processed updates and saves them accordingly + * Copied from MapLibreFragment, removing the labels */ - private fun updateBusPositionsInMap(tripsPatterns: java.util.HashMap> - ) { - //Log.d(MapFragment.DEBUG_TAG, "Updating positions of the buses") - //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); - // cleanup the patterns - // at first run, the buses which have no direction are still displayed. If those become missing in the data, - // it becomes clear that they don't have the same direction - val currentBusesTripsIds = HashSet(busPositionMarkersByTrip.keys) - for (tripID in currentBusesTripsIds){ - if (!tripsPatterns.keys.contains(tripID)){ - //the tripId is not in the updates anymore, remove it - removeBusMarker(tripID) - } - } - - val noPatternsTrips = ArrayList() - for (tripID in tripsPatterns.keys) { - val (update, tripWithPatternStops) = tripsPatterns[tripID] ?: continue - - var marker: Marker? = null - //check if Marker is already created - if (busPositionMarkersByTrip.containsKey(tripID)) { - - //check if the trip direction ID is the same, if not remove - if(tripWithPatternStops?.pattern != null && - tripWithPatternStops.pattern.directionId != viewingPattern?.pattern?.directionId){ - removeBusMarker(tripID) - - } else { - //need to change the position of the marker - marker = busPositionMarkersByTrip.get(tripID)!! - BusPositionUtils.updateBusPositionMarker(map, marker, update, tripMarkersAnimators, false) - // Set the pattern to add the info - if (marker.infoWindow != null && marker.infoWindow is BusInfoWindow) { - val window = marker.infoWindow as BusInfoWindow - if (window.pattern == null && tripWithPatternStops != null) { - //Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID); - window.setPatternAndDraw(tripWithPatternStops.pattern) + private fun updateBusPositionsInMap(incomingData: HashMap>){ + val vehsNew = HashSet(incomingData.values.map { up -> up.first.vehicle }) + val vehsOld = HashSet(updatesByVehDict.keys) + Log.d(DEBUG_TAG, "In fragment, have ${incomingData.size} updates to show") + + var countUpds = 0 + //val symbolsToUpdate = ArrayList() + for (upsWithTrp in incomingData.values){ + val pos = upsWithTrp.first + val patternStops = upsWithTrp.second + val vehID = pos.vehicle + var animate = false + if (vehsOld.contains(vehID)){ + //update position only if the starting or the stopping position of the animation are in the view + val oldPos = updatesByVehDict[vehID]?.posUpdate + val oldPattern = updatesByVehDict[vehID]?.pattern + var avoidShowingUpdateBecauseIsImpossible = false + oldPos?.let{ + + if(it.routeID!=pos.routeID) { + val dist = LatLng(it.latitude, it.longitude).distanceTo(LatLng(pos.latitude, pos.longitude)) + val speed = dist*3.6 / (pos.timestamp - it.timestamp) //this should be in km/h + Log.w(DEBUG_TAG, "Vehicle $vehID changed route from ${oldPos.routeID} to ${pos.routeID}, distance: $dist, speed: $speed") + if (speed > 120 || speed < 0){ + avoidShowingUpdateBecauseIsImpossible = true } } } - } else { - //marker is not there, need to make it - //if (mapView == null) Log.e(MapFragment.DEBUG_TAG, "Creating marker with null map, things will explode") - marker = Marker(map) - - //String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); - val mdraw = ResourcesCompat.getDrawable(getResources(), R.drawable.map_bus_position_icon, null)!! - //mdraw.setBounds(0,0,28,28); - - marker.icon = mdraw - var markerPattern: MatoPattern? = null - if (tripWithPatternStops != null) { - if (tripWithPatternStops.pattern != null) - markerPattern = tripWithPatternStops.pattern - } - marker.infoWindow = BusInfoWindow(map, update, markerPattern, true) { - // set pattern to show - if(it!=null) - showPatternWithStop(it.code) + if (avoidShowingUpdateBecauseIsImpossible){ + // DO NOT SHOW THIS SHIT + Log.w(DEBUG_TAG, "Update for vehicle $vehID skipped") + continue } - //marker.infoWindow as BusInfoWindow - marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) - marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) - BusPositionUtils.updateBusPositionMarker(map,marker, update, tripMarkersAnimators,true) - // the overlay is null when it's not attached yet? - // cannot recreate it because it becomes null very soon - // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); - //save the marker - if (busPositionsOverlay != null) { - busPositionsOverlay.add(marker) - busPositionMarkersByTrip.put(tripID, marker) + + val samePosition = oldPos?.let { (it.latitude==pos.latitude)&&(it.longitude == pos.longitude) }?:false + val setPattern = (oldPattern==null) && (patternStops!=null) + if((!samePosition)|| setPattern) { + //TODO RESTORE THIS PART + /*val isPositionInBounds = isInsideVisibleRegion( + pos.latitude, pos.longitude, true + ) || (oldPos?.let { isInsideVisibleRegion(it.latitude,it.longitude,true) } ?: false) + + */ + val skip = true + if (skip) { + //animate = true + // set the pattern data too + updatesByVehDict[vehID]!!.pattern = patternStops?.pattern + //this moves both the icon and the label + animateNewPositionMove(pos) + + } else { + //update + updatesByVehDict[vehID] = LivePositionTripPattern(pos,patternStops?.pattern) + /*busLabelSymbolsByVeh[vehID]?.let { + it.latLng = LatLng(pos.latitude, pos.longitude) + symbolsToUpdate.add(it) + }*/ + //if(vehShowing==vehID) + // map?.animateCamera(CameraUpdateFactory.newLatLng(LatLng(pos.latitude, pos.longitude)),500) + //TODO: Follow the vehicle + } } + countUpds++ + } + else{ + //not inside + // update it simply + updatesByVehDict[vehID] = LivePositionTripPattern(pos, patternStops?.pattern) + //createLabelForVehicle(pos) + //if(vehShowing==vehID) + // map?.animateCamera(CameraUpdateFactory.newLatLng(LatLng(pos.latitude, pos.longitude)),500) + } + if (vehID == vehShowing){ + //update the data + showVehicleTripInBottomSheet(vehID) + } + } + //symbolManager.update(symbolsToUpdate) + //remove old positions + Log.d(DEBUG_TAG, "Updated $countUpds vehicles") + vehsOld.removeAll(vehsNew) + //now vehsOld contains the vehicles id for those that have NOT been updated + val currentTimeStamp = System.currentTimeMillis() /1000 + for(vehID in vehsOld){ + //remove after 2 minutes of inactivity + if (updatesByVehDict[vehID]!!.posUpdate.timestamp - currentTimeStamp > 2*60){ + updatesByVehDict.remove(vehID) + //removeVehicleLabel(vehID) } } + //update UI + updatePositionsIcons(false) + } + + /** + * This is the tricky part, animating the transitions + * Basically, we need to set the new positions with the data and redraw them all + */ + private fun animateNewPositionMove(positionUpdate: LivePositionUpdate){ + if (positionUpdate.vehicle !in updatesByVehDict.keys) + return + val vehID = positionUpdate.vehicle + val currentUpdate = updatesByVehDict[positionUpdate.vehicle] + currentUpdate?.let { it -> + //cancel current animation on vehicle + animatorsByVeh[vehID]?.cancel() + val posUp = it.posUpdate + + val currentPos = LatLng(posUp.latitude, posUp.longitude) + val newPos = LatLng(positionUpdate.latitude, positionUpdate.longitude) + val valueAnimator = ValueAnimator.ofObject(MapLibreUtils.LatLngEvaluator(), currentPos, newPos) + valueAnimator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener { + private var latLng: LatLng? = null + override fun onAnimationUpdate(animation: ValueAnimator) { + latLng = animation.animatedValue as LatLng + //update position on animation + val update = updatesByVehDict[positionUpdate.vehicle]!! + latLng?.let { ll-> + update.posUpdate.latitude = ll.latitude + update.posUpdate.longitude = ll.longitude + updatePositionsIcons(false) + } + } + }) + /*valueAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + super.onAnimationStart(animation) + //val update = positionsByVehDict[positionUpdate.vehicle]!! + //remove the label at the start of the animation + //removeVehicleLabel(vehID) + val annot = busLabelSymbolsByVeh[vehID] + annot?.let { sym -> + sym.textOpacity = 0.0f + symbolsToUpdate.add(sym) + } + } + + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + /*val annot = busLabelSymbolsByVeh[vehID] + annot?.let { sym -> + sym.textOpacity = 1.0f + sym.latLng = newPos //LatLng(newPos) + symbolsToUpdate.add(sym) + } + + */ + } + }) + */ + animatorsByVeh[vehID]?.cancel() + //set the new position as the current one but with the old lat and lng + positionUpdate.latitude = posUp.latitude + positionUpdate.longitude = posUp.longitude + updatesByVehDict[vehID]!!.posUpdate = positionUpdate + valueAnimator.duration = 300 + valueAnimator.interpolator = LinearInterpolator() + valueAnimator.start() + + animatorsByVeh[vehID] = valueAnimator + + } ?: { + Log.e(DEBUG_TAG, "Have to run animation for veh ${positionUpdate.vehicle} but not in the dict, adding") + //updatesByVehDict[positionUpdate.vehicle] = positionUpdate + } + } + + /** + * Update the bus positions displayed on the map, from the existing data + */ + private fun updatePositionsIcons(forced: Boolean){ + //avoid frequent updates + val currentTime = System.currentTimeMillis() + if(!forced && currentTime - lastUpdateTime < 60){ + //DO NOT UPDATE THE MAP + return + } + val features = ArrayList()//stops.mapNotNull { stop -> + //stop.latitude?.let { lat -> + // stop.longitude?.let { lon -> + for (dat in updatesByVehDict.values){ + //if (s.latitude!=null && s.longitude!=null) + val pos = dat.posUpdate + val point = Point.fromLngLat(pos.longitude, pos.latitude) + features.add( + Feature.fromGeometry( + point, + JsonObject().apply { + addProperty("veh", pos.vehicle) + addProperty("trip", pos.tripID) + addProperty("bearing", pos.bearing ?:0.0f) + addProperty("line", pos.routeID) + } + ) + ) + /*busLabelSymbolsByVeh[pos.vehicle]?.let { + it.latLng = LatLng(pos.latitude, pos.longitude) + symbolsToUpdate.add(it) + } - if (noPatternsTrips.size > 0) { - Log.i(DEBUG_TAG, "These trips have no matching pattern: $noPatternsTrips") + */ } + busesSource.setGeoJson(FeatureCollection.fromFeatures(features)) + //update labels, clear cache to be used + //symbolManager.update(symbolsToUpdate) + //symbolsToUpdate.clear() + lastUpdateTime = System.currentTimeMillis() } + override fun onResume() { super.onResume() Log.d(DEBUG_TAG, "Resetting paused from onResume") + mapView.onResume() pausedFragment = false val keySourcePositions = getString(R.string.pref_positions_source) useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions, "mqtt").contentEquals("mqtt") //separate paths if(useMQTTPositions) liveBusViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) else liveBusViewModel.requestGTFSUpdates() if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ " zoom ${mapViewModel.currentZoom.value}") - val controller = map.controller + //THIS WAS A FIX FOR THE OLD OSMDROID MAP + /*val controller = map.controller viewLifecycleOwner.lifecycleScope.launch { delay(100) Log.d(DEBUG_TAG, "zooming back to point") controller.animateTo(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!), mapViewModel.currentZoom.value!!,null,null) //controller.setCenter(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!)) //controller.setZoom(mapViewModel.currentZoom.value!!) } - - //controller.setZoom() + */ } //initialize GUI here fragmentListener.readyGUIfor(FragmentKind.LINES) } override fun onPause() { super.onPause() - liveBusViewModel.stopMatoUpdates() + mapView.onPause() + if(useMQTTPositions) liveBusViewModel.stopMatoUpdates() pausedFragment = true //save map - val center = map.mapCenter - mapViewModel.currentLat.value = center.latitude - mapViewModel.currentLong.value = center.longitude - mapViewModel.currentZoom.value = map.zoomLevel.toDouble() + val camera = map?.cameraPosition + camera?.let {cam-> + mapViewModel.currentLat.value = cam.target?.latitude ?: -400.0 + mapViewModel.currentLong.value = cam.target?.longitude ?: -400.0 + mapViewModel.currentZoom.value = cam.zoom + } + + } + + override fun onStart() { + super.onStart() + mapView.onStart() + } + + override fun onDestroy() { + super.onDestroy() + mapView.onDestroy() + } + + override fun onStop() { + super.onStop() + mapView.onStop() + shownStopInBottomSheet?.let { + mapViewModel.stopShowing = it + } + shouldMapLocationBeReactivated = locationComponent.isLocationComponentEnabled + } + + override fun onDestroyView() { + map?.run { + Log.d(DEBUG_TAG, "Saving camera position") + savedCameraPosition = cameraPosition + } + + super.onDestroyView() + Log.d(DEBUG_TAG, "Destroying the views") + + /*mapStyle.removeLayer(STOPS_LAYER_ID) + + mapStyle?.removeSource(STOPS_SOURCE_ID) + + mapStyle.removeLayer(POLYLINE_LAYER) + mapStyle.removeSource(POLYLINE_SOURCE) + */ + //stopsLayerStarted = false + } + + override fun onMapDestroy() { + mapStyle.removeLayer(STOPS_LAYER_ID) + + mapStyle.removeSource(STOPS_SOURCE_ID) + + mapStyle.removeLayer(POLYLINE_LAYER) + mapStyle.removeSource(POLYLINE_SOURCE) + mapStyle.removeLayer(BUSES_LAYER_ID) + mapStyle.removeSource(BUSES_SOURCE_ID) + + + map?.locationComponent?.isLocationComponentEnabled = false } override fun getBaseViewForSnackBar(): View? { return null } companion object { private const val LINEID_KEY="lineID" - fun newInstance() = LinesDetailFragment() - const val DEBUG_TAG="LinesDetailFragment" + private const val STOPID_FROM_KEY="stopID" + private const val STOPS_SOURCE_ID = "stops-source" + private const val STOPS_LAYER_ID = "stops-layer" + private const val STOP_ACTIVE_IMG = "stop_active_img" + private const val STOP_IMAGE_ID = "stop-img" + private const val POLYLINE_LAYER = "polyline-layer" + private const val POLYLINE_SOURCE = "polyline-source" + + private const val POLY_ARROWS_LAYER = "arrows-layer" + private const val POLY_ARROWS_SOURCE = "arrows-source" + private const val POLY_ARROW ="poly-arrow-img" - fun makeArgs(lineID: String): Bundle{ + private const val DEBUG_TAG="BusTO-LineDetalFragment" + + fun makeArgs(lineID: String, stopIDFrom: String?): Bundle{ val b = Bundle() b.putString(LINEID_KEY, lineID) + b.putString(STOPID_FROM_KEY, stopIDFrom) return b } + fun newInstance(lineID: String?, stopIDFrom: String?) = LinesDetailFragment().apply { + lineID?.let { arguments = makeArgs(it, stopIDFrom) } + } @JvmStatic - private fun findOptimalPosition(stop: Stop, pointsList: MutableList): GeoPoint{ + private fun findOptimalPosition(stop: Stop, pointsList: MutableList): LatLng{ if(stop.latitude==null || stop.longitude ==null|| pointsList.isEmpty()) throw IllegalArgumentException() val sLat = stop.latitude!! val sLong = stop.longitude!! if(pointsList.size < 2) return pointsList[0] pointsList.sortBy { utils.measuredistanceBetween(sLat, sLong, it.latitude, it.longitude) } val p1 = pointsList[0] val p2 = pointsList[1] if (p1.longitude == p2.longitude){ //Log.e(DEBUG_TAG, "Same longitude") - return GeoPoint(sLat, p1.longitude) + return LatLng(sLat, p1.longitude) } else if (p1.latitude == p2.latitude){ //Log.d(DEBUG_TAG, "Same latitude") - return GeoPoint(p2.latitude,sLong) + return LatLng(p2.latitude,sLong) } val m = (p1.latitude - p2.latitude) / (p1.longitude - p2.longitude) val minv = (p1.longitude-p2.longitude)/(p1.latitude - p2.latitude) val cR = p1.latitude - p1.longitude * m val longNew = (minv * sLong + sLat -cR ) / (m+minv) val latNew = (m*longNew + cR) //Log.d(DEBUG_TAG,"Stop ${stop.ID} old pos: ($sLat, $sLong), new pos ($latNew,$longNew)") - return GeoPoint(latNew,longNew) + return LatLng(latNew,longNew) } private const val DEFAULT_CENTER_LAT = 45.12 private const val DEFAULT_CENTER_LON = 7.6858 } + + enum class BottomShowing{ + STOP, VEHICLE + } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt index f6a9cb9..5ce08c7 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesGridShowingFragment.kt @@ -1,430 +1,430 @@ package it.reyboz.bustorino.fragments import android.content.Context import android.os.Bundle import android.util.Log import android.view.* import android.view.animation.Animation import android.view.animation.LinearInterpolator import android.view.animation.RotateAnimation import android.widget.ImageView import android.widget.TextView import androidx.appcompat.widget.SearchView import androidx.core.view.MenuHost import androidx.core.view.MenuProvider import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.RecyclerView import androidx.work.WorkInfo import androidx.work.WorkManager import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.RouteAdapter import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter import it.reyboz.bustorino.adapters.StringListAdapter import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.DBUpdateWorker import it.reyboz.bustorino.data.PreferencesHolder import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager import it.reyboz.bustorino.util.LinesNameSorter import it.reyboz.bustorino.util.ViewUtils import it.reyboz.bustorino.viewmodels.LinesGridShowingViewModel class LinesGridShowingFragment : ScreenBaseFragment() { private val viewModel: LinesGridShowingViewModel by viewModels() //private lateinit var gridLayoutManager: AutoFitGridLayoutManager private lateinit var favoritesRecyclerView: RecyclerView private lateinit var urbanRecyclerView: RecyclerView private lateinit var extraurbanRecyclerView: RecyclerView private lateinit var touristRecyclerView: RecyclerView private lateinit var favoritesTitle: TextView private lateinit var urbanLinesTitle: TextView private lateinit var extrurbanLinesTitle: TextView private lateinit var touristLinesTitle: TextView private lateinit var updateMessageTextView: TextView //private lateinit var searchBar: SearchView private var routesByAgency = HashMap>() /*hashMapOf( AG_URBAN to ArrayList(), AG_EXTRAURB to ArrayList(), AG_TOUR to ArrayList() )*/ private lateinit var fragmentListener: CommonFragmentListener private val linesNameSorter = LinesNameSorter() private val linesComparator = Comparator { a,b -> return@Comparator linesNameSorter.compare(a.shortName, b.shortName) } private val routeClickListener = RouteAdapter.ItemClicker { - fragmentListener.showLineOnMap(it.gtfsId) + fragmentListener.showLineOnMap(it.gtfsId, null) } private val arrows = HashMap() private val durations = HashMap() //private val recyclerViewAdapters= HashMap() private val lastQueryEmptyForAgency = HashMap(3) private var openRecyclerView = "AG_URBAN" override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val rootView = inflater.inflate(R.layout.fragment_lines_grid, container, false) favoritesRecyclerView = rootView.findViewById(R.id.favoritesRecyclerView) urbanRecyclerView = rootView.findViewById(R.id.urbanLinesRecyclerView) extraurbanRecyclerView = rootView.findViewById(R.id.extraurbanLinesRecyclerView) touristRecyclerView = rootView.findViewById(R.id.touristLinesRecyclerView) updateMessageTextView = rootView.findViewById(R.id.updateMessageTextView) favoritesTitle = rootView.findViewById(R.id.favoritesTitleView) urbanLinesTitle = rootView.findViewById(R.id.urbanLinesTitleView) extrurbanLinesTitle = rootView.findViewById(R.id.extraurbanLinesTitleView) touristLinesTitle = rootView.findViewById(R.id.touristLinesTitleView) arrows[AG_URBAN] = rootView.findViewById(R.id.arrowUrb) arrows[AG_TOUR] = rootView.findViewById(R.id.arrowTourist) arrows[AG_EXTRAURB] = rootView.findViewById(R.id.arrowExtraurban) arrows[AG_FAV] = rootView.findViewById(R.id.arrowFavorites) //show urban expanded by default val recViews = listOf(urbanRecyclerView, extraurbanRecyclerView, touristRecyclerView) for (recyView in recViews) { val gridLayoutManager = AutoFitGridLayoutManager( requireContext().applicationContext, (utils.convertDipToPixels(context, COLUMN_WIDTH_DP.toFloat())).toInt() ) recyView.layoutManager = gridLayoutManager } //init favorites recyclerview val gridLayoutManager = AutoFitGridLayoutManager( requireContext().applicationContext, (utils.convertDipToPixels(context, 70f)).toInt() ) favoritesRecyclerView.layoutManager = gridLayoutManager viewModel.getLinesLiveData().observe(viewLifecycleOwner){ //routesList = ArrayList(it) //routesList.sortWith(linesComparator) routesByAgency.clear() for (k in AGENCIES){ routesByAgency[k] = ArrayList() } for(route in it){ val agency = route.agencyID if(agency !in routesByAgency.keys){ Log.e(DEBUG_TAG, "The agency $agency is not present in the predefined agencies (${routesByAgency.keys})") } routesByAgency[agency]?.add(route) } //zip agencies and recyclerviews Companion.AGENCIES.zip(recViews) { ag, recView -> routesByAgency[ag]?.let { routeList -> if (routeList.size > 0) { routeList.sortWith(linesComparator) //val adapter = RouteOnlyLineAdapter(it.map { rt -> rt.shortName }) val adapter = RouteAdapter(routeList, routeClickListener) val lastQueryEmpty = if(ag in lastQueryEmptyForAgency.keys) lastQueryEmptyForAgency[ag]!! else true if (lastQueryEmpty) recView.adapter = adapter else recView.swapAdapter(adapter, false) lastQueryEmptyForAgency[ag] = false } else { val messageString = if(viewModel.getLineQueryValue().isNotEmpty()) getString(R.string.no_lines_found_query) else getString(R.string.no_lines_found) val extraAdapter = StringListAdapter(listOf(messageString)) recView.adapter = extraAdapter lastQueryEmptyForAgency[ag] = true } durations[ag] = if(routeList.size < 20) ViewUtils.DEF_DURATION else 1000 } } } viewModel.favoritesLines.observe(viewLifecycleOwner){ routes-> val routesNames = routes.map { it.shortName } //create new item click listener every time val adapter = RouteOnlyLineAdapter(routesNames){ pos, _ -> val r = routes[pos] - fragmentListener.showLineOnMap(r.gtfsId) + fragmentListener.showLineOnMap(r.gtfsId, null) } favoritesRecyclerView.adapter = adapter } //onClicks urbanLinesTitle.setOnClickListener { openLinesAndCloseOthersIfNeeded(AG_URBAN) } extrurbanLinesTitle.setOnClickListener { openLinesAndCloseOthersIfNeeded(AG_EXTRAURB) } touristLinesTitle.setOnClickListener { openLinesAndCloseOthersIfNeeded(AG_TOUR) } favoritesTitle.setOnClickListener { closeOpenFavorites() } arrows[AG_FAV]?.setOnClickListener { closeOpenFavorites() } //arrows onClicks for(k in Companion.AGENCIES){ //k is either AG_TOUR, AG_EXTRAURBAN, AG_URBAN arrows[k]?.setOnClickListener { openLinesAndCloseOthersIfNeeded(k) } } // watch for the db update WorkManager.getInstance(requireContext()).getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG).observe(viewLifecycleOwner){ workInfoList -> if (workInfoList == null || workInfoList.isEmpty()) { return@observe } var showProgress = false for (workInfo in workInfoList) { if (workInfo.state == WorkInfo.State.RUNNING) { updateMessageTextView.visibility = View.VISIBLE } else{ updateMessageTextView.visibility = View.GONE } break } } return rootView } fun setUserSearch(textSearch:String){ viewModel.setLineQuery(textSearch) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val menuHost: MenuHost = requireActivity() // Add menu items without using the Fragment Menu APIs // Note how we can tie the MenuProvider to the viewLifecycleOwner // and an optional Lifecycle.State (here, RESUMED) to indicate when // the menu should be visible menuHost.addMenuProvider(object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { // Add menu items here menuInflater.inflate(R.menu.menu_search, menu) val search = menu.findItem(R.id.searchMenuItem).actionView as SearchView search.setOnQueryTextListener(object : SearchView.OnQueryTextListener{ override fun onQueryTextSubmit(query: String?): Boolean { setUserSearch(query ?: "") return true } override fun onQueryTextChange(query: String?): Boolean { setUserSearch(query ?: "") return true } }) search.queryHint = getString(R.string.search_box_lines_suggestion_filter) } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { // Handle the menu selection if (menuItem.itemId == R.id.searchMenuItem){ Log.d(DEBUG_TAG, "Clicked on search menu") } else{ Log.d(DEBUG_TAG, "Clicked on something else") } return false } }, viewLifecycleOwner, Lifecycle.State.RESUMED) } private fun closeOpenFavorites(){ if(favoritesRecyclerView.visibility == View.VISIBLE){ //close it favoritesRecyclerView.visibility = View.GONE setOpen(arrows[AG_FAV]!!, false) viewModel.favoritesExpanded.value = false } else{ favoritesRecyclerView.visibility = View.VISIBLE setOpen(arrows[AG_FAV]!!, true) viewModel.favoritesExpanded.value = true } } private fun openLinesAndCloseOthersIfNeeded(agency: String){ if(openRecyclerView!="" && openRecyclerView!= agency) { switchRecyclerViewStatus(openRecyclerView) } switchRecyclerViewStatus(agency) } private fun switchRecyclerViewStatus(agency: String){ val recyclerView = when(agency){ AG_TOUR -> touristRecyclerView AG_EXTRAURB -> extraurbanRecyclerView AG_URBAN -> urbanRecyclerView else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid") } val expandedLiveData = when(agency){ AG_TOUR -> viewModel.isTouristExpanded AG_URBAN -> viewModel.isUrbanExpanded AG_EXTRAURB -> viewModel.isExtraUrbanExpanded else -> throw IllegalArgumentException("$DEBUG_TAG: Agency Invalid") } val duration = durations[agency] val arrow = arrows[agency] val durArrow = if(duration == null || duration==ViewUtils.DEF_DURATION) 500 else duration if(duration!=null&&arrow!=null) when (recyclerView.visibility){ View.GONE -> { Log.d(DEBUG_TAG, "Open recyclerview $agency") //val a =ViewUtils.expand(recyclerView, duration, 0) recyclerView.visibility = View.VISIBLE expandedLiveData.value = true Log.d(DEBUG_TAG, "Arrow for $agency has rotation: ${arrow.rotation}") setOpen(arrow, true) //arrow.startAnimation(rotateArrow(true,durArrow)) openRecyclerView = agency } View.VISIBLE -> { Log.d(DEBUG_TAG, "Close recyclerview $agency") //ViewUtils.collapse(recyclerView, duration) recyclerView.visibility = View.GONE expandedLiveData.value = false //arrow.rotation = 90f Log.d(DEBUG_TAG, "Arrow for $agency has rotation ${arrow.rotation} pre-rotate") setOpen(arrow, false) //arrow.startAnimation(rotateArrow(false,durArrow)) openRecyclerView = "" } View.INVISIBLE -> { TODO() } } } override fun onAttach(context: Context) { super.onAttach(context) if(context is CommonFragmentListener){ fragmentListener = context } else throw RuntimeException("$context must implement CommonFragmentListener") } override fun getBaseViewForSnackBar(): View? { return null } override fun onResume() { super.onResume() val pref = PreferencesHolder.getMainSharedPreferences(requireContext()) val res = pref.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) res?.let { viewModel.setFavoritesLinesIDs(HashSet(it))} //restore state viewModel.favoritesExpanded.value?.let { if(!it){ //close it favoritesRecyclerView.visibility = View.GONE setOpen(arrows[AG_FAV]!!, false) } else{ favoritesRecyclerView.visibility = View.VISIBLE setOpen(arrows[AG_FAV]!!, true) } } viewModel.isUrbanExpanded.value?.let { if(it) { urbanRecyclerView.visibility = View.VISIBLE arrows[AG_URBAN]?.rotation= 90f openRecyclerView = AG_URBAN Log.d(DEBUG_TAG, "RecyclerView gtt:U is expanded") } else { urbanRecyclerView.visibility = View.GONE arrows[AG_URBAN]?.rotation= 0f } } viewModel.isTouristExpanded.value?.let { val recview = touristRecyclerView if(it) { recview.visibility = View.VISIBLE arrows[AG_TOUR]?.rotation=90f openRecyclerView = AG_TOUR } else { recview.visibility = View.GONE arrows[AG_TOUR]?.rotation= 0f } } viewModel.isExtraUrbanExpanded.value?.let { val recview = extraurbanRecyclerView if(it) { openRecyclerView = AG_EXTRAURB recview.visibility = View.VISIBLE arrows[AG_EXTRAURB]?.rotation=90f } else { recview.visibility = View.GONE arrows[AG_EXTRAURB]?.rotation=0f } } fragmentListener.readyGUIfor(FragmentKind.LINES) } companion object { private const val COLUMN_WIDTH_DP=200 private const val AG_FAV = "fav" private const val AG_URBAN = "gtt:U" private const val AG_EXTRAURB ="gtt:E" private const val AG_TOUR ="gtt:T" private const val DEBUG_TAG ="BusTO-LinesGridFragment" const val FRAGMENT_TAG = "LinesGridShowingFragment" private val AGENCIES = listOf(AG_URBAN, AG_EXTRAURB, AG_TOUR) fun newInstance() = LinesGridShowingFragment() @JvmStatic fun setOpen(imageView: ImageView, value: Boolean){ if(value) imageView.rotation = 90f else imageView.rotation = 0f } @JvmStatic fun rotateArrow(toOpen: Boolean, duration: Long): RotateAnimation{ val start = if (toOpen) 0f else 90f val stop = if(toOpen) 90f else 0f Log.d(DEBUG_TAG, "Rotate arrow from $start to $stop") val rotate = RotateAnimation(start, stop, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f) rotate.duration = duration rotate.interpolator = LinearInterpolator() //rotate.fillAfter = true rotate.fillBefore = false return rotate } } override fun showSnackbarOnDBUpdate(): Boolean { return false } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java index 6d043e9..20d9817 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,881 +1,881 @@ package it.reyboz.bustorino.fragments; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.location.Criteria; import android.location.Location; import android.net.Uri; import android.os.Build; import android.os.Bundle; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageButton; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ProgressBar; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.List; import java.util.Map; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.data.PreferencesHolder; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.middleware.AsyncArrivalsSearcher; import it.reyboz.bustorino.middleware.AsyncStopsSearcher; import it.reyboz.bustorino.middleware.BarcodeScanContract; import it.reyboz.bustorino.middleware.BarcodeScanOptions; import it.reyboz.bustorino.middleware.BarcodeScanUtils; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.Permissions; import org.jetbrains.annotations.NotNull; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS; /** * A simple {@link Fragment} subclass. * Use the {@link MainScreenFragment#newInstance} factory method to * create an instance of this fragment. */ public class MainScreenFragment extends ScreenBaseFragment implements FragmentListenerMain{ private static final String SAVED_FRAGMENT="saved_fragment"; private static final String DEBUG_TAG = "BusTO - MainFragment"; public static final String PENDING_STOP_SEARCH="PendingStopSearch"; public final static String FRAGMENT_TAG = "MainScreenFragment"; private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; private FrameLayout resultFrameLayout; private boolean setupOnStart = true; private boolean suppressArrivalsReload = false; //private Snackbar snackbar; /* * Search mode */ private static final int SEARCH_BY_NAME = 0; private static final int SEARCH_BY_ID = 1; //private static final int SEARCH_BY_ROUTE = 2; // implement this -- DONE! private int searchMode; //private ImageButton addToFavorites; //// HIDDEN BUT IMPORTANT ELEMENTS //// FragmentManager childFragMan; Handler mainHandler; private final Runnable refreshStop = new Runnable() { public void run() { if(getContext() == null) return; List fetcherList = utils.getDefaultArrivalsFetchers(getContext()); ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[fetcherList.size()]; arrivalsFetchers = fetcherList.toArray(arrivalsFetchers); if (childFragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) childFragMan.findFragmentById(R.id.resultFrame); if (fragment == null){ //we create a new fragment, which is WRONG Log.e("BusTO-RefreshStop", "Asking for refresh when there is no fragment"); // AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute(); } else{ String stopName = fragment.getStopID(); new AsyncArrivalsSearcher(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName); } } else //we create a new fragment, which is WRONG new AsyncArrivalsSearcher(fragmentHelper, arrivalsFetchers, getContext()).execute(); } }; // private final ActivityResultLauncher barcodeLauncher = registerForActivityResult(new BarcodeScanContract(), result -> { if(result!=null && result.getContents()!=null) { //Toast.makeText(MyActivity.this, "Cancelled", Toast.LENGTH_LONG).show(); Uri uri; try { uri = Uri.parse(result.getContents()); // this apparently prevents NullPointerException. Somehow. } catch (NullPointerException e) { if (getContext()!=null) Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); return; } String busStopID = getBusStopIDFromUri(uri); busStopSearchByIDEditText.setText(busStopID); requestArrivalsForStopID(busStopID); } else { //Toast.makeText(MyActivity.this, "Scanned: " + result.getContents(), Toast.LENGTH_LONG).show(); if (getContext()!=null) Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); } }); /// LOCATION STUFF /// boolean pendingIntroRun = false; boolean pendingNearbyStopsFragmentRequest = false; boolean locationPermissionGranted, locationPermissionAsked = false; AppLocationManager locationManager; private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { @Override public void onActivityResult(Map result) { if(result==null) return; if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null || result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; Log.d(DEBUG_TAG, "Permissions for location are: "+result); if(Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)) || Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))){ locationPermissionGranted = true; Log.w(DEBUG_TAG, "Starting position"); if (mListener!= null && getContext()!=null){ if (locationManager==null) locationManager = AppLocationManager.getInstance(getContext()); locationManager.addLocationRequestFor(requester); } // show nearby fragment //showNearbyStopsFragment(); Log.d(DEBUG_TAG, "We have location permission"); if(pendingNearbyStopsFragmentRequest){ showNearbyFragmentIfPossible(); pendingNearbyStopsFragmentRequest = false; } } if(pendingNearbyStopsFragmentRequest) pendingNearbyStopsFragmentRequest =false; } }); private final LocationCriteria cr = new LocationCriteria(2000, 10000); //Location private AppLocationManager.LocationRequester requester = new AppLocationManager.LocationRequester() { @Override public void onLocationChanged(Location loc) { } @Override public void onLocationStatusChanged(int status) { if(status == AppLocationManager.LOCATION_GPS_AVAILABLE && !isNearbyFragmentShown() && checkLocationPermission()){ //request Stops //pendingNearbyStopsRequest = false; if (getContext()!= null && !isNearbyFragmentShown()) //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); showNearbyFragmentIfPossible(); } } @Override public long getLastUpdateTimeMillis() { return 50; } @Override public LocationCriteria getLocationCriteria() { return cr; } @Override public void onLocationProviderAvailable() { //Log.w(DEBUG_TAG, "pendingNearbyStopRequest: "+pendingNearbyStopsRequest); if(!isNearbyFragmentShown() && getContext()!=null){ // we should have the location permission if(!checkLocationPermission()) Log.e(DEBUG_TAG, "Asking to show nearbystopfragment when " + "we have no location permission"); pendingNearbyStopsFragmentRequest = true; //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); showNearbyFragmentIfPossible(); } } @Override public void onLocationDisabled() { } }; //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; private CoordinatorLayout coordLayout; public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance() { return new MainScreenFragment(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing Log.d(DEBUG_TAG, "ARGS ARE NOT NULL: "+getArguments()); if (getArguments().getString(PENDING_STOP_SEARCH)!=null) pendingStopID = getArguments().getString(PENDING_STOP_SEARCH); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_main_screen, container, false); /// UI ELEMENTS // busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); resultFrameLayout = root.findViewById(R.id.resultFrame); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); swipeRefreshLayout .setOnRefreshListener(() -> mainHandler.post(refreshStop)); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); coordLayout = root.findViewById(R.id.coord_layout); floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); AppCompatImageButton searchButton = root.findViewById(R.id.searchButton); searchButton.setOnClickListener(this::onSearchClick); // Fragment stuff childFragMan = getChildFragmentManager(); childFragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED")); fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); setSearchModeBusStopID(); cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); locationManager = AppLocationManager.getInstance(requireContext()); Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Log.d(DEBUG_TAG, "onViewCreated, SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE)); Log.d(DEBUG_TAG, "Saved instance state is: "+savedInstanceState); //Restore instance state /*if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnStart = false; } } */ if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Log.d(DEBUG_TAG, "Saving instance state"); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); if (fragmentHelper!=null) fragmentHelper.setBlockAllActivities(true); } public void setSuppressArrivalsReload(boolean value){ suppressArrivalsReload = value; // we have to suppress the reloading of the (possible) ArrivalsFragment /*if(value) { Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment) { ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } } */ } /** * Cancel the reload of the arrival times * because we are going to pop the fragment */ public void cancelReloadArrivalsIfNeeded(){ if(getContext()==null) return; //we are not attached //Fragment fr = getChildFragmentManager().findFragmentById(R.id.resultFrame); fragmentHelper.stopLastRequestIfNeeded(true); toggleSpinner(false); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+ setupOnStart); mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onStart() { super.onStart(); Log.d(DEBUG_TAG, "onStart called, setupOnStart: "+setupOnStart); if (setupOnStart) { if (pendingStopID==null){ if(PreferencesHolder.hasIntroFinishedOneShot(requireContext())){ Log.d(DEBUG_TAG, "Showing nearby stops"); if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsFragmentRequest = true; } else { showNearbyFragmentIfPossible(); } } else { //The Introductory Activity is about to be started, hence pause the request and show later pendingIntroRun = true; } } else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnStart = false; } } @Override public void onResume() { super.onResume(); final Context con = requireContext(); Log.w(DEBUG_TAG, "OnResume called, setupOnStart: "+ setupOnStart); if (locationManager == null) locationManager = AppLocationManager.getInstance(con); //recheck the introduction activity has been run if(pendingIntroRun && PreferencesHolder.hasIntroFinishedOneShot(con)){ //request position permission if needed if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsFragmentRequest = true; } else { showNearbyFragmentIfPossible(); } //deactivate flag pendingIntroRun = false; } if(Permissions.bothLocationPermissionsGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); if(!locationManager.isRequesterRegistered(requester)) locationManager.addLocationRequestFor(requester); } //don't request permission // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); //this is the second time we are attaching this fragment -> Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); //TODO: if we come back to this from another fragment, and the user has given again the permission // for the Location, we should show the Nearby Stops if(!suppressArrivalsReload && pendingStopID==null){ //none of the following cases are true // check if we are showing any fragment final Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if(fragment==null || swipeRefreshLayout.getVisibility() != View.VISIBLE){ //we are not showing anything if(Permissions.anyLocationPermissionsGranted(getContext())){ showNearbyFragmentIfPossible(); } } } if (suppressArrivalsReload){ // we have to suppress the reloading of the (possible) ArrivalsFragment Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment){ ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } //deactivate suppressArrivalsReload = false; } if(pendingStopID!=null){ Log.d(DEBUG_TAG, "Pending request for arrivals at stop ID: "+pendingStopID); requestArrivalsForStopID(pendingStopID); pendingStopID = null; } mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); fragmentHelper.setBlockAllActivities(false); } @Override public void onPause() { //mainHandler = null; locationManager.removeLocationRequestFor(requester); super.onPause(); fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(true); } /* GUI METHODS */ /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { BarcodeScanOptions scanOptions = new BarcodeScanOptions(); Intent intent = scanOptions.createScanIntent(); if(!BarcodeScanUtils.checkTargetPackageExists(getContext(), intent)){ BarcodeScanUtils.showDownloadDialog(null, this); }else { barcodeLauncher.launch(scanOptions); } } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); fragmentHelper.stopLastRequestIfNeeded(true); requestArrivalsForStopID(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); query = query.trim(); if(getContext()!=null) { if (query.length() < 1) { Toast.makeText(getContext(), R.string.insert_bus_stop_name_error, Toast.LENGTH_SHORT).show(); } else if(query.length()< 2){ Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); } else { fragmentHelper.stopLastRequestIfNeeded(true); new AsyncStopsSearcher(fragmentHelper, stopsFinderByNames).execute(query); } } } } public void onToggleKeyboardLayout(View v) { if (searchMode == SEARCH_BY_NAME) { setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } else { // searchMode == SEARCH_BY_ID setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } } } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// public void showKeyboard() { if(getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } private void setSearchModeBusStopID() { searchMode = SEARCH_BY_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SEARCH_BY_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } protected boolean isNearbyFragmentShown(){ Fragment fragment = getChildFragmentManager().findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); return (fragment!= null && fragment.isResumed()); } /** * Having that cursor at the left of the edit text makes me cancer. * * @param busStopID bus stop ID */ private void setBusStopSearchByIDEditText(String busStopID) { busStopSearchByIDEditText.setText(busStopID); busStopSearchByIDEditText.setSelection(busStopID.length()); } @Nullable @org.jetbrains.annotations.Nullable @Override public View getBaseViewForSnackBar() { return coordLayout; } @Override public void toggleSpinner(boolean enable) { if (enable) { //already set by the RefreshListener when needed //swipeRefreshLayout.setRefreshing(true); progressBar.setVisibility(View.VISIBLE); } else { swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } } private void prepareGUIForBusLines() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void actuallyShowNearbyStopsFragment(){ swipeRefreshLayout.setVisibility(View.VISIBLE); final Fragment existingFrag = childFragMan.findFragmentById(R.id.resultFrame); // fragment; if (!(existingFrag instanceof NearbyStopsFragment)){ Log.d(DEBUG_TAG, "actually showing Nearby Stops Fragment"); //there is no fragment showing final NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.FragType.STOPS); FragmentTransaction ft = childFragMan.beginTransaction(); ft.replace(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG); if (getActivity()!=null && !getActivity().isFinishing()) ft.commit(); else Log.e(DEBUG_TAG, "Not showing nearby fragment because activity null or is finishing"); } } @Override public void showFloatingActionButton(boolean yes) { mListener.showFloatingActionButton(yes); } /** * This provides a temporary fix to make the transition * to a single asynctask go smoother * * @param fragmentType the type of fragment created */ @Override public void readyGUIfor(FragmentKind fragmentType) { //if we are getting results, already, stop waiting for nearbyStops if (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS) { hideKeyboard(); if (pendingNearbyStopsFragmentRequest) { locationManager.removeLocationRequestFor(requester); pendingNearbyStopsFragmentRequest = false; } } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForBusLines(); break; case STOPS: prepareGUIForBusStops(); break; default: Log.d(DEBUG_TAG, "Fragment type is unknown"); return; } // Shows hints } @Override - public void showLineOnMap(String routeGtfsId) { + public void showLineOnMap(String routeGtfsId, @Nullable String stopIDFrom) { //pass to activity - mListener.showLineOnMap(routeGtfsId); + mListener.showLineOnMap(routeGtfsId, stopIDFrom); } @Override public void showMapCenteredOnStop(Stop stop) { if(mListener!=null) mListener.showMapCenteredOnStop(stop); } /** * Main method for stops requests * @param ID the Stop ID */ @Override public void requestArrivalsForStopID(String ID) { if (!isResumed()){ //defer request pendingStopID = ID; Log.d(DEBUG_TAG, "Deferring update for stop "+ID+ " saved: "+pendingStopID); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } ArrivalsFetcher[] fetchers = utils.getDefaultArrivalsFetchers(getContext()).toArray(new ArrivalsFetcher[0]); if (ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress showToastMessage(R.string.insert_bus_stop_number_error, true); toggleSpinner(false); } else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); if (fragment != null && fragment.getStopID() != null && fragment.getStopID().equals(ID)){ // Run with previous fetchers //fragment.getCurrentFetchers().toArray() new AsyncArrivalsSearcher(fragmentHelper,fragment.getCurrentFetchersAsArray(), getContext()).execute(ID); } else{ new AsyncArrivalsSearcher(fragmentHelper, fetchers, getContext()).execute(ID); } } else { Log.d(DEBUG_TAG, "This is probably the first arrivals search, preparing GUI"); prepareGUIForBusLines(); new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop " + ID); } } private boolean checkLocationPermission(){ final Context context = getContext(); if(context==null) return false; final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; return isOldVersion || !noPermission; } private void requestLocationPermission(){ requestPermissionLauncher.launch(LOCATION_PERMISSIONS); } private void showNearbyFragmentIfPossible() { if (isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "Asked to show nearby fragment but we already are showing it"); return; } if (getContext() == null) { Log.e(DEBUG_TAG, "Wanting to show nearby fragment but context is null"); return; } if (fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !childFragMan.isDestroyed()) { //Go ahead with the request actuallyShowNearbyStopsFragment(); pendingNearbyStopsFragmentRequest = false; } } /////////// LOCATION METHODS ////////// /* private void startStopRequest(String provider) { Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); if (locmgr != null && mainHandler != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { } } */ /* * Run location requests separately and asynchronously class NearbyStopsRequester implements Runnable { Context appContext; Criteria cr; public NearbyStopsRequester(Context appContext, Criteria criteria) { this.appContext = appContext.getApplicationContext(); this.cr = criteria; } @Override public void run() { if(isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); return; } final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; //if we don't have the permission, we have to ask for it, if we haven't // asked too many times before if (noPermission) { if (!isOldVersion) { pendingNearbyStopsRequest = true; //Permissions.assertLocationPermissions(appContext,getActivity()); requestPermissionLauncher.launch(LOCATION_PERMISSIONS); Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission); return; } else { Toast.makeText(appContext, "Asked for permission position too many times", Toast.LENGTH_LONG).show(); } } else setOption(LOCATION_PERMISSION_GIVEN, true); AppLocationManager appLocationManager = AppLocationManager.getInstance(appContext); final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); if (haveProviders && fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !fragMan.isDestroyed()) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); showNearbyStopsFragment(); pendingNearbyStopsRequest = false; } else if(!haveProviders){ Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); } } } */ } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java index fddea79..8173f7c 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java @@ -1,837 +1,837 @@ /* BusTO - Fragments components Copyright (C) 2020 Andrea Ugo Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; import android.Manifest; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.drawable.Drawable; import android.location.Location; import android.location.LocationManager; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.content.res.ResourcesCompat; import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate; import it.reyboz.bustorino.backend.mato.MQTTMatoClient; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.gtfs.MatoPattern; import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops; import it.reyboz.bustorino.map.*; import it.reyboz.bustorino.viewmodels.LivePositionsViewModel; import it.reyboz.bustorino.viewmodels.StopsMapViewModel; import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; import org.osmdroid.events.DelayedMapListener; import org.osmdroid.events.MapListener; import org.osmdroid.events.ScrollEvent; import org.osmdroid.events.ZoomEvent; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.FolderOverlay; import org.osmdroid.views.overlay.Marker; import org.osmdroid.views.overlay.infowindow.InfoWindow; import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider; import java.util.*; import kotlin.Pair; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.middleware.GeneralActivity; import it.reyboz.bustorino.util.Permissions; import static it.reyboz.bustorino.fragments.SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE; public class MapFragment extends ScreenBaseFragment { //private static final String TAG = "Busto-MapActivity"; private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom"; private static final String MAP_CENTER_LAT_KEY = "map-center-lat"; private static final String MAP_CENTER_LON_KEY = "map-center-lon"; private static final String FOLLOWING_LOCAT_KEY ="following"; public static final String BUNDLE_LATIT = "lat"; public static final String BUNDLE_LONGIT = "lon"; public static final String BUNDLE_NAME = "name"; public static final String BUNDLE_ID = "ID"; public static final String BUNDLE_ROUTES_STOPPING = "routesStopping"; public static final String FRAGMENT_TAG="BusTOMapFragment"; private static final double DEFAULT_CENTER_LAT = 45.0708; private static final double DEFAULT_CENTER_LON = 7.6858; private static final double POSITION_FOUND_ZOOM = 18.3; public static final double NO_POSITION_ZOOM = 17.1; private static final String DEBUG_TAG=FRAGMENT_TAG; protected FragmentListenerMain listenerMain; private HashSet shownStops = null; private MapView map = null; public Context ctx; private LocationOverlay mLocationOverlay = null; private FolderOverlay stopsFolderOverlay = null; private Bundle savedMapState = null; protected ImageButton btCenterMap; protected ImageButton btFollowMe; protected CoordinatorLayout coordLayout; private boolean hasMapStartFinished = false; private boolean followingLocation = false; //the ViewModel from which we get the stop to display in the map private StopsMapViewModel stopsViewModel; //private GtfsPositionsViewModel gtfsPosViewModel; //= new ViewModelProvider(this).get(MapViewModel.class); private LivePositionsViewModel livePositionsViewModel; private Boolean useMQTTViewModel = true; private final HashMap busPositionMarkersByTrip = new HashMap<>(); private FolderOverlay busPositionsOverlay = null; private final HashMap tripMarkersAnimators = new HashMap<>(); protected final CustomInfoWindow.TouchResponder responder = new CustomInfoWindow.TouchResponder() { @Override public void onActionUp(@NonNull String stopID, @Nullable String stopName) { if (listenerMain!= null){ Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: "+stopID); listenerMain.requestArrivalsForStopID(stopID); } } }; protected final LocationOverlay.OverlayCallbacks locationCallbacks = new LocationOverlay.OverlayCallbacks() { @Override public void onDisableFollowMyLocation() { updateGUIForLocationFollowing(false); followingLocation=false; } @Override public void onEnableFollowMyLocation() { updateGUIForLocationFollowing(true); followingLocation=true; } }; private final ActivityResultLauncher positionRequestLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> { if (result == null){ Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?"); } else if(Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION)) && Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))){ map.getOverlays().remove(mLocationOverlay); startLocationOverlay(true, map); if(getContext()==null || getContext().getSystemService(Context.LOCATION_SERVICE)==null) return; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { map.getController().setZoom(POSITION_FOUND_ZOOM); GeoPoint startPoint = new GeoPoint(userLocation); setLocationFollowing(true); map.getController().setCenter(startPoint); } } else Log.w(DEBUG_TAG,"No location permission"); }); public MapFragment() { } public static MapFragment getInstance(){ return new MapFragment(); } public static MapFragment getInstance(@NonNull Stop stop){ MapFragment fragment= new MapFragment(); Bundle args = new Bundle(); args.putDouble(BUNDLE_LATIT, stop.getLatitude()); args.putDouble(BUNDLE_LONGIT, stop.getLongitude()); args.putString(BUNDLE_NAME, stop.getStopDisplayName()); args.putString(BUNDLE_ID, stop.ID); args.putString(BUNDLE_ROUTES_STOPPING, stop.routesThatStopHereToString()); fragment.setArguments(args); return fragment; } //public static MapFragment getInstance(@NonNull Stop stop){ // return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID); //} @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { //use the same layout as the activity View root = inflater.inflate(R.layout.fragment_map, container, false); if (getContext() == null){ throw new IllegalStateException(); } ctx = getContext().getApplicationContext(); Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)); map = root.findViewById(R.id.map); map.setTileSource(TileSourceFactory.MAPNIK); //map.setTilesScaledToDpi(true); map.setFlingEnabled(true); // add ability to zoom with 2 fingers map.setMultiTouchControls(true); - btCenterMap = root.findViewById(R.id.icon_center_map); - btFollowMe = root.findViewById(R.id.icon_follow); + btCenterMap = root.findViewById(R.id.centerMapImageButton); + btFollowMe = root.findViewById(R.id.followUserImageButton); coordLayout = root.findViewById(R.id.coord_layout); //setup FolderOverlay stopsFolderOverlay = new FolderOverlay(); //setup Bus Markers Overlay busPositionsOverlay = new FolderOverlay(); //reset shown bus updates busPositionMarkersByTrip.clear(); tripMarkersAnimators.clear(); //set map not done hasMapStartFinished = false; String keySourcePositions=getString(R.string.pref_positions_source); useMQTTViewModel = ( PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions,LIVE_POSITIONS_PREF_MQTT_VALUE).contentEquals(LIVE_POSITIONS_PREF_MQTT_VALUE)); //Start map from bundle if (savedInstanceState !=null) startMap(getArguments(), savedInstanceState); else startMap(getArguments(), savedMapState); //set listeners map.addMapListener(new DelayedMapListener(new MapListener() { @Override public boolean onScroll(ScrollEvent paramScrollEvent) { requestStopsToShow(); //Log.d(DEBUG_TAG, "Scrolling"); //if (moveTriggeredByCode) moveTriggeredByCode =false; //else setLocationFollowing(false); return true; } @Override public boolean onZoom(ZoomEvent event) { requestStopsToShow(); return true; } })); btCenterMap.setOnClickListener(v -> { //Log.i(TAG, "centerMap clicked "); if(Permissions.bothLocationPermissionsGranted(getContext())) { final GeoPoint myPosition = mLocationOverlay.getMyLocation(); map.getController().animateTo(myPosition); } else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); btFollowMe.setOnClickListener(v -> { //Log.i(TAG, "btFollowMe clicked "); if(Permissions.bothLocationPermissionsGranted(getContext())) setLocationFollowing(!followingLocation); else Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT) .show(); }); return root; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); ViewModelProvider provider = new ViewModelProvider(this); //gtfsPosViewModel = provider.get(GtfsPositionsViewModel.class); livePositionsViewModel = provider.get(LivePositionsViewModel.class); stopsViewModel = provider.get(StopsMapViewModel.class); if (context instanceof FragmentListenerMain) { listenerMain = (FragmentListenerMain) context; } else { throw new RuntimeException(context.toString() + " must implement FragmentListenerMain"); } } @Override public void onDetach() { super.onDetach(); listenerMain = null; //stop animations // setupOnAttached = true; Log.w(DEBUG_TAG, "Fragment detached"); } @Override public void onPause() { super.onPause(); Log.w(DEBUG_TAG, "On pause called mapfrag"); saveMapState(); for (ObjectAnimator animator : tripMarkersAnimators.values()) { if(animator!=null && animator.isRunning()){ animator.cancel(); } } tripMarkersAnimators.clear(); if(useMQTTViewModel) livePositionsViewModel.stopMatoUpdates(); } /** * Save the map state inside the fragment * (calls saveMapState(bundle)) */ private void saveMapState(){ savedMapState = new Bundle(); saveMapState(savedMapState); } /** * Save the state of the map to restore it to a later time * @param bundle the bundle in which to save the data */ private void saveMapState(Bundle bundle){ Log.d(DEBUG_TAG, "Saving state, location following: "+followingLocation); bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation); if (map == null){ //The map is null, it can happen? Log.e(DEBUG_TAG, "Cannot save map center, map is null"); return; } final IGeoPoint loc = map.getMapCenter(); bundle.putDouble(MAP_CENTER_LAT_KEY, loc.getLatitude()); bundle.putDouble(MAP_CENTER_LON_KEY, loc.getLongitude()); bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble()); } @Override public void onResume() { super.onResume(); //TODO: cleanup duplicate code (maybe merging the positions classes?) if(listenerMain!=null) listenerMain.readyGUIfor(FragmentKind.MAP); /// choose which to use String keySourcePositions=getString(R.string.pref_positions_source); useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions,LIVE_POSITIONS_PREF_MQTT_VALUE).contentEquals( LIVE_POSITIONS_PREF_MQTT_VALUE); if(livePositionsViewModel !=null) { //gtfsPosViewModel.requestUpdates(); if(useMQTTViewModel) livePositionsViewModel.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL); else livePositionsViewModel.requestGTFSUpdates(); //mapViewModel.testCascade(); livePositionsViewModel.isLastWorkResultGood().observe(this, d -> Log.d(DEBUG_TAG, "Last trip download result is "+d)); livePositionsViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> { Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat); livePositionsViewModel.downloadTripsFromMato(dat); /*MatoTripsDownloadWorker.Companion.requestMatoTripsDownload(dat, requireContext().getApplicationContext(), "BusTO-MatoTripDownload"); */ }); } /*else if(gtfsPosViewModel!=null){ gtfsPosViewModel.requestUpdates(); gtfsPosViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> { Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat); //gtfsPosViewModel.downloadTripsFromMato(dat); MatoTripsDownloadWorker.Companion.downloadTripsFromMato(dat,getContext().getApplicationContext(), "BusTO-MatoTripDownload"); }); } */ else Log.e(DEBUG_TAG, "livePositionsViewModel is null at onResume"); //rerequest stop stopsViewModel.requestStopsInBoundingBox(map.getBoundingBox()); } private void startRequestsPositions(){ if (livePositionsViewModel != null) { //should always be the case livePositionsViewModel.getUpdatesWithTripAndPatterns().observe(getViewLifecycleOwner(), data -> { Log.d(DEBUG_TAG, "Have " + data.size() + " trip updates, has Map start finished: " + hasMapStartFinished); if (hasMapStartFinished) updateBusPositionsInMap(data); if(!isDetached() && !useMQTTViewModel) livePositionsViewModel.requestDelayedGTFSUpdates(3000); }); } else { Log.e(DEBUG_TAG, "PositionsViewModel is null"); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { saveMapState(outState); super.onSaveInstanceState(outState); } //own methods /** * Switch following the location on and off * @param value true if we want to follow location */ public void setLocationFollowing(Boolean value){ followingLocation = value; if(mLocationOverlay==null || getContext() == null || map ==null) //nothing else to do return; if (value){ mLocationOverlay.enableFollowLocation(); } else { mLocationOverlay.disableFollowLocation(); } } /** * Do all the stuff you need to do on the gui, when parameter is changed to value * @param following value */ protected void updateGUIForLocationFollowing(boolean following){ if (following) btFollowMe.setImageResource(R.drawable.ic_follow_me_on); else btFollowMe.setImageResource(R.drawable.ic_follow_me); } /** * Build the location overlay. Enable only when * a) we know we have the permission * b) the location map is set */ private void startLocationOverlay(boolean enableLocation, MapView map){ if(getActivity()== null) throw new IllegalStateException("Cannot enable LocationOverlay now"); // Location Overlay // from OpenBikeSharing (THANK GOD) Log.d(DEBUG_TAG, "Starting position overlay"); GpsMyLocationProvider imlp = new GpsMyLocationProvider(getActivity().getBaseContext()); imlp.setLocationUpdateMinDistance(5); imlp.setLocationUpdateMinTime(2000); final LocationOverlay overlay = new LocationOverlay(imlp,map, locationCallbacks); if (enableLocation) overlay.enableMyLocation(); overlay.setOptionsMenuEnabled(true); //map.getOverlays().add(this.mLocationOverlay); this.mLocationOverlay = overlay; map.getOverlays().add(mLocationOverlay); } public void startMap(Bundle incoming, Bundle savedInstanceState) { //Check that we're attached GeneralActivity activity = getActivity() instanceof GeneralActivity ? (GeneralActivity) getActivity() : null; if(getContext()==null|| activity==null){ //we are not attached Log.e(DEBUG_TAG, "Calling startMap when not attached"); return; }else{ Log.d(DEBUG_TAG, "Starting map from scratch"); } //clear previous overlays map.getOverlays().clear(); //parse incoming bundle GeoPoint marker = null; String name = null; String ID = null; String routesStopping = ""; if (incoming != null) { double lat = incoming.getDouble(BUNDLE_LATIT); double lon = incoming.getDouble(BUNDLE_LONGIT); marker = new GeoPoint(lat, lon); name = incoming.getString(BUNDLE_NAME); ID = incoming.getString(BUNDLE_ID); routesStopping = incoming.getString(BUNDLE_ROUTES_STOPPING, ""); } //ask for location permission if(!Permissions.bothLocationPermissionsGranted(activity)){ if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){ //TODO: show dialog for permission rationale Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show(); } positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS); } shownStops = new HashSet<>(); // move the map on the marker position or on a default view point: Turin, Piazza Castello // and set the start zoom IMapController mapController = map.getController(); GeoPoint startPoint = null; startLocationOverlay(Permissions.bothLocationPermissionsGranted(activity), map); // set the center point if (marker != null) { //startPoint = marker; mapController.setZoom(POSITION_FOUND_ZOOM); setLocationFollowing(false); // put the center a little bit off (animate later) startPoint = new GeoPoint(marker); startPoint.setLatitude(marker.getLatitude()+ utils.angleRawDifferenceFromMeters(20)); startPoint.setLongitude(marker.getLongitude()-utils.angleRawDifferenceFromMeters(20)); //don't need to do all the rest since we want to show a point } else if (savedInstanceState != null && savedInstanceState.containsKey(MAP_CURRENT_ZOOM_KEY)) { mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY)); mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY), savedInstanceState.getDouble(MAP_CENTER_LON_KEY))); Log.d(DEBUG_TAG, "Location following from savedInstanceState: "+savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); setLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)); } else { Log.d(DEBUG_TAG, "No position found from intent or saved state"); boolean found = false; LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); //check for permission if (locationManager != null && Permissions.bothLocationPermissionsGranted(activity)) { @SuppressLint("MissingPermission") Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (userLocation != null) { double distan = utils.measuredistanceBetween(userLocation.getLatitude(), userLocation.getLongitude(), DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); if (distan < 100_000.0) { mapController.setZoom(POSITION_FOUND_ZOOM); startPoint = new GeoPoint(userLocation); found = true; setLocationFollowing(true); } } } if(!found){ startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON); mapController.setZoom(NO_POSITION_ZOOM); setLocationFollowing(false); } } // set the minimum zoom level map.setMinZoomLevel(15.0); //add contingency check (shouldn't happen..., but) if (startPoint != null) { mapController.setCenter(startPoint); } //add stops overlay //map.getOverlays().add(mLocationOverlay); map.getOverlays().add(this.stopsFolderOverlay); Log.d(DEBUG_TAG, "Requesting stops load"); // This is not necessary, by setting the center we already move // the map and we trigger a stop request //requestStopsToShow(); if (marker != null) { // make a marker with the info window open for the searched marker //TODO: make Stop Bundle-able Marker stopMarker = makeMarker(marker, ID , name, routesStopping,true); map.getController().animateTo(marker); } //add the overlays with the bus stops if(busPositionsOverlay == null){ //Log.i(DEBUG_TAG, "Null bus positions overlay,redo"); busPositionsOverlay = new FolderOverlay(); } startRequestsPositions(); if(stopsViewModel !=null){ stopsViewModel.getStopsInBoundingBox().observe(getViewLifecycleOwner(), this::showStopsMarkers ); } else Log.d(DEBUG_TAG, "Cannot observe new stops in map, stopsViewModel is null"); map.getOverlays().add(this.busPositionsOverlay); //set map as started hasMapStartFinished = true; } /** * Start a request to load the stops that are in the current view * from the database */ private void requestStopsToShow(){ // get the top, bottom, left and right screen's coordinate BoundingBox bb = map.getBoundingBox(); Log.d(DEBUG_TAG, "Requesting stops in bounding box, stopViewModel is null "+(stopsViewModel==null)); if(stopsViewModel!=null){ stopsViewModel.requestStopsInBoundingBox(bb); } /*double latFrom = bb.getLatSouth(); double latTo = bb.getLatNorth(); double lngFrom = bb.getLonWest(); double lngTo = bb.getLonEast(); if (stopFetcher!= null && stopFetcher.getStatus()!= AsyncTask.Status.FINISHED) stopFetcher.cancel(true); stopFetcher = new AsyncStopFetcher(this); stopFetcher.execute( new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo)); */ } private void updateBusMarker(final Marker marker, final LivePositionUpdate posUpdate, @Nullable boolean justCreated){ GeoPoint position; final String updateID = posUpdate.getTripID(); if(!justCreated){ position = marker.getPosition(); if(posUpdate.getLatitude()!=position.getLatitude() || posUpdate.getLongitude()!=position.getLongitude()){ GeoPoint newpos = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()); ObjectAnimator valueAnimator = MarkerUtils.makeMarkerAnimator( map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200); valueAnimator.setAutoCancel(true); tripMarkersAnimators.put(updateID,valueAnimator); valueAnimator.start(); } //marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude())); } else { position = new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude()); marker.setPosition(position); } if(posUpdate.getBearing()!=null) marker.setRotation(posUpdate.getBearing()*(-1.f)); } private void updateBusPositionsInMap(HashMap> tripsPatterns){ Log.d(DEBUG_TAG, "Updating positions of the buses"); //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); final ArrayList noPatternsTrips = new ArrayList<>(); for(String tripID: tripsPatterns.keySet()) { final Pair pair = tripsPatterns.get(tripID); if (pair == null) continue; final LivePositionUpdate update = pair.getFirst(); final TripAndPatternWithStops tripWithPatternStops = pair.getSecond(); //check if Marker is already created if (busPositionMarkersByTrip.containsKey(tripID)){ //need to change the position of the marker final Marker marker = busPositionMarkersByTrip.get(tripID); assert marker!=null; updateBusMarker(marker, update, false); if(marker.getInfoWindow()!=null && marker.getInfoWindow() instanceof BusInfoWindow){ BusInfoWindow window = (BusInfoWindow) marker.getInfoWindow(); if(tripWithPatternStops != null) { //Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID); window.setPatternAndDraw(tripWithPatternStops.getPattern()); } } } else{ //marker is not there, need to make it if(map==null) Log.e(DEBUG_TAG, "Creating marker with null map, things will explode"); final Marker marker = new Marker(map); /*final Drawable mDrawable = DrawableUtils.Companion.getScaledDrawableResources( getResources(), R.drawable.point_heading_icon, R.dimen.map_icons_size, R.dimen.map_icons_size); */ //String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); final Drawable mdraw = ResourcesCompat.getDrawable(getResources(),R.drawable.map_bus_position_icon, null); /*final Drawable mdraw = DrawableUtils.Companion.writeOnDrawable(getResources(), R.drawable.point_heading_icon, R.color.white, route,12); */ assert mdraw != null; //mdraw.setBounds(0,0,28,28); marker.setIcon(mdraw); if(tripWithPatternStops == null){ noPatternsTrips.add(tripID); } MatoPattern markerPattern = null; if(tripWithPatternStops != null && tripWithPatternStops.getPattern()!=null) markerPattern = tripWithPatternStops.getPattern(); marker.setInfoWindow(new BusInfoWindow(map, update, markerPattern , false, (pattern) -> { })); marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER); marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER); updateBusMarker(marker, update, true); // the overlay is null when it's not attached yet?5 // cannot recreate it because it becomes null very soon // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); //save the marker if(busPositionsOverlay!=null) { busPositionsOverlay.add(marker); busPositionMarkersByTrip.put(tripID, marker); } } } if(noPatternsTrips.size()>0){ Log.i(DEBUG_TAG, "These trips have no matching pattern: "+noPatternsTrips); } } /** * Add stops as Markers on the map * @param stops the list of stops that must be included */ protected void showStopsMarkers(List stops){ if (getContext() == null || stops == null){ //we are not attached return; } boolean good = true; for (Stop stop : stops) { if (shownStops.contains(stop.ID)){ continue; } if(stop.getLongitude()==null || stop.getLatitude()==null) continue; shownStops.add(stop.ID); if(!map.isShown()){ if(good) Log.d(DEBUG_TAG, "Need to show stop but map is not shown, probably detached already"); good = false; continue; } else if(map.getRepository() == null){ Log.e(DEBUG_TAG, "Map view repository is null"); } GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude()); Marker stopMarker = makeMarker(marker, stop, false); stopsFolderOverlay.add(stopMarker); if (!map.getOverlays().contains(stopsFolderOverlay)) { Log.w(DEBUG_TAG, "Map doesn't have folder overlay"); } good=true; } //Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay"); //force redraw of markers map.invalidate(); } public Marker makeMarker(GeoPoint geoPoint, Stop stop, boolean isStartMarker){ return makeMarker(geoPoint,stop.ID, stop.getStopDefaultName(), stop.routesThatStopHereToString(), isStartMarker); } public Marker makeMarker(GeoPoint geoPoint, String stopID, String stopName, String routesStopping, boolean isStartMarker) { // add a marker final Marker marker = new Marker(map); // set custom info window as info window CustomInfoWindow popup = new CustomInfoWindow(map, stopID, stopName, routesStopping, responder, R.layout.linedetail_stop_infowindow, R.color.red_darker); marker.setInfoWindow(popup); // make the marker clickable marker.setOnMarkerClickListener((thisMarker, mapView) -> { if (thisMarker.isInfoWindowOpen()) { // on second click Log.w(DEBUG_TAG, "Pressed on the click marker"); } else { // on first click // hide all opened info window InfoWindow.closeAllInfoWindowsOn(map); // show this particular info window thisMarker.showInfoWindow(); // move the map to its position map.getController().animateTo(thisMarker.getPosition()); } return true; }); // set its position marker.setPosition(geoPoint); marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER); // add to it an icon //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_stop, ctx.getTheme())); // add to it a title marker.setTitle(stopName); // set the description as the ID marker.setSnippet(stopID); // show popup info window of the searched marker if (isStartMarker) { marker.showInfoWindow(); //map.getController().animateTo(marker.getPosition()); } return marker; } @Nullable @Override public View getBaseViewForSnackBar() { return coordLayout; } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt new file mode 100644 index 0000000..4cad925 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragmentKt.kt @@ -0,0 +1,750 @@ +/* + BusTO - Fragments components + Copyright (C) 2020 Andrea Ugo + Copyright (C) 2021 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package it.reyboz.bustorino.fragments + +import android.Manifest +import android.animation.ObjectAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.location.LocationManager +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.Toast +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.contract.ActivityResultContracts +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.viewModels +import androidx.preference.PreferenceManager +import it.reyboz.bustorino.R +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate +import it.reyboz.bustorino.backend.mato.MQTTMatoClient +import it.reyboz.bustorino.backend.utils +import it.reyboz.bustorino.data.gtfs.MatoPattern +import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops +import it.reyboz.bustorino.map.BusInfoWindow +import it.reyboz.bustorino.map.CustomInfoWindow +import it.reyboz.bustorino.map.CustomInfoWindow.TouchResponder +import it.reyboz.bustorino.map.LocationOverlay +import it.reyboz.bustorino.map.LocationOverlay.OverlayCallbacks +import it.reyboz.bustorino.map.MarkerUtils +import it.reyboz.bustorino.middleware.GeneralActivity +import it.reyboz.bustorino.util.Permissions +import it.reyboz.bustorino.viewmodels.LivePositionsViewModel +import it.reyboz.bustorino.viewmodels.StopsMapViewModel +import org.osmdroid.config.Configuration +import org.osmdroid.events.DelayedMapListener +import org.osmdroid.events.MapListener +import org.osmdroid.events.ScrollEvent +import org.osmdroid.events.ZoomEvent +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.MapView +import org.osmdroid.views.overlay.FolderOverlay +import org.osmdroid.views.overlay.Marker +import org.osmdroid.views.overlay.infowindow.InfoWindow +import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider + +open class MapFragmentKt : ScreenBaseFragment() { + protected var listenerMain: FragmentListenerMain? = null + private var shownStops: HashSet? = null + private lateinit var map: MapView + var ctx: Context? = null + private lateinit var mLocationOverlay: LocationOverlay + private lateinit var stopsFolderOverlay: FolderOverlay + private var savedMapState: Bundle? = null + protected lateinit var btCenterMap: ImageButton + protected lateinit var btFollowMe: ImageButton + protected var coordLayout: CoordinatorLayout? = null + private var hasMapStartFinished = false + private var followingLocation = false + + //the ViewModel from which we get the stop to display in the map + private val stopsViewModel: StopsMapViewModel by viewModels() + + //private GtfsPositionsViewModel gtfsPosViewModel; //= new ViewModelProvider(this).get(MapViewModel.class); + private val livePositionsViewModel: LivePositionsViewModel by viewModels() + private var useMQTTViewModel = true + private val busPositionMarkersByTrip = HashMap() + private var busPositionsOverlay: FolderOverlay? = null + private val tripMarkersAnimators = HashMap() + protected val responder = TouchResponder { stopID, stopName -> + if (listenerMain != null) { + Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: $stopID") + listenerMain!!.requestArrivalsForStopID(stopID) + } + } + protected val locationCallbacks: OverlayCallbacks = object : OverlayCallbacks { + override fun onDisableFollowMyLocation() { + updateGUIForLocationFollowing(false) + followingLocation = false + } + + override fun onEnableFollowMyLocation() { + updateGUIForLocationFollowing(true) + followingLocation = true + } + } + private val positionRequestLauncher = + registerForActivityResult, Map>( + ActivityResultContracts.RequestMultiplePermissions(), + ActivityResultCallback { result -> + if (result == null) { + Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?") + } else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION] + && java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) { + // We can use the position, restart location overlay + map.overlays.remove(mLocationOverlay) + startLocationOverlay(true, map) + if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null) + return@ActivityResultCallback ///@registerForActivityResult + val locationManager = + requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager + @SuppressLint("MissingPermission") val userLocation = + locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + if (userLocation != null) { + map!!.controller.setZoom(POSITION_FOUND_ZOOM) + val startPoint = GeoPoint(userLocation) + setLocationFollowing(true) + map!!.controller.setCenter(startPoint) + } + } else Log.w(DEBUG_TAG, "No location permission") + }) + + //public static MapFragment getInstance(@NonNull Stop stop){ + // return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID); + //} + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + //use the same layout as the activity + val root = inflater.inflate(R.layout.fragment_map, container, false) + val context = requireContext() + ctx = context.applicationContext + Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(context)) + map = root.findViewById(R.id.map) + map.setTileSource(TileSourceFactory.MAPNIK) + //map.setTilesScaledToDpi(true); + map.setFlingEnabled(true) + + // add ability to zoom with 2 fingers + map.setMultiTouchControls(true) + btCenterMap = root.findViewById(R.id.centerMapImageButton) + btFollowMe = root.findViewById(R.id.followUserImageButton) + coordLayout = root.findViewById(R.id.coord_layout) + + //setup FolderOverlay + stopsFolderOverlay = FolderOverlay() + //setup Bus Markers Overlay + busPositionsOverlay = FolderOverlay() + //reset shown bus updates + busPositionMarkersByTrip.clear() + tripMarkersAnimators.clear() + //set map not done + hasMapStartFinished = false + val keySourcePositions = getString(R.string.pref_positions_source) + useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getString(keySourcePositions, SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE) + .contentEquals(SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE) + + + //Start map from bundle + if (savedInstanceState != null) startMap(arguments, savedInstanceState) else startMap( + arguments, savedMapState + ) + //set listeners + map.addMapListener(DelayedMapListener(object : MapListener { + override fun onScroll(paramScrollEvent: ScrollEvent): Boolean { + requestStopsToShow() + //Log.d(DEBUG_TAG, "Scrolling"); + //if (moveTriggeredByCode) moveTriggeredByCode =false; + //else setLocationFollowing(false); + return true + } + + override fun onZoom(event: ZoomEvent): Boolean { + requestStopsToShow() + return true + } + })) + btCenterMap.setOnClickListener(View.OnClickListener { v: View? -> + //Log.i(TAG, "centerMap clicked "); + if (Permissions.bothLocationPermissionsGranted(context)) { + val myPosition = mLocationOverlay!!.myLocation + map.getController().animateTo(myPosition) + } else Toast.makeText(context, R.string.enable_position_message_map, Toast.LENGTH_SHORT) + .show() + }) + btFollowMe.setOnClickListener(View.OnClickListener { v: View? -> + //Log.i(TAG, "btFollowMe clicked "); + if (Permissions.bothLocationPermissionsGranted(context)) setLocationFollowing(!followingLocation) else Toast.makeText( + context, R.string.enable_position_message_map, Toast.LENGTH_SHORT + ) + .show() + }) + return root + } + + override fun onAttach(context: Context) { + super.onAttach(context) + listenerMain = if (context is FragmentListenerMain) { + context + } else { + throw RuntimeException( + context.toString() + + " must implement FragmentListenerMain" + ) + } + } + + override fun onDetach() { + super.onDetach() + listenerMain = null + + Log.w(DEBUG_TAG, "Fragment detached") + } + + override fun onPause() { + super.onPause() + Log.w(DEBUG_TAG, "On pause called mapfrag") + saveMapState() + for (animator in tripMarkersAnimators.values) { + if (animator != null && animator.isRunning) { + animator.cancel() + } + } + tripMarkersAnimators.clear() + if (useMQTTViewModel) livePositionsViewModel.stopMatoUpdates() + } + + /** + * Save the map state inside the fragment + * (calls saveMapState(bundle)) + */ + private fun saveMapState() { + savedMapState = Bundle() + saveMapState(savedMapState!!) + } + + /** + * Save the state of the map to restore it to a later time + * @param bundle the bundle in which to save the data + */ + private fun saveMapState(bundle: Bundle) { + Log.d(DEBUG_TAG, "Saving state, location following: $followingLocation") + bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation) + if (map == null) { + //The map is null, it can happen? + Log.e(DEBUG_TAG, "Cannot save map center, map is null") + return + } + val loc = map!!.mapCenter + bundle.putDouble(MAP_CENTER_LAT_KEY, loc.latitude) + bundle.putDouble(MAP_CENTER_LON_KEY, loc.longitude) + bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map!!.zoomLevelDouble) + } + + override fun onResume() { + super.onResume() + //TODO: cleanup duplicate code (maybe merging the positions classes?) + if (listenerMain != null) listenerMain!!.readyGUIfor(FragmentKind.MAP) + /// choose which to use + val keySourcePositions = getString(R.string.pref_positions_source) + useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getString(keySourcePositions, SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE) + .contentEquals( + SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE + ) + //gtfsPosViewModel.requestUpdates(); + if (useMQTTViewModel) livePositionsViewModel.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL) + else livePositionsViewModel.requestGTFSUpdates() + //mapViewModel.testCascade(); + livePositionsViewModel.isLastWorkResultGood.observe(this) { d: Boolean -> + Log.d( + DEBUG_TAG, "Last trip download result is $d" + ) + } + livePositionsViewModel.tripsGtfsIDsToQuery.observe(this) { dat: List -> + Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: $dat") + livePositionsViewModel.downloadTripsFromMato(dat) + } + + //rerequest stop + stopsViewModel!!.requestStopsInBoundingBox(map!!.boundingBox) + } + + private fun startRequestsPositions() { + if (livePositionsViewModel != null) { + //should always be the case + livePositionsViewModel!!.updatesWithTripAndPatterns.observe(viewLifecycleOwner) { data: HashMap> -> + Log.d( + DEBUG_TAG, + "Have " + data.size + " trip updates, has Map start finished: " + hasMapStartFinished + ) + if (hasMapStartFinished) updateBusPositionsInMap(data) + if (!isDetached && !useMQTTViewModel) livePositionsViewModel!!.requestDelayedGTFSUpdates( + 3000 + ) + } + } else { + Log.e(DEBUG_TAG, "PositionsViewModel is null") + } + } + + override fun onSaveInstanceState(outState: Bundle) { + saveMapState(outState) + super.onSaveInstanceState(outState) + } + //own methods + /** + * Switch following the location on and off + * @param value true if we want to follow location + */ + fun setLocationFollowing(value: Boolean) { + followingLocation = value + if (mLocationOverlay == null || context == null || map == null) //nothing else to do + return + if (value) { + mLocationOverlay!!.enableFollowLocation() + } else { + mLocationOverlay!!.disableFollowLocation() + } + } + + /** + * Do all the stuff you need to do on the gui, when parameter is changed to value + * @param following value + */ + protected fun updateGUIForLocationFollowing(following: Boolean) { + if (following) btFollowMe!!.setImageResource(R.drawable.ic_follow_me_on) else btFollowMe!!.setImageResource( + R.drawable.ic_follow_me + ) + } + + /** + * Build the location overlay. Enable only when + * a) we know we have the permission + * b) the location map is set + */ + private fun startLocationOverlay(enableLocation: Boolean, map: MapView?) { + checkNotNull(activity) { "Cannot enable LocationOverlay now" } + // Location Overlay + // from OpenBikeSharing (THANK GOD) + Log.d(DEBUG_TAG, "Starting position overlay") + val imlp = GpsMyLocationProvider(requireActivity().baseContext) + imlp.locationUpdateMinDistance = 5f + imlp.locationUpdateMinTime = 2000 + val overlay = LocationOverlay(imlp, map, locationCallbacks) + if (enableLocation) overlay.enableMyLocation() + overlay.isOptionsMenuEnabled = true + + //map.getOverlays().add(this.mLocationOverlay); + mLocationOverlay = overlay + map!!.overlays.add(mLocationOverlay) + } + + fun startMap(incoming: Bundle?, savedInstanceState: Bundle?) { + //Check that we're attached + val activity = if (activity is GeneralActivity) activity as GeneralActivity? else null + if (context == null || activity == null) { + //we are not attached + Log.e(DEBUG_TAG, "Calling startMap when not attached") + return + } else { + Log.d(DEBUG_TAG, "Starting map from scratch") + } + //clear previous overlays + map!!.overlays.clear() + + + //parse incoming bundle + var marker: GeoPoint? = null + var name: String? = null + var ID: String? = null + var routesStopping: String? = "" + if (incoming != null) { + val lat = incoming.getDouble(BUNDLE_LATIT) + val lon = incoming.getDouble(BUNDLE_LONGIT) + marker = GeoPoint(lat, lon) + name = incoming.getString(BUNDLE_NAME) + ID = incoming.getString(BUNDLE_ID) + routesStopping = incoming.getString(BUNDLE_ROUTES_STOPPING, "") + } + + + //ask for location permission + if (!Permissions.bothLocationPermissionsGranted(activity)) { + if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { + //TODO: show dialog for permission rationale + Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT) + .show() + } + positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS) + } + shownStops = HashSet() + // move the map on the marker position or on a default view point: Turin, Piazza Castello + // and set the start zoom + val mapController = map!!.controller + var startPoint: GeoPoint? = null + startLocationOverlay( + Permissions.bothLocationPermissionsGranted(activity), + map + ) + // set the center point + if (marker != null) { + //startPoint = marker; + mapController.setZoom(POSITION_FOUND_ZOOM) + setLocationFollowing(false) + // put the center a little bit off (animate later) + startPoint = GeoPoint(marker) + startPoint.latitude = marker.latitude + utils.angleRawDifferenceFromMeters(20.0) + startPoint.longitude = marker.longitude - utils.angleRawDifferenceFromMeters(20.0) + //don't need to do all the rest since we want to show a point + } else if (savedInstanceState != null && savedInstanceState.containsKey(MAP_CURRENT_ZOOM_KEY)) { + mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY)) + mapController.setCenter( + GeoPoint( + savedInstanceState.getDouble(MAP_CENTER_LAT_KEY), + savedInstanceState.getDouble(MAP_CENTER_LON_KEY) + ) + ) + Log.d( + DEBUG_TAG, + "Location following from savedInstanceState: " + savedInstanceState.getBoolean( + FOLLOWING_LOCAT_KEY + ) + ) + setLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY)) + } else { + Log.d(DEBUG_TAG, "No position found from intent or saved state") + var found = false + val locationManager = + requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager + //check for permission + if (Permissions.bothLocationPermissionsGranted(activity)) { + @SuppressLint("MissingPermission") val userLocation = + locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + if (userLocation != null) { + val distan = utils.measuredistanceBetween( + userLocation.latitude, userLocation.longitude, + DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON + ) + if (distan < 100000.0) { + mapController.setZoom(POSITION_FOUND_ZOOM) + startPoint = GeoPoint(userLocation) + found = true + setLocationFollowing(true) + } + } + } + if (!found) { + startPoint = GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) + mapController.setZoom(NO_POSITION_ZOOM) + setLocationFollowing(false) + } + } + + // set the minimum zoom level + map!!.minZoomLevel = 15.0 + //add contingency check (shouldn't happen..., but) + if (startPoint != null) { + mapController.setCenter(startPoint) + } + + + //add stops overlay + //map.getOverlays().add(mLocationOverlay); + map!!.overlays.add(stopsFolderOverlay) + Log.d(DEBUG_TAG, "Requesting stops load") + // This is not necessary, by setting the center we already move + // the map and we trigger a stop request + //requestStopsToShow(); + if (marker != null) { + // make a marker with the info window open for the searched marker + //TODO: make Stop Bundle-able + val stopMarker = makeMarker(marker, ID, name, routesStopping, true) + map!!.controller.animateTo(marker) + } + //add the overlays with the bus stops + if (busPositionsOverlay == null) { + //Log.i(DEBUG_TAG, "Null bus positions overlay,redo"); + busPositionsOverlay = FolderOverlay() + } + startRequestsPositions() + if (stopsViewModel != null) { + stopsViewModel!!.stopsInBoundingBox.observe(viewLifecycleOwner) { stops: List? -> + showStopsMarkers( + stops + ) + } + } else Log.d(DEBUG_TAG, "Cannot observe new stops in map, stopsViewModel is null") + map!!.overlays.add(busPositionsOverlay) + //set map as started + hasMapStartFinished = true + } + + /** + * Start a request to load the stops that are in the current view + * from the database + */ + private fun requestStopsToShow() { + // get the top, bottom, left and right screen's coordinate + val bb = map!!.boundingBox + Log.d( + DEBUG_TAG, + "Requesting stops in bounding box, stopViewModel is null " + (false) + ) + stopsViewModel.requestStopsInBoundingBox(bb) + + } + + private fun updateBusMarker( + marker: Marker?, + posUpdate: LivePositionUpdate, + justCreated: Boolean + ) { + val position: GeoPoint + val updateID = posUpdate.tripID + if (!justCreated) { + position = marker!!.position + if (posUpdate.latitude != position.latitude || posUpdate.longitude != position.longitude) { + val newpos = GeoPoint(posUpdate.latitude, posUpdate.longitude) + val valueAnimator = MarkerUtils.makeMarkerAnimator( + map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200 + ) + valueAnimator.setAutoCancel(true) + tripMarkersAnimators[updateID] = valueAnimator + valueAnimator.start() + } + //marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude())); + } else { + position = GeoPoint(posUpdate.latitude, posUpdate.longitude) + marker!!.position = position + } + marker.rotation = posUpdate.bearing?.let { it*-1f } ?: 0.0f + } + + private fun updateBusPositionsInMap(tripsPatterns: HashMap>) { + Log.d(DEBUG_TAG, "Updating positions of the buses") + //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); + val noPatternsTrips = ArrayList() + for (tripID in tripsPatterns.keys) { + val (update, tripWithPatternStops) = tripsPatterns[tripID] ?: continue + + + //check if Marker is already created + if (busPositionMarkersByTrip.containsKey(tripID)) { + //need to change the position of the marker + val marker = busPositionMarkersByTrip[tripID]!! + updateBusMarker(marker, update, false) + if (marker.infoWindow != null && marker.infoWindow is BusInfoWindow) { + val window = marker.infoWindow as BusInfoWindow + if (tripWithPatternStops != null) { + //Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID); + window.setPatternAndDraw(tripWithPatternStops.pattern) + } + } + } else { + //marker is not there, need to make it + val marker = Marker(map) + + /*final Drawable mDrawable = DrawableUtils.Companion.getScaledDrawableResources( + getResources(), + R.drawable.point_heading_icon, + R.dimen.map_icons_size, R.dimen.map_icons_size); + + */ + //String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); + val mdraw = + ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon, null)!! + //mdraw.setBounds(0,0,28,28); + marker.icon = mdraw + if (tripWithPatternStops == null) { + noPatternsTrips.add(tripID) + } + var markerPattern: MatoPattern? = null + if (tripWithPatternStops != null && tripWithPatternStops.pattern != null) markerPattern = + tripWithPatternStops.pattern + marker.infoWindow = + BusInfoWindow(map!!, update, markerPattern, false) { pattern: MatoPattern? -> } + marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) + updateBusMarker(marker, update, true) + // the overlay is null when it's not attached yet?5 + // cannot recreate it because it becomes null very soon + // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); + //save the marker + if (busPositionsOverlay != null) { + busPositionsOverlay!!.add(marker) + busPositionMarkersByTrip[tripID] = marker + } + } + } + if (noPatternsTrips.size > 0) { + Log.i(DEBUG_TAG, "These trips have no matching pattern: $noPatternsTrips") + } + } + + /** + * Add stops as Markers on the map + * @param stops the list of stops that must be included + */ + protected fun showStopsMarkers(stops: List?) { + if (context == null || stops == null) { + //we are not attached + return + } + var good = true + for (stop in stops) { + if (shownStops!!.contains(stop.ID)) { + continue + } + if (stop.longitude == null || stop.latitude == null) continue + shownStops!!.add(stop.ID) + if (!map!!.isShown) { + if (good) Log.d( + DEBUG_TAG, + "Need to show stop but map is not shown, probably detached already" + ) + good = false + continue + } else if (map!!.repository == null) { + Log.e(DEBUG_TAG, "Map view repository is null") + } + val marker = GeoPoint(stop.latitude!!, stop.longitude!!) + val stopMarker = makeMarker(marker, stop, false) + stopsFolderOverlay!!.add(stopMarker) + if (!map!!.overlays.contains(stopsFolderOverlay)) { + Log.w(DEBUG_TAG, "Map doesn't have folder overlay") + } + good = true + } + //Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay"); + //force redraw of markers + map!!.invalidate() + } + + fun makeMarker(geoPoint: GeoPoint?, stop: Stop, isStartMarker: Boolean): Marker { + return makeMarker( + geoPoint, stop.ID, + stop.stopDefaultName, + stop.routesThatStopHereToString(), isStartMarker + ) + } + + fun makeMarker( + geoPoint: GeoPoint?, stopID: String?, stopName: String?, + routesStopping: String?, isStartMarker: Boolean + ): Marker { + + // add a marker + val marker = Marker(map) + + // set custom info window as info window + val popup = CustomInfoWindow( + map, stopID, stopName, routesStopping, + responder, R.layout.linedetail_stop_infowindow, R.color.red_darker + ) + marker.infoWindow = popup + + // make the marker clickable + marker.setOnMarkerClickListener { thisMarker: Marker, mapView: MapView? -> + if (thisMarker.isInfoWindowOpen) { + // on second click + Log.w(DEBUG_TAG, "Pressed on the click marker") + } else { + // on first click + + // hide all opened info window + InfoWindow.closeAllInfoWindowsOn(map) + // show this particular info window + thisMarker.showInfoWindow() + // move the map to its position + map!!.controller.animateTo(thisMarker.position) + } + true + } + + // set its position + marker.position = geoPoint + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) + // add to it an icon + //marker.setIcon(getResources().getDrawable(R.drawable.bus_marker)); + marker.icon = ResourcesCompat.getDrawable(resources, R.drawable.bus_stop, ctx!!.theme) + // add to it a title + marker.title = stopName + // set the description as the ID + marker.snippet = stopID + + // show popup info window of the searched marker + if (isStartMarker) { + marker.showInfoWindow() + //map.getController().animateTo(marker.getPosition()); + } + return marker + } + + override fun getBaseViewForSnackBar(): View? { + return coordLayout + } + + companion object { + //private static final String TAG = "Busto-MapActivity"; + private const val MAP_CURRENT_ZOOM_KEY = "map-current-zoom" + private const val MAP_CENTER_LAT_KEY = "map-center-lat" + private const val MAP_CENTER_LON_KEY = "map-center-lon" + private const val FOLLOWING_LOCAT_KEY = "following" + const val BUNDLE_LATIT = "lat" + const val BUNDLE_LONGIT = "lon" + const val BUNDLE_NAME = "name" + const val BUNDLE_ID = "ID" + const val BUNDLE_ROUTES_STOPPING = "routesStopping" + const val FRAGMENT_TAG = "BusTOMapFragment" + private const val DEFAULT_CENTER_LAT = 45.0708 + private const val DEFAULT_CENTER_LON = 7.6858 + private const val POSITION_FOUND_ZOOM = 18.3 + const val NO_POSITION_ZOOM = 17.1 + private const val DEBUG_TAG = FRAGMENT_TAG + + @JvmStatic + fun getInstance(): MapFragmentKt { + return MapFragmentKt() + } + @JvmStatic + fun getInstance(stop: Stop): MapFragmentKt { + val fragment = MapFragmentKt() + val args = Bundle() + args.putDouble(MapFragment.BUNDLE_LATIT, stop.latitude!!) + args.putDouble(MapFragment.BUNDLE_LONGIT, stop.longitude!!) + args.putString(MapFragment.BUNDLE_NAME, stop.stopDisplayName) + args.putString(MapFragment.BUNDLE_ID, stop.ID) + args.putString(MapFragment.BUNDLE_ROUTES_STOPPING, stop.routesThatStopHereToString()) + fragment.arguments = args + + return fragment + } + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt new file mode 100644 index 0000000..3d84f56 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapLibreFragment.kt @@ -0,0 +1,1148 @@ +package it.reyboz.bustorino.fragments + + +import android.Manifest +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.LinearInterpolator +import android.widget.ImageButton +import android.widget.RelativeLayout +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.contract.ActivityResultContracts +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.preference.PreferenceManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.gson.Gson +import com.google.gson.JsonObject +import it.reyboz.bustorino.R +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate +import it.reyboz.bustorino.backend.mato.MQTTMatoClient +import it.reyboz.bustorino.data.PreferencesHolder +import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops +import it.reyboz.bustorino.fragments.SettingsFragment.LIVE_POSITIONS_PREF_MQTT_VALUE +import it.reyboz.bustorino.map.MapLibreUtils +import it.reyboz.bustorino.map.Styles +import it.reyboz.bustorino.util.Permissions +import it.reyboz.bustorino.util.ViewUtils +import it.reyboz.bustorino.viewmodels.LivePositionsViewModel +import it.reyboz.bustorino.viewmodels.StopsMapViewModel +import org.maplibre.android.MapLibre +import org.maplibre.android.camera.CameraPosition +import org.maplibre.android.camera.CameraUpdateFactory +import org.maplibre.android.geometry.LatLng +import org.maplibre.android.geometry.LatLngBounds +import org.maplibre.android.location.LocationComponent +import org.maplibre.android.location.LocationComponentOptions +import org.maplibre.android.location.modes.CameraMode +import org.maplibre.android.maps.MapLibreMap +import org.maplibre.android.maps.MapView +import org.maplibre.android.maps.OnMapReadyCallback +import org.maplibre.android.maps.Style +import org.maplibre.android.plugins.annotation.Symbol +import org.maplibre.android.plugins.annotation.SymbolManager +import org.maplibre.android.plugins.annotation.SymbolOptions +import org.maplibre.android.style.expressions.Expression +import org.maplibre.android.style.layers.Property +import org.maplibre.android.style.layers.Property.* +import org.maplibre.android.style.layers.PropertyFactory +import org.maplibre.android.style.layers.SymbolLayer +import org.maplibre.android.style.sources.GeoJsonSource +import org.maplibre.geojson.Feature +import org.maplibre.geojson.FeatureCollection +import org.maplibre.geojson.Point + + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val STOP_TO_SHOW = "stoptoshow" + +/** + * A simple [Fragment] subclass. + * Use the [MapLibreFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class MapLibreFragment : GeneralMapLibreFragment() { + + + protected var fragmentListener: CommonFragmentListener? = null + private lateinit var locationComponent: LocationComponent + private var lastLocation: Location? = null + private val stopsViewModel: StopsMapViewModel by viewModels() + private var stopsShowing = ArrayList(0) + private var isBottomSheetShowing = false + //private lateinit var symbolManager: SymbolManager + + + // Sources for stops and buses are in GeneralMapLibreFragment + + private var stopsLayerStarted = false + private var lastStopsSizeShown = 0 + private var lastBBox = LatLngBounds.from(2.0, 2.0, 1.0,1.0) + private var mapInitCompleted =false + private var stopsRedrawnTimes = 0 + + //bottom Sheet behavior + private lateinit var bottomSheetBehavior: BottomSheetBehavior + private var bottomLayout: RelativeLayout? = null + private lateinit var stopTitleTextView: TextView + private lateinit var stopNumberTextView: TextView + private lateinit var linesPassingTextView: TextView + private lateinit var arrivalsCard: CardView + private lateinit var directionsCard: CardView + + //private var stopActiveSymbol: Symbol? = null + + // Location stuff + private lateinit var locationManager: LocationManager + private lateinit var showUserPositionButton: ImageButton + private lateinit var centerUserButton: ImageButton + private lateinit var followUserButton: ImageButton + private var followingUserLocation = false + private var pendingLocationActivation = false + private var ignoreCameraMovementForFollowing = true + private var enablingPositionFromClick = false + private val positionRequestLauncher = + registerForActivityResult, Map>( + ActivityResultContracts.RequestMultiplePermissions(), + ActivityResultCallback { result -> + if (result == null) { + Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?") + } else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION] + && java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) { + // We can use the position, restart location overlay + Log.d(DEBUG_TAG, "HAVE THE PERMISSIONS") + if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null) + return@ActivityResultCallback ///@registerForActivityResult + val locationManager = + requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager + @SuppressLint("MissingPermission") val userLocation = + locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + if (userLocation != null) { + if(LatLng(userLocation.latitude, userLocation.longitude).distanceTo(DEFAULT_LATLNG) >= MAX_DIST_KM*1000){ + setMapLocationEnabled(true, true, false) + } + } else requestInitialUserLocation() + + } else{ + Toast.makeText(requireContext(),"User location disabled", Toast.LENGTH_SHORT).show() + Log.w(DEBUG_TAG, "No location permission") + } + }) + private val showUserPositionRequestLauncher = + registerForActivityResult, Map>( + ActivityResultContracts.RequestMultiplePermissions(), + ActivityResultCallback { result -> + if (result == null) { + Log.w(DEBUG_TAG, "Got asked permission but request is null, doing nothing?") + } else if (java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_COARSE_LOCATION] + && java.lang.Boolean.TRUE == result[Manifest.permission.ACCESS_FINE_LOCATION]) { + // We can use the position, restart location overlay + if (context == null || requireContext().getSystemService(Context.LOCATION_SERVICE) == null) + return@ActivityResultCallback ///@registerForActivityResult + setMapLocationEnabled(true, true, enablingPositionFromClick) + } else Log.w(DEBUG_TAG, "No location permission") + }) + + //BUS POSITIONS + private var useMQTTViewModel = true + private val livePositionsViewModel : LivePositionsViewModel by viewModels() + + private val positionsByVehDict = HashMap(5) + private val animatorsByVeh = HashMap() + private var lastUpdateTime : Long = -1 + //private var busLabelSymbolsByVeh = HashMap() + private val symbolsToUpdate = ArrayList() + + private var initialStopToShow : Stop? = null + private var initialStopShown = false + + //shown stuff + //private var savedStateOnStop : Bundle? = null + + private val showBusLayer = true + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + initialStopToShow = Stop.fromBundle(arguments) + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + val rootView = inflater.inflate(R.layout.fragment_map_libre, + container, false) + //reset the counter + lastStopsSizeShown = 0 + stopsRedrawnTimes = 0 + stopsLayerStarted = false + symbolsToUpdate.clear() + + // Init layout view + + // Init the MapView + mapView = rootView.findViewById(R.id.libreMapView) + + val restoreBundle = stopsViewModel.savedState + if(restoreBundle!=null){ + mapView.onCreate(restoreBundle) + } else mapView.onCreate(savedInstanceState) + mapView.getMapAsync(this) //{ //map -> + //map.setStyle("https://demotiles.maplibre.org/style.json") } + + + //init bottom sheet + val bottomSheet = rootView.findViewById(R.id.bottom_sheet) + bottomLayout = bottomSheet + stopTitleTextView = bottomSheet.findViewById(R.id.stopTitleTextView) + stopNumberTextView = bottomSheet.findViewById(R.id.stopNumberTextView) + linesPassingTextView = bottomSheet.findViewById(R.id.linesPassingTextView) + arrivalsCard = bottomSheet.findViewById(R.id.arrivalsCardButton) + directionsCard = bottomSheet.findViewById(R.id.directionsCardButton) + + showUserPositionButton = rootView.findViewById(R.id.locationEnableIcon) + showUserPositionButton.setOnClickListener(this::switchUserLocationStatus) + followUserButton = rootView.findViewById(R.id.followUserImageButton) + centerUserButton = rootView.findViewById(R.id.centerMapImageButton) + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) + bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + + arrivalsCard.setOnClickListener { + if(context!=null){ + Toast.makeText(context,"ARRIVALS", Toast.LENGTH_SHORT).show() + } + } + centerUserButton.setOnClickListener { + if(context!=null && locationComponent.isLocationComponentEnabled) { + val location = locationComponent.lastKnownLocation + + location?.let { + mapView.getMapAsync { map -> + map.animateCamera(CameraUpdateFactory.newCameraPosition( + CameraPosition.Builder().target(LatLng(location.latitude, location.longitude)).build()), 500) + } + } + } + } + followUserButton.setOnClickListener { + // onClick user following button + if(context!=null && locationComponent.isLocationComponentEnabled){ + if(followingUserLocation) + locationComponent.cameraMode = CameraMode.NONE + else locationComponent.cameraMode = CameraMode.TRACKING + // CameraMode.TRACKING makes the camera move and jump to the location + + setFollowingUser(!followingUserLocation) + } + } + locationManager = requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager + if (Permissions.bothLocationPermissionsGranted(requireContext())) { + requestInitialUserLocation() + } else{ + if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { + //TODO: show dialog for permission rationale + Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT) + .show() + } + positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS) + } + + + // Setup close button + rootView.findViewById(R.id.btnClose).setOnClickListener { + hideStopBottomSheet() + } + + Log.d(DEBUG_TAG, "Fragment View Created!") + + //TODO: Reshow last open stop when switching back to the map fragment + return rootView + } + + /** + * This method sets up the map and the layers + */ + override fun onMapReady(mapReady: MapLibreMap) { + this.map = mapReady + val context = requireContext() + val mjson = Styles.getJsonStyleFromAsset(context, PreferencesHolder.getMapLibreStyleFile(context)) + //ViewUtils.loadJsonFromAsset(requireContext(),"map_style_good.json") + + activity?.run { + val builder = Style.Builder().fromJson(mjson!!) + + mapReady.setStyle(builder) { style -> + + mapStyle = style + //setupLayers(style) + + // Start observing data + observeStops() + initMapLocation(style, mapReady, requireContext()) + //init stop layer with this + val stopsInCache = stopsViewModel.getAllStopsLoaded() + if(stopsInCache.isEmpty()) + initStopsLayer(style, FeatureCollection.fromFeatures(ArrayList())) + else + displayStops(stopsInCache) + if(showBusLayer) setupBusLayer(style) + + + /*symbolManager = SymbolManager(mapView,mapReady,style, null, "symbol-transit-airfield") + symbolManager.iconAllowOverlap = true + symbolManager.textAllowOverlap = false + symbolManager.textIgnorePlacement =true + + + */ + /*symbolManager.addClickListener{ _ -> + if (stopActiveSymbol!=null){ + hideStopBottomSheet() + + return@addClickListener true + } else + return@addClickListener false + } + + */ + + } + + mapReady.addOnCameraIdleListener { + map?.let { + val newBbox = it.projection.visibleRegion.latLngBounds + if ((newBbox.center==lastBBox.center) && (newBbox.latitudeSpan==lastBBox.latitudeSpan) && (newBbox.longitudeSpan==lastBBox.latitudeSpan)){ + //do nothing + } else { + stopsViewModel.loadStopsInLatLngBounds(newBbox) + lastBBox = newBbox + + } + + } + + } + mapReady.addOnCameraMoveStartedListener { + + map?.let { setFollowingUser(it.locationComponent.cameraMode == CameraMode.TRACKING) } + //setFollowingUser() + + } + + mapReady.addOnMapClickListener { point -> + onMapClickReact(point) + } + + mapInitCompleted = true + // we start requesting the bus positions now + startRequestingPositions() + } + savedMapStateOnPause?.let{ + restoreMapStateFromBundle(it) + pendingLocationActivation = false + Log.d(DEBUG_TAG, "Restored map state from the saved bundle") + } + //reset saved State at the end + if( savedMapStateOnPause == null) { + //set initial position + val zoom = 15.0 + //center position + val latlngTarget = initialStopToShow?.let { + LatLng(it.latitude!!, it.longitude!!) + } ?: LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) + mapReady.cameraPosition = CameraPosition.Builder().target(latlngTarget).zoom(zoom).build() + } + //reset saved state + savedMapStateOnPause = null + } + + private fun onMapClickReact(point: LatLng): Boolean{ + map?.let { mapReady -> + val screenPoint = mapReady.projection.toScreenLocation(point) + val features = mapReady.queryRenderedFeatures(screenPoint, STOPS_LAYER_ID) + val busNearby = mapReady.queryRenderedFeatures(screenPoint, BUSES_LAYER_ID) + if (features.isNotEmpty()) { + val feature = features[0] + val id = feature.getStringProperty("id") + val name = feature.getStringProperty("name") + //Toast.makeText(requireContext(), "Clicked on $name ($id)", Toast.LENGTH_SHORT).show() + val stop = stopsViewModel.getStopByID(id) + stop?.let { newstop -> + val sameStopClicked = shownStopInBottomSheet?.let { newstop.ID==it.ID } ?: false + if (isBottomSheetShowing) { + hideStopBottomSheet() + } + if(!sameStopClicked){ + openStopInBottomSheet(newstop) + //isBottomSheetShowing = true + //move camera + if (newstop.latitude != null && newstop.longitude != null) + //mapReady.cameraPosition = CameraPosition.Builder().target(LatLng(it.latitude!!, it.longitude!!)).build() + mapReady.animateCamera( + CameraUpdateFactory.newLatLng(LatLng(newstop.latitude!!, newstop.longitude!!)), + 750 + ) + } + + } + return true + } else if (busNearby.isNotEmpty()) { + val feature = busNearby[0] + val vehid = feature.getStringProperty("veh") + val route = feature.getStringProperty("line") + + Toast.makeText(context, "Veh $vehid on route $route", Toast.LENGTH_SHORT).show() + return true + } + } + return false + } + + + private fun initStopsLayer(style: Style, features:FeatureCollection){ + + stopsSource = GeoJsonSource(STOPS_SOURCE_ID,features) + style.addSource(stopsSource) + + // add icon + style.addImage(STOP_IMAGE_ID, + ResourcesCompat.getDrawable(resources,R.drawable.bus_stop_new, activity?.theme)!!) + + style.addImage(STOP_ACTIVE_IMG, ResourcesCompat.getDrawable(resources, R.drawable.bus_stop_new_highlight, activity?.theme)!!) + style.addImage("ball",ResourcesCompat.getDrawable(resources, R.drawable.ball, activity?.theme)!!) + // Stops layer + val stopsLayer = SymbolLayer(STOPS_LAYER_ID, STOPS_SOURCE_ID) + stopsLayer.withProperties( + PropertyFactory.iconImage(STOP_IMAGE_ID), + PropertyFactory.iconAnchor(ICON_ANCHOR_CENTER), + PropertyFactory.iconAllowOverlap(true), + PropertyFactory.iconIgnorePlacement(true) + ) + + style.addLayerBelow(stopsLayer, "symbol-transit-airfield") //"label_country_1") this with OSM Bright + + + selectedStopSource = GeoJsonSource(SEL_STOP_SOURCE, FeatureCollection.fromFeatures(ArrayList())) + style.addSource(selectedStopSource) + + val selStopLayer = SymbolLayer(SEL_STOP_LAYER, SEL_STOP_SOURCE) + selStopLayer.withProperties( + PropertyFactory.iconImage(STOP_ACTIVE_IMG), + PropertyFactory.iconAllowOverlap(true), + PropertyFactory.iconIgnorePlacement(true), + PropertyFactory.iconAnchor(ICON_ANCHOR_CENTER), + + ) + style.addLayerAbove(selStopLayer, STOPS_LAYER_ID) + + stopsLayerStarted = true + } + + /** + * Setup the Map Layers + */ + private fun setupBusLayer(style: Style) { + // Buses source + busesSource = GeoJsonSource(BUSES_SOURCE_ID) + style.addSource(busesSource) + style.addImage("bus_symbol",ResourcesCompat.getDrawable(resources, R.drawable.map_bus_position_icon, activity?.theme)!!) + + // Buses layer + val busesLayer = SymbolLayer(BUSES_LAYER_ID, BUSES_SOURCE_ID).apply { + withProperties( + PropertyFactory.iconImage("bus_symbol"), + PropertyFactory.iconSize(1.2f), + PropertyFactory.iconAllowOverlap(true), + PropertyFactory.iconIgnorePlacement(true), + PropertyFactory.iconRotate(Expression.get("bearing")), + PropertyFactory.iconRotationAlignment(ICON_ROTATION_ALIGNMENT_MAP), + + PropertyFactory.textAnchor(TEXT_ANCHOR_CENTER), + PropertyFactory.textAllowOverlap(true), + PropertyFactory.textField(Expression.get("line")), + PropertyFactory.textColor(Color.WHITE), + PropertyFactory.textRotationAlignment(TEXT_ROTATION_ALIGNMENT_VIEWPORT), + PropertyFactory.textSize(12f), + PropertyFactory.textFont(arrayOf("noto_sans_regular")) + ) + } + style.addLayerAbove(busesLayer, STOPS_LAYER_ID) + + //Line names layer + /*vehiclesLabelsSource = GeoJsonSource(LABELS_SOURCE) + style.addSource(vehiclesLabelsSource) + val textLayer = SymbolLayer(LABELS_LAYER_ID, LABELS_SOURCE).apply { + withProperties( + PropertyFactory.textField("label"), + PropertyFactory.textSize(30f), + //PropertyFactory.textHaloColor(Color.BLACK), + //PropertyFactory.textHaloWidth(1f), + + PropertyFactory.textAnchor(TEXT_ANCHOR_CENTER), + PropertyFactory.textAllowOverlap(true), + PropertyFactory.textField(Expression.get("line")), + PropertyFactory.textColor(Color.WHITE), + PropertyFactory.textRotationAlignment(TEXT_ROTATION_ALIGNMENT_VIEWPORT), + PropertyFactory.textSize(12f) + + + ) + } + style.addLayerAbove(textLayer, BUSES_LAYER_ID) + + */ + + } + + /** + * Update the bottom sheet with the stop information + */ + override fun openStopInBottomSheet(stop: Stop){ + bottomLayout?.let { + + //lay.findViewById(R.id.stopTitleTextView).text ="${stop.ID} - ${stop.stopDefaultName}" + val stopName = stop.stopUserName ?: stop.stopDefaultName + stopTitleTextView.text = stopName//stop.stopDefaultName + stopNumberTextView.text = stop.ID + val string_show = if (stop.numRoutesStopping==0) "" + else if (stop.numRoutesStopping <= 1) + requireContext().getString(R.string.line_fill, stop.routesThatStopHereToString()) + else requireContext().getString(R.string.lines_fill, stop.routesThatStopHereToString()) + linesPassingTextView.text = string_show + + //SET ON CLICK LISTENER + arrivalsCard.setOnClickListener{ + fragmentListener?.requestArrivalsForStopID(stop.ID) + } + + directionsCard.setOnClickListener { + ViewUtils.openStopInOutsideApp(stop, context) + } + + + } + //add stop marker + if (stop.latitude!=null && stop.longitude!=null) { + /*stopActiveSymbol = symbolManager.create( + SymbolOptions() + .withLatLng(LatLng(stop.latitude!!, stop.longitude!!)) + .withIconImage(STOP_ACTIVE_IMG) + .withIconAnchor(ICON_ANCHOR_CENTER) + //.withTextFont(arrayOf("noto_sans_regular"))) + */ + Log.d(DEBUG_TAG, "Showing stop: ${stop.ID}") + val list = ArrayList() + list.add(stopToGeoJsonFeature(stop)) + selectedStopSource.setGeoJson( + FeatureCollection.fromFeatures(list) + ) + } + shownStopInBottomSheet = stop + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + isBottomSheetShowing = true + } + override fun onAttach(context: Context) { + super.onAttach(context) + fragmentListener = if (context is CommonFragmentListener) { + context + } else { + throw RuntimeException( + context.toString() + + " must implement FragmentListenerMain" + ) + } + } + override fun onDetach() { + super.onDetach() + fragmentListener = null + } + + override fun onStart() { + super.onStart() + mapView.onStart() + } + + override fun onResume() { + super.onResume() + mapView.onResume() + + val keySourcePositions = getString(R.string.pref_positions_source) + if(showBusLayer) { + useMQTTViewModel = PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getString(keySourcePositions, LIVE_POSITIONS_PREF_MQTT_VALUE) + .contentEquals(LIVE_POSITIONS_PREF_MQTT_VALUE) + + if (useMQTTViewModel) livePositionsViewModel.requestMatoPosUpdates(MQTTMatoClient.LINES_ALL) + else livePositionsViewModel.requestGTFSUpdates() + //mapViewModel.testCascade(); + livePositionsViewModel.isLastWorkResultGood.observe(this) { d: Boolean -> + Log.d( + DEBUG_TAG, "Last trip download result is $d" + ) + } + livePositionsViewModel.tripsGtfsIDsToQuery.observe(this) { dat: List -> + Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: $dat") + livePositionsViewModel.downloadTripsFromMato(dat) + } + } + + fragmentListener?.readyGUIfor(FragmentKind.MAP) + } + + override fun onPause() { + super.onPause() + mapView.onPause() + Log.d(DEBUG_TAG, "Fragment paused") + + savedMapStateOnPause = saveMapStateInBundle() + if (useMQTTViewModel) livePositionsViewModel.stopMatoUpdates() + + } + + override fun onStop() { + super.onStop() + mapView.onStop() + Log.d(DEBUG_TAG, "Fragment stopped!") + stopsViewModel.savedState = Bundle().let { + mapView.onSaveInstanceState(it) + it + } + + } + + override fun onDestroy() { + super.onDestroy() + mapView.onDestroy() + Log.d(DEBUG_TAG, "Destroyed map Fragment!!") + } + + override fun onMapDestroy() { + mapStyle.removeLayer(STOPS_LAYER_ID) + mapStyle.removeSource(STOPS_SOURCE_ID) + + mapStyle.removeLayer(BUSES_LAYER_ID) + mapStyle.removeSource(BUSES_SOURCE_ID) + + + map?.locationComponent?.isLocationComponentEnabled = false + } + override fun getBaseViewForSnackBar(): View? { + return mapView + } + + private fun observeStops() { + // Observe stops + stopsViewModel.stopsToShow.observe(viewLifecycleOwner) { stops -> + stopsShowing = ArrayList(stops) + displayStops(stopsShowing) + initialStopToShow?.let{ s-> + //show the stop in the bottom sheet + if(!initialStopShown) { + openStopInBottomSheet(s) + initialStopShown = true + } + } + } + + } + + /** + * Add the stops to the layers + */ + private fun displayStops(stops: List?) { + if (stops.isNullOrEmpty()) return + + if (stops.size==lastStopsSizeShown){ + Log.d(DEBUG_TAG, "Not updating, have same number of stops. After 3 times") + return + } + /*if(stops.size> lastStopsSizeShown){ + stopsRedrawnTimes = 0 + } else{ + stopsRedrawnTimes++ + } + + */ + + val features = ArrayList()//stops.mapNotNull { stop -> + //stop.latitude?.let { lat -> + // stop.longitude?.let { lon -> + for (s in stops){ + if (s.latitude!=null && s.longitude!=null) + features.add(stopToGeoJsonFeature(s)) + + + } + Log.d(DEBUG_TAG,"Have put ${features.size} stops to display") + + // if the layer is already started, substitute the stops inside, otherwise start it + if (stopsLayerStarted) { + stopsSource.setGeoJson(FeatureCollection.fromFeatures(features)) + lastStopsSizeShown = features.size + } else + map?.let { + Log.d(DEBUG_TAG, "Map stop layer is not started yet, init layer") + initStopsLayer(mapStyle, FeatureCollection.fromFeatures(features)) + Log.d(DEBUG_TAG,"Started stops layer on map") + lastStopsSizeShown = features.size + stopsLayerStarted = true + } + } + // Hide the bottom sheet and remove extra symbol + private fun hideStopBottomSheet(){ + /*if (stopActiveSymbol!=null){ + symbolManager.delete(stopActiveSymbol) + stopActiveSymbol = null + } + */ + //empty the source + selectedStopSource.setGeoJson(FeatureCollection.fromFeatures(ArrayList())) + + bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + //remove initial stop + if(initialStopToShow!=null){ + initialStopToShow = null + } + //set showing + isBottomSheetShowing = false + shownStopInBottomSheet = null + } + // --------------- BUS LOCATIONS STUFF -------------------------- + /** + * Start requesting position updates + */ + private fun startRequestingPositions() { + livePositionsViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner) { data: HashMap> -> + Log.d( + DEBUG_TAG, + "Have " + data.size + " trip updates, has Map start finished: " + mapInitCompleted + ) + if (mapInitCompleted) updateBusPositionsInMap(data) + if (!isDetached && !useMQTTViewModel) livePositionsViewModel.requestDelayedGTFSUpdates( + 3000 + ) + } + } + private fun isInsideVisibleRegion(latitude: Double, longitude: Double, nullValue: Boolean): Boolean{ + var isInside = nullValue + val visibleRegion = map?.projection?.visibleRegion + visibleRegion?.let { + val bounds = it.latLngBounds + isInside = bounds.contains(LatLng(latitude, longitude)) + } + return isInside + } + + /*private fun createLabelForVehicle(positionUpdate: LivePositionUpdate){ + val symOpt = SymbolOptions() + .withLatLng(LatLng(positionUpdate.latitude, positionUpdate.longitude)) + .withTextColor("#ffffff") + .withTextField(positionUpdate.routeID.substringBeforeLast('U')) + .withTextSize(13f) + .withTextAnchor(TEXT_ANCHOR_CENTER) + .withTextFont(arrayOf( "noto_sans_regular"))//"noto_sans_regular", "sans-serif")) //"noto_sans_regular")) + + val newSymbol = symbolManager.create(symOpt + ) + Log.d(DEBUG_TAG, "Symbol for veh ${positionUpdate.vehicle}: $newSymbol") + busLabelSymbolsByVeh[positionUpdate.vehicle] = newSymbol + } + private fun removeVehicleLabel(vehicle: String){ + busLabelSymbolsByVeh[vehicle]?.let { + symbolManager.delete(it) + busLabelSymbolsByVeh.remove(vehicle) + } + } + + */ + + /** + * Update function for the bus positions + * Takes the processed updates and saves them accordingly + */ + private fun updateBusPositionsInMap(incomingData: HashMap>){ + val vehsNew = HashSet(incomingData.values.map { up -> up.first.vehicle }) + val vehsOld = HashSet(positionsByVehDict.keys) + + val symbolsToUpdate = ArrayList() + for (upsWithTrp in incomingData.values){ + val pos = upsWithTrp.first + val vehID = pos.vehicle + var animate = false + if (vehsOld.contains(vehID)){ + //update position only if the starting or the stopping position of the animation are in the view + val oldPos = positionsByVehDict[vehID] + var avoidShowingUpdateBecauseIsImpossible = false + oldPos?.let{ + if(oldPos.routeID!=pos.routeID) { + val dist = LatLng(it.latitude, it.longitude).distanceTo(LatLng(pos.latitude, pos.longitude)) + val speed = dist*3.6 / (pos.timestamp - it.timestamp) //this should be in km/h + Log.w(DEBUG_TAG, "Vehicle $vehID changed route from ${oldPos.routeID} to ${pos.routeID}, distance: $dist, speed: $speed") + if (speed > 120 || speed < 0){ + avoidShowingUpdateBecauseIsImpossible = true + } + } + } + if (avoidShowingUpdateBecauseIsImpossible){ + // DO NOT SHOW THIS SHIT + Log.w(DEBUG_TAG, "Update for vehicle $vehID skipped") + continue + } + + val samePosition = oldPos?.let { (oldPos.latitude==pos.latitude)&&(oldPos.longitude == pos.longitude) }?:false + if(!samePosition) { + val isPositionInBounds = isInsideVisibleRegion( + pos.latitude, pos.longitude, true + ) || (oldPos?.let { isInsideVisibleRegion(it.latitude,it.longitude,true) } ?: false) + if (isPositionInBounds) { + //animate = true + //this moves both the icon and the label + moveVehicleToNewPosition(pos) + } else { + positionsByVehDict[vehID] = pos + /*busLabelSymbolsByVeh[vehID]?.let { + it.latLng = LatLng(pos.latitude, pos.longitude) + symbolsToUpdate.add(it) + } + + */ + } + } + } + else if(pos.latitude>0 && pos.longitude>0) { + //we should not have to check for this + // update it simply + positionsByVehDict[vehID] = pos + //createLabelForVehicle(pos) + }else{ + Log.w(DEBUG_TAG, "Update ignored for veh $vehID on line ${pos.routeID}, lat: ${pos.latitude}, lon ${pos.longitude}") + } + + } + // symbolManager.update(symbolsToUpdate) + //remove old positions + vehsOld.removeAll(vehsNew) + //now vehsOld contains the vehicles id for those that have NOT been updated + val currentTimeStamp = System.currentTimeMillis() /1000 + for(vehID in vehsOld){ + //remove after 2 minutes of inactivity + if (positionsByVehDict[vehID]!!.timestamp - currentTimeStamp > 2*60){ + positionsByVehDict.remove(vehID) + //removeVehicleLabel(vehID) + } + } + //update UI + updatePositionsIcons() + } + + /** + * This is the tricky part, animating the transitions + * Basically, we need to set the new positions with the data and redraw them all + */ + private fun moveVehicleToNewPosition(positionUpdate: LivePositionUpdate){ + if (positionUpdate.vehicle !in positionsByVehDict.keys) + return + val vehID = positionUpdate.vehicle + val currentUpdate = positionsByVehDict[positionUpdate.vehicle] + currentUpdate?.let { it -> + //cancel current animation on vehicle + animatorsByVeh[vehID]?.cancel() + + val currentPos = LatLng(it.latitude, it.longitude) + val newPos = LatLng(positionUpdate.latitude, positionUpdate.longitude) + val valueAnimator = ValueAnimator.ofObject(MapLibreUtils.LatLngEvaluator(), currentPos, newPos) + valueAnimator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener { + private var latLng: LatLng? = null + override fun onAnimationUpdate(animation: ValueAnimator) { + latLng = animation.animatedValue as LatLng + //update position on animation + val update = positionsByVehDict[positionUpdate.vehicle]!! + latLng?.let { ll-> + update.latitude = ll.latitude + update.longitude = ll.longitude + updatePositionsIcons() + } + } + }) + valueAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + super.onAnimationStart(animation) + //val update = positionsByVehDict[positionUpdate.vehicle]!! + //remove the label at the start of the animation + /*val annot = busLabelSymbolsByVeh[vehID] + annot?.let { sym -> + sym.textOpacity = 0.0f + symbolsToUpdate.add(sym) + } + + */ + + } + + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + //recreate the label at the end of the animation + //createLabelForVehicle(positionUpdate) + /*val annot = busLabelSymbolsByVeh[vehID] + annot?.let { sym -> + sym.textOpacity = 1.0f + sym.latLng = newPos //LatLng(newPos) + symbolsToUpdate.add(sym) + } + + */ + } + }) + + //set the new position as the current one but with the old lat and lng + positionUpdate.latitude = currentUpdate.latitude + positionUpdate.longitude = currentUpdate.longitude + positionsByVehDict[vehID] = positionUpdate + valueAnimator.duration = 300 + valueAnimator.interpolator = LinearInterpolator() + valueAnimator.start() + + animatorsByVeh[vehID] = valueAnimator + + } ?: { + Log.e(DEBUG_TAG, "Have to run animation for veh ${positionUpdate.vehicle} but not in the dict, adding") + positionsByVehDict[positionUpdate.vehicle] = positionUpdate + } + } + + /** + * Update the bus positions displayed on the map, from the existing data + */ + private fun updatePositionsIcons(){ + //avoid frequent updates + val currentTime = System.currentTimeMillis() + if(currentTime - lastUpdateTime < 60){ + //DO NOT UPDATE THE MAP + return + } + val features = ArrayList()//stops.mapNotNull { stop -> + //stop.latitude?.let { lat -> + // stop.longitude?.let { lon -> + for (pos in positionsByVehDict.values){ + //if (s.latitude!=null && s.longitude!=null) + val point = Point.fromLngLat(pos.longitude, pos.latitude) + features.add( + Feature.fromGeometry( + point, + JsonObject().apply { + addProperty("veh", pos.vehicle) + addProperty("trip", pos.tripID) + addProperty("bearing", pos.bearing ?:0.0f) + addProperty("line", pos.routeID.substringBeforeLast('U')) + } + ) + ) + /*busLabelSymbolsByVeh[pos.vehicle]?.let { + it.latLng = LatLng(pos.latitude, pos.longitude) + symbolsToUpdate.add(it) + } + + */ + } + //this updates the positions + busesSource.setGeoJson(FeatureCollection.fromFeatures(features)) + //update labels, clear cache to be used + //symbolManager.update(symbolsToUpdate) + symbolsToUpdate.clear() + lastUpdateTime = System.currentTimeMillis() + } + + // ------ LOCATION STUFF ----- + @SuppressLint("MissingPermission") + private fun requestInitialUserLocation() { + val provider : String = LocationManager.GPS_PROVIDER//getBestLocationProvider() + + //provider.let { + setLocationIconEnabled(true) + Toast.makeText(requireContext(), R.string.position_searching_message, Toast.LENGTH_SHORT).show() + pendingLocationActivation = true + locationManager.requestSingleUpdate(provider, object : LocationListener { + override fun onLocationChanged(location: Location) { + val userLatLng = LatLng(location.latitude, location.longitude) + val distanceToTarget = userLatLng.distanceTo(DEFAULT_LATLNG) + + if (distanceToTarget <= MAX_DIST_KM*1000.0) { + map?.let{ + // if we are still waiting for the position to enable + if(pendingLocationActivation) + setMapLocationEnabled(true, true, false) + } + } else { + Toast.makeText(context, "You are too far, not showing the position", Toast.LENGTH_SHORT).show() + } + } + + override fun onProviderDisabled(provider: String) {} + override fun onProviderEnabled(provider: String) {} + + @Deprecated("Deprecated in Java") + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} + }, null) + + } + + /** + * Initialize the map location, but do not enable the component + */ + @SuppressLint("MissingPermission") + private fun initMapLocation(style: Style, map: MapLibreMap, context: Context){ + locationComponent = map.locationComponent + val locationComponentOptions = + LocationComponentOptions.builder(context) + .pulseEnabled(true) + .build() + val locationComponentActivationOptions = + MapLibreUtils.buildLocationComponentActivationOptions(style, locationComponentOptions, context) + locationComponent.activateLocationComponent(locationComponentActivationOptions) + locationComponent.isLocationComponentEnabled = false + + lastLocation?.let { + if (it.accuracy < 200) + locationComponent.forceLocationUpdate(it) + } + } + + + + /** + * Handles logic of enabling the user location on the map + */ + @SuppressLint("MissingPermission") + private fun setMapLocationEnabled(enabled: Boolean, assumePermissions: Boolean, fromClick: Boolean) { + if (enabled) { + val permissionOk = assumePermissions || Permissions.bothLocationPermissionsGranted(requireContext()) + + if (permissionOk) { + Log.d(DEBUG_TAG, "Permission OK, starting location component, assumed: $assumePermissions") + locationComponent.isLocationComponentEnabled = true + if (initialStopToShow==null) { + locationComponent.cameraMode = CameraMode.TRACKING //CameraMode.TRACKING + setFollowingUser(true) + } + setLocationIconEnabled(true) + if (fromClick) Toast.makeText(context, R.string.location_enabled, Toast.LENGTH_SHORT).show() + } else { + if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { + //TODO: show dialog for permission rationale + Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show() + } + Log.d(DEBUG_TAG, "Requesting permission to show user location") + enablingPositionFromClick = fromClick + showUserPositionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS) + } + } else{ + locationComponent.isLocationComponentEnabled = false + setFollowingUser(false) + setLocationIconEnabled(false) + if (fromClick) { + Toast.makeText(requireContext(), R.string.location_disabled, Toast.LENGTH_SHORT).show() + if(pendingLocationActivation) pendingLocationActivation=false //Cancel the request for the enablement of the position + } + } + + } + + + private fun setLocationIconEnabled(enabled: Boolean){ + if (enabled) + showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_red)) + else + showUserPositionButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.location_circlew_grey)) + + } + + /** + * Helper method for GUI + */ + private fun updateFollowingIcon(enabled: Boolean){ + if(enabled) + followUserButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_follow_me_on)) + else + followUserButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_follow_me)) + + } + private fun setFollowingUser(following: Boolean){ + updateFollowingIcon(following) + followingUserLocation = following + if(following) + ignoreCameraMovementForFollowing = true + } + + + + private fun switchUserLocationStatus(view: View?){ + if(pendingLocationActivation || locationComponent.isLocationComponentEnabled) setMapLocationEnabled(false, false, true) + else{ + Log.d(DEBUG_TAG, "Request enable location") + setMapLocationEnabled(true, false, true) + } + } + + companion object { + private const val STOPS_SOURCE_ID = "stops-source" + private const val STOPS_LAYER_ID = "stops-layer" + private const val STOPS_LAYER_SEL_ID ="stops-layer-selected" + + private const val LABELS_LAYER_ID = "bus-labels-layer" + private const val LABELS_SOURCE = "labels-source" + private const val STOP_IMAGE_ID ="bus-stop-icon" + const val DEFAULT_CENTER_LAT = 45.0708 + const val DEFAULT_CENTER_LON = 7.6858 + private val DEFAULT_LATLNG = LatLng(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) + private const val POSITION_FOUND_ZOOM = 16.5 + private const val NO_POSITION_ZOOM = 17.1 + private const val MAX_DIST_KM = 90.0 + + private const val DEBUG_TAG = "BusTO-MapLibreFrag" + private const val STOP_ACTIVE_IMG = "Stop-active" + + private const val LOCATION_PERMISSION_REQUEST_CODE = 981202 + + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @param stop Eventual stop to center the map into + * @return A new instance of fragment MapLibreFragment. + */ + @JvmStatic + fun newInstance(stop: Stop?) = + MapLibreFragment().apply { + arguments = Bundle().let { + // Cannot use Parcelable as it requires higher version of Android + //stop?.let{putParcelable(STOP_TO_SHOW, it)} + stop?.toBundle(it) + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java index 2ac5f0c..fff1183 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/SettingsFragment.java @@ -1,232 +1,234 @@ /* BusTO - Fragments components Copyright (C) 2020 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.fragments; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.Observer; import androidx.preference.*; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkInfo; import androidx.work.WorkManager; import it.reyboz.bustorino.ActivityBackup; import it.reyboz.bustorino.R; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.data.GtfsMaintenanceWorker; import org.jetbrains.annotations.NotNull; import java.lang.ref.WeakReference; import java.util.HashSet; import java.util.List; public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = SettingsFragment.class.getName(); private static final String DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG"; //private static final Handler mHandler; + // Matching preferences.xml public final static String PREF_KEY_STARTUP_SCREEN="startup_screen_to_show"; public final static String KEY_ARRIVALS_FETCHERS_USE = "arrivals_fetchers_use_setting"; public final static String LIVE_POSITIONS_PREF_MQTT_VALUE="mqtt"; + public final static String LIBREMAP_STYLE_PREF_KEY = "libremap_style_1"; private boolean setSummaryStartupPref = false; @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { mHandler = new Handler(); return super.onCreateView(inflater, container, savedInstanceState); } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { //getPreferenceManager().setSharedPreferencesName(getString(R.string.mainSharedPreferences)); convertStringPrefToIntIfNeeded(getString(R.string.pref_key_num_recents), getContext()); getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); setPreferencesFromResource(R.xml.preferences,rootKey); /*EditTextPreference editPref = findPreference(getString(R.string.pref_key_num_recents)); editPref.setOnBindEditTextListener(editText -> { editText.setInputType(InputType.TYPE_CLASS_NUMBER); editText.setSelection(0,editText.getText().length()); }); */ ListPreference startupScreenPref = findPreference(PREF_KEY_STARTUP_SCREEN); if(startupScreenPref !=null){ if (startupScreenPref.getValue()==null){ startupScreenPref.setSummary(getString(R.string.nav_arrivals_text)); setSummaryStartupPref = true; } } //Log.d("BusTO-PrefFrag","startup screen pref is "+startupScreenPref.getValue()); Preference dbUpdateNow = findPreference("pref_db_update_now"); if (dbUpdateNow!=null) - dbUpdateNow.setOnPreferenceClickListener( + dbUpdateNow.setOnPreferenceClickListener( preference -> { //trigger update if(getContext()!=null) { DatabaseUpdate.requestDBUpdateWithWork(getContext().getApplicationContext(), true, true); Toast.makeText(getContext(),R.string.requesting_db_update,Toast.LENGTH_SHORT).show(); return true; } return false; } ); //set click listener on backup item final Preference backupPref = findPreference("pref_backup_open"); if (backupPref!=null) backupPref.setOnPreferenceClickListener( preference -> { if(getActivity()!=null){ startActivity( new Intent(getActivity().getApplicationContext(), ActivityBackup.class) ); return true; } else { return false; } } ); else { Log.e("BusTO-Preferences", "Cannot find db update preference"); } Preference clearGtfsTrips = findPreference("pref_clear_gtfs_trips"); if (clearGtfsTrips != null) { clearGtfsTrips.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(@NonNull @NotNull Preference preference) { if (getContext() != null) { OneTimeWorkRequest requ = GtfsMaintenanceWorker.Companion.makeOneTimeRequest(GtfsMaintenanceWorker.CLEAR_GTFS_TRIPS); WorkManager.getInstance(getContext()).enqueue(requ); WorkManager.getInstance(getContext()).getWorkInfosByTagLiveData(GtfsMaintenanceWorker.CLEAR_GTFS_TRIPS).observe(getViewLifecycleOwner(), (Observer>) workInfos -> { if(workInfos.isEmpty()) return; if(workInfos.get(0).getState()==(WorkInfo.State.SUCCEEDED)){ Toast.makeText( getContext(), R.string.all_trips_removed, Toast.LENGTH_SHORT ).show(); } }); return true; } return false; } }); } } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Preference pref = findPreference(key); Log.d(TAG,"Preference key "+key+" changed"); if (key.equals(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE)){ Log.d(TAG, "New value is: "+sharedPreferences.getStringSet(key, new HashSet<>())); } //sometimes this happens if(getContext()==null) return; if(key.equals(PREF_KEY_STARTUP_SCREEN) && setSummaryStartupPref && pref !=null){ ListPreference listPref = (ListPreference) pref; pref.setSummary(listPref.getEntry()); } /* THIS CODE STAYS COMMENTED FOR FUTURE REFERENCES if (key.equals(getString(R.string.pref_key_num_recents))){ //check that is it an int String value = sharedPreferences.getString(key,""); boolean valid = value.length() != 0; try{ Integer intValue = Integer.parseInt(value); } catch (NumberFormatException ex){ valid = false; } if (!valid){ Toast.makeText(getContext(), R.string.invalid_number, Toast.LENGTH_SHORT).show(); if(pref instanceof EditTextPreference){ EditTextPreference prefEdit = (EditTextPreference) pref; //Intent intent = prefEdit.getIntent(); Log.d(TAG, "opening preference, dialog showing "+ (getParentFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG)!=null) ); //getPreferenceManager().showDialog(pref); //onDisplayPreferenceDialog(prefEdit); mHandler.postDelayed(new DelayedDisplay(prefEdit), 500); } } } */ Log.d("BusTO Settings", "changed "+key+"\n "+sharedPreferences.getAll()); } private void convertStringPrefToIntIfNeeded(String preferenceKey, Context con){ if (con == null) return; SharedPreferences defaultSharedPref = PreferenceManager.getDefaultSharedPreferences(con); try{ Integer val = defaultSharedPref.getInt(preferenceKey, 0); } catch (NumberFormatException | ClassCastException ex){ //convert the preference //final String preferenceNumRecents = getString(R.string.pref_key_num_recents); Log.d("Preference - BusTO", "Converting to integer the string preference "+preferenceKey); String currentValue = defaultSharedPref.getString(preferenceKey, "10"); int newValue; try{ newValue = Integer.parseInt(currentValue); } catch (NumberFormatException e){ newValue = 10; } final SharedPreferences.Editor editor = defaultSharedPref.edit(); editor.remove(preferenceKey); editor.putInt(preferenceKey, newValue); editor.apply(); } } class DelayedDisplay implements Runnable{ private final WeakReference preferenceWeakReference; public DelayedDisplay(DialogPreference preference) { this.preferenceWeakReference = new WeakReference<>(preference); } @Override public void run() { if(preferenceWeakReference.get()==null) return; getPreferenceManager().showDialog(preferenceWeakReference.get()); } } } diff --git a/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt b/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt index bd1af1d..9e6d7f7 100644 --- a/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt +++ b/app/src/main/java/it/reyboz/bustorino/map/BusPositionUtils.kt @@ -1,41 +1,41 @@ package it.reyboz.bustorino.map import android.animation.ObjectAnimator import android.util.Log import androidx.core.content.res.ResourcesCompat import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.data.gtfs.MatoPattern import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops -import it.reyboz.bustorino.fragments.MapFragment import org.osmdroid.util.GeoPoint import org.osmdroid.views.MapView import org.osmdroid.views.overlay.Marker class BusPositionUtils { companion object{ @JvmStatic public fun updateBusPositionMarker(map: MapView, marker: Marker?, posUpdate: LivePositionUpdate, tripMarkersAnimators: HashMap, justCreated: Boolean) { val position: GeoPoint val updateID = posUpdate.tripID if (!justCreated) { position = marker!!.position if (posUpdate.latitude != position.latitude || posUpdate.longitude != position.longitude) { val newpos = GeoPoint(posUpdate.latitude, posUpdate.longitude) val valueAnimator = MarkerUtils.makeMarkerAnimator( map, marker, newpos, MarkerUtils.LINEAR_ANIMATION, 1200 ) valueAnimator.setAutoCancel(true) tripMarkersAnimators.put(updateID, valueAnimator) valueAnimator.start() } //marker.setPosition(new GeoPoint(posUpdate.getLatitude(), posUpdate.getLongitude())); } else { position = GeoPoint(posUpdate.latitude, posUpdate.longitude) marker!!.position = position } - if (posUpdate.bearing != null) marker.rotation = posUpdate.bearing * -1f + //if (posUpdate.bearing != null) marker.rotation = posUpdate.bearing * -1f + marker.rotation = posUpdate.bearing?.let { it*-1f } ?: 0.0f } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/map/MapLibreUtils.kt b/app/src/main/java/it/reyboz/bustorino/map/MapLibreUtils.kt new file mode 100644 index 0000000..f0ecc69 --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/map/MapLibreUtils.kt @@ -0,0 +1,229 @@ +package it.reyboz.bustorino.map + +import android.animation.TypeEvaluator +import android.content.Context +import android.util.Log +import it.reyboz.bustorino.backend.Stop +import org.maplibre.android.geometry.LatLng +import org.maplibre.android.location.LocationComponentActivationOptions +import org.maplibre.android.location.LocationComponentOptions +import org.maplibre.android.location.engine.LocationEngineRequest +import org.maplibre.android.maps.Style +import org.maplibre.geojson.Point +import org.maplibre.turf.TurfMeasurement + + +class MapLibreUtils { + companion object{ + const val STYLE_BRIGHT_DEFAULT_JSON = "map_style_good_noshops.json" + const val STYLE_VERSATILES_COLORFUL_JSON = "versatiles_colorful_light.json" + const val STYLE_OSM_RASTER="openstreetmap_raster.json" + private const val DEBUG_TAG ="BusTO-MapLibreUtils" + + @JvmStatic + fun getDefaultStyleJson() = STYLE_VERSATILES_COLORFUL_JSON + + @JvmStatic + fun shortestRotation(from: Float, to: Float): Float { + var delta = (to - from) % 360 + if (delta > 180) delta -= 360 + if (delta < -180) delta += 360 + return from + delta + } + @JvmStatic + fun buildLocationComponentActivationOptions( + style: Style, + locationComponentOptions: LocationComponentOptions, + context: Context + ): LocationComponentActivationOptions { + return LocationComponentActivationOptions + .builder(context, style) + .locationComponentOptions(locationComponentOptions) + .useDefaultLocationEngine(true) + .locationEngineRequest( + LocationEngineRequest.Builder(750) + .setFastestInterval(750) + .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) + .build() + ) + .build() + } + @JvmStatic + fun calcDistanceInSegment(points: List, from: Int, to:Int): Double{ + var d=0.0 + var prev = points[from] + for(i in from+1..to){ + d += prev.distanceTo(points[i]) + prev = points[i] + } + return d + } + @JvmStatic + fun findIndexMidPoint(points: List, from: Int, to: Int, distThresh:Double): Int{ + var totdist=0.0 + var idx = -2 + var prev = points[from] + for(i in from+1..to){ + totdist += prev.distanceTo(points[i]) + prev = points[i] + if (totdist >= distThresh){ + idx = i + break + } + } + if(idx==-2) throw Error("Distance out of bounds, total distance is $totdist") + return idx + } + + @JvmStatic + fun findPointsToPutDirectionMarkers(polyPoints: List, stops: List, distanceIcon: Double): List{ + val closestIndices = findIndicesClosestPointsForStops(polyPoints, stops) + Log.d(DEBUG_TAG, "idcs: $closestIndices") + + val distancesSec = mutableListOf() + var pi = closestIndices[0] + val cumulativeDist = mutableListOf() + var sum = 0.0 + + val pointsOutput = mutableListOf() + var nPoints = 0 + var distFromLastPoint = 0.0 + for(i in 1..= distanceIcon){ + Log.d(DEBUG_TAG, "Add between stop ${stops[i-1]} and stop ${stops[i]}, distance between: $dd") + if(dd>100) { + val imid = findIndexMidPoint(polyPoints, pi, newi, dd / 2) + pointsOutput.add(imid) + nPoints += 1 + distFromLastPoint=0.0 + } + } else{ + //add the last distance + //distFromLastPoint+=dd/2 + } + pi= newi + } + return pointsOutput + } + /* + VERSION WITH TOTAL DISTANCE + val distancesSec = mutableListOf() + var prevk = 0 + val cumulativeDist = mutableListOf() + var sum = 0.0 + + val pointsOutput = mutableListOf() + var nPoints = 0 + //for(i in 1..= distanceIcon ){ + + //Log.d(DEBUG_TAG, "Add between stop ${stops[lastStopidx]} and stop ${stops[lastStopidx+1]}") + //find closest stops + var stopDist = + Math.min(polyPoints[prevk].distanceTo(polyPoints[lastStopidx]),polyPoints[k].distanceTo(polyPoints[lastStopidx])) + + if(lastStopidx+1 < stops.size){ + stopDist = Math.min(stopDist, + Math.min(polyPoints[prevk].distanceTo(polyPoints[lastStopidx+1]),polyPoints[k].distanceTo(polyPoints[lastStopidx+1])) + ) + } + if(stopDist>100) { + val imid = findIndexMidPoint(polyPoints, prevk, k, dd / 2) + pointsOutput.add(imid) + nPoints += 1 + distFromLast = 0.0 + } + } + prevk = k + if (k>closestIndices[lastStopidx]) + lastStopidx +=1 + } + return pointsOutput + */ + @JvmStatic + fun splitPolyWhenDistanceTooBig(points: List, distMax: Double): List{ + val outList = mutableListOf(points[0]) + var oldP = points[0] + for(i in 1.. distMax){ + val newLat = (oldP.latitude+newP.latitude)/2 + val newLong = (oldP.longitude+newP.longitude)/2 + val extraP = LatLng(newLat,newLong) + outList.add(extraP) + } + outList.add(newP) + + oldP=newP + } + + return outList + } + + @JvmStatic + fun findIndicesClosestPointsForStops(points:List, stops:List): List { + + val closestIndices = stops.map { s-> + val p = LatLng(s.latitude!!, s.longitude!!) + var dist = 10_000_000.0 // in meters + var id = -1 + for (j in points.indices){ + val newd = p.distanceTo(points[j]) + if (newd { + private val latLng = LatLng() + override fun evaluate(fraction: Float, startValue: LatLng, endValue: LatLng): LatLng { + latLng.latitude = startValue.latitude + (endValue.latitude - startValue.latitude) * fraction + latLng.longitude = startValue.longitude + (endValue.longitude - startValue.longitude) * fraction + return latLng + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/map/MapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/map/MapViewModel.kt index 87651c3..462f122 100644 --- a/app/src/main/java/it/reyboz/bustorino/map/MapViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/map/MapViewModel.kt @@ -1,15 +1,18 @@ package it.reyboz.bustorino.map import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import it.reyboz.bustorino.backend.Stop class MapViewModel : ViewModel() { val currentLat = MutableLiveData(INVALID) val currentLong = MutableLiveData(INVALID) val currentZoom = MutableLiveData(-10.0) + var stopShowing: Stop? = null + companion object{ const val INVALID = -1000.0 } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/map/Styles.kt b/app/src/main/java/it/reyboz/bustorino/map/Styles.kt new file mode 100644 index 0000000..8c76ada --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/map/Styles.kt @@ -0,0 +1,56 @@ +package it.reyboz.bustorino.map +import android.content.Context +import it.reyboz.bustorino.util.ViewUtils +import org.maplibre.android.maps.Style + +object Styles { + const val DEMOTILES = "https://demotiles.maplibre.org/style.json" + + const val VERSATILES = "https://tiles.versatiles.org/assets/styles/colorful.json" + + const val AMERICANA = "https://americanamap.org/style.json" + + const val OPENFREEMAP_LIBERTY = "https://tiles.openfreemap.org/styles/liberty" + + const val OPENFREEMAP_BRIGHT = "https://tiles.openfreemap.org/styles/bright" + + const val BASIC_V8 = "mapbox://styles/mapbox/streets-v8" + + + const val AWS_OPEN_DATA_STANDARD_LIGHT = + "https://maps.geo.us-east-2.amazonaws.com/maps/v0/maps/OpenDataStyle/style-descriptor?key=v1.public.eyJqdGkiOiI1NjY5ZTU4My0yNWQwLTQ5MjctODhkMS03OGUxOTY4Y2RhMzgifR_7GLT66TNRXhZJ4KyJ-GK1TPYD9DaWuc5o6YyVmlikVwMaLvEs_iqkCIydspe_vjmgUVsIQstkGoInXV_nd5CcmqRMMa-_wb66SxDdbeRDvmmkpy2Ow_LX9GJDgL2bbiCws0wupJPFDwWCWFLwpK9ICmzGvNcrPbX5uczOQL0N8V9iUvziA52a1WWkZucIf6MUViFRf3XoFkyAT15Ll0NDypAzY63Bnj8_zS8bOaCvJaQqcXM9lrbTusy8Ftq8cEbbK5aMFapXRjug7qcrzUiQ5sr0g23qdMvnKJQFfo7JuQn8vwAksxrQm6A0ByceEXSfyaBoVpFcTzEclxUomhY.NjAyMWJkZWUtMGMyOS00NmRkLThjZTMtODEyOTkzZTUyMTBi" + + private fun protomaps(style: String): String { + return "https://api.protomaps.com/styles/v2/${style}.json?key=e761cc7daedf832a" + } + + private fun makeStyleMapBoxUrl(dark: Boolean) = + if (dark) + "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json" + else //"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json" + "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json" + + val PROTOMAPS_LIGHT = protomaps("light") + + val PROTOMAPS_DARK = protomaps("dark") + + val PROTOMAPS_GRAYSCALE = protomaps("grayscale") + + val PROTOMAPS_WHITE = protomaps("white") + + val PROTOMAPS_BLACK = protomaps("black") + + val CARTO_DARK = makeStyleMapBoxUrl(true) + + val CARTO_VOYAGER = makeStyleMapBoxUrl(false) + + fun getPredefinedStyleWithFallback(name: String): String { + try { + val style = Style.getPredefinedStyle(name) + return style + } catch (e: Exception) { + return OPENFREEMAP_LIBERTY + } + } + fun getJsonStyleFromAsset(context: Context, filename: String) = ViewUtils.loadJsonFromAsset(context,filename) +} \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/util/ViewUtils.kt b/app/src/main/java/it/reyboz/bustorino/util/ViewUtils.kt index bb4071c..05ea059 100644 --- a/app/src/main/java/it/reyboz/bustorino/util/ViewUtils.kt +++ b/app/src/main/java/it/reyboz/bustorino/util/ViewUtils.kt @@ -1,109 +1,159 @@ package it.reyboz.bustorino.util -import android.R import android.content.Context +import android.content.Intent import android.content.res.Resources.Theme import android.graphics.Rect +import android.net.Uri +import android.os.Bundle import android.util.Log import android.util.TypedValue import android.view.View import android.view.WindowManager import android.view.animation.Animation import android.view.animation.Transformation -import androidx.annotation.ColorInt +import android.widget.Toast import androidx.core.widget.NestedScrollView +import it.reyboz.bustorino.R +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.fragments.LinesDetailFragment +import it.reyboz.bustorino.fragments.LinesDetailFragment.Companion +import java.io.IOException class ViewUtils { companion object{ const val DEBUG_TAG="BusTO:ViewUtils" fun isViewFullyVisibleInScroll(view: View, scrollView: NestedScrollView): Boolean { val scrollBounds = Rect() scrollView.getDrawingRect(scrollBounds) val top = view.y val bottom = top + view.height Log.d(DEBUG_TAG, "Scroll bounds are $scrollBounds, top:${view.y}, bottom $bottom") return (scrollBounds.top < top && scrollBounds.bottom > bottom) } fun isViewPartiallyVisibleInScroll(view: View, scrollView: NestedScrollView): Boolean{ val scrollBounds = Rect() scrollView.getHitRect(scrollBounds) Log.d(DEBUG_TAG, "Scroll bounds are $scrollBounds") if (view.getLocalVisibleRect(scrollBounds)) { return true } else { return false } } //from https://stackoverflow.com/questions/4946295/android-expand-collapse-animation fun expand(v: View,duration: Long, layoutHeight: Int = WindowManager.LayoutParams.WRAP_CONTENT) { val matchParentMeasureSpec = View.MeasureSpec.makeMeasureSpec((v.parent as View).width, View.MeasureSpec.EXACTLY) val wrapContentMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) v.measure(matchParentMeasureSpec, wrapContentMeasureSpec) val targetHeight = v.measuredHeight // Older versions of android (pre API 21) cancel animations for views with a height of 0. v.layoutParams.height = 1 v.visibility = View.VISIBLE val a: Animation = object : Animation() { override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { v.layoutParams.height = if (interpolatedTime == 1f) layoutHeight else (targetHeight * interpolatedTime).toInt() v.requestLayout() } override fun willChangeBounds(): Boolean { return true } } // Expansion speed of 1dp/ms if(duration == DEF_DURATION) a.duration = (targetHeight / v.context.resources.displayMetrics.density).toInt().toLong() else a.duration = duration v.startAnimation(a) } fun collapse(v: View, duration: Long): Animation { val initialHeight = v.measuredHeight val a: Animation = object : Animation() { override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { if (interpolatedTime == 1f) { v.visibility = View.GONE } else { v.layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt() v.requestLayout() } } override fun willChangeBounds(): Boolean { return true } } // Collapse speed of 1dp/ms if (duration == DEF_DURATION) a.duration = (initialHeight / v.context.resources.displayMetrics.density).toInt().toLong() else a.duration = duration v.startAnimation(a) return a } const val DEF_DURATION: Long = -2 fun getColorFromTheme(context: Context, resId: Int): Int { val typedValue = TypedValue() val theme: Theme = context.getTheme() theme.resolveAttribute(resId, typedValue, true) val color = typedValue.data return color } + fun loadJsonFromAsset(context: Context, fileName: String): String? { + return try { + context.assets.open(fileName).bufferedReader().use { it.readText() } + } catch (e: IOException) { + e.printStackTrace() + null + } + } + @JvmStatic + fun insertSpaces(input: String): String { + return input.replace(Regex("(?<=[A-Za-z])(?=[0-9])|(?<=[0-9])(?=[A-Za-z])"), " ") + } + + @JvmStatic + fun mergeBundles(bundle1: Bundle?, bundle2: Bundle?): Bundle { + if (bundle1 == null) { + return if (bundle2 == null) Bundle() else Bundle(bundle2) // Return a copy to avoid modification + } + if (bundle2 != null) { + bundle1.putAll(bundle2) + } + return bundle1 + } + @JvmStatic + fun openStopInOutsideApp(stop: Stop, context: Context?){ + if(stop.latitude==null || stop.longitude==null){ + Log.e(DEBUG_TAG, "Navigate to stop but longitude and/or latitude are null") + }else{ + val stopName = stop.stopUserName ?: stop.stopDefaultName + + val uri = "geo:?q=${stop.latitude},${stop.longitude}(${stop.ID} - $stopName)" + //This below is the full URI, in the correct format. However it seems that apps accept the above one + //val uri_real = "geo:${stop.latitude},${stop.longitude}?q=${stop.latitude},${stop.longitude}(${stop.ID} - $stopName)" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) + context?.run{ + if(intent.resolveActivity(packageManager)!=null){ + startActivity(intent) + } else{ + Toast.makeText(this, R.string.no_map_app_to_show_stop, Toast.LENGTH_SHORT).show() + } + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt index 96d16d9..d73e400 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LinesViewModel.kt @@ -1,111 +1,132 @@ package it.reyboz.bustorino.viewmodels import android.app.Application import android.util.Log import androidx.lifecycle.* import it.reyboz.bustorino.backend.Result import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.data.GtfsRepository import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.OldDataRepository import it.reyboz.bustorino.data.gtfs.GtfsDatabase import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.PatternStop +import org.maplibre.android.geometry.LatLng import java.util.concurrent.Executors class LinesViewModel(application: Application) : AndroidViewModel(application) { private val gtfsRepo: GtfsRepository private val oldRepo: OldDataRepository //val patternsByRouteLiveData: LiveData> private val routeIDToSearch = MutableLiveData() private var lastShownPatternStops = ArrayList() val currentPatternStops = MutableLiveData>() val selectedPatternLiveData = MutableLiveData() val stopsForPatternLiveData = MutableLiveData>() private val executor = Executors.newFixedThreadPool(2) val mapShowing = MutableLiveData(true) fun setMapShowing(yes: Boolean){ mapShowing.value = yes //retrigger redraw stopsForPatternLiveData.postValue(stopsForPatternLiveData.value) } init { val gtfsDao = GtfsDatabase.getGtfsDatabase(application).gtfsDao() gtfsRepo = GtfsRepository(gtfsDao) oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) } val routesGTTLiveData: LiveData> by lazy{ gtfsRepo.getLinesLiveDataForFeed("gtt") } val patternsWithStopsByRouteLiveData = routeIDToSearch.switchMap { gtfsRepo.getPatternsWithStopsForRouteID(it) } val gtfsRoute = routeIDToSearch.switchMap { gtfsRepo.getRouteFromGtfsId(it) } fun setRouteIDQuery(routeID: String){ routeIDToSearch.value = routeID } fun getRouteIDQueried(): String?{ return routeIDToSearch.value } var shouldShowMessage = true fun setPatternToDisplay(patternStops: MatoPatternWithStops){ selectedPatternLiveData.value = patternStops } /** * Find the */ private fun requestStopsForGTFSIDs(gtfsIDs: List){ if (gtfsIDs.equals(lastShownPatternStops)){ //nothing to do return } oldRepo.requestStopsWithGtfsIDs(gtfsIDs) { if (it.isSuccess) { stopsForPatternLiveData.postValue(it.result) } else { Log.e("BusTO-LinesVM", "Got error on callback with stops for gtfsID") it.exception?.printStackTrace() } } lastShownPatternStops.clear() for(id in gtfsIDs) lastShownPatternStops.add(id) } fun requestStopsForPatternWithStops(patternStops: MatoPatternWithStops){ val gtfsIDs = ArrayList() for(pat in patternStops.stopsIndices){ gtfsIDs.add(pat.stopGtfsId) } requestStopsForGTFSIDs(gtfsIDs) } + fun getStopByID(id:String) : Stop?{ + //var stop : Stop? = null + val stop = stopsForPatternLiveData.value?.let { stops -> + for (s in stops){ + if(s.ID == id) + return@let s + } + return@let null + } + return stop + } + + private var lastMapPos: Pair? = null + + fun saveMapPos(latLng: LatLng, zoom: Float){ + lastMapPos = Pair(latLng, zoom) + } + + fun getLastMapPos(): Pair? = lastMapPos + /*fun getLinesGTT(): MutableLiveData> { val routesData = MutableLiveData>() viewModelScope.launch { val routes=gtfsRepo.getLinesForFeed("gtt") routesData.postValue(routes) } return routesData }*/ } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt index 27f3df6..6050e7b 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt @@ -1,287 +1,355 @@ /* BusTO - ViewModel components Copyright (C) 2023 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.viewmodels import android.app.Application import android.util.Log import androidx.lifecycle.* import androidx.work.WorkInfo import androidx.work.WorkManager import com.android.volley.DefaultRetryPolicy import com.android.volley.Response import it.reyboz.bustorino.backend.NetworkVolleyManager import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest +import it.reyboz.bustorino.backend.gtfs.GtfsUtils import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.mato.MQTTMatoClient import it.reyboz.bustorino.data.GtfsRepository import it.reyboz.bustorino.data.MatoPatternsDownloadWorker import it.reyboz.bustorino.data.MatoTripsDownloadWorker +import it.reyboz.bustorino.data.gtfs.MatoPattern import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap import kotlin.collections.HashSet +typealias FullPositionUpdatesMap = HashMap> +typealias FullPositionUpdate = Pair class LivePositionsViewModel(application: Application): AndroidViewModel(application) { private val gtfsRepo = GtfsRepository(application) //private val updates = UpdatesMap() - private val updatesLiveData = MutableLiveData>() + private val positionsToBeMatchedLiveData = MutableLiveData>() private val netVolleyManager = NetworkVolleyManager.getInstance(application) private var mqttClient = MQTTMatoClient() private var lineListening = "" private var lastTimeReceived: Long = 0 private val gtfsRtRequestRunning = MutableLiveData(false) private val lastFailedTripsRequest = HashMap() private val workManager = WorkManager.getInstance(application) private var lastRequestedDownloadTrips = MutableLiveData>() + //INPUT FILTER FOR LINE + private var gtfsLineToFilterPos = MutableLiveData>() + + fun setGtfsLineToFilterPos(line: String, pattern: MatoPattern?){ + gtfsLineToFilterPos.value = Pair(line, pattern) + } + var isLastWorkResultGood = workManager .getWorkInfosForUniqueWorkLiveData(MatoTripsDownloadWorker.TAG_TRIPS).map { it -> if (it.isEmpty()) return@map false var res = true if(it[0].state == WorkInfo.State.FAILED){ val currDate = Date() res = false lastRequestedDownloadTrips.value?.let { trips-> for(tr in trips){ lastFailedTripsRequest[tr] = currDate } } } return@map res } /** * Responder to the MQTT Client */ private val matoPositionListener = MQTTMatoClient.Companion.MQTTMatoListener{ val mupds = ArrayList() if(lineListening==MQTTMatoClient.LINES_ALL){ for(sdic in it.values){ for(update in sdic.values){ mupds.add(update) } } } else{ //we're listening to one if (it.containsKey(lineListening.trim()) ){ for(up in it[lineListening]?.values!!){ mupds.add(up) } } } val time = System.currentTimeMillis() if(lastTimeReceived == (0.toLong()) || (time-lastTimeReceived)>500){ - updatesLiveData.postValue(mupds) + positionsToBeMatchedLiveData.postValue(mupds) lastTimeReceived = time } } //find the trip IDs in the updates - private val tripsIDsInUpdates = updatesLiveData.map { it -> + private val tripsIDsInUpdates = positionsToBeMatchedLiveData.map { it -> //Log.d(DEBUG_TI, "Updates map has keys ${upMap.keys}") it.map { pos -> "gtt:"+pos.tripID } } // get the trip IDs in the DB private val gtfsTripsPatternsInDB = tripsIDsInUpdates.switchMap { //Log.i(DEBUG_TI, "tripsIds in updates: ${it.size}") gtfsRepo.gtfsDao.getTripPatternStops(it) } //trip IDs to query, which are not present in the DB //REMEMBER TO OBSERVE THIS IN THE MAP val tripsGtfsIDsToQuery: LiveData> = gtfsTripsPatternsInDB.map { tripswithPatterns -> val tripNames=tripswithPatterns.map { twp-> twp.trip.tripID } Log.i(DEBUG_TI, "Have ${tripswithPatterns.size} trips in the DB") if (tripsIDsInUpdates.value!=null) return@map tripsIDsInUpdates.value!!.filter { !(tripNames.contains(it) || it.contains("null"))} else { Log.e(DEBUG_TI,"Got results for gtfsTripsInDB but not tripsIDsInUpdates??") return@map ArrayList() } } // unify trips with updates val updatesWithTripAndPatterns = gtfsTripsPatternsInDB.map { tripPatterns-> Log.i(DEBUG_TI, "Mapping trips and patterns") - val mdict = HashMap>() + val mdict = HashMap() //missing patterns val routesToDownload = HashSet() - if(updatesLiveData.value!=null) - for(update in updatesLiveData.value!!){ + if(positionsToBeMatchedLiveData.value!=null) + for(update in positionsToBeMatchedLiveData.value!!){ val trID:String = update.tripID var found = false for(trip in tripPatterns){ if (trip.pattern == null){ //pattern is null, which means we have to download // the pattern data from MaTO routesToDownload.add(trip.trip.routeID) } if (trip.trip.tripID == "gtt:$trID"){ found = true //insert directly mdict[trID] = Pair(update,trip) break } } if (!found){ //Log.d(DEBUG_TI, "Cannot find pattern ${tr}") //give the update anyway mdict[trID] = Pair(update,null) } } //have to request download of missing Patterns if (routesToDownload.size > 0){ Log.d(DEBUG_TI, "Have ${routesToDownload.size} missing patterns from the DB: $routesToDownload") //downloadMissingPatterns (ArrayList(routesToDownload)) MatoPatternsDownloadWorker.downloadPatternsForRoutes(routesToDownload.toList(), getApplication()) } return@map mdict } + //OBSERVE THIS TO GET THE LOCATION UPDATES FILTERED + val filteredLocationUpdates = MediatorLiveData>>() + init { + filteredLocationUpdates.addSource(updatesWithTripAndPatterns){ + filteredLocationUpdates.value = filterUpdatesForGtfsLine(it, gtfsLineToFilterPos.value!!) + } + + filteredLocationUpdates.addSource(gtfsLineToFilterPos){ + updatesWithTripAndPatterns.value?.let{ ups-> filteredLocationUpdates.value = filterUpdatesForGtfsLine(ups, it)} + } + + } + + private fun filterUpdatesForGtfsLine(updates: FullPositionUpdatesMap, + linePatt: Pair): + Pair, List>{ + val gtfsLineId = linePatt.first + val pattern = linePatt.second + + + val filtdLineID = GtfsUtils.stripGtfsPrefix(gtfsLineId) + //filter buses with direction, show those only with the same direction + val updsForTripId = HashMap>() + val directionId = pattern?.directionId ?: -100 + val numUpds = updates.entries.size + Log.d(DEBUG_TI, "Got $numUpds updates, current pattern is: ${pattern?.name}, directionID: ${pattern?.directionId}") + // cannot understand where this is used + //val patternsDirections = HashMap() + val vehicleOnWrongDirection = mutableListOf() + for((tripId, pair) in updates.entries){ + //remove trips with wrong line + val posUp = pair.first + val vehicle = pair.first.vehicle + if(pair.first.routeID!=filtdLineID) + continue + + if(directionId!=-100 && pair.second!=null && pair.second?.pattern !=null){ + val dir = pair.second!!.pattern!!.directionId + + if(dir == directionId){ + //add the trip + updsForTripId[tripId] = pair + } else{ + vehicleOnWrongDirection.add(vehicle) + } + //patternsDirections[tripId] = dir ?: -10 + } else{ + updsForTripId[tripId] = pair + //Log.d(DEBUG_TAG, "No pattern for tripID: $tripId") + //patternsDirections[tripId] = -10 + } + } + Log.d(DEBUG_TI, " Filtered updates are ${updsForTripId.keys.size}") // Original updates directs: $patternsDirections\n + + return Pair(updsForTripId, vehicleOnWrongDirection) + } + fun requestMatoPosUpdates(line: String){ lineListening = line viewModelScope.launch { mqttClient.startAndSubscribe(line,matoPositionListener, getApplication()) } //updatePositions(1000) } fun stopMatoUpdates(){ viewModelScope.launch { val tt = System.currentTimeMillis() mqttClient.stopMatoRequests(matoPositionListener) val time = System.currentTimeMillis() -tt Log.d(DEBUG_TI, "Took $time ms to unsubscribe") } } fun retriggerPositionUpdate(){ - if(updatesLiveData.value!=null){ - updatesLiveData.postValue(updatesLiveData.value) + if(positionsToBeMatchedLiveData.value!=null){ + positionsToBeMatchedLiveData.postValue(positionsToBeMatchedLiveData.value) } } //Gtfs Real time private val gtfsPositionsReqListener = object: GtfsRtPositionsRequest.Companion.RequestListener{ override fun onResponse(response: ArrayList?) { Log.i(DEBUG_TI,"Got response from the GTFS RT server") response?.let {it:ArrayList -> if (it.size == 0) { Log.w(DEBUG_TI,"No position updates from the GTFS RT server") return } else { //Log.i(DEBUG_TI, "Posting value to positionsLiveData") - viewModelScope.launch { updatesLiveData.postValue(it) } + viewModelScope.launch { positionsToBeMatchedLiveData.postValue(it) } } } gtfsRtRequestRunning.postValue(false) } } private val positionRequestErrorListener = Response.ErrorListener { Log.e(DEBUG_TI, "Could not download the update", it) gtfsRtRequestRunning.postValue(false) } fun requestGTFSUpdates(){ if(gtfsRtRequestRunning.value == null || !gtfsRtRequestRunning.value!!) { val request = GtfsRtPositionsRequest(positionRequestErrorListener, gtfsPositionsReqListener) request.setRetryPolicy( DefaultRetryPolicy(1000,10,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT) ) netVolleyManager.requestQueue.add(request) Log.i(DEBUG_TI, "Requested GTFS realtime position updates") gtfsRtRequestRunning.value = true } } fun requestDelayedGTFSUpdates(timems: Long){ viewModelScope.launch { delay(timems) requestGTFSUpdates() } } override fun onCleared() { //stop the MQTT Service Log.d(DEBUG_TI, "Clearing the live positions view model, stopping the mqttClient") mqttClient.disconnect() super.onCleared() } //Request trips download fun downloadTripsFromMato(trips: List): Boolean{ if(trips.isEmpty()) return false var shouldContinue = false val currentDateTime = Date().time for (tr in trips){ if (!lastFailedTripsRequest.containsKey(tr)){ shouldContinue = true break } else{ //Log.i(DEBUG_TI, "Last time the trip has failed is ${lastFailedTripsRequest[tr]}") if ((lastFailedTripsRequest[tr]!!.time - currentDateTime) > MAX_TIME_RETRY){ shouldContinue =true break } } } if (shouldContinue) { //if one trip val workRequ =MatoTripsDownloadWorker.requestMatoTripsDownload(trips, getApplication(), "BusTO-MatoTripsDown") workRequ?.let { req -> Log.d(DEBUG_TI, "Enqueueing new work, saving work info") lastRequestedDownloadTrips.postValue(trips) //isLastWorkResultGood = } } else{ Log.w(DEBUG_TI, "Requested to fetch data for ${trips.size} trips but they all have failed before in the last $MAX_MINUTES_RETRY mins") } return shouldContinue } companion object{ private const val DEBUG_TI = "BusTO-LivePosViewModel" private const val MAX_MINUTES_RETRY = 3 private const val MAX_TIME_RETRY = MAX_MINUTES_RETRY*60*1000 //3 minutes (in milliseconds) } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt index 9d07f4f..1c75b83 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/StopsMapViewModel.kt @@ -1,45 +1,88 @@ package it.reyboz.bustorino.viewmodels import android.app.Application +import android.os.Bundle import android.util.Log import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.map -import it.reyboz.bustorino.backend.Result import it.reyboz.bustorino.backend.Stop -import it.reyboz.bustorino.data.GtfsRepository import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.OldDataRepository -import it.reyboz.bustorino.data.gtfs.GtfsDatabase +import org.maplibre.android.geometry.LatLngBounds import org.osmdroid.util.BoundingBox -import java.util.ArrayList import java.util.concurrent.Executors +import kotlin.collections.ArrayList class StopsMapViewModel(application: Application): AndroidViewModel(application) { private val executor = Executors.newFixedThreadPool(2) private val oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) + val stopsToShow = MutableLiveData(ArrayList()) + private var stopsShownIDs = HashSet() + private var allStopsLoaded = HashMap() + val stopsInBoundingBox = MutableLiveData>() private val callback = OldDataRepository.Callback> { res -> if(res.isSuccess){ stopsInBoundingBox.postValue(res.result) Log.d(DEBUG_TAG, "Setting value of stops in bounding box") } } + private val addStopsCallback = + OldDataRepository.Callback> { res -> + if(res.isSuccess) res.result?.let{ newStops -> + val stopsAdd = stopsToShow.value ?: ArrayList() + for (s in newStops){ + if (s.ID !in stopsShownIDs){ + stopsShownIDs.add(s.ID) + stopsAdd.add(s) + allStopsLoaded[s.ID] = s + } + } + + stopsToShow.postValue(stopsAdd) + //Log.d(DEBUG_TAG, "Loaded ${stopsAdd.size} stops in total") + } + } + + fun getStopByID(id: String): Stop? { + if (id in allStopsLoaded) return allStopsLoaded[id] + else return null + } + + fun getAllStopsLoaded(): ArrayList{ + return ArrayList(allStopsLoaded.values) + } + fun requestStopsInBoundingBox(bb: BoundingBox) { bb.let { Log.d(DEBUG_TAG, "Launching stop request") oldRepo.requestStopsInArea(it.latSouth, it.latNorth, it.lonWest, it.lonEast, callback) } } + fun requestStopsInLatLng(bb: LatLngBounds) { + bb.let { + Log.d(DEBUG_TAG, "Launching stop request") + oldRepo.requestStopsInArea(it.latitudeSouth, it.latitudeNorth, it.longitudeWest, it.longitudeEast, callback) + } + } + fun loadStopsInLatLngBounds(bb: LatLngBounds?){ + bb?.let { + Log.d(DEBUG_TAG, "Launching stop request") + oldRepo.requestStopsInArea(it.latitudeSouth, it.latitudeNorth, it.longitudeWest, it.longitudeEast, + addStopsCallback) + } + } + + var savedState: Bundle? = null + companion object{ private const val DEBUG_TAG = "BusTOStopMapViewModel" } } \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_up_box_fill.xml b/app/src/main/res/drawable/arrow_up_box_fill.xml new file mode 100644 index 0000000..b164660 --- /dev/null +++ b/app/src/main/res/drawable/arrow_up_box_fill.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/bottom_sheet_background.xml b/app/src/main/res/drawable/bottom_sheet_background.xml new file mode 100644 index 0000000..9b1165c --- /dev/null +++ b/app/src/main/res/drawable/bottom_sheet_background.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bus_stop_new.xml b/app/src/main/res/drawable/bus_stop_new.xml new file mode 100644 index 0000000..cfbb96a --- /dev/null +++ b/app/src/main/res/drawable/bus_stop_new.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/bus_stop_new_highlight.xml b/app/src/main/res/drawable/bus_stop_new_highlight.xml new file mode 100644 index 0000000..f5dc16f --- /dev/null +++ b/app/src/main/res/drawable/bus_stop_new_highlight.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_departure_board_24.xml b/app/src/main/res/drawable/ic_baseline_departure_board_24.xml index 0104e53..e9c8599 100644 --- a/app/src/main/res/drawable/ic_baseline_departure_board_24.xml +++ b/app/src/main/res/drawable/ic_baseline_departure_board_24.xml @@ -1,5 +1,6 @@ - - + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android" + android:tint="?attr/colorOnPrimary"> + diff --git a/app/src/main/res/drawable/ic_magnifying_glass.xml b/app/src/main/res/drawable/ic_magnifying_glass.xml new file mode 100644 index 0000000..96d592b --- /dev/null +++ b/app/src/main/res/drawable/ic_magnifying_glass.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/app/src/main/res/drawable/navigation_right.xml b/app/src/main/res/drawable/navigation_right.xml new file mode 100644 index 0000000..4f6f0b3 --- /dev/null +++ b/app/src/main/res/drawable/navigation_right.xml @@ -0,0 +1,21 @@ + + + + diff --git a/app/src/main/res/layout/fragment_lines_detail.xml b/app/src/main/res/layout/fragment_lines_detail.xml index 27ad761..f24a5e0 100644 --- a/app/src/main/res/layout/fragment_lines_detail.xml +++ b/app/src/main/res/layout/fragment_lines_detail.xml @@ -1,155 +1,280 @@ - - + + + + + \ No newline at end of file + /> + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml index 6c12770..e1b14d9 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -1,45 +1,45 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_map_libre.xml b/app/src/main/res/layout/fragment_map_libre.xml new file mode 100644 index 0000000..552d378 --- /dev/null +++ b/app/src/main/res/layout/fragment_map_libre.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a3eae77..8195d32 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,261 +1,267 @@ Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy. Cerca Codice QR Si No Prossimo Precedente Installare Barcode Scanner? Questa azione richiede un\'altra app per scansionare i codici QR. Vuoi installare Barcode Scanner? Numero fermata Nome fermata Inserisci il numero della fermata Inserisci il nome della fermata Verifica l\'accesso ad Internet! Sembra che nessuna fermata abbia questo nome Nessun passaggio trovato alla fermata Errore di lettura del sito 5T/GTT (dannato sito!) Fermata: %1$s Linea Linee Linee urbane Linee extraurbane Linee turistiche Direzione: Nessuna linea in questa categoria Nessuna linea corrisponde alla ricerca Filtra per nome Linea: %1$s Linee: %1$s Scegli la fermata… Nessun passaggio Nessun QR code trovato, prova ad usare un\'altra app Preferiti Aiuto Informazioni Più informazioni Contribuisci https://gitpull.it/w/librebusto/it/ Codice sorgente Licenza Incontra l\'autore Fermata aggiunta ai preferiti Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti Mappa Nessun preferito? Arghh!\nSchiaccia sulla stella di una fermata per aggiungerla a questa lista! Rimuovi Rinomina Rinomina fermata Reset Informazioni Tocca la stella per aggiungere la fermata ai preferiti\n\nCome leggere gli orari: \n12:56* Orario in tempo reale\n12:56 Orario programmato\n\nTrascina giù per aggiornare l\'orario. \nTocca a lungo su Fonte Orari per cambiare sorgente degli orari di arrivo OK! Benvenuto!

Grazie per aver scelto BusTO, un\'app open source e indipendente da GTT/5T, per spostarsi a Torino attraverso software libero!

BusTO rispetta la tua privacy non raccogliendo nessun dato sull\'utilizzo, ed è leggera e senza pubblicità!


Qui puoi trovare più informazioni e link riguardo al progetto.


Schermata iniziale

Se vuoi rivedere la schermata iniziale, usa il pulsante qui sotto: ]]> Notizie e aggiornamenti

Nel canale Telegram puoi trovare informazioni sugli ultimi aggiornamenti dell\'app

]]>
Ma come funziona?

Quest\'app ottiene i passaggi dei bus, le fermate e altre informazioni utili unendo dati forniti dal sito www.gtt.to.it, www.5t.torino.it, muoversiatorino.it "per uso personale" e altre fonti Open Data (aperto.comune.torino.it).


Ingredienti:
- Fabio Mazza attuale rockstar developer anziano.
- Andrea Ugo attuale rockstar developer in formazione.
- Silviu Chiriac designer del logo 2021.
- Marco M formidabile tester e cacciatore di bug.
- Ludovico Pavesi ex rockstar developer anziano asd.
- Valerio Bozzolan attuale manutentore.
- Marco Gagino apprezzato ex collaboratore, ideatore icona e grafica.
- JSoup libreria per "web scaping".
- Google icone e librerie di supporto e design.
- Altre icone da Bootstrap, Feather e Hero Icons
- Tutti i contributori e i beta tester!


Se vuoi avere più informazioni tecniche e contribuire allo sviluppo, usa i pulsanti qui sotto! ]]>
Licenze

L\'app e il relativo codice sorgente sono distribuiti sotto la licenza GNU General Public License v3 (https://www.gnu.org/licenses/gpl-3.0.html). Ciò significa che puoi usare, studiare, migliorare e ricondividere quest\'app con qualunque mezzo e per qualsiasi scopo: a patto di mantenere sempre questi diritti a tua volta e di dare credito a Valerio Bozzolan e agli altri autori del codice dell\'app.


Note

Quest\'applicazione è rilasciata nella speranza che sia utile a tutti ma senza NESSUNA garanzia sul suo funzionamento attuale e/o futuro.

Tutti i dati utilizzati dall\'app provengono direttamente da GTT o da simili agenzie pubbliche: se trovi che sono inesatti per qualche motivo, ti invitiamo a rivolgerti a loro.

Buon utilizzo! :)

]]>
Nome troppo corto, digita più caratteri e riprova %1$s verso %2$s %s (destinazione sconosciuta) Errore interno inaspettato, impossibile estrarre dati dal sito GTT/5T Visualizza sulla mappa Non trovo un\'applicazione dove mostrarla Posizione della fermata non trovata Fermate vicine Ricerca della posizione Nessuna fermata nei dintorni Preferenze Aggiornamento del database… Aggiornamento del database Aggiornamento database forzato Tocca per aggiornare ora il database Numero minimo di fermate Il numero di fermate da ricercare non è valido Valore errato, inserisci un numero Impostazioni Distanza massima di ricerca (m) Funzionalità sperimentali Impostazioni Generali Fermate recenti Impostazioni generali Gestione del database Comincia aggiornamento manuale del database Consenti l\'accesso alla posizione per mostrarla sulla mappa Consenti l\'accesso alla posizione per mostrare le fermate vicine Abilitare il GPS arriva alle alla fermata Mostra arrivi Mostra fermate Arrivi qui vicino Fermata rimossa dai preferiti Canale telegram Mostra introduzione La mia posizione Segui posizione Attiva o disattiva posizione Posizione attivata Posizione disattivata La posizione è disabilitata sul dispositivo Fonte orari: %1$s App GTT Sito GTT Sito 5T Torino App Muoversi a Torino Sconosciuta Fonti orari di arrivo Scegli le fonti di orari da usare Cambiamento sorgente orari… Premi a lungo per cambiare la sorgente degli orari Nessun passaggio per le linee: Canale default delle notifiche Operazioni sul database Informazioni sul database (aggiornamento) BusTO - posizioni in tempo reale Posizioni in tempo reale Attività del servizio delle posizioni in tempo reale Servizio posizioni MaTO in tempo reale attivo Download dei trip dal server MaTO Chiesto troppe volte per il permesso %1$s Non si può usare questa funzionalità senza il permesso di archivio! di archivio Un bug ha fatto crashare l\'app! \nPremi \"OK\" per inviare il report agli sviluppatori via email, così potranno scovare e risolvere il tuo bug! \nIl report contiene piccole informazioni non sensibili sulla configurazione del tuo telefono e sullo stato dell\'app al momento del crash. L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima che si interrompesse: Arrivi Mappa Preferiti Apri drawer Chiudi drawer Esperimenti Offrici un caffè Mappa Ricerca fermate Versione app Orari di arrivo Richiesto aggiornamento del database Download dati dal server MaTO Mostra direzioni in maiuscolo Non cambiare Tutto in maiuscolo Solo prima lettera maiuscola Mostra arrivi quando tocchi una fermata Abilita esperimenti Schermata da mostrare all\'avvio Tocca per cambiare Fonte posizioni in tempo reale di bus e tram MaTO (aggiornate più spesso, può non funzionare) GTFS RT (più stabile) Linea aggiunta ai preferiti Linea rimossa dai preferiti Preferite Tocca a lungo la fermata per le opzioni + Stile della mappa + Versatiles (vettoriale) + OSM Legacy (raster, più leggera) Rimuovi i dati dei trip (libera spazio) Tutti i trip GTFS sono rimossi dal database Mostra introduzione open source per il trasporto pubblico di Torino. Stai usando un\'app indipendente, senza pubblicità e senza nessun tracciamento.]]> Se ti trovi a una fermata, puoi scansionare il codice QR presente sulla palina toccando l\'icona a sinistra della barra di ricerca.]]> preferiti toccando la stella a fianco del nome.]]> fermate più vicine a te direttamente nella schermata principale...]]> posizioni in tempo reale dei bus e tram (in blu)]]> Guarda nelle Impostazioni per personalizzare l\'app come preferisci, e su Informazioni per sapere di più sull\'app e il team di sviluppo.]]> Capito, chiudi introduzione Chiudi introduzione Abilita accesso alla posizione Accesso alla posizione abilitato Accesso alla posizione non consentito dall\'utente Abilita notifiche Notifiche abilitate Backup e ripristino Importa / esporta dati Dati salvati Salva backup Importa i dati dal backup Backup importato Seleziona almeno un elemento da importare! Importa preferiti dal backup Importa preferenze dal backup -
\ No newline at end of file + + Nessuna app disponibile per mostrare la fermata! + Destinazione sconosciuta + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 99c8dc1..f32206d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,64 +1,65 @@ #ff9800 + #E77D13 #F57C00 #cc6600 #994d00 #2196F3 #2a65e8 #2060dd #8A4247 #2378e8 #0079f5 #2a968b #0067ff #2F59CC #CC5E43 #548017 #228b22 #0ABA34 #009688 #4DB6AC #80cbc4 #008175 #F5F5F5 #dddddd #f8f8f8 #757575 #444 #353535 #303030 #DE0908 #b30000 #dd441f #b30d0d #2060DD #FFFFFF #000000 #1c1c1c @color/blue_mid_2 @color/red_dark @color/blue_extra #FF039BE5 #FF01579B #FF40C4FF #FF00B0FF #66000000 #00000000 @color/orange_500 @color/blue_extraurbano @color/metro_red \ No newline at end of file diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index 38a9ea6..fca17aa 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -1,33 +1,39 @@ - layout_pref - pref_update_db_now - + layout_pref + pref_update_db_now + arrivals favorites map lines matofetcher fivetapifetcher gttjsonfetcher fivetscraper matofetcher gttjsonfetcher fivetscraper pref_positions_source mqtt gtfsrt + + + versatiles_c + osm_legacy + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e96c7c8..eccee43 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,346 +1,362 @@ BusTO Libre BusTO BusTO dev BusTO git You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code Yes No Next Previous Install Barcode Scanner? This application requires an app to scan the QR codes. Would you like to install Barcode Scanner now? Bus stop number Bus stop name Insert bus stop number Insert bus stop name %1$s towards %2$s %s (unknown destination) Verify your Internet connection! Seems that no bus stop have this name No arrivals found for this stop Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry Arrivals at: %1$s Choose the bus stop… Line Lines Urban lines Extra urban lines Tourist lines No lines found in this category No lines match the searched name Destination: Lines: %1$s Line: %1$s No timetable found No QR code found, try using another app to scan Unexpected internal error, cannot extract data from GTT/5T website Help About the app More about Contribute https://gitpull.it/w/librebusto/en/ Source code Licence11 Meet the author Bus stop is now in your favorites Bus stop removed from your favorites Added line to favorites Remove line from favorites Favorites Favorites Favorites Map No favorites? Arghh! Press on a bus stop star to populate this list! Delete Rename Rename the bus stop Reset About the app Tap the star to add the bus stop to the favourites\n\nHow to read timelines:\n   12:56* Real-time arrivals\n   12:56   Scheduled arrivals\n\nPull down to refresh the timetable \n Long press on Arrivals source to change the source of the arrival times GOT IT! Arrival times No arrivals found for lines: Welcome!

Thanks for using BusTO, an open source and independent app useful to move around Torino using a Free/Libre software.


Why use this app?

- You\'ll never be tracked
- You\'ll never see boring ads
- We\'ll always respect your privacy
- Moreover, it\'s lightweight!


Introductory tutorial

If you want to see the introduction again, use the button below:

]]>
News and Updates

On the Telegram channel, you can find information about the latest app updates

]]>
How does it work?

This app is able to do all the amazing things it does by pulling data from www.gtt.to.it, www.5t.torino.it or muoversiatorino.it "for personal use", along with open data from the AperTO (aperto.comune.torino.it) website.


The work of several people is behind this app, in particular:
- Fabio Mazza, current senior rockstar developer.
- Andrea Ugo, current junior rockstar developer.
- Silviu Chiriac, designer of the 2021 logo.
- Marco M, rockstar tester and bug hunter.
- Ludovico Pavesi, previous senior rockstar developer (asd).
- Valerio Bozzolan, maintainer and infrastructure (sponsor).
- Marco Gagino, contributor and first icon creator.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google for icons and support and design libraries.
- Other icons from Bootstrap, Feather, and Hero Icons.
- All the contributors, and the beta testers, too!


If you want more technical information or to contribute to development, use the buttons below! ]]>
Licenses

The app and the related source code are released by Valerio Bozzolan and the other authors under the terms of the GNU General Public License v3+). So everyone is allowed to use, to study, to improve and to share this app by any kind of means and for any purpose: under the conditions of maintaining this rights and of attributing the original work to Valerio Bozzolan.


Notes

This app has been developed with the hope to be useful to everyone, but comes without ANY warranty of any kind.

The data used by the app comes directly from GTT and other public agencies: if you find any errors, please take it up to them, not to us.

This translation is kindly provided by Riccardo Caniato, Marco Gagino and Fabio Mazza.

Now you can hack public transport, too! :)

]]>
Cannot add to favorites (storage full or corrupted database?)! View on a map Cannot find any application to show it in Cannot find the position of the stop ListFragment - BusTO it.reyboz.bustorino.preferences db_is_updating Nearby stops Nearby connections App version The number of stops to show in the recent stops is invalid Invalid value, put a valid number Finding location No stops nearby Minimum number of stops Preferences Settings Settings General Experimental features Maximum distance (meters) Recent stops General settings Database management Launch manual database update Allow access to location to show it on the map Allow access to location to show stops nearby Please enable location on the device Database update in progress… Updating the database Force database update Touch to update the app database now is arriving at at the stop %1$s - %2$s Show arrivals Show stops Join Telegram channel Show introduction Center on my location Follow me Enable or disable location Location enabled Location disabled Location is disabled on device Arrivals source: %1$s GTT App GTT Website 5T Torino website Muoversi a Torino app Undetermined Changing arrival times source… Long press to change the source of arrivals @string/source_mato @string/fivetapifetcher @string/gttjsonfetcher @string/fivetscraper Sources of arrival times Select which sources of arrival times to use Default Default channel for notifications Database operations Updates of the app database BusTO - live position service Live positions Showing activity related to the live positions service MaTO live bus positions service is running Downloading trips from MaTO server Asked for %1$s permission too many times Cannot use the map with the storage permission! storage The application has crashed because you encountered a bug. \nIf you want, you can help the developers by sending the crash report via email. \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app configuration/state. The application crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n Arrivals Map Favorites Open navigation drawer Close navigation drawer Experiments Buy us a coffee Map Search by stop Filter by name Launching database update Downloading data from MaTO server Capitalize directions @string/directions_capitalize_no_change @string/directions_capitalize_everything @string/directions_capitalize_first_letter Do not change arrivals directions Capitalize everything Capitalize only first letter KEEP CAPITALIZE_ALL CAPITALIZE_FIRST Section to show on startup Touch to change it Show arrivals touching on stop Enable experiments Long press the stop for options @string/nav_arrivals_text @string/nav_favorites_text @string/nav_map_text @string/lines Source of real time positions for buses and trams @string/positions_source_mato_descr @string/positions_source_gtfsrt_descr MaTO (updated more frequently, might be offline) GTFS RT (more stable, less frequently updated) + Style of the map + Versatiles (vector) + OSM legacy (raster, lighter) + + @string/map_style_versatiles + @string/map_style_legacy_raster + + MaTO (updated more frequently, might be offline) + GTFS RT (more stable, less frequently updated) Remove trips data (free up space) All GTFS trips have been removed from the database Show tutorial open source app for Turin public transport. This is an independent app, with no ads and no tracking whatsoever.]]> favorites by touching the star next to its name]]> blue)]]> Settings to customize the app behaviour, and in the About the app section if you want to know more about the app and the developers.]]> Notifications permission to show the information about background processing. Press the button below to grant it]]> Grant location permission Location permission granted Location permission has not been granted OK, close the tutorial Close the tutorial Enable notifications Notifications enabled Backup and restore Import/export preferences Data saved Backup to file Import data from backup Backup has been imported Check at least one item to import! Import favorites from backup Import preferences from backup + + Hello blank fragment + + No map app present to show the stop! + Direction is already shown + Loading destination… + Destination unknown
diff --git a/app/src/main/res/values/theme.xml b/app/src/main/res/values/theme.xml index 48c0dde..d6bf14f 100644 --- a/app/src/main/res/values/theme.xml +++ b/app/src/main/res/values/theme.xml @@ -1,28 +1,30 @@ \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 9f663b1..84c4504 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -1,116 +1,131 @@ + + + + + android:summary="%s" + + /> + diff --git a/build.gradle b/build.gradle index 1b79b23..0376513 100644 --- a/build.gradle +++ b/build.gradle @@ -1,49 +1,49 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { mavenCentral() maven { url 'https://maven.google.com' } google() maven { url 'https://jitpack.io' } } //kotlin - ext.kotlin_version = '1.9.0' + ext.kotlin_version = '1.9.21' ext.coroutines_version = "1.8.0" dependencies { - classpath 'com.android.tools.build:gradle:8.1.4' + classpath 'com.android.tools.build:gradle:8.5.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } ext { androidXTestVersion = "1.5.0" //multidex multidex_version = "2.0.1" //libraries versions fragment_version = "1.6.1" activity_version = "1.7.2" appcompat_version = "1.6.1" preference_version = "1.2.1" work_version = "2.9.0" acra_version = "5.11.3" lifecycle_version = "2.7.0" arch_version = "2.1.0" room_version = "2.5.2" } allprojects { repositories { maven { url 'https://maven.google.com' } google() mavenCentral() maven { url "https://jitpack.io" } } } diff --git a/gradle.properties b/gradle.properties index 31c5e97..9b72ad3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ android.defaults.buildfeatures.buildconfig=true android.nonFinalResIds=false android.nonTransitiveRClass=false -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true +org.gradle.jvmargs=-Xms200M -Xmx1G diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 758de96..d64cd49 100755 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7339845..b82aa23 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Sat Apr 24 16:03:07 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index cccdd3d..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -1,172 +1,249 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index e95643d..6689b85 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,84 +1,92 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega