Compare commits

..

30 Commits
0.2.2 ... 0.3.2

Author SHA1 Message Date
Johan von Forstner
19a8b5c9fe Release 0.3.2 2020-08-12 19:50:54 +02:00
Johan von Forstner
7f3c481dcb allow to configure API keys with Gradle properties
(necessary for F-Droid)
2020-08-12 19:50:31 +02:00
Johan von Forstner
8a54b5cb05 SliderFilter: fix default value for min > 0 2020-08-12 19:30:57 +02:00
johan12345
91b3234a45 fix URL of sonatype snapshots repo 2020-08-12 08:23:16 +02:00
johan12345
ab7cbc981b fix URL of sonatype snapshots repo 2020-08-12 08:12:02 +02:00
johan12345
a2c1a2cf82 move signingConfigs configuration for F-Droid 2020-08-11 20:10:14 +02:00
johan12345
167ede4e62 Release 0.3.1 2020-08-11 19:41:38 +02:00
johan12345
63900996e7 update .gitignore 2020-08-11 19:41:05 +02:00
johan12345
c626f3d5a5 update AnyMaps (fixes crash) 2020-08-11 19:40:22 +02:00
johan12345
8779e65846 set default map provider in google flavor back to google 2020-08-11 19:23:34 +02:00
johan12345
0c8bf84e56 adjust signingConfig configuration for compatibility with F-Droid 2020-08-11 19:18:46 +02:00
johan12345
90972cf933 fix lint errors 2020-08-10 20:49:18 +02:00
johan12345
7d9a9605fb Release 0.3.0 2020-08-10 20:43:04 +02:00
johan12345
a0bc0f2981 update dependencies 2020-08-10 20:35:53 +02:00
johan12345
f3b4c8a8ff implement donations for FOSS version (PayPal) 2020-08-10 20:31:35 +02:00
Johan von Forstner
6a8220c1c2 implement autocomplete for Mapbox 2020-08-09 17:35:31 +02:00
Johan von Forstner
84c28748a4 update travis configuration with build flavors 2020-08-09 13:21:55 +02:00
Johan von Forstner
7c29b619a5 implement switching between map providers in settings
Google Maps and Mapbox
2020-08-09 13:09:48 +02:00
Johan von Forstner
ccfdbbe826 update AnyMaps 2020-08-09 12:37:31 +02:00
Johan von Forstner
7052ce3c3c fixes for Google Maps and OSM variants 2020-08-09 12:22:58 +02:00
Johan von Forstner
d73ca8aa9d Travis CI: add Mapbox API Key 2020-08-08 19:50:42 +02:00
Johan von Forstner
64703a8c28 update AnyMaps 2020-08-08 19:46:28 +02:00
Johan von Forstner
eb54658bf4 update Gradle plugin 2020-08-06 19:54:20 +02:00
Johan von Forstner
54d1c8ba61 update anymap with mapbox fixes 2020-08-06 19:54:08 +02:00
Johan von Forstner
1c04f6211f update anymap, use mapbox 2020-07-31 18:40:46 +02:00
Johan von Forstner
45497f9208 OSM: implement night mode 2020-07-24 20:27:12 +02:00
Johan von Forstner
140c634397 start splitting app in FOSS and Google variants 2020-07-23 12:20:09 +02:00
johan12345
be1b3813a9 update AnyMaps 2020-07-20 22:52:50 +02:00
johan12345
f7ed7f1e93 use AnyMaps to make the map view able to use OSM maps 2020-07-20 22:39:22 +02:00
johan12345
0df72ac4ad update Material Components library 2020-07-19 21:03:43 +02:00
59 changed files with 2059 additions and 403 deletions

3
.gitignore vendored
View File

