Compare commits

..

71 Commits
0.4.3 ... 0.7.1

Author SHA1 Message Date
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
103 changed files with 5886 additions and 427 deletions

View File

@@ -5,6 +5,7 @@ env:
- 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
@@ -34,7 +35,7 @@ cache:
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.
@@ -32,10 +32,10 @@ Development setup
The App is developed using Android Studio.
For testing the app, you need to obtain API Keys for the
For testing the app, you need to obtain free API Keys for the
[GoingElectric API](https://www.goingelectric.de/stromtankstellen/api/)
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 APIs 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:
@@ -44,6 +44,9 @@ 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>

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

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 30
versionCode 37
versionName "0.4.3"
versionCode 45
versionName "0.7.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -85,6 +85,13 @@ 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
}
}
}
@@ -92,11 +99,10 @@ 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.core:core-ktx:1.5.0-rc01'
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.core:core:1.3.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'
@@ -109,6 +115,8 @@ 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 '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"
@@ -119,8 +127,11 @@ dependencies {
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-rc01'
// AnyMaps
def anyMapsVersion = '7753eeb7b0'
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"
@@ -180,3 +191,15 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
}
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,664 @@
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)
}
}
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

@@ -38,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

@@ -9,6 +9,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 +19,15 @@ 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
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 +65,66 @@ 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 pos = intent.data?.schemeSpecificPart?.split("?")?.get(0)
val query = intent.data?.query?.split("=")?.get(1)
val coords = pos?.split(",")?.map { it.toDoubleOrNull() }
if (coords != null && coords.size == 2) {
val lat = coords[0]
val lon = coords[1]
if (lat != null && lon != null && lat != 0.0 && lon != 0.0) {
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) {

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,91 @@ 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()
}
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).meta = meta
}
}
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

@@ -94,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,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,9 +1,9 @@
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
@@ -150,7 +150,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
"unspecified" -> "unknown"
"unknown" -> "unknown"
"saej1772" -> "unknown"
else -> throw IllegalArgumentException("unrecognized type ${connector.connectorType}")
else -> "unknown"
}
val status = when (statusStr) {
"Unavailable" -> ChargepointStatus.FAULTED

View File

@@ -0,0 +1,75 @@
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>
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,285 @@
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")
data class ChargepriceTariff(
val provider: String,
val name: String,
@field:Json(name = "direct_payment") val directPayment: Boolean,
@field:Json(name = "provider_customer_tariff") val providerCustomerTariff: Boolean,
@field:Json(name = "charge_card_id") val chargeCardId: String // GE charge card ID
) : Resource()
@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>
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
}
}
}
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
*
@@ -114,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,205 @@
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
}
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 -> {
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

@@ -33,6 +33,8 @@ 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 {
@@ -40,6 +42,7 @@ class FilterProfilesFragment : Fragment() {
}
})
private var deleteSnackbar: Snackbar? = null
private var toDelete: FilterProfile? = null
override fun onCreateView(
inflater: LayoutInflater,
@@ -64,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
) {
@@ -173,7 +176,7 @@ class FilterProfilesFragment : Fragment() {
}
})
val adapter = FilterProfilesAdapter(touchHelper, onDelete = { fp ->
adapter = FilterProfilesAdapter(touchHelper, onDelete = { fp ->
delete(fp)
}, onRename = { fp ->
showEditTextDialog(requireContext()) { dialog, input ->
@@ -183,7 +186,7 @@ class FilterProfilesFragment : Fragment() {
.setMessage(R.string.save_profile_enter_name)
.setPositiveButton(R.string.ok) { di, button ->
lifecycleScope.launch {
vm.insert(fp.copy(name = input.text.toString()))
vm.update(fp.copy(name = input.text.toString()))
}
}
.setNegativeButton(R.string.cancel) { di, button ->
@@ -192,7 +195,7 @@ class FilterProfilesFragment : Fragment() {
}
})
binding.filterProfilesList.apply {
this.adapter = adapter
this.adapter = this@FilterProfilesFragment.adapter
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
addItemDecoration(
@@ -210,19 +213,43 @@ class FilterProfilesFragment : Fragment() {
}
fun delete(fp: FilterProfile) {
vm.delete(fp.id)
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) {
vm.insert(fp.copy(id = 0))
}
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

@@ -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
@@ -50,6 +53,8 @@ import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
import com.mapzen.android.lost.api.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
@@ -72,15 +77,18 @@ 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.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 {
@@ -94,6 +102,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
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()
@@ -223,12 +232,19 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
(requireActivity() as MapsActivity).appBarConfiguration
)
if (!PreferenceDataSource(requireContext()).welcomeDialogShown) {
val prefs = PreferenceDataSource(requireContext())
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
}
}
}
@@ -237,6 +253,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val hostActivity = activity as? MapsActivity ?: return
hostActivity.fragmentCallback = this
vm.reloadPrefs()
if (requestingLocationUpdates && ContextCompat.checkSelfPermission(
requireContext(),
ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED && locationClient.isConnected
) {
requestLocationUpdates()
}
}
private fun setupClickListeners() {
@@ -273,8 +296,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
@@ -302,6 +327,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
}
@@ -357,7 +387,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()
}
}
@@ -411,6 +446,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 {
@@ -460,7 +497,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)
)
)
}
@@ -474,7 +511,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)
@@ -487,28 +524,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
@@ -677,6 +699,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 -> {
@@ -702,6 +737,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)
@@ -715,6 +752,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) {
@@ -722,28 +762,62 @@ 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 cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(LatLng(lat, lon), 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(lat, lon), null)
}
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)
vm.searchResult.value = PlaceWithBounds(latLng, null)
}
}
}
if (ContextCompat.checkSelfPermission(
requireContext(),
@@ -769,15 +843,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)
@@ -808,7 +885,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)
)
)
}
@@ -825,7 +902,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)
@@ -840,7 +917,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)
val marker = map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
@@ -1054,6 +1131,33 @@ 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() {
@@ -1065,11 +1169,52 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
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,35 @@ 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.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
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
@@ -28,6 +43,20 @@ class SettingsFragment : PreferenceFragmentCompat(),
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
myVehiclePreference = findPreference<ListPreference>("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}" }
}
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -51,6 +80,15 @@ 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
}
}
}
}
}

