diff --git a/.android.ort.yml b/.android.ort.yml new file mode 100644 index 0000000..a7653ba --- /dev/null +++ b/.android.ort.yml @@ -0,0 +1,5 @@ +excludes: + paths: + - pattern: "ios/**" + reason: "OTHER" + comment: "This directory is part of iOS Reference App." diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ad9774 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# HERE SDK 4.0 for flutter +plugins/here_sdk diff --git a/.ios.ort.yml b/.ios.ort.yml new file mode 100644 index 0000000..7b8fe39 --- /dev/null +++ b/.ios.ort.yml @@ -0,0 +1,5 @@ +excludes: + paths: + - pattern: "android/**" + reason: "OTHER" + comment: "This directory is part of Android Reference App." diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..f0274b3 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 1aafb3a8b9b0c36241c5f5b34ee914770f015818 + channel: stable + +project_type: app diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..366557c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..bb5dd96 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# Contributing to the HERE SDK Reference Application for Flutter + +## Where to start? ## +- If you already used the HERE SDK Reference Application and found issues or having improvement ideas, awesome! [Please report these](https://github.com/heremaps/here-sdk-ref-app-flutter/issues/new)! +- You can pick an [issue](https://github.com/heremaps/here-sdk-ref-app-flutter/issues) if you like, some are marked as "good first issue" and should be easy to pick up. +- If you stumbled upon a bug or some unclear or incorrect documentation and you already have a fix, great! Open a [pull request](https://github.com/heremaps/here-sdk-ref-app-flutter/pulls)! + +## Signing each Commit + +Please sign off each commit of a pull request. There are two ways to do it: + +Automatically: Use `-s` (or `--signoff`) flag of `git commit` command, see example below: + +`$ git commit -s -m 'README.md: Fix minor spelling mistake'` + +Manually add `Signed-off-by:`, as shown in the example below: + +``` + README.md: Fix minor spelling mistake + + Signed-off-by: John Doe +``` + +Any Pull Request with commits that are not signed off will be rejected by the automatic +[DCO check](https://probot.github.io/apps/dco/). A DCO is lightweight way for a contributor to confirm that they wrote or otherwise have the right to submit code or documentation to a project. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..757fc78 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# Reference Application for the HERE SDK for Flutter (_Navigate Edition_) + +The reference application for the [HERE SDK for Flutter (_Navigate Edition_)](https://developer.here.com/documentation/flutter-sdk-navigate/) shows how a complex and release-ready project targeting iOS and Android devices may look like. You can use it as a source of inspiration for your own HERE SDK based projects - in parts or as a whole. + +## Overview + +With this blueprint reference application you can see how UX flows can be built for the HERE SDK - covering the main use cases from searching for POIs, planning and picking a route and finally starting the trip to your destination. + +- Learn how the [HERE SDK 4.x](https://developer.here.com/products/here-sdk) can be complemented with rich UI for your own application development. +- Discover how to avoid common pitfalls, master edge cases and benefit from optimized end user flows. +- All code using the HERE SDK is implemented in pure Dart following well-established clean code standards. +- On top, the reference application is enriched with tailored graphical assets - adapted for various screen sizes and ready to be used in your own projects under the [License](#license) you can find below. + +If you are looking for smaller bits & pieces or just want to get started with the integration of the HERE SDK into a simpler project, you may want to start looking into our [example apps](https://github.com/heremaps/here-sdk-examples/tree/master/examples/latest/navigate/flutter) selection including a stripped down [hello_map_app](https://github.com/heremaps/here-sdk-examples/tree/master/examples/latest/navigate/flutter/hello_map_app) that accompanies the [Developer's Guide](https://developer.here.com/documentation/flutter-sdk-navigate/) for the HERE SDK. + +The reference application hosted in this repo focuses on how specific features can be implemented and used within the context of a full blown Flutter application - not only to show the usage of our APIs and the HERE SDK functionality as clear and understandable as possible, but also to show how complex Flutter projects in general can be organized and developed with production quality. + +### Supported features (so far): + +- [Search](https://developer.here.com/documentation/flutter-sdk-navigate/api_reference/search/search-library.html): Including suggestions, text search and search along a route corridor. +- [Routing](https://developer.here.com/documentation/flutter-sdk-navigate/api_reference/routing/routing-library.html): As of now, the reference application supports the following transport modes: car, truck, scooter and pedestrian. +- [Turn-By-Turn Navigation](https://developer.here.com/documentation/flutter-sdk-navigate/api_reference/navigation/navigation-library.html): Including maneuver instructions with visual feedback and voice guidance. + +![screenshots](assets/screenshots.png) + +## Get Started + +The reference application for the HERE SDK for Flutter (_Navigate Edition_) requires the following prerequisites: + +- The [HERE SDK for Flutter (_Navigate Edition_), Version 4.7.0.0](https://developer.here.com/documentation/flutter-sdk-navigate/4.7.0.0/dev_guide/index.html) is required and needs to be downloaded from the [HERE platform](https://platform.here.com). For now, the _Navigate Edition_ is only available upon request. Please [contact us](https://developer.here.com/help#how-can-we-help-you) to receive access including a set of evaluation credentials. +- If not already done, install the [Flutter SDK](https://flutter.dev/docs/get-started/install). We use [Version 2.0.6](https://flutter.dev/docs/development/tools/sdk/releases). Please note, due to the HERE SDK dependencies higher Flutter versions are not supported yet. + +On top you need an IDE of your choice. This could be a text editor or IDEs such as [Visual Studio Code](https://code.visualstudio.com/) with the [Flutter extension](https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter) or [Android Studio](https://developer.android.com/studio). We use Android Studio 4.1.3 for development. + +Note: If you want to compile, build & run for iOS devices, you also need to have [Xcode](https://developer.apple.com/xcode/) and [CocoaPods](https://cocoapods.org/) (Version 1.10.0 or higher) installed. We use Xcode 12.4 for development. If you only target Android devices, Xcode is _not_ required. + +### Add the HERE SDK Plugin + +Make sure you have cloned this repository and you have downloaded the HERE SDK for Flutter (_Navigate Edition_), see above. + +1. Unzip the downloaded HERE SDK for Flutter _package_. This folder contains various files including various documentation assets. +2. Inside the unzipped package you will find a TAR file that contains the HERE SDK _plugin_. +3. Unzip the TAR file and rename the folder to 'here_sdk'. Move it inside the [plugins](./plugins/) folder. + +### Build the Reference Application + +1. Set your HERE SDK credentials: The reference application does not require hardcoded credentials for the `AndroidManifest` or `Plist` file. Instead the credentials are read from your local environment. Therefore, you need to add two _system environment_ variables, `HERESDK_ACCESS_KEY_ID` and `HERESDK_ACCESS_KEY_SECRET`. For example, from a MacOS terminal execute: + + - `export HERESDK_ACCESS_KEY_ID="YOUR_ACCESS_KEY_ID"` + - `export HERESDK_ACCESS_KEY_SECRET="YOUR_ACCESS_KEY_SECRET"` + + You may need to restart the terminal application to take effect. + + Note that for iOS builds an extra step is needed: Execute the bash script [setup_ios_here_sdk_keys.sh](./ios/setup_ios_here_sdk_keys.sh) with `setup_ios_here_sdk_keys.sh` from within the `iOS` folder of this repository. As a result, a new file should be created under `ios/Flutter/GeneratedKeys.xcconfig` which will contain your credentials. + +2. Go to the repository root folder which contains the `pubspec.yaml` and run the terminal command `flutter pub get` to fetch the required dependencies. + +3. Open the project in your IDE of choice and execute the Flutter project for your target platform. + +#### How to build Flutter apps for Android and iOS + +If you are new to Flutter, here are more detailed steps for you. You may also want to consult the official [Flutter](https://flutter.dev) site in general and the [Flutter SDK](https://flutter.dev/docs/development/tools/sdk/overview) documentation in particular first. + +- Build for Android: + - Build an Android APK by executing `flutter build apk` or use the command `flutter run` to build and run on an attached device. +- Build for iOS: + - Run `pod install` in the [ios folder](./ios/). + - Then go back to the repository root folder and type `flutter build ios` to build a Runner.app. Type `flutter run` to build and run on an attached device. + - You can open the `/repository root/ios/Runner.xcworkspace` project in Xcode and execute and debug from there. + - Note: You need to have valid _development certificates_ available to sign the app for device deployment. + +## Code Usage + +This is an open source project and you are free to use the code and selected assets in this repository for your own applications. For more details, see the [License](#license) section below. + +## Contribution + +You can contribute code back to this open source project and improve this code for other people. There are many ways to contribute to this project, whether you want to create an issue, submit bug reports or improve the documentation - we are happy to see your merge requests. Have a look into our [contribution guide](./CONTRIBUTING.md) and happy coding! + +When you plan to contribute, please follow our [code of conduct](./CODE_OF_CONDUCT.md). ![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg) + +## Get in Touch + +- If you have any questions on the reference application or the HERE SDK in general, please check the tag here-api on [stackoverflow.com](https://stackoverflow.com/questions/tagged/here-api). +- If you have more questions or need further help, please [contact us](https://developer.here.com/help). + +Thank you for using the reference application for the HERE SDK for Flutter (_Navigate Edition_). + +## HERE Notice + +We provide the code 'AS IS'. Using the source code comes not with any additional grant of customer support or specific feature development. For sure, we still support you and in case you have questions please [contact us](https://developer.here.com/help#how-can-we-help-you). Please note, if you integrate parts if this application or the HERE SDK itself then please do not forget to include the respective HERE notices to your project as well. Furthermore, we ask you not to re-sell the included icons. + +## License + +Copyright (C) 2020-2021 HERE Europe B.V. + +See the [LICENSE](./LICENSE) file in the root folder of this project for license details. + +For other use cases not listed in the license terms, please [contact us](https://developer.here.com/help). diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..ca5635d --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + exclude: + - plugins/** diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..0a741cb --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..5fbad9c --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,66 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +apply from: "${rootProject.projectDir}/readCredentials.gradle" + +android { + compileSdkVersion 29 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.RefApp" + minSdkVersion 21 + targetSdkVersion 29 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + manifestPlaceholders = readCredentials() + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..0921981 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5ec903 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/RefApp/FlutterForegroundService.kt b/android/app/src/main/kotlin/com/example/RefApp/FlutterForegroundService.kt new file mode 100644 index 0000000..1184e87 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/RefApp/FlutterForegroundService.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package com.example.RefApp + +import android.app.* +import android.content.Intent +import android.graphics.BitmapFactory +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat + +class FlutterForegroundService : Service() { + companion object { + const val START_FOREGROUND_ACTION = "com.example.RefApp.flutter_foreground_service.action.start_foreground" + const val UPDATE_FOREGROUND_ACTION = "com.example.RefApp.flutter_foreground_service.action.update_foreground" + const val STOP_FOREGROUND_ACTION = "com.example.RefApp.flutter_foreground_service.action.stop_foreground" + const val NOTIFICATION_CHANNEL_ID = "flutter_channel_id" + const val NOTIFICATION_CHANNEL_NAME = "flutter_foreground_service_channel" + const val ONGOING_NOTIFICATION_ID = 1 + + const val TITLE_ARG = "title" + const val CONTENT_ARG = "content" + const val LARGE_ICON_ARG = "large_icon" + const val SOUND_ENABLED_ARG = "sound_enabled" + } + + override fun onCreate() { + super.onCreate() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, + NotificationManager.IMPORTANCE_HIGH) + (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(channel) + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent == null || intent.action == null) { + return START_NOT_STICKY + } + + when (intent.action) { + START_FOREGROUND_ACTION -> { + val bundle = intent.extras ?: return START_NOT_STICKY + startForeground(ONGOING_NOTIFICATION_ID, createNotification(bundle)) + } + + UPDATE_FOREGROUND_ACTION -> { + val bundle = intent.extras ?: return START_NOT_STICKY + val nm = NotificationManagerCompat.from(this) + nm.notify(ONGOING_NOTIFICATION_ID, createNotification(bundle)) + } + + STOP_FOREGROUND_ACTION -> { + stopForeground(true) + stopSelf() + } + } + + return START_NOT_STICKY + } + + private fun createNotification(bundle: Bundle): Notification { + val pm = applicationContext.packageManager + val notificationIntent = pm.getLaunchIntentForPackage(applicationContext.packageName) + val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0) + + val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher_notification) + .setContentTitle(bundle.getString(TITLE_ARG)) + .setContentText(bundle.getString(CONTENT_ARG)) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setContentIntent(pendingIntent) + .setOngoing(true) + .setLocalOnly(true) + .setPriority(NotificationCompat.PRIORITY_MAX) + + if (bundle.getString(LARGE_ICON_ARG) != null) { + val bitmap = BitmapFactory.decodeFile(bundle.getString(LARGE_ICON_ARG)) + builder.setLargeIcon(bitmap) + } + + if (!bundle.getBoolean(SOUND_ENABLED_ARG, false)) { + builder.setNotificationSilent() + } + + return builder.build() + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } +} diff --git a/android/app/src/main/kotlin/com/example/RefApp/MainActivity.kt b/android/app/src/main/kotlin/com/example/RefApp/MainActivity.kt new file mode 100644 index 0000000..8348160 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/RefApp/MainActivity.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package com.example.RefApp + +import android.content.Intent +import android.os.Build +import com.example.RefApp.FlutterForegroundService.Companion.START_FOREGROUND_ACTION +import com.example.RefApp.FlutterForegroundService.Companion.STOP_FOREGROUND_ACTION +import com.example.RefApp.FlutterForegroundService.Companion.UPDATE_FOREGROUND_ACTION +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.android.SplashScreen +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +class MainActivity : FlutterActivity() { + companion object { + private const val CHANNEL = "com.example.RefApp/foreground_service_channel" + private const val START_SERVICE = "startService" + private const val STOP_SERVICE = "stopService" + private const val UPDATE_SERVICE = "updateService" + } + + override fun provideSplashScreen(): SplashScreen? { + return SplashScreen() + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> + when (call.method) { + START_SERVICE -> launchForegroundService(createIntent(call)) + + STOP_SERVICE -> stopForegroundService(createIntent(call)) + + UPDATE_SERVICE -> updateForegroundService(createIntent(call)) + + else -> result.notImplemented() + } + } + } + + private fun createIntent(call: MethodCall): Intent { + val intent = Intent(context, FlutterForegroundService::class.java) + + val title = call.argument(FlutterForegroundService.TITLE_ARG) + if (title != null) { + intent.putExtra(FlutterForegroundService.TITLE_ARG, title) + } + + val content = call.argument(FlutterForegroundService.CONTENT_ARG) + if (content != null) { + intent.putExtra(FlutterForegroundService.CONTENT_ARG, content) + } + + val largeIcon = call.argument(FlutterForegroundService.LARGE_ICON_ARG) + if (largeIcon != null) { + intent.putExtra(FlutterForegroundService.LARGE_ICON_ARG, largeIcon) + } + + val soundEnabled = call.argument(FlutterForegroundService.SOUND_ENABLED_ARG) + if (soundEnabled != null) { + intent.putExtra(FlutterForegroundService.SOUND_ENABLED_ARG, soundEnabled) + } + + return intent + } + + private fun launchForegroundService(intent: Intent) { + intent.action = START_FOREGROUND_ACTION; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + + private fun updateForegroundService(intent: Intent) { + intent.action = UPDATE_FOREGROUND_ACTION + context.startService(intent) + } + + private fun stopForegroundService(intent: Intent) { + intent.action = STOP_FOREGROUND_ACTION + context.startService(intent) + } +} diff --git a/android/app/src/main/kotlin/com/example/RefApp/SplashScreen.kt b/android/app/src/main/kotlin/com/example/RefApp/SplashScreen.kt new file mode 100644 index 0000000..8d54e82 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/RefApp/SplashScreen.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package com.example.RefApp + +import android.content.Context +import android.os.Bundle +import android.view.View +import io.flutter.embedding.android.SplashScreen + +class SplashScreen : SplashScreen { + override fun createSplashView(context: Context, savedInstanceState: Bundle?): View? { + return SplashScreenView(context) + } + + override fun transitionToFlutter(onTransitionComplete: Runnable) { + onTransitionComplete.run() + } +} diff --git a/android/app/src/main/kotlin/com/example/RefApp/SplashScreenView.kt b/android/app/src/main/kotlin/com/example/RefApp/SplashScreenView.kt new file mode 100644 index 0000000..726eb5c --- /dev/null +++ b/android/app/src/main/kotlin/com/example/RefApp/SplashScreenView.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package com.example.RefApp + +import android.content.Context +import android.view.LayoutInflater +import android.widget.LinearLayout + +class SplashScreenView(context: Context?) : LinearLayout(context) { + init { + LayoutInflater.from(context).inflate(R.layout.splash_screen, this, true) + } +} diff --git a/android/app/src/main/res/drawable/ic_avatar.xml b/android/app/src/main/res/drawable/ic_avatar.xml new file mode 100644 index 0000000..6a97979 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_avatar.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..a55bf66 --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,4 @@ + + + + diff --git a/android/app/src/main/res/layout/splash_screen.xml b/android/app/src/main/res/layout/splash_screen.xml new file mode 100644 index 0000000..8e85094 --- /dev/null +++ b/android/app/src/main/res/layout/splash_screen.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..70afaf3 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_notification.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_notification.png new file mode 100644 index 0000000..21af79c Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_notification.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..4f1d7e3 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_notification.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_notification.png new file mode 100644 index 0000000..cc63d03 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_notification.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..e4f8b4c Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_notification.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_notification.png new file mode 100644 index 0000000..99d3643 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_notification.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..3007d9e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_notification.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_notification.png new file mode 100644 index 0000000..07f5294 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_notification.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..a19d525 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_notification.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_notification.png new file mode 100644 index 0000000..d35adb0 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_notification.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..adb7d4c --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #FF262F3A + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..d9b0ef5 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + HERE SDK\nReference Application\nfor Flutter + Version 1.0.0 + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..1f83a33 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..0921981 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..3100ad2 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..a673820 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..296b146 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/android/readCredentials.gradle b/android/readCredentials.gradle new file mode 100644 index 0000000..40a5329 --- /dev/null +++ b/android/readCredentials.gradle @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +ext { + readCredentials = { -> + def keyIdName = 'HERESDK_ACCESS_KEY_ID' + def keySecretName = 'HERESDK_ACCESS_KEY_SECRET' + + def keyId = System.env[keyIdName] + def keySecret = System.env[keySecretName] + + if (keyId != null && keySecret != null) { + return [HERESDK_ACCESS_KEY_ID: keyId.trim(), + HERESDK_ACCESS_KEY_SECRET: keySecret.trim()] + } + + def exceptionText = "To build this application, an access key id and secret are required to be defined. Please create environment\n" + + "variables named ${keyIdName} and ${keySecretName} which contain these values and try again.\n" + + throw new GradleException(exceptionText) + } +} diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/assets/app_logo.svg b/assets/app_logo.svg new file mode 100644 index 0000000..b39acc4 --- /dev/null +++ b/assets/app_logo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/atm_icon.svg b/assets/atm_icon.svg new file mode 100644 index 0000000..0bfbaad --- /dev/null +++ b/assets/atm_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/atm_icon_disabled.svg b/assets/atm_icon_disabled.svg new file mode 100644 index 0000000..86f0c22 --- /dev/null +++ b/assets/atm_icon_disabled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/car.svg b/assets/car.svg new file mode 100644 index 0000000..19b6a90 --- /dev/null +++ b/assets/car.svg @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/assets/depart_marker.svg b/assets/depart_marker.svg new file mode 100644 index 0000000..c168832 --- /dev/null +++ b/assets/depart_marker.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/eat_and_drink_icon.svg b/assets/eat_and_drink_icon.svg new file mode 100644 index 0000000..88ce7c2 --- /dev/null +++ b/assets/eat_and_drink_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/eat_and_drink_icon_disabled.svg b/assets/eat_and_drink_icon_disabled.svg new file mode 100644 index 0000000..eee66b1 --- /dev/null +++ b/assets/eat_and_drink_icon_disabled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/fueling_station_icon.svg b/assets/fueling_station_icon.svg new file mode 100644 index 0000000..3fd618f --- /dev/null +++ b/assets/fueling_station_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/fueling_station_icon_disabled.svg b/assets/fueling_station_icon_disabled.svg new file mode 100644 index 0000000..9b1caae --- /dev/null +++ b/assets/fueling_station_icon_disabled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/gps.svg b/assets/gps.svg new file mode 100644 index 0000000..b9f2bab --- /dev/null +++ b/assets/gps.svg @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/assets/maneuver.svg b/assets/maneuver.svg new file mode 100644 index 0000000..206c6a4 --- /dev/null +++ b/assets/maneuver.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/maneuvers/dark/arrive.svg b/assets/maneuvers/dark/arrive.svg new file mode 100644 index 0000000..8403380 --- /dev/null +++ b/assets/maneuvers/dark/arrive.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/maneuvers/dark/continueOn.svg b/assets/maneuvers/dark/continueOn.svg new file mode 100644 index 0000000..08c0160 --- /dev/null +++ b/assets/maneuvers/dark/continueOn.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/maneuvers/dark/depart.svg b/assets/maneuvers/dark/depart.svg new file mode 100644 index 0000000..49d39e3 --- /dev/null +++ b/assets/maneuvers/dark/depart.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/maneuvers/dark/ferry.svg b/assets/maneuvers/dark/ferry.svg new file mode 100644 index 0000000..3e7c9e9 --- /dev/null +++ b/assets/maneuvers/dark/ferry.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/maneuvers/dark/leftExit.svg b/assets/maneuvers/dark/leftExit.svg new file mode 100644 index 0000000..bfd9743 --- /dev/null +++ b/assets/maneuvers/dark/leftExit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/maneuvers/dark/leftFork.svg b/assets/maneuvers/dark/leftFork.svg new file mode 100644 index 0000000..debf0d5 --- /dev/null +++ b/assets/maneuvers/dark/leftFork.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/maneuvers/dark/leftRamp.svg b/assets/maneuvers/dark/leftRamp.svg new file mode 100644 index 0000000..fc085a6 --- /dev/null +++ b/assets/maneuvers/dark/leftRamp.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/maneuvers/dark/leftRoundaboutExit1.svg b/assets/maneuvers/dark/leftRoundaboutExit1.svg new file mode 100644 index 0000000..2d44515 --- /dev/null +++ b/assets/maneuvers/dark/leftRoundaboutExit1.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/maneuvers/dark/leftRoundaboutExit10.svg b/assets/maneuvers/dark/leftRoundaboutExit10.svg new file mode 100644 index 0000000..34b9d0f --- /dev/null +++ b/assets/maneuvers/dark/leftRoundaboutExit10.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/leftRoundaboutExit11.svg b/assets/maneuvers/dark/leftRoundaboutExit11.svg new file mode 100644 index 0000000..918dce5 --- /dev/null +++ b/assets/maneuvers/dark/leftRoundaboutExit11.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/leftRoundaboutExit12.svg b/assets/maneuvers/dark/leftRoundaboutExit12.svg new file mode 100644 index 0000000..e399d59 --- /dev/null +++ b/assets/maneuvers/dark/leftRoundaboutExit12.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/leftRoundaboutExit2.svg b/assets/maneuvers/dark/leftRoundaboutExit2.svg new file mode 100644 index 0000000..74fa41d --- /dev/null +++ b/assets/maneuvers/dark/leftRoundaboutExit2.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/leftRoundaboutExit3.svg b/assets/maneuvers/dark/leftRoundaboutExit3.svg new file mode 100644 index 0000000..5c262ea --- /dev/null +++ b/assets/maneuvers/dark/leftRoundaboutExit3.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/leftRoundaboutExit4.svg b/assets/maneuvers/dark/leftRoundaboutExit4.svg new file mode 100644 index 0000000..3bc6ce7 --- /dev/null +++ b/assets/maneuvers/dark/leftRoundaboutExit4.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/leftRoundaboutExit5.svg b/assets/maneuvers/dark/leftRoundaboutExit5.svg new file mode 100644 index 0000000..a8b9553 --- /dev/null +++ b/assets/maneuvers/dark/leftRoundaboutExit5.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/leftRoundaboutExit6.svg b/assets/maneuvers/dark/leftRoundaboutExit6.svg new file mode 100644 index 0000000..5cf2846 --- /dev/null +++ b/assets/maneuvers/dark/leftRoundaboutExit6.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/leftRoundaboutExit7.svg b/assets/maneuvers/dark/leftRoundaboutExit7.svg new file mode 100644 index 0000000..0822876 --- /dev/null +++ b/assets/maneuvers/dark/leftRoundaboutExit7.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/leftRoundaboutExit8.svg b/assets/maneuvers/dark/leftRoundaboutExit8.svg new file mode 100644 index 0000000..291fcfe --- /dev/null +++ b/assets/maneuvers/dark/leftRoundaboutExit8.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/leftRoundaboutExit9.svg b/assets/maneuvers/dark/leftRoundaboutExit9.svg new file mode 100644 index 0000000..56c162c --- /dev/null +++ b/assets/maneuvers/dark/leftRoundaboutExit9.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/leftTurn.svg b/assets/maneuvers/dark/leftTurn.svg new file mode 100644 index 0000000..5817a0a --- /dev/null +++ b/assets/maneuvers/dark/leftTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/leftUTurn.svg b/assets/maneuvers/dark/leftUTurn.svg new file mode 100644 index 0000000..bd9fe03 --- /dev/null +++ b/assets/maneuvers/dark/leftUTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/middleFork.svg b/assets/maneuvers/dark/middleFork.svg new file mode 100644 index 0000000..05aeba0 --- /dev/null +++ b/assets/maneuvers/dark/middleFork.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/png/arrive.png b/assets/maneuvers/dark/png/arrive.png new file mode 100644 index 0000000..fed4d3a Binary files /dev/null and b/assets/maneuvers/dark/png/arrive.png differ diff --git a/assets/maneuvers/dark/png/continueOn.png b/assets/maneuvers/dark/png/continueOn.png new file mode 100644 index 0000000..5738a5e Binary files /dev/null and b/assets/maneuvers/dark/png/continueOn.png differ diff --git a/assets/maneuvers/dark/png/depart.png b/assets/maneuvers/dark/png/depart.png new file mode 100644 index 0000000..6d695f7 Binary files /dev/null and b/assets/maneuvers/dark/png/depart.png differ diff --git a/assets/maneuvers/dark/png/ferry.png b/assets/maneuvers/dark/png/ferry.png new file mode 100644 index 0000000..1e3ee55 Binary files /dev/null and b/assets/maneuvers/dark/png/ferry.png differ diff --git a/assets/maneuvers/dark/png/leftExit.png b/assets/maneuvers/dark/png/leftExit.png new file mode 100644 index 0000000..5aa707a Binary files /dev/null and b/assets/maneuvers/dark/png/leftExit.png differ diff --git a/assets/maneuvers/dark/png/leftFork.png b/assets/maneuvers/dark/png/leftFork.png new file mode 100644 index 0000000..cb668d2 Binary files /dev/null and b/assets/maneuvers/dark/png/leftFork.png differ diff --git a/assets/maneuvers/dark/png/leftRamp.png b/assets/maneuvers/dark/png/leftRamp.png new file mode 100644 index 0000000..23bfc13 Binary files /dev/null and b/assets/maneuvers/dark/png/leftRamp.png differ diff --git a/assets/maneuvers/dark/png/leftRoundaboutExit1.png b/assets/maneuvers/dark/png/leftRoundaboutExit1.png new file mode 100644 index 0000000..4f1323c Binary files /dev/null and b/assets/maneuvers/dark/png/leftRoundaboutExit1.png differ diff --git a/assets/maneuvers/dark/png/leftRoundaboutExit10.png b/assets/maneuvers/dark/png/leftRoundaboutExit10.png new file mode 100644 index 0000000..81c70f2 Binary files /dev/null and b/assets/maneuvers/dark/png/leftRoundaboutExit10.png differ diff --git a/assets/maneuvers/dark/png/leftRoundaboutExit11.png b/assets/maneuvers/dark/png/leftRoundaboutExit11.png new file mode 100644 index 0000000..2d6e3db Binary files /dev/null and b/assets/maneuvers/dark/png/leftRoundaboutExit11.png differ diff --git a/assets/maneuvers/dark/png/leftRoundaboutExit12.png b/assets/maneuvers/dark/png/leftRoundaboutExit12.png new file mode 100644 index 0000000..ec1cac8 Binary files /dev/null and b/assets/maneuvers/dark/png/leftRoundaboutExit12.png differ diff --git a/assets/maneuvers/dark/png/leftRoundaboutExit2.png b/assets/maneuvers/dark/png/leftRoundaboutExit2.png new file mode 100644 index 0000000..057a906 Binary files /dev/null and b/assets/maneuvers/dark/png/leftRoundaboutExit2.png differ diff --git a/assets/maneuvers/dark/png/leftRoundaboutExit3.png b/assets/maneuvers/dark/png/leftRoundaboutExit3.png new file mode 100644 index 0000000..d26ce0c Binary files /dev/null and b/assets/maneuvers/dark/png/leftRoundaboutExit3.png differ diff --git a/assets/maneuvers/dark/png/leftRoundaboutExit4.png b/assets/maneuvers/dark/png/leftRoundaboutExit4.png new file mode 100644 index 0000000..9d576c8 Binary files /dev/null and b/assets/maneuvers/dark/png/leftRoundaboutExit4.png differ diff --git a/assets/maneuvers/dark/png/leftRoundaboutExit5.png b/assets/maneuvers/dark/png/leftRoundaboutExit5.png new file mode 100644 index 0000000..6740670 Binary files /dev/null and b/assets/maneuvers/dark/png/leftRoundaboutExit5.png differ diff --git a/assets/maneuvers/dark/png/leftRoundaboutExit6.png b/assets/maneuvers/dark/png/leftRoundaboutExit6.png new file mode 100644 index 0000000..165d18f Binary files /dev/null and b/assets/maneuvers/dark/png/leftRoundaboutExit6.png differ diff --git a/assets/maneuvers/dark/png/leftRoundaboutExit7.png b/assets/maneuvers/dark/png/leftRoundaboutExit7.png new file mode 100644 index 0000000..8313273 Binary files /dev/null and b/assets/maneuvers/dark/png/leftRoundaboutExit7.png differ diff --git a/assets/maneuvers/dark/png/leftRoundaboutExit8.png b/assets/maneuvers/dark/png/leftRoundaboutExit8.png new file mode 100644 index 0000000..5591429 Binary files /dev/null and b/assets/maneuvers/dark/png/leftRoundaboutExit8.png differ diff --git a/assets/maneuvers/dark/png/leftRoundaboutExit9.png b/assets/maneuvers/dark/png/leftRoundaboutExit9.png new file mode 100644 index 0000000..582ffed Binary files /dev/null and b/assets/maneuvers/dark/png/leftRoundaboutExit9.png differ diff --git a/assets/maneuvers/dark/png/leftTurn.png b/assets/maneuvers/dark/png/leftTurn.png new file mode 100644 index 0000000..3a95ee4 Binary files /dev/null and b/assets/maneuvers/dark/png/leftTurn.png differ diff --git a/assets/maneuvers/dark/png/leftUTurn.png b/assets/maneuvers/dark/png/leftUTurn.png new file mode 100644 index 0000000..4525903 Binary files /dev/null and b/assets/maneuvers/dark/png/leftUTurn.png differ diff --git a/assets/maneuvers/dark/png/middleFork.png b/assets/maneuvers/dark/png/middleFork.png new file mode 100644 index 0000000..c085184 Binary files /dev/null and b/assets/maneuvers/dark/png/middleFork.png differ diff --git a/assets/maneuvers/dark/png/rightExit.png b/assets/maneuvers/dark/png/rightExit.png new file mode 100644 index 0000000..a290bf1 Binary files /dev/null and b/assets/maneuvers/dark/png/rightExit.png differ diff --git a/assets/maneuvers/dark/png/rightFork.png b/assets/maneuvers/dark/png/rightFork.png new file mode 100644 index 0000000..bd89ff2 Binary files /dev/null and b/assets/maneuvers/dark/png/rightFork.png differ diff --git a/assets/maneuvers/dark/png/rightRamp.png b/assets/maneuvers/dark/png/rightRamp.png new file mode 100644 index 0000000..4dd1ecc Binary files /dev/null and b/assets/maneuvers/dark/png/rightRamp.png differ diff --git a/assets/maneuvers/dark/png/rightRoundaboutExit1.png b/assets/maneuvers/dark/png/rightRoundaboutExit1.png new file mode 100644 index 0000000..80641ef Binary files /dev/null and b/assets/maneuvers/dark/png/rightRoundaboutExit1.png differ diff --git a/assets/maneuvers/dark/png/rightRoundaboutExit10.png b/assets/maneuvers/dark/png/rightRoundaboutExit10.png new file mode 100644 index 0000000..96bb2f3 Binary files /dev/null and b/assets/maneuvers/dark/png/rightRoundaboutExit10.png differ diff --git a/assets/maneuvers/dark/png/rightRoundaboutExit11.png b/assets/maneuvers/dark/png/rightRoundaboutExit11.png new file mode 100644 index 0000000..3658cf6 Binary files /dev/null and b/assets/maneuvers/dark/png/rightRoundaboutExit11.png differ diff --git a/assets/maneuvers/dark/png/rightRoundaboutExit12.png b/assets/maneuvers/dark/png/rightRoundaboutExit12.png new file mode 100644 index 0000000..4739c0c Binary files /dev/null and b/assets/maneuvers/dark/png/rightRoundaboutExit12.png differ diff --git a/assets/maneuvers/dark/png/rightRoundaboutExit2.png b/assets/maneuvers/dark/png/rightRoundaboutExit2.png new file mode 100644 index 0000000..a3f868e Binary files /dev/null and b/assets/maneuvers/dark/png/rightRoundaboutExit2.png differ diff --git a/assets/maneuvers/dark/png/rightRoundaboutExit3.png b/assets/maneuvers/dark/png/rightRoundaboutExit3.png new file mode 100644 index 0000000..02790b7 Binary files /dev/null and b/assets/maneuvers/dark/png/rightRoundaboutExit3.png differ diff --git a/assets/maneuvers/dark/png/rightRoundaboutExit4.png b/assets/maneuvers/dark/png/rightRoundaboutExit4.png new file mode 100644 index 0000000..d9b750a Binary files /dev/null and b/assets/maneuvers/dark/png/rightRoundaboutExit4.png differ diff --git a/assets/maneuvers/dark/png/rightRoundaboutExit5.png b/assets/maneuvers/dark/png/rightRoundaboutExit5.png new file mode 100644 index 0000000..732f670 Binary files /dev/null and b/assets/maneuvers/dark/png/rightRoundaboutExit5.png differ diff --git a/assets/maneuvers/dark/png/rightRoundaboutExit6.png b/assets/maneuvers/dark/png/rightRoundaboutExit6.png new file mode 100644 index 0000000..19fcfe3 Binary files /dev/null and b/assets/maneuvers/dark/png/rightRoundaboutExit6.png differ diff --git a/assets/maneuvers/dark/png/rightRoundaboutExit7.png b/assets/maneuvers/dark/png/rightRoundaboutExit7.png new file mode 100644 index 0000000..d97d876 Binary files /dev/null and b/assets/maneuvers/dark/png/rightRoundaboutExit7.png differ diff --git a/assets/maneuvers/dark/png/rightRoundaboutExit8.png b/assets/maneuvers/dark/png/rightRoundaboutExit8.png new file mode 100644 index 0000000..fba856b Binary files /dev/null and b/assets/maneuvers/dark/png/rightRoundaboutExit8.png differ diff --git a/assets/maneuvers/dark/png/rightRoundaboutExit9.png b/assets/maneuvers/dark/png/rightRoundaboutExit9.png new file mode 100644 index 0000000..481f78f Binary files /dev/null and b/assets/maneuvers/dark/png/rightRoundaboutExit9.png differ diff --git a/assets/maneuvers/dark/png/rightTurn.png b/assets/maneuvers/dark/png/rightTurn.png new file mode 100644 index 0000000..fbaaa1a Binary files /dev/null and b/assets/maneuvers/dark/png/rightTurn.png differ diff --git a/assets/maneuvers/dark/png/rightUTurn.png b/assets/maneuvers/dark/png/rightUTurn.png new file mode 100644 index 0000000..72f0da9 Binary files /dev/null and b/assets/maneuvers/dark/png/rightUTurn.png differ diff --git a/assets/maneuvers/dark/png/sharpLeftTurn.png b/assets/maneuvers/dark/png/sharpLeftTurn.png new file mode 100644 index 0000000..295d737 Binary files /dev/null and b/assets/maneuvers/dark/png/sharpLeftTurn.png differ diff --git a/assets/maneuvers/dark/png/sharpRightTurn.png b/assets/maneuvers/dark/png/sharpRightTurn.png new file mode 100644 index 0000000..ce598b1 Binary files /dev/null and b/assets/maneuvers/dark/png/sharpRightTurn.png differ diff --git a/assets/maneuvers/dark/png/slightLeftTurn.png b/assets/maneuvers/dark/png/slightLeftTurn.png new file mode 100644 index 0000000..8d0de2f Binary files /dev/null and b/assets/maneuvers/dark/png/slightLeftTurn.png differ diff --git a/assets/maneuvers/dark/png/slightRightTurn.png b/assets/maneuvers/dark/png/slightRightTurn.png new file mode 100644 index 0000000..f414192 Binary files /dev/null and b/assets/maneuvers/dark/png/slightRightTurn.png differ diff --git a/assets/maneuvers/dark/rightExit.svg b/assets/maneuvers/dark/rightExit.svg new file mode 100644 index 0000000..1fe5045 --- /dev/null +++ b/assets/maneuvers/dark/rightExit.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightFork.svg b/assets/maneuvers/dark/rightFork.svg new file mode 100644 index 0000000..4db7965 --- /dev/null +++ b/assets/maneuvers/dark/rightFork.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightRamp.svg b/assets/maneuvers/dark/rightRamp.svg new file mode 100644 index 0000000..a86901b --- /dev/null +++ b/assets/maneuvers/dark/rightRamp.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightRoundaboutExit1.svg b/assets/maneuvers/dark/rightRoundaboutExit1.svg new file mode 100644 index 0000000..1273bab --- /dev/null +++ b/assets/maneuvers/dark/rightRoundaboutExit1.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightRoundaboutExit10.svg b/assets/maneuvers/dark/rightRoundaboutExit10.svg new file mode 100644 index 0000000..fb159b5 --- /dev/null +++ b/assets/maneuvers/dark/rightRoundaboutExit10.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightRoundaboutExit11.svg b/assets/maneuvers/dark/rightRoundaboutExit11.svg new file mode 100644 index 0000000..e3184b0 --- /dev/null +++ b/assets/maneuvers/dark/rightRoundaboutExit11.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightRoundaboutExit12.svg b/assets/maneuvers/dark/rightRoundaboutExit12.svg new file mode 100644 index 0000000..0900a9f --- /dev/null +++ b/assets/maneuvers/dark/rightRoundaboutExit12.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightRoundaboutExit2.svg b/assets/maneuvers/dark/rightRoundaboutExit2.svg new file mode 100644 index 0000000..4a3459b --- /dev/null +++ b/assets/maneuvers/dark/rightRoundaboutExit2.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightRoundaboutExit3.svg b/assets/maneuvers/dark/rightRoundaboutExit3.svg new file mode 100644 index 0000000..5531939 --- /dev/null +++ b/assets/maneuvers/dark/rightRoundaboutExit3.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightRoundaboutExit4.svg b/assets/maneuvers/dark/rightRoundaboutExit4.svg new file mode 100644 index 0000000..daeb02c --- /dev/null +++ b/assets/maneuvers/dark/rightRoundaboutExit4.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightRoundaboutExit5.svg b/assets/maneuvers/dark/rightRoundaboutExit5.svg new file mode 100644 index 0000000..79ef52a --- /dev/null +++ b/assets/maneuvers/dark/rightRoundaboutExit5.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightRoundaboutExit6.svg b/assets/maneuvers/dark/rightRoundaboutExit6.svg new file mode 100644 index 0000000..c76aa75 --- /dev/null +++ b/assets/maneuvers/dark/rightRoundaboutExit6.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightRoundaboutExit7.svg b/assets/maneuvers/dark/rightRoundaboutExit7.svg new file mode 100644 index 0000000..15adfdc --- /dev/null +++ b/assets/maneuvers/dark/rightRoundaboutExit7.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightRoundaboutExit8.svg b/assets/maneuvers/dark/rightRoundaboutExit8.svg new file mode 100644 index 0000000..dfbd767 --- /dev/null +++ b/assets/maneuvers/dark/rightRoundaboutExit8.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightRoundaboutExit9.svg b/assets/maneuvers/dark/rightRoundaboutExit9.svg new file mode 100644 index 0000000..593124b --- /dev/null +++ b/assets/maneuvers/dark/rightRoundaboutExit9.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightTurn.svg b/assets/maneuvers/dark/rightTurn.svg new file mode 100644 index 0000000..814bae9 --- /dev/null +++ b/assets/maneuvers/dark/rightTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/rightUTurn.svg b/assets/maneuvers/dark/rightUTurn.svg new file mode 100644 index 0000000..bed4063 --- /dev/null +++ b/assets/maneuvers/dark/rightUTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/sharpLeftTurn.svg b/assets/maneuvers/dark/sharpLeftTurn.svg new file mode 100644 index 0000000..106175d --- /dev/null +++ b/assets/maneuvers/dark/sharpLeftTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/sharpRightTurn.svg b/assets/maneuvers/dark/sharpRightTurn.svg new file mode 100644 index 0000000..7be2fca --- /dev/null +++ b/assets/maneuvers/dark/sharpRightTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/slightLeftTurn.svg b/assets/maneuvers/dark/slightLeftTurn.svg new file mode 100644 index 0000000..779e9a3 --- /dev/null +++ b/assets/maneuvers/dark/slightLeftTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/dark/slightRightTurn.svg b/assets/maneuvers/dark/slightRightTurn.svg new file mode 100644 index 0000000..87ea0e4 --- /dev/null +++ b/assets/maneuvers/dark/slightRightTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/arrive.svg b/assets/maneuvers/light/arrive.svg new file mode 100644 index 0000000..ab4a9a9 --- /dev/null +++ b/assets/maneuvers/light/arrive.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/maneuvers/light/continueOn.svg b/assets/maneuvers/light/continueOn.svg new file mode 100644 index 0000000..5497dcf --- /dev/null +++ b/assets/maneuvers/light/continueOn.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/maneuvers/light/depart.svg b/assets/maneuvers/light/depart.svg new file mode 100644 index 0000000..3c3e3b2 --- /dev/null +++ b/assets/maneuvers/light/depart.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/maneuvers/light/ferry.svg b/assets/maneuvers/light/ferry.svg new file mode 100644 index 0000000..aa42cb2 --- /dev/null +++ b/assets/maneuvers/light/ferry.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/maneuvers/light/leftExit.svg b/assets/maneuvers/light/leftExit.svg new file mode 100644 index 0000000..8d43d93 --- /dev/null +++ b/assets/maneuvers/light/leftExit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/maneuvers/light/leftFork.svg b/assets/maneuvers/light/leftFork.svg new file mode 100644 index 0000000..0ef03a0 --- /dev/null +++ b/assets/maneuvers/light/leftFork.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/maneuvers/light/leftRamp.svg b/assets/maneuvers/light/leftRamp.svg new file mode 100644 index 0000000..83cf60d --- /dev/null +++ b/assets/maneuvers/light/leftRamp.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/maneuvers/light/leftRoundaboutExit1.svg b/assets/maneuvers/light/leftRoundaboutExit1.svg new file mode 100644 index 0000000..31f2703 --- /dev/null +++ b/assets/maneuvers/light/leftRoundaboutExit1.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/maneuvers/light/leftRoundaboutExit10.svg b/assets/maneuvers/light/leftRoundaboutExit10.svg new file mode 100644 index 0000000..17d7d93 --- /dev/null +++ b/assets/maneuvers/light/leftRoundaboutExit10.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/leftRoundaboutExit11.svg b/assets/maneuvers/light/leftRoundaboutExit11.svg new file mode 100644 index 0000000..41b95a2 --- /dev/null +++ b/assets/maneuvers/light/leftRoundaboutExit11.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/leftRoundaboutExit12.svg b/assets/maneuvers/light/leftRoundaboutExit12.svg new file mode 100644 index 0000000..5e044cd --- /dev/null +++ b/assets/maneuvers/light/leftRoundaboutExit12.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/leftRoundaboutExit2.svg b/assets/maneuvers/light/leftRoundaboutExit2.svg new file mode 100644 index 0000000..7e63816 --- /dev/null +++ b/assets/maneuvers/light/leftRoundaboutExit2.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/leftRoundaboutExit3.svg b/assets/maneuvers/light/leftRoundaboutExit3.svg new file mode 100644 index 0000000..c3f23a2 --- /dev/null +++ b/assets/maneuvers/light/leftRoundaboutExit3.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/leftRoundaboutExit4.svg b/assets/maneuvers/light/leftRoundaboutExit4.svg new file mode 100644 index 0000000..c01bf19 --- /dev/null +++ b/assets/maneuvers/light/leftRoundaboutExit4.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/leftRoundaboutExit5.svg b/assets/maneuvers/light/leftRoundaboutExit5.svg new file mode 100644 index 0000000..a52e652 --- /dev/null +++ b/assets/maneuvers/light/leftRoundaboutExit5.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/leftRoundaboutExit6.svg b/assets/maneuvers/light/leftRoundaboutExit6.svg new file mode 100644 index 0000000..1722a43 --- /dev/null +++ b/assets/maneuvers/light/leftRoundaboutExit6.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/leftRoundaboutExit7.svg b/assets/maneuvers/light/leftRoundaboutExit7.svg new file mode 100644 index 0000000..f384040 --- /dev/null +++ b/assets/maneuvers/light/leftRoundaboutExit7.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/leftRoundaboutExit8.svg b/assets/maneuvers/light/leftRoundaboutExit8.svg new file mode 100644 index 0000000..791fcd2 --- /dev/null +++ b/assets/maneuvers/light/leftRoundaboutExit8.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/leftRoundaboutExit9.svg b/assets/maneuvers/light/leftRoundaboutExit9.svg new file mode 100644 index 0000000..9bd8b72 --- /dev/null +++ b/assets/maneuvers/light/leftRoundaboutExit9.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/leftTurn.svg b/assets/maneuvers/light/leftTurn.svg new file mode 100644 index 0000000..81a2cca --- /dev/null +++ b/assets/maneuvers/light/leftTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/leftUTurn.svg b/assets/maneuvers/light/leftUTurn.svg new file mode 100644 index 0000000..7c4de73 --- /dev/null +++ b/assets/maneuvers/light/leftUTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/middleFork.svg b/assets/maneuvers/light/middleFork.svg new file mode 100644 index 0000000..8043de8 --- /dev/null +++ b/assets/maneuvers/light/middleFork.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/png/arrive.png b/assets/maneuvers/light/png/arrive.png new file mode 100644 index 0000000..7b1f06c Binary files /dev/null and b/assets/maneuvers/light/png/arrive.png differ diff --git a/assets/maneuvers/light/png/continueOn.png b/assets/maneuvers/light/png/continueOn.png new file mode 100644 index 0000000..dd70d97 Binary files /dev/null and b/assets/maneuvers/light/png/continueOn.png differ diff --git a/assets/maneuvers/light/png/depart.png b/assets/maneuvers/light/png/depart.png new file mode 100644 index 0000000..d8b3190 Binary files /dev/null and b/assets/maneuvers/light/png/depart.png differ diff --git a/assets/maneuvers/light/png/ferry.png b/assets/maneuvers/light/png/ferry.png new file mode 100644 index 0000000..3692f68 Binary files /dev/null and b/assets/maneuvers/light/png/ferry.png differ diff --git a/assets/maneuvers/light/png/leftExit.png b/assets/maneuvers/light/png/leftExit.png new file mode 100644 index 0000000..7e5050a Binary files /dev/null and b/assets/maneuvers/light/png/leftExit.png differ diff --git a/assets/maneuvers/light/png/leftFork.png b/assets/maneuvers/light/png/leftFork.png new file mode 100644 index 0000000..484337b Binary files /dev/null and b/assets/maneuvers/light/png/leftFork.png differ diff --git a/assets/maneuvers/light/png/leftRamp.png b/assets/maneuvers/light/png/leftRamp.png new file mode 100644 index 0000000..9dd1ad3 Binary files /dev/null and b/assets/maneuvers/light/png/leftRamp.png differ diff --git a/assets/maneuvers/light/png/leftRoundaboutExit1.png b/assets/maneuvers/light/png/leftRoundaboutExit1.png new file mode 100644 index 0000000..44d0779 Binary files /dev/null and b/assets/maneuvers/light/png/leftRoundaboutExit1.png differ diff --git a/assets/maneuvers/light/png/leftRoundaboutExit10.png b/assets/maneuvers/light/png/leftRoundaboutExit10.png new file mode 100644 index 0000000..c16d347 Binary files /dev/null and b/assets/maneuvers/light/png/leftRoundaboutExit10.png differ diff --git a/assets/maneuvers/light/png/leftRoundaboutExit11.png b/assets/maneuvers/light/png/leftRoundaboutExit11.png new file mode 100644 index 0000000..3a127c9 Binary files /dev/null and b/assets/maneuvers/light/png/leftRoundaboutExit11.png differ diff --git a/assets/maneuvers/light/png/leftRoundaboutExit12.png b/assets/maneuvers/light/png/leftRoundaboutExit12.png new file mode 100644 index 0000000..89811f6 Binary files /dev/null and b/assets/maneuvers/light/png/leftRoundaboutExit12.png differ diff --git a/assets/maneuvers/light/png/leftRoundaboutExit2.png b/assets/maneuvers/light/png/leftRoundaboutExit2.png new file mode 100644 index 0000000..8bdb9ba Binary files /dev/null and b/assets/maneuvers/light/png/leftRoundaboutExit2.png differ diff --git a/assets/maneuvers/light/png/leftRoundaboutExit3.png b/assets/maneuvers/light/png/leftRoundaboutExit3.png new file mode 100644 index 0000000..b35ef17 Binary files /dev/null and b/assets/maneuvers/light/png/leftRoundaboutExit3.png differ diff --git a/assets/maneuvers/light/png/leftRoundaboutExit4.png b/assets/maneuvers/light/png/leftRoundaboutExit4.png new file mode 100644 index 0000000..62d0bd4 Binary files /dev/null and b/assets/maneuvers/light/png/leftRoundaboutExit4.png differ diff --git a/assets/maneuvers/light/png/leftRoundaboutExit5.png b/assets/maneuvers/light/png/leftRoundaboutExit5.png new file mode 100644 index 0000000..d2510cc Binary files /dev/null and b/assets/maneuvers/light/png/leftRoundaboutExit5.png differ diff --git a/assets/maneuvers/light/png/leftRoundaboutExit6.png b/assets/maneuvers/light/png/leftRoundaboutExit6.png new file mode 100644 index 0000000..616157e Binary files /dev/null and b/assets/maneuvers/light/png/leftRoundaboutExit6.png differ diff --git a/assets/maneuvers/light/png/leftRoundaboutExit7.png b/assets/maneuvers/light/png/leftRoundaboutExit7.png new file mode 100644 index 0000000..177b4f2 Binary files /dev/null and b/assets/maneuvers/light/png/leftRoundaboutExit7.png differ diff --git a/assets/maneuvers/light/png/leftRoundaboutExit8.png b/assets/maneuvers/light/png/leftRoundaboutExit8.png new file mode 100644 index 0000000..e293a93 Binary files /dev/null and b/assets/maneuvers/light/png/leftRoundaboutExit8.png differ diff --git a/assets/maneuvers/light/png/leftRoundaboutExit9.png b/assets/maneuvers/light/png/leftRoundaboutExit9.png new file mode 100644 index 0000000..e2a874e Binary files /dev/null and b/assets/maneuvers/light/png/leftRoundaboutExit9.png differ diff --git a/assets/maneuvers/light/png/leftTurn.png b/assets/maneuvers/light/png/leftTurn.png new file mode 100644 index 0000000..51381b5 Binary files /dev/null and b/assets/maneuvers/light/png/leftTurn.png differ diff --git a/assets/maneuvers/light/png/leftUTurn.png b/assets/maneuvers/light/png/leftUTurn.png new file mode 100644 index 0000000..1671c7e Binary files /dev/null and b/assets/maneuvers/light/png/leftUTurn.png differ diff --git a/assets/maneuvers/light/png/middleFork.png b/assets/maneuvers/light/png/middleFork.png new file mode 100644 index 0000000..7cdde6e Binary files /dev/null and b/assets/maneuvers/light/png/middleFork.png differ diff --git a/assets/maneuvers/light/png/rightExit.png b/assets/maneuvers/light/png/rightExit.png new file mode 100644 index 0000000..737329b Binary files /dev/null and b/assets/maneuvers/light/png/rightExit.png differ diff --git a/assets/maneuvers/light/png/rightFork.png b/assets/maneuvers/light/png/rightFork.png new file mode 100644 index 0000000..bd822f2 Binary files /dev/null and b/assets/maneuvers/light/png/rightFork.png differ diff --git a/assets/maneuvers/light/png/rightRamp.png b/assets/maneuvers/light/png/rightRamp.png new file mode 100644 index 0000000..554b106 Binary files /dev/null and b/assets/maneuvers/light/png/rightRamp.png differ diff --git a/assets/maneuvers/light/png/rightRoundaboutExit1.png b/assets/maneuvers/light/png/rightRoundaboutExit1.png new file mode 100644 index 0000000..f8a550a Binary files /dev/null and b/assets/maneuvers/light/png/rightRoundaboutExit1.png differ diff --git a/assets/maneuvers/light/png/rightRoundaboutExit10.png b/assets/maneuvers/light/png/rightRoundaboutExit10.png new file mode 100644 index 0000000..3c14685 Binary files /dev/null and b/assets/maneuvers/light/png/rightRoundaboutExit10.png differ diff --git a/assets/maneuvers/light/png/rightRoundaboutExit11.png b/assets/maneuvers/light/png/rightRoundaboutExit11.png new file mode 100644 index 0000000..e4bda1c Binary files /dev/null and b/assets/maneuvers/light/png/rightRoundaboutExit11.png differ diff --git a/assets/maneuvers/light/png/rightRoundaboutExit12.png b/assets/maneuvers/light/png/rightRoundaboutExit12.png new file mode 100644 index 0000000..46b245d Binary files /dev/null and b/assets/maneuvers/light/png/rightRoundaboutExit12.png differ diff --git a/assets/maneuvers/light/png/rightRoundaboutExit2.png b/assets/maneuvers/light/png/rightRoundaboutExit2.png new file mode 100644 index 0000000..efda3c4 Binary files /dev/null and b/assets/maneuvers/light/png/rightRoundaboutExit2.png differ diff --git a/assets/maneuvers/light/png/rightRoundaboutExit3.png b/assets/maneuvers/light/png/rightRoundaboutExit3.png new file mode 100644 index 0000000..05723f7 Binary files /dev/null and b/assets/maneuvers/light/png/rightRoundaboutExit3.png differ diff --git a/assets/maneuvers/light/png/rightRoundaboutExit4.png b/assets/maneuvers/light/png/rightRoundaboutExit4.png new file mode 100644 index 0000000..aceacd3 Binary files /dev/null and b/assets/maneuvers/light/png/rightRoundaboutExit4.png differ diff --git a/assets/maneuvers/light/png/rightRoundaboutExit5.png b/assets/maneuvers/light/png/rightRoundaboutExit5.png new file mode 100644 index 0000000..5a1e233 Binary files /dev/null and b/assets/maneuvers/light/png/rightRoundaboutExit5.png differ diff --git a/assets/maneuvers/light/png/rightRoundaboutExit6.png b/assets/maneuvers/light/png/rightRoundaboutExit6.png new file mode 100644 index 0000000..1e6c527 Binary files /dev/null and b/assets/maneuvers/light/png/rightRoundaboutExit6.png differ diff --git a/assets/maneuvers/light/png/rightRoundaboutExit7.png b/assets/maneuvers/light/png/rightRoundaboutExit7.png new file mode 100644 index 0000000..8330f75 Binary files /dev/null and b/assets/maneuvers/light/png/rightRoundaboutExit7.png differ diff --git a/assets/maneuvers/light/png/rightRoundaboutExit8.png b/assets/maneuvers/light/png/rightRoundaboutExit8.png new file mode 100644 index 0000000..5aad350 Binary files /dev/null and b/assets/maneuvers/light/png/rightRoundaboutExit8.png differ diff --git a/assets/maneuvers/light/png/rightRoundaboutExit9.png b/assets/maneuvers/light/png/rightRoundaboutExit9.png new file mode 100644 index 0000000..32fea52 Binary files /dev/null and b/assets/maneuvers/light/png/rightRoundaboutExit9.png differ diff --git a/assets/maneuvers/light/png/rightTurn.png b/assets/maneuvers/light/png/rightTurn.png new file mode 100644 index 0000000..e31e209 Binary files /dev/null and b/assets/maneuvers/light/png/rightTurn.png differ diff --git a/assets/maneuvers/light/png/rightUTurn.png b/assets/maneuvers/light/png/rightUTurn.png new file mode 100644 index 0000000..ec6c4b8 Binary files /dev/null and b/assets/maneuvers/light/png/rightUTurn.png differ diff --git a/assets/maneuvers/light/png/sharpLeftTurn.png b/assets/maneuvers/light/png/sharpLeftTurn.png new file mode 100644 index 0000000..af6d9d3 Binary files /dev/null and b/assets/maneuvers/light/png/sharpLeftTurn.png differ diff --git a/assets/maneuvers/light/png/sharpRightTurn.png b/assets/maneuvers/light/png/sharpRightTurn.png new file mode 100644 index 0000000..71777e9 Binary files /dev/null and b/assets/maneuvers/light/png/sharpRightTurn.png differ diff --git a/assets/maneuvers/light/png/slightLeftTurn.png b/assets/maneuvers/light/png/slightLeftTurn.png new file mode 100644 index 0000000..d2dcad2 Binary files /dev/null and b/assets/maneuvers/light/png/slightLeftTurn.png differ diff --git a/assets/maneuvers/light/png/slightRightTurn.png b/assets/maneuvers/light/png/slightRightTurn.png new file mode 100644 index 0000000..f52b65b Binary files /dev/null and b/assets/maneuvers/light/png/slightRightTurn.png differ diff --git a/assets/maneuvers/light/rightExit.svg b/assets/maneuvers/light/rightExit.svg new file mode 100644 index 0000000..796228b --- /dev/null +++ b/assets/maneuvers/light/rightExit.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightFork.svg b/assets/maneuvers/light/rightFork.svg new file mode 100644 index 0000000..d50566f --- /dev/null +++ b/assets/maneuvers/light/rightFork.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightRamp.svg b/assets/maneuvers/light/rightRamp.svg new file mode 100644 index 0000000..bc4bd6e --- /dev/null +++ b/assets/maneuvers/light/rightRamp.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightRoundaboutExit1.svg b/assets/maneuvers/light/rightRoundaboutExit1.svg new file mode 100644 index 0000000..8a94f03 --- /dev/null +++ b/assets/maneuvers/light/rightRoundaboutExit1.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightRoundaboutExit10.svg b/assets/maneuvers/light/rightRoundaboutExit10.svg new file mode 100644 index 0000000..8e09353 --- /dev/null +++ b/assets/maneuvers/light/rightRoundaboutExit10.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightRoundaboutExit11.svg b/assets/maneuvers/light/rightRoundaboutExit11.svg new file mode 100644 index 0000000..eb15058 --- /dev/null +++ b/assets/maneuvers/light/rightRoundaboutExit11.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightRoundaboutExit12.svg b/assets/maneuvers/light/rightRoundaboutExit12.svg new file mode 100644 index 0000000..30b0306 --- /dev/null +++ b/assets/maneuvers/light/rightRoundaboutExit12.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightRoundaboutExit2.svg b/assets/maneuvers/light/rightRoundaboutExit2.svg new file mode 100644 index 0000000..c4af392 --- /dev/null +++ b/assets/maneuvers/light/rightRoundaboutExit2.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightRoundaboutExit3.svg b/assets/maneuvers/light/rightRoundaboutExit3.svg new file mode 100644 index 0000000..35b01b2 --- /dev/null +++ b/assets/maneuvers/light/rightRoundaboutExit3.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightRoundaboutExit4.svg b/assets/maneuvers/light/rightRoundaboutExit4.svg new file mode 100644 index 0000000..18899f8 --- /dev/null +++ b/assets/maneuvers/light/rightRoundaboutExit4.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightRoundaboutExit5.svg b/assets/maneuvers/light/rightRoundaboutExit5.svg new file mode 100644 index 0000000..469376f --- /dev/null +++ b/assets/maneuvers/light/rightRoundaboutExit5.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightRoundaboutExit6.svg b/assets/maneuvers/light/rightRoundaboutExit6.svg new file mode 100644 index 0000000..e9e9e81 --- /dev/null +++ b/assets/maneuvers/light/rightRoundaboutExit6.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightRoundaboutExit7.svg b/assets/maneuvers/light/rightRoundaboutExit7.svg new file mode 100644 index 0000000..83ce1a9 --- /dev/null +++ b/assets/maneuvers/light/rightRoundaboutExit7.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightRoundaboutExit8.svg b/assets/maneuvers/light/rightRoundaboutExit8.svg new file mode 100644 index 0000000..84a2454 --- /dev/null +++ b/assets/maneuvers/light/rightRoundaboutExit8.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightRoundaboutExit9.svg b/assets/maneuvers/light/rightRoundaboutExit9.svg new file mode 100644 index 0000000..53389e9 --- /dev/null +++ b/assets/maneuvers/light/rightRoundaboutExit9.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightTurn.svg b/assets/maneuvers/light/rightTurn.svg new file mode 100644 index 0000000..7141cf6 --- /dev/null +++ b/assets/maneuvers/light/rightTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/rightUTurn.svg b/assets/maneuvers/light/rightUTurn.svg new file mode 100644 index 0000000..dc9a6ff --- /dev/null +++ b/assets/maneuvers/light/rightUTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/sharpLeftTurn.svg b/assets/maneuvers/light/sharpLeftTurn.svg new file mode 100644 index 0000000..8e6d807 --- /dev/null +++ b/assets/maneuvers/light/sharpLeftTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/sharpRightTurn.svg b/assets/maneuvers/light/sharpRightTurn.svg new file mode 100644 index 0000000..421ea74 --- /dev/null +++ b/assets/maneuvers/light/sharpRightTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/slightLeftTurn.svg b/assets/maneuvers/light/slightLeftTurn.svg new file mode 100644 index 0000000..4457c11 --- /dev/null +++ b/assets/maneuvers/light/slightLeftTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/maneuvers/light/slightRightTurn.svg b/assets/maneuvers/light/slightRightTurn.svg new file mode 100644 index 0000000..5cd0067 --- /dev/null +++ b/assets/maneuvers/light/slightRightTurn.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/map_marker.svg b/assets/map_marker.svg new file mode 100644 index 0000000..310df35 --- /dev/null +++ b/assets/map_marker.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/map_marker_big.svg b/assets/map_marker_big.svg new file mode 100644 index 0000000..38068fc --- /dev/null +++ b/assets/map_marker_big.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/map_marker_wp.svg b/assets/map_marker_wp.svg new file mode 100644 index 0000000..d6c7aa9 --- /dev/null +++ b/assets/map_marker_wp.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/nothing_found.svg b/assets/nothing_found.svg new file mode 100644 index 0000000..957efc4 --- /dev/null +++ b/assets/nothing_found.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/position.svg b/assets/position.svg new file mode 100644 index 0000000..b81b8e8 --- /dev/null +++ b/assets/position.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/route.svg b/assets/route.svg new file mode 100644 index 0000000..866cee8 --- /dev/null +++ b/assets/route.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/scooter.svg b/assets/scooter.svg new file mode 100644 index 0000000..5584872 --- /dev/null +++ b/assets/scooter.svg @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/assets/screenshots.png b/assets/screenshots.png new file mode 100644 index 0000000..0b99557 Binary files /dev/null and b/assets/screenshots.png differ diff --git a/assets/traffic_off.svg b/assets/traffic_off.svg new file mode 100644 index 0000000..c8db2a3 --- /dev/null +++ b/assets/traffic_off.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/traffic_on.svg b/assets/traffic_on.svg new file mode 100644 index 0000000..78fa1fc --- /dev/null +++ b/assets/traffic_on.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/truck.svg b/assets/truck.svg new file mode 100644 index 0000000..b8e0e84 --- /dev/null +++ b/assets/truck.svg @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/assets/walk.svg b/assets/walk.svg new file mode 100644 index 0000000..4152d3c --- /dev/null +++ b/assets/walk.svg @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..2ab986c --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +GeneratedKeys.xcconfig +Podfile.lock +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..6b4c0f7 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..98cff58 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,3 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" +#include "GeneratedKeys.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..5df6507 --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,3 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" +#include "GeneratedKeys.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..102b248 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,60 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + + # Enable bitcode by default as recommended by Apple. + # Including bitcode will allow Apple to re-optimize your app binary in the future + # without the need to submit a new version of your app to the App Store. + config.build_settings['ENABLE_BITCODE'] = 'YES' + + # Setup permission_handler plugin. + # for more information: https://github.com/BaseflowIT/flutter-permission-handler/blob/develop/permission_handler/ios/Classes/PermissionHandlerEnums.h + # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0' + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + 'PERMISSION_CONTACTS=0', + 'PERMISSION_EVENTS=0', + 'PERMISSION_MEDIA_LIBRARY=0', + 'PERMISSION_REMINDERS=0', + 'PERMISSION_SPEECH_RECOGNIZER=0' + ] + end + end +end diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9147035 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,575 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 76B3B22C05B13B40164D5490 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C6984022197CF17C15B0C1E /* Pods_Runner.framework */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0C6984022197CF17C15B0C1E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 23EDD78AC938B5FD24F889EB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A52A0F3E7FAFE578EB8F0491 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + C50A4E4F0B32893E0B921A38 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 76B3B22C05B13B40164D5490 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + C0E1DB037504E8E5B62FB990 /* Pods */, + A6DFA2C039B45D0FCB1E7E01 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + A6DFA2C039B45D0FCB1E7E01 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0C6984022197CF17C15B0C1E /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + C0E1DB037504E8E5B62FB990 /* Pods */ = { + isa = PBXGroup; + children = ( + C50A4E4F0B32893E0B921A38 /* Pods-Runner.debug.xcconfig */, + 23EDD78AC938B5FD24F889EB /* Pods-Runner.release.xcconfig */, + A52A0F3E7FAFE578EB8F0491 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + C3D49AA60F16592A6023321E /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + CB98278FE3A10CAC870F61D0 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + C3D49AA60F16592A6023321E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + CB98278FE3A10CAC870F61D0 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.4; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.RefApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.4; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.4; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.RefApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.RefApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..a28140c --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..10b05ae --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,14 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..a291238 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..972bc56 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..7f8cae3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..c526a3e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..a00c5ae Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..0bb5cd6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..d5484fd Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..7f8cae3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..3c3dfaf Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..b19de22 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..b19de22 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..91cd23f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..b479d9c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..2ec63e2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..5b4c5b1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/avatar.imageset/Contents.json b/ios/Runner/Assets.xcassets/avatar.imageset/Contents.json new file mode 100644 index 0000000..23f4e05 --- /dev/null +++ b/ios/Runner/Assets.xcassets/avatar.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "avatar.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/avatar.imageset/avatar.svg b/ios/Runner/Assets.xcassets/avatar.imageset/avatar.svg new file mode 100644 index 0000000..4573e80 --- /dev/null +++ b/ios/Runner/Assets.xcassets/avatar.imageset/avatar.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..a6df3c1 --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..a8d3431 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + UIRequiredDeviceCapabilities + + location-services + gps + + NSLocationAlwaysAndWhenInUseUsageDescription + This app needs to access your current location to display it on the map. + NSLocationWhenInUseUsageDescription + This app needs to access your current location to display it on the map. + NSMotionUsageDescription + Motion detection is needed to determine more accurate locations, when no GPS signal is found or used. + io.flutter.embedded_views_preview + + UIBackgroundModes + + audio + location + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + RefApp + CFBundleDisplayName + HERE SDK Ref App + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + HERECredentials + + AccessKeyId + $(HERESDK_ACCESS_KEY_ID) + AccessKeySecret + $(HERESDK_ACCESS_KEY_SECRET) + + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/setup_ios_here_sdk_keys.sh b/ios/setup_ios_here_sdk_keys.sh new file mode 100644 index 0000000..9d25aae --- /dev/null +++ b/ios/setup_ios_here_sdk_keys.sh @@ -0,0 +1,31 @@ +#!/bin/bash +#! +#! Copyright (C) 2020-2021 HERE Europe B.V. +#! +#! Licensed under the Apache License, Version 2.0 (the "License"); +#! you may not use this file except in compliance with the License. +#! You may obtain a copy of the License at +#! +#! http://www.apache.org/licenses/LICENSE-2.0 +#! +#! Unless required by applicable law or agreed to in writing, software +#! distributed under the License is distributed on an "AS IS" BASIS, +#! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#! See the License for the specific language governing permissions and +#! limitations under the License. +#! +#! SPDX-License-Identifier: Apache-2.0 +#! License-Filename: LICENSE + +START_DIR=$(dirname "$0") +cd "$START_DIR" + +if [ -z "$HERESDK_ACCESS_KEY_ID" ] || [ -z "$HERESDK_ACCESS_KEY_SECRET" ]; then + echo "To build this application, an access key id and secret are required to be defined. Please create environment" + echo "variables named HERESDK_ACCESS_KEY_ID and HERESDK_ACCESS_KEY_SECRET which contain these values and try again." + exit 1 +fi + +touch Flutter/GeneratedKeys.xcconfig +echo HERESDK_ACCESS_KEY_ID=$HERESDK_ACCESS_KEY_ID > Flutter/GeneratedKeys.xcconfig +echo HERESDK_ACCESS_KEY_SECRET=$HERESDK_ACCESS_KEY_SECRET >> Flutter/GeneratedKeys.xcconfig diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..15338f2 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart diff --git a/lib/common/dismiss_keyboard_on_scroll.dart b/lib/common/dismiss_keyboard_on_scroll.dart new file mode 100644 index 0000000..86c92b4 --- /dev/null +++ b/lib/common/dismiss_keyboard_on_scroll.dart @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; + +/// A widget that hides the keyboard when the user scrolls child widget. +class DismissKeyboardOnScroll extends StatelessWidget { + /// Child widget. + final Widget child; + /// Called when the keyboard is dismissed. + final Function onDismiss; + + /// Constructs a widget. + const DismissKeyboardOnScroll({ + Key key, + this.child, + this.onDismiss, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (x) { + if (x.dragDetails == null) { + return false; + } + + FocusScope.of(context).unfocus(); + if (onDismiss != null) { + onDismiss(); + } + return false; + }, + child: child, + ); + } +} diff --git a/lib/common/draggable_popup_here_logo_helper.dart b/lib/common/draggable_popup_here_logo_helper.dart new file mode 100644 index 0000000..cfa55d5 --- /dev/null +++ b/lib/common/draggable_popup_here_logo_helper.dart @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:here_sdk/mapview.dart'; + +/// A widget that controls position of the HERE logo on the map. +/// It observes the state and position of the child draggable sheet widget and updates the position of the logo +/// to prevent it from overlapping. +class DraggablePopupHereLogoHelper extends StatefulWidget { + /// [HereMapController] that contains the logo. + final HereMapController hereMapController; + /// [Key] of the map widget. + final GlobalKey hereMapKey; + /// Child draggable scrollable sheet widget. + final DraggableScrollableSheet draggableScrollableSheet; + /// Should be true if the child sheet is disposable. + final bool modal; + + /// Constructs a widget. + DraggablePopupHereLogoHelper({ + Key key, + @required this.hereMapController, + @required this.hereMapKey, + @required this.draggableScrollableSheet, + this.modal = false, + }) : super(key: key); + + @override + _DraggablePopupHereLogoHelperState createState() => _DraggablePopupHereLogoHelperState(); +} + +class _DraggablePopupHereLogoHelperState extends State { + bool _processEvents = true; + + @override + void initState() { + super.initState(); + SchedulerBinding.instance.scheduleFrameCallback((timeStamp) => SchedulerBinding.instance.addPostFrameCallback( + (timeStamp) => _updateHereLogoPosition(widget.draggableScrollableSheet.initialChildSize))); + } + + @override + Widget build(BuildContext context) { + return NotificationListener( + child: widget.draggableScrollableSheet, + onNotification: (notification) { + // The modal sheet is going to close, so don't touch the logo + if (widget.modal && notification.minExtent == notification.extent) { + _processEvents = false; + } + + _updateHereLogoPosition(notification.extent); + return false; + }, + ); + } + + void _updateHereLogoPosition(double extent) { + if (widget.hereMapKey.currentContext == null || widget.hereMapController == null || !_processEvents) { + return; + } + + final double height = MediaQuery.of(context).size.height; + final double popupHeight = height * extent; + final RenderBox box = widget.hereMapKey.currentContext.findRenderObject() as RenderBox; + final double margin = (popupHeight - (height - box.paintBounds.bottom)) * widget.hereMapController.pixelScale; + + if (margin >= 0) { + widget.hereMapController.setWatermarkPosition(WatermarkPlacement.bottomCenter, margin.truncate()); + } else { + widget.hereMapController.setWatermarkPosition(WatermarkPlacement.bottomLeft, 0); + } + } +} diff --git a/lib/common/file_utility.dart b/lib/common/file_utility.dart new file mode 100644 index 0000000..4885408 --- /dev/null +++ b/lib/common/file_utility.dart @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; + +/// Helper class for maneuver notifications. +class FileUtility { + static const String _maneuverDarkImagesDir = "assets/maneuvers/dark/png/"; + static const String _maneuverLightImagesDir = "assets/maneuvers/light/png/"; + + static void _createDirsIfNotExist(String docsDirectory, String imagesDirectory) async { + final Directory maneuversDirectory = Directory("$docsDirectory/$imagesDirectory"); + if (!(await maneuversDirectory.exists())) { + await maneuversDirectory.create(recursive: true); + } + } + + /// Saves an image of the maneuver at [imagePath] to the device's document folder for use in notifications. + static Future saveManeuverImageFromBundle(String imagePath) async { + final Directory docsDirectory = await getApplicationDocumentsDirectory(); + + await _createDirsIfNotExist(docsDirectory.path, _maneuverDarkImagesDir); + await _createDirsIfNotExist(docsDirectory.path, _maneuverLightImagesDir); + + final String filePath = '${docsDirectory.path}/$imagePath'; + + final File file = File(filePath); + if (!(await file.exists())) { + final imageData = await rootBundle.load(imagePath); + final bytes = imageData.buffer.asUint8List(); + await file.writeAsBytes(bytes, flush: true); + } + return filePath; + } +} diff --git a/lib/common/local_notifications_helper.dart b/lib/common/local_notifications_helper.dart new file mode 100644 index 0000000..d2f0cad --- /dev/null +++ b/lib/common/local_notifications_helper.dart @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +import 'file_utility.dart'; + +/// Helper class for maneuver notifications. +class LocalNotificationsHelper { + static FlutterLocalNotificationsPlugin _maneuverLocalNotificationsPlugin; + static const MethodChannel _kAndroidServiceChannel = + const MethodChannel("com.example.RefApp/foreground_service_channel"); + static const String _kTitleParam = "title"; + static const String _kContentParam = "content"; + static const String _kLargeIconParam = "large_icon"; + static const String _kSoundEnabledParam = "sound_enabled"; + static const String _kAndroidServiceStartCommand = "startService"; + static const String _kAndroidServiceUpdateCommand = "updateService"; + static const String _kAndroidServiceStopCommand = "stopService"; + + /// Initializes maneuvers notifications on the device with initial [title], [body] and an image at [imagePath]. + static Future startNotifications(String title, String body, String imagePath) async { + if (Platform.isIOS) { + _setupNotificationsIOS(); + } else { + _setupNotificationsAndroid(title, body, imagePath); + } + } + + /// Hides maneuvers notifications on the device. + static Future stopNotifications() async { + if (Platform.isAndroid) { + _stopNotificationsAndroid(); + } + } + + /// Shows maneuvers notifications on the device with a [title], notification [body], and an image at [imagePath]. + /// The [presentSound] parameter indicates whether the sound will be played. + static Future showManeuverNotification(String title, String body, String imagePath, bool presentSound) async { + if (Platform.isIOS) { + _showManeuverNotificationIOS(title, body, imagePath, presentSound); + } else { + _showManeuverNotificationAndroid(title, body, imagePath, presentSound); + } + } + + static Future _showManeuverNotificationIOS(String title, String body, String imagePath, bool presentSound) async { + final String savedImagePath = await FileUtility.saveManeuverImageFromBundle(imagePath); + + final IOSNotificationDetails iOSNotificationDetails = + IOSNotificationDetails(presentSound: presentSound, attachments: [IOSNotificationAttachment(savedImagePath)]); + + var platformChannelSpecifics = NotificationDetails( + iOS: iOSNotificationDetails, + ); + await _maneuverLocalNotificationsPlugin.show(0, title, body, platformChannelSpecifics); + } + + static Future _showManeuverNotificationAndroid(String title, String body, String imagePath, bool presentSound) async { + final String savedImagePath = await FileUtility.saveManeuverImageFromBundle(imagePath); + + await _kAndroidServiceChannel.invokeMethod(_kAndroidServiceUpdateCommand, { + _kTitleParam: title, + _kContentParam: body, + _kLargeIconParam: savedImagePath, + _kSoundEnabledParam: presentSound, + }); + } + + static _setupNotificationsIOS() { + if (_maneuverLocalNotificationsPlugin != null) { + return; + } + + _maneuverLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + var initSettings = InitializationSettings( + iOS: IOSInitializationSettings(), + ); + + _maneuverLocalNotificationsPlugin.initialize(initSettings); + } + + static Future _setupNotificationsAndroid(String title, String body, String imagePath) async { + final String savedImagePath = await FileUtility.saveManeuverImageFromBundle(imagePath); + + await _kAndroidServiceChannel.invokeMethod(_kAndroidServiceStartCommand, { + _kTitleParam: title, + _kContentParam: body, + _kLargeIconParam: savedImagePath, + }); + } + + static Future _stopNotificationsAndroid() async { + await _kAndroidServiceChannel.invokeMethod(_kAndroidServiceStopCommand); + } +} diff --git a/lib/common/marquee_widget.dart b/lib/common/marquee_widget.dart new file mode 100644 index 0000000..6483f8c --- /dev/null +++ b/lib/common/marquee_widget.dart @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; + +/// A Flutter widget that scrolls Widget Text. +class MarqueeWidget extends StatefulWidget { + static const int _kDefaultAnimationDuration = 3000; + static const int _kDefaultBackDuration = 1000; + static const int _kDefaultPauseDuration = 1000; + + /// Child widget. + final Widget child; + /// Scroll direction. + final Axis direction; + /// Animation duration. + final Duration animationDuration; + /// Back animation duration. + final Duration backDuration; + /// Pause duration. + final Duration pauseDuration; + + /// Constructs the widget. + MarqueeWidget({ + @required this.child, + this.direction: Axis.horizontal, + this.animationDuration: const Duration( + milliseconds: _kDefaultAnimationDuration, + ), + this.backDuration: const Duration( + milliseconds: _kDefaultBackDuration, + ), + this.pauseDuration: const Duration( + milliseconds: _kDefaultPauseDuration, + ), + }); + + @override + _MarqueeWidgetState createState() => _MarqueeWidgetState(); +} + +class _MarqueeWidgetState extends State { + ScrollController _scrollController; + + @override + void initState() { + _scrollController = ScrollController(); + WidgetsBinding.instance.addPostFrameCallback(scroll); + super.initState(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: widget.child, + scrollDirection: widget.direction, + controller: _scrollController, + ); + } + + void scroll(_) async { + while (_scrollController.hasClients) { + await Future.delayed(widget.pauseDuration); + if (_scrollController.hasClients) + await _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: widget.animationDuration, + curve: Curves.ease, + ); + await Future.delayed(widget.pauseDuration); + if (_scrollController.hasClients) + await _scrollController.animateTo( + 0.0, + duration: widget.backDuration, + curve: Curves.easeOut, + ); + } + } +} diff --git a/lib/common/place_actions_popup.dart b/lib/common/place_actions_popup.dart new file mode 100644 index 0000000..d178a75 --- /dev/null +++ b/lib/common/place_actions_popup.dart @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/core.threading.dart'; +import 'package:here_sdk/mapview.dart'; +import 'package:here_sdk/search.dart'; + +import 'ui_style.dart'; +import 'util.dart' as Util; + +typedef PlaceActionCallback = void Function(Place place); + +/// A widget that displays a pop-up window for creating a waypoint from a point on the map. +class PlaceActionsPopup extends StatefulWidget { + /// Coordinates of the point on the map. + final GeoCoordinates coordinates; + + /// Map controller. + final HereMapController hereMapController; + + /// Called when the right button is tapped or otherwise activated. + final PlaceActionCallback onRightButtonPressed; + + /// Right button icon. + final Widget rightButtonIcon; + + /// Called when the left button is tapped or otherwise activated. + final PlaceActionCallback onLeftButtonPressed; + + /// Left button icon. + final Widget leftButtonIcon; + + /// Creates a widget. + PlaceActionsPopup({ + Key key, + @required this.hereMapController, + @required this.coordinates, + @required this.onRightButtonPressed, + this.rightButtonIcon = const Icon( + Icons.add, + color: UIStyle.addWayPointPopupForegroundColor, + ), + this.onLeftButtonPressed = null, + this.leftButtonIcon = null, + }) : assert(onLeftButtonPressed != null || onRightButtonPressed != null), + assert((onLeftButtonPressed == null) == (leftButtonIcon == null)), + super(key: key); + + @override + _PlaceActionsPopupState createState() => _PlaceActionsPopupState(); +} + +class _PlaceActionsPopupState extends State { + static const double _kMaxPopupWidth = 150; + + final SearchOptions _searchOptions = new SearchOptions(LanguageCode.enUs, 1); + final SearchEngine _searchEngine = SearchEngine(); + TaskHandle _searchTask; + String _title; + Place _place; + MapMarker _mapMarker; + + @override + void initState() { + super.initState(); + _searchTask = _searchEngine.searchByCoordinates(widget.coordinates, _searchOptions, _onSearchEnd); + _title = widget.coordinates.toPrettyString(); + int markerSize = (widget.hereMapController.pixelScale * UIStyle.searchMarkerSize * 2).round(); + _mapMarker = Util.createMarkerWithImagePath( + widget.coordinates, + "assets/map_marker_wp.svg", + markerSize, + markerSize, + drawOrder: UIStyle.waypointsMarkerDrawOrder, + anchor: Anchor2D.withHorizontalAndVertical(0.5, 1), + ); + widget.hereMapController.mapScene.addMapMarker(_mapMarker); + } + + @override + void dispose() { + _searchTask?.cancel(); + _searchTask?.release(); + _searchEngine.release(); + _place?.release(); + widget.hereMapController.mapScene.removeMapMarker(_mapMarker); + _mapMarker.release(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Material( + color: UIStyle.addWayPointPopupBackgroundColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(UIStyle.popupsBorderRadius)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.onLeftButtonPressed != null) + IconButton( + icon: widget.leftButtonIcon, + onPressed: () { + widget.onLeftButtonPressed(_place); + _place = null; + }, + ), + Padding( + padding: EdgeInsets.all(UIStyle.contentMarginMedium), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: _kMaxPopupWidth, + ), + child: Text( + _title, + style: TextStyle( + color: UIStyle.addWayPointPopupForegroundColor, + ), + ), + ), + ), + if (widget.onRightButtonPressed != null) + IconButton( + icon: widget.rightButtonIcon, + onPressed: () { + widget.onRightButtonPressed(_place); + _place = null; + }, + ), + ], + ), + ), + Container( + height: UIStyle.searchMarkerSize * 2.0 + UIStyle.contentMarginMedium, + ), + ], + ); + + void _onSearchEnd(SearchError error, List places) { + if (error != null) { + print('Search failed. Error: ${error.toString()}'); + } + + setState(() { + _place = places.first; + _title = _place.address.addressText; + }); + } +} diff --git a/lib/common/reset_location_button.dart b/lib/common/reset_location_button.dart new file mode 100644 index 0000000..f87ae49 --- /dev/null +++ b/lib/common/reset_location_button.dart @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; + +import 'ui_style.dart'; + +/// A widget for the reset current location floating button. +class ResetLocationButton extends StatelessWidget { + /// Called when the button is tapped or otherwise activated. + final VoidCallback onPressed; + + /// Constructs a widget. + ResetLocationButton({this.onPressed}); + + @override + Widget build(BuildContext context) { + return Container( + width: UIStyle.mediumButtonHeight, + height: UIStyle.mediumButtonHeight, + child: FloatingActionButton( + heroTag: null, + backgroundColor: Theme.of(context).backgroundColor, + child: Icon(Icons.gps_fixed), + onPressed: () => onPressed(), + ), + ); + } +} diff --git a/lib/common/ui_style.dart b/lib/common/ui_style.dart new file mode 100644 index 0000000..6ac785d --- /dev/null +++ b/lib/common/ui_style.dart @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +/// Helper class that contains all definitions of colors, fonts, sizes and other UI parameters that are used in +/// the application. +class UIStyle { + static const Color buttonPrimaryColor = Color(0xFF6B9CFF); // Primary button color + static const Color buttonSecondaryColor = Color(0xFF65EBE2); // Secondary button color + static const Color accuracyCircleColor = Color(0x550BC7C2); + static const Color selectedListTileColor = Color(0xFFF5F5F5); + static const Color acceptedConsentColor = Color(0xFF80E3C1); + static const Color revokedConsentColor = Color(0xFFFF0000); + static const Color trafficWarningColor = Color(0xFFFA9D00); + static const Color currentPositionColor = Color(0xFF0BC7C2); + + static const Color stopNavigationButtonColor = Color(0xFFFB0425); + static const Color stopNavigationButtonIconColor = Color(0xFFFFFFFF); + static const Color reroutingProgressBackgroundColor = Color(0xFFC4C4C4); + static const Color reroutingProgressColor = Color(0xFFFFFFFF); + + static const Color optionsBorderColor = Color(0x5500123E); + static const Color tabBarBackgroundColor = Color(0xFFF9FAFC); + static const Color preferencesBackgroundColor = Color(0xFFFFFFFF); + + static const Color noLocationWarningBackgroundColor = Color(0xCC000A19); + static const Color noLocationWarningColor = Color(0xFFFFFFFF); + + static const Color routeColor = Color(0xFF929FB2); + static const Color routeBorderColor = Color(0xFF6F7F90); + static const Color selectedRouteColor = Color(0xFF126EF8); + static const Color selectedRouteBorderColor = Color(0xFF195BB9); + static const Color addWayPointPopupBackgroundColor = Color(0xFF333B47); + static const Color addWayPointPopupForegroundColor = Color(0xFFFFFFFF); + static const Color removeWayPointBackgroundColor = Color(0xFFFB0425); + static const Color removeWayPointIconColor = Color(0xFFFFFFFF); + static const double routeLineWidth = 20; + static const double routeOutLineWidth = 5; + + static const int locationMarkerSize = 30; // logical pixels + static const int searchMarkerSize = 30; // logical pixels + static const int poiMarkerSize = 45; // logical pixels + static const int maneuverMarkerSize = 15; // logical pixels + + static const double contentMarginExtraSmall = 2; + static const double contentMarginSmall = 4; + static const double contentMarginMedium = 8; + static const double contentMarginLarge = 12; + static const double contentMarginExtraLarge = 16; + static const double contentMarginHuge = 20; + static const double contentMarginExtraHuge = 24; + + static const double extraHugeFontSize = 30; + static const double hugeFontSize = 20; + static const double bigFontSize = 16; + static const double mediumFontSize = 14; + static const double smallFontSize = 10; + + static const double bigButtonHeight = 56; + static const double mediumButtonHeight = 48; + static const double smallButtonHeight = 40; + + static const double bigIconSize = 24; + static const double mediumIconSize = 20; + static const double smallIconSize = 16; + + static const double drawerHeaderHeight = 160; + + static const double optionsRectBorderRadius = 2; + static const double optionsRectBorderWidth = 1; + static const double popupsBorderRadius = 10; + + static const double maxBottomDraggableSheetSize = 0.8; + + static const double cupertinoPickerHeight = 250; + + static const int searchMarkerDrawOrder = 5; + static const int waypointsMarkerDrawOrder = 10; + + // private UI constants + static const double _defaultFontSize = 16; + static const double _defaultHeadingFontSize = 18; + + // HERE colors + static const Color _lightBackground = Color.fromARGB(0xff, 0xf5, 0xf5, 0xf5); + static const Color _lightAccent = Color.fromARGB(0xff, 0x12, 0x6e, 0xf8); + static const Color _lightAccentSecondary = Color.fromARGB(0xff, 0x2c, 0x48, 0xa1); + static const Color _lightForeground = Color.fromARGB(0xff, 0x27, 0x2d, 0x37); + static const Color _lightForegroundSecondary = Color.fromARGB(0xff, 0x6f, 0x73, 0x7a); + static const Color _lightForegroundHint = Color.fromARGB(0xff, 0xb7, 0xb9, 0xbc); + + static final ThemeData lightTheme = ThemeData( + scaffoldBackgroundColor: _lightBackground, + appBarTheme: const AppBarTheme( + color: _lightForeground, + iconTheme: IconThemeData( + color: _lightAccent, + ), + ), + colorScheme: const ColorScheme.light( + primary: _lightForeground, + secondary: _lightAccent, + secondaryVariant: _lightAccentSecondary, + onPrimary: _lightBackground, + onSecondary: _lightForegroundSecondary, + background: _lightBackground, + ), + iconTheme: const IconThemeData( + color: _lightAccent, + ), + textTheme: _lightTextTheme, + textSelectionTheme: const TextSelectionThemeData( + cursorColor: _lightAccent, + selectionColor: _lightAccent, + selectionHandleColor: _lightAccent, + ), + inputDecorationTheme: _lightInputDecorationTheme, + accentColor: _lightAccent, + backgroundColor: _lightBackground, + hintColor: _lightForegroundHint, + highlightColor: _lightAccentSecondary, + ); + + static const TextStyle _lightBodyTextStyle = TextStyle( + fontSize: _defaultFontSize, + color: _lightForeground, + ); + static const TextStyle _lightLabelTextStyle = TextStyle( + color: _lightForeground, + ); + static const TextStyle _lightButtonTextStyle = TextStyle( + color: _lightBackground, + ); + static const TextStyle _lightHeading = TextStyle( + color: _lightBackground, + fontWeight: FontWeight.bold, + fontSize: _defaultHeadingFontSize, + ); + static const TextStyle _lightHeadlinePrimary = TextStyle( + color: _lightForeground, + ); + static const TextStyle _lightHintTextStyle = TextStyle( + color: _lightForegroundHint, + ); + static const TextStyle _lightSecondaryTextStyle = TextStyle( + color: _lightForeground, + ); + + static const TextTheme _lightTextTheme = TextTheme( + bodyText1: _lightBodyTextStyle, + bodyText2: _lightLabelTextStyle, + button: _lightButtonTextStyle, + headline5: _lightHeading, + headline6: _lightHeadlinePrimary, + subtitle1: _lightSecondaryTextStyle, + subtitle2: _lightHintTextStyle, + ); + + static const InputDecorationTheme _lightInputDecorationTheme = InputDecorationTheme( + filled: false, + fillColor: Colors.transparent, + border: UnderlineInputBorder( + borderSide: BorderSide( + color: _lightForegroundHint, + )), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: _lightForegroundHint, + )), + hintStyle: TextStyle( + color: _lightForegroundHint, + ), + ); + + /// Creates text style for the options section text. + static TextStyle optionsSectionStyle(BuildContext context) { + return TextStyle( + fontSize: bigFontSize, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ); + } + + /// Creates rounded rect shape decoration. + static ShapeDecoration roundedRectDecoration() => ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: optionsRectBorderWidth, + style: BorderStyle.solid, + color: optionsBorderColor, + ), + borderRadius: BorderRadius.all( + Radius.circular(optionsRectBorderRadius), + ), + ), + ); + + /// Creates bottom divider decoration. + static BoxDecoration bottomDividerDecoration() => BoxDecoration( + border: Border( + bottom: BorderSide(color: optionsBorderColor, width: 1.0), + ), + ); + + /// Creates top rounded border shape. + static ShapeBorder topRoundedBorder() => RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(popupsBorderRadius), + topRight: Radius.circular(popupsBorderRadius), + ), + ); + + /// Creates bottom rounded border shape. + static ShapeBorder bottomRoundedBorder() => RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(popupsBorderRadius), + bottomRight: Radius.circular(popupsBorderRadius), + ), + ); +} diff --git a/lib/common/util.dart b/lib/common/util.dart new file mode 100644 index 0000000..2277368 --- /dev/null +++ b/lib/common/util.dart @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/mapview.dart'; +import 'package:intl/intl.dart'; + +import 'ui_style.dart'; + +const String _placeholderPattern = '(\{\{([a-zA-Z0-9]+)\}\})'; + +/// Returns a formatted string constructed from a [template] and a list of [replacements]. +String formatString(String template, List replacements) { + final regExp = RegExp(_placeholderPattern); + assert( + regExp.allMatches(template).length == replacements.length, "Template and Replacements length are incompatible"); + + for (final replacement in replacements) { + template = template.replaceFirst(regExp, replacement.toString()); + } + + return template; +} + +/// Returns localized [distance] string in meters. +String makeDistanceString(BuildContext context, int distance) { + if (distance == null) { + return ""; + } else if (distance < 1000) { + return "$distance ${AppLocalizations.of(context).meterAbbreviationText} "; + } else if (distance < 10000) { + return "${(distance / 1000.0).toStringAsFixed(1)} ${AppLocalizations.of(context).kilometerAbbreviationText} "; + } else { + return "${(distance / 1000).truncate()} ${AppLocalizations.of(context).kilometerAbbreviationText} "; + } +} + +/// An extension for lists that allows swapping of two elements at indices [index1], [index2]. +extension ListSwap on List { + List swap(int index1, int index2) { + final length = this.length; + RangeError.checkValidIndex(index1, this, "index1", length); + RangeError.checkValidIndex(index2, this, "index2", length); + if (index1 != index2) { + final tmp1 = this[index1]; + this[index1] = this[index2]; + this[index2] = tmp1; + } + + return this; + } +} + +/// Creates [MapMarker] in [coordinates] using an image at [imagePath], with [width], [height], [drawOrder] +/// and [anchor]. +MapMarker createMarkerWithImagePath( + GeoCoordinates coordinates, + String imagePath, + int width, + int height, { + int drawOrder, + Anchor2D anchor, +}) { + MapImage mapImage = MapImage.withFilePathAndWidthAndHeight(imagePath, width, height); + MapMarker mapMarker = createMarkerWithImage(coordinates, mapImage, drawOrder: drawOrder, anchor: anchor); + mapImage.release(); + return mapMarker; +} + +/// Creates [MapMarker] in [coordinates] using an [image], [drawOrder] and [anchor]. +MapMarker createMarkerWithImage( + GeoCoordinates coordinates, + MapImage image, { + int drawOrder, + Anchor2D anchor, +}) { + MapMarker mapMarker = MapMarker(coordinates, image); + if (drawOrder != null) { + mapMarker.drawOrder = drawOrder; + } + if (anchor != null) { + mapMarker.anchor = anchor; + } + + return mapMarker; +} + +/// Returns the localized [dateTime] string. +String stringFromDateTime(BuildContext context, DateTime dateTime) { + if (dateTime == null) return ""; + + return DateFormat(AppLocalizations.of(context).dateTimeFormat).format(dateTime); +} + +/// An extension for the [HereMapController]. +extension LogicalCoords on HereMapController { + /// Zooms map area specified by [geoBox] into [viewPort] with [margin]. + void zoomGeoBoxToLogicalViewPort({ + @required GeoBox geoBox, + @required Rect viewPort, + double margin = UIStyle.contentMarginExtraHuge, + }) { + this.camera.lookAtAreaWithOrientationAndViewRectangle( + geoBox, + MapCameraOrientationUpdate.withDefaults(), + Rectangle2D( + Point2D(viewPort.left + margin, viewPort.top + margin) * this.pixelScale, + Size2D( + (viewPort.width - margin * 2) * this.pixelScale, + (viewPort.height - margin * 2) * this.pixelScale, + ))); + } + + /// Zooms map area specified by [geoBox] into entire map area. + void zoomToLogicalViewPort({@required GeoBox geoBox, @required BuildContext context}) { + final RenderBox box = context.findRenderObject() as RenderBox; + + zoomGeoBoxToLogicalViewPort( + geoBox: geoBox, + viewPort: Rect.fromLTRB(0, MediaQuery.of(context).padding.top, box.size.width, box.size.height) + .deflate(UIStyle.locationMarkerSize.toDouble()), + ); + } +} + +/// An extension for the [GeoCoordinates]. +extension GeoCoordinatesExtensions on GeoCoordinates { + /// Returns formatted string. + String toPrettyString({int fractionDigits = 5}) => "${latitude.toStringAsFixed(5)}, ${longitude.toStringAsFixed(5)}"; +} + +/// An extension for the [Point2D]. +extension Point2DExtensions on Point2D { + /// Returns [Point2D], each of whose fields is multiplied by [factor]. + Point2D operator *(double factor) => Point2D(x * factor, y * factor); + + /// Returns [Point2D], each of whose fields is divided by [factor]. + Point2D operator /(double factor) => Point2D(x / factor, y / factor); +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..b69c9bf --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,1718 @@ +{ + "appTitle": "HERE SDK for Flutter - Reference Application", + "@appTitle": { + "description": "Title of the Application" + }, + "appTitleHeader": "HERE SDK\nReference Application", + "@appTitleHeader": { + "description": "Title of the Application in the drawer header" + }, + "userConsentDescription": "Please help HERE to improve the provided services", + "@userConsentDescription": { + "description": "Description text about information gathering" + }, + "userConsentTitle": "Consent", + "@userConsentTitle": { + "description": "Text for the user consent menu" + }, + "consentGranted": "Accepted", + "@consentGranted": { + "description": "Text for granted consent" + }, + "consentDenied": "Revoked", + "@consentDenied": { + "description": "Text for denied consent" + }, + "searchHint": "Where to?", + "@searchHint": { + "description": "Hint for the search text field" + }, + "cancelTitle": "Cancel", + "@cancelTitle": { + "description": "Cancel button title" + }, + "doneTitle": "Done", + "@doneTitle": { + "description": "Done button title" + }, + "recentlySearchTitle": "Recent searches", + "@recentlySearchTitle": { + "description": "Title of the recent searches header" + }, + "matchingResultsTitle": "Matching results", + "@matchingResultsTitle": { + "description": "Title of the matching results header" + }, + "noResultsFoundText": "Sorry, nothing found", + "@noResultsFoundText": { + "description": "Message if no search results were found" + }, + "noResultsFoundDescription": "Try to use another keywords", + "@noResultsFoundDescription": { + "description": "Message if no search results were found" + }, + "searchErrorText": "Search failed with error: {{error}}", + "@searchErrorText": { + "description": "Search error message" + }, + "meterAbbreviationText": "m", + "@meterAbbreviationText": { + "description": "Text for the abbreviation of the meter" + }, + "kilometerAbbreviationText": "km", + "@kilometerAbbreviationText": { + "description": "Text for the abbreviation of the kilometer" + }, + "minuteAbbreviationText": "min", + "@minuteAbbreviationText": { + "description": "Text for the abbreviation of the minute" + }, + "hourAbbreviationText": "h", + "@hourAbbreviationText": { + "description": "Text for the abbreviation of the hour" + }, + "kmhAbbreviationText": "km/h", + "@kmhAbbreviationText": { + "description": "Text for the abbreviation of the kilometer per hour" + }, + "yourLocationTitle": "Your location", + "@yourLocationTitle": { + "description": "Your location title" + }, + "defaultLocationTitle": "Default location", + "@defaultLocationTitle": { + "description": "Default location title" + }, + "fastestRouteTitle": "Fastest route", + "@fastestRouteTitle": { + "description": "Title of fastest route mode" + }, + "shortestRouteTitle": "Shortest route", + "@shortestRouteTitle": { + "description": "Title of shortest route mode" + }, + "trafficDelayText": "Incl. {{0}} delay", + "@trafficDelayText": { + "description": "Template for the traffic delay text" + }, + "noTrafficDelaysText": "No delays", + "@noTrafficDelaysText": { + "description": "No traffic delays text" + }, + "routeToButtonTitle": "Route", + "@routeToButtonTitle": { + "description": "Title of the route to button" + }, + "addToRouteButton": "Add", + "@addToRouteButton": { + "description": "Title of the add to route button" + }, + "preferencesTitle": "Preferences", + "@preferencesTitle": { + "description": "Preferences button title" + }, + "routingErrorText": "Routing failed with error: {{error}}", + "@routingErrorText": { + "description": "Routing error message" + }, + "avoidanceOptionsTitle": "Avoidance Options", + "@avoidanceOptionsTitle": { + "description": "Title of avoidance options section in route preferences" + }, + "avoidRoadFeaturesTitle": "Avoid road features", + "@avoidRoadFeaturesTitle": { + "description": "Title of avoid road row in route preferences" + }, + "seasonalClosure": "Seasonal closure", + "@seasonalClosure": { + "description": "Avoid road feature name for route preferences" + }, + "tollRoad": "Toll road", + "@tollRoad": { + "description": "Avoid road feature name for route preferences" + }, + "controlledAccessHighway": "Controlled access highway", + "@controlledAccessHighway": { + "description": "Avoid road feature name for route preferences" + }, + "ferry": "Ferry", + "@ferry": { + "description": "Avoid road feature name for route preferences" + }, + "carShuttleTrain": "Car shuttle train", + "@carShuttleTrain": { + "description": "Avoid road feature name for route preferences" + }, + "tunnel": "Tunnel", + "@tunnel": { + "description": "Avoid road feature name for route preferences" + }, + "dirtRoad": "Dirt road", + "@dirtRoad": { + "description": "Avoid road feature name for route preferences" + }, + "difficultTurns": "Difficult turns", + "@difficultTurns": { + "description": "Avoid road feature name for route preferences" + }, + "avoidCountriesTitle": "Avoid countries", + "@avoidCountriesTitle": { + "description": "Title of avoid country row in route preferences" + }, + "routePreferencesScreenTitle": "Route Preferences", + "@routePreferencesScreenTitle": { + "description": "Title of route preferences screen" + }, + "routeOptionsTitle": "Route Options", + "@routeOptionsTitle": { + "description": "Title of route options section in preferences" + }, + "routeTextOptionsTitle": "Route Text Options", + "@routeTextOptionsTitle": { + "description": "Title of route text options section in preferences" + }, + "routeAlternativesTitle": "Route alternatives", + "@routeAlternativesTitle": { + "description": "Title of route alternatives setting in route options" + }, + "eatAndDrinkTitle": "Eat and Drink", + "@eatAndDrinkTitle": { + "description": "Text for the Eat and Drink place category" + }, + "fuelingStationsTitle": "Fueling Stations", + "@fuelingStationsTitle": { + "description": "Text for the Fueling Stations place category" + }, + "atmTitle": "ATM", + "@atmTitle": { + "description": "Text for the ATM place category" + }, + "selectPositioningDialogTitle": "Choose a positioning method", + "@selectPositioningDialogTitle": { + "description": "Title of the navigation positioning source dialog" + }, + "simulatedLocationSourceTitle": "Simulated location events (along the route)", + "@simulatedLocationSourceTitle": { + "description": "Title of the simulated location events source" + }, + "realLocationSourceTitle": "Real location events", + "@realLocationSourceTitle": { + "description": "Title of the real location events source" + }, + "arrivalTitle": "arrival", + "@arrivalTitle": { + "description": "Label for the arrival time" + }, + "arrivalTimeTitle": "Arrival time", + "@arrivalTimeTitle": { + "description": "Label for the arrival time local notification" + }, + "stopNavigationDialogTitle": "Do you want to exit navigation mode?", + "@stopNavigationDialogTitle": { + "description": "Title of the navigation stop dialog" + }, + "stopNavigationDialogSubtitle": "You will permanently lose you progress and destination history", + "@stopNavigationDialogSubtitle": { + "description": "Title of the navigation stop dialog" + }, + "stopNavigationAcceptButtonCaption": "Exit navigation mode", + "@stopNavigationAcceptButtonCaption": { + "description": "Caption of the navigation stop dialog" + }, + "reroutingInProgressText": "Updating...", + "@reroutingInProgressText": { + "description": "Text for the rerouting indicator" + }, + "departureTimeTitle": "Departure time", + "@departureTimeTitle": { + "description": "Title of departure time setting in route options" + }, + "optimizationModeTitle": "Optimization mode", + "@optimizationModeTitle": { + "description": "Title of optimization mode setting in route options" + }, + "textFormatTitle": "Text format", + "@textFormatTitle": { + "description": "Title of text mode setting in route text options" + }, + "languageCodeTitle": "Language code", + "@languageCodeTitle": { + "description": "Title of language code setting in route text options" + }, + "unitSystemTitle": "Unit system", + "@unitSystemTitle": { + "description": "Title of unit system setting in route options" + }, + "textFormatHtml": "Html", + "@textFormatHtml": { + "description": "Title of text instructions html format setting in route text options" + }, + "textFormatPlain": "Plain", + "@textFormatPlain": { + "description": "Title of text instructions plain format setting in route text options" + }, + "unitSystemMetric": "Metric", + "@unitSystemMetric": { + "description": "Title of metric unit system setting in route text options" + }, + "unitSystemImperialUk": "Imperial UK", + "@unitSystemImperialUk": { + "description": "Title of imperial UK unit system setting in route text options" + }, + "unitSystemImperialUs": "Imperial US", + "@unitSystemImperialUs": { + "description": "Title of imperial US unit system setting in route text options" + }, + "truckWidthRowTitle": "Width", + "@truckWidthRowTitle": { + "description": "Title for truck specification preferences" + }, + "truckWidthHint": "Width in cm", + "@truckWidthHint": { + "description": "Hint for truck specification preferences" + }, + "truckHeightRowTitle": "Height", + "@truckHeightRowTitle": { + "description": "Title for truck specification preferences" + }, + "truckHeightHint": "Height in cm", + "@truckHeightHint": { + "description": "Hint for truck specification preferences" + }, + "truckLengthRowTitle": "Length", + "@truckLengthRowTitle": { + "description": "Title for truck specification preferences" + }, + "truckLengthtHint": "Length in cm", + "@truckLengthtHint": { + "description": "Hint for truck specification preferences" + }, + "truckAxleCountRowTitle": "Axel count", + "@truckAxleCountRowTitle": { + "description": "Title for truck specification preferences" + }, + "truckAxlesCountHint": "Total number of vehicle axles", + "@truckAxlesCountHint": { + "description": "Hint for truck specification preferences" + }, + "truckWeightPerAxleRowTitle": "Weight per axle", + "@truckWeightPerAxleRowTitle": { + "description": "Title for truck specification preferences" + }, + "truckAxleWeightHint": "Axle weight in kg", + "@truckAxleWeightHint": { + "description": "Hint for truck specification preferences" + }, + "truckGrossWeightRowTitle": "Gross weight", + "@truckGrossWeightRowTitle": { + "description": "Title for truck specification preferences" + }, + "truckTotalWeightHint": "Total weight in kg", + "@truckTotalWeightHint": { + "description": "Hint for truck specification preferences" + }, + "languageCodeArSa": "Arabic (Saudi Arabia)", + "@languageCodeArSa": { + "description": "Language code for Arabic (Saudi Arabia)" + }, + "languageCodeZhCn": "Chinese (Simplified China)", + "@languageCodeZhCn": { + "description": "Language code for Chinese (Simplified China)" + }, + "languageCodeEnUs": "English (United States)", + "@languageCodeEnUs": { + "description": "Language code for English (United States)" + }, + "languageCodeFrFr": "French", + "@languageCodeFrFr": { + "description": "Language code for French" + }, + "languageCodeDeDe": "German", + "@languageCodeDeDe": { + "description": "Language code for German" + }, + "languageCodeHiIn": "Hindi", + "@languageCodeHiIn": { + "description": "Language code for Hindi" + }, + "languageCodePtPt": "Portuguese (Portugal)", + "@languageCodePtPt": { + "description": "Language code for Portuguese (Portugal)" + }, + "languageCodeEsEs": "Spanish (Spain)", + "@languageCodeEsEs": { + "description": "Language code for Spanish (Spain)" + }, + "highwayTitle": "Highway", + "@highwayTitle": { + "description": "Route preferences section title" + }, + "allowHighwayTitle": "Allow highway", + "@allowHighwayTitle": { + "description": "Route preferences switcher title" + }, + "walkSpeedTitle": "Walk Speed", + "@walkSpeedTitle": { + "description": "Pedestrian route settings title" + }, + "walkSpeedUnitTitle": "Walk speed (m/s)", + "@walkSpeedUnitTitle": { + "description": "Pedestrian route settings" + }, + "truckSpecificationsTitle": "Truck Specifications", + "@truckSpecificationsTitle": { + "description": "Route truck preferences section title" + }, + "specificationsTitle": "Specifications", + "@specificationsTitle": { + "description": "Route truck specification preferences row title" + }, + "hazardousGoodsTitle": "Hazardous goods", + "@hazardousGoodsTitle": { + "description": "Route truck hazardous goods preferences row title" + }, + "hazardousGoodsExplosive": "Explosive", + "@hazardousGoodsExplosive": { + "description": "Hazardous goods explosive" + }, + "hazardousGoodsGas": "Gas", + "@hazardousGoodsGas": { + "description": "Hazardous goods gas" + }, + "hazardousGoodsFlammable": "Flammable", + "@hazardousGoodsFlammable": { + "description": "Hazardous goods flammable" + }, + "hazardousGoodsCombustible": "Combustible", + "@hazardousGoodsCombustible": { + "description": "Hazardous goods combustible" + }, + "hazardousGoodsOrganic": "Organic", + "@hazardousGoodsOrganic": { + "description": "Hazardous goods organic" + }, + "hazardousGoodsPoison": "Poison", + "@hazardousGoodsPoison": { + "description": "Hazardous goods poison" + }, + "hazardousGoodsRadioactive": "Radioactive", + "@hazardousGoodsRadioactive": { + "description": "Hazardous goods radioactive" + }, + "hazardousGoodsCorrosive": "Corrosive", + "@hazardousGoodsCorrosive": { + "description": "Hazardous goods corrosive" + }, + "hazardousGoodsPoisonousInhalation": "Poisonous Inhalation", + "@hazardousGoodsPoisonousInhalation": { + "description": "Hazardous goods poisonous inhalation" + }, + "hazardousGoodsHarmfulToWater": "Harmful To Water", + "@hazardousGoodsHarmfulToWater": { + "description": "Hazardous goods harmful to water" + }, + "hazardousGoodsOther": "Other", + "@hazardousGoodsOther": { + "description": "Hazardous goods other" + }, + "tunnelCategoryTitle": "Tunnel category", + "@tunnelCategoryTitle": { + "description": "Route truck preferences setting" + }, + "tunnelCategoryB": "B category", + "@tunnelCategoryB": { + "description": "Tunnel category B" + }, + "tunnelCategoryC": "C category", + "@tunnelCategoryC": { + "description": "Tunnel category C" + }, + "tunnelCategoryD": "D category", + "@tunnelCategoryD": { + "description": "Tunnel category D" + }, + "tunnelCategoryE": "E category", + "@tunnelCategoryE": { + "description": "Tunnel category E" + }, + "noneTitle": "None", + "@noneTitle": { + "description": "Route setting is missing" + }, + "selectDateTimeTitle": "Select date and time", + "@selectDateTimeTitle": { + "description": "Title for date time picker" + }, + "dateTimeFormat": "dd/MM/yyyy, hh:mm a", + "@dateTimeFormat": { + "description": "Date and time format" + }, + "poiSettingsTitle": "Points of interest", + "@poiSettingsTitle": { + "description": "Title of the settings for POI" + }, + "wayPointsListTitle": "Waypoints", + "@wayPointsListTitle": { + "description": "Title of the waypoints list" + }, + "noLocationWarning": "Location Services are not available. We cannot locate your position.", + "@noLocationWarning": { + "description": "No location services warning" + }, + "countryCodeAbw": "Aruba", + "@countryCodeAbw": { + "description": "Country name title" + }, + "countryCodeAfg": "Afghanistan", + "@countryCodeAfg": { + "description": "Country name title" + }, + "countryCodeAgo": "Angola", + "@countryCodeAgo": { + "description": "Country name title" + }, + "countryCodeAia": "Anguilla", + "@countryCodeAia": { + "description": "Country name title" + }, + "countryCodeAla": "Aland Islands", + "@countryCodeAla": { + "description": "Country name title" + }, + "countryCodeAlb": "Albania", + "@countryCodeAlb": { + "description": "Country name title" + }, + "countryCodeAnd": "Andorra", + "@countryCodeAnd": { + "description": "Country name title" + }, + "countryCodeAre": "United Arab Emirates", + "@countryCodeAre": { + "description": "Country name title" + }, + "countryCodeArg": "Argentina", + "@countryCodeArg": { + "description": "Country name title" + }, + "countryCodeArm": "Armenia", + "@countryCodeArm": { + "description": "Country name title" + }, + "countryCodeAsm": "American Samoa", + "@countryCodeAsm": { + "description": "Country name title" + }, + "countryCodeAta": "Antarctica", + "@countryCodeAta": { + "description": "Country name title" + }, + "countryCodeAtf": "French Southern Territories", + "@countryCodeAtf": { + "description": "Country name title" + }, + "countryCodeAtg": "Antigua And Barbuda", + "@countryCodeAtg": { + "description": "Country name title" + }, + "countryCodeAus": "Australia", + "@countryCodeAus": { + "description": "Country name title" + }, + "countryCodeAut": "Austria", + "@countryCodeAut": { + "description": "Country name title" + }, + "countryCodeAze": "Azerbaijan", + "@countryCodeAze": { + "description": "Country name title" + }, + "countryCodeBdi": "Burundi", + "@countryCodeBdi": { + "description": "Country name title" + }, + "countryCodeBel": "Belgium", + "@countryCodeBel": { + "description": "Country name title" + }, + "countryCodeBen": "Benin", + "@countryCodeBen": { + "description": "Country name title" + }, + "countryCodeBes": "Bonaire, Sint Eustatius And Saba", + "@countryCodeBes": { + "description": "Country name title" + }, + "countryCodeBfa": "Burkina Faso", + "@countryCodeBfa": { + "description": "Country name title" + }, + "countryCodeBgd": "Bangladesh", + "@countryCodeBgd": { + "description": "Country name title" + }, + "countryCodeBgr": "Bulgaria", + "@countryCodeBgr": { + "description": "Country name title" + }, + "countryCodeBhr": "Bahrain", + "@countryCodeBhr": { + "description": "Country name title" + }, + "countryCodeBhs": "Bahamas", + "@countryCodeBhs": { + "description": "Country name title" + }, + "countryCodeBih": "Bosnia And Herzegovina", + "@countryCodeBih": { + "description": "Country name title" + }, + "countryCodeBlm": "Saint Barthelemy", + "@countryCodeBlm": { + "description": "Country name title" + }, + "countryCodeBlr": "Belarus", + "@countryCodeBlr": { + "description": "Country name title" + }, + "countryCodeBlz": "Belize", + "@countryCodeBlz": { + "description": "Country name title" + }, + "countryCodeBmu": "Bermuda", + "@countryCodeBmu": { + "description": "Country name title" + }, + "countryCodeBol": "Bolivia (plurinational State Of)", + "@countryCodeBol": { + "description": "Country name title" + }, + "countryCodeBra": "Brazil", + "@countryCodeBra": { + "description": "Country name title" + }, + "countryCodeBrb": "Barbados", + "@countryCodeBrb": { + "description": "Country name title" + }, + "countryCodeBrn": "Brunei Darussalam", + "@countryCodeBrn": { + "description": "Country name title" + }, + "countryCodeBtn": "Bhutan", + "@countryCodeBtn": { + "description": "Country name title" + }, + "countryCodeBvt": "Bouvet Island", + "@countryCodeBvt": { + "description": "Country name title" + }, + "countryCodeBwa": "Botswana", + "@countryCodeBwa": { + "description": "Country name title" + }, + "countryCodeCaf": "Central African Republic", + "@countryCodeCaf": { + "description": "Country name title" + }, + "countryCodeCan": "Canada", + "@countryCodeCan": { + "description": "Country name title" + }, + "countryCodeCck": "Cocos (keeling) Islands", + "@countryCodeCck": { + "description": "Country name title" + }, + "countryCodeChe": "Switzerland", + "@countryCode": { + "description": "Country name title" + }, + "countryCodeChl": "Chile", + "@countryCodeChl": { + "description": "Country name title" + }, + "countryCodeChn": "China", + "@countryCodeChn": { + "description": "Country name title" + }, + "countryCodeCiv": "Cote D'ivoire", + "@countryCodeCiv": { + "description": "Country name title" + }, + "countryCodeCmr": "Cameroon", + "@countryCodeCmr": { + "description": "Country name title" + }, + "countryCodeCod": "Congo, Democratic Republic Of The", + "@countryCodeCod": { + "description": "Country name title" + }, + "countryCodeCog": "Congo", + "@countryCodeCog": { + "description": "Country name title" + }, + "countryCodeCok": "Cook Islands", + "@countryCodeCok": { + "description": "Country name title" + }, + "countryCodeCol": "Colombia", + "@countryCodeCol": { + "description": "Country name title" + }, + "countryCodeCom": "Comoros", + "@countryCodeCom": { + "description": "Country name title" + }, + "countryCodeCpv": "Cabo Verde", + "@countryCodeCpv": { + "description": "Country name title" + }, + "countryCodeCri": "Costa Rica", + "@countryCodeCri": { + "description": "Country name title" + }, + "countryCodeCub": "Cuba", + "@countryCodeCub": { + "description": "Country name title" + }, + "countryCodeCuw": "Curacao", + "@countryCodeCuw": { + "description": "Country name title" + }, + "countryCodeCxr": "Christmas Island", + "@countryCodeCxr": { + "description": "Country name title" + }, + "countryCodeCym": "Cayman Islands", + "@countryCodeCym": { + "description": "Country name title" + }, + "countryCodeCyp": "Cyprus", + "@countryCodeCyp": { + "description": "Country name title" + }, + "countryCodeCze": "Czechia", + "@countryCodeCze": { + "description": "Country name title" + }, + "countryCodeDeu": "Germany", + "@countryCodeDeu": { + "description": "Country name title" + }, + "countryCodeDji": "Djibouti", + "@countryCodeDji": { + "description": "Country name title" + }, + "countryCodeDma": "Dominica", + "@countryCodeDma": { + "description": "Country name title" + }, + "countryCodeDnk": "Denmark", + "@countryCodeDnk": { + "description": "Country name title" + }, + "countryCodeDom": "Dominican Republic", + "@countryCodeDom": { + "description": "Country name title" + }, + "countryCodeDza": "Algeria", + "@countryCodeDza": { + "description": "Country name title" + }, + "countryCodeEcu": "Ecuador", + "@countryCodeEcu": { + "description": "Country name title" + }, + "countryCodeEgy": "Egypt", + "@countryCodeEgy": { + "description": "Country name title" + }, + "countryCodeEri": "Eritrea", + "@countryCodeEri": { + "description": "Country name title" + }, + "countryCodeEsh": "Western Sahara", + "@countryCodeEsh": { + "description": "Country name title" + }, + "countryCodeEsp": "Spain", + "@countryCodeEsp": { + "description": "Country name title" + }, + "countryCodeEst": "Estonia", + "@countryCodeEst": { + "description": "Country name title" + }, + "countryCodeEth": "Ethiopia", + "@countryCodeEth": { + "description": "Country name title" + }, + "countryCodeFin": "Finland", + "@countryCodeFin": { + "description": "Country name title" + }, + "countryCodeFji": "Fiji", + "@countryCodeFji": { + "description": "Country name title" + }, + "countryCodeFlk": "Falkland Islands (malvinas)", + "@countryCodeFlk": { + "description": "Country name title" + }, + "countryCodeFra": "France", + "@countryCodeFra": { + "description": "Country name title" + }, + "countryCodeFro": "Faroe Islands", + "@countryCodeFro": { + "description": "Country name title" + }, + "countryCodeFsm": "Micronesia (federated States Of)", + "@countryCodeFsm": { + "description": "Country name title" + }, + "countryCodeGab": "Gabon", + "@countryCodeGab": { + "description": "Country name title" + }, + "countryCodeGbr": "United Kingdom Of Great Britain And Northern Ireland", + "@countryCodeGbr": { + "description": "Country name title" + }, + "countryCodeGeo": "Georgia", + "@countryCodeGeo": { + "description": "Country name title" + }, + "countryCodeGgy": "Guernsey", + "@countryCodeGgy": { + "description": "Country name title" + }, + "countryCodeGha": "Ghana", + "@countryCodeGha": { + "description": "Country name title" + }, + "countryCodeGib": "Gibraltar", + "@countryCodeGib": { + "description": "Country name title" + }, + "countryCodeGin": "Guinea", + "@countryCodeGin": { + "description": "Country name title" + }, + "countryCodeGlp": "Guadeloupe", + "@countryCodeGlp": { + "description": "Country name title" + }, + "countryCodeGmb": "Gambia", + "@countryCodeGmb": { + "description": "Country name title" + }, + "countryCodeGnb": "Guinea-bissau", + "@countryCodeGnb": { + "description": "Country name title" + }, + "countryCodeGnq": "Equatorial Guinea", + "@countryCodeGnq": { + "description": "Country name title" + }, + "countryCodeGrc": "Greece", + "@countryCodeGrc": { + "description": "Country name title" + }, + "countryCodeGrd": "Grenada", + "@countryCodeGrd": { + "description": "Country name title" + }, + "countryCodeGrl": "Greenland", + "@countryCodeGrl": { + "description": "Country name title" + }, + "countryCodeGtm": "Guatemala", + "@countryCodeGtm": { + "description": "Country name title" + }, + "countryCodeGuf": "French Guiana", + "@countryCodeGuf": { + "description": "Country name title" + }, + "countryCodeGum": "Guam", + "@countryCodeGum": { + "description": "Country name title" + }, + "countryCodeGuy": "Guyana", + "@countryCodeGuy": { + "description": "Country name title" + }, + "countryCodeHkg": "Hong Kong", + "@countryCodeHkg": { + "description": "Country name title" + }, + "countryCodeHmd": "Heard Island And Mcdonald Islands", + "@countryCodeHmd": { + "description": "Country name title" + }, + "countryCodeHnd": "Honduras", + "@countryCodeHnd": { + "description": "Country name title" + }, + "countryCodeHrv": "Croatia", + "@countryCodeHrv": { + "description": "Country name title" + }, + "countryCodeHti": "Haiti", + "@countryCodeHti": { + "description": "Country name title" + }, + "countryCodeHun": "Hungary", + "@countryCodeHun": { + "description": "Country name title" + }, + "countryCodeIdn": "Indonesia", + "@countryCodeIdn": { + "description": "Country name title" + }, + "countryCodeImn": "Isle Of Man", + "@countryCodeImn": { + "description": "Country name title" + }, + "countryCodeInd": "India", + "@countryCodeInd": { + "description": "Country name title" + }, + "countryCodeIot": "British Indian Ocean Territory", + "@countryCodeIot": { + "description": "Country name title" + }, + "countryCodeIrl": "Ireland", + "@countryCodeIrl": { + "description": "Country name title" + }, + "countryCodeIrn": "Iran (islamic Republic Of)", + "@countryCodeIrn": { + "description": "Country name title" + }, + "countryCodeIrq": "Iraq", + "@countryCodeIrq": { + "description": "Country name title" + }, + "countryCodeIsl": "Iceland", + "@countryCodeIsl": { + "description": "Country name title" + }, + "countryCodeIsr": "Israel", + "@countryCodeIsr": { + "description": "Country name title" + }, + "countryCodeIta": "Italy", + "@countryCodeIta": { + "description": "Country name title" + }, + "countryCodeJam": "Jamaica", + "@countryCodeJam": { + "description": "Country name title" + }, + "countryCodeJey": "Jersey", + "@countryCodeJey": { + "description": "Country name title" + }, + "countryCodeJor": "Jordan", + "@countryCodeJor": { + "description": "Country name title" + }, + "countryCodeJpn": "Japan", + "@countryCodeJpn": { + "description": "Country name title" + }, + "countryCodeKaz": "Kazakhstan", + "@countryCodeKaz": { + "description": "Country name title" + }, + "countryCodeKen": "Kenya", + "@countryCodeKen": { + "description": "Country name title" + }, + "countryCodeKgz": "Kyrgyzstan", + "@countryCodeKgz": { + "description": "Country name title" + }, + "countryCodeKhm": "Cambodia", + "@countryCodeKhm": { + "description": "Country name title" + }, + "countryCodeKir": "Kiribati", + "@countryCodeKir": { + "description": "Country name title" + }, + "countryCodeKna": "Saint Kitts And Nevis", + "@countryCodeKna": { + "description": "Country name title" + }, + "countryCodeKor": "Korea, Republic Of", + "@countryCodeKor": { + "description": "Country name title" + }, + "countryCodeKwt": "Kuwait", + "@countryCodeKwt": { + "description": "Country name title" + }, + "countryCodeLao": "Lao People's Democratic Republic", + "@countryCodeLao": { + "description": "Country name title" + }, + "countryCodeLbn": "Lebanon", + "@countryCodeLbn": { + "description": "Country name title" + }, + "countryCodeLbr": "Liberia", + "@countryCodeLbr": { + "description": "Country name title" + }, + "countryCodeLby": "Libya", + "@countryCodeLby": { + "description": "Country name title" + }, + "countryCodeLca": "Saint Lucia", + "@countryCodeLca": { + "description": "Country name title" + }, + "countryCodeLie": "Liechtenstein", + "@countryCodeLie": { + "description": "Country name title" + }, + "countryCodeLka": "Sri Lanka", + "@countryCodeLka": { + "description": "Country name title" + }, + "countryCodeLso": "Lesotho", + "@countryCodeLso": { + "description": "Country name title" + }, + "countryCodeLtu": "Lithuania", + "@countryCodeLtu": { + "description": "Country name title" + }, + "countryCodeLux": "Luxembourg", + "@countryCodeLux": { + "description": "Country name title" + }, + "countryCodeLva": "Latvia", + "@countryCodeLva": { + "description": "Country name title" + }, + "countryCodeMac": "Macao", + "@countryCodeMac": { + "description": "Country name title" + }, + "countryCodeMaf": "Saint Martin (french Part)", + "@countryCodeMaf": { + "description": "Country name title" + }, + "countryCodeMar": "Morocco", + "@countryCodeMar": { + "description": "Country name title" + }, + "countryCodeMco": "Monaco", + "@countryCodeMco": { + "description": "Country name title" + }, + "countryCodeMda": "Moldova, Republic Of", + "@countryCodeMda": { + "description": "Country name title" + }, + "countryCodeMdg": "Madagascar", + "@countryCodeMdg": { + "description": "Country name title" + }, + "countryCodeMdv": "Maldives", + "@countryCodeMdv": { + "description": "Country name title" + }, + "countryCodeMex": "Mexico", + "@countryCodeMex": { + "description": "Country name title" + }, + "countryCodeMhl": "Marshall Islands", + "@countryCodeMhl": { + "description": "Country name title" + }, + "countryCodeMkd": "North Macedonia", + "@countryCodeMkd": { + "description": "Country name title" + }, + "countryCodeMli": "Mali", + "@countryCodeMli": { + "description": "Country name title" + }, + "countryCodeMlt": "Malta", + "@countryCodeMlt": { + "description": "Country name title" + }, + "countryCodeMmr": "Myanmar", + "@countryCodeMmr": { + "description": "Country name title" + }, + "countryCodeMne": "Montenegro", + "@countryCodeMne": { + "description": "Country name title" + }, + "countryCodeMng": "Mongolia", + "@countryCodeMng": { + "description": "Country name title" + }, + "countryCodeMnp": "Northern Mariana Islands", + "@countryCodeMnp": { + "description": "Country name title" + }, + "countryCodeMoz": "Mozambique", + "@countryCodeMoz": { + "description": "Country name title" + }, + "countryCodeMrt": "Mauritania", + "@countryCodeMrt": { + "description": "Country name title" + }, + "countryCodeMsr": "Montserrat", + "@countryCodeMsr": { + "description": "Country name title" + }, + "countryCodeMtq": "Martinique", + "@countryCodeMtq": { + "description": "Country name title" + }, + "countryCodeMus": "Mauritius", + "@countryCodeMus": { + "description": "Country name title" + }, + "countryCodeMwi": "Malawi", + "@countryCodeMwi": { + "description": "Country name title" + }, + "countryCodeMys": "Malaysia", + "@countryCodeMys": { + "description": "Country name title" + }, + "countryCodeMyt": "Mayotte", + "@countryCodeMyt": { + "description": "Country name title" + }, + "countryCodeNam": "Namibia", + "@countryCodeNam": { + "description": "Country name title" + }, + "countryCodeNcl": "New Caledonia", + "@countryCodeNcl": { + "description": "Country name title" + }, + "countryCodeNer": "Niger", + "@countryCodeNer": { + "description": "Country name title" + }, + "countryCodeNfk": "Norfolk Island", + "@countryCodeNfk": { + "description": "Country name title" + }, + "countryCodeNga": "Nigeria", + "@countryCodeNga": { + "description": "Country name title" + }, + "countryCodeNic": "Nicaragua", + "@countryCodeNic": { + "description": "Country name title" + }, + "countryCodeNiu": "Niue", + "@countryCodeNiu": { + "description": "Country name title" + }, + "countryCodeNld": "Netherlands", + "@countryCodeNld": { + "description": "Country name title" + }, + "countryCodeNor": "Norway", + "@countryCodeNor": { + "description": "Country name title" + }, + "countryCodeNpl": "Nepal", + "@countryCodeNpl": { + "description": "Country name title" + }, + "countryCodeNru": "Nauru", + "@countryCodeNru": { + "description": "Country name title" + }, + "countryCodeNzl": "New Zealand", + "@countryCodeNzl": { + "description": "Country name title" + }, + "countryCodeOmn": "Oman", + "@countryCodeOmn": { + "description": "Country name title" + }, + "countryCodePak": "Pakistan", + "@countryCodePak": { + "description": "Country name title" + }, + "countryCodePan": "Panama", + "@countryCodePan": { + "description": "Country name title" + }, + "countryCodePcn": "Pitcairn", + "@countryCodePcn": { + "description": "Country name title" + }, + "countryCodePer": "Peru", + "@countryCodePer": { + "description": "Country name title" + }, + "countryCodePhl": "Philippines", + "@countryCodePhl": { + "description": "Country name title" + }, + "countryCodePlw": "Palau", + "@countryCodePlw": { + "description": "Country name title" + }, + "countryCodePng": "Papua New Guinea", + "@countryCodePng": { + "description": "Country name title" + }, + "countryCodePol": "Poland", + "@countryCodePol": { + "description": "Country name title" + }, + "countryCodePri": "Puerto Rico", + "@countryCodePri": { + "description": "Country name title" + }, + "countryCodePrk": "Korea (democratic People's Republic Of)", + "@countryCodePrk": { + "description": "Country name title" + }, + "countryCodePrt": "Portugal", + "@countryCodePrt": { + "description": "Country name title" + }, + "countryCodePry": "Paraguay", + "@countryCodePry": { + "description": "Country name title" + }, + "countryCodePse": "Palestine, State Of", + "@countryCodePse": { + "description": "Country name title" + }, + "countryCodePyf": "French Polynesia", + "@countryCodePyf": { + "description": "Country name title" + }, + "countryCodeQat": "Qatar", + "@countryCodeQat": { + "description": "Country name title" + }, + "countryCodeReu": "Reunion", + "@countryCodeReu": { + "description": "Country name title" + }, + "countryCodeRou": "Romania", + "@countryCodeRou": { + "description": "Country name title" + }, + "countryCodeRus": "Russian Federation", + "@countryCodeRus": { + "description": "Country name title" + }, + "countryCodeRwa": "Rwanda", + "@countryCodeRwa": { + "description": "Country name title" + }, + "countryCodeSau": "Saudi Arabia", + "@countryCodeSau": { + "description": "Country name title" + }, + "countryCodeSdn": "Sudan", + "@countryCodeSdn": { + "description": "Country name title" + }, + "countryCodeSen": "Senegal", + "@countryCodeSen": { + "description": "Country name title" + }, + "countryCodeSgp": "Singapore", + "@countryCodeSgp": { + "description": "Country name title" + }, + "countryCodeSgs": "South Georgia And The South Sandwich Islands", + "@countryCodeSgs": { + "description": "Country name title" + }, + "countryCodeShn": "Saint Helena, Ascension And Tristan Da Cunha", + "@countryCodeShn": { + "description": "Country name title" + }, + "countryCodeSjm": "Svalbard And Jan Mayen", + "@countryCodeSjm": { + "description": "Country name title" + }, + "countryCodeSlb": "Solomon Islands", + "@countryCodeSlb": { + "description": "Country name title" + }, + "countryCodeSle": "Sierra Leone", + "@countryCodeSle": { + "description": "Country name title" + }, + "countryCodeSlv": "El Salvador", + "@countryCodeSlv": { + "description": "Country name title" + }, + "countryCodeSmr": "San Marino", + "@countryCodeSmr": { + "description": "Country name title" + }, + "countryCodeSom": "Somalia", + "@countryCodeSom": { + "description": "Country name title" + }, + "countryCodeSpm": "Saint Pierre And Miquelon", + "@countryCodeSpm": { + "description": "Country name title" + }, + "countryCodeSrb": "Serbia", + "@countryCodeSrb": { + "description": "Country name title" + }, + "countryCodeSsd": "South Sudan", + "@countryCodeSsd": { + "description": "Country name title" + }, + "countryCodeStp": "Sao Tome And Principe", + "@countryCodeStp": { + "description": "Country name title" + }, + "countryCodeSur": "Suriname", + "@countryCodeSur": { + "description": "Country name title" + }, + "countryCodeSvk": "Slovakia", + "@countryCodeSvk": { + "description": "Country name title" + }, + "countryCodeSvn": "Slovenia", + "@countryCodeSvn": { + "description": "Country name title" + }, + "countryCodeSwe": "Sweden", + "@countryCodeSwe": { + "description": "Country name title" + }, + "countryCodeSwz": "Eswatini", + "@countryCodeSwz": { + "description": "Country name title" + }, + "countryCodeSxm": "Sint Maarten (dutch Part)", + "@countryCodeSxm": { + "description": "Country name title" + }, + "countryCodeSyc": "Seychelles", + "@countryCodeSyc": { + "description": "Country name title" + }, + "countryCodeSyr": "Syrian Arab Republic", + "@countryCodeSyr": { + "description": "Country name title" + }, + "countryCodeTca": "Turks And Caicos Islands", + "@countryCodeTca": { + "description": "Country name title" + }, + "countryCodeTcd": "Chad", + "@countryCodeTcd": { + "description": "Country name title" + }, + "countryCodeTgo": "Togo", + "@countryCodeTgo": { + "description": "Country name title" + }, + "countryCodeTha": "Thailand", + "@countryCodeTha": { + "description": "Country name title" + }, + "countryCodeTjk": "Tajikistan", + "@countryCodeTjk": { + "description": "Country name title" + }, + "countryCodeTkl": "Tokelau", + "@countryCodeTkl": { + "description": "Country name title" + }, + "countryCodeTkm": "Turkmenistan", + "@countryCodeTkm": { + "description": "Country name title" + }, + "countryCodeTls": "Timor-leste", + "@countryCodeTls": { + "description": "Country name title" + }, + "countryCodeTon": "Tonga", + "@countryCodeTon": { + "description": "Country name title" + }, + "countryCodeTto": "Trinidad And Tobago", + "@countryCodeTto": { + "description": "Country name title" + }, + "countryCodeTun": "Tunisia", + "@countryCodeTun": { + "description": "Country name title" + }, + "countryCodeTur": "Turkey", + "@countryCodeTur": { + "description": "Country name title" + }, + "countryCodeTuv": "Tuvalu", + "@countryCodeTuv": { + "description": "Country name title" + }, + "countryCodeTwn": "Taiwan, Province Of China", + "@countryCodeTwn": { + "description": "Country name title" + }, + "countryCodeTza": "Tanzania, United Republic Of", + "@countryCodeTza": { + "description": "Country name title" + }, + "countryCodeUga": "Uganda", + "@countryCodeUga": { + "description": "Country name title" + }, + "countryCodeUkr": "Ukraine", + "@countryCodeUkr": { + "description": "Country name title" + }, + "countryCodeUmi": "United States Minor Outlying Islands", + "@countryCodeUmi": { + "description": "Country name title" + }, + "countryCodeUry": "Uruguay", + "@countryCodeUry": { + "description": "Country name title" + }, + "countryCodeUsa": "United States Of America", + "@countryCodeUsa": { + "description": "Country name title" + }, + "countryCodeUzb": "Uzbekistan", + "@countryCodeUzb": { + "description": "Country name title" + }, + "countryCodeVat": "Holy See", + "@countryCodeVat": { + "description": "Country name title" + }, + "countryCodeVct": "Saint Vincent And The Grenadines", + "@countryCodeVct": { + "description": "Country name title" + }, + "countryCodeVen": "Venezuela (bolivarian Republic Of)", + "@countryCodeVen": { + "description": "Country name title" + }, + "countryCodeVgb": "Virgin Islands (british)", + "@countryCodeVgb": { + "description": "Country name title" + }, + "countryCodeVir": "Virgin Islands (u.s.)", + "@countryCodeVir": { + "description": "Country name title" + }, + "countryCodeVnm": "Viet Nam", + "@countryCodeVnm": { + "description": "Country name title" + }, + "countryCodeVut": "Vanuatu", + "@countryCodeVut": { + "description": "Country name title" + }, + "countryCodeWlf": "Wallis And Futuna", + "@countryCodeWlf": { + "description": "Country name title" + }, + "countryCodeWsm": "Samoa", + "@countryCodeWsm": { + "description": "Country name title" + }, + "countryCodeYem": "Yemen", + "@countryCodeYem": { + "description": "Country name title" + }, + "countryCodeZaf": "South Africa", + "@countryCodeZaf": { + "description": "Country name title" + }, + "countryCodeZmb": "Zambia", + "@countryCodeZmb": { + "description": "Country name title" + }, + "countryCodeZwe": "Zimbabwe", + "@countryCodeZwe": { + "description": "Country name title" + }, + "arriveActionText": "Arrive at destination", + "@arriveActionText": { + "description": "Text for the arrive maneuver action" + }, + "continueOnActionText": "Continue on", + "@continueOnActionText": { + "description": "Text for the continueOn maneuver action" + }, + "continueOnActionRoadText": "Continue on {{0}}", + "@continueOnActionRoadText": { + "description": "Text for the continueOn maneuver action" + }, + "departActionText": "Start", + "@departActionText": { + "description": "Text for the depart maneuver action" + }, + "departActionRoadText": "Start at {{0}}", + "@departActionRoadText": { + "description": "Text for the depart maneuver action" + }, + "ferryActionText": "Take the ferry", + "@ferryActionText": { + "description": "Text for the ferry maneuver action" + }, + "ferryActionNextRoadText": "Take the ferry to {{0}}", + "@ferryActionNextRoadText": { + "description": "Text for the ferry maneuver action" + }, + "leftExitActionText": "Take the left exit", + "@leftExitActionText": { + "description": "Text for the leftExit maneuver action" + }, + "leftExitActionNextRoadText": "Take the left exit to {{0}}", + "@leftExitActionNextRoadText": { + "description": "Text for the leftExit maneuver action" + }, + "leftForkActionText": "Take the left fork", + "@leftForkActionText": { + "description": "Text for the leftFork maneuver action" + }, + "leftForkActionNextRoadText": "Take the left fork onto {{0}}", + "@leftForkActionNextRoadText": { + "description": "Text for the leftFork maneuver action" + }, + "leftRampActionText": "Take the left ramp", + "@leftRampActionText": { + "description": "Text for the leftRamp maneuver action" + }, + "leftRampActionNextRoadText": "Take the left ramp onto {{0}}", + "@leftRampActionNextRoadText": { + "description": "Text for the leftRamp maneuver action" + }, + "leftRoundaboutEnterActionText": "Enter the roundabout", + "@leftRoundaboutEnterActionText": { + "description": "Text for the leftRoundaboutEnter maneuver action" + }, + "leftRoundaboutExit1ActionText": "Take the first exit of the roundabout", + "@leftRoundaboutExit1ActionText": { + "description": "Text for the leftRoundaboutExit1 maneuver action" + }, + "leftRoundaboutExit10ActionText": "Take the 10th exit of the roundabout", + "@leftRoundaboutExit10ActionText": { + "description": "Text for the leftRoundaboutExit10 maneuver action" + }, + "leftRoundaboutExit11ActionText": "Take the 11th exit of the roundabout", + "@leftRoundaboutExit11ActionText": { + "description": "Text for the leftRoundaboutExit11 maneuver action" + }, + "leftRoundaboutExit12ActionText": "Take the 12th exit of the roundabout", + "@leftRoundaboutExit12ActionText": { + "description": "Text for the leftRoundaboutExit12 maneuver action" + }, + "leftRoundaboutExit2ActionText": "Take the second exit of the roundabout", + "@leftRoundaboutExit2ActionText": { + "description": "Text for the leftRoundaboutExit2 maneuver action" + }, + "leftRoundaboutExit3ActionText": "Take the third exit of the roundabout", + "@leftRoundaboutExit3ActionText": { + "description": "Text for the leftRoundaboutExit3 maneuver action" + }, + "leftRoundaboutExit4ActionText": "Take the fourth exit of the roundabout", + "@leftRoundaboutExit4ActionText": { + "description": "Text for the leftRoundaboutExit4 maneuver action" + }, + "leftRoundaboutExit5ActionText": "Take the fifth exit of the roundabout", + "@leftRoundaboutExit5ActionText": { + "description": "Text for the leftRoundaboutExit5 maneuver action" + }, + "leftRoundaboutExit6ActionText": "Take the sixth exit of the roundabout", + "@leftRoundaboutExit6ActionText": { + "description": "Text for the leftRoundaboutExit6 maneuver action" + }, + "leftRoundaboutExit7ActionText": "Take the 7th exit of the roundabout", + "@leftRoundaboutExit7ActionText": { + "description": "Text for the leftRoundaboutExit7 maneuver action" + }, + "leftRoundaboutExit8ActionText": "Take the 8th exit of the roundabout", + "@leftRoundaboutExit8ActionText": { + "description": "Text for the leftRoundaboutExit8 maneuver action" + }, + "leftRoundaboutExit9ActionText": "Take the 9th exit of the roundabout", + "@leftRoundaboutExit9ActionText": { + "description": "Text for the leftRoundaboutExit9 maneuver action" + }, + "leftRoundaboutPassActionText": "Pass the roundabout", + "@leftRoundaboutPassActionText": { + "description": "Text for the leftRoundaboutPass maneuver action" + }, + "leftTurnActionText": "Turn left", + "@leftTurnActionText": { + "description": "Text for the leftTurn maneuver action" + }, + "leftTurnActionNextRoadText": "Turn left on {{0}}", + "@leftTurnActionNextRoadText": { + "description": "Text for the leftTurn maneuver action" + }, + "leftUTurnActionText": "Make a left U-turn", + "@leftUTurnActionText": { + "description": "Text for the leftUTurn maneuver action" + }, + "leftUTurnActionNextRoadText": "Make a left U-turn at {{0}}", + "@leftUTurnActionNextRoadText": { + "description": "Text for the leftUTurn maneuver action" + }, + "middleForkActionText": "Take the middle fork", + "@middleForkActionText": { + "description": "Text for the middleFork maneuver action" + }, + "middleForkActionNextRoadText": "Take the middle fork onto {{0}}", + "@middleForkActionNextRoadText": { + "description": "Text for the middleFork maneuver action" + }, + "rightExitActionText": "Take the right exit", + "@rightExitActionText": { + "description": "Text for the rightExit maneuver action" + }, + "rightExitActionNextRoadText": "Take the right exit to {{0}}", + "@rightExitActionNextRoadText": { + "description": "Text for the rightExit maneuver action" + }, + "rightForkActionText": "Take the right fork", + "@rightForkActionText": { + "description": "Text for the rightFork maneuver action" + }, + "rightForkActionNextRoadText": "Take the right fork onto {{0}}", + "@rightForkActionNextRoadText": { + "description": "Text for the rightFork maneuver action" + }, + "rightRampActionText": "Take the right ramp", + "@rightRampActionText": { + "description": "Text for the rightRamp maneuver action" + }, + "rightRampActionNextRoadText": "Take the right ramp onto {{0}}", + "@rightRampActionNextRoadText": { + "description": "Text for the rightRamp maneuver action" + }, + "rightRoundaboutEnterActionText": "Enter the roundabout", + "@rightRoundaboutEnterActionText": { + "description": "Text for the rightRoundaboutEnter maneuver action" + }, + "rightRoundaboutExit1ActionText": "Take the first exit of the roundabout", + "@rightRoundaboutExit1ActionText": { + "description": "Text for the rightRoundaboutExit1 maneuver action" + }, + "rightRoundaboutExit10ActionText": "Take the 10th exit of the roundabout", + "@rightRoundaboutExit10ActionText": { + "description": "Text for the rightRoundaboutExit10 maneuver action" + }, + "rightRoundaboutExit11ActionText": "Take the 11th exit of the roundabout", + "@rightRoundaboutExit11ActionText": { + "description": "Text for the rightRoundaboutExit11 maneuver action" + }, + "rightRoundaboutExit12ActionText": "Take the 12th exit of the roundabout", + "@rightRoundaboutExit12ActionText": { + "description": "Text for the rightRoundaboutExit12 maneuver action" + }, + "rightRoundaboutExit2ActionText": "Take the second exit of the roundabout", + "@rightRoundaboutExit2ActionText": { + "description": "Text for the rightRoundaboutExit2 maneuver action" + }, + "rightRoundaboutExit3ActionText": "Take the third exit of the roundabout", + "@rightRoundaboutExit3ActionText": { + "description": "Text for the rightRoundaboutExit3 maneuver action" + }, + "rightRoundaboutExit4ActionText": "Take the fourth exit of the roundabout", + "@rightRoundaboutExit4ActionText": { + "description": "Text for the rightRoundaboutExit4 maneuver action" + }, + "rightRoundaboutExit5ActionText": "Take the fifth exit of the roundabout", + "@rightRoundaboutExit5ActionText": { + "description": "Text for the rightRoundaboutExit5 maneuver action" + }, + "rightRoundaboutExit6ActionText": "Take the sixth exit of the roundabout", + "@rightRoundaboutExit6ActionText": { + "description": "Text for the rightRoundaboutExit6 maneuver action" + }, + "rightRoundaboutExit7ActionText": "Take the 7th exit of the roundabout", + "@rightRoundaboutExit7ActionText": { + "description": "Text for the rightRoundaboutExit7 maneuver action" + }, + "rightRoundaboutExit8ActionText": "Take the 8th exit of the roundabout", + "@rightRoundaboutExit8ActionText": { + "description": "Text for the rightRoundaboutExit8 maneuver action" + }, + "rightRoundaboutExit9ActionText": "Take the 9th exit of the roundabout", + "@rightRoundaboutExit9ActionText": { + "description": "Text for the rightRoundaboutExit9 maneuver action" + }, + "rightRoundaboutPassActionText": "Take the 10th exit of the roundabout", + "@rightRoundaboutPassActionText": { + "description": "Text for the rightRoundaboutPass maneuver action" + }, + "rightTurnActionText": "Turn right", + "@rightTurnActionText": { + "description": "Text for the rightTurn maneuver action" + }, + "rightTurnActionNextRoadText": "Turn right on {{0}}", + "@rightTurnActionNextRoadText": { + "description": "Text for the rightTurn maneuver action" + }, + "rightUTurnActionText": "Make a right U-turn", + "@rightUTurnActionText": { + "description": "Text for the rightUTurn maneuver action" + }, + "rightUTurnActionNextRoadText": "Make a right U-turn at {{0}}", + "@rightUTurnActionNextRoadText": { + "description": "Text for the rightUTurn maneuver action" + }, + "sharpLeftTurnActionText": "Make a hard left turn", + "@sharpLeftTurnActionText": { + "description": "Text for the sharpLeftTurn maneuver action" + }, + "sharpLeftTurnActionNextRoadText": "Make a hard left turn onto {{0}}", + "@sharpLeftTurnActionNextRoadText": { + "description": "Text for the sharpLeftTurn maneuver action" + }, + "sharpRightTurnActionText": "Make a hard right turn", + "@sharpRightTurnActionText": { + "description": "Text for the sharpRightTurn maneuver action" + }, + "sharpRightTurnActionNextRoadText": "Make a hard right turn onto {{0}}", + "@sharpRightTurnActionNextRoadText": { + "description": "Text for the sharpRightTurn maneuver action" + }, + "slightLeftTurnActionText": "Bear left", + "@slightLeftTurnActionText": { + "description": "Text for the slightLeftTurn maneuver action" + }, + "slightLeftTurnActionNextRoadText": "Bear left onto {{0}}", + "@slightLeftTurnActionNextRoadText": { + "description": "Text for the slightLeftTurn maneuver action" + }, + "slightRightTurnActionText": "Bear right", + "@slightRightTurnActionText": { + "description": "Text for the slightRightTurn maneuver action" + }, + "slightRightTurnActionNextRoadText": "Bear right onto {{0}}", + "@slightRightTurnActionNextRoadText": { + "description": "Text for the slightRightTurn maneuver action" + } +} diff --git a/lib/landing_screen.dart b/lib/landing_screen.dart new file mode 100644 index 0000000..83479e0 --- /dev/null +++ b/lib/landing_screen.dart @@ -0,0 +1,488 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:here_sdk/consent.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/gestures.dart'; +import 'package:here_sdk/location.dart'; +import 'package:here_sdk/mapview.dart'; +import 'package:here_sdk/search.dart'; + +import 'common/place_actions_popup.dart'; +import 'common/reset_location_button.dart'; +import 'positioning/no_location_warning_widget.dart'; +import 'positioning/positioning.dart'; +import 'routing/routing_screen.dart'; +import 'routing/waypoint_info.dart'; +import 'search/search_popup.dart'; +import 'common/ui_style.dart'; +import 'common/util.dart' as Util; + +/// The home screen of the application. +class LandingScreen extends StatefulWidget { + static const String navRoute = "/"; + + LandingScreen({Key key}) : super(key: key); + + @override + _LandingScreenState createState() => _LandingScreenState(); +} + +class _LandingScreenState extends State with WidgetsBindingObserver, Positioning { + bool _mapInitSuccess = false; + HereMapController _hereMapController; + GlobalKey _hereMapKey = GlobalKey(); + OverlayEntry _locationWarningOverlay; + + MapMarker _routeFromMarker; + Place _routeFromPlace; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + if (userConsentState == ConsentUserReply.notHandled) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) => requestUserConsent()); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _hereMapController?.release(); + _routeFromMarker?.release(); + _routeFromPlace?.release(); + releaseLocationEngine(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + // this callback will be called after the user consent screen is closed + // rebuild layout with a new key for the map widget + setState(() {}); + } + } + + @override + Widget build(BuildContext context) { + if (userConsentState == ConsentUserReply.requesting || userConsentState == ConsentUserReply.notHandled) { + // don't show the map until asking the user for consent + return Scaffold(); + } + + return Scaffold( + body: Stack( + children: [ + HereMap( + key: _hereMapKey, + onMapCreated: _onMapCreated, + ), + if (Platform.isAndroid) _buildMenuButton(), + ], + ), + floatingActionButton: _mapInitSuccess ? _buildFAB(context) : null, + drawer: Platform.isAndroid ? _buildDrawer(context) : null, + extendBodyBehindAppBar: true, + ); + } + + void _onMapCreated(HereMapController hereMapController) { + _hereMapController?.release(); + _hereMapController = hereMapController; + + hereMapController.mapScene.loadSceneFromConfigurationFile('preview.normal.day.json', (MapError error) { + if (error != null) { + print('Map scene not loaded. MapError: ${error.toString()}'); + return; + } + + hereMapController.camera.lookAtPointWithDistance(Positioning.initPosition, Positioning.initDistanceToEarth); + hereMapController.setWatermarkPosition(WatermarkPlacement.bottomLeft, 0); + _addGestureListeners(); + + initLocationEngine( + hereMapController: hereMapController, + onLocationEngineStatus: (status) => _checkLocationStatus(status), + ); + + setState(() { + _mapInitSuccess = true; + }); + }); + } + + Widget _buildMenuButton() { + ColorScheme colorScheme = Theme.of(context).colorScheme; + + return SafeArea( + child: Builder( + builder: (context) => Padding( + padding: EdgeInsets.all(UIStyle.contentMarginLarge), + child: Material( + color: colorScheme.background, + borderRadius: BorderRadius.circular(UIStyle.popupsBorderRadius), + elevation: 2, + child: InkWell( + child: Padding( + padding: EdgeInsets.all(UIStyle.contentMarginMedium), + child: Icon( + Icons.menu, + color: colorScheme.primary, + ), + ), + onTap: () => Scaffold.of(context).openDrawer(), + ), + ), + ), + ), + ); + } + + List _buildUserConsentItems(BuildContext context) { + if (userConsentState == null) { + return []; + } + + ColorScheme colorScheme = Theme.of(context).colorScheme; + AppLocalizations appLocalizations = AppLocalizations.of(context); + + return [ + if (userConsentState != ConsentUserReply.granted) + ListTile( + title: Text( + appLocalizations.userConsentDescription, + style: TextStyle( + color: colorScheme.onSecondary, + ), + ), + ), + ListTile( + leading: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.privacy_tip, + color: userConsentState == ConsentUserReply.granted + ? UIStyle.acceptedConsentColor + : UIStyle.revokedConsentColor, + ), + ], + ), + title: Text( + appLocalizations.userConsentTitle, + style: TextStyle( + color: colorScheme.onPrimary, + ), + ), + subtitle: userConsentState == ConsentUserReply.granted + ? Text( + appLocalizations.consentGranted, + style: TextStyle( + color: UIStyle.acceptedConsentColor, + ), + ) + : Text( + appLocalizations.consentDenied, + style: TextStyle( + color: UIStyle.revokedConsentColor, + ), + ), + trailing: Icon( + Icons.arrow_forward, + color: colorScheme.onPrimary, + ), + onTap: () { + Navigator.of(context).pop(); + _mapInitSuccess = false; + // force-recreating the map view next time to avoid rendering issues + _hereMapKey = GlobalKey(); + requestUserConsent(); + }, + ), + ]; + } + + Widget _buildDrawer(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + AppLocalizations appLocalizations = AppLocalizations.of(context); + + return Drawer( + child: Ink( + color: colorScheme.primary, + child: ListView( + padding: EdgeInsets.zero, + children: [ + Container( + height: UIStyle.drawerHeaderHeight, + child: DrawerHeader( + padding: EdgeInsets.all(UIStyle.contentMarginHuge), + decoration: BoxDecoration( + color: colorScheme.onSecondary, + ), + child: Row( + children: [ + AspectRatio( + aspectRatio: 1, + child: SvgPicture.asset("assets/app_logo.svg"), + ), + SizedBox( + width: UIStyle.contentMarginMedium, + ), + Expanded( + child: Text( + appLocalizations.appTitleHeader, + style: TextStyle( + color: colorScheme.onPrimary, + ), + ), + ), + ], + ), + ), + ), + ..._buildUserConsentItems(context), + ], + ), + ), + ); + } + + Widget _buildFAB(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!enableMapUpdate) + ResetLocationButton( + onPressed: _resetCurrentPosition, + ), + Container( + height: UIStyle.contentMarginMedium, + ), + FloatingActionButton( + child: ClipOval( + child: Ink( + width: UIStyle.bigButtonHeight, + height: UIStyle.bigButtonHeight, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + UIStyle.buttonPrimaryColor, + UIStyle.buttonSecondaryColor, + ], + ), + ), + child: Icon(Icons.search), + ), + ), + onPressed: () => _onSearch(context), + ), + ], + ), + ], + ); + } + + void _addGestureListeners() { + _hereMapController.gestures.panListener = + PanListener.fromLambdas(lambda_onPan: (state, origin, translation, velocity) { + if (enableMapUpdate) { + setState(() => enableMapUpdate = false); + } + }); + + _hereMapController.gestures.tapListener = TapListener.fromLambdas(lambda_onTap: (point) { + if (_hereMapController.widgetPins.isEmpty) { + _removeRouteFromMarker(); + } + _dismissWayPointPopup(); + }); + + _hereMapController.gestures.longPressListener = LongPressListener.fromLambdas(lambda_onLongPress: (state, point) { + if (state == GestureState.begin) { + _showWayPointPopup(point); + } + }); + } + + void _dismissWayPointPopup() { + if (_hereMapController.widgetPins.isNotEmpty) { + _hereMapController.widgetPins.first.unpin(); + } + } + + void _showWayPointPopup(Point2D point) { + _dismissWayPointPopup(); + GeoCoordinates coordinates = _hereMapController.viewToGeoCoordinates(point); + + _hereMapController.pinWidget( + PlaceActionsPopup( + coordinates: coordinates, + hereMapController: _hereMapController, + onLeftButtonPressed: (place) { + _dismissWayPointPopup(); + _routeFromPlace = place; + _addRouteFromPoint(coordinates); + }, + leftButtonIcon: SvgPicture.asset( + "assets/depart_marker.svg", + width: UIStyle.bigIconSize, + height: UIStyle.bigIconSize, + ), + onRightButtonPressed: (place) { + _dismissWayPointPopup(); + _showRoutingScreen(place != null + ? WayPointInfo.withPlace( + place: place, + originalCoordinates: coordinates, + ) + : WayPointInfo.withCoordinates( + coordinates: coordinates, + )); + }, + rightButtonIcon: SvgPicture.asset( + "assets/route.svg", + color: UIStyle.addWayPointPopupForegroundColor, + width: UIStyle.bigIconSize, + height: UIStyle.bigIconSize, + ), + ), + coordinates, + anchor: Anchor2D.withHorizontalAndVertical(0.5, 1), + ); + } + + void _addRouteFromPoint(GeoCoordinates coordinates) { + if (_routeFromMarker == null) { + int markerSize = (_hereMapController.pixelScale * UIStyle.searchMarkerSize).round(); + _routeFromMarker = Util.createMarkerWithImagePath( + coordinates, + "assets/depart_marker.svg", + markerSize, + markerSize, + drawOrder: UIStyle.waypointsMarkerDrawOrder, + anchor: Anchor2D.withHorizontalAndVertical(0.5, 1), + ); + _hereMapController.mapScene.addMapMarker(_routeFromMarker); + if (!isLocationEngineStarted) { + locationVisible = false; + } + } else { + _routeFromMarker.coordinates = coordinates; + } + } + + void _removeRouteFromMarker() { + if (_routeFromMarker != null) { + _hereMapController.mapScene.removeMapMarker(_routeFromMarker); + _routeFromMarker.release(); + _routeFromMarker = null; + _routeFromPlace?.release(); + _routeFromPlace = null; + locationVisible = true; + } + } + + void _resetCurrentPosition() { + GeoCoordinates coordinates = lastKnownLocation != null ? lastKnownLocation.coordinates : Positioning.initPosition; + + _hereMapController.camera.lookAtPointWithOrientationAndDistance( + coordinates, MapCameraOrientationUpdate.withDefaults(), Positioning.initDistanceToEarth); + + setState(() => enableMapUpdate = true); + } + + void _dismissLocationWarningPopup() { + _locationWarningOverlay?.remove(); + _locationWarningOverlay = null; + } + + void _checkLocationStatus(LocationEngineStatus status) { + if (status == LocationEngineStatus.engineStarted || status == LocationEngineStatus.alreadyStarted) { + _dismissLocationWarningPopup(); + return; + } + + if (_locationWarningOverlay == null) { + _locationWarningOverlay = OverlayEntry( + builder: (context) => NoLocationWarning( + onPressed: () => _dismissLocationWarningPopup(), + ), + ); + + Overlay.of(context).insert(_locationWarningOverlay); + } + } + + void _onSearch(BuildContext context) async { + GeoCoordinates currentPosition = _hereMapController.camera.state.targetCoordinates; + + final result = await showSearchPopup( + context: context, + currentPosition: currentPosition, + hereMapController: _hereMapController, + hereMapKey: _hereMapKey, + ); + if (result != null) { + SearchResult searchResult = result; + assert(searchResult.place != null); + _showRoutingScreen(WayPointInfo.withPlace( + place: searchResult.place, + )); + } + } + + void _showRoutingScreen(WayPointInfo destination) async { + final GeoCoordinates currentPosition = + lastKnownLocation != null ? lastKnownLocation.coordinates : Positioning.initPosition; + + await Navigator.of(context).pushNamed( + RoutingScreen.navRoute, + arguments: [ + currentPosition, + _routeFromMarker != null + ? _routeFromPlace != null + ? WayPointInfo.withPlace( + place: _routeFromPlace, + originalCoordinates: _routeFromMarker.coordinates, + ) + : WayPointInfo.withCoordinates( + coordinates: _routeFromMarker.coordinates, + ) + : WayPointInfo( + coordinates: currentPosition, + ), + destination, + ], + ); + + _routeFromPlace = null; + _removeRouteFromMarker(); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..6ab7d3a --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'route_preferences/route_preferences_model.dart'; +import 'package:flutter/material.dart'; +import 'package:here_sdk/core.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; + +import 'landing_screen.dart'; +import 'navigation/navigation_screen.dart'; +import 'search/recent_search_data_model.dart'; +import 'routing/route_details_screen.dart'; +import 'routing/routing_screen.dart'; +import 'search/search_results_screen.dart'; +import 'common/ui_style.dart'; + +/// The entry point of the application. +void main() { + WidgetsFlutterBinding.ensureInitialized(); + SdkContext.init(IsolateOrigin.main); + runApp(MyApp()); +} + +/// Application root widget. +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (context) => RecentSearchDataModel()), + ChangeNotifierProvider(create: (context) => RoutePreferencesModel.withDefaults()), + ], + child: MaterialApp( + localizationsDelegates: [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en', ''), + ], + theme: UIStyle.lightTheme, + onGenerateTitle: (BuildContext context) => AppLocalizations.of(context).appTitle, + onGenerateRoute: (RouteSettings settings) { + Map routes = { + LandingScreen.navRoute: (BuildContext context) => LandingScreen(), + SearchResultsScreen.navRoute: (BuildContext context) { + List arguments = settings.arguments; + assert(arguments != null && arguments.length == 3); + return SearchResultsScreen( + queryString: arguments[0], + places: arguments[1], + currentPosition: arguments[2], + ); + }, + RoutingScreen.navRoute: (BuildContext context) { + List arguments = settings.arguments; + assert(arguments != null && arguments.length == 3); + return RoutingScreen( + currentPosition: arguments[0], + departure: arguments[1], + destination: arguments[2], + ); + }, + RouteDetailsScreen.navRoute: (BuildContext context) { + List arguments = settings.arguments; + assert(arguments != null && arguments.length == 2); + return RouteDetailsScreen( + route: arguments[0], + wayPointsController: arguments[1], + ); + }, + NavigationScreen.navRoute: (BuildContext context) { + List arguments = settings.arguments; + assert(arguments != null && arguments.length == 2); + return NavigationScreen( + route: arguments[0], + wayPoints: arguments[1], + ); + }, + }; + + WidgetBuilder builder = routes[settings.name]; + assert(builder != null); + return MaterialPageRoute( + builder: (ctx) => builder(ctx), + settings: settings, + ); + }, + initialRoute: LandingScreen.navRoute, + ), + ); + } +} diff --git a/lib/navigation/current_maneuver_widget.dart b/lib/navigation/current_maneuver_widget.dart new file mode 100644 index 0000000..6c96148 --- /dev/null +++ b/lib/navigation/current_maneuver_widget.dart @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:here_sdk/routing.dart' as Routing; + +import '../common/ui_style.dart'; +import '../common/util.dart' as Util; + +extension _ManeuverImagePath on Routing.ManeuverAction { + String get imagePath { + return "assets/maneuvers/light/" + toString().split(".").last + ".svg"; + } +} + +/// A widget that displays the current navigation maneuver. +class CurrentManeuver extends StatelessWidget { + /// The maneuver action. + final Routing.ManeuverAction action; + /// Distance to the maneuver. + final int distance; + /// Instruction for the maneuver. + final String text; + + /// Constructs a widget. + CurrentManeuver({@required this.action, @required this.distance, @required this.text}); + + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Row( + children: [ + Padding( + padding: EdgeInsets.all(UIStyle.contentMarginLarge), + child: SvgPicture.asset( + action.imagePath, + width: UIStyle.bigButtonHeight, + height: UIStyle.bigButtonHeight, + ), + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + Util.makeDistanceString(context, distance), + style: TextStyle( + color: colorScheme.background, + fontSize: UIStyle.extraHugeFontSize, + ), + ), + Container( + height: UIStyle.contentMarginSmall, + ), + Text( + text, + maxLines: 2, + style: TextStyle( + color: colorScheme.background, + fontSize: UIStyle.bigFontSize, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/navigation/maneuver_action_text_helper.dart b/lib/navigation/maneuver_action_text_helper.dart new file mode 100644 index 0000000..439e2ae --- /dev/null +++ b/lib/navigation/maneuver_action_text_helper.dart @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:here_sdk/routing.dart'; + +import '../common/util.dart'; + +String _makeActionString(String text, String template, String roadName) { + if (roadName == null || roadName.isEmpty) { + return text; + } + + return formatString(template, [roadName]); +} + +/// Helper extension class for the [Maneuver] class. +extension ManeuverActionTextHelper on Maneuver { + + /// Returns the localized text for the navigation maneuver. + String getActionText(BuildContext context) { + final AppLocalizations localizations = AppLocalizations.of(context); + final String roadName = roadTexts.names.getDefaultValue(); + final String nextRoadName = nextRoadTexts.names.getDefaultValue(); + + switch (action) { + case ManeuverAction.arrive: + return localizations.arriveActionText; + case ManeuverAction.continueOn: + return _makeActionString(localizations.continueOnActionText, localizations.continueOnActionRoadText, roadName); + case ManeuverAction.depart: + return _makeActionString(localizations.departActionText, localizations.departActionRoadText, roadName); + case ManeuverAction.ferry: + return _makeActionString(localizations.ferryActionText, localizations.ferryActionNextRoadText, nextRoadName); + case ManeuverAction.leftExit: + return _makeActionString( + localizations.leftExitActionText, localizations.leftExitActionNextRoadText, nextRoadName); + case ManeuverAction.leftFork: + return _makeActionString( + localizations.leftForkActionText, localizations.leftForkActionNextRoadText, nextRoadName); + case ManeuverAction.leftRamp: + return _makeActionString( + localizations.leftRampActionText, localizations.leftRampActionNextRoadText, nextRoadName); + case ManeuverAction.leftRoundaboutEnter: + return localizations.leftRoundaboutEnterActionText; + case ManeuverAction.leftRoundaboutExit1: + return localizations.leftRoundaboutExit1ActionText; + case ManeuverAction.leftRoundaboutExit10: + return localizations.leftRoundaboutExit10ActionText; + case ManeuverAction.leftRoundaboutExit11: + return localizations.leftRoundaboutExit11ActionText; + case ManeuverAction.leftRoundaboutExit12: + return localizations.leftRoundaboutExit12ActionText; + case ManeuverAction.leftRoundaboutExit2: + return localizations.leftRoundaboutExit2ActionText; + case ManeuverAction.leftRoundaboutExit3: + return localizations.leftRoundaboutExit3ActionText; + case ManeuverAction.leftRoundaboutExit4: + return localizations.leftRoundaboutExit4ActionText; + case ManeuverAction.leftRoundaboutExit5: + return localizations.leftRoundaboutExit5ActionText; + case ManeuverAction.leftRoundaboutExit6: + return localizations.leftRoundaboutExit6ActionText; + case ManeuverAction.leftRoundaboutExit7: + return localizations.leftRoundaboutExit7ActionText; + case ManeuverAction.leftRoundaboutExit8: + return localizations.leftRoundaboutExit8ActionText; + case ManeuverAction.leftRoundaboutExit9: + return localizations.leftRoundaboutExit9ActionText; + case ManeuverAction.leftRoundaboutPass: + return localizations.leftRoundaboutPassActionText; + case ManeuverAction.leftTurn: + return _makeActionString( + localizations.leftTurnActionText, localizations.leftTurnActionNextRoadText, nextRoadName); + case ManeuverAction.leftUTurn: + return _makeActionString( + localizations.leftUTurnActionText, localizations.leftUTurnActionNextRoadText, nextRoadName); + case ManeuverAction.middleFork: + return _makeActionString( + localizations.middleForkActionText, localizations.middleForkActionNextRoadText, nextRoadName); + case ManeuverAction.rightExit: + return _makeActionString( + localizations.rightExitActionText, localizations.rightExitActionNextRoadText, nextRoadName); + case ManeuverAction.rightFork: + return _makeActionString( + localizations.rightForkActionText, localizations.rightForkActionNextRoadText, nextRoadName); + case ManeuverAction.rightRamp: + return _makeActionString( + localizations.rightRampActionText, localizations.rightRampActionNextRoadText, nextRoadName); + case ManeuverAction.rightRoundaboutEnter: + return localizations.rightRoundaboutEnterActionText; + case ManeuverAction.rightRoundaboutExit1: + return localizations.rightRoundaboutExit1ActionText; + case ManeuverAction.rightRoundaboutExit10: + return localizations.rightRoundaboutExit10ActionText; + case ManeuverAction.rightRoundaboutExit11: + return localizations.rightRoundaboutExit11ActionText; + case ManeuverAction.rightRoundaboutExit12: + return localizations.rightRoundaboutExit12ActionText; + case ManeuverAction.rightRoundaboutExit2: + return localizations.rightRoundaboutExit2ActionText; + case ManeuverAction.rightRoundaboutExit3: + return localizations.rightRoundaboutExit3ActionText; + case ManeuverAction.rightRoundaboutExit4: + return localizations.rightRoundaboutExit4ActionText; + case ManeuverAction.rightRoundaboutExit5: + return localizations.rightRoundaboutExit5ActionText; + case ManeuverAction.rightRoundaboutExit6: + return localizations.rightRoundaboutExit6ActionText; + case ManeuverAction.rightRoundaboutExit7: + return localizations.rightRoundaboutExit7ActionText; + case ManeuverAction.rightRoundaboutExit8: + return localizations.rightRoundaboutExit8ActionText; + case ManeuverAction.rightRoundaboutExit9: + return localizations.rightRoundaboutExit9ActionText; + case ManeuverAction.rightRoundaboutPass: + return localizations.rightRoundaboutPassActionText; + case ManeuverAction.rightTurn: + return _makeActionString( + localizations.rightTurnActionText, localizations.rightTurnActionNextRoadText, nextRoadName); + case ManeuverAction.rightUTurn: + return _makeActionString( + localizations.rightUTurnActionText, localizations.rightUTurnActionNextRoadText, nextRoadName); + case ManeuverAction.sharpLeftTurn: + return _makeActionString( + localizations.sharpLeftTurnActionText, localizations.sharpLeftTurnActionNextRoadText, nextRoadName); + case ManeuverAction.sharpRightTurn: + return _makeActionString( + localizations.sharpRightTurnActionText, localizations.sharpRightTurnActionNextRoadText, nextRoadName); + case ManeuverAction.slightLeftTurn: + return _makeActionString( + localizations.slightLeftTurnActionText, localizations.slightLeftTurnActionNextRoadText, nextRoadName); + case ManeuverAction.slightRightTurn: + return _makeActionString( + localizations.slightRightTurnActionText, localizations.slightRightTurnActionNextRoadText, nextRoadName); + } + + return ""; + } +} diff --git a/lib/navigation/navigation_dialogs.dart b/lib/navigation/navigation_dialogs.dart new file mode 100644 index 0000000..2b4bb49 --- /dev/null +++ b/lib/navigation/navigation_dialogs.dart @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../common/ui_style.dart'; + +/// Creates a dialog for selecting a location source for navigation. +/// There are two options available, simulation and device location. +Future askForPositionSource(BuildContext context) async { + AppLocalizations appLocalizations = AppLocalizations.of(context); + + return await showDialog( + context: context, + builder: (context) => SimpleDialog( + title: Text(appLocalizations.selectPositioningDialogTitle), + children: [ + SimpleDialogOption( + child: ListTile( + leading: SvgPicture.asset( + "assets/route.svg", + color: Theme.of(context).colorScheme.onSecondary, + width: UIStyle.mediumIconSize, + height: UIStyle.mediumIconSize, + ), + title: Text(appLocalizations.simulatedLocationSourceTitle), + ), + onPressed: () => Navigator.of(context).pop(true), + ), + SimpleDialogOption( + child: ListTile( + leading: Icon(Icons.gps_fixed), + title: Text(appLocalizations.realLocationSourceTitle), + ), + onPressed: () => Navigator.of(context).pop(false), + ), + ], + ), + ); +} + +/// Creates a confirmation dialog to stop navigation. +Future askForExitFromNavigation(BuildContext context) async { + AppLocalizations appLocalizations = AppLocalizations.of(context); + + return await showDialog( + context: context, + builder: (context) => SimpleDialog( + children: [ + Padding( + padding: EdgeInsets.only( + left: UIStyle.contentMarginLarge, + right: UIStyle.contentMarginLarge, + top: UIStyle.contentMarginMedium, + bottom: UIStyle.contentMarginMedium, + ), + child: Text( + appLocalizations.stopNavigationDialogTitle, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: UIStyle.hugeFontSize, + fontWeight: FontWeight.bold, + ), + ), + ), + Padding( + padding: EdgeInsets.only( + left: UIStyle.contentMarginLarge, + right: UIStyle.contentMarginLarge, + top: UIStyle.contentMarginMedium, + bottom: UIStyle.contentMarginMedium, + ), + child: Text( + appLocalizations.stopNavigationDialogSubtitle, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + ), + ), + ), + SimpleDialogOption( + child: Material( + elevation: 2, + color: UIStyle.stopNavigationButtonColor, + borderRadius: BorderRadius.circular(UIStyle.bigButtonHeight), + child: Container( + height: UIStyle.bigButtonHeight, + child: Center( + child: Text( + appLocalizations.stopNavigationAcceptButtonCaption, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + fontWeight: FontWeight.bold, + color: UIStyle.stopNavigationButtonIconColor, + ), + ), + ), + ), + ), + onPressed: () => Navigator.of(context).pop(true), + ), + SimpleDialogOption( + child: Padding( + padding: EdgeInsets.only( + left: UIStyle.contentMarginLarge, + right: UIStyle.contentMarginLarge, + top: UIStyle.contentMarginMedium, + bottom: UIStyle.contentMarginMedium, + ), + child: Center( + child: Text( + appLocalizations.cancelTitle, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSecondary, + ), + ), + ), + ), + onPressed: () => Navigator.of(context).pop(false), + ), + ], + ), + ); +} diff --git a/lib/navigation/navigation_progress_widget.dart b/lib/navigation/navigation_progress_widget.dart new file mode 100644 index 0000000..cb813de --- /dev/null +++ b/lib/navigation/navigation_progress_widget.dart @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; + +import '../common/ui_style.dart'; +import 'route_progress_widget.dart'; + +/// A widget that displays the current navigation progress. +class NavigationProgress extends StatelessWidget { + /// The length of the route. + final int routeLengthInMeters; + /// Remaining distance in meters. + final int remainingDistanceInMeters; + /// Remaining time in seconds. + final int remainingDurationInSeconds; + + /// Constructs a widget. + NavigationProgress({ + Key key, + @required this.routeLengthInMeters, + @required this.remainingDistanceInMeters, + @required this.remainingDurationInSeconds, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + AppLocalizations appLocalizations = AppLocalizations.of(context); + + DateTime dt = DateTime.now(); + DateTime dtArrival = dt.add(Duration(seconds: remainingDurationInSeconds)); + + int remainingHours = (remainingDurationInSeconds / 3600).truncate(); + int remainingMinutes = ((remainingDurationInSeconds - remainingHours * 3600) / 60).truncate(); + + String remainingDistanceUnits = appLocalizations.kilometerAbbreviationText; + int remainingDistance = (remainingDistanceInMeters / 1000).truncate(); + if (remainingDistance == 0) { + remainingDistance = remainingDistanceInMeters; + remainingDistanceUnits = appLocalizations.meterAbbreviationText; + } + + return BottomAppBar( + color: colorScheme.background, + child: Padding( + padding: EdgeInsets.all(UIStyle.contentMarginMedium), + child: Column( + children: [ + Row( + children: [ + Column( + children: [ + Text( + DateFormat.Hm().format(dtArrival), + style: TextStyle( + fontSize: UIStyle.extraHugeFontSize, + ), + ), + Text( + appLocalizations.arrivalTitle, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + color: colorScheme.onSecondary, + ), + ), + ], + ), + Container( + width: UIStyle.contentMarginHuge, + ), + Column( + children: [ + Text( + remainingHours.toString(), + style: TextStyle( + fontSize: UIStyle.extraHugeFontSize, + ), + ), + Text( + appLocalizations.hourAbbreviationText, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + color: colorScheme.onSecondary, + ), + ), + ], + ), + Container( + width: UIStyle.contentMarginLarge, + ), + Column( + children: [ + Text( + remainingMinutes.toString(), + style: TextStyle( + fontSize: UIStyle.extraHugeFontSize, + ), + ), + Text( + appLocalizations.minuteAbbreviationText, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + color: colorScheme.onSecondary, + ), + ), + ], + ), + Container( + width: UIStyle.contentMarginHuge, + ), + Column( + children: [ + Text( + remainingDistance.toString(), + style: TextStyle( + fontSize: UIStyle.extraHugeFontSize, + ), + ), + Text( + remainingDistanceUnits.toString(), + style: TextStyle( + fontSize: UIStyle.bigFontSize, + color: colorScheme.onSecondary, + ), + ), + ], + ), + ], + ), + Expanded( + child: Container( + width: double.infinity, + child: RouteProgress( + routeLengthInMeters: routeLengthInMeters, + remainingDistanceInMeters: remainingDistanceInMeters, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/navigation/navigation_screen.dart b/lib/navigation/navigation_screen.dart new file mode 100644 index 0000000..70a38e1 --- /dev/null +++ b/lib/navigation/navigation_screen.dart @@ -0,0 +1,672 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/scheduler.dart'; +import 'package:flutter_ringtone_player/flutter_ringtone_player.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_tts/flutter_tts.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/location.dart'; +import 'package:here_sdk/mapview.dart'; +import 'package:here_sdk/navigation.dart' as Navigation; +import 'package:here_sdk/routing.dart' as Routing; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:screen/screen.dart'; + +import '../landing_screen.dart'; +import '../route_preferences/route_preferences_model.dart'; +import '../common/marquee_widget.dart'; +import '../common/ui_style.dart'; +import '../common/util.dart' as Util; +import '../common/local_notifications_helper.dart'; +import 'current_maneuver_widget.dart'; +import 'maneuver_action_text_helper.dart'; +import 'navigation_dialogs.dart' as Dialogs; +import 'navigation_progress_widget.dart'; +import 'navigation_speed_widget.dart'; +import 'next_maneuver_widget.dart'; +import 'rerouting_handler.dart'; +import 'rerouting_indicator_widget.dart'; + +/// Navigation mode screen widget. +class NavigationScreen extends StatefulWidget { + static const String navRoute = "/navigation"; + + /// Initial route for navigation. + final Routing.Route route; + /// Waypoints lists of the route. + final List wayPoints; + + /// Constructs a widget. + NavigationScreen({ + Key key, + @required this.route, + @required this.wayPoints, + }) : super(key: key); + + @override + _NavigationScreenState createState() => _NavigationScreenState(); +} + +class _NavigationScreenState extends State with WidgetsBindingObserver { + static const double _kInitDistanceToEarth = 1000; // meters + static const double _kSpeedFactor = 1.3; + static const int _kNotificationIntervalInMilliseconds = 500; + static const double _kDistanceToShowNextManeuver = 500; + static const double _kTopBarHeight = 100; + static const double _kBottomBarHeight = 130; + static const double _kHereLogoOffset = 75; + static const double _kPrincipalPointOffset = 160; + + // This is example code and not for real use. + // These values are usually country specific and may vary depending on the navigation segment. + static const double _kDefaultSpeedLimitOffset = 1; + static const double _kDefaultSpeedLimitBoundary = 50; + + final GlobalKey _mapKey = GlobalKey(); + + Routing.Route _currentRoute; + + HereMapController _hereMapController; + MapPolyline _mapRoute; + MapMarker _startMarker; + MapMarker _finishMarker; + + Navigation.LocationSimulator _locationSimulator; + LocationEngine _locationEngine; + Navigation.VisualNavigator _visualNavigator; + bool _navigationStarted = false; + + bool _soundEnabled = true; + FlutterTts _flutterTts = FlutterTts(); + + int _remainingDistanceInMeters; + int _remainingDurationInSeconds; + int _currentManeuverIndex; + int _currentManeuverDistance = 0; + int _nextManeuverIndex; + int _nextManeuverDistance = 0; + String _currentStreetName; + double _currentSpeedLimit; + double _currentSpeed; + Navigation.SpeedWarningStatus _speedWarningStatus = Navigation.SpeedWarningStatus.speedLimitRestored; + + ReroutingHandler _reroutingHandler; + bool _reroutingInProgress = false; + + AppLifecycleState _appLifecycleState; + + @override + void initState() { + super.initState(); + Screen.keepOn(true); + _visualNavigator = Navigation.VisualNavigator.make(); + _remainingDistanceInMeters = widget.route.lengthInMeters; + _remainingDurationInSeconds = widget.route.durationInSeconds; + _currentRoute = widget.route; + WidgetsBinding.instance.addObserver(this); + + _reroutingHandler = ReroutingHandler( + visualNavigator: _visualNavigator, + wayPoints: widget.wayPoints, + preferences: context.read(), + onBeginRerouting: () => setState(() => _reroutingInProgress = true), + onNewRoute: _onNewRoute, + ); + } + + @override + void dispose() { + _reroutingHandler.release(); + _visualNavigator.release(); + _locationSimulator?.stop(); + _locationSimulator?.release(); + _locationEngine?.release(); + _startMarker?.release(); + _finishMarker?.release(); + _releaseCurrentRoute(); + _hereMapController?.release(); + _flutterTts.stop(); + WidgetsBinding.instance.removeObserver(this); + Screen.keepOn(false); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Widget nextManeuverWidget = _reroutingInProgress ? null : _buildNextManeuver(context); + Widget topBarWidget = _buildTopBar(context); + double topOffset = MediaQuery.of(context).padding.top - UIStyle.popupsBorderRadius; + + return WillPopScope( + child: Scaffold( + appBar: topBarWidget, + body: Padding( + padding: EdgeInsets.only( + top: topBarWidget != null ? _kTopBarHeight + topOffset : 0, + ), + child: Stack( + children: [ + HereMap( + key: _mapKey, + onMapCreated: _onMapCreated, + ), + if (nextManeuverWidget != null) nextManeuverWidget, + if (_navigationStarted) _buildNavigationControls(context), + ], + ), + ), + extendBodyBehindAppBar: true, + bottomNavigationBar: _navigationStarted + ? Container( + height: _kBottomBarHeight, + child: NavigationProgress( + routeLengthInMeters: _currentRoute.lengthInMeters, + remainingDistanceInMeters: _remainingDistanceInMeters, + remainingDurationInSeconds: _remainingDurationInSeconds, + ), + ) + : null, + ), + onWillPop: () async { + if (await Dialogs.askForExitFromNavigation(context)) { + await _stopNavigation(); + return true; + } + return false; + }, + ); + } + + void _onMapCreated(HereMapController hereMapController) { + _hereMapController?.release(); + _hereMapController = hereMapController; + + hereMapController.mapScene.loadSceneFromConfigurationFile('preview.normal.day.json', (MapError error) async { + if (error != null) { + print('Map scene not loaded. MapError: ${error.toString()}'); + return; + } + + hereMapController.camera.lookAtPointWithDistance(_currentRoute.polyline.first, _kInitDistanceToEarth); + hereMapController.setWatermarkPosition(WatermarkPlacement.bottomLeft, 0); + + hereMapController.mapScene.setLayerState(MapSceneLayers.trafficFlow, MapSceneLayerState.visible); + hereMapController.mapScene.setLayerState(MapSceneLayers.trafficIncidents, MapSceneLayerState.visible); + + _addRouteToMap(); + bool result = await Dialogs.askForPositionSource(context); + if (result == null) { + // Nothing answered. Go back. + Navigator.of(context).pop(); + return; + } + + if (result) { + _startSimulatedLocations(); + } else { + _startRealLocations(); + } + + _startNavigation(); + }); + } + + void _createMapRoute() { + GeoPolyline routeGeoPolyline = GeoPolyline(_currentRoute.polyline); + _mapRoute = MapPolyline(routeGeoPolyline, UIStyle.routeLineWidth, UIStyle.selectedRouteColor); + _mapRoute.outlineColor = UIStyle.selectedRouteBorderColor; + _mapRoute.outlineWidth = UIStyle.routeOutLineWidth; + _hereMapController.mapScene.addMapPolyline(_mapRoute); + } + + void _addRouteToMap() { + _createMapRoute(); + + int markerSize = (_hereMapController.pixelScale * UIStyle.locationMarkerSize).round(); + _startMarker = Util.createMarkerWithImagePath( + _currentRoute.polyline.first, + "assets/position.svg", + markerSize, + markerSize, + drawOrder: UIStyle.waypointsMarkerDrawOrder, + ); + _hereMapController.mapScene.addMapMarker(_startMarker); + + markerSize = (_hereMapController.pixelScale * UIStyle.searchMarkerSize * 2).round(); + _finishMarker = Util.createMarkerWithImagePath( + _currentRoute.polyline.last, + "assets/map_marker_big.svg", + markerSize, + markerSize, + drawOrder: UIStyle.waypointsMarkerDrawOrder, + anchor: Anchor2D.withHorizontalAndVertical(0.5, 1), + ); + _hereMapController.mapScene.addMapMarker(_finishMarker); + + _zoomToWholeRoute(); + } + + void _zoomToWholeRoute() { + final BuildContext context = _mapKey.currentContext; + if (context != null) { + _hereMapController.zoomToLogicalViewPort(geoBox: widget.route.boundingBox, context: context); + } + } + + void _startSimulatedLocations() { + Navigation.LocationSimulatorOptions options = + Navigation.LocationSimulatorOptions(_kSpeedFactor, _kNotificationIntervalInMilliseconds); + + _locationSimulator = Navigation.LocationSimulator.withRoute(widget.route, options); + _locationSimulator.listener = _visualNavigator; + _locationSimulator.start(); + } + + void _startRealLocations() { + _locationEngine = LocationEngine(); + _locationEngine.setBackgroundLocationAllowed(true); + _locationEngine.setBackgroundLocationIndicatorVisible(true); + _locationEngine.setPauseLocationUpdatesAutomatically(true); + _locationEngine.addLocationListener(_visualNavigator); + _locationEngine.startWithLocationAccuracy(LocationAccuracy.bestAvailable); + } + + void _startNavigation() { + _hereMapController.mapScene.removeMapMarker(_startMarker); + _startMarker.release(); + _startMarker = null; + + _visualNavigator.startRendering(_hereMapController); + _visualNavigator.isRouteVisible = false; + + _setupListeners(); + _setupVoiceTextMessages(); + + _visualNavigator.route = _currentRoute; + + setState(() { + _navigationStarted = true; + }); + } + + void _setupListeners() { + _visualNavigator.routeProgressListener = + Navigation.RouteProgressListener.fromLambdas(lambda_onRouteProgressUpdated: (routeProgress) { + List sectionProgressList = routeProgress.sectionProgress; + + int currentManeuverIndex; + int currentManeuverDistance = 0; + int nextManeuverIndex; + int nextManeuverDistance = 0; + + List nextManeuverList = routeProgress.maneuverProgress; + if (nextManeuverList != null && nextManeuverList.isNotEmpty) { + currentManeuverIndex = nextManeuverList.first.maneuverIndex; + currentManeuverDistance = nextManeuverList.first.remainingDistanceInMeters; + + if (nextManeuverList.length > 1) { + nextManeuverIndex = nextManeuverList[1].maneuverIndex; + nextManeuverDistance = nextManeuverList[1].remainingDistanceInMeters; + } + } + + setState(() { + _remainingDistanceInMeters = sectionProgressList.last.remainingDistanceInMeters; + _remainingDurationInSeconds = sectionProgressList.last.remainingDurationInSeconds; + + _currentManeuverIndex = currentManeuverIndex; + _currentManeuverDistance = currentManeuverDistance; + _nextManeuverIndex = nextManeuverIndex; + _nextManeuverDistance = nextManeuverDistance; + }); + }); + + _visualNavigator.navigableLocationListener = Navigation.NavigableLocationListener.fromLambdas( + lambda_onNavigableLocationUpdated: (location) { + if (_currentSpeed != location.originalLocation.speedInMetersPerSecond) { + setState(() { + _currentSpeed = location.originalLocation.speedInMetersPerSecond; + }); + } + }, + ); + + _visualNavigator.roadTextsListener = + Navigation.RoadTextsListener.fromLambdas(lambda_onRoadTextsUpdated: (roadTexts) { + if (_currentStreetName != roadTexts.names.getDefaultValue()) { + setState(() => _currentStreetName = roadTexts.names.getDefaultValue()); + } + }); + + if (_currentRoute.transportMode != Routing.TransportMode.pedestrian) { + _visualNavigator.speedLimitListener = + Navigation.SpeedLimitListener.fromLambdas(lambda_onSpeedLimitUpdated: (speedLimit) { + if (_currentSpeedLimit != speedLimit.speedLimitInMetersPerSecond) { + setState(() => _currentSpeedLimit = speedLimit.speedLimitInMetersPerSecond); + } + }); + + _visualNavigator.speedWarningOptions = Navigation.SpeedWarningOptions(Navigation.SpeedLimitOffset( + _kDefaultSpeedLimitOffset, _kDefaultSpeedLimitOffset, _kDefaultSpeedLimitBoundary)); + _visualNavigator.speedWarningListener = + Navigation.SpeedWarningListener.fromLambdas(lambda_onSpeedWarningStatusChanged: (status) { + if (status == Navigation.SpeedWarningStatus.speedLimitExceeded && _soundEnabled) { + FlutterRingtonePlayer.playNotification(); + } + setState(() => _speedWarningStatus = status); + }); + } + + _visualNavigator.destinationReachedListener = + Navigation.DestinationReachedListener.fromLambdas(lambda_onDestinationReached: () async { + await _stopNavigation(); + Navigator.of(context).popUntil((route) => route.settings.name == LandingScreen.navRoute); + }); + + _visualNavigator.routeDeviationListener = _reroutingHandler; + _visualNavigator.milestoneReachedListener = _reroutingHandler; + } + + void _setupVoiceTextMessages() async { + await _flutterTts.setLanguage("en-US"); + + _visualNavigator.maneuverNotificationListener = Navigation.ManeuverNotificationListener.fromLambdas( + lambda_onManeuverNotification: (text) { + if (_soundEnabled) { + _flutterTts.speak(text); + } + + if (_appLifecycleState == AppLifecycleState.paused) { + Routing.Maneuver maneuver = _visualNavigator.getManeuver(_currentManeuverIndex); + + LocalNotificationsHelper.showManeuverNotification( + _getRemainingTimeString(), + text, + maneuver.action.imagePath, + !_soundEnabled, + ); + + maneuver.release(); + } + }, + ); + } + + String _getRemainingTimeString() { + String arrivalInfo = AppLocalizations.of(context).arrivalTimeTitle + + ": " + + DateFormat.Hm().format(DateTime.now().add(Duration(seconds: _remainingDurationInSeconds))); + return arrivalInfo; + } + + void _stopNavigation() async { + _visualNavigator.route = null; + await _visualNavigator.stopRendering(); + _locationSimulator?.stop(); + _locationEngine?.setBackgroundLocationAllowed(false); + _locationEngine?.setBackgroundLocationIndicatorVisible(false); + _locationEngine?.setPauseLocationUpdatesAutomatically(false); + } + + void _releaseCurrentRoute() { + _hereMapController.mapScene.removeMapPolyline(_mapRoute); + _mapRoute.release(); + _mapRoute = null; + + if (_currentRoute != widget.route) { + _currentRoute.release(); + } + } + + void _onNewRoute(Routing.Route newRoute) { + if (newRoute == null) { + // rerouting failed + setState(() => _reroutingInProgress = false); + return; + } + + _visualNavigator.route = null; + _releaseCurrentRoute(); + + _currentRoute = newRoute; + _remainingDistanceInMeters = _currentRoute.lengthInMeters; + _remainingDurationInSeconds = _currentRoute.durationInSeconds; + _currentManeuverIndex = null; + _nextManeuverIndex = null; + _currentManeuverDistance = 0; + _visualNavigator.route = _currentRoute; + _createMapRoute(); + _finishMarker.coordinates = newRoute.polyline.last; + + setState(() => _reroutingInProgress = false); + } + + Widget _buildTopBar(BuildContext context) { + if (_currentManeuverIndex == null && !_reroutingInProgress) { + return null; + } + + ColorScheme colorScheme = Theme.of(context).colorScheme; + Widget child; + + if (_reroutingInProgress) { + child = ReroutingIndicator(); + } else { + Routing.Maneuver maneuver = _visualNavigator.getManeuver(_currentManeuverIndex); + assert(maneuver != null); + + child = CurrentManeuver( + action: maneuver.action, + distance: _currentManeuverDistance, + text: maneuver.getActionText(context), + ); + + maneuver.release(); + } + + return PreferredSize( + preferredSize: Size.fromHeight(_kTopBarHeight), + child: AppBar( + shape: UIStyle.bottomRoundedBorder(), + automaticallyImplyLeading: false, + backgroundColor: colorScheme.secondary, + flexibleSpace: SafeArea( + child: child, + ), + ), + ); + } + + Widget _buildNextManeuver(BuildContext context) { + if (_currentManeuverDistance > _kDistanceToShowNextManeuver || _reroutingInProgress) { + return null; + } + + Routing.Maneuver maneuver = _nextManeuverIndex != null ? _visualNavigator.getManeuver(_nextManeuverIndex) : null; + if (maneuver == null) { + return null; + } + + Routing.ManeuverAction action = maneuver.action; + String text = maneuver.getActionText(context); + + maneuver.release(); + + return Align( + alignment: Alignment.topCenter, + child: Material( + color: Theme.of(context).colorScheme.secondaryVariant, + shape: UIStyle.bottomRoundedBorder(), + elevation: 2, + child: Padding( + padding: EdgeInsets.only( + top: UIStyle.popupsBorderRadius, + ), + child: NextManeuver( + action: action, + distance: _nextManeuverDistance, + text: text, + ), + ), + ), + ); + } + + Widget _buildButtons(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + FloatingActionButton( + heroTag: null, + child: Icon( + _soundEnabled ? Icons.volume_up : Icons.volume_off, + ), + backgroundColor: Theme.of(context).colorScheme.background, + onPressed: () async { + await _flutterTts.stop(); + setState(() => _soundEnabled = !_soundEnabled); + }, + ), + Container( + height: UIStyle.contentMarginLarge, + ), + FloatingActionButton( + heroTag: null, + child: Icon( + Icons.close, + color: UIStyle.stopNavigationButtonIconColor, + ), + backgroundColor: UIStyle.stopNavigationButtonColor, + onPressed: () async { + if (await Dialogs.askForExitFromNavigation(context)) { + await _stopNavigation(); + Navigator.of(context).popUntil((route) => route.settings.name == LandingScreen.navRoute); + } + }, + ), + ], + ); + } + + void _setupLogoAndPrincipalPointPosition() { + if (_hereMapController == null) { + return; + } + + _hereMapController.setWatermarkPosition(WatermarkPlacement.bottomCenter, + _currentStreetName != null ? (_kHereLogoOffset * _hereMapController.pixelScale).truncate() : 0); + _hereMapController.camera.principalPoint = Point2D(_hereMapController.viewportSize.width / 2, + _hereMapController.viewportSize.height - _kPrincipalPointOffset * _hereMapController.pixelScale); + } + + Widget _buildNavigationControls(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + + _setupLogoAndPrincipalPointPosition(); + + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: EdgeInsets.fromLTRB(UIStyle.contentMarginLarge, UIStyle.contentMarginLarge, UIStyle.contentMarginLarge, + UIStyle.contentMarginLarge + UIStyle.popupsBorderRadius), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.max, + children: [ + if (_currentSpeed != null) + NavigationSpeed( + currentSpeed: _currentSpeed, + speedLimit: _currentSpeedLimit, + speedWarningStatus: _speedWarningStatus, + ), + if (_currentStreetName == null) Spacer(), + if (_currentStreetName != null) + Expanded( + child: Padding( + padding: EdgeInsets.only( + left: UIStyle.contentMarginLarge, + right: UIStyle.contentMarginLarge, + ), + child: Material( + elevation: 2, + color: colorScheme.background, + borderRadius: BorderRadius.circular(UIStyle.bigButtonHeight), + child: Padding( + padding: EdgeInsets.only( + left: UIStyle.contentMarginMedium, + right: UIStyle.contentMarginMedium, + ), + child: Container( + height: UIStyle.bigButtonHeight, + child: Center( + child: MarqueeWidget( + child: Text( + _currentStreetName, + style: TextStyle( + fontSize: UIStyle.hugeFontSize, + color: colorScheme.onSecondary, + ), + maxLines: 1, + ), + ), + ), + ), + ), + ), + ), + ), + _buildButtons(context), + ], + ), + ), + ); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (!_navigationStarted) { + return; + } + + if (state == AppLifecycleState.paused) { + final Routing.Maneuver maneuver = _visualNavigator.getManeuver(_currentManeuverIndex); + + LocalNotificationsHelper.startNotifications( + _getRemainingTimeString(), maneuver.getActionText(context), maneuver.action.imagePath); + maneuver.release(); + _visualNavigator.stopRendering(); + } + if (state == AppLifecycleState.resumed) { + LocalNotificationsHelper.stopNotifications(); + _visualNavigator.startRendering(_hereMapController); + } + _appLifecycleState = state; + } +} + +extension _ManeuverImagePath on Routing.ManeuverAction { + String get imagePath { + final String subDir = SchedulerBinding.instance.window.platformBrightness == Brightness.light ? "dark" : "light"; + return "assets/maneuvers/$subDir/png/${toString().split(".").last}.png"; + } +} diff --git a/lib/navigation/navigation_speed_widget.dart b/lib/navigation/navigation_speed_widget.dart new file mode 100644 index 0000000..5f3b0b5 --- /dev/null +++ b/lib/navigation/navigation_speed_widget.dart @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:here_sdk/navigation.dart' as Navigation; + +import '../common/ui_style.dart'; + +/// A widget that displays the current speed. +class NavigationSpeed extends StatelessWidget { + static const double _kSpeedWidgetHeight = 135; + static const double _kSpeedSignBorderWidth = 5; + static const double _kKMpHinMpS = 3.6; + + /// Current speed. + final double currentSpeed; + /// Current speed limit. + final double speedLimit; + /// Current speed warning status. + final Navigation.SpeedWarningStatus speedWarningStatus; + + /// Constructs a widget. + NavigationSpeed({ + @required this.currentSpeed, + double speedLimit, + this.speedWarningStatus, + }) : speedLimit = speedLimit != null && speedLimit > 0 ? speedLimit : null; + + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Container( + width: UIStyle.bigButtonHeight + _kSpeedSignBorderWidth * 2, + height: _kSpeedWidgetHeight, + child: Stack( + children: [ + Align( + alignment: Alignment.bottomCenter, + child: Material( + elevation: 2, + color: colorScheme.background, + borderRadius: BorderRadius.circular(UIStyle.bigButtonHeight), + child: Padding( + padding: EdgeInsets.only( + top: UIStyle.contentMarginLarge, + bottom: UIStyle.contentMarginLarge, + ), + child: Container( + width: UIStyle.bigButtonHeight, + height: speedLimit != null ? _kSpeedWidgetHeight : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if (speedLimit != null) Spacer(), + Text( + (currentSpeed * _kKMpHinMpS).truncate().toString(), + style: TextStyle( + fontSize: UIStyle.extraHugeFontSize, + color: speedWarningStatus == Navigation.SpeedWarningStatus.speedLimitExceeded + ? Colors.red + : colorScheme.primary, + ), + ), + Text( + AppLocalizations.of(context).kmhAbbreviationText, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + color: colorScheme.onSecondary, + ), + ), + ], + ), + ), + ), + ), + ), + if (speedLimit != null) + Align( + alignment: Alignment.topCenter, + child: Material( + elevation: 2, + color: colorScheme.background, + borderRadius: BorderRadius.circular(UIStyle.bigButtonHeight), + child: Container( + width: UIStyle.bigButtonHeight + _kSpeedSignBorderWidth * 2, + height: UIStyle.bigButtonHeight + _kSpeedSignBorderWidth * 2, + decoration: BoxDecoration( + border: Border.all( + color: Colors.red, + width: _kSpeedSignBorderWidth, + ), + borderRadius: BorderRadius.circular(UIStyle.bigButtonHeight), + ), + child: Center( + child: Text( + (speedLimit * _kKMpHinMpS).truncate().toString(), + style: TextStyle( + fontSize: UIStyle.extraHugeFontSize, + color: colorScheme.primary, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/navigation/next_maneuver_widget.dart b/lib/navigation/next_maneuver_widget.dart new file mode 100644 index 0000000..656a4c1 --- /dev/null +++ b/lib/navigation/next_maneuver_widget.dart @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:here_sdk/routing.dart' as Routing; + +import '../common/ui_style.dart'; +import '../common/util.dart' as Util; + +extension _ManeuverImagePath on Routing.ManeuverAction { + String get imagePath { + return "assets/maneuvers/light/" + toString().split(".").last + ".svg"; + } +} + +/// A widget that displays the upcoming navigation maneuver. +class NextManeuver extends StatelessWidget { + /// Upcoming maneuver action. + final Routing.ManeuverAction action; + /// Distance to the upcoming maneuver. + final int distance; + /// Instruction text for the upcoming maneuver. + final String text; + + /// Constructs a widget. + NextManeuver({ + @required this.action, + @required this.distance, + @required this.text + }); + + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Row( + children: [ + Padding( + padding: EdgeInsets.all(UIStyle.contentMarginLarge), + child: SvgPicture.asset( + action.imagePath, + width: UIStyle.smallButtonHeight, + height: UIStyle.smallButtonHeight, + ), + ), + Text( + Util.makeDistanceString(context, distance), + style: TextStyle( + color: colorScheme.background, + fontSize: UIStyle.hugeFontSize, + ), + ), + Container( + height: 0, + width: UIStyle.contentMarginMedium, + ), + Expanded( + child: Text( + text, + style: TextStyle( + color: colorScheme.background, + fontSize: UIStyle.bigFontSize, + ), + ), + ), + ], + ); + } +} diff --git a/lib/navigation/rerouting_handler.dart b/lib/navigation/rerouting_handler.dart new file mode 100644 index 0000000..b89632c --- /dev/null +++ b/lib/navigation/rerouting_handler.dart @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'dart:async'; + +import '../route_preferences/route_preferences_model.dart'; +import 'package:flutter/material.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/navigation.dart' as Navigation; +import 'package:here_sdk/routing.dart' as Routing; + +typedef ReroutingCallback = void Function(Routing.Route newRoute); + +/// Helper class that monitors the deviation from the current route and performs route recalculation if necessary. +/// [Routing.RoutingEngine] is used to calculate a new route. Calculation of a new route starts when the deviation from +/// the current route exceeds [_kMaxRouteDeviation] meters for [_kMaxRouteDeviationTime] seconds. +class ReroutingHandler implements Navigation.RouteDeviationListener, Navigation.MilestoneReachedListener { + /// The maximum deviation distance in meters. + static const int _kMaxRouteDeviation = 20; + /// The maximum duration of deviation from the route in seconds. + static const int _kMaxRouteDeviationTime = 5; + + /// [Navigation.VisualNavigator] that runs navigation. + final Navigation.VisualNavigator visualNavigator; + /// List of way points. + List _wayPoints; + /// Routing preferences. + final RoutePreferencesModel preferences; + /// Called when route calculations is started. + final VoidCallback onBeginRerouting; + /// Called when route calculations is finished. + final ReroutingCallback onNewRoute; + + Routing.RoutingEngine _routingEngine = Routing.RoutingEngine(); + bool _reroutingInProgress = false; + Timer _reroutingTimer; + int _passedWayPointIndex = 0; + + /// Constructs a [ReroutingHandler] object. + ReroutingHandler({ + @required this.visualNavigator, + @required List wayPoints, + @required this.preferences, + @required this.onBeginRerouting, + @required this.onNewRoute, + }) : _wayPoints = wayPoints; + + /// Called by [Navigator] whenever route deviation has been observed. + @override + onRouteDeviation(Navigation.RouteDeviation routeDeviation) { + Routing.Route route = visualNavigator.route; + if (route == null || _reroutingInProgress) { + return; + } + + // Get current geographic coordinates. + Navigation.MapMatchedLocation currentMapMatchedLocation = routeDeviation.currentLocation.mapMatchedLocation; + GeoCoordinates currentGeoCoordinates = currentMapMatchedLocation == null + ? routeDeviation.currentLocation.originalLocation.coordinates + : currentMapMatchedLocation.coordinates; + double heading = currentMapMatchedLocation?.bearingInDegrees; + + // Get last geographic coordinates on route. + GeoCoordinates lastGeoCoordinatesOnRoute; + if (routeDeviation.lastLocationOnRoute != null) { + Navigation.MapMatchedLocation lastMapMatchedLocationOnRoute = + routeDeviation.lastLocationOnRoute.mapMatchedLocation; + lastGeoCoordinatesOnRoute = lastMapMatchedLocationOnRoute == null + ? routeDeviation.lastLocationOnRoute.originalLocation.coordinates + : lastMapMatchedLocationOnRoute.coordinates; + } else { + print('User was never following the route. So, we take the start of the route instead.'); + lastGeoCoordinatesOnRoute = route.sections.first.departurePlace.originalCoordinates; + } + + int distanceInMeters = currentGeoCoordinates.distanceTo(lastGeoCoordinatesOnRoute).truncate(); + if (distanceInMeters > _kMaxRouteDeviation) { + _reroutingTimer ??= Timer( + Duration(seconds: _kMaxRouteDeviationTime), () => _beginRerouting(currentGeoCoordinates, route, heading)); + } else { + _reroutingTimer?.cancel(); + _reroutingTimer = null; + } + } + + /// Releases resources. + @override + void release() { + _reroutingTimer?.cancel(); + _routingEngine.release(); + } + + void _beginRerouting(GeoCoordinates currentPosition, Routing.Route oldRoute, double heading) { + print("Begin rerouting..."); + _reroutingInProgress = true; + _reroutingTimer = null; + onBeginRerouting(); + + List newWayPoints = [ + Routing.Waypoint.withDefaults(currentPosition)..headingInDegrees = heading, + ..._wayPoints.sublist(_passedWayPointIndex + 1) + ]; + + switch (oldRoute.transportMode) { + case Routing.TransportMode.car: + _routingEngine.calculateCarRoute( + newWayPoints, preferences.carOptions, (error, routes) => _onReroutingEnd(error, routes, newWayPoints)); + break; + + case Routing.TransportMode.truck: + _routingEngine.calculateTruckRoute( + newWayPoints, preferences.truckOptions, (error, routes) => _onReroutingEnd(error, routes, newWayPoints)); + break; + + case Routing.TransportMode.scooter: + _routingEngine.calculateScooterRoute( + newWayPoints, preferences.scooterOptions, (error, routes) => _onReroutingEnd(error, routes, newWayPoints)); + break; + + case Routing.TransportMode.pedestrian: + _routingEngine.calculatePedestrianRoute(newWayPoints, preferences.pedestrianOptions, + (error, routes) => _onReroutingEnd(error, routes, newWayPoints)); + break; + + default: + assert(false); + } + } + + void _onReroutingEnd(Routing.RoutingError error, List routes, List newWayPoints) { + if (error != null) { + print('Routing failed. Error: ${error.toString()}'); + onNewRoute(null); + _reroutingInProgress = false; + return; + } + + onNewRoute(routes.first); + for (int i = 1; i < routes.length; ++i) { + routes[i].release(); + } + + _wayPoints = newWayPoints; + _passedWayPointIndex = 0; + _reroutingInProgress = false; + } + + /// Called by [Navigator] when a milestone has been reached. + @override + onMilestoneReached(Navigation.Milestone milestone) { + _passedWayPointIndex = milestone.waypointIndex; + } +} diff --git a/lib/navigation/rerouting_indicator_widget.dart b/lib/navigation/rerouting_indicator_widget.dart new file mode 100644 index 0000000..767be60 --- /dev/null +++ b/lib/navigation/rerouting_indicator_widget.dart @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../common/ui_style.dart'; + +/// A widget indicating that a rerouting is in progress. +class ReroutingIndicator extends StatelessWidget { + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.all(UIStyle.contentMarginLarge), + child: Container( + width: UIStyle.bigButtonHeight, + height: UIStyle.bigButtonHeight, + child: CircularProgressIndicator( + backgroundColor: UIStyle.reroutingProgressBackgroundColor, + valueColor: AlwaysStoppedAnimation(UIStyle.reroutingProgressColor), + ), + ), + ), + Expanded( + child: Text( + AppLocalizations.of(context).reroutingInProgressText, + style: TextStyle( + color: colorScheme.background, + fontSize: UIStyle.extraHugeFontSize, + ), + ), + ), + ], + ); + } +} diff --git a/lib/navigation/route_progress_widget.dart b/lib/navigation/route_progress_widget.dart new file mode 100644 index 0000000..0702e03 --- /dev/null +++ b/lib/navigation/route_progress_widget.dart @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; + +import '../common/ui_style.dart'; + +/// A widget that displays the progress on the route as a progress bar. +class RouteProgress extends StatelessWidget { + /// Length of the route. + final int routeLengthInMeters; + /// Remaining distance of the route. + final int remainingDistanceInMeters; + + /// Constructs a widget. + RouteProgress({ + Key key, + @required this.routeLengthInMeters, + @required this.remainingDistanceInMeters, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomPaint( + size: Size.infinite, + painter: _RoutePainter( + routeLengthInMeters: routeLengthInMeters, + remainingDistanceInMeters: remainingDistanceInMeters, + travelledColor: Theme.of(context).colorScheme.onSecondary, + remainingColor: Theme.of(context).colorScheme.secondary, + currentColor: UIStyle.currentPositionColor, + ), + ); + } +} + +class _RoutePainter extends CustomPainter { + static const double _kLineWidth = 5; + static const double _kPositionSize = 10; + + final int routeLengthInMeters; + final int remainingDistanceInMeters; + final Color travelledColor; + final Color remainingColor; + final Color currentColor; + + _RoutePainter({ + @required this.routeLengthInMeters, + @required this.remainingDistanceInMeters, + @required this.travelledColor, + @required this.remainingColor, + @required this.currentColor, + }); + + @override + void paint(Canvas canvas, Size size) { + Paint paint = Paint(); + double currentPosition = + (routeLengthInMeters - remainingDistanceInMeters) / routeLengthInMeters * (size.width - _kLineWidth * 4) + + _kLineWidth * 2; + + paint.color = travelledColor; + paint.style = PaintingStyle.stroke; + paint.strokeWidth = _kLineWidth; + + canvas.drawLine(Offset(_kLineWidth * 2, size.height / 2), Offset(currentPosition, size.height / 2), paint); + paint.strokeWidth = 1; + canvas.drawCircle(Offset(_kLineWidth, size.height / 2), _kLineWidth, paint); + + paint.strokeWidth = _kLineWidth; + paint.color = remainingColor; + canvas.drawLine( + Offset(currentPosition, size.height / 2), Offset(size.width - _kLineWidth * 2, size.height / 2), paint); + paint.strokeWidth = 1; + canvas.drawCircle(Offset(size.width - _kLineWidth, size.height / 2), _kLineWidth, paint); + + Path currentPositionShape = Path() + ..moveTo(_kPositionSize, 0) + ..lineTo(-_kPositionSize, -_kPositionSize) + ..lineTo(-_kPositionSize / 2, 0) + ..lineTo(-_kPositionSize, _kPositionSize) + ..lineTo(_kPositionSize, 0); + Matrix4 matrix4 = Matrix4.identity(); + matrix4.translate(currentPosition, size.height / 2); + currentPositionShape = currentPositionShape.transform(matrix4.storage); + + paint.style = PaintingStyle.fill; + paint.color = currentColor; + canvas.drawPath(currentPositionShape, paint); + paint.style = PaintingStyle.stroke; + paint.color = Colors.white; + canvas.drawPath(currentPositionShape, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} diff --git a/lib/positioning/no_location_warning_widget.dart b/lib/positioning/no_location_warning_widget.dart new file mode 100644 index 0000000..981bfea --- /dev/null +++ b/lib/positioning/no_location_warning_widget.dart @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../common/ui_style.dart'; + +/// A widget that displays a warning that positioning is not available.. +class NoLocationWarning extends StatelessWidget { + static const double _kOverlayPosition = 100; + static const double _kOverlayHeight = 75; + + /// Called when the close button is tapped or otherwise activated. + final VoidCallback onPressed; + + /// Constructs a widget. + NoLocationWarning({ + Key key, + @required this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Positioned( + left: UIStyle.contentMarginMedium, + right: UIStyle.contentMarginMedium, + bottom: _kOverlayPosition, + child: Material( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(UIStyle.popupsBorderRadius)), + ), + color: UIStyle.noLocationWarningBackgroundColor, + elevation: 2, + child: SizedBox( + height: _kOverlayHeight, + child: Center( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.all(UIStyle.contentMarginMedium), + child: SvgPicture.asset( + "assets/gps.svg", + color: UIStyle.noLocationWarningColor, + width: UIStyle.bigIconSize, + height: UIStyle.bigIconSize, + ), + ), + Expanded( + child: Text( + AppLocalizations.of(context).noLocationWarning, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + color: UIStyle.noLocationWarningColor, + ), + ), + ), + IconButton( + icon: Icon( + Icons.close, + color: UIStyle.noLocationWarningColor, + ), + onPressed: () => onPressed(), + ), + ], + ), + ), + ), + ), + ); +} diff --git a/lib/positioning/positioning.dart b/lib/positioning/positioning.dart new file mode 100644 index 0000000..de4cb34 --- /dev/null +++ b/lib/positioning/positioning.dart @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:here_sdk/consent.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/location.dart'; +import 'package:here_sdk/mapview.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../common/ui_style.dart'; +import '../common/util.dart' as Util; + +typedef LocationEngineStatusCallback = void Function(LocationEngineStatus status); +typedef LocationUpdatedCallback = void Function(Location location); + +/// Mixin that implements logic for positioning. It asks for user consent, obtains the necessary permissions, +/// and provides current location updates to classes that use this mixin. +/// The current implementation will only ask for user consent on Android devices. +mixin Positioning { + static const double initDistanceToEarth = 8000; // meters + static final GeoCoordinates initPosition = GeoCoordinates(52.530932, 13.384915); + static final ConsentEngine _consentEngine = Platform.isAndroid ? ConsentEngine() : null; + + HereMapController _hereMapController; + LocationEngine _locationEngine; + + LocationEngineStatusCallback _onLocationEngineStatus; + LocationUpdatedCallback _onLocationUpdatedCallback; + + MapPolygon _locationAccuracyCircle; + MapMarker _locationMarker; + bool _locationMarkerVisible = false; + + bool enableMapUpdate = true; + + /// Gets last known location. + Location get lastKnownLocation => _locationEngine?.lastKnownLocation; + + /// Gets the state of the location engine. + bool get isLocationEngineStarted => _locationEngine != null ? _locationEngine.isStarted : false; + + /// Gets the state of the current location marker. + bool get locationVisible => _locationMarkerVisible; + + /// Sets the state of the current location marker. + set locationVisible(bool visible) { + if (_hereMapController != null && _locationMarker != null) { + if (visible) { + _hereMapController.mapScene.addMapMarker(_locationMarker); + _hereMapController.mapScene.addMapPolygon(_locationAccuracyCircle); + } else { + _hereMapController.mapScene.removeMapMarker(_locationMarker); + _hereMapController.mapScene.removeMapPolygon(_locationAccuracyCircle); + } + _locationMarkerVisible = visible; + } + } + + /// Releases resources. + void releaseLocationEngine() { + _locationEngine?.release(); + _consentEngine?.release(); + _locationAccuracyCircle?.release(); + _locationMarker?.release(); + } + + /// Initializes the location engine. The [hereMapController] is used to display current position marker, + /// [onLocationEngineStatus] and [onLocationUpdated] callbacks are required to get location updates. + void initLocationEngine({ + @required HereMapController hereMapController, + LocationEngineStatusCallback onLocationEngineStatus, + LocationUpdatedCallback onLocationUpdated, + }) async { + _hereMapController = hereMapController; + _onLocationEngineStatus = onLocationEngineStatus; + _onLocationUpdatedCallback = onLocationUpdated; + + await _askPermissions(); + } + + /// Displays user consent form. + void requestUserConsent() { + _consentEngine?.requestUserConsent(); + } + + /// Gets user consent state. + ConsentUserReply get userConsentState { + return _consentEngine?.userConsentState; + } + + void _askPermissions() async { + Map statuses = await [ + Permission.location, + ].request(); + + final bool locationEnabled = await Permission.location.serviceStatus.isEnabled; + + if (statuses.containsKey(Permission.location) && statuses[Permission.location].isGranted && locationEnabled) { + // The required permissions have been granted, let's start the location engine + await _createAndInitLocationEngine(); + return; + } + + _addMyLocationToMap(geoCoordinates: initPosition); + if (_onLocationEngineStatus != null) { + _onLocationEngineStatus(LocationEngineStatus.missingPermissions); + } + } + + void _createAndInitLocationEngine() async { + releaseLocationEngine(); + + _locationEngine = LocationEngine(); + _locationEngine.addLocationListener(LocationListener.fromLambdas( + lambda_onLocationUpdated: (location) => _onLocationUpdated(location), + )); + _locationEngine.addLocationStatusListener(LocationStatusListener.fromLambdas( + lambda_onStatusChanged: _onStatusChanged, + lambda_onFeaturesNotAvailable: null, + )); + + LocationEngineStatus status = _locationEngine.startWithLocationAccuracy(LocationAccuracy.bestAvailable); + if (status != LocationEngineStatus.alreadyStarted && status != LocationEngineStatus.engineStarted) { + return; + } + + final Location lastKnownLocation = _locationEngine.lastKnownLocation; + if (lastKnownLocation != null) { + final double accuracy = + (lastKnownLocation.horizontalAccuracyInMeters != null) ? lastKnownLocation.horizontalAccuracyInMeters : 0; + + // Show the obtained last known location on a map. + _addMyLocationToMap(geoCoordinates: lastKnownLocation.coordinates, accuracyRadiusInMeters: accuracy); + // Update the map viewport to be centered on the location. + if (enableMapUpdate) { + _hereMapController.camera.lookAtPointWithDistance(lastKnownLocation.coordinates, initDistanceToEarth); + } + } else { + // No last known location available, show a pre-defined location. + _addMyLocationToMap(geoCoordinates: initPosition); + // Update the map viewport to be centered on the location. + if (enableMapUpdate) { + _hereMapController.camera.lookAtPointWithDistance(initPosition, initDistanceToEarth); + } + } + } + + void _addMyLocationToMap({ + GeoCoordinates geoCoordinates, + double accuracyRadiusInMeters = 0, + }) { + int locationMarkerSize = (UIStyle.locationMarkerSize * _hereMapController.pixelScale).truncate(); + + // Transparent halo around the current location. + _locationAccuracyCircle = + MapPolygon(_createGeometry(geoCoordinates, accuracyRadiusInMeters), UIStyle.accuracyCircleColor); + // Image on top of the current location. + _locationMarker = Util.createMarkerWithImagePath( + geoCoordinates, + "assets/position.svg", + locationMarkerSize, + locationMarkerSize, + ); + + // Add the circle to the map. + _hereMapController.mapScene.addMapPolygon(_locationAccuracyCircle); + _hereMapController.mapScene.addMapMarker(_locationMarker); + _locationMarkerVisible = true; + } + + GeoPolygon _createGeometry(GeoCoordinates geoCoordinates, double accuracyRadiusInMeters) { + GeoCircle geoCircle = GeoCircle(geoCoordinates, accuracyRadiusInMeters); + GeoPolygon geoPolygon = GeoPolygon.withGeoCircle(geoCircle); + return geoPolygon; + } + + void _onLocationUpdated(Location location) { + final double accuracy = (location.horizontalAccuracyInMeters != null) ? location.horizontalAccuracyInMeters : 0.0; + if (_locationAccuracyCircle != null) { + _locationAccuracyCircle.geometry = _createGeometry(location.coordinates, accuracy); + } + if (_locationMarker != null) { + _locationMarker.coordinates = location.coordinates; + } + + // Update the map viewport to be centered on the location. + if (enableMapUpdate) { + _hereMapController.camera.lookAtPoint(location.coordinates); + } + + if (_onLocationUpdatedCallback != null) { + _onLocationUpdatedCallback(location); + } + } + + void _onStatusChanged(LocationEngineStatus status) { + if (_onLocationEngineStatus != null) { + _onLocationEngineStatus(status); + } + } +} diff --git a/lib/route_preferences/avoidance/country_avoidance_screen.dart b/lib/route_preferences/avoidance/country_avoidance_screen.dart new file mode 100644 index 0000000..ab521b0 --- /dev/null +++ b/lib/route_preferences/avoidance/country_avoidance_screen.dart @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'dart:collection'; + +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/routing.dart'; + +import '../enum_string_helper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../route_preferences_model.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../../common/ui_style.dart'; + +/// Country avoidance options screen widget. +class CountryAvoidanceScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + final AvoidanceOptions avoidanceOptions = + context.select((RoutePreferencesModel model) => model.sharedAvoidanceOptions); + + LinkedHashMap countryCodesMap = EnumStringHelper.countryCodesMap(context); + List sortedCountryNames = countryCodesMap.keys.toList()..sort(); + + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context).avoidCountriesTitle), + centerTitle: true, + backgroundColor: UIStyle.preferencesBackgroundColor, + textTheme: Theme.of(context).textTheme), + body: Container( + color: UIStyle.preferencesBackgroundColor, + child: ListView.builder( + itemCount: sortedCountryNames.length, + itemBuilder: (context, index) { + CountryCode code = countryCodesMap[sortedCountryNames[index]]; + return CheckboxListTile( + title: Text(sortedCountryNames[index]), + value: avoidanceOptions.countries.contains(code), + onChanged: (bool enable) { + List updatedCountries = List.from(avoidanceOptions.countries); + enable ? updatedCountries.add(code) : updatedCountries.remove(code); + + context.read().sharedAvoidanceOptions = AvoidanceOptions( + avoidanceOptions.roadFeatures, + updatedCountries, + avoidanceOptions.avoidAreas, + [], + ); + }, + ); + }), + ), + ); + } +} diff --git a/lib/route_preferences/avoidance/road_features_avoidance_screen.dart b/lib/route_preferences/avoidance/road_features_avoidance_screen.dart new file mode 100644 index 0000000..e9c6205 --- /dev/null +++ b/lib/route_preferences/avoidance/road_features_avoidance_screen.dart @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'dart:collection'; +import '../enum_string_helper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:here_sdk/routing.dart'; +import 'package:provider/provider.dart'; +import '../route_preferences_model.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../../common/ui_style.dart'; + +/// Road features avoidance options screen widget. +class RoadFeaturesAvoidanceScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + final AvoidanceOptions avoidanceOptions = + context.select((RoutePreferencesModel model) => model.sharedAvoidanceOptions); + + LinkedHashMap roadFeaturesMap = EnumStringHelper.sortedRoadFeaturesMap(context); + + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context).avoidRoadFeaturesTitle), + centerTitle: true, + backgroundColor: UIStyle.preferencesBackgroundColor, + textTheme: Theme.of(context).textTheme), + body: Container( + color: UIStyle.preferencesBackgroundColor, + child: ListView( + children: roadFeaturesMap.keys.map((String key) { + return CheckboxListTile( + title: Text(key), + value: avoidanceOptions.roadFeatures.contains(roadFeaturesMap[key]), + onChanged: (bool enable) { + RoadFeatures changedFeature = roadFeaturesMap[key]; + List updatedFeatures = List.from(avoidanceOptions.roadFeatures); + enable ? updatedFeatures.add(changedFeature) : updatedFeatures.remove(changedFeature); + + context.read().sharedAvoidanceOptions = AvoidanceOptions( + updatedFeatures, + avoidanceOptions.countries, + avoidanceOptions.avoidAreas, + [], + ); + }, + ); + }).toList(), + ), + ), + ); + } +} diff --git a/lib/route_preferences/avoidance/route_avoidance_options_widget.dart b/lib/route_preferences/avoidance/route_avoidance_options_widget.dart new file mode 100644 index 0000000..5b7f8ac --- /dev/null +++ b/lib/route_preferences/avoidance/route_avoidance_options_widget.dart @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:RefApp/route_preferences/enum_string_helper.dart'; +import 'package:here_sdk/routing.dart'; +import 'package:provider/provider.dart'; +import '../route_preferences_model.dart'; +import 'road_features_avoidance_screen.dart'; +import '../preferences_section_title_widget.dart'; +import '../preferences_disclosure_row_widget.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'country_avoidance_screen.dart'; + +/// Route avoidance options screen widget. +class RouteAvoidanceOptionsWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + final AvoidanceOptions avoidanceOptions = + context.select((RoutePreferencesModel model) => model.sharedAvoidanceOptions); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PreferencesSectionTitle(title: AppLocalizations.of(context).avoidanceOptionsTitle), + PreferencesDisclosureRowWidget( + title: AppLocalizations.of(context).avoidRoadFeaturesTitle, + subTitle: EnumStringHelper.roadFeatureNamesToString(context, avoidanceOptions.roadFeatures), + onPressed: () => + Navigator.push(context, MaterialPageRoute(builder: (context) => RoadFeaturesAvoidanceScreen())), + ), + PreferencesDisclosureRowWidget( + title: AppLocalizations.of(context).avoidCountriesTitle, + subTitle: EnumStringHelper.countryCodeNamesToString(context, avoidanceOptions.countries), + onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => CountryAvoidanceScreen())), + ), + ], + ); + } +} diff --git a/lib/route_preferences/car_options_screen.dart b/lib/route_preferences/car_options_screen.dart new file mode 100644 index 0000000..aeee289 --- /dev/null +++ b/lib/route_preferences/car_options_screen.dart @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'avoidance/route_avoidance_options_widget.dart'; +import 'route_options_widget.dart'; +import 'package:flutter/cupertino.dart'; +import 'route_text_options_widget.dart'; + +/// Routing settings widget for car mode. +class CarOptionsScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [RouteOptionsWidget(), RouteTextOptionsWidget(), RouteAvoidanceOptionsWidget()], + ), + ); + } +} diff --git a/lib/route_preferences/dropdown_widget.dart b/lib/route_preferences/dropdown_widget.dart new file mode 100644 index 0000000..d7ef9f5 --- /dev/null +++ b/lib/route_preferences/dropdown_widget.dart @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'enum_string_helper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +/// Dropdown list widget. +class DropdownWidget extends StatelessWidget { + /// Creates a widget. + DropdownWidget({ + Key key, + @required this.data, + @required this.selectedValue, + @required this.onChanged, + }) : super(key: key); + + /// Dropdown list items. + final Map data; + /// Id of the selected value. + final int selectedValue; + /// Called when the selected item is changed. + final Function onChanged; + + @override + Widget build(BuildContext context) { + return DropdownButton( + isExpanded: true, + value: selectedValue ?? EnumStringHelper.noneValueIndex, + items: data.entries + .map( + (entry) => DropdownMenuItem( + child: ListTile(title: Text(entry.value)), + value: entry.key, + ), + ) + .toList(), + onChanged: onChanged, + ); + } +} diff --git a/lib/route_preferences/enum_string_helper.dart b/lib/route_preferences/enum_string_helper.dart new file mode 100644 index 0000000..9dab209 --- /dev/null +++ b/lib/route_preferences/enum_string_helper.dart @@ -0,0 +1,1023 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'dart:collection'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/routing.dart'; + +/// Helper class for the routing options strings. +class EnumStringHelper { + static int noneValueIndex = -1; + + /// Returns the mapping of [TextFormat] values to the corresponding strings. + static Map routeInstructionsFormatMap(BuildContext context) { + final Map result = Map(); + for (TextFormat value in TextFormat.values) { + switch (value) { + case TextFormat.html: + result[value.index] = AppLocalizations.of(context).textFormatHtml; + break; + case TextFormat.plain: + result[value.index] = AppLocalizations.of(context).textFormatPlain; + break; + default: + throw StateError("Invalid enum value $value for TextFormat enum."); + } + } + return result; + } + + /// Returns the mapping of [OptimizationMode] values to the corresponding strings. + static Map routeOptimizationModeMap(BuildContext context) { + final Map result = Map(); + for (OptimizationMode value in OptimizationMode.values) { + switch (value) { + case OptimizationMode.fastest: + result[value.index] = AppLocalizations.of(context).fastestRouteTitle; + break; + case OptimizationMode.shortest: + result[value.index] = AppLocalizations.of(context).shortestRouteTitle; + break; + default: + throw StateError("Invalid enum value $value for OptimizationMode enum."); + } + } + return result; + } + + /// Returns the mapping of [UnitSystem] values to the corresponding strings. + static Map routeUnitSystemMap(BuildContext context) { + final Map result = Map(); + for (UnitSystem value in UnitSystem.values) { + switch (value) { + case UnitSystem.metric: + result[value.index] = AppLocalizations.of(context).unitSystemMetric; + break; + case UnitSystem.imperialUk: + result[value.index] = AppLocalizations.of(context).unitSystemImperialUk; + break; + case UnitSystem.imperialUs: + result[value.index] = AppLocalizations.of(context).unitSystemImperialUs; + break; + default: + throw StateError("Invalid enum value $value for TextFormat enum."); + } + } + return result; + } + + // The app currently only supports a few languages for routing. + // For the whole list please check LanguageCode enum. + /// Returns the mapping of [LanguageCode] values to the corresponding strings. + static Map routeLanguageMap(BuildContext context) { + final Map result = Map(); + for (LanguageCode value in LanguageCode.values) { + switch (value) { + case LanguageCode.arSa: + result[value.index] = AppLocalizations.of(context).languageCodeArSa; + break; + case LanguageCode.enUs: + result[value.index] = AppLocalizations.of(context).languageCodeEnUs; + break; + case LanguageCode.frFr: + result[value.index] = AppLocalizations.of(context).languageCodeFrFr; + break; + case LanguageCode.deDe: + result[value.index] = AppLocalizations.of(context).languageCodeDeDe; + break; + case LanguageCode.esEs: + result[value.index] = AppLocalizations.of(context).languageCodeEsEs; + break; + case LanguageCode.ptPt: + result[value.index] = AppLocalizations.of(context).languageCodePtPt; + break; + case LanguageCode.zhCn: + result[value.index] = AppLocalizations.of(context).languageCodeZhCn; + break; + case LanguageCode.hiIn: + result[value.index] = AppLocalizations.of(context).languageCodeHiIn; + break; + + default: + } + } + return result; + } + + /// Returns the mapping of [TunnelCategory] values to the corresponding strings. + static Map tunnelCategoryMap(BuildContext context) { + final Map result = Map(); + result[noneValueIndex] = AppLocalizations.of(context).noneTitle; + + for (TunnelCategory value in TunnelCategory.values) { + switch (value) { + case TunnelCategory.b: + result[value.index] = AppLocalizations.of(context).tunnelCategoryB; + break; + case TunnelCategory.c: + result[value.index] = AppLocalizations.of(context).tunnelCategoryC; + break; + case TunnelCategory.d: + result[value.index] = AppLocalizations.of(context).tunnelCategoryD; + break; + case TunnelCategory.e: + result[value.index] = AppLocalizations.of(context).tunnelCategoryE; + break; + default: + throw StateError("Invalid enum value $value for TunnelCategory enum."); + } + } + return result; + } + + static String roadFeatureNamesToString(BuildContext context, List roadFeatures) { + List result = []; + sortedRoadFeaturesMap(context).forEach((key, value) { + if (roadFeatures.contains(value)) result.add(key); + }); + return result.join(", "); + } + + /// Returns the mapping of [RoadFeatures] values to the corresponding strings. + static LinkedHashMap sortedRoadFeaturesMap(BuildContext context) { + final Map result = Map(); + AppLocalizations localizations = AppLocalizations.of(context); + + for (RoadFeatures value in RoadFeatures.values) { + switch (value) { + case RoadFeatures.seasonalClosure: + result[localizations.seasonalClosure] = value; + break; + case RoadFeatures.tollRoad: + result[localizations.tollRoad] = value; + break; + case RoadFeatures.controlledAccessHighway: + result[localizations.controlledAccessHighway] = value; + break; + case RoadFeatures.ferry: + result[localizations.ferry] = value; + break; + case RoadFeatures.carShuttleTrain: + result[localizations.carShuttleTrain] = value; + break; + case RoadFeatures.tunnel: + result[localizations.tunnel] = value; + break; + case RoadFeatures.dirtRoad: + result[localizations.dirtRoad] = value; + break; + case RoadFeatures.difficultTurns: + result[localizations.difficultTurns] = value; + break; + default: + throw StateError("Invalid enum value $value for RoadFeatures enum."); + } + } + return LinkedHashMap.fromIterable(result.keys.toList()..sort(), key: (k) => k, value: (k) => result[k]); + } + + /// Returns concatenated string of the all values from the [hazardousGoods] list. + static String hazardousGoodsNamesToString(BuildContext context, List hazardousGoods) { + List result = []; + sortedHazardousGoodsMap(context).forEach((key, value) { + if (hazardousGoods.contains(value)) result.add(key); + }); + return result.join(", "); + } + + /// Returns the mapping of [HazardousGood] values to the corresponding strings. + static LinkedHashMap sortedHazardousGoodsMap(BuildContext context) { + final Map result = Map(); + AppLocalizations localizations = AppLocalizations.of(context); + + for (HazardousGood value in HazardousGood.values) { + switch (value) { + case HazardousGood.explosive: + result[localizations.hazardousGoodsExplosive] = value; + break; + case HazardousGood.gas: + result[localizations.hazardousGoodsGas] = value; + break; + case HazardousGood.flammable: + result[localizations.hazardousGoodsFlammable] = value; + break; + case HazardousGood.combustible: + result[localizations.hazardousGoodsCombustible] = value; + break; + case HazardousGood.organic: + result[localizations.hazardousGoodsOrganic] = value; + break; + case HazardousGood.poison: + result[localizations.hazardousGoodsPoison] = value; + break; + case HazardousGood.radioactive: + result[localizations.hazardousGoodsRadioactive] = value; + break; + case HazardousGood.corrosive: + result[localizations.hazardousGoodsCorrosive] = value; + break; + case HazardousGood.poisonousInhalation: + result[localizations.hazardousGoodsPoisonousInhalation] = value; + break; + case HazardousGood.harmfulToWater: + result[localizations.hazardousGoodsHarmfulToWater] = value; + break; + case HazardousGood.other: + result[localizations.hazardousGoodsOther] = value; + break; + default: + throw StateError("Invalid enum value $value for HazardousGood enum."); + } + } + return LinkedHashMap.fromIterable(result.keys.toList()..sort(), key: (k) => k, value: (k) => result[k]); + } + + /// Returns concatenated string of the all values from the [countryCodes] list. + static String countryCodeNamesToString(BuildContext context, List countryCodes) { + List result = []; + countryCodesMap(context).forEach((key, value) { + if (countryCodes.contains(value)) result.add(key); + }); + return (result..sort()).join(", "); + } + + /// Returns the mapping of [CountryCode] values to the corresponding strings. + static Map countryCodesMap(BuildContext context) { + final Map result = Map(); + AppLocalizations localizations = AppLocalizations.of(context); + + // Keep enum order(sorted) as in SDK + for (CountryCode value in CountryCode.values) { + switch (value) { + case CountryCode.abw: + result[localizations.countryCodeAbw] = value; + break; + case CountryCode.afg: + result[localizations.countryCodeAfg] = value; + break; + case CountryCode.ago: + result[localizations.countryCodeAgo] = value; + break; + case CountryCode.aia: + result[localizations.countryCodeAia] = value; + break; + case CountryCode.ala: + result[localizations.countryCodeAla] = value; + break; + case CountryCode.alb: + result[localizations.countryCodeAlb] = value; + break; + case CountryCode.and: + result[localizations.countryCodeAnd] = value; + break; + case CountryCode.are: + result[localizations.countryCodeAre] = value; + break; + case CountryCode.arg: + result[localizations.countryCodeArg] = value; + break; + case CountryCode.arm: + result[localizations.countryCodeArm] = value; + break; + case CountryCode.asm: + result[localizations.countryCodeAsm] = value; + break; + case CountryCode.ata: + result[localizations.countryCodeAta] = value; + break; + case CountryCode.atf: + result[localizations.countryCodeAtf] = value; + break; + case CountryCode.atg: + result[localizations.countryCodeAtg] = value; + break; + case CountryCode.aus: + result[localizations.countryCodeAus] = value; + break; + case CountryCode.aut: + result[localizations.countryCodeAut] = value; + break; + case CountryCode.aze: + result[localizations.countryCodeAze] = value; + break; + case CountryCode.bdi: + result[localizations.countryCodeBdi] = value; + break; + case CountryCode.bel: + result[localizations.countryCodeBel] = value; + break; + case CountryCode.ben: + result[localizations.countryCodeBen] = value; + break; + case CountryCode.bes: + result[localizations.countryCodeBes] = value; + break; + case CountryCode.bfa: + result[localizations.countryCodeBfa] = value; + break; + case CountryCode.bgd: + result[localizations.countryCodeBgd] = value; + break; + case CountryCode.bgr: + result[localizations.countryCodeBgr] = value; + break; + case CountryCode.bhr: + result[localizations.countryCodeBhr] = value; + break; + case CountryCode.bhs: + result[localizations.countryCodeBhs] = value; + break; + case CountryCode.bih: + result[localizations.countryCodeBih] = value; + break; + case CountryCode.blm: + result[localizations.countryCodeBlm] = value; + break; + case CountryCode.blr: + result[localizations.countryCodeBlr] = value; + break; + case CountryCode.blz: + result[localizations.countryCodeBlz] = value; + break; + case CountryCode.bmu: + result[localizations.countryCodeBmu] = value; + break; + case CountryCode.bol: + result[localizations.countryCodeBol] = value; + break; + case CountryCode.bra: + result[localizations.countryCodeBra] = value; + break; + case CountryCode.brb: + result[localizations.countryCodeBrb] = value; + break; + case CountryCode.brn: + result[localizations.countryCodeBrn] = value; + break; + case CountryCode.btn: + result[localizations.countryCodeBtn] = value; + break; + case CountryCode.bvt: + result[localizations.countryCodeBvt] = value; + break; + case CountryCode.bwa: + result[localizations.countryCodeBwa] = value; + break; + case CountryCode.caf: + result[localizations.countryCodeCaf] = value; + break; + case CountryCode.can: + result[localizations.countryCodeCan] = value; + break; + case CountryCode.cck: + result[localizations.countryCodeCck] = value; + break; + case CountryCode.che: + result[localizations.countryCodeChe] = value; + break; + case CountryCode.chl: + result[localizations.countryCodeChl] = value; + break; + case CountryCode.chn: + result[localizations.countryCodeChn] = value; + break; + case CountryCode.civ: + result[localizations.countryCodeCiv] = value; + break; + case CountryCode.cmr: + result[localizations.countryCodeCmr] = value; + break; + case CountryCode.cod: + result[localizations.countryCodeCod] = value; + break; + case CountryCode.cog: + result[localizations.countryCodeCog] = value; + break; + case CountryCode.cok: + result[localizations.countryCodeCok] = value; + break; + case CountryCode.col: + result[localizations.countryCodeCol] = value; + break; + case CountryCode.com: + result[localizations.countryCodeCom] = value; + break; + case CountryCode.cpv: + result[localizations.countryCodeCpv] = value; + break; + case CountryCode.cri: + result[localizations.countryCodeCri] = value; + break; + case CountryCode.cub: + result[localizations.countryCodeCub] = value; + break; + case CountryCode.cuw: + result[localizations.countryCodeCuw] = value; + break; + case CountryCode.cxr: + result[localizations.countryCodeCxr] = value; + break; + case CountryCode.cym: + result[localizations.countryCodeCym] = value; + break; + case CountryCode.cyp: + result[localizations.countryCodeCyp] = value; + break; + case CountryCode.cze: + result[localizations.countryCodeCze] = value; + break; + case CountryCode.deu: + result[localizations.countryCodeDeu] = value; + break; + case CountryCode.dji: + result[localizations.countryCodeDji] = value; + break; + case CountryCode.dma: + result[localizations.countryCodeDma] = value; + break; + case CountryCode.dnk: + result[localizations.countryCodeDnk] = value; + break; + case CountryCode.dom: + result[localizations.countryCodeDom] = value; + break; + case CountryCode.dza: + result[localizations.countryCodeDza] = value; + break; + case CountryCode.ecu: + result[localizations.countryCodeEcu] = value; + break; + case CountryCode.egy: + result[localizations.countryCodeEgy] = value; + break; + case CountryCode.eri: + result[localizations.countryCodeEri] = value; + break; + case CountryCode.esh: + result[localizations.countryCodeEsh] = value; + break; + case CountryCode.esp: + result[localizations.countryCodeEsp] = value; + break; + case CountryCode.est: + result[localizations.countryCodeEst] = value; + break; + case CountryCode.eth: + result[localizations.countryCodeEth] = value; + break; + case CountryCode.fin: + result[localizations.countryCodeFin] = value; + break; + case CountryCode.fji: + result[localizations.countryCodeFji] = value; + break; + case CountryCode.flk: + result[localizations.countryCodeFlk] = value; + break; + case CountryCode.fra: + result[localizations.countryCodeFra] = value; + break; + case CountryCode.fro: + result[localizations.countryCodeFro] = value; + break; + case CountryCode.fsm: + result[localizations.countryCodeFsm] = value; + break; + case CountryCode.gab: + result[localizations.countryCodeGab] = value; + break; + case CountryCode.gbr: + result[localizations.countryCodeGbr] = value; + break; + case CountryCode.geo: + result[localizations.countryCodeGeo] = value; + break; + case CountryCode.ggy: + result[localizations.countryCodeGgy] = value; + break; + case CountryCode.gha: + result[localizations.countryCodeGha] = value; + break; + case CountryCode.gib: + result[localizations.countryCodeGib] = value; + break; + case CountryCode.gin: + result[localizations.countryCodeGin] = value; + break; + case CountryCode.glp: + result[localizations.countryCodeGlp] = value; + break; + case CountryCode.gmb: + result[localizations.countryCodeGmb] = value; + break; + case CountryCode.gnb: + result[localizations.countryCodeGnb] = value; + break; + case CountryCode.gnq: + result[localizations.countryCodeGnq] = value; + break; + case CountryCode.grc: + result[localizations.countryCodeGrc] = value; + break; + case CountryCode.grd: + result[localizations.countryCodeGrd] = value; + break; + case CountryCode.grl: + result[localizations.countryCodeGrl] = value; + break; + case CountryCode.gtm: + result[localizations.countryCodeGtm] = value; + break; + case CountryCode.guf: + result[localizations.countryCodeGuf] = value; + break; + case CountryCode.gum: + result[localizations.countryCodeGum] = value; + break; + case CountryCode.guy: + result[localizations.countryCodeGuy] = value; + break; + case CountryCode.hkg: + result[localizations.countryCodeHkg] = value; + break; + case CountryCode.hmd: + result[localizations.countryCodeHmd] = value; + break; + case CountryCode.hnd: + result[localizations.countryCodeHnd] = value; + break; + case CountryCode.hrv: + result[localizations.countryCodeHrv] = value; + break; + case CountryCode.hti: + result[localizations.countryCodeHti] = value; + break; + case CountryCode.hun: + result[localizations.countryCodeHun] = value; + break; + case CountryCode.idn: + result[localizations.countryCodeIdn] = value; + break; + case CountryCode.imn: + result[localizations.countryCodeImn] = value; + break; + case CountryCode.ind: + result[localizations.countryCodeInd] = value; + break; + case CountryCode.iot: + result[localizations.countryCodeIot] = value; + break; + case CountryCode.irl: + result[localizations.countryCodeIrl] = value; + break; + case CountryCode.irn: + result[localizations.countryCodeIrn] = value; + break; + case CountryCode.irq: + result[localizations.countryCodeIrq] = value; + break; + case CountryCode.isl: + result[localizations.countryCodeIsl] = value; + break; + case CountryCode.isr: + result[localizations.countryCodeIsr] = value; + break; + case CountryCode.ita: + result[localizations.countryCodeIta] = value; + break; + case CountryCode.jam: + result[localizations.countryCodeJam] = value; + break; + case CountryCode.jey: + result[localizations.countryCodeJey] = value; + break; + case CountryCode.jor: + result[localizations.countryCodeJor] = value; + break; + case CountryCode.jpn: + result[localizations.countryCodeJpn] = value; + break; + case CountryCode.kaz: + result[localizations.countryCodeKaz] = value; + break; + case CountryCode.ken: + result[localizations.countryCodeKen] = value; + break; + case CountryCode.kgz: + result[localizations.countryCodeKgz] = value; + break; + case CountryCode.khm: + result[localizations.countryCodeKhm] = value; + break; + case CountryCode.kir: + result[localizations.countryCodeKir] = value; + break; + case CountryCode.kna: + result[localizations.countryCodeKna] = value; + break; + case CountryCode.kor: + result[localizations.countryCodeKor] = value; + break; + case CountryCode.kwt: + result[localizations.countryCodeKwt] = value; + break; + case CountryCode.lao: + result[localizations.countryCodeLao] = value; + break; + case CountryCode.lbn: + result[localizations.countryCodeLbn] = value; + break; + case CountryCode.lbr: + result[localizations.countryCodeLbr] = value; + break; + case CountryCode.lby: + result[localizations.countryCodeLby] = value; + break; + case CountryCode.lca: + result[localizations.countryCodeLca] = value; + break; + case CountryCode.lie: + result[localizations.countryCodeLie] = value; + break; + case CountryCode.lka: + result[localizations.countryCodeLka] = value; + break; + case CountryCode.lso: + result[localizations.countryCodeLso] = value; + break; + case CountryCode.ltu: + result[localizations.countryCodeLtu] = value; + break; + case CountryCode.lux: + result[localizations.countryCodeLux] = value; + break; + case CountryCode.lva: + result[localizations.countryCodeLva] = value; + break; + case CountryCode.mac: + result[localizations.countryCodeMac] = value; + break; + case CountryCode.maf: + result[localizations.countryCodeMaf] = value; + break; + case CountryCode.mar: + result[localizations.countryCodeMar] = value; + break; + case CountryCode.mco: + result[localizations.countryCodeMco] = value; + break; + case CountryCode.mda: + result[localizations.countryCodeMda] = value; + break; + case CountryCode.mdg: + result[localizations.countryCodeMdg] = value; + break; + case CountryCode.mdv: + result[localizations.countryCodeMdv] = value; + break; + case CountryCode.mex: + result[localizations.countryCodeMex] = value; + break; + case CountryCode.mhl: + result[localizations.countryCodeMhl] = value; + break; + case CountryCode.mkd: + result[localizations.countryCodeMkd] = value; + break; + case CountryCode.mli: + result[localizations.countryCodeMli] = value; + break; + case CountryCode.mlt: + result[localizations.countryCodeMlt] = value; + break; + case CountryCode.mmr: + result[localizations.countryCodeMmr] = value; + break; + case CountryCode.mne: + result[localizations.countryCodeMne] = value; + break; + case CountryCode.mng: + result[localizations.countryCodeMng] = value; + break; + case CountryCode.mnp: + result[localizations.countryCodeMnp] = value; + break; + case CountryCode.moz: + result[localizations.countryCodeMoz] = value; + break; + case CountryCode.mrt: + result[localizations.countryCodeMrt] = value; + break; + case CountryCode.msr: + result[localizations.countryCodeMsr] = value; + break; + case CountryCode.mtq: + result[localizations.countryCodeMtq] = value; + break; + case CountryCode.mus: + result[localizations.countryCodeMus] = value; + break; + case CountryCode.mwi: + result[localizations.countryCodeMwi] = value; + break; + case CountryCode.mys: + result[localizations.countryCodeMys] = value; + break; + case CountryCode.myt: + result[localizations.countryCodeMyt] = value; + break; + case CountryCode.nam: + result[localizations.countryCodeNam] = value; + break; + case CountryCode.ncl: + result[localizations.countryCodeNcl] = value; + break; + case CountryCode.ner: + result[localizations.countryCodeNer] = value; + break; + case CountryCode.nfk: + result[localizations.countryCodeNfk] = value; + break; + case CountryCode.nga: + result[localizations.countryCodeNga] = value; + break; + case CountryCode.nic: + result[localizations.countryCodeNic] = value; + break; + case CountryCode.niu: + result[localizations.countryCodeNiu] = value; + break; + case CountryCode.nld: + result[localizations.countryCodeNld] = value; + break; + case CountryCode.nor: + result[localizations.countryCodeNor] = value; + break; + case CountryCode.npl: + result[localizations.countryCodeNpl] = value; + break; + case CountryCode.nru: + result[localizations.countryCodeNru] = value; + break; + case CountryCode.nzl: + result[localizations.countryCodeNzl] = value; + break; + case CountryCode.omn: + result[localizations.countryCodeOmn] = value; + break; + case CountryCode.pak: + result[localizations.countryCodePak] = value; + break; + case CountryCode.pan: + result[localizations.countryCodePan] = value; + break; + case CountryCode.pcn: + result[localizations.countryCodePcn] = value; + break; + case CountryCode.per: + result[localizations.countryCodePer] = value; + break; + case CountryCode.phl: + result[localizations.countryCodePhl] = value; + break; + case CountryCode.plw: + result[localizations.countryCodePlw] = value; + break; + case CountryCode.png: + result[localizations.countryCodePng] = value; + break; + case CountryCode.pol: + result[localizations.countryCodePol] = value; + break; + case CountryCode.pri: + result[localizations.countryCodePri] = value; + break; + case CountryCode.prk: + result[localizations.countryCodePrk] = value; + break; + case CountryCode.prt: + result[localizations.countryCodePrt] = value; + break; + case CountryCode.pry: + result[localizations.countryCodePry] = value; + break; + case CountryCode.pse: + result[localizations.countryCodePse] = value; + break; + case CountryCode.pyf: + result[localizations.countryCodePyf] = value; + break; + case CountryCode.qat: + result[localizations.countryCodeQat] = value; + break; + case CountryCode.reu: + result[localizations.countryCodeReu] = value; + break; + case CountryCode.rou: + result[localizations.countryCodeRou] = value; + break; + case CountryCode.rus: + result[localizations.countryCodeRus] = value; + break; + case CountryCode.rwa: + result[localizations.countryCodeRwa] = value; + break; + case CountryCode.sau: + result[localizations.countryCodeSau] = value; + break; + case CountryCode.sdn: + result[localizations.countryCodeSdn] = value; + break; + case CountryCode.sen: + result[localizations.countryCodeSen] = value; + break; + case CountryCode.sgp: + result[localizations.countryCodeSgp] = value; + break; + case CountryCode.sgs: + result[localizations.countryCodeSgs] = value; + break; + case CountryCode.shn: + result[localizations.countryCodeShn] = value; + break; + case CountryCode.sjm: + result[localizations.countryCodeSjm] = value; + break; + case CountryCode.slb: + result[localizations.countryCodeSlb] = value; + break; + case CountryCode.sle: + result[localizations.countryCodeSle] = value; + break; + case CountryCode.slv: + result[localizations.countryCodeSlv] = value; + break; + case CountryCode.smr: + result[localizations.countryCodeSmr] = value; + break; + case CountryCode.som: + result[localizations.countryCodeSom] = value; + break; + case CountryCode.spm: + result[localizations.countryCodeSpm] = value; + break; + case CountryCode.srb: + result[localizations.countryCodeSrb] = value; + break; + case CountryCode.ssd: + result[localizations.countryCodeSsd] = value; + break; + case CountryCode.stp: + result[localizations.countryCodeStp] = value; + break; + case CountryCode.sur: + result[localizations.countryCodeSur] = value; + break; + case CountryCode.svk: + result[localizations.countryCodeSvk] = value; + break; + case CountryCode.svn: + result[localizations.countryCodeSvn] = value; + break; + case CountryCode.swe: + result[localizations.countryCodeSwe] = value; + break; + case CountryCode.swz: + result[localizations.countryCodeSwz] = value; + break; + case CountryCode.sxm: + result[localizations.countryCodeSxm] = value; + break; + case CountryCode.syc: + result[localizations.countryCodeSyc] = value; + break; + case CountryCode.syr: + result[localizations.countryCodeSyr] = value; + break; + case CountryCode.tca: + result[localizations.countryCodeTca] = value; + break; + case CountryCode.tcd: + result[localizations.countryCodeTcd] = value; + break; + case CountryCode.tgo: + result[localizations.countryCodeTgo] = value; + break; + case CountryCode.tha: + result[localizations.countryCodeTha] = value; + break; + case CountryCode.tjk: + result[localizations.countryCodeTjk] = value; + break; + case CountryCode.tkl: + result[localizations.countryCodeTkl] = value; + break; + case CountryCode.tkm: + result[localizations.countryCodeTkm] = value; + break; + case CountryCode.tls: + result[localizations.countryCodeTls] = value; + break; + case CountryCode.ton: + result[localizations.countryCodeTon] = value; + break; + case CountryCode.tto: + result[localizations.countryCodeTto] = value; + break; + case CountryCode.tun: + result[localizations.countryCodeTun] = value; + break; + case CountryCode.tur: + result[localizations.countryCodeTur] = value; + break; + case CountryCode.tuv: + result[localizations.countryCodeTuv] = value; + break; + case CountryCode.twn: + result[localizations.countryCodeTwn] = value; + break; + case CountryCode.tza: + result[localizations.countryCodeTza] = value; + break; + case CountryCode.uga: + result[localizations.countryCodeUga] = value; + break; + case CountryCode.ukr: + result[localizations.countryCodeUkr] = value; + break; + case CountryCode.umi: + result[localizations.countryCodeUmi] = value; + break; + case CountryCode.ury: + result[localizations.countryCodeUry] = value; + break; + case CountryCode.usa: + result[localizations.countryCodeUsa] = value; + break; + case CountryCode.uzb: + result[localizations.countryCodeUzb] = value; + break; + case CountryCode.vat: + result[localizations.countryCodeVat] = value; + break; + case CountryCode.vct: + result[localizations.countryCodeVct] = value; + break; + case CountryCode.ven: + result[localizations.countryCodeVen] = value; + break; + case CountryCode.vgb: + result[localizations.countryCodeVgb] = value; + break; + case CountryCode.vir: + result[localizations.countryCodeVir] = value; + break; + case CountryCode.vnm: + result[localizations.countryCodeVnm] = value; + break; + case CountryCode.vut: + result[localizations.countryCodeVut] = value; + break; + case CountryCode.wlf: + result[localizations.countryCodeWlf] = value; + break; + case CountryCode.wsm: + result[localizations.countryCodeWsm] = value; + break; + case CountryCode.yem: + result[localizations.countryCodeYem] = value; + break; + case CountryCode.zaf: + result[localizations.countryCodeZaf] = value; + break; + case CountryCode.zmb: + result[localizations.countryCodeZmb] = value; + break; + case CountryCode.zwe: + result[localizations.countryCodeZwe] = value; + break; + default: + throw StateError("Invalid enum value $value for CountryCode enum."); + } + } + return result; + } +} diff --git a/lib/route_preferences/numeric_text_field_widget.dart b/lib/route_preferences/numeric_text_field_widget.dart new file mode 100644 index 0000000..d557110 --- /dev/null +++ b/lib/route_preferences/numeric_text_field_widget.dart @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../common/ui_style.dart'; + +/// A widget that allows to enter numeric values. +class NumericTextField extends StatelessWidget { + /// Initial value. + final String initialValue; + /// Hint text. + final String hintText; + /// True if the input value is to be interpreted as an integer, otherwise it is decimal. + final bool isInteger; + /// Called when the value is changed. + final Function onChanged; + + /// Constructs a widget. + NumericTextField({Key key, this.isInteger, this.initialValue, this.hintText, @required this.onChanged}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: UIStyle.roundedRectDecoration(), + child: TextFormField( + initialValue: initialValue ?? "", + keyboardType: isInteger ? TextInputType.number : TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + hintText: hintText ?? "", + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: UIStyle.contentMarginMedium), + ), + onChanged: onChanged, + ), + ); + } +} diff --git a/lib/route_preferences/pedestrian_options_screen.dart b/lib/route_preferences/pedestrian_options_screen.dart new file mode 100644 index 0000000..f7b4a21 --- /dev/null +++ b/lib/route_preferences/pedestrian_options_screen.dart @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'route_preferences_model.dart'; +import 'preferences_row_title_widget.dart'; +import 'preferences_section_title_widget.dart'; +import 'numeric_text_field_widget.dart'; +import 'route_options_widget.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:here_sdk/routing.dart'; +import 'route_text_options_widget.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:provider/provider.dart'; + +/// Routing settings widget for pedestrian mode. +class PedestrianOptionsScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + final PedestrianOptions pedestrianOptions = + context.select((RoutePreferencesModel model) => model.pedestrianOptions); + + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RouteOptionsWidget(), + RouteTextOptionsWidget(), + PreferencesSectionTitle(title: AppLocalizations.of(context).walkSpeedTitle), + PreferencesRowTitle(title: AppLocalizations.of(context).walkSpeedUnitTitle), + NumericTextField( + initialValue: pedestrianOptions.walkSpeedInMetersPerSecond.toString(), + isInteger: false, + onChanged: (text) => context.read().pedestrianOptions = PedestrianOptions( + pedestrianOptions.routeOptions, + pedestrianOptions.textOptions, + double.tryParse(text) ?? 0, + ), + ), + ], + ), + ); + } +} diff --git a/lib/route_preferences/preferences_disclosure_row_widget.dart b/lib/route_preferences/preferences_disclosure_row_widget.dart new file mode 100644 index 0000000..320cbea --- /dev/null +++ b/lib/route_preferences/preferences_disclosure_row_widget.dart @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../common/ui_style.dart'; + +/// Widget for preference disclosure row. +class PreferencesDisclosureRowWidget extends StatelessWidget { + /// Title + final String title; + /// Sub-title + final String subTitle; + /// Called when the widget is tapped or otherwise activated. + final Function onPressed; + + /// Constructs a widget. + PreferencesDisclosureRowWidget({@required this.title, @required this.onPressed, this.subTitle}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPressed, + child: Padding( + padding: const EdgeInsets.only(top: UIStyle.contentMarginExtraLarge), + child: Container( + decoration: UIStyle.bottomDividerDecoration(), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle(color: Theme.of(context).colorScheme.primary, fontSize: UIStyle.bigFontSize), + ), + if (subTitle?.isNotEmpty ?? false) + Text( + subTitle, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: Theme.of(context).colorScheme.onSecondary), + textAlign: TextAlign.left, + ) + ], + ), + ), + IconButton( + icon: Icon(Icons.keyboard_arrow_right, color: Theme.of(context).colorScheme.onSecondary), + onPressed: onPressed, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/route_preferences/preferences_row_title_widget.dart b/lib/route_preferences/preferences_row_title_widget.dart new file mode 100644 index 0000000..d8e74d3 --- /dev/null +++ b/lib/route_preferences/preferences_row_title_widget.dart @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/cupertino.dart'; + +import '../common/ui_style.dart'; + +/// Widget for preference title row. +class PreferencesRowTitle extends StatelessWidget { + /// Title + final String title; + + /// Constructs a widget. + PreferencesRowTitle({@required this.title}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: UIStyle.contentMarginExtraLarge, bottom: UIStyle.contentMarginMedium), + child: Row( + children: [Text(title)], + ), + ); + } +} diff --git a/lib/route_preferences/preferences_section_title_widget.dart b/lib/route_preferences/preferences_section_title_widget.dart new file mode 100644 index 0000000..dccc8cc --- /dev/null +++ b/lib/route_preferences/preferences_section_title_widget.dart @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/cupertino.dart'; + +import '../common/ui_style.dart'; + +/// Widget for preference section title. +class PreferencesSectionTitle extends StatelessWidget { + /// Title + final String title; + + /// Constructs a widget. + PreferencesSectionTitle({@required this.title}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: UIStyle.contentMarginExtraHuge), + child: Row(children: [ + Text(title, style: UIStyle.optionsSectionStyle(context)), + ])); + } +} diff --git a/lib/route_preferences/route_options_widget.dart b/lib/route_preferences/route_options_widget.dart new file mode 100644 index 0000000..5a363f6 --- /dev/null +++ b/lib/route_preferences/route_options_widget.dart @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'route_preferences_model.dart'; +import 'package:provider/provider.dart'; +import 'numeric_text_field_widget.dart'; +import 'enum_string_helper.dart'; +import 'preferences_row_title_widget.dart'; +import 'preferences_section_title_widget.dart'; + +import 'dropdown_widget.dart'; + +import '../common/ui_style.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:here_sdk/routing.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'dart:io' show Platform; + +import '../common/util.dart' as Util; + +/// Routing options widget. +class RouteOptionsWidget extends StatelessWidget { + static const int _departureYearDelta = 10; + static const String _alternativesRangeHint = "[0-6]"; + + @override + Widget build(BuildContext context) { + final RouteOptions routeOptions = context.select((RoutePreferencesModel model) => model.sharedRouteOptions); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PreferencesSectionTitle(title: AppLocalizations.of(context).routeOptionsTitle), + PreferencesRowTitle(title: AppLocalizations.of(context).routeAlternativesTitle), + NumericTextField( + initialValue: routeOptions.alternatives.toString(), + isInteger: true, + hintText: _alternativesRangeHint, + onChanged: (text) => context.read().sharedRouteOptions = RouteOptions( + routeOptions.optimizationMode, + int.tryParse(text) ?? 0, + routeOptions.departureTime, + ), + ), + PreferencesRowTitle(title: AppLocalizations.of(context).departureTimeTitle), + Container( + decoration: UIStyle.roundedRectDecoration(), + child: InkWell( + onTap: () async { + DateTime newDate = await (Platform.isIOS ? _selectDateTimeCupertino(context) : _selectDateTime(context)); + if (newDate != null) + context.read().sharedRouteOptions = + RouteOptions(routeOptions.optimizationMode, routeOptions.alternatives, newDate); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: UIStyle.contentMarginMedium), + child: Text(Util.stringFromDateTime(context, routeOptions.departureTime))), + Visibility( + maintainSize: true, + maintainAnimation: true, + maintainState: true, + visible: routeOptions.departureTime != null, + child: IconButton( + icon: Icon( + Icons.clear_rounded, + color: UIStyle.optionsBorderColor, + ), + onPressed: () => context.read().sharedRouteOptions = RouteOptions( + routeOptions.optimizationMode, + routeOptions.alternatives, + null, + ), + ), + ), + ], + ), + ), + ), + PreferencesRowTitle(title: AppLocalizations.of(context).optimizationModeTitle), + Container( + decoration: UIStyle.roundedRectDecoration(), + child: DropdownButtonHideUnderline( + child: DropdownWidget( + data: EnumStringHelper.routeOptimizationModeMap(context), + selectedValue: routeOptions.optimizationMode.index, + onChanged: (mode) => context.read().sharedRouteOptions = RouteOptions( + OptimizationMode.values[mode], + routeOptions.alternatives, + routeOptions.departureTime, + ), + ), + ), + ), + ], + ); + } + + Future _selectDateTime(BuildContext context) async { + final DateTime date = await _selectDate(context); + if (date == null) return null; + + final TimeOfDay time = await _selectTime(context); + if (time == null) return null; + + return DateTime(date.year, date.month, date.day, time.hour, time.minute); + } + + Future _selectDate(BuildContext context) async { + DateTime now = DateTime.now(); + return showDatePicker( + context: context, + initialDate: now, + firstDate: DateTime(now.year - _departureYearDelta), + lastDate: DateTime(now.year + _departureYearDelta), + ); + } + + Future _selectTime(BuildContext context) async { + return showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(DateTime.now()), + ); + } + + Future _selectDateTimeCupertino(BuildContext context) async { + DateTime result = await showModalBottomSheet( + context: context, + builder: (context) { + DateTime selectedDateTime = DateTime.now(); + return Container( + height: UIStyle.cupertinoPickerHeight, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(UIStyle.contentMarginMedium), + child: Row( + children: [ + Text( + AppLocalizations.of(context).selectDateTimeTitle, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: UIStyle.mediumFontSize, + fontWeight: FontWeight.bold, + ), + ) + ], + ), + ), + Expanded( + child: Container( + child: CupertinoDatePicker(onDateTimeChanged: (DateTime dateTime) => selectedDateTime = dateTime), + ), + ), + Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + CupertinoButton( + child: Text( + AppLocalizations.of(context).cancelTitle, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + ), + ), + onPressed: () => Navigator.of(context).pop(), + ), + CupertinoButton( + child: Text( + AppLocalizations.of(context).doneTitle, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + ), + ), + onPressed: () => Navigator.of(context).pop(selectedDateTime), + ), + ], + ), + ), + ], + ), + ); + }, + ); + if (result == null) return null; + + return DateTime(result.year, result.month, result.day, result.hour, result.minute); + } +} diff --git a/lib/route_preferences/route_preferences_model.dart b/lib/route_preferences/route_preferences_model.dart new file mode 100644 index 0000000..e5ea1df --- /dev/null +++ b/lib/route_preferences/route_preferences_model.dart @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/cupertino.dart'; +import 'package:here_sdk/routing.dart'; + +/// A helper class that contains all of the routing settings. +class RoutePreferencesModel extends ChangeNotifier { + static final defaultAlternativeRoutes = 1; + + // Keep transport Options readonly to prevent accidental overwriting + CarOptions _carOptions; + TruckOptions _truckOptions; + ScooterOptions _scooterOptions; + PedestrianOptions _pedestrianOptions; + + RouteOptions _sharedRouteOptions; + RouteTextOptions _sharedRouteTextOptions; + AvoidanceOptions _sharedAvoidanceOptions; + + /// Sets new routing settings for car mode. + set carOptions(CarOptions value) { + _carOptions = value; + notifyListeners(); + } + + /// Sets new routing settings for truck mode. + set truckOptions(TruckOptions value) { + _truckOptions = value; + notifyListeners(); + } + + /// Sets new routing settings for scooter mode. + set scooterOptions(ScooterOptions value) { + _scooterOptions = value; + notifyListeners(); + } + + /// Sets new routing settings for pedestrian mode. + set pedestrianOptions(PedestrianOptions value) { + _pedestrianOptions = value; + notifyListeners(); + } + + /// Sets new routing settings. + set sharedRouteOptions(RouteOptions value) { + _sharedRouteOptions = value; + _carOptions.routeOptions = _truckOptions.routeOptions = + _scooterOptions.routeOptions = _pedestrianOptions.routeOptions = _sharedRouteOptions; + notifyListeners(); + } + + /// Sets new route text settings. + set sharedRouteTextOptions(RouteTextOptions value) { + _sharedRouteTextOptions = value; + _carOptions.textOptions = _truckOptions.textOptions = + _scooterOptions.textOptions = _pedestrianOptions.textOptions = _sharedRouteTextOptions; + notifyListeners(); + } + + /// Sets new route avoidance settings. + set sharedAvoidanceOptions(AvoidanceOptions value) { + _sharedAvoidanceOptions = value; + _carOptions.avoidanceOptions = + _truckOptions.avoidanceOptions = _scooterOptions.avoidanceOptions = _sharedAvoidanceOptions; + notifyListeners(); + } + + /// Gets routing settings for car mode. + CarOptions get carOptions => _carOptions; + + /// Gets routing settings for truck mode. + TruckOptions get truckOptions => _truckOptions; + + /// Gets routing settings for scooter mode. + ScooterOptions get scooterOptions => _scooterOptions; + + /// Gets routing settings for pedestrian mode. + PedestrianOptions get pedestrianOptions => _pedestrianOptions; + + /// Gets routing settings. + RouteOptions get sharedRouteOptions => _sharedRouteOptions; + + /// Gets route text settings. + RouteTextOptions get sharedRouteTextOptions => _sharedRouteTextOptions; + + /// Gets route avoidance settings. + AvoidanceOptions get sharedAvoidanceOptions => _sharedAvoidanceOptions; + + /// Constructs a settings objects with default values. + RoutePreferencesModel.withDefaults() + : _carOptions = CarOptions.withDefaults(), + _truckOptions = TruckOptions.withDefaults(), + _scooterOptions = ScooterOptions.withDefaults(), + _pedestrianOptions = PedestrianOptions.withDefaults() { + _setupSharedOptions(); + } + + _setupSharedOptions() { + _sharedRouteTextOptions = RouteTextOptions.withDefaults(); + _sharedAvoidanceOptions = AvoidanceOptions.withDefaults(); + _sharedRouteOptions = RouteOptions.withDefaults(); + + _sharedRouteOptions.alternatives = defaultAlternativeRoutes; + + _carOptions.routeOptions = _truckOptions.routeOptions = + _scooterOptions.routeOptions = _pedestrianOptions.routeOptions = _sharedRouteOptions; + + _carOptions.textOptions = _truckOptions.textOptions = + _scooterOptions.textOptions = _pedestrianOptions.textOptions = _sharedRouteTextOptions; + + _carOptions.avoidanceOptions = + _truckOptions.avoidanceOptions = _scooterOptions.avoidanceOptions = _sharedAvoidanceOptions; + } +} diff --git a/lib/route_preferences/route_preferences_screen.dart b/lib/route_preferences/route_preferences_screen.dart new file mode 100644 index 0000000..f89a03f --- /dev/null +++ b/lib/route_preferences/route_preferences_screen.dart @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'transport_modes_widget.dart'; + +import 'car_options_screen.dart'; +import 'pedestrian_options_screen.dart'; +import 'scooter_options_screen.dart'; +import 'truck_options_screen.dart'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../common/ui_style.dart'; + +/// Routing preferences screen widget. +class RoutePreferencesScreen extends StatefulWidget { + /// Constructs a widget. + RoutePreferencesScreen({Key key, int this.activeTransportTab}) : super(key: key); + + /// Active transport mode for display. + final int activeTransportTab; + + @override + _RoutePreferencesScreenState createState() => _RoutePreferencesScreenState(); +} + +class _RoutePreferencesScreenState extends State with TickerProviderStateMixin { + TabController _transportModesTabController; + + @override + void initState() { + super.initState(); + _transportModesTabController = TabController(length: TransportModes.values.length, vsync: this); + _transportModesTabController.index = widget.activeTransportTab; + } + + @override + void dispose() { + _transportModesTabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context).routePreferencesScreenTitle), + centerTitle: true, + backgroundColor: UIStyle.preferencesBackgroundColor, + textTheme: Theme.of(context).textTheme, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(UIStyle.mediumButtonHeight), + child: Container( + color: UIStyle.tabBarBackgroundColor, + child: TransportModesWidget(tabController: _transportModesTabController)))), + body: WillPopScope( + onWillPop: () async { + Navigator.pop(context, _transportModesTabController.index); + return false; + }, + child: Container( + color: UIStyle.preferencesBackgroundColor, + child: GestureDetector( + onTap: () => FocusScope.of(context).requestFocus(new FocusNode()), + child: Padding( + padding: const EdgeInsets.only( + left: UIStyle.contentMarginMedium, + right: UIStyle.contentMarginMedium, + bottom: UIStyle.contentMarginHuge), + child: TabBarView( + controller: _transportModesTabController, + children: [CarOptionsScreen(), TruckOptionsScreen(), ScooterOptionsScreen(), PedestrianOptionsScreen()], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/route_preferences/route_text_options_widget.dart b/lib/route_preferences/route_text_options_widget.dart new file mode 100644 index 0000000..18523b7 --- /dev/null +++ b/lib/route_preferences/route_text_options_widget.dart @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'route_preferences_model.dart'; +import 'package:provider/provider.dart'; +import 'enum_string_helper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/routing.dart'; +import '../common/ui_style.dart'; +import 'dropdown_widget.dart'; +import 'preferences_section_title_widget.dart'; +import 'preferences_row_title_widget.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +/// Route text options widget. +class RouteTextOptionsWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + final RouteTextOptions textOptions = context.select((RoutePreferencesModel model) => model.sharedRouteTextOptions); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PreferencesSectionTitle(title: AppLocalizations.of(context).routeTextOptionsTitle), + PreferencesRowTitle(title: AppLocalizations.of(context).textFormatTitle), + Container( + decoration: UIStyle.roundedRectDecoration(), + child: DropdownButtonHideUnderline( + child: DropdownWidget( + data: EnumStringHelper.routeInstructionsFormatMap(context), + selectedValue: textOptions.instructionFormat.index, + onChanged: (format) => context.read().sharedRouteTextOptions = RouteTextOptions( + textOptions.language, + TextFormat.values[format], + textOptions.unitSystem, + ), + ), + ), + ), + PreferencesRowTitle(title: AppLocalizations.of(context).unitSystemTitle), + Container( + decoration: UIStyle.roundedRectDecoration(), + child: DropdownButtonHideUnderline( + child: DropdownWidget( + data: EnumStringHelper.routeUnitSystemMap(context), + selectedValue: textOptions.unitSystem.index, + onChanged: (unit) => context.read().sharedRouteTextOptions = RouteTextOptions( + textOptions.language, + textOptions.instructionFormat, + UnitSystem.values[unit], + ), + ), + ), + ), + PreferencesRowTitle(title: AppLocalizations.of(context).languageCodeTitle), + Container( + decoration: UIStyle.roundedRectDecoration(), + child: DropdownButtonHideUnderline( + child: DropdownWidget( + data: EnumStringHelper.routeLanguageMap(context), + selectedValue: textOptions.language.index, + onChanged: (language) => context.read().sharedRouteTextOptions = RouteTextOptions( + LanguageCode.values[language], + textOptions.instructionFormat, + textOptions.unitSystem, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/route_preferences/scooter_options_screen.dart b/lib/route_preferences/scooter_options_screen.dart new file mode 100644 index 0000000..42cfe93 --- /dev/null +++ b/lib/route_preferences/scooter_options_screen.dart @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'avoidance/route_avoidance_options_widget.dart'; +import 'route_preferences_model.dart'; +import 'package:provider/provider.dart'; +import 'preferences_row_title_widget.dart'; +import 'preferences_section_title_widget.dart'; +import 'package:flutter/material.dart'; +import 'route_options_widget.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:here_sdk/routing.dart'; +import 'route_text_options_widget.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +/// Routing settings widget for scooter mode. +class ScooterOptionsScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + final ScooterOptions scooterOptions = context.select((RoutePreferencesModel model) => model.scooterOptions); + + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RouteOptionsWidget(), + RouteTextOptionsWidget(), + RouteAvoidanceOptionsWidget(), + PreferencesSectionTitle(title: AppLocalizations.of(context).highwayTitle), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PreferencesRowTitle(title: AppLocalizations.of(context).allowHighwayTitle), + Switch.adaptive( + value: scooterOptions.allowHighway, + onChanged: (value) => context.read().scooterOptions = ScooterOptions( + scooterOptions.routeOptions, + scooterOptions.textOptions, + scooterOptions.avoidanceOptions, + value, + ), + ), + ], + ) + ], + ), + ); + } +} diff --git a/lib/route_preferences/transport_modes_widget.dart b/lib/route_preferences/transport_modes_widget.dart new file mode 100644 index 0000000..ec18e27 --- /dev/null +++ b/lib/route_preferences/transport_modes_widget.dart @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../common/ui_style.dart'; + +/// Available transport modes currently supported by the Ref App. +/// The HERE SDK supports more transport modes than featured by this application. +enum TransportModes { + car, + truck, + scooter, + walk, +} + +/// Widget for switching between transport modes. +class TransportModesWidget extends StatelessWidget { + /// This widget's selection and animation state. + final TabController tabController; + + /// Constructs a widget. + TransportModesWidget({ + Key key, + @required this.tabController, + }) : super(key: key); + + @override + Widget build(BuildContext context) => + TabBar(controller: tabController, tabs: _buildTransportTabs(context, tabController.index)); + + List _buildTransportTabs(BuildContext context, int selectedIndex) { + return List.generate( + TransportModes.values.length, + (index) => _buildTransportTab(context, index, selectedIndex == index), + ); + } + + Widget _buildTransportTab(BuildContext context, int index, bool isSelected) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + Color color = isSelected ? colorScheme.primary : colorScheme.onSecondary; + + return Tab( + icon: SvgPicture.asset( + TransportModes.values[index].icon, + color: color, + width: UIStyle.bigIconSize, + height: UIStyle.bigIconSize, + ), + ); + } +} + +extension _TransportModeIcon on TransportModes { + String get icon { + switch (this) { + case TransportModes.car: + return "assets/car.svg"; + case TransportModes.truck: + return "assets/truck.svg"; + case TransportModes.scooter: + return "assets/scooter.svg"; + case TransportModes.walk: + return "assets/walk.svg"; + } + return ""; + } +} diff --git a/lib/route_preferences/truck_hazardous_goods_screen.dart b/lib/route_preferences/truck_hazardous_goods_screen.dart new file mode 100644 index 0000000..e308874 --- /dev/null +++ b/lib/route_preferences/truck_hazardous_goods_screen.dart @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'dart:collection'; + +import 'enum_string_helper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:here_sdk/routing.dart'; +import 'package:provider/provider.dart'; +import 'route_preferences_model.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../common/ui_style.dart'; + +/// Truck hazardous goods preferences screen widget. +class TruckHazardousGoodsScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + final TruckOptions truckOptions = context.select((RoutePreferencesModel model) => model.truckOptions); + LinkedHashMap hazardousGoodsMap = EnumStringHelper.sortedHazardousGoodsMap(context); + + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context).hazardousGoodsTitle), + centerTitle: true, + backgroundColor: UIStyle.preferencesBackgroundColor, + textTheme: Theme.of(context).textTheme), + body: Container( + color: UIStyle.preferencesBackgroundColor, + child: ListView( + children: hazardousGoodsMap.keys.map((String key) { + return CheckboxListTile( + title: Text(key), + value: truckOptions.hazardousGoods.contains(hazardousGoodsMap[key]), + onChanged: (bool enable) { + HazardousGood changedFeature = hazardousGoodsMap[key]; + List updatedFeatures = List.from(truckOptions.hazardousGoods); + enable ? updatedFeatures.add(changedFeature) : updatedFeatures.remove(changedFeature); + + context.read().truckOptions = TruckOptions( + truckOptions.routeOptions, + truckOptions.textOptions, + truckOptions.avoidanceOptions, + truckOptions.specifications, + truckOptions.tunnelCategory, + updatedFeatures); + }, + ); + }).toList(), + ), + ), + ); + } +} diff --git a/lib/route_preferences/truck_options_screen.dart b/lib/route_preferences/truck_options_screen.dart new file mode 100644 index 0000000..d694098 --- /dev/null +++ b/lib/route_preferences/truck_options_screen.dart @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'truck_hazardous_goods_screen.dart'; +import 'preferences_disclosure_row_widget.dart'; +import 'avoidance/route_avoidance_options_widget.dart'; +import 'truck_specifications_screen.dart'; +import 'route_preferences_model.dart'; +import 'package:provider/provider.dart'; +import 'preferences_row_title_widget.dart'; +import 'preferences_section_title_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'dropdown_widget.dart'; +import 'enum_string_helper.dart'; +import 'route_options_widget.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:here_sdk/routing.dart'; +import 'route_text_options_widget.dart'; + +import '../common/ui_style.dart'; + +/// Routing settings widget for truck mode. +class TruckOptionsScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + final TruckOptions truckOptions = context.select((RoutePreferencesModel model) => model.truckOptions); + + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RouteOptionsWidget(), + RouteTextOptionsWidget(), + RouteAvoidanceOptionsWidget(), + PreferencesSectionTitle(title: AppLocalizations.of(context).truckSpecificationsTitle), + PreferencesDisclosureRowWidget( + title: AppLocalizations.of(context).specificationsTitle, + subTitle: _truckSpecificationsToString(context, truckOptions.specifications), + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => TruckSpecificationsScreen()), + ), + ), + PreferencesDisclosureRowWidget( + title: AppLocalizations.of(context).hazardousGoodsTitle, + subTitle: EnumStringHelper.hazardousGoodsNamesToString(context, truckOptions.hazardousGoods), + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => TruckHazardousGoodsScreen()), + ), + ), + PreferencesRowTitle(title: AppLocalizations.of(context).tunnelCategoryTitle), + Container( + decoration: UIStyle.roundedRectDecoration(), + child: DropdownButtonHideUnderline( + child: DropdownWidget( + data: EnumStringHelper.tunnelCategoryMap(context), + selectedValue: truckOptions.tunnelCategory?.index, + onChanged: (category) => context.read().truckOptions = TruckOptions( + truckOptions.routeOptions, + truckOptions.textOptions, + truckOptions.avoidanceOptions, + truckOptions.specifications, + category == EnumStringHelper.noneValueIndex ? null : TunnelCategory.values[category], + truckOptions.hazardousGoods, + ), + ), + ), + ), + ], + ), + ); + } + + String _truckSpecificationsToString(BuildContext context, TruckSpecifications specifications) { + AppLocalizations localizations = AppLocalizations.of(context); + return [ + if (specifications.widthInCentimeters != null) + localizations.truckWidthRowTitle + " = " + specifications.widthInCentimeters.toString(), + if (specifications.heightInCentimeters != null) + localizations.truckHeightRowTitle + " = " + specifications.heightInCentimeters.toString(), + if (specifications.lengthInCentimeters != null) + localizations.truckLengthRowTitle + " = " + specifications.lengthInCentimeters.toString(), + if (specifications.axleCount != null) + localizations.truckAxleCountRowTitle + " = " + specifications.axleCount.toString(), + if (specifications.weightPerAxleInKilograms != null) + localizations.truckWeightPerAxleRowTitle + " = " + specifications.weightPerAxleInKilograms.toString(), + if (specifications.grossWeightInKilograms != null) + localizations.truckGrossWeightRowTitle + " = " + specifications.grossWeightInKilograms.toString() + ].join(", "); + } +} diff --git a/lib/route_preferences/truck_specifications_screen.dart b/lib/route_preferences/truck_specifications_screen.dart new file mode 100644 index 0000000..87de863 --- /dev/null +++ b/lib/route_preferences/truck_specifications_screen.dart @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ +import 'preferences_row_title_widget.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:here_sdk/routing.dart'; +import 'package:provider/provider.dart'; +import 'route_preferences_model.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../common/ui_style.dart'; +import 'numeric_text_field_widget.dart'; + +/// Truck specifications screen widget. +class TruckSpecificationsScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + final TruckOptions truckOptions = context.select((RoutePreferencesModel model) => model.truckOptions); + TruckSpecifications specs = truckOptions.specifications; + AppLocalizations localizations = AppLocalizations.of(context); + + return Scaffold( + appBar: AppBar( + title: Text(localizations.truckSpecificationsTitle), + centerTitle: true, + backgroundColor: UIStyle.preferencesBackgroundColor, + textTheme: Theme.of(context).textTheme, + ), + body: GestureDetector( + onTap: () => FocusScope.of(context).requestFocus(new FocusNode()), + child: Container( + color: UIStyle.preferencesBackgroundColor, + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(UIStyle.contentMarginMedium), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + PreferencesRowTitle(title: localizations.truckWidthRowTitle), + NumericTextField( + initialValue: specs.widthInCentimeters == null ? "" : specs.widthInCentimeters.toString(), + hintText: localizations.truckWidthHint, + isInteger: true, + onChanged: (text) => context.read().truckOptions = _truckOptionsFrom( + truckOptions, + _truckSpecificationsFrom(specs, widthInCentimeters: int.tryParse(text) ?? null), + ), + ), + PreferencesRowTitle(title: localizations.truckHeightRowTitle), + NumericTextField( + initialValue: specs.heightInCentimeters == null ? "" : specs.heightInCentimeters.toString(), + hintText: localizations.truckHeightHint, + isInteger: true, + onChanged: (text) => context.read().truckOptions = _truckOptionsFrom( + truckOptions, + _truckSpecificationsFrom(specs, heightInCentimeters: int.tryParse(text) ?? null), + ), + ), + PreferencesRowTitle(title: localizations.truckLengthRowTitle), + NumericTextField( + initialValue: specs.lengthInCentimeters == null ? "" : specs.lengthInCentimeters.toString(), + hintText: localizations.truckLengthtHint, + isInteger: true, + onChanged: (text) => context.read().truckOptions = _truckOptionsFrom( + truckOptions, + _truckSpecificationsFrom(specs, lengthInCentimeters: int.tryParse(text) ?? null), + ), + ), + PreferencesRowTitle(title: localizations.truckAxleCountRowTitle), + NumericTextField( + initialValue: specs.axleCount == null ? "" : specs.axleCount.toString(), + hintText: localizations.truckAxlesCountHint, + isInteger: true, + onChanged: (text) => context.read().truckOptions = _truckOptionsFrom( + truckOptions, + _truckSpecificationsFrom(specs, axleCount: int.tryParse(text) ?? null), + ), + ), + PreferencesRowTitle(title: localizations.truckWeightPerAxleRowTitle), + NumericTextField( + initialValue: + specs.weightPerAxleInKilograms == null ? "" : specs.weightPerAxleInKilograms.toString(), + hintText: localizations.truckAxleWeightHint, + isInteger: true, + onChanged: (text) => context.read().truckOptions = _truckOptionsFrom( + truckOptions, + _truckSpecificationsFrom(specs, weightPerAxleInKilograms: int.tryParse(text) ?? null), + ), + ), + PreferencesRowTitle(title: localizations.truckGrossWeightRowTitle), + NumericTextField( + initialValue: specs.grossWeightInKilograms == null ? "" : specs.grossWeightInKilograms.toString(), + hintText: localizations.truckTotalWeightHint, + isInteger: true, + onChanged: (text) => context.read().truckOptions = _truckOptionsFrom( + truckOptions, + _truckSpecificationsFrom(specs, grossWeightInKilograms: int.tryParse(text) ?? null), + ), + ) + ], + ), + ), + ], + ), + ), + ), + ); + } + + TruckOptions _truckOptionsFrom(TruckOptions truckOptions, TruckSpecifications truckSpecifications) => TruckOptions( + truckOptions.routeOptions, + truckOptions.textOptions, + truckOptions.avoidanceOptions, + truckSpecifications, + truckOptions.tunnelCategory, + truckOptions.hazardousGoods, + ); + + TruckSpecifications _truckSpecificationsFrom(TruckSpecifications specs, + {int grossWeightInKilograms, + int weightPerAxleInKilograms, + int heightInCentimeters, + int widthInCentimeters, + int lengthInCentimeters, + int axleCount}) => + TruckSpecifications( + grossWeightInKilograms ?? specs.grossWeightInKilograms, + weightPerAxleInKilograms ?? specs.weightPerAxleInKilograms, + heightInCentimeters ?? specs.heightInCentimeters, + widthInCentimeters ?? specs.widthInCentimeters, + lengthInCentimeters ?? specs.lengthInCentimeters, + axleCount ?? specs.axleCount, + ); +} diff --git a/lib/routing/poi_svg_helper.dart b/lib/routing/poi_svg_helper.dart new file mode 100644 index 0000000..f9566bc --- /dev/null +++ b/lib/routing/poi_svg_helper.dart @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; + +import '../common/util.dart' as Util; + +const String _atmIcon = ''' + + + + + + +'''; + +const String _eatAndDrinkIcon = ''' + + + + + + +'''; + +const String _fuelingIcon = ''' + + + + + + +'''; + +const String _unknownIcon = ''' + + + + + +'''; + +const String _poiTemplate = ''' + + + +{{3}} +{{4}} + +'''; + +/// Class representing SVG image for a POI. +class SvgInfo { + final String svg; + final int width; + final int height; + + SvgInfo({ + @required this.svg, + @required this.width, + @required this.height, + }); +} + +enum PoiIconType { + atm, + eatAndDrink, + fueling, + unknown, +} + +/// Helper class that allows to create SVG images for desired POI type and text. +class PoiSVGHelper { + static const int _iconHeight = 24; + static const int _minIconSize = 20; + static const int _charAverageWidth = 4; + + /// returns SVG images for desired POI icon [type] and [text]. + static SvgInfo getPoiSvgForCategoryAndText({ + @required PoiIconType type, + String text, + }) { + int width = text != null ? text.length * _charAverageWidth + _minIconSize : 0; + + String icon; + switch (type) { + case PoiIconType.atm: + icon = _atmIcon; + break; + case PoiIconType.eatAndDrink: + icon = _eatAndDrinkIcon; + break; + case PoiIconType.fueling: + icon = _fuelingIcon; + break; + case PoiIconType.unknown: + icon = _unknownIcon; + break; + } + + return SvgInfo( + svg: Util.formatString(_poiTemplate, [ + width, // view port width + width - 1, // rectangle width + width / 2 - 5, // anchor offset + icon, // icon + text, // text to display + ]), + width: width, + height: _iconHeight, + ); + } +} diff --git a/lib/routing/route_details_screen.dart b/lib/routing/route_details_screen.dart new file mode 100644 index 0000000..9ae7e47 --- /dev/null +++ b/lib/routing/route_details_screen.dart @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/gestures.dart'; +import 'package:here_sdk/mapview.dart'; +import 'package:here_sdk/routing.dart' as Routing; + +import '../common/draggable_popup_here_logo_helper.dart'; +import '../navigation/navigation_screen.dart'; +import '../common/ui_style.dart'; +import '../common/util.dart' as Util; +import 'route_info_widget.dart'; +import 'waypoints_controller.dart'; + +/// Route details mode screen widget. +class RouteDetailsScreen extends StatefulWidget { + static const String navRoute = "/routes/details"; + + /// The route. + final Routing.Route route; + /// [WayPointsController] that contains way points for the route. + final WayPointsController wayPointsController; + + /// Constructs a widget. + RouteDetailsScreen({ + Key key, + @required this.route, + @required this.wayPointsController, + }) : super(key: key); + + @override + _RouteDetailsScreenState createState() => _RouteDetailsScreenState(); +} + +extension _ManeuverImagePath on Routing.ManeuverAction { + String get imagePath { + return "assets/maneuvers/dark/" + toString().split(".").last + ".svg"; + } +} + +class _RouteDetailsScreenState extends State { + final GlobalKey _mapKey = GlobalKey(); + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _bottomSheetKey = GlobalKey(); + + static const double _kBottomSheetMinSize = 200; + static const double _kBottomSheetHeaderSize = 75; + static const double _kZoomDistanceToManeuver = 500; + static const double _kTapRadius = 5; + + HereMapController _hereMapController; + MapPolyline _mapRoute; + List _maneuverMarkers = []; + List _wpMarkers; + List _maneuvers = []; + + bool _hasBeenZoomedToManeuver = false; + bool _maneuversSheetIsExpanded = false; + + @override + void initState() { + widget.route.sections.forEach((section) => section.maneuvers.forEach((maneuver) => _maneuvers.add(maneuver))); + super.initState(); + } + + @override + void dispose() { + _hereMapController?.release(); + _clearMapRoute(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => WillPopScope( + onWillPop: () async { + if (_hasBeenZoomedToManeuver) { + if (_maneuversSheetIsExpanded) { + DraggableScrollableActuator.reset(_bottomSheetKey.currentContext); + _hereMapController?.setWatermarkPosition(WatermarkPlacement.bottomCenter, 0); + } + + _zoomToWholeRoute(); + _hasBeenZoomedToManeuver = false; + return false; + } + + return true; + }, + child: Scaffold( + key: _scaffoldKey, + body: Column( + children: [ + Expanded( + child: HereMap( + key: _mapKey, + onMapCreated: _onMapCreated, + ), + ), + Container( + height: _kBottomSheetMinSize, + ), + ], + ), + extendBodyBehindAppBar: true, + extendBody: true, + bottomNavigationBar: _buildBottomSheet(context), + ), + ); + + void _onMapCreated(HereMapController hereMapController) { + _hereMapController?.release(); + setState(() => _hereMapController = hereMapController); + + hereMapController.mapScene.loadSceneFromConfigurationFile('preview.normal.day.json', (MapError error) { + if (error != null) { + print('Map scene not loaded. MapError: ${error.toString()}'); + return; + } + + hereMapController.setWatermarkPosition(WatermarkPlacement.bottomCenter, 0); + _addRouteToMap(); + _setTapGestureHandler(); + }); + } + + void _setTapGestureHandler() { + _hereMapController.gestures.tapListener = TapListener.fromLambdas(lambda_onTap: (Point2D touchPoint) { + _pickMapMarker(touchPoint); + }); + } + + _pickMapMarker(Point2D touchPoint) { + _hereMapController.pickMapItems(touchPoint, _kTapRadius, (pickMapItemsResult) { + List mapMarkersList = pickMapItemsResult.markers; + if (mapMarkersList.length == 0) { + print("No map markers found."); + return; + } + + int index = _maneuverMarkers.indexOf(mapMarkersList.first); + if (index >= 0) { + _zoomToManeuver(index); + } + }); + } + + _clearMapRoute() { + _mapRoute?.release(); + _maneuverMarkers.forEach((marker) => marker.release()); + _maneuverMarkers.clear(); + _wpMarkers?.forEach((marker) => marker.release()); + _wpMarkers = null; + } + + _addRouteToMap() { + _clearMapRoute(); + + GeoPolyline routeGeoPolyline = GeoPolyline(widget.route.polyline); + _mapRoute = MapPolyline(routeGeoPolyline, UIStyle.routeLineWidth, UIStyle.selectedRouteColor); + _mapRoute.outlineColor = UIStyle.selectedRouteBorderColor; + _mapRoute.outlineWidth = UIStyle.routeOutLineWidth; + + _hereMapController.mapScene.addMapPolyline(_mapRoute); + + int markerSize = (_hereMapController.pixelScale * UIStyle.maneuverMarkerSize).round(); + MapImage mapImage = MapImage.withFilePathAndWidthAndHeight("assets/maneuver.svg", markerSize, markerSize); + + widget.route.sections.forEach((section) { + section.maneuvers.forEach((maneuver) { + if (maneuver.action == Routing.ManeuverAction.depart || maneuver.action == Routing.ManeuverAction.arrive) { + return; + } + MapMarker maneuverMarker = Util.createMarkerWithImage( + maneuver.coordinates, + mapImage, + drawOrder: UIStyle.waypointsMarkerDrawOrder, + ); + _hereMapController.mapScene.addMapMarker(maneuverMarker); + _maneuverMarkers.add(maneuverMarker); + }); + }); + + mapImage.release(); + + _wpMarkers = widget.wayPointsController.buildMapMarkersForController(_hereMapController); + + _zoomToWholeRoute(); + } + + _zoomToManeuver(int index) { + _hereMapController.camera.lookAtPointWithDistance(_maneuvers[index].coordinates, _kZoomDistanceToManeuver); + _hasBeenZoomedToManeuver = true; + } + + _zoomToWholeRoute() { + final BuildContext context = _mapKey.currentContext; + if (context != null) { + final RenderBox box = context.findRenderObject() as RenderBox; + + _hereMapController.camera.principalPoint = + Point2D(box.size.width, box.size.height) / 2 * _hereMapController.pixelScale; + _hereMapController.zoomToLogicalViewPort(geoBox: widget.route.boundingBox, context: context); + } + } + + _updateMapPrincipalPoint() { + final RenderBox mapBox = _mapKey.currentContext?.findRenderObject() as RenderBox; + final RenderBox bottomSheetBox = _bottomSheetKey.currentContext?.findRenderObject() as RenderBox; + + if (mapBox != null && bottomSheetBox != null) { + _hereMapController.camera.principalPoint = + Point2D(mapBox.size.width, mapBox.size.height + _kBottomSheetMinSize - bottomSheetBox.size.height) / + 2 * + _hereMapController.pixelScale; + } + } + + Widget _buildManeuverItem(BuildContext context, int index) { + return ListTile( + leading: SvgPicture.asset(_maneuvers[index].action.imagePath), + title: Text(_maneuvers[index].text), + onTap: () => _zoomToManeuver(index), + ); + } + + Widget _buildBottomSheet(BuildContext context) { + double minSize = _kBottomSheetMinSize / MediaQuery.of(context).size.height; + + return BottomSheet( + onClosing: () {}, + builder: (context) => NotificationListener( + onNotification: (notification) { + _maneuversSheetIsExpanded = notification.minExtent != notification.extent; + _updateMapPrincipalPoint(); + return true; + }, + child: DraggableScrollableActuator( + child: DraggablePopupHereLogoHelper( + hereMapController: _hereMapController, + hereMapKey: _mapKey, + draggableScrollableSheet: DraggableScrollableSheet( + key: _bottomSheetKey, + maxChildSize: UIStyle.maxBottomDraggableSheetSize, + initialChildSize: minSize, + minChildSize: minSize, + expand: false, + builder: (context, controller) => SafeArea( + child: CustomScrollView( + controller: controller, + semanticChildCount: _maneuvers.length, + slivers: [ + SliverPersistentHeader( + delegate: _HeaderBuildDelegate( + route: widget.route, + controller: widget.wayPointsController, + extent: _kBottomSheetHeaderSize, + ), + pinned: true, + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final int itemIndex = index ~/ 2; + return index.isEven + ? _buildManeuverItem(context, itemIndex) + : Divider( + height: 1, + ); + }, + semanticIndexCallback: (Widget widget, int localIndex) { + if (localIndex.isEven) { + return localIndex ~/ 2; + } + return null; + }, + childCount: _maneuvers.length * 2 - 1, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} + +class _HeaderBuildDelegate extends SliverPersistentHeaderDelegate { + final Routing.Route route; + final WayPointsController controller; + final double extent; + + _HeaderBuildDelegate({ + @required this.route, + @required this.controller, + @required this.extent, + }); + + @override + Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => Material( + color: Theme.of(context).cardColor, + elevation: shrinkOffset == 0 ? 0 : 2, + child: Row( + children: [ + Padding( + padding: EdgeInsets.only( + left: UIStyle.contentMarginLarge, + right: UIStyle.contentMarginLarge, + ), + child: IconButton( + icon: Icon(Icons.arrow_back_ios), + onPressed: () => Navigator.of(context).maybePop(), + ), + ), + Expanded( + child: RouteInfo( + route: route, + onNavigation: () => Navigator.of(context).pushNamed( + NavigationScreen.navRoute, + arguments: [route, controller.value], + ), + ), + ), + ], + ), + ); + + @override + double get maxExtent => extent; + + @override + double get minExtent => extent; + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => true; +} diff --git a/lib/routing/route_info_widget.dart b/lib/routing/route_info_widget.dart new file mode 100644 index 0000000..c42006c --- /dev/null +++ b/lib/routing/route_info_widget.dart @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:here_sdk/routing.dart' as Routing; + +import '../common/ui_style.dart'; +import '../common/util.dart' as Util; + +/// A widget that displays the route length, duration (with traffic delays) and two buttons one for navigation and the +/// other for route details. +class RouteInfo extends StatelessWidget { + /// The route. + final Routing.Route route; + /// Called when the route details button is tapped or otherwise activated. + final VoidCallback onRouteDetails; + /// Called when the navigation button is tapped or otherwise activated. + final VoidCallback onNavigation; + + /// Constructs a widget. + RouteInfo({ + @required this.route, + this.onRouteDetails, + this.onNavigation, + }); + + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Padding( + padding: EdgeInsets.all(UIStyle.contentMarginMedium), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + text: _buildDurationString(context, route.durationInSeconds) + " ", + style: TextStyle( + fontSize: UIStyle.hugeFontSize, + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ), + children: [ + if (route.trafficDelayInSeconds > 0) + TextSpan( + text: Util.formatString(AppLocalizations.of(context).trafficDelayText, + [_buildDurationString(context, route.trafficDelayInSeconds)]), + style: TextStyle( + fontSize: UIStyle.mediumFontSize, + color: UIStyle.trafficWarningColor, + ), + ) + else + TextSpan( + text: AppLocalizations.of(context).noTrafficDelaysText, + style: TextStyle( + fontSize: UIStyle.smallFontSize, + color: colorScheme.onSecondary, + ), + ), + ], + ), + ), + Container( + height: UIStyle.contentMarginMedium, + ), + Text( + Util.makeDistanceString(context, route.lengthInMeters), + style: TextStyle( + color: colorScheme.onSecondary, + fontWeight: FontWeight.bold, + fontSize: UIStyle.hugeFontSize, + ), + ), + ], + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (onRouteDetails != null) + ClipOval( + child: Material( + child: Ink( + width: UIStyle.smallButtonHeight, + height: UIStyle.smallButtonHeight, + color: colorScheme.background, + child: InkWell( + child: Icon( + Icons.directions, + size: UIStyle.smallIconSize, + color: Theme.of(context).colorScheme.primary, + ), + onTap: onRouteDetails, + ), + ), + ), + ), + if (onRouteDetails != null && onNavigation != null) + Container( + width: UIStyle.contentMarginMedium, + ), + if (onNavigation != null) + ClipOval( + child: Material( + child: Ink( + width: UIStyle.smallButtonHeight, + height: UIStyle.smallButtonHeight, + color: colorScheme.background, + child: InkWell( + child: Icon( + Icons.navigation, + size: UIStyle.smallIconSize, + color: Theme.of(context).colorScheme.primary, + ), + onTap: onNavigation, + ), + ), + ), + ) + ], + ), + ], + ), + ); + } + + String _buildDurationString(BuildContext context, int durationInSeconds) { + if (durationInSeconds == null) { + return ""; + } + + int minutes = (durationInSeconds / 60).truncate(); + int hours = (minutes / 60).truncate(); + minutes = minutes % 60; + + if (hours == 0) { + return "$minutes ${AppLocalizations.of(context).minuteAbbreviationText}"; + } else { + String result = "$hours ${AppLocalizations.of(context).hourAbbreviationText}"; + if (minutes != 0) { + result += " $minutes ${AppLocalizations.of(context).minuteAbbreviationText}"; + } + return result; + } + } +} diff --git a/lib/routing/route_poi_handler.dart b/lib/routing/route_poi_handler.dart new file mode 100644 index 0000000..99dd5c7 --- /dev/null +++ b/lib/routing/route_poi_handler.dart @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/core.threading.dart'; +import 'package:here_sdk/mapview.dart'; +import 'package:here_sdk/routing.dart' as Routing; +import 'package:here_sdk/search.dart'; + +import '../common/ui_style.dart'; +import '../common/util.dart' as Util; +import 'poi_svg_helper.dart'; +import 'waypoints_controller.dart'; + +typedef GetTextForPoiMarkerCallback = String Function(Place); + +/// A class that searches for POI along a route, creates and keeps map markers for resulting POIs. +class RoutePoiHandler { + static const int _kGeoCorridorRadius = 20; + static const int _kMaxSearchSuggestion = 100; + + static final Map _categoryPoiTypes = { + PlaceCategory.eatAndDrink: PoiIconType.eatAndDrink, + PlaceCategory.businessAndServicesFuelingStation: PoiIconType.fueling, + PlaceCategory.businessAndServicesAtm: PoiIconType.atm, + }; + + final SearchOptions _searchOptions = new SearchOptions(LanguageCode.enUs, _kMaxSearchSuggestion); + + /// [HereMapController] of the map. + final HereMapController hereMapController; + /// Way points controller. + final WayPointsController wayPointsController; + /// Called to get a localized string describing a place. + final GetTextForPoiMarkerCallback onGetText; + + List _categories = []; + Map> _placesForRoutes = {}; + Set _releasedPlaces = {}; + Map _markers = {}; + SearchEngine _searchEngine = SearchEngine(); + TaskHandle _poiSearchTask; + + /// Constructs a [RoutePoiHandler] object. + RoutePoiHandler({ + @required this.hereMapController, + @required this.wayPointsController, + this.onGetText, + }); + + /// Releases resources. + void release() { + _stopCurrentSearch(); + _searchEngine.release(); + _clearMarkers(); + clearPlaces(); + } + + void _stopCurrentSearch() { + _poiSearchTask?.cancel(); + _poiSearchTask?.release(); + _poiSearchTask = null; + } + + void _clearMarkers() { + _markers.keys.forEach((marker) { + hereMapController.mapScene.removeMapMarker(marker); + marker.release(); + }); + _markers.clear(); + } + + /// Sets the desired categories of places to search. + set categories(List categories) { + if (ListEquality().equals(categories, _categories)) { + return; + } + + _categories = List.from(categories); + clearPlaces(); + } + + /// Returns true if the [mapMarker] is a POI marker. + bool isPoiMarker(MapMarker mapMarker) => _markers.containsKey(mapMarker); + + /// Returns [Place] of the [marker]. + Place getPlaceFromMarker(MapMarker marker) => _markers[marker]; + + /// Releases ownership of the [place]. + void releasePlace(Place place) => _releasedPlaces.add(place); + + /// Removes all found places. + void clearPlaces() { + _placesForRoutes.values.forEach((placesList) => placesList.forEach((place) { + if (!_releasedPlaces.contains(place)) { + place.release(); + } + })); + _releasedPlaces.clear(); + _placesForRoutes.clear(); + } + + /// Searches POI for the [route]. + void updatePoiForRoute(Routing.Route route) { + _clearMarkers(); + _stopCurrentSearch(); + + if (_placesForRoutes.containsKey(route)) { + _addMarkersForPlaces(_placesForRoutes[route]); + } else { + if (_categories.isEmpty) { + return; + } + + GeoCorridor geoCorridor = GeoCorridor.withRadius(route.polyline, _kGeoCorridorRadius); + + List categories = _categories.map((categoryId) => PlaceCategory(categoryId)).toList(); + CategoryQuery categoryQuery = CategoryQuery.withCorridorArea(categories, geoCorridor); + _poiSearchTask = _searchEngine.searchByCategory(categoryQuery, _searchOptions, (error, places) { + if (error != null) { + print('Search failed. Error: ${error.toString()}'); + return; + } + + _placesForRoutes[route] = places; + _addMarkersForPlaces(places); + }); + } + } + + List _filterPlaces(List places) { + final Set wayPointsPlacesIds = + wayPointsController.value.where((wp) => wp.place != null).map((wp) => wp.place.id).toSet(); + return places.whereNot((place) => wayPointsPlacesIds.contains(place.id)).toList(); + } + + void _addMarkersForPlaces(List places) { + int markerSize = (hereMapController.pixelScale * UIStyle.poiMarkerSize).round(); + + _filterPlaces(places).forEach((place) { + SvgInfo svgInfo = PoiSVGHelper.getPoiSvgForCategoryAndText( + type: _findPoiTypeForCategories(place.details.categories), + text: onGetText != null ? onGetText(place) : null, + ); + + MapImage mapImage = MapImage.withImageDataImageFormatWidthAndHeight(Uint8List.fromList(svgInfo.svg.codeUnits), + ImageFormat.svg, (svgInfo.width * (markerSize / svgInfo.height)).truncate(), markerSize); + + MapMarker mapMarker = Util.createMarkerWithImage( + place.geoCoordinates, + mapImage, + drawOrder: UIStyle.searchMarkerDrawOrder, + anchor: Anchor2D.withHorizontalAndVertical(0.5, 1), + ); + mapImage.release(); + + hereMapController.mapScene.addMapMarker(mapMarker); + _markers[mapMarker] = place; + }); + } + + PoiIconType _findPoiTypeForCategoryId(String categoryId) { + PoiIconType result = _categoryPoiTypes[categoryId]; + if (result != null) { + return result; + } + + int index = categoryId.lastIndexOf('-'); + if (index < 0) { + return null; + } + + return _findPoiTypeForCategoryId(categoryId.substring(0, index)); + } + + PoiIconType _findPoiTypeForCategories(List categories) { + for (int i = 0; i < categories.length; ++i) { + PoiIconType result = _findPoiTypeForCategoryId(categories[i].id); + if (result != null) { + return result; + } + } + + return PoiIconType.unknown; + } +} diff --git a/lib/routing/route_poi_options_button.dart b/lib/routing/route_poi_options_button.dart new file mode 100644 index 0000000..4446549 --- /dev/null +++ b/lib/routing/route_poi_options_button.dart @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:here_sdk/search.dart'; + +import '../common/ui_style.dart'; +import 'route_poi_options_item.dart'; + +class _PoiSettingInfo { + final String categoryId; + final String imageEnabled; + final String imageDisabled; + final IconData icon; + + _PoiSettingInfo({ + @required this.categoryId, + @required this.imageEnabled, + @required this.imageDisabled, + @required this.icon, + }); + + String getTitle(BuildContext context) { + AppLocalizations appLocalizations = AppLocalizations.of(context); + + if (categoryId == PlaceCategory.eatAndDrink) { + return appLocalizations.eatAndDrinkTitle; + } else if (categoryId == PlaceCategory.businessAndServicesFuelingStation) { + return appLocalizations.fuelingStationsTitle; + } else if (categoryId == PlaceCategory.businessAndServicesAtm) { + return appLocalizations.atmTitle; + } + + return null; + } +} + +final List<_PoiSettingInfo> _poiSettings = [ + _PoiSettingInfo( + categoryId: PlaceCategory.eatAndDrink, + imageEnabled: "assets/eat_and_drink_icon.svg", + imageDisabled: "assets/eat_and_drink_icon_disabled.svg", + icon: Icons.restaurant_rounded, + ), + _PoiSettingInfo( + categoryId: PlaceCategory.businessAndServicesFuelingStation, + imageEnabled: "assets/fueling_station_icon.svg", + imageDisabled: "assets/fueling_station_icon_disabled.svg", + icon: Icons.local_gas_station_rounded, + ), + _PoiSettingInfo( + categoryId: PlaceCategory.businessAndServicesAtm, + imageEnabled: "assets/atm_icon.svg", + imageDisabled: "assets/atm_icon_disabled.svg", + icon: Icons.local_atm, + ), +]; + +/// A widget that displays the POI categories enabled for search. +class RoutePoiOptionsButton extends StatelessWidget { + /// Set of categories currently enabled. + final Set categoryIds; + /// Called when the set of categories is changed. + final ValueChanged> onChanged; + + /// Constructs a widget. + RoutePoiOptionsButton({ + this.categoryIds = const {}, + @required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + child: Padding( + padding: EdgeInsets.only( + left: UIStyle.contentMarginMedium, + top: UIStyle.contentMarginMedium, + bottom: UIStyle.contentMarginMedium, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: _poiSettings + .map((poiInfo) => Padding( + padding: EdgeInsets.only( + right: UIStyle.contentMarginMedium, + ), + child: SvgPicture.asset( + categoryIds.contains(poiInfo.categoryId) ? poiInfo.imageEnabled : poiInfo.imageDisabled), + )) + .toList(), + ), + ), + onTap: () => _showPoiEnableMenu(context), + ); + } + + void _showPoiEnableMenu(BuildContext context) async { + Set categories = Set.from(categoryIds); + ColorScheme colorScheme = Theme.of(context).colorScheme; + + await showModalBottomSheet( + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UIStyle.popupsBorderRadius), + ), + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AppBar( + leading: null, + automaticallyImplyLeading: false, + primary: false, + centerTitle: false, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(UIStyle.popupsBorderRadius), + topRight: Radius.circular(UIStyle.popupsBorderRadius), + ), + ), + backgroundColor: colorScheme.background, + title: Text( + AppLocalizations.of(context).poiSettingsTitle, + style: TextStyle( + color: colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + actions: [ + IconButton( + icon: Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ..._poiSettings + .map((poiInfo) => RoutePoiOptionsItem( + value: categories.contains(poiInfo.categoryId), + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + poiInfo.icon, + color: colorScheme.primary, + ), + Container( + width: UIStyle.contentMarginLarge, + ), + Text(poiInfo.getTitle(context)), + ], + ), + onChanged: (value) => + value ? categories.add(poiInfo.categoryId) : categories.remove(poiInfo.categoryId), + )) + .toList() + ], + ), + ), + ); + + if (!SetEquality().equals(categories, categoryIds)) { + onChanged(categories); + } + } +} diff --git a/lib/routing/route_poi_options_item.dart b/lib/routing/route_poi_options_item.dart new file mode 100644 index 0000000..7daaf73 --- /dev/null +++ b/lib/routing/route_poi_options_item.dart @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; + +/// A widget that displays a toggle button. +class RoutePoiOptionsItem extends StatefulWidget { + /// Checkbox value. + final bool value; + /// Title of the button. + final Widget title; + /// Called when the value is changed. + final ValueChanged onChanged; + + /// Constructs a widget. + RoutePoiOptionsItem({ + this.value = false, + @required this.title, + @required this.onChanged, + }); + + @override + _RoutePoiOptionsItemState createState() => _RoutePoiOptionsItemState(); +} + +class _RoutePoiOptionsItemState extends State { + bool _value; + + @override + void initState() { + _value = widget.value; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return SwitchListTile( + value: _value, + title: widget.title, + onChanged: (value) { + setState(() => _value = value); + widget.onChanged(value); + }, + ); + } +} diff --git a/lib/routing/route_waypoints_list.dart b/lib/routing/route_waypoints_list.dart new file mode 100644 index 0000000..5c4c97f --- /dev/null +++ b/lib/routing/route_waypoints_list.dart @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:reorderables/reorderables.dart'; + +import '../common/ui_style.dart'; +import 'waypoint_info.dart'; + +/// Widget for displaying and editing the list of waypoints of the route. +class RouteWayPointsList extends StatefulWidget { + /// A waypoints list. + final List wayPoints; + /// Parent scroll controller. + final ScrollController controller; + /// Called when the list of waypoints is changed. + final ValueChanged> onChanged; + /// Title of the current location. + final String currentLocationTitle; + + /// Creates a widget. + RouteWayPointsList({ + Key key, + @required this.wayPoints, + this.controller, + @required this.onChanged, + @required this.currentLocationTitle, + }) : super(key: key); + + @override + _RouteWayPointsListState createState() => _RouteWayPointsListState(); +} + +class _RouteWayPointsListState extends State { + List _wayPoints; + + @override + void initState() { + super.initState(); + _wayPoints = widget.wayPoints.toList(); + } + + @override + Widget build(BuildContext context) { + return CustomScrollView( + controller: widget.controller, + slivers: [ + SliverAppBar( + leading: Container(), + shape: UIStyle.topRoundedBorder(), + leadingWidth: 0, + backgroundColor: Theme.of(context).colorScheme.background, + pinned: true, + titleSpacing: 0, + title: _buildHeader(context), + ), + ReorderableSliverList( + delegate: ReorderableSliverChildBuilderDelegate( + (context, index) { + if (index.isOdd) { + return Divider( + height: 1, + ); + } + + return _buildItem(context, index ~/ 2); + }, + semanticIndexCallback: (Widget widget, int localIndex) { + if (localIndex.isEven) { + return localIndex ~/ 2; + } + return null; + }, + childCount: _wayPoints.length * 2 - 1, + ), + onReorder: (oldIndex, newIndex) { + setState(() => _wayPoints.insert(newIndex ~/ 2, _wayPoints.removeAt(oldIndex ~/ 2))); + widget.onChanged(_wayPoints); + }, + ), + ], + ); + } + + Widget _buildHeader(BuildContext context) => Row( + children: [ + IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).pop(), + ), + Expanded( + child: Text( + AppLocalizations.of(context).wayPointsListTitle, + style: TextStyle( + fontSize: UIStyle.hugeFontSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], + ); + + Widget _buildItem(BuildContext context, int index) { + WayPointInfo wp = _wayPoints[index]; + ColorScheme colorScheme = Theme.of(context).colorScheme; + bool isCurrent = wp.sourceType == WayPointInfoSourceType.CurrentPosition; + + Widget itemTile = ListTile( + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.drag_handle, + color: colorScheme.primary, + size: UIStyle.mediumIconSize, + ), + Container( + width: UIStyle.contentMarginLarge, + ), + Icon( + isCurrent ? Icons.gps_fixed : Icons.location_on_rounded, + color: colorScheme.primary, + size: UIStyle.mediumIconSize, + ), + ], + ), + title: Text( + isCurrent ? widget.currentLocationTitle : wp.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: isCurrent ? colorScheme.secondary : colorScheme.primary, + ), + ), + ); + + if (_wayPoints.length <= 2) { + return itemTile; + } + + List dismissBackgroundItems = [ + Container( + width: UIStyle.contentMarginLarge, + ), + Icon( + Icons.delete, + color: UIStyle.removeWayPointIconColor, + ), + Spacer(), + ]; + + return Dismissible( + key: ObjectKey(wp), + background: Container( + color: UIStyle.removeWayPointBackgroundColor, + child: Row( + children: dismissBackgroundItems, + ), + ), + secondaryBackground: Container( + color: UIStyle.removeWayPointBackgroundColor, + child: Row( + children: dismissBackgroundItems.reversed.toList(), + ), + ), + onDismissed: (direction) async { + setState(() => _wayPoints.removeAt(index)); + widget.onChanged(_wayPoints); + }, + child: itemTile, + ); + } +} diff --git a/lib/routing/route_waypoints_widget.dart b/lib/routing/route_waypoints_widget.dart new file mode 100644 index 0000000..28cbfda --- /dev/null +++ b/lib/routing/route_waypoints_widget.dart @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:RefApp/common/draggable_popup_here_logo_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/mapview.dart'; + +import '../search/search_popup.dart'; +import '../common/ui_style.dart'; +import '../common/util.dart' as Util; +import 'route_waypoints_list.dart'; +import 'waypoint_info.dart'; +import 'waypoints_controller.dart'; + +typedef QueryCurrentLocationCallback = GeoCoordinates Function(); + +/// A widget that displays the route start and destination waypoints. +class RouteWayPoints extends StatefulWidget { + /// Waypoints controller. + final WayPointsController controller; + /// Map controller. + final HereMapController hereMapController; + /// Key of the current map. + final GlobalKey hereMapKey; + /// Title of the current location. + final String currentLocationTitle; + + /// Creates a widget. + RouteWayPoints({ + Key key, + @required this.controller, + @required this.hereMapController, + @required this.hereMapKey, + this.currentLocationTitle, + }) : super(key: key); + + @override + _RouteWayPointsState createState() => _RouteWayPointsState(); +} + +class _RouteWayPointsState extends State { + @override + void initState() { + super.initState(); + widget.controller.addListener(() => setState(() {})); + } + + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + bool displayCurrentLocationButton = widget.controller.value.fold(true, + (previousValue, element) => previousValue && element.sourceType != WayPointInfoSourceType.CurrentPosition); + + return Row( + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildWayPointItem(context, 0, displayCurrentLocationButton), + Divider( + height: 1, + ), + _buildWayPointItem(context, widget.controller.length - 1, displayCurrentLocationButton), + ], + ), + ), + Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.circular(100), + ), + child: widget.controller.length > 2 + ? IconButton( + padding: EdgeInsets.all(UIStyle.contentMarginMedium), + icon: Icon( + Icons.list, + color: colorScheme.primary, + ), + onPressed: () => _showWayPointsEditPopup(context), + ) + : IconButton( + padding: EdgeInsets.all(UIStyle.contentMarginMedium), + icon: Icon( + Icons.swap_vert, + color: colorScheme.primary, + ), + onPressed: () => setState( + () => widget.controller.value = widget.controller.value.swap(0, widget.controller.length - 1)), + ), + ), + ], + ); + } + + Widget _buildWayPointItem(BuildContext context, int index, bool displayCurrentLocationButton) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + final WayPointInfo wayPoint = widget.controller[index]; + bool isCurrent = wayPoint.sourceType == WayPointInfoSourceType.CurrentPosition; + + return ListTile( + dense: true, + leading: Icon( + isCurrent ? Icons.gps_fixed : Icons.location_on_rounded, + color: colorScheme.primary, + size: UIStyle.mediumIconSize, + ), + title: Text( + isCurrent ? widget.currentLocationTitle : wayPoint.title, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + color: isCurrent ? colorScheme.secondary : colorScheme.primary, + ), + ), + onTap: () async { + GeoCoordinates currentPosition = widget.controller.currentLocation; + final result = await showSearchPopup( + context: context, + currentPosition: currentPosition, + hereMapController: widget.hereMapController, + hereMapKey: widget.hereMapKey, + currentLocationTitle: displayCurrentLocationButton || isCurrent ? widget.currentLocationTitle : null, + ); + if (result != null) { + setState(() { + SearchResult searchResult = result; + if (searchResult.place != null) { + widget.controller[index] = WayPointInfo.withPlace( + place: searchResult.place, + ); + } else { + widget.controller[index] = WayPointInfo( + coordinates: currentPosition, + ); + } + }); + } + }, + ); + } + + void _showWayPointsEditPopup(BuildContext context) async { + List wayPoints = widget.controller.value; + + await showModalBottomSheet( + context: context, + shape: UIStyle.topRoundedBorder(), + isScrollControlled: true, + builder: (context) => DraggablePopupHereLogoHelper( + hereMapController: widget.hereMapController, + hereMapKey: widget.hereMapKey, + modal: true, + draggableScrollableSheet: DraggableScrollableSheet( + maxChildSize: UIStyle.maxBottomDraggableSheetSize, + initialChildSize: 0.5, + minChildSize: 0.5, + expand: false, + builder: (context, controller) => RouteWayPointsList( + wayPoints: wayPoints, + controller: controller, + onChanged: (value) => wayPoints = value, + currentLocationTitle: widget.currentLocationTitle, + ), + ), + ), + ); + + widget.hereMapController.setWatermarkPosition(WatermarkPlacement.bottomLeft, 0); + widget.controller.value = wayPoints; + } +} diff --git a/lib/routing/routing_screen.dart b/lib/routing/routing_screen.dart new file mode 100644 index 0000000..10722fc --- /dev/null +++ b/lib/routing/routing_screen.dart @@ -0,0 +1,636 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/gestures.dart'; +import 'package:here_sdk/mapview.dart'; +import 'package:here_sdk/routing.dart' as Routing; +import 'package:here_sdk/search.dart'; +import 'package:provider/provider.dart'; + +import '../common/reset_location_button.dart'; +import '../navigation/navigation_screen.dart'; +import '../positioning/positioning.dart'; +import 'route_details_screen.dart'; +import '../route_preferences/route_preferences_screen.dart'; +import '../route_preferences/route_preferences_model.dart'; +import '../route_preferences/transport_modes_widget.dart'; +import '../search/place_details_popup.dart'; +import '../common/ui_style.dart'; +import '../common/util.dart' as Util; +import 'route_info_widget.dart'; +import 'route_poi_handler.dart'; +import 'route_poi_options_button.dart'; +import 'route_waypoints_widget.dart'; +import '../common/place_actions_popup.dart'; +import 'waypoint_info.dart'; +import 'waypoints_controller.dart'; + +/// Routing mode screen widget. +class RoutingScreen extends StatefulWidget { + static const String navRoute = "/routing"; + + /// Coordinates of the current position. + final GeoCoordinates currentPosition; + + /// Departure point. + final WayPointInfo departure; + + /// Destination point. + final WayPointInfo destination; + + /// Creates a widget. + RoutingScreen({ + Key key, + @required this.currentPosition, + @required this.departure, + @required this.destination, + }) : super(key: key); + + @override + _RoutingScreenState createState() => _RoutingScreenState(); +} + +class _RoutingScreenState extends State with TickerProviderStateMixin, Positioning { + static const double _kTapRadius = 30; // pixels + static const double _kRouteCardHeight = 85; + + final GlobalKey _bottomBarKey = GlobalKey(); + final GlobalKey _scaffoldKey = new GlobalKey(); + final GlobalKey _hereMapKey = GlobalKey(); + + HereMapController _hereMapController; + List _routes = []; + List _mapRoutes = []; + + Set _poiCategories = {}; + RoutePoiHandler _routePoiHandler; + + Routing.RoutingEngine _routingEngine = Routing.RoutingEngine(); + + TabController _routesTabController; + GlobalKey _tabBarViewKey = GlobalKey(); + int _selectedRouteIndex = 0; + bool _routingInProgress = false; + + TabController _transportModesTabController; + + WayPointsController _wayPointsController; + + bool _enableTraffic = false; + + @override + void initState() { + super.initState(); + + _routesTabController = TabController( + length: _routes.length, + vsync: this, + ); + _transportModesTabController = TabController( + length: TransportModes.values.length, + vsync: this, + ); + _transportModesTabController.addListener(() { + if (!_transportModesTabController.indexIsChanging) { + _beginRouting(); + } + }); + enableMapUpdate = false; + + _wayPointsController = WayPointsController( + wayPoints: [ + widget.departure, + widget.destination, + ], + currentLocation: widget.currentPosition, + ); + _wayPointsController.addListener(() => _beginRouting()); + } + + @override + void dispose() { + _clearMapRoutes(); + _clearRoutes(); + _dismissWayPointPopup(); + _wayPointsController.dispose(); + _routingEngine.release(); + _routePoiHandler?.release(); + _hereMapController?.release(); + releaseLocationEngine(); + _transportModesTabController.dispose(); + _routesTabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Stack( + children: [ + Scaffold( + key: _scaffoldKey, + body: Stack( + children: [ + HereMap( + key: _hereMapKey, + onMapCreated: _onMapCreated, + ), + _buildTrafficButton(context), + ], + ), + extendBodyBehindAppBar: true, + bottomNavigationBar: _buildBottomNavigationBar(context), + floatingActionButton: enableMapUpdate + ? null + : ResetLocationButton( + onPressed: _resetCurrentPosition, + ), + ), + if (_routingInProgress) + Container( + color: Colors.white54, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ], + ); + + void _onMapCreated(HereMapController hereMapController) { + _clearMapRoutes(); + _wayPointsController.mapController = null; + _hereMapController?.release(); + _routePoiHandler?.release(); + + _routePoiHandler = null; + _hereMapController = hereMapController; + + hereMapController.mapScene.loadSceneFromConfigurationFile('preview.normal.day.json', (MapError error) { + if (error != null) { + print('Map scene not loaded. MapError: ${error.toString()}'); + return; + } + + hereMapController.setWatermarkPosition(WatermarkPlacement.bottomLeft, 0); + _hereMapController.camera.lookAtPointWithOrientationAndDistance( + widget.currentPosition, MapCameraOrientationUpdate.withDefaults(), Positioning.initDistanceToEarth); + _routePoiHandler = RoutePoiHandler( + hereMapController: hereMapController, + wayPointsController: _wayPointsController, + onGetText: (place) => Util.makeDistanceString(context, place.distanceInMeters), + ); + + initLocationEngine( + hereMapController: hereMapController, + onLocationUpdated: (location) => _wayPointsController.currentLocation = + location?.coordinates ?? _hereMapController.camera.state.targetCoordinates, + ); + + _addGestureListeners(); + _wayPointsController.mapController = hereMapController; + _beginRouting(); + }); + } + + void _addGestureListeners() { + _hereMapController.gestures.panListener = PanListener.fromLambdas( + lambda_onPan: (state, origin, translation, velocity) { + if (enableMapUpdate) { + setState(() => enableMapUpdate = false); + } + }, + ); + + _hereMapController.gestures.tapListener = TapListener.fromLambdas( + lambda_onTap: (Point2D touchPoint) { + _dismissWayPointPopup(); + _pickMapItem(touchPoint); + }, + ); + + _hereMapController.gestures.longPressListener = LongPressListener.fromLambdas( + lambda_onLongPress: (state, point) { + if (state == GestureState.begin) { + _showWayPointPopup(point); + } + }, + ); + } + + void _resetCurrentPosition() { + GeoCoordinates coordinates = lastKnownLocation != null ? lastKnownLocation.coordinates : widget.currentPosition; + + _hereMapController.camera.lookAtPointWithOrientationAndDistance( + coordinates, MapCameraOrientationUpdate.withDefaults(), Positioning.initDistanceToEarth); + + setState(() => enableMapUpdate = true); + } + + void _pickMapItem(Point2D touchPoint) { + _hereMapController.pickMapItems(touchPoint, _kTapRadius, (pickMapItemsResult) async { + List mapMarkersList = pickMapItemsResult.markers; + if (mapMarkersList.length != 0 && _routePoiHandler.isPoiMarker(mapMarkersList.first)) { + Place place = _routePoiHandler.getPlaceFromMarker(mapMarkersList.first); + + PlaceDetailsPopupResult result = await showPlaceDetailsPopup( + context: context, + place: place, + routeToEnabled: true, + addToRouteEnabled: true, + ); + + if (result == null) { + return; + } + + _routePoiHandler.releasePlace(place); + WayPointInfo wp = WayPointInfo.withPlace( + place: place, + ); + + switch (result) { + case PlaceDetailsPopupResult.routeTo: + _wayPointsController.value = [ + WayPointInfo(coordinates: lastKnownLocation?.coordinates ?? widget.currentPosition), + wp, + ]; + break; + case PlaceDetailsPopupResult.addToRoute: + _wayPointsController.insert(_appropriateIndexToInsertWaypoint(wp), wp); + break; + } + + return; + } + + List mapPolyLinesList = pickMapItemsResult.polylines; + if (mapPolyLinesList.length == 0) { + print("No map poly lines found."); + return; + } + + _routesTabController.animateTo(_mapRoutes.indexOf(mapPolyLinesList.first)); + }); + } + + int _appropriateIndexToInsertWaypoint(WayPointInfo wayPointInfo) { + final List waypoints = _wayPointsController.value; + final GeoPolyline routeLine = GeoPolyline(_routes[_selectedRouteIndex].polyline); + final int indexOnRoute = routeLine.getNearestIndexTo(wayPointInfo.coordinates); + + for (int i = 1; i < waypoints.length - 1; ++i) { + if (routeLine.getNearestIndexTo(waypoints[i].coordinates) > indexOnRoute) { + return i; + } + } + + return waypoints.length - 1; + } + + void _dismissWayPointPopup() { + if (_hereMapController.widgetPins.isNotEmpty) { + _hereMapController.widgetPins.first.unpin(); + } + } + + void _showWayPointPopup(Point2D point) { + _dismissWayPointPopup(); + GeoCoordinates coordinates = _hereMapController.viewToGeoCoordinates(point); + + _hereMapController.pinWidget( + PlaceActionsPopup( + coordinates: coordinates, + hereMapController: _hereMapController, + onRightButtonPressed: (place) { + _dismissWayPointPopup(); + _wayPointsController.add(place != null + ? WayPointInfo.withPlace( + place: place, + originalCoordinates: coordinates, + ) + : WayPointInfo.withCoordinates( + coordinates: coordinates, + )); + }, + ), + coordinates, + anchor: Anchor2D.withHorizontalAndVertical(0.5, 1), + ); + } + + _clearRoutes() { + _routes.forEach((route) { + route.sections.forEach((section) { + section.maneuvers.forEach((maneuver) => maneuver.release()); + section.release(); + }); + route.release(); + }); + } + + _clearMapRoutes() { + _mapRoutes.forEach((route) { + _hereMapController.mapScene.removeMapPolyline(route); + route.release(); + }); + _mapRoutes.clear(); + } + + _addRoutesToMap() { + _clearMapRoutes(); + + for (int i = 0; i < _routes.length; ++i) { + _addRouteToMap(_routes[i], i == _selectedRouteIndex); + } + } + + _zoomToRoutes() { + List bounds = []; + + for (int i = 0; i < _routes.length; ++i) { + GeoBox geoBox = _routes[i].boundingBox; + bounds.add(geoBox.northEastCorner); + bounds.add(geoBox.southWestCorner); + } + + if (_bottomBarKey.currentContext != null) { + final RenderBox bottomBarBox = _bottomBarKey.currentContext.findRenderObject() as RenderBox; + final GeoBox geoBox = GeoBox.containingGeoCoordinates(bounds); + + _hereMapController.zoomGeoBoxToLogicalViewPort( + geoBox: geoBox, + viewPort: Rect.fromLTRB( + 0, + MediaQuery.of(context).padding.top, + bottomBarBox.size.width, + MediaQuery.of(context).size.height - bottomBarBox.size.height, + ).deflate(UIStyle.locationMarkerSize.toDouble()), + ); + } + + setState(() => enableMapUpdate = false); + } + + _addRouteToMap(Routing.Route route, bool selected) { + GeoPolyline routeGeoPolyline = GeoPolyline(route.polyline); + MapPolyline routeMapPolyline = MapPolyline( + routeGeoPolyline, UIStyle.routeLineWidth, selected ? UIStyle.selectedRouteColor : UIStyle.routeColor); + routeMapPolyline.drawOrder = selected ? 1 : 0; + routeMapPolyline.outlineColor = selected ? UIStyle.selectedRouteBorderColor : UIStyle.routeBorderColor; + routeMapPolyline.outlineWidth = UIStyle.routeOutLineWidth; + + _hereMapController.mapScene.addMapPolyline(routeMapPolyline); + _mapRoutes.add(routeMapPolyline); + } + + _updateSelectedRoute() { + _mapRoutes[_selectedRouteIndex].lineColor = UIStyle.routeColor; + _mapRoutes[_selectedRouteIndex].outlineColor = UIStyle.routeBorderColor; + _mapRoutes[_selectedRouteIndex].drawOrder = 0; + + _mapRoutes[_routesTabController.index].lineColor = UIStyle.selectedRouteColor; + _mapRoutes[_routesTabController.index].outlineColor = UIStyle.selectedRouteBorderColor; + _mapRoutes[_routesTabController.index].drawOrder = 1; + + _selectedRouteIndex = _routesTabController.index; + + _zoomToRoutes(); + _routePoiHandler.updatePoiForRoute(_routes[_selectedRouteIndex]); + } + + Widget _buildTrafficButton(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Align( + alignment: Alignment.topRight, + child: SafeArea( + child: Padding( + padding: EdgeInsets.all(UIStyle.contentMarginLarge), + child: Material( + color: colorScheme.background, + borderRadius: BorderRadius.circular(UIStyle.popupsBorderRadius), + elevation: 2, + child: InkWell( + child: Padding( + padding: EdgeInsets.all(UIStyle.contentMarginMedium), + child: SvgPicture.asset( + _enableTraffic ? "assets/traffic_off.svg" : "assets/traffic_on.svg", + color: colorScheme.primary, + width: UIStyle.bigIconSize, + height: UIStyle.bigIconSize, + ), + ), + onTap: () => setState(() { + _enableTraffic = !_enableTraffic; + MapSceneLayerState newState = _enableTraffic ? MapSceneLayerState.visible : MapSceneLayerState.hidden; + _hereMapController.mapScene.setLayerState(MapSceneLayers.trafficFlow, newState); + _hereMapController.mapScene.setLayerState(MapSceneLayers.trafficIncidents, newState); + }), + ), + ), + ), + ), + ); + } + + Widget _buildTransportTypeWidget(BuildContext context) { + return Container( + color: UIStyle.selectedListTileColor, child: TransportModesWidget(tabController: _transportModesTabController)); + } + + Widget _buildBottomNavigationBar(context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + AppLocalizations appLocalization = AppLocalizations.of(context); + + return BottomAppBar( + key: _bottomBarKey, + color: colorScheme.background, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: RouteWayPoints( + controller: _wayPointsController, + hereMapController: _hereMapController, + hereMapKey: _hereMapKey, + currentLocationTitle: lastKnownLocation != null + ? appLocalization.yourLocationTitle + : appLocalization.defaultLocationTitle, + ), + ), + IconButton( + icon: Icon(Icons.close), + color: colorScheme.primary, + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + _buildTransportTypeWidget(context), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + RoutePoiOptionsButton( + categoryIds: _poiCategories, + onChanged: (categoryIds) { + setState(() => _poiCategories = categoryIds); + _routePoiHandler.categories = categoryIds.toList(); + if (_routes.isNotEmpty) { + _routePoiHandler.updatePoiForRoute(_routes[_selectedRouteIndex]); + } + }, + ), + Spacer(), + TextButton( + child: Text( + AppLocalizations.of(context).preferencesTitle, + style: TextStyle(color: colorScheme.secondary), + ), + onPressed: () => _awaitOptionsFromPreferenceScreen(context), + ), + ], + ), + if (_routesTabController.length > 0) + Container( + width: double.infinity, + height: _kRouteCardHeight, + child: TabBarView( + key: _tabBarViewKey, + controller: _routesTabController, + children: _routes + .map( + (route) => Card( + elevation: 2, + child: RouteInfo( + route: route, + onRouteDetails: () => Navigator.of(context).pushNamed( + RouteDetailsScreen.navRoute, + arguments: [_routes[_routesTabController.index], _wayPointsController], + ), + onNavigation: () => Navigator.of(context).pushNamed( + NavigationScreen.navRoute, + arguments: [route, _wayPointsController.value], + ), + ), + ), + ) + .toList(), + ), + ), + ], + ), + ); + } + + _beginRouting() { + _dismissWayPointPopup(); + setState(() => _routingInProgress = true); + RoutePreferencesModel preferences = Provider.of(context, listen: false); + + switch (_transportModesTabController.index) { + case 0: // car + _routingEngine.calculateCarRoute(_wayPointsController.value, preferences.carOptions, _onRoutingEnd); + break; + case 1: // truck + _routingEngine.calculateTruckRoute(_wayPointsController.value, preferences.truckOptions, _onRoutingEnd); + break; + case 2: // scooter + _routingEngine.calculateScooterRoute(_wayPointsController.value, preferences.scooterOptions, _onRoutingEnd); + break; + case 3: // pedestrian + _routingEngine.calculatePedestrianRoute( + _wayPointsController.value, preferences.pedestrianOptions, _onRoutingEnd); + break; + } + } + + _onRoutingEnd(Routing.RoutingError error, List routes) { + if (error != null) { + setState(() => _routingInProgress = false); + + _showError(error); + return; + } + + _clearRoutes(); + _routePoiHandler.clearPlaces(); + _selectedRouteIndex = 0; + _routesTabController.dispose(); + + _routes = routes; + _tabBarViewKey = GlobalKey(); + _routesTabController = TabController( + length: _routes.length, + vsync: this, + ); + _routesTabController.addListener(() => _updateSelectedRoute()); + + _addRoutesToMap(); + + setState(() => _routingInProgress = false); + _routePoiHandler.updatePoiForRoute(_routes[_selectedRouteIndex]); + + SchedulerBinding.instance.scheduleFrameCallback( + (timeStamp) => SchedulerBinding.instance.addPostFrameCallback((timeStamp) => _zoomToRoutes())); + } + + void _showError(Routing.RoutingError error) { + ScaffoldMessenger.of(_scaffoldKey.currentContext).showSnackBar(SnackBar( + backgroundColor: Colors.red, + content: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: EdgeInsets.all(UIStyle.contentMarginMedium), + child: Icon(Icons.error), + ), + Expanded( + child: Padding( + padding: EdgeInsets.all(UIStyle.contentMarginMedium), + child: Text( + Util.formatString(AppLocalizations.of(context).routingErrorText, [error.toString()]), + style: TextStyle(fontSize: UIStyle.hugeFontSize), + maxLines: 2, + ), + ), + ), + ], + ), + )); + } + + void _awaitOptionsFromPreferenceScreen(BuildContext context) async { + final activeTransportTab = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RoutePreferencesScreen( + activeTransportTab: _transportModesTabController.index, + ), + )); + + setState(() => _transportModesTabController.index = activeTransportTab); + _beginRouting(); + } +} diff --git a/lib/routing/waypoint_info.dart b/lib/routing/waypoint_info.dart new file mode 100644 index 0000000..548f44d --- /dev/null +++ b/lib/routing/waypoint_info.dart @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/routing.dart' as Routing; +import 'package:here_sdk/search.dart'; + +import '../common/util.dart' as Util; + +/// Source type for a waypoint. +enum WayPointInfoSourceType { + /// Current position. + CurrentPosition, + /// Arbitrary coordinates. + Coordinates, + /// A [Place]. + Place, +} + +/// Helper class that contains additional information about waypoint. +class WayPointInfo extends Routing.Waypoint { + /// Place of the waypoint. + final Place place; + /// Source type. + final WayPointInfoSourceType sourceType; + /// Ownership of the provided [Place]. + final bool releasePlace; + + String get title => place?.title ?? coordinates.toPrettyString(); + + WayPointInfo({ + @required GeoCoordinates coordinates, + }) : place = null, + releasePlace = false, + sourceType = WayPointInfoSourceType.CurrentPosition, + super.withDefaults(coordinates); + + WayPointInfo.withCoordinates({ + @required GeoCoordinates coordinates, + }) : place = null, + releasePlace = false, + sourceType = WayPointInfoSourceType.Coordinates, + super.withDefaults(coordinates); + + WayPointInfo.withPlace({ + @required this.place, + this.releasePlace = true, + GeoCoordinates originalCoordinates = null, + }) : sourceType = WayPointInfoSourceType.Place, + super.withDefaults(originalCoordinates ?? place.geoCoordinates); + + void release() { + if (place != null && releasePlace) { + place.release(); + } + } +} diff --git a/lib/routing/waypoints_controller.dart b/lib/routing/waypoints_controller.dart new file mode 100644 index 0000000..7e0e938 --- /dev/null +++ b/lib/routing/waypoints_controller.dart @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/mapview.dart'; + +import '../common/ui_style.dart'; +import 'waypoint_info.dart'; +import '../common/util.dart' as Util; + +/// Helper class that manages routing waypoints. It uses [HereMapController] to display waypoint markers. +class WayPointsController extends ValueNotifier> { + /// Current location. + GeoCoordinates currentLocation; + HereMapController _hereMapController; + List _wpMarkers = []; + + /// Creates a [WayPointsController] object. + WayPointsController({ + @required List wayPoints, + @required this.currentLocation, + }) : super(wayPoints) { + addListener(() { + _clearWpMarkers(); + _createWpMarkers(); + }); + } + + /// Sets current [HereMapController]. + set mapController(HereMapController hereMapController) { + _clearWpMarkers(); + _hereMapController = hereMapController; + _createWpMarkers(); + } + + /// Returns waypoints list. + @override + List get value => super.value.toList(); + + /// Sets waypoints list. + @override + set value(List value) { + assert(value != null); + if (ListEquality().equals(super.value, value)) { + return; + } + + super.value.forEach((wp) { + if (!value.contains(wp)) { + wp.release(); + } + }); + super.value = value; + } + + @override + void dispose() { + super.value.forEach((wp) => wp.release()); + super.dispose(); + _clearWpMarkers(); + } + + /// Gets a waypoint by index [i]. + operator [](int i) => super.value[i]; + + /// Sets a waypoint by index [i]. + operator []=(int i, WayPointInfo wp) { + if (super.value[i] != wp) { + super.value[i].release(); + } + super.value[i] = wp; + notifyListeners(); + } + + /// Returns length of the waypoints list. + int get length => super.value.length; + + /// Adds waypoint to the list. + void add(WayPointInfo wp) { + super.value.add(wp); + notifyListeners(); + } + + /// Adds waypoint [wp] to the list at [index]. + void insert(int index, WayPointInfo wp) { + super.value.insert(index, wp); + notifyListeners(); + } + + /// Removes waypoint from the list at [index]. + void removeAt(int index) { + super.value.removeAt(index); + notifyListeners(); + } + + void _clearWpMarkers() { + _wpMarkers.forEach((marker) { + _hereMapController?.mapScene?.removeMapMarker(marker); + marker.release(); + }); + _wpMarkers.clear(); + } + + void _createWpMarkers() { + _wpMarkers = buildMapMarkersForController(_hereMapController); + } + + /// Creates [MapMarker] for each waypoint in the list an adds it to the [controller]. + /// Returns a list of created markers. + List buildMapMarkersForController(HereMapController controller) { + if (super.value == null || controller == null) { + return []; + } + + List markers = []; + + int locationMarkerSize = (controller.pixelScale * UIStyle.locationMarkerSize).truncate(); + int placeBigMarkerSize = (controller.pixelScale * UIStyle.searchMarkerSize).truncate() * 2; + + MapMarker marker = Util.createMarkerWithImagePath( + super.value[0].coordinates, + "assets/depart_marker.svg", + locationMarkerSize, + locationMarkerSize, + drawOrder: UIStyle.waypointsMarkerDrawOrder, + ); + controller.mapScene.addMapMarker(marker); + markers.add(marker); + + for (int i = 1; i < super.value.length; ++i) { + marker = Util.createMarkerWithImagePath( + super.value[i].coordinates, + "assets/map_marker_big.svg", + placeBigMarkerSize, + placeBigMarkerSize, + drawOrder: UIStyle.waypointsMarkerDrawOrder, + anchor: Anchor2D.withHorizontalAndVertical(0.5, 1), + ); + + controller.mapScene.addMapMarker(marker); + markers.add(marker); + } + + return markers; + } +} diff --git a/lib/search/place_details_popup.dart b/lib/search/place_details_popup.dart new file mode 100644 index 0000000..1efad1c --- /dev/null +++ b/lib/search/place_details_popup.dart @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/core.threading.dart'; +import 'package:here_sdk/search.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../common/ui_style.dart'; + +enum PlaceDetailsPopupResult { + routeTo, + addToRoute, +} + +/// Displays a pop-up window with detailed info of the [place]. +Future showPlaceDetailsPopup({ + @required BuildContext context, + @required Place place, + bool routeToEnabled = false, + bool addToRouteEnabled = false, +}) async { + Future placeDetailsFuture = _getPlaceDetails(place); + + PlaceDetailsPopupResult result = await showDialog( + context: context, + builder: (context) => FutureBuilder( + initialData: place, + future: placeDetailsFuture, + builder: (context, snapshot) => _createPopupFromPlace(context, snapshot.data, routeToEnabled, addToRouteEnabled), + ), + ); + + Place placeDetails = await placeDetailsFuture; + if (placeDetails != place) { + placeDetails.release(); + } + + return result; +} + +Future _getPlaceDetails(Place place) async { + final SearchEngine _searchEngine = SearchEngine(); + final Completer completer = Completer(); + + TaskHandle taskHandle = + _searchEngine.searchByPlaceIdWithLanguageCode(PlaceIdQuery(place.id), LanguageCode.enUs, (error, place) { + if (error != null) { + print('Search failed. Error: ${error.toString()}'); + completer.complete(); + } + + completer.complete(place); + }); + + Place newPlace = await completer.future; + taskHandle.release(); + _searchEngine.release(); + + if (newPlace == null) { + newPlace = place; + } + + return newPlace; +} + +Widget _createPopupFromPlace(BuildContext context, Place place, bool routeToEnabled, bool addToRouteEnabled) { + return SimpleDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(UIStyle.popupsBorderRadius)), + ), + titlePadding: EdgeInsets.zero, + title: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.all(UIStyle.contentMarginLarge), + child: Text( + place.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: UIStyle.hugeFontSize, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + IconButton( + icon: Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + Padding( + padding: EdgeInsets.only( + left: UIStyle.contentMarginLarge, + right: UIStyle.contentMarginLarge, + ), + child: Text( + place.address.addressText, + style: TextStyle( + color: Theme.of(context).colorScheme.onSecondary, + fontSize: UIStyle.bigFontSize, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + children: [ + ...?_buildPhonesList(context, place), + ...?_buildOpeningHours(place), + ...?_buildURLsList(context, place), + if (routeToEnabled || addToRouteEnabled) + Padding( + padding: EdgeInsets.only( + top: UIStyle.contentMarginLarge, + left: UIStyle.contentMarginLarge, + right: UIStyle.contentMarginLarge, + ), + child: Row( + children: [ + if (routeToEnabled) ...[ + Spacer(), + _buildOptionButton( + context, + SvgPicture.asset( + "assets/route.svg", + color: Theme.of(context).colorScheme.primary, + width: UIStyle.smallIconSize, + height: UIStyle.smallIconSize, + ), + AppLocalizations.of(context).routeToButtonTitle, + () => Navigator.of(context).pop(PlaceDetailsPopupResult.routeTo), + ), + ], + if (addToRouteEnabled) ...[ + Spacer(), + _buildOptionButton( + context, + Icon( + Icons.add_location, + color: Theme.of(context).colorScheme.primary, + size: UIStyle.smallIconSize, + ), + AppLocalizations.of(context).addToRouteButton, + () => Navigator.of(context).pop(PlaceDetailsPopupResult.addToRoute), + ), + ], + Spacer(), + ], + ), + ), + ], + ); +} + +List _buildPhonesList(BuildContext context, Place place) { + if (place.details.contacts.isEmpty) { + return null; + } + + List phoneWidgets = [ + ...place.details.contacts.first.landlinePhones.map((phone) => _buildPhoneTile(Icons.phone, phone.phoneNumber)), + ...place.details.contacts.first.mobilePhones.map((phone) => _buildPhoneTile(Icons.phone_iphone, phone.phoneNumber)), + ]; + + return _convertToExpansionTile(phoneWidgets); +} + +ListTile _buildPhoneTile(IconData icon, String phoneNumber) => + _buildInfoTile(icon, phoneNumber, () => launch("tel:" + phoneNumber)); + +List _buildOpeningHours(Place place) { + if (place.details.openingHours.isEmpty) { + return null; + } + + List openingHoursWidgets = []; + place.details.openingHours.forEach((openingHours) => + openingHours.text.forEach((hour) => openingHoursWidgets.add(_buildInfoTile(Icons.access_time, hour, null)))); + + return _convertToExpansionTile(openingHoursWidgets); +} + +List _buildURLsList(BuildContext context, Place place) { + if (place.details.contacts.isEmpty) { + return null; + } + + List urlsWidgets = place.details.contacts.first.websites + .map((site) => _buildInfoTile(Icons.language, site.address, () => launch(site.address))) + .toList(); + + return _convertToExpansionTile(urlsWidgets); +} + +List _convertToExpansionTile(List tiles) { + if (tiles.isEmpty) { + return null; + } + + return [ + ListTileTheme.merge( + horizontalTitleGap: 0, + child: tiles.length == 1 + ? tiles.first + : ExpansionTile( + leading: tiles.first.leading, + title: InkWell( + child: Padding( + padding: EdgeInsets.only( + top: UIStyle.contentMarginLarge, + bottom: UIStyle.contentMarginLarge, + ), + child: tiles.first.title, + ), + onTap: tiles.first.onTap, + ), + children: tiles.sublist(1), + ), + ), + ]; +} + +ListTile _buildInfoTile(IconData icon, String text, VoidCallback onTap) => ListTile( + leading: Icon(icon), + title: Text( + text, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + onTap: onTap, + ); + +Widget _buildOptionButton(BuildContext context, Widget icon, String title, VoidCallback onPressed) => + SimpleDialogOption( + padding: EdgeInsets.zero, + child: Material( + borderRadius: BorderRadius.circular(UIStyle.bigButtonHeight), + child: Container( + height: UIStyle.bigButtonHeight, + child: Padding( + padding: EdgeInsets.only( + left: UIStyle.contentMarginLarge, + right: UIStyle.contentMarginLarge, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + icon, + Container( + width: UIStyle.contentMarginMedium, + ), + Text( + title, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ), + onPressed: onPressed, + ); diff --git a/lib/search/recent_search_data_model.dart b/lib/search/recent_search_data_model.dart new file mode 100644 index 0000000..c9012c6 --- /dev/null +++ b/lib/search/recent_search_data_model.dart @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:here_sdk/search.dart'; +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; + +/// A class that represents the recently searched element. +class RecentSearchItem { + /// Unique id. + final int id; + /// Title. + final String title; + /// Id of the [Place]. + final String placeId; + + RecentSearchItem(this.id, this.title, this.placeId); +} + +/// A class that represents the recently searched place. +class RecentSearchPlace { + /// Title. + final String title; + /// [Place]. + final Place place; + + RecentSearchPlace(this.title, this.place); +} + +/// Class that implements a storage for the MRU list for the searching for places. +class RecentSearchDataModel extends ChangeNotifier { + static final _kDbName = "recent_search"; + static final _kTableName = "items"; + static final _kTitleField = "title"; + static final _kPlaceIdField = "place_id"; + static final _kTimeStampField = "timestamp"; + + Database _db; + + RecentSearchDataModel() { + _init(); + } + + Future _init() async { + final dbPath = await getDatabasesPath(); + _db = await openDatabase( + join(dbPath, _kDbName), + onCreate: (db, version) => db.execute("CREATE TABLE $_kTableName(" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "$_kTitleField TEXT, " + "$_kPlaceIdField TEXT, " + "$_kTimeStampField DATETIME)"), + version: 1, + ); + + notifyListeners(); + } + + Future _updateTimeStamp(int id) { + return _db.update( + _kTableName, + { + _kTimeStampField: DateTime.now().millisecondsSinceEpoch, + }, + where: "id = ?", + whereArgs: [id], + ); + } + + /// Adds [text] to the list of recently searched items. + Future insertText(String text) async { + final queryResult = await _db.query( + _kTableName, + where: "$_kTitleField = ?", + whereArgs: [text], + limit: 1, + ); + if (queryResult.isNotEmpty) { + await _updateTimeStamp(queryResult.first["id"]); + } else { + await _db.insert( + _kTableName, + { + _kTitleField: text, + _kTimeStampField: DateTime.now().millisecondsSinceEpoch, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + notifyListeners(); + } + + /// Adds id of a place to the list of recently searched items. + Future insertPlaceId(String placeId) async { + final queryResult = await _db.query( + _kTableName, + where: "$_kPlaceIdField = ?", + whereArgs: [placeId], + limit: 1, + ); + if (queryResult.isNotEmpty) { + await _updateTimeStamp(queryResult.first["id"]); + } else { + await _db.insert( + _kTableName, + { + _kPlaceIdField: placeId, + _kTimeStampField: DateTime.now().millisecondsSinceEpoch, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + notifyListeners(); + } + + /// Removes an element from the list. + Future delete(int id) async { + await _db.delete( + _kTableName, + where: "id = ?", + whereArgs: [id], + ); + } + + /// Returns recently searched items list. + Future> getData() async { + if (_db == null) { + return []; + } + + List> results = await _db.query(_kTableName, orderBy: "$_kTimeStampField DESC"); + return results + .map((e) => RecentSearchItem(e["id"] as int, e[_kTitleField] as String, e[_kPlaceIdField] as String)) + .toList(); + } +} diff --git a/lib/search/search_popup.dart b/lib/search/search_popup.dart new file mode 100644 index 0000000..4d295e2 --- /dev/null +++ b/lib/search/search_popup.dart @@ -0,0 +1,681 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/core.threading.dart'; +import 'package:here_sdk/mapview.dart'; +import 'package:here_sdk/search.dart'; +import 'package:provider/provider.dart'; + +import '../common/dismiss_keyboard_on_scroll.dart'; +import '../common/draggable_popup_here_logo_helper.dart'; +import 'recent_search_data_model.dart'; +import 'search_results_screen.dart'; +import '../common/ui_style.dart'; +import '../common/util.dart' as Util; + +class SearchResult { + final Place place; // if null the current location should be used + + SearchResult({this.place}); + + SearchResult.currentLocation() : place = null; +} + +/// Displays a popup window with a text search for a place. The [currentPosition] is used as center of the search area. +Future showSearchPopup({ + @required BuildContext context, + @required GeoCoordinates currentPosition, + @required HereMapController hereMapController, + @required GlobalKey hereMapKey, + String currentLocationTitle = null, +}) async { + return showModalBottomSheet( + context: context, + shape: UIStyle.topRoundedBorder(), + isScrollControlled: true, + builder: (context) => DraggablePopupHereLogoHelper( + hereMapController: hereMapController, + hereMapKey: hereMapKey, + modal: true, + draggableScrollableSheet: DraggableScrollableSheet( + maxChildSize: UIStyle.maxBottomDraggableSheetSize, + initialChildSize: UIStyle.maxBottomDraggableSheetSize, + minChildSize: 0.5, + expand: false, + builder: (context, controller) => _SearchPopup( + currentPosition: currentPosition, + controller: controller, + currentLocationTitle: currentLocationTitle, + ), + ), + ), + ).then((value) { + hereMapController.setWatermarkPosition(WatermarkPlacement.bottomLeft, 0); + return value; + }); +} + +class _SearchPopup extends StatefulWidget { + final GeoCoordinates currentPosition; + final ScrollController controller; + final String currentLocationTitle; + + _SearchPopup({ + Key key, + @required this.currentPosition, + this.controller, + this.currentLocationTitle, + }) : super(key: key); + + @override + _SearchPopupState createState() => _SearchPopupState(); +} + +class _SearchPopupState extends State<_SearchPopup> { + static const int _kMaxSearchSuggestion = 20; + static const double _kHeaderHeight = 110; + static const double _kHeaderHeightExt = 140; + + final TextEditingController _dstTextEditCtrl = new TextEditingController(); + final SearchOptions _searchOptions = new SearchOptions(LanguageCode.enUs, _kMaxSearchSuggestion); + final SearchEngine _searchEngine = SearchEngine(); + + GeoCoordinates _lastPosition; + List _suggestions; + List _suggestionsPlaces = []; // we must free all the places we get from suggestions, so we store them + List _searchResults; + Place _searchResult; + Map _recentPlaces = {}; + TaskHandle _searchTaskHandle; + bool _searchInProgress = false; + SearchError _lastError; + + @override + void initState() { + _lastPosition = widget.currentPosition; + super.initState(); + } + + @override + void dispose() { + _dstTextEditCtrl.dispose(); + _searchEngine.release(); + _clearSuggestions(); + _clearSearchResults(); + _clearRecentPlace(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Consumer( + builder: (context, model, child) => WillPopScope( + onWillPop: () async { + _stopCurrentSearch(); + return true; + }, + child: DismissKeyboardOnScroll( + child: SafeArea( + child: Stack( + children: [ + CustomScrollView( + controller: widget.controller, + slivers: [ + SliverAppBar( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UIStyle.popupsBorderRadius), + ), + leading: Container(), + leadingWidth: 0, + backgroundColor: colorScheme.background, + pinned: true, + primary: false, + titleSpacing: UIStyle.contentMarginMedium, + title: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSearchHeader(context), + Container( + height: UIStyle.contentMarginMedium, + ), + if (widget.currentLocationTitle != null) + ListTile( + dense: true, + leading: Icon( + Icons.gps_fixed, + color: colorScheme.primary, + size: UIStyle.mediumIconSize, + ), + title: Text( + widget.currentLocationTitle, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + color: colorScheme.secondary, + ), + ), + onTap: () { + _stopCurrentSearch(); + Navigator.of(context).pop(SearchResult.currentLocation()); + }, + ), + _buildResultsHeader(context), + ], + ), + toolbarHeight: widget.currentLocationTitle != null ? _kHeaderHeightExt : _kHeaderHeight, + ), + if (_lastError != null) _buildErrorWidget(context), + if (_lastError == null) + _suggestions != null ? _buildSuggestionsWidget(context) : _buildRecentSearchWidget(context), + ], + ), + if (_searchInProgress) + Container( + color: Colors.white54, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildSearchHeader(BuildContext context) { + Color foregroundColor = Theme.of(context).colorScheme.onSecondary; + + return Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + border: Border.all( + color: foregroundColor, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + width: UIStyle.contentMarginLarge, + ), + Expanded( + child: TextField( + decoration: InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + hintText: AppLocalizations.of(context).searchHint, + ), + controller: _dstTextEditCtrl, + textInputAction: TextInputAction.search, + onChanged: (value) => _suggestionsForText(value), + onSubmitted: (value) => _searchForText(context, value), + ), + ), + IconButton( + icon: Icon(Icons.clear), + color: foregroundColor, + onPressed: () => setState(() { + _lastError = null; + _dstTextEditCtrl.clear(); + _clearSuggestions(); + }), + ), + ], + ), + ), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + AppLocalizations.of(context).cancelTitle, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + ], + ); + } + + Widget _buildResultsHeader(BuildContext context) { + return Text( + _suggestions != null + ? AppLocalizations.of(context).matchingResultsTitle + : AppLocalizations.of(context).recentlySearchTitle, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + color: Theme.of(context).colorScheme.onSecondary, + ), + ); + } + + List _makeHighlightedText(String text, List highlights) { + List result = []; + + if (highlights == null) { + result.add(TextSpan( + text: text, + )); + } else { + int lastPosition = 0; + + highlights.forEach((element) { + result.add(TextSpan( + text: text.substring(lastPosition, element.start), + )); + result.add(TextSpan( + text: text.substring(element.start, element.end), + style: TextStyle( + fontWeight: FontWeight.bold, + ))); + lastPosition = element.end; + }); + + result.add(TextSpan( + text: text.substring(lastPosition), + )); + } + + return result; + } + + Widget _buildSearchTile(BuildContext context, String title, {Map> highlights}) { + List textSpans = _makeHighlightedText(title, (highlights ?? const {})[HighlightType.title]); + + return ListTile( + leading: Icon(Icons.search), + title: RichText( + text: TextSpan( + text: "\"", + style: TextStyle( + fontSize: UIStyle.hugeFontSize, + color: Theme.of(context).colorScheme.primary, + ), + children: [ + ...textSpans, + TextSpan( + text: "\"", + ), + ], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Container(), + onTap: () => _searchForText(context, title), + ); + } + + Widget _buildPlaceTile(BuildContext context, Place place, {Map> highlights}) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + List titleTextSpans = _makeHighlightedText(place.title, (highlights ?? const {})[HighlightType.title]); + List addressTextSpans = + _makeHighlightedText(place.address.addressText, (highlights ?? const {})[HighlightType.addressLabel]); + + return ListTile( + leading: Icon(Icons.location_on_rounded), + title: RichText( + text: TextSpan( + text: "", + style: TextStyle( + fontSize: UIStyle.hugeFontSize, + color: colorScheme.primary, + ), + children: titleTextSpans, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Padding( + padding: EdgeInsets.only( + top: UIStyle.contentMarginSmall, + bottom: UIStyle.contentMarginSmall, + ), + child: RichText( + text: TextSpan( + text: "", + style: TextStyle( + color: colorScheme.onSecondary, + fontSize: UIStyle.bigFontSize, + ), + children: [ + TextSpan( + text: Util.makeDistanceString(context, place.distanceInMeters), + style: TextStyle( + color: colorScheme.secondary, + ), + ), + ...addressTextSpans, + ], + ), + maxLines: 2, + ), + ), + onTap: () { + FocusScope.of(context).unfocus(); + RecentSearchDataModel model = Provider.of(context, listen: false); + model.insertPlaceId(place.id); + _showSearchResults(context, null, [place]); + }, + ); + } + + Stream> _recentSearchItemsStream(BuildContext context) async* { + RecentSearchDataModel model = Provider.of(context, listen: false); + List items = await model.getData(); + List result = []; + + for (RecentSearchItem item in items) { + if (item.placeId == null) { + result.add(RecentSearchPlace(item.title, null)); + } else { + Place place = _recentPlaces[item.placeId]; + + if (place == null) { + final Completer completer = Completer(); + // try to find a place by id, if it is not there, then remove it from the list + TaskHandle taskHandle = _searchEngine + .searchByPlaceIdWithLanguageCode(PlaceIdQuery(item.placeId), LanguageCode.enUs, (error, place) { + if (error != null) { + if (error == SearchError.noResultsFound) { + model.delete(item.id); + } + completer.complete(); + } + + completer.complete(place); + }); + + place = await completer.future; + taskHandle.release(); + + if (place == null) { + continue; + } + + _recentPlaces[item.placeId] = place; + } + + result.add(RecentSearchPlace(item.title, place)); + } + + yield result; + } + } + + Widget _buildRecentSearchWidget(BuildContext context) { + return StreamBuilder>( + initialData: [], + stream: _recentSearchItemsStream(context), + builder: (context, snapshot) => snapshot.hasData + ? SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index.isOdd) { + return Divider( + height: 1, + ); + } + + final RecentSearchPlace item = snapshot.data[index ~/ 2]; + return item.place != null + ? _buildPlaceTile(context, item.place) + : _buildSearchTile(context, item.title); + }, + semanticIndexCallback: (Widget widget, int localIndex) { + if (localIndex.isEven) { + return localIndex ~/ 2; + } + return null; + }, + childCount: snapshot.data.length * 2 - 1, + ), + ) + : SliverFillRemaining(), + ); + } + + Widget _buildSuggestionsWidget(BuildContext context) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index.isOdd) { + return Divider( + height: 1, + ); + } + + Widget suggestionsWidget; + Suggestion suggestion = _suggestions[index ~/ 2]; + Place place = suggestion.place; + Map> highlights = suggestion.getHighlights(); + + if (place == null) { + suggestionsWidget = _buildSearchTile( + context, + suggestion.title, + highlights: highlights, + ); + } else { + _suggestionsPlaces.add(place); + suggestionsWidget = _buildPlaceTile( + context, + place, + highlights: highlights, + ); + } + + highlights?.forEach((key, value) => value?.forEach((element) => element.release())); + + return suggestionsWidget; + }, + semanticIndexCallback: (Widget widget, int localIndex) { + if (localIndex.isEven) { + return localIndex ~/ 2; + } + return null; + }, + childCount: _suggestions.length * 2 - 1, + ), + ); + } + + Widget _buildErrorWidget(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + AppLocalizations appLocalizations = AppLocalizations.of(context); + + if (_lastError == SearchError.noResultsFound) { + return SliverFillRemaining( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset("assets/nothing_found.svg"), + Text( + appLocalizations.noResultsFoundText, + style: TextStyle( + fontSize: UIStyle.hugeFontSize, + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ), + ), + Text( + appLocalizations.noResultsFoundDescription, + style: TextStyle( + fontSize: UIStyle.bigFontSize, + color: colorScheme.onSecondary, + ), + ), + ], + ), + ), + ); + } else { + return SliverFillRemaining( + child: Center( + child: Text( + Util.formatString(appLocalizations.searchErrorText, [_lastError.toString()]), + style: TextStyle( + fontSize: UIStyle.hugeFontSize, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + ), + ); + } + } + + void _clearSuggestions() { + if (_suggestions != null) { + _suggestions.forEach((element) => element.release()); + _suggestions = null; + } + _suggestionsPlaces.forEach((place) { + if (place != _searchResult) { + place.release(); + } + }); + _suggestionsPlaces.clear(); + } + + void _clearSearchResults() { + if (_searchResults != null) { + _searchResults.forEach((place) { + if (place != _searchResult) { + place.release(); + } + }); + _searchResults = null; + } + } + + void _clearRecentPlace() { + _recentPlaces.values.forEach((place) { + if (place != _searchResult) { + place.release(); + } + }); + } + + void _stopCurrentSearch() { + if (_searchTaskHandle != null) { + _searchTaskHandle.cancel(); + _searchTaskHandle.release(); + _searchTaskHandle = null; + } + } + + void _suggestionsForText(String text) { + _stopCurrentSearch(); + + if (_lastError != null) { + setState(() { + _lastError = null; + }); + } + + if (text.isEmpty) { + // clear suggestions + setState(() { + _clearSuggestions(); + }); + } else { + // start searching + _searchTaskHandle = + _searchEngine.suggest(TextQuery.withAreaCenter(text, _lastPosition), _searchOptions, (error, suggestions) { + if (error != null) { + print('Search failed. Error: ${error.toString()}'); + } + + setState(() { + _clearSuggestions(); + _suggestions = suggestions ?? []; + }); + }); + } + } + + void _searchForText(BuildContext context, String text) { + _stopCurrentSearch(); + FocusScope.of(context).unfocus(); + + if (text.isEmpty) { + return; + } + + setState(() { + _searchInProgress = true; + }); + + RecentSearchDataModel model = Provider.of(context, listen: false); + model.insertText(text); + + _searchTaskHandle = _searchEngine.searchByText(TextQuery.withAreaCenter(text, _lastPosition), _searchOptions, + (error, places) async { + if (error != null) { + print('Search failed. Error: ${error.toString()}'); + } else { + _clearSearchResults(); + _searchResults = places; + await _showSearchResults(context, text, places); + } + setState(() { + _lastError = error; + _searchInProgress = false; + }); + }); + } + + void _showSearchResults(BuildContext context, String queryString, List places) async { + final result = await Navigator.of(context).pushNamed( + SearchResultsScreen.navRoute, + arguments: [queryString, places, _lastPosition], + ); + + if (result != null) { + if (result is GeoCoordinates) { + _lastPosition = result; + } else if (result is Place) { + _searchResult = result; + Navigator.of(context).pop(SearchResult( + place: _searchResult, + )); + } else { + assert(false); + } + } + } +} diff --git a/lib/search/search_results_screen.dart b/lib/search/search_results_screen.dart new file mode 100644 index 0000000..24fc803 --- /dev/null +++ b/lib/search/search_results_screen.dart @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2020-2021 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:here_sdk/core.dart'; +import 'package:here_sdk/gestures.dart'; +import 'package:here_sdk/mapview.dart'; +import 'package:here_sdk/search.dart'; + +import "../common/draggable_popup_here_logo_helper.dart"; +import '../common/reset_location_button.dart'; +import '../positioning/positioning.dart'; +import '../common/ui_style.dart'; +import '../common/util.dart' as Util; +import 'place_details_popup.dart'; + +/// Search results screen widget. +class SearchResultsScreen extends StatefulWidget { + static const String navRoute = "/search/results"; + + /// Original query string. + final String queryString; + + /// Resulting list of places. + final List places; + + /// Current position. + final GeoCoordinates currentPosition; + + /// Creates a widget. + SearchResultsScreen({ + Key key, + @required this.queryString, + @required this.places, + @required this.currentPosition, + }) : super(key: key); + + @override + _SearchResultsScreenState createState() => _SearchResultsScreenState(); +} + +class _SearchResultsScreenState extends State with TickerProviderStateMixin, Positioning { + static const double _kZoomDistanceToEarth = 1000; // meters + static const double _kTapRadius = 3; // pixels + static const double _kPlaceCardHeight = 80; + + final GlobalKey _bottomBarKey = GlobalKey(); + final GlobalKey _hereMapKey = GlobalKey(); + + HereMapController _hereMapController; + + MapImage _smallMarkerImage; + MapImage _bigMarkerImage; + List _markers; + TabController _tabController; + int _selectedIndex = -1; + + @override + void initState() { + _tabController = TabController( + length: widget.places.length, + vsync: this, + ); + _tabController.addListener(() => _updateSelectedPlace()); + enableMapUpdate = false; + super.initState(); + } + + @override + void dispose() { + _hereMapController?.release(); + _smallMarkerImage?.release(); + _bigMarkerImage?.release(); + _markers.forEach((element) => element.release()); + releaseLocationEngine(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => DefaultTabController( + length: widget.places.length, + child: WillPopScope( + onWillPop: () async { + if (_hereMapController != null) { + Navigator.of(context).pop(_hereMapController.camera.state.targetCoordinates); + return false; + } + + return true; + }, + child: Scaffold( + body: HereMap( + key: _hereMapKey, + onMapCreated: _onMapCreated, + ), + bottomNavigationBar: _buildBottomNavigationBar(context), + extendBodyBehindAppBar: true, + floatingActionButton: enableMapUpdate + ? null + : ResetLocationButton( + onPressed: _resetCurrentPosition, + ), + ), + ), + ); + + void _onMapCreated(HereMapController hereMapController) { + _hereMapController = hereMapController; + + hereMapController.mapScene.loadSceneFromConfigurationFile('preview.normal.day.json', (MapError error) { + if (error != null) { + print('Map scene not loaded. MapError: ${error.toString()}'); + return; + } + + hereMapController.setWatermarkPosition(WatermarkPlacement.bottomLeft, 0); + _hereMapController.camera.lookAtPointWithOrientationAndDistance( + widget.currentPosition, MapCameraOrientationUpdate.withDefaults(), Positioning.initDistanceToEarth); + + _addPanListener(); + _createResultsMarkers(); + _setTapGestureHandler(); + + initLocationEngine( + hereMapController: hereMapController, + ); + }); + } + + void _addPanListener() { + _hereMapController.gestures.panListener = + PanListener.fromLambdas(lambda_onPan: (state, origin, translation, velocity) { + if (enableMapUpdate) { + setState(() => enableMapUpdate = false); + } + }); + } + + void _resetCurrentPosition() { + GeoCoordinates coordinates = lastKnownLocation != null ? lastKnownLocation.coordinates : widget.currentPosition; + + _hereMapController.camera.lookAtPointWithOrientationAndDistance( + coordinates, MapCameraOrientationUpdate.withDefaults(), Positioning.initDistanceToEarth); + + setState(() => enableMapUpdate = true); + } + + void _setTapGestureHandler() { + _hereMapController.gestures.tapListener = TapListener.fromLambdas(lambda_onTap: (Point2D touchPoint) { + _pickMapMarker(touchPoint); + }); + } + + void _pickMapMarker(Point2D touchPoint) { + _hereMapController.pickMapItems(touchPoint, _kTapRadius, (pickMapItemsResult) { + List mapMarkerList = pickMapItemsResult.markers; + if (mapMarkerList.length == 0) { + print("No map markers found."); + return; + } + + int index = _markers.indexOf(mapMarkerList.first); + _tabController.animateTo(index); + _showPlaceDetailsPopup(context, widget.places[index]); + }); + } + + void _updateSelectedPlace() { + if (_selectedIndex >= 0) { + _markers[_selectedIndex].image = _smallMarkerImage; + _markers[_selectedIndex].drawOrder = UIStyle.searchMarkerDrawOrder; + } + + _markers[_tabController.index].image = _bigMarkerImage; + _markers[_tabController.index].drawOrder = UIStyle.waypointsMarkerDrawOrder; + + _selectedIndex = _tabController.index; + _zoomToPlace(_selectedIndex); + if (enableMapUpdate) { + setState(() => enableMapUpdate = false); + } + } + + void _zoomToPlace(int index) { + _hereMapController.camera.lookAtPointWithDistance(widget.places[index].geoCoordinates, _kZoomDistanceToEarth); + } + + void _createResultsMarkers() { + assert(widget.places.isNotEmpty); + + int markerSize = (_hereMapController.pixelScale * UIStyle.searchMarkerSize).truncate(); + _smallMarkerImage = MapImage.withFilePathAndWidthAndHeight("assets/map_marker.svg", markerSize, markerSize); + _bigMarkerImage = + MapImage.withFilePathAndWidthAndHeight("assets/map_marker_big.svg", markerSize * 2, markerSize * 2); + _markers = []; + + for (int i = 0; i < widget.places.length; ++i) { + MapMarker mapMarker = Util.createMarkerWithImage( + widget.places[i].geoCoordinates, + _smallMarkerImage, + anchor: Anchor2D.withHorizontalAndVertical(0.5, 1), + ); + + _markers.add(mapMarker); + _hereMapController.mapScene.addMapMarker(mapMarker); + } + + _updateSelectedPlace(); + + if (widget.places.length == 1) { + _hereMapController.camera + .lookAtPointWithDistance(widget.places.first.geoCoordinates, Positioning.initDistanceToEarth); + } else { + GeoBox geoBox = GeoBox.containingGeoCoordinates(widget.places.map((e) => e.geoCoordinates).toList()); + + if (_bottomBarKey.currentContext != null) { + final RenderBox bottomBarBox = _bottomBarKey.currentContext.findRenderObject() as RenderBox; + final double topOffset = MediaQuery.of(context).padding.top; + + _hereMapController.zoomGeoBoxToLogicalViewPort( + geoBox: geoBox, + viewPort: Rect.fromLTRB( + 0, topOffset, bottomBarBox.size.width, MediaQuery.of(context).size.height - bottomBarBox.size.height)); + } + } + } + + Widget _buildPlacesTabs(BuildContext context) { + return Container( + alignment: Alignment.bottomCenter, + padding: EdgeInsets.all(UIStyle.contentMarginSmall), + child: Container( + width: double.infinity, + height: _kPlaceCardHeight, + child: TabBarView( + controller: _tabController, + children: widget.places + .map((place) => Card( + elevation: 2, + key: UniqueKey(), + child: _buildPlaceTile(context, place, null), + )) + .toList(), + ), + ), + ); + } + + Widget _buildNavigationHeader(BuildContext context, bool expanded) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Row( + children: [ + IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).pop(_hereMapController.camera.state.targetCoordinates), + ), + Expanded( + child: Text( + widget.queryString ?? "", + style: TextStyle( + fontSize: UIStyle.hugeFontSize, + color: colorScheme.primary, + ), + ), + ), + if (widget.places.length > 1) + IconButton( + icon: Icon( + expanded ? Icons.expand_more : Icons.expand_less, + ), + onPressed: expanded ? () => Navigator.of(context).pop() : () => _showResultsList(context), + ), + ], + ); + } + + Widget _buildBottomNavigationBar(BuildContext context) { + return BottomAppBar( + key: _bottomBarKey, + color: Theme.of(context).colorScheme.background, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildNavigationHeader(context, false), + _buildPlacesTabs(context), + ], + ), + ); + } + + Widget _buildPlaceTile(BuildContext context, Place place, int index) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + + return Container( + margin: EdgeInsets.only(left: UIStyle.contentMarginExtraSmall), + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: _selectedIndex == index ? Theme.of(context).accentColor : Colors.transparent, + width: UIStyle.contentMarginExtraSmall), + ), + ), + child: ListTile( + tileColor: _selectedIndex == index ? UIStyle.selectedListTileColor : null, + title: Text( + place.title, + style: TextStyle(fontSize: UIStyle.hugeFontSize), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Padding( + padding: EdgeInsets.only( + top: UIStyle.contentMarginSmall, + bottom: UIStyle.contentMarginSmall, + ), + child: RichText( + text: TextSpan( + text: Util.makeDistanceString(context, place.distanceInMeters), + style: TextStyle( + color: colorScheme.onSecondary, + fontWeight: FontWeight.bold, + fontSize: UIStyle.bigFontSize, + ), + children: [ + TextSpan( + text: place.address.addressText, + style: TextStyle( + fontWeight: FontWeight.normal, + ), + ), + ]), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + trailing: ClipOval( + child: Material( + child: Ink( + width: UIStyle.smallButtonHeight, + height: UIStyle.smallButtonHeight, + color: colorScheme.background, + child: InkWell( + child: Center( + child: SvgPicture.asset( + "assets/route.svg", + color: Theme.of(context).colorScheme.onSecondary, + width: UIStyle.smallIconSize, + height: UIStyle.smallIconSize, + ), + ), + onTap: () { + if (index != null) { + // close bottom sheet + Navigator.of(context).pop(); + } + Navigator.of(context).pop(place); + }, + ), + ), + ), + ), + onTap: index != null + ? () { + Navigator.of(context).pop(); + _tabController.animateTo(index); + } + : () => _showPlaceDetailsPopup(context, place), + ), + ); + } + + void _showPlaceDetailsPopup(BuildContext context, Place place) async { + final PlaceDetailsPopupResult result = await showPlaceDetailsPopup( + context: context, + place: place, + routeToEnabled: true, + ); + if (result == PlaceDetailsPopupResult.routeTo) { + Navigator.of(context).pop(place); + } + } + + _showResultsList(BuildContext context) async { + await showModalBottomSheet( + isScrollControlled: true, + context: context, + shape: UIStyle.topRoundedBorder(), + builder: (context) => DraggablePopupHereLogoHelper( + hereMapController: _hereMapController, + hereMapKey: _hereMapKey, + modal: true, + draggableScrollableSheet: DraggableScrollableSheet( + maxChildSize: UIStyle.maxBottomDraggableSheetSize, + initialChildSize: 0.6, + expand: false, + builder: (context, controller) => CustomScrollView( + semanticChildCount: widget.places.length, + controller: controller, + slivers: [ + SliverAppBar( + leading: Container(), + shape: UIStyle.topRoundedBorder(), + leadingWidth: 0, + backgroundColor: Theme.of(context).colorScheme.background, + pinned: true, + titleSpacing: 0, + title: _buildNavigationHeader(context, true), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final int itemIndex = index ~/ 2; + return index.isEven + ? _buildPlaceTile(context, widget.places[itemIndex], itemIndex) + : Divider( + height: 1, + ); + }, + semanticIndexCallback: (Widget widget, int localIndex) { + if (localIndex.isEven) { + return localIndex ~/ 2; + } + return null; + }, + childCount: widget.places.length * 2 - 1, + ), + ), + ], + ), + ), + ), + ); + + _hereMapController.setWatermarkPosition(WatermarkPlacement.bottomLeft, 0); + } +} diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000..13b1657 --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,8 @@ +Place the here_sdk plugin folder here! + +How to get the plugin folder: + +1. Download the HERE SDK package for the _Navigate Edition_ from the [HERE platform](https://platform.here.com/sdk) portal. +2. Inside, you'll find zipped documentation assets and a zipped file named "heresdk-navigate-flutter-{version}.tar.gz". +3. Unzip this *.tar.gz file as it's containing the HERE SDK plugin. +4. Rename the unzipped folder to "here_sdk" and move the entire folder including its content to here. diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..f58d5ac --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,451 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1+2" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0+1" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_ringtone_player: + dependency: "direct main" + description: + name: flutter_ringtone_player + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + url: "https://pub.dartlang.org" + source: hosted + version: "0.19.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_tts: + dependency: "direct main" + description: + name: flutter_tts + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + here_sdk: + dependency: "direct main" + description: + path: "plugins/here_sdk" + relative: true + source: path + version: "4.7.0" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.10" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4" + path: + dependency: "direct main" + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + path_drawing: + dependency: transitive + description: + name: path_drawing + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.1+1" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.28" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+2" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+8" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+3" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.1+1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.1" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.2+3" + reorderables: + dependency: "direct main" + description: + name: reorderables + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.2" + screen: + dependency: "direct main" + description: + name: screen + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + sqflite: + dependency: "direct main" + description: + name: sqflite + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.2+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2+1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0+2" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.19" + timezone: + dependency: transitive + description: + name: timezone + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.4+1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.1" +sdks: + dart: ">=2.12.0 <3.0.0" + flutter: ">=1.24.0-10.1.pre" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..eee0917 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,104 @@ +name: RefApp +description: A new Flutter project. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 + +environment: + sdk: ">=2.9.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + sqflite: + path: + flutter_localizations: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.0 + intl: ^0.17.0 + permission_handler: ^5.0.1+1 + provider: ^4.3.2+3 + flutter_svg: ^0.19.3 + flutter_tts: ^2.1.0 + reorderables: ^0.3.2 + flutter_ringtone_player: ^3.0.0 + flutter_local_notifications: ^4.0.0 + path_provider: ^1.6.17 + url_launcher: ^6.0.2 + screen: ^0.0.5 + + # The following adds the HERE SDK for Flutter plugin folder to your application. + here_sdk: + path: plugins/here_sdk + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + assets: + - assets/ + - assets/maneuvers/dark/ + - assets/maneuvers/light/ + - assets/maneuvers/dark/png/ + - assets/maneuvers/light/png/ + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages + + generate: true