@@ -8,5 +8,6 @@
.externalNativeBuild
.cxx
apikeys.xml
/app/release/app-release.aab
/app/**/*.aab
/app/**/*.apk
/_img/connectors/*.ai

View File

@@ -8,10 +8,11 @@ env:
global:
- secure: KYdFlMarsyXw+OHht1Atp+Kirbw9O09Ck14EjFuKb1eNtknurZ/tGEXuD+8xWh1W8W21kgHEG7s3rzru53t29buz+FW9f+ZmhEWXFP3OydyvXLw4BAVVOjm6xG2uHX/8MOGLJNM7cfaF25EPQ+kznHe84R29KaLH90mNRr2lPa4VnfbcnvDStiVaez/vJ72UoYSP5HICAzoF70yC3ZvvCK1hZv71UIysCbFE2IkxvMhG9OOGebdnRmFssaRCrvfRLjitobcLzkPWzZZIqdjNASf8/iAxX8VgGBYfVj8ID06AfMrtgXNJRCvcD0LICraQ+WPUbikMunRieGO8PNHSB5vKdPoC50aLUa0RoRb4G3QM1pR2A8xAFlIJFX2R7iY+2t24L9hRFqB98+QoQzutfkAI1T0rzem/wtpZpuan+bDawDJEHGCeYbE0aPDAl6lytgrEE9fRgV3c1jJLQzu0xIWG8YLl3iMg0hL+c0wCKXoeqrfCFS6kYmmG7W+rQp4tCZifvRbWfAXwIPQieffKxqdEuUwiUsYxdzCu9v9uU3nflEOLLuRgeMP3gV8mpur9b5GztpkfgfzcAqsF+NiY01kYgGtrgCYlMy0TxASE+UuALrtkQtU01wwhs9RH7Az0Ib3C+MT5DTjxHQCYETIViocmNEG2vfAbgHazCpGAhcY=
- secure: Hoko50vP+Mwm/O4CWvPvjMxd1gGhi+Bultjyy1WpludSmZFCfKz45Mj1EqzeYk6MeMLvGOEkSLB5wjdXdgJ9j5gEOF5K34k4vESJA7+DqDO3I7Xw9cnWgOXdFqB0qGHar0TVP3Dfg7ZcRgtmeX6t2uoELFLiS9GvTnbZXk4PCUybd979Xi8XHjEQV7+3EZbSjtsI4GFeIK1rrjd0I+UM88zYrWnz1KhdCjWvQb4iZjo+ib6NmGGEMqR7jJPRZz3KB01Y+n5h21qq9/Nv31zQIqt2B5nRxy3vBvvqKapgprIk+hVOpNnBU8w89uWUU6tYUeFk0t7z1TYWjgaBrMmGCM+aKkQ2q5F/ygNzDwB+KkpJx709Yhf0ZX8g2yvkdz+Ok7moYuvmrOPOf4E/U3BlfZxbGtRD2bGYbDgHLFYlTn6v5J2kJHDJAz31yvF5jvJejDaPp2IBVfoMRy7ZJFmGUNHGd9Se6bRwxS++AoobP5WDrBiUXNe2KKDMs3e7vbaO+hLbZ9XHpjeWIJGUfvtTee8EHZqF/8A3ju53V4/R0ehlOv2UZbpYNqcmwrsy9/R4pgMfDkG3Q054LmYrxD8DIC9b8excVMwWRP5aQ1TqZnxO2B1/vJU87RcnGl3jekeHTdHXbRq9BMV4dAdPfB9X3nGIi2GgV0iTBk+24xccOc0=
- secure: RsN/2nBVv0byMzwchuAhDti1AWKECg3Uzqk6Ahgaqg02zx8GHj60j0qNax7KuMUg70X3G4b3m8ZzndAU0wcJ98UyIku7ofUYgXHm0XYKTJwiyyhrKJ8pZ4qoeCoRkitIGIitlb84fSufVamcoLyLNbLUsO5OzL+Uscyhq/BwAhyOhj5FB9DM+GE9ntanQ4m3k4guMyMctR9CGS6Tk4LKJ1WCm0AnjTPalc+we+7yORY2J/9d6VHLKfaFXbpuWmrdnFfDZqxqcUODsxPVE0LuUtXyKQDTjjfQc8106Z/z19AJS+oLm/2UND84PD3MqsjX6EX0/9k3fY0OsoCuQAivDRmtevQ0bDQrTAyeBLcfnPCw/MYiJWyBcaYBAYK42EAfsFTDBRIduFB/Dpvg+8GuLZSdm4xVYpTlQ2pMtlGNWIGIRQsjk9LZ8swq8QBMiiF/bpMGKdfmnyQj8jkEOCWaAzkR9O+4E4Y7PuBENef9XuV0hLMryCrML2YXigxAKkEUOPhfnG+AbEY+g2kAMp+2EtXaG3tsGxoMhEYA+uRd3o2cacQMMwRpnePH5DYg6F05mrvdHPpt+P9UR1iHaHmjBPAYeksmrdP8bS088zcnePgiL6+6N0m3+l8Krihmxg5RyWjnH18IwX4RO+xg4x3cW+zaScCDbbotDMEYtChF+Hs=
before_install:
- openssl aes-256-cbc -K $encrypted_53968681344a_key -iv $encrypted_53968681344a_iv -in _ci/keystore.jks.enc -out _ci/keystore.jks -d
script:
- "./gradlew lintDebug testDebugUnitTest"
- "./gradlew lintFossDebug testFossDebugUnitTest lintGoogleDebug testGoogleDebugUnitTest"
- "./gradlew assembleRelease"
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
@@ -25,7 +26,9 @@ deploy:
provider: releases
api_key:
secure: B+V5Fz8k9HbpecyMjpJuLr8aVBrdwtDBDkQh4YQ8nu+Da4AiYwEJZseWXhOWs+oms0gNen9bBxsakQQKu7GKYDs8gIXZZtANWsc0gse8xo+cYT7NqEM3jP6mM3ytAv7VNRX3N2cdL7xazELK3/5+mghfORAAdXXYKUFGG5eTKoML8zgdPVN8E9QFqiusLXqoKhxOMCSE4NS+Di7CGlUmnidRTWg6yxhE085zljmYv2owS0NRbr5a4/zW6Z9xZPALGAqsOvIvpZHuOC2s0eMJWMmYGkK/Ws/LAVxfj4U+YkFp9hlZC0zEg/JoS19Gf57QmEu+vsoQ3uOBYBFv9NPI/R9kVH6o0hcOxId3J0u+ewSGWuceGLRpizXuMxKIvLTS5j6GWkxdSieWjwh/OuVB+ciAHNM31B7GP4FWnfz0ZaEVxI/tPenNipZdl9oXdyyBQQ00vPlYp0jT80XhaMh5rDwWMUPaEjRafvymcNyqZ0iVOr0rq1CbdT92STMSmA1U3/rmhtCMD5IGD0b+gQl+VpPKe1QXViYftVxCGL+s4ke4DUZD7HR20fGs8zu61Elnwci1HufbetKFL5TmxoKSLkWFSkzrtBaJnEruZIxhNUMkUL2UPynaOcPNzLoumjHXrUb3m3s0yE4OFelmJ6mJfXswP38sS8kj3wB7R/gC4rw=
file: app/build/outputs/apk/release/app-release.apk
file:
- app/build/outputs/apk/foss/release/app-foss-release.apk
- app/build/outputs/apk/google/release/app-google-release.apk
on:
repo: johan12345/EVMap
tags: true

6
_img/paypal.svg Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0"?>
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px"
height="24px">
<path
d="M20.055,7.713c-0.677-0.842-1.673-1.41-2.764-1.615C16.773,3.971,14.877,3,13.045,3H7.057C6.425,3,5.878,3.409,5.689,4.009 c-0.015,0.04-0.026,0.082-0.036,0.125L3.034,16.262c-0.009,0.041-0.015,0.083-0.019,0.125C3.006,16.449,3,16.513,3,16.56 C3,17.354,3.648,18,4.444,18h2.316l-0.267,1.262c-0.008,0.04-0.014,0.081-0.018,0.121c-0.009,0.063-0.016,0.126-0.016,0.173 C6.461,20.353,7.109,21,7.905,21h3.259c0.056,0,0.111-0.005,0.166-0.015c0.549-0.063,1.011-0.437,1.191-0.963 c0.021-0.05,0.038-0.103,0.05-0.156L13.475,16h1.398c3.365,0,5.38-1.445,5.989-4.295C21.278,9.752,20.653,8.456,20.055,7.713z M5.137,16L7.512,5h5.533c0.293,0,1.5,0.061,2.078,1.013h-4.706c-0.626,0-1.17,0.401-1.363,0.99c-0.019,0.049-0.033,0.1-0.043,0.151 l-1.034,5.093L7.183,16H5.137z M18.906,11.287C18.5,13.188,17.293,14,14.873,14h-1.857c-0.823,0-1.338,0.652-1.405,1.198L10.721,19 H8.594l1.271-6h1.444c4.259,0,5.665-2.394,6.094-4.402c0.027-0.128,0.045-0.256,0.057-0.382c0.378,0.151,0.749,0.393,1.038,0.751 C18.971,9.557,19.108,10.337,18.906,11.287z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -13,14 +13,23 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 29
versionCode 20
versionName "0.2.2"
versionCode 23
versionName "0.3.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
release
release {
def isRunningOnTravis = System.getenv("CI") == "true"
if (isRunningOnTravis) {
// configure keystore
storeFile = file("../_ci/keystore.jks")
storePassword = System.getenv("keystore_password")
keyAlias = System.getenv("keystore_alias")
keyPassword = System.getenv("keystore_alias_password")
}
}
}
buildTypes {
@@ -35,13 +44,16 @@ android {
}
}
def isRunningOnTravis = System.getenv("CI") == "true"
if (isRunningOnTravis) {
// configure keystore
signingConfigs.release.storeFile = file("../_ci/keystore.jks")
signingConfigs.release.storePassword = System.getenv("keystore_password")
signingConfigs.release.keyAlias = System.getenv("keystore_alias")
signingConfigs.release.keyPassword = System.getenv("keystore_alias_password")
flavorDimensions "dependencies"
productFlavors {
foss {
dimension "dependencies"
versionNameSuffix "-foss"
}
google {
dimension "dependencies"
versionNameSuffix "-google"
}
}
compileOptions {
@@ -61,29 +73,33 @@ android {
// add API keys from environment variable if not set in apikeys.xml
applicationVariants.all { variant ->
ext.env = System.getenv()
def goingelectricKey = env.GOINGELECTRIC_API_KEY
def goingelectricKey = env.GOINGELECTRIC_API_KEY ?: project.findProperty("GOINGELECTRIC_API_KEY")
if (goingelectricKey != null) {
variant.resValue "string", "goingelectric_key", goingelectricKey
}
def googleMapsKey = env.GOOGLE_MAPS_API_KEY
def googleMapsKey = env.GOOGLE_MAPS_API_KEY ?: project.findProperty("GOOGLE_MAPS_API_KEY")
if (googleMapsKey != null) {
variant.resValue "string", "google_maps_key", googleMapsKey
}
def mapboxKey = env.MAPBOX_API_KEY ?: project.findProperty("MAPBOX_API_KEY")
if (mapboxKey != null) {
variant.resValue "string", "mapbox_key", mapboxKey
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.1'
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.core:core:1.3.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.2.0-beta01'
implementation 'com.google.android.material:material:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.browser:browser:1.2.0'
@@ -97,21 +113,30 @@ dependencies {
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
implementation 'com.airbnb.android:lottie:3.4.0'
implementation 'io.michaelrocks:bimap:1.0.2'
implementation 'com.mapzen.android:lost:3.0.2'
// AnyMaps
def anyMapsVersion = 'e6e014dd11'
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
// Google Maps v3 Beta
implementation 'com.google.android.libraries.maps:maps:3.1.0-beta'
implementation name:'places-maps-sdk-3.1.0-beta', ext:'aar'
implementation 'com.google.maps.android:android-maps-utils-v3:1.3.3'
implementation 'com.android.volley:volley:1.1.1'
implementation 'com.google.android.gms:play-services-base:17.3.0'
implementation 'com.google.android.gms:play-services-basement:17.3.0'
implementation 'com.google.android.gms:play-services-gcm:17.0.0'
implementation 'com.google.android.gms:play-services-location:17.0.0'
implementation 'com.google.android.gms:play-services-tasks:17.1.0'
implementation 'com.google.auto.value:auto-value-annotations:1.6.3'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.google.android.datatransport:transport-runtime:2.2.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
googleImplementation 'com.google.android.libraries.maps:maps:3.1.0-beta'
googleImplementation name:'places-maps-sdk-3.1.0-beta', ext:'aar'
googleImplementation 'com.android.volley:volley:1.1.1'
googleImplementation 'com.google.android.gms:play-services-base:17.3.0'
googleImplementation 'com.google.android.gms:play-services-basement:17.3.0'
googleImplementation 'com.google.android.gms:play-services-gcm:17.0.0'
googleImplementation 'com.google.android.gms:play-services-location:17.0.0'
googleImplementation 'com.google.android.gms:play-services-tasks:17.1.0'
googleImplementation 'com.google.auto.value:auto-value-annotations:1.6.3'
googleImplementation 'com.google.code.gson:gson:2.8.6'
googleImplementation 'com.google.android.datatransport:transport-runtime:2.2.3'
googleImplementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
// Mapbox places (autocomplete)
implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-places-v9:0.12.0'
// navigation library
def nav_version = "2.3.0"
@@ -131,8 +156,8 @@ dependencies {
// billing library
def billing_version = "3.0.0"
implementation "com.android.billingclient:billing:$billing_version"
implementation "com.android.billingclient:billing-ktx:$billing_version"
googleImplementation "com.android.billingclient:billing:$billing_version"
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
// debug tools
implementation 'com.facebook.stetho:stetho:1.5.1'

View File

@@ -0,0 +1,12 @@
package net.vonforst.evmap
import android.app.Activity
import android.content.Context
fun init(context: Context) {
}
fun checkPlayServices(activity: Activity): Boolean {
return true
}

View File

@@ -0,0 +1,49 @@
package net.vonforst.evmap.autocomplete
import android.content.Context
import android.content.Intent
import android.view.inputmethod.InputMethodManager
import androidx.fragment.app.Fragment
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import com.mapbox.geojson.BoundingBox
import com.mapbox.geojson.Point
import com.mapbox.mapboxsdk.plugins.places.autocomplete.PlaceAutocomplete
import com.mapbox.mapboxsdk.plugins.places.autocomplete.model.PlaceOptions
import net.vonforst.evmap.R
import net.vonforst.evmap.fragment.REQUEST_AUTOCOMPLETE
import net.vonforst.evmap.viewmodel.PlaceWithBounds
fun launchAutocomplete(fragment: Fragment) {
val placeOptions = PlaceOptions.builder()
.build(PlaceOptions.MODE_CARDS)
val intent = PlaceAutocomplete.IntentBuilder()
.accessToken(fragment.getString(R.string.mapbox_key))
.placeOptions(placeOptions)
.build(fragment.requireActivity())
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
fragment.startActivityForResult(intent, REQUEST_AUTOCOMPLETE)
// show keyboard
val imm = fragment.requireContext()
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(0, 0)
}
fun handleAutocompleteResult(intent: Intent): PlaceWithBounds? {
val place = PlaceAutocomplete.getPlace(intent) ?: return null
val bbox = place.bbox()?.toLatLngBounds()
val center = place.center()!!.toLatLng()
return PlaceWithBounds(center, bbox)
}
private fun BoundingBox.toLatLngBounds(): LatLngBounds {
return LatLngBounds(
southwest().toLatLng(),
northeast().toLatLng()
)
}
private fun Point.toLatLng(): LatLng = LatLng(this.latitude(), this.longitude())

View File

@@ -0,0 +1,37 @@
package net.vonforst.evmap.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import kotlinx.android.synthetic.foss.fragment_donate.*
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
class DonateFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_donate, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(requireActivity() as AppCompatActivity).setSupportActionBar(toolbar)
val navController = findNavController()
toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
btnDonate.setOnClickListener {
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link))
}
}
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/linearLayout2"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>
<Button
android:id="@+id/btnDonate"
style="@style/Widget.MaterialComponents.Button.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/donate_paypal"
app:icon="@drawable/ic_paypal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView20" />
<TextView
android:id="@+id/textView20"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/donations_info"
app:layout_constraintBottom_toTopOf="@+id/btnDonate"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
app:layout_constraintVertical_chainStyle="packed" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_map_provider_names">
<item>OpenStreetMap (Mapbox)</item>
</string-array>
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 30% Gebühren ab.</string>
<string name="donate_paypal">Mit PayPal spenden</string>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_map_provider_names">
<item>OpenStreetMap (Mapbox)</item>
</string-array>
<string-array name="pref_map_provider_values" tranlatable="false">
<item>mapbox</item>
</string-array>
<string name="pref_map_provider_default" translatable="false">mapbox</string>
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.</string>
<string name="donate_paypal">Donate with PayPal</string>
<string name="paypal_link" translatable="false">https://paypal.me/johan98</string>
</resources>

View File

@@ -0,0 +1,27 @@
package net.vonforst.evmap
import android.app.Activity
import android.content.Context
import android.util.Log
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.libraries.places.api.Places
fun init(context: Context) {
Places.initialize(context, context.getString(R.string.google_maps_key));
}
fun checkPlayServices(activity: Activity): Boolean {
val request = 9000
val apiAvailability = GoogleApiAvailability.getInstance()
val resultCode = apiAvailability.isGooglePlayServicesAvailable(activity)
if (resultCode != ConnectionResult.SUCCESS) {
if (apiAvailability.isUserResolvableError(resultCode)) {
apiAvailability.getErrorDialog(activity, resultCode, request).show()
} else {
Log.d("EVMap", "This device is not supported.")
}
return false
}
return true
}

View File

@@ -0,0 +1,8 @@
package net.vonforst.evmap.adapter
import net.vonforst.evmap.R
import net.vonforst.evmap.viewmodel.DonationItem
class DonationAdapter() : DataBindingAdapter<DonationItem>() {
override fun getItemViewType(position: Int): Int = R.layout.item_donation
}

View File

@@ -0,0 +1,33 @@
package net.vonforst.evmap.autocomplete
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.view.inputmethod.InputMethodManager
import androidx.fragment.app.Fragment
import com.car2go.maps.google.adapter.AnyMapAdapter
import com.google.android.libraries.places.api.model.Place
import com.google.android.libraries.places.widget.Autocomplete
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode
import net.vonforst.evmap.fragment.REQUEST_AUTOCOMPLETE
import net.vonforst.evmap.viewmodel.PlaceWithBounds
fun launchAutocomplete(fragment: Fragment) {
val fields = listOf(Place.Field.LAT_LNG, Place.Field.VIEWPORT)
val intent: Intent = Autocomplete.IntentBuilder(
AutocompleteActivityMode.OVERLAY, fields
)
.build(fragment.requireActivity())
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
fragment.startActivityForResult(intent, REQUEST_AUTOCOMPLETE)
// show keyboard
val imm = fragment.requireContext()
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(0, 0)
}
fun handleAutocompleteResult(intent: Intent): PlaceWithBounds? {
val place = Autocomplete.getPlaceFromIntent(intent)
return PlaceWithBounds(AnyMapAdapter.adapt(place.latLng), AnyMapAdapter.adapt(place.viewport))
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_map_provider_names">
<item>Google Maps</item>
<item>OpenStreetMap (Mapbox)</item>
</string-array>
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 30% Gebühren ab.</string>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_map_provider_names">
<item>Google Maps</item>
<item>OpenStreetMap (Mapbox)</item>
</string-array>
<string-array name="pref_map_provider_values" tranlatable="false">
<item>google</item>
<item>mapbox</item>
</string-array>
<string name="pref_map_provider_default" translatable="false">google</string>
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 30% off every donation.</string>
</resources>

View File

@@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.vonforst.evmap">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
@@ -24,6 +25,9 @@
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key" />
<meta-data
android:name="com.mapbox.ACCESS_TOKEN"
android:value="@string/mapbox_key" />
<activity
android:name=".MapsActivity"

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.clustering;
import com.car2go.maps.model.LatLng;
import java.util.Collection;
/**
* A collection of ClusterItems that are nearby each other.
*/
public interface Cluster<T extends ClusterItem> {
LatLng getPosition();
Collection<T> getItems();
int getSize();
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.clustering;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.car2go.maps.model.LatLng;
/**
* ClusterItem represents a marker on the map.
*/
public interface ClusterItem {
/**
* The position of this marker. This must always return the same value.
*/
@NonNull
LatLng getPosition();
/**
* The title of this marker.
*/
@Nullable
String getTitle();
/**
* The description of this marker.
*/
@Nullable
String getSnippet();
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2020 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.clustering.algo;
import com.google.maps.android.clustering.ClusterItem;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Base Algorithm class that implements lock/unlock functionality.
*/
public abstract class AbstractAlgorithm<T extends ClusterItem> implements Algorithm<T> {
private final ReadWriteLock mLock = new ReentrantReadWriteLock();
@Override
public void lock() {
mLock.writeLock().lock();
}
@Override
public void unlock() {
mLock.writeLock().unlock();
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.clustering.algo;
import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.clustering.ClusterItem;
import java.util.Collection;
import java.util.Set;
/**
* Logic for computing clusters
*/
public interface Algorithm<T extends ClusterItem> {
/**
* Adds an item to the algorithm
*
* @param item the item to be added
* @return true if the algorithm contents changed as a result of the call
*/
boolean addItem(T item);
/**
* Adds a collection of items to the algorithm
*
* @param items the items to be added
* @return true if the algorithm contents changed as a result of the call
*/
boolean addItems(Collection<T> items);
void clearItems();
/**
* Removes an item from the algorithm
*
* @param item the item to be removed
* @return true if this algorithm contained the specified element (or equivalently, if this
* algorithm changed as a result of the call).
*/
boolean removeItem(T item);
/**
* Updates the provided item in the algorithm
*
* @param item the item to be updated
* @return true if the item existed in the algorithm and was updated, or false if the item did
* not exist in the algorithm and the algorithm contents remain unchanged.
*/
boolean updateItem(T item);
/**
* Removes a collection of items from the algorithm
*
* @param items the items to be removed
* @return true if this algorithm contents changed as a result of the call
*/
boolean removeItems(Collection<T> items);
Set<? extends Cluster<T>> getClusters(float zoom);
Collection<T> getItems();
void setMaxDistanceBetweenClusteredItems(int maxDistance);
int getMaxDistanceBetweenClusteredItems();
void lock();
void unlock();
}

View File

@@ -0,0 +1,314 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.clustering.algo;
import com.car2go.maps.model.LatLng;
import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.clustering.ClusterItem;
import com.google.maps.android.geometry.Bounds;
import com.google.maps.android.geometry.Point;
import com.google.maps.android.projection.SphericalMercatorProjection;
import com.google.maps.android.quadtree.PointQuadTree;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* A simple clustering algorithm with O(nlog n) performance. Resulting clusters are not
* hierarchical.
* <p/>
* High level algorithm:<br>
* 1. Iterate over items in the order they were added (candidate clusters).<br>
* 2. Create a cluster with the center of the item. <br>
* 3. Add all items that are within a certain distance to the cluster. <br>
* 4. Move any items out of an existing cluster if they are closer to another cluster. <br>
* 5. Remove those items from the list of candidate clusters.
* <p/>
* Clusters have the center of the first element (not the centroid of the items within it).
*/
public class NonHierarchicalDistanceBasedAlgorithm<T extends ClusterItem> extends AbstractAlgorithm<T> {
private static final int DEFAULT_MAX_DISTANCE_AT_ZOOM = 100; // essentially 100 dp.
private int mMaxDistance = DEFAULT_MAX_DISTANCE_AT_ZOOM;
/**
* Any modifications should be synchronized on mQuadTree.
*/
private final Collection<QuadItem<T>> mItems = new LinkedHashSet<>();
/**
* Any modifications should be synchronized on mQuadTree.
*/
private final PointQuadTree<QuadItem<T>> mQuadTree = new PointQuadTree<>(0, 1, 0, 1);
private static final SphericalMercatorProjection PROJECTION = new SphericalMercatorProjection(1);
/**
* Adds an item to the algorithm
*
* @param item the item to be added
* @return true if the algorithm contents changed as a result of the call
*/
@Override
public boolean addItem(T item) {
boolean result;
final QuadItem<T> quadItem = new QuadItem<>(item);
synchronized (mQuadTree) {
result = mItems.add(quadItem);
if (result) {
mQuadTree.add(quadItem);
}
}
return result;
}
/**
* Adds a collection of items to the algorithm
*
* @param items the items to be added
* @return true if the algorithm contents changed as a result of the call
*/
@Override
public boolean addItems(Collection<T> items) {
boolean result = false;
for (T item : items) {
boolean individualResult = addItem(item);
if (individualResult) {
result = true;
}
}
return result;
}
@Override
public void clearItems() {
synchronized (mQuadTree) {
mItems.clear();
mQuadTree.clear();
}
}
/**
* Removes an item from the algorithm
*
* @param item the item to be removed
* @return true if this algorithm contained the specified element (or equivalently, if this
* algorithm changed as a result of the call).
*/
@Override
public boolean removeItem(T item) {
boolean result;
// QuadItem delegates hashcode() and equals() to its item so,
// removing any QuadItem to that item will remove the item
final QuadItem<T> quadItem = new QuadItem<>(item);
synchronized (mQuadTree) {
result = mItems.remove(quadItem);
if (result) {
mQuadTree.remove(quadItem);
}
}
return result;
}
/**
* Removes a collection of items from the algorithm
*
* @param items the items to be removed
* @return true if this algorithm contents changed as a result of the call
*/
@Override
public boolean removeItems(Collection<T> items) {
boolean result = false;
synchronized (mQuadTree) {
for (T item : items) {
// QuadItem delegates hashcode() and equals() to its item so,
// removing any QuadItem to that item will remove the item
final QuadItem<T> quadItem = new QuadItem<>(item);
boolean individualResult = mItems.remove(quadItem);
if (individualResult) {
mQuadTree.remove(quadItem);
result = true;
}
}
}
return result;
}
/**
* Updates the provided item in the algorithm
*
* @param item the item to be updated
* @return true if the item existed in the algorithm and was updated, or false if the item did
* not exist in the algorithm and the algorithm contents remain unchanged.
*/
@Override
public boolean updateItem(T item) {
// TODO - Can this be optimized to update the item in-place if the location hasn't changed?
boolean result;
synchronized (mQuadTree) {
result = removeItem(item);
if (result) {
// Only add the item if it was removed (to help prevent accidental duplicates on map)
result = addItem(item);
}
}
return result;
}
@Override
public Set<? extends Cluster<T>> getClusters(float zoom) {
final int discreteZoom = (int) zoom;
final double zoomSpecificSpan = mMaxDistance / Math.pow(2, discreteZoom) / 256;
final Set<QuadItem<T>> visitedCandidates = new HashSet<>();
final Set<Cluster<T>> results = new HashSet<>();
final Map<QuadItem<T>, Double> distanceToCluster = new HashMap<>();
final Map<QuadItem<T>, StaticCluster<T>> itemToCluster = new HashMap<>();
synchronized (mQuadTree) {
for (QuadItem<T> candidate : getClusteringItems(mQuadTree, zoom)) {
if (visitedCandidates.contains(candidate)) {
// Candidate is already part of another cluster.
continue;
}
Bounds searchBounds = createBoundsFromSpan(candidate.getPoint(), zoomSpecificSpan);
Collection<QuadItem<T>> clusterItems;
clusterItems = mQuadTree.search(searchBounds);
if (clusterItems.size() == 1) {
// Only the current marker is in range. Just add the single item to the results.
results.add(candidate);
visitedCandidates.add(candidate);
distanceToCluster.put(candidate, 0d);
continue;
}
StaticCluster<T> cluster = new StaticCluster<>(candidate.mClusterItem.getPosition());
results.add(cluster);
for (QuadItem<T> clusterItem : clusterItems) {
Double existingDistance = distanceToCluster.get(clusterItem);
double distance = distanceSquared(clusterItem.getPoint(), candidate.getPoint());
if (existingDistance != null) {
// Item already belongs to another cluster. Check if it's closer to this cluster.
if (existingDistance < distance) {
continue;
}
// Move item to the closer cluster.
itemToCluster.get(clusterItem).remove(clusterItem.mClusterItem);
}
distanceToCluster.put(clusterItem, distance);
cluster.add(clusterItem.mClusterItem);
itemToCluster.put(clusterItem, cluster);
}
visitedCandidates.addAll(clusterItems);
}
}
return results;
}
protected Collection<QuadItem<T>> getClusteringItems(PointQuadTree<QuadItem<T>> quadTree, float zoom) {
return mItems;
}
@Override
public Collection<T> getItems() {
final Set<T> items = new LinkedHashSet<>();
synchronized (mQuadTree) {
for (QuadItem<T> quadItem : mItems) {
items.add(quadItem.mClusterItem);
}
}
return items;
}
@Override
public void setMaxDistanceBetweenClusteredItems(int maxDistance) {
mMaxDistance = maxDistance;
}
@Override
public int getMaxDistanceBetweenClusteredItems() {
return mMaxDistance;
}
private double distanceSquared(Point a, Point b) {
return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
}
private Bounds createBoundsFromSpan(Point p, double span) {
// TODO: Use a span that takes into account the visual size of the marker, not just its
// LatLng.
double halfSpan = span / 2;
return new Bounds(
p.x - halfSpan, p.x + halfSpan,
p.y - halfSpan, p.y + halfSpan);
}
protected static class QuadItem<T extends ClusterItem> implements PointQuadTree.Item, Cluster<T> {
private final T mClusterItem;
private final Point mPoint;
private final LatLng mPosition;
private Set<T> singletonSet;
private QuadItem(T item) {
mClusterItem = item;
mPosition = item.getPosition();
mPoint = PROJECTION.toPoint(mPosition);
singletonSet = Collections.singleton(mClusterItem);
}
@Override
public Point getPoint() {
return mPoint;
}
@Override
public LatLng getPosition() {
return mPosition;
}
@Override
public Set<T> getItems() {
return singletonSet;
}
@Override
public int getSize() {
return 1;
}
@Override
public int hashCode() {
return mClusterItem.hashCode();
}
@Override
public boolean equals(Object other) {
if (!(other instanceof QuadItem<?>)) {
return false;
}
return ((QuadItem<?>) other).mClusterItem.equals(mClusterItem);
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.clustering.algo;
import com.car2go.maps.model.LatLng;
import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.clustering.ClusterItem;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* A cluster whose center is determined upon creation.
*/
public class StaticCluster<T extends ClusterItem> implements Cluster<T> {
private final LatLng mCenter;
private final List<T> mItems = new ArrayList<T>();
public StaticCluster(LatLng center) {
mCenter = center;
}
public boolean add(T t) {
return mItems.add(t);
}
@Override
public LatLng getPosition() {
return mCenter;
}
public boolean remove(T t) {
return mItems.remove(t);
}
@Override
public Collection<T> getItems() {
return mItems;
}
@Override
public int getSize() {
return mItems.size();
}
@Override
public String toString() {
return "StaticCluster{" +
"mCenter=" + mCenter +
", mItems.size=" + mItems.size() +
'}';
}
@Override
public int hashCode() {
return mCenter.hashCode() + mItems.hashCode();
}
@Override
public boolean equals(Object other) {
if (!(other instanceof StaticCluster<?>)) {
return false;
}
return ((StaticCluster<?>) other).mCenter.equals(mCenter)
&& ((StaticCluster<?>) other).mItems.equals(mItems);
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.geometry;
/**
* Represents an area in the cartesian plane.
*/
public class Bounds {
public final double minX;
public final double minY;
public final double maxX;
public final double maxY;
public final double midX;
public final double midY;
public Bounds(double minX, double maxX, double minY, double maxY) {
this.minX = minX;
this.minY = minY;
this.maxX = maxX;
this.maxY = maxY;
midX = (minX + maxX) / 2;
midY = (minY + maxY) / 2;
}
public boolean contains(double x, double y) {
return minX <= x && x <= maxX && minY <= y && y <= maxY;
}
public boolean contains(Point point) {
return contains(point.x, point.y);
}
public boolean intersects(double minX, double maxX, double minY, double maxY) {
return minX < this.maxX && this.minX < maxX && minY < this.maxY && this.minY < maxY;
}
public boolean intersects(Bounds bounds) {
return intersects(bounds.minX, bounds.maxX, bounds.minY, bounds.maxY);
}
public boolean contains(Bounds bounds) {
return bounds.minX >= minX && bounds.maxX <= maxX && bounds.minY >= minY && bounds.maxY <= maxY;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.geometry;
public class Point {
public final double x;
public final double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.projection;
/**
* @deprecated since 0.2. Use {@link com.google.maps.android.geometry.Point} instead.
*/
@Deprecated
public class Point extends com.google.maps.android.geometry.Point {
public Point(double x, double y) {
super(x, y);
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.projection;
import com.car2go.maps.model.LatLng;
public class SphericalMercatorProjection {
final double mWorldWidth;
public SphericalMercatorProjection(final double worldWidth) {
mWorldWidth = worldWidth;
}
@SuppressWarnings("deprecation")
public Point toPoint(final LatLng latLng) {
final double x = latLng.longitude / 360 + .5;
final double siny = Math.sin(Math.toRadians(latLng.latitude));
final double y = 0.5 * Math.log((1 + siny) / (1 - siny)) / -(2 * Math.PI) + .5;
return new Point(x * mWorldWidth, y * mWorldWidth);
}
public LatLng toLatLng(com.google.maps.android.geometry.Point point) {
final double x = point.x / mWorldWidth - 0.5;
final double lng = x * 360;
double y = .5 - (point.y / mWorldWidth);
final double lat = 90 - Math.toDegrees(Math.atan(Math.exp(-y * 2 * Math.PI)) * 2);
return new LatLng(lat, lng);
}
}

View File

@@ -0,0 +1,226 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.quadtree;
import com.google.maps.android.geometry.Bounds;
import com.google.maps.android.geometry.Point;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* A quad tree which tracks items with a Point geometry.
* See http://en.wikipedia.org/wiki/Quadtree for details on the data structure.
* This class is not thread safe.
*/
public class PointQuadTree<T extends PointQuadTree.Item> {
public interface Item {
Point getPoint();
}
/**
* The bounds of this quad.
*/
private final Bounds mBounds;
/**
* The depth of this quad in the tree.
*/
private final int mDepth;
/**
* Maximum number of elements to store in a quad before splitting.
*/
private final static int MAX_ELEMENTS = 50;
/**
* The elements inside this quad, if any.
*/
private Set<T> mItems;
/**
* Maximum depth.
*/
private final static int MAX_DEPTH = 40;
/**
* Child quads.
*/
private List<PointQuadTree<T>> mChildren = null;
/**
* Creates a new quad tree with specified bounds.
*
* @param minX
* @param maxX
* @param minY
* @param maxY
*/
public PointQuadTree(double minX, double maxX, double minY, double maxY) {
this(new Bounds(minX, maxX, minY, maxY));
}
public PointQuadTree(Bounds bounds) {
this(bounds, 0);
}
private PointQuadTree(double minX, double maxX, double minY, double maxY, int depth) {
this(new Bounds(minX, maxX, minY, maxY), depth);
}
private PointQuadTree(Bounds bounds, int depth) {
mBounds = bounds;
mDepth = depth;
}
/**
* Insert an item.
*/
public void add(T item) {
Point point = item.getPoint();
if (this.mBounds.contains(point.x, point.y)) {
insert(point.x, point.y, item);
}
}
private void insert(double x, double y, T item) {
if (this.mChildren != null) {
if (y < mBounds.midY) {
if (x < mBounds.midX) { // top left
mChildren.get(0).insert(x, y, item);
} else { // top right
mChildren.get(1).insert(x, y, item);
}
} else {
if (x < mBounds.midX) { // bottom left
mChildren.get(2).insert(x, y, item);
} else {
mChildren.get(3).insert(x, y, item);
}
}
return;
}
if (mItems == null) {
mItems = new LinkedHashSet<>();
}
mItems.add(item);
if (mItems.size() > MAX_ELEMENTS && mDepth < MAX_DEPTH) {
split();
}
}
/**
* Split this quad.
*/
private void split() {
mChildren = new ArrayList<PointQuadTree<T>>(4);
mChildren.add(new PointQuadTree<T>(mBounds.minX, mBounds.midX, mBounds.minY, mBounds.midY, mDepth + 1));
mChildren.add(new PointQuadTree<T>(mBounds.midX, mBounds.maxX, mBounds.minY, mBounds.midY, mDepth + 1));
mChildren.add(new PointQuadTree<T>(mBounds.minX, mBounds.midX, mBounds.midY, mBounds.maxY, mDepth + 1));
mChildren.add(new PointQuadTree<T>(mBounds.midX, mBounds.maxX, mBounds.midY, mBounds.maxY, mDepth + 1));
Set<T> items = mItems;
mItems = null;
for (T item : items) {
// re-insert items into child quads.
insert(item.getPoint().x, item.getPoint().y, item);
}
}
/**
* Remove the given item from the set.
*
* @return whether the item was removed.
*/
public boolean remove(T item) {
Point point = item.getPoint();
if (this.mBounds.contains(point.x, point.y)) {
return remove(point.x, point.y, item);
} else {
return false;
}
}
private boolean remove(double x, double y, T item) {
if (this.mChildren != null) {
if (y < mBounds.midY) {
if (x < mBounds.midX) { // top left
return mChildren.get(0).remove(x, y, item);
} else { // top right
return mChildren.get(1).remove(x, y, item);
}
} else {
if (x < mBounds.midX) { // bottom left
return mChildren.get(2).remove(x, y, item);
} else {
return mChildren.get(3).remove(x, y, item);
}
}
} else {
if (mItems == null) {
return false;
} else {
return mItems.remove(item);
}
}
}
/**
* Removes all points from the quadTree
*/
public void clear() {
mChildren = null;
if (mItems != null) {
mItems.clear();
}
}
/**
* Search for all items within a given bounds.
*/
public Collection<T> search(Bounds searchBounds) {
final List<T> results = new ArrayList<T>();
search(searchBounds, results);
return results;
}
private void search(Bounds searchBounds, Collection<T> results) {
if (!mBounds.intersects(searchBounds)) {
return;
}
if (this.mChildren != null) {
for (PointQuadTree<T> quad : mChildren) {
quad.search(searchBounds, results);
}
} else if (mItems != null) {
if (searchBounds.contains(mBounds)) {
results.addAll(mItems);
} else {
for (T item : mItems) {
if (searchBounds.contains(item.getPoint())) {
results.add(item);
}
}
}
}
}
}

View File

@@ -0,0 +1,268 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.ui;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import net.vonforst.evmap.R;
/**
* IconGenerator generates icons that contain text (or custom content) within an info
* window-like shape.
* <p/>
* The icon {@link Bitmap}s generated by the factory should be used in conjunction with a
* BitmapDescriptorFactory.
* <p/>
* This class is not thread safe.
*/
public class IconGenerator {
private final Context mContext;
private ViewGroup mContainer;
private RotationLayout mRotationLayout;
private TextView mTextView;
private View mContentView;
private int mRotation;
private float mAnchorU = 0.5f;
private float mAnchorV = 1f;
/**
* Creates a new IconGenerator with the default style.
*/
public IconGenerator(Context context) {
mContext = context;
mContainer = (ViewGroup) LayoutInflater.from(mContext).inflate(R.layout.amu_text_bubble, null);
mRotationLayout = (RotationLayout) mContainer.getChildAt(0);
mContentView = mTextView = (TextView) mRotationLayout.findViewById(R.id.amu_text);
}
/**
* Sets the text content, then creates an icon with the current style.
*
* @param text the text content to display inside the icon.
*/
public Bitmap makeIcon(CharSequence text) {
if (mTextView != null) {
mTextView.setText(text);
}
return makeIcon();
}
/**
* Creates an icon with the current content and style.
* <p/>
* This method is useful if a custom view has previously been set, or if text content is not
* applicable.
*/
public Bitmap makeIcon() {
int measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
mContainer.measure(measureSpec, measureSpec);
int measuredWidth = mContainer.getMeasuredWidth();
int measuredHeight = mContainer.getMeasuredHeight();
mContainer.layout(0, 0, measuredWidth, measuredHeight);
if (mRotation == 1 || mRotation == 3) {
measuredHeight = mContainer.getMeasuredWidth();
measuredWidth = mContainer.getMeasuredHeight();
}
Bitmap r = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888);
r.eraseColor(Color.TRANSPARENT);
Canvas canvas = new Canvas(r);
switch (mRotation) {
case 0:
// do nothing
break;
case 1:
canvas.translate(measuredWidth, 0);
canvas.rotate(90);
break;
case 2:
canvas.rotate(180, measuredWidth / 2, measuredHeight / 2);
break;
case 3:
canvas.translate(0, measuredHeight);
canvas.rotate(270);
break;
}
mContainer.draw(canvas);
return r;
}
/**
* Sets the child view for the icon.
* <p/>
* If the view contains a {@link TextView} with the id "text", operations such as {@link
* #setTextAppearance} and {@link #makeIcon(CharSequence)} will operate upon that {@link TextView}.
*/
public void setContentView(View contentView) {
mRotationLayout.removeAllViews();
mRotationLayout.addView(contentView);
mContentView = contentView;
final View view = mRotationLayout.findViewById(R.id.amu_text);
mTextView = view instanceof TextView ? (TextView) view : null;
}
/**
* Rotates the contents of the icon.
*
* @param degrees the amount the contents should be rotated, as a multiple of 90 degrees.
*/
public void setContentRotation(int degrees) {
mRotationLayout.setViewRotation(degrees);
}
/**
* Rotates the icon.
*
* @param degrees the amount the icon should be rotated, as a multiple of 90 degrees.
*/
public void setRotation(int degrees) {
mRotation = ((degrees + 360) % 360) / 90;
}
/**
* @return u coordinate of the anchor, with rotation applied.
*/
public float getAnchorU() {
return rotateAnchor(mAnchorU, mAnchorV);
}
/**
* @return v coordinate of the anchor, with rotation applied.
*/
public float getAnchorV() {
return rotateAnchor(mAnchorV, mAnchorU);
}
/**
* Rotates the anchor around (u, v) = (0, 0).
*/
private float rotateAnchor(float u, float v) {
switch (mRotation) {
case 0:
return u;
case 1:
return 1 - v;
case 2:
return 1 - u;
case 3:
return v;
}
throw new IllegalStateException();
}
/**
* Sets the text color, size, style, hint color, and highlight color from the specified
* <code>TextAppearance</code> resource.
*
* @param resid the identifier of the resource.
*/
public void setTextAppearance(Context context, int resid) {
if (mTextView != null) {
mTextView.setTextAppearance(context, resid);
}
}
/**
* Sets the text color, size, style, hint color, and highlight color from the specified
* <code>TextAppearance</code> resource.
*
* @param resid the identifier of the resource.
*/
public void setTextAppearance(int resid) {
setTextAppearance(mContext, resid);
}
/**
* Set the background to a given Drawable, or remove the background.
*
* @param background the Drawable to use as the background, or null to remove the background.
*/
@SuppressWarnings("deprecation")
// View#setBackgroundDrawable is compatible with pre-API level 16 (Jelly Bean).
public void setBackground(Drawable background) {
mContainer.setBackgroundDrawable(background);
// Force setting of padding.
// setBackgroundDrawable does not call setPadding if the background has 0 padding.
if (background != null) {
Rect rect = new Rect();
background.getPadding(rect);
mContainer.setPadding(rect.left, rect.top, rect.right, rect.bottom);
} else {
mContainer.setPadding(0, 0, 0, 0);
}
}
/**
* Sets the padding of the content view. The default padding of the content view (i.e. text
* view) is 5dp top/bottom and 10dp left/right.
*
* @param left the left padding in pixels.
* @param top the top padding in pixels.
* @param right the right padding in pixels.
* @param bottom the bottom padding in pixels.
*/
public void setContentPadding(int left, int top, int right, int bottom) {
mContentView.setPadding(left, top, right, bottom);
}
public static final int STYLE_DEFAULT = 1;
public static final int STYLE_WHITE = 2;
public static final int STYLE_RED = 3;
public static final int STYLE_BLUE = 4;
public static final int STYLE_GREEN = 5;
public static final int STYLE_PURPLE = 6;
public static final int STYLE_ORANGE = 7;
private static int getStyleColor(int style) {
switch (style) {
default:
case STYLE_DEFAULT:
case STYLE_WHITE:
return 0xffffffff;
case STYLE_RED:
return 0xffcc0000;
case STYLE_BLUE:
return 0xff0099cc;
case STYLE_GREEN:
return 0xff669900;
case STYLE_PURPLE:
return 0xff9933cc;
case STYLE_ORANGE:
return 0xffff8800;
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.FrameLayout;
/**
* RotationLayout rotates the contents of the layout by multiples of 90 degrees.
* <p/>
* May not work with padding.
*/
public class RotationLayout extends FrameLayout {
private int mRotation;
public RotationLayout(Context context) {
super(context);
}
public RotationLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RotationLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mRotation == 1 || mRotation == 3) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
/**
* @param degrees the rotation, in degrees.
*/
public void setViewRotation(int degrees) {
mRotation = ((degrees + 360) % 360) / 90;
}
@Override
public void dispatchDraw(Canvas canvas) {
if (mRotation == 0) {
super.dispatchDraw(canvas);
return;
}
if (mRotation == 1) {
canvas.translate(getWidth(), 0);
canvas.rotate(90, getWidth() / 2, 0);
canvas.translate(getHeight() / 2, getWidth() / 2);
} else if (mRotation == 2) {
canvas.rotate(180, getWidth() / 2, getHeight() / 2);
} else {
canvas.translate(0, getHeight());
canvas.rotate(270, getWidth() / 2, 0);
canvas.translate(getHeight() / 2, -getWidth() / 2);
}
super.dispatchDraw(canvas);
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import androidx.appcompat.widget.AppCompatTextView;
public class SquareTextView extends AppCompatTextView {
private int mOffsetTop = 0;
private int mOffsetLeft = 0;
public SquareTextView(Context context) {
super(context);
}
public SquareTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SquareTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int dimension = Math.max(width, height);
if (width > height) {
mOffsetTop = width - height;
mOffsetLeft = 0;
} else {
mOffsetTop = 0;
mOffsetLeft = height - width;
}
setMeasuredDimension(dimension, dimension);
}
@Override
public void draw(Canvas canvas) {
canvas.translate(mOffsetLeft / 2, mOffsetTop / 2);
super.draw(canvas);
}
}

View File

@@ -2,7 +2,6 @@ package net.vonforst.evmap
import android.app.Application
import com.facebook.stetho.Stetho
import com.google.android.libraries.places.api.Places
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.updateNightMode
@@ -11,6 +10,6 @@ class EvMapApplication : Application() {
super.onCreate()
updateNightMode(PreferenceDataSource(this))
Stetho.initializeWithDefaults(this);
Places.initialize(applicationContext, getString(R.string.google_maps_key));
init(applicationContext)
}
}

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
@@ -14,8 +13,6 @@ import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import net.vonforst.evmap.api.goingelectric.ChargeLocation
@@ -64,7 +61,7 @@ class MapsActivity : AppCompatActivity() {
prefs = PreferenceDataSource(this)
checkPlayServices()
checkPlayServices(this)
}
fun navigateTo(charger: ChargeLocation) {
@@ -110,19 +107,4 @@ class MapsActivity : AppCompatActivity() {
}
startActivity(intent)
}
private fun checkPlayServices(): Boolean {
val request = 9000
val apiAvailability = GoogleApiAvailability.getInstance()
val resultCode = apiAvailability.isGooglePlayServicesAvailable(this)
if (resultCode != ConnectionResult.SUCCESS) {
if (apiAvailability.isUserResolvableError(resultCode)) {
apiAvailability.getErrorDialog(this, resultCode, request).show()
} else {
Log.d("EVMap", "This device is not supported.")
}
return false
}
return true
}
}

View File

@@ -11,7 +11,6 @@ import net.vonforst.evmap.BR
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.viewmodel.DonationItem
import net.vonforst.evmap.viewmodel.FavoritesViewModel
interface Equatable {
@@ -89,8 +88,4 @@ class FavoritesAdapter(val vm: FavoritesViewModel) :
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
override fun getItemId(position: Int): Long = getItem(position).charger.id
}
class DonationAdapter() : DataBindingAdapter<DonationItem>() {
override fun getItemViewType(position: Int): Int = R.layout.item_donation
}

View File

@@ -2,7 +2,6 @@ package net.vonforst.evmap.fragment
import android.Manifest
import android.content.pm.PackageManager
import android.location.Location
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -16,9 +15,9 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.google.android.libraries.maps.model.LatLng
import com.car2go.maps.model.LatLng
import com.mapzen.android.lost.api.LocationServices
import com.mapzen.android.lost.api.LostApiClient
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.FavoritesAdapter
@@ -26,9 +25,9 @@ import net.vonforst.evmap.databinding.FragmentFavoritesBinding
import net.vonforst.evmap.viewmodel.FavoritesViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class FavoritesFragment : Fragment() {
class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
private lateinit var binding: FragmentFavoritesBinding
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var locationClient: LostApiClient
private val vm: FavoritesViewModel by viewModels(factoryProducer = {
viewModelFactory {
@@ -51,7 +50,8 @@ class FavoritesFragment : Fragment() {
binding.lifecycleOwner = this
binding.vm = vm
fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext())
locationClient = LostApiClient.Builder(requireContext())
.addConnectionCallbacks(this).build()
return binding.root
}
@@ -82,16 +82,23 @@ class FavoritesFragment : Fragment() {
)
}
locationClient.connect()
}
override fun onConnected() {
if (ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
fusedLocationClient.lastLocation.addOnSuccessListener { location: Location? ->
if (location != null) {
vm.location.value = LatLng(location.latitude, location.longitude)
}
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient)
if (location != null) {
vm.location.value = LatLng(location.latitude, location.longitude)
}
}
}
override fun onConnectionSuspended() {
}
}

View File

@@ -1,9 +1,8 @@
package net.vonforst.evmap.fragment
import android.Manifest
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
@@ -11,11 +10,12 @@ import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.view.*
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.annotation.RequiresPermission
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu
import androidx.core.app.ActivityCompat
import androidx.core.app.SharedElementCallback
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
@@ -33,16 +33,13 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.google.android.libraries.maps.CameraUpdateFactory
import com.google.android.libraries.maps.GoogleMap
import com.google.android.libraries.maps.OnMapReadyCallback
import com.google.android.libraries.maps.SupportMapFragment
import com.google.android.libraries.maps.model.*
import com.google.android.libraries.places.api.model.Place
import com.google.android.libraries.places.widget.Autocomplete
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode
import com.car2go.maps.AnyMap
import com.car2go.maps.MapFragment
import com.car2go.maps.OnMapReadyCallback
import com.car2go.maps.model.BitmapDescriptor
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.Marker
import com.car2go.maps.model.MarkerOptions
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.MaterialArcMotion
import com.google.android.material.transition.MaterialContainerTransform
@@ -50,6 +47,8 @@ import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
import com.mapzen.android.lost.api.LocationServices
import com.mapzen.android.lost.api.LostApiClient
import io.michaelrocks.bimap.HashBiMap
import io.michaelrocks.bimap.MutableBiMap
import kotlinx.android.synthetic.main.fragment_map.*
@@ -60,7 +59,10 @@ import net.vonforst.evmap.adapter.GalleryAdapter
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.ChargeLocationCluster
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
import net.vonforst.evmap.autocomplete.handleAutocompleteResult
import net.vonforst.evmap.autocomplete.launchAutocomplete
import net.vonforst.evmap.databinding.FragmentMapBinding
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.ClusterIconGenerator
import net.vonforst.evmap.ui.MarkerAnimator
@@ -72,7 +74,8 @@ const val ARG_CHARGER_ID = "chargerId"
const val ARG_LAT = "lat"
const val ARG_LON = "lon"
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback {
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback,
LostApiClient.ConnectionCallbacks {
private lateinit var binding: FragmentMapBinding
private val vm: MapViewModel by viewModels(factoryProducer = {
viewModelFactory {
@@ -83,13 +86,15 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
})
private val galleryVm: GalleryViewModel by activityViewModels()
private var map: GoogleMap? = null
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var mapFragment: MapFragment
private var map: AnyMap? = null
private lateinit var locationClient: LostApiClient
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
private var clusterMarkers: List<Marker> = emptyList()
private var searchResultMarker: Marker? = null
private var searchResultIcon: BitmapDescriptor? = null
private var connectionErrorSnackbar: Snackbar? = null
private var previousChargepointIds: Set<Long>? = null
@@ -125,10 +130,37 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
binding.lifecycleOwner = this
binding.vm = vm
fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext())
mapFragment = MapFragment()
val provider = PreferenceDataSource(requireContext()).mapProvider
mapFragment.setPriority(
arrayOf(
when (provider) {
"mapbox" -> MapFragment.MAPBOX
"google" -> MapFragment.GOOGLE
else -> null
},
MapFragment.GOOGLE,
MapFragment.MAPBOX
)
)
requireActivity().supportFragmentManager
.beginTransaction()
.replace(R.id.map, mapFragment)
.commit()
// reset map-related stuff (map provider may have changed)
map = null
markers.clear()
clusterMarkers = emptyList()
searchResultMarker = null
searchResultIcon = null
locationClient = LostApiClient.Builder(requireContext())
.addConnectionCallbacks(this)
.build()
locationClient.connect()
clusterIconGenerator = ClusterIconGenerator(requireContext())
chargerIconGenerator = ChargerIconGenerator(requireContext())
animator = MarkerAnimator(chargerIconGenerator)
setHasOptionsMenu(true)
postponeEnterTransition()
@@ -153,7 +185,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet)
detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
@@ -181,13 +212,17 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private fun setupClickListeners() {
binding.fabLocate.setOnClickListener {
if (!hasLocationPermission()) {
if (ContextCompat.checkSelfPermission(
requireContext(),
ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
requestPermissions(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
arrayOf(ACCESS_FINE_LOCATION),
REQUEST_LOCATION_PERMISSION
)
} else {
enableLocation(true, true)
enableLocation(moveTo = true, animate = true)
}
}
binding.fabDirections.setOnClickListener {
@@ -216,17 +251,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
}
binding.search.setOnClickListener {
val fields = listOf(Place.Field.LAT_LNG, Place.Field.VIEWPORT)
val intent: Intent = Autocomplete.IntentBuilder(
AutocompleteActivityMode.OVERLAY, fields
)
.build(requireContext())
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
startActivityForResult(intent, REQUEST_AUTOCOMPLETE)
val imm =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(0, 0)
launchAutocomplete(this)
}
binding.detailAppBar.toolbar.setNavigationOnClickListener {
bottomSheetBehavior.state = STATE_COLLAPSED
@@ -351,12 +376,21 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
if (place != null) {
if (place.viewport != null) {
map.animateCamera(CameraUpdateFactory.newLatLngBounds(place.viewport, 0))
map.animateCamera(map.cameraUpdateFactory.newLatLngBounds(place.viewport, 0))
} else {
map.animateCamera(CameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
map.animateCamera(map.cameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
}
searchResultMarker = map.addMarker(MarkerOptions().position(place.latLng!!))
if (searchResultIcon == null) {
searchResultIcon =
map.bitmapDescriptorFactory.fromResource(R.drawable.ic_map_marker)
}
searchResultMarker = map.addMarker(
MarkerOptions()
.position(place.latLng)
.icon(searchResultIcon)
.anchor(0.5f, 1f)
)
}
updateBackPressedCallback()
@@ -367,10 +401,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
updateBackPressedCallback()
})
vm.mapType.observe(viewLifecycleOwner, Observer {
map?.mapType = it
map?.setMapType(it)
})
vm.mapTrafficEnabled.observe(viewLifecycleOwner, Observer {
map?.isTrafficEnabled = it
map?.setTrafficEnabled(it)
})
updateBackPressedCallback()
@@ -528,11 +562,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}.show()
}
override fun onMapReady(map: GoogleMap) {
override fun onMapReady(map: AnyMap) {
this.map = map
map.uiSettings.isTiltGesturesEnabled = false
map.isIndoorEnabled = false
map.uiSettings.isIndoorLevelPickerEnabled = false
chargerIconGenerator = ChargerIconGenerator(requireContext(), map.bitmapDescriptorFactory)
animator = MarkerAnimator(chargerIconGenerator)
map.uiSettings.setTiltGesturesEnabled(false)
map.setIndoorEnabled(false)
map.uiSettings.setIndoorLevelPickerEnabled(false)
map.setOnCameraIdleListener {
vm.mapPosition.value = MapPosition(
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
@@ -546,7 +582,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
in clusterMarkers -> {
val newZoom = map.cameraPosition.zoom + 2
map.animateCamera(CameraUpdateFactory.newLatLngZoom(marker.position, newZoom))
map.animateCamera(
map.cameraUpdateFactory.newLatLngZoom(
marker.position,
newZoom
)
)
true
}
else -> false
@@ -564,9 +605,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
map.setMapStyle(
if (mode == Configuration.UI_MODE_NIGHT_YES) {
MapStyleOptions.loadRawResourceStyle(context, R.raw.maps_night_mode)
} else null
if (mode == Configuration.UI_MODE_NIGHT_YES) AnyMap.Style.DARK else AnyMap.Style.NORMAL
)
@@ -577,12 +616,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
if (position != null) {
val cameraUpdate =
CameraUpdateFactory.newLatLngZoom(position.bounds.center, position.zoom)
map.cameraUpdateFactory.newLatLngZoom(position.bounds.center, position.zoom)
map.moveCamera(cameraUpdate)
positionSet = true
} else if (lat != null && lon != null) {
// show given position
val cameraUpdate = CameraUpdateFactory.newLatLngZoom(LatLng(lat, lon), 16f)
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(LatLng(lat, lon), 16f)
map.moveCamera(cameraUpdate)
// show charger detail after chargers were loaded
@@ -603,13 +642,18 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
positionSet = true
}
if (hasLocationPermission()) {
if (ContextCompat.checkSelfPermission(
requireContext(),
ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
enableLocation(!positionSet, false)
positionSet = true
}
if (!positionSet) {
// center the camera on Europe
val cameraUpdate = CameraUpdateFactory.newLatLngZoom(LatLng(50.113388, 9.252536), 3.5f)
val cameraUpdate =
map.cameraUpdateFactory.newLatLngZoom(LatLng(50.113388, 9.252536), 3.5f)
map.moveCamera(cameraUpdate)
}
@@ -618,33 +662,29 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
)
}
@SuppressLint("MissingPermission")
@RequiresPermission(ACCESS_FINE_LOCATION)
private fun enableLocation(moveTo: Boolean, animate: Boolean) {
val map = this.map ?: return
map.isMyLocationEnabled = true
map.setMyLocationEnabled(true)
vm.myLocationEnabled.value = true
map.uiSettings.isMyLocationButtonEnabled = false
if (moveTo) {
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
if (location != null) {
val latLng = LatLng(location.latitude, location.longitude)
val camUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 13f)
if (animate) {
map.animateCamera(camUpdate)
} else {
map.moveCamera(camUpdate)
}
}
}
map.uiSettings.setMyLocationButtonEnabled(false)
if (moveTo && locationClient.isConnected) {
moveToCurrentLocation(map, animate)
}
}
private fun hasLocationPermission(): Boolean {
val context = context ?: return false
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
@RequiresPermission(ACCESS_FINE_LOCATION)
private fun moveToCurrentLocation(map: AnyMap, animate: Boolean) {
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient)
if (location != null) {
val latLng = LatLng(location.latitude, location.longitude)
val camUpdate = map.cameraUpdateFactory.newLatLngZoom(latLng, 13f)
if (animate) {
map.animateCamera(camUpdate)
} else {
map.moveCamera(camUpdate)
}
}
}
@Synchronized
@@ -697,7 +737,16 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val marker = map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
.visible(false)
.icon(
chargerIconGenerator.getBitmapDescriptor(
tint,
0,
255,
highlight,
fault
)
)
.anchor(0.5f, 1f)
)
animator.animateMarkerAppear(marker, tint, highlight, fault)
markers[marker] = charger
@@ -709,11 +758,19 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
map.addMarker(
MarkerOptions()
.position(LatLng(cluster.coordinates.lat, cluster.coordinates.lng))
.icon(BitmapDescriptorFactory.fromBitmap(clusterIconGenerator.makeIcon(cluster.clusterCount.toString())))
.icon(
map.bitmapDescriptorFactory.fromBitmap(
clusterIconGenerator.makeIcon(
cluster.clusterCount.toString()
)
)
)
.anchor(0.5f, 0.5f)
)
}
}
@SuppressLint("MissingPermission")
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
@@ -722,7 +779,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
when (requestCode) {
REQUEST_LOCATION_PERMISSION -> {
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
enableLocation(true, true)
enableLocation(moveTo = true, animate = true)
}
}
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
@@ -792,8 +849,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_AUTOCOMPLETE -> {
if (resultCode == Activity.RESULT_OK) {
vm.searchResult.value = Autocomplete.getPlaceFromIntent(data!!)
if (resultCode == Activity.RESULT_OK && data != null) {
vm.searchResult.value = handleAutocompleteResult(data)
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
@@ -829,4 +886,20 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
}
}
override fun onConnected() {
val map = this.map ?: return
if (vm.myLocationEnabled.value == true) {
if (ActivityCompat.checkSelfPermission(
requireContext(),
ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
moveToCurrentLocation(map, false)
}
}
}
override fun onConnectionSuspended() {
}
}

View File

@@ -2,9 +2,10 @@ package net.vonforst.evmap.storage
import android.content.Context
import androidx.preference.PreferenceManager
import net.vonforst.evmap.R
import java.time.Instant
class PreferenceDataSource(context: Context) {
class PreferenceDataSource(val context: Context) {
val sp = PreferenceManager.getDefaultSharedPreferences(context)
var navigateUseMaps: Boolean
@@ -42,4 +43,10 @@ class PreferenceDataSource(context: Context) {
val darkmode: String
get() = sp.getString("darkmode", "default")!!
val mapProvider: String
get() = sp.getString(
"map_provider",
context.getString(R.string.pref_map_provider_default)
)!!
}

View File

@@ -1,6 +1,6 @@
package net.vonforst.evmap.ui;
import com.google.android.libraries.maps.model.LatLng
import com.car2go.maps.model.LatLng
import com.google.maps.android.clustering.ClusterItem
import com.google.maps.android.clustering.algo.NonHierarchicalDistanceBasedAlgorithm
import net.vonforst.evmap.api.goingelectric.ChargeLocation

View File

@@ -11,8 +11,8 @@ import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.widget.TextViewCompat
import com.google.android.libraries.maps.model.BitmapDescriptor
import com.google.android.libraries.maps.model.BitmapDescriptorFactory
import com.car2go.maps.BitmapDescriptorFactory
import com.car2go.maps.model.BitmapDescriptor
import com.google.maps.android.ui.IconGenerator
import com.google.maps.android.ui.SquareTextView
import net.vonforst.evmap.R
@@ -32,7 +32,7 @@ class ClusterIconGenerator(context: Context) : IconGenerator(context) {
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
id = com.google.maps.android.R.id.amu_text
id = R.id.amu_text
setPadding(twelveDpi, twelveDpi, twelveDpi, twelveDpi)
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_AppCompat)
setTextColor(ContextCompat.getColor(context, android.R.color.white))
@@ -41,7 +41,7 @@ class ClusterIconGenerator(context: Context) : IconGenerator(context) {
}
class ChargerIconGenerator(val context: Context) {
class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFactory) {
data class BitmapData(
val tint: Int,
val scale: Int,
@@ -94,7 +94,7 @@ class ChargerIconGenerator(val context: Context) {
cachedImg
} else {
val bitmap = generateBitmap(data)
val bmd = BitmapDescriptorFactory.fromBitmap(bitmap)
val bmd = factory.fromBitmap(bitmap)
cache.put(data, bmd)
bmd
}

View File

@@ -5,7 +5,7 @@ import android.view.animation.BounceInterpolator
import androidx.core.animation.addListener
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import com.google.android.libraries.maps.model.Marker
import com.car2go.maps.model.Marker
import net.vonforst.evmap.R
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import kotlin.math.max
@@ -22,7 +22,7 @@ fun getMarkerTint(
}
class MarkerAnimator(val gen: ChargerIconGenerator) {
private val animatingMarkers = hashMapOf<String, ValueAnimator>()
private val animatingMarkers = hashMapOf<Marker, ValueAnimator>()
fun animateMarkerAppear(
marker: Marker,
@@ -30,9 +30,9 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
highlight: Boolean,
fault: Boolean
) {
animatingMarkers[marker.id]?.let {
animatingMarkers[marker]?.let {
it.cancel()
animatingMarkers.remove(marker.id)
animatingMarkers.remove(marker)
}
val anim = ValueAnimator.ofInt(0, 20).apply {
@@ -48,15 +48,14 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
fault = fault
)
)
marker.isVisible = true
}
addListener(onEnd = {
animatingMarkers.remove(marker.id)
animatingMarkers.remove(marker)
}, onCancel = {
animatingMarkers.remove(marker.id)
animatingMarkers.remove(marker)
})
}
animatingMarkers[marker.id] = anim
animatingMarkers[marker] = anim
anim.start()
}
@@ -66,9 +65,9 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
highlight: Boolean,
fault: Boolean
) {
animatingMarkers[marker.id]?.let {
animatingMarkers[marker]?.let {
it.cancel()
animatingMarkers.remove(marker.id)
animatingMarkers.remove(marker)
}
val anim = ValueAnimator.ofInt(20, 0).apply {
@@ -87,28 +86,28 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
}
addListener(onEnd = {
marker.remove()
animatingMarkers.remove(marker.id)
animatingMarkers.remove(marker)
}, onCancel = {
marker.remove()
animatingMarkers.remove(marker.id)
animatingMarkers.remove(marker)
})
}
animatingMarkers[marker.id] = anim
animatingMarkers[marker] = anim
anim.start()
}
fun deleteMarker(marker: Marker) {
animatingMarkers[marker.id]?.let {
animatingMarkers[marker]?.let {
it.cancel()
animatingMarkers.remove(marker.id)
animatingMarkers.remove(marker)
}
marker.remove()
}
fun animateMarkerBounce(marker: Marker) {
animatingMarkers[marker.id]?.let {
animatingMarkers[marker]?.let {
it.cancel()
animatingMarkers.remove(marker.id)
animatingMarkers.remove(marker)
}
val anim = ValueAnimator.ofFloat(0f, 1f).apply {
@@ -119,12 +118,12 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
marker.setAnchor(0.5f, 1.0f + t)
}
addListener(onEnd = {
animatingMarkers.remove(marker.id)
animatingMarkers.remove(marker)
}, onCancel = {
animatingMarkers.remove(marker.id)
animatingMarkers.remove(marker)
})
}
animatingMarkers[marker.id] = anim
animatingMarkers[marker] = anim
anim.start()
}
}

View File

@@ -2,7 +2,7 @@ package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.*
import com.google.android.libraries.maps.model.LatLng
import com.car2go.maps.model.LatLng
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch

View File

@@ -193,7 +193,7 @@ data class SliderFilter(
val unit: String? = ""
) : Filter<SliderFilterValue>() {
override val valueClass: KClass<SliderFilterValue> = SliderFilterValue::class
override fun defaultValue() = SliderFilterValue(key, 0)
override fun defaultValue() = SliderFilterValue(key, min)
}
sealed class FilterValue : BaseObservable(), Equatable {
@@ -230,4 +230,4 @@ data class SliderFilterValue(
var value: Int
) : FilterValue()
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable

View File

@@ -2,9 +2,9 @@ package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.*
import com.google.android.libraries.maps.GoogleMap
import com.google.android.libraries.maps.model.LatLngBounds
import com.google.android.libraries.places.api.model.Place
import com.car2go.maps.AnyMap
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -20,6 +20,8 @@ import java.io.IOException
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
data class PlaceWithBounds(val latLng: LatLng, val viewport: LatLngBounds?)
internal fun getClusterDistance(zoom: Float): Int? {
return when (zoom) {
in 0.0..7.0 -> 100
@@ -152,13 +154,13 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
db.chargeLocationsDao().getAllChargeLocations()
}
val searchResult: MutableLiveData<Place> by lazy {
MutableLiveData<Place>()
val searchResult: MutableLiveData<PlaceWithBounds> by lazy {
MutableLiveData<PlaceWithBounds>()
}
val mapType: MutableLiveData<Int> by lazy {
MutableLiveData<Int>().apply {
value = GoogleMap.MAP_TYPE_NORMAL
val mapType: MutableLiveData<AnyMap.Type> by lazy {
MutableLiveData<AnyMap.Type>().apply {
value = AnyMap.Type.NORMAL
}
}
@@ -177,7 +179,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
}
fun setMapType(type: Int) {
fun setMapType(type: AnyMap.Type) {
mapType.value = type
}

View File

@@ -0,0 +1,12 @@
<vector android:height="44.11976dp"
android:viewportHeight="368.4"
android:viewportWidth="233.8"
android:width="28dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#00e676"
android:pathData="M109.8,0h13.6c33.9,1.9 67.1,18.5 87.7,45.8c13.5,17.2 21,38.6 22.7,60.3v8.1c-0.8,42.1 -27.7,76.6 -51,109.4c-26.2,37 -50.4,77.3 -57.1,122.9c-1.8,7.7 0.4,18.5 -8.9,22c-2.2,-1.7 -4.7,-3.1 -6.2,-5.4c-2.7,-25.5 -9.1,-50.7 -20,-73.9c-12.3,-27.1 -29.5,-51.6 -47,-75.6C33,199 23,184.2 14.7,168.3c-13,-23.8 -17.9,-51.9 -12.5,-78.6c4.4,-21.1 15.4,-40.6 30.6,-55.7C53.3,14 81.1,1.8 109.8,0zM107.2,74.1c-16.1,3.6 -29.6,17.8 -31,34.5c-1.7,15.5 7.4,31.3 21.3,38.1c15.1,8 34.9,5.5 47.5,-6c11.8,-10.4 16,-28.3 9.9,-42.9C147.6,79.7 126,69.3 107.2,74.1z" />
<path
android:fillColor="#007e41"
android:pathData="M107.2,74.1c18.9,-4.8 40.4,5.5 47.7,23.7c6.1,14.5 1.9,32.5 -9.9,42.9c-12.6,11.5 -32.4,14 -47.5,6c-13.9,-6.8 -23,-22.6 -21.3,-38.1C77.6,92 91.1,77.7 107.2,74.1z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M20.055,7.713c-0.677,-0.842 -1.673,-1.41 -2.764,-1.615C16.773,3.971 14.877,3 13.045,3H7.057C6.425,3 5.878,3.409 5.689,4.009c-0.015,0.04 -0.026,0.082 -0.036,0.125L3.034,16.262c-0.009,0.041 -0.015,0.083 -0.019,0.125C3.006,16.449 3,16.513 3,16.56C3,17.354 3.648,18 4.444,18h2.316l-0.267,1.262c-0.008,0.04 -0.014,0.081 -0.018,0.121c-0.009,0.063 -0.016,0.126 -0.016,0.173C6.461,20.353 7.109,21 7.905,21h3.259c0.056,0 0.111,-0.005 0.166,-0.015c0.549,-0.063 1.011,-0.437 1.191,-0.963c0.021,-0.05 0.038,-0.103 0.05,-0.156L13.475,16h1.398c3.365,0 5.38,-1.445 5.989,-4.295C21.278,9.752 20.653,8.456 20.055,7.713zM5.137,16L7.512,5h5.533c0.293,0 1.5,0.061 2.078,1.013h-4.706c-0.626,0 -1.17,0.401 -1.363,0.99c-0.019,0.049 -0.033,0.1 -0.043,0.151l-1.034,5.093L7.183,16H5.137zM18.906,11.287C18.5,13.188 17.293,14 14.873,14h-1.857c-0.823,0 -1.338,0.652 -1.405,1.198L10.721,19H8.594l1.271,-6h1.444c4.259,0 5.665,-2.394 6.094,-4.402c0.027,-0.128 0.045,-0.256 0.057,-0.382c0.378,0.151 0.749,0.393 1.038,0.751C18.971,9.557 19.108,10.337 18.906,11.287z"
android:fillColor="#000000" />
</vector>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2020 Google Inc.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.maps.android.ui.RotationLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@id/amu_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:paddingBottom="5dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="5dp" />
</com.google.maps.android.ui.RotationLayout>
</LinearLayout>

View File

@@ -19,12 +19,10 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
<FrameLayout
android:id="@+id/map"
android:name="com.google.android.libraries.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MapsActivity" />
android:layout_height="match_parent" />
<FrameLayout
android:id="@+id/toolbar_container"

View File

@@ -41,7 +41,7 @@
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:contentDescription="@{item.contentDescription}"
android:tint="?colorPrimary"
app:tint="?colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@{item.icon}"

View File

@@ -47,7 +47,7 @@
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:contentDescription="@{item.contentDescription}"
android:tint="?colorPrimary"
app:tint="?colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@{item.icon}"

View File

@@ -6,7 +6,7 @@
<import type="net.vonforst.evmap.viewmodel.MapViewModel" />
<import type="com.google.android.libraries.maps.GoogleMap" />
<import type="com.car2go.maps.AnyMap" />
<variable
name="vm"
@@ -51,24 +51,24 @@
android:id="@+id/rbStandard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{vm.mapType.equals(GoogleMap.MAP_TYPE_NORMAL)}"
android:onClick="@{() -> vm.setMapType(GoogleMap.MAP_TYPE_NORMAL)}"
android:checked="@{vm.mapType.equals(AnyMap.Type.NORMAL)}"
android:onClick="@{() -> vm.setMapType(AnyMap.Type.NORMAL)}"
android:text="@string/map_type_normal" />
<RadioButton
android:id="@+id/rbSatellite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{vm.mapType.equals(GoogleMap.MAP_TYPE_HYBRID)}"
android:onClick="@{() -> vm.setMapType(GoogleMap.MAP_TYPE_HYBRID)}"
android:checked="@{vm.mapType.equals(AnyMap.Type.HYBRID)}"
android:onClick="@{() -> vm.setMapType(AnyMap.Type.HYBRID)}"
android:text="@string/map_type_satellite" />
<RadioButton
android:id="@+id/rbTerrain"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{vm.mapType.equals(GoogleMap.MAP_TYPE_TERRAIN)}"
android:onClick="@{() -> vm.setMapType(GoogleMap.MAP_TYPE_TERRAIN)}"
android:checked="@{vm.mapType.equals(AnyMap.Type.TERRAIN)}"
android:onClick="@{() -> vm.setMapType(AnyMap.Type.TERRAIN)}"
android:text="@string/map_type_terrain" />
</RadioGroup>

View File

@@ -1,191 +0,0 @@
[
{
"featureType": "all",
"elementType": "geometry",
"stylers": [
{
"color": "#242f3e"
}
]
},
{
"featureType": "all",
"elementType": "labels.text.stroke",
"stylers": [
{
"lightness": -80
}
]
},
{
"featureType": "administrative",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#746855"
}
]
},
{
"featureType": "administrative.locality",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "poi.park",
"elementType": "geometry",
"stylers": [
{
"color": "#263c3f"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#6b9a76"
}
]
},
{
"featureType": "road",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#2b3544"
}
]
},
{
"featureType": "road",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9ca5b3"
}
]
},
{
"featureType": "road.arterial",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#38414e"
}
]
},
{
"featureType": "road.arterial",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#212a37"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#746855"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#1f2835"
}
]
},
{
"featureType": "road.highway",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#f3d19c"
}
]
},
{
"featureType": "road.local",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#38414e"
}
]
},
{
"featureType": "road.local",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#212a37"
}
]
},
{
"featureType": "transit",
"elementType": "geometry",
"stylers": [
{
"color": "#2f3948"
}
]
},
{
"featureType": "transit.station",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#d59563"
}
]
},
{
"featureType": "water",
"elementType": "geometry",
"stylers": [
{
"color": "#17263c"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#515c6d"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.stroke",
"stylers": [
{
"lightness": -20
}
]
}
]

View File

@@ -34,6 +34,8 @@
<string name="github_link_title">Quellcode</string>
<string name="oss_licenses">Open Source-Lizenzen</string>
<string name="settings">Einstellungen</string>
<string name="settings_ui">Oberfläche</string>
<string name="settings_map">Karte</string>
<string name="copyright">Copyright</string>
<string name="copyright_summary">©2020 Johan von Forstner</string>
<string name="other">Sonstiges</string>
@@ -66,7 +68,6 @@
<string name="show_less">weniger…</string>
<string name="favorites_empty_state">Wenn du Ladestationen als Favorit markierst, tauchen sie hier auf.</string>
<string name="donate">Spenden</string>
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 30% Gebühren ab.</string>
<string name="donation_successful">Vielen Dank! ❤️</string>
<string name="donation_failed">Etwas ist schiefgelaufen. 😕</string>
<string name="map_type_normal">Standard</string>
@@ -101,6 +102,7 @@
<string name="filter_exclude_faults">Ladesäulen mit Störung ausschließen</string>
<string name="charge_cards">Ladetarife</string>
<string name="and_n_others">und %d weitere</string>
<string name="pref_map_provider">Kartenanbieter</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d kompatibler Ladetarif</item>
<item quantity="other">%d kompatible Ladetarife</item>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="amu_text" type="id" />
</resources>

View File

@@ -33,6 +33,8 @@
<string name="github_link_title">Source code</string>
<string name="oss_licenses">Open Source Licenses</string>
<string name="settings">Settings</string>
<string name="settings_ui">User Interface</string>
<string name="settings_map">Map</string>
<string name="copyright">Copyright</string>
<string name="copyright_summary">©2020 Johan von Forstner</string>
<string name="other">Other</string>
@@ -65,7 +67,6 @@
<string name="show_less">less…</string>
<string name="favorites_empty_state">If you add chargers as favorites, they will show up here.</string>
<string name="donate">Donate</string>
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 30% off every donation.</string>
<string name="donation_successful">Thank you! ❤️</string>
<string name="donation_failed">Something went wrong. 😕</string>
<string name="map_type_normal">Default</string>
@@ -100,6 +101,7 @@
<string name="filter_exclude_faults">Exclude chargers with reported faults</string>
<string name="charge_cards">Payment methods</string>
<string name="and_n_others">and %d others</string>
<string name="pref_map_provider">Map provider</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d compatible payment method</item>
<item quantity="other">%d compatible payment methods</item>

View File

@@ -1,14 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/settings">
<CheckBoxPreference
android:key="navigate_use_maps"
android:title="@string/pref_navigate_use_maps"
android:summaryOn="@string/pref_navigate_use_maps_on"
android:summaryOff="@string/pref_navigate_use_maps_off"
android:defaultValue="true" />
<PreferenceCategory android:title="@string/settings_ui">
<ListPreference
android:key="language"
@@ -27,4 +20,22 @@
android:summary="@string/pref_darkmode_summary" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_map">
<ListPreference
android:key="map_provider"
android:title="@string/pref_map_provider"
android:entries="@array/pref_map_provider_names"
android:entryValues="@array/pref_map_provider_values"
android:defaultValue="@string/pref_map_provider_default"
android:summary="%s" />
<CheckBoxPreference
android:key="navigate_use_maps"
android:title="@string/pref_navigate_use_maps"
android:summaryOn="@string/pref_navigate_use_maps_on"
android:summaryOff="@string/pref_navigate_use_maps_off"
android:defaultValue="true" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -8,7 +8,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.android.tools.build:gradle:4.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
@@ -25,6 +25,7 @@ allprojects {
google()
jcenter()
maven { url 'https://jitpack.io' }
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
flatDir {
dirs 'libs'
}