View File

@@ -25,7 +25,9 @@ class WelcomeDialogFragment : AppCompatDialogFragment() {
super.onViewCreated(view, savedInstanceState)
binding.btnOk.setOnClickListener {
PreferenceDataSource(requireContext()).welcomeDialogShown = true
val prefs = PreferenceDataSource(requireContext())
prefs.welcomeDialogShown = true
prefs.update060AndroidAutoDialogShown = true
dismiss()
}
}

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
@@ -73,9 +74,52 @@ class PreferenceDataSource(val context: Context) {
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 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()
}
}

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

@@ -7,17 +7,21 @@ 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
@@ -50,6 +54,25 @@ 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(
@@ -80,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")
@@ -171,19 +181,51 @@ fun setLinkify(textView: TextView, oldValue: Int, newValue: Int) {
}
}
@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))
@@ -204,4 +246,49 @@ fun availabilityText(status: List<ChargepointStatus>?): String? {
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)
}

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

@@ -43,8 +43,11 @@ class ClusterIconGenerator(context: Context) : IconGenerator(context) {
class ChargerIconGenerator(
val context: Context, val factory: BitmapDescriptorFactory,
val scaleResolution: Int = 20
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,
@@ -58,7 +61,6 @@ class ChargerIconGenerator(
// 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 oversize = 1.4f // increase to add padding for fault icon or scale > 1
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
@@ -110,12 +112,30 @@ class ChargerIconGenerator(
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)!!
@@ -123,17 +143,22 @@ class ChargerIconGenerator(
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)
@@ -142,8 +167,8 @@ class ChargerIconGenerator(
canvas.scale(
scale,
scale,
leftPadding + vd.intrinsicWidth / 2f,
topPadding + vd.intrinsicHeight.toFloat()
leftPadding + width / 2f,
topPadding + height.toFloat()
)
vd.draw(canvas)
@@ -153,8 +178,8 @@ class ChargerIconGenerator(
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)
@@ -164,7 +189,7 @@ class ChargerIconGenerator(
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

View File

@@ -0,0 +1,34 @@
package net.vonforst.evmap.utils
import android.location.Location
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)
}
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
}

View File

@@ -0,0 +1,198 @@
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 = listOf(20f, 80f)
}
}
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 {
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 })
}
}
}
}
}
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
)
}, 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

