huawei mobile services

This commit is contained in:
Thibault Deckers 2022-04-25 17:12:25 +09:00
parent c125cf9270
commit e29f1897a3
96 changed files with 1613 additions and 596 deletions

View file

@ -59,6 +59,9 @@ jobs:
cp build/app/outputs/bundle/playRelease/*.aab outputs cp build/app/outputs/bundle/playRelease/*.aab outputs
flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_2.10.5.sksl.json flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_2.10.5.sksl.json
cp build/app/outputs/apk/play/release/*.apk outputs cp build/app/outputs/apk/play/release/*.apk outputs
(cd scripts/; ./apply_flavor_huawei.sh)
flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_2.10.5.sksl.json
cp build/app/outputs/apk/huawei/release/*.apk outputs
(cd scripts/; ./apply_flavor_izzy.sh) (cd scripts/; ./apply_flavor_izzy.sh)
flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_2.10.5.sksl.json flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_2.10.5.sksl.json
cp build/app/outputs/apk/izzy/release/*.apk outputs cp build/app/outputs/apk/izzy/release/*.apk outputs

View file

@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
### Changed ### Changed
- upgraded Flutter to stable v2.10.5 - upgraded Flutter to stable v2.10.5
- `huawei` flavor (Petal Maps, no Crashlytics)
## <a id="v1.6.4"></a>[v1.6.4] - 2022-04-19 ## <a id="v1.6.4"></a>[v1.6.4] - 2022-04-19

View file

@ -0,0 +1,75 @@
{
"agcgw_all":{
"CN":"connect-drcn.dbankcloud.cn",
"CN_back":"connect-drcn.hispace.hicloud.com",
"DE":"connect-dre.dbankcloud.cn",
"DE_back":"connect-dre.hispace.hicloud.com",
"RU":"connect-drru.hispace.dbankcloud.ru",
"RU_back":"connect-drru.hispace.dbankcloud.ru",
"SG":"connect-dra.dbankcloud.cn",
"SG_back":"connect-dra.hispace.hicloud.com"
},
"client":{
"cp_id":"2640082000020010713",
"product_id":"99536292102197525",
"client_id":"874325707927340288",
"client_secret":"DCAFAE5C0440ABDBD6DDB2B6EBD7D9B0870C10FCA64759CCD63020D168803AB5",
"project_id":"99536292102197525",
"app_id":"106014023",
"api_key":"DAEDAEzScQA5ri36P2NEiVPSFrOJeYZ0DbEJZMGJrBadW+QudBr5BGHD3vO0tsL1VeBy0RPZefPic3hAWUijcBxCv0zRv0iBjQEptQ==",
"package_name":"deckers.thibault.aves"
},
"oauth_client":{
"client_id":"106014023",
"client_type":1
},
"app_info":{
"app_id":"106014023",
"package_name":"deckers.thibault.aves"
},
"configuration_version":"3.0",
"appInfos":[
{
"package_name":"deckers.thibault.aves.profile",
"client":{
"app_id":"106031461"
},
"app_info":{
"package_name":"deckers.thibault.aves.profile",
"app_id":"106031461"
},
"oauth_client":{
"client_type":1,
"client_id":"106031461"
}
},
{
"package_name":"deckers.thibault.aves.debug",
"client":{
"app_id":"106014297"
},
"app_info":{
"package_name":"deckers.thibault.aves.debug",
"app_id":"106014297"
},
"oauth_client":{
"client_type":1,
"client_id":"106014297"
}
},
{
"package_name":"deckers.thibault.aves",
"client":{
"app_id":"106014023"
},
"app_info":{
"package_name":"deckers.thibault.aves",
"app_id":"106014023"
},
"oauth_client":{
"client_type":1,
"client_id":"106014023"
}
}
]
}

View file

@ -1,5 +1,6 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'com.huawei.agconnect'
id 'kotlin-android' id 'kotlin-android'
id 'kotlin-kapt' id 'kotlin-kapt'
} }
@ -85,6 +86,14 @@ android {
ext.useNdkAbiFilters = true ext.useNdkAbiFilters = true
} }
huawei {
// Huawei AppGallery
dimension "store"
ext.useCrashlytics = false
// generate a universal APK without x86 native libs
ext.useNdkAbiFilters = true
}
izzy { izzy {
// IzzyOnDroid // IzzyOnDroid
// check offending libraries with `scanapk` // check offending libraries with `scanapk`
@ -153,6 +162,7 @@ dependencies {
// forked, built by JitPack, cf https://jitpack.io/p/deckerst/pixymeta-android // forked, built by JitPack, cf https://jitpack.io/p/deckerst/pixymeta-android
implementation 'com.github.deckerst:pixymeta-android:706bd73d6e' implementation 'com.github.deckerst:pixymeta-android:706bd73d6e'
implementation 'com.github.bumptech.glide:glide:4.13.1' implementation 'com.github.bumptech.glide:glide:4.13.1'
implementation 'com.huawei.agconnect:agconnect-core:1.5.2.300'
kapt 'androidx.annotation:annotation:1.3.0' kapt 'androidx.annotation:annotation:1.3.0'
kapt 'com.github.bumptech.glide:compiler:4.13.0' kapt 'com.github.bumptech.glide:compiler:4.13.0'

View file

@ -308,6 +308,8 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e) Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e) Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
} }
} }
result.success(metadataMap) result.success(metadataMap)

View file

@ -175,6 +175,8 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
Log.w(LOG_TAG, "failed to extract file from XMP", e) Log.w(LOG_TAG, "failed to extract file from XMP", e)
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to extract file from XMP", e) Log.w(LOG_TAG, "failed to extract file from XMP", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to extract file from XMP", e)
} }
} }
result.error("extractXmpDataProp-empty", "failed to extract file from XMP uri=$uri prop=$dataPropPath", null) result.error("extractXmpDataProp-empty", "failed to extract file from XMP uri=$uri prop=$dataPropPath", null)

View file

@ -331,6 +331,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} }
} }
@ -601,6 +603,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} }
} }
@ -727,6 +731,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} }
} }
@ -784,6 +790,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} }
} }
result.error("getGeoTiffInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null) result.error("getGeoTiffInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null)
@ -844,6 +852,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} }
} }
result.error("getPanoramaInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null) result.error("getPanoramaInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null)
@ -894,7 +904,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
result.error("getXmp-exception", "failed to read XMP for mimeType=$mimeType uri=$uri", e.message) result.error("getXmp-exception", "failed to read XMP for mimeType=$mimeType uri=$uri", e.message)
return return
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
result.error("getXmp-error", "failed to read XMP for mimeType=$mimeType uri=$uri", e.message) result.error("getXmp-noclass", "failed to read XMP for mimeType=$mimeType uri=$uri", e.message)
return
} catch (e: AssertionError) {
result.error("getXmp-assert", "failed to read XMP for mimeType=$mimeType uri=$uri", e.message)
return return
} }
} }
@ -1031,6 +1044,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} }
} }

View file

@ -167,6 +167,8 @@ object MultiPage {
Log.w(LOG_TAG, "failed to get motion photo offset from uri=$uri", e) Log.w(LOG_TAG, "failed to get motion photo offset from uri=$uri", e)
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to get motion photo offset from uri=$uri", e) Log.w(LOG_TAG, "failed to get motion photo offset from uri=$uri", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to get motion photo offset from uri=$uri", e)
} }
return null return null
} }

View file

@ -204,6 +204,8 @@ class SourceEntry {
// ignore // ignore
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
// ignore // ignore
} catch (e: AssertionError) {
// ignore
} }
} }

View file

@ -33,6 +33,8 @@ internal class ContentImageProvider : ImageProvider() {
Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e) Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e)
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e) Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e)
} }
val mimeType = extractorMimeType ?: sourceMimeType val mimeType = extractorMimeType ?: sourceMimeType

View file

@ -1,9 +1,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.6.20' ext.kotlin_version = '1.6.21'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url 'https://developer.huawei.com/repo/' }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.1.3' classpath 'com.android.tools.build:gradle:7.1.3'
@ -11,6 +12,8 @@ buildscript {
// GMS & Firebase Crashlytics are not actually used by all flavors // GMS & Firebase Crashlytics are not actually used by all flavors
classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
// HMS
classpath 'com.huawei.agconnect:agcp:1.5.2.300'
} }
} }
@ -18,6 +21,7 @@ allprojects {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven {url 'https://developer.huawei.com/repo/'}
} }
// gradle.projectsEvaluated { // gradle.projectsEvaluated {
// tasks.withType(JavaCompile) { // tasks.withType(JavaCompile) {

View file

@ -1,5 +1,13 @@
enum AppFlavor { play, izzy } enum AppFlavor { play, huawei, izzy }
extension ExtraAppFlavor on AppFlavor { extension ExtraAppFlavor on AppFlavor {
bool get canEnableErrorReporting => this == AppFlavor.play; bool get canEnableErrorReporting {
switch (this) {
case AppFlavor.play:
return true;
case AppFlavor.huawei:
case AppFlavor.izzy:
return false;
}
}
} }

View file

@ -124,6 +124,8 @@
"mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleNormal": "Google Maps",
"mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleHybrid": "Google Maps (Hybrid)",
"mapStyleGoogleTerrain": "Google Maps (Gelände)", "mapStyleGoogleTerrain": "Google Maps (Gelände)",
"mapStyleHuaweiNormal": "Petal Maps",
"mapStyleHuaweiTerrain": "Petal Maps (Gelände)",
"mapStyleOsmHot": "Humanitäres OSM", "mapStyleOsmHot": "Humanitäres OSM",
"mapStyleStamenToner": "Stamen Toner (SchwarzWeiß)", "mapStyleStamenToner": "Stamen Toner (SchwarzWeiß)",
"mapStyleStamenWatercolor": "Stamen Watercolor (Aquarell)", "mapStyleStamenWatercolor": "Stamen Watercolor (Aquarell)",

View file

@ -164,6 +164,8 @@
"mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleNormal": "Google Maps",
"mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleHybrid": "Google Maps (Hybrid)",
"mapStyleGoogleTerrain": "Google Maps (Terrain)", "mapStyleGoogleTerrain": "Google Maps (Terrain)",
"mapStyleHuaweiNormal": "Petal Maps",
"mapStyleHuaweiTerrain": "Petal Maps (Terrain)",
"mapStyleOsmHot": "Humanitarian OSM", "mapStyleOsmHot": "Humanitarian OSM",
"mapStyleStamenToner": "Stamen Toner", "mapStyleStamenToner": "Stamen Toner",
"mapStyleStamenWatercolor": "Stamen Watercolor", "mapStyleStamenWatercolor": "Stamen Watercolor",

View file

@ -118,9 +118,11 @@
"videoControlsPlayOutside": "Reproducir externamente", "videoControlsPlayOutside": "Reproducir externamente",
"videoControlsNone": "Ninguno", "videoControlsNone": "Ninguno",
"mapStyleGoogleNormal": "Mapas de Google", "mapStyleGoogleNormal": "Google Maps",
"mapStyleGoogleHybrid": "Mapas de Google (Híbrido)", "mapStyleGoogleHybrid": "Google Maps (Híbrido)",
"mapStyleGoogleTerrain": "Mapas de Google (Superficie)", "mapStyleGoogleTerrain": "Google Maps (Relieve)",
"mapStyleHuaweiNormal": "Petal Maps",
"mapStyleHuaweiTerrain": "Petal Maps (Relieve)",
"mapStyleOsmHot": "OSM Humanitario", "mapStyleOsmHot": "OSM Humanitario",
"mapStyleStamenToner": "Stamen Toner (Monocromático)", "mapStyleStamenToner": "Stamen Toner (Monocromático)",
"mapStyleStamenWatercolor": "Stamen Watercolor (Acuarela)", "mapStyleStamenWatercolor": "Stamen Watercolor (Acuarela)",

View file

@ -124,6 +124,8 @@
"mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleNormal": "Google Maps",
"mapStyleGoogleHybrid": "Google Maps (Satellite)", "mapStyleGoogleHybrid": "Google Maps (Satellite)",
"mapStyleGoogleTerrain": "Google Maps (Relief)", "mapStyleGoogleTerrain": "Google Maps (Relief)",
"mapStyleHuaweiNormal": "Petal Maps",
"mapStyleHuaweiTerrain": "Petal Maps (Relief)",
"mapStyleOsmHot": "OSM Humanitaire", "mapStyleOsmHot": "OSM Humanitaire",
"mapStyleStamenToner": "Stamen Toner (Monochrome)", "mapStyleStamenToner": "Stamen Toner (Monochrome)",
"mapStyleStamenWatercolor": "Stamen Watercolor (Aquarelle)", "mapStyleStamenWatercolor": "Stamen Watercolor (Aquarelle)",

View file

@ -124,6 +124,8 @@
"mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleNormal": "Google Maps",
"mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleHybrid": "Google Maps (Hybrid)",
"mapStyleGoogleTerrain": "Google Maps (Terrain)", "mapStyleGoogleTerrain": "Google Maps (Terrain)",
"mapStyleHuaweiNormal": "Petal Maps",
"mapStyleHuaweiTerrain": "Petal Maps (Terrain)",
"mapStyleOsmHot": "Humanitarian OSM", "mapStyleOsmHot": "Humanitarian OSM",
"mapStyleStamenToner": "Stamen Toner", "mapStyleStamenToner": "Stamen Toner",
"mapStyleStamenWatercolor": "Stamen Watercolor", "mapStyleStamenWatercolor": "Stamen Watercolor",

View file

@ -124,6 +124,8 @@
"mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleNormal": "Google Maps",
"mapStyleGoogleHybrid": "Google Maps (Ibrido)", "mapStyleGoogleHybrid": "Google Maps (Ibrido)",
"mapStyleGoogleTerrain": "Google Maps (Terreno)", "mapStyleGoogleTerrain": "Google Maps (Terreno)",
"mapStyleHuaweiNormal": "Petal Maps",
"mapStyleHuaweiTerrain": "Petal Maps (Terreno)",
"mapStyleOsmHot": "OSM umanitario", "mapStyleOsmHot": "OSM umanitario",
"mapStyleStamenToner": "Stamen Toner (Monocromatico)", "mapStyleStamenToner": "Stamen Toner (Monocromatico)",
"mapStyleStamenWatercolor": "Stamen Watercolor (Acquerello)", "mapStyleStamenWatercolor": "Stamen Watercolor (Acquerello)",

View file

@ -121,6 +121,8 @@
"mapStyleGoogleNormal": "Google マップ", "mapStyleGoogleNormal": "Google マップ",
"mapStyleGoogleHybrid": "Google マップ(ハイブリッド)", "mapStyleGoogleHybrid": "Google マップ(ハイブリッド)",
"mapStyleGoogleTerrain": "Google マップ(地形)", "mapStyleGoogleTerrain": "Google マップ(地形)",
"mapStyleHuaweiNormal": "Petal マップ",
"mapStyleHuaweiTerrain": "Petal マップ(地形)",
"mapStyleOsmHot": "Humanitarian OSM", "mapStyleOsmHot": "Humanitarian OSM",
"mapStyleStamenToner": "Stamen Toner", "mapStyleStamenToner": "Stamen Toner",
"mapStyleStamenWatercolor": "Stamen Watercolor", "mapStyleStamenWatercolor": "Stamen Watercolor",

View file

@ -121,9 +121,11 @@
"videoControlsPlayOutside": "다른 앱에서 열기", "videoControlsPlayOutside": "다른 앱에서 열기",
"videoControlsNone": "없음", "videoControlsNone": "없음",
"mapStyleGoogleNormal": "구글 지도", "mapStyleGoogleNormal": "Google 지도",
"mapStyleGoogleHybrid": "구글 지도 (위성)", "mapStyleGoogleHybrid": "Google 지도 (위성)",
"mapStyleGoogleTerrain": "구글 지도 (지형)", "mapStyleGoogleTerrain": "Google 지도 (지형)",
"mapStyleHuaweiNormal": "Petal 지도",
"mapStyleHuaweiTerrain": "Petal 지도 (지형)",
"mapStyleOsmHot": "Humanitarian OSM", "mapStyleOsmHot": "Humanitarian OSM",
"mapStyleStamenToner": "Stamen Toner (토너)", "mapStyleStamenToner": "Stamen Toner (토너)",
"mapStyleStamenWatercolor": "Stamen Watercolor (수채화)", "mapStyleStamenWatercolor": "Stamen Watercolor (수채화)",

View file

@ -124,6 +124,8 @@
"mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleNormal": "Google Maps",
"mapStyleGoogleHybrid": "Google Maps (Híbrido)", "mapStyleGoogleHybrid": "Google Maps (Híbrido)",
"mapStyleGoogleTerrain": "Google Maps (Terreno)", "mapStyleGoogleTerrain": "Google Maps (Terreno)",
"mapStyleHuaweiNormal": "Petal Maps",
"mapStyleHuaweiTerrain": "Petal Maps (Terreno)",
"mapStyleOsmHot": "OSM Humanitário", "mapStyleOsmHot": "OSM Humanitário",
"mapStyleStamenToner": "Stamen Toner (Monocromático)", "mapStyleStamenToner": "Stamen Toner (Monocromático)",
"mapStyleStamenWatercolor": "Stamen Watercolor (Aquarela)", "mapStyleStamenWatercolor": "Stamen Watercolor (Aquarela)",

View file

@ -124,6 +124,8 @@
"mapStyleGoogleNormal": "Google Карты", "mapStyleGoogleNormal": "Google Карты",
"mapStyleGoogleHybrid": "Google Карты (Гибридный)", "mapStyleGoogleHybrid": "Google Карты (Гибридный)",
"mapStyleGoogleTerrain": "Google Карты (Местность)", "mapStyleGoogleTerrain": "Google Карты (Местность)",
"mapStyleHuaweiNormal": "Petal Карты",
"mapStyleHuaweiTerrain": "Petal Карты (Местность)",
"mapStyleOsmHot": "Команда гуманитарной картопомощи", "mapStyleOsmHot": "Команда гуманитарной картопомощи",
"mapStyleStamenToner": "Stamen Toner", "mapStyleStamenToner": "Stamen Toner",
"mapStyleStamenWatercolor": "Stamen Watercolor", "mapStyleStamenWatercolor": "Stamen Watercolor",

View file

@ -121,9 +121,11 @@
"videoControlsPlayOutside": "用其他播放器打开", "videoControlsPlayOutside": "用其他播放器打开",
"videoControlsNone": "无", "videoControlsNone": "无",
"mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleNormal": "Google 地图",
"mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleHybrid": "Google 地图 (卫星图像)",
"mapStyleGoogleTerrain": "Google Maps (Terrain)", "mapStyleGoogleTerrain": "Google 地图 (地形)",
"mapStyleHuaweiNormal": "Petal 地图",
"mapStyleHuaweiTerrain": "Petal 地图 (地形)",
"mapStyleOsmHot": "Humanitarian OSM", "mapStyleOsmHot": "Humanitarian OSM",
"mapStyleStamenToner": "Stamen Toner", "mapStyleStamenToner": "Stamen Toner",
"mapStyleStamenWatercolor": "Stamen Watercolor", "mapStyleStamenWatercolor": "Stamen Watercolor",

6
lib/main_huawei.dart Normal file
View file

@ -0,0 +1,6 @@
import 'package:aves/app_flavor.dart';
import 'package:aves/main_common.dart';
void main() {
mainCommon(AppFlavor.huawei);
}

View file

@ -1,22 +1,20 @@
import 'package:aves/model/device.dart'; import 'package:aves/model/device.dart';
import 'package:aves_services_platform/aves_services_platform.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:google_api_availability/google_api_availability.dart';
abstract class AvesAvailability { abstract class AvesAvailability {
void onResume(); void onResume();
Future<bool> get isConnected; Future<bool> get isConnected;
Future<bool> get hasPlayServices;
Future<bool> get canLocatePlaces; Future<bool> get canLocatePlaces;
Future<bool> get canUseGoogleMaps; Future<bool> get canUseDeviceMaps;
} }
class LiveAvesAvailability implements AvesAvailability { class LiveAvesAvailability implements AvesAvailability {
bool? _isConnected, _hasPlayServices; bool? _isConnected;
LiveAvesAvailability() { LiveAvesAvailability() {
Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult); Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult);
@ -41,19 +39,14 @@ class LiveAvesAvailability implements AvesAvailability {
} }
} }
// local geocoding with `geocoder` seems to require Google Play Services
// what about devices with Huawei Mobile Services?
@override @override
Future<bool> get hasPlayServices async { Future<bool> get canLocatePlaces => Future.wait<bool>([
if (_hasPlayServices != null) return SynchronousFuture(_hasPlayServices!); isConnected,
final result = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability(); PlatformMobileServices().isServiceAvailable(),
_hasPlayServices = result == GooglePlayServicesAvailability.success; ]).then((results) => results.every((result) => result));
debugPrint('Device has Play Services=$_hasPlayServices');
return _hasPlayServices!;
}
// local geocoding with `geocoder` requires Play Services
@override
Future<bool> get canLocatePlaces => Future.wait<bool>([isConnected, hasPlayServices]).then((results) => results.every((result) => result));
@override @override
Future<bool> get canUseGoogleMaps async => device.canRenderGoogleMaps && await hasPlayServices; Future<bool> get canUseDeviceMaps async => device.canRenderGoogleMaps && await PlatformMobileServices().isServiceAvailable();
} }

View file

@ -4,8 +4,8 @@ import 'package:aves/model/settings/enums/coordinate_format.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/geo_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';

View file

@ -5,9 +5,8 @@ import 'dart:ui' as ui;
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/entry_images.dart'; import 'package:aves/model/entry_images.dart';
import 'package:aves/ref/geotiff.dart'; import 'package:aves/ref/geotiff.dart';
import 'package:aves/utils/geo_utils.dart';
import 'package:aves/utils/math_utils.dart'; import 'package:aves/utils/math_utils.dart';
import 'package:aves/widgets/common/map/tile.dart'; import 'package:aves_map/aves_map.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
@ -40,7 +39,7 @@ class GeoTiffInfo extends Equatable {
} }
} }
class MappedGeoTiff { class MappedGeoTiff with MapOverlay {
final AvesEntry entry; final AvesEntry entry;
late LatLng? Function(Point<int> pixel) pointToLatLng; late LatLng? Function(Point<int> pixel) pointToLatLng;
late Point<int>? Function(Point<double> smPoint) epsg3857ToPoint; late Point<int>? Function(Point<double> smPoint) epsg3857ToPoint;
@ -129,6 +128,7 @@ class MappedGeoTiff {
}; };
} }
@override
Future<MapTile?> getTile(int tx, int ty, int? zoomLevel) async { Future<MapTile?> getTile(int tx, int ty, int? zoomLevel) async {
zoomLevel ??= 0; zoomLevel ??= 0;
@ -217,15 +217,24 @@ class MappedGeoTiff {
); );
} }
@override
String get id => entry.uri;
@override
ImageProvider get imageProvider => entry.uriImage;
int get width => entry.width; int get width => entry.width;
int get height => entry.height; int get height => entry.height;
@override
bool get canOverlay => center != null; bool get canOverlay => center != null;
LatLng? get center => pointToLatLng(Point((width / 2).round(), (height / 2).round())); LatLng? get center => pointToLatLng(Point((width / 2).round(), (height / 2).round()));
@override
LatLng? get topLeft => pointToLatLng(const Point(0, 0)); LatLng? get topLeft => pointToLatLng(const Point(0, 0));
@override
LatLng? get bottomRight => pointToLatLng(Point(width, height)); LatLng? get bottomRight => pointToLatLng(Point(width, height));
} }

View file

@ -8,6 +8,7 @@ import 'package:aves/model/source/enums.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class SettingsDefaults { class SettingsDefaults {

View file

@ -1,5 +1,4 @@
import 'package:aves/l10n/l10n.dart'; import 'package:aves/l10n/l10n.dart';
import 'package:aves/utils/geo_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -33,14 +32,32 @@ extension ExtraCoordinateFormat on CoordinateFormat {
final locale = l10n.localeName; final locale = l10n.localeName;
final lat = latLng.latitude; final lat = latLng.latitude;
final lng = latLng.longitude; final lng = latLng.longitude;
final latSexa = GeoUtils.decimal2sexagesimal(lat, minuteSecondPadding, secondDecimals, locale); final latSexa = _decimal2sexagesimal(lat, minuteSecondPadding, secondDecimals, locale);
final lngSexa = GeoUtils.decimal2sexagesimal(lng, minuteSecondPadding, secondDecimals, locale); final lngSexa = _decimal2sexagesimal(lng, minuteSecondPadding, secondDecimals, locale);
return [ return [
l10n.coordinateDms(latSexa, lat < 0 ? l10n.coordinateDmsSouth : l10n.coordinateDmsNorth), l10n.coordinateDms(latSexa, lat < 0 ? l10n.coordinateDmsSouth : l10n.coordinateDmsNorth),
l10n.coordinateDms(lngSexa, lng < 0 ? l10n.coordinateDmsWest : l10n.coordinateDmsEast), l10n.coordinateDms(lngSexa, lng < 0 ? l10n.coordinateDmsWest : l10n.coordinateDmsEast),
]; ];
} }
static String _decimal2sexagesimal(
double degDecimal,
bool minuteSecondPadding,
int secondDecimals,
String locale,
) {
final degAbs = degDecimal.abs();
final deg = degAbs.toInt();
final minDecimal = (degAbs - deg) * 60;
final min = minDecimal.toInt();
final sec = (minDecimal - min) * 60;
var minText = NumberFormat('0' * (minuteSecondPadding ? 2 : 1), locale).format(min);
var secText = NumberFormat('${'0' * (minuteSecondPadding ? 2 : 1)}${secondDecimals > 0 ? '.${'0' * secondDecimals}' : ''}', locale).format(sec);
return '$deg° $minText $secText';
}
static List<String> _toDecimal(AppLocalizations l10n, LatLng latLng) { static List<String> _toDecimal(AppLocalizations l10n, LatLng latLng) {
final locale = l10n.localeName; final locale = l10n.localeName;
final formatter = NumberFormat('0.000000°', locale); final formatter = NumberFormat('0.000000°', locale);

View file

@ -12,9 +12,6 @@ enum CoordinateFormat { dms, decimal }
enum EntryBackground { black, white, checkered } enum EntryBackground { black, white, checkered }
// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/
enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor }
enum HomePageSetting { collection, albums } enum HomePageSetting { collection, albums }
enum KeepScreenOn { never, viewerOnly, always } enum KeepScreenOn { never, viewerOnly, always }

View file

@ -1,8 +1,7 @@
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraEntryMapStyle on EntryMapStyle { extension ExtraEntryMapStyle on EntryMapStyle {
String getName(BuildContext context) { String getName(BuildContext context) {
switch (this) { switch (this) {
@ -12,6 +11,10 @@ extension ExtraEntryMapStyle on EntryMapStyle {
return context.l10n.mapStyleGoogleHybrid; return context.l10n.mapStyleGoogleHybrid;
case EntryMapStyle.googleTerrain: case EntryMapStyle.googleTerrain:
return context.l10n.mapStyleGoogleTerrain; return context.l10n.mapStyleGoogleTerrain;
case EntryMapStyle.hmsNormal:
return context.l10n.mapStyleHuaweiNormal;
case EntryMapStyle.hmsTerrain:
return context.l10n.mapStyleHuaweiTerrain;
case EntryMapStyle.osmHot: case EntryMapStyle.osmHot:
return context.l10n.mapStyleOsmHot; return context.l10n.mapStyleOsmHot;
case EntryMapStyle.stamenToner: case EntryMapStyle.stamenToner:
@ -21,14 +24,27 @@ extension ExtraEntryMapStyle on EntryMapStyle {
} }
} }
bool get isGoogleMaps { bool get isHeavy {
switch (this) { switch (this) {
case EntryMapStyle.googleNormal: case EntryMapStyle.googleNormal:
case EntryMapStyle.googleHybrid: case EntryMapStyle.googleHybrid:
case EntryMapStyle.googleTerrain: case EntryMapStyle.googleTerrain:
case EntryMapStyle.hmsNormal:
case EntryMapStyle.hmsTerrain:
return true; return true;
default: default:
return false; return false;
} }
} }
bool get needDeviceService {
switch (this) {
case EntryMapStyle.osmHot:
case EntryMapStyle.stamenToner:
case EntryMapStyle.stamenWatercolor:
return false;
default:
return true;
}
}
} }

View file

@ -11,6 +11,8 @@ import 'package:aves/model/settings/enums/map_style.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/services/accessibility_service.dart'; import 'package:aves/services/accessibility_service.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves_map/aves_map.dart';
import 'package:aves_services_platform/aves_services_platform.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -161,11 +163,11 @@ class Settings extends ChangeNotifier {
enableOverlayBlurEffect = performanceClass >= 29; enableOverlayBlurEffect = performanceClass >= 29;
// availability // availability
final canUseGoogleMaps = await availability.canUseGoogleMaps; final isDeviceMapAvailable = await availability.canUseDeviceMaps;
if (canUseGoogleMaps) { if (isDeviceMapAvailable) {
infoMapStyle = EntryMapStyle.googleNormal; infoMapStyle = PlatformMobileServices().defaultMapStyle;
} else { } else {
final styles = EntryMapStyle.values.whereNot((v) => v.isGoogleMaps).toList(); final styles = EntryMapStyle.values.whereNot((v) => v.needDeviceService).toList();
infoMapStyle = styles[Random().nextInt(styles.length)]; infoMapStyle = styles[Random().nextInt(styles.length)];
} }

View file

@ -116,17 +116,6 @@ class Constants {
license: 'MIT', license: 'MIT',
sourceUrl: 'https://github.com/ajinasokan/flutter_displaymode', sourceUrl: 'https://github.com/ajinasokan/flutter_displaymode',
), ),
Dependency(
name: 'Google API Availability',
license: 'MIT',
sourceUrl: 'https://github.com/Baseflow/flutter-google-api-availability',
),
Dependency(
name: 'Google Maps for Flutter',
license: 'BSD 3-Clause',
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter/LICENSE',
sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter',
),
Dependency( Dependency(
name: 'Package Info Plus', name: 'Package Info Plus',
license: 'BSD 3-Clause', license: 'BSD 3-Clause',
@ -172,7 +161,39 @@ class Constants {
), ),
]; ];
static const List<Dependency> _googleMobileServices = [
Dependency(
name: 'Google API Availability',
license: 'MIT',
sourceUrl: 'https://github.com/Baseflow/flutter-google-api-availability',
),
Dependency(
name: 'Google Maps for Flutter',
license: 'BSD 3-Clause',
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter/LICENSE',
sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter',
),
];
static const List<Dependency> _huaweiMobileServices = [
Dependency(
name: 'Huawei Mobile Services (Availability, Map)',
license: 'Apache 2.0',
licenseUrl: 'https://github.com/HMS-Core/hms-flutter-plugin/blob/master/LICENCE',
sourceUrl: 'https://github.com/HMS-Core/hms-flutter-plugin',
),
];
static const List<Dependency> _flutterPluginsHuaweiOnly = [
..._huaweiMobileServices,
];
static const List<Dependency> _flutterPluginsIzzyOnly = [
..._googleMobileServices,
];
static const List<Dependency> _flutterPluginsPlayOnly = [ static const List<Dependency> _flutterPluginsPlayOnly = [
..._googleMobileServices,
Dependency( Dependency(
name: 'FlutterFire (Core, Crashlytics)', name: 'FlutterFire (Core, Crashlytics)',
license: 'BSD 3-Clause', license: 'BSD 3-Clause',
@ -182,6 +203,8 @@ class Constants {
static List<Dependency> flutterPlugins(AppFlavor flavor) => [ static List<Dependency> flutterPlugins(AppFlavor flavor) => [
..._flutterPluginsCommon, ..._flutterPluginsCommon,
if (flavor == AppFlavor.huawei) ..._flutterPluginsHuaweiOnly,
if (flavor == AppFlavor.izzy) ..._flutterPluginsIzzyOnly,
if (flavor == AppFlavor.play) ..._flutterPluginsPlayOnly, if (flavor == AppFlavor.play) ..._flutterPluginsPlayOnly,
]; ];

View file

@ -15,6 +15,7 @@ import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/identity/buttons.dart'; import 'package:aves/widgets/common/identity/buttons.dart';
import 'package:aves_services_platform/aves_services_platform.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -159,7 +160,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
final androidInfo = await DeviceInfoPlugin().androidInfo; final androidInfo = await DeviceInfoPlugin().androidInfo;
final installer = await androidAppService.getAppInstaller(); final installer = await androidAppService.getAppInstaller();
final hasPlayServices = await availability.hasPlayServices; final hasMobileServices = await PlatformMobileServices().isServiceAvailable();
final flavor = context.read<AppFlavor>().toString().split('.')[1]; final flavor = context.read<AppFlavor>().toString().split('.')[1];
return [ return [
'Aves version: ${packageInfo.version}-$flavor (Build ${packageInfo.buildNumber})', 'Aves version: ${packageInfo.version}-$flavor (Build ${packageInfo.buildNumber})',
@ -167,7 +168,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
'Android version: ${androidInfo.version.release} (SDK ${androidInfo.version.sdkInt})', 'Android version: ${androidInfo.version.release} (SDK ${androidInfo.version.sdkInt})',
'Android build: ${androidInfo.display}', 'Android build: ${androidInfo.display}',
'Device: ${androidInfo.manufacturer} ${androidInfo.model}', 'Device: ${androidInfo.manufacturer} ${androidInfo.model}',
'Google Play services: ${hasPlayServices ? 'ready' : 'not available'}', 'Mobile services: ${hasMobileServices ? 'ready' : 'not available'}',
'System locales: ${WidgetsBinding.instance!.window.locales.join(', ')}', 'System locales: ${WidgetsBinding.instance!.window.locales.join(', ')}',
'Aves locale: ${settings.locale ?? 'system'} -> ${settings.appliedLocale}', 'Aves locale: ${settings.locale ?? 'system'} -> ${settings.appliedLocale}',
'Installer: $installer', 'Installer: $installer',

View file

@ -30,6 +30,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/welcome_page.dart'; import 'package:aves/widgets/welcome_page.dart';
import 'package:aves_services_platform/aves_services_platform.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:fijkplayer/fijkplayer.dart'; import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -271,14 +272,14 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
FlutterError.onError = reportService.recordFlutterError; FlutterError.onError = reportService.recordFlutterError;
final now = DateTime.now(); final now = DateTime.now();
final hasPlayServices = await availability.hasPlayServices; final hasMobileServices = await PlatformMobileServices().isServiceAvailable();
await reportService.setCustomKeys({ await reportService.setCustomKeys({
'build_mode': kReleaseMode 'build_mode': kReleaseMode
? 'release' ? 'release'
: kProfileMode : kProfileMode
? 'profile' ? 'profile'
: 'debug', : 'debug',
'has_play_services': hasPlayServices, 'has_mobile_services': hasMobileServices,
'locales': WidgetsBinding.instance!.window.locales.join(', '), 'locales': WidgetsBinding.instance!.window.locales.join(', '),
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})', 'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
}); });

View file

@ -1,6 +1,6 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';

View file

@ -0,0 +1,55 @@
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:aves/widgets/common/fx/borders.dart';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class MapOverlayButton extends StatelessWidget {
final Widget icon;
final String tooltip;
final VoidCallback? onPressed;
const MapOverlayButton({
Key? key,
required this.icon,
required this.tooltip,
required this.onPressed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final blurred = settings.enableOverlayBlurEffect;
return Selector<MapThemeData, Animation<double>>(
selector: (context, v) => v.scale,
builder: (context, scale, child) => ScaleTransition(
scale: scale,
child: child,
),
child: BlurredOval(
enabled: blurred,
child: Material(
type: MaterialType.circle,
color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred),
child: Ink(
decoration: BoxDecoration(
border: AvesBorder.border(context),
shape: BoxShape.circle,
),
child: Selector<MapThemeData, VisualDensity?>(
selector: (context, v) => v.visualDensity,
builder: (context, visualDensity, child) => IconButton(
iconSize: 20,
visualDensity: visualDensity,
icon: icon,
onPressed: onPressed,
tooltip: tooltip,
),
),
),
),
),
);
}
}

View file

@ -0,0 +1,111 @@
import 'package:aves/model/filters/coordinate.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class OverlayCoordinateFilterChip extends StatefulWidget {
final ValueNotifier<ZoomedBounds> boundsNotifier;
final double padding;
const OverlayCoordinateFilterChip({
Key? key,
required this.boundsNotifier,
required this.padding,
}) : super(key: key);
@override
State<OverlayCoordinateFilterChip> createState() => _OverlayCoordinateFilterChipState();
}
class _OverlayCoordinateFilterChipState extends State<OverlayCoordinateFilterChip> {
final Debouncer _debouncer = Debouncer(delay: Durations.mapInfoDebounceDelay);
final ValueNotifier<ZoomedBounds?> _idleBoundsNotifier = ValueNotifier(null);
@override
void initState() {
super.initState();
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant OverlayCoordinateFilterChip oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget(OverlayCoordinateFilterChip widget) {
widget.boundsNotifier.addListener(_onBoundsChanged);
}
void _unregisterWidget(OverlayCoordinateFilterChip widget) {
widget.boundsNotifier.removeListener(_onBoundsChanged);
}
@override
Widget build(BuildContext context) {
final blurred = settings.enableOverlayBlurEffect;
final theme = Theme.of(context);
return Theme(
data: theme.copyWith(
scaffoldBackgroundColor: Themes.overlayBackgroundColor(brightness: theme.brightness, blurred: blurred),
),
child: Align(
alignment: Alignment.topLeft,
child: Selector<MapThemeData, Animation<double>>(
selector: (context, v) => v.scale,
builder: (context, scale, child) => SizeTransition(
sizeFactor: scale,
axisAlignment: 1,
child: FadeTransition(
opacity: scale,
child: child,
),
),
child: ValueListenableBuilder<ZoomedBounds?>(
valueListenable: _idleBoundsNotifier,
builder: (context, bounds, child) {
if (bounds == null) return const SizedBox();
final filter = CoordinateFilter(
bounds.sw,
bounds.ne,
// more stable format when bounds change
minuteSecondPadding: true,
);
return Padding(
padding: EdgeInsets.all(widget.padding),
child: BlurredRRect.all(
enabled: blurred,
borderRadius: AvesFilterChip.defaultRadius,
child: AvesFilterChip(
filter: filter,
useFilterColor: false,
maxWidth: double.infinity,
onTap: (filter) => FilterSelectedNotification(CoordinateFilter(bounds.sw, bounds.ne)).dispatch(context),
),
),
);
},
),
),
),
);
}
void _onBoundsChanged() {
_debouncer(() => _idleBoundsNotifier.value = widget.boundsNotifier.value);
}
}

View file

@ -1,31 +1,23 @@
import 'package:aves/model/filters/coordinate.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/enums/map_style.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/map/buttons/button.dart';
import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/map/buttons/coordinate_filter.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/map/compass.dart'; import 'package:aves/widgets/common/map/compass.dart';
import 'package:aves/widgets/common/map/theme.dart';
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves_map/aves_map.dart';
import 'package:aves_services_platform/aves_services_platform.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
typedef MapOpener = void Function(BuildContext context);
class MapButtonPanel extends StatelessWidget { class MapButtonPanel extends StatelessWidget {
final ValueNotifier<ZoomedBounds> boundsNotifier; final ValueNotifier<ZoomedBounds> boundsNotifier;
final Future<void> Function(double amount)? zoomBy; final Future<void> Function(double amount)? zoomBy;
final MapOpener? openMapPage; final void Function(BuildContext context)? openMapPage;
final VoidCallback? resetRotation; final VoidCallback? resetRotation;
const MapButtonPanel({ const MapButtonPanel({
@ -123,7 +115,7 @@ class MapButtonPanel extends StatelessWidget {
), ),
showCoordinateFilter showCoordinateFilter
? Expanded( ? Expanded(
child: _OverlayCoordinateFilterChip( child: OverlayCoordinateFilterChip(
boundsNotifier: boundsNotifier, boundsNotifier: boundsNotifier,
padding: padding, padding: padding,
), ),
@ -134,8 +126,11 @@ class MapButtonPanel extends StatelessWidget {
child: MapOverlayButton( child: MapOverlayButton(
icon: const Icon(AIcons.layers), icon: const Icon(AIcons.layers),
onPressed: () async { onPressed: () async {
final canUseGoogleMaps = await availability.canUseGoogleMaps; final canUseDeviceMaps = await availability.canUseDeviceMaps;
final availableStyles = EntryMapStyle.values.where((style) => !style.isGoogleMaps || canUseGoogleMaps); final availableStyles = [
if (canUseDeviceMaps) ...PlatformMobileServices().mapStyles,
...EntryMapStyle.values.where((v) => !v.needDeviceService),
];
final preferredStyle = settings.infoMapStyle; final preferredStyle = settings.infoMapStyle;
final initialStyle = availableStyles.contains(preferredStyle) ? preferredStyle : availableStyles.first; final initialStyle = availableStyles.contains(preferredStyle) ? preferredStyle : availableStyles.first;
await showSelectionDialog<EntryMapStyle>( await showSelectionDialog<EntryMapStyle>(
@ -181,151 +176,3 @@ class MapButtonPanel extends StatelessWidget {
); );
} }
} }
class MapOverlayButton extends StatelessWidget {
final Widget icon;
final String tooltip;
final VoidCallback? onPressed;
const MapOverlayButton({
Key? key,
required this.icon,
required this.tooltip,
required this.onPressed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final blurred = settings.enableOverlayBlurEffect;
return Selector<MapThemeData, Animation<double>>(
selector: (context, v) => v.scale,
builder: (context, scale, child) => ScaleTransition(
scale: scale,
child: child,
),
child: BlurredOval(
enabled: blurred,
child: Material(
type: MaterialType.circle,
color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred),
child: Ink(
decoration: BoxDecoration(
border: AvesBorder.border(context),
shape: BoxShape.circle,
),
child: Selector<MapThemeData, VisualDensity?>(
selector: (context, v) => v.visualDensity,
builder: (context, visualDensity, child) => IconButton(
iconSize: 20,
visualDensity: visualDensity,
icon: icon,
onPressed: onPressed,
tooltip: tooltip,
),
),
),
),
),
);
}
}
class _OverlayCoordinateFilterChip extends StatefulWidget {
final ValueNotifier<ZoomedBounds> boundsNotifier;
final double padding;
const _OverlayCoordinateFilterChip({
Key? key,
required this.boundsNotifier,
required this.padding,
}) : super(key: key);
@override
State<_OverlayCoordinateFilterChip> createState() => _OverlayCoordinateFilterChipState();
}
class _OverlayCoordinateFilterChipState extends State<_OverlayCoordinateFilterChip> {
final Debouncer _debouncer = Debouncer(delay: Durations.mapInfoDebounceDelay);
final ValueNotifier<ZoomedBounds?> _idleBoundsNotifier = ValueNotifier(null);
@override
void initState() {
super.initState();
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant _OverlayCoordinateFilterChip oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget(_OverlayCoordinateFilterChip widget) {
widget.boundsNotifier.addListener(_onBoundsChanged);
}
void _unregisterWidget(_OverlayCoordinateFilterChip widget) {
widget.boundsNotifier.removeListener(_onBoundsChanged);
}
@override
Widget build(BuildContext context) {
final blurred = settings.enableOverlayBlurEffect;
final theme = Theme.of(context);
return Theme(
data: theme.copyWith(
scaffoldBackgroundColor: Themes.overlayBackgroundColor(brightness: theme.brightness, blurred: blurred),
),
child: Align(
alignment: Alignment.topLeft,
child: Selector<MapThemeData, Animation<double>>(
selector: (context, v) => v.scale,
builder: (context, scale, child) => SizeTransition(
sizeFactor: scale,
axisAlignment: 1,
child: FadeTransition(
opacity: scale,
child: child,
),
),
child: ValueListenableBuilder<ZoomedBounds?>(
valueListenable: _idleBoundsNotifier,
builder: (context, bounds, child) {
if (bounds == null) return const SizedBox();
final filter = CoordinateFilter(
bounds.sw,
bounds.ne,
// more stable format when bounds change
minuteSecondPadding: true,
);
return Padding(
padding: EdgeInsets.all(widget.padding),
child: BlurredRRect.all(
enabled: blurred,
borderRadius: AvesFilterChip.defaultRadius,
child: AvesFilterChip(
filter: filter,
useFilterColor: false,
maxWidth: double.infinity,
onTap: (filter) => FilterSelectedNotification(CoordinateFilter(bounds.sw, bounds.ne)).dispatch(context),
),
),
);
},
),
),
),
);
}
void _onBoundsChanged() {
_debouncer(() => _idleBoundsNotifier.value = widget.boundsNotifier.value);
}
}

View file

@ -1,5 +1,5 @@
import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/borders.dart';
import 'package:aves/widgets/common/map/theme.dart'; import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View file

@ -2,8 +2,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/geotiff.dart'; import 'package:aves/model/entry_images.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/enums/map_style.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
@ -11,17 +10,13 @@ import 'package:aves/utils/change_notifier.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/utils/math_utils.dart'; import 'package:aves/utils/math_utils.dart';
import 'package:aves/widgets/common/map/attribution.dart'; import 'package:aves/widgets/common/map/attribution.dart';
import 'package:aves/widgets/common/map/buttons.dart'; import 'package:aves/widgets/common/map/buttons/panel.dart';
import 'package:aves/widgets/common/map/controller.dart';
import 'package:aves/widgets/common/map/decorator.dart'; import 'package:aves/widgets/common/map/decorator.dart';
import 'package:aves/widgets/common/map/geo_entry.dart';
import 'package:aves/widgets/common/map/google/map.dart';
import 'package:aves/widgets/common/map/leaflet/map.dart'; import 'package:aves/widgets/common/map/leaflet/map.dart';
import 'package:aves/widgets/common/map/marker.dart'; import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/common/map/theme.dart'; import 'package:aves_map/aves_map.dart';
import 'package:aves/widgets/common/map/zoomed_bounds.dart'; import 'package:aves_services_platform/aves_services_platform.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:fluster/fluster.dart'; import 'package:fluster/fluster.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
@ -35,14 +30,11 @@ class GeoMap extends StatefulWidget {
final ValueNotifier<bool> isAnimatingNotifier; final ValueNotifier<bool> isAnimatingNotifier;
final ValueNotifier<LatLng?>? dotLocationNotifier; final ValueNotifier<LatLng?>? dotLocationNotifier;
final ValueNotifier<double>? overlayOpacityNotifier; final ValueNotifier<double>? overlayOpacityNotifier;
final MappedGeoTiff? overlayEntry; final MapOverlay? overlayEntry;
final UserZoomChangeCallback? onUserZoomChange; final UserZoomChangeCallback? onUserZoomChange;
final void Function(LatLng location)? onMapTap; final MapTapCallback? onMapTap;
final MarkerTapCallback? onMarkerTap; final void Function(LatLng averageLocation, AvesEntry markerEntry, Set<AvesEntry> Function() getClusterEntries)? onMarkerTap;
final MapOpener? openMapPage; final void Function(BuildContext context)? openMapPage;
static const markerImageExtent = 48.0;
static const markerArrowSize = Size(8, 6);
const GeoMap({ const GeoMap({
Key? key, Key? key,
@ -71,14 +63,16 @@ class _GeoMapState extends State<GeoMap> {
// cf https://github.com/flutter/flutter/issues/28493 // cf https://github.com/flutter/flutter/issues/28493
// it is especially severe the first time, but still significant afterwards // it is especially severe the first time, but still significant afterwards
// so we prevent loading it while scrolling or animating // so we prevent loading it while scrolling or animating
bool _googleMapsLoaded = false; bool _heavyMapLoaded = false;
late final ValueNotifier<ZoomedBounds> _boundsNotifier; late final ValueNotifier<ZoomedBounds> _boundsNotifier;
Fluster<GeoEntry>? _defaultMarkerCluster; Fluster<GeoEntry<AvesEntry>>? _defaultMarkerCluster;
Fluster<GeoEntry>? _slowMarkerCluster; Fluster<GeoEntry<AvesEntry>>? _slowMarkerCluster;
final AChangeNotifier _clusterChangeNotifier = AChangeNotifier(); final AChangeNotifier _clusterChangeNotifier = AChangeNotifier();
List<AvesEntry> get entries => widget.entries; List<AvesEntry> get entries => widget.entries;
static final _platformMobileServices = PlatformMobileServices();
// cap initial zoom to avoid a zoom change // cap initial zoom to avoid a zoom change
// when toggling overlay on Google map initial state // when toggling overlay on Google map initial state
static const double minInitialZoom = 3; static const double minInitialZoom = 3;
@ -121,7 +115,7 @@ class _GeoMapState extends State<GeoMap> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
void _onMarkerTap(GeoEntry geoEntry) { void _onMarkerTap(GeoEntry<AvesEntry> geoEntry) {
final onTap = widget.onMarkerTap; final onTap = widget.onMarkerTap;
if (onTap == null) return; if (onTap == null) return;
@ -159,60 +153,74 @@ class _GeoMapState extends State<GeoMap> {
return Selector<Settings, EntryMapStyle>( return Selector<Settings, EntryMapStyle>(
selector: (context, s) => s.infoMapStyle, selector: (context, s) => s.infoMapStyle,
builder: (context, mapStyle, child) { builder: (context, mapStyle, child) {
final isGoogleMaps = mapStyle.isGoogleMaps; final isHeavy = mapStyle.isHeavy;
final progressive = !isGoogleMaps; Widget _buildMarkerWidget(MarkerKey<AvesEntry> key) => ImageMarker(
Widget _buildMarkerWidget(MarkerKey key) => ImageMarker(
key: key, key: key,
entry: key.entry,
count: key.count, count: key.count,
extent: GeoMap.markerImageExtent, buildThumbnailImage: (extent) => ThumbnailImage(
arrowSize: GeoMap.markerArrowSize, entry: key.entry,
progressive: progressive, extent: extent,
progressive: !isHeavy,
),
); );
bool _isMarkerImageReady(MarkerKey<AvesEntry> key) => key.entry.isThumbnailReady(extent: MapThemeData.markerImageExtent);
Widget child = isGoogleMaps Widget child = const SizedBox();
? EntryGoogleMap( switch (mapStyle) {
case EntryMapStyle.googleNormal:
case EntryMapStyle.googleHybrid:
case EntryMapStyle.googleTerrain:
case EntryMapStyle.hmsNormal:
case EntryMapStyle.hmsTerrain:
child = _platformMobileServices.buildMap<AvesEntry>(
controller: widget.controller, controller: widget.controller,
clusterListenable: _clusterChangeNotifier, clusterListenable: _clusterChangeNotifier,
boundsNotifier: _boundsNotifier, boundsNotifier: _boundsNotifier,
minZoom: 0,
maxZoom: 20,
style: mapStyle, style: mapStyle,
decoratorBuilder: _decorateMap,
buttonPanelBuilder: _buildButtonPanel,
markerClusterBuilder: _buildMarkerClusters, markerClusterBuilder: _buildMarkerClusters,
markerWidgetBuilder: _buildMarkerWidget, markerWidgetBuilder: _buildMarkerWidget,
markerImageReadyChecker: _isMarkerImageReady,
dotLocationNotifier: widget.dotLocationNotifier, dotLocationNotifier: widget.dotLocationNotifier,
overlayOpacityNotifier: widget.overlayOpacityNotifier, overlayOpacityNotifier: widget.overlayOpacityNotifier,
overlayEntry: widget.overlayEntry, overlayEntry: widget.overlayEntry,
onUserZoomChange: widget.onUserZoomChange, onUserZoomChange: widget.onUserZoomChange,
onMapTap: widget.onMapTap, onMapTap: widget.onMapTap,
onMarkerTap: _onMarkerTap, onMarkerTap: _onMarkerTap,
openMapPage: widget.openMapPage, );
) break;
: EntryLeafletMap( case EntryMapStyle.osmHot:
case EntryMapStyle.stamenToner:
case EntryMapStyle.stamenWatercolor:
child = EntryLeafletMap<AvesEntry>(
controller: widget.controller, controller: widget.controller,
clusterListenable: _clusterChangeNotifier, clusterListenable: _clusterChangeNotifier,
boundsNotifier: _boundsNotifier, boundsNotifier: _boundsNotifier,
minZoom: 2, minZoom: 2,
maxZoom: 16, maxZoom: 16,
style: mapStyle, style: mapStyle,
decoratorBuilder: _decorateMap,
buttonPanelBuilder: _buildButtonPanel,
markerClusterBuilder: _buildMarkerClusters, markerClusterBuilder: _buildMarkerClusters,
markerWidgetBuilder: _buildMarkerWidget, markerWidgetBuilder: _buildMarkerWidget,
dotLocationNotifier: widget.dotLocationNotifier, dotLocationNotifier: widget.dotLocationNotifier,
markerSize: Size( markerSize: Size(
GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2, MapThemeData.markerImageExtent + MapThemeData.markerOuterBorderWidth * 2,
GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2 + GeoMap.markerArrowSize.height, MapThemeData.markerImageExtent + MapThemeData.markerOuterBorderWidth * 2 + MapThemeData.markerArrowSize.height,
), ),
dotMarkerSize: const Size( dotMarkerSize: const Size(
DotMarker.diameter + ImageMarker.outerBorderWidth * 2, DotMarker.diameter + MapThemeData.markerOuterBorderWidth * 2,
DotMarker.diameter + ImageMarker.outerBorderWidth * 2, DotMarker.diameter + MapThemeData.markerOuterBorderWidth * 2,
), ),
overlayOpacityNotifier: widget.overlayOpacityNotifier, overlayOpacityNotifier: widget.overlayOpacityNotifier,
overlayEntry: widget.overlayEntry, overlayEntry: widget.overlayEntry,
onUserZoomChange: widget.onUserZoomChange, onUserZoomChange: widget.onUserZoomChange,
onMapTap: widget.onMapTap, onMapTap: widget.onMapTap,
onMarkerTap: _onMarkerTap, onMarkerTap: _onMarkerTap,
openMapPage: widget.openMapPage,
); );
break;
}
final mapHeight = context.select<MapThemeData, double?>((v) => v.mapHeight); final mapHeight = context.select<MapThemeData, double?>((v) => v.mapHeight);
child = Column( child = Column(
@ -239,8 +247,8 @@ class _GeoMapState extends State<GeoMap> {
child: ValueListenableBuilder<bool>( child: ValueListenableBuilder<bool>(
valueListenable: widget.isAnimatingNotifier, valueListenable: widget.isAnimatingNotifier,
builder: (context, animating, child) { builder: (context, animating, child) {
if (!animating && isGoogleMaps) { if (!animating && isHeavy) {
_googleMapsLoaded = true; _heavyMapLoaded = true;
} }
Widget replacement = Stack( Widget replacement = Stack(
children: [ children: [
@ -258,7 +266,7 @@ class _GeoMapState extends State<GeoMap> {
); );
} }
return Visibility( return Visibility(
visible: !isGoogleMaps || _googleMapsLoaded, visible: !isHeavy || _heavyMapLoaded,
replacement: replacement, replacement: replacement,
child: child!, child: child!,
); );
@ -303,10 +311,10 @@ class _GeoMapState extends State<GeoMap> {
_clusterChangeNotifier.notify(); _clusterChangeNotifier.notify();
} }
Fluster<GeoEntry> _buildFluster({int nodeSize = 64}) { Fluster<GeoEntry<AvesEntry>> _buildFluster({int nodeSize = 64}) {
final markers = entries.map((entry) { final markers = entries.map((entry) {
final latLng = entry.latLng!; final latLng = entry.latLng!;
return GeoEntry( return GeoEntry<AvesEntry>(
entry: entry, entry: entry,
latitude: latLng.latitude, latitude: latLng.latitude,
longitude: latLng.longitude, longitude: latLng.longitude,
@ -314,7 +322,7 @@ class _GeoMapState extends State<GeoMap> {
); );
}).toList(); }).toList();
return Fluster<GeoEntry>( return Fluster<GeoEntry<AvesEntry>>(
// we keep clustering on the whole range of zooms (including the maximum) // we keep clustering on the whole range of zooms (including the maximum)
// to avoid collocated entries overlapping // to avoid collocated entries overlapping
minZoom: 0, minZoom: 0,
@ -329,11 +337,11 @@ class _GeoMapState extends State<GeoMap> {
// use lambda instead of tear-off because of runtime exception when using // use lambda instead of tear-off because of runtime exception when using
// `T Function(BaseCluster, double, double)` for `T Function(BaseCluster?, double?, double?)` // `T Function(BaseCluster, double, double)` for `T Function(BaseCluster?, double?, double?)`
// ignore: unnecessary_lambdas // ignore: unnecessary_lambdas
createCluster: (base, lng, lat) => GeoEntry.createCluster(base, lng, lat), createCluster: (base, lng, lat) => GeoEntry<AvesEntry>.createCluster(base, lng, lat),
); );
} }
Map<MarkerKey, GeoEntry> _buildMarkerClusters() { Map<MarkerKey<AvesEntry>, GeoEntry<AvesEntry>> _buildMarkerClusters() {
final bounds = _boundsNotifier.value; final bounds = _boundsNotifier.value;
final geoEntries = _defaultMarkerCluster?.clusters(bounds.boundingBox, bounds.zoom.round()) ?? []; final geoEntries = _defaultMarkerCluster?.clusters(bounds.boundingBox, bounds.zoom.round()) ?? [];
return Map.fromEntries(geoEntries.map((v) { return Map.fromEntries(geoEntries.map((v) {
@ -345,20 +353,17 @@ class _GeoMapState extends State<GeoMap> {
return MapEntry(MarkerKey(v.entry!, null), v); return MapEntry(MarkerKey(v.entry!, null), v);
})); }));
} }
Widget _decorateMap(BuildContext context, Widget? child) => MapDecorator(child: child);
Widget _buildButtonPanel(
Future<void> Function(double amount) zoomBy,
VoidCallback resetRotation,
) =>
MapButtonPanel(
boundsNotifier: _boundsNotifier,
zoomBy: zoomBy,
openMapPage: widget.openMapPage,
resetRotation: resetRotation,
);
} }
@immutable
class MarkerKey extends LocalKey with EquatableMixin {
final AvesEntry entry;
final int? count;
@override
List<Object?> get props => [entry, count];
const MarkerKey(this.entry, this.count);
}
typedef MarkerClusterBuilder = Map<MarkerKey, GeoEntry> Function();
typedef MarkerWidgetBuilder = Widget Function(MarkerKey key);
typedef UserZoomChangeCallback = void Function(double zoom);
typedef MarkerTapCallback = void Function(LatLng averageLocation, AvesEntry markerEntry, Set<AvesEntry> Function() getClusterEntries);

View file

@ -1,17 +0,0 @@
import 'package:aves/model/geotiff.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class GeoTiffTileProvider extends TileProvider {
MappedGeoTiff overlayEntry;
GeoTiffTileProvider(this.overlayEntry);
@override
Future<Tile> getTile(int x, int y, int? zoom) async {
final tile = await overlayEntry.getTile(x, y, zoom);
if (tile != null) {
return Tile(tile.width, tile.height, tile.data);
}
return TileProvider.noTile;
}
}

View file

@ -1,4 +1,4 @@
import 'package:aves/widgets/common/map/latlng_utils.dart'; import 'package:aves/widgets/common/map/leaflet/latlng_utils.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';

View file

@ -1,43 +1,34 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/entry_images.dart';
import 'package:aves/model/geotiff.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/utils/debouncer.dart'; import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/map/buttons.dart'; import 'package:aves/widgets/common/map/leaflet/latlng_tween.dart';
import 'package:aves/widgets/common/map/controller.dart';
import 'package:aves/widgets/common/map/decorator.dart';
import 'package:aves/widgets/common/map/geo_entry.dart';
import 'package:aves/widgets/common/map/geo_map.dart';
import 'package:aves/widgets/common/map/latlng_tween.dart';
import 'package:aves/widgets/common/map/leaflet/scale_layer.dart'; import 'package:aves/widgets/common/map/leaflet/scale_layer.dart';
import 'package:aves/widgets/common/map/leaflet/tile_layers.dart'; import 'package:aves/widgets/common/map/leaflet/tile_layers.dart';
import 'package:aves/widgets/common/map/marker.dart'; import 'package:aves_map/aves_map.dart';
import 'package:aves/widgets/common/map/theme.dart';
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class EntryLeafletMap extends StatefulWidget { class EntryLeafletMap<T> extends StatefulWidget {
final AvesMapController? controller; final AvesMapController? controller;
final Listenable clusterListenable; final Listenable clusterListenable;
final ValueNotifier<ZoomedBounds> boundsNotifier; final ValueNotifier<ZoomedBounds> boundsNotifier;
final double minZoom, maxZoom; final double minZoom, maxZoom;
final EntryMapStyle style; final EntryMapStyle style;
final MarkerClusterBuilder markerClusterBuilder; final TransitionBuilder decoratorBuilder;
final MarkerWidgetBuilder markerWidgetBuilder; final ButtonPanelBuilder buttonPanelBuilder;
final MarkerClusterBuilder<T> markerClusterBuilder;
final MarkerWidgetBuilder<T> markerWidgetBuilder;
final ValueNotifier<LatLng?>? dotLocationNotifier; final ValueNotifier<LatLng?>? dotLocationNotifier;
final Size markerSize, dotMarkerSize; final Size markerSize, dotMarkerSize;
final ValueNotifier<double>? overlayOpacityNotifier; final ValueNotifier<double>? overlayOpacityNotifier;
final MappedGeoTiff? overlayEntry; final MapOverlay? overlayEntry;
final UserZoomChangeCallback? onUserZoomChange; final UserZoomChangeCallback? onUserZoomChange;
final void Function(LatLng location)? onMapTap; final MapTapCallback? onMapTap;
final void Function(GeoEntry geoEntry)? onMarkerTap; final MarkerTapCallback<T>? onMarkerTap;
final MapOpener? openMapPage;
const EntryLeafletMap({ const EntryLeafletMap({
Key? key, Key? key,
@ -47,6 +38,8 @@ class EntryLeafletMap extends StatefulWidget {
this.minZoom = 0, this.minZoom = 0,
this.maxZoom = 22, this.maxZoom = 22,
required this.style, required this.style,
required this.decoratorBuilder,
required this.buttonPanelBuilder,
required this.markerClusterBuilder, required this.markerClusterBuilder,
required this.markerWidgetBuilder, required this.markerWidgetBuilder,
required this.dotLocationNotifier, required this.dotLocationNotifier,
@ -57,17 +50,16 @@ class EntryLeafletMap extends StatefulWidget {
this.onUserZoomChange, this.onUserZoomChange,
this.onMapTap, this.onMapTap,
this.onMarkerTap, this.onMarkerTap,
this.openMapPage,
}) : super(key: key); }) : super(key: key);
@override @override
State<StatefulWidget> createState() => _EntryLeafletMapState(); State<StatefulWidget> createState() => _EntryLeafletMapState<T>();
} }
class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderStateMixin { class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProviderStateMixin {
final MapController _leafletMapController = MapController(); final MapController _leafletMapController = MapController();
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
Map<MarkerKey, GeoEntry> _geoEntryByMarkerKey = {}; Map<MarkerKey<T>, GeoEntry<T>> _geoEntryByMarkerKey = {};
final Debouncer _debouncer = Debouncer(delay: Durations.mapIdleDebounceDelay); final Debouncer _debouncer = Debouncer(delay: Durations.mapIdleDebounceDelay);
ValueNotifier<ZoomedBounds> get boundsNotifier => widget.boundsNotifier; ValueNotifier<ZoomedBounds> get boundsNotifier => widget.boundsNotifier;
@ -85,7 +77,7 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
} }
@override @override
void didUpdateWidget(covariant EntryLeafletMap oldWidget) { void didUpdateWidget(covariant EntryLeafletMap<T> oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget); _unregisterWidget(oldWidget);
_registerWidget(widget); _registerWidget(widget);
@ -97,7 +89,7 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
super.dispose(); super.dispose();
} }
void _registerWidget(EntryLeafletMap widget) { void _registerWidget(EntryLeafletMap<T> widget) {
final avesMapController = widget.controller; final avesMapController = widget.controller;
if (avesMapController != null) { if (avesMapController != null) {
_subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(event.latLng))); _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(event.latLng)));
@ -107,7 +99,7 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
widget.boundsNotifier.addListener(_onBoundsChange); widget.boundsNotifier.addListener(_onBoundsChange);
} }
void _unregisterWidget(EntryLeafletMap widget) { void _unregisterWidget(EntryLeafletMap<T> widget) {
widget.clusterListenable.removeListener(_updateMarkers); widget.clusterListenable.removeListener(_updateMarkers);
widget.boundsNotifier.removeListener(_onBoundsChange); widget.boundsNotifier.removeListener(_onBoundsChange);
_subscriptions _subscriptions
@ -119,15 +111,8 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
children: [ children: [
MapDecorator( widget.decoratorBuilder(context, _buildMap()),
child: _buildMap(), widget.buttonPanelBuilder(_zoomBy, _resetRotation),
),
MapButtonPanel(
boundsNotifier: boundsNotifier,
zoomBy: _zoomBy,
openMapPage: widget.openMapPage,
resetRotation: _resetRotation,
),
], ],
); );
} }
@ -239,7 +224,7 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
overlayImages: [ overlayImages: [
OverlayImage( OverlayImage(
bounds: LatLngBounds(corner1, corner2), bounds: LatLngBounds(corner1, corner2),
imageProvider: overlayEntry.entry.uriImage, imageProvider: overlayEntry.imageProvider,
opacity: overlayOpacity, opacity: overlayOpacity,
), ),
], ],

View file

@ -1,9 +1,8 @@
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
enum MapNavigationButton { back, map }
class MapTheme extends StatelessWidget { class MapTheme extends StatelessWidget {
final bool interactive, showCoordinateFilter; final bool interactive, showCoordinateFilter;
final MapNavigationButton navigationButton; final MapNavigationButton navigationButton;
@ -40,20 +39,3 @@ class MapTheme extends StatelessWidget {
); );
} }
} }
class MapThemeData {
final bool interactive, showCoordinateFilter;
final MapNavigationButton navigationButton;
final Animation<double> scale;
final VisualDensity? visualDensity;
final double? mapHeight;
const MapThemeData({
required this.interactive,
required this.showCoordinateFilter,
required this.navigationButton,
required this.scale,
required this.visualDensity,
required this.mapHeight,
});
}

View file

@ -12,12 +12,10 @@ import 'package:aves/utils/constants.dart';
import 'package:aves/utils/debouncer.dart'; import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/buttons.dart'; import 'package:aves/widgets/common/identity/buttons.dart';
import 'package:aves/widgets/common/map/controller.dart';
import 'package:aves/widgets/common/map/geo_map.dart'; import 'package:aves/widgets/common/map/geo_map.dart';
import 'package:aves/widgets/common/map/marker.dart'; import 'package:aves/widgets/common/providers/map_theme_provider.dart';
import 'package:aves/widgets/common/map/theme.dart';
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves_map/aves_map.dart';
import 'package:decorated_icon/decorated_icon.dart'; import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@ -82,7 +80,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
void initState() { void initState() {
super.initState(); super.initState();
if (settings.infoMapStyle.isGoogleMaps) { if (settings.infoMapStyle.isHeavy) {
_isPageAnimatingNotifier = ValueNotifier(true); _isPageAnimatingNotifier = ValueNotifier(true);
Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) { Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) {
if (!mounted) return; if (!mounted) return;

View file

@ -7,7 +7,7 @@ import 'package:aves/theme/format.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/map/marker.dart'; import 'package:aves_map/aves_map.dart';
import 'package:decorated_icon/decorated_icon.dart'; import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View file

@ -6,7 +6,6 @@ import 'package:aves/model/filters/coordinate.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/geotiff.dart'; import 'package:aves/model/geotiff.dart';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/enums/map_style.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
@ -17,16 +16,15 @@ import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/common/map/controller.dart';
import 'package:aves/widgets/common/map/geo_map.dart'; import 'package:aves/widgets/common/map/geo_map.dart';
import 'package:aves/widgets/common/map/theme.dart';
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/common/providers/map_theme_provider.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/thumbnail/scroller.dart'; import 'package:aves/widgets/common/thumbnail/scroller.dart';
import 'package:aves/widgets/map/map_info_row.dart'; import 'package:aves/widgets/map/map_info_row.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves_map/aves_map.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@ -110,7 +108,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
void initState() { void initState() {
super.initState(); super.initState();
if (settings.infoMapStyle.isGoogleMaps) { if (settings.infoMapStyle.isHeavy) {
_isPageAnimatingNotifier.value = true; _isPageAnimatingNotifier.value = true;
Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) { Future.delayed(Durations.pageTransitionAnimation * timeDilation).then((_) {
if (!mounted) return; if (!mounted) return;
@ -176,8 +174,8 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
selector: (context, s) => s.infoMapStyle, selector: (context, s) => s.infoMapStyle,
builder: (context, mapStyle, child) { builder: (context, mapStyle, child) {
late Widget scroller; late Widget scroller;
if (mapStyle.isGoogleMaps) { if (mapStyle.isHeavy) {
// the Google map widget is too heavy for a smooth resizing animation // the map widget is too heavy for a smooth resizing animation
// so we just toggle visibility when overlay animation is done // so we just toggle visibility when overlay animation is done
scroller = ValueListenableBuilder<double>( scroller = ValueListenableBuilder<double>(
valueListenable: _overlayAnimationController, valueListenable: _overlayAnimationController,
@ -190,7 +188,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
child: child, child: child,
); );
} else { } else {
// the Leaflet map widget is light enough for a smooth resizing animation // the map widget is light enough for a smooth resizing animation
scroller = FadeTransition( scroller = FadeTransition(
opacity: _scrollerSize, opacity: _scrollerSize,
child: SizeTransition( child: SizeTransition(

View file

@ -7,11 +7,11 @@ import 'package:aves/services/common/services.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/map/controller.dart';
import 'package:aves/widgets/common/map/geo_map.dart'; import 'package:aves/widgets/common/map/geo_map.dart';
import 'package:aves/widgets/common/map/theme.dart'; import 'package:aves/widgets/common/providers/map_theme_provider.dart';
import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/map/map_page.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class LocationSection extends StatefulWidget { class LocationSection extends StatefulWidget {

29
plugins/aves_map/.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/

View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 5464c5bac742001448fe4fc0597be939379f88ea
channel: stable
project_type: package

View file

@ -0,0 +1 @@
include: ../../analysis_options.yaml

View file

@ -0,0 +1,15 @@
library aves_map;
export 'src/controller.dart';
export 'src/geo_entry.dart';
export 'src/geo_utils.dart';
export 'src/interface.dart';
export 'src/marker/dot.dart';
export 'src/marker/generator.dart';
export 'src/marker/image.dart';
export 'src/marker/key.dart';
export 'src/overlay/overlay.dart';
export 'src/overlay/tile.dart';
export 'src/style.dart';
export 'src/theme.dart';
export 'src/zoomed_bounds.dart';

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/widgets/common/map/zoomed_bounds.dart'; import 'package:aves_map/src/zoomed_bounds.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
class AvesMapController { class AvesMapController {

View file

@ -1,9 +1,8 @@
import 'package:aves/model/entry.dart';
import 'package:fluster/fluster.dart'; import 'package:fluster/fluster.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
class GeoEntry extends Clusterable { class GeoEntry<T> extends Clusterable {
AvesEntry? entry; T? entry;
GeoEntry({ GeoEntry({
this.entry, this.entry,

View file

@ -1,27 +1,8 @@
import 'dart:math'; import 'dart:math';
import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
class GeoUtils { class GeoUtils {
static String decimal2sexagesimal(
double degDecimal,
bool minuteSecondPadding,
int secondDecimals,
String locale,
) {
final degAbs = degDecimal.abs();
final deg = degAbs.toInt();
final minDecimal = (degAbs - deg) * 60;
final min = minDecimal.toInt();
final sec = (minDecimal - min) * 60;
var minText = NumberFormat('0' * (minuteSecondPadding ? 2 : 1), locale).format(min);
var secText = NumberFormat('${'0' * (minuteSecondPadding ? 2 : 1)}${secondDecimals > 0 ? '.${'0' * secondDecimals}' : ''}', locale).format(sec);
return '$deg° $minText $secText';
}
static LatLng getLatLngCenter(List<LatLng> points) { static LatLng getLatLngCenter(List<LatLng> points) {
double x = 0; double x = 0;
double y = 0; double y = 0;

View file

@ -0,0 +1,12 @@
import 'package:aves_map/src/geo_entry.dart';
import 'package:aves_map/src/marker/key.dart';
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
typedef ButtonPanelBuilder = Widget Function(Future<void> Function(double amount) zoomBy, VoidCallback resetRotation);
typedef MarkerClusterBuilder<T> = Map<MarkerKey<T>, GeoEntry<T>> Function();
typedef MarkerWidgetBuilder<T> = Widget Function(MarkerKey<T> key);
typedef MarkerImageReadyChecker<T> = bool Function(MarkerKey<T> key);
typedef UserZoomChangeCallback = void Function(double zoom);
typedef MapTapCallback = void Function(LatLng location);
typedef MarkerTapCallback<T> = void Function(GeoEntry<T> geoEntry);

View file

@ -0,0 +1,54 @@
import 'package:aves_map/src/theme.dart';
import 'package:flutter/material.dart';
class DotMarker extends StatelessWidget {
const DotMarker({Key? key}) : super(key: key);
static const double diameter = 16;
static const double outerBorderRadiusDim = diameter;
static const double outerBorderWidth = MapThemeData.markerOuterBorderWidth;
static const double innerBorderWidth = MapThemeData.markerInnerBorderWidth;
static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim));
static const innerRadius = Radius.circular(outerBorderRadiusDim - outerBorderWidth);
static const innerBorderRadius = BorderRadius.all(innerRadius);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final outerBorderColor = MapThemeData.markerThemedOuterBorderColor(isDark);
final innerBorderColor = MapThemeData.markerThemedInnerBorderColor(isDark);
final outerDecoration = BoxDecoration(
border: Border.fromBorderSide(BorderSide(
color: outerBorderColor,
width: outerBorderWidth,
)),
borderRadius: outerBorderRadius,
);
final innerDecoration = BoxDecoration(
border: Border.fromBorderSide(BorderSide(
color: innerBorderColor,
width: innerBorderWidth,
)),
borderRadius: innerBorderRadius,
);
return Container(
decoration: outerDecoration,
child: DecoratedBox(
decoration: innerDecoration,
position: DecorationPosition.foreground,
child: ClipRRect(
borderRadius: innerBorderRadius,
child: Container(
width: diameter,
height: diameter,
color: theme.colorScheme.secondary,
),
),
),
);
}
}

View file

@ -1,45 +1,29 @@
import 'package:aves/model/entry.dart'; import 'package:aves_map/src/theme.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:custom_rounded_rectangle_border/custom_rounded_rectangle_border.dart'; import 'package:custom_rounded_rectangle_border/custom_rounded_rectangle_border.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ImageMarker extends StatelessWidget { class ImageMarker extends StatelessWidget {
final AvesEntry? entry;
final int? count; final int? count;
final double extent; final Widget Function(double extent) buildThumbnailImage;
final Size arrowSize;
final bool progressive;
static const double outerBorderRadiusDim = 8; static const double outerBorderRadiusDim = 8;
static const double outerBorderWidth = 1.5; static const outerBorderWidth = MapThemeData.markerOuterBorderWidth;
static const double innerBorderWidth = 2; static const innerBorderWidth = MapThemeData.markerInnerBorderWidth;
static const extent = MapThemeData.markerImageExtent;
static const arrowSize = MapThemeData.markerArrowSize;
static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim)); static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim));
static const innerRadius = Radius.circular(outerBorderRadiusDim - outerBorderWidth); static const innerRadius = Radius.circular(outerBorderRadiusDim - outerBorderWidth);
static const innerBorderRadius = BorderRadius.all(innerRadius); static const innerBorderRadius = BorderRadius.all(innerRadius);
static Color themedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26;
static Color themedInnerBorderColor(bool isDark) => isDark ? const Color(0xFF212121) : Colors.white;
const ImageMarker({ const ImageMarker({
Key? key, Key? key,
required this.entry,
required this.count, required this.count,
required this.extent, required this.buildThumbnailImage,
required this.arrowSize,
required this.progressive,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child = entry != null Widget child = buildThumbnailImage(extent);
? ThumbnailImage(
entry: entry!,
extent: extent,
progressive: progressive,
)
: const SizedBox();
// need to be sized for the Google map marker generator // need to be sized for the Google map marker generator
child = SizedBox( child = SizedBox(
@ -50,8 +34,8 @@ class ImageMarker extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark; final isDark = theme.brightness == Brightness.dark;
final outerBorderColor = themedOuterBorderColor(isDark); final outerBorderColor = MapThemeData.markerThemedOuterBorderColor(isDark);
final innerBorderColor = themedInnerBorderColor(isDark); final innerBorderColor = MapThemeData.markerThemedInnerBorderColor(isDark);
final outerDecoration = BoxDecoration( final outerDecoration = BoxDecoration(
border: Border.fromBorderSide(BorderSide( border: Border.fromBorderSide(BorderSide(
@ -90,7 +74,7 @@ class ImageMarker extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 2), padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 2),
decoration: ShapeDecoration( decoration: ShapeDecoration(
color: theme.colorScheme.secondary, color: theme.colorScheme.secondary,
shape: context.isRtl shape: Directionality.of(context) == TextDirection.rtl
? CustomRoundedRectangleBorder( ? CustomRoundedRectangleBorder(
leftSide: borderSide, leftSide: borderSide,
rightSide: borderSide, rightSide: borderSide,
@ -188,53 +172,3 @@ class _MarkerArrowPainter extends CustomPainter {
@override @override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false; bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
} }
class DotMarker extends StatelessWidget {
const DotMarker({Key? key}) : super(key: key);
static const double diameter = 16;
static const double outerBorderRadiusDim = diameter;
static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim));
static const innerRadius = Radius.circular(outerBorderRadiusDim - ImageMarker.outerBorderWidth);
static const innerBorderRadius = BorderRadius.all(innerRadius);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final outerBorderColor = ImageMarker.themedOuterBorderColor(isDark);
final innerBorderColor = ImageMarker.themedInnerBorderColor(isDark);
final outerDecoration = BoxDecoration(
border: Border.fromBorderSide(BorderSide(
color: outerBorderColor,
width: ImageMarker.outerBorderWidth,
)),
borderRadius: outerBorderRadius,
);
final innerDecoration = BoxDecoration(
border: Border.fromBorderSide(BorderSide(
color: innerBorderColor,
width: ImageMarker.innerBorderWidth,
)),
borderRadius: innerBorderRadius,
);
return Container(
decoration: outerDecoration,
child: DecoratedBox(
decoration: innerDecoration,
position: DecorationPosition.foreground,
child: ClipRRect(
borderRadius: innerBorderRadius,
child: Container(
width: diameter,
height: diameter,
color: theme.colorScheme.secondary,
),
),
),
);
}
}

View file

@ -0,0 +1,13 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
@immutable
class MarkerKey<T> extends LocalKey with EquatableMixin {
final T entry;
final int? count;
@override
List<Object?> get props => [entry, count];
const MarkerKey(this.entry, this.count);
}

View file

@ -0,0 +1,17 @@
import 'package:aves_map/src/overlay/tile.dart';
import 'package:flutter/painting.dart';
import 'package:latlong2/latlong.dart';
mixin MapOverlay {
String get id;
bool get canOverlay;
LatLng? get topLeft;
LatLng? get bottomRight;
ImageProvider get imageProvider;
Future<MapTile?> getTile(int tx, int ty, int? zoomLevel);
}

View file

@ -0,0 +1,14 @@
enum EntryMapStyle {
// Google
googleNormal,
googleHybrid,
googleTerrain,
// Huawei
hmsNormal,
hmsTerrain,
// Leaflet
// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/
osmHot,
stamenToner,
stamenWatercolor,
}

View file

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
enum MapNavigationButton { back, map }
class MapThemeData {
final bool interactive, showCoordinateFilter;
final MapNavigationButton navigationButton;
final Animation<double> scale;
final VisualDensity? visualDensity;
final double? mapHeight;
const MapThemeData({
required this.interactive,
required this.showCoordinateFilter,
required this.navigationButton,
required this.scale,
required this.visualDensity,
required this.mapHeight,
});
static const double markerOuterBorderWidth = 1.5;
static const double markerInnerBorderWidth = 2;
static const double markerImageExtent = 48.0;
static const Size markerArrowSize = Size(8, 6);
static Color markerThemedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26;
static Color markerThemedInnerBorderColor(bool isDark) => isDark ? const Color(0xFF212121) : Colors.white;
}

View file

@ -1,6 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/utils/geo_utils.dart'; import 'package:aves_map/src/geo_utils.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';

View file

@ -0,0 +1,22 @@
name: aves_map
version: 0.0.1
publish_to: none
environment:
sdk: ">=2.16.2 <3.0.0"
dependencies:
flutter:
sdk: flutter
# TODO TLAD as of 2022/02/22, null safe version is pre-release
custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0'
equatable:
fluster:
flutter_map:
latlong2:
provider:
dev_dependencies:
flutter_lints:
flutter:

View file

@ -3,8 +3,7 @@ version: 0.0.1
publish_to: none publish_to: none
environment: environment:
sdk: ">=2.12.0 <3.0.0" sdk: ">=2.16.2 <3.0.0"
flutter: ">=1.20.0"
dependencies: dependencies:
flutter: flutter:

View file

@ -3,8 +3,7 @@ version: 0.0.1
publish_to: none publish_to: none
environment: environment:
sdk: ">=2.12.0 <3.0.0" sdk: ">=2.16.2 <3.0.0"
flutter: ">=1.17.0"
dependencies: dependencies:
flutter: flutter:

View file

@ -3,8 +3,7 @@ version: 0.0.1
publish_to: none publish_to: none
environment: environment:
sdk: ">=2.12.0 <3.0.0" sdk: ">=2.16.2 <3.0.0"
flutter: ">=1.17.0"
dependencies: dependencies:
flutter: flutter:

29
plugins/aves_services/.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/

View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 5464c5bac742001448fe4fc0597be939379f88ea
channel: stable
project_type: package

View file

@ -0,0 +1 @@
include: ../../analysis_options.yaml

View file

@ -0,0 +1,31 @@
library aves_services;
import 'package:aves_map/aves_map.dart';
import 'package:flutter/widgets.dart';
import 'package:latlong2/latlong.dart';
abstract class MobileServices {
Future<bool> isServiceAvailable();
EntryMapStyle get defaultMapStyle;
List<EntryMapStyle> get mapStyles;
Widget buildMap<T>({
required AvesMapController? controller,
required Listenable clusterListenable,
required ValueNotifier<ZoomedBounds> boundsNotifier,
required EntryMapStyle style,
required TransitionBuilder decoratorBuilder,
required ButtonPanelBuilder buttonPanelBuilder,
required MarkerClusterBuilder<T> markerClusterBuilder,
required MarkerWidgetBuilder<T> markerWidgetBuilder,
required MarkerImageReadyChecker<T> markerImageReadyChecker,
required ValueNotifier<LatLng?>? dotLocationNotifier,
required ValueNotifier<double>? overlayOpacityNotifier,
required MapOverlay? overlayEntry,
required UserZoomChangeCallback? onUserZoomChange,
required MapTapCallback? onMapTap,
required MarkerTapCallback<T>? onMarkerTap,
});
}

View file

@ -0,0 +1,18 @@
name: aves_services
version: 0.0.1
publish_to: none
environment:
sdk: ">=2.16.2 <3.0.0"
dependencies:
flutter:
sdk: flutter
aves_map:
path: ../aves_map
latlong2:
dev_dependencies:
flutter_lints:
flutter:

29
plugins/aves_services_google/.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/

View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 5464c5bac742001448fe4fc0597be939379f88ea
channel: stable
project_type: package

View file

@ -0,0 +1 @@
include: ../../analysis_options.yaml

View file

@ -0,0 +1,67 @@
library aves_services_platform;
import 'package:aves_map/aves_map.dart';
import 'package:aves_services/aves_services.dart';
import 'package:aves_services_platform/src/map.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:google_api_availability/google_api_availability.dart';
import 'package:latlong2/latlong.dart';
class PlatformMobileServices extends MobileServices {
bool? _isAvailable;
@override
Future<bool> isServiceAvailable() async {
if (_isAvailable != null) return SynchronousFuture(_isAvailable!);
final result = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability();
_isAvailable = result == GooglePlayServicesAvailability.success;
debugPrint('Device has Google Play Services=$_isAvailable');
return _isAvailable!;
}
@override
EntryMapStyle get defaultMapStyle => EntryMapStyle.googleNormal;
@override
List<EntryMapStyle> get mapStyles => [EntryMapStyle.googleNormal, EntryMapStyle.googleHybrid, EntryMapStyle.googleTerrain];
@override
Widget buildMap<T>({
required AvesMapController? controller,
required Listenable clusterListenable,
required ValueNotifier<ZoomedBounds> boundsNotifier,
required EntryMapStyle style,
required TransitionBuilder decoratorBuilder,
required ButtonPanelBuilder buttonPanelBuilder,
required MarkerClusterBuilder<T> markerClusterBuilder,
required MarkerWidgetBuilder<T> markerWidgetBuilder,
required MarkerImageReadyChecker<T> markerImageReadyChecker,
required ValueNotifier<LatLng?>? dotLocationNotifier,
required ValueNotifier<double>? overlayOpacityNotifier,
required MapOverlay? overlayEntry,
required UserZoomChangeCallback? onUserZoomChange,
required MapTapCallback? onMapTap,
required MarkerTapCallback<T>? onMarkerTap,
}) {
return EntryGoogleMap<T>(
controller: controller,
clusterListenable: clusterListenable,
boundsNotifier: boundsNotifier,
minZoom: 0,
maxZoom: 20,
style: style,
decoratorBuilder: decoratorBuilder,
buttonPanelBuilder: buttonPanelBuilder,
markerClusterBuilder: markerClusterBuilder,
markerWidgetBuilder: markerWidgetBuilder,
markerImageReadyChecker: markerImageReadyChecker,
dotLocationNotifier: dotLocationNotifier,
overlayOpacityNotifier: overlayOpacityNotifier,
overlayEntry: overlayEntry,
onUserZoomChange: onUserZoomChange,
onMapTap: onMapTap,
onMarkerTap: onMarkerTap,
);
}
}

View file

@ -1,40 +1,29 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:aves/model/entry_images.dart'; import 'package:aves_map/aves_map.dart';
import 'package:aves/model/geotiff.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/utils/change_notifier.dart';
import 'package:aves/widgets/common/map/buttons.dart';
import 'package:aves/widgets/common/map/controller.dart';
import 'package:aves/widgets/common/map/decorator.dart';
import 'package:aves/widgets/common/map/geo_entry.dart';
import 'package:aves/widgets/common/map/geo_map.dart';
import 'package:aves/widgets/common/map/google/geotiff_tile_provider.dart';
import 'package:aves/widgets/common/map/google/marker_generator.dart';
import 'package:aves/widgets/common/map/marker.dart';
import 'package:aves/widgets/common/map/theme.dart';
import 'package:aves/widgets/common/map/zoomed_bounds.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:latlong2/latlong.dart' as ll; import 'package:latlong2/latlong.dart' as ll;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class EntryGoogleMap extends StatefulWidget { class EntryGoogleMap<T> extends StatefulWidget {
final AvesMapController? controller; final AvesMapController? controller;
final Listenable clusterListenable; final Listenable clusterListenable;
final ValueNotifier<ZoomedBounds> boundsNotifier; final ValueNotifier<ZoomedBounds> boundsNotifier;
final double? minZoom, maxZoom; final double? minZoom, maxZoom;
final EntryMapStyle style; final EntryMapStyle style;
final MarkerClusterBuilder markerClusterBuilder; final TransitionBuilder decoratorBuilder;
final MarkerWidgetBuilder markerWidgetBuilder; final ButtonPanelBuilder buttonPanelBuilder;
final MarkerClusterBuilder<T> markerClusterBuilder;
final MarkerWidgetBuilder<T> markerWidgetBuilder;
final MarkerImageReadyChecker<T> markerImageReadyChecker;
final ValueNotifier<ll.LatLng?>? dotLocationNotifier; final ValueNotifier<ll.LatLng?>? dotLocationNotifier;
final ValueNotifier<double>? overlayOpacityNotifier; final ValueNotifier<double>? overlayOpacityNotifier;
final MappedGeoTiff? overlayEntry; final MapOverlay? overlayEntry;
final UserZoomChangeCallback? onUserZoomChange; final UserZoomChangeCallback? onUserZoomChange;
final void Function(ll.LatLng location)? onMapTap; final MapTapCallback? onMapTap;
final void Function(GeoEntry geoEntry)? onMarkerTap; final MarkerTapCallback<T>? onMarkerTap;
final MapOpener? openMapPage;
const EntryGoogleMap({ const EntryGoogleMap({
Key? key, Key? key,
@ -44,27 +33,29 @@ class EntryGoogleMap extends StatefulWidget {
this.minZoom, this.minZoom,
this.maxZoom, this.maxZoom,
required this.style, required this.style,
required this.decoratorBuilder,
required this.buttonPanelBuilder,
required this.markerClusterBuilder, required this.markerClusterBuilder,
required this.markerWidgetBuilder, required this.markerWidgetBuilder,
required this.markerImageReadyChecker,
required this.dotLocationNotifier, required this.dotLocationNotifier,
this.overlayOpacityNotifier, this.overlayOpacityNotifier,
this.overlayEntry, this.overlayEntry,
this.onUserZoomChange, this.onUserZoomChange,
this.onMapTap, this.onMapTap,
this.onMarkerTap, this.onMarkerTap,
this.openMapPage,
}) : super(key: key); }) : super(key: key);
@override @override
State<StatefulWidget> createState() => _EntryGoogleMapState(); State<StatefulWidget> createState() => _EntryGoogleMapState<T>();
} }
class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObserver { class _EntryGoogleMapState<T> extends State<EntryGoogleMap<T>> with WidgetsBindingObserver {
GoogleMapController? _googleMapController; GoogleMapController? _serviceMapController;
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
Map<MarkerKey, GeoEntry> _geoEntryByMarkerKey = {}; Map<MarkerKey<T>, GeoEntry<T>> _geoEntryByMarkerKey = {};
final Map<MarkerKey, Uint8List> _markerBitmaps = {}; final Map<MarkerKey<T>, Uint8List> _markerBitmaps = {};
final AChangeNotifier _markerBitmapChangeNotifier = AChangeNotifier(); final StreamController<MarkerKey<T>> _markerBitmapReadyStreamController = StreamController.broadcast();
Uint8List? _dotMarkerBitmap; Uint8List? _dotMarkerBitmap;
ValueNotifier<ZoomedBounds> get boundsNotifier => widget.boundsNotifier; ValueNotifier<ZoomedBounds> get boundsNotifier => widget.boundsNotifier;
@ -81,7 +72,7 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
} }
@override @override
void didUpdateWidget(covariant EntryGoogleMap oldWidget) { void didUpdateWidget(covariant EntryGoogleMap<T> oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget); _unregisterWidget(oldWidget);
_registerWidget(widget); _registerWidget(widget);
@ -90,20 +81,20 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
@override @override
void dispose() { void dispose() {
_unregisterWidget(widget); _unregisterWidget(widget);
_googleMapController?.dispose(); _serviceMapController?.dispose();
WidgetsBinding.instance!.removeObserver(this); WidgetsBinding.instance!.removeObserver(this);
super.dispose(); super.dispose();
} }
void _registerWidget(EntryGoogleMap widget) { void _registerWidget(EntryGoogleMap<T> widget) {
final avesMapController = widget.controller; final avesMapController = widget.controller;
if (avesMapController != null) { if (avesMapController != null) {
_subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toGoogleLatLng(event.latLng)))); _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng))));
} }
widget.clusterListenable.addListener(_updateMarkers); widget.clusterListenable.addListener(_updateMarkers);
} }
void _unregisterWidget(EntryGoogleMap widget) { void _unregisterWidget(EntryGoogleMap<T> widget) {
widget.clusterListenable.removeListener(_updateMarkers); widget.clusterListenable.removeListener(_updateMarkers);
_subscriptions _subscriptions
..forEach((sub) => sub.cancel()) ..forEach((sub) => sub.cancel())
@ -120,7 +111,7 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
case AppLifecycleState.resumed: case AppLifecycleState.resumed:
// workaround for blank Google map when resuming app // workaround for blank Google map when resuming app
// cf https://github.com/flutter/flutter/issues/40284 // cf https://github.com/flutter/flutter/issues/40284
_googleMapController?.setMapStyle(null); _serviceMapController?.setMapStyle(null);
break; break;
} }
} }
@ -134,31 +125,24 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
isReadyToRender: (key) => true, isReadyToRender: (key) => true,
onRendered: (key, bitmap) => _dotMarkerBitmap = bitmap, onRendered: (key, bitmap) => _dotMarkerBitmap = bitmap,
), ),
MarkerGeneratorWidget<MarkerKey>( MarkerGeneratorWidget<MarkerKey<T>>(
markers: _geoEntryByMarkerKey.keys.map(widget.markerWidgetBuilder).toList(), markers: _geoEntryByMarkerKey.keys.map(widget.markerWidgetBuilder).toList(),
isReadyToRender: (key) => key.entry.isThumbnailReady(extent: GeoMap.markerImageExtent), isReadyToRender: widget.markerImageReadyChecker,
onRendered: (key, bitmap) { onRendered: (key, bitmap) {
_markerBitmaps[key] = bitmap; _markerBitmaps[key] = bitmap;
_markerBitmapChangeNotifier.notify(); _markerBitmapReadyStreamController.add(key);
}, },
), ),
MapDecorator( widget.decoratorBuilder(context, _buildMap()),
child: _buildMap(), widget.buttonPanelBuilder(_zoomBy, _resetRotation),
),
MapButtonPanel(
boundsNotifier: boundsNotifier,
zoomBy: _zoomBy,
openMapPage: widget.openMapPage,
resetRotation: _resetRotation,
),
], ],
); );
} }
Widget _buildMap() { Widget _buildMap() {
return AnimatedBuilder( return StreamBuilder(
animation: _markerBitmapChangeNotifier, stream: _markerBitmapReadyStreamController.stream,
builder: (context, child) { builder: (context, _) {
final markers = <Marker>{}; final markers = <Marker>{};
_geoEntryByMarkerKey.forEach((markerKey, geoEntry) { _geoEntryByMarkerKey.forEach((markerKey, geoEntry) {
final bytes = _markerBitmaps[markerKey]; final bytes = _markerBitmaps[markerKey];
@ -185,11 +169,11 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
return GoogleMap( return GoogleMap(
initialCameraPosition: CameraPosition( initialCameraPosition: CameraPosition(
bearing: -bounds.rotation, bearing: -bounds.rotation,
target: _toGoogleLatLng(bounds.projectedCenter), target: _toServiceLatLng(bounds.projectedCenter),
zoom: bounds.zoom, zoom: bounds.zoom,
), ),
onMapCreated: (controller) async { onMapCreated: (controller) async {
_googleMapController = controller; _serviceMapController = controller;
final zoom = await controller.getZoomLevel(); final zoom = await controller.getZoomLevel();
await _updateVisibleRegion(zoom: zoom, rotation: bounds.rotation); await _updateVisibleRegion(zoom: zoom, rotation: bounds.rotation);
if (mounted) { if (mounted) {
@ -220,7 +204,7 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
anchor: const Offset(.5, .5), anchor: const Offset(.5, .5),
consumeTapEvents: true, consumeTapEvents: true,
icon: BitmapDescriptor.fromBytes(_dotMarkerBitmap!), icon: BitmapDescriptor.fromBytes(_dotMarkerBitmap!),
position: _toGoogleLatLng(dotLocation), position: _toServiceLatLng(dotLocation),
zIndex: 1, zIndex: 1,
) )
}, },
@ -228,14 +212,14 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
tileOverlays: { tileOverlays: {
if (overlayEntry != null && overlayEntry.canOverlay) if (overlayEntry != null && overlayEntry.canOverlay)
TileOverlay( TileOverlay(
tileOverlayId: TileOverlayId(overlayEntry.entry.uri), tileOverlayId: TileOverlayId(overlayEntry.id),
tileProvider: GeoTiffTileProvider(overlayEntry), tileProvider: GmsGeoTiffTileProvider(overlayEntry),
transparency: 1 - overlayOpacity, transparency: 1 - overlayOpacity,
), ),
}, },
onCameraMove: (position) => _updateVisibleRegion(zoom: position.zoom, rotation: -position.bearing), onCameraMove: (position) => _updateVisibleRegion(zoom: position.zoom, rotation: -position.bearing),
onCameraIdle: _onIdle, onCameraIdle: _onIdle,
onTap: (position) => widget.onMapTap?.call(_fromGoogleLatLng(position)), onTap: (position) => widget.onMapTap?.call(_fromServiceLatLng(position)),
); );
}, },
); );
@ -258,13 +242,13 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
Future<void> _updateVisibleRegion({required double zoom, required double rotation}) async { Future<void> _updateVisibleRegion({required double zoom, required double rotation}) async {
if (!mounted) return; if (!mounted) return;
final bounds = await _googleMapController?.getVisibleRegion(); final bounds = await _serviceMapController?.getVisibleRegion();
if (bounds != null && (bounds.northeast != uninitializedLatLng || bounds.southwest != uninitializedLatLng)) { if (bounds != null && (bounds.northeast != uninitializedLatLng || bounds.southwest != uninitializedLatLng)) {
final sw = bounds.southwest; final sw = bounds.southwest;
final ne = bounds.northeast; final ne = bounds.northeast;
boundsNotifier.value = ZoomedBounds( boundsNotifier.value = ZoomedBounds(
sw: _fromGoogleLatLng(sw), sw: _fromServiceLatLng(sw),
ne: _fromGoogleLatLng(ne), ne: _fromServiceLatLng(ne),
zoom: zoom, zoom: zoom,
rotation: rotation, rotation: rotation,
); );
@ -278,17 +262,17 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
} }
Future<void> _resetRotation() async { Future<void> _resetRotation() async {
final controller = _googleMapController; final controller = _serviceMapController;
if (controller == null) return; if (controller == null) return;
await controller.animateCamera(CameraUpdate.newCameraPosition(CameraPosition( await controller.animateCamera(CameraUpdate.newCameraPosition(CameraPosition(
target: _toGoogleLatLng(bounds.projectedCenter), target: _toServiceLatLng(bounds.projectedCenter),
zoom: bounds.zoom, zoom: bounds.zoom,
))); )));
} }
Future<void> _zoomBy(double amount) async { Future<void> _zoomBy(double amount) async {
final controller = _googleMapController; final controller = _serviceMapController;
if (controller == null) return; if (controller == null) return;
widget.onUserZoomChange?.call(await controller.getZoomLevel() + amount); widget.onUserZoomChange?.call(await controller.getZoomLevel() + amount);
@ -296,16 +280,16 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
} }
Future<void> _moveTo(LatLng point) async { Future<void> _moveTo(LatLng point) async {
final controller = _googleMapController; final controller = _serviceMapController;
if (controller == null) return; if (controller == null) return;
await controller.animateCamera(CameraUpdate.newLatLng(point)); await controller.animateCamera(CameraUpdate.newLatLng(point));
} }
// `LatLng` used by `google_maps_flutter` is not the one from `latlong2` package // `LatLng` used by `google_maps_flutter` is not the one from `latlong2` package
LatLng _toGoogleLatLng(ll.LatLng location) => LatLng(location.latitude, location.longitude); LatLng _toServiceLatLng(ll.LatLng location) => LatLng(location.latitude, location.longitude);
ll.LatLng _fromGoogleLatLng(LatLng location) => ll.LatLng(location.latitude, location.longitude); ll.LatLng _fromServiceLatLng(LatLng location) => ll.LatLng(location.latitude, location.longitude);
MapType _toMapType(EntryMapStyle style) { MapType _toMapType(EntryMapStyle style) {
switch (style) { switch (style) {
@ -320,3 +304,18 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
} }
} }
} }
class GmsGeoTiffTileProvider extends TileProvider {
MapOverlay overlayEntry;
GmsGeoTiffTileProvider(this.overlayEntry);
@override
Future<Tile> getTile(int x, int y, int? zoom) async {
final tile = await overlayEntry.getTile(x, y, zoom);
if (tile != null) {
return Tile(tile.width, tile.height, tile.data);
}
return TileProvider.noTile;
}
}

View file

@ -0,0 +1,23 @@
name: aves_services_platform
version: 0.0.1
publish_to: none
environment:
sdk: ">=2.16.2 <3.0.0"
dependencies:
flutter:
sdk: flutter
aves_map:
path: ../aves_map
aves_services:
path: ../aves_services
google_api_availability:
google_maps_flutter:
latlong2:
provider:
dev_dependencies:
flutter_lints:
flutter:

29
plugins/aves_services_huawei/.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/

View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 5464c5bac742001448fe4fc0597be939379f88ea
channel: stable
project_type: package

View file

@ -0,0 +1 @@
include: ../../analysis_options.yaml

View file

@ -0,0 +1,70 @@
library aves_services_platform;
import 'package:aves_map/aves_map.dart';
import 'package:aves_services/aves_services.dart';
import 'package:aves_services_platform/src/map.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:huawei_hmsavailability/huawei_hmsavailability.dart';
import 'package:latlong2/latlong.dart';
class PlatformMobileServices extends MobileServices {
// cf https://developer.huawei.com/consumer/en/doc/development/hmscore-common-References/huaweiapiavailability-0000001050121134#section9492524178
static const int _hmsCoreAvailable = 0;
bool? _isAvailable;
@override
Future<bool> isServiceAvailable() async {
if (_isAvailable != null) return SynchronousFuture(_isAvailable!);
final result = await HmsApiAvailability().isHMSAvailable();
_isAvailable = result == _hmsCoreAvailable;
debugPrint('Device has Huawei Mobile Services=$_isAvailable');
return _isAvailable!;
}
@override
EntryMapStyle get defaultMapStyle => EntryMapStyle.hmsNormal;
@override
List<EntryMapStyle> get mapStyles => [EntryMapStyle.hmsNormal, EntryMapStyle.hmsTerrain];
@override
Widget buildMap<T>({
required AvesMapController? controller,
required Listenable clusterListenable,
required ValueNotifier<ZoomedBounds> boundsNotifier,
required EntryMapStyle style,
required TransitionBuilder decoratorBuilder,
required ButtonPanelBuilder buttonPanelBuilder,
required MarkerClusterBuilder<T> markerClusterBuilder,
required MarkerWidgetBuilder<T> markerWidgetBuilder,
required MarkerImageReadyChecker<T> markerImageReadyChecker,
required ValueNotifier<LatLng?>? dotLocationNotifier,
required ValueNotifier<double>? overlayOpacityNotifier,
required MapOverlay? overlayEntry,
required UserZoomChangeCallback? onUserZoomChange,
required MapTapCallback? onMapTap,
required MarkerTapCallback<T>? onMarkerTap,
}) {
return EntryHmsMap<T>(
controller: controller,
clusterListenable: clusterListenable,
boundsNotifier: boundsNotifier,
minZoom: 3,
maxZoom: 20,
style: style,
decoratorBuilder: decoratorBuilder,
buttonPanelBuilder: buttonPanelBuilder,
markerClusterBuilder: markerClusterBuilder,
markerWidgetBuilder: markerWidgetBuilder,
markerImageReadyChecker: markerImageReadyChecker,
dotLocationNotifier: dotLocationNotifier,
overlayOpacityNotifier: overlayOpacityNotifier,
overlayEntry: overlayEntry,
onUserZoomChange: onUserZoomChange,
onMapTap: onMapTap,
onMarkerTap: onMarkerTap,
);
}
}

View file

@ -0,0 +1,325 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart';
import 'package:huawei_map/map.dart';
import 'package:latlong2/latlong.dart' as ll;
import 'package:provider/provider.dart';
class EntryHmsMap<T> extends StatefulWidget {
final AvesMapController? controller;
final Listenable clusterListenable;
final ValueNotifier<ZoomedBounds> boundsNotifier;
final double? minZoom, maxZoom;
final EntryMapStyle style;
final TransitionBuilder decoratorBuilder;
final ButtonPanelBuilder buttonPanelBuilder;
final MarkerClusterBuilder<T> markerClusterBuilder;
final MarkerWidgetBuilder<T> markerWidgetBuilder;
final MarkerImageReadyChecker<T> markerImageReadyChecker;
final ValueNotifier<ll.LatLng?>? dotLocationNotifier;
final ValueNotifier<double>? overlayOpacityNotifier;
final MapOverlay? overlayEntry;
final UserZoomChangeCallback? onUserZoomChange;
final MapTapCallback? onMapTap;
final MarkerTapCallback<T>? onMarkerTap;
const EntryHmsMap({
Key? key,
this.controller,
required this.clusterListenable,
required this.boundsNotifier,
this.minZoom,
this.maxZoom,
required this.style,
required this.decoratorBuilder,
required this.buttonPanelBuilder,
required this.markerClusterBuilder,
required this.markerWidgetBuilder,
required this.markerImageReadyChecker,
required this.dotLocationNotifier,
this.overlayOpacityNotifier,
this.overlayEntry,
this.onUserZoomChange,
this.onMapTap,
this.onMarkerTap,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _EntryHmsMapState<T>();
}
class _EntryHmsMapState<T> extends State<EntryHmsMap<T>> {
HuaweiMapController? _serviceMapController;
final List<StreamSubscription> _subscriptions = [];
Map<MarkerKey<T>, GeoEntry<T>> _geoEntryByMarkerKey = {};
final Map<MarkerKey<T>, Uint8List> _markerBitmaps = {};
final StreamController<MarkerKey<T>> _markerBitmapReadyStreamController = StreamController.broadcast();
Uint8List? _dotMarkerBitmap;
ValueNotifier<ZoomedBounds> get boundsNotifier => widget.boundsNotifier;
ZoomedBounds get bounds => boundsNotifier.value;
static const uninitializedLatLng = LatLng(0, 0);
@override
void initState() {
super.initState();
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant EntryHmsMap<T> oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget(EntryHmsMap<T> widget) {
final avesMapController = widget.controller;
if (avesMapController != null) {
_subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng))));
}
widget.clusterListenable.addListener(_updateMarkers);
}
void _unregisterWidget(EntryHmsMap<T> widget) {
widget.clusterListenable.removeListener(_updateMarkers);
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
MarkerGeneratorWidget<Key>(
markers: const [DotMarker(key: Key('dot'))],
isReadyToRender: (key) => true,
onRendered: (key, bitmap) => _dotMarkerBitmap = bitmap,
),
MarkerGeneratorWidget<MarkerKey<T>>(
markers: _geoEntryByMarkerKey.keys.map(widget.markerWidgetBuilder).toList(),
isReadyToRender: widget.markerImageReadyChecker,
onRendered: (key, bitmap) {
_markerBitmaps[key] = bitmap;
_markerBitmapReadyStreamController.add(key);
},
),
widget.decoratorBuilder(context, _buildMap()),
widget.buttonPanelBuilder(_zoomBy, _resetRotation),
],
);
}
Widget _buildMap() {
return StreamBuilder(
stream: _markerBitmapReadyStreamController.stream,
builder: (context, _) {
final markers = <Marker>{};
_geoEntryByMarkerKey.forEach((markerKey, geoEntry) {
final bytes = _markerBitmaps[markerKey];
if (bytes != null) {
final point = LatLng(geoEntry.latitude!, geoEntry.longitude!);
markers.add(Marker(
markerId: MarkerId(geoEntry.markerId!),
clickable: true,
icon: BitmapDescriptor.fromBytes(bytes),
position: point,
onClick: () => widget.onMarkerTap?.call(geoEntry),
));
}
});
final interactive = context.select<MapThemeData, bool>((v) => v.interactive);
// final overlayEntry = widget.overlayEntry;
return ValueListenableBuilder<ll.LatLng?>(
valueListenable: widget.dotLocationNotifier ?? ValueNotifier(null),
builder: (context, dotLocation, child) {
return ValueListenableBuilder<double>(
valueListenable: widget.overlayOpacityNotifier ?? ValueNotifier(1),
builder: (context, overlayOpacity, child) {
return HuaweiMap(
initialCameraPosition: CameraPosition(
bearing: bounds.rotation,
target: _toServiceLatLng(bounds.projectedCenter),
zoom: bounds.zoom,
),
mapType: _toMapType(widget.style),
// compass disabled to use provider agnostic controls
compassEnabled: false,
mapToolbarEnabled: false,
minMaxZoomPreference: MinMaxZoomPreference(
widget.minZoom ?? MinMaxZoomPreference.unbounded.minZoom,
widget.maxZoom ?? MinMaxZoomPreference.unbounded.maxZoom,
),
// `allGesturesEnabled`, if defined overrides specific gesture settings
rotateGesturesEnabled: interactive,
scrollGesturesEnabled: interactive,
// zoom controls disabled to use provider agnostic controls
zoomControlsEnabled: false,
zoomGesturesEnabled: interactive,
// tilt disabled to match leaflet
tiltGesturesEnabled: false,
myLocationEnabled: false,
myLocationButtonEnabled: false,
trafficEnabled: false,
isScrollGesturesEnabledDuringRotateOrZoom: true,
markers: {
...markers,
if (dotLocation != null && _dotMarkerBitmap != null)
Marker(
markerId: MarkerId('dot'),
anchor: const Offset(.5, .5),
clickable: true,
icon: BitmapDescriptor.fromBytes(_dotMarkerBitmap!),
position: _toServiceLatLng(dotLocation),
zIndex: 1,
)
},
// TODO TLAD [hms] GeoTIFF ground overlay
// groundOverlays: {
// if (overlayEntry != null && overlayEntry.canOverlay)
// GroundOverlay(
// groundOverlayId: GroundOverlayId('overlay'),
// // Google Maps API allows defining overlay either via
// // 1) position, anchor and width/height (in meters)
// // 2) bounds
// // Huawei requires width/height (in meters?), but also allows bounds...
// width: 42,
// height: 42,
// imageDescriptor: BitmapDescriptor.defaultMarker,
// position: _toServiceLatLng(overlayEntry.center!),
// ),
// },
// TODO TLAD [hms] dynamic tile provider from current bounds,
// tileOverlays: {
// if (overlayEntry != null && overlayEntry.canOverlay)
// TileOverlay(
// tileOverlayId: TileOverlayId(overlayEntry.entry.uri),
// // `tileProvider` is `RepetitiveTile`, `UrlTile` or List<Tile>
// // tileProvider: <Tile>[
// // Tile(
// // x: x,
// // y: y,
// // zoom: zoom,
// // imageData: imageData,
// // ),
// // ],
// transparency: 1 - overlayOpacity,
// ),
// },
onMapCreated: (controller) async {
_serviceMapController = controller;
final zoom = await controller.getZoomLevel();
await _updateVisibleRegion(zoom: zoom ?? bounds.zoom, rotation: bounds.rotation);
if (mounted) {
setState(() {});
}
},
onCameraMove: (position) => _updateVisibleRegion(zoom: position.zoom, rotation: position.bearing),
onCameraIdle: _onIdle,
onClick: (position) => widget.onMapTap?.call(_fromServiceLatLng(position)),
onPoiClick: (poi) {
final poiPosition = poi.latLng;
if (poiPosition != null) {
widget.onMapTap?.call(_fromServiceLatLng(poiPosition));
}
},
logoPadding: const EdgeInsets.all(8),
// lite mode disabled because it is not interactive
liteMode: false,
);
},
);
},
);
},
);
}
void _onIdle() {
if (!mounted) return;
widget.controller?.notifyIdle(bounds);
_updateMarkers();
}
void _updateMarkers() {
setState(() => _geoEntryByMarkerKey = widget.markerClusterBuilder());
}
Future<void> _updateVisibleRegion({required double zoom, required double rotation}) async {
if (!mounted) return;
final bounds = await _serviceMapController?.getVisibleRegion();
if (bounds != null && (bounds.northeast != uninitializedLatLng || bounds.southwest != uninitializedLatLng)) {
final sw = bounds.southwest;
final ne = bounds.northeast;
boundsNotifier.value = ZoomedBounds(
sw: _fromServiceLatLng(sw),
ne: _fromServiceLatLng(ne),
zoom: zoom,
rotation: rotation,
);
} else {
// the visible region is sometimes uninitialized when queried right after creation,
// so we query it again next frame
WidgetsBinding.instance!.addPostFrameCallback((_) {
_updateVisibleRegion(zoom: zoom, rotation: rotation);
});
}
}
Future<void> _resetRotation() async {
final controller = _serviceMapController;
if (controller == null) return;
await controller.animateCamera(CameraUpdate.newCameraPosition(CameraPosition(
target: _toServiceLatLng(bounds.projectedCenter),
zoom: bounds.zoom,
)));
}
Future<void> _zoomBy(double amount) async {
final controller = _serviceMapController;
if (controller == null) return;
final zoom = await controller.getZoomLevel();
if (zoom == null) return;
widget.onUserZoomChange?.call(zoom + amount);
await controller.animateCamera(CameraUpdate.zoomBy(amount));
}
Future<void> _moveTo(LatLng point) async {
final controller = _serviceMapController;
if (controller == null) return;
await controller.animateCamera(CameraUpdate.newLatLng(point));
}
// `LatLng` used by `google_maps_flutter` is not the one from `latlong2` package
LatLng _toServiceLatLng(ll.LatLng location) => LatLng(location.latitude, location.longitude);
ll.LatLng _fromServiceLatLng(LatLng location) => ll.LatLng(location.lat, location.lng);
MapType _toMapType(EntryMapStyle style) {
switch (style) {
case EntryMapStyle.hmsNormal:
return MapType.normal;
case EntryMapStyle.hmsTerrain:
return MapType.terrain;
default:
return MapType.none;
}
}
}

View file

@ -0,0 +1,23 @@
name: aves_services_platform
version: 0.0.1
publish_to: none
environment:
sdk: ">=2.16.2 <3.0.0"
dependencies:
flutter:
sdk: flutter
aves_map:
path: ../aves_map
aves_services:
path: ../aves_services
huawei_hmsavailability:
huawei_map:
latlong2:
provider:
dev_dependencies:
flutter_lints:
flutter:

View file

@ -36,6 +36,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.2" version: "2.8.2"
aves_map:
dependency: "direct main"
description:
path: "plugins/aves_map"
relative: true
source: path
version: "0.0.1"
aves_report: aves_report:
dependency: "direct main" dependency: "direct main"
description: description:
@ -50,6 +57,20 @@ packages:
relative: true relative: true
source: path source: path
version: "0.0.1" version: "0.0.1"
aves_services:
dependency: "direct main"
description:
path: "plugins/aves_services"
relative: true
source: path
version: "0.0.1"
aves_services_platform:
dependency: "direct main"
description:
path: "plugins/aves_services_google"
relative: true
source: path
version: "0.0.1"
barcode: barcode:
dependency: transitive dependency: transitive
description: description:
@ -184,7 +205,7 @@ packages:
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
custom_rounded_rectangle_border: custom_rounded_rectangle_border:
dependency: "direct main" dependency: transitive
description: description:
name: custom_rounded_rectangle_border name: custom_rounded_rectangle_border
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
@ -305,7 +326,7 @@ packages:
name: firebase_core name: firebase_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.14.1" version: "1.15.0"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -326,14 +347,14 @@ packages:
name: firebase_crashlytics name: firebase_crashlytics
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.6.2" version: "2.6.3"
firebase_crashlytics_platform_interface: firebase_crashlytics_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_crashlytics_platform_interface name: firebase_crashlytics_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.3" version: "3.2.4"
flex_color_picker: flex_color_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -456,14 +477,14 @@ packages:
source: hosted source: hosted
version: "2.0.2" version: "2.0.2"
google_api_availability: google_api_availability:
dependency: "direct main" dependency: transitive
description: description:
name: google_api_availability name: google_api_availability
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
google_maps_flutter: google_maps_flutter:
dependency: "direct main" dependency: transitive
description: description:
name: google_maps_flutter name: google_maps_flutter
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
@ -1268,5 +1289,5 @@ packages:
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
sdks: sdks:
dart: ">=2.16.0 <3.0.0" dart: ">=2.16.2 <3.0.0"
flutter: ">=2.10.0" flutter: ">=2.10.0"

View file

@ -10,7 +10,7 @@ version: 1.6.4+70
publish_to: none publish_to: none
environment: environment:
sdk: '>=2.16.0 <3.0.0' sdk: ">=2.16.2 <3.0.0"
# following https://github.blog/2021-09-01-improving-git-protocol-security-github/ # following https://github.blog/2021-09-01-improving-git-protocol-security-github/
# dependency GitHub repos should be referenced via `https://`, not `git://` # dependency GitHub repos should be referenced via `https://`, not `git://`
@ -21,16 +21,20 @@ dependencies:
sdk: flutter sdk: flutter
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
aves_map:
path: plugins/aves_map
aves_report: aves_report:
path: plugins/aves_report path: plugins/aves_report
aves_report_platform: aves_report_platform:
path: plugins/aves_report_crashlytics path: plugins/aves_report_crashlytics
aves_services:
path: plugins/aves_services
aves_services_platform:
path: plugins/aves_services_google
charts_flutter: charts_flutter:
collection: collection:
connectivity_plus: connectivity_plus:
country_code: country_code:
# TODO TLAD as of 2022/02/22, null safe version is pre-release
custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0'
decorated_icon: decorated_icon:
device_info_plus: device_info_plus:
equatable: equatable:
@ -50,8 +54,6 @@ dependencies:
flutter_markdown: flutter_markdown:
flutter_staggered_animations: flutter_staggered_animations:
get_it: get_it:
google_api_availability:
google_maps_flutter:
intl: intl:
latlong2: latlong2:
material_design_icons_flutter: material_design_icons_flutter:

9
scripts/apply_flavor_huawei.sh Executable file
View file

@ -0,0 +1,9 @@
#!/bin/bash
PUBSPEC_PATH="../pubspec.yaml"
flutter clean
sed -i 's/aves_services_google/aves_services_huawei/g' "$PUBSPEC_PATH"
sed -i 's/aves_report_crashlytics/aves_report_console/g' "$PUBSPEC_PATH"
flutter pub get

View file

@ -3,6 +3,7 @@ PUBSPEC_PATH="../pubspec.yaml"
flutter clean flutter clean
sed -i 's/aves_services_huawei/aves_services_google/g' "$PUBSPEC_PATH"
sed -i 's/aves_report_crashlytics/aves_report_console/g' "$PUBSPEC_PATH" sed -i 's/aves_report_crashlytics/aves_report_console/g' "$PUBSPEC_PATH"
flutter pub get flutter pub get

View file

@ -3,6 +3,7 @@ PUBSPEC_PATH="../pubspec.yaml"
flutter clean flutter clean
sed -i 's/aves_services_huawei/aves_services_google/g' "$PUBSPEC_PATH"
sed -i 's/aves_report_console/aves_report_crashlytics/g' "$PUBSPEC_PATH" sed -i 's/aves_report_console/aves_report_crashlytics/g' "$PUBSPEC_PATH"
flutter pub get flutter pub get

View file

@ -1,6 +1,6 @@
import 'package:aves/l10n/l10n.dart'; import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/settings/enums/coordinate_format.dart'; import 'package:aves/model/settings/enums/coordinate_format.dart';
import 'package:aves/utils/geo_utils.dart'; import 'package:aves_map/aves_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';

View file

@ -3,6 +3,7 @@ import 'package:aves/model/settings/defaults.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves_map/src/style.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';

View file

@ -4,6 +4,7 @@ import 'package:aves/main_play.dart' as app;
import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/defaults.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves_map/src/style.dart';
import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';