Compare commits

..

122 Commits
0.4.0 ... 0.7.3

Author SHA1 Message Date
johan12345
498dc63f91 Release 0.7.3 2021-07-03 13:15:15 +02:00
johan12345
c48330dc35 add button to explicitly close the layers menu (#50) 2021-07-03 13:11:00 +02:00
johan12345
ca8abd9b12 fix #92 by using forked version of Mapbox Places Plugin 2021-07-03 12:41:22 +02:00
johan12345
72b2b34af3 disable myTariffsPreference until tariffs are loaded 2021-07-03 11:32:37 +02:00
johan12345
6a7b7a7d39 avoid passing empty string to Android Auto, which caused crash 2021-07-03 11:31:08 +02:00
johan12345
c1af372a06 avoid crash when no browser is installed 2021-07-03 11:23:59 +02:00
johan12345
7946663299 be a bit smarter about bounding boxes for various types of shared locations 2021-06-10 21:05:48 +02:00
johan12345
232aecfe3b add support for another format of geo intent (fixes #90) 2021-06-10 20:41:45 +02:00
johan12345
ac1db7f10d fix lint errors 2021-05-24 10:48:18 +02:00
johan12345
ef99441844 various dependency updates 2021-05-23 23:05:29 +02:00
Johan von Forstner
c4e3534682 Update README.md
fix typo in README
2021-05-23 21:59:38 +02:00
johan12345
d335d7cab0 Release 0.7.2 2021-05-09 15:25:03 +02:00
johan12345
f7c3faa7bd Chargeprice: show my tariffs first in overview 2021-05-09 15:18:35 +02:00
johan12345
1338e2306e Chargeprice: show currently selected currency as summary 2021-05-09 14:45:31 +02:00
johan12345
83a2b42408 Chargeprice: add "my plans" selection preference 2021-05-09 14:44:18 +02:00
johan12345
0ce5938f5b Chargeprice: show provider name only if tariff name doesn't start with it 2021-05-09 14:38:57 +02:00
johan12345
5ab50e04ae README.md: add info about necessary Chargeprice API key 2021-04-28 22:51:05 +02:00
johan12345
ee0fd4e8d8 Chargeprice: store charging range (#86) 2021-04-28 22:47:42 +02:00
johan12345
369b7d9410 Chargeprice: implement currency selection (#86) 2021-04-28 22:41:08 +02:00
johan12345
c9a0b270cd Chargeprice: do not show two error messages if car was not yet selected 2021-04-28 22:38:56 +02:00
johan12345
c8aa64fa7c fix missing German translation 2021-04-24 19:41:39 +02:00
Johan von Forstner
d5b18bd6fb README: Update Features 2021-04-23 08:12:19 +02:00
johan12345
eb7ade5e48 Release 0.7.1 2021-04-22 22:52:47 +02:00
johan12345
a59444e24b Android Auto: handle network connection issues 2021-04-22 22:48:47 +02:00
johan12345
c6b7157d5b GoingElectricApi: avoid crash when opening hours don't match expected format
(not sure why this happens)
2021-04-22 22:09:19 +02:00
johan12345
3d9a622f09 Chargeprice battery range slider: only update after sliding 2021-04-22 21:47:02 +02:00
johan12345
50bb245777 fix tests 2021-04-21 08:45:17 +02:00
johan12345
128cebfc20 fix multiline titles of preferences (#82) 2021-04-20 23:22:18 +02:00
johan12345
c106bc40cc Chargeprice: detect which connectors are compatible with vehicle before sending request (#82) 2021-04-20 23:20:04 +02:00
johan12345
52af10d549 Chargeprice: update "my vehicle" preference summary when changed (#82) 2021-04-20 21:53:51 +02:00
johan12345
8c03d1e9eb Release 0.7.0 2021-04-18 14:41:19 +02:00
johan12345
f1d49e317d Live data: differentiate between occupied and broken chargers (fixes #73) 2021-04-18 14:17:34 +02:00
johan12345
f3be8ed97b Chargeprice: add network error handling 2021-04-18 13:59:26 +02:00
johan12345
258edb87c9 add ability to pass Chargeprice API key in encrypted form to Gradle build
(needed for F-Droid)
2021-04-18 13:51:23 +02:00
Johan von Forstner
703dd40879 Merge pull request #76 from johan12345/chargeprice
Native integration of Chargeprice.app
2021-04-18 00:13:44 +02:00
johan12345
18f7ed19e0 add Chargeprice API Key to Travis CI build 2021-04-18 00:12:46 +02:00
johan12345
3d30e746a0 Implement Chargeprice GUI 2021-04-18 00:12:43 +02:00
johan12345
51d085dbb0 Implement Chargeprice API 2021-04-18 00:11:37 +02:00
johan12345
66b4627d21 Fix positioning of drawer logo on devices with display cutout 2021-04-17 00:05:12 +02:00
johan12345
99263e9a66 add toast with information about GoingElectric.de edit page 2021-04-16 23:46:54 +02:00
johan12345
999c5b0836 Further restrict intent filters to avoid handling intents to GoingElectric.de charge card info 2021-04-16 23:41:41 +02:00
johan12345
52aa1b198d Show information about barrier-free charging (fixes #77) 2021-04-16 22:55:30 +02:00
johan12345
36a702a6f4 Support geo: intents that only send a text and not the geo coordinates
such as those from aCalendar (#79)
2021-04-16 22:25:16 +02:00
johan12345
512be8b0c9 feature graphic: convert text to paths 2021-04-12 21:53:12 +02:00
johan12345
3dabd07969 use feature graphic as logo in GitHub README.md 2021-04-12 21:52:26 +02:00
johan12345
29bec90001 feature graphic: embed font 2021-04-12 21:51:28 +02:00
johan12345
1191ac732b Android Auto: fix crash when charger has no photo 2021-04-12 21:21:05 +02:00
johan12345
a80fcebe94 Release 0.6.1 2021-04-11 21:51:02 +02:00
johan12345
35b21b10e3 detail view: display long names in a better way 2021-04-11 21:42:13 +02:00
johan12345
22d8f9a628 detail view: add fault report icon 2021-04-11 21:37:44 +02:00
johan12345
42e8d999d3 fix crash on Android Auto
(unbinding service that was not bound)
2021-04-10 19:39:06 +02:00
johan12345
cf4d18e23e fix another crash related to location service 2021-04-10 19:34:57 +02:00
johan12345
bfa1c45ae6 Android Auto: increase search radius to 25 km if not enough chargers found within 5 km radius 2021-04-09 23:01:01 +02:00
johan12345
6e888499c4 Avoid repetitive requests to GE API when location is enabled, but not moving 2021-04-08 23:09:39 +02:00
johan12345
9223b70eba When moving map, close map layers menu (fixes #74) 2021-04-06 22:54:43 +02:00
johan12345
fe55855876 update GitHub token for Travis CI 2021-04-06 22:38:37 +02:00
johan12345
6aa8a3d7a2 Release 0.6.0 2021-04-05 22:52:34 +02:00
johan12345
887702b729 Add Android Auto information dialog 2021-04-05 22:42:26 +02:00
johan12345
0417c4f1ae Show GoingElectric verified state 2021-04-05 22:01:52 +02:00
johan12345
0b95785f49 add Android Auto screenshots 2021-04-05 21:22:44 +02:00
Johan von Forstner
2772e9ad4d Merge pull request #63 from johan12345/android-auto
Android Auto support
2021-04-05 21:14:16 +02:00
johan12345
8a16fa3a5c Android Auto: update car app library 2021-04-05 21:13:53 +02:00
johan12345
84d3127675 Android Auto: migrate to new version of car app library 2021-04-05 21:13:53 +02:00
Johan von Forstner
e684fbc0dc Android Auto: avoid crash after maximum number of updates is reached 2021-04-05 21:13:53 +02:00
Johan von Forstner
bb92d26be9 Android Auto: display fault reports 2021-04-05 21:13:53 +02:00
Johan von Forstner
f74bb8e4a5 Android Auto: fix typo for cost string 2021-04-05 21:13:53 +02:00
Johan von Forstner
5d72be8e87 Android Auto: Add charger icon 2021-04-05 21:13:53 +02:00
Johan von Forstner
04e6f63cd7 Android Auto: Add permission screen, add selection between nearby and favorites 2021-04-05 21:13:53 +02:00
Johan von Forstner
ffb0b77f37 Android Auto: implement detail view to app link 2021-04-05 21:13:53 +02:00
Johan von Forstner
9d621c3149 Android Auto: add more information in detail view 2021-04-05 21:13:52 +02:00
Johan von Forstner
7126c3c67c Android Auto: add detail view with button to navigate 2021-04-05 21:13:52 +02:00
Johan von Forstner
62197f99cb GoingElectricApi: use coroutines for loading charger details 2021-04-05 21:13:51 +02:00
johan12345
db68452f55 Android Auto: initial implementation 2021-04-05 21:13:51 +02:00
johan12345
9ec5010495 NewMotionAvailabilityDetector: fail silently for unknown connector types 2021-04-05 21:03:56 +02:00
johan12345
5978b90da2 fix crash if charger ID was not found 2021-04-05 21:00:25 +02:00
johan12345
223d9d394f fix crash if location client is not connected 2021-04-05 20:58:31 +02:00
johan12345
38b82abc48 Preserve map traffic enabled state across app restarts
like map type, which was implemented in 6cb682f0
2021-04-05 20:56:03 +02:00
johan12345
aade4ec488 increase touch target size for search bar 2021-04-05 20:51:33 +02:00
johan12345
38a02f8304 use more restrictive pattern for intent-filter
For example edit button (url ending with /edit/) would try to open in EVMap
2021-04-05 20:47:50 +02:00
johan12345
8f7e1c5629 disable location following when search result is shown 2021-04-05 19:11:09 +02:00
johan12345
0be90d8801 Release 0.5.0 2021-03-28 23:12:37 +02:00
johan12345
4ca9cc68cb Handle intents to https://www.goingelectric.de/stromtankstellen website 2021-03-28 23:02:24 +02:00
johan12345
62e9acf9be throttle repetitive loading of chargepoints to 500 ms 2021-03-28 22:43:08 +02:00
johan12345
6cb682f065 Preserve selected map type across app restarts 2021-03-28 21:46:59 +02:00
johan12345
4cfd5c8ef2 follow current location in map view (fixes #56) 2021-03-28 21:42:26 +02:00
johan12345
24bf66ddbe fix calculation of total chargers from filtered availability introduced in a0b0339c8b 2021-03-28 18:42:07 +02:00
johan12345
a0b0339c8b Handle geo intents to open map (fixes #69) 2021-03-27 21:35:42 +01:00
johan12345
2c9081b313 filter availability displayed in sparse view by selected connectors 2021-03-27 20:58:38 +01:00
johan12345
bd245801b0 refactoring of FilterValues using typealias and extension function 2021-03-27 20:48:15 +01:00
johan12345
11dac62b94 update copyright year 2021-03-24 08:43:25 +01:00
Johan von Forstner
a8bac7875a README.md: document Mapbox API key 2021-02-08 22:17:51 +01:00
johan12345
dbba00b51b Rework filter profile delete undo functionality (similar bug to #70) 2021-01-28 22:45:05 +01:00
johan12345
90cddce54c fix #70: Renaming filter profile resets settings 2021-01-28 21:47:47 +01:00
Johan von Forstner
f0f6c08610 Release 0.4.3 2021-01-17 14:15:46 +01:00
Johan von Forstner
a2fe9a06c5 fix another IllegalStateException 2021-01-17 14:09:37 +01:00
Johan von Forstner
cb79f17c23 catch IllegalArgumentException 2021-01-17 14:08:28 +01:00
Johan von Forstner
0009895537 fix IllegalStateException 2021-01-17 14:07:20 +01:00
Johan von Forstner
df705670b1 fix ClassCastException 2021-01-17 14:00:35 +01:00
Johan von Forstner
c616e9fdbd README.md: Describe map backends
see also #36
2021-01-06 19:30:45 +01:00
Johan von Forstner
c70a092d99 Release 0.4.2 2021-01-03 16:47:15 +01:00
Johan von Forstner
34fee47c08 Fix incorrect linking of text (fixes #29) 2021-01-03 16:23:07 +01:00
Johan von Forstner
bf97a14fe3 add station availability in map screen (fixes #52) 2021-01-03 15:28:58 +01:00
Johan von Forstner
60d4d56f80 Fix links to Google Maps
(maps app was not found due to https://developer.android.com/training/basics/intents/package-visibility)
2021-01-03 11:00:22 +01:00
Johan von Forstner
8bf33c7384 FilterProfilesFragment: Add rename and delete buttons + undo function 2021-01-03 10:45:56 +01:00
Johan von Forstner
595e6e9a8f Welcome dialog: replace > with ≥ 2021-01-03 09:52:07 +01:00
Johan von Forstner
9efbdfc046 Fix typo in welcome page 2021-01-02 22:38:54 +01:00
Johan von Forstner
e1d4b6bcc5 welcome dialog: fix height on small screens 2021-01-02 20:09:12 +01:00
Johan von Forstner
a6db74488e release 0.4.1 2020-12-31 20:29:48 +01:00
Johan von Forstner
821f5d61b5 add welcome dialog on first start (fixes #66) 2020-12-31 19:18:58 +01:00
Johan von Forstner
f83ac17c83 don't generate icons in background for Mapbox 2020-12-30 20:10:52 +01:00
Johan von Forstner
3519c7f699 decrease memory usage of charger icons
by allowing "fault" and "highlight" only with scale == 1f
refs #59
2020-12-30 20:01:47 +01:00
Licaon_Kter
78d9706cb7 Remove suffix for fdroid (#67)
...as you'd know it's not-google 👍 
Also, breaks AutoUpdate
2020-12-30 19:01:03 +01:00
Johan von Forstner
a593a8054b fix some gallery glitches (#61) 2020-12-30 18:58:55 +01:00
Johan von Forstner
9556be6b85 Gallery fixes 2020-12-29 20:24:22 +01:00
Johan von Forstner
e8669f8a3d Gallery: replace Picasso with Coil 2020-12-29 18:09:29 +01:00
Johan von Forstner
6a887ee1e4 NewMotionAvailabilityDetector: add some more plug types
(rarely occurring)
2020-12-29 18:08:37 +01:00
Johan von Forstner
6dbaaa3099 travis CI: use latest android SDK commandline tools 2020-12-28 11:14:24 +01:00
Johan von Forstner
7f9242da1e fix license file 2020-12-28 10:58:07 +01:00
Johan von Forstner
2c3151089f CI: accept android licenses 2020-12-28 10:53:42 +01:00
Johan von Forstner
1ee388126f travis: build-tools;30.0.3 2020-12-28 10:46:18 +01:00
Johan von Forstner
964cecdf66 travis CI: use Android 30 sdk 2020-12-28 10:38:15 +01:00
Johan von Forstner
7141eb5013 update to Android 11 SDK 2020-12-27 17:15:18 +01:00
130 changed files with 7175 additions and 683 deletions

View File

@@ -1,16 +1,24 @@
language: android
language: java
dist: trusty
android:
components:
- build-tools-29.0.3
- android-29
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=
- secure: LQHMdhaPUlCuJPFrCPpUphJSY6xzAFI/7RrcAVLtLcPhGdS+MeNifIkkAH7MeitTHroOC0dGkZ4bg/8/7bKfgwY4vPH9P50kZcnX5mI6zfBHgNYJzuthj+vJH9RAtkdQOW9Fe1uPIx8R9GUWUOVnkoJh0PQ1gDXdZW5fePqUtn1kYrcCCBE+Bhe3wz6QzTBqGS1nsVRTxQfSJNGi9uH1oi9kQGgQFuCCiJ/P0A6MIhSItkOfuggx/iorA+iASbhWkB4nXYQBbFe/ZhFJWbVfgYlOM0HtpKh8B2AqKw21Em32JoovCbUof4adkY7cH8/4Rt9SujC9YOw+a6oM+e//jJT0sie77V7zl670j+qODTuNvV4qVUwtoxShyc1Sfbd+Xb0xn/OC7DzBg97YuYCF/84yyuq12rl/cofynWE1L5YvGNSJk241XUw98Bvl0MK4VIfQvG9zJP0HnQZcWKt6kFOIEJSCRbmkd2tPPAZFBXBQf/bvpULOoKwneGJZBSapRoCyGwemM+EAzVB9UOXAqsXZ4FHkt1SSJVrTVwgxvXpCfmF6LZPhbz6nvouRWGsC/GdWjrHtdW5lEOvS27qKEL5rXwQ0o+71ZICGo8j4E0GOHXyi857qZhvO7cbOnts+iiawXiWzPXv2gGGabuqPwcU8JPEoWdaiIaeGUczfjBU=
- ANDROID_HOME=$HOME/android-sdk
before_install:
- openssl aes-256-cbc -K $encrypted_53968681344a_key -iv $encrypted_53968681344a_iv -in _ci/keystore.jks.enc -out _ci/keystore.jks -d
install:
# Download and unzip the Android command line tools (if not already there thanks to the cache mechanism)
# Latest version of this file available here: https://developer.android.com/studio/#command-tools
- if test ! -e $HOME/android-cmdline-tools/cmdline-tools.zip ; then curl https://dl.google.com/android/repository/commandlinetools-linux-6609375_latest.zip > $HOME/android-cmdline-tools/cmdline-tools.zip ; fi
- unzip -qq -n $HOME/android-cmdline-tools/cmdline-tools.zip -d $HOME/android-cmdline-tools
# Install or update Android SDK components (will not do anything if already up to date thanks to the cache mechanism)
- echo y | $HOME/android-cmdline-tools/tools/bin/sdkmanager --sdk_root=$HOME/android-sdk 'platform-tools' > /dev/null
# Latest version of build-tools available here: https://developer.android.com/studio/releases/build-tools.html
- echo y | $HOME/android-cmdline-tools/tools/bin/sdkmanager --sdk_root=$HOME/android-sdk 'build-tools;29.0.3' > /dev/null
- echo y | $HOME/android-cmdline-tools/tools/bin/sdkmanager --sdk_root=$HOME/android-sdk 'platforms;android-29' > /dev/null
script:
- "./gradlew lintFossDebug testFossDebugUnitTest lintGoogleDebug testGoogleDebugUnitTest"
- "./gradlew assembleRelease"
@@ -22,10 +30,12 @@ cache:
- "$HOME/.gradle/caches/"
- "$HOME/.gradle/wrapper/"
- "$HOME/.android/build-cache"
- "$HOME/android-cmdline-tools"
- "$HOME/android-sdk"
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=
secure: "XQR4GUrGkPKYVV0xMbJifX/ewKAnenBPlM/pPacQ9irAmYNYa/yEkySz4x1K6MP8cEnuJbxHFakcDqhNRCqD7Cq2NcnCi3qtTEXHK6ApLoVl/92eyiWxu/bYlidOEZb+YPcVNtTR253NiI8GYda+CrhLd4uCmsAgES+XPFJd/t2esMlDOSAp7xalZv/zFhhlB9+SevfPFMc6kkrqeHpKnMs9SK8ltVQmh3nch2KjtDvqgDW6d3nuwn7/HAer6/HY86hmA4Rh6Mo2cV6OloX0bdJ7hvA1GOT4p3+K3lWbTRxzE0o1DXAtT7+D158iKvxHFPuF3h+CTjSlLeiss6kQZL9nFjw/KhAvu+GJOp37PcMoI++mpMiFoWPlzKpp17BVKIDinYbgi8kiU4zG+QHhe2cY85SbfAplXUaysq7uzxEZwEUYHSAHNahshVooXRqvuzkthcH0/nvinfeXrzx2xDvQ3if1NENMRgttwewU0kvU61iKUwpcf/UN2bHK3DaPes0VzSH4PTHAGjoRpksDfqUwb7S8YxbYr+44aMbSPYN8Lbjda0BxPSKWwHM5/pi7FBJN1a1w3t7sV/EiACWUWr8OovmX4ljyCybbR0w9cPzRC1zAYeSUHslLXMTW2Pp9h594RnYh3q3VfeYlFCikFvuvrafwXmTkz35uhLb+2ws="
file:
- app/build/outputs/apk/foss/release/app-foss-release.apk
- app/build/outputs/apk/google/release/app-google-release.apk

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 Johan von Forstner
Copyright (c) 2020-2021 Johan von Forstner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,7 +1,7 @@
EVMap [![Build Status](https://travis-ci.org/johan12345/EVMap.svg?branch=master)](https://travis-ci.org/johan12345/EVMap)
=====
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/appicon_cropped.svg?sanitize=true" width=80 alt="Logo"/>
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
Android app to access the goingelectric.de electric vehicle charging station directory.
@@ -18,8 +18,11 @@ Features
- Realtime availability information (beta)
- Search places
- Favorites list, also with availability information
- Charging price comparison, powered by [Chargeprice.app](https://chargeprice.app)
- Android Auto integration
- No ads, fully open source
- Compatible with Android 5.0 and above
- Can use Google Maps or Mapbox (OpenStreetMap) as map backends - the version available on F-Droid only uses Mapbox.
Screenshots
-----------
@@ -31,10 +34,11 @@ Development setup
The App is developed using Android Studio.
For testing the app, you need to obtain API Keys for the
[GoingElectric API](https://www.goingelectric.de/stromtankstellen/api/)
For testing the app, you need to obtain free API Keys for the
[GoingElectric API](https://www.goingelectric.de/stromtankstellen/api/),
the [Chargeprice API](https://github.com/chargeprice/chargeprice-api-docs)
as well as for [Google APIs](https://console.developers.google.com/)
("Maps SDK for Android" and "Places API" need to be activated). These APIs need to be put into the
("Maps SDK for Android" and "Places API" need to be activated) and/or [Mapbox](https://www.mapbox.com/). These API keys need to be put into the
app in the form of a resource file called `apikeys.xml` under `app/src/main/res/values`, with the
following content:
@@ -43,8 +47,14 @@ following content:
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">
insert your Google Maps key here
</string>
<string name="mapbox_key" translatable="false">
insert your Mapbox key here
</string>
<string name="goingelectric_key" translatable="false">
insert your GoingElectric key here
</string>
<string name="chargeprice_key" translatable="false">
insert your Chargeprice key here
</string>
</resources>
```

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 1024 500"
style="enable-background:new 0 0 1024 500;" xml:space="preserve">
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 1024 500" style="enable-background:new 0 0 1024 500;" xml:space="preserve">
<style type="text/css">
.st0{fill:#E6E6E6;}
.st1{fill:#DCDCDC;}
@@ -15,7 +15,7 @@
.st10{fill:#666666;}
.st11{fill:#D1D1D1;}
.st12{opacity:0.2;fill:#808080;enable-background:new ;}
.st13{opacity:0.5;}
.st13{opacity:0.5;enable-background:new ;}
.st14{fill:#FFB300;}
.st15{fill:#90A4AE;}
.st16{fill:#546E7A;}
@@ -23,10 +23,9 @@
.st18{fill:#FFFFFF;fill-opacity:0.2;}
.st19{fill:#3E2723;fill-opacity:0.2;}
.st20{opacity:0.45;enable-background:new ;}
.st21{font-family:'Roboto-Light';}
.st22{font-size:136.5333px;}
.st21{enable-background:new ;}
</style>
<g id="Ebene_1">
<g id="Ebene_1_1_">
<rect y="-34.4" class="st0" width="1024" height="568.9" />
<g>
<path class="st1"
@@ -35,25 +34,25 @@
d="M145.4,335.9L38.1,228.7c-6-6-6-15.4,0-21.3L91,154.1c5.7-5.7,15.4-5.7,21.3,0l107.2,107.2" />
<path class="st2" d="M131.7,209.9L93.6,248c-2.8,2.8-7.7,2.8-10.5,0l-24.7-24.7c-2.8-2.8-2.8-7.7,0-10.5l38.1-38.1
c2.8-2.8,7.7-2.8,10.5,0l24.7,24.7C134.8,202.2,134.5,207,131.7,209.9z" />
<path class="st3" d="M223.3,265.1c-2,2-5.4,2-7.4,0l-107.2-107c-3.7-3.7-10.2-4-13.9,0L41.8,211c-3.7,3.7-3.7,10,0,13.9L149,332.2
c2,2,2,5.4,0,7.4c-2,2-5.4,2-7.4,0L34.4,232.4c-8-8-8-20.8,0-28.7l52.9-53.2c8-8,20.8-8,28.7,0l107.2,107.2
<path class="st3" d="M223.3,265.1c-2,2-5.4,2-7.4,0l-107.2-107c-3.7-3.7-10.2-4-13.9,0l-53,52.9c-3.7,3.7-3.7,10,0,13.9L149,332.2
c2,2,2,5.4,0,7.4s-5.4,2-7.4,0L34.4,232.4c-8-8-8-20.8,0-28.7l52.9-53.2c8-8,20.8-8,28.7,0l107.2,107.2
C225.3,260,225.3,263.1,223.3,265.1z" />
<path class="st4" d="M131.7,209.9L93.6,248c-2.8,2.8-7.7,2.8-10.5,0l-24.7-24.7c-2.8-2.8-2.8-7.7,0-10.5l38.1-38.1
c2.8-2.8,7.7-2.8,10.5,0l24.7,24.7C134.8,202.2,134.5,207,131.7,209.9z" />
<path class="st3" d="M135.4,213.6l-38.1,38.1c-4.8,4.8-13.1,4.8-17.9,0L54.6,227c-4.8-4.8-4.8-13.1,0-17.9l38.1-38.1
<path class="st3" d="M135.4,213.6l-38.1,38.1c-4.8,4.8-13.1,4.8-17.9,0L54.6,227c-4.8-4.8-4.8-13.1,0-17.9L92.7,171
c4.8-4.8,13.1-4.8,17.9,0l24.7,24.7C140.5,200.5,140.5,208.5,135.4,213.6z M62,216.4c-0.9,0.9-0.9,2.3,0,3.1l24.7,24.7
c0.9,0.9,2.3,0.9,3.1,0l38.1-38.1c0.9-0.9,0.9-2.3,0-3.1l-24.7-24.7c-0.9-0.9-2.3-0.9-3.1,0L62,216.4z M233.8,254.6l-95.3,95.3
c-2,2-5.4,2-7.4,0c-2-2-2-5.4,0-7.4l95.3-95.3c2-2,5.4-2,7.4,0S235.8,252.6,233.8,254.6z M228.4,238.3c-4.8,4.8-13.1,4.8-17.9,0
l-43-42.7c-0.9-0.9-2.3-0.9-3.1,0l-5.4,5.4c-2,2-5.4,2-7.4,0c-2-2-2-5.4,0-7.4l5.4-5.4c4.8-4.8,13.1-4.8,17.9,0l43,43
c0.9,0.9,2.3,0.9,3.1,0c0.9-0.9,0.9-2.3,0-3.1l-58.9-59.2c-2-2-2-5.4,0-7.4c2-2,5.4-2,7.4,0l58.9,58.9
c-2,2-5.4,2-7.4,0s-2-5.4,0-7.4l95.3-95.3c2-2,5.4-2,7.4,0S235.8,252.6,233.8,254.6z M228.4,238.3c-4.8,4.8-13.1,4.8-17.9,0
l-43-42.7c-0.9-0.9-2.3-0.9-3.1,0L159,201c-2,2-5.4,2-7.4,0s-2-5.4,0-7.4l5.4-5.4c4.8-4.8,13.1-4.8,17.9,0l43,43
c0.9,0.9,2.3,0.9,3.1,0c0.9-0.9,0.9-2.3,0-3.1l-58.9-59.2c-2-2-2-5.4,0-7.4s5.4-2,7.4,0l58.9,58.9
C233.2,225.3,233.2,233.5,228.4,238.3z" />
<path class="st3" d="M174.6,163.8l-10.5,10.5c-2,2-5.4,2-7.4,0l-21.6-21.6c-2-2-2-5.4,0-7.4l1.7-1.7l-4.8-4.6c-2-2-2-5.4,0-7.4
c2-2,5.4-2,7.4,0l4.8,4.8l1.4-1.4c2-2,5.4-2,7.4,0l21.3,21.3C176.4,158.4,176.6,161.8,174.6,163.8z M160.4,163.2l3.1-3.1
l-13.9-13.9l-3.1,3.1L160.4,163.2z" />
<path class="st3" d="M174.6,163.8l-10.5,10.5c-2,2-5.4,2-7.4,0l-21.6-21.6c-2-2-2-5.4,0-7.4l1.7-1.7L132,139c-2-2-2-5.4,0-7.4
s5.4-2,7.4,0l4.8,4.8l1.4-1.4c2-2,5.4-2,7.4,0l21.3,21.3C176.4,158.4,176.6,161.8,174.6,163.8z M160.4,163.2l3.1-3.1l-13.9-13.9
l-3.1,3.1L160.4,163.2z" />
<g>
<path class="st5" d="M163.8,290.1c-0.6,0.6-1.4,1.1-2.6,1.4c-2.8,0.6-5.7-1.1-6.3-3.7l-3.7-16.2l-3.7,5.4c-1.1,1.7-3.1,2.6-5.1,2
c-2-0.6-3.7-2-4-4l-6.5-27c-0.6-2.8,1.1-5.7,3.7-6.3c2.8-0.6,5.7,1.1,6.3,3.7l3.7,16.2l3.7-5.4c1.1-1.7,3.1-2.6,5.1-2
c2,0.6,3.7,2,4,4l6.3,27C165.5,287.3,165,289,163.8,290.1z" />
s-3.7-2-4-4l-6.5-27c-0.6-2.8,1.1-5.7,3.7-6.3c2.8-0.6,5.7,1.1,6.3,3.7l3.7,16.2l3.7-5.4c1.1-1.7,3.1-2.6,5.1-2s3.7,2,4,4l6.3,27
C165.5,287.3,165,289,163.8,290.1z" />
</g>
</g>
<g>
@@ -66,11 +65,11 @@
<path class="st3" d="M156.4,462.5l-40.1,40.1c-4.6,4.6-11.7,4.6-16.2,0l-68.3-68.3c-4.6-4.6-4.6-11.7,0-16.2L72,378
c4.6-4.6,11.7-4.6,16.2,0l68.3,68.3C161,450.8,161,457.9,156.4,462.5z M37.8,424.4c-1.1,1.1-1.1,2.8,0,4l68.3,68.3
c1.1,1.1,2.8,1.1,4,0l40.1-40.1c1.1-1.1,1.1-2.8,0-4l-68.3-68.3c-1.1-1.1-2.8-1.1-4,0L37.8,424.4z" />
<path class="st2" d="M111.2,487.5c-1.7,1.7-4.3,1.7-6,0l-25-25c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0l25,25
<path class="st2" d="M111.2,487.5c-1.7,1.7-4.3,1.7-6,0l-25-25c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l25,25
C112.9,483.2,112.9,485.8,111.2,487.5z M71.1,447.4c-0.9,0.9-2,1.4-3.1,1.1c-1.1,0-2.3-0.3-3.1-1.1c-0.9-0.9-1.4-2-1.1-3.1
c0-0.3,0-0.6,0-0.9s0-0.6,0.3-0.9s0.3-0.6,0.3-0.9c0.9-1.1,2-2,3.4-2c1.1,0,2.3,0.3,3.1,1.1c0.3,0.3,0.3,0.3,0.6,0.6
c0.3,0.3,0.3,0.6,0.3,0.9c0,0.3,0.3,0.6,0.3,0.9s0,0.6,0,0.9C72.2,445.4,71.7,446.6,71.1,447.4z" />
<path class="st3" d="M68,393.9c-1.7,1.7-4.3,1.7-6,0l-9.1-9.1L39,398.8l1.1,1.1c1.7,1.7,1.7,4.3,0,6c-1.7,1.7-4.3,1.7-6,0l-4-4
c0.3,0.3,0.3,0.6,0.3,0.9s0.3,0.6,0.3,0.9s0,0.6,0,0.9C72.2,445.4,71.7,446.6,71.1,447.4z" />
<path class="st3" d="M68,393.9c-1.7,1.7-4.3,1.7-6,0l-9.1-9.1l-13.9,14l1.1,1.1c1.7,1.7,1.7,4.3,0,6s-4.3,1.7-6,0l-4-4
c-1.7-1.7-1.7-4.3,0-6l20.2-20.2c1.7-1.7,4.3-1.7,6,0l11.9,11.9C69.7,389.7,69.7,392.2,68,393.9z" />
</g>
<g>
@@ -78,35 +77,35 @@
C390.3,425.8,354.1,476.4,333.4,497.2z" />
<path class="st5" d="M293.5,387.7l48.1-11.7c1.7-0.3,2.6,1.7,1.1,2.6l-28.4,18.8l9.7,9.7c0.9,0.9,0.3,2.3-0.9,2.3l-43.8,6.8
c-1.4,0.3-2.3-1.7-1.1-2.6l23.6-14.5l-9.4-9.4C292.4,389.1,292.7,388,293.5,387.7z" />
<path class="st3" d="M374.6,440l-15.1-15.1c-7.1-7.1-7.1-18.8,0-26.2l8-8c1.7-1.7,4.3-1.7,6,0c1.7,1.7,1.7,4.3,0,6l-8,8
c-4,4-4,10.2,0,13.9l15.1,15.1c1.7,1.7,1.7,4.3,0,6C378.9,441.4,376,441.4,374.6,440z" />
<path class="st3" d="M374.6,440l-15.1-15.1c-7.1-7.1-7.1-18.8,0-26.2l8-8c1.7-1.7,4.3-1.7,6,0s1.7,4.3,0,6l-8,8
c-4,4-4,10.2,0,13.9l15.1,15.1c1.7,1.7,1.7,4.3,0,6S376,441.4,374.6,440z" />
<path class="st3" d="M327.4,503.2L226.7,402.5c-1.7-1.7-1.7-4.3,0-6l73.4-73.4c7.1-7.1,18.8-7.1,26.2,0l70.3,70.3l0,0l8,8
c1.7,1.7,1.7,4.3,0,6c-1.7,1.7-4.3,1.7-6,0l-2.3-2.3c-8.2,31.3-41.5,76.2-60,94.7l-3.1,3.1C331.7,504.9,328.8,504.9,327.4,503.2z
c1.7,1.7,1.7,4.3,0,6s-4.3,1.7-6,0l-2.3-2.3c-8.2,31.3-41.5,76.2-60,94.7l-3.1,3.1C331.7,504.9,328.8,504.9,327.4,503.2z
M235.8,399.6l94.4,94.4c21.6-21.6,54.6-69.1,58.9-96.1l-68.8-68.8c-4-4-10.2-4-13.9,0C306.1,329.4,235.8,399.6,235.8,399.6z" />
<path class="st3" d="M327.4,503.2L222.7,398.5c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0l104.7,104.7c1.7,1.7,1.7,4.3,0,6
<path class="st3" d="M327.4,503.2L222.7,398.5c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l104.7,104.7c1.7,1.7,1.7,4.3,0,6
C331.7,504.9,328.8,504.9,327.4,503.2z" />
<path class="st2" d="M251.2,410.1c-5.4-5.4-13.9-5.4-19.3,0s-5.4,13.9,0,19.3l0,0c5.4,5.4,13.9,5.4,19.3,0
S256.6,415.3,251.2,410.1L251.2,410.1z M315.4,474.4c-5.4-5.4-13.9-5.4-19.3,0c-5.4,5.4-5.4,13.9,0,19.3l0,0
c5.4,5.4,13.9,5.4,19.3,0S320.9,479.8,315.4,474.4L315.4,474.4z" />
<path class="st3" d="M228.7,432.3c-7.1-7.1-6.8-18.5,0-25.3c6.8-6.8,18.5-6.8,25.3,0c6.8,6.8,6.8,18.5,0,25.3
C247.2,439.4,235.8,439.4,228.7,432.3z M248,413c-3.7-3.7-9.7-3.7-13.4,0c-3.7,3.7-3.7,9.7,0,13.4s9.7,3.7,13.4,0
S256.6,415.3,251.2,410.1L251.2,410.1z M315.4,474.4c-5.4-5.4-13.9-5.4-19.3,0s-5.4,13.9,0,19.3l0,0c5.4,5.4,13.9,5.4,19.3,0
S320.9,479.8,315.4,474.4L315.4,474.4z" />
<path class="st3" d="M228.7,432.3c-7.1-7.1-6.8-18.5,0-25.3s18.5-6.8,25.3,0c6.8,6.8,6.8,18.5,0,25.3
C247.2,439.4,235.8,439.4,228.7,432.3z M248,413c-3.7-3.7-9.7-3.7-13.4,0s-3.7,9.7,0,13.4s9.7,3.7,13.4,0
C251.7,422.7,251.7,416.7,248,413z" />
<g>
<path class="st3" d="M293.3,496.6c-7.1-7.1-6.8-18.5,0-25.3c6.8-6.8,18.5-6.8,25.3,0c7.1,7.1,6.8,18.5,0,25.3
<path class="st3" d="M293.3,496.6c-7.1-7.1-6.8-18.5,0-25.3s18.5-6.8,25.3,0c7.1,7.1,6.8,18.5,0,25.3
C311.5,503.7,300.1,503.7,293.3,496.6z M312.6,477.6c-3.7-3.7-9.7-3.7-13.4,0c-3.7,3.7-3.7,9.7,0,13.4s9.7,3.7,13.4,0
C316.3,487.2,316.3,481.3,312.6,477.6z" />
</g>
<g>
<path class="st3" d="M293.3,496.6l-84.5-84.5c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0l84.5,84.5c1.7,1.7,1.7,4.3,0,6
<path class="st3" d="M293.3,496.6l-84.5-84.5c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l84.5,84.5c1.7,1.7,1.7,4.3,0,6
C297.5,498.3,295,498.3,293.3,496.6z" />
</g>
<g>
<path class="st3" d="M194.6,398.2c-0.3-0.3-0.3-0.3-0.6-0.6c-0.3-0.3-0.3-0.6-0.3-0.9c0-0.3-0.3-0.6-0.3-0.9s0-0.6,0-0.9
c0-0.3,0-0.6,0-0.9c0-0.3,0-0.6,0.3-0.9c0.3,0,0.3-0.3,0.6-0.6s0.3-0.6,0.6-0.6c0.3,0,0.3-0.3,0.6-0.6c0.3-0.3,0.6-0.3,0.9-0.3
c0.3,0,0.6,0,0.9-0.3c0.3-0.3,0.6,0,0.9,0c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0,0.9,0.3c0.3,0,0.6,0.3,0.9,0.3c0.6,0.3,0.9,0.6,1.1,1.1
c0.3,0.3,0.3,0.6,0.3,0.9c0,0.3,0.3,0.6,0.3,0.9c0,0.3,0,0.6,0,0.9s0,0.6,0,0.9c0,0.3,0,0.6-0.3,0.9c-0.3,0.3-0.3,0.6-0.3,0.9
c-0.3,0.3-0.3,0.6-0.6,0.6s-0.3,0.3-0.6,0.6c-0.3,0.3-0.6,0.3-0.9,0.3c-0.3,0-0.6,0.3-0.9,0.3s-0.6,0-0.9,0c-0.3,0-0.6,0-0.9,0
c-0.3,0-0.6,0-0.9-0.3c-0.3,0-0.6-0.3-0.9-0.3C195.1,398.8,194.8,398.5,194.6,398.2z" />
<path class="st3" d="M194.6,398.2c-0.3-0.3-0.3-0.3-0.6-0.6c-0.3-0.3-0.3-0.6-0.3-0.9s-0.3-0.6-0.3-0.9s0-0.6,0-0.9s0-0.6,0-0.9
s0-0.6,0.3-0.9c0.3,0,0.3-0.3,0.6-0.6s0.3-0.6,0.6-0.6s0.3-0.3,0.6-0.6c0.3-0.3,0.6-0.3,0.9-0.3c0.3,0,0.6,0,0.9-0.3
c0.3-0.3,0.6,0,0.9,0c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0,0.9,0.3c0.3,0,0.6,0.3,0.9,0.3c0.6,0.3,0.9,0.6,1.1,1.1
c0.3,0.3,0.3,0.6,0.3,0.9s0.3,0.6,0.3,0.9s0,0.6,0,0.9s0,0.6,0,0.9s0,0.6-0.3,0.9s-0.3,0.6-0.3,0.9c-0.3,0.3-0.3,0.6-0.6,0.6
s-0.3,0.3-0.6,0.6c-0.3,0.3-0.6,0.3-0.9,0.3c-0.3,0-0.6,0.3-0.9,0.3s-0.6,0-0.9,0c-0.3,0-0.6,0-0.9,0c-0.3,0-0.6,0-0.9-0.3
c-0.3,0-0.6-0.3-0.9-0.3C195.1,398.8,194.8,398.5,194.6,398.2z" />
</g>
</g>
<g>
@@ -121,46 +120,43 @@
l0.3,0.3C940.1,42.4,940.1,60.8,928.7,72.2z M893.2,36.7c-8,8-8,21,0,29.3l0.3,0.3c8,8,21,8,29.3,0c8-8,8-21,0-29.3l-0.3-0.3
C914.2,28.7,901.1,28.7,893.2,36.7z" />
<path class="st3" d="M896.9,55.2c-2,2-2,5.1,0,7.1c2,2,5.1,2,7.1,0l0,0c2-2,2-5.1,0-7.1C902.3,53.2,898.8,53.2,896.9,55.2
L896.9,55.2z M911.4,40.6c-2,2-2,5.1,0,7.1c2,2,5.1,2,7.1,0l0,0c2-2,2-5.1,0-7.1C916.8,38.7,913.4,38.7,911.4,40.6L911.4,40.6z" />
L896.9,55.2z M911.4,40.6c-2,2-2,5.1,0,7.1s5.1,2,7.1,0l0,0c2-2,2-5.1,0-7.1C916.8,38.7,913.4,38.7,911.4,40.6L911.4,40.6z" />
<path class="st2"
d="M971.9,88.7l10.8,10.8c7.4,7.4,7.4,19.3,0,26.7l0,0c-7.4,7.4-19.3,7.4-26.7,0l-10.8-10.8L971.9,88.7z" />
<path class="st3" d="M985.9,129.4c-9.1,9.1-23.6,9.1-32.7,0l-8.8-8.8c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0l8.8,8.8
c5.7,5.7,15.1,5.7,20.8,0c5.7-5.7,5.7-15.1,0-20.8l-8.8-8.8c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l8.8,8.8
C995,105.8,995,120.3,985.9,129.4z" />
<path class="st3" d="M953.2,112.9c-1.7,1.7-4.3,1.7-6,0l-9.4-9.4c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0l9.4,9.4
C954.9,108.6,954.9,111.2,953.2,112.9z M969.4,97c-1.7,1.7-4.3,1.7-6,0l-9.4-9.4c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0
l9.4,9.4C971.1,92.4,971.1,95.3,969.4,97z" />
<path class="st3" d="M978.8,88.2l-34.1,34.1c-1.7,1.7-4.3,1.7-6,0c-1.7-1.7-1.7-4.3,0-6l34.1-34.1c1.7-1.7,4.3-1.7,6,0
C980.5,83.9,980.5,86.4,978.8,88.2z M994.4,137.9c-1.7,1.7-4.3,1.7-6,0l-8.5-8.5c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0
l8.5,8.5C995.8,133.4,996.1,136.2,994.4,137.9z" />
<path class="st3" d="M985.9,129.4c-9.1,9.1-23.6,9.1-32.7,0l-8.8-8.8c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l8.8,8.8
c5.7,5.7,15.1,5.7,20.8,0s5.7-15.1,0-20.8l-8.8-8.8c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l8.8,8.8C995,105.8,995,120.3,985.9,129.4z" />
<path class="st3" d="M953.2,112.9c-1.7,1.7-4.3,1.7-6,0l-9.4-9.4c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l9.4,9.4
C954.9,108.6,954.9,111.2,953.2,112.9z M969.4,97c-1.7,1.7-4.3,1.7-6,0l-9.4-9.4c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l9.4,9.4
C971.1,92.4,971.1,95.3,969.4,97z" />
<path class="st3" d="M978.8,88.2l-34.1,34.1c-1.7,1.7-4.3,1.7-6,0s-1.7-4.3,0-6l34.1-34.1c1.7-1.7,4.3-1.7,6,0
C980.5,83.9,980.5,86.4,978.8,88.2z M994.4,137.9c-1.7,1.7-4.3,1.7-6,0l-8.5-8.5c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l8.5,8.5
C995.8,133.4,996.1,136.2,994.4,137.9z" />
<g>
<path class="st5" d="M966.5,110.1c-1.4,1.4-1.4,4,0,5.4c1.4,1.4,4,1.4,5.4,0l0,0c1.4-1.4,1.4-4,0-5.4
C970.5,108.6,968,108.6,966.5,110.1L966.5,110.1z" />
<path class="st5" d="M966.5,110.1c-1.4,1.4-1.4,4,0,5.4s4,1.4,5.4,0l0,0c1.4-1.4,1.4-4,0-5.4C970.5,108.6,968,108.6,966.5,110.1
L966.5,110.1z" />
</g>
</g>
<g>
<path class="st7" d="M486.4,389.4l97.6,97.6l10.8-10.8c6-6,6-15.6,0-21.6l-38.7-38.7l-1.7-0.6c-6.5-2.3-8.5-10.5-3.7-15.6
l-3.4-3.4C529.9,378.6,509.4,379.4,486.4,389.4" />
<path class="st7" d="M486.4,389.4L584,487l10.8-10.8c6-6,6-15.6,0-21.6l-38.7-38.7l-1.7-0.6c-6.5-2.3-8.5-10.5-3.7-15.6l-3.4-3.4
C529.9,378.6,509.4,379.4,486.4,389.4" />
<path class="st8" d="M517.7,382.6C517.4,382.6,517.4,382.6,517.7,382.6c-1.1-0.3-2-0.3-2.8-0.3c-8.8,0.3-18.2,2.8-28.2,6.8l0,0
l31,31C527.9,410.1,527.9,393.1,517.7,382.6 M599.3,465c-10.2-7.7-25-6.8-34.7,2.6l19.3,19.3l10.8-10.8
C597.9,473,599.3,469,599.3,465" />
<path class="st3" d="M581.1,490.1l-97.6-97.8c-1.1-1.1-1.4-2.3-1.1-3.7c0.3-1.4,1.1-2.6,2.6-3.1c27.9-11.9,48.6-9.4,65.7,8
l0.3,0.3c1.7,1.7,1.7,4.3,0,6s-4.3,1.7-6,0l-0.3-0.3c-13.4-13.4-29.3-16.2-50.6-8.2L584,481l8-8c4.3-4.3,4.3-11.4,0-15.6
l-34.4-34.4c-1.1-1.1-1.4-2.3-1.1-3.7l1.7-10.8c0.3-2.3,2.6-4,4.8-3.4c2.3,0.3,4,2.6,3.4,4.8l-1.4,8.5l33,33
L557.6,423c-1.1-1.1-1.4-2.3-1.1-3.7l1.7-10.8c0.3-2.3,2.6-4,4.8-3.4c2.3,0.3,4,2.6,3.4,4.8l-1.4,8.5l33,33
c7.7,7.7,7.7,20.2,0,27.9l-10.8,10.8C585.4,491.8,582.8,491.5,581.1,490.1z" />
<path class="st2" d="M509.4,390.5c-6-6-15.6-6-21.6,0c-6,6-6,15.6,0,21.6l0,0c6,6,15.6,6,21.6,0
C515.7,406.4,515.7,396.8,509.4,390.5L509.4,390.5z M565.8,434.6c-3.4-3.4-8.8-3.4-11.9,0c-3.1,3.4-3.4,8.8,0,11.9
c3.4,3.1,8.8,3.4,11.9,0C569.2,443.4,569.2,438,565.8,434.6z" />
<path class="st3" d="M484.7,415.3c-7.7-7.7-7.7-20.2,0-27.9s20.2-7.7,27.9,0c7.7,7.7,7.7,20.2,0,27.9
C504.9,422.9,492.4,422.9,484.7,415.3z M506.6,393.6c-4.3-4.3-11.4-4.3-15.6,0c-4.3,4.3-4.3,11.4,0,15.6c4.3,4.3,11.4,4.3,15.6,0
C510.9,405,510.9,397.9,506.6,393.6z" />
<path class="st2" d="M509.4,390.5c-6-6-15.6-6-21.6,0s-6,15.6,0,21.6l0,0c6,6,15.6,6,21.6,0C515.7,406.4,515.7,396.8,509.4,390.5
L509.4,390.5z M565.8,434.6c-3.4-3.4-8.8-3.4-11.9,0c-3.1,3.4-3.4,8.8,0,11.9s8.8,3.4,11.9,0C569.2,443.4,569.2,438,565.8,434.6z" />
<path class="st3" d="M484.7,415.3c-7.7-7.7-7.7-20.2,0-27.9s20.2-7.7,27.9,0s7.7,20.2,0,27.9C504.9,422.9,492.4,422.9,484.7,415.3
z M506.6,393.6c-4.3-4.3-11.4-4.3-15.6,0c-4.3,4.3-4.3,11.4,0,15.6c4.3,4.3,11.4,4.3,15.6,0C510.9,405,510.9,397.9,506.6,393.6z" />
<path class="st2" d="M594.5,475.6c-6-6-15.6-6-21.6,0s-6,15.6,0,21.6l0,0c6,6,15.6,6,21.6,0S600.5,481.5,594.5,475.6L594.5,475.6z
" />
<path class="st3" d="M569.7,500.3c-7.7-7.7-7.7-20.2,0-27.9s20.2-7.7,27.9,0s7.7,20.2,0,27.9C589.9,508,577.4,508,569.7,500.3z
M591.4,478.4c-4.3-4.3-11.4-4.3-15.6,0s-4.3,11.4,0,15.6s11.4,4.3,15.6,0S595.6,483,591.4,478.4z" />
<path class="st3" d="M569.7,500.3l-98.1-98.1c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0l98.1,98.1c1.7,1.7,1.7,4.3,0,6
C574,502,571.4,502,569.7,500.3z M459.7,390.2c-0.9-0.9-1.4-2-1.1-3.1c0-1.1,0.3-2.3,1.1-3.1c0.9-0.9,2-1.4,3.1-1.1
c2.3,0,4.3,2,4.3,4.3c0,1.1-0.3,2.3-1.1,3.1c-0.9,0.9-2,1.4-3.1,1.1C461.4,391.4,460.5,391.1,459.7,390.2z" />
<path class="st3" d="M569.7,500.3l-98.1-98.1c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l98.1,98.1c1.7,1.7,1.7,4.3,0,6
S571.4,502,569.7,500.3z M459.7,390.2c-0.9-0.9-1.4-2-1.1-3.1c0-1.1,0.3-2.3,1.1-3.1c0.9-0.9,2-1.4,3.1-1.1c2.3,0,4.3,2,4.3,4.3
c0,1.1-0.3,2.3-1.1,3.1c-0.9,0.9-2,1.4-3.1,1.1C461.4,391.4,460.5,391.1,459.7,390.2z" />
</g>
<g>
<path class="st2" d="M306.9,39.8l-35-35.3c-7.7-7.7-19.9-8.2-28.2-1.7L218.2,23l70.3,70.3L308.6,68
@@ -174,17 +170,16 @@
<path class="st3" d="M196.8,64.3c-1.7-1.7-1.7-4.3,0-6L215,40.1c4-4,4-10.2,0-13.9c-4-4-10.2-4-13.9,0l-18.2,18.2
c-1.7,1.7-4.3,1.7-6,0s-1.7-4.3,0-6l18.2-18.2c7.1-7.1,18.8-7.1,26.2,0s7.1,18.8,0,26.2l-18.2,18.2C201.1,66,198.5,66,196.8,64.3z
M267.1,134.5c-1.7-1.7-1.7-4.3,0-6l18.2-18.2c4-4,4-10.2,0-13.9c-4-3.7-10.2-4-13.9,0l-18.2,18.2c-1.7,1.7-4.3,1.7-6,0
c-1.7-1.7-1.7-4.3,0-6l18.2-18.2c7.1-7.1,18.8-7.1,26.2,0c7.1,7.1,7.1,18.8,0,26.2l-18.2,18.2
C271.6,136.2,268.8,136.2,267.1,134.5z" />
s-1.7-4.3,0-6l18.2-18.2c7.1-7.1,18.8-7.1,26.2,0c7.1,7.1,7.1,18.8,0,26.2l-18.2,18.2C271.6,136.2,268.8,136.2,267.1,134.5z" />
<path class="st3" d="M289.6,90.1l-0.3-0.3c-19.1-25.9-41.8-48.6-67.4-67.7c-1.7-1.4-2.3-4-0.9-6c1.4-1.7,4-2.3,6-0.9
c26.5,19.6,49.8,43,69.4,69.4c1.4,1.7,1.1,4.6-0.9,6C293.5,91.6,291.3,91.6,289.6,90.1z" />
<path class="st3" d="M215,25.9l-0.3-0.3c-1.4-1.7-1.1-4.6,0.6-6l25.6-20.2c10.2-8,24.7-7.1,34.1,2l35,35.3c9.4,9.4,10,23.9,2,34.1
l-20.2,25.3c-1.4,1.7-4,2-6,0.6c-2-1.4-2-4-0.6-6l20.2-25.3c5.4-6.8,4.8-16.5-1.1-22.8L268.8,7.4c-6.3-6.3-15.9-6.8-22.8-1.4
l-25.6,20.2C218.7,27.8,216.5,27.6,215,25.9z M264.2,125.7l-13.9,13.9l-8-8l13.9-13.9 M193.7,55.2l-13.9,13.9l-8-8l13.9-13.9" />
l-20.2,25.3c-1.4,1.7-4,2-6,0.6s-2-4-0.6-6l20.2-25.3c5.4-6.8,4.8-16.5-1.1-22.8L268.8,7.4C262.5,1.1,252.9,0.6,246,6l-25.6,20.2
C218.7,27.8,216.5,27.6,215,25.9z M264.2,125.7l-13.9,13.9l-8-8l13.9-13.9 M193.7,55.2l-13.9,13.9l-8-8l13.9-13.9" />
<path class="st3" d="M247.2,142.8l-8.2-8.2c-1.7-1.7-1.7-4.3,0-6l13.9-13.9c1.7-1.7,4.3-1.7,6,0s1.7,4.3,0,6l-11.1,11.1l2,2
l11.1-11.1c1.7-1.7,4.3-1.7,6,0c1.7,1.7,1.7,4.3,0,6l-13.7,14.2C251.4,144.5,248.9,144.5,247.2,142.8z M176.6,72.2l-8-8
c-1.7-1.7-1.7-4.3,0-6l13.9-13.9c1.7-1.7,4.3-1.7,6,0s1.7,4.3,0,6l-11.1,11.1l2,2l11.1-11.1c1.7-1.7,4.3-1.7,6,0
c1.7,1.7,1.7,4.3,0,6l-13.9,13.9C180.9,73.9,178.3,73.9,176.6,72.2z" />
l11.1-11.1c1.7-1.7,4.3-1.7,6,0s1.7,4.3,0,6l-13.7,14.2C251.4,144.5,248.9,144.5,247.2,142.8z M176.6,72.2l-8-8
c-1.7-1.7-1.7-4.3,0-6l13.9-13.9c1.7-1.7,4.3-1.7,6,0s1.7,4.3,0,6l-11.1,11.1l2,2l11.1-11.1c1.7-1.7,4.3-1.7,6,0s1.7,4.3,0,6
l-13.9,13.9C180.9,73.9,178.3,73.9,176.6,72.2z" />
<path class="st3" d="M270.2,137.6l-96.4-96.4c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l96.4,96.4c1.7,1.7,1.7,4.3,0,6
C274.5,139.4,271.9,139.4,270.2,137.6z" />
<path class="st2" d="M239.5,88.7l-16.8-16.8c-1.7-1.7-1.7-4.3,0-6l10-10c6.3-6.3,16.5-6.3,23,0l0,0c6.3,6.3,6.3,16.5,0,23l-10,10
@@ -194,7 +189,7 @@
<path class="st2" d="M213,28.1c-2.8-2.8-7.4-2.8-10,0c-2.8,2.8-2.8,7.4,0,10c2.8,2.8,7.4,2.8,10,0C215.6,35.2,215.6,30.7,213,28.1
z M283.3,98.4c-2.8-2.8-7.4-2.8-10,0s-2.8,7.4,0,10s7.4,2.8,10,0C285.9,105.5,286.2,101.2,283.3,98.4z" />
<path class="st3" d="M253.2,148.7L169,64.5c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l84.2,84.2c1.7,1.7,1.7,4.3,0,6
C257.4,150.4,254.9,150.4,253.2,148.7z M274.2,157.8c-1.7-1.7-4.3-1.7-6,0s-1.7,4.3,0,6c1.7,1.7,4.3,1.7,6,0
C257.4,150.4,254.9,150.4,253.2,148.7z M274.2,157.8c-1.7-1.7-4.3-1.7-6,0s-1.7,4.3,0,6s4.3,1.7,6,0
C275.9,162.1,275.9,159.5,274.2,157.8z" />
</g>
<g>
@@ -208,7 +203,7 @@
<path class="st3" d="M467.1,235.8l-40.4,40.1c-4.6,4.6-11.7,4.6-16.2,0l-68.3-68.3c-4.6-4.6-4.6-11.7,0-16.2l40.1-40.1
c4.6-4.6,11.7-4.6,16.2,0l68.3,68.3C471.3,224.1,471.3,231.2,467.1,235.8z M348.2,197.7c-1.1,1.1-1.1,2.8,0,4l68.3,68.3
c1.1,1.1,2.8,1.1,4,0l40.4-40.1c1.1-1.1,1.1-2.8,0-4l-68.3-68.3c-1.1-1.1-2.8-1.1-4,0L348.2,197.7z" />
<path class="st2" d="M421.5,260.8c-1.7,1.7-4.3,1.7-6,0l-25-25c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0l25,25
<path class="st2" d="M421.5,260.8c-1.7,1.7-4.3,1.7-6,0l-25-25c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l25,25
C423.3,256.5,423.3,259.1,421.5,260.8z M381.4,220.7c-0.9,0.9-2,1.4-3.1,1.1c-1.1,0-2.3-0.3-3.1-1.1c-0.9-0.9-1.4-2-1.1-3.1
c0-0.3,0-0.6,0-0.9c0-0.3,0-0.6,0.3-0.9c0.3-0.3,0.3-0.6,0.3-0.9c0.9-1.1,2-2,3.4-2c1.1,0,2.3,0.3,3.1,1.1
c0.3,0.3,0.3,0.3,0.6,0.6c0.3,0.3,0.3,0.6,0.3,0.9c0,0.3,0.3,0.6,0.3,0.9c0,0.3,0,0.6,0,0.9C382.9,218.7,382.3,219.8,381.4,220.7z
@@ -222,25 +217,25 @@
d="M905.4,490.4L798.2,383.1c-6-6-6-15.4,0-21.3l53.2-53.2c5.7-5.7,15.4-5.7,21.3,0l107.2,107.2" />
<path class="st2" d="M892,364.3l-38.1,38.1c-2.8,2.8-7.7,2.8-10.5,0l-24.7-24.7c-2.8-2.8-2.8-7.7,0-10.5l38.1-38.1
c2.8-2.8,7.7-2.8,10.5,0l24.7,24.7C894.9,356.7,894.9,361.5,892,364.3z" />
<path class="st3" d="M983.3,419.5c-2,2-5.4,2-7.4,0l-107-107c-3.7-3.7-10.2-4-13.9,0l-53.2,53.2c-3.7,3.7-3.7,10,0,13.9
l107.2,107.2c2,2,2,5.4,0,7.4c-2,2-5.4,2-7.4,0L794.5,387.1c-8-8-8-20.8,0-28.7l53.2-53.2c8-8,20.8-8,28.7,0l107.2,107.2
<path class="st3" d="M983.3,419.5c-2,2-5.4,2-7.4,0l-107-107c-3.7-3.7-10.2-4-13.9,0l-53.2,53.2c-3.7,3.7-3.7,10,0,13.9L909,486.8
c2,2,2,5.4,0,7.4s-5.4,2-7.4,0L794.5,387.1c-8-8-8-20.8,0-28.7l53.2-53.2c8-8,20.8-8,28.7,0l107.2,107.2
C985.6,414.4,985.3,417.5,983.3,419.5z" />
<path class="st4" d="M892,364.3l-38.1,38.1c-2.8,2.8-7.7,2.8-10.5,0l-24.7-24.7c-2.8-2.8-2.8-7.7,0-10.5l38.1-38.1
c2.8-2.8,7.7-2.8,10.5,0l24.7,24.7C894.9,356.7,894.9,361.5,892,364.3z" />
<path class="st3" d="M895.7,368l-38.1,38.1c-4.8,4.8-13.1,4.8-17.9,0l-24.7-24.7c-4.8-4.8-4.8-13.1,0-17.9l38.1-38.1
<path class="st3" d="M895.7,368l-38.1,38.1c-4.8,4.8-13.1,4.8-17.9,0L815,381.4c-4.8-4.8-4.8-13.1,0-17.9l38.1-38.1
c4.8-4.8,13.1-4.8,17.9,0l24.7,24.7C900.6,355,900.6,362.9,895.7,368z M822.3,370.9c-0.9,0.9-0.9,2.3,0,3.1l24.7,24.7
c0.9,0.9,2.3,0.9,3.1,0l38.1-38.1c0.9-0.9,0.9-2.3,0-3.1l-24.7-24.7c-0.9-0.9-2.3-0.9-3.1,0L822.3,370.9z M994.1,409l-95.3,95.3
c-2,2-5.4,2-7.4,0c-2-2-2-5.4,0-7.4l95.3-95.3c2-2,5.4-2,7.4,0C996.1,403.6,996.1,407,994.1,409z M988.4,392.8
c-4.8,4.8-13.1,4.8-17.9,0l-43-43c-0.9-0.9-2.3-0.9-3.1,0l-5.4,5.4c-2,2-5.4,2-7.4,0c-2-2-2-5.4,0-7.4l5.4-5.4
c4.8-4.8,13.1-4.8,17.9,0l43,43c0.9,0.9,2.3,0.9,3.1,0c0.9-0.9,0.9-2.3,0-3.1l-58.9-58.9c-2-2-2-5.4,0-7.4c2-2,5.4-2,7.4,0
l58.9,58.9C993.3,379.7,993.3,388,988.4,392.8z" />
c-2,2-5.4,2-7.4,0s-2-5.4,0-7.4l95.3-95.3c2-2,5.4-2,7.4,0C996.1,403.6,996.1,407,994.1,409z M988.4,392.8
c-4.8,4.8-13.1,4.8-17.9,0l-43-43c-0.9-0.9-2.3-0.9-3.1,0l-5.4,5.4c-2,2-5.4,2-7.4,0s-2-5.4,0-7.4l5.4-5.4
c4.8-4.8,13.1-4.8,17.9,0l43,43c0.9,0.9,2.3,0.9,3.1,0c0.9-0.9,0.9-2.3,0-3.1l-58.9-58.9c-2-2-2-5.4,0-7.4s5.4-2,7.4,0l58.9,58.9
C993.3,379.7,993.3,388,988.4,392.8z" />
<path class="st3" d="M934.7,318.3l-10.5,10.5c-2,2-5.4,2-7.4,0l-21.6-21.6c-2-2-2-5.4,0-7.4l1.7-1.7l-4.8-4.8c-2-2-2-5.4,0-7.4
c2-2,5.4-2,7.4,0l4.8,4.8l1.4-1.4c2-2,5.4-2,7.4,0l21.3,21.3C936.7,312.9,936.7,316.3,934.7,318.3z M920.5,317.7l3.1-3.1
l-13.9-13.9l-3.1,3.1L920.5,317.7z" />
s5.4-2,7.4,0l4.8,4.8l1.4-1.4c2-2,5.4-2,7.4,0l21.3,21.3C936.7,312.9,936.7,316.3,934.7,318.3z M920.5,317.7l3.1-3.1l-13.9-13.9
l-3.1,3.1L920.5,317.7z" />
<g>
<path class="st5" d="M923.9,444.6c-0.6,0.6-1.4,1.1-2.6,1.4c-2.8,0.6-5.7-1.1-6.3-3.7l-3.7-16.2l-3.7,5.4c-1.1,1.7-3.1,2.6-5.1,2
c-2-0.6-3.7-2-4-4l-6.5-27c-0.6-2.8,1.1-5.7,3.7-6.3c2.8-0.6,5.7,1.1,6.3,3.7l3.7,16.2l3.7-5.4c1.1-1.7,3.1-2.6,5.1-2
c2,0.6,3.7,2,4,4l6.3,27C925.6,441.7,925.3,443.4,923.9,444.6z" />
s-3.7-2-4-4l-6.5-27c-0.6-2.8,1.1-5.7,3.7-6.3c2.8-0.6,5.7,1.1,6.3,3.7l3.7,16.2l3.7-5.4c1.1-1.7,3.1-2.6,5.1-2s3.7,2,4,4l6.3,27
C925.6,441.7,925.3,443.4,923.9,444.6z" />
</g>
</g>
<g>
@@ -251,9 +246,9 @@
<path class="st12" d="M749.8,413.3l3.7,3.7c1.1,1.1,1.4,2.8,0.6,4.6L708.6,496l-10-10l51.8-68.3
C751.2,416.1,750.9,414.4,749.8,413.3z" />
<g>
<path class="st3" d="M706.6,498L663,454.5c-0.6-0.6-0.9-1.4-0.9-2.3c0-0.9,0.6-1.7,1.4-2l74.5-45.2c2.6-1.4,5.7-1.1,8,0.9
l9.4,9.4c2,2,2.6,5.4,0.9,8l-45.5,74.2c-0.6,0.9-1.1,1.1-2,1.4C708,498.9,707.1,498.6,706.6,498z M669.6,453.1l38.4,38.4
l43.5-71.7c0.3-0.3,0.3-0.9,0-0.9l-9.4-9.4c-0.3-0.3-0.6-0.3-0.9,0L669.6,453.1z" />
<path class="st3" d="M706.6,498L663,454.5c-0.6-0.6-0.9-1.4-0.9-2.3s0.6-1.7,1.4-2L738,405c2.6-1.4,5.7-1.1,8,0.9l9.4,9.4
c2,2,2.6,5.4,0.9,8l-45.5,74.2c-0.6,0.9-1.1,1.1-2,1.4C708,498.9,707.1,498.6,706.6,498z M669.6,453.1l38.4,38.4l43.5-71.7
c0.3-0.3,0.3-0.9,0-0.9l-9.4-9.4c-0.3-0.3-0.6-0.3-0.9,0L669.6,453.1z" />
<path class="st3" d="M655.6,447.1c-1.1-1.1-1.1-2.8,0-4s2.8-1.1,4,0l58,58l0,0c1.1,1.1,1.1,2.8,0,4s-2.8,1.1-4,0L655.6,447.1
L655.6,447.1z" />
</g>
@@ -274,22 +269,21 @@
c0.3-0.6,0.3-1.1,0.6-2l-23.3-23.3c-2.8-2.8-7.1-2.8-10,0l-2.3,2.3c-2.8,2.8-2.8,7.1,0,10L664.2,347.8L664.2,347.8z" />
<path class="st3" d="M623.8,389.9l-0.3-0.3c-2.8-2.8-2.6-7.4,0.3-10.2c1.7-1.7,4.3-1.7,6,0c0.6,0.6,1.1,1.4,1.1,2.3l3.1-2.6
c14.5-11.7,25-27.3,31.3-44.9l1.1-3.4c2.8-8.8,0.6-18.2-6-24.7l-64.3-64.3c-9.4-9.4-24.7-9.4-34.1,0l-35.8,35.8
c-1.7,1.7-4.3,1.7-6,0c-1.7-1.7-1.7-4.3,0-6l35.8-35.8c12.8-12.8,33.6-12.8,46.4,0l64.3,64.3c8.8,8.8,11.9,21.6,8,33.6l-1.1,3.4
c-6.5,19.1-18.2,35.8-33.8,48.6l-5.7,4.8C630.6,392.8,626.6,392.5,623.8,389.9z M644.8,388.2c-1.7-1.7-1.7-4.3,0-6l18.2-18.2
c1.7-1.7,4.3-1.7,6,0c1.7,1.7,1.7,4.3,0,6l-18.2,18.2C649.4,389.9,646.5,389.9,644.8,388.2z" />
<path class="st3" d="M599.6,361.2l-46.9-46.9c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0l47.2,46.9c1.7,1.7,1.7,4.3,0,6
c-1.7,1.7-4.3,1.7-6,0s-1.7-4.3,0-6l35.8-35.8c12.8-12.8,33.6-12.8,46.4,0l64.3,64.3c8.8,8.8,11.9,21.6,8,33.6l-1.1,3.4
c-6.5,19.1-18.2,35.8-33.8,48.6l-5.7,4.8C630.6,392.8,626.6,392.5,623.8,389.9z M644.8,388.2c-1.7-1.7-1.7-4.3,0-6L663,364
c1.7-1.7,4.3-1.7,6,0s1.7,4.3,0,6l-18.2,18.2C649.4,389.9,646.5,389.9,644.8,388.2z" />
<path class="st3" d="M599.6,361.2l-46.9-46.9c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l47.2,46.9c1.7,1.7,1.7,4.3,0,6
C604.2,362.9,601.3,362.9,599.6,361.2z M531.3,292.7L516,277.3c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l15.4,15.4c1.7,1.7,1.7,4.3,0,6
C535.6,294.4,533,294.4,531.3,292.7z M671.9,299.2l-66.6-66.6c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l66.6,66.6c1.7,1.7,1.7,4.3,0,6
S673.6,300.9,671.9,299.2z M640.3,379.7c-1.7-1.7-1.7-4-0.3-6c7.7-8.8,5.4-26.5-4.6-36.4l-87.6-87.6c-1.7-1.7-1.7-4.3,0-6
s4.3-1.7,6,0l87.6,87.6c13.1,13.1,15.4,35.8,4.8,48.1C644.8,381.1,642.3,381.4,640.3,379.7L640.3,379.7z M565.2,246.9l-6.3-6.3
c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0l6.3,6.3c1.7,1.7,1.7,4.3,0,6C569.7,248.6,566.9,248.6,565.2,246.9z M577.4,242.9
l-6.3-6.3c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0l6.3,6.3c1.7,1.7,1.7,4.3,0,6C581.7,244.6,579.1,244.6,577.4,242.9z" />
c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l6.3,6.3c1.7,1.7,1.7,4.3,0,6C569.7,248.6,566.9,248.6,565.2,246.9z M577.4,242.9l-6.3-6.3
c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l6.3,6.3c1.7,1.7,1.7,4.3,0,6C581.7,244.6,579.1,244.6,577.4,242.9z" />
<path class="st2" d="M623.5,359.5c-5.4-5.4-13.9-5.4-19.3,0c-5.4,5.4-5.4,13.9,0,19.3l0,0c5.4,5.4,13.9,5.4,19.3,0
C628.6,373.4,628.6,364.9,623.5,359.5L623.5,359.5z" />
<path class="st3" d="M601,382c-7.1-7.1-6.8-18.5,0-25.3c6.8-6.8,18.5-6.8,25.3,0c6.8,6.8,7.1,18.5,0,25.3
C619.5,389.1,608.1,388.8,601,382z M620.4,362.6c-3.7-3.7-9.7-3.7-13.4,0c-3.7,3.7-3.7,9.7,0,13.4c3.7,3.7,9.7,3.7,13.4,0
C624.1,372.3,624.1,366.3,620.4,362.6z" />
<path class="st2" d="M535.3,290.7c-5.4,5.4-5.4,13.9,0,19.3c5.4,5.4,13.9,5.4,19.3,0c2.3-2.3,3.7-5.1,4-8.2l-15.1-15.1
<path class="st3" d="M601,382c-7.1-7.1-6.8-18.5,0-25.3s18.5-6.8,25.3,0s7.1,18.5,0,25.3C619.5,389.1,608.1,388.8,601,382z
M620.4,362.6c-3.7-3.7-9.7-3.7-13.4,0s-3.7,9.7,0,13.4c3.7,3.7,9.7,3.7,13.4,0C624.1,372.3,624.1,366.3,620.4,362.6z" />
<path class="st2" d="M535.3,290.7c-5.4,5.4-5.4,13.9,0,19.3s13.9,5.4,19.3,0c2.3-2.3,3.7-5.1,4-8.2l-15.1-15.1
C540.4,287.3,537.6,288.4,535.3,290.7z" />
<path class="st3" d="M532.5,313.1c-7.1-7.1-6.8-18.5,0-25.3c2.8-2.8,6.5-4.6,10.8-5.1c1.4,0,2.6,0.3,3.4,1.1l15.1,15.1
c0.9,0.9,1.4,2.3,1.1,3.4c-0.3,4-2.3,8-5.1,10.8C550.7,320.3,539.3,320,532.5,313.1z M542.2,291.5c-1.4,0.3-2.6,1.1-3.7,2.3
@@ -300,7 +294,7 @@
</g>
<g>
<path class="st3" d="M623.8,385.4l-2.6-2.6c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l2.6,2.6c1.7,1.7,1.7,4.3,0,6
C628.1,387.1,625.5,387.1,623.8,385.4z" />
S625.5,387.1,623.8,385.4z" />
</g>
</g>
<g>
@@ -308,35 +302,35 @@
C611.3,101.5,575.1,152.2,554.4,172.9z" />
<path class="st5" d="M514.8,63.4l48.1-11.7c1.7-0.3,2.6,1.7,1.1,2.6l-28.4,18.8l9.7,9.7c0.9,0.9,0.3,2.3-0.9,2.3l-43.8,6.8
c-1.4,0.3-2.3-1.7-1.1-2.6l23.6-14.5l-9.4-9.4C513.4,64.8,513.7,63.7,514.8,63.4z" />
<path class="st3" d="M595.6,115.5l-15.1-15.1c-7.1-7.1-7.1-18.8,0-26.2l8-8c1.7-1.7,4.3-1.7,6,0c1.7,1.7,1.7,4.3,0,6l-8,8
<path class="st3" d="M595.6,115.5l-15.1-15.1c-7.1-7.1-7.1-18.8,0-26.2l8-8c1.7-1.7,4.3-1.7,6,0s1.7,4.3,0,6l-8,8
c-4,4-4,10.2,0,13.9l15.1,15.1c1.7,1.7,1.7,4.3,0,6C599.9,117.2,597.3,117.2,595.6,115.5z" />
<path class="st3" d="M548.4,178.9L447.7,78.2c-1.7-1.7-1.7-4.3,0-6l73.4-73.4c7.1-7.1,18.8-7.1,26.2,0l70.3,70.3l0,0l8,8
c1.7,1.7,1.7,4.3,0,6c-1.7,1.7-4.3,1.7-6,0l-2.3-2.3c-8.2,31.3-41.5,76.2-60,94.7l-3.1,3.1C552.7,180.6,549.8,180.6,548.4,178.9z
c1.7,1.7,1.7,4.3,0,6s-4.3,1.7-6,0l-2.3-2.3c-8.2,31.3-41.5,76.2-60,94.7l-3.1,3.1C552.7,180.6,549.8,180.6,548.4,178.9z
M456.8,75.4l94.4,94.4c21.6-21.6,54.6-69.1,58.9-96.1L541.3,4.8c-4-4-10.2-4-13.9,0C527.1,5.1,456.8,75.4,456.8,75.4z" />
<path class="st3" d="M548.4,178.9L443.7,74.2c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0l104.7,104.7c1.7,1.7,1.7,4.3,0,6
<path class="st3" d="M548.4,178.9L443.7,74.2c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l104.7,104.7c1.7,1.7,1.7,4.3,0,6
C552.7,180.6,549.8,180.6,548.4,178.9z" />
<path class="st2" d="M472.2,85.6c-5.4-5.4-13.9-5.4-19.3,0c-5.4,5.4-5.4,13.9,0,19.3l0,0c5.4,5.4,13.9,5.4,19.3,0
<path class="st2" d="M472.2,85.6c-5.4-5.4-13.9-5.4-19.3,0s-5.4,13.9,0,19.3l0,0c5.4,5.4,13.9,5.4,19.3,0
C477.6,99.8,477.6,91,472.2,85.6L472.2,85.6z M536.5,150.2c-5.4-5.4-13.9-5.4-19.3,0c-5.4,5.4-5.4,13.9,0,19.3l0,0
c5.4,5.4,13.9,5.4,19.3,0C541.9,164.1,541.9,155.3,536.5,150.2L536.5,150.2z" />
<path class="st3" d="M450,108.1c-7.1-7.1-6.8-18.5,0-25.3c6.8-6.8,18.5-6.8,25.3,0c6.8,6.8,6.8,18.5,0,25.3
<path class="st3" d="M450,108.1c-7.1-7.1-6.8-18.5,0-25.3c6.8-6.8,18.5-6.8,25.3,0s6.8,18.5,0,25.3
C468.2,114.9,456.8,115.2,450,108.1z M469,88.7c-3.7-3.7-9.7-3.7-13.4,0s-3.7,9.7,0,13.4c3.7,3.7,9.7,3.7,13.4,0
C472.7,98.4,473,92.4,469,88.7z" />
<g>
<path class="st3" d="M514.3,172.3c-7.1-7.1-6.8-18.5,0-25.3c6.8-6.8,18.5-6.8,25.3,0c7.1,7.1,6.8,18.5,0,25.3
C532.8,179.5,521.4,179.5,514.3,172.3z M533.6,153c-3.7-3.7-9.7-3.7-13.4,0c-3.7,3.7-3.7,9.7,0,13.4c3.7,3.7,9.7,3.7,13.4,0
<path class="st3" d="M514.3,172.3c-7.1-7.1-6.8-18.5,0-25.3s18.5-6.8,25.3,0c7.1,7.1,6.8,18.5,0,25.3
C532.8,179.5,521.4,179.5,514.3,172.3z M533.6,153c-3.7-3.7-9.7-3.7-13.4,0s-3.7,9.7,0,13.4s9.7,3.7,13.4,0
C537,162.7,537.3,156.7,533.6,153z" />
</g>
<g>
<path class="st3" d="M514.3,172.3l-84.5-84.5c-1.7-1.7-1.7-4.3,0-6c1.7-1.7,4.3-1.7,6,0l84.5,84.5c1.7,1.7,1.7,4.3,0,6
<path class="st3" d="M514.3,172.3l-84.5-84.5c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l84.5,84.5c1.7,1.7,1.7,4.3,0,6
C518.5,174.1,516,174.1,514.3,172.3z" />
</g>
<g>
<path class="st3" d="M415.6,73.9c-0.3-0.3-0.3-0.3-0.6-0.6c0-0.3-0.3-0.6-0.3-0.9s-0.3-0.6-0.3-0.9s0-0.6,0-0.9s0-0.6,0-0.9
c0-0.3,0-0.6,0.3-0.9c0.3,0,0.3-0.3,0.6-0.6c0.3-0.3,0.3-0.6,0.6-0.6c0.3,0,0.3-0.3,0.6-0.6c0.3-0.3,0.6-0.3,0.9-0.3
s0.6,0,0.9-0.3s0.6,0,0.9,0c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0,0.9,0.3c0.3,0.3,0.6,0.3,0.9,0.3c0.6,0.3,0.9,0.6,1.1,1.1
c0.3,0.3,0.3,0.6,0.3,0.9s0.3,0.6,0.3,0.9c0,0.3,0,0.6,0,0.9c0,0.3,0,0.6,0,0.9s0,0.6-0.3,0.9c0,0.3-0.3,0.6-0.3,0.9
c-0.3,0.3-0.3,0.6-0.6,0.6c-0.3,0-0.3,0.3-0.6,0.6c-0.3,0.3-0.6,0.3-0.9,0.3s-0.6,0.3-0.9,0.3c-0.3,0-0.6,0-0.9,0
c-0.3,0-0.6,0-0.9,0s-0.6,0-0.9-0.3c-0.3,0-0.6-0.3-0.9-0.3C416.1,74.5,415.9,73.9,415.6,73.9z" />
s0-0.6,0.3-0.9c0.3,0,0.3-0.3,0.6-0.6c0.3-0.3,0.3-0.6,0.6-0.6c0.3,0,0.3-0.3,0.6-0.6c0.3-0.3,0.6-0.3,0.9-0.3s0.6,0,0.9-0.3
s0.6,0,0.9,0s0.6,0,0.9,0s0.6,0,0.9,0.3s0.6,0.3,0.9,0.3c0.6,0.3,0.9,0.6,1.1,1.1c0.3,0.3,0.3,0.6,0.3,0.9s0.3,0.6,0.3,0.9
s0,0.6,0,0.9s0,0.6,0,0.9s0,0.6-0.3,0.9c0,0.3-0.3,0.6-0.3,0.9c-0.3,0.3-0.3,0.6-0.6,0.6c-0.3,0-0.3,0.3-0.6,0.6
c-0.3,0.3-0.6,0.3-0.9,0.3s-0.6,0.3-0.9,0.3s-0.6,0-0.9,0s-0.6,0-0.9,0s-0.6,0-0.9-0.3c-0.3,0-0.6-0.3-0.9-0.3
C416.1,74.5,415.9,73.9,415.6,73.9z" />
</g>
</g>
<path class="st3"
@@ -350,8 +344,8 @@
<path class="st3" d="M78.2,8.2l-8.8-8.5c-1.1-1.1-1.1-2.8,0-4c1.1-1.1,2.8-1.1,4,0l8.8,8.8c1.1,1.1,1.1,2.8,0,4
C81.4,9.6,79.4,9.4,78.2,8.2z" />
<g>
<path class="st2" d="M57.7,69.9L44.7,56.6c-3.7-3.7-3.7-10,0-13.7L82.2,5.4c3.7-3.7,10-3.7,13.7,0l13.4,13.4
c3.7,3.7,3.7,10,0,13.7L71.4,69.9C67.7,73.6,61.7,73.6,57.7,69.9z" />
<path class="st2" d="M57.7,69.9l-13-13.3c-3.7-3.7-3.7-10,0-13.7L82.2,5.4c3.7-3.7,10-3.7,13.7,0l13.4,13.4c3.7,3.7,3.7,10,0,13.7
L71.4,69.9C67.7,73.6,61.7,73.6,57.7,69.9z" />
</g>
<g>
<path class="st11" d="M95.6,18.2c-4.3-4.3-11.1-4.3-15.1,0c-4.3,4.3-4.3,11.1,0,15.1l0,0c4.3,4.3,11.1,4.3,15.1,0
@@ -371,8 +365,8 @@
C32.4,85.3,30.4,85.3,29.3,84.2z" />
</g>
<g>
<path class="st5" d="M802.4,224.7l-70.5-70.5l-46.9,46.9c-4.3,4.3-4.3,11.4,0,15.6l54.9,54.9c4.3,4.3,11.4,4.3,15.6,0L802.4,224.7
z" />
<path class="st5" d="M802.4,224.7l-70.5-70.5L685,201.1c-4.3,4.3-4.3,11.4,0,15.6l54.9,54.9c4.3,4.3,11.4,4.3,15.6,0L802.4,224.7z
" />
<path class="st2" d="M731,207.9l33.3-8.2c1.1-0.3,1.7,1.1,0.9,1.7l-19.6,13.1l6.8,6.8c0.6,0.6,0.3,1.4-0.6,1.7l-30.2,4.8
c-1.1,0.3-1.4-1.1-0.6-1.7l16.2-9.7l-6.5-6.5C729.9,209,730.2,208.2,731,207.9z" />
<path class="st6" d="M794.7,217l-45.5,45.5c-0.9,0.9-2.3,0.9-3.1,0L712.5,229c-2.3-2.3-5.7-2.3-8,0l0,0c-2.3,2.3-2.3,5.7,0,8
@@ -383,11 +377,11 @@
c-3.1,3.1-3.1,8,0,11.1l54.9,54.9c3.1,3.1,8,3.1,11.1,0l46.9-46.9c1.4-1.4,3.4-1.4,4.8,0c1.4,1.4,1.4,3.4,0,4.8l-46.9,46.9
C752.1,279.6,743.3,279.6,737.6,274.2z" />
<path class="st2" d="M714,188c-1.4-1.4-1.4-3.4,0-4.8l23.6-23.6c1.4-1.4,3.4-1.4,4.8,0c1.4,1.4,1.4,3.4,0,4.8L718.8,188
C717.4,189.1,715.4,189.1,714,188z M702.3,199.7c-0.3-0.3-0.3-0.3-0.3-0.6c-0.3-0.3-0.3-0.3-0.3-0.6c0-0.3,0-0.3-0.3-0.6
C717.4,189.1,715.4,189.1,714,188z M702.3,199.7c-0.3-0.3-0.3-0.3-0.3-0.6c-0.3-0.3-0.3-0.3-0.3-0.6s0-0.3-0.3-0.6
c0-0.3,0-0.3,0-0.6s0-0.6,0-0.6c0-0.3,0-0.3,0.3-0.6c0-0.3,0.3-0.3,0.3-0.6s0.3,0,0.3-0.3c0.3-0.3,0.3-0.3,0.6-0.3
c0.3,0,0.3-0.3,0.6-0.3s0.6-0.3,0.6-0.3c0.3,0,0.3,0,0.6,0s0.6,0,0.6,0s0.3,0,0.6,0.3c0.3,0,0.3,0.3,0.6,0.3
c0.9,0.6,1.4,1.7,1.4,2.8c0,0.3,0,0.6,0,0.6c0,0.3,0,0.3-0.3,0.6c0,0.3-0.3,0.3-0.3,0.6s-0.6,0.3-0.6,0.3
c-0.6,0.6-1.4,1.1-2.3,0.9C703.7,200.8,702.9,200.2,702.3,199.7z" />
s0.3-0.3,0.6-0.3s0.6-0.3,0.6-0.3c0.3,0,0.3,0,0.6,0s0.6,0,0.6,0s0.3,0,0.6,0.3c0.3,0,0.3,0.3,0.6,0.3c0.9,0.6,1.4,1.7,1.4,2.8
c0,0.3,0,0.6,0,0.6c0,0.3,0,0.3-0.3,0.6c0,0.3-0.3,0.3-0.3,0.6s-0.6,0.3-0.6,0.3c-0.6,0.6-1.4,1.1-2.3,0.9
C703.7,200.8,702.9,200.2,702.3,199.7z" />
<path class="st3" d="M804.1,230.9l-78.2-78.2c-1.4-1.4-1.4-3.4,0-4.8l9.4-9.4c1.4-1.4,3.4-1.4,4.8,0l78.2,78.2
c1.4,1.4,1.4,3.4,0,4.8l-9.4,9.4C807.5,232.4,805.3,232.4,804.1,230.9z M732.7,150.4l73.7,73.7l4.8-4.8l-73.7-73.7L732.7,150.4z" />
<path class="st3" d="M758.6,166.7c-1.4-1.4-1.4-3.4,0-4.8l7.1-7.1l-7.1-7.1l-0.9,0.9c-1.4,1.4-3.4,1.4-4.8,0
@@ -401,20 +395,20 @@
<path class="st8" d="M909.4,164.4C909.4,164.1,909.4,164.1,909.4,164.4c-1.1-0.3-2-0.3-2.8-0.3c-8.8,0.3-18.2,2.8-28.2,6.8l0,0
l31,31C919.6,191.7,919.6,174.6,909.4,164.4 M991,246.6c-10.2-7.7-25-6.8-34.7,2.6l19.3,19.3l10.8-10.8
C989.6,254.6,991,250.6,991,246.6" />
<path class="st3" d="M972.8,271.6l-97.6-97.6c-1.1-1.1-1.4-2.3-1.1-3.7c0.3-1.4,1.1-2.6,2.6-3.1c27.9-11.9,48.6-9.4,65.7,8
l0.3,0.3c1.7,1.7,1.7,4.3,0,6c-1.7,1.7-4.3,1.7-6,0l-0.3-0.3c-13.4-13.4-29.3-16.2-50.6-8.2l89.9,89.9l8-8
c4.3-4.3,4.3-11.4,0-15.6l-34.4-34.7c-1.1-1.1-1.4-2.3-1.1-3.7l1.7-10.8c0.3-2.3,2.6-4,4.8-3.4c2.3,0.3,4,2.6,3.4,4.8l-1.4,8.5
l33.3,33c7.7,7.7,7.7,20.2,0,27.9l-10.8,10.8C977.4,273.3,974.5,273.3,972.8,271.6z" />
<path class="st2" d="M901.4,172.3c-6-6-15.6-6-21.6,0c-6,6-6,15.6,0,21.6l0,0c6,6,15.6,6,21.6,0
C907.4,188,907.4,178.3,901.4,172.3L901.4,172.3z M957.7,216.4c-3.4-3.4-8.8-3.4-11.9,0c-3.4,3.4-3.4,8.8,0,11.9
c3.4,3.1,8.8,3.4,11.9,0C960.9,225,960.9,219.6,957.7,216.4z" />
<path class="st3" d="M876.7,197.1c-7.7-7.7-7.7-20.2,0-27.9c7.7-7.7,20.2-7.7,27.9,0s7.7,20.2,0,27.9
C896.6,204.8,884.3,204.8,876.7,197.1z M898.3,175.2c-4.3-4.3-11.4-4.3-15.6,0c-4.3,4.3-4.3,11.4,0,15.6c4.3,4.3,11.4,4.3,15.6,0
C902.5,186.6,902.5,179.7,898.3,175.2z" />
<path class="st2" d="M986.2,257.1c-6-6-15.6-6-21.6,0c-6,6-6,15.6,0,21.6l0,0c6,6,15.6,6,21.6,0
C992.1,272.8,992.1,263.1,986.2,257.1L986.2,257.1z" />
<path class="st3" d="M972.8,271.6L875.2,174c-1.1-1.1-1.4-2.3-1.1-3.7c0.3-1.4,1.1-2.6,2.6-3.1c27.9-11.9,48.6-9.4,65.7,8l0.3,0.3
c1.7,1.7,1.7,4.3,0,6s-4.3,1.7-6,0l-0.3-0.3c-13.4-13.4-29.3-16.2-50.6-8.2l89.9,89.9l8-8c4.3-4.3,4.3-11.4,0-15.6l-34.4-34.7
c-1.1-1.1-1.4-2.3-1.1-3.7l1.7-10.8c0.3-2.3,2.6-4,4.8-3.4c2.3,0.3,4,2.6,3.4,4.8l-1.4,8.5l33.3,33c7.7,7.7,7.7,20.2,0,27.9
l-10.8,10.8C977.4,273.3,974.5,273.3,972.8,271.6z" />
<path class="st2" d="M901.4,172.3c-6-6-15.6-6-21.6,0s-6,15.6,0,21.6l0,0c6,6,15.6,6,21.6,0C907.4,188,907.4,178.3,901.4,172.3
L901.4,172.3z M957.7,216.4c-3.4-3.4-8.8-3.4-11.9,0c-3.4,3.4-3.4,8.8,0,11.9c3.4,3.1,8.8,3.4,11.9,0
C960.9,225,960.9,219.6,957.7,216.4z" />
<path class="st3" d="M876.7,197.1c-7.7-7.7-7.7-20.2,0-27.9s20.2-7.7,27.9,0s7.7,20.2,0,27.9C896.6,204.8,884.3,204.8,876.7,197.1
z M898.3,175.2c-4.3-4.3-11.4-4.3-15.6,0c-4.3,4.3-4.3,11.4,0,15.6c4.3,4.3,11.4,4.3,15.6,0C902.5,186.6,902.5,179.7,898.3,175.2z
" />
<path class="st2" d="M986.2,257.1c-6-6-15.6-6-21.6,0s-6,15.6,0,21.6l0,0c6,6,15.6,6,21.6,0C992.1,272.8,992.1,263.1,986.2,257.1
L986.2,257.1z" />
<path class="st3" d="M961.4,281.9c-7.7-7.7-7.7-20.2,0-27.9c7.7-7.7,20.2-7.7,27.9,0c7.7,7.7,7.7,20.2,0,27.9
C981.6,289.5,969.1,289.5,961.4,281.9z M983.3,260.2c-4.3-4.3-11.4-4.3-15.6,0s-4.3,11.4,0,15.6c4.3,4.3,11.4,4.3,15.6,0
C981.6,289.5,969.1,289.5,961.4,281.9z M983.3,260.2c-4.3-4.3-11.4-4.3-15.6,0c-4.2,4.3-4.3,11.4,0,15.6c4.3,4.3,11.4,4.3,15.6,0
C987.6,271.6,987.6,264.5,983.3,260.2z" />
<path class="st3" d="M961.4,281.9l-98.1-98.1c-1.7-1.7-1.7-4.3,0-6s4.3-1.7,6,0l98.1,98.1c1.7,1.7,1.7,4.3,0,6
S963.1,283.6,961.4,281.9z M851.3,171.8c-0.9-0.9-1.4-2-1.1-3.1c0-1.1,0.3-2.3,1.1-3.1c0.9-0.9,2-1.4,3.1-1.1c2.3,0,4.3,2,4.3,4.3
@@ -423,7 +417,7 @@
<g>
<path class="st3" d="M789.9,46.3c0-8.5,6.8-15.4,15.4-15.4c8.5,0,15.4,6.8,15.4,15.4s-6.8,15.4-15.4,15.4 M752.6,8.8
c0-8.5,6.8-15.4,15.4-15.4s15.4,6.8,15.4,15.4s-6.8,15.4-15.4,15.4" />
<path class="st11" d="M782.2,91.6l-59.7-59.7c-0.6-0.6-0.6-1.7,0-2.6l19.3-19.3c0.6-0.6,1.7-0.6,2.6,0l59.7,59.7
<path class="st11" d="M782.2,91.6l-59.7-59.7c-0.6-0.6-0.6-1.7,0-2.6L741.8,10c0.6-0.6,1.7-0.6,2.6,0l59.7,59.7
c0.6,0.6,0.6,1.7,0,2.6l-19.3,19.3C783.9,92.4,782.8,92.4,782.2,91.6z" />
<path class="st2" d="M725.6,26.4l-3.1,3.1c-0.6,0.6-0.6,1.7,0,2.6l5.1,5.1l40.4-3.4l0,0L757.2,23L725.6,26.4z M737.3,46.9
l-0.6-0.6l11.4,11.4l40.4-3.1l0,0l-10.8-10.8L737.3,46.9z M801,75.6l3.1-3.1c0.6-0.6,0.6-1.7,0-2.6l-5.1-5.1L758.6,68l0,0
@@ -437,7 +431,7 @@
</g>
<rect y="-34.4" class="st13" width="1024" height="568.9" />
</g>
<g id="Ebene_2">
<g id="Ebene_2_1_">
<g>
<g>
<g>
@@ -455,19 +449,36 @@
</g>
<g>
<g>
<path class="st17" d="M267.6,140.1c-37,0-67,30-67,67c0,50.5,56.4,76.9,63.2,148.9c0.2,2.1,1.9,3.6,4,3.6c2.1,0,3.8-1.5,4-3.6
<path class="st17" d="M267.6,140.1c-37,0-67,30-67,67c0,50.5,56.4,76.9,63.2,148.9c0.2,2.1,1.9,3.6,4,3.6s3.8-1.5,4-3.6
c6.8-72,63.2-98.4,63.2-148.9C334.5,169.9,304.5,140.1,267.6,140.1z" />
<path class="st18" d="M267.6,141.6c36.8,0,66.5,29.6,67,66.1c0-0.2,0-0.4,0-0.6c0-37-30-67-67-67s-67,29.8-67,67
c0,0.2,0,0.4,0,0.6C201,171.2,230.8,141.6,267.6,141.6L267.6,141.6z" />
<path class="st19" d="M271.6,354.4c-0.2,2.1-1.9,3.6-4,3.6s-3.8-1.5-4-3.6c-6.5-71.8-62.5-98.2-63-148.1c0,0.4,0,0.6,0,1.1
c0,50.5,56.4,76.9,63.2,148.9c0.2,2.1,1.9,3.6,4,3.6c2.1,0,3.8-1.5,4-3.6c6.8-72,63.2-98.4,63.2-148.9c0-0.4,0-0.6,0-1.1
c0,50.5,56.4,76.9,63.2,148.9c0.2,2.1,1.9,3.6,4,3.6s3.8-1.5,4-3.6c6.8-72,63.2-98.4,63.2-148.9c0-0.4,0-0.6,0-1.1
C334.1,256.1,278.1,282.5,271.6,354.4L271.6,354.4z" />
</g>
<path class="st20"
d="M252.2,174.4v40.6h11v33.2l25.8-44.4h-14.8l14.8-29.6C289.1,174.4,252.2,174.4,252.2,174.4z" />
d="M252.2,174.4V215h11v33.2l25.8-44.4h-14.8l14.8-29.6C289.1,174.4,252.2,174.4,252.2,174.4z" />
</g>
</g>
<text transform="matrix(1 0 0 1 417.7245 289.5375)" class="st2 st21 st22">EVMap</text>
<g class="st21">
<path class="st2"
d="M483.6,243h-45.4v39.6h52.2v6.9H430v-97.1h60.1v6.9h-51.9v36.7h45.4V243z" />
<path class="st2"
d="M536.9,277.5l0.5,2.1l0.6-2.1l30.5-85.1h9l-36.1,97.1h-7.9l-36.1-97.1h8.9L536.9,277.5z" />
<path class="st2" d="M602.7,192.5l35.8,85.7l35.9-85.7h10.9v97.1h-8.2v-42.3l0.7-43.3l-36.1,85.6h-6.3l-36-85.3l0.7,42.7v42.5
h-8.2v-97.1H602.7z" />
<path class="st2" d="M753.7,289.5c-0.8-2.3-1.3-5.6-1.5-10.1c-2.8,3.6-6.4,6.5-10.7,8.4c-4.3,2-8.9,3-13.8,3
c-6.9,0-12.5-1.9-16.8-5.8c-4.3-3.9-6.4-8.8-6.4-14.7c0-7,2.9-12.6,8.8-16.7c5.8-4.1,14-6.1,24.4-6.1h14.5v-8.2
c0-5.2-1.6-9.2-4.8-12.2c-3.2-3-7.8-4.4-13.9-4.4c-5.6,0-10.2,1.4-13.8,4.3c-3.6,2.8-5.5,6.3-5.5,10.3l-8-0.1
c0-5.7,2.7-10.7,8-14.9c5.3-4.2,11.9-6.3,19.7-6.3c8,0,14.4,2,19,6c4.6,4,7,9.6,7.2,16.8v34.1c0,7,0.7,12.2,2.2,15.7v0.8H753.7z
M728.6,283.8c5.3,0,10.1-1.3,14.3-3.9c4.2-2.6,7.3-6,9.2-10.3v-15.9h-14.3c-8,0.1-14.2,1.5-18.7,4.4c-4.5,2.8-6.7,6.7-6.7,11.6
c0,4,1.5,7.4,4.5,10.1S723.8,283.8,728.6,283.8z" />
<path class="st2" d="M839.3,254.2c0,11.2-2.5,20.2-7.5,26.8c-5,6.6-11.6,9.9-20,9.9c-9.9,0-17.4-3.5-22.7-10.4v36.8h-7.9v-99.9
h7.4l0.4,10.2c5.2-7.7,12.7-11.5,22.6-11.5c8.6,0,15.4,3.3,20.3,9.8c4.9,6.5,7.4,15.6,7.4,27.2V254.2z M831.3,252.8
c0-9.2-1.9-16.5-5.7-21.8c-3.8-5.3-9-8-15.8-8c-4.9,0-9.1,1.2-12.6,3.5c-3.5,2.4-6.2,5.8-8.1,10.3v34.6c1.9,4.1,4.6,7.3,8.2,9.5
c3.6,2.2,7.8,3.3,12.6,3.3c6.7,0,11.9-2.7,15.7-8C829.4,270.7,831.3,263,831.3,252.8z" />
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="363.80554mm"
height="72.554214mm" viewBox="0 0 1289.0747 257.0819" id="svg4985" version="1.1"
inkscape:version="0.92.3 (2405546, 2018-03-11)" sodipodi:docname="logo_text_small2.svg"
inkscape:export-filename="/home/nih/Desktop/cp/logos/powered_by/logo_text_white_small.png"
inkscape:export-xdpi="42.009968" inkscape:export-ydpi="42.009968">
<defs id="defs4987">
<inkscape:perspective sodipodi:type="inkscape:persp3d" inkscape:vp_x="0 : 152.57443 : 1"
inkscape:vp_y="0 : 1000.0001 : 0" inkscape:vp_z="305.14877 : 152.57443 : 1"
inkscape:persp3d-origin="152.57439 : 101.71629 : 1" id="perspective4145" />
</defs>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="1" inkscape:cx="572.73795"
inkscape:cy="173.48708" inkscape:document-units="px" inkscape:current-layer="layer4"
showgrid="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0"
fit-margin-bottom="0" inkscape:window-width="1857" inkscape:window-height="1052"
inkscape:window-x="63" inkscape:window-y="0" inkscape:window-maximized="1" />
<metadata id="metadata4990">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g inkscape:groupmode="layer" id="layer4" inkscape:label="Layer 2"
transform="translate(-220.97188,-392.31605)">
<g aria-label="chargeprice" transform="matrix(0.93750004,0,0,0.93750004,231.60533,392.30136)"
style="font-style:normal;font-weight:normal;font-size:25px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="flowRoot825">
<path
d="m 350.50175,132.92433 q 15.168,0 25.152,4.8 10.176,4.8 10.176,12.288 0,3.264 -2.112,5.952 -2.112,2.496 -5.376,2.496 -2.496,0 -4.032,-0.768 -1.344,-0.768 -3.84,-2.496 -1.152,-1.152 -3.648,-2.688 -2.304,-1.152 -6.528,-1.92 -4.224,-0.768 -7.68,-0.768 -9.984,0 -17.664,4.608 -7.68,4.608 -11.904,12.864 -4.224,8.064 -4.224,18.048 0,10.176 4.032,18.24 4.224,8.064 11.712,12.672 7.488,4.608 17.088,4.608 9.984,0 16.128,-3.072 1.344,-0.768 3.648,-2.496 1.92,-1.536 3.264,-2.304 1.536,-0.768 3.648,-0.768 3.84,0 5.952,2.496 2.304,2.304 2.304,6.144 0,4.032 -5.184,8.064 -4.992,3.84 -13.632,6.336 -8.448,2.496 -18.24,2.496 -14.592,0 -25.728,-6.72 -11.136,-6.912 -17.28,-18.816 -5.952,-12.096 -5.952,-26.88 0,-14.784 6.336,-26.688 6.336,-12.096 17.664,-18.816 11.328,-6.912 25.92,-6.912 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:192px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Medium'"
id="path824" />
<path
d="m 455.46275,133.50033 q 33.792,0 33.792,41.856 v 51.264 q 0,3.84 -2.688,6.528 -2.496,2.688 -6.528,2.688 -3.84,0 -6.528,-2.688 -2.496,-2.688 -2.496,-6.528 v -51.264 q 0,-24.96 -21.12,-24.96 -11.328,0 -18.816,7.296 -7.488,7.104 -7.488,17.664 v 51.264 q 0,3.84 -2.688,6.528 -2.496,2.688 -6.528,2.688 -4.032,0 -6.528,-2.496 -2.496,-2.688 -2.496,-6.72 v -123.648 q 0,-3.840001 2.496,-6.528001 2.688,-2.688 6.528,-2.688 4.032,0 6.528,2.688 2.688,2.688 2.688,6.528001 v 48.576 q 4.8,-7.488 13.44,-12.672 8.64,-5.376 18.432,-5.376 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:192px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Medium'"
id="path826" />
<path
d="m 597.95075,133.88433 q 4.032,0 6.528,2.688 2.688,2.496 2.688,6.72 v 83.328 q 0,3.84 -2.688,6.528 -2.496,2.688 -6.528,2.688 -4.032,0 -6.528,-2.496 -2.496,-2.688 -2.496,-6.72 v -4.992 q -4.992,6.72 -13.632,11.52 -8.64,4.608 -18.624,4.608 -13.056,0 -23.808,-6.72 -10.56,-6.72 -16.704,-18.624 -5.952,-12.096 -5.952,-27.072 0,-14.976 5.952,-26.88 6.144,-12.096 16.704,-18.816 10.56,-6.72 23.232,-6.72 10.176,0 18.816,4.224 8.832,4.224 14.016,10.752 v -4.608 q 0,-4.032 2.496,-6.72 2.496,-2.688 6.528,-2.688 z m -39.168,86.976 q 9.024,0 15.936,-4.608 7.104,-4.608 10.944,-12.672 4.032,-8.064 4.032,-18.24 0,-9.984 -4.032,-18.048 -3.84,-8.064 -10.944,-12.672 -6.912,-4.8 -15.936,-4.8 -9.024,0 -16.128,4.608 -6.912,4.608 -10.944,12.672 -3.84,8.064 -3.84,18.24 0,10.176 3.84,18.24 4.032,8.064 10.944,12.672 7.104,4.608 16.128,4.608 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:192px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Medium'"
id="path828" />
<path
d="m 684.21275,132.92433 q 4.992,0 8.64,2.688 3.648,2.496 3.648,6.336 0,4.608 -2.496,7.104 -2.304,2.304 -5.76,2.304 -1.728,0 -5.184,-1.152 -4.032,-1.344 -6.336,-1.344 -5.952,0 -11.712,4.224 -5.568,4.032 -9.216,11.328 -3.456,7.104 -3.456,15.936 v 46.272 q 0,3.84 -2.688,6.528 -2.496,2.688 -6.528,2.688 -4.032,0 -6.528,-2.496 -2.496,-2.688 -2.496,-6.72 v -82.176 q 0,-3.84 2.496,-6.528 2.688,-2.688 6.528,-2.688 4.032,0 6.528,2.688 2.688,2.688 2.688,6.528 v 9.792 q 4.224,-9.408 12.672,-15.168 8.448,-5.952 19.2,-6.144 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:192px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Medium'"
id="path830" />
<path
d="m 794.32175,133.88433 q 4.032,0 6.528,2.688 2.688,2.496 2.688,6.72 v 84.48 q 0,15.552 -6.72,25.92 -6.528,10.56 -17.856,15.552 -11.328,4.992 -25.536,4.992 -7.68,0 -18.048,-2.688 -10.176,-2.688 -13.056,-5.568 -5.952,-3.072 -5.952,-7.68 0,-1.152 0.768,-3.072 2.112,-4.8 7.104,-4.8 2.496,0 5.376,1.152 15.36,5.952 24,5.952 15.36,0 23.424,-7.488 8.256,-7.296 8.256,-20.16 v -10.368 q -4.032,7.488 -13.632,12.864 -9.408,5.376 -19.968,5.376 -13.248,0 -24.192,-6.72 -10.944,-6.72 -17.28,-18.624 -6.144,-12.096 -6.144,-27.072 0,-14.976 6.144,-26.88 6.336,-12.096 17.088,-18.816 10.944,-6.72 24,-6.72 10.56,0 19.584,4.8 9.216,4.8 14.4,11.712 v -6.144 q 0,-4.032 2.496,-6.72 2.496,-2.688 6.528,-2.688 z m -40.512,86.976 q 9.408,0 16.704,-4.416 7.296,-4.608 11.328,-12.672 4.224,-8.256 4.224,-18.432 0,-10.176 -4.224,-18.24 -4.032,-8.064 -11.328,-12.672 -7.296,-4.608 -16.704,-4.608 -9.216,0 -16.512,4.608 -7.296,4.608 -11.52,12.864 -4.032,8.064 -4.032,18.048 0,9.984 4.032,18.24 4.224,8.064 11.52,12.672 7.296,4.608 16.512,4.608 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:192px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Medium'"
id="path832" />
<path
d="m 915.17075,181.69233 q -0.192,3.456 -2.88,5.952 -2.688,2.304 -6.336,2.304 h -67.584 q 1.344,14.016 10.56,22.464 9.408,8.448 22.848,8.448 9.216,0 14.976,-2.688 5.76,-2.688 10.176,-6.912 2.88,-1.728 5.568,-1.728 3.264,0 5.376,2.304 2.304,2.304 2.304,5.376 0,4.032 -3.84,7.296 -5.568,5.568 -14.784,9.408 -9.216,3.84 -18.816,3.84 -15.552,0 -27.456,-6.528 -11.712,-6.528 -18.24,-18.24 -6.336,-11.712 -6.336,-26.496 0,-16.128 6.528,-28.224 6.72,-12.288 17.472,-18.816 10.944,-6.528 23.424,-6.528 12.288,0 23.04,6.336 10.752,6.336 17.28,17.472 6.528,11.136 6.72,24.96 z m -47.04,-31.872 q -10.752,0 -18.624,6.144 -7.872,5.952 -10.368,18.624 h 56.64 v -1.536 q -0.96,-10.176 -9.216,-16.704 -8.064,-6.528 -18.432,-6.528 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:192px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Medium'"
id="path834" />
<path
d="m 986.84675,133.50033 q 13.056,0 23.61605,6.72 10.5599,6.528 16.512,18.432 6.144,11.904 6.144,26.88 0,14.976 -6.144,26.88 -5.9521,11.712 -16.512,18.432 -10.56005,6.72 -23.23205,6.72 -9.984,0 -18.624,-4.416 -8.64,-4.416 -14.016,-10.752 v 42.624 q 0,3.84 -2.688,6.528 -2.496,2.688 -6.528,2.688 -3.84,0 -6.528,-2.688 -2.496,-2.496 -2.496,-6.528 v -120.768 q 0,-4.032 2.496,-6.72 2.496,-2.688 6.528,-2.688 4.032,0 6.528,2.688 2.688,2.688 2.688,6.72 v 5.568 q 4.608,-6.72 13.44,-11.52 8.832,-4.8 18.816,-4.8 z m -2.112,87.168 q 8.832,0 15.93595,-4.608 7.1041,-4.608 10.944,-12.48 4.032,-8.064 4.032,-18.048 0,-9.984 -4.032,-17.856 -3.8399,-8.064 -10.944,-12.672 -7.10395,-4.608 -15.93595,-4.608 -9.024,0 -16.128,4.608 -7.104,4.416 -11.136,12.48 -3.84,8.064 -3.84,18.048 0,9.984 3.84,18.048 4.032,8.064 11.136,12.672 7.104,4.416 16.128,4.416 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:192px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Medium'"
id="path836" />
<path
d="m 1104.2128,132.92433 q 4.9919,0 8.64,2.688 3.648,2.496 3.648,6.336 0,4.608 -2.496,7.104 -2.304,2.304 -5.76,2.304 -1.728,0 -5.184,-1.152 -4.032,-1.344 -6.336,-1.344 -5.952,0 -11.712,4.224 -5.568,4.032 -9.216,11.328 -3.456,7.104 -3.456,15.936 v 46.272 q 0,3.84 -2.688,6.528 -2.496,2.688 -6.528,2.688 -4.032,0 -6.528,-2.496 -2.496,-2.688 -2.496,-6.72 v -82.176 q 0,-3.84 2.496,-6.528 2.688,-2.688 6.528,-2.688 4.032,0 6.528,2.688 2.688,2.688 2.688,6.528 v 9.792 q 4.224,-9.408 12.672,-15.168 8.448,-5.952 19.2,-6.144 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:192px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Medium'"
id="path838" />
<path
d="m 1150.5058,226.62033 q 0,3.84 -2.688,6.528 -2.496,2.688 -6.528,2.688 -3.84,0 -6.5281,-2.688 -2.4959,-2.688 -2.4959,-6.528 v -83.136 q 0,-3.84 2.4959,-6.528 2.6881,-2.688 6.5281,-2.688 4.032,0 6.528,2.688 2.688,2.688 2.688,6.528 z m -9.216,-105.024 q -5.568,0 -8.064,-1.92 -2.304,-2.112 -2.304,-6.528 v -3.072 q 0,-4.608 2.496,-6.528 2.688,-1.92 8.064,-1.92 5.3759,0 7.68,2.112 2.496,1.92 2.496,6.336 v 3.072 q 0,4.608 -2.496,6.528 -2.496,1.92 -7.872,1.92 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:192px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Medium'"
id="path840" />
<path
d="m 1220.5018,132.92433 q 15.168,0 25.152,4.8 10.1759,4.8 10.1759,12.288 0,3.264 -2.1119,5.952 -2.112,2.496 -5.376,2.496 -2.496,0 -4.0321,-0.768 -1.3439,-0.768 -3.8399,-2.496 -1.152,-1.152 -3.648,-2.688 -2.304,-1.152 -6.528,-1.92 -4.224,-0.768 -7.68,-0.768 -9.984,0 -17.664,4.608 -7.68,4.608 -11.904,12.864 -4.224,8.064 -4.224,18.048 0,10.176 4.032,18.24 4.224,8.064 11.712,12.672 7.488,4.608 17.088,4.608 9.984,0 16.128,-3.072 1.344,-0.768 3.648,-2.496 1.92,-1.536 3.264,-2.304 1.536,-0.768 3.648,-0.768 3.84,0 5.952,2.496 2.304,2.304 2.304,6.144 0,4.032 -5.184,8.064 -4.992,3.84 -13.632,6.336 -8.448,2.496 -18.24,2.496 -14.592,0 -25.728,-6.72 -11.136,-6.912 -17.28,-18.816 -5.952,-12.096 -5.952,-26.88 0,-14.784 6.336,-26.688 6.336,-12.096 17.664,-18.816 11.328,-6.912 25.92,-6.912 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:192px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Medium'"
id="path842" />
<path
d="m 1363.6708,181.69233 q -0.192,3.456 -2.88,5.952 -2.688,2.304 -6.3361,2.304 h -67.5839 q 1.344,14.016 10.56,22.464 9.4079,8.448 22.848,8.448 9.216,0 14.976,-2.688 5.76,-2.688 10.176,-6.912 2.88,-1.728 5.568,-1.728 3.264,0 5.376,2.304 2.304,2.304 2.304,5.376 0,4.032 -3.8401,7.296 -5.5679,5.568 -14.7839,9.408 -9.2161,3.84 -18.816,3.84 -15.552,0 -27.456,-6.528 -11.712,-6.528 -18.24,-18.24 -6.336,-11.712 -6.336,-26.496 0,-16.128 6.528,-28.224 6.72,-12.288 17.472,-18.816 10.944,-6.528 23.424,-6.528 12.288,0 23.04,6.336 10.752,6.336 17.28,17.472 6.528,11.136 6.72,24.96 z m -47.04,-31.872 q -10.752,0 -18.624,6.144 -7.872,5.952 -10.368,18.624 h 56.64 v -1.536 q -0.96,-10.176 -9.216,-16.704 -8.064,-6.528 -18.432,-6.528 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:192px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Medium'"
id="path844" />
</g>
<g aria-label="POWERED BY" transform="matrix(0.93750004,0,0,0.93750004,231.12524,266.21949)"
style="font-style:normal;font-weight:normal;font-size:25px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="flowRoot825-6">
<path
d="m 331.92042,135.56966 q 5.44,0 10.34666,3.30667 4.90667,3.2 7.89334,8.74667 2.98666,5.44 2.98666,11.94666 0,6.4 -2.98666,11.94667 -2.98667,5.54666 -7.89334,8.85333 -4.90666,3.2 -10.34666,3.2 h -18.56 v 20.16 q 0,2.88 -1.70667,4.69333 -1.70667,1.81334 -4.48,1.81334 -2.66667,0 -4.37333,-1.81334 -1.70667,-1.92 -1.70667,-4.69333 v -61.65333 q 0,-2.77333 1.81333,-4.58667 1.92,-1.92 4.69334,-1.92 z m 0,35.84 q 2.02666,0 3.94666,-1.70667 2.02667,-1.70666 3.2,-4.37333 1.28,-2.77333 1.28,-5.76 0,-2.98667 -1.28,-5.65333 -1.17333,-2.77333 -3.2,-4.37333 -1.92,-1.70667 -3.94666,-1.70667 h -18.56 v 23.57333 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:106.66666412px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Bold'"
id="path883" />
<path
d="m 434.16208,172.90299 q 0,10.56 -4.69333,19.41334 -4.69333,8.74666 -13.01333,13.86666 -8.21334,5.12 -18.56,5.12 -10.34667,0 -18.66667,-5.12 -8.21333,-5.12 -12.90667,-13.86666 -4.58666,-8.85334 -4.58666,-19.41334 0,-10.56 4.58666,-19.30666 4.69334,-8.85333 12.90667,-13.97333 8.32,-5.12 18.66667,-5.12 10.34666,0 18.56,5.12 8.32,5.12 13.01333,13.97333 4.69333,8.74666 4.69333,19.30666 z m -13.86666,0 q 0,-7.14666 -2.88,-12.90666 -2.88,-5.86667 -8,-9.28 -5.12,-3.41333 -11.52,-3.41333 -6.50667,0 -11.62667,3.41333 -5.01333,3.30666 -7.89333,9.17333 -2.77334,5.86667 -2.77334,13.01333 0,7.14667 2.77334,13.01334 2.88,5.86666 7.89333,9.28 5.12,3.30666 11.62667,3.30666 6.4,0 11.52,-3.41333 5.12,-3.41333 8,-9.17333 2.88,-5.86667 2.88,-13.01334 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:106.66666412px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Bold'"
id="path885" />
<path
d="m 529.83207,135.24966 q 2.56,0 4.69333,2.02667 2.24,1.92 2.24,4.90667 0,0.96 -0.32,2.13333 l -21.01333,61.86666 q -0.64,1.81334 -2.24,2.88 -1.6,1.06667 -3.52,1.17334 -1.92,0 -3.62667,-1.06667 -1.70666,-1.06667 -2.66666,-3.09333 l -15.14667,-34.45334 -15.25333,34.45334 q -0.96,2.02666 -2.66667,3.09333 -1.70666,1.06667 -3.62666,1.06667 -1.92,-0.10667 -3.52,-1.17334 -1.6,-1.06666 -2.24,-2.88 l -21.01334,-61.86666 q -0.32,-1.17333 -0.32,-2.13333 0,-2.98667 2.13334,-4.90667 2.24,-2.02667 4.90666,-2.02667 2.13334,0 3.84,1.17334 1.70667,1.06666 2.34667,2.98666 l 15.89333,48.21333 13.86667,-33.28 q 0.85333,-1.91999 2.45333,-2.98666 1.6,-1.17333 3.62667,-1.06667 2.02667,-0.10666 3.52,1.06667 1.6,1.06667 2.45333,2.98666 l 13.12,32.96 15.78667,-47.89333 q 0.64,-1.92 2.34666,-2.98666 1.81334,-1.17334 3.94667,-1.17334 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:106.66666412px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Bold'"
id="path887" />
<path
d="m 588.75041,197.96966 q 2.77333,0 4.58666,1.92 1.92,1.81333 1.92,4.26667 0,2.66666 -1.92,4.37333 -1.81333,1.70667 -4.58666,1.70667 h -35.73334 q -2.77333,0 -4.69333,-1.81334 -1.81333,-1.92 -1.81333,-4.69333 v -61.65333 q 0,-2.77333 1.81333,-4.58667 1.92,-1.92 4.69333,-1.92 h 35.73334 q 2.77333,0 4.58666,1.81334 1.92,1.70666 1.92,4.48 0,2.66666 -1.81333,4.37333 -1.81333,1.6 -4.69333,1.6 h -28.90667 v 18.13333 h 24.10667 q 2.77333,0 4.58666,1.81333 1.92,1.70667 1.92,4.48 0,2.66667 -1.81333,4.37334 -1.81333,1.6 -4.69333,1.6 h -24.10667 v 19.73333 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:106.66666412px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Bold'"
id="path889" />
<path
d="m 666.11206,199.78299 q 1.38667,0.85334 2.13333,2.24 0.85334,1.38667 0.85334,2.88 0,1.92 -1.28,3.52 -1.6,1.92 -4.90667,1.92 -2.56,0 -4.69333,-1.17333 -7.68,-4.37333 -7.68,-17.81333 0,-3.84 -2.56,-6.08 -2.45333,-2.24 -7.14667,-2.24 H 620.8854 v 20.69333 q 0,2.88 -1.6,4.69333 -1.49334,1.81334 -4.05334,1.81334 -3.09333,0 -5.44,-1.81334 -2.24,-1.92 -2.24,-4.69333 v -61.65333 q 0,-2.77333 1.81334,-4.58667 1.92,-1.92 4.69333,-1.92 h 30.72 q 5.54667,0 10.45333,2.98667 4.90667,2.98667 7.78667,8.21333 2.98666,5.22667 2.98666,11.73333 0,5.33334 -2.88,10.45334 -2.88,5.01333 -7.46666,8 6.72,4.69333 7.36,12.58666 0.32,1.70667 0.32,3.30667 0.42666,3.30667 0.85333,4.8 0.42667,1.38667 1.92,2.13333 z M 644.2454,172.04966 q 1.92,0 3.73333,-1.81333 1.81333,-1.81334 2.98667,-4.8 1.17333,-3.09334 1.17333,-6.61334 0,-2.98666 -1.17333,-5.43999 -1.17334,-2.56 -2.98667,-4.05334 -1.81333,-1.49333 -3.73333,-1.49333 h -23.36 v 24.21333 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:106.66666412px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Bold'"
id="path891" />
<path
d="m 723.12541,197.96966 q 2.77333,0 4.58666,1.92 1.92,1.81333 1.92,4.26667 0,2.66666 -1.92,4.37333 -1.81333,1.70667 -4.58666,1.70667 h -35.73334 q -2.77333,0 -4.69333,-1.81334 -1.81333,-1.92 -1.81333,-4.69333 v -61.65333 q 0,-2.77333 1.81333,-4.58667 1.92,-1.92 4.69333,-1.92 h 35.73334 q 2.77333,0 4.58666,1.81334 1.92,1.70666 1.92,4.48 0,2.66666 -1.81333,4.37333 -1.81333,1.6 -4.69333,1.6 h -28.90667 v 18.13333 h 24.10667 q 2.77333,0 4.58666,1.81333 1.92,1.70667 1.92,4.48 0,2.66667 -1.81333,4.37334 -1.81333,1.6 -4.69333,1.6 h -24.10667 v 19.73333 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:106.66666412px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Bold'"
id="path893" />
<path
d="m 773.92706,135.56966 q 10.02667,0 17.17333,5.01334 7.25334,4.90666 10.98667,13.43999 3.84,8.42667 3.84,18.88 0,10.45334 -3.84,18.98667 -3.73333,8.42667 -10.98667,13.44 -7.14666,4.90667 -17.17333,4.90667 h -25.49333 q -2.77333,0 -4.69333,-1.81334 -1.81334,-1.92 -1.81334,-4.69333 v -61.65333 q 0,-2.77333 1.81334,-4.58667 1.92,-1.92 4.69333,-1.92 z m -1.06666,62.4 q 9.6,0 14.4,-7.04 4.79999,-7.14667 4.79999,-18.02667 0,-10.88 -4.90666,-17.92 -4.8,-7.14666 -14.29333,-7.14666 h -17.6 v 50.13333 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:106.66666412px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Bold'"
id="path895" />
<path
d="m 892.76875,168.84966 q 5.65333,2.24 9.17333,6.82667 3.62667,4.58666 3.62667,11.84 0,12.69333 -7.25333,17.70666 -7.25334,5.01334 -17.28,5.01334 h -26.56 q -2.77334,0 -4.69334,-1.81334 -1.81333,-1.92 -1.81333,-4.69333 v -61.65333 q 0,-2.77333 1.81333,-4.58667 1.92,-1.92 4.69334,-1.92 h 26.88 q 20.26666,0 20.26666,18.98667 0,4.8 -2.34666,8.53333 -2.24,3.62667 -6.50667,5.76 z m -5.01333,-11.94667 q 0,-4.37333 -2.24,-6.50666 -2.13334,-2.24 -6.08,-2.24 h -17.6 v 16.64 h 17.92 q 3.2,0 5.54666,-2.13334 2.45334,-2.13333 2.45334,-5.76 z m -6.72,41.06667 q 5.01333,0 7.78666,-2.66667 2.88,-2.66666 2.88,-7.78666 0,-6.29334 -3.30666,-8.21334 -3.30667,-1.92 -8.10667,-1.92 h -18.45333 v 20.58667 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:106.66666412px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Bold'"
id="path897" />
<path
d="m 969.23371,141.863 q 0,2.13333 -1.17334,3.94666 l -22.29333,31.89333 v 26.02667 q 0,2.77333 -1.81333,4.69333 -1.81333,1.81334 -4.37333,1.81334 -2.66667,0 -4.58667,-1.81334 -1.81333,-1.92 -1.81333,-4.69333 v -27.52 l -22.18667,-29.44 q -1.92,-2.56 -1.92,-5.01333 0,-2.77333 2.13333,-4.58667 2.24,-1.92 4.69334,-1.92 2.98666,0 5.22666,2.98667 l 18.77334,25.92 17.59999,-25.70667 q 2.24,-3.2 5.33334,-3.2 2.56,0 4.48,1.92 1.92,1.92 1.92,4.69334 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:106.66666412px;font-family:Quicksand;-inkscape-font-specification:'Quicksand Bold'"
id="path899" />
</g>
</g>
<g inkscape:groupmode="layer" id="layer2" inkscape:label="Layer 3"
transform="translate(-27.815468,21.198496)" />
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1"
transform="translate(-220.97188,-392.31605)">
<path inkscape:connector-curvature="0" style="fill:#000016;fill-opacity:1"
d="m 467.91504,565.96351 -11.49602,-6.03301 10.02002,-5.23601 c 3.41201,-1.785 3.41501,-4.70701 0.01,-6.49601 l -10.76202,-5.64801 10.75602,-5.62101 c 3.41201,-1.78501 3.41501,-4.70701 0.01,-6.49602 L 380.3709,485.25335 c -3.40901,-1.789 -8.99702,-1.809 -12.41802,-0.041 L 223.53563,559.8065 c -3.422,1.766 -3.418,4.65001 0.01,6.41001 l 11.01802,5.66202 -11.02302,5.69301 c -3.422,1.766 -3.418,4.65001 0.01,6.41001 l 11.75602,6.04101 -10.28901,5.31401 c -3.42201,1.76801 -3.41801,4.65201 0.01,6.41201 l 88.01014,45.22309 c 3.42601,1.76001 9.02002,1.74001 12.43002,-0.043 l 142.46424,-74.46914 c 3.40901,-1.78301 3.41201,-4.70701 0,-6.49602 z m -93.03415,-58.62311 -21.59604,24.90005 c -1.08,1.246 -0.764,2.883 0.703,3.637 l 17.18203,8.82802 c 1.467,0.754 1.469,1.99201 0,2.74801 l -53.92109,27.85005 c -1.467,0.758 -1.781,0.357 -0.699,-0.889 l 21.59404,-24.90005 c 1.082,-1.246 0.766,-2.885 -0.70101,-3.63901 l -17.18202,-8.82801 c -1.46701,-0.75401 -1.46901,-1.99001 0,-2.74801 l 53.92209,-27.85005 c 1.465,-0.75601 1.78,-0.35501 0.699,0.891 z"
id="path11" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View File

@@ -6,15 +6,15 @@ apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 29
versionCode 27
versionName "0.4.0"
targetSdkVersion 30
versionCode 47
versionName "0.7.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -48,7 +48,6 @@ android {
productFlavors {
foss {
dimension "dependencies"
versionNameSuffix "-foss"
}
google {
dimension "dependencies"
@@ -86,23 +85,28 @@ android {
if (mapboxKey != null) {
variant.resValue "string", "mapbox_key", mapboxKey
}
def chargepriceKey = env.CHARGEPRICE_API_KEY ?: project.findProperty("CHARGEPRICE_API_KEY")
if (chargepriceKey == null && project.hasProperty("CHARGEPRICE_API_KEY_ENCRYPTED")) {
chargepriceKey = decode(project.findProperty("CHARGEPRICE_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
}
if (chargepriceKey != null) {
variant.resValue "string", "chargeprice_key", chargepriceKey
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.2'
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.core:core-ktx:1.5.0'
implementation "androidx.activity:activity-ktx:1.2.3"
implementation "androidx.fragment:fragment-ktx:1.3.4"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.core:core:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.2.1'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0'
implementation 'androidx.browser:browser:1.3.0'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
@@ -110,18 +114,23 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.9.2'
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'com.github.MikeOrtiz:TouchImageView:2.3.3'
implementation 'moe.banana:moshi-jsonapi:3.5.0'
implementation 'moe.banana:moshi-jsonapi-retrofit-converter:3.5.0'
implementation 'io.coil-kt:coil:1.1.0'
implementation 'com.github.MikeOrtiz:TouchImageView:3.0.3'
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
implementation 'com.airbnb.android:lottie:3.4.0'
implementation 'io.michaelrocks:bimap:1.0.2'
implementation 'io.michaelrocks.bimap:bimap:1.1.0'
implementation 'com.mapzen.android:lost:3.0.2'
implementation 'com.google.guava:guava:29.0-android'
implementation 'com.github.pengrad:mapscaleview:1.6.0'
// Android Auto
googleImplementation 'androidx.car.app:app:1.0.0'
// AnyMaps
def anyMapsVersion = '631708a156'
def anyMapsVersion = '1f050d860f'
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
@@ -129,7 +138,7 @@ dependencies {
// Google Maps v3 Beta
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.android.volley:volley:1.2.0'
googleImplementation 'com.google.android.gms:play-services-base:17.5.0'
googleImplementation 'com.google.android.gms:play-services-basement:17.5.0'
googleImplementation 'com.google.android.gms:play-services-gcm:17.0.0'
@@ -141,29 +150,29 @@ dependencies {
googleImplementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
// Mapbox places (autocomplete)
implementation('com.mapbox.mapboxsdk:mapbox-android-plugin-places-v9:0.12.0') {
// forked this library and included through JitPack to fix https://github.com/mapbox/mapbox-plugins-android/issues/1011
implementation('com.github.johan12345.mapbox-plugins-android:mapbox-android-plugin-places-v9:922bf877f6') {
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
}
// navigation library
def nav_version = "2.3.2"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// viewmodel library
def lifecycle_version = "2.2.0"
def lifecycle_version = "2.3.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// room library
def room_version = "2.2.6"
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
// billing library
def billing_version = "3.0.2"
def billing_version = "4.0.0"
googleImplementation "com.android.billingclient:billing:$billing_version"
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
@@ -179,5 +188,17 @@ dependencies {
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
}
private static String decode(String s, String key) {
return new String(xorWithKey(s.decodeBase64(), key.getBytes()), "UTF-8");
}
private static byte[] xorWithKey(byte[] a, byte[] key) {
byte[] out = new byte[a.length];
for (int i = 0; i < a.length; i++) {
out[i] = (byte) (a[i] ^ key[i%key.length]);
}
return out;
}

View File

@@ -1,10 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="net.vonforst.evmap">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
<uses-sdk tools:overrideLibrary="androidx.car.app" />
<application>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key" />
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key" />
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
<meta-data
android:name="androidx.car.app.theme"
android:resource="@style/CarAppTheme" />
<service
android:name=".auto.CarAppService"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:exported="true">
<intent-filter>
<action
android:name="androidx.car.app.CarAppService"
android:category="androidx.car.app.category.CHARGING" />
</intent-filter>
</service>
<service
android:name=".auto.CarLocationService"
android:foregroundServiceType="location"
android:enabled="true" />
<activity android:name=".auto.PermissionActivity" />
</application>
</manifest>

View File

@@ -0,0 +1,666 @@
package net.vonforst.evmap.auto
import android.Manifest
import android.content.*
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.location.Location
import android.net.Uri
import android.os.Bundle
import android.os.IBinder
import android.os.ResultReceiver
import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.Session
import androidx.car.app.model.*
import androidx.car.app.model.Distance.UNIT_KILOMETERS
import androidx.car.app.validation.HostValidator
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import coil.imageLoader
import coil.request.ImageRequest
import kotlinx.coroutines.*
import net.vonforst.evmap.*
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.utils.distanceBetween
import java.io.IOException
import java.time.Duration
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import kotlin.math.roundToInt
interface LocationAwareScreen {
fun updateLocation(location: Location)
}
class CarAppService : androidx.car.app.CarAppService() {
override fun createHostValidator(): HostValidator {
return if (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) {
HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
} else {
HostValidator.Builder(applicationContext)
.addAllowedHosts(androidx.car.app.R.array.hosts_allowlist_sample)
.build()
}
}
override fun onCreateSession(): Session {
return EVMapSession(this)
}
}
class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
var mapScreen: LocationAwareScreen? = null
set(value) {
field = value
location?.let { value?.updateLocation(it) }
}
private var location: Location? = null
private var locationService: CarLocationService? = null
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, ibinder: IBinder) {
val binder: CarLocationService.LocalBinder = ibinder as CarLocationService.LocalBinder
locationService = binder.service
locationService?.requestLocationUpdates()
}
override fun onServiceDisconnected(name: ComponentName?) {
locationService = null
}
}
init {
lifecycle.addObserver(this)
}
override fun onCreateScreen(intent: Intent): Screen {
return if (locationPermissionGranted()) {
WelcomeScreen(carContext, this)
} else {
PermissionScreen(carContext, this)
}
}
private fun locationPermissionGranted() =
ContextCompat.checkSelfPermission(
carContext,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
private val locationReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val location = intent.getParcelableExtra(CarLocationService.EXTRA_LOCATION) as Location?
val mapScreen = this@EVMapSession.mapScreen
if (location != null && mapScreen != null) {
mapScreen.updateLocation(location)
}
this@EVMapSession.location = location
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun bindLocationService() {
if (!locationPermissionGranted()) return
cas.bindService(
Intent(cas, CarLocationService::class.java),
serviceConnection,
Context.BIND_AUTO_CREATE
)
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
private fun unbindLocationService() {
locationService?.let { service ->
service.removeLocationUpdates()
cas.unbindService(serviceConnection)
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
private fun registerBroadcastReceiver() {
LocalBroadcastManager.getInstance(cas).registerReceiver(
locationReceiver,
IntentFilter(CarLocationService.ACTION_BROADCAST)
);
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
private fun unregisterBroadcastReceiver() {
LocalBroadcastManager.getInstance(cas).unregisterReceiver(locationReceiver)
}
}
/**
* Welcome screen with selection between favorites and nearby chargers
*/
class WelcomeScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), LocationAwareScreen {
private var location: Location? = null
override fun onGetTemplate(): Template {
session.mapScreen = this
return PlaceListMapTemplate.Builder().apply {
setTitle(carContext.getString(R.string.app_name))
location?.let {
setAnchor(Place.Builder(CarLocation.create(it)).build())
}
setItemList(ItemList.Builder().apply {
addItem(Row.Builder()
.setTitle(carContext.getString(R.string.auto_chargers_closeby))
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_address
)
)
.setTint(CarColor.DEFAULT).build()
)
.setBrowsable(true)
.setOnClickListener {
screenManager.push(MapScreen(carContext, session, favorites = false))
}
.build())
addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.auto_favorites))
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_fav
)
)
.setTint(CarColor.DEFAULT).build()
)
.setBrowsable(true)
.setOnClickListener {
screenManager.push(MapScreen(carContext, session, favorites = true))
}
.build())
}.build())
setCurrentLocationEnabled(true)
setHeaderAction(Action.APP_ICON)
build()
}.build()
}
override fun updateLocation(location: Location) {
this.location = location
invalidate()
}
}
/**
* Screen to grant location permission
*/
class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
override fun onGetTemplate(): Template {
return MessageTemplate.Builder(carContext.getString(R.string.auto_location_permission_needed))
.setTitle(carContext.getString(R.string.app_name))
.setHeaderAction(Action.APP_ICON)
.addAction(
Action.Builder()
.setTitle(carContext.getString(R.string.grant_on_phone))
.setBackgroundColor(CarColor.PRIMARY)
.setOnClickListener(ParkedOnlyOnClickListener.create {
val intent = Intent(carContext, PermissionActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(
PermissionActivity.EXTRA_RESULT_RECEIVER,
object : ResultReceiver(null) {
override fun onReceiveResult(
resultCode: Int,
resultData: Bundle?
) {
if (resultData!!.getBoolean(PermissionActivity.RESULT_GRANTED)) {
session.bindLocationService()
screenManager.push(
WelcomeScreen(
carContext,
session
)
)
}
}
})
carContext.startActivity(intent)
CarToast.makeText(
carContext,
R.string.opened_on_phone,
CarToast.LENGTH_LONG
).show()
})
.build()
)
.addAction(
Action.Builder()
.setTitle(carContext.getString(R.string.cancel))
.setOnClickListener {
carContext.finishCarApp()
}
.build(),
)
.build()
}
}
/**
* Main map screen showing either nearby chargers or favorites
*/
class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boolean = false) :
Screen(ctx), LocationAwareScreen {
private var updateCoroutine: Job? = null
private var numUpdates = 0
private val maxNumUpdates = 3
private var location: Location? = null
private var lastUpdateLocation: Location? = null
private var chargers: List<ChargeLocation>? = null
private val api by lazy {
GoingElectricApi.create(ctx.getString(R.string.goingelectric_key), context = ctx)
}
private val searchRadius = 5 // kilometers
private val updateThreshold = 2000 // meters
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus>> =
HashMap()
private val maxRows = 6
override fun onGetTemplate(): Template {
session.mapScreen = this
return PlaceListMapTemplate.Builder().apply {
setTitle(
carContext.getString(
if (favorites) {
R.string.auto_favorites
} else {
R.string.auto_chargers_closeby
}
)
)
location?.let {
setAnchor(Place.Builder(CarLocation.create(it)).build())
} ?: setLoading(true)
chargers?.take(maxRows)?.let { chargerList ->
val builder = ItemList.Builder()
chargerList.forEach { charger ->
builder.addItem(formatCharger(charger))
}
builder.setNoItemsMessage(
carContext.getString(
if (favorites) {
R.string.auto_no_favorites_found
} else {
R.string.auto_no_chargers_found
}
)
)
setItemList(builder.build())
} ?: setLoading(true)
setCurrentLocationEnabled(true)
setHeaderAction(Action.BACK)
build()
}.build()
}
private fun formatCharger(charger: ChargeLocation): Row {
val color = ContextCompat.getColor(carContext, getMarkerTint(charger))
val place =
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
.setMarker(
PlaceMarker.Builder()
.setColor(CarColor.createCustom(color, color))
.build()
)
.build()
return Row.Builder().apply {
setTitle(charger.name)
val text = SpannableStringBuilder()
// distance
location?.let {
val distance = distanceBetween(
it.latitude, it.longitude,
charger.coordinates.lat, charger.coordinates.lng
) / 1000
text.append(
"distance",
DistanceSpan.create(Distance.create(distance, UNIT_KILOMETERS)),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
// power
if (text.isNotEmpty()) text.append(" · ")
text.append("${charger.maxPower.roundToInt()} kW")
// availability
availabilities[charger.id]?.second?.let { av ->
val status = av.status.values.flatten()
val available = availabilityText(status)
val total = charger.chargepoints.sumBy { it.count }
if (text.isNotEmpty()) text.append(" · ")
text.append(
"$available/$total",
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
addText(text)
setMetadata(
Metadata.Builder()
.setPlace(place)
.build()
)
setOnClickListener {
screenManager.push(ChargerDetailScreen(carContext, charger))
}
}.build()
}
override fun updateLocation(location: Location) {
this.location = location
if (updateCoroutine != null) {
// don't update while still loading last update
return
}
invalidate()
if (lastUpdateLocation == null ||
location.distanceTo(lastUpdateLocation) > updateThreshold
) {
lastUpdateLocation = location
// update displayed chargers
loadChargers(location)
}
}
private val db = AppDatabase.getInstance(carContext)
private fun loadChargers(location: Location) {
numUpdates++
println(numUpdates)
if (numUpdates > maxNumUpdates) {
CarToast.makeText(carContext, R.string.auto_no_refresh_possible, CarToast.LENGTH_LONG)
.show()
return
}
updateCoroutine = lifecycleScope.launch {
try {
// load chargers
if (favorites) {
chargers = db.chargeLocationsDao().getAllChargeLocationsAsync().sortedBy {
distanceBetween(
location.latitude, location.longitude,
it.coordinates.lat, it.coordinates.lng
)
}
} else {
val response = api.getChargepointsRadius(
location.latitude,
location.longitude,
searchRadius,
zoom = 16f
)
chargers =
response.body()?.chargelocations?.filterIsInstance(ChargeLocation::class.java)
chargers?.let {
if (it.size < 6) {
// try again with larger radius
val response = api.getChargepointsRadius(
location.latitude,
location.longitude,
searchRadius * 5,
zoom = 16f
)
chargers =
response.body()?.chargelocations?.filterIsInstance(ChargeLocation::class.java)
}
}
}
// remove outdated availabilities
availabilities = availabilities.filter {
Duration.between(
it.value.first,
ZonedDateTime.now()
) > availabilityUpdateThreshold
}.toMutableMap()
// update availabilities
chargers?.take(maxRows)?.map {
lifecycleScope.async {
// update only if not yet stored
if (!availabilities.containsKey(it.id)) {
val date = ZonedDateTime.now()
val availability = getAvailability(it).data
if (availability != null) {
availabilities[it.id] = date to availability
}
}
}
}?.awaitAll()
updateCoroutine = null
invalidate()
} catch (e: IOException) {
withContext(Dispatchers.Main) {
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
.show()
}
}
}
}
}
class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Screen(ctx) {
var charger: ChargeLocation? = null
var photo: Bitmap? = null
private var availability: ChargeLocationStatus? = null
val apikey = ctx.getString(R.string.goingelectric_key)
private val api by lazy {
GoingElectricApi.create(apikey, context = ctx)
}
private val iconGen = ChargerIconGenerator(carContext, null, oversize = 1.4f, height = 64)
override fun onGetTemplate(): Template {
if (charger == null) loadCharger()
return PaneTemplate.Builder(
Pane.Builder().apply {
charger?.let { charger ->
addRow(Row.Builder().apply {
setTitle(charger.address.toString())
val icon = iconGen.getBitmap(
tint = getMarkerTint(charger),
fault = charger.faultReport != null,
multi = charger.isMulti()
)
setImage(
CarIcon.Builder(IconCompat.createWithBitmap(icon)).build(),
Row.IMAGE_TYPE_LARGE
)
val chargepointsText = SpannableStringBuilder()
charger.chargepointsMerged.forEachIndexed { i, cp ->
if (i > 0) chargepointsText.append(" · ")
chargepointsText.append(
"${cp.count}× ${
nameForPlugType(
carContext,
cp.type
)
} ${cp.formatPower()}"
)
availability?.status?.get(cp)?.let { status ->
chargepointsText.append(
" (${availabilityText(status)}/${cp.count})",
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
addText(chargepointsText)
}.build())
addRow(Row.Builder().apply {
photo?.let {
setImage(
CarIcon.Builder(IconCompat.createWithBitmap(photo)).build(),
Row.IMAGE_TYPE_LARGE
)
}
val operatorText = StringBuilder().apply {
charger.operator?.let { append(it) }
charger.network?.let {
if (isNotEmpty()) append(" · ")
append(it)
}
}.ifEmpty {
carContext.getString(R.string.unknown_operator)
}
setTitle(operatorText)
charger.cost?.let { addText(it.getStatusText(carContext, emoji = true)) }
charger.faultReport?.created?.let {
addText(
carContext.getString(
R.string.auto_fault_report_date,
it.atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
)
)
}
/*val types = charger.chargepoints.map { it.type }.distinct()
if (types.size == 1) {
setImage(
CarIcon.of(IconCompat.createWithResource(carContext, iconForPlugType(types[0]))),
Row.IMAGE_TYPE_ICON)
}*/
}.build())
addAction(Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_navigation
)
).build()
)
.setTitle(carContext.getString(R.string.navigate))
.setBackgroundColor(CarColor.PRIMARY)
.setOnClickListener {
navigateToCharger(charger)
}
.build())
addAction(
Action.Builder()
.setTitle(carContext.getString(R.string.open_in_app))
.setOnClickListener(ParkedOnlyOnClickListener.create {
val intent = Intent(carContext, MapsActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_CHARGER_ID, charger.id)
.putExtra(EXTRA_LAT, charger.coordinates.lat)
.putExtra(EXTRA_LON, charger.coordinates.lng)
carContext.startActivity(intent)
CarToast.makeText(
carContext,
R.string.opened_on_phone,
CarToast.LENGTH_LONG
).show()
})
.build()
)
} ?: setLoading(true)
}.build()
).apply {
setTitle(chargerSparse.name)
setHeaderAction(Action.BACK)
}.build()
}
private fun navigateToCharger(charger: ChargeLocation) {
val coord = charger.coordinates
val intent =
Intent(
CarContext.ACTION_NAVIGATE,
Uri.parse("geo:0,0?q=${coord.lat},${coord.lng}(${charger.name})")
)
carContext.startCarApp(intent)
}
private fun loadCharger() {
lifecycleScope.launch {
try {
val response = api.getChargepointDetail(chargerSparse.id)
charger = response.body()?.chargelocations?.get(0) as ChargeLocation
val photo = charger?.photos?.firstOrNull()
photo?.let {
val size = (carContext.resources.displayMetrics.density * 64).roundToInt()
val url = "https://api.goingelectric.de/chargepoints/photo/?key=${apikey}" +
"&id=${photo.id}&size=${size}"
val request = ImageRequest.Builder(carContext).data(url).build()
this@ChargerDetailScreen.photo =
(carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap
}
availability = charger?.let { getAvailability(it).data }
invalidate()
} catch (e: IOException) {
withContext(Dispatchers.Main) {
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
.show()
}
}
}
}
}
fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
val unknown = status.any { it == ChargepointStatus.UNKNOWN }
val available = status.count { it == ChargepointStatus.AVAILABLE }
val allFaulted = status.all { it == ChargepointStatus.FAULTED }
return if (unknown) {
CarColor.DEFAULT
} else if (available > 0) {
CarColor.GREEN
} else if (allFaulted) {
CarColor.RED
} else {
CarColor.BLUE
}
}

View File

@@ -0,0 +1,163 @@
package net.vonforst.evmap.auto
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.location.Location
import android.os.*
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.gms.location.*
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
class CarLocationService : Service() {
private lateinit var serviceHandler: Handler
private lateinit var locationRequest: LocationRequest
private lateinit var notificationManager: NotificationManager
private lateinit var locationCallback: LocationCallback
private lateinit var fusedLocationClient: FusedLocationProviderClient
private val binder: IBinder = LocalBinder(this)
private var location: Location? = null
private val UPDATE_INTERVAL_IN_MILLISECONDS = 10000L
private val FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = UPDATE_INTERVAL_IN_MILLISECONDS / 2
private val CHANNEL_ID = "car_location"
private val NOTIFICATION_ID = 1000
private val TAG = "CarLocationService"
companion object {
const val ACTION_BROADCAST: String = BuildConfig.APPLICATION_ID + ".car_location_broadcast"
const val EXTRA_LOCATION: String = BuildConfig.APPLICATION_ID + ".location"
}
override fun onCreate() {
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
super.onLocationResult(locationResult)
onNewLocation(locationResult.lastLocation)
}
}
createLocationRequest()
getLastLocation()
val handlerThread = HandlerThread(TAG)
handlerThread.start()
serviceHandler = Handler(handlerThread.looper)
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
// Android O requires a Notification Channel.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name: CharSequence = getString(R.string.app_name)
// Create the channel for the notification
val mChannel =
NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_DEFAULT)
// Set the Notification Channel for the Notification Manager.
notificationManager.createNotificationChannel(mChannel)
}
startForeground(NOTIFICATION_ID, getNotification())
}
/**
* Returns the [NotificationCompat] used as part of the foreground service.
*/
private fun getNotification(): Notification {
val builder: NotificationCompat.Builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentText(getString(R.string.auto_location_service))
.setContentTitle(getString(R.string.app_name))
.setOngoing(true)
.setPriority(NotificationManagerCompat.IMPORTANCE_HIGH)
.setSmallIcon(R.mipmap.ic_launcher)
.setTicker(getString(R.string.auto_location_service))
.setWhen(System.currentTimeMillis())
return builder.build()
}
override fun onBind(intent: Intent?): IBinder {
return binder
}
private fun createLocationRequest() {
locationRequest = LocationRequest()
locationRequest.interval = UPDATE_INTERVAL_IN_MILLISECONDS
locationRequest.fastestInterval = FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS
locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}
private fun onNewLocation(location: Location) {
Log.i(TAG, "New location: $location")
this.location = location
// Notify anyone listening for broadcasts about the new location.
val intent = Intent(ACTION_BROADCAST)
intent.putExtra(EXTRA_LOCATION, location)
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
}
private fun getLastLocation() {
try {
fusedLocationClient.lastLocation
.addOnCompleteListener { task ->
if (task.isSuccessful && task.result != null) {
location = task.result
} else {
Log.w(TAG, "Failed to get location.")
}
}
} catch (unlikely: SecurityException) {
Log.e(TAG, "Lost location permission.$unlikely")
}
}
/**
* Makes a request for location updates. Note that in this sample we merely log the
* [SecurityException].
*/
fun requestLocationUpdates() {
Log.i(TAG, "Requesting location updates")
startService(Intent(applicationContext, CarLocationService::class.java))
try {
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback, Looper.myLooper()
)
} catch (unlikely: SecurityException) {
Log.e(TAG, "Lost location permission. Could not request updates. $unlikely")
}
}
/**
* Removes location updates. Note that in this sample we merely log the
* [SecurityException].
*/
fun removeLocationUpdates() {
Log.i(TAG, "Removing location updates")
try {
fusedLocationClient.removeLocationUpdates(locationCallback)
stopSelf()
} catch (unlikely: SecurityException) {
Log.e(TAG, "Lost location permission. Could not remove updates. $unlikely")
}
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Log.i(TAG, "Service started")
// Tells the system to not try to recreate the service after it has been killed.
return START_NOT_STICKY
}
override fun onDestroy() {
serviceHandler.removeCallbacksAndMessages(null)
}
class LocalBinder(val service: CarLocationService) : Binder()
}

View File

@@ -0,0 +1,72 @@
package net.vonforst.evmap.auto
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.ResultReceiver
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
class PermissionActivity : Activity() {
companion object {
const val EXTRA_RESULT_RECEIVER = "result_receiver";
const val RESULT_GRANTED = "granted"
}
private lateinit var resultReceiver: ResultReceiver
private val permissions = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
private val requestCode = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent != null) {
resultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER)!!
if (!hasPermissions(permissions)) {
ActivityCompat.requestPermissions(this, permissions, requestCode)
} else {
onComplete(
requestCode,
permissions,
intArrayOf(PackageManager.PERMISSION_GRANTED)
)
}
} else {
finish()
}
}
private fun onComplete(requestCode: Int, permissions: Array<String>?, grantResults: IntArray) {
val bundle = Bundle()
bundle.putBoolean(
RESULT_GRANTED,
grantResults.all { it == PackageManager.PERMISSION_GRANTED })
resultReceiver.send(requestCode, bundle)
finish()
}
private fun hasPermissions(permissions: Array<String>): Boolean {
var result = true
for (permission in permissions) {
if (ContextCompat.checkSelfPermission(
this,
permission
) != PackageManager.PERMISSION_GRANTED
) {
result = false
break
}
}
return result
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
onComplete(requestCode, permissions, grantResults)
}
}

View File

@@ -5,4 +5,15 @@
<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="auto_location_service">EVMap läuft unter Android Auto und nutzt dafür deinen Standort.</string>
<string name="auto_no_chargers_found">Keine Ladestationen in der Nähe gefunden</string>
<string name="auto_no_favorites_found">Keine Favoriten gefunden</string>
<string name="open_in_app">In App öffnen</string>
<string name="opened_on_phone">Auf dem Telefon geöffnet</string>
<string name="auto_location_permission_needed">Um EVMap auf Android Auto zu nutzen, braucht die App Zugriff auf deinen Standort.</string>
<string name="grant_on_phone">Auf Telefon zulassen</string>
<string name="auto_chargers_closeby">In der Nähe</string>
<string name="auto_favorites">Favoriten</string>
<string name="auto_fault_report_date">⚠️ Störungsmeldung (%s)</string>
<string name="auto_no_refresh_possible">Weitere Aktualisierung nicht möglich. Bitte zurück gehen und neu starten.</string>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="CarAppTheme">
<item name="carColorPrimary">@color/colorPrimary</item>
<item name="carColorPrimaryDark">@color/colorPrimaryVariant</item>
<item name="carColorSecondary">@color/colorSecondary</item>
<item name="carColorSecondaryDark">@color/colorSecondaryVariant</item>
</style>
</resources>

View File

@@ -10,4 +10,15 @@
</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>
<string name="auto_location_service">EVMap is running on Android Auto and using your location.</string>
<string name="auto_no_chargers_found">No nearby chargers found</string>
<string name="auto_no_favorites_found">No favorites found</string>
<string name="open_in_app">Open in app</string>
<string name="opened_on_phone">Opened on phone</string>
<string name="auto_location_permission_needed">To run EVMap on Android Auto, you need to grant access to your location.</string>
<string name="grant_on_phone">Grant on phone</string>
<string name="auto_chargers_closeby">Nearby chargers</string>
<string name="auto_favorites">Favorites</string>
<string name="auto_fault_report_date">⚠️ Fault report (%s)</string>
<string name="auto_no_refresh_possible">Further updates not possible. Please go back and restart.</string>
</resources>

View File

@@ -0,0 +1,5 @@
<automotiveApp xmlns:tools="http://schemas.android.com/tools">
<uses
name="template"
tools:ignore="InvalidUsesTagAttribute" />
</automotiveApp>

View File

@@ -5,6 +5,17 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="geo" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="google.navigation" />
</intent>
</queries>
<application
android:name=".EvMapApplication"
android:allowBackup="true"
@@ -27,7 +38,224 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="geo" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Deutschland/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Oesterreich/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Schweiz/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Albanien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Andorra/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Aruba/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Belarus/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Belgien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Bosnien-und-Herzegowina/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Bulgarien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Daenemark/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Estland/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Faeroeer/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Finnland/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Frankreich/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Gibraltar/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Griechenland/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Grossbritannien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Irland/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Island/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Italien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Jordanien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Kasachstan/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Kroatien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Lettland/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Liechtenstein/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Litauen/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Luxemburg/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Malta/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Marokko/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Mazedonien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Moldawien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Monaco/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Montenegro/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Niederlande/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Norwegen/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Polen/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Portugal/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Rumaenien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Russland/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/San-Marino/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Schweden/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Serbien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Slowakei/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Slowenien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Spanien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Tuerkei/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Tschechien/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/USA/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Ukraine/..*/..*/..*/"
android:scheme="https" />
<data
android:host="www.goingelectric.de"
android:pathPattern="/stromtankstellen/Ungarn/..*/..*/..*/"
android:scheme="https" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -1,5 +1,6 @@
package net.vonforst.evmap
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
@@ -9,6 +10,8 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.NavController
import androidx.navigation.findNavController
@@ -17,11 +20,16 @@ import androidx.navigation.ui.setupWithNavController
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.fragment.MapFragment
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.utils.LocaleContextWrapper
import net.vonforst.evmap.utils.getLocationFromIntent
const val REQUEST_LOCATION_PERMISSION = 1
const val EXTRA_CHARGER_ID = "chargerId"
const val EXTRA_LAT = "lat"
const val EXTRA_LON = "lon"
class MapsActivity : AppCompatActivity() {
interface FragmentCallback {
@@ -59,11 +67,63 @@ class MapsActivity : AppCompatActivity() {
),
findViewById<DrawerLayout>(R.id.drawer_layout)
)
findViewById<NavigationView>(R.id.nav_view).setupWithNavController(navController)
val navView = findViewById<NavigationView>(R.id.nav_view)
navView.setupWithNavController(navController)
val header = navView.getHeaderView(0)
ViewCompat.setOnApplyWindowInsetsListener(header) { v, insets ->
v.setPadding(0, insets.getInsets(WindowInsetsCompat.Type.statusBars()).top, 0, 0)
insets
}
prefs = PreferenceDataSource(this)
checkPlayServices(this)
if (intent?.scheme == "geo") {
val query = intent.data?.query?.split("=")?.get(1)
val coords = getLocationFromIntent(intent)
if (coords != null) {
val lat = coords[0]
val lon = coords[1]
val deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragment.showLocation(lat, lon))
.createPendingIntent()
deepLink.send()
} else if (query != null && query.isNotEmpty()) {
val deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragment.showLocationByName(query))
.createPendingIntent()
deepLink.send()
}
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
if (id != null) {
val deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragment.showChargerById(id))
.createPendingIntent()
deepLink.send()
}
} else if (intent.hasExtra(EXTRA_CHARGER_ID)) {
navController.createDeepLink()
.setDestination(R.id.map)
.setArguments(
MapFragment.showCharger(
intent.getLongExtra(EXTRA_CHARGER_ID, 0),
intent.getDoubleExtra(EXTRA_LAT, 0.0),
intent.getDoubleExtra(EXTRA_LON, 0.0)
)
)
.createPendingIntent()
.send()
}
}
fun navigateTo(charger: ChargeLocation) {
@@ -91,7 +151,7 @@ class MapsActivity : AppCompatActivity() {
cb.getRootView(),
R.string.no_maps_app_found,
Snackbar.LENGTH_SHORT
)
).show()
}
}
@@ -103,7 +163,16 @@ class MapsActivity : AppCompatActivity() {
.build()
)
.build()
intent.launchUrl(this, Uri.parse(url))
try {
intent.launchUrl(this, Uri.parse(url))
} catch (e: ActivityNotFoundException) {
val cb = fragmentCallback ?: return
Snackbar.make(
cb.getRootView(),
R.string.no_browser_app_found,
Snackbar.LENGTH_SHORT
).show()
}
}
fun shareUrl(url: String) {

View File

@@ -1,16 +1,24 @@
package net.vonforst.evmap.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import net.vonforst.evmap.BR
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.chargeprice.ChargePrice
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
import net.vonforst.evmap.api.chargeprice.ChargepriceTag
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.databinding.ItemChargepriceBinding
import net.vonforst.evmap.databinding.ItemConnectorButtonBinding
import net.vonforst.evmap.ui.CheckableConstraintLayout
import net.vonforst.evmap.viewmodel.FavoritesViewModel
interface Equatable {
@@ -88,4 +96,105 @@ 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 ChargepriceAdapter() :
DataBindingAdapter<ChargePrice>() {
val viewPool = RecyclerView.RecycledViewPool();
var meta: ChargepriceChargepointMeta? = null
set(value) {
field = value
notifyDataSetChanged()
}
var myTariffs: Set<String>? = null
set(value) {
field = value
notifyDataSetChanged()
}
var myTariffsAll: Boolean? = null
set(value) {
field = value
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<ChargePrice> {
val holder = super.onCreateViewHolder(parent, viewType)
val binding = holder.binding as ItemChargepriceBinding
binding.rvTags.apply {
adapter = ChargepriceTagsAdapter()
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false).apply {
recycleChildrenOnDetach = true
}
itemAnimator = null
setRecycledViewPool(viewPool)
}
return holder
}
override fun bind(holder: ViewHolder<ChargePrice>, item: ChargePrice) {
super.bind(holder, item)
(holder.binding as ItemChargepriceBinding).apply {
this.meta = this@ChargepriceAdapter.meta
this.myTariffs = this@ChargepriceAdapter.myTariffs
this.myTariffsAll = this@ChargepriceAdapter.myTariffsAll
}
}
}
class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
private var checkedItem: Int? = 0
var enabledConnectors: List<String>? = null
get() = field
set(value) {
field = value
checkedItem?.let {
if (value != null && getItem(it).type !in value) {
val index = currentList.indexOfFirst {
it.type in value
}
checkedItem = if (index == -1) null else index
onCheckedItemChangedListener?.invoke(getCheckedItem())
}
}
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int = R.layout.item_connector_button
override fun onBindViewHolder(holder: ViewHolder<Chargepoint>, position: Int) {
val item = getItem(position)
super.bind(holder, item)
val binding = holder.binding as ItemConnectorButtonBinding
binding.enabled = enabledConnectors?.let { item.type in it } ?: true
val root = binding.root as CheckableConstraintLayout
root.isChecked = checkedItem == position
root.setOnClickListener {
root.isChecked = true
}
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
if (checked) {
checkedItem = position
notifyDataSetChanged()
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
}
}
}
fun getCheckedItem(): Chargepoint? = checkedItem?.let { getItem(it) }
fun setCheckedItem(item: Chargepoint?) {
checkedItem = item?.let { currentList.indexOf(item) } ?: null
}
var onCheckedItemChangedListener: ((Chargepoint?) -> Unit)? = null
}
class ChargepriceTagsAdapter() :
DataBindingAdapter<ChargepriceTag>() {
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice_tag
}

View File

@@ -48,7 +48,8 @@ fun buildDetails(
R.drawable.ic_address,
R.string.address,
loc.address.toString(),
loc.locationDescription
loc.locationDescription,
clickable = true
),
if (loc.operator != null) DetailsAdapter.Detail(
R.drawable.ic_operator,
@@ -93,16 +94,24 @@ fun buildDetails(
loc.cost.descriptionLong ?: loc.cost.descriptionShort
)
else null,
if (loc.chargecards != null && loc.chargecards.isNotEmpty()) DetailsAdapter.Detail(
R.drawable.ic_payment,
R.string.charge_cards,
ctx.resources.getQuantityString(
R.plurals.charge_cards_compatible_num,
loc.chargecards.size, loc.chargecards.size
),
formatChargeCards(loc.chargecards, chargeCards, filteredChargeCards, ctx),
clickable = true
) else null,
if (loc.chargecards != null && loc.chargecards.isNotEmpty() || loc.barrierFree == true)
DetailsAdapter.Detail(
R.drawable.ic_payment,
R.string.charge_cards,
listOfNotNull(
if (loc.barrierFree == true) ctx.resources.getString(R.string.charging_barrierfree) else null,
if (loc.chargecards != null && loc.chargecards.isNotEmpty()) {
ctx.resources.getQuantityString(
R.plurals.charge_cards_compatible_num,
loc.chargecards.size, loc.chargecards.size
)
} else null
).joinToString(", "),
if (loc.chargecards != null && loc.chargecards.isNotEmpty()) {
formatChargeCards(loc.chargecards, chargeCards, filteredChargeCards, ctx)
} else null,
clickable = true
) else null,
DetailsAdapter.Detail(
R.drawable.ic_location,
R.string.coordinates,

View File

@@ -1,16 +1,23 @@
package net.vonforst.evmap.adapter
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.animation.AccelerateInterpolator
import androidx.recyclerview.widget.ItemTouchHelper
import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.ItemFilterProfileBinding
import net.vonforst.evmap.storage.FilterProfile
class FilterProfilesAdapter(val dragHelper: ItemTouchHelper) : DataBindingAdapter<FilterProfile>() {
class FilterProfilesAdapter(
val dragHelper: ItemTouchHelper,
val onDelete: (FilterProfile) -> Unit,
val onRename: (FilterProfile) -> Unit
) : DataBindingAdapter<FilterProfile>() {
init {
setHasStableIds(true)
}
@SuppressLint("ClickableViewAccessibility")
override fun bind(
holder: ViewHolder<FilterProfile>,
item: FilterProfile
@@ -24,6 +31,20 @@ class FilterProfilesAdapter(val dragHelper: ItemTouchHelper) : DataBindingAdapte
}
false
}
binding.foreground.translationX = 0f
binding.btnDelete.setOnClickListener {
binding.foreground.animate()
.translationX(binding.foreground.width.toFloat())
.setDuration(250)
.setInterpolator(AccelerateInterpolator())
.withEndAction {
onDelete(item)
}
.start()
}
binding.btnRename.setOnClickListener {
onRename(item)
}
}
override fun getItemId(position: Int): Long {

View File

@@ -7,9 +7,11 @@ import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.memory.MemoryCache
import coil.size.OriginalSize
import coil.size.SizeResolver
import com.ortiz.touchview.TouchImageView
import com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
import net.vonforst.evmap.R
import net.vonforst.evmap.api.goingelectric.ChargerPhoto
@@ -19,17 +21,19 @@ class GalleryAdapter(
val itemClickListener: ItemClickListener? = null,
val detailView: Boolean = false,
val pageToLoad: Int? = null,
val imageCacheKey: MemoryCache.Key? = null,
val loadedListener: (() -> Unit)? = null
) :
ListAdapter<ChargerPhoto, GalleryAdapter.ViewHolder>(ChargerPhotoDiffCallback()) {
class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view)
interface ItemClickListener {
fun onItemClick(view: View, position: Int)
fun onItemClick(view: View, position: Int, imageCacheKey: MemoryCache.Key?)
}
val apikey = context.getString(R.string.goingelectric_key)
var loaded = false
val memoryKeys = HashMap<String, MemoryCache.Key?>()
@SuppressLint("ClickableViewAccessibility")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@@ -73,46 +77,63 @@ class GalleryAdapter(
if (detailView) {
(holder.view as TouchImageView).resetZoom()
}
Picasso.get()
.load(
"https://api.goingelectric.de/chargepoints/photo/?key=${apikey}" +
"&id=${getItem(position).id}" +
if (detailView) {
"&size=1000"
} else {
"&height=${holder.view.height}"
}
)
.into(holder.view, object : Callback {
override fun onSuccess() {
if (!loaded && loadedListener != null && pageToLoad == position) {
holder.view.viewTreeObserver.addOnPreDrawListener(object :
ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
holder.view.viewTreeObserver.removeOnPreDrawListener(this)
loadedListener.invoke()
return true
}
})
loaded = true
}
val id = getItem(position).id
val url = "https://api.goingelectric.de/chargepoints/photo/?key=${apikey}" +
"&id=$id" +
if (detailView) {
"&size=1000"
} else {
"&height=${holder.view.height}"
}
override fun onError(e: Exception?) {
holder.view.load(
url
) {
if (pageToLoad == position && imageCacheKey != null) {
placeholderMemoryCacheKey(imageCacheKey)
}
size(SizeResolver(OriginalSize))
allowHardware(false)
listener(
onSuccess = { _, metadata ->
memoryKeys[id] = metadata.memoryCacheKey
if (pageToLoad == position) invokeLoadedListener(holder.view)
},
onError = { _, _ ->
if (!loaded && loadedListener != null && pageToLoad == position) {
loadedListener.invoke()
loaded = true
}
}
})
)
}
if (pageToLoad == position && imageCacheKey != null) {
// start transition immediately
if (pageToLoad == position) invokeLoadedListener(holder.view)
}
holder.view.transitionName = galleryTransitionName(position)
if (itemClickListener != null) {
holder.view.setOnClickListener {
itemClickListener.onItemClick(holder.view, position)
itemClickListener.onItemClick(holder.view, position, memoryKeys[id])
}
}
}
private fun invokeLoadedListener(
view: ImageView
) {
if (!loaded && loadedListener != null) {
view.viewTreeObserver.addOnPreDrawListener(object :
ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
view.viewTreeObserver.removeOnPreDrawListener(this)
loadedListener.invoke()
return true
}
})
loaded = true
}
}
}
fun galleryTransitionName(position: Int) = "gallery_$position"

View File

@@ -1,7 +1,11 @@
package net.vonforst.evmap.api
import android.content.Context
import androidx.annotation.DrawableRes
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
import net.vonforst.evmap.R
import net.vonforst.evmap.api.goingelectric.Chargepoint
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
@@ -39,26 +43,35 @@ suspend fun Call.await(): Response {
}
}
const val earthRadiusKm: Double = 6372.8
private val plugNames = mapOf(
Chargepoint.TYPE_1 to R.string.plug_type_1,
Chargepoint.TYPE_2 to R.string.plug_type_2,
Chargepoint.TYPE_3 to R.string.plug_type_3,
Chargepoint.CCS to R.string.plug_ccs,
Chargepoint.SCHUKO to R.string.plug_schuko,
Chargepoint.CHADEMO to R.string.plug_chademo,
Chargepoint.SUPERCHARGER to R.string.plug_supercharger,
Chargepoint.CEE_BLAU to R.string.plug_cee_blau,
Chargepoint.CEE_ROT to R.string.plug_cee_rot,
Chargepoint.TESLA_ROADSTER_HPC to R.string.plug_roadster_hpc
)
/**
* Calculates the distance between two points on Earth in meters.
* Latitude and longitude should be given in degrees.
*/
fun distanceBetween(
startLatitude: Double, startLongitude: Double,
endLatitude: Double, endLongitude: Double
): Double {
// see https://rosettacode.org/wiki/Haversine_formula#Java
val dLat = Math.toRadians(endLatitude - startLatitude);
val dLon = Math.toRadians(endLongitude - startLongitude);
val originLat = Math.toRadians(startLatitude);
val destinationLat = Math.toRadians(endLatitude);
fun nameForPlugType(ctx: Context, type: String): String =
plugNames[type]?.let {
ctx.getString(it)
} ?: type
val a = Math.pow(Math.sin(dLat / 2), 2.toDouble()) + Math.pow(
Math.sin(dLon / 2),
2.toDouble()
) * Math.cos(originLat) * Math.cos(destinationLat);
val c = 2 * Math.asin(Math.sqrt(a));
return earthRadiusKm * c * 1000;
}
@DrawableRes
fun iconForPlugType(type: String): Int =
when (type) {
Chargepoint.CCS -> R.drawable.ic_connector_ccs
Chargepoint.CHADEMO -> R.drawable.ic_connector_chademo
Chargepoint.SCHUKO -> R.drawable.ic_connector_schuko
Chargepoint.SUPERCHARGER -> R.drawable.ic_connector_supercharger
Chargepoint.TYPE_2 -> R.drawable.ic_connector_typ2
Chargepoint.CEE_BLAU -> R.drawable.ic_connector_cee_blau
Chargepoint.CEE_ROT -> R.drawable.ic_connector_cee_rot
Chargepoint.TYPE_1 -> R.drawable.ic_connector_typ1
// TODO: add other connectors
else -> 0
}

View File

@@ -8,7 +8,10 @@ import net.vonforst.evmap.api.RateLimitInterceptor
import net.vonforst.evmap.api.await
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.viewmodel.FilterValues
import net.vonforst.evmap.viewmodel.Resource
import net.vonforst.evmap.viewmodel.getMultipleChoiceValue
import net.vonforst.evmap.viewmodel.getSliderValue
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
import okhttp3.Request
@@ -113,7 +116,21 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
data class ChargeLocationStatus(
val status: Map<Chargepoint, List<ChargepointStatus>>,
val source: String
)
) {
fun applyFilters(filters: FilterValues?): ChargeLocationStatus {
if (filters == null) return this
val connectorsVal = filters.getMultipleChoiceValue("connectors")
val minPower = filters.getSliderValue("min_power")
val statusFiltered = status.filterKeys {
(connectorsVal.all || it.type in connectorsVal.values) && it.power > minPower
}
return this.copy(status = statusFiltered)
}
val totalChargepoints = status.map { it.key.count }.sum()
}
enum class ChargepointStatus {
AVAILABLE, UNKNOWN, CHARGING, OCCUPIED, FAULTED

View File

@@ -1,14 +1,15 @@
package net.vonforst.evmap.api.availability
import com.squareup.moshi.JsonClass
import net.vonforst.evmap.api.distanceBetween
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.utils.distanceBetween
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
import java.util.*
private const val coordRange = 0.1 // range of latitude and longitude for loading the map
private const val maxDistance = 15 // max distance between reported positions in meters
@@ -138,15 +139,18 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
connectorStatus.forEach { (connector, statusStr) ->
val id = connector.uid
val power = connector.electricalProperties.getPower()
val type = when (connector.connectorType) {
"Type3" -> Chargepoint.TYPE_3
"Type2" -> Chargepoint.TYPE_2
"Type1" -> Chargepoint.TYPE_1
"Domestic" -> Chargepoint.SCHUKO
"Type2Combo" -> Chargepoint.CCS
"TepcoCHAdeMO" -> Chargepoint.CHADEMO
"Unspecified" -> "unspecified"
else -> throw IllegalArgumentException("unrecognized type ${connector.connectorType}")
val type = when (connector.connectorType.toLowerCase(Locale.ROOT)) {
"type3" -> Chargepoint.TYPE_3
"type2" -> Chargepoint.TYPE_2
"type1" -> Chargepoint.TYPE_1
"domestic" -> Chargepoint.SCHUKO
"type1combo" -> Chargepoint.CCS // US CCS, aka type1_combo
"type2combo" -> Chargepoint.CCS // EU CCS, aka type2_combo
"tepcochademo" -> Chargepoint.CHADEMO
"unspecified" -> "unknown"
"unknown" -> "unknown"
"saej1772" -> "unknown"
else -> "unknown"
}
val status = when (statusStr) {
"Unavailable" -> ChargepointStatus.FAULTED

View File

@@ -0,0 +1,78 @@
package net.vonforst.evmap.api.chargeprice
import android.content.Context
import com.facebook.stetho.okhttp3.StethoInterceptor
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import moe.banana.jsonapi2.ArrayDocument
import moe.banana.jsonapi2.JsonApiConverterFactory
import moe.banana.jsonapi2.ResourceAdapterFactory
import net.vonforst.evmap.BuildConfig
import okhttp3.Cache
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
interface ChargepriceApi {
@POST("charge_prices")
suspend fun getChargePrices(
@Body request: ChargepriceRequest,
@Header("Accept-Language") language: String
): ArrayDocument<ChargePrice>
@GET("vehicles")
suspend fun getVehicles(): ArrayDocument<ChargepriceCar>
@GET("tariffs")
suspend fun getTariffs(): ArrayDocument<ChargepriceTariff>
companion object {
private val cacheSize = 1L * 1024 * 1024 // 1MB
val supportedLanguages = setOf("de", "en", "fr", "nl")
private val jsonApiAdapterFactory = ResourceAdapterFactory.builder()
.add(ChargepriceRequest::class.java)
.add(ChargepriceTariff::class.java)
.add(ChargepriceBrand::class.java)
.add(ChargePrice::class.java)
.add(ChargepriceCar::class.java)
.build()
val moshi = Moshi.Builder()
.add(jsonApiAdapterFactory)
.add(KotlinJsonAdapterFactory())
.build()
fun create(
apikey: String,
baseurl: String = "https://api.chargeprice.app/v1/",
context: Context? = null
): ChargepriceApi {
val client = OkHttpClient.Builder().apply {
addInterceptor { chain ->
// add API key to every request
val original = chain.request()
val new = original.newBuilder()
.header("API-Key", apikey)
.header("Content-Type", "application/json")
.build()
chain.proceed(new)
}
if (BuildConfig.DEBUG) {
addNetworkInterceptor(StethoInterceptor())
}
if (context != null) {
cache(Cache(context.getCacheDir(), cacheSize))
}
}.build()
val retrofit = Retrofit.Builder()
.baseUrl(baseurl)
.addConverterFactory(JsonApiConverterFactory.create(moshi))
.client(client)
.build()
return retrofit.create(ChargepriceApi::class.java)
}
}
}

View File

@@ -0,0 +1,321 @@
package net.vonforst.evmap.api.chargeprice
import android.content.Context
import com.squareup.moshi.Json
import moe.banana.jsonapi2.HasMany
import moe.banana.jsonapi2.HasOne
import moe.banana.jsonapi2.JsonApi
import moe.banana.jsonapi2.Resource
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.ui.currency
import kotlin.math.ceil
import kotlin.math.floor
@JsonApi(type = "charge_price_request")
class ChargepriceRequest : Resource() {
@field:Json(name = "data_adapter")
lateinit var dataAdapter: String
lateinit var station: ChargepriceStation
lateinit var options: ChargepriceOptions
var tariffs: HasMany<ChargepriceTariff>? = null
var vehicle: HasOne<ChargepriceCar>? = null
}
data class ChargepriceStation(
val longitude: Double,
val latitude: Double,
val country: String?,
val network: String?,
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepoint>
) {
companion object {
fun fromGoingelectric(
geCharger: ChargeLocation,
compatibleConnectors: List<String>
): ChargepriceStation {
return ChargepriceStation(
geCharger.coordinates.lng,
geCharger.coordinates.lat,
geCharger.address.country,
geCharger.network,
geCharger.chargepoints.filter {
it.type in compatibleConnectors
}.map {
ChargepriceChargepoint(it.power, it.type)
}
)
}
}
}
data class ChargepriceChargepoint(
val power: Double,
val plug: String
)
data class ChargepriceOptions(
@Json(name = "max_monthly_fees") val maxMonthlyFees: Double? = null,
val energy: Double? = null,
val duration: Int? = null,
@Json(name = "battery_range") val batteryRange: List<Double>? = null,
@Json(name = "car_ac_phases") val carAcPhases: Int? = null,
val currency: String? = null,
@Json(name = "start_time") val startTime: Int? = null,
@Json(name = "allow_unbalanced_load") val allowUnbalancedLoad: Boolean? = null,
@Json(name = "provider_customer_tariffs") val providerCustomerTariffs: Boolean? = null
)
@JsonApi(type = "tariff")
class ChargepriceTariff() : Resource() {
lateinit var provider: String
lateinit var name: String
@field:Json(name = "direct_payment")
var directPayment: Boolean = false
@field:Json(name = "provider_customer_tariff")
var providerCustomerTariff: Boolean = false
@field:Json(name = "supported_cuntries")
lateinit var supportedCountries: Set<String>
@field:Json(name = "charge_card_id")
lateinit var chargeCardId: String // GE charge card ID
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as ChargepriceTariff
if (provider != other.provider) return false
if (name != other.name) return false
if (directPayment != other.directPayment) return false
if (providerCustomerTariff != other.providerCustomerTariff) return false
if (supportedCountries != other.supportedCountries) return false
if (chargeCardId != other.chargeCardId) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + provider.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + directPayment.hashCode()
result = 31 * result + providerCustomerTariff.hashCode()
result = 31 * result + supportedCountries.hashCode()
result = 31 * result + chargeCardId.hashCode()
return result
}
}
@JsonApi(type = "car")
class ChargepriceCar : Resource() {
lateinit var name: String
lateinit var brand: String
@field:Json(name = "dc_charge_ports")
lateinit var dcChargePorts: List<String>
lateinit var manufacturer: HasOne<ChargepriceBrand>
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as ChargepriceCar
if (name != other.name) return false
if (brand != other.brand) return false
if (dcChargePorts != other.dcChargePorts) return false
if (manufacturer != other.manufacturer) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + brand.hashCode()
result = 31 * result + dcChargePorts.hashCode()
result = 31 * result + manufacturer.hashCode()
return result
}
}
@JsonApi(type = "brand")
class ChargepriceBrand : Resource()
@JsonApi(type = "charge_price")
class ChargePrice : Resource(), Equatable, Cloneable {
lateinit var provider: String
@field:Json(name = "tariff_name")
lateinit var tariffName: String
lateinit var url: String
@field:Json(name = "monthly_min_sales")
var monthlyMinSales: Double = 0.0
@field:Json(name = "total_monthly_fee")
var totalMonthlyFee: Double = 0.0
@field:Json(name = "flat_rate")
var flatRate: Boolean = false
@field:Json(name = "direct_payment")
var directPayment: Boolean = false
@field:Json(name = "provider_customer_tariff")
var providerCustomerTariff: Boolean = false
lateinit var currency: String
@field:Json(name = "start_time")
var startTime: Int = 0
lateinit var tags: List<ChargepriceTag>
@field:Json(name = "charge_point_prices")
lateinit var chargepointPrices: List<ChargepointPrice>
var tariff: HasOne<ChargepriceTariff>? = null
fun formatMonthlyFees(ctx: Context): String {
return listOfNotNull(
if (totalMonthlyFee > 0) {
ctx.getString(R.string.chargeprice_base_fee, totalMonthlyFee, currency(currency))
} else null,
if (monthlyMinSales > 0) {
ctx.getString(R.string.chargeprice_min_spend, monthlyMinSales, currency(currency))
} else null
).joinToString(", ")
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as ChargePrice
if (provider != other.provider) return false
if (tariffName != other.tariffName) return false
if (url != other.url) return false
if (monthlyMinSales != other.monthlyMinSales) return false
if (totalMonthlyFee != other.totalMonthlyFee) return false
if (flatRate != other.flatRate) return false
if (directPayment != other.directPayment) return false
if (providerCustomerTariff != other.providerCustomerTariff) return false
if (currency != other.currency) return false
if (startTime != other.startTime) return false
if (tags != other.tags) return false
if (chargepointPrices != other.chargepointPrices) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + provider.hashCode()
result = 31 * result + tariffName.hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + monthlyMinSales.hashCode()
result = 31 * result + totalMonthlyFee.hashCode()
result = 31 * result + flatRate.hashCode()
result = 31 * result + directPayment.hashCode()
result = 31 * result + providerCustomerTariff.hashCode()
result = 31 * result + currency.hashCode()
result = 31 * result + startTime
result = 31 * result + tags.hashCode()
result = 31 * result + chargepointPrices.hashCode()
return result
}
public override fun clone(): ChargePrice {
return ChargePrice().apply {
chargepointPrices = this@ChargePrice.chargepointPrices
currency = this@ChargePrice.currency
directPayment = this@ChargePrice.directPayment
flatRate = this@ChargePrice.flatRate
monthlyMinSales = this@ChargePrice.monthlyMinSales
provider = this@ChargePrice.provider
providerCustomerTariff = this@ChargePrice.providerCustomerTariff
startTime = this@ChargePrice.startTime
tags = this@ChargePrice.tags
tariffName = this@ChargePrice.tariffName
totalMonthlyFee = this@ChargePrice.totalMonthlyFee
url = this@ChargePrice.url
tariff = this@ChargePrice.tariff
}
}
}
data class ChargepointPrice(
val power: Double,
val plug: String,
val price: Double,
@Json(name = "price_distribution") val priceDistribution: PriceDistribution,
@Json(name = "blocking_fee_start") val blockingFeeStart: Int?,
@Json(name = "no_price_reason") var noPriceReason: String?
) {
fun formatDistribution(ctx: Context): String {
fun percent(value: Double): String {
return ctx.getString(R.string.percent_format, value * 100) + "\u00a0"
}
fun time(value: Int): String {
val h = floor(value.toDouble() / 60).toInt();
val min = ceil(value.toDouble() % 60).toInt();
if (h == 0 && min > 0) return "${min}min";
// be slightly sloppy (3:01 is shown as 3h) to save space
else if (h > 0 && (min == 0 || min == 1)) return "${h}h";
else return "%d:%02dh".format(h, min);
}
// based on https://github.com/chargeprice/chargeprice-client/blob/d420bb2f216d9ad91a210a36dd0859a368a8229a/src/views/priceList.js
with(priceDistribution) {
return listOfNotNull(
if (session != null && session > 0.0) {
(if (session < 1) percent(session) else "") + ctx.getString(R.string.chargeprice_session_fee)
} else null,
if (kwh != null && kwh > 0.0 && !isOnlyKwh) {
(if (kwh < 1) percent(kwh) else "") + ctx.getString(R.string.chargeprice_per_kwh)
} else null,
if (minute != null && minute > 0.0) {
(if (minute < 1) percent(minute) else "") + ctx.getString(R.string.chargeprice_per_minute) +
if (blockingFeeStart != null) {
" (${
ctx.getString(
R.string.chargeprice_blocking_fee,
time(blockingFeeStart)
)
})"
} else ""
} else null,
if ((minute == null || minute == 0.0) && blockingFeeStart != null) {
ctx.getString(R.string.chargeprice_blocking_fee, time(blockingFeeStart))
} else null
).joinToString(" +\u00a0")
}
}
}
data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) {
val isOnlyKwh =
kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0)
}
data class ChargepriceTag(val kind: String, val text: String, val url: String?) : Equatable
data class ChargepriceMeta(
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepointMeta>
)
data class ChargepriceChargepointMeta(
val power: Double,
val plug: String,
val energy: Double,
val duration: Double
)

View File

@@ -1,5 +1,6 @@
package net.vonforst.evmap.api.goingelectric
import android.util.Log
import com.squareup.moshi.*
import java.lang.reflect.Type
import java.time.Instant
@@ -130,11 +131,18 @@ internal class HoursAdapter {
return Hours(null, null)
} else {
val match = regex.find(str)
?: throw IllegalArgumentException("$str does not match hours format")
return Hours(
LocalTime.parse(match.groupValues[1]),
LocalTime.parse(match.groupValues[2])
)
if (match != null) {
return Hours(
LocalTime.parse(match.groupValues[1]),
LocalTime.parse(match.groupValues[2])
)
} else {
// I cannot reproduce this case, but it seems to occur once in a while
Log.e("GoingElectricApi", "invalid hours value: " + str)
return Hours(
LocalTime.MIN, LocalTime.MIN
)
}
}
}

View File

@@ -6,7 +6,6 @@ import com.squareup.moshi.Moshi
import net.vonforst.evmap.BuildConfig
import okhttp3.Cache
import okhttp3.OkHttpClient
import retrofit2.Call
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
@@ -16,7 +15,7 @@ import retrofit2.http.Query
interface GoingElectricApi {
@GET("chargepoints/")
suspend fun getChargepoints(
@Query("sw_lat") swlat: Double, @Query("sw_lng") sw_lng: Double,
@Query("sw_lat") sw_lat: Double, @Query("sw_lng") sw_lng: Double,
@Query("ne_lat") ne_lat: Double, @Query("ne_lng") ne_lng: Double,
@Query("zoom") zoom: Float,
@Query("clustering") clustering: Boolean = false,
@@ -35,7 +34,28 @@ interface GoingElectricApi {
): Response<ChargepointList>
@GET("chargepoints/")
fun getChargepointDetail(@Query("ge_id") id: Long): Call<ChargepointList>
suspend fun getChargepointsRadius(
@Query("lat") lat: Double, @Query("lng") lng: Double,
@Query("radius") radius: Int,
@Query("zoom") zoom: Float,
@Query("orderby") orderby: String = "distance",
@Query("clustering") clustering: Boolean = false,
@Query("cluster_distance") clusterDistance: Int? = null,
@Query("freecharging") freecharging: Boolean = false,
@Query("freeparking") freeparking: Boolean = false,
@Query("min_power") minPower: Int = 0,
@Query("plugs") plugs: String? = null,
@Query("chargecards") chargecards: String? = null,
@Query("networks") networks: String? = null,
@Query("categories") categories: String? = null,
@Query("startkey") startkey: Int? = null,
@Query("open_twentyfourseven") open247: Boolean = false,
@Query("barrierfree") barrierfree: Boolean = false,
@Query("exclude_faults") excludeFaults: Boolean = false
): Response<ChargepointList>
@GET("chargepoints/")
suspend fun getChargepointDetail(@Query("ge_id") id: Long): Response<ChargepointList>
@GET("chargepoints/pluglist/")
suspend fun getPlugs(): Response<StringList>
@@ -49,6 +69,13 @@ interface GoingElectricApi {
companion object {
private val cacheSize = 10L * 1024 * 1024 // 10MB
val moshi = Moshi.Builder()
.add(ChargepointListItemJsonAdapterFactory())
.add(JsonObjectOrFalseAdapter.Factory())
.add(HoursAdapter())
.add(InstantAdapter())
.build()
fun create(
apikey: String,
baseurl: String = "https://api.goingelectric.de",
@@ -70,13 +97,6 @@ interface GoingElectricApi {
}
}.build()
val moshi = Moshi.Builder()
.add(ChargepointListItemJsonAdapterFactory())
.add(JsonObjectOrFalseAdapter.Factory())
.add(HoursAdapter())
.add(InstantAdapter())
.build()
val retrofit = Retrofit.Builder()
.baseUrl(baseurl)
.addConverterFactory(MoshiConverterFactory.create(moshi))

View File

@@ -54,6 +54,7 @@ data class ChargeLocation(
val url: String,
@Embedded(prefix = "fault_report_") @JsonObjectOrFalse @Json(name = "fault_report") val faultReport: FaultReport?,
val verified: Boolean,
@Json(name = "barrierfree") val barrierFree: Boolean?,
// only shown in details:
@JsonObjectOrFalse val operator: String?,
@JsonObjectOrFalse @Json(name = "general_information") val generalInformation: String?,
@@ -80,6 +81,21 @@ data class ChargeLocation(
.map { it.power }.maxOrNull() ?: 0.0
}
fun isMulti(filteredConnectors: Set<String>? = null): Boolean {
var chargepoints = chargepointsMerged
.filter { filteredConnectors?.contains(it.type) ?: true }
if (maxPower(filteredConnectors) >= 43) {
// fast charger -> only count fast chargers
chargepoints = chargepoints.filter { it.power >= 43 }
}
val connectors = chargepoints.map { it.type }.distinct().toSet()
// check if there is more than one plug for any connector type
val chargepointsPerConnector =
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumBy { it.count } }
return chargepointsPerConnector.any { it > 1 }
}
/**
* Merges chargepoints if they have the same plug and power
*
@@ -97,6 +113,9 @@ data class ChargeLocation(
}
}
val totalChargepoints: Int
get() = chargepoints.sumBy { it.count }
fun formatChargepoints(): String {
return chargepointsMerged.map {
"${it.count} × ${it.type} ${it.formatPower()}"
@@ -111,14 +130,16 @@ data class Cost(
@JsonObjectOrFalse @Json(name = "description_short") val descriptionShort: String?,
@JsonObjectOrFalse @Json(name = "description_long") val descriptionLong: String?
) {
fun getStatusText(ctx: Context): CharSequence {
return HtmlCompat.fromHtml(
ctx.getString(
R.string.cost_detail,
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid),
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
), 0
)
fun getStatusText(ctx: Context, emoji: Boolean = false): CharSequence {
val charging =
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
val parking =
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
return if (emoji) {
"$charging · \uD83C\uDD7F $parking"
} else {
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail, charging, parking), 0)
}
}
}

View File

@@ -0,0 +1,212 @@
package net.vonforst.evmap.fragment
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.ChargepriceAdapter
import net.vonforst.evmap.adapter.CheckableConnectorAdapter
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.viewModelFactory
import java.text.NumberFormat
class ChargepriceFragment : DialogFragment() {
private lateinit var binding: FragmentChargepriceBinding
private var connectionErrorSnackbar: Snackbar? = null
private val vm: ChargepriceViewModel by viewModels(factoryProducer = {
viewModelFactory {
ChargepriceViewModel(
requireActivity().application,
getString(R.string.chargeprice_key)
)
}
})
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
dialog?.window?.attributes?.windowAnimations = R.style.ChargepriceDialogAnimation
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_chargeprice, container, false
)
binding.lifecycleOwner = this
binding.vm = vm
binding.toolbar.inflateMenu(R.menu.chargeprice)
binding.toolbar.setTitle(R.string.chargeprice_title)
return binding.root
}
override fun onStart() {
super.onStart()
// dialog with 95% screen height
dialog?.window?.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
(resources.displayMetrics.heightPixels * 0.95).toInt()
)
}
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
val navController = findNavController()
toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
val jsonAdapter = GoingElectricApi.moshi.adapter(ChargeLocation::class.java)
val charger = jsonAdapter.fromJson(requireArguments().getString(ARG_CHARGER)!!)!!
vm.charger.value = charger
if (vm.chargepoint.value == null) {
vm.chargepoint.value = charger.chargepointsMerged.get(0)
}
val chargepriceAdapter = ChargepriceAdapter().apply {
onClickListener = {
(requireActivity() as MapsActivity).openUrl(it.url)
}
}
binding.chargePricesList.apply {
adapter = chargepriceAdapter
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
addItemDecoration(
DividerItemDecoration(
context, LinearLayoutManager.VERTICAL
)
)
}
vm.chargepriceMetaForChargepoint.observe(viewLifecycleOwner) {
chargepriceAdapter.meta = it?.data
}
vm.myTariffs.observe(viewLifecycleOwner) {
chargepriceAdapter.myTariffs = it
}
vm.myTariffsAll.observe(viewLifecycleOwner) {
chargepriceAdapter.myTariffsAll = it
}
val connectorsAdapter = CheckableConnectorAdapter()
val observer: Observer<Chargepoint> = Observer {
connectorsAdapter.setCheckedItem(it)
}
vm.chargepoint.observe(viewLifecycleOwner, observer)
connectorsAdapter.onCheckedItemChangedListener = {
vm.chargepoint.removeObserver(observer)
vm.chargepoint.value = it
vm.chargepoint.observe(viewLifecycleOwner, observer)
}
vm.vehicleCompatibleConnectors.observe(viewLifecycleOwner) {
connectorsAdapter.enabledConnectors = it
}
binding.connectorsList.apply {
adapter = connectorsAdapter
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
}
binding.imgChargepriceLogo.setOnClickListener {
(requireActivity() as MapsActivity).openUrl("https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=going_electric")
}
binding.btnSettings.setOnClickListener {
navController.navigate(R.id.action_chargeprice_to_settingsFragment)
}
binding.batteryRange.setLabelFormatter { value: Float ->
val fmt = NumberFormat.getNumberInstance()
fmt.maximumFractionDigits = 0
fmt.format(value.toDouble()) + "%"
}
binding.batteryRange.setOnTouchListener { _: View, motionEvent: MotionEvent ->
when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> vm.batteryRangeSliderDragging.value = true
MotionEvent.ACTION_UP -> vm.batteryRangeSliderDragging.value = false
}
false
}
binding.toolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_close -> {
dismiss()
true
}
else -> false
}
}
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) { res ->
when (res?.status) {
Status.ERROR -> {
if (vm.vehicle.value == null) return@observe
connectionErrorSnackbar?.dismiss()
connectionErrorSnackbar = Snackbar
.make(
view,
R.string.chargeprice_connection_error,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.retry) {
connectionErrorSnackbar?.dismiss()
vm.loadPrices()
}
connectionErrorSnackbar!!.show()
}
Status.SUCCESS, null -> {
connectionErrorSnackbar?.dismiss()
}
Status.LOADING -> {
}
}
}
}
companion object {
val ARG_CHARGER = "charger"
fun showCharger(charger: ChargeLocation): Bundle {
return Bundle().apply {
putString(
ARG_CHARGER,
GoingElectricApi.moshi.adapter(ChargeLocation::class.java).toJson(charger)
)
}
}
}
}

View File

@@ -86,8 +86,9 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
}
override fun onConnected() {
val context = this.context ?: return
if (ContextCompat.checkSelfPermission(
requireContext(),
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {

View File

@@ -1,14 +1,7 @@
package net.vonforst.evmap.fragment
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.view.*
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.FrameLayout
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.databinding.DataBindingUtil
@@ -24,6 +17,7 @@ import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.FiltersAdapter
import net.vonforst.evmap.databinding.FragmentFilterBinding
import net.vonforst.evmap.ui.showEditTextDialog
import net.vonforst.evmap.viewmodel.FilterViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
@@ -96,54 +90,22 @@ class FilterFragment : Fragment() {
true
}
R.id.menu_save_profile -> {
val container = FrameLayout(requireContext())
container.setPadding(
(16 * resources.displayMetrics.density).toInt(), 0,
(16 * resources.displayMetrics.density).toInt(), 0
)
val input = EditText(requireContext())
input.isSingleLine = true
vm.filterProfile.value?.let { profile ->
input.setText(profile.name)
}
container.addView(input)
val dialog = AlertDialog.Builder(requireContext())
.setTitle(R.string.save_as_profile)
.setMessage(R.string.save_profile_enter_name)
.setView(container)
.setPositiveButton(R.string.ok) { di, button ->
lifecycleScope.launch {
vm.saveAsProfile(input.text.toString())
findNavController().popBackStack()
}
showEditTextDialog(requireContext()) { dialog, input ->
vm.filterProfile.value?.let { profile ->
input.setText(profile.name)
}
.setNegativeButton(R.string.cancel) { di, button ->
}.show()
// move dialog to top
val attrs = dialog.window?.attributes?.apply {
gravity = Gravity.TOP
}
dialog.window?.attributes = attrs
// focus and show keyboard
input.requestFocus()
input.postDelayed({
val imm =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT)
}, 100)
input.setOnEditorActionListener { _, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
val text = input.text
if (text != null) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
return@setOnEditorActionListener true
dialog.setTitle(R.string.save_as_profile)
.setMessage(R.string.save_profile_enter_name)
.setPositiveButton(R.string.ok) { di, button ->
lifecycleScope.launch {
vm.saveAsProfile(input.text.toString())
findNavController().popBackStack()
}
}
.setNegativeButton(R.string.cancel) { di, button ->
}
}
false
}
true
}

View File

@@ -11,29 +11,38 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.DataBindingAdapter
import net.vonforst.evmap.adapter.FilterProfilesAdapter
import net.vonforst.evmap.databinding.FragmentFilterProfilesBinding
import net.vonforst.evmap.databinding.ItemFilterProfileBinding
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.ui.showEditTextDialog
import net.vonforst.evmap.viewmodel.FilterProfilesViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class FilterProfilesFragment : Fragment() {
private lateinit var touchHelper: ItemTouchHelper
private lateinit var adapter: FilterProfilesAdapter
private lateinit var binding: FragmentFilterProfilesBinding
private val vm: FilterProfilesViewModel by viewModels(factoryProducer = {
viewModelFactory {
FilterProfilesViewModel(requireActivity().application)
}
})
private var deleteSnackbar: Snackbar? = null
private var toDelete: FilterProfile? = null
override fun onCreateView(
inflater: LayoutInflater,
@@ -58,7 +67,7 @@ class FilterProfilesFragment : Fragment() {
)
val touchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
touchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT
) {
@@ -86,7 +95,8 @@ class FilterProfilesFragment : Fragment() {
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
vm.delete(viewHolder.itemId)
val fp = vm.filterProfiles.value?.find { it.id == viewHolder.itemId }
fp?.let { delete(it) }
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
@@ -166,9 +176,26 @@ class FilterProfilesFragment : Fragment() {
}
})
val adapter = FilterProfilesAdapter(touchHelper)
adapter = FilterProfilesAdapter(touchHelper, onDelete = { fp ->
delete(fp)
}, onRename = { fp ->
showEditTextDialog(requireContext()) { dialog, input ->
input.setText(fp.name)
dialog.setTitle(R.string.rename)
.setMessage(R.string.save_profile_enter_name)
.setPositiveButton(R.string.ok) { di, button ->
lifecycleScope.launch {
vm.update(fp.copy(name = input.text.toString()))
}
}
.setNegativeButton(R.string.cancel) { di, button ->
}
}
})
binding.filterProfilesList.apply {
this.adapter = adapter
this.adapter = this@FilterProfilesFragment.adapter
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
addItemDecoration(
@@ -184,4 +211,45 @@ class FilterProfilesFragment : Fragment() {
findNavController().popBackStack()
}
}
fun delete(fp: FilterProfile) {
val position = vm.filterProfiles.value?.indexOf(fp) ?: return
// if there is already a profile to delete, delete it now
actuallyDelete()
deleteSnackbar?.dismiss()
toDelete = fp
view?.let {
val snackbar = Snackbar.make(
it,
getString(R.string.deleted_filterprofile, fp.name),
Snackbar.LENGTH_LONG
).setAction(R.string.undo) {
toDelete = null
adapter.notifyItemChanged(position)
}.addCallback(object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
// if undo was not clicked, actually delete
if (event == DISMISS_EVENT_TIMEOUT || event == DISMISS_EVENT_SWIPE) {
actuallyDelete()
}
}
})
deleteSnackbar = snackbar
snackbar.show()
} ?: run {
actuallyDelete()
}
}
private fun actuallyDelete() {
toDelete?.let { vm.delete(it.id) }
toDelete = null
}
override fun onStop() {
super.onStop()
actuallyDelete()
}
}

View File

@@ -12,6 +12,7 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.transition.TransitionInflater
import androidx.viewpager2.widget.ViewPager2
import coil.memory.MemoryCache
import com.ortiz.touchview.TouchImageView
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.GalleryAdapter
@@ -24,18 +25,23 @@ class GalleryFragment : Fragment() {
companion object {
private const val EXTRA_POSITION = "position"
private const val EXTRA_PHOTOS = "photos"
private const val EXTRA_IMAGE_CACHE_KEY = "image_cache_key"
private const val SAVED_CURRENT_PAGE_POSITION = "current_page_position"
fun buildArgs(photos: List<ChargerPhoto>, position: Int): Bundle {
fun buildArgs(
photos: List<ChargerPhoto>,
position: Int,
imageCacheKey: MemoryCache.Key?
): Bundle {
return Bundle().apply {
putParcelableArrayList(EXTRA_PHOTOS, ArrayList(photos))
putInt(EXTRA_POSITION, position)
putParcelable(EXTRA_IMAGE_CACHE_KEY, imageCacheKey)
}
}
}
private lateinit var binding: FragmentGalleryBinding
private var isReturning: Boolean = false
private var startingPosition: Int = 0
private var currentPosition: Int = 0
private lateinit var galleryAdapter: GalleryAdapter
@@ -49,7 +55,6 @@ class GalleryFragment : Fragment() {
if (image != null && image.currentZoom !in 0.95f..1.05f) {
image.setZoomAnimated(1f, 0.5f, 0.5f)
} else {
isReturning = true
galleryVm.galleryPosition.value = currentPosition
findNavController().popBackStack()
}
@@ -73,7 +78,10 @@ class GalleryFragment : Fragment() {
savedInstanceState?.getInt(SAVED_CURRENT_PAGE_POSITION) ?: startingPosition
galleryAdapter =
GalleryAdapter(requireContext(), detailView = true, pageToLoad = currentPosition) {
GalleryAdapter(
requireContext(), detailView = true, pageToLoad = currentPosition,
imageCacheKey = args.getParcelable(EXTRA_IMAGE_CACHE_KEY)
) {
startPostponedEnterTransition()
}
binding.gallery.setPageTransformer { page, _ ->
@@ -120,10 +128,8 @@ class GalleryFragment : Fragment() {
names: MutableList<String>,
sharedElements: MutableMap<String, View>
) {
if (isReturning) {
val currentPage = currentPage ?: return
sharedElements[names[0]] = currentPage
}
val currentPage = currentPage ?: return
sharedElements[names[0]] = currentPage
}
}

View File

@@ -7,10 +7,13 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.location.Geocoder
import android.location.Location
import android.os.Bundle
import android.os.Handler
import android.view.*
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.annotation.RequiresPermission
import androidx.appcompat.app.AlertDialog
@@ -35,6 +38,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import coil.memory.MemoryCache
import com.car2go.maps.AnyMap
import com.car2go.maps.MapFragment
import com.car2go.maps.OnMapReadyCallback
@@ -49,11 +53,15 @@ 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.LocationListener
import com.mapzen.android.lost.api.LocationRequest
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.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.*
import net.vonforst.evmap.adapter.ConnectorAdapter
import net.vonforst.evmap.adapter.DetailsAdapter
@@ -69,15 +77,19 @@ import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.ClusterIconGenerator
import net.vonforst.evmap.ui.MarkerAnimator
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.utils.boundingBox
import net.vonforst.evmap.utils.distanceBetween
import net.vonforst.evmap.viewmodel.*
const val REQUEST_AUTOCOMPLETE = 2
const val ARG_CHARGER_ID = "chargerId"
const val ARG_LAT = "lat"
const val ARG_LON = "lon"
const val ARG_LOCATION_NAME = "locationName"
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback,
LostApiClient.ConnectionCallbacks {
LostApiClient.ConnectionCallbacks, LocationListener {
private lateinit var binding: FragmentMapBinding
private val vm: MapViewModel by viewModels(factoryProducer = {
viewModelFactory {
@@ -88,9 +100,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
})
private val galleryVm: GalleryViewModel by activityViewModels()
private lateinit var mapFragment: MapFragment
private var mapFragment: MapFragment? = null
private var map: AnyMap? = null
private lateinit var locationClient: LostApiClient
private var requestingLocationUpdates = false
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
@@ -123,19 +136,29 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
locationClient = LostApiClient.Builder(requireContext())
.addConnectionCallbacks(this)
.build()
locationClient.connect()
clusterIconGenerator = ClusterIconGenerator(requireContext())
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_map, container, false)
binding.lifecycleOwner = this
binding.vm = vm
mapFragment = MapFragment()
val provider = PreferenceDataSource(requireContext()).mapProvider
mapFragment.setPriority(
arrayOf(
if (mapFragment == null || mapFragment!!.priority[0] != provider) {
mapFragment = MapFragment()
mapFragment!!.priority = arrayOf(
when (provider) {
"mapbox" -> MapFragment.MAPBOX
"google" -> MapFragment.GOOGLE
@@ -144,25 +167,18 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
MapFragment.GOOGLE,
MapFragment.MAPBOX
)
)
requireActivity().supportFragmentManager
.beginTransaction()
.replace(R.id.map, mapFragment)
.commit()
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())
// reset map-related stuff (map provider may have changed)
map = null
markers.clear()
clusterMarkers = emptyList()
searchResultMarker = null
searchResultIcon = null
}
setHasOptionsMenu(true)
postponeEnterTransition()
@@ -186,7 +202,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
insets
}
setExitSharedElementCallback(exitElementCallback)
setExitSharedElementCallback(reenterSharedElementCallback)
exitTransition = TransitionInflater.from(requireContext())
.inflateTransition(R.transition.map_exit_transition)
@@ -199,7 +215,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mapFragment.getMapAsync(this)
mapFragment!!.getMapAsync(this)
bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet)
detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
@@ -209,20 +225,44 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
setupObservers()
setupClickListeners()
setupAdapters()
val navController = findNavController()
(activity as? MapsActivity)?.setSupportActionBar(binding.toolbar)
binding.toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
val prefs = PreferenceDataSource(requireContext())
val navController = findNavController()
if (!prefs.welcomeDialogShown) {
try {
navController.navigate(R.id.action_map_to_welcome)
} catch (ignored: IllegalArgumentException) {
// when there is already another navigation going on
}
} else if (!prefs.update060AndroidAutoDialogShown) {
try {
navController.navigate(R.id.action_map_to_update_060_androidauto)
} catch (ignored: IllegalArgumentException) {
// when there is already another navigation going on
}
}
}
override fun onResume() {
super.onResume()
val hostActivity = activity as? MapsActivity ?: return
hostActivity.fragmentCallback = this
val navController = findNavController()
binding.toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
vm.reloadPrefs()
if (requestingLocationUpdates && ContextCompat.checkSelfPermission(
requireContext(),
ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED && locationClient.isConnected
) {
requestLocationUpdates()
}
}
private fun setupClickListeners() {
@@ -251,6 +291,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
binding.fabLayers.setOnClickListener {
openLayersMenu()
}
binding.layers.btnClose.setOnClickListener {
closeLayersMenu()
}
binding.detailView.goingelectricButton.setOnClickListener {
val charger = vm.charger.value?.data
if (charger != null) {
@@ -259,8 +302,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
binding.detailView.btnChargeprice.setOnClickListener {
val charger = vm.charger.value?.data ?: return@setOnClickListener
(activity as? MapsActivity)?.openUrl(
"https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=going_electric")
findNavController().navigate(
R.id.action_map_to_chargepriceFragment,
ChargepriceFragment.showCharger(charger)
)
}
binding.detailView.topPart.setOnClickListener {
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
@@ -288,6 +333,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val charger = vm.charger.value?.data
if (charger != null) {
(activity as? MapsActivity)?.openUrl("https:${charger.url}edit/")
Toast.makeText(
requireContext(),
R.string.edit_on_goingelectric_info,
Toast.LENGTH_LONG
).show()
}
true
}
@@ -343,7 +393,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.bottomSheetState.value = newState
updateBackPressedCallback()
if (vm.layersMenuOpen.value!! && newState !in listOf(BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING, BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN, BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED)) {
if (vm.layersMenuOpen.value!! && newState !in listOf(
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING,
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN,
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
)
) {
closeLayersMenu()
}
}
@@ -397,6 +452,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
searchResultMarker = null
if (place != null) {
// disable location following when search result is shown
vm.myLocationEnabled.value = false
if (place.viewport != null) {
map.animateCamera(map.cameraUpdateFactory.newLatLngBounds(place.viewport, 0))
} else {
@@ -446,7 +503,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
getMarkerTint(c, vm.filteredConnectors.value),
highlight = false,
fault = c.faultReport != null,
multi = getMarkerMulti(c, vm.filteredConnectors.value)
multi = c.isMulti(vm.filteredConnectors.value)
)
)
}
@@ -460,7 +517,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
getMarkerTint(charger, vm.filteredConnectors.value),
highlight = true,
fault = charger.faultReport != null,
multi = getMarkerMulti(charger, vm.filteredConnectors.value)
multi = charger.isMulti(vm.filteredConnectors.value)
)
)
animator.animateMarkerBounce(marker)
@@ -473,28 +530,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
getMarkerTint(c, vm.filteredConnectors.value),
highlight = false,
fault = c.faultReport != null,
multi = getMarkerMulti(c, vm.filteredConnectors.value)
multi = c.isMulti(vm.filteredConnectors.value)
)
)
}
}
}
private fun getMarkerMulti(charger: ChargeLocation, filteredConnectors: Set<String>?): Boolean {
var chargepoints = charger.chargepointsMerged
.filter { filteredConnectors?.contains(it.type) ?: true }
if (charger.maxPower(filteredConnectors) >= 43) {
// fast charger -> only count fast chargers
chargepoints = chargepoints.filter { it.power >= 43 }
}
val connectors = chargepoints.map { it.type }.distinct().toSet()
// check if there is more than one plug for any connector type
val chargepointsPerConnector =
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumBy { it.count } }
return chargepointsPerConnector.any { it > 1 }
}
private fun updateFavoriteToggle() {
val favs = vm.favorites.value ?: return
val charger = vm.chargerSparse.value ?: return
@@ -507,12 +549,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private fun setupAdapters() {
val galleryClickListener = object : GalleryAdapter.ItemClickListener {
override fun onItemClick(view: View, position: Int) {
override fun onItemClick(view: View, position: Int, imageCacheKey: MemoryCache.Key?) {
val photos = vm.charger.value?.data?.photos ?: return
val extras = FragmentNavigatorExtras(view to view.transitionName)
view.findNavController().navigate(
R.id.action_map_to_galleryFragment,
GalleryFragment.buildArgs(photos, position),
GalleryFragment.buildArgs(photos, position, imageCacheKey),
null,
extras
)
@@ -531,17 +573,43 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
DividerItemDecoration(
context, LinearLayoutManager.HORIZONTAL
).apply {
setDrawable(context.getDrawable(R.drawable.gallery_divider)!!)
setDrawable(ContextCompat.getDrawable(context, R.drawable.gallery_divider)!!)
})
}
if (galleryPosition == null) {
startPostponedEnterTransition()
} else {
binding.gallery.scrollToPosition(galleryPosition)
binding.gallery.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
v.removeOnLayoutChangeListener(this)
val layoutManager = binding.gallery.layoutManager!!
val viewAtPosition = layoutManager.findViewByPosition(galleryPosition)
if (viewAtPosition == null || layoutManager.isViewPartiallyVisible(
viewAtPosition,
false,
true
)
) {
binding.gallery.post {
layoutManager.scrollToPosition(galleryPosition)
}
}
}
})
// make sure that the app does not freeze waiting for a picture to load
Handler().postDelayed({
startPostponedEnterTransition()
}, 500)
}, 100)
}
binding.detailView.connectors.apply {
@@ -557,7 +625,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val charger = vm.chargerDetails.value?.data
if (charger != null) {
when (it.icon) {
R.drawable.ic_location -> {
R.drawable.ic_location, R.drawable.ic_address -> {
(activity as? MapsActivity)?.showLocation(charger)
}
R.drawable.ic_fault_report -> {
@@ -609,6 +677,21 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
override fun onMapReady(map: AnyMap) {
this.map = map
chargerIconGenerator = ChargerIconGenerator(requireContext(), map.bitmapDescriptorFactory)
if (BuildConfig.FLAVOR == "google" && mapFragment!!.priority[0] == MapFragment.GOOGLE) {
// Google Maps: icons can be generated in background thread
lifecycleScope.launch {
withContext(Dispatchers.IO) {
chargerIconGenerator.preloadCache()
}
}
} else {
// Mapbox: needs to be run on main thread
chargerIconGenerator.preloadCache()
}
animator = MarkerAnimator(chargerIconGenerator)
map.uiSettings.setTiltGesturesEnabled(false)
map.setIndoorEnabled(false)
@@ -622,6 +705,19 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
map.setOnCameraMoveListener {
binding.scaleView.update(map.cameraPosition.zoom, map.cameraPosition.target.latitude)
}
map.setOnCameraMoveStartedListener { reason ->
if (reason == AnyMap.OnCameraMoveStartedListener.REASON_GESTURE) {
if (vm.myLocationEnabled.value == true) {
// disable location following when manually scrolling the map
vm.myLocationEnabled.value = false
removeLocationUpdates()
}
if (vm.layersMenuOpen.value == true) {
// close layers menu if open
closeLayersMenu()
}
}
}
map.setOnMarkerClickListener { marker ->
when (marker) {
in markers -> {
@@ -647,6 +743,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
backPressedCallback.handleOnBackPressed()
}
}
map.setMapType(vm.mapType.value)
map.setTrafficEnabled(vm.mapTrafficEnabled.value ?: false)
// set padding so that compass is not obstructed by toolbar
map.setPadding(0, binding.toolbarContainer.height, 0, 0)
@@ -660,6 +758,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val position = vm.mapPosition.value
val lat = arguments?.optDouble(ARG_LAT)
val lon = arguments?.optDouble(ARG_LON)
val chargerId = arguments?.optLong(ARG_CHARGER_ID)
val locationName = arguments?.getString(ARG_LOCATION_NAME)
var positionSet = false
if (position != null) {
@@ -667,28 +768,72 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
map.cameraUpdateFactory.newLatLngZoom(position.bounds.center, position.zoom)
map.moveCamera(cameraUpdate)
positionSet = true
} else if (lat != null && lon != null) {
// show given position
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(LatLng(lat, lon), 16f)
map.moveCamera(cameraUpdate)
// show charger detail after chargers were loaded
val chargerId = arguments?.optLong(ARG_CHARGER_ID)
vm.chargepoints.observe(
} else if (chargerId != null && (lat == null || lon == null)) {
// show given charger ID
vm.loadChargerById(chargerId)
vm.chargerSparse.observe(
viewLifecycleOwner,
object : Observer<Resource<List<ChargepointListItem>>> {
override fun onChanged(res: Resource<List<ChargepointListItem>>) {
if (res.data == null) return
for (item in res.data) {
if (item is ChargeLocation && item.id == chargerId) {
vm.chargerSparse.value = item
vm.chargepoints.removeObserver(this)
}
object : Observer<ChargeLocation> {
override fun onChanged(item: ChargeLocation?) {
if (item?.id == chargerId) {
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(
LatLng(item.coordinates.lat, item.coordinates.lng), 16f
)
map.moveCamera(cameraUpdate)
vm.chargerSparse.removeObserver(this)
}
}
})
positionSet = true
} else if (lat != null && lon != null) {
// show given position
val latLng = LatLng(lat, lon)
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(latLng, 16f)
map.moveCamera(cameraUpdate)
if (chargerId != null) {
// show charger detail after chargers were loaded
vm.chargepoints.observe(
viewLifecycleOwner,
object : Observer<Resource<List<ChargepointListItem>>> {
override fun onChanged(res: Resource<List<ChargepointListItem>>) {
if (res.data == null) return
for (item in res.data) {
if (item is ChargeLocation && item.id == chargerId) {
vm.chargerSparse.value = item
vm.chargepoints.removeObserver(this)
}
}
}
})
} else {
// mark location as search result
vm.searchResult.value = PlaceWithBounds(latLng, boundingBox(latLng, 750.0))
}
positionSet = true
} else if (locationName != null) {
lifecycleScope.launch {
val address = withContext(Dispatchers.IO) {
Geocoder(requireContext()).getFromLocationName(locationName, 1).getOrNull(0)
}
address?.let {
val latLng = LatLng(it.latitude, it.longitude)
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(latLng, 16f)
map.moveCamera(cameraUpdate)
val bboxSize = if (it.subAdminArea != null) {
750.0 // this is a place within a city
} else if (it.adminArea != null && it.adminArea != it.featureName) {
4000.0 // this is a city
} else if (it.adminArea != null) {
100000.0 // this is a top-level administrative area (i.e. state)
} else {
500000.0 // this is a country
}
vm.searchResult.value = PlaceWithBounds(latLng, boundingBox(latLng, bboxSize))
}
}
}
if (ContextCompat.checkSelfPermission(
requireContext(),
@@ -714,15 +859,18 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private fun enableLocation(moveTo: Boolean, animate: Boolean) {
val map = this.map ?: return
map.setMyLocationEnabled(true)
vm.myLocationEnabled.value = true
map.uiSettings.setMyLocationButtonEnabled(false)
if (moveTo && locationClient.isConnected) {
moveToCurrentLocation(map, animate)
if (moveTo) {
vm.myLocationEnabled.value = true
if (locationClient.isConnected) {
moveToLastLocation(map, animate)
requestLocationUpdates()
}
}
}
@RequiresPermission(ACCESS_FINE_LOCATION)
private fun moveToCurrentLocation(map: AnyMap, animate: Boolean) {
private fun moveToLastLocation(map: AnyMap, animate: Boolean) {
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient)
if (location != null) {
val latLng = LatLng(location.latitude, location.longitude)
@@ -753,7 +901,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
getMarkerTint(charger, vm.filteredConnectors.value),
highlight = charger == vm.chargerSparse.value,
fault = charger.faultReport != null,
multi = getMarkerMulti(charger, vm.filteredConnectors.value)
multi = charger.isMulti(vm.filteredConnectors.value)
)
)
}
@@ -770,7 +918,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
val multi = getMarkerMulti(charger, vm.filteredConnectors.value)
val multi = charger.isMulti(vm.filteredConnectors.value)
animator.animateMarkerDisappear(marker, tint, highlight, fault, multi)
} else {
animator.deleteMarker(marker)
@@ -785,14 +933,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
val multi = getMarkerMulti(charger, vm.filteredConnectors.value)
val multi = charger.isMulti(vm.filteredConnectors.value)
val marker = map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
.icon(
chargerIconGenerator.getBitmapDescriptor(
tint,
0,
0f,
255,
highlight,
fault,
@@ -974,16 +1122,17 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
return binding.root
}
private val exitElementCallback: SharedElementCallback = object : SharedElementCallback() {
override fun onMapSharedElements(
names: MutableList<String>,
sharedElements: MutableMap<String, View>
) {
// Locate the ViewHolder for the clicked position.
val position = galleryVm.galleryPosition.value ?: return
private val reenterSharedElementCallback: SharedElementCallback =
object : SharedElementCallback() {
override fun onMapSharedElements(
names: MutableList<String>,
sharedElements: MutableMap<String, View>
) {
// Locate the ViewHolder for the clicked position.
val position = galleryVm.galleryPosition.value ?: return
val vh = binding.gallery.findViewHolderForAdapterPosition(position)
if (vh?.itemView == null) return
val vh = binding.gallery.findViewHolderForAdapterPosition(position)
if (vh?.itemView == null) return
// Map the first shared element name to the child ImageView.
sharedElements[names[0]] = vh.itemView
@@ -998,21 +1147,90 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
putDouble(ARG_LON, charger.coordinates.lng)
}
}
fun showLocation(lat: Double, lon: Double): Bundle {
return Bundle().apply {
putDouble(ARG_LAT, lat)
putDouble(ARG_LON, lon)
}
}
fun showChargerById(id: Long): Bundle {
return Bundle().apply {
putLong(ARG_CHARGER_ID, id)
}
}
fun showCharger(id: Long, lat: Double, lon: Double): Bundle {
return Bundle().apply {
putLong(ARG_CHARGER_ID, id)
putDouble(ARG_LAT, lat)
putDouble(ARG_LON, lon)
}
}
fun showLocationByName(query: String): Bundle {
return Bundle().apply {
putString(ARG_LOCATION_NAME, query)
}
}
}
override fun onConnected() {
val map = this.map ?: return
val context = this.context ?: return
if (vm.myLocationEnabled.value == true) {
if (ActivityCompat.checkSelfPermission(
requireContext(),
context,
ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
moveToCurrentLocation(map, false)
moveToLastLocation(map, false)
requestLocationUpdates()
}
}
}
@RequiresPermission(ACCESS_FINE_LOCATION)
private fun requestLocationUpdates() {
val request: LocationRequest = LocationRequest.create()
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
.setInterval(5000)
LocationServices.FusedLocationApi.requestLocationUpdates(locationClient, request, this)
requestingLocationUpdates = true
}
private fun removeLocationUpdates() {
if (locationClient.isConnected) {
LocationServices.FusedLocationApi.removeLocationUpdates(locationClient, this)
}
}
override fun onConnectionSuspended() {
}
override fun onLocationChanged(location: Location?) {
val map = this.map ?: return
if (location == null || vm.myLocationEnabled.value == false) return
val latLng = LatLng(location.latitude, location.longitude)
val oldLoc = vm.location.value
if (latLng != oldLoc && (oldLoc == null || distanceBetween(
latLng.latitude,
latLng.longitude,
oldLoc.latitude,
oldLoc.longitude
) > 1)
) {
// only update map if location changed by more than 1 meter
vm.location.value = latLng
val camUpdate = map.cameraUpdateFactory.newLatLng(latLng)
map.animateCamera(camUpdate)
}
}
override fun onPause() {
super.onPause()
removeLocationUpdates()
}
}

View File

@@ -4,20 +4,37 @@ import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.updateNightMode
import net.vonforst.evmap.viewmodel.SettingsViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class SettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
private lateinit var prefs: PreferenceDataSource
private val vm: SettingsViewModel by viewModels(factoryProducer = {
viewModelFactory {
SettingsViewModel(
requireActivity().application,
getString(R.string.chargeprice_key)
)
}
})
private lateinit var myVehiclePreference: ListPreference
private lateinit var myTariffsPreference: MultiSelectListPreference
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
@@ -28,6 +45,47 @@ class SettingsFragment : PreferenceFragmentCompat(),
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
myVehiclePreference.isEnabled = false
vm.vehicles.observe(viewLifecycleOwner) { res ->
res.data?.let { cars ->
val sortedCars = cars.sortedBy { it.brand }
myVehiclePreference.entryValues = sortedCars.map { it.id }.toTypedArray()
myVehiclePreference.entries =
sortedCars.map { "${it.brand} ${it.name}" }.toTypedArray()
myVehiclePreference.isEnabled = true
myVehiclePreference.summary = cars.find { it.id == prefs.chargepriceMyVehicle }
?.let { "${it.brand} ${it.name}" }
}
}
myTariffsPreference = findPreference("chargeprice_my_tariffs")!!
myTariffsPreference.isEnabled = false
vm.tariffs.observe(viewLifecycleOwner) { res ->
res.data?.let { tariffs ->
myTariffsPreference.entryValues = tariffs.map { it.id }.toTypedArray()
myTariffsPreference.entries = tariffs.map {
if (!it.name.startsWith(it.provider)) {
"${it.provider} ${it.name}"
} else {
it.name
}
}.toTypedArray()
myTariffsPreference.isEnabled = true
updateMyTariffsSummary()
}
}
}
private fun updateMyTariffsSummary() {
myTariffsPreference.summary = if (prefs.chargepriceMyTariffsAll) {
getString(R.string.chargeprice_all_tariffs_selected)
} else {
val n = prefs.chargepriceMyTariffs?.size ?: 0
requireContext().resources
.getQuantityString(R.plurals.chargeprice_some_tariffs_selected, n, n)
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -51,6 +109,18 @@ class SettingsFragment : PreferenceFragmentCompat(),
"darkmode" -> {
updateNightMode(prefs)
}
"chargeprice_my_vehicle" -> {
vm.vehicles.value?.data?.let { cars ->
val vehicle = cars.find { it.id == prefs.chargepriceMyVehicle }
vehicle?.let {
myVehiclePreference.summary = "${it.brand} ${it.name}"
prefs.chargepriceMyVehicleDcChargeports = it.dcChargePorts
}
}
}
"chargeprice_my_tariffs" -> {
updateMyTariffsSummary()
}
}
}

View File

@@ -0,0 +1,42 @@
package net.vonforst.evmap.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import net.vonforst.evmap.databinding.DialogWelcomeBinding
import net.vonforst.evmap.storage.PreferenceDataSource
class WelcomeDialogFragment : AppCompatDialogFragment() {
private lateinit var binding: DialogWelcomeBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DialogWelcomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnOk.setOnClickListener {
val prefs = PreferenceDataSource(requireContext())
prefs.welcomeDialogShown = true
prefs.update060AndroidAutoDialogShown = true
dismiss()
}
}
override fun onStart() {
super.onStart()
dialog?.window?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT
)
}
}

View File

@@ -0,0 +1,40 @@
package net.vonforst.evmap.fragment.updatedialogs
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import net.vonforst.evmap.databinding.DialogUpdate060AndroidautoBinding
import net.vonforst.evmap.storage.PreferenceDataSource
class Update060AndroidAutoDialogFramgent : AppCompatDialogFragment() {
private lateinit var binding: DialogUpdate060AndroidautoBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DialogUpdate060AndroidautoBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnOk.setOnClickListener {
PreferenceDataSource(requireContext()).update060AndroidAutoDialogShown = true
dismiss()
}
}
override fun onStart() {
super.onStart()
dialog?.window?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT
)
}
}

View File

@@ -14,4 +14,7 @@ interface ChargeLocationsDao {
@Query("SELECT * FROM chargelocation")
fun getAllChargeLocations(): LiveData<List<ChargeLocation>>
@Query("SELECT * FROM chargelocation")
suspend fun getAllChargeLocationsAsync(): List<ChargeLocation>
}

View File

@@ -24,7 +24,7 @@ import net.vonforst.evmap.viewmodel.SliderFilterValue
Plug::class,
Network::class,
ChargeCard::class
], version = 10
], version = 11
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
@@ -41,7 +41,7 @@ abstract class AppDatabase : RoomDatabase() {
Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db")
.addMigrations(
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
@@ -170,5 +170,11 @@ abstract class AppDatabase : RoomDatabase() {
db.execSQL("ALTER TABLE `FilterProfile` ADD `order` INTEGER NOT NULL DEFAULT 0")
}
}
private val MIGRATION_11 = object : Migration(10, 11) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `ChargeLocation` ADD `barrierFree` INTEGER")
}
}
}
}

View File

@@ -2,6 +2,7 @@ package net.vonforst.evmap.storage
import android.content.Context
import androidx.preference.PreferenceManager
import com.car2go.maps.AnyMap
import net.vonforst.evmap.R
import net.vonforst.evmap.viewmodel.FILTERS_CUSTOM
import net.vonforst.evmap.viewmodel.FILTERS_DISABLED
@@ -72,4 +73,82 @@ class PreferenceDataSource(val context: Context) {
"map_provider",
context.getString(R.string.pref_map_provider_default)
)!!
var mapType: AnyMap.Type
get() = AnyMap.Type.valueOf(sp.getString("map_type", null) ?: AnyMap.Type.NORMAL.toString())
set(type) {
sp.edit().putString("map_type", type.toString()).apply()
}
var mapTrafficEnabled: Boolean
get() = sp.getBoolean("map_traffic_enabled", false)
set(value) {
sp.edit().putBoolean("map_traffic_enabled", value).apply()
}
var welcomeDialogShown: Boolean
get() = sp.getBoolean("welcome_dialog_shown", false)
set(value) {
sp.edit().putBoolean("welcome_dialog_shown", value).apply()
}
var update060AndroidAutoDialogShown: Boolean
get() = sp.getBoolean("update_0.6.0_androidauto_dialog_shown", false)
set(value) {
sp.edit().putBoolean("update_0.6.0_androidauto_dialog_shown", value).apply()
}
var chargepriceMyVehicle: String?
get() = sp.getString("chargeprice_my_vehicle", null)
set(value) {
sp.edit().putString("chargeprice_my_vehicle", value).apply()
}
var chargepriceMyVehicleDcChargeports: List<String>?
get() = sp.getString("chargeprice_my_vehicle_dc_chargeports", null)?.split(",")
set(value) {
sp.edit().putString("chargeprice_my_vehicle_dc_chargeports", value?.joinToString(","))
.apply()
}
var chargepriceMyTariffs: Set<String>?
get() = sp.getStringSet("chargeprice_my_tariffs", null)
set(value) {
sp.edit().putStringSet("chargeprice_my_tariffs", value).apply()
}
var chargepriceMyTariffsAll: Boolean
get() = sp.getBoolean("chargeprice_my_tariffs_all", true)
set(value) {
sp.edit().putBoolean("chargeprice_my_tariffs_all", value).apply()
}
var chargepriceNoBaseFee: Boolean
get() = sp.getBoolean("chargeprice_no_base_fee", false)
set(value) {
sp.edit().putBoolean("chargeprice_no_base_fee", value).apply()
}
var chargepriceShowProviderCustomerTariffs: Boolean
get() = sp.getBoolean("chargeprice_show_provider_customer_tariffs", false)
set(value) {
sp.edit().putBoolean("chargeprice_show_provider_customer_tariffs", value).apply()
}
var chargepriceCurrency: String
get() = sp.getString("chargeprice_currency", null) ?: "EUR"
set(value) {
sp.edit().putString("chargeprice_currency", value).apply()
}
var chargepriceBatteryRange: List<Float>
get() = listOf(
sp.getFloat("chargeprice_battery_range_min", 20f),
sp.getFloat("chargeprice_battery_range_max", 80f),
)
set(value) {
sp.edit().putFloat("chargeprice_battery_range_min", value[0])
.putFloat("chargeprice_battery_range_max", value[1])
.apply()
}
}

View File

@@ -0,0 +1,33 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.text.Layout
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import kotlin.math.ceil
class BalancedBreakingTextView(context: Context, attrs: AttributeSet) :
AppCompatTextView(context, attrs) {
@Override
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (layout != null) {
val width =
ceil(getMaxLineWidth(layout)).toInt() + compoundPaddingLeft + compoundPaddingRight
val height = measuredHeight
setMeasuredDimension(width, height)
}
}
private fun getMaxLineWidth(layout: Layout): Float {
var maxWidth = 0.0f
for (i in 0 until layout.lineCount) {
if (layout.getLineWidth(i) > maxWidth) {
maxWidth = layout.getLineWidth(i)
}
}
return maxWidth
}
}

View File

@@ -2,21 +2,26 @@ package net.vonforst.evmap.ui
import android.content.Context
import android.content.res.ColorStateList
import android.text.SpannableString
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.core.content.res.use
import androidx.core.text.HtmlCompat
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.slider.RangeSlider
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.api.iconForPlugType
import kotlin.math.roundToInt
@@ -25,11 +30,49 @@ fun goneUnless(view: View, visible: Boolean) {
view.visibility = if (visible) View.VISIBLE else View.GONE
}
@BindingAdapter("goneUnlessAnimated")
fun goneUnlessAnimated(view: View, oldValue: Boolean, newValue: Boolean) {
if (oldValue == newValue) return
view.animate().cancel()
if (newValue) {
view.visibility = View.VISIBLE
view.alpha = 0f
view.animate().alpha(1f).withEndAction {
view.alpha = 1f
}
} else {
view.animate().alpha(0f).withEndAction {
view.alpha = 1f
view.visibility = View.GONE
}
}
}
@BindingAdapter("invisibleUnless")
fun invisibleUnless(view: View, visible: Boolean) {
view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
}
@BindingAdapter("invisibleUnlessAnimated")
fun invisibleUnlessAnimated(view: View, oldValue: Boolean, newValue: Boolean) {
if (oldValue == newValue) return
view.animate().cancel()
if (newValue) {
view.visibility = View.VISIBLE
view.alpha = 0f
view.animate().alpha(1f).withEndAction {
view.alpha = 1f
}
} else {
view.animate().alpha(0f).withEndAction {
view.alpha = 1f
view.visibility = View.INVISIBLE
}
}
}
@BindingAdapter("isFabActive")
fun isFabActive(view: FloatingActionButton, isColored: Boolean) {
val color = view.context.theme.obtainStyledAttributes(
@@ -60,20 +103,7 @@ fun <T> setRecyclerViewData(recyclerView: ViewPager2, items: List<T>?) {
@BindingAdapter("connectorIcon")
fun getConnectorItem(view: ImageView, type: String) {
view.setImageResource(
when (type) {
Chargepoint.CCS -> R.drawable.ic_connector_ccs
Chargepoint.CHADEMO -> R.drawable.ic_connector_chademo
Chargepoint.SCHUKO -> R.drawable.ic_connector_schuko
Chargepoint.SUPERCHARGER -> R.drawable.ic_connector_supercharger
Chargepoint.TYPE_2 -> R.drawable.ic_connector_typ2
Chargepoint.CEE_BLAU -> R.drawable.ic_connector_cee_blau
Chargepoint.CEE_ROT -> R.drawable.ic_connector_cee_rot
Chargepoint.TYPE_1 -> R.drawable.ic_connector_typ1
// TODO: add other connectors
else -> 0
}
)
view.setImageResource(iconForPlugType(type))
}
@BindingAdapter("srcCompat")
@@ -131,19 +161,71 @@ fun setTopMargin(view: View, topMargin: Float) {
view.layoutParams = layoutParams
}
/**
* Linkify is already possible using the autoLink and linksClickable attributes, but this does not
* remove spans correctly. So we implement a new version that manually removes the spans.
*/
@BindingAdapter("linkify")
fun setLinkify(textView: TextView, oldValue: Int, newValue: Int) {
if (oldValue == newValue) return
textView.autoLinkMask = newValue
textView.linksClickable = newValue != 0
// remove spans
val text = textView.text
if (newValue == 0 && text != null && text is SpannableString) {
text.getSpans(0, text.length, Any::class.java).forEach {
text.removeSpan(it)
}
}
}
@BindingAdapter("chargepriceTagColor")
fun setChargepriceTagColor(view: TextView, kind: String) {
view.backgroundTintList = ColorStateList.valueOf(
ContextCompat.getColor(
view.context,
when (kind) {
"star" -> R.color.chargeprice_star
"alert" -> R.color.chargeprice_alert
"info" -> R.color.chargeprice_info
"lock" -> R.color.chargeprice_lock
else -> R.color.chip_background
}
)
)
}
@BindingAdapter("chargepriceTagIcon")
fun setChargepriceTagIcon(view: TextView, kind: String) {
view.setCompoundDrawablesRelativeWithIntrinsicBounds(
when (kind) {
"star" -> R.drawable.ic_chargeprice_star
"alert" -> R.drawable.ic_chargeprice_alert
"info" -> R.drawable.ic_chargeprice_info
"lock" -> R.drawable.ic_chargeprice_lock
else -> 0
}, 0, 0, 0
)
}
private fun availabilityColor(
status: List<ChargepointStatus>?,
context: Context
): Int = if (status != null) {
val unknown = status.any { it == ChargepointStatus.UNKNOWN }
val available = status.count { it == ChargepointStatus.AVAILABLE }
val allFaulted = status.all { it == ChargepointStatus.FAULTED }
if (unknown) {
ContextCompat.getColor(context, R.color.unknown)
} else if (available > 0) {
ContextCompat.getColor(context, R.color.available)
} else {
} else if (allFaulted) {
ContextCompat.getColor(context, R.color.unavailable)
} else {
ContextCompat.getColor(context, R.color.charging)
}
} else {
val ta = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorControlNormal))
@@ -160,4 +242,64 @@ fun availabilityText(status: List<ChargepointStatus>?): String? {
return if (unknown > 0) {
if (unknown == total) "?" else "$available?"
} else available.toString()
}
fun flatten(it: Iterable<Iterable<ChargepointStatus>>?): List<ChargepointStatus>? {
return it?.flatten()
}
fun currency(currency: String): String {
// shorthands for currencies
return when (currency) {
"EUR" -> ""
"USD" -> "$"
"DKK", "SEK", "NOK" -> "kr."
"PLN" -> ""
"CHF" -> "Fr. "
"CZK" -> ""
"GBP" -> "£"
"HRK" -> "kn"
"HUF" -> "Ft"
"ISK" -> "Kr"
else -> currency
}
}
@InverseBindingAdapter(attribute = "app:values")
fun getRangeSliderValue(slider: RangeSlider) = slider.values
@BindingAdapter("app:valuesAttrChanged")
fun setRangeSliderListeners(slider: RangeSlider, attrChange: InverseBindingListener) {
slider.addOnChangeListener { _, _, _ ->
attrChange.onChange()
}
}
@ColorInt
fun colorEnabled(ctx: Context, enabled: Boolean): Int {
val attr = if (enabled) {
android.R.attr.textColorSecondary
} else {
android.R.attr.textColorHint
}
val typedValue = ctx.obtainStyledAttributes(intArrayOf(attr))
val color = typedValue.getColor(0, 0)
typedValue.recycle()
return color
}
@BindingAdapter("app:tint")
fun setImageTintList(view: ImageView, @ColorInt color: Int) {
view.imageTintList = ColorStateList.valueOf(color)
}
@BindingAdapter("myTariffsBackground")
fun myTariffsBackground(view: View, myTariff: Boolean) {
if (myTariff) {
view.background = ContextCompat.getDrawable(view.context, R.drawable.my_tariff_background)
} else {
view.context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
view.background = it.getDrawable(0)
}
}
}

View File

@@ -0,0 +1,48 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.Checkable
import androidx.constraintlayout.widget.ConstraintLayout
class CheckableConstraintLayout(ctx: Context, attrs: AttributeSet) : ConstraintLayout(ctx, attrs),
Checkable {
private var onCheckedChangeListener: ((View, Boolean) -> Unit)? = null
private var checked = false
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
override fun setChecked(b: Boolean) {
if (b != checked) {
checked = b;
refreshDrawableState();
onCheckedChangeListener?.invoke(this, checked);
}
}
override fun isChecked(): Boolean {
return checked
}
override fun toggle() {
checked = !checked
}
override fun onCreateDrawableState(extraSpace: Int): IntArray? {
val drawableState = super.onCreateDrawableState(extraSpace + 1)
if (isChecked) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET)
}
return drawableState
}
/**
* Register a callback to be invoked when the checked state of this view changes.
*
* @param listener the callback to call on checked state change
*/
fun setOnCheckedChangeListener(listener: (View, Boolean) -> Unit) {
onCheckedChangeListener = listener
}
}

View File

@@ -0,0 +1,63 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.content.DialogInterface
import android.view.Gravity
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.FrameLayout
import androidx.appcompat.app.AlertDialog
private fun dialogEditText(ctx: Context): Pair<View, EditText> {
val container = FrameLayout(ctx)
container.setPadding(
(16 * ctx.resources.displayMetrics.density).toInt(), 0,
(16 * ctx.resources.displayMetrics.density).toInt(), 0
)
val input = EditText(ctx)
input.isSingleLine = true
container.addView(input)
return container to input
}
fun showEditTextDialog(
ctx: Context,
customize: (AlertDialog.Builder, EditText) -> Unit
): AlertDialog {
val (container, input) = dialogEditText(ctx)
val dialogBuilder = AlertDialog.Builder(ctx)
.setView(container)
customize(dialogBuilder, input)
val dialog = dialogBuilder.show()
// move dialog to top
val attrs = dialog.window?.attributes?.apply {
gravity = Gravity.TOP
}
dialog.window?.attributes = attrs
// focus and show keyboard
input.requestFocus()
input.postDelayed({
val imm =
ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT)
}, 100)
input.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
val text = input.text
val button = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
if (text != null && button != null) {
button.performClick()
return@setOnEditorActionListener true
}
}
false
}
return dialog
}

View File

@@ -16,6 +16,7 @@ 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
import kotlin.math.roundToInt
class ClusterIconGenerator(context: Context) : IconGenerator(context) {
init {
@@ -41,8 +42,14 @@ class ClusterIconGenerator(context: Context) : IconGenerator(context) {
}
class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFactory) {
data class BitmapData(
class ChargerIconGenerator(
val context: Context,
val factory: BitmapDescriptorFactory?,
val scaleResolution: Int = 20,
val oversize: Float = 1.4f, // increase to add padding for fault icon or scale > 1
val height: Int = 44
) {
private data class BitmapData(
val tint: Int,
val scale: Int,
val alpha: Int,
@@ -51,20 +58,16 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
val multi: Boolean
)
val cacheSize = 840; // 840 items: 21 sizes, 5 colors, highlight, fault, multi on/off
val cache = LruCache<BitmapData, BitmapDescriptor>(cacheSize)
val oversize = 1.4f // increase to add padding for fault icon or scale > 1
val icon = R.drawable.ic_map_marker_charging
val multiIcon = R.drawable.ic_map_marker_charging_multiple
val highlightIcon = R.drawable.ic_map_marker_highlight
val highlightIconMulti = R.drawable.ic_map_marker_charging_highlight_multiple
val faultIcon = R.drawable.ic_map_marker_fault
// 230 items: (21 sizes, 5 colors, multi on/off) + highlight + fault (only with scale = 1)
private val cacheSize = (scaleResolution + 3) * 5 * 2;
private val cache = LruCache<BitmapData, BitmapDescriptor>(cacheSize)
private val icon = R.drawable.ic_map_marker_charging
private val multiIcon = R.drawable.ic_map_marker_charging_multiple
private val highlightIcon = R.drawable.ic_map_marker_highlight
private val highlightIconMulti = R.drawable.ic_map_marker_charging_highlight_multiple
private val faultIcon = R.drawable.ic_map_marker_fault
init {
preloadCache()
}
private fun preloadCache() {
fun preloadCache() {
// pre-generates images for scale from 0 to 255 for all possible tint colors
val tints = listOf(
R.color.charger_100kw,
@@ -77,8 +80,11 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
for (highlight in listOf(false, true)) {
for (multi in listOf(false, true)) {
for (tint in tints) {
for (scale in 0..20) {
getBitmapDescriptor(tint, scale, 255, highlight, fault, multi)
for (scale in 0..scaleResolution) {
getBitmapDescriptor(
tint, scale.toFloat() / scaleResolution,
255, highlight, fault, multi
)
}
}
}
@@ -88,24 +94,48 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
fun getBitmapDescriptor(
@ColorRes tint: Int,
scale: Int = 20,
scale: Float = 1f,
alpha: Int = 255,
highlight: Boolean = false,
fault: Boolean = false,
multi: Boolean = false
): BitmapDescriptor? {
val data = BitmapData(tint, scale, alpha, highlight, fault, multi)
val data = BitmapData(
tint, (scale * scaleResolution).roundToInt(),
alpha,
if (scale == 1f) highlight else false,
if (scale == 1f) fault else false,
multi
)
val cachedImg = cache[data]
return if (cachedImg != null) {
cachedImg
} else {
val bitmap = generateBitmap(data)
val bmd = factory.fromBitmap(bitmap)
val bmd = factory!!.fromBitmap(bitmap)
cache.put(data, bmd)
bmd
}
}
fun getBitmap(
@ColorRes tint: Int,
scale: Float = 1f,
alpha: Int = 255,
highlight: Boolean = false,
fault: Boolean = false,
multi: Boolean = false
): Bitmap {
val data = BitmapData(
tint, (scale * scaleResolution).roundToInt(),
alpha,
if (scale == 1f) highlight else false,
if (scale == 1f) fault else false,
multi
)
return generateBitmap(data)
}
private fun generateBitmap(data: BitmapData): Bitmap {
val icon = if (data.multi) multiIcon else icon
val vd: Drawable = ContextCompat.getDrawable(context, icon)!!
@@ -113,27 +143,32 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
DrawableCompat.setTint(vd, ContextCompat.getColor(context, data.tint));
DrawableCompat.setTintMode(vd, PorterDuff.Mode.MULTIPLY);
val leftPadding = vd.intrinsicWidth * (oversize - 1) / 2
val topPadding = vd.intrinsicHeight * (oversize - 1)
val density = context.resources.displayMetrics.density
val width =
(height.toFloat() * density / vd.intrinsicHeight * vd.intrinsicWidth).roundToInt()
val height = (height * density).roundToInt()
val leftPadding = width * (oversize - 1) / 2
val topPadding = height * (oversize - 1)
vd.setBounds(
leftPadding.toInt(), topPadding.toInt(),
leftPadding.toInt() + vd.intrinsicWidth,
topPadding.toInt() + vd.intrinsicHeight
leftPadding.toInt() + width,
topPadding.toInt() + height
)
vd.alpha = data.alpha
val bm = Bitmap.createBitmap(
(vd.intrinsicWidth * oversize).toInt(), (vd.intrinsicHeight * oversize).toInt(),
(width * oversize).toInt(), (height * oversize).toInt(),
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bm)
val scale = data.scale / 20f
val scale = data.scale.toFloat() / scaleResolution
canvas.scale(
scale,
scale,
leftPadding + vd.intrinsicWidth / 2f,
topPadding + vd.intrinsicHeight.toFloat()
leftPadding + width / 2f,
topPadding + height.toFloat()
)
vd.draw(canvas)
@@ -143,8 +178,8 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
val highlightDrawable = ContextCompat.getDrawable(context, hIcon)!!
highlightDrawable.setBounds(
leftPadding.toInt(), topPadding.toInt(),
leftPadding.toInt() + vd.intrinsicWidth,
topPadding.toInt() + vd.intrinsicHeight
leftPadding.toInt() + width,
topPadding.toInt() + height
)
highlightDrawable.alpha = data.alpha
highlightDrawable.draw(canvas)
@@ -154,7 +189,7 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
val faultDrawable = ContextCompat.getDrawable(context, faultIcon)!!
val faultSize = 0.75
val faultShift = 0.25
val base = vd.intrinsicWidth
val base = width
faultDrawable.setBounds(
(leftPadding.toInt() + base * (1 - faultSize + faultShift)).toInt(),
(topPadding.toInt() - base * faultShift).toInt(),

View File

@@ -12,7 +12,7 @@ import kotlin.math.max
fun getMarkerTint(
charger: ChargeLocation,
connectors: Set<String>?
connectors: Set<String>? = null
): Int = when {
charger.maxPower(connectors) >= 100 -> R.color.charger_100kw
charger.maxPower(connectors) >= 43 -> R.color.charger_43kw
@@ -36,11 +36,11 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
animatingMarkers.remove(marker)
}
val anim = ValueAnimator.ofInt(0, 20).apply {
val anim = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 250
interpolator = LinearOutSlowInInterpolator()
addUpdateListener { animationState ->
val scale = animationState.animatedValue as Int
val scale = animationState.animatedValue as Float
marker.setIcon(
gen.getBitmapDescriptor(
tint,
@@ -73,11 +73,11 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
animatingMarkers.remove(marker)
}
val anim = ValueAnimator.ofInt(20, 0).apply {
val anim = ValueAnimator.ofFloat(1f, 0f).apply {
duration = 200
interpolator = FastOutLinearInInterpolator()
addUpdateListener { animationState ->
val scale = animationState.animatedValue as Int
val scale = animationState.animatedValue as Float
marker.setIcon(
gen.getBitmapDescriptor(
tint,

View File

@@ -0,0 +1,31 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.MultiSelectListPreference
import net.vonforst.evmap.fragment.MultiSelectDialog
class MultiSelectDialogPreference(ctx: Context, attrs: AttributeSet) :
MultiSelectListPreference(ctx, attrs) {
override fun onClick() {
val dialog =
MultiSelectDialog.getInstance(
title.toString(),
entryValues.map { it.toString() }.zip(entries.map { it.toString() }).toMap(),
if (all) entryValues.map { it.toString() }.toSet() else values,
emptySet()
)
dialog.okListener = { selected ->
all = selected == entryValues.toSet()
values = selected
}
dialog.show((context as AppCompatActivity).supportFragmentManager, null)
}
var all: Boolean
get() = sharedPreferences.getBoolean(key + "_all", true)
set(value) {
sharedPreferences.edit().putBoolean(key + "_all", value).apply()
}
}

View File

@@ -0,0 +1,77 @@
package net.vonforst.evmap.utils
import android.content.Intent
import android.location.Location
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlin.math.*
/**
* Adds a certain distance in meters to a location. Approximate calculation.
*/
fun Location.plusMeters(dx: Double, dy: Double): Pair<Double, Double> {
val lat = this.latitude + (180 / Math.PI) * (dx / 6378137.0)
val lon = this.longitude + (180 / Math.PI) * (dy / 6378137.0) / cos(Math.toRadians(lat))
return Pair(lat, lon)
}
fun LatLng.plusMeters(dx: Double, dy: Double): LatLng {
val lat = this.latitude + (180 / Math.PI) * (dx / 6378137.0)
val lon = this.longitude + (180 / Math.PI) * (dy / 6378137.0) / cos(Math.toRadians(lat))
return LatLng(lat, lon)
}
const val earthRadiusM = 6378137.0
/**
* Calculates the distance between two points on Earth in meters.
* Latitude and longitude should be given in degrees.
*/
fun distanceBetween(
startLatitude: Double, startLongitude: Double,
endLatitude: Double, endLongitude: Double
): Double {
// see https://rosettacode.org/wiki/Haversine_formula#Java
val dLat = Math.toRadians(endLatitude - startLatitude)
val dLon = Math.toRadians(endLongitude - startLongitude)
val originLat = Math.toRadians(startLatitude)
val destinationLat = Math.toRadians(endLatitude)
val a = sin(dLat / 2).pow(2.0) + sin(dLon / 2).pow(2.0) * cos(originLat) * cos(destinationLat)
val c = 2 * asin(sqrt(a))
return earthRadiusM * c
}
fun getLocationFromIntent(intent: Intent): List<Double>? {
val pos = intent.data?.schemeSpecificPart?.split("?")?.get(0)
var coords = stringToCoords(pos)
if (coords != null) {
return coords
}
val query = intent.data?.query?.split("=")?.get(1)
coords = stringToCoords(query)
if (coords != null) {
return coords
} else {
return null
}
}
internal fun stringToCoords(s: String?): List<Double>? {
if (s == null) return null
val coords = s.split(",").mapNotNull { it.toDoubleOrNull() }
return if (coords.size == 2 && !(coords[0] == 0.0 && coords[1] == 0.0)) {
coords
} else {
null
}
}
fun boundingBox(pos: LatLng, sizeMeters: Double): LatLngBounds {
return LatLngBounds(
pos.plusMeters(-sizeMeters, -sizeMeters),
pos.plusMeters(sizeMeters, sizeMeters)
)
}

View File

@@ -0,0 +1,220 @@
package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import moe.banana.jsonapi2.HasOne
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
import java.io.IOException
import java.util.*
class ChargepriceViewModel(application: Application, chargepriceApiKey: String) :
AndroidViewModel(application) {
private var api = ChargepriceApi.create(chargepriceApiKey)
private var prefs = PreferenceDataSource(application)
val charger: MutableLiveData<ChargeLocation> by lazy {
MutableLiveData<ChargeLocation>()
}
val chargepoint: MutableLiveData<Chargepoint> by lazy {
MutableLiveData<Chargepoint>()
}
val vehicle: LiveData<ChargepriceCar> by lazy {
MutableLiveData<ChargepriceCar>().apply {
value = prefs.chargepriceMyVehicle?.let { ChargepriceCar().apply { id = it } }
}
}
private val acConnectors = listOf(
Chargepoint.CEE_BLAU,
Chargepoint.CEE_ROT,
Chargepoint.SCHUKO,
Chargepoint.TYPE_1,
Chargepoint.TYPE_2
)
private val plugMapping = mapOf(
"ccs" to Chargepoint.CCS,
"tesla_suc" to Chargepoint.SUPERCHARGER,
"tesla_ccs" to Chargepoint.CCS,
"chademo" to Chargepoint.CHADEMO
)
val vehicleCompatibleConnectors: LiveData<List<String>> by lazy {
MutableLiveData<List<String>>().apply {
value = prefs.chargepriceMyVehicleDcChargeports?.map {
plugMapping.get(it)
}?.filterNotNull()?.plus(acConnectors)
}
}
val noCompatibleConnectors: LiveData<Boolean> by lazy {
MediatorLiveData<Boolean>().apply {
value = false
listOf(charger, vehicleCompatibleConnectors).forEach {
addSource(it) {
val charger = charger.value ?: return@addSource
val connectors = vehicleCompatibleConnectors.value ?: return@addSource
value = !charger.chargepoints.map { it.type }.any { it in connectors }
}
}
}
}
val batteryRange: MutableLiveData<List<Float>> by lazy {
MutableLiveData<List<Float>>().apply {
value = prefs.chargepriceBatteryRange
observeForever {
prefs.chargepriceBatteryRange = it
}
}
}
val batteryRangeSliderDragging: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply {
value = false
}
}
val chargePrices: MediatorLiveData<Resource<List<ChargePrice>>> by lazy {
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
value = Resource.loading(null)
listOf(
charger,
vehicle,
batteryRange,
batteryRangeSliderDragging,
vehicleCompatibleConnectors
).forEach {
addSource(it) {
if (!batteryRangeSliderDragging.value!!) loadPrices()
}
}
}
}
val chargePriceMeta: MutableLiveData<Resource<ChargepriceMeta>> by lazy {
MutableLiveData<Resource<ChargepriceMeta>>().apply {
value = Resource.loading(null)
}
}
val chargePricesForChargepoint: MediatorLiveData<Resource<List<ChargePrice>>> by lazy {
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
listOf(chargePrices, chargepoint).forEach {
addSource(it) {
val cps = chargePrices.value
val chargepoint = chargepoint.value
if (cps == null || chargepoint == null) {
value = null
} else if (cps.status == Status.ERROR) {
value = Resource.error(cps.message, null)
} else if (cps.status == Status.LOADING) {
value = Resource.loading(null)
} else {
val myTariffs = prefs.chargepriceMyTariffs
value = Resource.success(cps.data!!.map { cp ->
val filteredPrices =
cp.chargepointPrices.filter { it.plug == chargepoint.type && it.power == chargepoint.power }
if (filteredPrices.isEmpty()) {
null
} else {
cp.clone().apply {
chargepointPrices = filteredPrices
}
}
}.filterNotNull()
.sortedBy { it.chargepointPrices.first().price }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
myTariffs != null && it.tariff?.get()?.id in myTariffs
}
)
}
}
}
}
}
val myTariffs: LiveData<Set<String>> by lazy {
MutableLiveData<Set<String>>().apply {
value = prefs.chargepriceMyTariffs
}
}
val myTariffsAll: LiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply {
value = prefs.chargepriceMyTariffsAll
}
}
val chargepriceMetaForChargepoint: MediatorLiveData<Resource<ChargepriceChargepointMeta>> by lazy {
MediatorLiveData<Resource<ChargepriceChargepointMeta>>().apply {
listOf(chargePriceMeta, chargepoint).forEach {
addSource(it) {
val cpMeta = chargePriceMeta.value
val chargepoint = chargepoint.value
if (cpMeta == null || chargepoint == null) {
value = null
} else if (cpMeta.status == Status.ERROR) {
value = Resource.error(cpMeta.message, null)
} else if (cpMeta.status == Status.LOADING) {
value = Resource.loading(null)
} else {
value =
Resource.success(cpMeta.data!!.chargePoints.filter { it.plug == chargepoint.type && it.power == chargepoint.power }[0])
}
}
}
}
}
private var loadPricesJob: Job? = null
fun loadPrices() {
chargePrices.value = Resource.loading(null)
val geCharger = charger.value
val car = vehicle.value
val compatibleConnectors = vehicleCompatibleConnectors.value
if (geCharger == null || car == null || compatibleConnectors == null) {
chargePrices.value = Resource.error(null, null)
return
}
val cpStation = ChargepriceStation.fromGoingelectric(geCharger, compatibleConnectors)
loadPricesJob?.cancel()
loadPricesJob = viewModelScope.launch {
try {
val result = api.getChargePrices(ChargepriceRequest().apply {
dataAdapter = "going_electric"
station = cpStation
vehicle = HasOne(car)
options = ChargepriceOptions(
batteryRange = batteryRange.value!!.map { it.toDouble() },
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency
)
}, getChargepriceLanguage())
val meta =
result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta
chargePrices.value = Resource.success(result)
chargePriceMeta.value = Resource.success(meta)
} catch (e: IOException) {
chargePrices.value = Resource.error(e.message, null)
chargePriceMeta.value = Resource.error(e.message, null)
}
}
}
private fun getChargepriceLanguage(): String {
val locale = Locale.getDefault().language
return if (ChargepriceApi.supportedLanguages.contains(locale)) {
locale
} else {
"en"
}
}
}

View File

@@ -10,10 +10,10 @@ import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.distanceBetween
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.utils.distanceBetween
class FavoritesViewModel(application: Application, geApiKey: String) :
AndroidViewModel(application) {

View File

@@ -27,6 +27,18 @@ class FilterProfilesViewModel(application: Application) : AndroidViewModel(appli
}
}
fun insert(item: FilterProfile) {
viewModelScope.launch {
db.filterProfileDao().insert(item)
}
}
fun update(item: FilterProfile) {
viewModelScope.launch {
db.filterProfileDao().update(item)
}
}
fun reorderProfiles(list: List<FilterProfile>) {
viewModelScope.launch {
db.filterProfileDao().update(*list.toTypedArray())

View File

@@ -12,6 +12,7 @@ import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.api.goingelectric.ChargeCard
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.storage.*
import kotlin.math.abs
import kotlin.reflect.KClass
@@ -30,21 +31,9 @@ internal fun getFilters(
chargeCards: LiveData<List<ChargeCard>>
): LiveData<List<Filter<FilterValue>>> {
return MediatorLiveData<List<Filter<FilterValue>>>().apply {
val plugNames = mapOf(
Chargepoint.TYPE_1 to application.getString(R.string.plug_type_1),
Chargepoint.TYPE_2 to application.getString(R.string.plug_type_2),
Chargepoint.TYPE_3 to application.getString(R.string.plug_type_3),
Chargepoint.CCS to application.getString(R.string.plug_ccs),
Chargepoint.SCHUKO to application.getString(R.string.plug_schuko),
Chargepoint.CHADEMO to application.getString(R.string.plug_chademo),
Chargepoint.SUPERCHARGER to application.getString(R.string.plug_supercharger),
Chargepoint.CEE_BLAU to application.getString(R.string.plug_cee_blau),
Chargepoint.CEE_ROT to application.getString(R.string.plug_cee_rot),
Chargepoint.TESLA_ROADSTER_HPC to application.getString(R.string.plug_roadster_hpc)
)
listOf(plugs, networks, chargeCards).forEach { source ->
addSource(source) { _ ->
buildFilters(plugs, plugNames, networks, chargeCards, application)
buildFilters(plugs, networks, chargeCards, application)
}
}
}
@@ -52,13 +41,12 @@ internal fun getFilters(
private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
plugs: LiveData<List<Plug>>,
plugNames: Map<String, String>,
networks: LiveData<List<Network>>,
chargeCards: LiveData<List<ChargeCard>>,
application: Application
) {
val plugMap = plugs.value?.map { plug ->
plug.name to (plugNames[plug.name] ?: plug.name)
plug.name to nameForPlugType(application, plug.name)
}?.toMap() ?: return
val networkMap = networks.value?.map { it.name to it.name }?.toMap() ?: return
val chargecardMap = chargeCards.value?.map { it.id.toString() to it.name }?.toMap() ?: return
@@ -135,8 +123,8 @@ private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
internal fun filtersWithValue(
filters: LiveData<List<Filter<FilterValue>>>,
filterValues: LiveData<List<FilterValue>>
): MediatorLiveData<List<FilterWithValue<out FilterValue>>> =
MediatorLiveData<List<FilterWithValue<out FilterValue>>>().apply {
): MediatorLiveData<FilterValues> =
MediatorLiveData<FilterValues>().apply {
listOf(filters, filterValues).forEach {
addSource(it) {
val f = filters.value ?: return@addSource
@@ -173,7 +161,7 @@ class FilterViewModel(application: Application, geApiKey: String) :
db.filterValueDao().getFilterValues(FILTERS_CUSTOM)
}
val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
val filtersWithValue: LiveData<FilterValues> by lazy {
filtersWithValue(filters, filterValues)
}
@@ -331,5 +319,19 @@ data class SliderFilterValue(
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable
typealias FilterValues = List<FilterWithValue<out FilterValue>>
fun FilterValues.getBooleanValue(key: String) =
(this.find { it.value.key == key }!!.value as BooleanFilterValue).value
fun FilterValues.getSliderValue(key: String) =
(this.find { it.value.key == key }!!.value as SliderFilterValue).value
fun FilterValues.getMultipleChoiceFilter(key: String) =
this.find { it.value.key == key }!!.filter as MultipleChoiceFilter
fun FilterValues.getMultipleChoiceValue(key: String) =
this.find { it.value.key == key }!!.value as MultipleChoiceFilterValue
const val FILTERS_DISABLED = -2L
const val FILTERS_CUSTOM = -1L

View File

@@ -6,17 +6,16 @@ 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
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.distanceBetween
import net.vonforst.evmap.api.goingelectric.*
import net.vonforst.evmap.api.goingelectric.ChargeCard
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.storage.*
import net.vonforst.evmap.ui.cluster
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import net.vonforst.evmap.utils.distanceBetween
import java.io.IOException
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
@@ -37,7 +36,6 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
private var api = GoingElectricApi.create(geApiKey, context = application)
private var db = AppDatabase.getInstance(application)
private var prefs = PreferenceDataSource(application)
private var chargepointLoader: Job? = null
val bottomSheetState: MutableLiveData<Int> by lazy {
MutableLiveData<Int>()
@@ -69,7 +67,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
private val filters = getFilters(application, plugs, networks, chargeCards)
private val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
private val filtersWithValue: LiveData<FilterValues> by lazy {
filtersWithValue(filters, filterValues)
}
@@ -147,7 +145,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
val callback = { _: Any? ->
val loc = location.value
val charger = chargerSparse.value
value = if (loc != null && charger != null && myLocationEnabled.value == true) {
value = if (loc != null && charger != null) {
distanceBetween(
loc.latitude,
loc.longitude,
@@ -158,7 +156,6 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
addSource(chargerSparse, callback)
addSource(location, callback)
addSource(myLocationEnabled, callback)
}
}
val location: MutableLiveData<LatLng> by lazy {
@@ -177,6 +174,21 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
}
}
val filteredAvailability: MediatorLiveData<Resource<ChargeLocationStatus>> by lazy {
MediatorLiveData<Resource<ChargeLocationStatus>>().apply {
val callback = { _: Any? ->
val av = availability.value
val filters = filtersWithValue.value
if (av?.status == Status.SUCCESS && filters != null) {
value = Resource.success(av.data!!.applyFilters(filters))
} else {
value = av
}
}
addSource(availability, callback)
addSource(filtersWithValue, callback)
}
}
val myLocationEnabled: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>()
}
@@ -196,13 +208,19 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
val mapType: MutableLiveData<AnyMap.Type> by lazy {
MutableLiveData<AnyMap.Type>().apply {
value = AnyMap.Type.NORMAL
value = prefs.mapType
observeForever {
prefs.mapType = it
}
}
}
val mapTrafficEnabled: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply {
value = false
value = prefs.mapTrafficEnabled
observeForever {
prefs.mapTrafficEnabled = it
}
}
}
@@ -260,39 +278,41 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
loadChargepoints(pos, filters)
}
private fun loadChargepoints(
mapPosition: MapPosition,
filters: List<FilterWithValue<out FilterValue>>
) {
chargepointLoader?.cancel()
private var chargepointLoader =
throttleLatest(500L, viewModelScope) { data: Pair<MapPosition, FilterValues> ->
chargepoints.value = Resource.loading(chargepoints.value?.data)
filteredConnectors.value = null
filteredChargeCards.value = null
chargepoints.value = Resource.loading(chargepoints.value?.data)
filteredConnectors.value = null
filteredChargeCards.value = null
val bounds = mapPosition.bounds
val zoom = mapPosition.zoom
chargepointLoader = viewModelScope.launch {
val result = getChargepointsWithFilters(bounds, zoom, filters)
val mapPosition = data.first
val filters = data.second
val result = getChargepointsWithFilters(mapPosition.bounds, mapPosition.zoom, filters)
filteredConnectors.value = result.second
filteredChargeCards.value = result.third
chargepoints.value = result.first
}
private fun loadChargepoints(
mapPosition: MapPosition,
filters: FilterValues
) {
chargepointLoader(Pair(mapPosition, filters))
}
private suspend fun getChargepointsWithFilters(
bounds: LatLngBounds,
zoom: Float,
filters: List<FilterWithValue<out FilterValue>>
filters: FilterValues
): Triple<Resource<List<ChargepointListItem>>, Set<String>?, Set<Long>?> {
val freecharging = getBooleanValue(filters, "freecharging")
val freeparking = getBooleanValue(filters, "freeparking")
val open247 = getBooleanValue(filters, "open_247")
val barrierfree = getBooleanValue(filters, "barrierfree")
val excludeFaults = getBooleanValue(filters, "exclude_faults")
val minPower = getSliderValue(filters, "min_power")
val minConnectors = getSliderValue(filters, "min_connectors")
val freecharging = filters.getBooleanValue("freecharging")
val freeparking = filters.getBooleanValue("freeparking")
val open247 = filters.getBooleanValue("open_247")
val barrierfree = filters.getBooleanValue("barrierfree")
val excludeFaults = filters.getBooleanValue("exclude_faults")
val minPower = filters.getSliderValue("min_power")
val minConnectors = filters.getSliderValue("min_connectors")
val connectorsVal = getMultipleChoiceValue(filters, "connectors")
val connectorsVal = filters.getMultipleChoiceValue("connectors")
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
// no connectors chosen
return Triple(Resource.success(emptyList()), null, null)
@@ -300,7 +320,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
val connectors = formatMultipleChoice(connectorsVal)
val filteredConnectors = if (connectorsVal.all) null else connectorsVal.values
val chargeCardsVal = getMultipleChoiceValue(filters, "chargecards")
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")
if (chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) {
// no chargeCards chosen
return Triple(Resource.success(emptyList()), filteredConnectors, null)
@@ -309,14 +329,14 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
val filteredChargeCards =
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }.toSet()
val networksVal = getMultipleChoiceValue(filters, "networks")
val networksVal = filters.getMultipleChoiceValue("networks")
if (networksVal.values.isEmpty() && !networksVal.all) {
// no networks chosen
return Triple(Resource.success(emptyList()), filteredConnectors, filteredChargeCards)
}
val networks = formatMultipleChoice(networksVal)
val categoriesVal = getMultipleChoiceValue(filters, "categories")
val categoriesVal = filters.getMultipleChoiceValue("categories")
if (categoriesVal.values.isEmpty() && !categoriesVal.all) {
// no categories chosen
return Triple(Resource.success(emptyList()), filteredConnectors, filteredChargeCards)
@@ -398,26 +418,6 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
private fun formatMultipleChoice(connectorsVal: MultipleChoiceFilterValue) =
if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
private fun getBooleanValue(
filters: List<FilterWithValue<out FilterValue>>,
key: String
) = (filters.find { it.value.key == key }!!.value as BooleanFilterValue).value
private fun getSliderValue(
filters: List<FilterWithValue<out FilterValue>>,
key: String
) = (filters.find { it.value.key == key }!!.value as SliderFilterValue).value
private fun getMultipleChoiceFilter(
filters: List<FilterWithValue<out FilterValue>>,
key: String
) = filters.find { it.value.key == key }!!.filter as MultipleChoiceFilter
private fun getMultipleChoiceValue(
filters: List<FilterWithValue<out FilterValue>>,
key: String
) = filters.find { it.value.key == key }!!.value as MultipleChoiceFilterValue
private suspend fun loadAvailability(charger: ChargeLocation) {
availability.value = Resource.loading(null)
availability.value = getAvailability(charger)
@@ -425,24 +425,41 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
private fun loadChargerDetails(charger: ChargeLocation) {
chargerDetails.value = Resource.loading(null)
api.getChargepointDetail(charger.id).enqueue(object :
Callback<ChargepointList> {
override fun onFailure(call: Call<ChargepointList>, t: Throwable) {
chargerDetails.value = Resource.error(t.message, null)
t.printStackTrace()
}
override fun onResponse(
call: Call<ChargepointList>,
response: Response<ChargepointList>
) {
viewModelScope.launch {
try {
val response = api.getChargepointDetail(charger.id)
if (!response.isSuccessful || response.body()!!.status != "ok") {
chargerDetails.value = Resource.error(response.message(), null)
} else {
chargerDetails.value =
Resource.success(response.body()!!.chargelocations[0] as ChargeLocation)
}
} catch (e: IOException) {
chargerDetails.value = Resource.error(e.message, null)
e.printStackTrace()
}
})
}
}
fun loadChargerById(chargerId: Long) {
chargerDetails.value = Resource.loading(null)
chargerSparse.value = null
viewModelScope.launch {
val response = api.getChargepointDetail(chargerId)
if (!response.isSuccessful || response.body()!!.status != "ok") {
chargerSparse.value = null
chargerDetails.value = Resource.error(response.message(), null)
} else {
val chargers = response.body()!!.chargelocations
if (chargers.isNotEmpty()) {
val charger = chargers[0] as ChargeLocation
chargerDetails.value =
Resource.success(charger)
chargerSparse.value = charger} else {
chargerDetails.value = Resource.error("not found", null)
chargerSparse.value = null
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
import java.io.IOException
class SettingsViewModel(application: Application, chargepriceApiKey: String) :
AndroidViewModel(application) {
private var api = ChargepriceApi.create(chargepriceApiKey)
val vehicles: MutableLiveData<Resource<List<ChargepriceCar>>> by lazy {
MutableLiveData<Resource<List<ChargepriceCar>>>().apply {
value = Resource.loading(null)
loadVehicles()
}
}
val tariffs: MutableLiveData<Resource<List<ChargepriceTariff>>> by lazy {
MutableLiveData<Resource<List<ChargepriceTariff>>>().apply {
value = Resource.loading(null)
loadTariffs()
}
}
private fun loadVehicles() {
viewModelScope.launch {
try {
val result = api.getVehicles()
vehicles.value = Resource.success(result)
} catch (e: IOException) {
vehicles.value = Resource.error(e.message, null)
}
}
}
private fun loadTariffs() {
viewModelScope.launch {
try {
val result = api.getTariffs()
tariffs.value = Resource.success(result)
} catch (e: IOException) {
tariffs.value = Resource.error(e.message, null)
}
}
}
}

View File

@@ -3,6 +3,10 @@ package net.vonforst.evmap.viewmodel
import androidx.annotation.MainThread
import androidx.annotation.Nullable
import androidx.lifecycle.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
@@ -63,4 +67,27 @@ class SingleLiveEvent<T> : MutableLiveData<T>() {
fun call() {
value = null
}
}
fun <T> throttleLatest(
skipMs: Long = 300L,
coroutineScope: CoroutineScope,
destinationFunction: suspend (T) -> Unit
): (T) -> Unit {
var throttleJob: Job? = null
var waitingParam: T? = null
return { param: T ->
if (throttleJob?.isCompleted != false) {
throttleJob = coroutineScope.launch {
destinationFunction(param)
delay(skipMs)
waitingParam?.let { wParam ->
waitingParam = null
destinationFunction(wParam)
}
}
} else {
waitingParam = param
}
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="300"
android:fromXScale="0.7"
android:fromYScale="0.2"
android:toXScale="1.0"
android:toYScale="1.0"
android:pivotX="50%"
android:pivotY="50%"
android:interpolator="@android:anim/decelerate_interpolator" />
<alpha
android:duration="250"
android:fromAlpha="0.1"
android:toAlpha="1.0"
android:interpolator="@android:anim/decelerate_interpolator" />
</set>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="250"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:toXScale="0.7"
android:toYScale="0.2"
android:pivotX="50%"
android:pivotY="50%"
android:interpolator="@android:anim/accelerate_interpolator" />
<alpha
android:duration="250"
android:fromAlpha="1.0"
android:toAlpha="0.1"
android:interpolator="@android:anim/accelerate_interpolator" />
</set>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View File

@@ -0,0 +1,42 @@
<vector android:height="45.9264dp"
android:viewportHeight="480"
android:viewportWidth="501.334"
android:width="48dp"
xmlns:aapt="http://schemas.android.com/aapt"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#03a9f4"
android:fillType="evenOdd"
android:pathData="m32,416c-17.68,0 -32,-14.32 -32,-32 0,-5.814 1.547,-11.28 4.267,-15.974l202.667,-352c5.52,-9.573 15.866,-16.026 27.733,-16.026s22.213,6.453 27.733,16.026l202.667,352c2.72,4.694 4.267,10.16 4.267,15.974 0,17.68 -14.32,32 -32,32z" />
<path
android:fillType="evenOdd"
android:pathData="m234.667,149.333v266.667h266.667z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="459.19528"
android:endY="410.17865"
android:startX="177.41093"
android:startY="250.14912"
android:type="linear">
<item
android:color="#331A237E"
android:offset="0" />
<item
android:color="#051A237E"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#039be5"
android:fillType="evenOdd"
android:pathData="m206.934,16.026 l-202.667,352c-2.72,4.694 -4.267,10.16 -4.267,15.974 0,17.68 14.32,32 32,32h202.667v-416c-11.867,0 -22.213,6.453 -27.733,16.026z" />
<path
android:fillColor="#f1f1f1"
android:fillType="evenOdd"
android:pathData="m234.667,149.333 l181.333,320 -10.666,10.667 -170.667,-64 -170.667,64 -10.666,-10.667z" />
<path
android:fillColor="#e1e1e1"
android:fillType="evenOdd"
android:pathData="m234.667,149.333 l181.333,320 -10.666,10.667 -170.667,-64" />
</vector>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<shape android:shape="rectangle">
<stroke android:color="?attr/colorPrimary" android:width="1dp" />
<corners android:radius="4dp" />
</shape>
</item>
<item android:state_checked="false">
<shape android:shape="rectangle">
<stroke android:color="#1F000000" android:width="1dp" />
<corners android:radius="4dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="#66FFFFFF" />
<size
android:height="24dp"
android:width="24dp" />
</shape>
</item>
<item android:drawable="?attr/controlBackground" />
</layer-list>

View File

@@ -0,0 +1,10 @@
<vector android:height="20dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="20dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector android:height="20dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="20dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector android:height="16dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="16dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector android:height="16dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="16dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2L4.5,20.29l0.71,0.71L12,18l6.79,3 0.71,-0.71z" />
</vector>

View File

@@ -0,0 +1,149 @@
<vector android:height="26dp"
android:viewportHeight="257.0819"
android:viewportWidth="1289.0747"
android:width="130.4dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#000000"
android:pathData="m339.23,124.6q14.22,0 23.58,4.5 9.54,4.5 9.54,11.52 0,3.06 -1.98,5.58 -1.98,2.34 -5.04,2.34 -2.34,0 -3.78,-0.72 -1.26,-0.72 -3.6,-2.34 -1.08,-1.08 -3.42,-2.52 -2.16,-1.08 -6.12,-1.8 -3.96,-0.72 -7.2,-0.72 -9.36,0 -16.56,4.32 -7.2,4.32 -11.16,12.06 -3.96,7.56 -3.96,16.92 0,9.54 3.78,17.1 3.96,7.56 10.98,11.88 7.02,4.32 16.02,4.32 9.36,0 15.12,-2.88 1.26,-0.72 3.42,-2.34 1.8,-1.44 3.06,-2.16 1.44,-0.72 3.42,-0.72 3.6,0 5.58,2.34 2.16,2.16 2.16,5.76 0,3.78 -4.86,7.56 -4.68,3.6 -12.78,5.94 -7.92,2.34 -17.1,2.34 -13.68,0 -24.12,-6.3 -10.44,-6.48 -16.2,-17.64 -5.58,-11.34 -5.58,-25.2 0,-13.86 5.94,-25.02 5.94,-11.34 16.56,-17.64 10.62,-6.48 24.3,-6.48z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m437.63,125.14q31.68,0 31.68,39.24l0,48.06q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.6,0 -6.12,-2.52 -2.34,-2.52 -2.34,-6.12l0,-48.06q0,-23.4 -19.8,-23.4 -10.62,0 -17.64,6.84 -7.02,6.66 -7.02,16.56l0,48.06q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.78,0 -6.12,-2.34 -2.34,-2.52 -2.34,-6.3l0,-115.92q0,-3.6 2.34,-6.12 2.52,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.12l0,45.54q4.5,-7.02 12.6,-11.88 8.1,-5.04 17.28,-5.04z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m571.21,125.5q3.78,0 6.12,2.52 2.52,2.34 2.52,6.3l0,78.12q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.78,0 -6.12,-2.34 -2.34,-2.52 -2.34,-6.3l0,-4.68q-4.68,6.3 -12.78,10.8 -8.1,4.32 -17.46,4.32 -12.24,0 -22.32,-6.3 -9.9,-6.3 -15.66,-17.46 -5.58,-11.34 -5.58,-25.38 0,-14.04 5.58,-25.2 5.76,-11.34 15.66,-17.64 9.9,-6.3 21.78,-6.3 9.54,0 17.64,3.96 8.28,3.96 13.14,10.08l0,-4.32q0,-3.78 2.34,-6.3 2.34,-2.52 6.12,-2.52zM534.49,207.04q8.46,0 14.94,-4.32 6.66,-4.32 10.26,-11.88 3.78,-7.56 3.78,-17.1 0,-9.36 -3.78,-16.92 -3.6,-7.56 -10.26,-11.88 -6.48,-4.5 -14.94,-4.5 -8.46,0 -15.12,4.32 -6.48,4.32 -10.26,11.88 -3.6,7.56 -3.6,17.1 0,9.54 3.6,17.1 3.78,7.56 10.26,11.88 6.66,4.32 15.12,4.32z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m652.08,124.6q4.68,0 8.1,2.52 3.42,2.34 3.42,5.94 0,4.32 -2.34,6.66 -2.16,2.16 -5.4,2.16 -1.62,0 -4.86,-1.08 -3.78,-1.26 -5.94,-1.26 -5.58,0 -10.98,3.96 -5.22,3.78 -8.64,10.62 -3.24,6.66 -3.24,14.94l0,43.38q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.78,0 -6.12,-2.34 -2.34,-2.52 -2.34,-6.3l0,-77.04q0,-3.6 2.34,-6.12 2.52,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.12l0,9.18q3.96,-8.82 11.88,-14.22 7.92,-5.58 18,-5.76z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m755.31,125.5q3.78,0 6.12,2.52 2.52,2.34 2.52,6.3l0,79.2q0,14.58 -6.3,24.3 -6.12,9.9 -16.74,14.58 -10.62,4.68 -23.94,4.68 -7.2,0 -16.92,-2.52 -9.54,-2.52 -12.24,-5.22 -5.58,-2.88 -5.58,-7.2 0,-1.08 0.72,-2.88 1.98,-4.5 6.66,-4.5 2.34,0 5.04,1.08 14.4,5.58 22.5,5.58 14.4,0 21.96,-7.02 7.74,-6.84 7.74,-18.9l0,-9.72q-3.78,7.02 -12.78,12.06 -8.82,5.04 -18.72,5.04 -12.42,0 -22.68,-6.3 -10.26,-6.3 -16.2,-17.46 -5.76,-11.34 -5.76,-25.38 0,-14.04 5.76,-25.2 5.94,-11.34 16.02,-17.64 10.26,-6.3 22.5,-6.3 9.9,0 18.36,4.5 8.64,4.5 13.5,10.98l0,-5.76q0,-3.78 2.34,-6.3 2.34,-2.52 6.12,-2.52zM717.33,207.04q8.82,0 15.66,-4.14 6.84,-4.32 10.62,-11.88 3.96,-7.74 3.96,-17.28 0,-9.54 -3.96,-17.1 -3.78,-7.56 -10.62,-11.88 -6.84,-4.32 -15.66,-4.32 -8.64,0 -15.48,4.32 -6.84,4.32 -10.8,12.06 -3.78,7.56 -3.78,16.92 0,9.36 3.78,17.1 3.96,7.56 10.8,11.88 6.84,4.32 15.48,4.32z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m868.61,170.32q-0.18,3.24 -2.7,5.58 -2.52,2.16 -5.94,2.16l-63.36,0q1.26,13.14 9.9,21.06 8.82,7.92 21.42,7.92 8.64,0 14.04,-2.52 5.4,-2.52 9.54,-6.48 2.7,-1.62 5.22,-1.62 3.06,0 5.04,2.16 2.16,2.16 2.16,5.04 0,3.78 -3.6,6.84 -5.22,5.22 -13.86,8.82 -8.64,3.6 -17.64,3.6 -14.58,0 -25.74,-6.12 -10.98,-6.12 -17.1,-17.1 -5.94,-10.98 -5.94,-24.84 0,-15.12 6.12,-26.46 6.3,-11.52 16.38,-17.64 10.26,-6.12 21.96,-6.12 11.52,0 21.6,5.94 10.08,5.94 16.2,16.38 6.12,10.44 6.3,23.4zM824.51,140.44q-10.08,0 -17.46,5.76 -7.38,5.58 -9.72,17.46l53.1,0l0,-1.44q-0.9,-9.54 -8.64,-15.66 -7.56,-6.12 -17.28,-6.12z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m935.8,125.14q12.24,0 22.14,6.3 9.9,6.12 15.48,17.28 5.76,11.16 5.76,25.2 0,14.04 -5.76,25.2 -5.58,10.98 -15.48,17.28 -9.9,6.3 -21.78,6.3 -9.36,0 -17.46,-4.14 -8.1,-4.14 -13.14,-10.08l0,39.96q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.6,0 -6.12,-2.52 -2.34,-2.34 -2.34,-6.12l0,-113.22q0,-3.78 2.34,-6.3 2.34,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.3l0,5.22q4.32,-6.3 12.6,-10.8 8.28,-4.5 17.64,-4.5zM933.82,206.86q8.28,0 14.94,-4.32 6.66,-4.32 10.26,-11.7 3.78,-7.56 3.78,-16.92 0,-9.36 -3.78,-16.74 -3.6,-7.56 -10.26,-11.88 -6.66,-4.32 -14.94,-4.32 -8.46,0 -15.12,4.32 -6.66,4.14 -10.44,11.7 -3.6,7.56 -3.6,16.92 0,9.36 3.6,16.92 3.78,7.56 10.44,11.88 6.66,4.14 15.12,4.14z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m1045.83,124.6q4.68,0 8.1,2.52 3.42,2.34 3.42,5.94 0,4.32 -2.34,6.66 -2.16,2.16 -5.4,2.16 -1.62,0 -4.86,-1.08 -3.78,-1.26 -5.94,-1.26 -5.58,0 -10.98,3.96 -5.22,3.78 -8.64,10.62 -3.24,6.66 -3.24,14.94l0,43.38q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.78,0 -6.12,-2.34 -2.34,-2.52 -2.34,-6.3l0,-77.04q0,-3.6 2.34,-6.12 2.52,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.12l0,9.18q3.96,-8.82 11.88,-14.22 7.92,-5.58 18,-5.76z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m1089.23,212.44q0,3.6 -2.52,6.12 -2.34,2.52 -6.12,2.52 -3.6,0 -6.12,-2.52 -2.34,-2.52 -2.34,-6.12l0,-77.94q0,-3.6 2.34,-6.12 2.52,-2.52 6.12,-2.52 3.78,0 6.12,2.52 2.52,2.52 2.52,6.12zM1080.59,113.98q-5.22,0 -7.56,-1.8 -2.16,-1.98 -2.16,-6.12l0,-2.88q0,-4.32 2.34,-6.12 2.52,-1.8 7.56,-1.8 5.04,0 7.2,1.98 2.34,1.8 2.34,5.94l0,2.88q0,4.32 -2.34,6.12 -2.34,1.8 -7.38,1.8z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m1154.85,124.6q14.22,0 23.58,4.5 9.54,4.5 9.54,11.52 0,3.06 -1.98,5.58 -1.98,2.34 -5.04,2.34 -2.34,0 -3.78,-0.72 -1.26,-0.72 -3.6,-2.34 -1.08,-1.08 -3.42,-2.52 -2.16,-1.08 -6.12,-1.8 -3.96,-0.72 -7.2,-0.72 -9.36,0 -16.56,4.32 -7.2,4.32 -11.16,12.06 -3.96,7.56 -3.96,16.92 0,9.54 3.78,17.1 3.96,7.56 10.98,11.88 7.02,4.32 16.02,4.32 9.36,0 15.12,-2.88 1.26,-0.72 3.42,-2.34 1.8,-1.44 3.06,-2.16 1.44,-0.72 3.42,-0.72 3.6,0 5.58,2.34 2.16,2.16 2.16,5.76 0,3.78 -4.86,7.56 -4.68,3.6 -12.78,5.94 -7.92,2.34 -17.1,2.34 -13.68,0 -24.12,-6.3 -10.44,-6.48 -16.2,-17.64 -5.58,-11.34 -5.58,-25.2 0,-13.86 5.94,-25.02 5.94,-11.34 16.56,-17.64 10.62,-6.48 24.3,-6.48z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m1289.07,170.32q-0.18,3.24 -2.7,5.58 -2.52,2.16 -5.94,2.16l-63.36,0q1.26,13.14 9.9,21.06 8.82,7.92 21.42,7.92 8.64,0 14.04,-2.52 5.4,-2.52 9.54,-6.48 2.7,-1.62 5.22,-1.62 3.06,0 5.04,2.16 2.16,2.16 2.16,5.04 0,3.78 -3.6,6.84 -5.22,5.22 -13.86,8.82 -8.64,3.6 -17.64,3.6 -14.58,0 -25.74,-6.12 -10.98,-6.12 -17.1,-17.1 -5.94,-10.98 -5.94,-24.84 0,-15.12 6.12,-26.46 6.3,-11.52 16.38,-17.64 10.26,-6.12 21.96,-6.12 11.52,0 21.6,5.94 10.08,5.94 16.2,16.38 6.12,10.44 6.3,23.4zM1244.97,140.44q-10.08,0 -17.46,5.76 -7.38,5.58 -9.72,17.46l53.1,0l0,-1.44q-0.9,-9.54 -8.64,-15.66 -7.56,-6.12 -17.28,-6.12z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m321.33,1q5.1,0 9.7,3.1 4.6,3 7.4,8.2 2.8,5.1 2.8,11.2 0,6 -2.8,11.2 -2.8,5.2 -7.4,8.3 -4.6,3 -9.7,3l-17.4,0l0,18.9q0,2.7 -1.6,4.4 -1.6,1.7 -4.2,1.7 -2.5,0 -4.1,-1.7 -1.6,-1.8 -1.6,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8zM321.33,34.6q1.9,0 3.7,-1.6 1.9,-1.6 3,-4.1 1.2,-2.6 1.2,-5.4 0,-2.8 -1.2,-5.3 -1.1,-2.6 -3,-4.1 -1.8,-1.6 -3.7,-1.6l-17.4,0l0,22.1z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m417.18,36q0,9.9 -4.4,18.2 -4.4,8.2 -12.2,13 -7.7,4.8 -17.4,4.8 -9.7,0 -17.5,-4.8 -7.7,-4.8 -12.1,-13 -4.3,-8.3 -4.3,-18.2 0,-9.9 4.3,-18.1 4.4,-8.3 12.1,-13.1 7.8,-4.8 17.5,-4.8 9.7,0 17.4,4.8 7.8,4.8 12.2,13.1 4.4,8.2 4.4,18.1zM404.18,36q0,-6.7 -2.7,-12.1 -2.7,-5.5 -7.5,-8.7 -4.8,-3.2 -10.8,-3.2 -6.1,0 -10.9,3.2 -4.7,3.1 -7.4,8.6 -2.6,5.5 -2.6,12.2 0,6.7 2.6,12.2 2.7,5.5 7.4,8.7 4.8,3.1 10.9,3.1 6,0 10.8,-3.2 4.8,-3.2 7.5,-8.6 2.7,-5.5 2.7,-12.2z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m506.87,0.7q2.4,0 4.4,1.9 2.1,1.8 2.1,4.6 0,0.9 -0.3,2l-19.7,58q-0.6,1.7 -2.1,2.7 -1.5,1 -3.3,1.1 -1.8,0 -3.4,-1 -1.6,-1 -2.5,-2.9l-14.2,-32.3 -14.3,32.3q-0.9,1.9 -2.5,2.9 -1.6,1 -3.4,1 -1.8,-0.1 -3.3,-1.1 -1.5,-1 -2.1,-2.7l-19.7,-58q-0.3,-1.1 -0.3,-2 0,-2.8 2,-4.6 2.1,-1.9 4.6,-1.9 2,0 3.6,1.1 1.6,1 2.2,2.8l14.9,45.2 13,-31.2q0.8,-1.8 2.3,-2.8 1.5,-1.1 3.4,-1 1.9,-0.1 3.3,1 1.5,1 2.3,2.8l12.3,30.9 14.8,-44.9q0.6,-1.8 2.2,-2.8 1.7,-1.1 3.7,-1.1z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m562.11,59.5q2.6,0 4.3,1.8 1.8,1.7 1.8,4 0,2.5 -1.8,4.1 -1.7,1.6 -4.3,1.6l-33.5,0q-2.6,0 -4.4,-1.7 -1.7,-1.8 -1.7,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8l33.5,0q2.6,0 4.3,1.7 1.8,1.6 1.8,4.2 0,2.5 -1.7,4.1 -1.7,1.5 -4.4,1.5l-27.1,0l0,17l22.6,0q2.6,0 4.3,1.7 1.8,1.6 1.8,4.2 0,2.5 -1.7,4.1 -1.7,1.5 -4.4,1.5l-22.6,0l0,18.5z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m634.63,61.2q1.3,0.8 2,2.1 0.8,1.3 0.8,2.7 0,1.8 -1.2,3.3 -1.5,1.8 -4.6,1.8 -2.4,0 -4.4,-1.1 -7.2,-4.1 -7.2,-16.7 0,-3.6 -2.4,-5.7 -2.3,-2.1 -6.7,-2.1L592.23,45.5l0,19.4q0,2.7 -1.5,4.4 -1.4,1.7 -3.8,1.7 -2.9,0 -5.1,-1.7 -2.1,-1.8 -2.1,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8l28.8,0q5.2,0 9.8,2.8 4.6,2.8 7.3,7.7 2.8,4.9 2.8,11 0,5 -2.7,9.8 -2.7,4.7 -7,7.5 6.3,4.4 6.9,11.8 0.3,1.6 0.3,3.1 0.4,3.1 0.8,4.5 0.4,1.3 1.8,2zM614.13,35.2q1.8,0 3.5,-1.7 1.7,-1.7 2.8,-4.5 1.1,-2.9 1.1,-6.2 0,-2.8 -1.1,-5.1 -1.1,-2.4 -2.8,-3.8 -1.7,-1.4 -3.5,-1.4l-21.9,0l0,22.7z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m688.08,59.5q2.6,0 4.3,1.8 1.8,1.7 1.8,4 0,2.5 -1.8,4.1 -1.7,1.6 -4.3,1.6l-33.5,0q-2.6,0 -4.4,-1.7 -1.7,-1.8 -1.7,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8l33.5,0q2.6,0 4.3,1.7 1.8,1.6 1.8,4.2 0,2.5 -1.7,4.1 -1.7,1.5 -4.4,1.5l-27.1,0l0,17l22.6,0q2.6,0 4.3,1.7 1.8,1.6 1.8,4.2 0,2.5 -1.7,4.1 -1.7,1.5 -4.4,1.5l-22.6,0l0,18.5z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m735.71,1q9.4,0 16.1,4.7 6.8,4.6 10.3,12.6 3.6,7.9 3.6,17.7 0,9.8 -3.6,17.8 -3.5,7.9 -10.3,12.6 -6.7,4.6 -16.1,4.6l-23.9,0q-2.6,0 -4.4,-1.7 -1.7,-1.8 -1.7,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8zM734.71,59.5q9,0 13.5,-6.6 4.5,-6.7 4.5,-16.9 0,-10.2 -4.6,-16.8 -4.5,-6.7 -13.4,-6.7l-16.5,0l0,47z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m847.12,32.2q5.3,2.1 8.6,6.4 3.4,4.3 3.4,11.1 0,11.9 -6.8,16.6 -6.8,4.7 -16.2,4.7l-24.9,0q-2.6,0 -4.4,-1.7 -1.7,-1.8 -1.7,-4.4l0,-57.8q0,-2.6 1.7,-4.3 1.8,-1.8 4.4,-1.8l25.2,0q19,0 19,17.8 0,4.5 -2.2,8 -2.1,3.4 -6.1,5.4zM842.42,21q0,-4.1 -2.1,-6.1 -2,-2.1 -5.7,-2.1l-16.5,0l0,15.6l16.8,0q3,0 5.2,-2 2.3,-2 2.3,-5.4zM836.12,59.5q4.7,0 7.3,-2.5 2.7,-2.5 2.7,-7.3 0,-5.9 -3.1,-7.7 -3.1,-1.8 -7.6,-1.8l-17.3,0l0,19.3z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000000"
android:pathData="m918.81,6.9q0,2 -1.1,3.7l-20.9,29.9l0,24.4q0,2.6 -1.7,4.4 -1.7,1.7 -4.1,1.7 -2.5,0 -4.3,-1.7 -1.7,-1.8 -1.7,-4.4l0,-25.8l-20.8,-27.6q-1.8,-2.4 -1.8,-4.7 0,-2.6 2,-4.3 2.1,-1.8 4.4,-1.8 2.8,0 4.9,2.8l17.6,24.3 16.5,-24.1q2.1,-3 5,-3 2.4,0 4.2,1.8 1.8,1.8 1.8,4.4z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="1" />
<path
android:fillColor="#000016"
android:pathData="m246.94,173.65 l-11.5,-6.03 10.02,-5.24c3.41,-1.78 3.42,-4.71 0.01,-6.5l-10.76,-5.65 10.76,-5.62c3.41,-1.79 3.42,-4.71 0.01,-6.5L159.4,92.94c-3.41,-1.79 -9,-1.81 -12.42,-0.04L2.56,167.49c-3.42,1.77 -3.42,4.65 0.01,6.41l11.02,5.66 -11.02,5.69c-3.42,1.77 -3.42,4.65 0.01,6.41l11.76,6.04 -10.29,5.31c-3.42,1.77 -3.42,4.65 0.01,6.41l88.01,45.22c3.43,1.76 9.02,1.74 12.43,-0.04l142.46,-74.47c3.41,-1.78 3.41,-4.71 0,-6.5zM153.91,115.02 L132.31,139.92c-1.08,1.25 -0.76,2.88 0.7,3.64l17.18,8.83c1.47,0.75 1.47,1.99 0,2.75l-53.92,27.85c-1.47,0.76 -1.78,0.36 -0.7,-0.89l21.59,-24.9c1.08,-1.25 0.77,-2.88 -0.7,-3.64l-17.18,-8.83c-1.47,-0.75 -1.47,-1.99 0,-2.75l53.92,-27.85c1.47,-0.76 1.78,-0.36 0.7,0.89z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M23,12l-2.44,-2.79l0.34,-3.69l-3.61,-0.82L15.4,1.5L12,2.96L8.6,1.5L6.71,4.69L3.1,5.5L3.44,9.2L1,12l2.44,2.79l-0.34,3.7l3.61,0.82L8.6,22.5l3.4,-1.47l3.4,1.46l1.89,-3.19l3.61,-0.82l-0.34,-3.69L23,12zM10.09,16.72l-3.8,-3.81l1.48,-1.48l2.32,2.33l5.85,-5.87l1.48,1.48L10.09,16.72z" />
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/chip_background" />
<item android:drawable="?selectableItemBackground" />
</layer-list>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?android:colorBackground" />
<corners android:radius="16dp" />
</shape>

View File

@@ -0,0 +1,27 @@
<?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:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_launcher_foreground" />
<TextView
android:id="@+id/textView14"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline4"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/imageView2"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -21,6 +21,8 @@
<import type="net.vonforst.evmap.viewmodel.Status" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<variable
name="charger"
type="Resource&lt;ChargeLocation&gt;" />
@@ -33,6 +35,10 @@
name="availability"
type="Resource&lt;ChargeLocationStatus&gt;" />
<variable
name="filteredAvailability"
type="Resource&lt;ChargeLocationStatus&gt;" />
<variable
name="chargeCards"
type="java.util.Map&lt;Long, ChargeCard&gt;" />
@@ -41,6 +47,10 @@
name="filteredChargeCards"
type="java.util.Set&lt;Long&gt;" />
<variable
name="expanded"
type="Boolean" />
</data>
<androidx.cardview.widget.CardView
@@ -59,14 +69,17 @@
android:paddingBottom="8dp">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:id="@+id/txtName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="30dp"
android:ellipsize="end"
android:maxLines="1"
android:maxLines="@{expanded ? 3 : 1}"
android:text="@{charger.data.name}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@+id/txtAvailability"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="Parkhaus" />
@@ -81,11 +94,11 @@
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView"
app:layout_constraintTop_toBottomOf="@+id/txtName"
tools:text="Beispielstraße 10, 12345 Berlin" />
<TextView
android:id="@+id/textView27"
android:id="@+id/txtDistance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
@@ -95,12 +108,33 @@
android:minWidth="50dp"
android:text="@{@string/distance_format(distance)}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintBottom_toBottomOf="@+id/topPart"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/textView3"
tools:text="10 km" />
<TextView
android:id="@+id/textView3"
android:id="@+id/txtAvailability"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="72dp"
android:background="@drawable/rounded_rect"
android:ellipsize="end"
android:gravity="end"
android:maxLines="1"
android:padding="2dp"
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(BindingAdaptersKt.flatten(filteredAvailability.data.status.values())), filteredAvailability.data.totalChargepoints)}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@android:color/white"
app:backgroundTintAvailability="@{BindingAdaptersKt.flatten(availability.data.status.values())}"
app:invisibleUnlessAnimated="@{availability.data != null &amp;&amp; !expanded}"
app:invisibleUnless="@{availability.data != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/txtName"
tools:backgroundTint="@color/available"
tools:text="2/2" />
<TextView
android:id="@+id/txtConnectors"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
@@ -108,7 +142,7 @@
android:maxLines="1"
android:text="@{charger.data.formatChargepoints()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/textView27"
app:layout_constraintEnd_toStartOf="@+id/txtDistance"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView2"
tools:text="2x Typ 2 22 kW" />
@@ -136,7 +170,7 @@
android:textColor="?colorPrimary"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView3" />
app:layout_constraintTop_toBottomOf="@+id/txtConnectors" />
<TextView
android:id="@+id/textView12"
@@ -263,10 +297,10 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="@+id/textView3"
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/textView" />
app:layout_constraintTop_toTopOf="@+id/txtName" />
<Button
android:id="@+id/btnChargeprice"
@@ -280,6 +314,39 @@
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView13" />
<ImageView
android:id="@+id/imgVerified"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="4dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:contentDescription="@string/verified"
android:tooltipText="@string/verified_desc"
app:goneUnless="@{ charger.data.verified }"
app:layout_constraintBottom_toBottomOf="@+id/txtName"
app:layout_constraintStart_toEndOf="@+id/imgFaultReport"
app:layout_constraintTop_toTopOf="@+id/txtName"
app:srcCompat="@drawable/ic_verified"
app:tint="@color/available"
tools:targetApi="o" />
<ImageView
android:id="@+id/imgFaultReport"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="4dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:contentDescription="@string/fault_report"
android:tooltipText="@string/fault_report"
app:goneUnless="@{ charger.data.faultReport != null }"
app:layout_constraintBottom_toBottomOf="@+id/txtName"
app:layout_constraintStart_toEndOf="@+id/txtName"
app:layout_constraintTop_toTopOf="@+id/txtName"
app:srcCompat="@drawable/ic_map_marker_fault"
tools:targetApi="o" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:maxWidth="200dp"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/linearLayout4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:id="@+id/topPanel"
android:layout_width="0dp"
android:layout_height="88dp"
android:background="@color/android_auto_accent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/imageView4"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_gravity="center"
android:background="@drawable/circle_bg_logo"
android:scaleType="center"
app:srcCompat="@drawable/android_auto" />
</FrameLayout>
<TextView
android:id="@+id/welcomeTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/update_060_androidauto_title"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/topPanel" />
<TextView
android:id="@+id/welcomeText1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="@string/update_060_androidauto_text"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/welcomeTitle" />
<ImageView
android:id="@+id/icon1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/welcomeText1"
app:srcCompat="@drawable/android_auto_screenshot" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<Button
android:id="@+id/btnOk"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:text="@string/ok" />
</LinearLayout>

View File

@@ -0,0 +1,199 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/linearLayout4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="16dp"
android:paddingTop="16dp"
android:paddingRight="16dp"
android:paddingBottom="16dp">
<include
android:id="@+id/include"
layout="@layout/app_logo"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/welcomeTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/welcome_to_evmap"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/include" />
<TextView
android:id="@+id/welcomeText1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/welcome_1"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/welcomeTitle" />
<ImageView
android:id="@+id/icon1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="@+id/iconLabel1"
app:layout_constraintStart_toStartOf="@+id/iconLabel1"
app:layout_constraintTop_toBottomOf="@+id/welcomeText1"
app:srcCompat="@drawable/ic_map_marker_charging"
app:tint="@color/charger_low"
app:tintMode="multiply" />
<TextView
android:id="@+id/iconLabel1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="&lt;11 kW"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/iconLabel2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/icon1" />
<ImageView
android:id="@+id/icon2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="@+id/iconLabel2"
app:layout_constraintStart_toStartOf="@+id/iconLabel2"
app:layout_constraintTop_toBottomOf="@+id/welcomeText1"
app:srcCompat="@drawable/ic_map_marker_charging"
app:tint="@color/charger_11kw"
app:tintMode="multiply" />
<TextView
android:id="@+id/iconLabel2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="≥11 kW"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/iconLabel3"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/iconLabel1"
app:layout_constraintTop_toBottomOf="@+id/icon2" />
<ImageView
android:id="@+id/icon3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="@+id/iconLabel3"
app:layout_constraintStart_toStartOf="@+id/iconLabel3"
app:layout_constraintTop_toBottomOf="@+id/welcomeText1"
app:srcCompat="@drawable/ic_map_marker_charging"
app:tint="@color/charger_20kw"
app:tintMode="multiply" />
<TextView
android:id="@+id/iconLabel3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="≥20 kW"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/iconLabel4"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/iconLabel2"
app:layout_constraintTop_toBottomOf="@+id/icon3" />
<ImageView
android:id="@+id/icon4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="@+id/iconLabel4"
app:layout_constraintStart_toStartOf="@+id/iconLabel4"
app:layout_constraintTop_toBottomOf="@+id/welcomeText1"
app:srcCompat="@drawable/ic_map_marker_charging"
app:tint="@color/charger_43kw"
app:tintMode="multiply" />
<TextView
android:id="@+id/iconLabel4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="≥43 kW"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/iconLabel5"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/iconLabel3"
app:layout_constraintTop_toBottomOf="@+id/icon4" />
<ImageView
android:id="@+id/icon5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="@+id/iconLabel5"
app:layout_constraintStart_toStartOf="@+id/iconLabel5"
app:layout_constraintTop_toBottomOf="@+id/welcomeText1"
app:srcCompat="@drawable/ic_map_marker_charging"
app:tint="@color/charger_100kw"
app:tintMode="multiply" />
<TextView
android:id="@+id/iconLabel5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="≥100 kW"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/iconLabel4"
app:layout_constraintTop_toBottomOf="@+id/icon5" />
<TextView
android:id="@+id/welcomeText2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/welcome_2"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iconLabel1" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<Button
android:id="@+id/btnOk"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ok"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:layout_gravity="end" />
</LinearLayout>

View File

@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="net.vonforst.evmap.viewmodel.ChargepriceViewModel" />
<import type="net.vonforst.evmap.viewmodel.Status" />
<variable
name="vm"
type="ChargepriceViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/linearLayout5"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<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="?android:actionBarSize">
<ImageView
android:id="@+id/imgChargepriceLogo"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/powered_by_chargeprice"
android:focusable="true"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:layout_gravity="right"
app:srcCompat="@drawable/ic_powered_by_chargeprice"
app:tint="?android:textColorPrimary"
tools:ignore="RtlSymmetry" />
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@string/chargeprice_select_connector"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
android:textColor="?colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_container" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/connectors_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:data="@{vm.charger.chargepointsMerged}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView"
tools:itemCount="3"
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_connector_button"
tools:orientation="horizontal" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@{String.format(@string/chargeprice_battery_range, vm.batteryRange[0], vm.batteryRange[1])}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
android:textColor="?colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
tools:text="Charge from 20% to 80%" />
<com.google.android.material.slider.RangeSlider
android:id="@+id/battery_range"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:valueFrom="0.0"
android:valueTo="100.0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2"
app:values="@={vm.batteryRange}" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/charge_prices_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:data="@{vm.chargePricesForChargepoint.data}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/battery_range"
tools:itemCount="3"
tools:listitem="@layout/item_chargeprice" />
<TextView
android:id="@+id/textView8"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:text="@string/chargeprice_no_tariffs_found"
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS &amp;&amp; vm.chargePricesForChargepoint.data.size() == 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
<TextView
android:id="@+id/textView9"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:text="@string/chargeprice_no_compatible_connectors"
app:goneUnless="@{vm.noCompatibleConnectors}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
<TextView
android:id="@+id/textView3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:text="@string/chargeprice_select_car_first"
app:goneUnless="@{vm.vehicle == null}"
app:layout_constraintBottom_toTopOf="@+id/btnSettings"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list"
app:layout_constraintVertical_chainStyle="packed" />
<ProgressBar
android:id="@+id/progressBar5"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.LOADING}"
app:layout_constraintBottom_toBottomOf="@+id/charge_prices_list"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
<Button
android:id="@+id/btnSettings"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/settings"
app:goneUnless="@{vm.vehicle == null}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView3" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -9,6 +9,8 @@
<import type="net.vonforst.evmap.viewmodel.Status" />
<import type="com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike" />
<variable
name="vm"
type="net.vonforst.evmap.viewmodel.MapViewModel" />
@@ -61,7 +63,8 @@
<TextView
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/search"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textColor="?android:textColorSecondary" />
@@ -88,7 +91,6 @@
android:layout_width="match_parent"
android:layout_height="@dimen/gallery_height_with_margin"
android:background="?android:colorBackground"
android:fitsSystemWindows="true"
app:layout_behavior="@string/BackDropBottomSheetBehavior">
<androidx.recyclerview.widget.RecyclerView
@@ -124,7 +126,6 @@
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:fillViewport="true"
android:orientation="vertical"
app:bottomsheetbehavior_anchorPoint="@dimen/gallery_height"
@@ -139,9 +140,11 @@
layout="@layout/detail_view"
app:charger="@{vm.charger}"
app:availability="@{vm.availability}"
app:filteredAvailability="@{vm.filteredAvailability}"
app:chargeCards="@{vm.chargeCardMap}"
app:filteredChargeCards="@{vm.filteredChargeCards}"
app:distance="@{vm.chargerDistance}" />
app:distance="@{vm.chargerDistance}"
app:expanded="@{vm.bottomSheetState != BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED &amp;&amp; vm.bottomSheetState != BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN}" />
</androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="net.vonforst.evmap.api.chargeprice.ChargePrice" />
<import type="net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<import type="java.util.Set" />
<variable
name="item"
type="ChargePrice" />
<variable
name="meta"
type="ChargepriceChargepointMeta" />
<variable
name="myTariffs"
type="Set&lt;String>" />
<variable
name="myTariffsAll"
type="Boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingTop="8dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
app:myTariffsBackground="@{!myTariffsAll &amp;&amp; myTariffs.contains(item.tariff.get().id)}">
<TextView
android:id="@+id/txtTariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@{item.tariffName}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintBottom_toTopOf="@+id/txtProvider"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="CheapCharge" />
<TextView
android:id="@+id/txtProvider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@{item.provider}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{!item.tariffName.startsWith(item.provider)}"
app:layout_constraintBottom_toTopOf="@+id/rvTags"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtTariff"
tools:text="Cheap Charging Co." />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvTags"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:nestedScrollingEnabled="false"
app:data="@{item.tags}"
app:layout_constraintBottom_toTopOf="@+id/txtProviderCustomerTariff"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtProvider"
tools:itemCount="1"
tools:listitem="@layout/item_chargeprice_tag" />
<TextView
android:id="@+id/txtProviderCustomerTariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@string/chargeprice_provider_customer_tariff"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{item.providerCustomerTariff}"
app:layout_constraintBottom_toTopOf="@id/txtMonthlyFee"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/rvTags" />
<TextView
android:id="@+id/txtMonthlyFee"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@{item.formatMonthlyFees(context)}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{item.totalMonthlyFee > 0 || item.monthlyMinSales > 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtProviderCustomerTariff"
tools:text="Base fee 1 €/month" />
<TextView
android:id="@+id/txtPrice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:gravity="end"
android:text="@{String.format(@string/charge_price_format, item.chargepointPrices.get(0).price, BindingAdaptersKt.currency(item.currency))}"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
app:layout_constraintBottom_toTopOf="@+id/txtAveragePrice"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline5"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="1,50 €" />
<TextView
android:id="@+id/txtAveragePrice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:gravity="end"
android:text="@{String.format(item.chargepointPrices.get(0).priceDistribution.isOnlyKwh ? @string/charge_price_kwh_format : @string/charge_price_average_format, item.chargepointPrices.get(0).price / meta.energy, BindingAdaptersKt.currency(item.currency))}"
app:goneUnless="@{item.chargepointPrices.get(0).price > 0}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintBottom_toTopOf="@id/txtPriceDetails"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline5"
app:layout_constraintTop_toBottomOf="@+id/txtPrice"
tools:text="⌀ 0,29 €/kWh" />
<TextView
android:id="@+id/txtPriceDetails"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:gravity="end"
android:text="@{item.chargepointPrices.get(0).formatDistribution(context)}"
app:goneUnless="@{!item.chargepointPrices.get(0).formatDistribution(context).empty}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline5"
app:layout_constraintTop_toBottomOf="@+id/txtAveragePrice"
tools:text="pro kWh + ab 4h Blockiergeb." />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.65" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="net.vonforst.evmap.api.chargeprice.ChargepriceTag" />
<variable
name="item"
type="ChargepriceTag" />
</data>
<net.vonforst.evmap.ui.BalancedBreakingTextView
android:id="@+id/rvTags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:background="@drawable/rounded_rect_16dp"
android:maxLines="3"
android:text="@{item.text}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?android:textColorPrimary"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark"
android:gravity="center_vertical"
android:drawablePadding="4dp"
android:paddingEnd="8dp"
android:paddingStart="3dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:drawableTint="?android:textColorPrimary"
android:breakStrategy="balanced"
app:chargepriceTagColor="@{item.kind}"
app:chargepriceTagIcon="@{item.kind}"
tools:backgroundTint="@color/chargeprice_alert"
tools:drawableLeft="@drawable/ic_chargeprice_alert"
tools:text="Only for drivers of blue cars" />
</layout>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="net.vonforst.evmap.api.goingelectric.Chargepoint" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<variable
name="item"
type="Chargepoint" />
<variable
name="enabled"
type="boolean" />
</data>
<net.vonforst.evmap.ui.CheckableConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_outline"
android:foreground="?selectableItemBackground"
android:clickable="@{enabled}"
android:focusable="true"
android:layout_margin="4dp">
<ImageView
android:id="@+id/imageView"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:tintMode="src_in"
android:contentDescription="@{item.type}"
app:connectorIcon="@{item.type}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@{BindingAdaptersKt.colorEnabled(context, enabled)}"
tools:tint="?colorControlNormal"
tools:srcCompat="@drawable/ic_connector_typ2" />
<TextView
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="38dp"
android:layout_marginTop="38dp"
android:layout_marginEnd="4dp"
android:text="@{String.format(&quot;× %d&quot;, item.count)}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@{BindingAdaptersKt.colorEnabled(context, enabled)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/imageView"
app:layout_constraintTop_toTopOf="@+id/imageView"
tools:text="×99" />
<TextView
android:id="@+id/textView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:text="@{item.formatPower()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@{BindingAdaptersKt.colorEnabled(context, enabled)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView"
tools:text="350 kW" />
</net.vonforst.evmap.ui.CheckableConstraintLayout>
</layout>

View File

@@ -18,7 +18,7 @@
app:selectableItemBackground="@{item.clickable}">
<TextView
android:id="@+id/textView9"
android:id="@+id/txtTariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
@@ -48,7 +48,7 @@
tools:srcCompat="@drawable/ic_address" />
<TextView
android:id="@+id/textView8"
android:id="@+id/txtProvider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
@@ -56,13 +56,12 @@
android:layout_marginBottom="14dp"
android:text="@{item.detailText}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:autoLink="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
android:linksClickable="@{item.links}"
app:linkify="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
app:goneUnless="@{item.detailText != null}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintTop_toBottomOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtTariff"
tools:text="Lorem ipsum" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -24,7 +24,7 @@
app:selectableItemBackground="@{item.clickable}">
<TextView
android:id="@+id/textView9"
android:id="@+id/txtTariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
@@ -54,21 +54,20 @@
tools:srcCompat="@drawable/ic_address" />
<TextView
android:id="@+id/textView8"
android:id="@+id/txtProvider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
android:autoLink="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
android:linksClickable="@{item.links}"
app:linkify="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
android:text="@{item.detailText}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{item.detailText != null}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/expandToggle"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintTop_toBottomOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtTariff"
app:layout_constraintVertical_bias="0.0"
tools:text="Lorem ipsum" />
@@ -83,8 +82,8 @@
app:goneUnless="@{expandToggle.checked}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintTop_toBottomOf="@+id/textView8" />
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtProvider" />
<include
android:id="@+id/hours_tue"
@@ -97,7 +96,7 @@
app:dayOfWeek="@{DayOfWeek.TUESDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_mon" />
<include
@@ -111,7 +110,7 @@
app:dayOfWeek="@{DayOfWeek.WEDNESDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_tue" />
<include
@@ -125,7 +124,7 @@
app:dayOfWeek="@{DayOfWeek.THURSDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_wed" />
<include
@@ -139,7 +138,7 @@
app:dayOfWeek="@{DayOfWeek.FRIDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_thu" />
<include
@@ -153,7 +152,7 @@
app:dayOfWeek="@{DayOfWeek.SATURDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_fri" />
<include
@@ -167,7 +166,7 @@
app:dayOfWeek="@{DayOfWeek.SUNDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_sat" />
<include
@@ -183,7 +182,7 @@
app:hours="@{item.hoursDays}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_sun" />
<ToggleButton

View File

@@ -45,7 +45,7 @@
tools:text="Beispielstraße 10, 12345 Berlin" />
<TextView
android:id="@+id/textView3"
android:id="@+id/txtConnectors"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"

View File

@@ -29,6 +29,7 @@
android:layout_marginRight="16dp"
app:tint="@android:color/white"
app:srcCompat="@drawable/ic_delete" />
</FrameLayout>
@@ -43,17 +44,42 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginEnd="8dp"
android:text="@{item.name}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/handle"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintEnd_toStartOf="@+id/btnRename"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.511"
tools:text="Lorem ipsum" />
<ImageButton
android:id="@+id/btnDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="?colorControlNormal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/handle"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_delete"
android:contentDescription="@string/delete" />
<ImageButton
android:id="@+id/btnRename"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="?colorControlNormal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnDelete"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_edit"
android:contentDescription="@string/rename" />
<ImageView
android:id="@+id/handle"
android:layout_width="wrap_content"

View File

@@ -31,7 +31,7 @@
android:layout_marginEnd="16dp"
android:text="@string/map_type"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/btnClose"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@@ -98,5 +98,18 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView23" />
<ImageButton
android:id="@+id/btnClose"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:layout_marginEnd="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/close"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_close"
app:tint="?colorControlNormal" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -1,28 +1,9 @@
<?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"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="24dp">
android:paddingTop="24dp"
android:id="@+id/nav_header">
<ImageView
android:id="@+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_launcher_foreground" />
<TextView
android:id="@+id/textView14"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline4"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/imageView2"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include layout="@layout/app_logo" />
</FrameLayout>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_close"
android:icon="@drawable/ic_close"
android:title="@string/close"
app:showAsAction="always" />
</menu>

View File

@@ -13,24 +13,37 @@
<action
android:id="@+id/action_map_to_galleryFragment"
app:destination="@id/gallery"
app:enterAnim="@anim/fragment_fade_enter"
app:exitAnim="@anim/fragment_fade_exit"
app:popEnterAnim="@anim/fragment_fade_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_map_to_filterFragment"
app:destination="@id/filter"
app:exitAnim="@anim/fragment_fade_exit"
app:enterAnim="@anim/fragment_fade_enter"
app:popEnterAnim="@anim/fragment_fade_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
app:exitAnim="@animator/nav_default_exit_anim"
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_map_to_filterProfilesFragment"
app:destination="@id/filter_profiles"
app:exitAnim="@anim/fragment_fade_exit"
app:enterAnim="@anim/fragment_fade_enter"
app:popEnterAnim="@anim/fragment_fade_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
app:exitAnim="@animator/nav_default_exit_anim"
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_map_to_chargepriceFragment"
app:destination="@id/chargeprice"
app:exitAnim="@animator/nav_default_exit_anim"
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_map_to_welcome"
app:destination="@id/welcome" />
<action
android:id="@+id/action_map_to_update_060_androidauto"
app:destination="@id/update_060_androidauto" />
</fragment>
<fragment
android:id="@+id/about"
@@ -70,11 +83,34 @@
android:name="net.vonforst.evmap.fragment.FilterProfilesFragment"
android:label="@string/menu_manage_filter_profiles"
tools:layout="@layout/fragment_filter_profiles" />
<dialog
android:id="@+id/chargeprice"
android:name="net.vonforst.evmap.fragment.ChargepriceFragment"
android:label="@string/chargeprice_title"
tools:layout="@layout/fragment_chargeprice">
<action
android:id="@+id/action_chargeprice_to_settingsFragment"
app:destination="@id/settings"
app:exitAnim="@animator/nav_default_exit_anim"
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_enter_anim"
app:popExitAnim="@animator/nav_default_exit_anim" />
</dialog>
<fragment
android:id="@+id/donate"
android:name="net.vonforst.evmap.fragment.DonateFragment"
android:label="@string/donate"
tools:layout="@layout/fragment_donate" />
<dialog
android:id="@+id/welcome"
android:name="net.vonforst.evmap.fragment.WelcomeDialogFragment"
android:label="@string/welcome_to_evmap"
tools:layout="@layout/dialog_welcome" />
<dialog
android:id="@+id/update_060_androidauto"
android:name="net.vonforst.evmap.fragment.updatedialogs.Update060AndroidAutoDialogFramgent"
android:label="@string/welcome_to_evmap"
tools:layout="@layout/dialog_update_060_androidauto" />
<chrome
android:id="@+id/report_new_charger"
app:url="@string/report_new_charger_url" />

View File

@@ -3,7 +3,6 @@
android:duration="375"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:transitionOrdering="together">
<changeClipBounds />
<changeTransform />
<changeImageTransform />
<changeBounds />
</transitionSet>

View File

@@ -4,6 +4,7 @@
<string name="title_activity_maps">EVMap</string>
<string name="connectors">Anschlüsse</string>
<string name="no_maps_app_found">Keine Navigations-App gefunden</string>
<string name="no_browser_app_found">Kein Webbrowser gefunden</string>
<string name="address">Adresse</string>
<string name="operator">Betreiber</string>
<string name="network">Verbund</string>
@@ -37,7 +38,7 @@
<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="copyright_summary">©20202021 Johan von Forstner</string>
<string name="other">Sonstiges</string>
<string name="privacy">Datenschutzerklärung</string>
<string name="fav_add">Zu Favoriten hinzufügen</string>
@@ -146,8 +147,68 @@
<string name="save_as_profile">Als Profil speichern</string>
<string name="save_profile_enter_name">Geben Sie den Namen des Filterprofils ein:</string>
<string name="filterprofiles_empty_state">Du hast noch keine Filterprofile gespeichert.</string>
<string name="welcome_to_evmap">Willkommen bei EVMap!</string>
<string name="welcome_1">Mit EVMap kannst du Ladestationen für Elektroautos in deiner Nähe finden. EVMap nutzt dafür die Community-gepflegte Datenbank von GoingElectric.de, die sich vor allem auf Europa und den deutschsprachigen Raum konzentriert. Über die Website GoingElectric.de kannst du selbst zum Verzeichnis beitragen.\n\nDie Ladestationen werden auf der Karte mit verschiedenen Farben angezeigt, die die maximale Ladeleistung angeben:</string>
<string name="welcome_2">EVMap ist kostenlos und Open Source. Du kannst bei GitHub zur Weiterentwicklung beitragen oder die Entwicklung mit Spenden unterstützen. Die entsprechenden Links findest du unter „Über EVMap” im Menü.</string>
<string name="deleted_filterprofile">„%s” gelöscht</string>
<string name="undo">Rückgängig</string>
<string name="rename">Umbenennen</string>
<string name="charging_barrierfree">Ohne Vertrag / Registrierung nutzbar</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d kompatibler Ladetarif</item>
<item quantity="other">%d kompatible Ladetarife</item>
</plurals>
</resources>
<string name="navigate">Navigieren</string>
<string name="verified">Verifiziert</string>
<string name="verified_desc">Verifiziert von der GoingElectric.de Community nicht zwangsläufig auch aktuell verfügbar.</string>
<string name="update_060_androidauto_title">Neues Update: Android Auto</string>
<string name="update_060_androidauto_text">Mit diesem neuen Update kannst du EVMap nutzen, um Ladestationen in der Nähe auf unterstützen Autos direkt aus Android Auto zu finden. Öffne einfach die EVMap-App aus dem Menü von Android Auto.</string>
<string name="charge_price_format">%1$.2f %2$s</string>
<string name="charge_price_average_format">⌀ %1$.2f %2$s/kWh</string>
<string name="charge_price_kwh_format">%1$.2f %2$s/kWh</string>
<string name="chargeprice_select_connector">Anschluss auswählen</string>
<string name="chargeprice_provider_customer_tariff">Nur für Energiekunden</string>
<string name="percent_format">%.0f%%</string>
<string name="chargeprice_session_fee">Startgebühr</string>
<string name="chargeprice_per_kwh">pro kWh</string>
<string name="chargeprice_per_minute">pro min</string>
<string name="chargeprice_blocking_fee">Blockiergeb. >%s</string>
<string name="chargeprice_no_tariffs_found">Keine geeigneten Tarife für diese Ladestation bei Chargeprice.app gefunden.</string>
<string name="powered_by_chargeprice">powered by Chargeprice</string>
<string name="chargeprice_base_fee">Fixkosten: %1$.2f %2$s/Monat</string>
<string name="chargeprice_min_spend">Mindestumsatz: %1$.2f %2$s/Monat</string>
<string name="settings_chargeprice">Preisvergleich</string>
<string name="pref_my_vehicle">Mein Fahrzeug</string>
<string name="pref_chargeprice_no_base_fee">Nur Tarife ohne monatliche Gebühren</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Exklusive Energiekunden-Tarife anzeigen</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Einige Anbieter bieten für ihre Kunden (z.B. Haushaltsstrom, Gas) günstigere Tarife an</string>
<string name="chargeprice_select_car_first">Bitte wähle zuerst dein Auto in den Einstellungen aus.</string>
<string name="chargeprice_battery_range">Laden von %1$.0f%% bis %2$.0f%%</string>
<string name="edit_on_goingelectric_info">Falls hier nur eine leere Seite erscheint, logge dich bitte zuerst bei GoingElectric.de ein.</string>
<string name="close">schließen</string>
<string name="chargeprice_title">Preisvergleich</string>
<string name="chargeprice_connection_error">Preise konnten nicht geladen werden</string>
<string name="chargeprice_no_compatible_connectors">Keiner der Anschlüsse dieser Ladestation ist mit deinem Fahrzeug kompatibel.</string>
<string name="pref_chargeprice_currency">Währung</string>
<string name="pref_my_tariffs">Meine Tarife</string>
<string name="chargeprice_all_tariffs_selected">alle Tarife ausgewählt</string>
<string-array name="pref_chargeprice_currency_names">
<item>Schweizer Franken (CHF)</item>
<item>Tschechische Krone (CZK)</item>
<item>Dänische Krone (DKK)</item>
<item>Euro (EUR)</item>
<item>Britisches Pfund (GBP)</item>
<item>Kroatische Kuna (HRK)</item>
<item>Ungarischer Forint (HUF)</item>
<item>Isländische Krone (ISK)</item>
<item>Norwegische Krone (NOK)</item>
<item>Polnischer Złoty (PLN)</item>
<item>Schwedische Krone (SEK)</item>
<item>US-Dollar (USD)</item>
</string-array>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d Tarif ausgewählt</item>
<item quantity="other">%d Tarife ausgewählt</item>
</plurals>
<string name="unknown_operator">Unbekannter Betreiber</string>
</resources>

View File

@@ -20,4 +20,32 @@
<item>on</item>
<item>off</item>
</string-array>
<string-array name="pref_chargeprice_currency_names">
<item>Swiss franc (CHF)</item>
<item>Czech koruna (CZK)</item>
<item>Danish krone (DKK)</item>
<item>Euro (EUR)</item>
<item>Pound sterling (GBP)</item>
<item>Croatian kuna (HRK)</item>
<item>Hungarian forint (HUF)</item>
<item>Icelandic króna (ISK)</item>
<item>Norwegian krone (NOK)</item>
<item>Polish złoty (PLN)</item>
<item>Swedish krona (SEK)</item>
<item>US dollar (USD)</item>
</string-array>
<string-array name="pref_chargeprice_currency_values" donottranslate="true">
<item>CHF</item>
<item>CZK</item>
<item>DKK</item>
<item>EUR</item>
<item>GBP</item>
<item>HRK</item>
<item>HUF</item>
<item>ISK</item>
<item>NOK</item>
<item>PLN</item>
<item>SEK</item>
<item>USD</item>
</string-array>
</resources>

View File

@@ -10,8 +10,15 @@
<color name="charger_11kw">#9e9e9e</color>
<color name="charger_low">#607d8b</color>
<color name="available">#4caf50</color>
<color name="charging">#00bcd4</color>
<color name="unavailable">#f44336</color>
<color name="unknown">#9e9e9e</color>
<color name="status_bar_scrim">#C3000000</color>
<color name="delete_red">#f44336</color>
<color name="android_auto_accent">#039be5</color>
<color name="chargeprice_alert">#DD2C00</color>
<color name="chargeprice_info">#2979FF</color>
<color name="chargeprice_lock">#546E7A</color>
<color name="chargeprice_star">#00C853</color>
<color name="chip_background">#1F000000</color>
</resources>

View File

@@ -3,6 +3,7 @@
<string name="title_activity_maps">EVMap</string>
<string name="connectors">Connectors</string>
<string name="no_maps_app_found">No navigation app found</string>
<string name="no_browser_app_found">No web browser found</string>
<string name="address">Address</string>
<string name="operator">Operator</string>
<string name="network">Network</string>
@@ -36,7 +37,7 @@
<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="copyright_summary">©20202021 Johan von Forstner</string>
<string name="other">Other</string>
<string name="privacy">Privacy Notice</string>
<string name="fav_add">Add to favorites</string>
@@ -145,8 +146,54 @@
<string name="save_as_profile">Save as profile</string>
<string name="save_profile_enter_name">Enter the name of the filter profile:</string>
<string name="filterprofiles_empty_state">You have not yet saved any filter profiles.</string>
<string name="welcome_to_evmap">Welcome to EVMap!</string>
<string name="welcome_1">Using EVMap, you can find electric vehicle chargers around you. EVMap uses the community-maintained database from GoingElectric.de, which focuses on chargers in Europe and the German-speaking countries. You can contribute to this database on the GoingElectric.de website.\n\nChargers are shown on the map in different colors, which correspond to their maximum charging power:</string>
<string name="welcome_2">EVMap is free and Open Source software. You can contribute to the development on GitHub or support me through donations. The corresponding links can be found under “About EVMap” in the menu.</string>
<string name="deleted_filterprofile">Deleted “%s”</string>
<string name="undo">Undo</string>
<string name="rename">Rename</string>
<string name="charging_barrierfree">Usable without registration</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d compatible payment method</item>
<item quantity="other">%d compatible payment methods</item>
</plurals>
<string name="navigate">Navigate</string>
<string name="verified">verified</string>
<string name="verified_desc">Charger verified by a member at the GoingElectric.de community — not necessarily working right now.</string>
<string name="update_060_androidauto_title">New update: Android Auto</string>
<string name="update_060_androidauto_text">With this new update, you can also use EVMap to find nearby chargers from within Android Auto on supported cars. Simply select the EVMap app in the Android Auto menu.</string>
<string name="charge_price_format">%2$s%1$.2f</string>
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWh</string>
<string name="charge_price_kwh_format">%2$s%1$.2f/kWh</string>
<string name="chargeprice_select_connector">Choose connector</string>
<string name="chargeprice_provider_customer_tariff">Only for provider customers</string>
<string name="edit_on_goingelectric_info">If only an empty page is showing here, please first log in to GoingElectric.de.</string>
<string name="percent_format">%.0f%%</string>
<string name="chargeprice_session_fee">session fee</string>
<string name="chargeprice_per_kwh">per kWh</string>
<string name="chargeprice_per_minute">per min</string>
<string name="chargeprice_blocking_fee">Blocking fee >%s</string>
<string name="chargeprice_no_tariffs_found">Chargeprice.app found no charging plans compatible with this charger.</string>
<string name="powered_by_chargeprice">powered by Chargeprice</string>
<string name="chargeprice_base_fee">Base fee: %2$s%1$.2f/month</string>
<string name="chargeprice_min_spend">Minimum spend: %2$s%1$.2f/month</string>
<string name="settings_chargeprice">Price comparison</string>
<string name="pref_my_vehicle">My vehicle</string>
<string name="pref_chargeprice_no_base_fee">Only show plans with no monthly fees</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Show customer-exclusive plans</string>
<string name="chargeprice_select_car_first">Please first select your car model in the settings.</string>
<string name="chargeprice_battery_range">Charge from %1$.0f%% to %2$.0f%%</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Some providers offer cheaper plans exclusively to their customers (e.g., household electricity, gas)</string>
<string name="close">close</string>
<string name="chargeprice_title">Prices</string>
<string name="chargeprice_connection_error">Could not load prices</string>
<string name="chargeprice_no_compatible_connectors">None of the connectors on this charging station is compatible with your vehicle.</string>
<string name="pref_chargeprice_currency">Currency</string>
<string name="pref_my_tariffs">My charging plans</string>
<string name="chargeprice_all_tariffs_selected">all plans selected</string>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d plan selected</item>
<item quantity="other">%d plans selected</item>
</plurals>
<string name="unknown_operator">Unknown operator</string>
</resources>

View File

@@ -26,4 +26,9 @@
<item name="android:windowIsFloating">false</item>
</style>
<style name="ChargepriceDialogAnimation">
<item name="android:windowEnterAnimation">@anim/chargeprice_dialog_enter</item>
<item name="android:windowExitAnimation">@anim/chargeprice_dialog_exit</item>
</style>
</resources>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="@string/settings_ui">
<ListPreference
android:key="language"
android:title="@string/pref_language"
@@ -38,4 +38,30 @@
android:defaultValue="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_chargeprice">
<ListPreference
android:key="chargeprice_my_vehicle"
android:title="@string/pref_my_vehicle" />
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_tariffs"
android:title="@string/pref_my_tariffs" />
<ListPreference
android:key="chargeprice_currency"
android:title="@string/pref_chargeprice_currency"
android:entries="@array/pref_chargeprice_currency_names"
android:entryValues="@array/pref_chargeprice_currency_values"
android:defaultValue="EUR"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
android:key="chargeprice_no_base_fee"
android:title="@string/pref_chargeprice_no_base_fee"
android:defaultValue="false"
app:singleLineTitle="false" />
<CheckBoxPreference
android:key="chargeprice_show_provider_customer_tariffs"
android:title="@string/pref_chargeprice_show_provider_customer_tariffs"
android:summary="@string/pref_chargeprice_show_provider_customer_tariffs_summary"
android:defaultValue="false"
app:singleLineTitle="false" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -1,12 +0,0 @@
package net.vonforst.evmap.api
import org.junit.Assert.assertEquals
import org.junit.Test
class UtilsTest {
@Test
fun testDistanceBetween() {
assertEquals(129412.71, distanceBetween(54.0, 9.0, 53.0, 8.0), 0.01)
}
}

View File

@@ -66,8 +66,7 @@ class NewMotionAvailabilityDetectorTest {
@Test
fun apiTest() {
for (chargepoint in listOf(2105L, 18284L)) {
val charger = api.getChargepointDetail(chargepoint)
.execute().body()!!
val charger = runBlocking { api.getChargepointDetail(chargepoint).body()!! }
.chargelocations[0] as ChargeLocation
println(charger)

View File

@@ -0,0 +1,77 @@
package net.vonforst.evmap.api.chargeprice
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.okResponse
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.Assert.assertEquals
import org.junit.Test
import java.net.HttpURLConnection
class ChargepriceApiTest {
val ge: GoingElectricApi
val webServer = MockWebServer()
val chargeprice: ChargepriceApi
init {
webServer.start()
val apikey = ""
val baseurl = webServer.url("/ge/").toString()
ge = GoingElectricApi.create(apikey, baseurl)
chargeprice = ChargepriceApi.create(
apikey,
webServer.url("/cp/").toString()
)
webServer.dispatcher = object : Dispatcher() {
val notFoundResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
override fun dispatch(request: RecordedRequest): MockResponse {
val segments = request.requestUrl!!.pathSegments
val urlHead = segments.subList(0, 2).joinToString("/")
when (urlHead) {
"ge/chargepoints" -> {
val id = request.requestUrl!!.queryParameter("ge_id")
return okResponse("/chargers/$id.json")
}
"cp/charge_prices" -> {
val body = request.body.readUtf8()
return okResponse("/chargeprice/2105.json")
}
else -> return notFoundResponse
}
}
}
}
private fun readResource(s: String) =
ChargepriceApiTest::class.java.getResource(s)?.readText()
@ExperimentalCoroutinesApi
@Test
fun apiTest() {
for (chargepoint in listOf(2105L, 18284L)) {
val charger = runBlocking { ge.getChargepointDetail(chargepoint).body()!! }
.chargelocations[0] as ChargeLocation
println(charger)
runBlocking {
val result = chargeprice.getChargePrices(
ChargepriceRequest().apply {
dataAdapter = "going_electric"
station =
ChargepriceStation.fromGoingelectric(charger, listOf("Typ2", "Schuko"))
options = ChargepriceOptions(energy = 22.0, duration = 60)
}, "en"
)
assertEquals(25, result.size)
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More