@@ -33,6 +33,12 @@ class FilterProfilesViewModel(application: Application) : AndroidViewModel(appli
}
}
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,33 @@
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 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()
}
}
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)
}
}
}
}

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,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

@@ -35,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;" />
@@ -66,14 +70,16 @@
<TextView
android:id="@+id/txtName"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
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_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" />
@@ -116,12 +122,12 @@
android:gravity="end"
android:maxLines="1"
android:padding="2dp"
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(BindingAdaptersKt.flatten(availability.data.status.values())), charger.data.totalChargepoints)}"
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:goneUnlessAnimated="@{availability.data != null &amp;&amp; !expanded}"
app:goneUnless="@{availability.data != null}"
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"
@@ -308,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,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

@@ -63,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" />
@@ -139,6 +140,7 @@
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}"

View File

@@ -0,0 +1,154 @@
<?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" />
<variable
name="item"
type="ChargePrice" />
<variable
name="meta"
type="ChargepriceChargepointMeta" />
</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"
android:background="?selectableItemBackground">
<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.provider.equals(item.tariffName)}"
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/txtTitle"
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/txtContent"
android:id="@+id/txtProvider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
@@ -60,8 +60,8 @@
app:goneUnless="@{item.detailText != null}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintTop_toBottomOf="@+id/txtTitle"
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/txtTitle"
android:id="@+id/txtTariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
@@ -54,7 +54,7 @@
tools:srcCompat="@drawable/ic_address" />
<TextView
android:id="@+id/txtContent"
android:id="@+id/txtProvider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
@@ -66,8 +66,8 @@
app:goneUnless="@{item.detailText != null}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/expandToggle"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintTop_toBottomOf="@+id/txtTitle"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtTariff"
app:layout_constraintVertical_bias="0.0"
tools:text="Lorem ipsum" />
@@ -82,8 +82,8 @@
app:goneUnless="@{expandToggle.checked}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintTop_toBottomOf="@+id/txtContent" />
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtProvider" />
<include
android:id="@+id/hours_tue"
@@ -96,7 +96,7 @@
app:dayOfWeek="@{DayOfWeek.TUESDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_mon" />
<include
@@ -110,7 +110,7 @@
app:dayOfWeek="@{DayOfWeek.WEDNESDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_tue" />
<include
@@ -124,7 +124,7 @@
app:dayOfWeek="@{DayOfWeek.THURSDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_wed" />
<include
@@ -138,7 +138,7 @@
app:dayOfWeek="@{DayOfWeek.FRIDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_thu" />
<include
@@ -152,7 +152,7 @@
app:dayOfWeek="@{DayOfWeek.SATURDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_fri" />
<include
@@ -166,7 +166,7 @@
app:dayOfWeek="@{DayOfWeek.SUNDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_sat" />
<include
@@ -182,7 +182,7 @@
app:hours="@{item.hoursDays}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@id/hours_sun" />
<ToggleButton

View File

@@ -2,7 +2,8 @@
<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">
<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

@@ -31,9 +31,19 @@
app:enterAnim="@anim/fragment_fade_enter"
app:popEnterAnim="@anim/fragment_fade_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
<action
android:id="@+id/action_map_to_chargepriceFragment"
app:destination="@id/chargeprice"
app:exitAnim="@anim/fragment_fade_exit"
app:enterAnim="@anim/fragment_fade_enter"
app:popEnterAnim="@anim/fragment_fade_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
<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"
@@ -73,6 +83,19 @@
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="@anim/fragment_fade_exit"
app:enterAnim="@anim/fragment_fade_enter"
app:popEnterAnim="@anim/fragment_fade_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
</dialog>
<fragment
android:id="@+id/donate"
android:name="net.vonforst.evmap.fragment.DonateFragment"
@@ -83,6 +106,11 @@
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

@@ -37,7 +37,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>
@@ -152,8 +152,40 @@
<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>
<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">Could not load prices</string>
<string name="chargeprice_no_compatible_connectors">Keiner der Anschlüsse dieser Ladestation ist mit deinem Fahrzeug kompatibel.</string>
</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

@@ -36,7 +36,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>
@@ -151,8 +151,40 @@
<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>
</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,20 @@
android:defaultValue="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_chargeprice">
<ListPreference
android:key="chargeprice_my_vehicle"
android:title="@string/pref_my_vehicle" />
<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)
}
}
}
}

View File

@@ -53,7 +53,7 @@ class GoingElectricApiTest {
@Test
fun testLoadChargepointDetail() {
val response = api.getChargepointDetail(2105).execute()
val response = runBlocking { api.getChargepointDetail(2105) }
assertTrue(response.isSuccessful)
val body = response.body()!!
assertEquals("ok", body.status)

View File

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

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
Neue Features:
- Kartenausschnitt folgt der aktuellen Position
- Verfügbarkeitsanzeige in der Kartenansicht beinhaltet nur die per Filter gewählten Anschlüsse (z.B. nur CCS)
- Links zu https://www.goingelectric.de/stromtankstellen können in EVMap geöffnet werden
- Geteilte Standorte (z.B. aus Messenger-Apps) können in EVMap geöffnet werden
Fehlerkorrekturen:
- Filtereinstellungen wurden bei Umbenennen eines Filterprofils fälschlicherweise gelöscht
- Ausgewählter Kartentyp (Satellit, Gelände, Standard) bleibt beim App-Neustart erhalten
- Copyright-Jahr aktualisiert

View File

@@ -0,0 +1,7 @@
Neue Features:
- Unterstützung für Android Auto
- Von der GoingElectric.de-Community verifizierte Ladestationen werden markiert
Verbesserungen:
- Aktivierte Verkehrsdaten auf der Karte werden nach App-Neustart beibehalten
- Abstürze behoben

View File

@@ -0,0 +1,5 @@
Verbesserungen:
- Kartentypenmenü wird bei Verschieben der Karte automatisch geschlossen
- Android Auto: Suchradius vergrößert, wenn wenige Ladestationen in der Nähe sind
- Verifizierungstatus wird unabhängig von Störungsmeldungen angezeigt
- Abstürze behoben

View File

@@ -0,0 +1,9 @@
Neue Features:
- Native Integration von Chargeprice.app
- Live-Daten: Unterscheidung zwischen belegten (türkis) und defekten (rot) Ladestationen
- Information, ob Laden ohne Registrierung möglich ist, wird angezeigt
Verbesserungen:
- Absturz auf Android Auto behoben, wenn eine Ladestation kein Foto hat
- Verbesserung für das Teilen von Links und Standorten zu EVMap
- Hinweis zur Behebung des Fehlers, dass beim Tippen auf den Bearbeiten-Knopf nur eine leere Seite angezeigt wird, hinzugefügt

View File

@@ -0,0 +1,3 @@
Verbesserungen:
- Kleinere Verbesserungen für die Chargeprice.app-Integration
- Verschiedene Abstürze behoben

View File

@@ -10,6 +10,7 @@ Funktionen:
- Suche nach Orten
- Erweiterte Filterfunktionen
- Favoritenliste, auch mit Anzeige der Verfügbarkeit
- Unterstützung für Android Auto
- Keine nervige Werbung
EVMap ist ein Open-Source-Projekt und unter https://github.com/johan12345/EVMap zu finden.

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

@@ -0,0 +1,10 @@
New Features:
- Map follows current location
- Availability indicator in map view only shows currently filtered connectors (e.g. only CCS)
- Links to https://www.goingelectric.de/stromtankstellen can be opened in EVMap
- Shared locations (e.g. from messenger apps) can be opened in EVMap
Bugfixes:
- Filter settings would be deleted when renaming a saved filter profile
- Selected map type (Default, Satellite, Terrain) will be kept across app restarts
- Updated copyright year

View File

@@ -0,0 +1,7 @@
New Features:
- Android Auto support
- Chargers verified by the GoingElectric.de community are marked
Improvements:
- Enabled traffic data on map is preserved after app restart
- Fixed crashes

View File

@@ -0,0 +1,5 @@
Improvements:
- Close map type menu when moving map
- Android Auto: Increased search radius when few chargers are closeby
- Verification status will be shown independently of fault reports
- Fixed crashes

View File

@@ -0,0 +1,9 @@
New Features:
- Native integration of Chargeprice.app
- Real-time data: Differentiate between occupied and broken chargers
- Add information if charging is possible without registration
Improvements:
- Fixed crash on Android Auto for chargers without photo
- Improvements for sharing locations and opening links in EVMap
- Add note about how to fix the problem that only an empty page is shown when tapping the "edit" button

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