From b7a470fdc8a86aca67a87143616d145dd07894a8 Mon Sep 17 00:00:00 2001 From: Matias Dahlin Holst Date: Wed, 16 Aug 2023 13:56:40 +0200 Subject: [PATCH] Added flutter federated plugin --- .vscode/launch.json | 28 + LICENSE | 11 + README.md | 271 ++++ mapsindoors_mapbox/CHANGELOG.md | 12 + mapsindoors_mapbox/LICENSE | 11 + mapsindoors_mapbox/README.md | 293 ++++ mapsindoors_mapbox/dartdoc_options.yaml | 28 + mapsindoors_mapbox/example/LICENSE | 11 + mapsindoors_mapbox/example/README.md | 16 + .../example/analysis_options.yaml | 29 + mapsindoors_mapbox/example/android/.gitignore | 13 + .../example/android/app/build.gradle | 71 + .../android/app/src/debug/AndroidManifest.xml | 8 + .../android/app/src/main/AndroidManifest.xml | 35 + .../mapsindoors_example/MainActivity.kt | 6 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../src/main/res/values/mapbox_api_key.xml | 5 + .../app/src/main/res/values/strings.xml | 4 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 8 + .../example/android/build.gradle | 45 + .../example/android/gradle.properties | 4 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../example/android/settings.gradle | 11 + .../integration_test/directions_test.dart | 139 ++ .../integration_test/displayrule_test.dart | 424 +++++ .../integration_test/mapcontrol_test.dart | 366 +++++ .../integration_test/mapsindoors_test.dart | 127 ++ mapsindoors_mapbox/example/ios/.gitignore | 34 + .../ios/Flutter/AppFrameworkInfo.plist | 26 + .../example/ios/Flutter/Debug.xcconfig | 2 + .../example/ios/Flutter/LICENSE | 11 + .../example/ios/Flutter/Release.xcconfig | 2 + mapsindoors_mapbox/example/ios/LICENSE | 11 + mapsindoors_mapbox/example/ios/Podfile | 96 ++ mapsindoors_mapbox/example/ios/Podfile.lock | 60 + .../ios/Runner.xcodeproj/project.pbxproj | 584 +++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../example/ios/Runner/AppDelegate.swift | 15 + .../AppIcon.appiconset/Contents.json | 122 ++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 564 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 1588 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1025 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 1716 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 1920 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 1895 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 3831 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 1888 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 3294 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 3612 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../example/ios/Runner/Info.plist | 51 + .../ios/Runner/Runner-Bridging-Header.h | 1 + .../lib/example_position_provider.dart | 58 + mapsindoors_mapbox/example/lib/main.dart | 792 +++++++++ mapsindoors_mapbox/example/pubspec.yaml | 94 ++ .../example/test/widget_test.dart | 26 + mapsindoors_mapbox/lib/core/mapsindoors.dart | 160 ++ .../lib/core/mapsindoors_widget.dart | 348 ++++ .../lib/core/mp_default_floor_selector.dart | 170 ++ .../lib/core/mp_directions_renderer.dart | 51 + .../lib/core/mp_directions_service.dart | 48 + .../lib/core/mp_floor_selector.dart | 3 + .../lib/core/mp_map_label_font.dart | 12 + mapsindoors_mapbox/lib/mapsindoors.dart | 87 + mapsindoors_mapbox/pubspec.yaml | 37 + mapsindoors_mapbox_android/CHANGELOG.md | 5 + mapsindoors_mapbox_android/LICENSE | 11 + mapsindoors_mapbox_android/README.md | 3 + .../android/build.gradle | 98 ++ .../android/gradle.properties | 1 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59821 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + mapsindoors_mapbox_android/android/gradlew | 234 +++ .../android/gradlew.bat | 89 + .../android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + .../mapspeople/mapsindoors/PlatformMapView.kt | 79 + .../kotlin/com/mapspeople/mapsindoors/Util.kt | 103 ++ .../mapsindoors/core/DirectionsRenderer.kt | 120 ++ .../mapsindoors/core/DirectionsService.kt | 66 + .../mapsindoors/core/DisplayRuleHandler.kt | 411 +++++ .../mapspeople/mapsindoors/core/MapView.kt | 533 ++++++ .../mapsindoors/core/MapViewFactory.kt | 21 + .../mapsindoors/core/MapsindoorsPlugin.kt | 409 +++++ .../core/PlatformMapViewInterface.kt | 15 + .../mapsindoors/core/models/Bounds.kt | 12 + .../mapsindoors/core/models/CameraPosition.kt | 11 + .../mapsindoors/core/models/CameraUpdate.kt | 37 + .../mapsindoors/core/models/Filter.kt | 47 + .../mapsindoors/core/models/Location.kt | 22 + .../mapsindoors/core/models/MPError.kt | 21 + .../mapsindoors/core/models/MapBehavior.kt | 37 + .../mapsindoors/core/models/MapConfig.kt | 15 + .../core/models/PositionProvider.kt | 44 + .../mapsindoors/core/models/PositionResult.kt | 105 ++ .../mapsindoors/core/models/Query.kt | 19 + .../src/main/res/values/mapbox_api_key.xml | 5 + mapsindoors_mapbox_android/pubspec.yaml | 31 + mapsindoors_mapbox_ios/CHANGELOG.md | 5 + mapsindoors_mapbox_ios/LICENSE | 11 + mapsindoors_mapbox_ios/README.md | 3 + mapsindoors_mapbox_ios/ios/Assets/.gitkeep | 0 .../ios/Classes/MapsIndoorsPlugin.swift | 101 ++ .../ios/Classes/MapsIndoorsView.swift | 174 ++ .../DirectionsRendererMethodChannel.swift | 245 +++ .../DirectionsServiceMethodChannel.swift | 203 +++ .../Channels/DisplayRuleMethodChannel.swift | 1443 +++++++++++++++++ .../MapControlFloorSelectorChannel.swift | 48 + .../MapControlListenerMethodChannel.swift | 181 +++ .../MapControl/MapControlMethodChannel.swift | 664 ++++++++ .../MapsIndoorsListenerChannel.swift | 76 + .../MapsIndoorsMethodChannel.swift | 485 ++++++ .../MapsIndoors/UtilMethodChannel.swift | 323 ++++ .../ios/Classes/core/FlutterMapView.swift | 14 + .../ios/Classes/core/MapsIndoorsData.swift | 301 ++++ .../Classes/core/Models/CameraPosition.swift | 14 + .../Classes/core/Models/CameraUpdate.swift | 20 + .../ios/Classes/core/Models/Filter.swift | 96 ++ .../core/Models/MPIconSizeCodable.swift | 36 + .../core/Models/MPLocationIdCodable.swift | 26 + .../ios/Classes/core/Models/Query.swift | 27 + .../ios/mapsindoors_mapbox_ios.podspec | 23 + mapsindoors_mapbox_ios/pubspec.yaml | 29 + mapsindoors_platform_interface/CHANGELOG.md | 7 + mapsindoors_platform_interface/LICENSE | 11 + mapsindoors_platform_interface/README.md | 3 + ...directionsrenderer_platform_interface.dart | 40 + .../directionsservice_platform_interface.dart | 30 + .../lib/display_rule_platform_interface.dart | 103 ++ .../lib/mapcontrol_platform_interface.dart | 69 + .../lib/mapsindoors_platform_interface.dart | 63 + .../collections/mp_building_collection.dart | 23 + .../collections/mp_category_collection.dart | 22 + .../lib/models/collections/mp_collection.dart | 23 + .../collections/mp_user_role_collection.dart | 12 + .../collections/mp_venue_collection.dart | 23 + .../lib/models/entities/mp_building.dart | 124 ++ .../lib/models/entities/mp_entity.dart | 9 + .../lib/models/entities/mp_floor.dart | 86 + .../lib/models/entities/mp_location.dart | 249 +++ .../lib/models/entities/mp_venue.dart | 123 ++ .../models/enums/live_data_domain_types.dart | 20 + .../lib/models/enums/mp_camera_event.dart | 4 + .../models/enums/mp_camera_view_fit_mode.dart | 11 + .../models/enums/mp_collision_handling.dart | 31 + .../lib/models/enums/mp_highway.dart | 22 + .../enums/mp_location_propery_names.dart | 30 + .../lib/models/enums/mp_location_type.dart | 4 + .../enums/mp_solution_display_rule.dart | 13 + .../lib/models/geometries/mp_bounds.dart | 76 + .../lib/models/geometries/mp_geometry.dart | 16 + .../models/geometries/mp_multi_polygon.dart | 101 ++ .../lib/models/geometries/mp_point.dart | 123 ++ .../lib/models/geometries/mp_polygon.dart | 108 ++ .../listeners/mp_camera_event_listener.dart | 4 + ...lding_found_at_camera_target_listener.dart | 4 + .../on_floor_selection_changed_listener.dart | 4 + .../listeners/on_floor_update_listener.dart | 4 + .../listeners/on_leg_selected_listener.dart | 4 + .../on_live_location_update_listener.dart | 4 + .../on_location_selected_listener.dart | 4 + .../listeners/on_map_click_listener.dart | 4 + .../listeners/on_map_ready_listener.dart | 4 + .../on_mapsindoors_ready_listener.dart | 4 + .../listeners/on_marker_click_listener.dart | 4 + .../on_marker_info_window_click_listener.dart | 4 + .../on_position_update_listener.dart | 4 + ...venue_found_at_camera_target_listener.dart | 4 + .../lib/models/map/mp_camera_position.dart | 50 + .../lib/models/map/mp_camera_update.dart | 28 + .../lib/models/mp_building_info.dart | 35 + .../lib/models/mp_category.dart | 22 + .../lib/models/mp_data_field.dart | 26 + .../lib/models/mp_display_rule.dart | 258 +++ .../lib/models/mp_error.dart | 49 + .../lib/models/mp_filter.dart | 153 ++ .../models/mp_floor_selector_interface.dart | 33 + .../lib/models/mp_geocode_result.dart | 33 + .../lib/models/mp_icon_size.dart | 23 + .../lib/models/mp_map_behavior.dart | 145 ++ .../lib/models/mp_map_style.dart | 28 + .../mp_position_provider_interface.dart | 18 + .../models/mp_position_result_interface.dart | 33 + .../lib/models/mp_property_data.dart | 77 + .../lib/models/mp_query.dart | 65 + .../lib/models/mp_route_result.dart | 9 + .../lib/models/mp_settings_3d.dart | 38 + .../lib/models/mp_solution.dart | 60 + .../lib/models/mp_solution_config.dart | 50 + .../lib/models/mp_user_role.dart | 27 + .../lib/models/mp_venue_info.dart | 19 + .../lib/models/routing/mp_route.dart | 50 + .../models/routing/mp_route_coordinate.dart | 33 + .../lib/models/routing/mp_route_leg.dart | 61 + .../lib/models/routing/mp_route_property.dart | 25 + .../lib/models/routing/mp_route_step.dart | 115 ++ .../lib/platform_library.dart | 98 ++ .../directionsrenderer_method_channel.dart | 77 + .../src/directionsservice_method_channel.dart | 51 + .../lib/src/display_rule_method_channel.dart | 374 +++++ .../lib/src/mapcontrol_method_channel.dart | 385 +++++ .../lib/src/mapsindoors_method_channel.dart | 227 +++ .../lib/src/util.dart | 108 ++ .../lib/src/util_method_channel.dart | 68 + .../lib/util_platform_interface.dart | 42 + mapsindoors_platform_interface/pubspec.yaml | 18 + 231 files changed, 16976 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 mapsindoors_mapbox/CHANGELOG.md create mode 100644 mapsindoors_mapbox/LICENSE create mode 100644 mapsindoors_mapbox/README.md create mode 100644 mapsindoors_mapbox/dartdoc_options.yaml create mode 100644 mapsindoors_mapbox/example/LICENSE create mode 100644 mapsindoors_mapbox/example/README.md create mode 100644 mapsindoors_mapbox/example/analysis_options.yaml create mode 100644 mapsindoors_mapbox/example/android/.gitignore create mode 100644 mapsindoors_mapbox/example/android/app/build.gradle create mode 100644 mapsindoors_mapbox/example/android/app/src/debug/AndroidManifest.xml create mode 100644 mapsindoors_mapbox/example/android/app/src/main/AndroidManifest.xml create mode 100644 mapsindoors_mapbox/example/android/app/src/main/kotlin/com/mapspeople/mapsindoors_example/MainActivity.kt create mode 100644 mapsindoors_mapbox/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 mapsindoors_mapbox/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 mapsindoors_mapbox/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mapsindoors_mapbox/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mapsindoors_mapbox/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mapsindoors_mapbox/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mapsindoors_mapbox/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mapsindoors_mapbox/example/android/app/src/main/res/values-night/styles.xml create mode 100644 mapsindoors_mapbox/example/android/app/src/main/res/values/mapbox_api_key.xml create mode 100644 mapsindoors_mapbox/example/android/app/src/main/res/values/strings.xml create mode 100644 mapsindoors_mapbox/example/android/app/src/main/res/values/styles.xml create mode 100644 mapsindoors_mapbox/example/android/app/src/profile/AndroidManifest.xml create mode 100644 mapsindoors_mapbox/example/android/build.gradle create mode 100644 mapsindoors_mapbox/example/android/gradle.properties create mode 100644 mapsindoors_mapbox/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 mapsindoors_mapbox/example/android/settings.gradle create mode 100644 mapsindoors_mapbox/example/integration_test/directions_test.dart create mode 100644 mapsindoors_mapbox/example/integration_test/displayrule_test.dart create mode 100644 mapsindoors_mapbox/example/integration_test/mapcontrol_test.dart create mode 100644 mapsindoors_mapbox/example/integration_test/mapsindoors_test.dart create mode 100644 mapsindoors_mapbox/example/ios/.gitignore create mode 100644 mapsindoors_mapbox/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 mapsindoors_mapbox/example/ios/Flutter/Debug.xcconfig create mode 100644 mapsindoors_mapbox/example/ios/Flutter/LICENSE create mode 100644 mapsindoors_mapbox/example/ios/Flutter/Release.xcconfig create mode 100644 mapsindoors_mapbox/example/ios/LICENSE create mode 100644 mapsindoors_mapbox/example/ios/Podfile create mode 100644 mapsindoors_mapbox/example/ios/Podfile.lock create mode 100644 mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 mapsindoors_mapbox/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 mapsindoors_mapbox/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 mapsindoors_mapbox/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 mapsindoors_mapbox/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 mapsindoors_mapbox/example/ios/Runner/AppDelegate.swift create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 mapsindoors_mapbox/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 mapsindoors_mapbox/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 mapsindoors_mapbox/example/ios/Runner/Info.plist create mode 100644 mapsindoors_mapbox/example/ios/Runner/Runner-Bridging-Header.h create mode 100644 mapsindoors_mapbox/example/lib/example_position_provider.dart create mode 100644 mapsindoors_mapbox/example/lib/main.dart create mode 100644 mapsindoors_mapbox/example/pubspec.yaml create mode 100644 mapsindoors_mapbox/example/test/widget_test.dart create mode 100644 mapsindoors_mapbox/lib/core/mapsindoors.dart create mode 100644 mapsindoors_mapbox/lib/core/mapsindoors_widget.dart create mode 100644 mapsindoors_mapbox/lib/core/mp_default_floor_selector.dart create mode 100644 mapsindoors_mapbox/lib/core/mp_directions_renderer.dart create mode 100644 mapsindoors_mapbox/lib/core/mp_directions_service.dart create mode 100644 mapsindoors_mapbox/lib/core/mp_floor_selector.dart create mode 100644 mapsindoors_mapbox/lib/core/mp_map_label_font.dart create mode 100644 mapsindoors_mapbox/lib/mapsindoors.dart create mode 100644 mapsindoors_mapbox/pubspec.yaml create mode 100644 mapsindoors_mapbox_android/CHANGELOG.md create mode 100644 mapsindoors_mapbox_android/LICENSE create mode 100644 mapsindoors_mapbox_android/README.md create mode 100644 mapsindoors_mapbox_android/android/build.gradle create mode 100644 mapsindoors_mapbox_android/android/gradle.properties create mode 100644 mapsindoors_mapbox_android/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 mapsindoors_mapbox_android/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 mapsindoors_mapbox_android/android/gradlew create mode 100644 mapsindoors_mapbox_android/android/gradlew.bat create mode 100644 mapsindoors_mapbox_android/android/settings.gradle create mode 100644 mapsindoors_mapbox_android/android/src/main/AndroidManifest.xml create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/PlatformMapView.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/Util.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/DirectionsRenderer.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/DirectionsService.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/DisplayRuleHandler.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/MapView.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/MapViewFactory.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/MapsindoorsPlugin.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/PlatformMapViewInterface.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Bounds.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/CameraPosition.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/CameraUpdate.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Filter.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Location.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/MPError.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/MapBehavior.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/MapConfig.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/PositionProvider.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/PositionResult.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Query.kt create mode 100644 mapsindoors_mapbox_android/android/src/main/res/values/mapbox_api_key.xml create mode 100644 mapsindoors_mapbox_android/pubspec.yaml create mode 100644 mapsindoors_mapbox_ios/CHANGELOG.md create mode 100644 mapsindoors_mapbox_ios/LICENSE create mode 100644 mapsindoors_mapbox_ios/README.md create mode 100644 mapsindoors_mapbox_ios/ios/Assets/.gitkeep create mode 100644 mapsindoors_mapbox_ios/ios/Classes/MapsIndoorsPlugin.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/MapsIndoorsView.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Channels/DirectionsRendererMethodChannel.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Channels/DirectionsServiceMethodChannel.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Channels/DisplayRuleMethodChannel.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapControl/MapControlFloorSelectorChannel.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapControl/MapControlListenerMethodChannel.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapControl/MapControlMethodChannel.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapsIndoors/MapsIndoorsListenerChannel.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapsIndoors/MapsIndoorsMethodChannel.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapsIndoors/UtilMethodChannel.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/FlutterMapView.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/MapsIndoorsData.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Models/CameraPosition.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Models/CameraUpdate.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Models/Filter.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Models/MPIconSizeCodable.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Models/MPLocationIdCodable.swift create mode 100644 mapsindoors_mapbox_ios/ios/Classes/core/Models/Query.swift create mode 100644 mapsindoors_mapbox_ios/ios/mapsindoors_mapbox_ios.podspec create mode 100644 mapsindoors_mapbox_ios/pubspec.yaml create mode 100644 mapsindoors_platform_interface/CHANGELOG.md create mode 100644 mapsindoors_platform_interface/LICENSE create mode 100644 mapsindoors_platform_interface/README.md create mode 100644 mapsindoors_platform_interface/lib/directionsrenderer_platform_interface.dart create mode 100644 mapsindoors_platform_interface/lib/directionsservice_platform_interface.dart create mode 100644 mapsindoors_platform_interface/lib/display_rule_platform_interface.dart create mode 100644 mapsindoors_platform_interface/lib/mapcontrol_platform_interface.dart create mode 100644 mapsindoors_platform_interface/lib/mapsindoors_platform_interface.dart create mode 100644 mapsindoors_platform_interface/lib/models/collections/mp_building_collection.dart create mode 100644 mapsindoors_platform_interface/lib/models/collections/mp_category_collection.dart create mode 100644 mapsindoors_platform_interface/lib/models/collections/mp_collection.dart create mode 100644 mapsindoors_platform_interface/lib/models/collections/mp_user_role_collection.dart create mode 100644 mapsindoors_platform_interface/lib/models/collections/mp_venue_collection.dart create mode 100644 mapsindoors_platform_interface/lib/models/entities/mp_building.dart create mode 100644 mapsindoors_platform_interface/lib/models/entities/mp_entity.dart create mode 100644 mapsindoors_platform_interface/lib/models/entities/mp_floor.dart create mode 100644 mapsindoors_platform_interface/lib/models/entities/mp_location.dart create mode 100644 mapsindoors_platform_interface/lib/models/entities/mp_venue.dart create mode 100644 mapsindoors_platform_interface/lib/models/enums/live_data_domain_types.dart create mode 100644 mapsindoors_platform_interface/lib/models/enums/mp_camera_event.dart create mode 100644 mapsindoors_platform_interface/lib/models/enums/mp_camera_view_fit_mode.dart create mode 100644 mapsindoors_platform_interface/lib/models/enums/mp_collision_handling.dart create mode 100644 mapsindoors_platform_interface/lib/models/enums/mp_highway.dart create mode 100644 mapsindoors_platform_interface/lib/models/enums/mp_location_propery_names.dart create mode 100644 mapsindoors_platform_interface/lib/models/enums/mp_location_type.dart create mode 100644 mapsindoors_platform_interface/lib/models/enums/mp_solution_display_rule.dart create mode 100644 mapsindoors_platform_interface/lib/models/geometries/mp_bounds.dart create mode 100644 mapsindoors_platform_interface/lib/models/geometries/mp_geometry.dart create mode 100644 mapsindoors_platform_interface/lib/models/geometries/mp_multi_polygon.dart create mode 100644 mapsindoors_platform_interface/lib/models/geometries/mp_point.dart create mode 100644 mapsindoors_platform_interface/lib/models/geometries/mp_polygon.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/mp_camera_event_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/on_building_found_at_camera_target_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/on_floor_selection_changed_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/on_floor_update_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/on_leg_selected_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/on_live_location_update_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/on_location_selected_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/on_map_click_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/on_map_ready_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/on_mapsindoors_ready_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/on_marker_click_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/on_marker_info_window_click_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/on_position_update_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/listeners/on_venue_found_at_camera_target_listener.dart create mode 100644 mapsindoors_platform_interface/lib/models/map/mp_camera_position.dart create mode 100644 mapsindoors_platform_interface/lib/models/map/mp_camera_update.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_building_info.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_category.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_data_field.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_display_rule.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_error.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_filter.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_floor_selector_interface.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_geocode_result.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_icon_size.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_map_behavior.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_map_style.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_position_provider_interface.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_position_result_interface.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_property_data.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_query.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_route_result.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_settings_3d.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_solution.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_solution_config.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_user_role.dart create mode 100644 mapsindoors_platform_interface/lib/models/mp_venue_info.dart create mode 100644 mapsindoors_platform_interface/lib/models/routing/mp_route.dart create mode 100644 mapsindoors_platform_interface/lib/models/routing/mp_route_coordinate.dart create mode 100644 mapsindoors_platform_interface/lib/models/routing/mp_route_leg.dart create mode 100644 mapsindoors_platform_interface/lib/models/routing/mp_route_property.dart create mode 100644 mapsindoors_platform_interface/lib/models/routing/mp_route_step.dart create mode 100644 mapsindoors_platform_interface/lib/platform_library.dart create mode 100644 mapsindoors_platform_interface/lib/src/directionsrenderer_method_channel.dart create mode 100644 mapsindoors_platform_interface/lib/src/directionsservice_method_channel.dart create mode 100644 mapsindoors_platform_interface/lib/src/display_rule_method_channel.dart create mode 100644 mapsindoors_platform_interface/lib/src/mapcontrol_method_channel.dart create mode 100644 mapsindoors_platform_interface/lib/src/mapsindoors_method_channel.dart create mode 100644 mapsindoors_platform_interface/lib/src/util.dart create mode 100644 mapsindoors_platform_interface/lib/src/util_method_channel.dart create mode 100644 mapsindoors_platform_interface/lib/util_platform_interface.dart create mode 100644 mapsindoors_platform_interface/pubspec.yaml diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..328b15e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "example", + "cwd": "mapsindoors/example", + "request": "launch", + "type": "dart" + }, + { + "name": "example (profile mode)", + "cwd": "mapsindoors/example", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "example (release mode)", + "cwd": "mapsindoors/example", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..75757d5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,11 @@ +Copyright 2023 Mapspeople + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..57109ca --- /dev/null +++ b/README.md @@ -0,0 +1,271 @@ + +A federated Flutter plugin for integrating with the native MapsIndoors SDK. + +| Platform | Android | iOS | +| ------------ | ------- | ----- | +| **Supports** | SDK 21+ | 13.0+ | + +# ReadMe + +## Features + +​ +Use this plugin to: +​ + +- Show indoor mapping and navigation. +- Perform real-time wayfinding. +- See location live updates. +​ +This plugin is based on the MapsIndoors V4 SDK for Android and iOS. At the moment only Google Maps is supported. + +## Getting Started + +​ +Add MapsIndoors version `1.0.0` to your `pubspec.yaml`. +​ + +```yaml +mapsindoors: ^1.0.0 +``` + +### Android + +​ + +#### Android Gooogle Maps Setup + +​ +To get the underlying Google Map to function, you need to perform the following steps: +​ + +1. Navigate to `android/app/src/main/res/value`. +2. Create a file in this folder called `google_maps_api_key.xml`. +3. Copy and paste the below code snippet and replace `YOUR_KEY_HERE` with your Google Maps API key. + +​ + +```xml + + + YOUR_KEY_HERE + +``` + +​ + +#### MapsIndoors Gradle Setup + +​ +The plugin Gradle project has trouble resolving the MapsIndoors dependency, so to ensure that it is resolved correctly, do the following: +​ + +1. Navigate to the app's project level `build.gradle`. +2. add `maven { url 'https://maven.mapsindoors.com/' }` to `allprojects`/`repositories` after `mavenCentral()` +​ + +```groovy +allprojects { + repositories { + google() + mavenCentral() + maven { url 'https://maven.mapsindoors.com/' } + } +} +``` + +​ + +### iOS + +#### iOS Gooogle Maps Setup + +​ +To get the underlying Google Map to function, you need to perform the following steps: + +#### Providing API key + +##### Swift + +1. Navigate to `iOS/Runner/AppDelegate.swift`. +2. Import GoogleMaps on the class +3. Add this code as the first line inside the application function: `GMSServices.provideAPIKey("YOUR GOOGLE MAPS API KEY HERE")` + +##### Objective-C + +1. Navigate to `iOS/Runner/AppDelegate.h`. +2. Import `#import "GoogleMaps/GoogleMaps.h"` on the class. +3. Add this code as the first line inside the application function: `[GMSServices provideAPIKey:@"YOUR GOOGLE MAPS API KEY HERE"];` + +#### Adding MapsIndoors script specific to Google Maps, to Podfile + +After this you should navigate into the iOS folder of your flutter project and add this script to the applications Podfile: [MapsIndoors podfile Post install](https://github.com/MapsIndoors/MapsIndoorsIOS/wiki/Podfile-post_install-v4) + +## Usage + +​ +This section has examples of code for the following tasks: +​ + +- [Showing your Map](#showing-your-map) +- [Showing a Route](#showing-a-route) +- [Searching Locations](#searching-locations) +- [Changing the look with DisplayRules](#changing-the-look-with-displayrules) + +​ + +### Showing your Map + +​ +This snippet shows how to set up `MapsIndoors` in a Flutter application. First, the `MapsIndoorsWidget` is added to the application's build tree. +​ +Optionally we can add a `MPFloorSelector` to the map, here we use `MPDefaultFloorSelector` as it is provided with the MapsIndoors package. The selector must be added both to the build tree as well as to `MapControl` in order to function correctly. +​ +Once `initState()` has been called, `MapsIndoors` begins initialization, and once that is done successfully, `MapControl` begins initialization. +​ +Once `MapControl` is initialize we can invoke the `goTo` method to move the camera to the default venue. +​ + +```Dart +class MapWidgetState extends State { + // Lets build a floor selector widget here, we need to add this to MapControl later. + final _floorSelectorWidget = MPDefaultFloorSelector(); + // MapControl will be initialized after MapsIndoors. + late final MapControl _mapControl; +​ + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + // Add the MapsIndoors Widget to your Widget, it will automatically fill the container it is placed in. + child: MapsIndoorsWidget( + // build with the default floor selector, this is optional. + floorSelector: _floorSelectorWidget, + ), + ), + ); + } +​ + @override + void initState() { + super.initState(); + // Start initializing mapsindoors, replace the string with your own api key. + _mapsIndoorsReadyListener(MapsIndoors.load("your_mapsindoors_api_key")); + } +​ + void _mapsIndoorsReadyListener(Future error) async { + if ((await error) == null) { + // if no errors occured during MapsIndoors load, then we can initialize MapControl + // we use setFloorSelector to allow MapControl to fill out the floor selector widget with floor information + MapControl.create(MPMapConfig().setFloorSelector(_floorSelectorWidget)).then((mc) async { + _mapControl = mc; + // After initialization, lets show some indoor maps, here we move the camera to the default venue. + _mapControl.goTo(await MapsIndoors.getDefaultVenue()); + }).catchError((err) { + print("Creating mapcontrol faced an issue: $err"); + }); + } else { + print("MapsIndoors faced an error: $error"); + } + } +} +``` + +​ + +### Showing a Route + +​ +This code snippet initializes the `MPDirectionsService` and `MPDirectionsRenderer` classes, and uses `_mapControl` which have been initialized elsewhere. +​ +The `showRouteToLocation` function is used to query a route from the user's current position to a specified location using `directionsService.getRoute()`. +​ +If successful, the route is displayed on the map using `directionsRenderer.setRoute(route)`. +​ + +```Dart +// We assume MapControl has already been initialized +late final _mapControl; +​ +// The user is positioned somewhere in the world +var _userLocation = MPPoint.withCoordinates(longitude: -98.44, latitude: 35.16); +/// Query a route to a location from the user's position +void showRouteToLocation(MPLocation location) async { + // initialize services + final directionsService = MPDirectionsService(); + final directionsRenderer = MPDirectionsRenderer(); +​ + // get the route from userlocation to the location + directionsService.getRoute(origin: _userPosition, destination: location.point).then((route) { + // When we have the route, we can show it on the map + directionsRenderer.setRoute(route); + }).catchError((err) => print("An error occured: $err")); // otherwise handle the error +} +``` + +​ + +### Searching Locations + +​ +This code snippet shows a function called `searchForParking` that takes a single argument of type `MPPoint`. The function uses `MapsIndoors` to search for locations matching the query string `"parking"` near the point specified. +​ +It mathces in the locations' descriptions, names, and external IDs to the query string. Once the search is complete, it is possible to update/get information from the locations (not specified in the code snippet). +​ + +```Dart +/// This method searches for locations +void searchForParking(MPPoint point) { + final mpq = MPQueryBuilder(); + // Set the search string + mpq.setQuery("parking"); + // Set the point where we would like to search around + mpq.setNear(point); + // We are searcing in the locations description, name and external id. + mpq.setQueryProperties([MPLocationPropertyNames.description.name, MPLocationPropertyNames.name.name, MPLocationPropertyNames.externalId.name]); + // Apply the query on MapsIndoors. + MapsIndoors.getLocationsByQuery(query: mpq.build()).then((locations) { + print("number of paring near the point: ${locations?.length}"); + // do something with the locations + }); +} +``` + +​ + +### Changing the look with DisplayRules + +​ +This code snippet shows three ways to manipulate display rules in the MapsIndoors SDK. +​ +The `hideLocationsByDefault()` method hides all markers that are not explicitly visible by setting the main display rule to not visible. +​ +The `showLocationsByDefault()` method ensures all markers are shown by setting the main display rule to visible. +​ +The `changeTypePolygonColor(String type, String color)` method changes the fill color for all polygons belonging to a specific type. It gets the display rule for the specified type using `getDisplayRuleByName`, and sets the fill color using `setPolygonFillColor`. +​ +These methods can all be used to customize the display of markers and polygons on the map. +​ + +```Dart +/// This method changes the main display rule to hide all markers, +/// This will cause all locations that are not explicitly visible to be hidden. +void hideLocationsByDefault() async { + final MPDisplayRule? main = await MapsIndoors.getMainDisplayRule(); + main?.setVisible(false); +} +​ +/// This method changes the main display rule to show all markers, +/// This will cause all locations that are not explicitly visible to be shown. +void showLocationsByDefault() async { + final MPDisplayRule? main = await MapsIndoors.getMainDisplayRule(); + main?.setVisible(true); +} +​ +/// This method changes the fill color for all polygons belonging to a specific [type] +/// the [color] MUST be a valid hex color string. +void changeTypePolygonColor(String type, String color) async { + final MPDisplayRule? rule = await MapsIndoors.getDisplayRuleByName(type); + rule?.setPolygonFillColor(color); +} +``` diff --git a/mapsindoors_mapbox/CHANGELOG.md b/mapsindoors_mapbox/CHANGELOG.md new file mode 100644 index 0000000..254eae5 --- /dev/null +++ b/mapsindoors_mapbox/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## 2.0.0 + +* Moved from [mapsindoors](https://pub.dev/packages/mapsindoors) to allow for multiple map providers +* Changes to classes: + * MapControl + * ``MapControl`` has merged with the ``MapsIndoorsWidget``, combining them into a single entity. the ``Widget`` will still be built in the build tree, and accepts a listener as a parameter to wait for the MapControl part to be initialized. + * MapsIndoors + * Has been split up into functions on the namespace to align better with dart language standards. Some methods have changed naming to avoid collision with popular method and parameter naming (eg. ``MapsIndoors.load()`` is now ``loadMapsIndoors()``) +* Changes to the Widget + * The ``MapsIndoorsWidget`` has been changed to be a [``UniqueWidget``](https://api.flutter.dev/flutter/widgets/UniqueWidget-class.html), this is to ensure that the underlying MapsIndoors in the platform code can function normally. \ No newline at end of file diff --git a/mapsindoors_mapbox/LICENSE b/mapsindoors_mapbox/LICENSE new file mode 100644 index 0000000..75757d5 --- /dev/null +++ b/mapsindoors_mapbox/LICENSE @@ -0,0 +1,11 @@ +Copyright 2023 Mapspeople + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/mapsindoors_mapbox/README.md b/mapsindoors_mapbox/README.md new file mode 100644 index 0000000..8439be2 --- /dev/null +++ b/mapsindoors_mapbox/README.md @@ -0,0 +1,293 @@ + +A federated Flutter plugin for integrating with the native MapsIndoors SDK. + +| Platform | Android | iOS | +| ------------ | ------- | ----- | +| **Supports** | SDK 21+ | 13.0+ | + +# ReadMe + +## Features + +​ +Use this plugin to: +​ + +- Show indoor mapping and navigation. +- Perform real-time wayfinding. +- See location live updates. +​ +This plugin is based on the MapsIndoors-Mabox V4 SDK for Android and iOS. + +## Getting Started + +​ +Add MapsIndoors version `2.0.0` to your `pubspec.yaml`. +​ + +```yaml +mapsindoors_mapbox: ^2.0.0 +``` + +### Android + +​ + +#### Android Gooogle Maps Setup + +​ +To get the underlying Mapbox map to function, you need to perform the following steps: +​ + +1. Navigate to `android/app/src/main/res/value`. +2. Create a file in this folder called `mapbox_api_key.xml`. +3. Copy and paste the below code snippet and replace `YOUR_KEY_HERE` with your Mapbox API key. + +​ + +```xml + + + YOUR_KEY_HERE + YOUR_KEY_HERE + + +``` + +​ + +#### MapsIndoors Gradle Setup + +​ +The plugin Gradle project has trouble resolving the MapsIndoors dependency, so to ensure that it is resolved correctly, do the following: +​ + +1. Navigate to the app's project level `build.gradle`. +2. add `maven { url 'https://maven.mapsindoors.com/' }` to `allprojects`/`repositories` after `mavenCentral()` +​ + +```groovy +allprojects { + repositories { + google() + mavenCentral() + maven { url 'https://maven.mapsindoors.com/' } + } +} +``` + +​ + +### iOS +​ +The MapsIndoors SDK requires iOS 13, so make sure that your podfile is configured for iOS 13. Add !use_frameworks inside your app target as well. + +``` +platform :ios, '13.0 + +target 'MyApp' do + use_frameworks! + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES' + end + ... +end +... +``` + +#### Providing API key +Navigate to your application settings and add your Mapbox public access token to info with the key MBXAccessToken +Setup your secret access token for downloading the sdk. Read how to do this here: [Configure credentials](https://docs.mapbox.com/ios/maps/guides/install/#configure-credentials) + +## Usage + +​ +This section has examples of code for the following tasks: +​ + +- [Showing your Map](#showing-your-map) +- [Showing a Route](#showing-a-route) +- [Searching Locations](#searching-locations) +- [Changing the look with DisplayRules](#changing-the-look-with-displayrules) + +​ + +### Showing your Map + +​ +This snippet shows how to set up `MapsIndoors` in a Flutter application. First, the `MapsIndoorsWidget` is added to the application's build tree. +​ +Optionally we can add a `MPFloorSelector` to the map. Here we use `MPDefaultFloorSelector` as it is provided with the MapsIndoors package. The selector must be added both to the build tree as well as to `MapControl` in order to function correctly. +​ +Once `initState()` has been called, `MapsIndoors` begins initialization, and once that is done successfully, `MapControl` begins initialization. +​ +Once `MapControl` is initialized we can invoke the `goTo` method to move the camera to the default venue. +​ + +```Dart +import 'package:mapsindoors/mapsindoors.dart' as MapsIndoors; +import 'package:mapsindoors/mapsindoors_library.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + // replace the string with your own api key + home: MapWidget("demo"), + ); + } +} + +class MapWidget extends StatefulWidget { + final String apiKey; + + const MapWidget({Key? key, required this.apiKey}) : super(key: key); + + @override + MapWidgetState createState() => MapWidgetState(); +} + +class MapWidgetState extends State { + // Let's build a floor selector widget here, we need to add this to MapControl later. + final _floorSelectorWidget = MPDefaultFloorSelector(); + // MapControl will be initialized after MapsIndoors. + late final MapsIndoorsWidget _mapController; +​ + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + // Add the MapsIndoors Widget to your Widget, it will automatically fill the container it is placed in. + child: _mapController = MapsIndoorsWidget( + // build with the default floor selector, this is optional. + floorSelector: _floorSelectorWidget, + readyListener: _mapControlReadyListener, + ), + ), + ); + } +​ + @override + void initState() { + super.initState(); + // Start initializing mapsindoors. + loadMapsIndoors(widget.apiKey); + } +​ + void _mapControlReadyListener(MPError? error) async { + if (error == null) { + // if no errors occured during MapControl load, then we can start using the map + // Here we move the camera to the default venue. + _mapController.goTo(await getDefaultVenue()); + + } else { + print("Creating mapcontrol faced an issue: $err"); + } + } +} +``` + +### Showing a Route +​ +This code snippet initializes the `MPDirectionsService` and `MPDirectionsRenderer` classes, and uses `_mapControl` which has been initialized elsewhere. +​ +The `showRouteToLocation` function is used to query a route from the user's current position to a specified location using `directionsService.getRoute()`. +​ +If successful, the route is displayed on the map using `directionsRenderer.setRoute(route)`. +​ + +```Dart +// We assume MapControl has already been initialized +late final _mapControl; +​ +// The user is positioned somewhere in the world +var _userLocation = MPPoint.withCoordinates(longitude: -98.44, latitude: 35.16); +/// Query a route to a location from the user's position +void showRouteToLocation(MPLocation location) async { + // initialize services + final directionsService = MPDirectionsService(); + final directionsRenderer = MPDirectionsRenderer(); +​ + // get the route from userlocation to the location + directionsService.getRoute(origin: _userPosition, destination: location.point).then((route) { + // When we have the route, we can show it on the map + directionsRenderer.setRoute(route); + }).catchError((err) => print("An error occured: $err")); // otherwise handle the error +} +``` + +​ + +### Searching Locations + +​ +This code snippet shows a function called `searchForParking` that takes a single argument of type `MPPoint`. The function uses `MapsIndoors` to search for locations matching the query string `"parking"` near the point specified. +​ +It mathces in the locations' descriptions, names, and external IDs to the query string. Once the search is complete, it is possible to update/get information from the locations (not specified in the code snippet). +​ + +```Dart +/// This method searches for locations +void searchForParking(MPPoint point) { + final mpq = MPQuery.builder() + // Set the search string + ..setQuery("parking") + // Set the point where we would like to search around + ..setNear(point) + // We are searcing in the locations description, name and external id. + ..setQueryProperties([MPLocationPropertyNames.description.name, MPLocationPropertyNames.name.name, MPLocationPropertyNames.externalId.name]); + // Apply the query on MapsIndoors. + getLocationsByQuery(query: mpq.build()).then((locations) { + print("number of paring near the point: ${locations?.length}"); + // do something with the locations + }); +} +``` + +​ + +### Changing the look with DisplayRules + +​ +This code snippet shows three ways to manipulate display rules in the MapsIndoors SDK. +​ +The `hideLocationsByDefault()` method hides all markers that are not explicitly visible by setting the main display rule to not visible. +​ +The `showLocationsByDefault()` method ensures all markers are shown by setting the main display rule to visible. +​ +The `changeTypePolygonColor(String type, String color)` method changes the fill color for all polygons belonging to a specific type. It gets the display rule for the specified type using `getDisplayRuleByName`, and sets the fill color using `setPolygonFillColor`. +​ +These methods can all be used to customize the display of markers and polygons on the map. +​ + +```Dart +/// This method changes the main display rule to hide all markers, +/// This will cause all locations that are not explicitly visible to be hidden. +void hideLocationsByDefault() async { + final MPDisplayRule? main = await getMainDisplayRule(); + main?.setVisible(false); +} +​ +/// This method changes the main display rule to show all markers, +/// This will cause all locations that are not explicitly visible to be shown. +void showLocationsByDefault() async { + final MPDisplayRule? main = await getMainDisplayRule(); + main?.setVisible(true); +} +​ +/// This method changes the fill color for all polygons belonging to a specific [type] +/// the [color] MUST be a valid hex color string. +void changeTypePolygonColor(String type, String color) async { + final MPDisplayRule? rule = await getDisplayRuleByName(type); + rule?.setPolygonFillColor(color); +} +``` diff --git a/mapsindoors_mapbox/dartdoc_options.yaml b/mapsindoors_mapbox/dartdoc_options.yaml new file mode 100644 index 0000000..756a61e --- /dev/null +++ b/mapsindoors_mapbox/dartdoc_options.yaml @@ -0,0 +1,28 @@ +# This file is used by dartdoc when generating API documentation for Flutter. +dartdoc: + ignore: + - reexported-private-api-across-packages + errors: + # Default errors of dartdoc: + - duplicate-file + - invalid-parameter + - no-defining-library-found + - tool-error + - unresolved-export + # Warnings that are elevated to errors: + - ambiguous-doc-reference + - ambiguous-reexport + - broken-link + - category-order-gives-missing-package-name + - deprecated + - ignored-canonical-for + - missing-from-search-index + - no-canonical-found + - no-documentable-libraries + - no-library-level-docs + - not-implemented + - orphaned-file + # - unknown-directive # Disabled due to https://github.com/dart-lang/dartdoc/issues/2353 + - unknown-file + - unknown-macro + - unresolved-doc-reference diff --git a/mapsindoors_mapbox/example/LICENSE b/mapsindoors_mapbox/example/LICENSE new file mode 100644 index 0000000..75757d5 --- /dev/null +++ b/mapsindoors_mapbox/example/LICENSE @@ -0,0 +1,11 @@ +Copyright 2023 Mapspeople + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/mapsindoors_mapbox/example/README.md b/mapsindoors_mapbox/example/README.md new file mode 100644 index 0000000..abdbdec --- /dev/null +++ b/mapsindoors_mapbox/example/README.md @@ -0,0 +1,16 @@ +# mapsindoors_example + +Demonstrates how to use the mapsindoors plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/mapsindoors_mapbox/example/analysis_options.yaml b/mapsindoors_mapbox/example/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/mapsindoors_mapbox/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/mapsindoors_mapbox/example/android/.gitignore b/mapsindoors_mapbox/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/mapsindoors_mapbox/example/android/.gitignore @@ -0,0 +1,13 @@ +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 +**/*.keystore +**/*.jks diff --git a/mapsindoors_mapbox/example/android/app/build.gradle b/mapsindoors_mapbox/example/android/app/build.gradle new file mode 100644 index 0000000..d9d574a --- /dev/null +++ b/mapsindoors_mapbox/example/android/app/build.gradle @@ -0,0 +1,71 @@ +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 FileNotFoundException()("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" + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.mapspeople.mapsindoors_example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion 21 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + 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" +} diff --git a/mapsindoors_mapbox/example/android/app/src/debug/AndroidManifest.xml b/mapsindoors_mapbox/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..b7a9a99 --- /dev/null +++ b/mapsindoors_mapbox/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/mapsindoors_mapbox/example/android/app/src/main/AndroidManifest.xml b/mapsindoors_mapbox/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..990a02d --- /dev/null +++ b/mapsindoors_mapbox/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + diff --git a/mapsindoors_mapbox/example/android/app/src/main/kotlin/com/mapspeople/mapsindoors_example/MainActivity.kt b/mapsindoors_mapbox/example/android/app/src/main/kotlin/com/mapspeople/mapsindoors_example/MainActivity.kt new file mode 100644 index 0000000..1a7080b --- /dev/null +++ b/mapsindoors_mapbox/example/android/app/src/main/kotlin/com/mapspeople/mapsindoors_example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.mapspeople.mapsindoors_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/mapsindoors_mapbox/example/android/app/src/main/res/drawable-v21/launch_background.xml b/mapsindoors_mapbox/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/mapsindoors_mapbox/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/mapsindoors_mapbox/example/android/app/src/main/res/drawable/launch_background.xml b/mapsindoors_mapbox/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/mapsindoors_mapbox/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/mapsindoors_mapbox/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/mapsindoors_mapbox/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/mapsindoors_mapbox/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mapsindoors_mapbox/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mapsindoors_mapbox/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/android/app/src/main/res/values-night/styles.xml b/mapsindoors_mapbox/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/mapsindoors_mapbox/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/mapsindoors_mapbox/example/android/app/src/main/res/values/mapbox_api_key.xml b/mapsindoors_mapbox/example/android/app/src/main/res/values/mapbox_api_key.xml new file mode 100644 index 0000000..02474a9 --- /dev/null +++ b/mapsindoors_mapbox/example/android/app/src/main/res/values/mapbox_api_key.xml @@ -0,0 +1,5 @@ + + + YOUR_KEY_HERE + YOUR_KEY_HERE + diff --git a/mapsindoors_mapbox/example/android/app/src/main/res/values/strings.xml b/mapsindoors_mapbox/example/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..0d2c4cc --- /dev/null +++ b/mapsindoors_mapbox/example/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mapsindoors_mapbox/example/android/app/src/main/res/values/styles.xml b/mapsindoors_mapbox/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/mapsindoors_mapbox/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/mapsindoors_mapbox/example/android/app/src/profile/AndroidManifest.xml b/mapsindoors_mapbox/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..b7a9a99 --- /dev/null +++ b/mapsindoors_mapbox/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/mapsindoors_mapbox/example/android/build.gradle b/mapsindoors_mapbox/example/android/build.gradle new file mode 100644 index 0000000..735e7c1 --- /dev/null +++ b/mapsindoors_mapbox/example/android/build.gradle @@ -0,0 +1,45 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven { url 'https://maven.mapsindoors.com/' } + maven { + url 'https://api.mapbox.com/downloads/v2/releases/maven' + authentication { + basic(BasicAuthentication) + } + credentials { + // Do not change the username below. + // This should always be `mapbox` (not your username). + username = "mapbox" + // Use the secret token you stored in gradle.properties as the password + password = project.properties['MAPBOX_DOWNLOADS_TOKEN'] ?: "" + } + } + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/mapsindoors_mapbox/example/android/gradle.properties b/mapsindoors_mapbox/example/android/gradle.properties new file mode 100644 index 0000000..7a0bbf3 --- /dev/null +++ b/mapsindoors_mapbox/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +MAPBOX_DOWNLOADS_TOKEN=YOUR_KEY_HERE \ No newline at end of file diff --git a/mapsindoors_mapbox/example/android/gradle/wrapper/gradle-wrapper.properties b/mapsindoors_mapbox/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cb24abd --- /dev/null +++ b/mapsindoors_mapbox/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/mapsindoors_mapbox/example/android/settings.gradle b/mapsindoors_mapbox/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/mapsindoors_mapbox/example/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/mapsindoors_mapbox/example/integration_test/directions_test.dart b/mapsindoors_mapbox/example/integration_test/directions_test.dart new file mode 100644 index 0000000..348450f --- /dev/null +++ b/mapsindoors_mapbox/example/integration_test/directions_test.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mapsindoors_mapbox/mapsindoors.dart'; + +import 'package:mapsindoors_example/main.dart' as app; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('end-to-end test', () { + testWidgets('Directions', (tester) async { + app.main(); + await tester.pumpAndSettle(); + final Finder fab = find.byType(ElevatedButton); + // Emulate a tap on the floating action button. + await tester.tap(fab); + + // Trigger a frame. + await tester.pumpAndSettle(const Duration(seconds: 4)); + + final app.MapWidgetState state = tester.state(find.byType(app.MapWidget)); + + MapsIndoorsWidget? mapControl = state.mapController; + + expect(mapControl, isNot(null)); + + MPDirectionsService service = MPDirectionsService(); + MPDirectionsRenderer renderer = MPDirectionsRenderer(); + + await service.addAvoidWayType(MPHighway.steps.name); + await service.addAvoidWayType("gert"); + await service.clearWayType(); + + await service.setIsDeparture(false); + await service.setIsDeparture(true); + await service.setTime(1675764926); + + await service.setTravelMode(MPDirectionsService.travelModeTransit); + await service.setTravelMode(MPDirectionsService.travelModeDriving); + await service.setTravelMode(MPDirectionsService.travelModeBicycling); + await service.setTravelMode(MPDirectionsService.travelModeWalking); + + // ensure null route does not throw an error + //TODO: this throws an error on android + //await service.getRoute(origin: MPPoint(), destination: MPPoint()); + + final MPPoint insideFloor10 = MPPoint.withCoordinates(longitude: 9.950697439483086, latitude: 57.05813516976602, floorIndex: 10); + final MPPoint insideFloor0 = MPPoint.withCoordinates(longitude: 9.950444542549313, latitude: 57.05799928441941, floorIndex: 0); + final MPPoint insideNoIndex = MPPoint.withCoordinates(longitude: 9.950444542549313, latitude: 57.05799928441941); + final MPPoint outside = MPPoint.withCoordinates(longitude: 9.948246747758377, latitude: 57.05846751376223, floorIndex: 0); + final MPPoint outsideNoIndex = MPPoint.withCoordinates(longitude: 9.948246747758377, latitude: 57.05846751376223); + final MPPoint outsideOther = MPPoint.withCoordinates(longitude: 9.94501902681362, latitude: 57.05904322246081, floorIndex: 0); + + // In/In (save route for renderer test) + MPRoute route1 = await service.getRoute(origin: insideFloor0, destination: insideFloor10); + // In/In reverse (also save this route) + MPRoute route2 = await service.getRoute(origin: insideFloor10, destination: insideFloor0); + // In/In one without floorIndex + await service.getRoute(origin: insideNoIndex, destination: insideFloor10); + // Out/In (see if the allows this) + MPRoute routeOutside = await service.getRoute(origin: outside, destination: insideFloor0); + // Out/In in without floorIndex + await service.getRoute(origin: outside, destination: insideNoIndex); + // In/Out + await service.getRoute(origin: insideFloor0, destination: outside); + // Out/In no floorIndex + await service.getRoute(origin: outsideNoIndex, destination: insideNoIndex); + // Out/Out + await service.getRoute(origin: outside, destination: outsideOther); + // In/In same point + await service.getRoute(origin: insideFloor0, destination: insideFloor0); + + // The route should work + expect(route1, isNot(null)); + expect(route2, isNot(null)); + expect(routeOutside, isNot(null)); + + int index = 0; + await renderer.setOnLegSelectedListener((legIndex) => index = legIndex); + + await renderer.setRoute(route1); + + expect(index, 0); + + // Should already be the first leg, test if previousLeg causes an issue + await renderer.previousLeg(); + + expect(index, 0); + + // Go back and forth + await renderer.nextLeg(); + expect(index, 1); + await renderer.previousLeg(); + expect(index, 0); + + await renderer.selectLegIndex(0); + expect(index, 0); + + var selectedLegFloorIndex = await renderer.getSelectedLegFloorIndex(); + expect(selectedLegFloorIndex, 0); + + await renderer.selectLegIndex(1); + expect(index, 1); + + selectedLegFloorIndex = await renderer.getSelectedLegFloorIndex(); + expect(selectedLegFloorIndex, 10); + + // These works because the SDK is not in debug mode + await renderer.selectLegIndex(-1); + await renderer.selectLegIndex(99); + + await renderer.setAnimatedPolyline(true, true, 600); + + await renderer.setAnimatedPolyline(false, true, 0); + + await renderer.setCameraAnimationDuration(0); + await renderer.setCameraAnimationDuration(50000); + await renderer.setCameraViewFitMode(MPCameraViewFitMode.firstStepAligned); + await renderer.setCameraViewFitMode(MPCameraViewFitMode.northAligned); + + await renderer.setRoute(route1); + await renderer.setRoute(route2); + + await renderer.setOnLegSelectedListener(null); + + await renderer.setPolyLineColors("#554433", "#667788"); + await renderer.setPolyLineColors("#FF554433", "#00667788"); + + try { + await renderer.setPolyLineColors("Gert", "Berthe"); + fail("Exception not thrown"); + } catch (e) { + expect(e, isInstanceOf()); + } + }); + }); +} diff --git a/mapsindoors_mapbox/example/integration_test/displayrule_test.dart b/mapsindoors_mapbox/example/integration_test/displayrule_test.dart new file mode 100644 index 0000000..1ffe58f --- /dev/null +++ b/mapsindoors_mapbox/example/integration_test/displayrule_test.dart @@ -0,0 +1,424 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mapsindoors_mapbox/mapsindoors.dart'; + +import 'package:mapsindoors_example/main.dart' as app; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('end-to-end test', () { + testWidgets('DisplayRule', (tester) async { + app.main(); + await tester.pumpAndSettle(); + + MPError? loaded = await loadMapsIndoors("mapspeople"); + expect(loaded, null); + + final MPLocation? loc = (await getLocations())?.last; + final MPDisplayRule? rule = await getDisplayRuleByLocation(loc!); + //displayRuleTester(rule1); + + expect(rule, isNot(null)); + + // neede to remove ?. notation + if (rule != null) { + /// IsVisible + { + bool? isVisible = await rule.isVisible(); + expect(isVisible, isNot(null)); + + await rule.setVisible(!isVisible!); + + bool? newIsVisible = await rule.isVisible(); + expect(newIsVisible, isNot(isVisible)); + } + + /// IsIconVisible + { + bool? isVisible = await rule.isIconVisible(); + expect(isVisible, isNot(null)); + + await rule.setIconVisible(!isVisible!); + + bool? newIsVisible = await rule.isIconVisible(); + expect(newIsVisible, isNot(isVisible)); + } + + /// ZoomFrom + { + num? zoomFrom = await rule.getZoomFrom(); + expect(zoomFrom, isNot(null)); + + await rule.setZoomFrom(zoomFrom! - 1.0); + + num? newZoomFrom = await rule.getZoomFrom(); + expect(newZoomFrom, isNot(zoomFrom)); + } + + /// ZoomTo + { + num? zoomTo = await rule.getZoomTo(); + expect(zoomTo, isNot(null)); + + await rule.setZoomTo(zoomTo! + 1.0); + + num? newZoomTo = await rule.getZoomTo(); + expect(newZoomTo, isNot(zoomTo)); + } + + /// IconUrl + { + String? url = await rule.getIconUrl(); + expect(url, isNot(null)); + + await rule.setIcon("${url!}new"); + + String? newUrl = await rule.getIconUrl(); + expect(newUrl, isNot(url)); + } + + /// IconSize + { + MPIconSize? size = await rule.getIconSize(); + expect(size, isNot(null)); + + await rule.setIconSize(MPIconSize(height: size!.height + 1, width: size.width + 1)); + + MPIconSize? newSize = await rule.getIconSize(); + expect(newSize, isNot(size)); + } + + /// IsLabelVisible + { + bool? isVisible = await rule.isLabelVisible(); + expect(isVisible, isNot(null)); + + await rule.setLabelVisible(!isVisible!); + + bool? newIsVisible = await rule.isLabelVisible(); + expect(newIsVisible, isNot(isVisible)); + } + + /// Label + { + String? label = await rule.getLabel(); + expect(label, isNot(null)); + + await rule.setLabel("${label!}new"); + + String? newLabel = await rule.getLabel(); + expect(newLabel, isNot(label)); + } + + /// LabelZoomFrom + { + num? zoomFrom = await rule.getLabelZoomFrom(); + expect(zoomFrom, isNot(null)); + + await rule.setLabelZoomFrom(zoomFrom! - 1.0); + + num? newZoomFrom = await rule.getLabelZoomFrom(); + expect(newZoomFrom, isNot(zoomFrom)); + } + + /// LabelZoomTo + { + num? zoomTo = await rule.getLabelZoomTo(); + expect(zoomTo, isNot(null)); + + await rule.setLabelZoomTo(zoomTo! + 1.0); + + num? newZoomTo = await rule.getLabelZoomTo(); + expect(newZoomTo, isNot(zoomTo)); + } + + /// LabelMaxWidth + { + int? maxWidth = await rule.getLabelMaxWidth(); + expect(maxWidth, isNot(null)); + + await rule.setLabelMaxWidth(maxWidth! + 1); + + int? newMaxWidth = await rule.getLabelMaxWidth(); + expect(newMaxWidth, isNot(maxWidth)); + } + + /// IsPolygonVisible + { + bool? isVisible = await rule.isPolygonVisible(); + expect(isVisible, isNot(null)); + + await rule.setPolygonVisible(!isVisible!); + + bool? newIsVisible = await rule.isPolygonVisible(); + expect(newIsVisible, isNot(isVisible)); + } + + /// PolygonZoomFrom + { + num? zoomFrom = await rule.getPolygonZoomFrom(); + expect(zoomFrom, isNot(null)); + + await rule.setPolygonZoomFrom(zoomFrom! - 1.0); + + num? newZoomFrom = await rule.getPolygonZoomFrom(); + expect(newZoomFrom, isNot(zoomFrom)); + } + + /// PolygonZoomTo + { + num? zoomTo = await rule.getPolygonZoomTo(); + expect(zoomTo, isNot(null)); + + await rule.setPolygonZoomTo(zoomTo! + 1.0); + + num? newZoomTo = await rule.getPolygonZoomTo(); + expect(newZoomTo, isNot(zoomTo)); + } + + /// PolygonStrokeWidth + { + num? strokeWidth = await rule.getPolygonStrokeWidth(); + expect(strokeWidth, isNot(null)); + + await rule.setPolygonStrokeWidth(strokeWidth! + 1.0); + num? newStrokeWidth = await rule.getPolygonStrokeWidth(); + expect(newStrokeWidth, isNot(strokeWidth)); + } + + /// PolygonStrokeColor + { + String? color = await rule.getPolygonStrokeColor(); + expect(color, isNot(null)); + await rule.setPolygonStrokeColor("#3071D3"); + String? newColor = await rule.getPolygonStrokeColor(); + expect(newColor, isNot(color)); + } + + /// PolygonStrokeOpacity + { + num? opacity = await rule.getPolygonStrokeOpacity(); + expect(opacity, isNot(null)); + + await rule.setPolygonStrokeOpacity(opacity! + 0.1); + + num? newOpacity = await rule.getPolygonStrokeOpacity(); + expect(newOpacity, isNot(opacity)); + } + + /// PolygonFillColor + { + String? color = await rule.getPolygonFillColor(); + expect(color, isNot(null)); + + await rule.setPolygonFillColor("#3071D2"); + + String? newColor = await rule.getPolygonFillColor(); + expect(newColor, isNot(color)); + } + + /// PolygonFillOpacity + { + num? opacity = await rule.getPolygonFillOpacity(); + expect(opacity, isNot(null)); + + await rule.setPolygonFillOpacity(opacity! + 0.1); + + num? newOpacity = await rule.getPolygonFillOpacity(); + expect(newOpacity, isNot(opacity)); + } + + /// IsWallVisible + { + bool? isVisible = await rule.isWallVisible(); + expect(isVisible, isNot(null)); + + await rule.setWallVisible(!isVisible!); + + bool? newIsVisible = await rule.isWallVisible(); + expect(newIsVisible, isNot(isVisible)); + } + + /// WallColor + { + String? color = await rule.getWallColor(); + expect(color, isNot(null)); + + await rule.setWallColor("#3071D9"); + + String? newColor = await rule.getWallColor(); + expect(newColor, isNot(color)); + } + + /// WallHeight + { + num? height = await rule.getWallHeight(); + expect(height, isNot(null)); + + await rule.setWallHeight(height! + 0.1); + + num? newHeight = await rule.getWallHeight(); + expect(newHeight, isNot(height)); + } + + /// WallZoomFrom + { + num? zoomFrom = await rule.getWallZoomFrom(); + expect(zoomFrom, isNot(null)); + + await rule.setWallZoomFrom(zoomFrom! - 1.0); + + num? newZoomFrom = await rule.getWallZoomFrom(); + expect(newZoomFrom, isNot(zoomFrom)); + } + + /// WallZoomTo + { + num? zoomTo = await rule.getWallZoomTo(); + expect(zoomTo, isNot(null)); + + await rule.setWallZoomTo(zoomTo! + 1.0); + + num? newZoomTo = await rule.getWallZoomTo(); + expect(newZoomTo, isNot(zoomTo)); + } + + /// IsExtrusionVisible + { + bool? isVisible = await rule.isExtrusionVisible(); + expect(isVisible, isNot(null)); + + await rule.setExtrusionVisible(!isVisible!); + + bool? newIsVisible = await rule.isExtrusionVisible(); + expect(newIsVisible, isNot(isVisible)); + } + + /// ExtrusionColor + { + String? color = await rule.getExtrusionColor(); + expect(color, isNot(null)); + + await rule.setExtrusionColor("#3071D9"); + + String? newColor = await rule.getExtrusionColor(); + expect(newColor, isNot(color)); + } + + /// ExtrusionHeight + { + num? height = await rule.getExtrusionHeight(); + expect(height, isNot(null)); + + await rule.setExtrusionHeight(height! + 0.1); + + num? newHeight = await rule.getExtrusionHeight(); + expect(newHeight, isNot(height)); + } + + /// ExtrusionZoomFrom + { + num? zoomFrom = await rule.getExtrusionZoomFrom(); + expect(zoomFrom, isNot(null)); + + await rule.setExtrusionZoomFrom(zoomFrom! - 1.0); + + num? newZoomFrom = await rule.getExtrusionZoomFrom(); + expect(newZoomFrom, isNot(zoomFrom)); + } + + /// ExtrusionZoomTo + { + num? zoomTo = await rule.getExtrusionZoomTo(); + expect(zoomTo, isNot(null)); + + await rule.setExtrusionZoomTo(zoomTo! + 1.0); + + num? newZoomTo = await rule.getExtrusionZoomTo(); + expect(newZoomTo, isNot(zoomTo)); + } + + /// Is2DModelVisible + { + bool? isVisible = await rule.isModel2DVisible(); + expect(isVisible, isNot(null)); + + await rule.setModel2DVisible(!isVisible!); + + bool? newIsVisible = await rule.isModel2DVisible(); + expect(newIsVisible, isNot(isVisible)); + } + + /// 2DModelZoomFrom + { + num? zoomFrom = await rule.getModel2DZoomFrom(); + expect(zoomFrom, isNot(null)); + + await rule.setModel2DZoomFrom(zoomFrom! - 1.0); + + num? newZoomFrom = await rule.getModel2DZoomFrom(); + expect(newZoomFrom, isNot(zoomFrom)); + } + + /// 2DModelZoomTo + { + num? zoomTo = await rule.getModel2DZoomTo(); + expect(zoomTo, isNot(null)); + + await rule.setModel2DZoomTo(zoomTo! + 1.0); + + num? newZoomTo = await rule.getModel2DZoomTo(); + expect(newZoomTo, isNot(zoomTo)); + } + + /// 2DModelModel + { + String? name = await rule.getModel2DModel(); + expect(name, null); + + await rule.setModel2DModel("new"); + + String? newName = await rule.getModel2DModel(); + expect(newName, isNot(name)); + } + + /// 2DModelHeight + { + num? height = await rule.getModel2DHeightMeters(); + expect(height, isNot(null)); + + await rule.setModel2DHeightMeters(height! + 1.0); + + num? newHeight = await rule.getModel2DHeightMeters(); + expect(newHeight, isNot(height)); + } + + /// 2DModelWidth + { + num? width = await rule.getModel2DWidthMeters(); + expect(width, isNot(null)); + + await rule.setModel2DWidthMeters(width! + 1.0); + + num? newWidth = await rule.getModel2DWidthMeters(); + expect(newWidth, isNot(width)); + } + + /// 2DModelBearing + { + num? bearing = await rule.getModel2DBearing(); + expect(bearing, isNot(null)); + + await rule.setModel2DBearing(bearing! + 1.0); + + num? newBearing = await rule.getModel2DBearing(); + expect(newBearing, isNot(bearing)); + } + + // reset the rule after validating again + await rule.reset(); + } + }); + }); +} diff --git a/mapsindoors_mapbox/example/integration_test/mapcontrol_test.dart b/mapsindoors_mapbox/example/integration_test/mapcontrol_test.dart new file mode 100644 index 0000000..063789a --- /dev/null +++ b/mapsindoors_mapbox/example/integration_test/mapcontrol_test.dart @@ -0,0 +1,366 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mapsindoors_mapbox/mapsindoors.dart'; + +import 'package:mapsindoors_example/main.dart' as app; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('end-to-end test', () { + testWidgets('MapControl', (tester) async { + app.main(); + await tester.pumpAndSettle(); + final Finder fab = find.byType(ElevatedButton); + await tester.pumpAndSettle(const Duration(seconds: 1)); + // Emulate a tap on the floating action button. + await tester.tap(fab); + + // Trigger a frame. + await tester.pumpAndSettle(const Duration(seconds: 4)); + + var ready = false; + while (ready == false) { + await tester.pumpAndSettle(const Duration(seconds: 1)); + ready = await isMapsIndoorsReady() ?? false; + } + + final app.MapWidgetState state = tester.state(find.byType(app.MapWidget)); + + MapsIndoorsWidget mapControl = state.mapController; + + expect(mapControl, isNot(null)); + + /// Get venue from mapsindoors and move towards it + /// + /// expected get current venue to return the venue + { + MPVenue? fetchedVenue = (await getVenues())?.getAll().first; + + expect(fetchedVenue, isNot(null)); + + await mapControl.selectVenue(fetchedVenue!, true); + + MPVenue? venue = await mapControl.getCurrentVenue(); + + expect(venue, equals(fetchedVenue)); + } + + /// Get building from mapsindoors and move towards it + /// + /// expected get current building to return the building + { + MPBuilding? fetchedBuilding = (await getBuildings())?.getAll().first; + + expect(fetchedBuilding, isNot(null)); + + mapControl.selectBuilding(fetchedBuilding!, true); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + MPBuilding? building = await mapControl.getCurrentBuilding(); + + expect(building, equals(fetchedBuilding)); + + // we know there is a building with a floor here, check that now + MPFloor? floor = await mapControl.getCurrentBuildingFloor(); + + expect(floor?.floorIndex, building?.initialFloorIndex); + } + + /// Set map padding, then get it again + /// + /// expected get returns padding + { + expect(mapControl.mapViewPaddingBottom, isNot(99)); + + await mapControl.setMapPadding(55, 66, 77, 99); + + expect(await mapControl.mapViewPaddingStart, equals(55)); + + expect(await mapControl.mapViewPaddingTop, equals(66)); + + expect(await mapControl.mapViewPaddingEnd, equals(77)); + + expect(await mapControl.mapViewPaddingBottom, equals(99)); + } + + /// get map style + /// + /// expected: mapstyle on mapcontrol is contained in mapsindoors + { + List? styles = await getMapStyles(); + + expect(styles, isNot(null)); + expect(styles?.length, isNot(0)); + + MPMapStyle? currentStyle = await mapControl.mapStyle; + + expect(currentStyle, isNot(null)); + + expect(styles?.contains(currentStyle), equals(true)); + + // Test is conditioned on more than 1 style for the current solution + if (styles!.length > 1) { + styles.remove(currentStyle); + + MPMapStyle? newStyle = await mapControl.mapStyle; + + expect(newStyle, isNot(currentStyle)); + } + } + + /// is floor selector hidden + /// + /// expected: not hidden, then hidden + { + bool? isFloorSelectorHidden = await mapControl.isFloorSelectorHidden; + + expect(isFloorSelectorHidden, false); + + await mapControl.hideFloorSelector(true); + + isFloorSelectorHidden = await mapControl.isFloorSelectorHidden; + + expect(isFloorSelectorHidden, true); + } + + /// Filter + /// + /// expected: nothing, test is to ensure the platform does not crash + { + // check that it does not crash when no filter is in use + await mapControl.clearFilter(); + + // filter with all values filled + MPFilter filterFilled = (MPFilterBuilder() + ..setCategories(["one", "two"]) + ..setDepth(2) + ..setFloorIndex(10) + ..setGeometry(MPBounds(northeast: MPPoint.withCoordinates(longitude: 89.0, latitude: 90.0), southwest: MPPoint.withCoordinates(longitude: 1.0, latitude: 2.0))) + ..setIgnoreLocationActiveStatus(true) + ..setIgnoreLocationSearchableStatus(false) + ..setLocations(["location"]) + ..setParents(["venue"]) + ..setSkip(20) + ..setTake(3) + ..setMapExtend(MPBounds(northeast: MPPoint.withCoordinates(longitude: 56.55467, latitude: 91.2345234), southwest: MPPoint.withCoordinates(longitude: -45.213342634, latitude: -20.4234)))) + .build(); + + // filter with some values filled + MPFilter filterSemi = (MPFilterBuilder() + ..setDepth(2) + ..setFloorIndex(10) + ..setMapExtend(MPBounds(northeast: MPPoint.withCoordinates(longitude: 56.55467, latitude: 91.2345234), southwest: MPPoint.withCoordinates(longitude: -45.213342634, latitude: -20.4234)))) + .build(); + + // filter with no values filled + MPFilter filterNone = MPFilterBuilder().build(); + + // behavior with values filled + MPFilterBehavior behavior = (MPFilterBehavior.builder() + ..setAllowFloorChange(false) + ..setAnimationDuration(20000) + ..setMoveCamera(true) + ..setShowInfoWindow(false) + ..setZoomToFit(false)) + .build(); + + await mapControl.setFilter(filterFilled, behavior); + await mapControl.setFilter(filterSemi, behavior); + await mapControl.clearFilter(); + await mapControl.setFilter(filterNone, behavior); + + await mapControl.clearFilter(); + + await mapControl.setFilter(filterFilled, MPFilterBehavior.DEFAULT); + await mapControl.setFilter(filterSemi, MPFilterBehavior.DEFAULT); + await mapControl.clearFilter(); + await mapControl.setFilter(filterNone, MPFilterBehavior.DEFAULT); + + // cleanup + await mapControl.clearFilter(); + + // Check the other setFilter + List? locations = (await getLocations())?.take(5).toList(); + + await mapControl.setFilterWithLocations(locations!, behavior); + await mapControl.setFilterWithLocations(locations, MPFilterBehavior.DEFAULT); + await mapControl.clearFilter(); + + await mapControl.setFilterWithLocations([], behavior); + await mapControl.clearFilter(); + } + + { + // Reset the listener as it might cause havoc in the app otherwise + mapControl.setOnLocationSelectedListener(null, true); + + MPLocation? location = (await getLocations())?.first; + + await mapControl.selectLocation(location); + await mapControl.deSelectLocation(); + await mapControl.selectLocation(location); + await mapControl.selectLocation(null); + + MPSelectionBehavior behavior = (MPSelectionBehavior.builder() + ..setAllowFloorChange(false) + ..setAnimationDuration(0) + ..setMoveCamera(false) + ..setZoomToFit(true) + ..setShowInfoWindow(true)) + .build(); + + await mapControl.selectLocation(location, behavior); + await mapControl.selectLocation(null, behavior); + + await mapControl.deSelectLocation(); + + await mapControl.selectLocation(location, MPSelectionBehavior.DEFAULT); + await mapControl.selectLocation(null, MPSelectionBehavior.DEFAULT); + + await mapControl.deSelectLocation(); + } + + /// goto + { + MPVenue? venue = (await getVenues())?.getAll().last; + MPBuilding? building = (await getBuildings())?.getAll().last; + MPFloor? floor = building?.floors.last; + MPLocation? location = (await getLocations())?.last; + + await mapControl.goTo(venue); + await mapControl.goTo(building); + await mapControl.goTo(location); + await mapControl.goTo(null); + // This is temporary until iOS has implemented floor as an entity + if (Platform.isAndroid) { + await mapControl.goTo(floor); + } + } + + /// getcurrentzoom + { + final zoom = await mapControl.getCurrentMapsindoorsZoom(); + + expect(zoom, isNot(null)); + } + + /// user position + { + bool? isPositionShown = await mapControl.isUserPositionShown; + + expect(isPositionShown, true); + + await mapControl.setShowUserPosition(false); + + isPositionShown = await mapControl.isUserPositionShown; + + expect(isPositionShown, false); + } + + /// livedata + { + // disable something that has not been set + mapControl.disableLiveData(LiveDataDomainTypes.count.name); + // enable all + mapControl.enableLiveData(LiveDataDomainTypes.any.name); + // enable one + mapControl.enableLiveData(LiveDataDomainTypes.availability.name); + // remove the one + mapControl.disableLiveData(LiveDataDomainTypes.availability.name); + // remove all + mapControl.disableLiveData(LiveDataDomainTypes.any.name); + } + + // listeners + { + // check case where listener has not been added yet + mapControl.removeOnCameraEventListner((event) => {}); + + mapControl.addOnCameraEventListner((event) => {}); + mapControl.removeOnCameraEventListner((event) => {}); + + mapControl.addOnFloorUpdateListener((floor) => {}); + mapControl.removeOnFloorUpdateListener((floor) => {}); + + mapControl.setOnCurrentVenueChangedListener((venue) => {}); + + mapControl.setOnCurrentBuildingChangedListener((building) => {}); + + // check in case the initial value has not been set + mapControl.setOnLocationSelectedListener((location) => {}, true); + mapControl.setOnLocationSelectedListener((locaiton) => {}, true); + + mapControl.setOnMapClickListener((point, locations) => {}, true); + mapControl.setOnMapClickListener(null, true); + + mapControl.setOnMarkerClickListener((locId) => {}, true); + mapControl.setOnMarkerClickListener(null, true); + + mapControl.setOnMarkerInfoWindowClickListener((locId) => {}); + mapControl.setOnMarkerInfoWindowClickListener(null); + } + + /// Camera mainpulation + { + final MPCameraPosition originalPosition = await mapControl.getCurrentCameraPosition(); + final MPCameraPosition copyPosition = (MPCameraPositionBuilder.fromPosition(originalPosition)..tilt = 20).build(); + + expect(originalPosition.bearing, copyPosition.bearing); + expect(originalPosition.zoom, copyPosition.zoom); + expect(originalPosition.tilt, isNot(copyPosition.tilt)); + + MPCameraPosition position = MPCameraPosition(zoom: 20, target: MPPoint.withCoordinates(longitude: 0, latitude: 0)); + + await mapControl.moveCamera(MPCameraUpdate.fromCameraPosition(position)); + + MPCameraUpdate update = MPCameraUpdate.fromPoint(MPPoint.withCoordinates(longitude: 10, latitude: 10)); + + await mapControl.moveCamera(update); + + update = MPCameraUpdate.fromPoint(MPPoint.withCoordinates(longitude: 1000, latitude: -1000)); + + await mapControl.moveCamera(update); + + update = MPCameraUpdate.fromBounds(bounds: MPBounds(northeast: MPPoint.withCoordinates(longitude: 20, latitude: 20), southwest: MPPoint.withCoordinates(longitude: -20, latitude: -20)), padding: 200); + + await mapControl.moveCamera(update); + + update = MPCameraUpdate.zoomBy(2); + + await mapControl.moveCamera(update); + + update = MPCameraUpdate.zoomBy(-2); + + await mapControl.moveCamera(update); + + update = MPCameraUpdate.zoomBy(2000); + + update = MPCameraUpdate.zoomTo(1); + + await mapControl.moveCamera(update); + + update = MPCameraUpdate.zoomTo(22); + + await mapControl.moveCamera(update); + + update = MPCameraUpdate.zoomTo(2000); + + await mapControl.moveCamera(update); + + update = MPCameraUpdate.zoomTo(-1); + + await mapControl.moveCamera(update); + + await mapControl.animateCamera(MPCameraUpdate.fromCameraPosition(originalPosition), 20000); + + await mapControl.animateCamera(MPCameraUpdate.fromCameraPosition(position)); + + await mapControl.animateCamera(MPCameraUpdate.fromCameraPosition(originalPosition), 100); + } + }); + }); +} diff --git a/mapsindoors_mapbox/example/integration_test/mapsindoors_test.dart b/mapsindoors_mapbox/example/integration_test/mapsindoors_test.dart new file mode 100644 index 0000000..c2d7439 --- /dev/null +++ b/mapsindoors_mapbox/example/integration_test/mapsindoors_test.dart @@ -0,0 +1,127 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mapsindoors_mapbox/mapsindoors.dart'; +import 'package:mapsindoors_example/example_position_provider.dart'; + +import 'package:mapsindoors_example/main.dart' as app; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('end-to-end test', () { + testWidgets('MapsIndoors', (tester) async { + app.main(); + await tester.pumpAndSettle(); + + MPError? loaded = await loadMapsIndoors("mapspeople"); + expect(loaded, null); + + MPDisplayRule? defaultDisplayRule = await getDefaultDisplayRule(); + expect(defaultDisplayRule?.dispalyRuleName, "default"); + + MPDisplayRule? mainDisplayRule = await getMainDisplayRule(); + expect(mainDisplayRule?.dispalyRuleName, "main"); + + MPDisplayRule? mainByName = await getDisplayRuleByName("main"); + expect(mainByName?.dispalyRuleName, "main"); + + String? apiKey = await getAPIKey(); + expect(apiKey, "mapspeople"); + + List? languages = await getMapsIndoorsAvailableLanguages(); + expect(languages?.length, 3); + + MPBuildingCollection? buildings = await getBuildings(); + expect(buildings?.size, 5); + + MPVenueCollection? venues = await getVenues(); + expect(venues?.size, 5); + + MPVenue? defaultVenue = await getDefaultVenue(); + expect(defaultVenue, isNotNull); + expect(defaultVenue?.administrativeId, "Stigsborgvej"); + + MPCategoryCollection? categories = await getCategories(); + expect(categories?.size, 18); + + String? language = await getMapsIndoorsLanguage(); + expect(language, "en"); + + MPSolution? solution = await getSolution(); + expect(solution?.availableLanguages.length, 3); + + MPLocation? location = await getLocationById("296994c98023439eac1f20d4"); + expect(location?.name, "Canteen"); + + List? locations = await getLocations(); + expect(locations?.isNotEmpty, true); + + List externalIds = ["0.32.05"]; + List? externalIdLocations = await getLocationsByExternalIds(externalIds); + + expect(externalIdLocations?.length, 1); + expect(externalIdLocations?[0].name, "Canteen"); + + MPQuery query = MPQuery.builder().build(); + MPFilter filter = (MPFilterBuilder()..setTypes(["Canteen"])).build(); + List? locationsByFilter = await getLocationsByQuery(query: query, filter: filter); + expect(locationsByFilter?.isNotEmpty, true); + + query = (MPQuery.builder()..setQuery("apisjgaelgasdf asdoæfk oæskg æaskf nasdlfnasue")).build(); + filter = MPFilterBuilder().build(); + locationsByFilter = await getLocationsByQuery(query: query, filter: filter); + expect(locationsByFilter?.isNotEmpty, false); + + List? mapstyles = await getMapStyles(); + expect(mapstyles?.length, 1); + expect(mapstyles?[0].displayName, "default"); + + ExamplePositionProvider posProv = ExamplePositionProvider(); + setPositionProvider(posProv); + expect(getPositionProvider()?.name, "random"); + + bool? validApi = await isAPIKeyValid(); + expect(validApi, true); + + bool? ready = await isMapsIndoorsReady(); + expect(ready, true); + + MPUserRoleCollection? userRoles = await getUserRoles(); + expect(userRoles?.size != 0, true); + + applyUserRoles(userRoles!.getAll()); + + var currAvailableUseRoles = await getAppliedUserRoles(); + expect(currAvailableUseRoles?.length, userRoles.size); + + applyUserRoles([]); + + currAvailableUseRoles = await getAppliedUserRoles(); + expect(currAvailableUseRoles?.length, 0); + + // This is temporary until iOS has implemented ReverseGeoCode feature + if (Platform.isAndroid) { + MPGeocodeResult? result = await reverseGeoCode(MPPoint.withCoordinates(longitude: 9.9508396, latitude: 57.0582701, floorIndex: 0)); + expect(result?.rooms.length, 1); + expect(result?.floors[0].floorIndex, 0); + } + + await setMapsIndoorsLanguage("da"); + expect(await getMapsIndoorsLanguage(), "da"); + + /*// Finds the floating action button to tap on. + final Finder fab = find.byTooltip('Increment'); + + // Emulate a tap on the floating action button. + await tester.tap(fab); + + // Trigger a frame. + await tester.pumpAndSettle(); + + // Verify the counter increments by 1. + expect(find.text('1'), findsOneWidget);*/ + }); + }); +} diff --git a/mapsindoors_mapbox/example/ios/.gitignore b/mapsindoors_mapbox/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/mapsindoors_mapbox/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.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/ephemeral/ +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/mapsindoors_mapbox/example/ios/Flutter/AppFrameworkInfo.plist b/mapsindoors_mapbox/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/mapsindoors_mapbox/example/ios/Flutter/Debug.xcconfig b/mapsindoors_mapbox/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/mapsindoors_mapbox/example/ios/Flutter/LICENSE b/mapsindoors_mapbox/example/ios/Flutter/LICENSE new file mode 100644 index 0000000..bd5d433 --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Flutter/LICENSE @@ -0,0 +1,11 @@ +Copyright 2022 Mapspeople + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/mapsindoors_mapbox/example/ios/Flutter/Release.xcconfig b/mapsindoors_mapbox/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/mapsindoors_mapbox/example/ios/LICENSE b/mapsindoors_mapbox/example/ios/LICENSE new file mode 100644 index 0000000..bd5d433 --- /dev/null +++ b/mapsindoors_mapbox/example/ios/LICENSE @@ -0,0 +1,11 @@ +Copyright 2022 Mapspeople + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/mapsindoors_mapbox/example/ios/Podfile b/mapsindoors_mapbox/example/ios/Podfile new file mode 100644 index 0000000..2a775d0 --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Podfile @@ -0,0 +1,96 @@ +# 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_ROOT_DIR = File.dirname(File.expand_path(__FILE__)) +PODS_DIR = File.join(PROJECT_ROOT_DIR, 'Pods') +PODS_TARGET_SUPPORT_FILES_DIR = File.join(PODS_DIR, 'Target Support Files') + +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) + end + remove_static_framework_duplicate_linkage({ + 'MapsIndoorsGoogleMaps' => ['GoogleMaps'] + }) +end + +# CocoaPods provides the abstract_target mechanism for sharing dependencies between distinct targets. +# However, due to the complexity of our project and use of shared frameworks, we cannot simply bundle everything under +# a single abstract_target. Using a pod in a shared framework target and an app target will cause CocoaPods to generate +# a build configuration that links the pod's frameworks with both targets. This is not an issue with dynamic frameworks, +# as the linker is smart enough to avoid duplicate linkage at runtime. Yet for static frameworks the linkage happens at +# build time, thus when the shared framework target and app target are combined to form an executable, the static +# framework will reside within multiple distinct address spaces. The end result is duplicated symbols, and global +# variables that are confined to each target's address space, i.e not truly global within the app's address space. + +def remove_static_framework_duplicate_linkage(static_framework_pods) + puts "Removing duplicate linkage of static frameworks" + + Dir.glob(File.join(PODS_TARGET_SUPPORT_FILES_DIR, "Pods-*")).each do |path| + pod_target = path.split('-', -1).last + + static_framework_pods.each do |target, pods| + next if pod_target == target + frameworks = pods.map { |pod| identify_frameworks(pod) }.flatten + + Dir.glob(File.join(path, "*.xcconfig")).each do |xcconfig| + lines = File.readlines(xcconfig) + + if other_ldflags_index = lines.find_index { |l| l.start_with?('OTHER_LDFLAGS') } + other_ldflags = lines[other_ldflags_index] + + frameworks.each do |framework| + other_ldflags.gsub!("-framework \"#{framework}\"", '') + end + + File.open(xcconfig, 'w') do |fd| + fd.write(lines.join) + end + end + end + end + end +end + +def identify_frameworks(pod) + frameworks = Dir.glob(File.join(PODS_DIR, pod, "**/*.framework")).map { |path| File.basename(path) } + + if frameworks.any? + return frameworks.map { |f| f.split('.framework').first } + end + + return pod +end diff --git a/mapsindoors_mapbox/example/ios/Podfile.lock b/mapsindoors_mapbox/example/ios/Podfile.lock new file mode 100644 index 0000000..c7774ad --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Podfile.lock @@ -0,0 +1,60 @@ +PODS: + - Flutter (1.0.0) + - GoogleMaps (7.4.0): + - GoogleMaps/Maps (= 7.4.0) + - GoogleMaps/Base (7.4.0) + - GoogleMaps/Maps (7.4.0): + - GoogleMaps/Base + - integration_test (0.0.1): + - Flutter + - MapsIndoors (4.0.2) + - mapsindoors_ios (4.0.0): + - Flutter + - MapsIndoorsCodable (= 4.0.2) + - MapsIndoorsGoogleMaps (= 4.0.2) + - MapsIndoorsCodable (4.0.2): + - MapsIndoors (= 4.0.2) + - MapsIndoorsCore (4.0.2): + - MapsIndoors (= 4.0.2) + - MapsIndoorsGoogleMaps (4.0.2): + - GoogleMaps (= 7.4.0) + - MapsIndoorsCore (= 4.0.2) + - ValueAnimator (= 0.6.8) + - ValueAnimator (0.6.8) + +DEPENDENCIES: + - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - mapsindoors_ios (from `.symlinks/plugins/mapsindoors_ios/ios`) + +SPEC REPOS: + trunk: + - GoogleMaps + - MapsIndoors + - MapsIndoorsCodable + - MapsIndoorsCore + - MapsIndoorsGoogleMaps + - ValueAnimator + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + mapsindoors_ios: + :path: ".symlinks/plugins/mapsindoors_ios/ios" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + GoogleMaps: 032f676450ba0779bd8ce16840690915f84e57ac + integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 + MapsIndoors: be722b256ba5421dd8b59ffde46c1caab00f53bd + mapsindoors_ios: f1494ea5bd0d614c6577fbc402ab0016cfbb327b + MapsIndoorsCodable: e51c6466bdf7cf2424b579a109a52c4d025aa3ca + MapsIndoorsCore: 6cc77f328e490c8740fe14672248f29ea2008d8b + MapsIndoorsGoogleMaps: 2c7a4c1680d1e2ab0aa3835684770addb26bac99 + ValueAnimator: b9ed721f3a0fc682ec857fd70dc933b28adb7522 + +PODFILE CHECKSUM: 209ba4354751c0eef9328209d91014d33cc744ce + +COCOAPODS: 1.12.0 diff --git a/mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.pbxproj b/mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..4067c21 --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,584 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + 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 */; }; + 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 */; }; + E36F7E3503C3B6AE35189179 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC9F0B885B5C56D6CA0C6CF /* Pods_Runner.framework */; }; +/* 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 */ + 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 = ""; }; + 3027ECFAE896FEEEC0F6CD8A /* 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 = ""; }; + 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 = ""; }; + 9FC9F0B885B5C56D6CA0C6CF /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A02B7017D7E624D6B5405428 /* 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 = ""; }; + BE9452CB1C4C6889CBE3ECC3 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E36F7E3503C3B6AE35189179 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9083620D5706E54EE7785CDD /* Pods */ = { + isa = PBXGroup; + children = ( + 3027ECFAE896FEEEC0F6CD8A /* Pods-Runner.debug.xcconfig */, + BE9452CB1C4C6889CBE3ECC3 /* Pods-Runner.release.xcconfig */, + A02B7017D7E624D6B5405428 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 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 */, + 9083620D5706E54EE7785CDD /* Pods */, + D4B20F0E04BAEEF3C52CF77D /* 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 = ""; + }; + D4B20F0E04BAEEF3C52CF77D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 9FC9F0B885B5C56D6CA0C6CF /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9DF5B8538A38F6D3937E37A9 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 7EB7F5E9E76A9CC4BA989ED4 /* [CP] Embed Pods Frameworks */, + 233BE1C656F0273903C8424D /* [CP] Copy Pods Resources */, + ); + 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 = 1300; + 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 */ + 233BE1C656F0273903C8424D /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + 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"; + }; + 7EB7F5E9E76A9CC4BA989ED4 /* [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; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + 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"; + }; + 9DF5B8538A38F6D3937E37A9 /* [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; + }; +/* 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; + 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 = 13.0; + 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; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = A8V6656PYZ; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.mapspeople.mapsindoorsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + 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; + 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 = 13.0; + 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; + 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 = 13.0; + 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 = { + "ARCHS[sdk=iphonesimulator*]" = x86_64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = A8V6656PYZ; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.mapspeople.mapsindoorsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + 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; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = A8V6656PYZ; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.mapspeople.mapsindoorsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + 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/mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/mapsindoors_mapbox/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mapsindoors_mapbox/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mapsindoors_mapbox/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/mapsindoors_mapbox/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/mapsindoors_mapbox/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mapsindoors_mapbox/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/mapsindoors_mapbox/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mapsindoors_mapbox/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/mapsindoors_mapbox/example/ios/Runner/AppDelegate.swift b/mapsindoors_mapbox/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..27830f9 --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,15 @@ +import UIKit +import Flutter +import GoogleMaps + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GMSServices.provideAPIKey("insert api key here") + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/mapsindoors_mapbox/example/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/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f091b6b0bca859a3f474b03065bef75ba58a9e4c GIT binary patch literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ef06e7edb86cdfe0d15b4b0d98334a86163658 GIT binary patch literal 1716 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 literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f9ed8f5cee1c98386d13b17e89f719e83555b2 GIT binary patch literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..75b2d164a5a98e212cca15ea7bf2ab5de5108680 GIT binary patch literal 3831 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 literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..c4df70d39da7941ef3f6dcb7f06a192d8dcb308d GIT binary patch literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/mapsindoors_mapbox/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/mapsindoors_mapbox/example/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/mapsindoors_mapbox/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/mapsindoors_mapbox/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mapsindoors_mapbox/example/ios/Runner/Base.lproj/Main.storyboard b/mapsindoors_mapbox/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mapsindoors_mapbox/example/ios/Runner/Info.plist b/mapsindoors_mapbox/example/ios/Runner/Info.plist new file mode 100644 index 0000000..ac8765b --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Mapsindoors + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + mapsindoors_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/mapsindoors_mapbox/example/ios/Runner/Runner-Bridging-Header.h b/mapsindoors_mapbox/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/mapsindoors_mapbox/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/mapsindoors_mapbox/example/lib/example_position_provider.dart b/mapsindoors_mapbox/example/lib/example_position_provider.dart new file mode 100644 index 0000000..6a2b090 --- /dev/null +++ b/mapsindoors_mapbox/example/lib/example_position_provider.dart @@ -0,0 +1,58 @@ +import 'package:mapsindoors_mapbox/mapsindoors.dart'; + +class ExamplePositionProvider extends MPPositionProviderInterface { + final List _listeners = List.empty(growable: true); + ExamplePositionResult? _latestPosition; + ExamplePositionProvider(); + + @override + void addOnPositionUpdateListener(OnPositionUpdateListener listener) { + _listeners.add(listener); + } + + @override + MPPositionResultInterface? get latestPosition { + return _latestPosition; + } + + @override + String get name => "random"; + + @override + void removeOnPositionUpdateListener(OnPositionUpdateListener listener) { + _listeners.remove(listener); + } + + void updatePosition(MPPoint pos, {num? accuracy, num? bearing, int? floorIndex}) { + _latestPosition = ExamplePositionResult(this, point: pos, accuracy: accuracy, bearing: bearing, floorIndex: floorIndex); + for (final listener in _listeners) { + listener(_latestPosition!); + } + } + + void clearPosition() { + _latestPosition = ExamplePositionResult(this); + for (final listener in _listeners) { + listener(_latestPosition!); + } + } +} + +class ExamplePositionResult extends MPPositionResultInterface { + ExamplePositionResult(this.provider, {this.accuracy, this.bearing, this.floorIndex, this.point}); + + @override + num? accuracy; + + @override + num? bearing; + + @override + int? floorIndex; + + @override + MPPoint? point; + + @override + MPPositionProviderInterface provider; +} diff --git a/mapsindoors_mapbox/example/lib/main.dart b/mapsindoors_mapbox/example/lib/main.dart new file mode 100644 index 0000000..389e396 --- /dev/null +++ b/mapsindoors_mapbox/example/lib/main.dart @@ -0,0 +1,792 @@ +// ignore_for_file: avoid_print + +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'dart:async'; +import 'package:mapsindoors_mapbox/mapsindoors.dart'; +import 'example_position_provider.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: SolutionSelector(), + ); + } +} + +class SolutionSelector extends StatefulWidget { + const SolutionSelector({super.key}); + + @override + State createState() => SolutionSelectorState(); +} + +class SolutionSelectorState extends State { + final myController = TextEditingController(); + + @override + Widget build(BuildContext context) { + myController.text = "mapspeople"; + return Scaffold( + body: Container( + decoration: const BoxDecoration(color: Colors.white), + child: Center( + child: TextField( + decoration: const InputDecoration(hintText: "Enter solution id"), + controller: myController, + autofocus: true, + autocorrect: false, + onSubmitted: _loadSolution, + ), + ), + ), + floatingActionButton: ElevatedButton( + onPressed: () => _loadSolution(myController.text), + child: const Text("load"), + ), + ); + } + + Future _loadSolution(String solutionId) async { + Navigator.of(context).push(MaterialPageRoute(builder: (context) => MapWidget(apiKey: solutionId.trim()))); + } +} + +class MapWidget extends StatefulWidget { + final String apiKey; + + const MapWidget({Key? key, required this.apiKey}) : super(key: key); + + @override + MapWidgetState createState() => MapWidgetState(); +} + +class MapWidgetState extends State { + MPBuilding? currBuilding; + final _positionProvider = ExamplePositionProvider(); + String? selectedVenueId; + + final MPPoint _userPosition = MPPoint.withCoordinates(longitude: 9.94855865542619, latitude: 57.05840589392954, floorIndex: 0); + var _showRemoveRouteButton = false; + MPDirectionsService? _directionsService; + MPDirectionsRenderer? _directionsRenderer; + PersistentBottomSheetController? directionsController; + late MapsIndoorsWidget mapController; + + List? categoryList = []; + final searchTextController = TextEditingController(); + + @override + void initState() { + super.initState(); + loadMapsIndoors(widget.apiKey); + } + + void _mapsIndoorsReadyListener(MPError? error) async { + if (error == null) { + mapController.goTo(await getDefaultVenue()); + /*mapController.enableLiveData(LiveDataDomainTypes.availability.name); + mapController.enableLiveData(LiveDataDomainTypes.occupancy.name); + _positionProvider.updatePosition(_userPosition, + floorIndex: 0, bearing: 76.2, accuracy: 5.0); + mapController.setFloorSelector(_floorSelectorWidget);*/ + mapController.setOnMarkerInfoWindowClickListener(onInfoWindowClick); + mapController.setOnLocationSelectedListener(onLocationSelected, false); + mapController.setOnMarkerClickListener(onMarkerClick, false); + mapController.setOnCurrentVenueChangedListener(onVenueFoundAtCameraTarget); + mapController.setOnCurrentBuildingChangedListener(onBuildingFoundAtCameraTarget); + mapController.addOnFloorUpdateListener(onFloorUpdate); + setPositionProvider(_positionProvider); + } + } + + final scaffoldKey = GlobalKey(); + PersistentBottomSheetController? controller; + List menuItems = []; + + @override + Widget build(BuildContext context) { + AppBar appBar = AppBar( + title: const Text('MapsIndoors Test App'), + leading: IconButton( + icon: const Icon(Icons.menu), + onPressed: () async { + if (menuItems.isEmpty) { + getCategories(); + } + scaffoldKey.currentState?.openDrawer(); + })); + return Scaffold( + key: scaffoldKey, + appBar: appBar, + floatingActionButton: Visibility( + visible: _showRemoveRouteButton, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: const Color.fromARGB(255, 250, 100, 0)), + onPressed: _removeRoute, + child: const Row( + children: [ + Icon(Icons.close), + SizedBox( + width: 5, + ), + Text("Clear Route") + ], + ), + ), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + drawer: Drawer( + child: Column( + children: [ + Container( + color: Colors.blueGrey, + child: DrawerHeader( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + children: [ + IconButton( + onPressed: openVenueDialog, + icon: const Icon( + Icons.menu_outlined, + color: Colors.black, + )), + Title(color: Colors.black, child: const Text("MapsIndoors")), + ], + ), + const Padding(padding: EdgeInsets.fromLTRB(0, 0, 0, 20)), + TextField( + controller: searchTextController, + onChanged: ((value) { + _search(); + }), + decoration: InputDecoration( + filled: true, + fillColor: Colors.white70, + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: (() { + searchTextController.clear(); + FocusScope.of(context).unfocus(); + getCategories(); + }), + icon: const Icon(Icons.clear)), + hintText: 'Search', + ), + ), + ], + ))), + Expanded( + child: ListView.builder( + itemBuilder: (context, index) { + return ListTile( + onTap: () => {doMenuItemAction(menuItems[index])}, + title: Text(getTitle(menuItems[index])), + ); + }, + itemCount: menuItems.length)) + ], + )), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + mapController = MapsIndoorsWidget( + showUserPosition: true, + readyListener: _mapsIndoorsReadyListener, + ), + ], + ), + ), + ); + } + + void closeBottomSheet() { + controller?.close(); + } + + Future getMenuCategories() async { + final temp = await getCategories(); + if (temp != null) { + menuItems.clear(); + menuItems.addAll(temp.getAll()); + setState(() {}); + } + } + + Future _search() async { + if (searchTextController.text.isEmpty) { + getMenuCategories(); + return; + } + var query = (MPQuery.builder()..setQuery(searchTextController.text)).build(); + var filterBuilder = MPFilter.builder(); + if (selectedVenueId != null) { + filterBuilder.setParents([selectedVenueId!]); + filterBuilder.setDepth(4); + } + var filter = filterBuilder.build(); + + final searchResults = await getLocationsByQuery(query: query, filter: filter); + if (searchResults != null) { + menuItems.clear(); + menuItems.addAll(searchResults); + setState(() {}); + } + } + + Future _selectVenue(MPVenue venue) async { + mapController.selectVenue(venue, true); + selectedVenueId = venue.id.value; + } + + Future openVenueDialog() async { + final venues = await getVenues(); + var options = []; + if (venues != null) { + for (var element in venues.getAll()) { + options.add(SimpleDialogOption( + onPressed: () => {_selectVenue(element), Navigator.pop(context)}, + child: Text(element.name!), + )); + } + + // ignore: use_build_context_synchronously + if (!context.mounted) return; + + showDialog( + context: context, + builder: (context) { + return SimpleDialog( + title: const Text("Select Venue"), + children: options, + ); + }); + } + } + + String getTitle(dynamic menuItem) { + if (menuItem is MPCategory) { + return menuItem.value; + } else if (menuItem is MPLocation) { + return menuItem.name; + } + return ""; + } + + void doMenuItemAction(dynamic menuItem) async { + if (menuItem is MPCategory) { + final query = MPQuery.builder().build(); + final filterBuilder = MPFilter.builder()..setCategories([menuItem.key]); + if (selectedVenueId != null) { + filterBuilder.setParents([selectedVenueId!]); + filterBuilder.setDepth(4); + } + List? locs = await getLocationsByQuery(query: query, filter: filterBuilder.build()); + if (locs != null) { + menuItems.clear(); + menuItems.addAll(locs); + setState(() {}); + } + } else if (menuItem is MPLocation) { + mapController.selectLocation(menuItem); + scaffoldKey.currentState?.closeDrawer(); + } + } + + void onLocationSelected(MPLocation? location) { + var description = location?.description; + var type = location?.typeName; + var building = location?.buildingName; + var floor = location?.floorName; + scaffoldKey.currentState!.showBottomSheet((context) { + return Container( + height: MediaQuery.of(context).size.height * 0.35, + width: MediaQuery.of(context).size.width, + color: Colors.white, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 30), + Text( + 'Description: $description', + ), + const SizedBox(height: 10), + Text( + 'type: $type', + ), + const SizedBox(height: 10), + Text( + '$building - $floor', + ), + const SizedBox(height: 30), + ElevatedButton( + onPressed: () => _routeStuff(location), + style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20), maximumSize: const Size(150, 40)), + child: const Row( + children: [ + Icon(Icons.keyboard_arrow_left_rounded), + SizedBox( + width: 5, + ), + Text("directions") + ], + )), + ElevatedButton( + onPressed: () => _editDisplayRule(location), + child: const Row( + children: [ + Icon(Icons.colorize), + SizedBox( + width: 5, + ), + Text("Display Rule") + ], + )) + ], + ), + ); + }); + } + + void _editDisplayRule(MPLocation? location) async { + if (location == null) { + return; + } + + final displayRule = await getDisplayRuleByLocation(location); + if (displayRule != null) { + showDisplayRule(displayRule); + } + } + + //Insane amount of controllers here, because flutter + var textController = TextEditingController(); + var numberController = TextEditingController(); + String chosenBooleanDisplayRule = "visible"; + String chosenStringDisplayRule = "icon"; + String chosenNumberDisplayRule = "zoomFrom"; + bool currentBooleanRuleState = true; + PersistentBottomSheetController? persistentBottomSheetController; + + void showDisplayRule(MPDisplayRule displayRule) async { + String? iconUrl = await displayRule.getIconUrl(); + if (iconUrl != null) { + textController.text = iconUrl; + } + + num? zoomFrom = await displayRule.getZoomFrom(); + if (zoomFrom != null) { + numberController.text = zoomFrom.toString(); + } + + persistentBottomSheetController = scaffoldKey.currentState!.showBottomSheet((context) { + return Container( + color: Colors.white, + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + child: Column( + children: [ + Title(color: Colors.black, child: const Text("String values")), + Row( + children: [ + DropdownButton( + items: const [ + DropdownMenuItem(value: "icon", child: Text("icon")), + DropdownMenuItem(value: "label", child: Text("label")), + DropdownMenuItem(value: "polygonStrokeColor", child: Text("polygonStrokeColor")), + DropdownMenuItem(value: "polygonFillColor", child: Text("polygonFillColor")), + ], + onChanged: (dynamic value) async { + if (value != null && value is String) { + chosenStringDisplayRule = value; + textController.text = await getDisplayRuleValue(value, displayRule); + persistentBottomSheetController?.setState!(() {}); + } + }, + value: chosenStringDisplayRule), + Expanded( + child: TextField( + controller: textController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + )), + ], + ), + ElevatedButton(onPressed: () => saveDisplayRuleValue(chosenStringDisplayRule, displayRule, textController.text), child: const Text("Save string")), + Title(color: Colors.black, child: const Text("Boolean values")), + Row( + children: [ + DropdownButton( + items: const [ + DropdownMenuItem(value: "visible", child: Text("visible")), + DropdownMenuItem(value: "iconVisible", child: Text("iconVisible")), + DropdownMenuItem(value: "labelVisible", child: Text("labelVisible")), + DropdownMenuItem(value: "polygonVisible", child: Text("polygonVisible")), + DropdownMenuItem(value: "wallVisible", child: Text("wallVisible")), + DropdownMenuItem(value: "extrusionVisible", child: Text("extrusionVisible")), + DropdownMenuItem(value: "2DModelVisible", child: Text("2DModelVisible")), + ], + onChanged: (dynamic value) async { + if (value != null && value is String) { + chosenBooleanDisplayRule = value; + currentBooleanRuleState = await getBooleanDisplayRuleValue(value, displayRule); + persistentBottomSheetController?.setState!(() {}); + } + }, + value: chosenBooleanDisplayRule, + ), + Expanded( + child: DropdownButton( + items: const [DropdownMenuItem(value: true, child: Text("true")), DropdownMenuItem(value: false, child: Text("false"))], + onChanged: (value) { + persistentBottomSheetController?.setState!( + () { + currentBooleanRuleState = value as bool; + }, + ); + }, + value: currentBooleanRuleState, + ), + ), + ], + ), + ElevatedButton(onPressed: () => saveBooleanDisplayRuleValue(chosenBooleanDisplayRule, displayRule, currentBooleanRuleState), child: const Text("Save bool")), + Title(color: Colors.black, child: const Text("Numerical values")), + Row( + children: [ + DropdownButton( + items: const [ + DropdownMenuItem(value: "zoomFrom", child: Text("zoomFrom")), + DropdownMenuItem(value: "zoomTo", child: Text("zoomTo")), + DropdownMenuItem(value: "iconSize", child: Text("iconSize")), + DropdownMenuItem(value: "labelZoomFrom", child: Text("labelZoomFrom")), + DropdownMenuItem(value: "labelZoomTo", child: Text("labelZoomTo")), + DropdownMenuItem(value: "labelMaxWidth", child: Text("labelMaxWidth")), + DropdownMenuItem(value: "polygonZoomFrom", child: Text("polygonZoomFrom")), + DropdownMenuItem(value: "polygonZoomTo", child: Text("polygonZoomTo")), + DropdownMenuItem(value: "polygonOpacity", child: Text("polygonOpacity")), + ], + onChanged: (dynamic value) async { + if (value != null && value is String) { + numberController.text = (await getNumericalDisplayRuleValue(value, displayRule)).toString(); + chosenNumberDisplayRule = value; + persistentBottomSheetController?.setState!(() {}); + } + }, + value: chosenNumberDisplayRule, + ), + Expanded( + child: TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.allow((RegExp("[.0-9]")))], + controller: numberController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + )), + ], + ), + ElevatedButton(onPressed: () => saveNumericalDisplayRuleValue(chosenNumberDisplayRule, displayRule, double.parse(numberController.text)), child: const Text("Save number")), + ], + ), + ); + }); + } + + void saveDisplayRuleValue(String value, MPDisplayRule displayRule, String setting) { + switch (value) { + case "icon": + displayRule.setIcon(setting); + break; + case "label": + displayRule.setLabel(setting); + break; + case "polygonStrokeColor": + displayRule.setPolygonStrokeColor(setting); + break; + case "polygonFillColor": + displayRule.setPolygonFillColor(setting); + break; + } + } + + Future getDisplayRuleValue(String value, MPDisplayRule displayRule) async { + switch (value) { + case "icon": + String? value = await displayRule.getIconUrl(); + if (value != null) { + return value; + } else { + return "null"; + } + case "label": + String? value = await displayRule.getLabel(); + if (value != null) { + return value; + } else { + return "null"; + } + case "polygonStrokeColor": + String? value = await displayRule.getPolygonStrokeColor(); + if (value != null) { + return value; + } else { + return "null"; + } + case "polygonFillColor": + String? value = await displayRule.getPolygonFillColor(); + if (value != null) { + return value; + } else { + return "null"; + } + default: + return "choose a value"; + } + } + + void saveBooleanDisplayRuleValue(String value, MPDisplayRule displayRule, bool setting) { + switch (value) { + case "visible": + displayRule.setVisible(setting); + break; + case "iconVisible": + displayRule.setIconVisible(setting); + break; + case "polygonVisible": + displayRule.setPolygonVisible(setting); + break; + case "labelVisible": + displayRule.setLabelVisible(setting); + break; + case "wallVisible": + displayRule.setWallVisible(setting); + break; + case "extrusionVisible": + displayRule.setExtrusionVisible(setting); + break; + case "2DModelVisible": + displayRule.setModel2DVisible(setting); + break; + } + } + + Future getBooleanDisplayRuleValue(String value, MPDisplayRule displayRule) async { + switch (value) { + case "visible": + bool? visible = await displayRule.isVisible(); + if (visible != null && visible) { + return true; + } else { + return false; + } + case "iconVisible": + bool? visible = await displayRule.isIconVisible(); + if (visible != null && visible) { + return true; + } else { + return false; + } + case "polygonVisible": + bool? visible = await displayRule.isPolygonVisible(); + if (visible != null && visible) { + return true; + } else { + return false; + } + case "labelVisible": + bool? visible = await displayRule.isLabelVisible(); + if (visible != null && visible) { + return true; + } else { + return false; + } + case "wallVisible": + bool? visible = await displayRule.isWallVisible(); + if (visible != null && visible) { + return true; + } else { + return false; + } + case "extrusionVisible": + bool? visible = await displayRule.isExtrusionVisible(); + if (visible != null && visible) { + return true; + } else { + return false; + } + case "2DModelVisible": + bool? visible = await displayRule.isModel2DVisible(); + if (visible != null && visible) { + return true; + } else { + return false; + } + default: + return false; + } + } + + void saveNumericalDisplayRuleValue(String value, MPDisplayRule displayRule, double setting) { + switch (value) { + case "zoomFrom": + displayRule.setZoomFrom(setting); + break; + case "zoomTo": + displayRule.setZoomTo(setting); + break; + } + } + + Future getNumericalDisplayRuleValue(String value, MPDisplayRule displayRule) async { + switch (value) { + case "zoomFrom": + var value = await displayRule.getZoomFrom(); + if (value != null) { + return value; + } else { + return 0; + } + case "zoomTo": + var value = await displayRule.getZoomTo(); + if (value != null) { + return value; + } else { + return 0; + } + } + return 0; + } + + void _routeStuff(MPLocation? location) async { + if (location == null) { + return; + } + _directionsService = MPDirectionsService(); + _directionsRenderer = MPDirectionsRenderer(); + _directionsRenderer?.setOnLegSelectedListener(onLegSelected); + + _directionsService?.getRoute(origin: _userPosition, destination: location.point).then((route) { + _directionsRenderer?.setRoute(route); + setState(() { + _currentRoute = route; + _showRemoveRouteButton = true; + }); + _showRoute(); + }).catchError((error) { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text( + "No Route: $error", + ), + )); + }); + } + + void _removeRoute() { + mapController.deSelectLocation(); + _directionsRenderer?.clear(); + directionsController?.close(); + setState(() { + _showRemoveRouteButton = false; + }); + } + + MPRoute? _currentRoute; + int _currentIndex = 0; + + void _showRoute() { + directionsController = scaffoldKey.currentState!.showBottomSheet((context) { + return Container( + height: MediaQuery.of(context).size.height * 0.35, + width: MediaQuery.of(context).size.width, + color: Colors.white, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () => updateDirectionsSheet(_currentIndex - 1), + icon: const Icon(Icons.keyboard_arrow_left), + iconSize: 50, + ), + Expanded( + child: Text( + expandRouteSteps(_currentRoute!.legs![_currentIndex].steps!), + softWrap: true, + textAlign: TextAlign.center, + ), + ), + IconButton( + onPressed: () => updateDirectionsSheet(_currentIndex + 1), + icon: const Icon(Icons.keyboard_arrow_right), + iconSize: 50, + ), + ], + ), + ); + }); + } + + String expandRouteSteps(List steps) { + String sum = "${steps[0].maneuver}"; + for (final step in steps.skip(1)) { + sum += ", ${step.maneuver}"; + } + return sum; + } + + void onLegSelected(int legIndex) { + directionsController!.setState!(() => _currentIndex = legIndex); + } + + void updateDirectionsSheet(int legIndex) { + if (legIndex < 0) { + return; + } + int tempIndex = min(legIndex, _currentRoute!.legs!.length - 1); + _directionsRenderer?.selectLegIndex(tempIndex); + directionsController!.setState!(() => _currentIndex = tempIndex); + } + + void onBuildingFoundAtCameraTarget(MPBuilding? building) { + if (building != null) { + print("building found = ${building.name}"); + } else { + print("building found or not found"); + } + } + + void onFloorUpdate(int floor) { + print("floor = $floor"); + } + + void onInfoWindowClick(String locationId) { + print("infowindow shown"); + } + + void onMarkerClick(String locationId) { + print("marker clicked location id is = $locationId"); + } + + void onVenueFoundAtCameraTarget(MPVenue? venue) { + if (venue != null) { + print("venue found = ${venue.administrativeId}"); + } else { + print("venue found or not found"); + } + } +} diff --git a/mapsindoors_mapbox/example/pubspec.yaml b/mapsindoors_mapbox/example/pubspec.yaml new file mode 100644 index 0000000..6bc42f9 --- /dev/null +++ b/mapsindoors_mapbox/example/pubspec.yaml @@ -0,0 +1,94 @@ +name: mapsindoors_example +description: Demonstrates how to use the mapsindoors plugin. +version: 2.0.0+1 + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: '>=2.18.2 <3.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + mapsindoors_mapbox: + # When depending on this package from a real application you should use: + # mapsindoors_mapbox: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 +dependency_overrides: + mapsindoors_mapbox_android: + path: ../../mapsindoors_mapbox_android + mapsindoors_mapbox_ios: + path: ../../mapsindoors_mapbox_ios + mapsindoors_platform_interface: + path: ../../mapsindoors_platform_interface + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# 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 packages. +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 + + # 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 diff --git a/mapsindoors_mapbox/example/test/widget_test.dart b/mapsindoors_mapbox/example/test/widget_test.dart new file mode 100644 index 0000000..8ff81c2 --- /dev/null +++ b/mapsindoors_mapbox/example/test/widget_test.dart @@ -0,0 +1,26 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:mapsindoors_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/mapsindoors_mapbox/lib/core/mapsindoors.dart b/mapsindoors_mapbox/lib/core/mapsindoors.dart new file mode 100644 index 0000000..e6ccafd --- /dev/null +++ b/mapsindoors_mapbox/lib/core/mapsindoors.dart @@ -0,0 +1,160 @@ +part of mapsindoors; + +/// gets the platform name and build version +Future getPlatformVersion() => UtilPlatform.instance.getPlatformVersion(); + +/// Loads content from the MapsIndoors solution matching the given API [key]. +/// +/// Use the [MPError] to determine if the SDK has loaded successfully. +Future loadMapsIndoors(String key) => MapsindoorsPlatform.instance.load(key); + +/// Retrieve the default display rule (hardcoded display rule in the SDK). +/// +/// Requires that [loadMapsIndoors] has successfully executed. +Future getDefaultDisplayRule() => Future.value(MapsindoorsPlatform.instance.createDisplayRuleWithName("default")); + +/// Retrieve the main display rule (can be configured in the CMS). +/// +/// Requires that [loadMapsIndoors] has successfully executed. +Future getMainDisplayRule() => Future.value(MapsindoorsPlatform.instance.createDisplayRuleWithName("main")); + +/// Retrieve the display rule for the given [location] +/// +/// Requires that [loadMapsIndoors] has successfully executed. +Future getDisplayRuleByLocation(FutureOr location) async { + final exists = await MapsindoorsPlatform.instance.locationDisplayRuleExists((await location).id); + if (exists == true) { + return MapsindoorsPlatform.instance.createDisplayRuleWithName((await location).id.value); + } else { + return null; + } +} + +/// Retrieve the display rule with a given [name]. +/// +/// Requires that [loadMapsIndoors] has successfully executed. +Future getDisplayRuleByName(String name) async { + final exists = await MapsindoorsPlatform.instance.displayRuleNameExists(name); + if (exists == true) { + return MapsindoorsPlatform.instance.createDisplayRuleWithName(name); + } else { + return null; + } +} + +/// Retrieve the corresponding display rule for the given [MPSolutionDisplayRuleEnum]. +/// +/// Requires that [loadMapsIndoors] has successfully executed. +Future getSolutionDisplayRule(MPSolutionDisplayRuleEnum solutionDisplayRule) => Future.value(MapsindoorsPlatform.instance.createDisplayRuleWithName(solutionDisplayRule.name)); + +/// Add a one time [listener] to be invoked when MapsIndoors is ready +void addOnMapsIndoorsReadyListener(OnMapsIndoorsReadyListener listener) => MapsindoorsPlatform.instance.addOnMapsIndoorsReadyListener(listener); + +/// Remove a MapsIndoors ready listener +void removeOnMapsIndoorsReadyListener(OnMapsIndoorsReadyListener listener) => MapsindoorsPlatform.instance.removeOnMapsIndoorsReadyListener(listener); + +/// Checks if there is on device data (embedded/locally stored) available. For this to return true, +/// data has to be available for all solution data types ([MPLocation], [MPBuilding]...) +/// +/// Returns true if data is available, otherwise returns false +Future checkOfflineDataAvailability() => MapsindoorsPlatform.instance.checkOfflineDataAvailability(); + +/// Clears the internal state of MapsIndoors SDK. Any loaded content is purged from memory. +/// +/// Invoke [loadMapsIndoors] to start the SDK anew. +void destroyMapsIndoors() => MapsindoorsPlatform.instance.destroy(); + +/// [disable] SDK event logging through MapsIndoors. No logs will be created or send with this disabled. +/// +/// By default it is enabled. But disabled in the CMS meaning logs will be created but never uploaded. +Future disableMapsIndoorsEventLogging(bool disable) => MapsindoorsPlatform.instance.disableEventLogging(disable); + +/// Retrieves the API key that was set by using [loadMapsIndoors] +/// +/// Returns the API key, or "" if no key has been set +Future getAPIKey() => MapsindoorsPlatform.instance.getAPIKey(); + +/// Returns a list of the current solution's available languages +Future?> getMapsIndoorsAvailableLanguages() => MapsindoorsPlatform.instance.getAvailableLanguages(); + +/// Gets a collection of all buildings for the current API key +Future getBuildings() => MapsindoorsPlatform.instance.getBuildings(); + +/// Gets a collection of all categories for the current API key +Future getCategories() => MapsindoorsPlatform.instance.getCategories(); + +/// Returns the current solution's default language +Future getMapsIndoorsDefaultLanguage() => MapsindoorsPlatform.instance.getDefaultLanguage(); + +/// Gets the current SDK language +Future getMapsIndoorsLanguage() => MapsindoorsPlatform.instance.getLanguage(); + +/// Retrieves a [MPLocation] by its [id] +Future getLocationById(String id) => MapsindoorsPlatform.instance.getLocationById(id); + +/// Gets all locations (a list of [MPLocation] objects) for the current API Key +Future?> getLocations() => MapsindoorsPlatform.instance.getLocations(); + +/// Runs a query on all the available [MPLocation]s with an optional [MPQuery] and/or [MPFilter] +Future?> getLocationsByQuery({MPQuery? query, MPFilter? filter}) => MapsindoorsPlatform.instance.getLocationsByQuery(query, filter); + +/// Retrieves a list of [MPLocation]s by external [ids] +Future?> getLocationsByExternalIds(List ids) => MapsindoorsPlatform.instance.getLocationsByExternalIds(ids); + +/// Gets a list of available map styles +Future?> getMapStyles() => MapsindoorsPlatform.instance.getMapStyles(); + +/// Returns the current position provider, if any is set +MPPositionProviderInterface? getPositionProvider() => MapsindoorsPlatform.instance.getPositionProvider(); + +/// Set a new position provider, or pass null to remove the current one +/// +/// Positioning starts as soon as the provider is set and has produced a position +void setPositionProvider(MPPositionProviderInterface? provider) => MapsindoorsPlatform.instance.setPositionProvider(provider); + +/// Gets the [MPSolution] for the current API key +Future getSolution() => MapsindoorsPlatform.instance.getSolution(); + +/// Gets a collection of all venues for the current API key +Future getVenues() => MapsindoorsPlatform.instance.getVenues(); + +/// Check if the current API key is valid +Future isAPIKeyValid() => MapsindoorsPlatform.instance.isAPIKeyValid(); + +/// Check if [loadMapsIndoors] has been called +Future isMapsIndoorsInitialized() => MapsindoorsPlatform.instance.isInitialized(); + +/// Check if the SDK is initialized and ready for use +Future isMapsIndoorsReady() => MapsindoorsPlatform.instance.isReady(); + +/// Sets the SDK's internal language. +/// +/// By default, the SDK language can be: +///
    +///
  • the solution's default language ([MPSolution.defaultLanguage])
  • +///
  • the current device language, if the MapsIndoors data isn't available (ie: first app run without network access)
  • +///
+Future setMapsIndoorsLanguage(String language) => MapsindoorsPlatform.instance.setLanguage(language); + +/// Main data synchronization method +/// +/// If not manually invoked, [MapsIndoorsWidget] will invoke it when built +Future synchronizeMapsIndoorsContent() => MapsindoorsPlatform.instance.synchronizeContent(); + +/// Gets the User Roles for the current solution +/// +/// Note that role names are localized +Future getUserRoles() => MapsindoorsPlatform.instance.getUserRoles(); + +/// Returns the list of [MPUserRole] that is currently applied +Future?> getAppliedUserRoles() => MapsindoorsPlatform.instance.getAppliedUserRoles(); + +/// Applies a list of [MPUserRole]s to the SDK which will get the UserRole specific locations. +Future applyUserRoles(List userRoles) => MapsindoorsPlatform.instance.applyUserRoles(userRoles); + +/// Get a [MPGeocodeResult] that contains lists of [MPLocation] (grouped by [MPLocationType]), +/// where the [point] is inside the locations geometry. When no floor index is set, locations on all floors are queried. +Future reverseGeoCode(MPPoint point) => MapsindoorsPlatform.instance.reverseGeoCode(point); + +/// Gets the default venue for this solution +Future getDefaultVenue() => MapsindoorsPlatform.instance.getDefaultVenue(); diff --git a/mapsindoors_mapbox/lib/core/mapsindoors_widget.dart b/mapsindoors_mapbox/lib/core/mapsindoors_widget.dart new file mode 100644 index 0000000..65bb730 --- /dev/null +++ b/mapsindoors_mapbox/lib/core/mapsindoors_widget.dart @@ -0,0 +1,348 @@ +part of mapsindoors; + +/// A [UniqueWidget] that contains the map used by MapsIndoors +class MapsIndoorsWidget extends UniqueWidget { + final MPFloorSelector? floorSelector; + final MPMapLabelFont? mapLabelFont; + final int? textSize; + final bool? showFloorSelector; + final bool? showInfoWindowOnClick; + final bool? showUserPosition; + final bool? enabletileFadeIn; + final Alignment? floorSelectorAlignment; + final bool useDefaultMapsIndoorsStyle; + final OnMapReadyListener? readyListener; + + /// Build the widget, MapsIndoors currently supports the following platforms: + /// * Android + /// * iOS + /// + /// Has optional [MPFloorSelector] widget. Package includes a [MPDefaultFloorSelector]. + /// + /// [floorSelectorAlignment] defaults to [Alignment.centerRight] if none is provided. + MapsIndoorsWidget({ + this.mapLabelFont, + this.textSize, + this.showFloorSelector, + this.showInfoWindowOnClick, + this.showUserPosition, + this.enabletileFadeIn, + this.floorSelector, + this.floorSelectorAlignment, + this.readyListener, + this.useDefaultMapsIndoorsStyle = true, + }) : super(key: new GlobalObjectKey(MapsIndoorsWidget)) { + if (this.readyListener != null) { + MapcontrolPlatform.instance.setOnMapControlReadyListener(this.readyListener!); + } + } + + @override + State createState() => _MapsIndoorsState(); + + /// Get the currently selected venue + Future getCurrentVenue() { + return MapcontrolPlatform.instance.getCurrentVenue(); + } + + /// Select a venue, optionally move the camera to the given venue + Future selectVenue(FutureOr venue, bool moveCamera) async { + return MapcontrolPlatform.instance.selectVenue(await venue, moveCamera); + } + + /// Get the currently selected building + Future getCurrentBuilding() { + return MapcontrolPlatform.instance.getCurrentBuilding(); + } + + /// Select a building, optionally move the camera to the given building + Future selectBuilding(FutureOr building, bool moveCamera) async { + return MapcontrolPlatform.instance.selectBuilding(await building, moveCamera); + } + + /// Invoke this method to restore the map to its default state (POIs shown based on their display rules, etc.) + Future clearFilter() { + return MapcontrolPlatform.instance.clearFilter(); + } + + /// Use this method to display temporary locations, not points of interests location. Use [clearFilter()] to exit this state + /// + /// Returns true if any locations are available + Future setFilter(MPFilter filter, MPFilterBehavior behavior) { + return MapcontrolPlatform.instance.setFilter(filter, behavior); + } + + /// Use this method to display temporary locations, not points of interests location. Use [clearFilter()] to exit this state + Future setFilterWithLocations(List locations, MPFilterBehavior behavior) { + return MapcontrolPlatform.instance.setFilterWithLocations(locations, behavior); + } + + /// Shows or hides the [MPFloorSelectorInterface], i.e. hiding the View from [MapsIndoorsWidget] + /// + /// [MapsIndoorsWidget] will still receive relevant events on floor updates, building change etc. + /// + /// The Interface will also receive the events, making it possible to show/hide in real time, + /// without refreshing the map. + Future hideFloorSelector(bool hide) { + return MapcontrolPlatform.instance.hideFloorSelector(hide); + } + + // setInfoWindowAdapter + + // setClusterIconAdapter + + /// Sets padding on the map. + Future setMapPadding(int start, int top, int end, int bottom) { + return MapcontrolPlatform.instance.setMapPadding(start, top, end, bottom); + } + + /// Sets the map style for MapsIndoors tiles + /// + /// [mapstyle] is a [MPMapStyle] object, a list of available [MPMapStyle]s can be fetched via [getMapStyles] + Future setMapStyle(MPMapStyle mapstyle) { + return MapcontrolPlatform.instance.setMapStyle(mapstyle); + } + + /// Gets the current map style of MapsIndoors tiles + Future get mapStyle { + return MapcontrolPlatform.instance.getMapStyle(); + } + + /// Gets the Map View bottom padding + Future get mapViewPaddingBottom { + return MapcontrolPlatform.instance.getMapViewPaddingBottom(); + } + + /// Gets the Map View end padding + Future get mapViewPaddingEnd { + return MapcontrolPlatform.instance.getMapViewPaddingEnd(); + } + + /// Gets the Map View start padding + Future get mapViewPaddingStart { + return MapcontrolPlatform.instance.getMapViewPaddingStart(); + } + + /// Gets the Map View top padding + Future get mapViewPaddingTop { + return MapcontrolPlatform.instance.getMapViewPaddingTop(); + } + + /// Enables/disables the info window on user-selected locations + /// + /// The info window is shown by default when the user selects a location (by tapping on it) + Future showInfoWindowOnClickedLocation(bool show) { + return MapcontrolPlatform.instance.showInfoWindowOnClickedLocation(show); + } + + /// Returns the visibility state of the currently used [MPFloorSelectorInterface]. + Future get isFloorSelectorHidden { + return MapcontrolPlatform.instance.isFloorSelectorHidden(); + } + + /// Call this to deselect a location previously selected with [selectLocation(MPLocation, MPSelectionBehavior)] + Future deSelectLocation() { + return MapcontrolPlatform.instance.deSelectLocation(); + } + + /// Sets the current visible floor to the given floorIndex one + /// + /// For floor names/z-index pairs check the value returned by [MPBuilding.floors] + Future selectFloor(int floorIndex) { + return MapcontrolPlatform.instance.selectFloor(floorIndex); + } + + /// Focus the map on the given [MPEntity]. + /// + /// Examples of classes of type [MPEntity] are: [MPVenue], [MPBuilding], + /// [MPBuilding], [MPLocation]. + Future goTo(FutureOr entity) async { + return MapcontrolPlatform.instance.goTo(await entity); + } + + /// Selects a location based on a [MPLocation] object. + /// + /// Optionally apply a [MPSelectionBehavior] + /// + /// Use [deSelectLocation()] or send null instead of a [MPLocation] to un-select the location. + Future selectLocation(FutureOr location, [MPSelectionBehavior? behavior]) async { + return MapcontrolPlatform.instance.selectLocation(await location, behavior ?? MPSelectionBehavior.DEFAULT); + } + + /// Selects a location based on a id string object. + /// + /// Optionally apply a [MPSelectionBehavior] + /// + /// Use [deSelectLocation] or send null instead of a [MPLocation] to un-select the location. + Future selectLocationById(String id, [MPSelectionBehavior? behavior]) { + return MapcontrolPlatform.instance.selectLocationById(id, behavior ?? MPSelectionBehavior.DEFAULT); + } + + /// Returns the current [MPFloor] of the current [MPBuilding] in focus + Future getCurrentBuildingFloor() { + return MapcontrolPlatform.instance.getCurrentBuildingFloor(); + } + + /// Returns the current floor index or [MPFloor.defaultGroundFloorIndex] if no [MPBuilding] is in focus + Future getCurrentFloorIndex() { + return MapcontrolPlatform.instance.getCurrentFloorIndex(); + } + + /// Replaces the default FloorSelector with a custom one. + Future setFloorSelector(MPFloorSelectorInterface floorSelector) { + return MapcontrolPlatform.instance.setFloorSelector(floorSelector, false); + } + + /// Get the zoom level that MapsIndoors is currently using for display icons etc. on the map + Future getCurrentMapsindoorsZoom() { + return MapcontrolPlatform.instance.getCurrentMapsIndoorsZoom(); + } + + /// Renders the positioning blue dot at the last known user position on the map + Future setShowUserPosition(bool show) { + return MapcontrolPlatform.instance.showUserPosition(show); + } + + /// Returns the current visibility state of the user location icon (blue dot) + Future get isUserPositionShown { + return MapcontrolPlatform.instance.isUserPositionShown(); + } + + /// Enables live data on a specific domain and uses MapsIndoors standard graphic implementation + /// + /// Uses a domainType string, use [LiveDataDomainTypes] to get supported strings + Future enableLiveData(String domainType, [OnLiveLocationUpdateListener? listener]) { + return MapcontrolPlatform.instance.enableLiveData(domainType, listener); + } + + /// Disables live data for a specific domainType + Future disableLiveData(String donaminType) { + return MapcontrolPlatform.instance.disableLiveData(donaminType); + } + + /// Add a camera event listener, invoked when a camera event occurs (e.g. moved, idle) (see [MPCameraEvent]) + void addOnCameraEventListner(MPCameraEventListener listener) { + MapcontrolPlatform.instance.addOnCameraEventListner(listener); + } + + /// Remove a camera event listener + void removeOnCameraEventListner(MPCameraEventListener listener) { + MapcontrolPlatform.instance.removeOnCameraEventListner(listener); + } + + /// Add a listener object to catch floor changes made by either the user or the positioning service + void addOnFloorUpdateListener(OnFloorUpdateListener listener) { + MapcontrolPlatform.instance.addOnFloorUpdateListener(listener); + } + + /// Remove a floor update listener + void removeOnFloorUpdateListener(OnFloorUpdateListener listener) { + MapcontrolPlatform.instance.removeOnFloorUpdateListener(listener); + } + + /// Set a location selection listener, invoked when a location is selected, + /// either by tapping on it, or programmatically with [selectLocation] + void setOnLocationSelectedListener(OnLocationSelectedListener? listener, [bool? consumeEvent]) { + MapcontrolPlatform.instance.setOnLocationSelectedListener(listener, consumeEvent); + } + + /// Set a listener for when the map has been tapped + void setOnMapClickListener(OnMapClickListener? listener, [bool? consumeEvent]) { + MapcontrolPlatform.instance.setOnMapClickListener(listener, consumeEvent); + } + + /// Set a marker click event listener, invoked when a marker is clicked + void setOnMarkerClickListener(OnMarkerClickListener? listener, [bool? consumeEvent]) { + MapcontrolPlatform.instance.setOnMarkerClickListener(listener, consumeEvent); + } + + /// Set a info window click listener, invoked when an info window is clicked + void setOnMarkerInfoWindowClickListener(OnMarkerInfoWindowClickListener? listener) { + MapcontrolPlatform.instance.setOnMarkerInfoWindowClickListener(listener); + } + + /// Uses a camera update to animate the camera. + /// + /// [duration] how long the animation should take. + Future animateCamera(MPCameraUpdate update, [int? duration]) { + return MapcontrolPlatform.instance.animateCamera(update, duration); + } + + /// Saves the current position of the camera into a [MPCameraPosition]. + Future getCurrentCameraPosition() { + return MapcontrolPlatform.instance.currentCameraPosition(); + } + + /// Uses a camera update to move the camera instantly. + Future moveCamera(MPCameraUpdate update) { + return MapcontrolPlatform.instance.moveCamera(update); + } + + /// Set a current building changed event listener, which is invoked when the currently selected building changes + void setOnCurrentBuildingChangedListener(OnBuildingFoundAtCameraTargetListener? listener) { + MapcontrolPlatform.instance.setOnCurrentBuildingChangedListener(listener); + } + + /// Set a current venue changed event listener, which is invoked when the currently selected venue changes + void setOnCurrentVenueChangedListener(OnVenueFoundAtCameraTargetListener? listener) { + MapcontrolPlatform.instance.setOnCurrentVenueChangedListener(listener); + } +} + +class _MapsIndoorsState extends State { + @override + Widget build(BuildContext context) { + // This is used in the platform side to register the view. + const String viewType = ''; + // Pass parameters to the platform side. + + final Map creationParams = { + "mapConfig": jsonEncode({ + "textSize": widget.textSize, + "showFloorSelector": widget.showFloorSelector, + "showInfoWindowOnLocationClicked": widget.showInfoWindowOnClick, + "showUserPosition": widget.showUserPosition, + "typeface": widget.mapLabelFont?.typeface, + "color": widget.mapLabelFont?.color, + "showHalo": widget.mapLabelFont?.showHalo, + "tileFadeInEnabled": widget.enabletileFadeIn, + "useDefaultMapsIndoorsStyle": widget.useDefaultMapsIndoorsStyle + }), + "floorSelectorAutoFloorChange": widget.floorSelector?.isAutoFloorChangeEnabled == true + }; + + final floorSelector = widget.floorSelector ?? MPDefaultFloorSelector(); + MapcontrolPlatform.instance.setFloorSelector(floorSelector, true); + final ret; + + if (Platform.isAndroid) { + ret = AndroidView( + viewType: viewType, + layoutDirection: TextDirection.ltr, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } else if (Platform.isIOS) { + ret = UiKitView( + viewType: viewType, + layoutDirection: TextDirection.ltr, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } else { + throw UnimplementedError(); + } + + return Flexible( + fit: FlexFit.tight, + flex: 1, + child: Stack(children: [ + ret, + Align( + alignment: widget.floorSelectorAlignment ?? Alignment.centerRight, + child: floorSelector, + ) + ]), + ); + } +} diff --git a/mapsindoors_mapbox/lib/core/mp_default_floor_selector.dart b/mapsindoors_mapbox/lib/core/mp_default_floor_selector.dart new file mode 100644 index 0000000..f22e7af --- /dev/null +++ b/mapsindoors_mapbox/lib/core/mp_default_floor_selector.dart @@ -0,0 +1,170 @@ +part of mapsindoors; + +class MPDefaultFloorSelector extends StatefulWidget with MPFloorSelector { + MPDefaultFloorSelector({super.key}); + final _Forwarder callForwarder = _Forwarder(); + + @override + Widget? getWidget() => this; + @override + bool get isAutoFloorChangeEnabled => true; + @override + set floors(List? floors) => callForwarder.setFloors?.call(floors); + @override + set onFloorSelectionChangedListener(OnFloorSelectionChangedListener listener) { + if (callForwarder.setOnFloorSelectionChangedListener != null) { + callForwarder.setOnFloorSelectionChangedListener?.call(listener); + } else { + callForwarder.listener = listener; + } + } + + @override + void setSelectedFloor(MPFloor floor) => callForwarder.setSelectedFloor?.call(floor); + @override + void setSelectedFloorByFloorIndex(int floorIndex) => callForwarder.setSelectedFloorByFloorIndex?.call(floorIndex); + @override + set userPositionFloor(int floorIndex) => callForwarder.setUserPositionFloor?.call(floorIndex); + @override + void show(bool show) => callForwarder.show?.call(show); + @override + void zoomLevelChanged(num newZoomLevel) => callForwarder.zoomLevelChanged?.call(newZoomLevel); + + @override + _FloorSelectorState createState() => _FloorSelectorState(); +} + +class _Forwarder { + OnFloorSelectionChangedListener? listener; + Function(List? floors)? setFloors; + Function(OnFloorSelectionChangedListener listener)? setOnFloorSelectionChangedListener; + Function(MPFloor floor)? setSelectedFloor; + Function(int floorIndex)? setSelectedFloorByFloorIndex; + Function(int floorIndex)? setUserPositionFloor; + Function(bool show)? show; + Function(num newZoomLevel)? zoomLevelChanged; +} + +class _FloorSelectorState extends State implements MPFloorSelectorInterface { + final List _floors = List.empty(growable: true); + OnFloorSelectionChangedListener? _listener; + bool _visible = true; + int _userPositionFloor = -1; + MPFloor? _selectedFloor; + num showOnZoomLevel = 17.0; + + @override + void initState() { + super.initState(); + widget.callForwarder.setFloors = (floors) => this.floors = floors; + widget.callForwarder.setOnFloorSelectionChangedListener = (listener) => this.onFloorSelectionChangedListener = listener; + widget.callForwarder.setSelectedFloor = (floor) => setSelectedFloor(floor); + widget.callForwarder.setSelectedFloorByFloorIndex = (floorIndex) => setSelectedFloorByFloorIndex(floorIndex); + widget.callForwarder.setUserPositionFloor = (floorIndex) => userPositionFloor = floorIndex; + widget.callForwarder.show = (show) => this.show(show); + widget.callForwarder.zoomLevelChanged = (newZoomLevel) => zoomLevelChanged(newZoomLevel); + if (widget.callForwarder.listener != null) { + this.onFloorSelectionChangedListener = widget.callForwarder.listener!; + } + } + + @override + Widget? getWidget() { + return null; + } + + @override + bool get isAutoFloorChangeEnabled { + return true; + } + + @override + set floors(List? floors) { + if (floors == null) { + return; + } + List list = List.from(floors); + list.sort(); + setState(() { + _floors.clear(); + _floors.addAll(list.reversed); + }); + } + + @override + set onFloorSelectionChangedListener(OnFloorSelectionChangedListener listener) { + setState(() { + _listener = listener; + }); + } + + @override + void setSelectedFloor(MPFloor floor) { + _listener?.call(floor); + setState(() { + _selectedFloor = floor; + }); + } + + @override + void setSelectedFloorByFloorIndex(int floorIndex) { + if (_floors.isEmpty) { + return; + } + + for (final floor in _floors) { + if (floor.floorIndex == floorIndex) { + setSelectedFloor(floor); + return; + } + } + } + + @override + set userPositionFloor(int floorIndex) { + setState(() { + _userPositionFloor = floorIndex; + }); + } + + @override + void show(bool show) { + if (_visible != show) { + setState(() { + _visible = show; + }); + } + } + + @override + void zoomLevelChanged(num newZoomLevel) => show(newZoomLevel >= showOnZoomLevel); + + @override + Widget build(BuildContext context) { + if (_visible && _floors.isNotEmpty) { + return SizedBox( + width: 50, + height: 400, + child: ListView.builder( + itemCount: _floors.length, + itemBuilder: (context, index) { + return ElevatedButton( + onPressed: () => setSelectedFloor(_floors[index]), + style: (_floors[index] == _selectedFloor) + ? _buttonStyleWithColor(Colors.green) + : (_userPositionFloor == _floors[index].floorIndex) + ? _buttonStyleWithColor(Colors.amber) + : null, + child: Text(_floors[index].displayName), + ); + }), + ); + } else { + return const SizedBox.shrink(); + } + } +} + +ButtonStyle _buttonStyleWithColor(Color color) { + return ButtonStyle(backgroundColor: MaterialStateProperty.resolveWith((states) => color)); +} diff --git a/mapsindoors_mapbox/lib/core/mp_directions_renderer.dart b/mapsindoors_mapbox/lib/core/mp_directions_renderer.dart new file mode 100644 index 0000000..ea613d0 --- /dev/null +++ b/mapsindoors_mapbox/lib/core/mp_directions_renderer.dart @@ -0,0 +1,51 @@ +part of mapsindoors; + +class MPDirectionsRenderer { + /// Set a route to be rendered. This also resets the selected leg and step indices to 0. + Future setRoute(MPRoute? route) => DirectionsRendererPlatform.instance.setRoute(route); + + /// Clears the route from the map + Future clear() => DirectionsRendererPlatform.instance.clear(); + + /// Selects the next leg if possible. + /// + /// Has no effect if the last leg is selected + Future nextLeg() => DirectionsRendererPlatform.instance.nextLeg(); + + /// Selects the previous leg if possible. + /// + /// Has no effect if the first leg is selected + Future previousLeg() => DirectionsRendererPlatform.instance.previousLeg(); + + /// Enable/Disable the polyline animation when displaying a route element on the map + Future setAnimatedPolyline(bool animated, bool repeating, int durationMs) => DirectionsRendererPlatform.instance.setAnimatedPolyline(animated, repeating, durationMs); + + /// Set the colors of the polyline + Future setPolyLineColors(String foreground, String background) => DirectionsRendererPlatform.instance.setPolyLineColors(foreground, background); + + /// Manually set the selected leg index on the route. + /// + /// This may throw an exception if the resulting internal state is invalid (parsed index is out of bounds) + Future selectLegIndex(int legIndex) => DirectionsRendererPlatform.instance.selectLegIndex(legIndex); + + /// Gets the currently selected leg's floor index. + Future getSelectedLegFloorIndex() => DirectionsRendererPlatform.instance.getSelectedLegFloorIndex(); + + /// Set the duration of camera animations (ms). + /// + /// If a duration < 0 then camera animations are disabled, and the camera will move instantly. + /// + /// The value is 1000 ms by default + Future setCameraAnimationDuration(int durationMs) => DirectionsRendererPlatform.instance.setCameraAnimationDuration(durationMs); + + /// Set the [MPCameraViewFitMode] of the camera, when displaying route elements on the map. + /// + /// The camera may be aligned to north, aligned with the first step, or aligned from + /// start point to end point. + Future setCameraViewFitMode(MPCameraViewFitMode mpCameraViewFitMode) => DirectionsRendererPlatform.instance.setCameraViewFitMode(mpCameraViewFitMode); + + /// Set a listener, which will be invoked when a new leg has been selected + /// + /// This is used for when the forward/back markers are selected on the map + Future setOnLegSelectedListener(OnLegSelectedListener? onLegSelectedListener) => DirectionsRendererPlatform.instance.setOnLegSelectedListener(onLegSelectedListener); +} diff --git a/mapsindoors_mapbox/lib/core/mp_directions_service.dart b/mapsindoors_mapbox/lib/core/mp_directions_service.dart new file mode 100644 index 0000000..e72cda0 --- /dev/null +++ b/mapsindoors_mapbox/lib/core/mp_directions_service.dart @@ -0,0 +1,48 @@ +part of mapsindoors; + +class MPDirectionsService { + static final String travelModeTransit = "transit"; + static final String travelModeDriving = "driving"; + static final String travelModeWalking = "walking"; + static final String travelModeBicycling = "bicycling"; + + /// Creates a [MPDirectionsService] object than can be queried for directions between different [MPPoint]s + MPDirectionsService() { + _create(); + } + + void _create() { + DirectionsServicePlatform.instance.create(); + } + + /// Add an avoidWayType, these are based on [OSM highways](https://wiki.openstreetmap.org/wiki/Key:highway) + /// + /// Supported types are defined in [MPHighway] + Future addAvoidWayType(String avoidWayType) => DirectionsServicePlatform.instance.addAvoidWayType(avoidWayType); + + /// Clears all added avoidWayTypes + Future clearWayType() => DirectionsServicePlatform.instance.clearWayType(); + + /// Sets whether routes should use departure time or arrival time when using the transit travel mode + Future setIsDeparture(bool isDeparture) => DirectionsServicePlatform.instance.setIsDeparture(isDeparture); + + /// Queries the routing network to generate a route from the [origin] to the [destination]. + /// + /// Can throw an [MPError] if unable to generate the route. + Future getRoute({required MPPoint origin, required MPPoint destination}) => DirectionsServicePlatform.instance.getRoute(origin, destination); + + /// defines the travel mode for routes, can be one of the following: + /// + /// walking + /// + /// bicycling + /// + /// driving + /// + /// transit + Future setTravelMode(String travelMode) => DirectionsServicePlatform.instance.setTravelMode(travelMode); + + /// sets the wanted arrival/departure time for routes generated with this [MPDirectionsService], + /// this setting is used in conjunction with [setIsDeparture] and [setTravelMode] + Future setTime(int time) => DirectionsServicePlatform.instance.setTime(time); +} diff --git a/mapsindoors_mapbox/lib/core/mp_floor_selector.dart b/mapsindoors_mapbox/lib/core/mp_floor_selector.dart new file mode 100644 index 0000000..e87b4bb --- /dev/null +++ b/mapsindoors_mapbox/lib/core/mp_floor_selector.dart @@ -0,0 +1,3 @@ +part of mapsindoors; + +mixin MPFloorSelector on Widget implements MPFloorSelectorInterface {} diff --git a/mapsindoors_mapbox/lib/core/mp_map_label_font.dart b/mapsindoors_mapbox/lib/core/mp_map_label_font.dart new file mode 100644 index 0000000..779bfc5 --- /dev/null +++ b/mapsindoors_mapbox/lib/core/mp_map_label_font.dart @@ -0,0 +1,12 @@ +part of mapsindoors; + +class MPMapLabelFont { + final String typeface; + final String color; + final bool showHalo; + const MPMapLabelFont({ + required this.typeface, + required this.color, + required this.showHalo, + }); +} diff --git a/mapsindoors_mapbox/lib/mapsindoors.dart b/mapsindoors_mapbox/lib/mapsindoors.dart new file mode 100644 index 0000000..02952b7 --- /dev/null +++ b/mapsindoors_mapbox/lib/mapsindoors.dart @@ -0,0 +1,87 @@ +library mapsindoors; + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; +import 'dart:io' show Platform; + +import 'package:mapsindoors_platform_interface/platform_library.dart'; +export 'package:mapsindoors_platform_interface/platform_library.dart' + show + MPBuilding, + MPEntity, + MPFloor, + MPLocation, + MPVenue, + LiveDataDomainTypes, + MPCameraEvent, + MPCameraViewFitMode, + MPCollisionHandling, + MPLocationPropertyNames, + MPLocationType, + MPSolutionDisplayRuleEnum, + MPBounds, + MPGeometry, + MPMultiPolygon, + MPPoint, + MPPolygon, + MPCameraEventListener, + OnBuildingFoundAtCameraTargetListener, + OnFloorSelectionChangedListener, + OnFloorUpdateListener, + OnLegSelectedListener, + OnLiveLocationUpdateListener, + OnLocationSelectedListener, + OnMapClickListener, + OnMapsIndoorsReadyListener, + OnMarkerClickListener, + OnMarkerInfoWindowClickListener, + OnPositionUpdateListener, + OnVenueFoundAtCameraTargetListener, + MPRouteCoordinate, + MPRouteLeg, + MPRouteProperty, + MPRouteStep, + MPRoute, + MPBuildingInfo, + MPCategory, + MPDataField, + MPDisplayRule, + MPError, + MPFilter, + MPFilterBuilder, + MPFloorSelectorInterface, + MPGeocodeResult, + MPIconSize, + MPFilterBehavior, + MPFilterBehaviorBuilder, + MPSelectionBehavior, + MPSelectionBehaviorBuilder, + MPRouteResult, + MPMapStyle, + MPPositionProviderInterface, + MPPositionResultInterface, + MPQuery, + MPQueryBuilder, + MPSettings3D, + MPSolutionConfig, + MPSolution, + MPUserRole, + MPHighway, + MPCameraPosition, + MPCameraPositionBuilder, + MPCameraUpdate, + MPBuildingCollection, + MPCategoryCollection, + MPVenueCollection, + MPUserRoleCollection; + +part 'core/mapsindoors_widget.dart'; +part 'core/mp_directions_service.dart'; +part 'core/mp_directions_renderer.dart'; +part 'core/mp_default_floor_selector.dart'; +part 'core/mp_floor_selector.dart'; +part 'core/mp_map_label_font.dart'; +part 'core/mapsindoors.dart'; diff --git a/mapsindoors_mapbox/pubspec.yaml b/mapsindoors_mapbox/pubspec.yaml new file mode 100644 index 0000000..9960de5 --- /dev/null +++ b/mapsindoors_mapbox/pubspec.yaml @@ -0,0 +1,37 @@ +name: mapsindoors_mapbox +description: A MapsIndoors flutter plugin using the Mapbox platform +version: 2.0.0 +repository: https://github.com/MapsPeople/MapsIndoors-sdk-flutter +homepage: https://www.mapsindoors.com/ + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + platforms: + android: + default_package: mapsindoors_mapbox_android + ios: + default_package: mapsindoors_mapbox_ios + +dependencies: + flutter: + sdk: flutter + mapsindoors_mapbox_android: ^2.0.0 + mapsindoors_mapbox_ios: ^2.0.0 + mapsindoors_platform_interface: ^2.0.0 +dependency_overrides: + mapsindoors_mapbox_android: + path: ../mapsindoors_mapbox_android + mapsindoors_mapbox_ios: + path: ../mapsindoors_mapbox_ios + mapsindoors_platform_interface: + path: ../mapsindoors_platform_interface + + +dev_dependencies: + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 \ No newline at end of file diff --git a/mapsindoors_mapbox_android/CHANGELOG.md b/mapsindoors_mapbox_android/CHANGELOG.md new file mode 100644 index 0000000..ded7a32 --- /dev/null +++ b/mapsindoors_mapbox_android/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 2.0.0 + +* Moved from [mapsindoors_android](https://pub.dev/packages/mapsindoors_android) to allow for multiple map providers diff --git a/mapsindoors_mapbox_android/LICENSE b/mapsindoors_mapbox_android/LICENSE new file mode 100644 index 0000000..75757d5 --- /dev/null +++ b/mapsindoors_mapbox_android/LICENSE @@ -0,0 +1,11 @@ +Copyright 2023 Mapspeople + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/mapsindoors_mapbox_android/README.md b/mapsindoors_mapbox_android/README.md new file mode 100644 index 0000000..2f000d0 --- /dev/null +++ b/mapsindoors_mapbox_android/README.md @@ -0,0 +1,3 @@ +A federated Flutter plugin for integrating with the native MapsIndoors SDK. + +This plugin contains the Android platform for the [mapsindoors](pub.dev/packages/mapsindoors) plugin. diff --git a/mapsindoors_mapbox_android/android/build.gradle b/mapsindoors_mapbox_android/android/build.gradle new file mode 100644 index 0000000..57229f2 --- /dev/null +++ b/mapsindoors_mapbox_android/android/build.gradle @@ -0,0 +1,98 @@ +group 'com.mapspeople.mapsindoors' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + maven { url uri("${projectDir}/libs") } + google() + mavenCentral() + maven { url 'https://maven.mapsindoors.com/' } + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + maven { url uri("${projectDir}/libs") } + google() + mavenCentral() + maven { url 'https://maven.mapsindoors.com/' } + maven { + url 'https://api.mapbox.com/downloads/v2/releases/maven' + authentication { + basic(BasicAuthentication) + } + credentials { + // Do not change the username below. + // This should always be `mapbox` (not your username). + username = "mapbox" + // Use the secret token you stored in gradle.properties as the password + password = project.properties['MAPBOX_DOWNLOADS_TOKEN'] ?: "" + } + } + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 21 + } +} + +configurations { + all*.exclude group: 'com.mapbox.plugin', module:'maps-lifecycle' +} + +dependencies { + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1' + repositories { + maven { + url uri("${rootProject.projectDir}/libs") + } + maven { + url 'https://maven.mapsindoors.com/' + } + maven { + url 'https://api.mapbox.com/downloads/v2/releases/maven' + authentication { + basic(BasicAuthentication) + } + credentials { + // Do not change the username below. + // This should always be `mapbox` (not your username). + username = "mapbox" + // Use the secret token you stored in gradle.properties as the password + password = project.properties['MAPBOX_DOWNLOADS_TOKEN'] ?: "" + } + } + } + + implementation ("com.mapbox.maps:android:10.14.1") { + exclude group: 'com.mapbox.plugin', module: 'maps-lifecycle' + } + implementation "com.mapspeople.mapsindoors:mapbox:4.1.10-flutter" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.0" +} diff --git a/mapsindoors_mapbox_android/android/gradle.properties b/mapsindoors_mapbox_android/android/gradle.properties new file mode 100644 index 0000000..2d8d1e4 --- /dev/null +++ b/mapsindoors_mapbox_android/android/gradle.properties @@ -0,0 +1 @@ +android.useAndroidX=true \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/gradle/wrapper/gradle-wrapper.jar b/mapsindoors_mapbox_android/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..41d9927a4d4fb3f96a785543079b8df6723c946b GIT binary patch literal 59821 zcma&NV|1p`(k7gaZQHhOJ9%QKV?D8LCmq{1JGRYE(y=?XJw0>InKkE~^UnAEs2gk5 zUVGPCwX3dOb!}xiFmPB95NK!+5D<~S0s;d1zn&lrfAn7 zC?Nb-LFlib|DTEqB8oDS5&$(u1<5;wsY!V`2F7^=IR@I9so5q~=3i_(hqqG<9SbL8Q(LqDrz+aNtGYWGJ2;p*{a-^;C>BfGzkz_@fPsK8{pTT~_VzB$E`P@> z7+V1WF2+tSW=`ZRj3&0m&d#x_lfXq`bb-Y-SC-O{dkN2EVM7@!n|{s+2=xSEMtW7( zz~A!cBpDMpQu{FP=y;sO4Le}Z)I$wuFwpugEY3vEGfVAHGqZ-<{vaMv-5_^uO%a{n zE_Zw46^M|0*dZ`;t%^3C19hr=8FvVdDp1>SY>KvG!UfD`O_@weQH~;~W=fXK_!Yc> z`EY^PDJ&C&7LC;CgQJeXH2 zjfM}2(1i5Syj)Jj4EaRyiIl#@&lC5xD{8hS4Wko7>J)6AYPC-(ROpVE-;|Z&u(o=X z2j!*>XJ|>Lo+8T?PQm;SH_St1wxQPz)b)Z^C(KDEN$|-6{A>P7r4J1R-=R7|FX*@! zmA{Ja?XE;AvisJy6;cr9Q5ovphdXR{gE_7EF`ji;n|RokAJ30Zo5;|v!xtJr+}qbW zY!NI6_Wk#6pWFX~t$rAUWi?bAOv-oL6N#1>C~S|7_e4 zF}b9(&a*gHk+4@J26&xpiWYf2HN>P;4p|TD4f586umA2t@cO1=Fx+qd@1Ae#Le>{-?m!PnbuF->g3u)7(n^llJfVI%Q2rMvetfV5 z6g|sGf}pV)3_`$QiKQnqQ<&ghOWz4_{`rA1+7*M0X{y(+?$|{n zs;FEW>YzUWg{sO*+D2l6&qd+$JJP_1Tm;To<@ZE%5iug8vCN3yH{!6u5Hm=#3HJ6J zmS(4nG@PI^7l6AW+cWAo9sFmE`VRcM`sP7X$^vQY(NBqBYU8B|n-PrZdNv8?K?kUTT3|IE`-A8V*eEM2=u*kDhhKsmVPWGns z8QvBk=BPjvu!QLtlF0qW(k+4i+?H&L*qf262G#fks9}D5-L{yiaD10~a;-j!p!>5K zl@Lh+(9D{ePo_S4F&QXv|q_yT`GIPEWNHDD8KEcF*2DdZD;=J6u z|8ICSoT~5Wd!>g%2ovFh`!lTZhAwpIbtchDc{$N%<~e$E<7GWsD42UdJh1fD($89f2on`W`9XZJmr*7lRjAA8K0!(t8-u>2H*xn5cy1EG{J;w;Q-H8Yyx+WW(qoZZM7p(KQx^2-yI6Sw?k<=lVOVwYn zY*eDm%~=|`c{tUupZ^oNwIr!o9T;H3Fr|>NE#By8SvHb&#;cyBmY1LwdXqZwi;qn8 zK+&z{{95(SOPXAl%EdJ3jC5yV^|^}nOT@M0)|$iOcq8G{#*OH7=DlfOb; z#tRO#tcrc*yQB5!{l5AF3(U4>e}nEvkoE_XCX=a3&A6Atwnr&`r&f2d%lDr8f?hBB zr1dKNypE$CFbT9I?n){q<1zHmY>C=5>9_phi79pLJG)f=#dKdQ7We8emMjwR*qIMF zE_P-T*$hX#FUa%bjv4Vm=;oxxv`B*`weqUn}K=^TXjJG=UxdFMSj-QV6fu~;- z|IsUq`#|73M%Yn;VHJUbt<0UHRzbaF{X@76=8*-IRx~bYgSf*H(t?KH=?D@wk*E{| z2@U%jKlmf~C^YxD=|&H?(g~R9-jzEb^y|N5d`p#2-@?BUcHys({pUz4Zto7XwKq2X zSB~|KQGgv_Mh@M!*{nl~2~VV_te&E7K39|WYH zCxfd|v_4!h$Ps2@atm+gj14Ru)DhivY&(e_`eA)!O1>nkGq|F-#-6oo5|XKEfF4hR z%{U%ar7Z8~B!foCd_VRHr;Z1c0Et~y8>ZyVVo9>LLi(qb^bxVkbq-Jq9IF7!FT`(- zTMrf6I*|SIznJLRtlP)_7tQ>J`Um>@pP=TSfaPB(bto$G1C zx#z0$=zNpP-~R);kM4O)9Mqn@5Myv5MmmXOJln312kq#_94)bpSd%fcEo7cD#&|<` zrcal$(1Xv(nDEquG#`{&9Ci~W)-zd_HbH-@2F6+|a4v}P!w!Q*h$#Zu+EcZeY>u&?hn#DCfC zVuye5@Ygr+T)0O2R1*Hvlt>%rez)P2wS}N-i{~IQItGZkp&aeY^;>^m7JT|O^{`78 z$KaK0quwcajja;LU%N|{`2o&QH@u%jtH+j!haGj;*ZCR*`UgOXWE>qpXqHc?g&vA& zt-?_g8k%ZS|D;()0Lf!>7KzTSo-8hUh%OA~i76HKRLudaNiwo*E9HxmzN4y>YpZNO zUE%Q|H_R_UmX=*f=2g=xyP)l-DP}kB@PX|(Ye$NOGN{h+fI6HVw`~Cd0cKqO;s6aiYLy7sl~%gs`~XaL z^KrZ9QeRA{O*#iNmB7_P!=*^pZiJ5O@iE&X2UmUCPz!)`2G3)5;H?d~3#P|)O(OQ_ zua+ZzwWGkWflk4j^Lb=x56M75_p9M*Q50#(+!aT01y80x#rs9##!;b-BH?2Fu&vx} za%4!~GAEDsB54X9wCF~juV@aU}fp_(a<`Ig0Pip8IjpRe#BR?-niYcz@jI+QY zBU9!8dAfq@%p;FX)X=E7?B=qJJNXlJ&7FBsz;4&|*z{^kEE!XbA)(G_O6I9GVzMAF z8)+Un(6od`W7O!!M=0Z)AJuNyN8q>jNaOdC-zAZ31$Iq%{c_SYZe+(~_R`a@ zOFiE*&*o5XG;~UjsuW*ja-0}}rJdd@^VnQD!z2O~+k-OSF%?hqcFPa4e{mV1UOY#J zTf!PM=KMNAzbf(+|AL%K~$ahX0Ol zbAxKu3;v#P{Qia{_WzHl`!@!8c#62XSegM{tW1nu?Ee{sQq(t{0TSq67YfG;KrZ$n z*$S-+R2G?aa*6kRiTvVxqgUhJ{ASSgtepG3hb<3hlM|r>Hr~v_DQ>|Nc%&)r0A9go z&F3Ao!PWKVq~aWOzLQIy&R*xo>}{UTr}?`)KS&2$3NR@a+>+hqK*6r6Uu-H};ZG^| zfq_Vl%YE1*uGwtJ>H*Y(Q9E6kOfLJRlrDNv`N;jnag&f<4#UErM0ECf$8DASxMFF& zK=mZgu)xBz6lXJ~WZR7OYw;4&?v3Kk-QTs;v1r%XhgzSWVf|`Sre2XGdJb}l1!a~z zP92YjnfI7OnF@4~g*LF>G9IZ5c+tifpcm6#m)+BmnZ1kz+pM8iUhwag`_gqr(bnpy zl-noA2L@2+?*7`ZO{P7&UL~ahldjl`r3=HIdo~Hq#d+&Q;)LHZ4&5zuDNug@9-uk; z<2&m#0Um`s=B}_}9s&70Tv_~Va@WJ$n~s`7tVxi^s&_nPI0`QX=JnItlOu*Tn;T@> zXsVNAHd&K?*u~a@u8MWX17VaWuE0=6B93P2IQ{S$-WmT+Yp!9eA>@n~=s>?uDQ4*X zC(SxlKap@0R^z1p9C(VKM>nX8-|84nvIQJ-;9ei0qs{}X>?f%&E#%-)Bpv_p;s4R+ z;PMpG5*rvN&l;i{^~&wKnEhT!S!LQ>udPzta#Hc9)S8EUHK=%x+z@iq!O{)*XM}aI zBJE)vokFFXTeG<2Pq}5Na+kKnu?Ch|YoxdPb&Z{07nq!yzj0=xjzZj@3XvwLF0}Pa zn;x^HW504NNfLY~w!}5>`z=e{nzGB>t4ntE>R}r7*hJF3OoEx}&6LvZz4``m{AZxC zz6V+^73YbuY>6i9ulu)2`ozP(XBY5n$!kiAE_Vf4}Ih)tlOjgF3HW|DF+q-jI_0p%6Voc^e;g28* z;Sr4X{n(X7eEnACWRGNsHqQ_OfWhAHwnSQ87@PvPcpa!xr9`9+{QRn;bh^jgO8q@v zLekO@-cdc&eOKsvXs-eMCH8Y{*~3Iy!+CANy+(WXYS&6XB$&1+tB?!qcL@@) zS7XQ|5=o1fr8yM7r1AyAD~c@Mo`^i~hjx{N17%pDX?j@2bdBEbxY}YZxz!h#)q^1x zpc_RnoC3`V?L|G2R1QbR6pI{Am?yW?4Gy`G-xBYfebXvZ=(nTD7u?OEw>;vQICdPJBmi~;xhVV zisVvnE!bxI5|@IIlDRolo_^tc1{m)XTbIX^<{TQfsUA1Wv(KjJED^nj`r!JjEA%MaEGqPB z9YVt~ol3%e`PaqjZt&-)Fl^NeGmZ)nbL;92cOeLM2H*r-zA@d->H5T_8_;Jut0Q_G zBM2((-VHy2&eNkztIpHk&1H3M3@&wvvU9+$RO%fSEa_d5-qZ!<`-5?L9lQ1@AEpo* z3}Zz~R6&^i9KfRM8WGc6fTFD%PGdruE}`X$tP_*A)_7(uI5{k|LYc-WY*%GJ6JMmw zNBT%^E#IhekpA(i zcB$!EB}#>{^=G%rQ~2;gbObT9PQ{~aVx_W6?(j@)S$&Ja1s}aLT%A*mP}NiG5G93- z_DaRGP77PzLv0s32{UFm##C2LsU!w{vHdKTM1X)}W%OyZ&{3d^2Zu-zw?fT=+zi*q z^fu6CXQ!i?=ljsqSUzw>g#PMk>(^#ejrYp(C)7+@Z1=Mw$Rw!l8c9}+$Uz;9NUO(kCd#A1DX4Lbis0k; z?~pO(;@I6Ajp}PL;&`3+;OVkr3A^dQ(j?`by@A!qQam@_5(w6fG>PvhO`#P(y~2ue zW1BH_GqUY&>PggMhhi@8kAY;XWmj>y1M@c`0v+l~l0&~Kd8ZSg5#46wTLPo*Aom-5 z>qRXyWl}Yda=e@hJ%`x=?I42(B0lRiR~w>n6p8SHN~B6Y>W(MOxLpv>aB)E<1oEcw z%X;#DJpeDaD;CJRLX%u!t23F|cv0ZaE183LXxMq*uWn)cD_ zp!@i5zsmcxb!5uhp^@>U;K>$B|8U@3$65CmhuLlZ2(lF#hHq-<<+7ZN9m3-hFAPgA zKi;jMBa*59ficc#TRbH_l`2r>z(Bm_XEY}rAwyp~c8L>{A<0@Q)j*uXns^q5z~>KI z)43=nMhcU1ZaF;CaBo>hl6;@(2#9yXZ7_BwS4u>gN%SBS<;j{{+p}tbD8y_DFu1#0 zx)h&?`_`=ti_6L>VDH3>PPAc@?wg=Omdoip5j-2{$T;E9m)o2noyFW$5dXb{9CZ?c z);zf3U526r3Fl+{82!z)aHkZV6GM@%OKJB5mS~JcDjieFaVn}}M5rtPnHQVw0Stn- zEHs_gqfT8(0b-5ZCk1%1{QQaY3%b>wU z7lyE?lYGuPmB6jnMI6s$1uxN{Tf_n7H~nKu+h7=%60WK-C&kEIq_d4`wU(*~rJsW< zo^D$-(b0~uNVgC+$J3MUK)(>6*k?92mLgpod{Pd?{os+yHr&t+9ZgM*9;dCQBzE!V zk6e6)9U6Bq$^_`E1xd}d;5O8^6?@bK>QB&7l{vAy^P6FOEO^l7wK4K=lLA45gQ3$X z=$N{GR1{cxO)j;ZxKI*1kZIT9p>%FhoFbRK;M(m&bL?SaN zzkZS9xMf={o@gpG%wE857u@9dq>UKvbaM1SNtMA9EFOp7$BjJQVkIm$wU?-yOOs{i z1^(E(WwZZG{_#aIzfpGc@g5-AtK^?Q&vY#CtVpfLbW?g0{BEX4Vlk(`AO1{-D@31J zce}#=$?Gq+FZG-SD^z)-;wQg9`qEO}Dvo+S9*PUB*JcU)@S;UVIpN7rOqXmEIerWo zP_lk!@RQvyds&zF$Rt>N#_=!?5{XI`Dbo0<@>fIVgcU*9Y+ z)}K(Y&fdgve3ruT{WCNs$XtParmvV;rjr&R(V&_#?ob1LzO0RW3?8_kSw)bjom#0; zeNllfz(HlOJw012B}rgCUF5o|Xp#HLC~of%lg+!pr(g^n;wCX@Yk~SQOss!j9f(KL zDiI1h#k{po=Irl)8N*KU*6*n)A8&i9Wf#7;HUR^5*6+Bzh;I*1cICa|`&`e{pgrdc zs}ita0AXb$c6{tu&hxmT0faMG0GFc)unG8tssRJd%&?^62!_h_kn^HU_kBgp$bSew zqu)M3jTn;)tipv9Wt4Ll#1bmO2n?^)t^ZPxjveoOuK89$oy4(8Ujw{nd*Rs*<+xFi z{k*9v%sl?wS{aBSMMWdazhs0#gX9Has=pi?DhG&_0|cIyRG7c`OBiVG6W#JjYf7-n zIQU*Jc+SYnI8oG^Q8So9SP_-w;Y00$p5+LZ{l+81>v7|qa#Cn->312n=YQd$PaVz8 zL*s?ZU*t-RxoR~4I7e^c!8TA4g>w@R5F4JnEWJpy>|m5la2b#F4d*uoz!m=i1;`L` zB(f>1fAd~;*wf%GEbE8`EA>IO9o6TdgbIC%+en!}(C5PGYqS0{pa?PD)5?ds=j9{w za9^@WBXMZ|D&(yfc~)tnrDd#*;u;0?8=lh4%b-lFPR3ItwVJp};HMdEw#SXg>f-zU zEiaj5H=jzRSy(sWVd%hnLZE{SUj~$xk&TfheSch#23)YTcjrB+IVe0jJqsdz__n{- zC~7L`DG}-Dgrinzf7Jr)e&^tdQ}8v7F+~eF*<`~Vph=MIB|YxNEtLo1jXt#9#UG5` zQ$OSk`u!US+Z!=>dGL>%i#uV<5*F?pivBH@@1idFrzVAzttp5~>Y?D0LV;8Yv`wAa{hewVjlhhBM z_mJhU9yWz9Jexg@G~dq6EW5^nDXe(sU^5{}qbd0*yW2Xq6G37f8{{X&Z>G~dUGDFu zgmsDDZZ5ZmtiBw58CERFPrEG>*)*`_B75!MDsOoK`T1aJ4GZ1avI?Z3OX|Hg?P(xy zSPgO$alKZuXd=pHP6UZy0G>#BFm(np+dekv0l6gd=36FijlT8^kI5; zw?Z*FPsibF2d9T$_L@uX9iw*>y_w9HSh8c=Rm}f>%W+8OS=Hj_wsH-^actull3c@!z@R4NQ4qpytnwMaY z)>!;FUeY?h2N9tD(othc7Q=(dF zZAX&Y1ac1~0n(z}!9{J2kPPnru1?qteJPvA2m!@3Zh%+f1VQt~@leK^$&ZudOpS!+ zw#L0usf!?Df1tB?9=zPZ@q2sG!A#9 zKZL`2cs%|Jf}wG=_rJkwh|5Idb;&}z)JQuMVCZSH9kkG%zvQO01wBN)c4Q`*xnto3 zi7TscilQ>t_SLij{@Fepen*a(`upw#RJAx|JYYXvP1v8f)dTHv9pc3ZUwx!0tOH?c z^Hn=gfjUyo!;+3vZhxNE?LJgP`qYJ`J)umMXT@b z{nU(a^xFfofcxfHN-!Jn*{Dp5NZ&i9#9r{)s^lUFCzs5LQL9~HgxvmU#W|iNs0<3O z%Y2FEgvts4t({%lfX1uJ$w{JwfpV|HsO{ZDl2|Q$-Q?UJd`@SLBsMKGjFFrJ(s?t^ z2Llf`deAe@YaGJf)k2e&ryg*m8R|pcjct@rOXa=64#V9!sp=6tC#~QvYh&M~zmJ;% zr*A}V)Ka^3JE!1pcF5G}b&jdrt;bM^+J;G^#R08x@{|ZWy|547&L|k6)HLG|sN<~o z?y`%kbfRN_vc}pwS!Zr}*q6DG7;be0qmxn)eOcD%s3Wk`=@GM>U3ojhAW&WRppi0e zudTj{ufwO~H7izZJmLJD3uPHtjAJvo6H=)&SJ_2%qRRECN#HEU_RGa(Pefk*HIvOH zW7{=Tt(Q(LZ6&WX_Z9vpen}jqge|wCCaLYpiw@f_%9+-!l{kYi&gT@Cj#D*&rz1%e z@*b1W13bN8^j7IpAi$>`_0c!aVzLe*01DY-AcvwE;kW}=Z{3RJLR|O~^iOS(dNEnL zJJ?Dv^ab++s2v!4Oa_WFDLc4fMspglkh;+vzg)4;LS{%CR*>VwyP4>1Tly+!fA-k? z6$bg!*>wKtg!qGO6GQ=cAmM_RC&hKg$~(m2LdP{{*M+*OVf07P$OHp*4SSj9H;)1p z^b1_4p4@C;8G7cBCB6XC{i@vTB3#55iRBZiml^jc4sYnepCKUD+~k}TiuA;HWC6V3 zV{L5uUAU9CdoU+qsFszEwp;@d^!6XnX~KI|!o|=r?qhs`(-Y{GfO4^d6?8BC0xonf zKtZc1C@dNu$~+p#m%JW*J7alfz^$x`U~)1{c7svkIgQ3~RK2LZ5;2TAx=H<4AjC8{ z;)}8OfkZy7pSzVsdX|wzLe=SLg$W1+`Isf=o&}npxWdVR(i8Rr{uzE516a@28VhVr zVgZ3L&X(Q}J0R2{V(}bbNwCDD5K)<5h9CLM*~!xmGTl{Mq$@;~+|U*O#nc^oHnFOy z9Kz%AS*=iTBY_bSZAAY6wXCI?EaE>8^}WF@|}O@I#i69ljjWQPBJVk zQ_rt#J56_wGXiyItvAShJpLEMtW_)V5JZAuK#BAp6bV3K;IkS zK0AL(3ia99!vUPL#j>?<>mA~Q!mC@F-9I$9Z!96ZCSJO8FDz1SP3gF~m`1c#y!efq8QN}eHd+BHwtm%M5586jlU8&e!CmOC z^N_{YV$1`II$~cTxt*dV{-yp61nUuX5z?N8GNBuZZR}Uy_Y3_~@Y3db#~-&0TX644OuG^D3w_`?Yci{gTaPWST8`LdE)HK5OYv>a=6B%R zw|}>ngvSTE1rh`#1Rey0?LXTq;bCIy>TKm^CTV4BCSqdpx1pzC3^ca*S3fUBbKMzF z6X%OSdtt50)yJw*V_HE`hnBA)1yVN3Ruq3l@lY;%Bu+Q&hYLf_Z@fCUVQY-h4M3)- zE_G|moU)Ne0TMjhg?tscN7#ME6!Rb+y#Kd&-`!9gZ06o3I-VX1d4b1O=bpRG-tDK0 zSEa9y46s7QI%LmhbU3P`RO?w#FDM(}k8T`&>OCU3xD=s5N7}w$GntXF;?jdVfg5w9OR8VPxp5{uw zD+_;Gb}@7Vo_d3UV7PS65%_pBUeEwX_Hwfe2e6Qmyq$%0i8Ewn%F7i%=CNEV)Qg`r|&+$ zP6^Vl(MmgvFq`Zb715wYD>a#si;o+b4j^VuhuN>+sNOq6Qc~Y;Y=T&!Q4>(&^>Z6* zwliz!_16EDLTT;v$@W(s7s0s zi*%p>q#t)`S4j=Ox_IcjcllyT38C4hr&mlr6qX-c;qVa~k$MG;UqdnzKX0wo0Xe-_)b zrHu1&21O$y5828UIHI@N;}J@-9cpxob}zqO#!U%Q*ybZ?BH#~^fOT_|8&xAs_rX24 z^nqn{UWqR?MlY~klh)#Rz-*%&e~9agOg*fIN`P&v!@gcO25Mec23}PhzImkdwVT|@ zFR9dYYmf&HiUF4xO9@t#u=uTBS@k*97Z!&hu@|xQnQDkLd!*N`!0JN7{EUoH%OD85 z@aQ2(w-N)1_M{;FV)C#(a4p!ofIA3XG(XZ2E#%j_(=`IWlJAHWkYM2&(+yY|^2TB0 z>wfC-+I}`)LFOJ%KeBb1?eNxGKeq?AI_eBE!M~$wYR~bB)J3=WvVlT8ZlF2EzIFZt zkaeyj#vmBTGkIL9mM3cEz@Yf>j=82+KgvJ-u_{bBOxE5zoRNQW3+Ahx+eMGem|8xo zL3ORKxY_R{k=f~M5oi-Z>5fgqjEtzC&xJEDQ@`<)*Gh3UsftBJno-y5Je^!D?Im{j za*I>RQ=IvU@5WKsIr?kC$DT+2bgR>8rOf3mtXeMVB~sm%X7W5`s=Tp>FR544tuQ>9qLt|aUSv^io&z93luW$_OYE^sf8DB?gx z4&k;dHMWph>Z{iuhhFJr+PCZ#SiZ9e5xM$A#0yPtVC>yk&_b9I676n|oAH?VeTe*1 z@tDK}QM-%J^3Ns6=_vh*I8hE?+=6n9nUU`}EX|;Mkr?6@NXy8&B0i6h?7%D=%M*Er zivG61Wk7e=v;<%t*G+HKBqz{;0Biv7F+WxGirONRxJij zon5~(a`UR%uUzfEma99QGbIxD(d}~oa|exU5Y27#4k@N|=hE%Y?Y3H%rcT zHmNO#ZJ7nPHRG#y-(-FSzaZ2S{`itkdYY^ZUvyw<7yMBkNG+>$Rfm{iN!gz7eASN9-B3g%LIEyRev|3)kSl;JL zX7MaUL_@~4ot3$woD0UA49)wUeu7#lj77M4ar8+myvO$B5LZS$!-ZXw3w;l#0anYz zDc_RQ0Ome}_i+o~H=CkzEa&r~M$1GC!-~WBiHiDq9Sdg{m|G?o7g`R%f(Zvby5q4; z=cvn`M>RFO%i_S@h3^#3wImmWI4}2x4skPNL9Am{c!WxR_spQX3+;fo!y(&~Palyjt~Xo0uy6d%sX&I`e>zv6CRSm)rc^w!;Y6iVBb3x@Y=`hl9jft zXm5vilB4IhImY5b->x{!MIdCermpyLbsalx8;hIUia%*+WEo4<2yZ6`OyG1Wp%1s$ zh<|KrHMv~XJ9dC8&EXJ`t3ETz>a|zLMx|MyJE54RU(@?K&p2d#x?eJC*WKO9^d17# zdTTKx-Os3k%^=58Sz|J28aCJ}X2-?YV3T7ee?*FoDLOC214J4|^*EX`?cy%+7Kb3(@0@!Q?p zk>>6dWjF~y(eyRPqjXqDOT`4^Qv-%G#Zb2G?&LS-EmO|ixxt79JZlMgd^~j)7XYQ; z62rGGXA=gLfgy{M-%1gR87hbhxq-fL)GSfEAm{yLQP!~m-{4i_jG*JsvUdqAkoc#q6Yd&>=;4udAh#?xa2L z7mFvCjz(hN7eV&cyFb%(U*30H@bQ8-b7mkm!=wh2|;+_4vo=tyHPQ0hL=NR`jbsSiBWtG ztMPPBgHj(JTK#0VcP36Z`?P|AN~ybm=jNbU=^3dK=|rLE+40>w+MWQW%4gJ`>K!^- zx4kM*XZLd(E4WsolMCRsdvTGC=37FofIyCZCj{v3{wqy4OXX-dZl@g`Dv>p2`l|H^ zS_@(8)7gA62{Qfft>vx71stILMuyV4uKb7BbCstG@|e*KWl{P1$=1xg(7E8MRRCWQ1g)>|QPAZot~|FYz_J0T+r zTWTB3AatKyUsTXR7{Uu) z$1J5SSqoJWt(@@L5a)#Q6bj$KvuC->J-q1!nYS6K5&e7vNdtj- zj9;qwbODLgIcObqNRGs1l{8>&7W?BbDd!87=@YD75B2ep?IY|gE~t)$`?XJ45MG@2 zz|H}f?qtEb_p^Xs$4{?nA=Qko3Lc~WrAS`M%9N60FKqL7XI+v_5H-UDiCbRm`fEmv z$pMVH*#@wQqml~MZe+)e4Ts3Gl^!Z0W3y$;|9hI?9(iw29b7en0>Kt2pjFXk@!@-g zTb4}Kw!@u|V!wzk0|qM*zj$*-*}e*ZXs#Y<6E_!BR}3^YtjI_byo{F+w9H9?f%mnBh(uE~!Um7)tgp2Ye;XYdVD95qt1I-fc@X zXHM)BfJ?^g(s3K|{N8B^hamrWAW|zis$`6|iA>M-`0f+vq(FLWgC&KnBDsM)_ez1# zPCTfN8{s^K`_bum2i5SWOn)B7JB0tzH5blC?|x;N{|@ch(8Uy-O{B2)OsfB$q0@FR z27m3YkcVi$KL;;4I*S;Z#6VfZcZFn!D2Npv5pio)sz-`_H*#}ROd7*y4i(y(YlH<4 zh4MmqBe^QV_$)VvzWgMXFy`M(vzyR2u!xx&%&{^*AcVLrGa8J9ycbynjKR~G6zC0e zlEU>zt7yQtMhz>XMnz>ewXS#{Bulz$6HETn?qD5v3td>`qGD;Y8&RmkvN=24=^6Q@DYY zxMt}uh2cSToMkkIWo1_Lp^FOn$+47JXJ*#q=JaeiIBUHEw#IiXz8cStEsw{UYCA5v_%cF@#m^Y!=+qttuH4u}r6gMvO4EAvjBURtLf& z6k!C|OU@hv_!*qear3KJ?VzVXDKqvKRtugefa7^^MSWl0fXXZR$Xb!b6`eY4A1#pk zAVoZvb_4dZ{f~M8fk3o?{xno^znH1t;;E6K#9?erW~7cs%EV|h^K>@&3Im}c7nm%Y zbLozFrwM&tSNp|46)OhP%MJ(5PydzR>8)X%i3!^L%3HCoCF#Y0#9vPI5l&MK*_ z6G8Y>$`~c)VvQle_4L_AewDGh@!bKkJeEs_NTz(yilnM!t}7jz>fmJb89jQo6~)%% z@GNIJ@AShd&K%UdQ5vR#yT<-goR+D@Tg;PuvcZ*2AzSWN&wW$Xc+~vW)pww~O|6hL zBxX?hOyA~S;3rAEfI&jmMT4f!-eVm%n^KF_QT=>!A<5tgXgi~VNBXqsFI(iI$Tu3x0L{<_-%|HMG4Cn?Xs zq~fvBhu;SDOCD7K5(l&i7Py-;Czx5byV*3y%#-Of9rtz?M_owXc2}$OIY~)EZ&2?r zLQ(onz~I7U!w?B%LtfDz)*X=CscqH!UE=mO?d&oYvtj|(u)^yomS;Cd>Men|#2yuD zg&tf(*iSHyo;^A03p&_j*QXay9d}qZ0CgU@rnFNDIT5xLhC5_tlugv()+w%`7;ICf z>;<#L4m@{1}Og76*e zHWFm~;n@B1GqO8s%=qu)+^MR|jp(ULUOi~v;wE8SB6^mK@adSb=o+A_>Itjn13AF& zDZe+wUF9G!JFv|dpj1#d+}BO~s*QTe3381TxA%Q>P*J#z%( z5*8N^QWxgF73^cTKkkvgvIzf*cLEyyKw)Wf{#$n{uS#(rAA~>TS#!asqQ2m_izXe3 z7$Oh=rR;sdmVx3G)s}eImsb<@r2~5?vcw*Q4LU~FFh!y4r*>~S7slAE6)W3Up2OHr z2R)+O<0kKo<3+5vB}v!lB*`%}gFldc+79iahqEx#&Im@NCQU$@PyCZbcTt?K{;o@4 z312O9GB)?X&wAB}*-NEU zn@6`)G`FhT8O^=Cz3y+XtbwO{5+{4-&?z!esFts-C zypwgI^4#tZ74KC+_IW|E@kMI=1pSJkvg$9G3Va(!reMnJ$kcMiZ=30dTJ%(Ws>eUf z;|l--TFDqL!PZbLc_O(XP0QornpP;!)hdT#Ts7tZ9fcQeH&rhP_1L|Z_ha#JOroe^qcsLi`+AoBWHPM7}gD z+mHuPXd14M?nkp|nu9G8hPk;3=JXE-a204Fg!BK|$MX`k-qPeD$2OOqvF;C(l8wm13?>i(pz7kRyYm zM$IEzf`$}B%ezr!$(UO#uWExn%nTCTIZzq&8@i8sP#6r8 z*QMUzZV(LEWZb)wbmf|Li;UpiP;PlTQ(X4zreD`|`RG!7_wc6J^MFD!A=#K*ze>Jg z?9v?p(M=fg_VB0+c?!M$L>5FIfD(KD5ku*djwCp+5GVIs9^=}kM2RFsxx0_5DE%BF zykxwjWvs=rbi4xKIt!z$&v(`msFrl4n>a%NO_4`iSyb!UiAE&mDa+apc zPe)#!ToRW~rqi2e1bdO1RLN5*uUM@{S`KLJhhY-@TvC&5D(c?a(2$mW-&N%h5IfEM zdFI6`6KJiJQIHvFiG-34^BtO3%*$(-Ht_JU*(KddiUYoM{coadlG&LVvke&*p>Cac z^BPy2Zteiq1@ulw0e)e*ot7@A$RJui0$l^{lsCt%R;$){>zuRv9#w@;m=#d%%TJmm zC#%eFOoy$V)|3*d<OC1iP+4R7D z8FE$E8l2Y?(o-i6wG=BKBh0-I?i3WF%hqdD7VCd;vpk|LFP!Et8$@voH>l>U8BY`Q zC*G;&y6|!p=7`G$*+hxCv!@^#+QD3m>^azyZoLS^;o_|plQaj-wx^ zRV&$HcY~p)2|Zqp0SYU?W3zV87s6JP-@D~$t0 zvd;-YL~JWc*8mtHz_s(cXus#XYJc5zdC=&!4MeZ;N3TQ>^I|Pd=HPjVP*j^45rs(n zzB{U4-44=oQ4rNN6@>qYVMH4|GmMIz#z@3UW-1_y#eNa+Q%(41oJ5i(DzvMO^%|?L z^r_+MZtw0DZ0=BT-@?hUtA)Ijk~Kh-N8?~X5%KnRH7cb!?Yrd8gtiEo!v{sGrQk{X zvV>h{8-DqTyuAxIE(hb}jMVtga$;FIrrKm>ye5t%M;p!jcH1(Bbux>4D#MVhgZGd> z=c=nVb%^9T?iDgM&9G(mV5xShc-lBLi*6RShenDqB%`-2;I*;IHg6>#ovKQ$M}dDb z<$USN%LMqa5_5DR7g7@(oAoQ%!~<1KSQr$rmS{UFQJs5&qBhgTEM_Y7|0Wv?fbP`z z)`8~=v;B)+>Jh`V*|$dTxKe`HTBkho^-!!K#@i{9FLn-XqX&fQcGsEAXp)BV7(`Lk zC{4&+Pe-0&<)C0kAa(MTnb|L;ZB5i|b#L1o;J)+?SV8T*U9$Vxhy}dm3%!A}SK9l_6(#5(e*>8|;4gNKk7o_%m_ zEaS=Z(ewk}hBJ>v`jtR=$pm_Wq3d&DU+6`BACU4%qdhH1o^m8hT2&j<4Z8!v=rMCk z-I*?48{2H*&+r<{2?wp$kh@L@=rj8c`EaS~J>W?)trc?zP&4bsNagS4yafuDoXpi5`!{BVqJ1$ZC3`pf$`LIZ(`0&Ik+!_Xa=NJW`R2 zd#Ntgwz`JVwC4A61$FZ&kP)-{T|rGO59`h#1enAa`cWxRR8bKVvvN6jBzAYePrc&5 z+*zr3en|LYB2>qJp479rEALk5d*X-dfKn6|kuNm;2-U2+P3_rma!nWjZQ-y*q3JS? zBE}zE-!1ZBR~G%v!$l#dZ*$UV4$7q}xct}=on+Ba8{b>Y9h*f-GW0D0o#vJ0%ALg( ztG2+AjWlG#d;myA(i&dh8Gp?y9HD@`CTaDAy?c&0unZ%*LbLIg4;m{Kc?)ws3^>M+ zt5>R)%KIJV*MRUg{0$#nW=Lj{#8?dD$yhjBOrAeR#4$H_Dc(eyA4dNjZEz1Xk+Bqt zB&pPl+?R{w8GPv%VI`x`IFOj320F1=cV4aq0(*()Tx!VVxCjua;)t}gTr=b?zY+U! zkb}xjXZ?hMJN{Hjw?w&?gz8Ow`htX z@}WG*_4<%ff8(!S6bf3)p+8h2!Rory>@aob$gY#fYJ=LiW0`+~l7GI%EX_=8 z{(;0&lJ%9)M9{;wty=XvHbIx|-$g4HFij`J$-z~`mW)*IK^MWVN+*>uTNqaDmi!M8 zurj6DGd)g1g(f`A-K^v)3KSOEoZXImXT06apJum-dO_%oR)z6Bam-QC&CNWh7kLOE zcxLdVjYLNO2V?IXWa-ys30Jbxw(Xm?U1{4kDs9`gZQHh8X{*w9=H&Zz&-6RL?uq#R zxN+k~JaL|gdsdvY_u6}}MHC?a@ElFeipA1Lud#M~)pp2SnG#K{a@tSpvXM;A8gz9> zRVDV5T1%%!LsNRDOw~LIuiAiKcj<%7WpgjP7G6mMU1#pFo6a-1>0I5ZdhxnkMX&#L z=Vm}?SDlb_LArobqpnU!WLQE*yVGWgs^4RRy4rrJwoUUWoA~ZJUx$mK>J6}7{CyC4 zv=8W)kKl7TmAnM%m;anEDPv5tzT{A{ON9#FPYF6c=QIc*OrPp96tiY&^Qs+#A1H>Y z<{XtWt2eDwuqM zQ_BI#UIP;2-olOL4LsZ`vTPv-eILtuB7oWosoSefWdM}BcP>iH^HmimR`G`|+9waCO z&M375o@;_My(qYvPNz;N8FBZaoaw3$b#x`yTBJLc8iIP z--la{bzK>YPP|@Mke!{Km{vT8Z4|#An*f=EmL34?!GJfHaDS#41j~8c5KGKmj!GTh&QIH+DjEI*BdbSS2~6VTt}t zhAwNQNT6%c{G`If3?|~Fp7iwee(LaUS)X9@I29cIb61} z$@YBq4hSplr&liE@ye!y&7+7n$fb+8nS~co#^n@oCjCwuKD61x$5|0ShDxhQES5MP z(gH|FO-s6#$++AxnkQR!3YMgKcF)!&aqr^a3^{gAVT`(tY9@tqgY7@ z>>ul3LYy`R({OY7*^Mf}UgJl(N7yyo$ag;RIpYHa_^HKx?DD`%Vf1D0s^ zjk#OCM5oSzuEz(7X`5u~C-Y~n4B}_3*`5B&8tEdND@&h;H{R`o%IFpIJ4~Kw!kUjehGT8W!CD7?d8sg_$KKp%@*dW)#fI1#R<}kvzBVpaog_2&W%c_jJfP` z6)wE+$3+Hdn^4G}(ymPyasc1<*a7s2yL%=3LgtZLXGuA^jdM^{`KDb%%}lr|ONDsl zy~~jEuK|XJ2y<`R{^F)Gx7DJVMvpT>gF<4O%$cbsJqK1;v@GKXm*9l3*~8^_xj*Gs z=Z#2VQ6`H@^~#5Pv##@CddHfm;lbxiQnqy7AYEH(35pTg^;u&J2xs-F#jGLuDw2%z z`a>=0sVMM+oKx4%OnC9zWdbpq*#5^yM;og*EQKpv`^n~-mO_vj=EgFxYnga(7jO?G z`^C87B4-jfB_RgN2FP|IrjOi;W9AM1qS}9W@&1a9Us>PKFQ9~YE!I~wTbl!m3$Th? z)~GjFxmhyyGxN}t*G#1^KGVXm#o(K0xJyverPe}mS=QgJ$#D}emQDw+dHyPu^&Uv> z4O=3gK*HLFZPBY|!VGq60Of6QrAdj`nj1h!$?&a;Hgaj{oo{l0P3TzpJK_q_eW8Ng zP6QF}1{V;xlolCs?pGegPoCSxx@bshb#3ng4Fkp4!7B0=&+1%187izf@}tvsjZ6{m z4;K>sR5rm97HJrJ`w}Y`-MZN$Wv2N%X4KW(N$v2@R1RkRJH2q1Ozs0H`@ zd5)X-{!{<+4Nyd=hQ8Wm3CCd}ujm*a?L79ztfT7@&(?B|!pU5&%9Rl!`i;suAg0+A zxb&UYpo-z}u6CLIndtH~C|yz&!OV_I*L;H#C7ie_5uB1fNRyH*<^d=ww=gxvE%P$p zRHKI{^{nQlB9nLhp9yj-so1is{4^`{Xd>Jl&;dX;J)#- z=fmE5GiV?-&3kcjM1+XG7&tSq;q9Oi4NUuRrIpoyp*Fn&nVNFdUuGQ_g)g>VzXGdneB7`;!aTUE$t* z5iH+8XPxrYl)vFo~+vmcU-2) zq!6R(T0SsoDnB>Mmvr^k*{34_BAK+I=DAGu){p)(ndZqOFT%%^_y;X(w3q-L``N<6 zw9=M zoQ8Lyp>L_j$T20UUUCzYn2-xdN}{e@$8-3vLDN?GbfJ>7*qky{n!wC#1NcYQr~d51 zy;H!am=EI#*S&TCuP{FA3CO)b0AAiN*tLnDbvKwxtMw-l;G2T@EGH)YU?-B`+Y=!$ zypvDn@5V1Tr~y~U0s$ee2+CL3xm_BmxD3w}d_Pd@S%ft#v~_j;6sC6cy%E|dJy@wj z`+(YSh2CrXMxI;yVy*=O@DE2~i5$>nuzZ$wYHs$y`TAtB-ck4fQ!B8a;M=CxY^Nf{ z+UQhn0jopOzvbl(uZZ1R-(IFaprC$9hYK~b=57@ zAJ8*pH%|Tjotzu5(oxZyCQ{5MAw+6L4)NI!9H&XM$Eui-DIoDa@GpNI=I4}m>Hr^r zZjT?xDOea}7cq+TP#wK1p3}sbMK{BV%(h`?R#zNGIP+7u@dV5#zyMau+w}VC1uQ@p zrFUjrJAx6+9%pMhv(IOT52}Dq{B9njh_R`>&j&5Sbub&r*hf4es)_^FTYdDX$8NRk zMi=%I`)hN@N9>X&Gu2RmjKVsUbU>TRUM`gwd?CrL*0zxu-g#uNNnnicYw=kZ{7Vz3 zULaFQ)H=7%Lm5|Z#k?<{ux{o4T{v-e zTLj?F(_qp{FXUzOfJxEyKO15Nr!LQYHF&^jMMBs z`P-}WCyUYIv>K`~)oP$Z85zZr4gw>%aug1V1A)1H(r!8l&5J?ia1x_}Wh)FXTxZUE zs=kI}Ix2cK%Bi_Hc4?mF^m`sr6m8M(n?E+k7Tm^Gn}Kf= zfnqoyVU^*yLypz?s+-XV5(*oOBwn-uhwco5b(@B(hD|vtT8y7#W{>RomA_KchB&Cd zcFNAD9mmqR<341sq+j+2Ra}N5-3wx5IZqg6Wmi6CNO#pLvYPGNER}Q8+PjvIJ42|n zc5r@T*p)R^U=d{cT2AszQcC6SkWiE|hdK)m{7ul^mU+ED1R8G#)#X}A9JSP_ubF5p z8Xxcl;jlGjPwow^p+-f_-a~S;$lztguPE6SceeUCfmRo=Qg zKHTY*O_ z;pXl@z&7hniVYVbGgp+Nj#XP^Aln2T!D*{(Td8h{8Dc?C)KFfjPybiC`Va?Rf)X>y z;5?B{bAhPtbmOMUsAy2Y0RNDQ3K`v`gq)#ns_C&ec-)6cq)d^{5938T`Sr@|7nLl; zcyewuiSUh7Z}q8iIJ@$)L3)m)(D|MbJm_h&tj^;iNk%7K-YR}+J|S?KR|29K?z-$c z<+C4uA43yfSWBv*%z=-0lI{ev`C6JxJ};A5N;lmoR(g{4cjCEn33 z-ef#x^uc%cM-f^_+*dzE?U;5EtEe;&8EOK^K}xITa?GH`tz2F9N$O5;)`Uof4~l+t z#n_M(KkcVP*yMYlk_~5h89o zlf#^qjYG8Wovx+f%x7M7_>@r7xaXa2uXb?_*=QOEe_>ErS(v5-i)mrT3&^`Oqr4c9 zDjP_6T&NQMD`{l#K&sHTm@;}ed_sQ88X3y`ON<=$<8Qq{dOPA&WAc2>EQ+U8%>yWR zK%(whl8tB;{C)yRw|@Gn4%RhT=bbpgMZ6erACc>l5^p)9tR`(2W-D*?Ph6;2=Fr|G- zdF^R&aCqyxqWy#P7#G8>+aUG`pP*ow93N=A?pA=aW0^^+?~#zRWcf_zlKL8q8-80n zqGUm=S8+%4_LA7qrV4Eq{FHm9#9X15%ld`@UKyR7uc1X*>Ebr0+2yCye6b?i=r{MPoqnTnYnq z^?HWgl+G&@OcVx4$(y;{m^TkB5Tnhx2O%yPI=r*4H2f_6Gfyasq&PN^W{#)_Gu7e= zVHBQ8R5W6j;N6P3O(jsRU;hkmLG(Xs_8=F&xh@`*|l{~0OjUVlgm z7opltSHg7Mb%mYamGs*v1-#iW^QMT**f+Nq*AzIvFT~Ur3KTD26OhIw1WQsL(6nGg znHUo-4e15cXBIiyqN};5ydNYJ6zznECVVR44%(P0oW!yQ!YH)FPY?^k{IrtrLo7Zo`?sg%%oMP9E^+H@JLXicr zi?eoI?LODRPcMLl90MH32rf8btf69)ZE~&4d%(&D{C45egC6bF-XQ;6QKkbmqW>_H z{86XDZvjiN2wr&ZPfi;^SM6W+IP0);50m>qBhzx+docpBkkiY@2bSvtPVj~E`CfEu zhQG5G>~J@dni5M5Jmv7GD&@%UR`k3ru-W$$onI259jM&nZ)*d3QFF?Mu?{`+nVzkx z=R*_VH=;yeU?9TzQ3dP)q;P)4sAo&k;{*Eky1+Z!10J<(cJC3zY9>bP=znA=<-0RR zMnt#<9^X7BQ0wKVBV{}oaV=?JA=>R0$az^XE%4WZcA^Em>`m_obQyKbmf-GA;!S-z zK5+y5{xbkdA?2NgZ0MQYF-cfOwV0?3Tzh8tcBE{u%Uy?Ky4^tn^>X}p>4&S(L7amF zpWEio8VBNeZ=l!%RY>oVGOtZh7<>v3?`NcHlYDPUBRzgg z0OXEivCkw<>F(>1x@Zk=IbSOn+frQ^+jI*&qdtf4bbydk-jgVmLAd?5ImK+Sigh?X zgaGUlbf^b-MH2@QbqCawa$H1Vb+uhu{zUG9268pa{5>O&Vq8__Xk5LXDaR1z$g;s~;+Ae82wq#l;wo08tX(9uUX6NJWq1vZLh3QbP$# zL`udY|Qp*4ER`_;$%)2 zmcJLj|FD`(;ts0bD{}Ghq6UAVpEm#>j`S$wHi0-D_|)bEZ}#6) zIiqH7Co;TB`<6KrZi1SF9=lO+>-_3=Hm%Rr7|Zu-EzWLSF{9d(H1v*|UZDWiiqX3} zmx~oQ6%9~$=KjPV_ejzz7aPSvTo+3@-a(OCCoF_u#2dHY&I?`nk zQ@t8#epxAv@t=RUM09u?qnPr6=Y5Pj;^4=7GJ`2)Oq~H)2V)M1sC^S;w?hOB|0zXT zQdf8$)jslO>Q}(4RQ$DPUF#QUJm-k9ysZFEGi9xN*_KqCs9Ng(&<;XONBDe1Joku? z*W!lx(i&gvfXZ4U(AE@)c0FI2UqrFLOO$&Yic|`L;Vyy-kcm49hJ^Mj^H9uY8Fdm2 z?=U1U_5GE_JT;Tx$2#I3rAAs(q@oebIK=19a$N?HNQ4jw0ljtyGJ#D}z3^^Y=hf^Bb--297h6LQxi0-`TB|QY2QPg92TAq$cEQdWE ze)ltSTVMYe0K4wte6;^tE+^>|a>Hit_3QDlFo!3Jd`GQYTwlR#{<^MzG zK!vW&))~RTKq4u29bc<+VOcg7fdorq-kwHaaCQe6tLB{|gW1_W_KtgOD0^$^|`V4C# z*D_S9Dt_DIxpjk3my5cBFdiYaq||#0&0&%_LEN}BOxkb3v*d$4L|S|z z!cZZmfe~_Y`46v=zul=aixZTQCOzb(jx>8&a%S%!(;x{M2!*$od2!Pwfs>RZ-a%GOZdO88rS)ZW~{$656GgW)$Q=@!x;&Nn~!K)lr4gF*%qVO=hlodHA@2)keS2 zC}7O=_64#g&=zY?(zhzFO3)f5=+`dpuyM!Q)zS&otpYB@hhn$lm*iK2DRt+#1n|L%zjM}nB*$uAY^2JIw zV_P)*HCVq%F))^)iaZD#R9n^{sAxBZ?Yvi1SVc*`;8|F2X%bz^+s=yS&AXjysDny)YaU5RMotF-tt~FndTK ziRve_5b!``^ZRLG_ks}y_ye0PKyKQSsQCJuK5()b2ThnKPFU?An4;dK>)T^4J+XjD zEUsW~H?Q&l%K4<1f5^?|?lyCQe(O3?!~OU{_Wxs#|Ff8?a_WPQUKvP7?>1()Cy6oLeA zjEF^d#$6Wb${opCc^%%DjOjll%N2=GeS6D-w=Ap$Ux2+0v#s#Z&s6K*)_h{KFfgKjzO17@p1nKcC4NIgt+3t}&}F z@cV; zZ1r#~?R@ZdSwbFNV(fFl2lWI(Zf#nxa<6f!nBZD>*K)nI&Fun@ngq@Ge!N$O< zySt*mY&0moUXNPe~Fg=%gIu)tJ;asscQ!-AujR@VJBRoNZNk;z4hs4T>Ud!y=1NwGs-k zlTNeBOe}=)Epw=}+dfX;kZ32h$t&7q%Xqdt-&tlYEWc>>c3(hVylsG{Ybh_M8>Cz0ZT_6B|3!_(RwEJus9{;u-mq zW|!`{BCtnao4;kCT8cr@yeV~#rf76=%QQs(J{>Mj?>aISwp3{^BjBO zLV>XSRK+o=oVDBnbv?Y@iK)MiFSl{5HLN@k%SQZ}yhPiu_2jrnI?Kk?HtCv>wN$OM zSe#}2@He9bDZ27hX_fZey=64#SNU#1~=icK`D>a;V-&Km>V6ZdVNj7d2 z-NmAoOQm_aIZ2lXpJhlUeJ95eZt~4_S zIfrDs)S$4UjyxKSaTi#9KGs2P zfSD>(y~r+bU4*#|r`q+be_dopJzKK5JNJ#rR978ikHyJKD>SD@^Bk$~D0*U38Y*IpYcH>aaMdZq|YzQ-Ixd(_KZK!+VL@MWGl zG!k=<%Y-KeqK%``uhx}0#X^@wS+mX@6Ul@90#nmYaKh}?uw>U;GS4fn3|X%AcV@iY z8v+ePk)HxSQ7ZYDtlYj#zJ?5uJ8CeCg3efmc#|a%2=u>+vrGGRg$S@^mk~0f;mIu! zWMA13H1<@hSOVE*o0S5D8y=}RiL#jQpUq42D}vW$z*)VB*FB%C?wl%(3>ANaY)bO@ zW$VFutemwy5Q*&*9HJ603;mJJkB$qp6yxNOY0o_4*y?2`qbN{m&*l{)YMG_QHXXa2 z+hTmlA;=mYwg{Bfusl zyF&}ib2J;#q5tN^e)D62fWW*Lv;Rnb3GO-JVtYG0CgR4jGujFo$Waw zSNLhc{>P~>{KVZE1Vl1!z)|HFuN@J7{`xIp_)6>*5Z27BHg6QIgqLqDJTmKDM+ON* zK0Fh=EG`q13l z+m--9UH0{ZGQ%j=OLO8G2WM*tgfY}bV~>3Grcrpehjj z6Xe<$gNJyD8td3EhkHjpKk}7?k55Tu7?#;5`Qcm~ki;BeOlNr+#PK{kjV>qfE?1No zMA07}b>}Dv!uaS8Hym0TgzxBxh$*RX+Fab6Gm02!mr6u}f$_G4C|^GSXJMniy^b`G z74OC=83m0G7L_dS99qv3a0BU({t$zHQsB-RI_jn1^uK9ka_%aQuE2+~J2o!7`735Z zb?+sTe}Gd??VEkz|KAPMfj(1b{om89p5GIJ^#Aics_6DD%WnNGWAW`I<7jT|Af|8g zZA0^)`p8i#oBvX2|I&`HC8Pn&0>jRuMF4i0s=}2NYLmgkZb=0w9tvpnGiU-gTUQhJ zR6o4W6ZWONuBZAiN77#7;TR1^RKE(>>OL>YU`Yy_;5oj<*}ac99DI(qGCtn6`949f ziMpY4k>$aVfffm{dNH=-=rMg|u?&GIToq-u;@1-W&B2(UOhC-O2N5_px&cF-C^tWp zXvChm9@GXEcxd;+Q6}u;TKy}$JF$B`Ty?|Y3tP$N@Rtoy(*05Wj-Ks32|2y2ZM>bM zi8v8E1os!yorR!FSeP)QxtjIKh=F1ElfR8U7StE#Ika;h{q?b?Q+>%78z^>gTU5+> zxQ$a^rECmETF@Jl8fg>MApu>btHGJ*Q99(tMqsZcG+dZ6Yikx7@V09jWCiQH&nnAv zY)4iR$Ro223F+c3Q%KPyP9^iyzZsP%R%-i^MKxmXQHnW6#6n7%VD{gG$E;7*g86G< zu$h=RN_L2(YHO3@`B<^L(q@^W_0#U%mLC9Q^XEo3LTp*~(I%?P_klu-c~WJxY1zTI z^PqntLIEmdtK~E-v8yc&%U+jVxW5VuA{VMA4Ru1sk#*Srj0Pk#tZuXxkS=5H9?8eb z)t38?JNdP@#xb*yn=<*_pK9^lx%;&yH6XkD6-JXgdddZty8@Mfr9UpGE!I<37ZHUe z_Rd+LKsNH^O)+NW8Ni-V%`@J_QGKA9ZCAMSnsN>Ych9VW zCE7R_1FVy}r@MlkbxZ*TRIGXu`ema##OkqCM9{wkWQJg^%3H${!vUT&vv2250jAWN zw=h)C!b2s`QbWhBMSIYmWqZ_~ReRW;)U#@C&ThctSd_V!=HA=kdGO-Hl57an|M1XC?~3f0{7pyjWY}0mChU z2Fj2(B*r(UpCKm-#(2(ZJD#Y|Or*Vc5VyLpJ8gO1;fCm@EM~{DqpJS5FaZ5%|ALw) zyumBl!i@T57I4ITCFmdbxhaOYud}i!0YkdiNRaQ%5$T5>*HRBhyB~<%-5nj*b8=i= z(8g(LA50%0Zi_eQe}Xypk|bt5e6X{aI^jU2*c?!p*$bGk=?t z+17R){lx~Z{!B34Zip~|A;8l@%*Gc}kT|kC0*Ny$&fI3@%M! zqk_zvN}7bM`x@jqFOtaxI?*^Im5ix@=`QEv;__i;Tek-&7kGm6yP17QANVL>*d0B=4>i^;HKb$k8?DYFMr38IX4azK zBbwjF%$>PqXhJh=*7{zH5=+gi$!nc%SqFZlwRm zmpctOjZh3bwt!Oc>qVJhWQf>`HTwMH2ibK^eE*j!&Z`-bs8=A`Yvnb^?p;5+U=Fb8 z@h>j_3hhazd$y^Z-bt%3%E3vica%nYnLxW+4+?w{%|M_=w^04U{a6^22>M_?{@mXP zS|Qjcn4&F%WN7Z?u&I3fU(UQVw4msFehxR*80dSb=a&UG4zDQp&?r2UGPy@G?0FbY zVUQ?uU9-c;f9z06$O5FO1TOn|P{pLcDGP?rfdt`&uw|(Pm@$n+A?)8 zP$nG(VG&aRU*(_5z#{+yVnntu`6tEq>%9~n^*ao}`F6ph_@6_8|AfAXtFfWee_14` zKKURYV}4}=UJmxv7{RSz5QlwZtzbYQs0;t3?kx*7S%nf-aY&lJ@h?-BAn%~0&&@j) zQd_6TUOLXErJ`A3vE?DJIbLE;s~s%eVt(%fMzUq^UfZV9c?YuhO&6pwKt>j(=2CkgTNEq7&c zfeGN+%5DS@b9HO>zsoRXv@}(EiA|t5LPi}*R3?(-=iASADny<{D0WiQG>*-BSROk4vI6%$R>q64J&v-T+(D<_(b!LD z9GL;DV;;N3!pZYg23mcg81tx>7)=e%f|i{6Mx0GczVpc}{}Mg(W_^=Wh0Rp+xXgX` z@hw|5=Je&nz^Xa>>vclstYt;8c2PY)87Ap;z&S&`yRN>yQVV#K{4&diVR7Rm;S{6m z6<+;jwbm`==`JuC6--u6W7A@o4&ZpJV%5+H)}toy0afF*!)AaG5=pz_i9}@OG%?$O z2cec6#@=%xE3K8;^ps<2{t4SnqH+#607gAHP-G4^+PBiC1s>MXf&bQ|Pa;WBIiErV z?3VFpR9JFl9(W$7p3#xe(Bd?Z93Uu~jHJFo7U3K_x4Ej-=N#=a@f;kPV$>;hiN9i9 z<6elJl?bLI$o=|d6jlihA4~bG;Fm2eEnlGxZL`#H%Cdes>uJfMJ4>@1SGGeQ81DwxGxy7L5 zm05Ik*WpSgZvHh@Wpv|2i|Y#FG?Y$hbRM5ZF0Z7FB3cY0+ei#km9mDSPI}^!<<`vr zuv$SPg2vU{wa)6&QMY)h1hbbxvR2cc_6WcWR`SH& z&KuUQcgu}!iW2Wqvp~|&&LSec9>t(UR_|f$;f-fC&tSO-^-eE0B~Frttnf+XN(#T) z^PsuFV#(pE#6ztaI8(;ywN%CtZh?w&;_)w_s@{JiA-SMjf&pQk+Bw<}f@Q8-xCQMwfaf zMgHsAPU=>>Kw~uDFS(IVRN{$ak(SV(hrO!UqhJ?l{lNnA1>U24!=>|q_p404Xd>M# z7?lh^C&-IfeIr`Dri9If+bc%oU0?|Rh8)%BND5;_9@9tuM)h5Kcw6}$Ca7H_n)nOf0pd`boCXItb`o11 zb`)@}l6I_h>n+;`g+b^RkYs7;voBz&Gv6FLmyvY|2pS)z#P;t8k;lS>49a$XeVDc4 z(tx2Pe3N%Gd(!wM`E7WRBZy)~vh_vRGt&esDa0NCua)rH#_39*H0!gIXpd>~{rGx+ zJKAeXAZ-z5n=mMVqlM5Km;b;B&KSJlScD8n?2t}kS4Wf9@MjIZSJ2R?&=zQn zs_`=+5J$47&mP4s{Y{TU=~O_LzSrXvEP6W?^pz<#Y*6Fxg@$yUGp31d(h+4x>xpb< zH+R639oDST6F*0iH<9NHC^Ep*8D4-%p2^n-kD6YEI<6GYta6-I;V^ZH3n5}syTD=P z3b6z=jBsdP=FlXcUe@I|%=tY4J_2j!EVNEzph_42iO3yfir|Dh>nFl&Lu9!;`!zJB zCis9?_(%DI?$CA(00pkzw^Up`O;>AnPc(uE$C^a9868t$m?5Q)CR%!crI$YZpiYK6m= z!jv}82He`QKF;10{9@roL2Q7CF)OeY{~dBp>J~X#c-Z~{YLAxNmn~kWQW|2u!Yq00 zl5LKbzl39sVCTpm9eDW_T>Z{x@s6#RH|P zA~_lYas7B@SqI`N=>x50Vj@S)QxouKC(f6Aj zz}7e5e*5n?j@GO;mCYEo^Jp_*BmLt3!N)(T>f#L$XHQWzZEVlJo(>qH@7;c%fy zS-jm^Adju9Sm8rOKTxfTU^!&bg2R!7C_-t+#mKb_K?0R72%26ASF;JWA_prJ8_SVW zOSC7C&CpSrgfXRp8r)QK34g<~!1|poTS7F;)NseFsbwO$YfzEeG3oo!qe#iSxQ2S# z1=Fxc9J;2)pCab-9o-m8%BLjf(*mk#JJX3k9}S7Oq)dV0jG)SOMbw7V^Z<5Q0Cy$< z^U0QUVd4(96W03OA1j|x%{sd&BRqIERDb6W{u1p1{J(a;fd6lnWzjeS`d?L3-0#o7 z{Qv&L7!Tm`9|}u=|IbwS_jgH(_V@o`S*R(-XC$O)DVwF~B&5c~m!zl14ydT6sK+Ly zn+}2hQ4RTC^8YvrQ~vk$f9u=pTN{5H_yTOcza9SVE&nt_{`ZC8zkmFji=UyD`G4~f zUfSTR=Kju>6u+y&|Bylb*W&^P|8fvEbQH3+w*DrKq|9xMzq2OiZyM=;(?>~4+O|jn zC_Et05oc>e%}w4ye2Fm%RIR??VvofwZS-}BL@X=_4jdHp}FlMhW_IW?Zh`4$z*Wr!IzQHa3^?1|);~VaWmsIcmc6 zJs{k0YW}OpkfdoTtr4?9F6IX6$!>hhA+^y_y@vvA_Gr7u8T+i-< zDX(~W5W{8mfbbM-en&U%{mINU#Q8GA`byo)iLF7rMVU#wXXY`a3ji3m{4;x53216i z`zA8ap?>_}`tQj7-%$K78uR}R$|@C2)qgop$}o=g(jOv0ishl!E(R73N=i0~%S)6+ z1xFP7|H0yt3Z_Re*_#C2m3_X{=zi1C&3CM7e?9-Y5lCtAlA%RFG9PDD=Quw1dfYnZ zdUL)#+m`hKx@PT`r;mIx_RQ6Txbti+&;xQorP;$H=R2r)gPMO9>l+!p*Mt04VH$$M zSLwJ81IFjQ5N!S#;MyBD^IS`2n04kuYbZ2~4%3%tp0jn^**BZQ05ELp zY%yntZ=52s6U5Y93Aao)v~M3y?6h7mZcVGp63pK*d&!TRjW99rUU;@s#3kYB76Bs$|LRwkH>L!0Xe zE=dz1o}phhnOVYZFsajQsRA^}IYZnk9Wehvo>gHPA=TPI?2A`plIm8=F1%QiHx*Zn zi)*Y@)$aXW0v1J|#+R2=$ysooHZ&NoA|Wa}htd`=Eud!(HD7JlT8ug|yeBZmpry(W z)pS>^1$N#nuo3PnK*>Thmaxz4pLcY?PP2r3AlhJ7jw(TI8V#c}>Ym;$iPaw+83L+* z!_QWpYs{UWYcl0u z(&(bT0Q*S_uUX9$jC;Vk%oUXw=A-1I+!c18ij1CiUlP@pfP9}CHAVm{!P6AEJ(7Dn z?}u#}g`Q?`*|*_0Rrnu8{l4PP?yCI28qC~&zlwgLH2AkfQt1?B#3AOQjW&10%@@)Q zDG?`6$8?Nz(-sChL8mRs#3z^uOA>~G=ZIG*mgUibWmgd{a|Tn4nkRK9O^37E(()Q% zPR0#M4e2Q-)>}RSt1^UOCGuv?dn|IT3#oW_$S(YR+jxAzxCD_L25p_dt|^>g+6Kgj zJhC8n)@wY;Y7JI6?wjU$MQU|_Gw*FIC)x~^Eq1k41BjLmr}U>6#_wxP0-2Ka?uK14u5M-lAFSX$K1K{WH!M1&q}((MWWUp#Uhl#n_yT5dFs4X`>vmM& z*1!p0lACUVqp&sZG1GWATvZEENs^0_7Ymwem~PlFN3hTHVBv(sDuP;+8iH07a)s(# z%a7+p1QM)YkS7>kbo${k2N1&*%jFP*7UABJ2d||c!eSXWM*<4(_uD7;1XFDod@cT$ zP>IC%^fbC${^QrUXy$f)yBwY^g@}}kngZKa1US!lAa+D=G4wklukaY8AEW%GL zh40pnuv*6D>9`_e14@wWD^o#JvxYVG-~P)+<)0fW zP()DuJN?O*3+Ab!CP-tGr8S4;JN-Ye^9D%(%8d{vb_pK#S1z)nZzE^ezD&%L6nYbZ z*62>?u)xQe(Akd=e?vZbyb5)MMNS?RheZDHU?HK<9;PBHdC~r{MvF__%T)-9ifM#cR#2~BjVJYbA>xbPyl9yNX zX)iFVvv-lfm`d?tbfh^j*A|nw)RszyD<#e>llO8X zou=q3$1|M@Ob;F|o4H0554`&y9T&QTa3{yn=w0BLN~l;XhoslF-$4KGNUdRe?-lcV zS4_WmftU*XpP}*wFM^oKT!D%_$HMT#V*j;9weoOq0mjbl1271$F)`Q(C z76*PAw3_TE{vntIkd=|(zw)j^!@j ^tV@s0U~V+mu)vv`xgL$Z9NQLnuRdZ;95D|1)!0Aybwv}XCE#xz1k?ZC zxAU)v@!$Sm*?)t2mWrkevNFbILU9&znoek=d7jn*k+~ptQ)6z`h6e4B&g?Q;IK+aH z)X(BH`n2DOS1#{AJD-a?uL)@Vl+`B=6X3gF(BCm>Q(9+?IMX%?CqgpsvK+b_de%Q> zj-GtHKf!t@p2;Gu*~#}kF@Q2HMevg~?0{^cPxCRh!gdg7MXsS}BLtG_a0IY0G1DVm z2F&O-$Dzzc#M~iN`!j38gAn`6*~h~AP=s_gy2-#LMFoNZ0<3q+=q)a|4}ur7F#><%j1lnr=F42Mbti zi-LYs85K{%NP8wE1*r4Mm+ZuZ8qjovmB;f##!E*M{*A(4^~vg!bblYi1M@7tq^L8- zH7tf_70iWXqcSQgENGdEjvLiSLicUi3l0H*sx=K!!HLxDg^K|s1G}6Tam|KBV>%YeU)Q>zxQe;ddnDTWJZ~^g-kNeycQ?u242mZs`i8cP)9qW`cwqk)Jf?Re0=SD=2z;Gafh(^X-=WJ$i7Z9$Pao56bTwb+?p>L3bi9 zP|qi@;H^1iT+qnNHBp~X>dd=Us6v#FPDTQLb9KTk%z{&OWmkx3uY(c6JYyK3w|z#Q zMY%FPv%ZNg#w^NaW6lZBU+}Znwc|KF(+X0RO~Q6*O{T-P*fi@5cPGLnzWMSyoOPe3 z(J;R#q}3?z5Ve%crTPZQFLTW81cNY-finw!LH9wr$(C)p_@v?(y#b-R^Pv!}_#7t+A?pHEUMY zoQZIwSETTKeS!W{H$lyB1^!jn4gTD{_mgG?#l1Hx2h^HrpCXo95f3utP-b&%w80F} zXFs@Jp$lbIL64@gc?k*gJ;OForPaapOH7zNMB60FdNP<*9<@hEXJk9Rt=XhHR-5_$Ck-R?+1py&J3Y9^sBBZuj?GwSzua;C@9)@JZpaI zE?x6{H8@j9P06%K_m%9#nnp0Li;QAt{jf-7X%Pd2jHoI4As-9!UR=h6Rjc z!3{UPWiSeLG&>1V5RlM@;5HhQW_&-wL2?%k@dvRS<+@B6Yaj*NG>qE5L*w~1ATP$D zmWu6(OE=*EHqy{($~U4zjxAwpPn42_%bdH9dMphiUU|) z*+V@lHaf%*GcXP079>vy5na3h^>X=n;xc;VFx)`AJEk zYZFlS#Nc-GIHc}j06;cOU@ zAD7Egkw<2a8TOcfO9jCp4U4oI*`|jpbqMWo(={gG3BjuM3QTGDG`%y|xithFck}0J zG}N#LyhCr$IYP`#;}tdm-7^9=72+CBfBsOZ0lI=LC_a%U@(t3J_I1t(UdiJ^@NubM zvvA0mGvTC%{fj53M^|Ywv$KbW;n8B-x{9}Z!K6v-tw&Xe_D2{7tX?eVk$sA*0826( zuGz!K7$O#;K;1w<38Tjegl)PmRso`fc&>fAT5s z7hzQe-_`lx`}2=c)jz6;yn(~F6#M@z_7@Z(@GWbIAo6A2&;aFf&>CVHpqoPh5#~=G zav`rZ3mSL2qwNL+Pg>aQv;%V&41e|YU$!fQ9Ksle!XZERpjAowHtX zi#0lnw{(zmk&}t`iFEMmx-y7FWaE*vA{Hh&>ieZg{5u0-3@a8BY)Z47E`j-H$dadu zIP|PXw1gjO@%aSz*O{GqZs_{ke|&S6hV{-dPkl*V|3U4LpqhG0eVdqfeNX28hrafI zE13WOsRE|o?24#`gQJs@v*EwL{@3>Ffa;knvI4@VEG2I>t-L(KRS0ShZ9N!bwXa}e zI0}@2#PwFA&Y9o}>6(ZaSaz>kw{U=@;d{|dYJ~lyjh~@bBL>n}#@KjvXUOhrZ`DbnAtf5bz3LD@0RpmAyC-4cgu<7rZo&C3~A_jA*0)v|Ctcdu} zt@c7nQ6hSDC@76c4hI&*v|5A0Mj4eQ4kVb0$5j^*$@psB zdouR@B?l6E%a-9%i(*YWUAhxTQ(b@z&Z#jmIb9`8bZ3Um3UW!@w4%t0#nxsc;*YrG z@x$D9Yj3EiA(-@|IIzi@!E$N)j?gedGJpW!7wr*7zKZwIFa>j|cy<(1`VV_GzWN=1 zc%OO)o*RRobvTZE<9n1s$#V+~5u8ZwmDaysD^&^cxynksn!_ypmx)Mg^8$jXu5lMo zK3K_8GJh#+7HA1rO2AM8cK(#sXd2e?%3h2D9GD7!hxOEKJZK&T`ZS0e*c9c36Y-6yz2D0>Kvqy(EuiQtUQH^~M*HY!$e z20PGLb2Xq{3Ceg^sn+99K6w)TkprP)YyNU(+^PGU8}4&Vdw*u;(`Bw!Um76gL_aMT z>*82nmA8Tp;~hwi0d3S{vCwD};P(%AVaBr=yJ zqB?DktZ#)_VFh_X69lAHQw(ZNE~ZRo2fZOIP;N6fD)J*3u^YGdgwO(HnI4pb$H#9) zizJ<>qI*a6{+z=j+SibowDLKYI*Je2Y>~=*fL@i*f&8**s~4l&B&}$~nwhtbOTr=G zFx>{y6)dpJPqv={_@*!q0=jgw3^j`qi@!wiWiT_$1`SPUgaG&9z9u9=m5C8`GpMaM zyMRSv2llS4F}L?233!)f?mvcYIZ~U z7mPng^=p)@Z*Fp9owSYA`Fe4OjLiJ`rdM`-U(&z1B1`S`ufK_#T@_BvenxDQU`deH$X5eMVO=;I4EJjh6?kkG2oc6AYF6|(t)L0$ukG}Zn=c+R`Oq;nC)W^ z{ek!A?!nCsfd_5>d&ozG%OJmhmnCOtARwOq&p!FzWl7M))YjqK8|;6sOAc$w2%k|E z`^~kpT!j+Y1lvE0B)mc$Ez_4Rq~df#vC-FmW;n#7E)>@kMA6K30!MdiC19qYFnxQ* z?BKegU_6T37%s`~Gi2^ewVbciy-m5%1P3$88r^`xN-+VdhhyUj4Kzg2 zlKZ|FLUHiJCZL8&<=e=F2A!j@3D@_VN%z?J;uw9MquL`V*f^kYTrpoWZ6iFq00uO+ zD~Zwrs!e4cqGedAtYxZ76Bq3Ur>-h(m1~@{x@^*YExmS*vw9!Suxjlaxyk9P#xaZK z)|opA2v#h=O*T42z>Mub2O3Okd3GL86KZM2zlfbS z{Vps`OO&3efvt->OOSpMx~i7J@GsRtoOfQ%vo&jZ6^?7VhBMbPUo-V^Znt%-4k{I# z8&X)=KY{3lXlQg4^FH^{jw0%t#2%skLNMJ}hvvyd>?_AO#MtdvH;M^Y?OUWU6BdMX zJ(h;PM9mlo@i)lWX&#E@d4h zj4Z0Czj{+ipPeW$Qtz_A52HA<4$F9Qe4CiNQSNE2Q-d1OPObk4?7-&`={{yod5Iy3kB=PK3%0oYSr`Gca120>CHbC#SqE*ivL2R(YmI1A|nAT?JmK*2qj_3p#?0h)$#ixdmP?UejCg9%AS2 z8I(=_QP(a(s)re5bu-kcNQc-&2{QZ%KE*`NBx|v%K2?bK@Ihz_e<5Y(o(gQ-h+s&+ zjpV>uj~?rfJ!UW5Mop~ro^|FP3Z`@B6A=@f{Wn78cm`)3&VJ!QE+P9&$;3SDNH>hI z_88;?|LHr%1kTX0t*xzG-6BU=LRpJFZucRBQ<^zy?O5iH$t>o}C}Fc+kM1EZu$hm% zTTFKrJkXmCylFgrA;QAA(fX5Sia5TNo z?=Ujz7$Q?P%kM$RKqRQisOexvV&L+bolR%`u`k;~!o(HqgzV9I6w9|g*5SVZN6+kT9H$-3@%h%k7BBnB zPn+wmPYNG)V2Jv`&$LoI*6d0EO^&Nh`E* z&1V^!!Szd`8_uf%OK?fuj~! z%p9QLJ?V*T^)72<6p1ONqpmD?Wm((40>W?rhjCDOz?#Ei^sXRt|GM3ULLnoa8cABQ zA)gCqJ%Q5J%D&nJqypG-OX1`JLT+d`R^|0KtfGQU+jw79la&$GHTjKF>*8BI z0}l6TC@XB6`>7<&{6WX2kX4k+0SaI`$I8{{mMHB}tVo*(&H2SmZLmW* z+P8N>(r}tR?f!O)?)df>HIu>$U~e~tflVmwk*+B1;TuqJ+q_^`jwGwCbCgSevBqj$ z<`Fj*izeO)_~fq%wZ0Jfvi6<3v{Afz;l5C^C7!i^(W>%5!R=Ic7nm(0gJ~9NOvHyA zqWH2-6w^YmOy(DY{VrN6ErvZREuUMko@lVbdLDq*{A+_%F>!@6Z)X9kR1VI1+Ler+ zLUPtth=u~23=CqZoAbQ`uGE_91kR(8Ie$mq1p`q|ilkJ`Y-ob_=Nl(RF=o7k{47*I)F%_XMBz9uwRH8q1o$TkV@8Pwl zzi`^7i;K6Ak7o58a_D-V0AWp;H8pSjbEs$4BxoJkkC6UF@QNL)0$NU;Wv0*5 z0Ld;6tm7eR%u=`hnUb)gjHbE2cP?qpo3f4w%5qM0J*W_Kl6&z4YKX?iD@=McR!gTyhpGGYj!ljQm@2GL^J70`q~4CzPv@sz`s80FgiuxjAZ zLq61rHv1O>>w1qOEbVBwGu4%LGS!!muKHJ#JjfT>g`aSn>83Af<9gM3XBdY)Yql|{ zUds}u*;5wuus)D>HmexkC?;R&*Z`yB4;k;4T*(823M&52{pOd1yXvPJ3PPK{Zs>6w zztXy*HSH0scZHn7qIsZ8y-zftJ*uIW;%&-Ka0ExdpijI&xInDg-Bv-Q#Islcbz+R! zq|xz?3}G5W@*7jSd`Hv9q^5N*yN=4?Lh=LXS^5KJC=j|AJ5Y(f_fC-c4YQNtvAvn|(uP9@5Co{dL z?7|=jqTzD8>(6Wr&(XYUEzT~-VVErf@|KeFpKjh=v51iDYN_`Kg&XLOIG;ZI8*U$@ zKig{dy?1H}UbW%3jp@7EVSD>6c%#abQ^YfcO(`)*HuvNc|j( zyUbYozBR15$nNU$0ZAE%ivo4viW?@EprUZr6oX=4Sc!-WvrpJdF`3SwopKPyX~F>L zJ>N>v=_plttTSUq6bYu({&rkq)d94m5n~Sk_MO*gY*tlkPFd2m=Pi>MK)ObVV@Sgs zmXMNMvvcAuz+<$GLR2!j4w&;{)HEkxl{$B^*)lUKIn&p5_huD6+%WDoH4`p}9mkw$ zXCPw6Y7tc%rn$o_vy>%UNBC`0@+Ih-#T05AT)ooKt?94^ROI5;6m2pIM@@tdT=&WP z{u09xEVdD}{(3v}8AYUyT82;LV%P%TaJa%f)c36?=90z>Dzk5mF2}Gs0jYCmufihid8(VFcZWs8#59;JCn{!tHu5kSBbm zL`F{COgE01gg-qcP2Lt~M9}mALg@i?TZp&i9ZM^G<3`WSDh}+Ceb3Q!QecJ|N;Xrs z{wH{D8wQ2+mEfBX#M8)-32+~q4MRVr1UaSPtw}`iwx@x=1Xv-?UT{t}w}W(J&WKAC zrZ%hssvf*T!rs}}#atryn?LB=>0U%PLwA9IQZt$$UYrSw`7++}WR7tfE~*Qg)vRrM zT;(1>Zzka?wIIz8vfrG86oc^rjM@P7^i8D~b(S23AoKYj9HBC(6kq9g`1gN@|9^xO z{~h zbxGMHqGZ@eJ17bgES?HQnwp|G#7I>@p~o2zxWkgZUYSUeB*KT{1Q z*J3xZdWt`eBsA}7(bAHNcMPZf_BZC(WUR5B8wUQa=UV^e21>|yp+uop;$+#JwXD!> zunhJVCIKgaol0AM_AwJNl}_k&q|uD?aTE@{Q*&hxZ=k_>jcwp}KwG6mb5J*pV@K+- zj*`r0WuEU_8O=m&1!|rj9FG7ad<2px63;Gl z9lJrXx$~mPnuiqIH&n$jSt*ReG}1_?r4x&iV#3e_z+B4QbhHwdjiGu^J3vcazPi`| zaty}NFSWe=TDry*a*4XB)F;KDI$5i9!!(5p@5ra4*iW;FlGFV0P;OZXF!HCQ!oLm1 zsK+rY-FnJ?+yTBd0}{*Y6su|hul)wJ>RNQ{eau*;wWM{vWM`d0dTC-}Vwx6@cd#P? zx$Qyk^2*+_ZnMC}q0)+hE-q)PKoox#;pc%DNJ&D5+if6X4j~p$A7-s&AjDkSEV)aM z(<3UOw*&f)+^5F0Mpzw3zB1ZHl*B?C~Cx) zuNg*>5RM9F5{EpU@a2E7hAE`m<89wbQ2Lz&?Egu-^sglNXG5Q;{9n(%&*kEb0vApd zRHrY@22=pkFN81%x)~acZeu`yvK zovAVJNykgxqkEr^hZksHkpxm>2I8FTu2%+XLs@?ym0n;;A~X>i32{g6NOB@o4lk8{ zB}7Z2MNAJi>9u=y%s4QUXaNdt@SlAZr54!S6^ETWoik6gw=k-itu_}Yl_M9!l+Rbv z(S&WD`{_|SE@@(|Wp7bq1Zq}mc4JAG?mr2WN~6}~u`7M_F@J9`sr0frzxfuqSF~mA z$m$(TWAuCIE99yLSwi%R)8geQhs;6VBlRhJb(4Cx zu)QIF%_W9+21xI45U>JknBRaZ9nYkgAcK6~E|Zxo!B&z9zQhjsi^fgwZI%K@rYbMq znWBXg1uCZ+ljGJrsW7@x3h2 z;kn!J!bwCeOrBx;oPkZ}FeP%wExyf4=XMp)N8*lct~SyfK~4^-75EZFpHYO5AnuRM z!>u?>Vj3+j=uiHc<=cD~JWRphDSwxFaINB42-{@ZJTWe85>-RcQ&U%?wK)vjz z5u5fJYkck##j(bP7W0*RdW#BmAIK`D3=(U~?b`cJ&U2jHj}?w6 z_4BM)#EoJ6)2?pcR4AqBd)qAUn@RtNQq})FIQoBK4ie+GB(Vih2D|Ds>RJo2zE~C- z7mI)7p)5(-O6JRh6a@VZ5~piVC+Xv=O-)=0eTMSJsRE^c1@bPQWlr}E31VqO-%739 zdcmE{`1m;5LH8w|7euK>>>U#Iod8l1yivC>;YWsg=z#07E%cU9x1yw#3l6AcIm%79 zGi^zH6rM#CZMow(S(8dcOq#5$kbHnQV6s?MRsU3et!!YK5H?OV9vf2qy-UHCn>}2d zTwI(A_fzmmCtE@10yAGgU7R&|Fl$unZJ_^0BgCEDE6(B*SzfkapE9#0N6adc>}dtH zJ#nt^F~@JMJg4=Pv}OdUHyPt-<<9Z&c0@H@^4U?KwZM&6q0XjXc$>K3c&3iXLD9_%(?)?2kmZ=Ykb;)M`Tw=%_d=e@9eheGG zk0<`4so}r={C{zr|6+_1mA_=a56(XyJq||g6Es1E6%fPg#l{r+vk9;)r6VB7D84nu zE0Z1EIxH{Y@}hT+|#$0xn+CdMy6Uhh80eK~nfMEIpM z`|G1v!USmx81nY8XkhEOSWto}pc#{Ut#`Pqb}9j$FpzkQ7`0<-@5D_!mrLah98Mpr zz(R7;ZcaR-$aKqUaO!j z=7QT;Bu0cvYBi+LDfE_WZ`e@YaE_8CCxoRc?Y_!Xjnz~Gl|aYjN2&NtT5v4#q3od2 zkCQZHe#bn(5P#J**Fj4Py%SaaAKJsmV6}F_6Z7V&n6QAu8UQ#9{gkq+tB=VF_Q6~^ zf(hXvhJ#tC(eYm6g|I>;55Lq-;yY*COpTp4?J}hGQ42MIVI9CgEC{3hYw#CZfFKVG zgD(steIg8veyqX%pYMoulq zMUmbj8I`t>mC`!kZ@A>@PYXy*@NprM@e}W2Q+s?XIRM-U1FHVLM~c60(yz1<46-*j zW*FjTnBh$EzI|B|MRU11^McTPIGVJrzozlv$1nah_|t4~u}Ht^S1@V8r@IXAkN;lH z_s|WHlN90k4X}*#neR5bX%}?;G`X!1#U~@X6bbhgDYKJK17~oFF0&-UB#()c$&V<0 z7o~Pfye$P@$)Lj%T;axz+G1L_YQ*#(qO zQND$QTz(~8EF1c3<%;>dAiD$>8j@7WS$G_+ktE|Z?Cx<}HJb=!aChR&4z ziD&FwsiZ)wxS4k6KTLn>d~!DJ^78yb>?Trmx;GLHrbCBy|Bip<@sWdAfP0I~;(Ybr zoc-@j?wA!$ zIP0m3;LZy+>dl#&Ymws@7|{i1+OFLYf@+8+)w}n?mHUBCqg2=-Hb_sBb?=q))N7Ej zDIL9%@xQFOA!(EQmchHiDN%Omrr;WvlPIN5gW;u#ByV)x2aiOd2smy&;vA2+V!u|D zc~K(OVI8} z0t|e0OQ7h23e01O;%SJ}Q#yeDh`|jZR7j-mL(T4E;{w^}2hzmf_6PF|`gWVj{I?^2T3MBK>{?nMXed4kgNox2DP!jvP9v`;pa6AV)OD zDt*Vd-x7s{-;E?E5}3p-V;Y#dB-@c5vTWfS7<=>E+tN$ME`Z7K$px@!%{5{uV`cH80|IzU! zDs9=$%75P^QKCRQ`mW7$q9U?mU@vrFMvx)NNDrI(uk>xwO;^($EUvqVev#{W&GdtR z0ew;Iwa}(-5D28zABlC{WnN{heSY5Eq5Fc=TN^9X#R}0z53!xP85#@;2E=&oNYHyo z46~#Sf!1M1X!rh}ioe`>G2SkPH{5nCoP`GT@}rH;-LP1Q7U_ypw4+lwsqiBql80aA zJE<(88yw$`xzNiSnU(hsyJqHGac<}{Av)x9lQ=&py9djsh0uc}6QkmKN3{P!TEy;P zzLDVQj4>+0r<9B0owxBt5Uz`!M_VSS|{(?`_e+qD9b=vZHoo6>?u;!IP zM7sqoyP>kWY|=v06gkhaGRUrO8n@zE?Yh8$om@8%=1}*!2wdIWsbrCg@;6HfF?TEN z+B_xtSvT6H3in#8e~jvD7eE|LTQhO_>3b823&O_l$R$CFvP@3~)L7;_A}JpgN@ax{ z2d9Ra)~Yh%75wsmHK8e87yAn-ZMiLo6#=<&PgdFsJw1bby-j&3%&4=9dQFltFR(VB z@=6XmyNN4yr^^o$ON8d{PQ=!OX17^CrdM~7D-;ZrC!||<+FEOxI_WI3 zCA<35va%4v>gcEX-@h8esj=a4szW7x z{0g$hwoWRQG$yK{@3mqd-jYiVofJE!Wok1*nV7Gm&Ssq#hFuvj1sRyHg(6PFA5U*Q z8Rx>-blOs=lb`qa{zFy&n4xY;sd$fE+<3EI##W$P9M{B3c3Si9gw^jlPU-JqD~Cye z;wr=XkV7BSv#6}DrsXWFJ3eUNrc%7{=^sP>rp)BWKA9<}^R9g!0q7yWlh;gr_TEOD|#BmGq<@IV;ue zg+D2}cjpp+dPf&Q(36sFU&K8}hA85U61faW&{lB`9HUl-WWCG|<1XANN3JVAkRYvr5U z4q6;!G*MTdSUt*Mi=z_y3B1A9j-@aK{lNvxK%p23>M&=KTCgR!Ee8c?DAO2_R?Bkaqr6^BSP!8dHXxj%N1l+V$_%vzHjq zvu7p@%Nl6;>y*S}M!B=pz=aqUV#`;h%M0rUHfcog>kv3UZAEB*g7Er@t6CF8kHDmK zTjO@rejA^ULqn!`LwrEwOVmHx^;g|5PHm#B6~YD=gjJ!043F+&#_;D*mz%Q60=L9O zve|$gU&~As5^uz@2-BfQ!bW)Khn}G+Wyjw-19qI#oB(RSNydn0t~;tAmK!P-d{b-@ z@E5|cdgOS#!>%#Rj6ynkMvaW@37E>@hJP^82zk8VXx|3mR^JCcWdA|t{0nPmYFOxN z55#^-rlqobcr==<)bi?E?SPymF*a5oDDeSdO0gx?#KMoOd&G(2O@*W)HgX6y_aa6i zMCl^~`{@UR`nMQE`>n_{_aY5nA}vqU8mt8H`oa=g0SyiLd~BxAj2~l$zRSDHxvDs; zI4>+M$W`HbJ|g&P+$!U7-PHX4RAcR0szJ*(e-417=bO2q{492SWrqDK+L3#ChUHtz z*@MP)e^%@>_&#Yk^1|tv@j4%3T)diEXATx4K*hcO`sY$jk#jN5WD<=C3nvuVs zRh||qDHnc~;Kf59zr0;c7VkVSUPD%NnnJC_l3F^#f_rDu8l}l8qcAz0FFa)EAt32I zUy_JLIhU_J^l~FRH&6-iv zSpG2PRqzDdMWft>Zc(c)#tb%wgmWN%>IOPmZi-noqS!^Ft zb81pRcQi`X#UhWK70hy4tGW1mz|+vI8c*h@fFGJtW3r>qV>1Z0r|L>7I3un^gcep$ zAAWfZHRvB|E*kktY$qQP_$YG60C z@X~tTQjB3%@`uz!qxtxF+LE!+=nrS^07hn`EgAp!h|r03h7B!$#OZW#ACD+M;-5J!W+{h z|6I;5cNnE(Y863%1(oH}_FTW})8zYb$7czPg~Szk1+_NTm6SJ0MS_|oSz%e(S~P-& zSFp;!k?uFayytV$8HPwuyELSXOs^27XvK-DOx-Dl!P|28DK6iX>p#Yb%3`A&CG0X2 zS43FjN%IB}q(!hC$fG}yl1y9W&W&I@KTg6@K^kpH8=yFuP+vI^+59|3%Zqnb5lTDAykf9S#X`3N(X^SpdMyWQGOQRjhiwlj!0W-yD<3aEj^ z&X%=?`6lCy~?`&WSWt?U~EKFcCG_RJ(Qp7j=$I%H8t)Z@6Vj zA#>1f@EYiS8MRHZphpMA_5`znM=pzUpBPO)pXGYpQ6gkine{ z6u_o!P@Q+NKJ}k!_X7u|qfpAyIJb$_#3@wJ<1SE2Edkfk9C!0t%}8Yio09^F`YGzp zaJHGk*-ffsn85@)%4@`;Fv^8q(-Wk7r=Q8pT&hD`5(f?M{gfzGbbwh8(}G#|#fDuk z7v1W)5H9wkorE0ZZjL0Q1=NRGY>zwgfm81DdoaVwNH;or{{e zSyybt)m<=zXoA^RALYG-2touH|L*BLvmm9cdMmn+KGopyR@4*=&0 z&4g|FLoreZOhRmh=)R0bg~T2(8V_q7~42-zvb)+y959OAv!V$u(O z3)%Es0M@CRFmG{5sovIq4%8Ahjk#*5w{+)+MWQoJI_r$HxL5km1#6(e@{lK3Udc~n z0@g`g$s?VrnQJ$!oPnb?IHh-1qA`Rz$)Ai<6w$-MJW-gKNvOhL+XMbE7&mFt`x1KY z>k4(!KbbpZ`>`K@1J<(#vVbjx@Z@(6Q}MF#Mnbr-f55)vXj=^j+#)=s+ThMaV~E`B z8V=|W_fZWDwiso8tNMTNse)RNBGi=gVwgg%bOg8>mbRN%7^Um-7oj4=6`$|(K7!+t^90a{$1 z8Z>}<#!bm%ZEFQ{X(yBZMc>lCz0f1I2w9SquGh<9<=AO&g6BZte6hn>Qmvv;Rt)*c zJfTr2=~EnGD8P$v3R|&1RCl&7)b+`=QGapiPbLg_pxm`+HZurtFZ;wZ=`Vk*do~$wBxoW&=j0OTbQ=Q%S8XJ%~qoa3Ea|au5 zo}_(P;=!y z-AjFrERh%8la!z6Fn@lR?^E~H12D? z8#ht=1F;7@o4$Q8GDj;sSC%Jfn01xgL&%F2wG1|5ikb^qHv&9hT8w83+yv&BQXOQy zMVJSBL(Ky~p)gU3#%|blG?I zR9rP^zUbs7rOA0X52Ao=GRt@C&zlyjNLv-}9?*x{y(`509qhCV*B47f2hLrGl^<@S zuRGR!KwHei?!CM10pBKpDIoBNyRuO*>3FU?HjipIE#B~y3FSfOsMfj~F9PNr*H?0o zHyYB^G(YyNh{SxcE(Y-`x5jFMKb~HO*m+R%rq|ic4fzJ#USpTm;X7K+E%xsT_3VHK ze?*uc4-FsILUH;kL>_okY(w`VU*8+l>o>JmiU#?2^`>arnsl#)*R&nf_%>A+qwl%o z{l(u)M?DK1^mf260_oteV3#E_>6Y4!_hhVDM8AI6MM2V*^_M^sQ0dmHu11fy^kOqX zqzps-c5efIKWG`=Es(9&S@K@)ZjA{lj3ea7_MBPk(|hBFRjHVMN!sNUkrB;(cTP)T97M$ z0Dtc&UXSec<+q?y>5=)}S~{Z@ua;1xt@=T5I7{`Z=z_X*no8s>mY;>BvEXK%b`a6(DTS6t&b!vf_z#HM{Uoy z_5fiB(zpkF{})ruka$iX*~pq1ZxD?q68dIoIZSVls9kFGsTwvr4{T_LidcWtt$u{k zJlW7moRaH6+A5hW&;;2O#$oKyEN8kx z`LmG)Wfq4ykh+q{I3|RfVpkR&QH_x;t41UwxzRFXt^E2B$domKT@|nNW`EHwyj>&< zJatrLQ=_3X%vd%nHh^z@vIk(<5%IRAa&Hjzw`TSyVMLV^L$N5Kk_i3ey6byDt)F^U zuM+Ub4*8+XZpnnPUSBgu^ijLtQD>}K;eDpe1bNOh=fvIfk`&B61+S8ND<(KC%>y&? z>opCnY*r5M+!UrWKxv0_QvTlJc>X#AaI^xoaRXL}t5Ej_Z$y*|w*$6D+A?Lw-CO-$ zitm^{2Ct82-<0IW)0KMNvJHgBrdsIR0v~=H?n6^}l{D``Me90`^o|q!olsF?UX3YS zq^6Vu>Ijm>>PaZI8G@<^NGw{Cx&%|PwYrfwR!gX_%AR=L3BFsf8LxI|K^J}deh0Zd zV?$3r--FEX`#INxsOG6_=!v)DI>0q|BxT)z-G6kzA01M?rba+G_mwNMQD1mbVbNTW zmBi*{s_v_Ft9m2Avg!^78(QFu&n6mbRJ2bAv!b;%yo{g*9l2)>tsZJOOp}U~8VUH`}$8p_}t*XIOehezolNa-a2x0BS})Y9}& z*TPgua{Ewn-=wVrmJUeU39EKx+%w%=ixQWKDLpwaNJs65#6o7Ln7~~X+p_o2BR1g~ zVCfxLzxA{HlWAI6^H;`juI=&r1jQrUv_q0Z1Ja-tjdktrrP>GOC*#p?*xfQU5MqjM zsBe!9lh(u8)w$e@Z|>aUHI5o;MGw*|Myiz3-f0;pHg~Q#%*Kx8MxH%AluVXjG2C$) zWL-K63@Q`#y9_k_+}eR(x4~dp7oV-ek0H>Igy8p#i4GN{>#v=pFYUQT(g&b$OeTy- zX_#FDgNF8XyfGY6R!>inYn8IR2RDa&O!(6NIHrC0H+Qpam1bNa=(`SRKjixBTtm&e z`j9porEci!zdlg1RI0Jw#b(_Tb@RQK1Zxr_%7SUeH6=TrXt3J@js`4iDD0=I zoHhK~I7^W8^Rcp~Yaf>2wVe|Hh1bXa_A{oZ9eG$he;_xYvTbTD#moBy zY57-f2Ef1TP^lBi&p5_s7WGG9|0T}dlfxOxXvScJO1Cnq`c`~{Dp;{;l<-KkCDE+p zmexJkd}zCgE{eF=)K``-qC~IT6GcRog_)!X?fK^F8UDz$(zFUrwuR$qro5>qqn>+Z z%<5>;_*3pZ8QM|yv9CAtrAx;($>4l^_$_-L*&?(77!-=zvnCVW&kUcZMb6;2!83si z518Y%R*A3JZ8Is|kUCMu`!vxDgaWjs7^0j(iTaS4HhQ)ldR=r)_7vYFUr%THE}cPF z{0H45FJ5MQW^+W>P+eEX2kLp3zzFe*-pFVAdDZRybv?H|>`9f$AKVjFWJ=wegO7hO zOIYCtd?Vj{EYLT*^gl35|HbMX|NAEUf2ra9dy1=O;figB>La=~eA^#>O6n4?EMugV zbbt{Dbfef5l^(;}5kZ@!XaWwF8z0vUr6r|+QN*|WpF z^*osUHzOnE$lHuWYO$G7>}Y)bY0^9UY4eDV`E{s+{}Z$O$2*lMEYl zTA`ki(<0(Yrm~}15V-E^e2W6`*`%ydED-3G@$UFm6$ZtLx z+av`BhsHcAWqdxPWfu2*%{}|Sptax4_=NpDMeWy$* zZM6__s`enB$~0aT1BU^2k`J9F%+n+lL_|8JklWOCVYt*0%o*j4w1CsB_H^tVpYT_LLyKuyk=CV6~1M<7~^FylL*+AIFf3h>J=x$ygY-BG}4LJ z8XxYPY!v7dO3PVwEoY=`)6krokmR^|Mg5ztX_^#QR}ibr^X-|_St#rtv3gukh0(#A=};NPlNz57ZDFJ9hf#NP50zS)+Fo=StX)i@ zWS?W}i6LjB>kAB~lupAPyIjFb)izFgRq*iS*(Jt509jNr3r72{Gj`5DGoj;J&k5G@Rm!dJ($ox>SbxR)fc zz|Phug;~A7!p@?|mMva@rWuf2fSDK_ZxN3vVmlYz>rrf?LpiNs)^z!y{As@`55JC~ zS*GD3#N-ptY!2<613UelAJ;M4EEI$dm)`8#n$|o{ce^dlyoUY3bsy2hgnj-;ovubb zg2h1rZA6Ot}K_cpYBpIuF&CyK~5R0Wv;kG|3A^8K3nk{rw$Be8u@aos#qvKQKJyVU$cX6biw&Ep#+q7upFX z%qo&`WZ){<%zh@BTl{MO@v9#;t+cb7so0Uz49Fmo1e4>y!vUyIHadguZS0T7-x#_drMXz*16*c zymR0u^`ZQpXN}2ofegbpSedL%F9aypdQcrzjzPlBW0j zMlPzC&ePZ@Cq!?d%9oQNEg0`rHALm8l#lUdXMVEqDvb(AID~H(?H9z!e9G98fG@IzhajKr)3{L_Clu1(Bwg`RM!-(MOuZi zbeDsj9I3(~EITsE=3Z)a|l_rn8W92U0DB70gF7YYfO0j!)h?QobY1lSR>0 z_TVw@$eP~3k8r9;%g%RlZzCJ2%f}DvY`rsZ$;ak&^~-`i%B%+O!pnADeVyV!dHj|} zzOj#q4eRx9Q8c2Z7vy9L&fGLj+3_?fp}+8o`Xpwyi(81H|7P8#65%FIS*lOi={o&v z4NV$xu7az4Nb50dRGZv<tdZCx4Ek<_o3!mAT} zL5l*|K3Qr-)W8paaG z&R6{ped_4e2cy}ejD0!dt{*PaC*^L@eB%(1Fmc%Y#4)~!jF#lCGfj#E??4LG-T;!M z>Uha}f;W>ib_ZL-I7-v9KZQls^G!-JmL^w;=^}?!RXK;m4$#MwI2AH-l7M2-0 zVMK8k^+4+>2S0k^N_40EDa#`7c;2!&3-o6MHsnBfRnq@>E@)=hDulVq-g5SQWDWbt zj6H5?QS2gRZ^Zvbs~cW|8jagJV|;^zqC0e=D1oUsQPJ3MCb+eRGw(XgIY9y8v_tXq z9$(xWntWpx_Uronmvho{JfyYdV{L1N$^s^|-Nj`Ll`lUsiWTjm&8fadUGMXreJGw$ zQ**m+Tj|(XG}DyUKY~2?&9&n6SJ@9VKa9Hcayv{ar^pNr0WHy zP$bQv&8O!vd;GoT!pLwod-42qB^`m!b7nP@YTX}^+1hzA$}LSLh}Ln|?`%8xGMazw z8WT!LoYJ-Aq3=2p6ZSP~uMgSSWv3f`&-I06tU}WhZsA^6nr&r17hjQIZE>^pk=yZ% z06}dfR$85MjWJPq)T?OO(RxoaF+E#4{Z7)i9}Xsb;Nf+dzig61HO;@JX1Lf9)R5j9)Oi6vPL{H z&UQ9ln=$Q8jnh6-t;`hKM6pHftdd?$=1Aq16jty4-TF~`Gx=C&R242uxP{Y@Q~%O3 z*(16@x+vJsbW@^3tzY=-5MHi#(kB};CU%Ep`mVY1j$MAPpYJBB3x$ue`%t}wZ-@CG z(lBv36{2HMjxT)2$n%(UtHo{iW9>4HX4>)%k8QNnzIQYXrm-^M%#Qk%9odbUrZDz1YPdY`2Z4w~p!5tb^m(mUfk}kZ9+EsmenQ)5iwiaulcy zCJ#2o4Dz?@%)aAKfVXYMF;3t@aqNh2tBBlBkCdj`F31b=h93y(46zQ-YK@+zX5qM9 z&=KkN&3@Ptp*>UD$^q-WpG|9O)HBXz{D>p!`a36aPKkgz7uxEo0J>-o+4HHVD9!Hn z${LD0d{tuGsW*wvZoHc8mJroAs(3!FK@~<}Pz1+vY|Gw}Lwfxp{4DhgiQ_SSlV)E| zZWZxYZLu2EB1=g_y@(ieCQC_1?WNA0J0*}eMZfxCCs>oL;?kHdfMcKB+A)Qull$v( z2x6(38utR^-(?DG>d1GyU()8>ih3ud0@r&I$`ZSS<*1n6(76=OmP>r_JuNCdS|-8U zxGKXL1)Lc2kWY@`_kVBt^%7t9FyLVYX(g%a6>j=yURS1!V<9ieT$$5R+yT!I>}jI5 z?fem|T=Jq;BfZmsvqz_Ud*m5;&xE66*o*S22vf-L+MosmUPPA}~wy`kntf8rIeP-m;;{`xe}9E~G7J!PYoVH_$q~NzQab?F8vWUja5BJ!T5%5IpyqI#Dkps0B;gQ*z?c#N>spFw|wRE$gY?y4wQbJ zku2sVLh({KQz6e0yo+X!rV#8n8<;bHWd{ZLL_(*9Oi)&*`LBdGWz>h zx+p`Wi00u#V$f=CcMmEmgFjw+KnbK3`mbaKfoCsB{;Q^oJgj*LWnd_(dk9Kcssbj` z?*g8l`%{*LuY!Ls*|Tm`1Gv-tRparW8q4AK(5pfJFY5>@qO( zcY>pt*na>LlB^&O@YBDnWLE$x7>pMdSmb-?qMh79eB+Wa{)$%}^kX@Z3g>fytppz! zl%>pMD(Yw+5=!UgYHLD69JiJ;YhiGeEyZM$Au{ff;i zCBbNQfO{d!b7z^F732XX&qhEsJA1UZtJjJEIPyDq+F`LeAUU_4`%2aTX#3NG3%W8u zC!7OvlB?QJ4s2#Ok^_8SKcu&pBd}L?vLRT8Kow#xARt`5&Cg=ygYuz>>c z4)+Vv$;<$l=is&E{k&4Lf-Lzq#BHuWc;wDfm4Fbd5Sr!40s{UpKT$kzmUi{V0t1yp zPOf%H8ynE$x@dQ_!+ISaI}#%72UcYm7~|D*(Fp8xiFAj$CmQ4oH3C+Q8W=Y_9Sp|B z+k<%5=y{eW=YvTivV(*KvC?qxo)xqcEU9(Te=?ITts~;xA0Jph-vpd4@Zw#?r2!`? zB3#XtIY^wxrpjJv&(7Xjvm>$TIg2ZC&+^j(gT0R|&4cb)=92-2Hti1`& z=+M;*O%_j3>9zW|3h{0Tfh5i)Fa;clGNJpPRcUmgErzC{B+zACiPHbff3SmsCZ&X; zp=tgI=zW-t(5sXFL8;ITHw0?5FL3+*z5F-KcLN130l=jAU6%F=DClRPrzO|zY+HD`zlZ-)JT}X?2g!o zxg4Ld-mx6&*-N0-MQ(z+zJo8c`B39gf{-h2vqH<=^T&o1Dgd>4BnVht+JwLcrjJl1 zsP!8`>3-rSls07q2i1hScM&x0lQyBbk(U=#3hI7Bkh*kj6H*&^p+J?OMiT_3*vw5R zEl&p|QQHZq6f~TlAeDGy(^BC0vUK?V&#ezC0*#R-h}_8Cw8-*${mVfHssathC8%VA zUE^Qd!;Rvym%|f@?-!sEj|73Vg8!$$zj_QBZAOraF5HCFKl=(Ac|_p%-P;6z<2WSf zz(9jF2x7ZR{w+p)ETCW06PVt0YnZ>gW9^sr&~`%a_7j-Ful~*4=o|&TM@k@Px2z>^ t{*Ed16F~3V5p+(suF-++X8+nHtT~NSfJ>UC3v)>lEpV}<+rIR_{{yMcG_L>v literal 0 HcmV?d00001 diff --git a/mapsindoors_mapbox_android/android/gradle/wrapper/gradle-wrapper.properties b/mapsindoors_mapbox_android/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..41dfb87 --- /dev/null +++ b/mapsindoors_mapbox_android/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mapsindoors_mapbox_android/android/gradlew b/mapsindoors_mapbox_android/android/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/mapsindoors_mapbox_android/android/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mapsindoors_mapbox_android/android/gradlew.bat b/mapsindoors_mapbox_android/android/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/mapsindoors_mapbox_android/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mapsindoors_mapbox_android/android/settings.gradle b/mapsindoors_mapbox_android/android/settings.gradle new file mode 100644 index 0000000..1f0c335 --- /dev/null +++ b/mapsindoors_mapbox_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'mapsindoors' diff --git a/mapsindoors_mapbox_android/android/src/main/AndroidManifest.xml b/mapsindoors_mapbox_android/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f2b44f4 --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/PlatformMapView.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/PlatformMapView.kt new file mode 100644 index 0000000..01a9f25 --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/PlatformMapView.kt @@ -0,0 +1,79 @@ +package com.mapspeople.mapsindoors + +import android.animation.Animator +import android.annotation.SuppressLint +import android.content.Context +import com.mapsindoors.core.MPFloorSelectorInterface +import com.mapsindoors.mapbox.MPMapConfig + +import android.view.View +import com.mapbox.maps.MapView +import com.mapbox.maps.MapboxMap +import com.mapbox.maps.Style +import com.mapbox.maps.plugin.animation.MapAnimationOptions +import com.mapbox.maps.plugin.animation.flyTo +import com.mapspeople.mapsindoors.core.* +import com.mapspeople.mapsindoors.core.models.* + +abstract class PlatformMapView(private val context: Context) : PlatformMapViewInterface { + private val mMap: MapView = MapView(context) + private var mMapboxMap: MapboxMap? = null + + init { + mMapboxMap = mMap.getMapboxMap() + mMapboxMap?.loadStyleUri(Style.MAPBOX_STREETS) + + } + + @SuppressLint("Lifecycle") + override fun disposeMap() { + mMap.onDestroy() + mMapboxMap = null + } + + override fun getMapView(): View { + return mMap + } + + override fun makeMPConfig(config: MapConfig?, floorSelectorInterface: MPFloorSelectorInterface) : MPMapConfig? { + return config?.makeMPMapConfig(context, mMapboxMap!!, mMap, context.getString(R.string.mapbox_api_key), floorSelectorInterface); + } + + override val currentCameraPosition: CameraPosition? get() { + return mMapboxMap?.cameraState?.toCameraPosition() + } + + override fun updateCamera(move: Boolean, update: CameraUpdate, duration: Int?, success: () -> Unit) { + mMapboxMap?.let { update.toCameraOptions(it) }?.let { + if (move) { + mMapboxMap?.setCamera(it) + success() + } else if (duration != null) { + mMapboxMap?.flyTo(it, MapAnimationOptions.mapAnimationOptions { + duration(duration.toLong()) + animatorListener(object : Animator.AnimatorListener { + override fun onAnimationStart(animation: Animator) { + /* Do nothing */ + } + + override fun onAnimationEnd(animation: Animator) { + success() + } + + override fun onAnimationCancel(animation: Animator) { + success() + } + + override fun onAnimationRepeat(animation: Animator) { + /* Do nothing */ + } + + }) + }) + } else { + mMapboxMap?.flyTo(it) + success() + } + } + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/Util.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/Util.kt new file mode 100644 index 0000000..4c2d1e6 --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/Util.kt @@ -0,0 +1,103 @@ +package com.mapspeople.mapsindoors + +import java.lang.reflect.Type + +import android.content.Context +import android.graphics.Typeface + +import com.google.gson.reflect.TypeToken +import com.mapbox.maps.CameraOptions + +import com.mapbox.maps.CameraState +import com.mapbox.maps.CoordinateBounds +import com.mapbox.maps.EdgeInsets +import com.mapbox.maps.MapView +import com.mapbox.maps.MapboxMap +import com.mapsindoors.core.MPFloorSelectorInterface +import com.mapsindoors.core.MPPoint +import com.mapsindoors.mapbox.MPMapConfig +import com.mapsindoors.mapbox.converters.toPoint +import com.mapspeople.mapsindoors.core.models.* +import com.mapspeople.mapsindoors.core.models.CameraUpdateMode.* + + + + +inline fun type(): Type = object: TypeToken() {}.type + +fun CameraState.toCameraPosition() : CameraPosition { + return CameraPosition( + zoom = zoom.toFloat(), + tilt = pitch.toFloat(), + bearing = bearing.toFloat(), + target = MPPoint(center.latitude(), center.longitude()) + ) +} + +fun CameraPosition.toCameraState() : CameraState { + return CameraState( + target!!.latLng.toPoint(), + EdgeInsets(0.0,0.0,0.0,0.0), + zoom!!.toDouble(), + bearing!!.toDouble(), + tilt!!.toDouble() + ) +} + + +fun CameraUpdate.toCameraOptions(map: MapboxMap) : CameraOptions = when (mode) { + FROMPOINT -> CameraOptions.Builder().center(point?.latLng?.toPoint()).build() + + FROMBOUNDS -> if (width != null && height != null) { + bounds!!.let { + val coordinateBounds = CoordinateBounds(it.southWest.latLng.toPoint(), it.northEast.latLng.toPoint()) + map.cameraForCoordinateBounds(coordinateBounds, EdgeInsets(height.toDouble(), width.toDouble(), height.toDouble(), width.toDouble())) + } + } else { + bounds!!.let { + val coordinateBounds = CoordinateBounds(it.southWest.latLng.toPoint(), it.northEast.latLng.toPoint()) + val doublePadding = padding!!.toDouble() + map.cameraForCoordinateBounds(coordinateBounds, EdgeInsets(doublePadding, doublePadding, doublePadding, doublePadding)) + } + } + + ZOOMBY -> CameraOptions.Builder().zoom(map.cameraState.zoom + zoom!!).build() + + ZOOMTO -> CameraOptions.Builder().zoom(zoom!!.toDouble()).build() + + FROMCAMERAPOSITION -> { + position!!.let { + CameraOptions.Builder().center(it.target!!.latLng.toPoint()).zoom(it.zoom!!.toDouble() - 1).bearing(it.bearing!!.toDouble()).pitch(it.tilt!!.toDouble()).build() + } + } + +} + + +fun MapConfig.makeMPMapConfig(context: Context, map: MapboxMap, mapView: MapView, apiKey: String, floorSelector: MPFloorSelectorInterface?) : MPMapConfig { + val builder = MPMapConfig.Builder(context, map, mapView, apiKey, useDefaultMapsIndoorsStyle) + + typeface?.let { + val tf = Typeface.create(it, Typeface.NORMAL) + builder.setMapLabelFont(tf, color!!, showHalo!!) + } + showFloorSelector?.let { + builder.setShowFloorSelector(it) + } + textSize?.let { + builder.setMapLabelTextSize(it.toInt()) + } + showInfoWindowOnLocationClicked?.let { + builder.setShowInfoWindowOnLocationClicked(it) + } + showUserPosition?.let { + builder.setShowUserPosition(it) + } + tileFadeInEnabled?.let { + builder.setTileFadeInEnabled(it) + } + floorSelector?.let { + builder.setFloorSelector(it) + } + return builder.build() +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/DirectionsRenderer.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/DirectionsRenderer.kt new file mode 100644 index 0000000..33eb45d --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/DirectionsRenderer.kt @@ -0,0 +1,120 @@ +package com.mapspeople.mapsindoors.core + +import android.content.Context +import android.graphics.Color +import com.google.gson.Gson +import com.mapsindoors.core.MPCameraViewFitMode +import com.mapsindoors.core.MPDirectionsRenderer +import com.mapsindoors.core.MPRoute +import com.mapsindoors.core.MapControl +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler + +class DirectionsRenderer(private val context: Context, binaryMessenger: BinaryMessenger) : MethodCallHandler { + private val directionsRendererChannel = MethodChannel(binaryMessenger, "DirectionsRendererMethodChannel") + private var mpDirectionsRenderer: MPDirectionsRenderer? = null + private var mMapControl: MapControl? = null + + init { + directionsRendererChannel.setMethodCallHandler(this) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + val method = call.method.drop(4) + when (method) { + "clear" -> { + mpDirectionsRenderer?.clear() + result.success("success") + } + "getSelectedLegFloorIndex" -> { + var selectedLegFloorIndex = mpDirectionsRenderer?.selectedLegFloorIndex + result.success(selectedLegFloorIndex) + } + "nextLeg" -> { + mpDirectionsRenderer?.nextLeg() + result.success("success") + } + "previousLeg" -> { + mpDirectionsRenderer?.previousLeg() + result.success("success") + } + "selectLegIndex" -> { + val int = call.argument("legIndex") + if (int != null) { + try { + mpDirectionsRenderer?.selectLegIndex(int) + } catch (e: java.lang.IllegalStateException) { + result.error("-1", e.message, call.method) + } + } + result.success("success") + } + "setAnimatedPolyline" -> { + val animated = call.argument("animated") + val repeated = call.argument("repeating") + val durationMs = call.argument("durationMs") + if (animated != null && repeated != null && durationMs != null) { + mpDirectionsRenderer?.setAnimatedPolyline(animated, repeated, durationMs) + } + result.success("success") + } + "setCameraAnimationDuration" -> { + val durationMs = call.argument("durationMs") + if (durationMs != null) { + mpDirectionsRenderer?.setCameraAnimationDuration(durationMs) + } + result.success("success") + } + "setCameraViewFitMode" -> { + val cameraFitMode = call.argument("cameraFitMode") + var cameraFitModeEnum: MPCameraViewFitMode? = null + when (cameraFitMode) { + 0 -> cameraFitModeEnum = MPCameraViewFitMode.NORTH_ALIGNED + 1 -> cameraFitModeEnum = MPCameraViewFitMode.FIRST_STEP_ALIGNED + 2 -> cameraFitModeEnum = MPCameraViewFitMode.START_TO_END_ALIGNED + else -> { + result.error("-1", "$cameraFitMode is not supported", call.method) + return + } + } + mpDirectionsRenderer?.setCameraViewFitMode(cameraFitModeEnum) + result.success("success") + } + "setOnLegSelectedListener" -> { + mpDirectionsRenderer?.setOnLegSelectedListener { + directionsRendererChannel.invokeMethod("onLegSelected", it) + } + result.success("success") + } + "setPolyLineColors" -> { + val foreground: Int + val background: Int + try { + foreground = Color.parseColor(call.argument("foreground")) + background = Color.parseColor(call.argument("background")) + } catch(e: java.lang.IllegalArgumentException) { + result.error("-1", "${e.message}: ${call.argument("foreground")}, ${call.argument("background")}", call.method) + return + } + mpDirectionsRenderer?.setPolylineColors(foreground, background) + result.success("success") + } + "setRoute" -> { + val gson = Gson() + val route = gson.fromJson(call.argument("route"), MPRoute::class.java) + mpDirectionsRenderer?.setRoute(route) + result.success("success") + } + "useContentOfNearbyLocations" -> { + result.success("success") + } + } + } + + fun setMapControl(mapControl: MapControl) { + mMapControl = mapControl + mpDirectionsRenderer = MPDirectionsRenderer(mapControl) + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/DirectionsService.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/DirectionsService.kt new file mode 100644 index 0000000..119116a --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/DirectionsService.kt @@ -0,0 +1,66 @@ +package com.mapspeople.mapsindoors.core + +import android.content.Context +import com.google.gson.Gson +import com.mapsindoors.core.MPDirectionsService +import com.mapsindoors.core.MPPoint +import com.mapspeople.mapsindoors.core.models.MPError +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import java.util.* +import kotlin.collections.HashMap + +class DirectionsService(private val context: Context, binaryMessenger: BinaryMessenger) : MethodCallHandler { + private var directionsServiceChannel = MethodChannel(binaryMessenger, "DirectionsServiceMethodChannel") + private var mpDirectionsService = MPDirectionsService(context) + private val gson = Gson() + + init { + directionsServiceChannel.setMethodCallHandler(this) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + fun arg(name: String) : T? = call.argument(name) + val method = call.method.drop(4) + when (method) { + "create" -> { + mpDirectionsService = MPDirectionsService(context) + result.success("success") + } + "addAvoidWayType" -> { + mpDirectionsService.addAvoidWayType(arg("wayType") as String) + result.success("success") + } + "clearWayType" -> { + mpDirectionsService.clearWayType() + result.success("success") + } + "setIsDeparture" -> { + mpDirectionsService.setIsDeparture(arg("isDeparture") as Boolean) + result.success("success") + } + "getRoute" -> { + mpDirectionsService.setRouteResultListener { route, error -> + result.success(mapOf("route" to gson.toJson(route), "error" to gson.toJson(MPError.fromMIError(error)))) + } + val origin = gson.fromJson(arg("origin"), MPPoint::class.java) + val destination = gson.fromJson(arg("destination"), MPPoint::class.java) + if (origin != null && destination != null) { + mpDirectionsService.query(origin, destination) + } + } + "setTravelMode" -> { + mpDirectionsService.setTravelMode(arg("travelMode") as String) + result.success("success") + } + "setTime" -> { + val date = Date() + date.time = (arg("time") as Int).toLong() + mpDirectionsService.setTime(date) + result.success("success") + } + } + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/DisplayRuleHandler.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/DisplayRuleHandler.kt new file mode 100644 index 0000000..25c74b6 --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/DisplayRuleHandler.kt @@ -0,0 +1,411 @@ +package com.mapspeople.mapsindoors.core + +import com.google.gson.Gson +import com.mapsindoors.core.* +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler + +class DisplayRuleHandler(messenger: BinaryMessenger) : MethodCallHandler { + private val displayRuleChannel: MethodChannel + private val gson = Gson() + init { + displayRuleChannel = MethodChannel(messenger, "DisplayRuleMethodChannel") + displayRuleChannel.setMethodCallHandler(this) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + val id = call.argument("id")!! + val dr: MPDisplayRule? = when (id) { + "buildingOutline" -> MapsIndoors.getDisplayRule(MPSolutionDisplayRule.BUILDING_OUTLINE) + "selectionHighlight" -> MapsIndoors.getDisplayRule(MPSolutionDisplayRule.SELECTION_HIGHLIGHT) + "positionIndicator" -> MapsIndoors.getDisplayRule(MPSolutionDisplayRule.POSITION_INDICATOR) + else -> MapsIndoors.getDisplayRule(id) + } + + fun error(details: Any? = null) { + result.error("-1", "Argument was null", details) + } + fun success(ret : Any? = "success") { + result.success(ret) + } + + val method = call.method.drop(4) + + dr?.apply { + when (method) { + "isVisible" -> success(isVisible) + "setVisible" -> { + val visible = call.argument("visible") + if (visible != null) { + isVisible = visible + success() + } else { + error() + } + } + "getIconSize" -> success(gson.toJson(iconSize)) + "getIconUrl" -> success(iconUrl) + "getLabel" -> success(label) + "getLabelMaxWidth" -> success(labelMaxWidth) + "getLabelZoomFrom" -> success(labelZoomFrom) + "getLabelZoomTo" -> success(labelZoomTo) + "getModel2DBearing" -> success(model2DBearing) + "getModel2DHeightMeters" -> success(model2DHeightMeters) + "getModel2DModel" -> success(model2DModel) + "getModel2DZoomTo" -> success(model2DZoomTo) + "getModel2DWidthMeters" -> success(model2DWidthMeters) + "getModel2DZoomFrom" -> success(model2DZoomFrom) + "getPolygonFillColor" -> success(polygonFillColor) + "getPolygonFillOpacity" -> success(polygonFillOpacity) + "getPolygonZoomTo" -> success(polygonZoomTo) + "getPolygonStrokeColor" -> success(polygonStrokeColor) + "getPolygonStrokeOpacity" -> success(polygonStrokeOpacity) + "getPolygonStrokeWidth" -> success(polygonStrokeWidth) + "getPolygonZoomFrom" -> success(polygonZoomFrom) + "getZoomFrom" -> success(zoomFrom) + "getZoomTo" -> success(zoomTo) + "isIconVisible" -> success(isIconVisible) + "isLabelVisible" -> success(isLabelVisible) + "isModel2DVisible" -> success(isModel2DVisible) + "isPolygonVisible" -> success(isPolygonVisible) + "isValid" -> success(isValid) + "reset" -> { + reset() + success() + } + "setIcon" -> { + val iconUrl = call.argument("url") + if (iconUrl != null) { + setIcon(iconUrl) + success() + } else { + error() + } + } + "setIconVisible" -> { + val visible = call.argument("visible") + if (visible != null) { + isIconVisible = visible + success() + } else { + error() + } + } + "setIconSize" -> { + val size = gson.fromJson(call.argument("size"), MPIconSize::class.java) + if (size != null) { + setIconSize(size.width, size.height) + success() + } else { + error() + } + } + "setLabel" -> { + val label = call.argument("label") + if (label != null) { + dr.label = label + success() + } else { + error() + } + } + "setLabelMaxWidth" -> { + val maxWidth = call.argument("maxWidth") + if (maxWidth != null) { + labelMaxWidth = maxWidth + success() + } else { + error() + } + } + "setLabelVisible" -> { + val visible = call.argument("visible") + if (visible != null) { + isLabelVisible = visible + success() + } else { + error() + } + } + "setLabelZoomFrom" -> { + val zoomFrom = call.argument("zoomFrom") + if (zoomFrom != null) { + labelZoomFrom = zoomFrom.toFloat() + success() + } else { + error() + } + } + "setLabelZoomTo" -> { + val zoomTo = call.argument("zoomTo") + if (zoomTo != null) { + labelZoomTo = zoomTo.toFloat() + success() + } else { + error() + } + } + "setModel2DBearing" -> { + val bearing = call.argument("bearing") + if (bearing != null) { + model2DBearing = bearing + success() + } else { + error() + } + } + "setModel2DModel" -> { + val model = call.argument("model") + if (model != null) { + model2DModel = model + success() + } else { + error() + } + } + "setModel2DHeightMeters" -> { + val height = call.argument("height") + if (height != null) { + model2DHeightMeters = height + success() + } else { + error() + } + } + "setModel2DVisible" -> { + val visible = call.argument("visible") + if (visible != null) { + isModel2DVisible = visible + success() + } else { + error() + } + } + "setModel2DWidthMeters" -> { + val width = call.argument("width") + if (width != null) { + model2DWidthMeters = width + success() + } else { + error() + } + } + "setModel2DZoomFrom" -> { + val zoomFrom = call.argument("zoomFrom") + if (zoomFrom != null) { + model2DZoomFrom = zoomFrom.toFloat() + success() + } else { + error() + } + } + "setModel2DZoomTo" -> { + val zoomTo = call.argument("zoomTo") + if (zoomTo != null) { + model2DZoomTo = zoomTo.toFloat() + success() + } else { + error() + } + } + "setPolygonFillColor" -> { + val color = call.argument("color") + if (color != null) { + polygonFillColor = color + success() + } else { + error() + } + } + "setPolygonFillOpacity" -> { + val opacity = call.argument("opacity") + if (opacity != null) { + polygonFillOpacity = opacity.toFloat() + success() + } else { + error() + } + } + "setPolygonStrokeColor" -> { + val color = call.argument("color") + if (color != null) { + polygonStrokeColor = color + success() + } else { + error() + } + } + "setPolygonStrokeOpacity" -> { + val opacity = call.argument("opacity") + if (opacity != null) { + polygonStrokeOpacity = opacity.toFloat() + success() + } else { + error() + } + } + "setPolygonStrokeWidth" -> { + val width = call.argument("width") + if (width != null) { + polygonStrokeWidth = width.toFloat() + success() + } else { + error() + } + } + "setPolygonVisible" -> { + val visible = call.argument("visible") + if (visible != null) { + isPolygonVisible = visible + success() + } else { + error() + } + } + "setPolygonZoomFrom" -> { + val zoomFrom = call.argument("zoomFrom") + if (zoomFrom != null) { + polygonZoomFrom = zoomFrom.toFloat() + success() + } else { + error() + } + } + "setPolygonZoomTo" -> { + val zoomTo = call.argument("zoomTo") + if (zoomTo != null) { + polygonZoomTo = zoomTo.toFloat() + success() + } else { + error() + } + } + "getExtrusionColor" -> success(extrusionColor) + "getExtrusionHeight" -> success(extrusionHeight) + "getExtrusionZoomFrom" -> success(extrusionZoomFrom) + "getExtrusionZoomTo" -> success(extrusionZoomTo) + "getWallColor" -> success(wallColor) + "getWallHeight" -> success(wallHeight) + "getWallZoomFrom" -> success(wallZoomFrom) + "getWallZoomTo" -> success(wallZoomTo) + "isExtrusionVisible" -> success(isExtrusionVisible) + "isWallVisible" -> success(isWallVisible) + "setExtrusionColor" -> { + val color = call.argument("color") + if (color != null) { + extrusionColor = color + success() + } else { + error() + } + } + "setExtrusionHeight" -> { + val height = call.argument("height") + if (height != null) { + extrusionHeight = height.toFloat() + success() + } else { + error() + } + } + "setExtrusionVisible" -> { + val visible = call.argument("visible") + if (visible != null) { + isExtrusionVisible = visible + success() + } else { + error() + } + } + "setExtrusionZoomFrom" -> { + val zoomFrom = call.argument("zoomFrom") + if (zoomFrom != null) { + extrusionZoomFrom = zoomFrom.toFloat() + success() + } else { + error() + } + } + "setExtrusionZoomTo" -> { + val zoomTo = call.argument("zoomTo") + if (zoomTo != null) { + extrusionZoomTo = zoomTo.toFloat() + success() + } else { + error() + } + } + "setWallColor" -> { + val color = call.argument("color") + if (color != null) { + wallColor = color + success() + } else { + error() + } + } + "setWallHeight" -> { + val height = call.argument("height") + if (height != null) { + wallHeight = height.toFloat() + success() + } else { + error() + } + } + "setWallVisible" -> { + val visible = call.argument("visible") + if (visible != null) { + isWallVisible = visible + success() + } else { + error() + } + } + "setWallZoomFrom" -> { + val zoomFrom = call.argument("zoomFrom") + if (zoomFrom != null) { + wallZoomFrom = zoomFrom.toFloat() + success() + } else { + error() + } + } + "setWallZoomTo" -> { + val zoomTo = call.argument("zoomTo") + if (zoomTo != null) { + wallZoomTo = zoomTo.toFloat() + success() + } else { + error() + } + } + "setZoomFrom" -> { + val zoomFromArg = call.argument("zoomFrom") + if (zoomFromArg != null) { + zoomFrom = zoomFromArg.toFloat() + success() + } else { + error() + } + } + "setZoomTo" -> { + val zoomToArg = call.argument("zoomTo") + if (zoomToArg != null) { + zoomTo = zoomToArg.toFloat() + success() + } else { + error() + } + } + else -> { + result.notImplemented() + } + } + return@onMethodCall + } + success(null) + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/MapView.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/MapView.kt new file mode 100644 index 0000000..cd1245b --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/MapView.kt @@ -0,0 +1,533 @@ +package com.mapspeople.mapsindoors.core + +import android.content.Context +import android.view.View +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.google.gson.Gson +import com.mapsindoors.core.* +import com.mapsindoors.core.models.MPCameraEventListener +import com.mapsindoors.core.models.MPCameraPosition +import com.mapsindoors.core.models.MPOnInfoWindowClickListener +import com.mapsindoors.core.models.MPOnMarkerClickListener +import com.mapsindoors.core.models.MPMapStyle +import com.mapspeople.mapsindoors.core.models.* +import com.mapspeople.mapsindoors.core.* +import com.mapspeople.mapsindoors.PlatformMapView +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.platform.PlatformView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class MapView(context: Context, binaryMessenger: BinaryMessenger, val args: HashMap<*,*>?, private val lifecycleProvider: MapsindoorsPlugin.LifecycleProvider) : PlatformMapView(context), PlatformView, MethodCallHandler, DefaultLifecycleObserver { + private val channel : MethodChannel = MethodChannel(binaryMessenger, "MapControlMethodChannel") + private val listenerChannel : MethodChannel = MethodChannel(binaryMessenger, "MapControlListenerMethodChannel") + private val floorSelectorChannel : MethodChannel = MethodChannel(binaryMessenger, "MapControlFloorSelectorChannel") + private var mMapControl : MapControl? = null + private val gson = Gson() + private val mDirectionsRenderer: DirectionsRenderer = DirectionsRenderer(context, binaryMessenger) + private var mConfig: MapConfig? = null + private var initializing: Boolean = false + + init { + lifecycleProvider.getLifecycle()?.addObserver(this) + mConfig = gson.fromJson(args?.get("mapConfig") as? String, MapConfig::class.java) + channel.setMethodCallHandler(this::onMethodCall) + listenerChannel.setMethodCallHandler(this::onListenerCall) + floorSelectorChannel.setMethodCallHandler { call, result -> + when (call.method) { + "FSE_onFloorChanged" -> { + floorSelectorInterface.listener?.onFloorSelectionChanged(gson.fromJson(call.argument("floor"), MPFloor::class.java)) + } + else -> result.notImplemented() + } + } + } + + override fun getView(): View { + return getMapView() + } + + override fun dispose() { + lifecycleProvider.getLifecycle()?.removeObserver(this) + disposeMap() + mMapControl?.onDestroy() + mMapControl = null + mConfig = null + } + + override fun whenMapReady() { + args?.get("floorSelectorAutoFloorChange")?.let { + floorSelectorInterface.autoFloorChange = it as Boolean + } + initialize() + } + + private val floorSelectorInterface = object : MPFloorSelectorInterface { + var autoFloorChange = true + var listener : OnFloorSelectionChangedListener? = null + override fun getView(): View? { + return null + } + + override fun setOnFloorSelectionChangedListener(p0: OnFloorSelectionChangedListener?) { + listener = p0 + } + + override fun setList(p0: MutableList?) { + val ret = if (p0 == null) null else gson.toJson(p0) + floorSelectorChannel.invokeMethod("setList", ret) + } + + override fun show(p0: Boolean, p1: Boolean) { + floorSelectorChannel.invokeMethod("show", mapOf("show" to p0, "animate" to p1)) + } + + override fun setSelectedFloor(p0: MPFloor) { + floorSelectorChannel.invokeMethod("setSelectedFloor", gson.toJson(p0)) + } + + override fun setSelectedFloorByZIndex(p0: Int) { + floorSelectorChannel.invokeMethod("setSelectedFloorByZIndex", p0) + } + + override fun zoomLevelChanged(p0: Float) { + floorSelectorChannel.invokeMethod("zoomLevelChanged", p0.toDouble()) + } + + override fun isAutoFloorChangeEnabled(): Boolean { + return autoFloorChange + } + + override fun setUserPositionFloor(p0: Int) { + floorSelectorChannel.invokeMethod("setUserPositionFloor", p0) + } + } + + public fun initialize() { + if (!initializing) { + initializing = true; + CoroutineScope(Dispatchers.Main).launch { + makeMPConfig(mConfig, floorSelectorInterface)?.let { + MapControl.create(it) { mc, e -> + if (e == null && mc != null) { + mDirectionsRenderer.setMapControl(mc) + mMapControl = mc + setupListeners() + channel.invokeMethod("create", gson.toJson(e)) + } + initializing = false + } + } + } + } + } + + private var cameraListener : MPCameraEventListener? = null + private var floorUpdateListener : OnFloorUpdateListener? = null + private var buildingFoundAtCameraTargetListener : OnBuildingFoundAtCameraTargetListener? = null + private var venueFoundAtCameraTargetListener : OnVenueFoundAtCameraTargetListener? = null + private var locationSelectedListener : OnLocationSelectedListener? = null + private var mapClickListener : MPOnMapClickListener? = null + private var markerClickListener : MPOnMarkerClickListener? = null + private var markerInfoWindowClickListener : MPOnInfoWindowClickListener? = null + + private fun setupListeners() { + cameraListener?.let { + mMapControl?.addOnCameraEventListener(it) + } + floorUpdateListener?.let { + mMapControl?.addOnFloorUpdateListener(it) + } + mMapControl?.setOnCurrentBuildingChangedListener(buildingFoundAtCameraTargetListener) + mMapControl?.setOnCurrentVenueChangedListener(venueFoundAtCameraTargetListener) + mMapControl?.setOnLocationSelectedListener(locationSelectedListener) + mMapControl?.setOnMapClickListener(mapClickListener) + mMapControl?.setOnMarkerClickListener(markerClickListener) + mMapControl?.setOnMarkerInfoWindowClickListener(markerInfoWindowClickListener) + } + + private fun setupFloorSelector(autoFloorChangeEnabled: Boolean?) { + autoFloorChangeEnabled?.let { + floorSelectorInterface.autoFloorChange = it + } + mMapControl?.floorSelector = floorSelectorInterface + mMapControl?.hideFloorSelector(false) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + fun error(code: String = "-1", message: String? = "Argument was null", details: Any? = null) = result.error(code, message, details) + + fun success(ret : Any? = "success") = result.success(ret) + + fun arg(name: String) : T? = call.argument(name) + + val method = call.method.drop(4) + + when (method) { + "selectFloor" -> { + val floor : Int = arg("floorIndex") as Int + try { + mMapControl?.selectFloor(floor) + success() + } catch (e: Exception) { + error("-1", e.message, call.method) + } + } + "clearFilter" -> { + mMapControl?.clearFilter() + success() + } + "deSelectLocation" -> { + mMapControl?.deSelectLocation() + success() + } + "getCurrentBuilding" -> { + success(gson.toJson(mMapControl?.currentBuilding)) + } + "getCurrentBuildingFloor" -> { + success(gson.toJson(mMapControl?.currentBuildingFloor)) + } + "getCurrentFloorIndex" -> { + success(mMapControl?.currentFloorIndex) + } + "setFloorSelector" -> { + setupFloorSelector(arg("isAutoFloorChangeEnabled")) + success() + } + "getCurrentMapsIndoorsZoom" -> { + success(mMapControl?.currentMapsIndoorsZoom) + } + "getCurrentVenue" -> { + success(gson.toJson(mMapControl?.currentVenue)) + } + "getMapStyle" -> { + success(gson.toJson(mMapControl?.mapStyle)) + } + "getMapViewPaddingBottom" -> { + success(mMapControl?.mapViewPaddingBottom) + } + "getMapViewPaddingEnd" -> { + success(mMapControl?.mapViewPaddingEnd) + } + "getMapViewPaddingStart" -> { + success(mMapControl?.mapViewPaddingStart) + } + "getMapViewPaddingTop" -> { + success(mMapControl?.mapViewPaddingTop) + } + "goTo" -> { + val json: String? = arg("entity") + if (json == null) { + success() + return + } + val entity : MPEntity? + try { + when (arg("type")) { + "MPLocation" -> { + entity = gson.fromJson(json, Location::class.java).toMPLocation() + } + "MPFloor" -> { + entity = gson.fromJson(json, MPFloor::class.java) + } + "MPBuilding" -> { + entity = gson.fromJson(json, MPBuilding::class.java) + } + "MPVenue" -> { + entity = gson.fromJson(json, MPVenue::class.java) + } + else -> { + result.error("-1", "Not a mapsIndoors entity", call.method) + return + } + } + } catch (e: Exception) { + error("-1", e.message) + return + } + mMapControl?.goTo(entity) + success() + } + "hideFloorSelector" -> { + mMapControl?.hideFloorSelector(arg("hide") as Boolean) + success() + } + "isFloorSelectorHidden" -> { + success(mMapControl?.isFloorSelectorHidden) + } + "isUserPositionShown" -> { + success(mMapControl?.isUserPositionShown) + } + "selectBuilding" -> { + val building = gson.fromJson(arg("building") as String?, MPBuilding::class.java) + val moveCamera = arg("moveCamera") + if (building != null && moveCamera != null) { + mMapControl?.selectBuilding(building, moveCamera) + success() + } else { + error("-1", "parameters are null", call.method) + } + } + "selectLocation" -> { + val location = MapsIndoors.getLocationById(arg("location")) + val behavior = gson.fromJson(arg("behavior") as String?, MapBehavior::class.java) + if (behavior != null) { + mMapControl?.selectLocation(location, behavior.toMPSelectionBehavior()) + success() + } else { + error("-1", "behavior is null", call.method) + } + } + "selectLocationById" -> { + val id = arg("id") + val behavior = gson.fromJson(arg("behavior"), MapBehavior::class.java) + if (id != null && behavior != null) { + mMapControl?.selectLocation(id, behavior.toMPSelectionBehavior()) + success("success") + } else { + error("-1", "parameters are null", call.method) + } + } + "selectVenue" -> { + val venue = gson.fromJson(arg("venue") as String?, MPVenue::class.java) + val moveCamera = arg("moveCamera") + if (venue != null && moveCamera != null) { + mMapControl?.selectVenue(venue, moveCamera) + success("success") + } else { + error("-1", "parameters are null", call.method) + } + } + "setFilter" -> { + val filter = gson.fromJson(arg("filter"), Filter::class.java) + val behavior = gson.fromJson(arg("behavior"), MapBehavior::class.java) + if (filter != null && behavior != null) { + mMapControl?.setFilter(filter.toMPFilter(), behavior.toMPFilterBehavior(), object : MPSuccessListener { + override fun onSuccess() { + success() + } + override fun onFailure() { + error("-1", "could not set filter", null) + } + }) + } else { + error("-1", "parameters are null", call.method) + } + } + "setFilterWithLocations" -> { + val locationIds = arg>("locations") as List? + val locations : MutableList = mutableListOf() + for (id in locationIds!!){ + locations.add(MapsIndoors.getLocationById(id)!!) + } + val behavior = gson.fromJson(arg("behavior"), MapBehavior::class.java) + if (locations != null && behavior != null) { + mMapControl?.setFilter(locations, behavior.toMPFilterBehavior()) + success() + } else { + error("-1", "parameters are null", call.method) + } + } + "setMapPadding" -> { + val start = arg("start") + val top = arg("top") + val end = arg("end") + val bottom = arg("bottom") + if (start != null && top != null && end != null && bottom != null) { + mMapControl?.setMapPadding(start, top, end, bottom) + success() + } else { + error("-1", "Some arguements were null", call.method) + } + } + "setMapStyle" -> { + val mapStyle = gson.fromJson(arg("mapStyle"), MPMapStyle::class.java) + if (mapStyle != null) { + mMapControl?.mapStyle = mapStyle + success() + } else { + error("-1", "some arguments were null", call.method) + } + } + "showInfoWindowOnClickedLocation" -> { + val should = arg("show") + if (should != null) { + mMapControl?.showInfoWindowOnClickedLocation(should) + success() + } else { + error("-1", "some arguments were null", call.method) + } + } + "showUserPosition" -> { + val should = arg("show") + if (should != null) { + mMapControl?.showUserPosition(should) + success() + } else { + error("-1", "some arguments were null", call.method) + } + } + "enableLiveData" -> { + val hasListener = arg("listener") + val domainType = arg("domainType") + if (hasListener == true) { + val listener = OnLiveLocationUpdateListener { + listenerChannel.invokeMethod("onLiveLocationUpdate", mapOf("location" to it, "domainType" to domainType)) + } + mMapControl?.enableLiveData(domainType, listener) + } else { + mMapControl?.enableLiveData(domainType) + } + success() + } + "disableLiveData" -> { + val domainType = arg("domainType") + if (domainType != null) { + mMapControl?.disableLiveData(domainType) + } + success() + } + "moveCamera", "animateCamera" -> { + val update = gson.fromJson(arg("update"), CameraUpdate::class.java) + val duration : Int? = arg("duration") + + + updateCamera(call.method == "moveCamera", update, duration) { + success() + } + } + "getCurrentCameraPosition" -> { + success(gson.toJson(currentCameraPosition)) + } + else -> { + result.notImplemented() + } + } + } + + private fun onListenerCall(call: MethodCall, result: MethodChannel.Result) { + val setup = call.argument("setupListener") + if (setup == null) { + result.error("-1", "Cannot modify listener when setup is $setup", null) + return + } + val consumeEvent = call.argument("consumeEvent") + + val method = call.method.drop(4) + + when (method) { + "cameraEventListener" -> { + if (setup) { + cameraListener = MPCameraEventListener { + listenerChannel.invokeMethod("onCameraEvent", it.ordinal) + }.also { + mMapControl?.addOnCameraEventListener(it) + } + } else { + cameraListener?.let { + mMapControl?.removeOnCameraEventListener(it) + } + cameraListener = null + } + } + "floorUpdateListener" -> { + if (setup) { + floorUpdateListener = OnFloorUpdateListener { _, floor -> + listenerChannel.invokeMethod("onFloorUpdate", floor) + }.also { + mMapControl?.addOnFloorUpdateListener(it) + } + } else { + floorUpdateListener?.let { + mMapControl?.removeOnFloorUpdateListener(it) + } + floorUpdateListener = null + } + } + "buildingFoundAtCameraTargetListener" -> { + if (setup) { + buildingFoundAtCameraTargetListener = OnBuildingFoundAtCameraTargetListener { + listenerChannel.invokeMethod("onBuildingFoundAtCameraTarget", gson.toJson(it)) + }.also { + mMapControl?.setOnCurrentBuildingChangedListener(it) + } + } else { + mMapControl?.setOnCurrentBuildingChangedListener(null) + buildingFoundAtCameraTargetListener = null + } + } + "venueFoundAtCameraTargetListener" -> { + if (setup) { + venueFoundAtCameraTargetListener = OnVenueFoundAtCameraTargetListener { + listenerChannel.invokeMethod("onVenueFoundAtCameraTarget", gson.toJson(it)) + }.also { + mMapControl?.setOnCurrentVenueChangedListener(it) + } + } else { + mMapControl?.setOnCurrentVenueChangedListener(null) + venueFoundAtCameraTargetListener = null + } + } + "locationSelectedListener" -> { + if (setup) { + locationSelectedListener = OnLocationSelectedListener { + listenerChannel.invokeMethod("onLocationSelected", gson.toJson(it)) + return@OnLocationSelectedListener consumeEvent == true + }.also { + mMapControl?.setOnLocationSelectedListener(it) + } + } else { + mMapControl?.setOnLocationSelectedListener(null) + locationSelectedListener = null + } + } + "mapClickListener" -> { + if (setup) { + mapClickListener = MPOnMapClickListener { latLng, locations -> + val point = gson.toJson(MPPoint(latLng)) + val locs = gson.toJson(locations) + CoroutineScope(Dispatchers.Main).launch { + listenerChannel.invokeMethod("onMapClick", mapOf("locations" to locs, "point" to point)) + } + return@MPOnMapClickListener consumeEvent == true + }.also { + mMapControl?.setOnMapClickListener(it) + } + } else { + mMapControl?.setOnMapClickListener(null) + mapClickListener = null + } + } + "markerClickListener" -> { + if (setup) { + markerClickListener = MPOnMarkerClickListener { + listenerChannel.invokeMethod("onMarkerClick", it?.id) + return@MPOnMarkerClickListener consumeEvent == true + }.also { + mMapControl?.setOnMarkerClickListener(it) + } + } else { + mMapControl?.setOnMarkerClickListener(null) + markerClickListener = null + } + } + "markerInfoWindowClickListener" -> { + if (setup) { + markerInfoWindowClickListener = MPOnInfoWindowClickListener { + listenerChannel.invokeMethod("onInfoWindowClick", it?.id) + }.also { + mMapControl?.setOnMarkerInfoWindowClickListener(it) + } + } else { + mMapControl?.setOnMarkerInfoWindowClickListener(null) + markerInfoWindowClickListener = null + } + } + } + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/MapViewFactory.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/MapViewFactory.kt new file mode 100644 index 0000000..6bda521 --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/MapViewFactory.kt @@ -0,0 +1,21 @@ +package com.mapspeople.mapsindoors.core + +import com.mapspeople.mapsindoors.core.* +import android.content.Context +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.StandardMessageCodec +import io.flutter.plugin.platform.PlatformView +import io.flutter.plugin.platform.PlatformViewFactory + +class MapViewFactory(private val binaryMessenger: BinaryMessenger, private val lifecycleProvider: MapsindoorsPlugin.LifecycleProvider, private val readyListener: OnMapViewReady?) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { + + override fun create(context: Context?, viewId: Int, args: Any?): PlatformView { + return MapView(context!!, binaryMessenger, args as HashMap<*,*>?, lifecycleProvider).also { + readyListener?.ready(it) + } + } + + public fun interface OnMapViewReady { + fun ready(view: MapView) + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/MapsindoorsPlugin.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/MapsindoorsPlugin.kt new file mode 100644 index 0000000..8e1fd51 --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/MapsindoorsPlugin.kt @@ -0,0 +1,409 @@ +package com.mapspeople.mapsindoors.core + +import android.app.Application +import androidx.lifecycle.Lifecycle +import com.google.gson.Gson +import com.mapsindoors.core.* +import com.mapspeople.mapsindoors.core.models.* +import com.mapspeople.mapsindoors.* +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter + +/** MapsindoorsPlugin */ +open class MapsindoorsPlugin : FlutterPlugin, ActivityAware { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var mapsIndoorsChannel: MethodChannel + private lateinit var utilChannel: MethodChannel + private lateinit var listenerChannel: MethodChannel + private lateinit var context: Application + private lateinit var mDisplayRuleHandler: DisplayRuleHandler + private var positionProvider: PositionProvider? = null + private lateinit var mDirectionsService: DirectionsService + private val gson = Gson() + private var view: MapView? = null + private var lifecycle: Lifecycle? = null + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + mapsIndoorsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "MapsIndoorsMethodChannel") + mapsIndoorsChannel.setMethodCallHandler(this::handleMapsIndoorsChannel) + utilChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "UtilMethodChannel") + utilChannel.setMethodCallHandler(this::handleUtilChannel) + listenerChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "MapsIndoorsListenerChannel") + listenerChannel.setMethodCallHandler(this::handleListenerChannel) + mDisplayRuleHandler = DisplayRuleHandler(flutterPluginBinding.binaryMessenger) + mDirectionsService = DirectionsService(flutterPluginBinding.applicationContext, flutterPluginBinding.binaryMessenger) + + flutterPluginBinding.platformViewRegistry.registerViewFactory( + "", + MapViewFactory(flutterPluginBinding.binaryMessenger, + object : LifecycleProvider { + override fun getLifecycle(): Lifecycle? { + return lifecycle + } + }) { + view = it + } + ) + context = flutterPluginBinding.applicationContext as Application + } + + private fun handleMapsIndoorsChannel(call: MethodCall, result: Result) { + + fun error(code: String = "-1", message: String? = "Argument was null", details: Any? = null) = result.error(code, message, details) + + fun success(ret : Any? = "success") = result.success(ret) + + fun arg(name: String) : T? = call.argument(name) + + val method = call.method.drop(4) + when (method) { + "initialize" -> { + val key = arg("key") + if (key == null) { + error() + return + } + MapsIndoors.load(context, key) { error -> + view?.initialize() + success(if (error == null) null else gson.toJson(MPError.fromMIError(error))) + } + } + "locationDisplayRuleExists" -> { + val loc = MapsIndoors.getLocationById(arg("id")) + if (loc != null) { + success(true) + } else { + error() + } + } + "displayRuleNameExists" -> { + success(MapsIndoors.getDisplayRule(arg("name")!!)?.id != null) + } + "checkOfflineDataAvailability" -> { + success(MapsIndoors.checkOfflineDataAvailability()) + } + "destroy" -> { + MapsIndoors.destroy() + success() + } + "disableEventLogging" -> { + MapsIndoors.disableEventLogging(arg("disable") as Boolean) + success() + } + "getAPIKey" -> { + success(MapsIndoors.getAPIKey()) + } + "getAvailableLanguages" -> { + success(MapsIndoors.getAvailableLanguages()) + } + "getBuildings" -> { + val buildings = MapsIndoors.getBuildings()?.buildings + success(if (buildings != null) gson.toJson(buildings) else null) + } + "getCategories" -> { + val categories = MapsIndoors.getCategories()?.categories + success(if (categories != null) gson.toJson(categories) else null) + } + "getDataSet" -> { + val dataset = MapsIndoors.getDataSet() + success(if (dataset != null) gson.toJson(dataset) else null) + } + "getDefaultLanguage" -> { + success(MapsIndoors.getDefaultLanguage()) + } + "getLanguage" -> { + success(MapsIndoors.getLanguage()) + } + "getLocationById" -> { + val location = MapsIndoors.getLocationById(arg("id")) + success(if (location != null) gson.toJson(location) else null) + } + "getLocations" -> { + success(gson.toJson(MapsIndoors.getLocations())) + } + "getLocationsByExternalIds" -> { + val ids = arg>("ids")!! + success(gson.toJson(MapsIndoors.getLocationsByExternalIds(ids))) + } + "getLocationsByQuery" -> { + val filter = gson.fromJson(arg("filter"), Filter::class.java)?.toMPFilter() + val query = gson.fromJson(arg("query"), Query::class.java)?.toMPQuery() + MapsIndoors.getLocationsAsync(query, filter) { mpLocations, miError -> + if (miError == null) { + success(gson.toJson(mpLocations)) + } else { + error(miError.message) + } + } + } + "getMapStyles" -> { + success(gson.toJson(MapsIndoors.getMapStyles())) + } + "getSolution" -> { + val solution = MapsIndoors.getSolution() + success(if (solution != null) gson.toJson(solution) else null) + } + "getVenues" -> { + val venues = MapsIndoors.getVenues()?.venues + success(if (venues != null) gson.toJson(venues) else null) + } + "isAPIKeyValid" -> { + success(MapsIndoors.isAPIKeyValid()) + } + "isInitialized" -> { + success(MapsIndoors.isInitialized()) + } + "isReady" -> { + success(MapsIndoors.isReady()) + } + "setLanguage" -> { + success(MapsIndoors.setLanguage(arg("language") as String)) + } + "synchronizeContent" -> { + MapsIndoors.synchronizeContent { + success(if (it == null) null else MPError.fromMIError(it)) + } + } + "applyUserRoles" -> { + val userRoles = gson.fromJson>(arg("userRoles"), type>()) + MapsIndoors.applyUserRoles(userRoles) + success() + } + "getAppliedUserRoles" -> { + val userRoles = MapsIndoors.getAppliedUserRoles() + success(if(userRoles != null) gson.toJson(userRoles) else null) + } + "getUserRoles" -> { + val userRoles = MapsIndoors.getUserRoles()?.userRoles + success(if(userRoles != null) gson.toJson(userRoles) else null) + } + "reverseGeoCode" -> { + val point = gson.fromJson(arg("point"), MPPoint::class.java) + if (point != null) { + MapsIndoors.reverseGeoCode(point) { + success(if (it != null) gson.toJson(it) else null) + } + } + } + "setPositionProvider" -> { + val remove = arg("remove") + val name = arg("name") + if (remove != null && !remove) { + if (name != null) { + if (positionProvider == null || positionProvider?.name != name) { + positionProvider = PositionProvider(name) + } + MapsIndoors.setPositionProvider(positionProvider) + } else { + error() + return + } + } else { + MapsIndoors.setPositionProvider(null) + } + success() + } + "getDefaultVenue" -> { + success(gson.toJson(MapsIndoors.getVenues()?.getDefaultVenue())) + } + else -> { + result.notImplemented() + } + } + } + + private fun handleUtilChannel(call: MethodCall, result: Result) { + + fun error(code: String = "-1", message: String? = "Argument was null", details: Any? = null) = result.error(code, message, details) + + fun success(ret : Any? = "success") = result.success(ret) + + fun getSolutionConfig() : MPSolutionConfig? = MapsIndoors.getSolution()?.config + + fun arg(name: String) : T? = call.argument(name) + + val method = call.method.drop(4) + + when (method) { + "getPlatformVersion" -> { + success("Android ${android.os.Build.VERSION.RELEASE}") + } + "venueHasGraph" -> { + val hasGraph = MapsIndoors.getVenues()?.getVenueById(arg("id"))?.hasGraph() + success(hasGraph ?: false) + } + "pointAngleBetween" -> { + try { + val it = gson.fromJson(arg("it"), MPPoint::class.java) + val other = gson.fromJson(arg("other"), MPPoint::class.java) + val angle = it.angleBetween(other) + success(angle) + } catch (e: java.lang.Exception) { + error("20", e.message, "Could not complete \"pointAngleBetween\" method") + } + } + "pointDistanceTo" -> { + try { + val it = gson.fromJson(arg("it"), MPPoint::class.java) + val other = gson.fromJson(arg("other"), MPPoint::class.java) + val distance = it.distanceTo(other) + success(distance) + } catch (e: java.lang.Exception) { + error("404", e.message, "Could not complete \"pointDistanceTo\" method") + } + } + "geometryIsInside" -> { + try { + val point = gson.fromJson(arg("point"), MPPoint::class.java) + val geo = gson.fromJson(arg("it"), MPGeometry::class.java) + when (geo.type) { + "MPPoint" -> { + val it = gson.fromJson(arg("it"), MPPoint::class.java) + success(it.isInside(point.latLng)) + } + "MPPolygon" -> { + val it = gson.fromJson(arg("it"), MPPolygonGeometry::class.java) + success(it.isInside(point.latLng)) + } + "MPMultiPolygon" -> { + val it = gson.fromJson(arg("it"), MPMultiPolygonGeometry::class.java) + success(it.isInside(point.latLng)) + } + } + } catch (e: java.lang.Exception) { + error("404", e.message, "Could not complete \"geometryIsInside\" method") + } + } + "geometryArea" -> { + try { + val geometry = arg("geometry") + val gson = Gson() + val geo = gson.fromJson(geometry, MPGeometry::class.java) + when (geo.type) { + "MPPoint" -> { + val it = gson.fromJson(geometry, MPPoint::class.java) + success(it.area) + } + "MPPolygon" -> { + val it = gson.fromJson(geometry, MPPolygonGeometry::class.java) + success(it.area) + } + "MPMultiPolygon" -> { + val it = gson.fromJson(geometry, MPMultiPolygonGeometry::class.java) + success(it.area) + } + } + } catch (e: java.lang.Exception) { + error("404", e.message, "Could not complete \"geometryArea\" method") + } + } + "polygonDistToClosestEdge" -> { + try { + val point = gson.fromJson(arg("point"), MPPoint::class.java) + val geo = gson.fromJson(arg("it"), MPGeometry::class.java) + when (geo.type) { + "MPPolygon" -> { + val it = gson.fromJson(arg("it"), MPPolygonGeometry::class.java) + success(it.getSquaredDistanceToClosestEdge(point)) + } + "MPMultiPolygon" -> { + val it = gson.fromJson(arg("it"), MPMultiPolygonGeometry::class.java) + success(it.getSquaredDistanceToClosestEdge(point)) + } + } + } catch (e: java.lang.Exception) { + error("404", e.message, "Could not complete \"polygonDistToClosestEdge\" method") + } + } + "parseMapClientUrl" -> { + val venueId = arg("venueId") + val locationId = arg("locationId") + val solution = MapsIndoors.getSolution() + if (venueId != null && locationId != null && solution != null) { + success(solution.parseMapClientUrl(venueId, locationId)) + } else { + error() + } + } + "setCollisionHandling" -> { + val collisionHandling = MPCollisionHandling.fromStringValue(arg("handling")!!) + getSolutionConfig()?.setCollisionHandling(collisionHandling) + success() + } + "setEnableClustering" -> { + val enable = arg("enable")!!.toBoolean() + getSolutionConfig()?.setEnableClustering(enable) + success() + } + "setExtrusionOpacity" -> { + val opacity = arg("opacity")!!.toDouble() + getSolutionConfig()?.settings3D?.setExtrusionOpacity(opacity.toFloat()) + success() + } + "setWallOpacity" -> { + val opacity = arg("opacity")!!.toDouble() + getSolutionConfig()?.settings3D?.setWallOpacity(opacity.toFloat()) + success() + } + else -> { + result.notImplemented() + } + } + } + + private var onMapsIndoorsReadyListener: OnMapsIndoorsReadyListener? = null + + private fun handleListenerChannel(call: MethodCall, result: Result) { + val method = call.method.drop(4) + when (method) { + "onMapsIndoorsReadyListener" -> { + val setup = call.argument("addListener") + if (setup == true) { + onMapsIndoorsReadyListener = OnMapsIndoorsReadyListener { + listenerChannel.invokeMethod("onMapsIndoorsReady", gson.toJson(gson.toJson(MPError.fromMIError(it)))) + } + MapsIndoors.addOnMapsIndoorsReadyListener(onMapsIndoorsReadyListener!!) + } else { + MapsIndoors.removeOnMapsIndoorsReadyListener(onMapsIndoorsReadyListener!!) + } + } + "onPositionUpdate" -> { + val update = gson.fromJson(call.argument("position"), PositionResult::class.java) + positionProvider?.updatePosition(update) + } + else -> result.notImplemented() + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + mapsIndoorsChannel.setMethodCallHandler(null) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding) + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding) + } + + override fun onDetachedFromActivity() { + lifecycle = null + } + + override fun onDetachedFromActivityForConfigChanges() { + lifecycle = null + } + + interface LifecycleProvider { + fun getLifecycle(): Lifecycle? + } +} diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/PlatformMapViewInterface.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/PlatformMapViewInterface.kt new file mode 100644 index 0000000..58c7217 --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/PlatformMapViewInterface.kt @@ -0,0 +1,15 @@ +package com.mapspeople.mapsindoors.core + +import com.mapspeople.mapsindoors.core.models.* +import com.mapsindoors.core.MPFloorSelectorInterface +import com.mapsindoors.core.MPIMapConfig +import android.view.View + +interface PlatformMapViewInterface { + val currentCameraPosition: CameraPosition? + fun disposeMap() + fun getMapView() : View + fun makeMPConfig(config: MapConfig?, floorSelectorInterface: MPFloorSelectorInterface) : MPIMapConfig? + fun updateCamera(move: Boolean, update: CameraUpdate, duration: Int?, success: () -> Unit) + fun whenMapReady() +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Bounds.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Bounds.kt new file mode 100644 index 0000000..5940c94 --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Bounds.kt @@ -0,0 +1,12 @@ +package com.mapspeople.mapsindoors.core.models + +import com.google.gson.annotations.SerializedName +import com.mapsindoors.core.MPPoint + +/** + * Used to deserialize MPBounds from dart + */ +data class Bounds( + @SerializedName("northeast") val northEast: MPPoint, + @SerializedName("southwest") val southWest: MPPoint +) \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/CameraPosition.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/CameraPosition.kt new file mode 100644 index 0000000..5147ddc --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/CameraPosition.kt @@ -0,0 +1,11 @@ +package com.mapspeople.mapsindoors.core.models + +import com.google.gson.annotations.SerializedName +import com.mapsindoors.core.MPPoint + +data class CameraPosition( + @SerializedName("zoom") val zoom: Float?, + @SerializedName("tilt") val tilt: Float?, + @SerializedName("bearing") val bearing: Float?, + @SerializedName("target") val target: MPPoint? +) \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/CameraUpdate.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/CameraUpdate.kt new file mode 100644 index 0000000..82655cd --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/CameraUpdate.kt @@ -0,0 +1,37 @@ +package com.mapspeople.mapsindoors.core.models + +import com.google.gson.annotations.SerializedName +import com.mapsindoors.core.MPPoint + +data class CameraUpdate( + @SerializedName("mode") private val modeString : String, + @SerializedName("point") val point : MPPoint?, + @SerializedName("bounds") val bounds : Bounds?, + @SerializedName("padding") val padding : Int?, + @SerializedName("width") val width : Int?, + @SerializedName("height") val height : Int?, + @SerializedName("zoom") val zoom : Float?, + @SerializedName("position") val position : CameraPosition?, +) { + val mode : CameraUpdateMode + get() { + return CameraUpdateMode.fromString(modeString) + } +} + +enum class CameraUpdateMode(val mode: String) { + FROMPOINT("fromPoint"), FROMBOUNDS("fromBounds"), ZOOMBY("zoomBy"), ZOOMTO("zoomTo"), FROMCAMERAPOSITION("fromCameraPosition"); + + companion object { + fun fromString(value : String) : CameraUpdateMode { + return when (value) { + "fromPoint" -> FROMPOINT + "fromBounds" -> FROMBOUNDS + "zoomBy" -> ZOOMBY + "zoomTo" -> ZOOMTO + "fromCameraPosition" -> FROMCAMERAPOSITION + else -> throw IllegalArgumentException() + } + } + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Filter.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Filter.kt new file mode 100644 index 0000000..005bcdb --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Filter.kt @@ -0,0 +1,47 @@ +package com.mapspeople.mapsindoors.core.models + +import com.google.gson.annotations.SerializedName +import com.mapsindoors.core.MPFilter +import com.mapsindoors.core.MPMapExtend +import com.mapsindoors.core.models.MPLatLngBounds + +/** + * Used to deserialize MPFilter from dart, and converting it into MPFilter in the MapsIndoors SDK + */ +data class Filter( +@SerializedName("take") private val mTake : Int?, +@SerializedName("skip") private val mSkip : Int?, +@SerializedName("categories") private val mCategories : List?, +@SerializedName("locations") private val mLocations : List?, +@SerializedName("types") private val mTypes : List?, +@SerializedName("parents") private val mParents : List?, +@SerializedName("mapExtend") private val mMapExtend : Bounds?, +@SerializedName("geometry") private val mGeometry : Bounds?, +@SerializedName("floorIndex") private val mFloorIndex : Int?, +@SerializedName("depth") private val mDepth : Int?, +@SerializedName("ignoreLocationSearchableStatus") private val mIgnoreLocationSearchableStatus : Boolean?, +@SerializedName("ignoreLocationActiveStatus") private val mIgnoreLocationActiveStatus : Boolean?, +) { + fun toMPFilter() : MPFilter { + val builder = MPFilter.Builder() + mTake?.let(builder::setTake) + mSkip?.let(builder::setSkip) + mCategories?.let(builder::setCategories) + mLocations?.let(builder::setLocations) + mTypes?.let(builder::setTypes) + mParents?.let(builder::setParents) + mMapExtend?.let { + builder.setMapExtend(MPMapExtend(it.southWest.latLng, it.northEast.latLng)) + } + mGeometry?.let { + val mpBounds = MPLatLngBounds(mGeometry.southWest.latLng, mGeometry.northEast.latLng) + builder.setGeometry(MPFilter.Geometry(mpBounds.center.lat, mpBounds.center.lng, MPMapExtend(mpBounds))) + } + mFloorIndex?.let(builder::setFloorIndex) + mDepth?.let(builder::setDepth) + mIgnoreLocationActiveStatus?.let(builder::setIgnoreLocationActiveStatus) + mIgnoreLocationSearchableStatus?.let(builder::setIgnoreLocationSearchableStatus) + return builder.build() + + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Location.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Location.kt new file mode 100644 index 0000000..9bb8ddc --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Location.kt @@ -0,0 +1,22 @@ +package com.mapspeople.mapsindoors.core.models + +import com.google.gson.annotations.SerializedName + +import com.mapsindoors.core.MapsIndoors +import com.mapsindoors.core.MPLocation + +data class Location(@SerializedName("locationId") private val id : String) { + fun toMPLocation() : MPLocation? { + return MapsIndoors.getLocationById(id) + } + + companion object { + fun fromMPLocation(location: MPLocation?) : Location? { + return if (location != null) { + Location(location.locationId) + } else { + null + } + } + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/MPError.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/MPError.kt new file mode 100644 index 0000000..1188c73 --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/MPError.kt @@ -0,0 +1,21 @@ +package com.mapspeople.mapsindoors.core.models + +import com.google.gson.annotations.SerializedName +import com.mapsindoors.core.errors.MIError + +data class MPError( + @SerializedName("code") private val mCode : Int, + @SerializedName("message") private val mMessage : String, + @SerializedName("status") private val mStatus : Int?, + @SerializedName("tag") private val mTag : Any? +) { + companion object { + fun fromMIError(error: MIError?) : MPError? { + return if (error != null) { + MPError(error.code, error.message, if (error.status != 0) error.status else null, error.tag) + } else { + null + } + } + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/MapBehavior.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/MapBehavior.kt new file mode 100644 index 0000000..f1fe555 --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/MapBehavior.kt @@ -0,0 +1,37 @@ +package com.mapspeople.mapsindoors.core.models + +import com.google.gson.annotations.SerializedName +import com.mapsindoors.core.MPFilterBehavior +import com.mapsindoors.core.MPSelectionBehavior + +/** + * Used to deserialize MPSelectionBehavior and MPFilterBehavior from dart + * and converting it into the corresponding behavior in the MapsIndoors SDK + */ +data class MapBehavior( + @SerializedName("allowFloorChange") private val mAllowFloorChange: Boolean?, + @SerializedName("moveCamera") private val mMoveCamera: Boolean?, + @SerializedName("animationDuration") private val mAnimationDuration : Int?, + @SerializedName("showInfoWindow") private val mShowInfoWindow : Boolean?, + @SerializedName("zoomToFit") private val mZoomToFit : Boolean? +) { + fun toMPFilterBehavior() : MPFilterBehavior { + val builder = MPFilterBehavior.Builder() + mAllowFloorChange?.let(builder::setAllowFloorChange) + mMoveCamera?.let(builder::setMoveCamera) + mAnimationDuration?.let(builder::setAnimationDuration) + mShowInfoWindow?.let(builder::setShowInfoWindow) + mZoomToFit?.let(builder::setZoomToFit) + return builder.build() + } + + fun toMPSelectionBehavior() : MPSelectionBehavior { + val builder = MPSelectionBehavior.Builder() + mAllowFloorChange?.let(builder::setAllowFloorChange) + mMoveCamera?.let(builder::setMoveCamera) + mAnimationDuration?.let(builder::setAnimationDuration) + mShowInfoWindow?.let(builder::setShowInfoWindow) + mZoomToFit?.let(builder::setZoomToFit) + return builder.build() + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/MapConfig.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/MapConfig.kt new file mode 100644 index 0000000..03a4f41 --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/MapConfig.kt @@ -0,0 +1,15 @@ +package com.mapspeople.mapsindoors.core.models + +import com.google.gson.annotations.SerializedName + +data class MapConfig( + @SerializedName("typeface") val typeface: String?, + @SerializedName("color") val color: String?, + @SerializedName("showHalo") val showHalo: Boolean?, + @SerializedName("showFloorSelector") val showFloorSelector: Boolean?, + @SerializedName("textSize") val textSize: Double?, + @SerializedName("showInfoWindowOnLocationClicked") val showInfoWindowOnLocationClicked: Boolean?, + @SerializedName("showUserPosition") val showUserPosition: Boolean?, + @SerializedName("tileFadeInEnabled") val tileFadeInEnabled: Boolean?, + @SerializedName("useDefaultMapsIndoorsStyle") val useDefaultMapsIndoorsStyle: Boolean, +) \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/PositionProvider.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/PositionProvider.kt new file mode 100644 index 0000000..ad5f56c --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/PositionProvider.kt @@ -0,0 +1,44 @@ +package com.mapspeople.mapsindoors.core.models + +import com.mapsindoors.core.MPPositionProvider +import com.mapsindoors.core.MPPositionResultInterface +import com.mapsindoors.core.OnPositionUpdateListener + +class PositionProvider(val name : String) : MPPositionProvider { + private var latest : MPPositionResultInterface? = null + private val listeners : MutableList + + init { + listeners = arrayListOf() + } + + override fun addOnPositionUpdateListener(p0: OnPositionUpdateListener) { + synchronized(listeners) { + listeners.add(p0) + } + } + + override fun removeOnPositionUpdateListener(p0: OnPositionUpdateListener) { + synchronized(listeners) { + listeners.remove(p0) + } + } + + override fun getLatestPosition(): MPPositionResultInterface? { + return latest + } + + fun updatePosition(pos : MPPositionResultInterface?) { + latest = pos?.also { update -> + synchronized(listeners) { + for (listener in listeners) { + listener.onPositionUpdate(update) + } + } + } + } + + override fun toString(): String { + return "{ $name }" + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/PositionResult.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/PositionResult.kt new file mode 100644 index 0000000..ceca5b2 --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/PositionResult.kt @@ -0,0 +1,105 @@ +package com.mapspeople.mapsindoors.core.models + +import android.location.Location +import com.google.gson.annotations.SerializedName +import com.mapsindoors.core.MPPoint +import com.mapsindoors.core.MPPositionProvider +import com.mapsindoors.core.MPPositionResultInterface +import com.mapsindoors.core.OnPositionUpdateListener + +class PositionResult : MPPositionResultInterface { + @SerializedName("point") + private var mPoint: MPPoint? = null + @SerializedName("floorIndex") + private var mFloorIndex: Int? = null + @SerializedName("bearing") + private var mBearing: Double? = null + @SerializedName("accuracy") + private var mAccuracy: Double? = null + @SerializedName("providerName") + private var mProviderName: String? = null + + override fun getPoint(): MPPoint? { + return mPoint + } + + override fun hasFloor(): Boolean { + return mFloorIndex != null + } + + override fun getFloorIndex(): Int { + return mFloorIndex ?: 0 + } + + override fun setFloorIndex(p0: Int) { + throw NotImplementedError() + } + + override fun hasBearing(): Boolean { + return mBearing != null + } + + override fun getBearing(): Float { + return mBearing?.toFloat() ?: 0.0f + } + + override fun setBearing(p0: Float) { + throw NotImplementedError() + } + + override fun hasAccuracy(): Boolean { + return mAccuracy != null + } + + override fun getAccuracy(): Float { + return mAccuracy?.toFloat() ?: 0.0f + } + + override fun setAccuracy(p0: Float) { + throw NotImplementedError() + } + + override fun getProvider(): MPPositionProvider { + return object : MPPositionProvider { + override fun addOnPositionUpdateListener(p0: OnPositionUpdateListener) { + throw NotImplementedError() + } + + override fun removeOnPositionUpdateListener(p0: OnPositionUpdateListener) { + throw NotImplementedError() + } + + override fun getLatestPosition(): MPPositionResultInterface? { + throw NotImplementedError() + } + + override fun toString(): String { + return "$mProviderName" + } + + } + } + + override fun setProvider(p0: MPPositionProvider?) { + throw NotImplementedError() + } + + override fun getAndroidLocation(): Location? { + return point?.let { position -> + val location = Location(mProviderName) + location.bearing = bearing + location.accuracy = accuracy + location.latitude = position.lat + location.longitude = position.lng + return location + } + } + + override fun setAndroidLocation(p0: Location?) { + throw NotImplementedError() + } + + override fun toString(): String { + return "{pos:[${point?.coordinatesAsString}], bearing:$bearing, accuracy:$accuracy, floorIndex:$floorIndex, provider:$provider}" + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Query.kt b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Query.kt new file mode 100644 index 0000000..17688cd --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/kotlin/com/mapspeople/mapsindoors/core/models/Query.kt @@ -0,0 +1,19 @@ +package com.mapspeople.mapsindoors.core.models + +import com.google.gson.annotations.SerializedName +import com.mapsindoors.core.MPPoint +import com.mapsindoors.core.MPQuery + +data class Query( + @SerializedName("query") private val mQuery : String?, + @SerializedName("near") private val mNear : MPPoint?, + @SerializedName("queryProperties") private val mQueryProperties : List? +) { + fun toMPQuery() : MPQuery { + val builder = MPQuery.Builder() + mQuery?.let(builder::setQuery) + mNear?.let(builder::setNear) + mQueryProperties?.let(builder::setQueryProperties) + return builder.build() + } +} \ No newline at end of file diff --git a/mapsindoors_mapbox_android/android/src/main/res/values/mapbox_api_key.xml b/mapsindoors_mapbox_android/android/src/main/res/values/mapbox_api_key.xml new file mode 100644 index 0000000..30c1b3d --- /dev/null +++ b/mapsindoors_mapbox_android/android/src/main/res/values/mapbox_api_key.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/mapsindoors_mapbox_android/pubspec.yaml b/mapsindoors_mapbox_android/pubspec.yaml new file mode 100644 index 0000000..65f85f0 --- /dev/null +++ b/mapsindoors_mapbox_android/pubspec.yaml @@ -0,0 +1,31 @@ +name: mapsindoors_mapbox_android +description: Android implementation of the mapsindoors plugin using the Mapbox platform +version: 2.0.0 +repository: https://github.com/MapsPeople/MapsIndoors-sdk-flutter +homepage: https://www.mapsindoors.com/ + +environment: + sdk: ">=2.18.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: mapsindoors + platforms: + android: + package: com.mapspeople.mapsindoors.core + pluginClass: MapsindoorsPlugin + +dependencies: + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.15 + mapsindoors_platform_interface: ^2.0.0 +dependency_overrides: + mapsindoors_platform_interface: + path: ../mapsindoors_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 \ No newline at end of file diff --git a/mapsindoors_mapbox_ios/CHANGELOG.md b/mapsindoors_mapbox_ios/CHANGELOG.md new file mode 100644 index 0000000..5f45fb6 --- /dev/null +++ b/mapsindoors_mapbox_ios/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 2.0.0 + +* Moved from [mapsindoors_ios](https://pub.dev/packages/mapsindoors_ios) to allow for multiple map providers diff --git a/mapsindoors_mapbox_ios/LICENSE b/mapsindoors_mapbox_ios/LICENSE new file mode 100644 index 0000000..75757d5 --- /dev/null +++ b/mapsindoors_mapbox_ios/LICENSE @@ -0,0 +1,11 @@ +Copyright 2023 Mapspeople + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/mapsindoors_mapbox_ios/README.md b/mapsindoors_mapbox_ios/README.md new file mode 100644 index 0000000..488e122 --- /dev/null +++ b/mapsindoors_mapbox_ios/README.md @@ -0,0 +1,3 @@ +A federated Flutter plugin for integrating with the native MapsIndoors SDK. + +This plugin contains the iOS platform for the [mapsindoors](pub.dev/packages/mapsindoors) plugin. diff --git a/mapsindoors_mapbox_ios/ios/Assets/.gitkeep b/mapsindoors_mapbox_ios/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/mapsindoors_mapbox_ios/ios/Classes/MapsIndoorsPlugin.swift b/mapsindoors_mapbox_ios/ios/Classes/MapsIndoorsPlugin.swift new file mode 100644 index 0000000..b7b233f --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/MapsIndoorsPlugin.swift @@ -0,0 +1,101 @@ +// +// MapsIndoorsPlugin.swift +// mapsindoors_ios +// +// Created by Martin Hansen on 21/02/2023. + +import Flutter +import Foundation +import UIKit +import MapsIndoors +import MapboxMaps + +public class MapsIndoorsPlugin: NSObject, FlutterPlugin { + + static let mapsIndoorsData: MapsIndoorsData = MapsIndoorsData() + static var mapsIndoorsListenerChannel: FlutterMethodChannel? = nil + static var mapControlListenerMethodChannel: FlutterMethodChannel? = nil + static var directionsRendererListenerMethodChannel: FlutterMethodChannel? = nil + static var mapControlFloorSelectorChannel: FlutterMethodChannel? = nil + + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = MapsIndoorsPlugin() + + directionsRendererListenerMethodChannel = FlutterMethodChannel(name: "DirectionsRendererMethodChannel", binaryMessenger: registrar.messenger()) + let directionsServiceMethodChannel = FlutterMethodChannel(name: "DirectionsServiceMethodChannel", binaryMessenger: registrar.messenger()) + let displayRuleMethodChannel = FlutterMethodChannel(name: "DisplayRuleMethodChannel", binaryMessenger: registrar.messenger()) + let mapControlMethodChannel = FlutterMethodChannel(name: "MapControlMethodChannel", binaryMessenger: registrar.messenger()) + let mapsIndoorsMethodChannel = FlutterMethodChannel(name: "MapsIndoorsMethodChannel", binaryMessenger: registrar.messenger()) + mapControlListenerMethodChannel = FlutterMethodChannel(name: "MapControlListenerMethodChannel", binaryMessenger: registrar.messenger()) + mapControlFloorSelectorChannel = FlutterMethodChannel(name: "MapControlFloorSelectorChannel", binaryMessenger: registrar.messenger()) + mapsIndoorsListenerChannel = FlutterMethodChannel(name: "MapsIndoorsListenerChannel", binaryMessenger: registrar.messenger()) + let utilMethodChannel = FlutterMethodChannel(name: "UtilMethodChannel", binaryMessenger: registrar.messenger()) + + mapsIndoorsData.mapControlMethodChannel = mapControlMethodChannel + mapsIndoorsData.mapsIndoorsMethodChannel = mapsIndoorsMethodChannel + mapsIndoorsData.directionsRendererMethodChannel = directionsRendererListenerMethodChannel + + registrar.addMethodCallDelegate(instance, channel: directionsRendererListenerMethodChannel!) + registrar.addMethodCallDelegate(instance, channel: directionsServiceMethodChannel) + registrar.addMethodCallDelegate(instance, channel: displayRuleMethodChannel) + registrar.addMethodCallDelegate(instance, channel: mapControlMethodChannel) + registrar.addMethodCallDelegate(instance, channel: mapsIndoorsMethodChannel) + registrar.addMethodCallDelegate(instance, channel: mapControlListenerMethodChannel!) + registrar.addMethodCallDelegate(instance, channel: mapControlFloorSelectorChannel!) + registrar.addMethodCallDelegate(instance, channel: mapsIndoorsListenerChannel!) + + registrar.addMethodCallDelegate(instance, channel: utilMethodChannel) + + registrar.addApplicationDelegate(instance) + + let factory = FLNativeViewFactory(messenger: registrar.messenger(), mapsIndoorsData: mapsIndoorsData) + registrar.register(factory, withId: "") + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? [String: Any] + if let method = DirectionsRendererMethodChannel.Methods(rawValue: call.method) + { + method.call(arguments: arguments, mapsIndoorsData: MapsIndoorsPlugin.mapsIndoorsData, result: result) + } + else if let method = MapControlMethodChannel.Methods(rawValue: call.method) + { + method.call(arguments: arguments, mapsIndoorsData: MapsIndoorsPlugin.mapsIndoorsData, result: result) + } + else if let method = DirectionsServiceMethodChannel.Methods(rawValue: call.method) + { + method.call(arguments: arguments, mapsIndoorsData: MapsIndoorsPlugin.mapsIndoorsData, result: result) + } + else if let method = DisplayRuleMethodChannel.Methods(rawValue: call.method) + { + method.call(arguments: arguments, mapsIndoorsData: MapsIndoorsPlugin.mapsIndoorsData, result: result) + } + //Map control channels + else if let method = MapControlListenerMethodChannel.Methods(rawValue: call.method) + { + method.call(arguments: arguments, mapsIndoorsData: MapsIndoorsPlugin.mapsIndoorsData, result: result, methodChannel: MapsIndoorsPlugin.mapControlListenerMethodChannel) + } + else if let method = MapControlFloorSelectorChannel.Methods(rawValue: call.method) + { + method.call(arguments: arguments, mapsIndoorsData: MapsIndoorsPlugin.mapsIndoorsData, result: result, methodChannel: MapsIndoorsPlugin.mapControlFloorSelectorChannel) + } + //Mapsindoors channels + else if let method = MapsIndoorsMethodChannel.Methods(rawValue: call.method) + { + method.call(arguments: arguments, mapsIndoorsData: MapsIndoorsPlugin.mapsIndoorsData, result: result) + } + else if let method = MapsIndoorsListenerChannel.Methods(rawValue: call.method) + { + method.call(arguments: arguments, mapsIndoorsData: MapsIndoorsPlugin.mapsIndoorsData, result: result, methodChannel: MapsIndoorsPlugin.mapsIndoorsListenerChannel) + } + else if let method = UtilMethodChannel.Methods(rawValue: call.method) + { + method.call(arguments: arguments, mapsIndoorsData: MapsIndoorsPlugin.mapsIndoorsData, result: result) + } + else + { + result(FlutterMethodNotImplemented) + } + } +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/MapsIndoorsView.swift b/mapsindoors_mapbox_ios/ios/Classes/MapsIndoorsView.swift new file mode 100644 index 0000000..b15a651 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/MapsIndoorsView.swift @@ -0,0 +1,174 @@ +import Flutter +import MapboxMaps +import MapsIndoors +import MapsIndoorsCore +import MapsIndoorsMapbox + +class FLNativeViewFactory: NSObject, FlutterPlatformViewFactory { + private var messenger: FlutterBinaryMessenger + private var mapsIndoorsData: MapsIndoorsData + + init( + messenger: FlutterBinaryMessenger, + mapsIndoorsData: MapsIndoorsData + ) { + self.messenger = messenger + self.mapsIndoorsData = mapsIndoorsData + super.init() + } + + public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { + return FlutterStandardMessageCodec.sharedInstance() + } + + func create( + withFrame frame: CGRect, + viewIdentifier viewId: Int64, + arguments args: Any? + ) -> FlutterPlatformView { + return FLNativeView( + frame: frame, + arguments: args, + binaryMessenger: messenger, + mapsIndoorsData: mapsIndoorsData) + } +} + +class FLNativeView: NSObject, FlutterPlatformView, MPMapControlDelegate, FlutterMapView { + + private var _MapView: MapView? + private let MP_APIKEY = "mapspeople" + private var mapsIndoorsData: MapsIndoorsData? = nil + private var args: Any? = nil + + init( + frame: CGRect, + arguments args: Any?, + binaryMessenger messenger: FlutterBinaryMessenger?, + mapsIndoorsData: MapsIndoorsData + ) { + super.init() + + self.args = args + self.mapsIndoorsData = mapsIndoorsData; + let mapInitOptions = MapInitOptions(resourceOptions: ResourceOptions(accessToken: Bundle.main.object(forInfoDictionaryKey: "MBXAccessToken") as? String ?? "")) + _MapView = MapView(frame: frame, mapInitOptions: mapInitOptions) + + if (MPMapsIndoors.shared.ready) { + mapsIndoorsIsReady(); + }else { + mapsIndoorsData.delegate.append(MIReadyDelegate(view: self)) + } + + mapsIndoorsData.mapView = self + } + + func view() -> UIView { + return _MapView! + } + + func mapsIndoorsIsReady() { + if (mapsIndoorsData?.mapView != nil) { + DispatchQueue.main.async { [self] in + let config = MPMapConfig.init(mapBoxView: _MapView!, accessToken: Bundle.main.object(forInfoDictionaryKey: "MBXAccessToken") as? String ?? "") + let mapControl = MPMapsIndoors.createMapControl(mapConfig: config) + if (mapControl != nil) { + //TODO: parse config + mapControl?.showUserPosition = true + //pretend config^ + mapsIndoorsData!.mapControl = mapControl + mapsIndoorsData?.mapControlMethodChannel?.invokeMethod("create", arguments: nil) + mapsIndoorsData?.directionsRenderer = nil + }else { + mapsIndoorsData?.mapControlMethodChannel?.invokeMethod("create", arguments: MPError.unknownError) + } + } + } + } + + func animateCamera(cameraUpdate: CameraUpdate, duration: Int) throws { + guard let update = makeMBCameraUpdate(cameraUpdate: cameraUpdate) else { + throw MPError.unknownError + } + + DispatchQueue.main.async { + self._MapView?.mapboxMap.setCamera(to: update) + } + } + + func moveCamera(cameraUpdate: CameraUpdate) throws { + guard let update = makeMBCameraUpdate(cameraUpdate: cameraUpdate) else { + throw MPError.unknownError + } + + DispatchQueue.main.async { + } + } + + func makeMBCameraUpdate(cameraUpdate: CameraUpdate) -> CameraOptions? { + var update: CameraOptions + switch cameraUpdate.mode { + case "fromPoint": + guard let point = cameraUpdate.point else { + return nil + } + let camera = _MapView!.mapboxMap.cameraState + + update = CameraOptions(cameraState: camera) + update.center = point.coordinate + case "fromBounds": + guard let bounds = cameraUpdate.bounds else { + return nil + } + let camera = _MapView!.mapboxMap.cameraState + + let camerabounds = CameraBoundsOptions(bounds: CoordinateBounds(southwest: bounds.southWest, northeast: bounds.northEast)) + + update = CameraOptions(cameraState: camera) + update.center = camerabounds.bounds?.center + case "zoomBy": + let camera = _MapView!.mapboxMap.cameraState + + update = CameraOptions(cameraState: camera) + update.zoom = CGFloat(cameraUpdate.zoom!) + case "zoomTo": + let camera = _MapView!.mapboxMap.cameraState + + update = CameraOptions(cameraState: camera) + update.zoom = CGFloat(cameraUpdate.zoom!) + case "fromCameraPosition": + guard let position = cameraUpdate.position else { + return nil + } + let camera = _MapView!.mapboxMap.cameraState + + update = CameraOptions(cameraState: camera) + update.center = position.target.coordinate + update.bearing = CLLocationDirection(position.bearing) + if let zoom = cameraUpdate.zoom { + update.zoom = CGFloat(zoom) + } + default: + return nil + } + + + return update + } +} + +class MIReadyDelegate: MapsIndoorsReadyDelegate { + let view: FLNativeView + + init(view: FLNativeView) { + self.view = view + } + + func isReady(error: MPError?) { + if (error == MPError.invalidApiKey || error == MPError.networkError || error == MPError.unknownError ) { + //TODO: Do nothing i guees? + }else { + view.mapsIndoorsIsReady() + } + } +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Channels/DirectionsRendererMethodChannel.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/DirectionsRendererMethodChannel.swift new file mode 100644 index 0000000..90f0525 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/DirectionsRendererMethodChannel.swift @@ -0,0 +1,245 @@ +// +// DirectionsRendererListenerMethodChannel.swift +// mapsindoors_ios +// +// Created by Martin Hansen on 21/02/2023. +// + +import Foundation +import Flutter +import UIKit +import MapsIndoors +import MapsIndoorsCodable + +public class DirectionsRendererMethodChannel: NSObject { + + static var isListeningForLegChanges: Bool = false + + enum Methods: String { + case DRE_clear + case DRE_getSelectedLegFloorIndex + case DRE_nextLeg + case DRE_previousLeg + case DRE_selectLegIndex + case DRE_setAnimatedPolyline + case DRE_setCameraAnimationDuration + case DRE_setCameraViewFitMode + case DRE_setOnLegSelectedListener + case DRE_setPolyLineColors + case DRE_setRoute + case DRE_useContentOfNearbyLocations + + func call(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + let runner: (_ arguments: [String: Any]?, _ mapsIndoorsData: MapsIndoorsData, _ result: @escaping FlutterResult) -> Void + + if (mapsIndoorsData.mapControl != nil && mapsIndoorsData.directionsRenderer == nil) { + mapsIndoorsData.directionsRenderer = mapsIndoorsData.mapControl?.newDirectionsRenderer() + } + + switch self { + case .DRE_clear: runner = clear + case .DRE_getSelectedLegFloorIndex: runner = getSelectedLegFloorIndex + case .DRE_nextLeg: runner = nextLeg + case .DRE_previousLeg: runner = previousLeg + case .DRE_selectLegIndex: runner = selectLegIndex + case .DRE_setAnimatedPolyline: runner = setAnimatedPolyline + case .DRE_setCameraAnimationDuration: runner = setCameraAnimationDuration + case .DRE_setCameraViewFitMode: runner = setCameraViewFitMode + case .DRE_setOnLegSelectedListener: runner = setOnLegSelectedListener + case .DRE_setPolyLineColors: runner = setPolyLineColors + case .DRE_setRoute: runner = setRoute + case .DRE_useContentOfNearbyLocations: runner = useContentOfNearbyLocations + } + + runner(arguments, mapsIndoorsData, result) + } + + func clear(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + mapsIndoorsData.directionsRenderer?.clear() + + result(nil) + } + + func getSelectedLegFloorIndex(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + let legIndex = mapsIndoorsData.directionsRenderer?.routeLegIndex + if (legIndex != nil) { + result(mapsIndoorsData.directionsRenderer?.route?.legs[legIndex!].end_location.zLevel.int32Value) + } + + result(nil) + } + + func nextLeg(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let renderer = mapsIndoorsData.directionsRenderer else { + result(FlutterError(code: "Unable to change leg: The directionsRenderer is not set", message: "DRE_nextLeg", details: nil)) + + return + } + let success = renderer.nextLeg() + + renderer.animate(duration: 5) + + if (DirectionsRendererMethodChannel.isListeningForLegChanges) { + mapsIndoorsData.directionsRendererMethodChannel?.invokeMethod("onLegSelected", arguments: renderer.routeLegIndex) + } + + result(nil) + } + + func previousLeg(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let renderer = mapsIndoorsData.directionsRenderer else { + result(FlutterError(code: "Unable to change leg: The directionsRenderer is not set", message: "DRE_previousLeg", details: nil)) + return + } + let success = renderer.previousLeg() + + renderer.animate(duration: 5) + + if (DirectionsRendererMethodChannel.isListeningForLegChanges) { + mapsIndoorsData.directionsRendererMethodChannel?.invokeMethod("onLegSelected", arguments: renderer.routeLegIndex) + } + + result(nil) + } + + func selectLegIndex(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "Initialized called without arguments", message: "DRE_selectLegIndex", details: nil)) + return + } + + guard let legIndex = args["legIndex"] as? Int else { + result(FlutterError(code: "Could not initialise MapsIndoors", message: "DRE_selectLegIndex", details: nil)) + return + } + + guard let renderer = mapsIndoorsData.directionsRenderer else { + result(FlutterError(code: "Unable to select leg: The directionsRenderer is not set", message: "DRE_selectLegIndex", details: nil)) + return + } + + guard let route = mapsIndoorsData.directionsRenderer!.route else { + result(FlutterError(code: "No route set", message: "DRE_selectLegIndex", details: nil)) + return + } + + guard legIndex >= 0 else { + result(nil) + return + } + + guard legIndex < (route.legs.count) else { + result(nil) + return + } + + + renderer.routeLegIndex = legIndex + + renderer.animate(duration: 5) + + + if (DirectionsRendererMethodChannel.isListeningForLegChanges) { + mapsIndoorsData.directionsRendererMethodChannel?.invokeMethod("onLegSelected", arguments: renderer.routeLegIndex) + } + + result(nil) + } + + func setAnimatedPolyline(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "Initialized called without arguments", message: "DRE_setAnimatedPolyline", details: nil)) + return + } + + guard let durationMs = args["durationMs"] as? Int else { + result(FlutterError(code: "Could not initialise MapsIndoors", message: "DRE_setAnimatedPolyline", details: nil)) + return + } + + mapsIndoorsData.directionsRenderer?.animate(duration: TimeInterval(durationMs)) + result(nil) + } + + func setCameraAnimationDuration(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + result(nil) + } + + func setCameraViewFitMode(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "Initialized called without arguments", message: "DRE_setCameraViewFitMode", details: nil)) + return + } + + guard let cameraFit = args["cameraFitMode"] as? Int else { + result(FlutterError(code: "Could not initialise MapsIndoors", message: "DRE_setCameraViewFitMode", details: nil)) + return + } + let cameraFitMode = MPCameraViewFitMode(rawValue: cameraFit) + if (cameraFitMode != nil) { + mapsIndoorsData.directionsRenderer?.fitMode = cameraFitMode! + } + + result(nil) + } + + func setOnLegSelectedListener(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + DirectionsRendererMethodChannel.isListeningForLegChanges = true + + result(nil) + } + + func setPolyLineColors(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "Initialized called without arguments", message: "DRE_setPolyLineColors", details: nil)) + return + } + + guard let color = args["foreground"] as? String else { + result(FlutterError(code: "Could not initialise MapsIndoors", message: "DRE_setPolyLineColors", details: nil)) + return + } + + let regex = try! NSRegularExpression(pattern: "^#[0-9A-Fa-f]{6}$|^#[0-9A-Fa-f]{8}$") + let range = NSRange(location: 0, length: color.utf16.count) + + if (regex.matches(in: color, range: range).count == 1) { + mapsIndoorsData.directionsRenderer?.pathColor = UIColor(fromHexString: color)! + } else { + result(FlutterError(code: "Color is not a hex color", message: "DRE_setPolyLineColors", details: nil)) + } + + result(nil) + } + + func setRoute(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "Initialized called without arguments", message: "DRE_setRoute", details: nil)) + return + } + + guard let routeJson = args["route"] as? String else { + result(FlutterError(code: "Could not initialise MapsIndoors", message: "DRE_setRoute", details: nil)) + return + } + + do { + let route = try JSONDecoder().decode(MPRouteCodable.self, from: Data(routeJson.utf8)) + mapsIndoorsData.directionsRenderer?.route = route + mapsIndoorsData.directionsRenderer?.routeLegIndex = 0 + mapsIndoorsData.directionsRenderer?.animate(duration: 5) + } + catch { + result(FlutterError(code: "Could not initialise MapsIndoors", message: error.localizedDescription, details: nil)) + return + } + + result(nil) + } + + func useContentOfNearbyLocations(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + result(nil) + } + } + +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Channels/DirectionsServiceMethodChannel.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/DirectionsServiceMethodChannel.swift new file mode 100644 index 0000000..e3efa09 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/DirectionsServiceMethodChannel.swift @@ -0,0 +1,203 @@ +// +// MapsIndoorsMethodChannel.swift +// mapsindoors_ios +// +// Created by Martin Hansen on 21/02/2023. +// + +import Foundation +import Flutter +import UIKit +import MapsIndoors +import MapsIndoorsCodable +import MapsIndoorsCore + +public class DirectionsServiceMethodChannel: NSObject { + + static var wayTypes: [MPHighway] = [] + static var isDeparture: Bool = true + static var travelMode: MPTravelMode = MPTravelMode.walking + static var date: Date? = nil + + enum Methods: String { + case DSE_create + case DSE_addAvoidWayType + case DSE_clearWayType + case DSE_setIsDeparture + case DSE_getRoute + case DSE_setTravelMode + case DSE_setTime + + func call(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + let runner: (_ arguments: [String: Any]?, _ mapsIndoorsData: MapsIndoorsData, _ result: @escaping FlutterResult) -> Void + + switch self { + case .DSE_create: runner = create + case .DSE_addAvoidWayType: runner = addAvoidWayType + case .DSE_clearWayType: runner = clearWayType + case .DSE_setIsDeparture: runner = setIsDeparture + case .DSE_getRoute: runner = getRoute + case .DSE_setTravelMode: runner = setTravelMode + case .DSE_setTime: runner = setTime + } + + runner(arguments, mapsIndoorsData, result) + } + + func create(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + DirectionsServiceMethodChannel.resetStaticVariables() + result(nil) + } + + func addAvoidWayType(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "Initialized called without arguments", message: "DSE_addAvoidWayType", details: nil)) + return + } + + guard let wayType = args["wayType"] as? String else { + result(FlutterError(code: "Could not initialise MapsIndoors", message: "DSE_addAvoidWayType", details: nil)) + return + } + + let avoidWaytype = MPHighway(typeString: wayType) + if (avoidWaytype != MPHighway.unclassified) { + DirectionsServiceMethodChannel.wayTypes.append(avoidWaytype) + } + + result(nil) + } + + func clearWayType(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + DirectionsServiceMethodChannel.wayTypes.removeAll() + result(nil) + } + + func setIsDeparture(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "Initialized called without arguments", message: "DSE_setIsDeparture", details: nil)) + return + } + + guard let isDeparture = args["isDeparture"] as? Bool else { + result(FlutterError(code: "Could not initialise MapsIndoors", message: "DSE_setIsDeparture", details: nil)) + return + } + + DirectionsServiceMethodChannel.isDeparture = isDeparture + result(nil) + } + + func getRoute(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments as? [String: String] else { + result(FlutterError(code: "Could not read arguments", message: "DSE_getRoute", details: nil)) + return + } + + guard let originJson = args["origin"] else { + result(FlutterError(code: "Could not read origin point", message: "DSE_getRoute", details: nil)) + return + } + + guard let destinationJson = args["destination"] else { + result(FlutterError(code: "Could not read destination point", message: "DSE_getRoute", details: nil)) + return + } + var origin: MPPoint? = nil + var destination: MPPoint? = nil + + do { + origin = try JSONDecoder().decode(MPPoint.self, from: Data(originJson.utf8)) + destination = try JSONDecoder().decode(MPPoint.self, from: Data(destinationJson.utf8)) + } catch { + result(FlutterError(code: "Unable to parse origin or destination point", message: "DSE_getRoute", details: nil)) + } + + if (origin != nil && destination != nil) { + let query = MPDirectionsQuery(originPoint: origin!, destinationPoint: destination!) + if (!DirectionsServiceMethodChannel.wayTypes.isEmpty) { + query.avoidWayTypes = DirectionsServiceMethodChannel.wayTypes + } + + if (DirectionsServiceMethodChannel.date != nil) { + if (DirectionsServiceMethodChannel.isDeparture) { + query.departure = DirectionsServiceMethodChannel.date + }else { + query.arrival = DirectionsServiceMethodChannel.date + } + } + + query.travelMode = DirectionsServiceMethodChannel.travelMode + + Task { + let route = try await MPMapsIndoors.shared.directionsService.routingWith(query: query) + if (route != nil) { + let routeData = try? JSONEncoder().encode(MPRouteCodable(withRoute: route!)) + if (routeData != nil) { + let routeRoute = String(data: routeData!, encoding: String.Encoding.utf8) + var map = Dictionary() + map["route"] = routeRoute + map["error"] = "null" + result(map) + }else { + result(FlutterError(code: "Could not parse route", message: "DSE_getRoute", details: nil)) + } + }else { + result(nil) + } + } + } + else { + result(FlutterError(code: "Origin or destination is nil", message: "DSE_getRoute", details: nil)) + } + } + + func setTravelMode(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "Initialized called without arguments", message: "DSE_setTravelMode", details: nil)) + return + } + + guard let travelMode = args["travelMode"] as? String else { + result(FlutterError(code: "Could not initialise MapsIndoors", message: "DSE_setTravelMode", details: nil)) + return + } + + var mpTravelMode: MPTravelMode? = nil + + switch(travelMode) { + case "walking": mpTravelMode = MPTravelMode.walking; + case "bicycling": mpTravelMode = MPTravelMode.bicycling; + case "driving": mpTravelMode = MPTravelMode.driving; + case "transit": mpTravelMode = MPTravelMode.transit; + default: + result(FlutterError(code: "TravelMode not found", message: "DSE_setTravelMode", details: nil)) + } + + DirectionsServiceMethodChannel.travelMode = mpTravelMode! + result(nil) + } + + func setTime(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "Initialized called without arguments", message: "DSE_setTime", details: nil)) + return + } + + guard let time = args["time"] as? Int else { + result(FlutterError(code: "Could not initialise MapsIndoors", message: "DSE_setTime", details: nil)) + return + } + + DirectionsServiceMethodChannel.date = Date(timeIntervalSince1970: TimeInterval(time/1000)) + result(nil) + } + } + + static func resetStaticVariables() { + wayTypes = [] + isDeparture = true + travelMode = MPTravelMode.walking + date = nil + } +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Channels/DisplayRuleMethodChannel.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/DisplayRuleMethodChannel.swift new file mode 100644 index 0000000..8dfdd93 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/DisplayRuleMethodChannel.swift @@ -0,0 +1,1443 @@ +// +// DisplayRuleMethodChannel.swift +// mapsindoors_ios +// +// Created by Martin Hansen on 21/02/2023. +// + +import Foundation +import Flutter +import UIKit +import MapsIndoors +import MapsIndoorsCore +import MapsIndoorsCodable + +public class DisplayRuleMethodChannel: NSObject { + + enum Methods: String { + case DRU_isVisible + case DRU_setVisible + case DRU_getIconSize + case DRU_getIconUrl + case DRU_getLabel + case DRU_getLabelMaxWidth + case DRU_getLabelZoomFrom + case DRU_getLabelZoomTo + case DRU_getModel2DBearing + case DRU_getModel2DHeightMeters + case DRU_getModel2DModel + case DRU_getModel2DZoomTo + case DRU_getModel2DWidthMeters + case DRU_getModel2DZoomFrom + case DRU_getPolygonFillColor + case DRU_getPolygonFillOpacity + case DRU_getPolygonZoomTo + case DRU_getPolygonStrokeColor + case DRU_getPolygonStrokeOpacity + case DRU_getPolygonStrokeWidth + case DRU_getPolygonZoomFrom + case DRU_getZoomFrom + case DRU_getZoomTo + case DRU_isIconVisible + case DRU_isLabelVisible + case DRU_isModel2DVisible + case DRU_isPolygonVisible + case DRU_isValid + case DRU_reset + case DRU_setIcon + case DRU_setIconVisible + case DRU_setIconSize + case DRU_setLabel + case DRU_setLabelMaxWidth + case DRU_setLabelVisible + case DRU_setLabelZoomFrom + case DRU_setLabelZoomTo + case DRU_setModel2DBearing + case DRU_setModel2DModel + case DRU_setModel2DHeightMeters + case DRU_setModel2DVisible + case DRU_setModel2DWidthMeters + case DRU_setModel2DZoomFrom + case DRU_setModel2DZoomTo + case DRU_setPolygonFillColor + case DRU_setPolygonFillOpacity + case DRU_setPolygonStrokeColor + case DRU_setPolygonStrokeOpacity + case DRU_setPolygonStrokeWidth + case DRU_setPolygonVisible + case DRU_setPolygonZoomFrom + case DRU_setPolygonZoomTo + case DRU_getExtrusionColor + case DRU_getExtrusionHeight + case DRU_getExtrusionZoomFrom + case DRU_getExtrusionZoomTo + case DRU_getWallColor + case DRU_getWallHeight + case DRU_getWallZoomFrom + case DRU_getWallZoomTo + case DRU_isExtrusionVisible + case DRU_isWallVisible + case DRU_setExtrusionColor + case DRU_setExtrusionHeight + case DRU_setExtrusionVisible + case DRU_setExtrusionZoomFrom + case DRU_setExtrusionZoomTo + case DRU_setWallColor + case DRU_setWallHeight + case DRU_setWallVisible + case DRU_setWallZoomFrom + case DRU_setWallZoomTo + case DRU_setZoomFrom + case DRU_setZoomTo + + func call(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + let runner: (_ arguments: [String: Any]?, _ mapsIndoorsData: MapsIndoorsData, _ result: @escaping FlutterResult) -> Void + + switch self { + case .DRU_isVisible: runner = isVisible + case .DRU_setVisible: runner = setVisible + case .DRU_getIconSize: runner = getIconSize + case .DRU_getIconUrl: runner = getIconUrl + case .DRU_getLabel: runner = getLabel + case .DRU_getLabelMaxWidth: runner = getLabelMaxWidth + case .DRU_getLabelZoomFrom: runner = getLabelZoomFrom + case .DRU_getLabelZoomTo: runner = getLabelZoomTo + case .DRU_getModel2DBearing: runner = getModel2DBearing + case .DRU_getModel2DHeightMeters: runner = getModel2DHeightMeters + case .DRU_getModel2DModel: runner = getModel2DModel + case .DRU_getModel2DZoomTo: runner = getModel2DZoomTo + case .DRU_getModel2DWidthMeters: runner = getModel2DWidthMeters + case .DRU_getModel2DZoomFrom: runner = getModel2DZoomFrom + case .DRU_getPolygonFillColor: runner = getPolygonFillColor + case .DRU_getPolygonFillOpacity: runner = getPolygonFillOpacity + case .DRU_getPolygonZoomTo: runner = getPolygonZoomTo + case .DRU_getPolygonStrokeColor: runner = getPolygonStrokeColor + case .DRU_getPolygonStrokeOpacity: runner = getPolygonStrokeOpacity + case .DRU_getPolygonStrokeWidth: runner = getPolygonStrokeWidth + case .DRU_getPolygonZoomFrom: runner = getPolygonZoomFrom + case .DRU_getZoomFrom: runner = getZoomFrom + case .DRU_getZoomTo: runner = getZoomTo + case .DRU_isIconVisible: runner = isIconVisible + case .DRU_isLabelVisible: runner = isLabelVisible + case .DRU_isModel2DVisible: runner = isModel2DVisible + case .DRU_isPolygonVisible: runner = isPolygonVisible + case .DRU_isValid: runner = isValid + case .DRU_reset: runner = reset + case .DRU_setIcon: runner = setIcon + case .DRU_setIconVisible: runner = setIconVisible + case .DRU_setIconSize: runner = setIconSize + case .DRU_setLabel: runner = setLabel + case .DRU_setLabelMaxWidth: runner = setLabelMaxWidth + case .DRU_setLabelVisible: runner = setLabelVisible + case .DRU_setLabelZoomFrom: runner = setLabelZoomFrom + case .DRU_setLabelZoomTo: runner = setLabelZoomTo + case .DRU_setModel2DBearing: runner = setModel2DBearing + case .DRU_setModel2DModel: runner = setModel2DModel + case .DRU_setModel2DHeightMeters: runner = setModel2DHeightMeters + case .DRU_setModel2DVisible: runner = setModel2DVisible + case .DRU_setModel2DWidthMeters: runner = setModel2DWidthMeters + case .DRU_setModel2DZoomFrom: runner = setModel2DZoomFrom + case .DRU_setModel2DZoomTo: runner = setModel2DZoomTo + case .DRU_setPolygonFillColor: runner = setPolygonFillColor + case .DRU_setPolygonFillOpacity: runner = setPolygonFillOpacity + case .DRU_setPolygonStrokeColor: runner = setPolygonStrokeColor + case .DRU_setPolygonStrokeOpacity: runner = setPolygonStrokeOpacity + case .DRU_setPolygonStrokeWidth: runner = setPolygonStrokeWidth + case .DRU_setPolygonVisible: runner = setPolygonVisible + case .DRU_setPolygonZoomFrom: runner = setPolygonZoomFrom + case .DRU_setPolygonZoomTo: runner = setPolygonZoomTo + case .DRU_getExtrusionColor: runner = getExtrusionColor + case .DRU_getExtrusionHeight: runner = getExtrusionHeight + case .DRU_getExtrusionZoomFrom: runner = getExtrusionZoomFrom + case .DRU_getExtrusionZoomTo: runner = getExtrusionZoomTo + case .DRU_getWallColor: runner = getWallColor + case .DRU_getWallHeight: runner = getWallHeight + case .DRU_getWallZoomFrom: runner = getWallZoomFrom + case .DRU_getWallZoomTo: runner = getWallZoomTo + case .DRU_isExtrusionVisible: runner = isExtrusionVisible + case .DRU_isWallVisible: runner = isWallVisible + case .DRU_setExtrusionColor: runner = setExtrusionColor + case .DRU_setExtrusionHeight: runner = setExtrusionHeight + case .DRU_setExtrusionVisible: runner = setExtrusionVisible + case .DRU_setExtrusionZoomFrom: runner = setExtrusionZoomFrom + case .DRU_setExtrusionZoomTo: runner = setExtrusionZoomTo + case .DRU_setWallColor: runner = setWallColor + case .DRU_setWallHeight: runner = setWallHeight + case .DRU_setWallVisible: runner = setWallVisible + case .DRU_setWallZoomFrom: runner = setWallZoomFrom + case .DRU_setWallZoomTo: runner = setWallZoomTo + case .DRU_setZoomFrom: runner = setZoomFrom + case .DRU_setZoomTo: runner = setZoomTo + } + + runner(arguments, mapsIndoorsData, result) + } + + func getDisplayRule(id: String) -> MPDisplayRule? { + let location = MPMapsIndoors.shared.locationWith(locationId: id) + if (location != nil) { + return MPMapsIndoors.shared.displayRuleFor(location: location!) + }else { + if (isSolutionDisplayRule(name: id) != nil) { + return MPMapsIndoors.shared.displayRuleFor(displayRuleType: isSolutionDisplayRule(name: id)!) + }else { + return MPMapsIndoors.shared.displayRuleFor(type: id) + } + } + } + + func hexStringFromColor(color: UIColor) -> String { + let components = color.cgColor.components + let r: CGFloat = components?[0] ?? 0.0 + let g: CGFloat = components?[1] ?? 0.0 + let b: CGFloat = components?[2] ?? 0.0 + + let hexString = String.init(format: "#%02lX%02lX%02lX", lroundf(Float(r * 255)), lroundf(Float(g * 255)), lroundf(Float(b * 255))) + print(hexString) + return hexString + } + + func isSolutionDisplayRule(name: String) -> MPDisplayRuleType? { + switch name { + case "buildingOutline": + return MPDisplayRuleType.buildingOutline + case "selectionHighlight": + return MPDisplayRuleType.selectionHighlight + case "positionIndicator": + return MPDisplayRuleType.blueDot + case "main": + return MPDisplayRuleType.main + case "default": + return MPDisplayRuleType.default + default: + return nil + } + } + + func isVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_isVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.visible) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_isVisible", details: nil)) + } + } + + func setVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let visibility = arguments?["visible"] as? Bool else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setVisible", details: nil)) + return + } + displayRule?.visible = visibility + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setVisible", details: nil)) + } + } + + func getIconSize(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getIconSize", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + let data = try? JSONEncoder().encode(MPIconSizeCodable(withCGSize: displayRule!.iconSize)) + if (data != nil) { + let iconSizeString = String(data: data!, encoding: String.Encoding.utf8) + result(iconSizeString) + } + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getIconSize", details: nil)) + } + } + + func getIconUrl(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getIconUrl", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.iconURL?.absoluteString) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getIconUrl", details: nil)) + } + } + + func getLabel(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getLabel", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.label) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getLabel", details: nil)) + } + } + + func getLabelMaxWidth(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getLabelMaxWidth", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.labelMaxWidth) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getLabelMaxWidth", details: nil)) + } + } + + func getLabelZoomFrom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getLabelZoomFrom", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.labelZoomFrom) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getLabelZoomFrom", details: nil)) + } + } + + func getLabelZoomTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getLabelZoomTo", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.labelZoomTo) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getLabelZoomTo", details: nil)) + } + } + + func getModel2DBearing(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getModel2DBearing", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.model2DBearing) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getModel2DBearing", details: nil)) + } + } + + func getModel2DHeightMeters(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getModel2DHeightMeters", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.model2DHeightMeters) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getModel2DHeightMeters", details: nil)) + } + } + + func getModel2DModel(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getModel2DModel", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.model2DModel) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getModel2DModel", details: nil)) + } + } + + func getModel2DZoomTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getModel2DZoomTo", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.model2DZoomTo) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getModel2DZoomTo", details: nil)) + } + } + + func getModel2DWidthMeters(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getModel2DWidthMeters", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.model2DWidthMeters) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getModel2DWidthMeters", details: nil)) + } + } + + func getModel2DZoomFrom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getModel2DZoomFrom", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.model2DZoomFrom) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getModel2DZoomFrom", details: nil)) + } + } + + func getPolygonFillColor(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getPolygonFillColor", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(hexStringFromColor(color: displayRule!.polygonFillColor!)) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getPolygonFillColor", details: nil)) + } + } + + func getPolygonFillOpacity(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getPolygonFillOpacity", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.polygonFillOpacity) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getPolygonFillOpacity", details: nil)) + } + } + + func getPolygonZoomTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getPolygonZoomTo", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.polygonZoomTo) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getPolygonZoomTo", details: nil)) + } + } + + func getPolygonStrokeColor(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getPolygonStrokeColor", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(hexStringFromColor(color: displayRule!.polygonStrokeColor!)) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getPolygonStrokeColor", details: nil)) + } + } + + func getPolygonStrokeOpacity(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getPolygonStrokeOpacity", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.polygonStrokeOpacity) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getPolygonStrokeOpacity", details: nil)) + } + } + + func getPolygonStrokeWidth(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getPolygonStrokeWidth", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.polygonStrokeWidth) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getPolygonStrokeWidth", details: nil)) + } + } + + func getPolygonZoomFrom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getPolygonZoomFrom", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.polygonZoomFrom) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getPolygonZoomFrom", details: nil)) + } + } + + func getZoomFrom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getZoomFrom", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.zoomFrom) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getZoomFrom", details: nil)) + } + } + + func getZoomTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getZoomTo", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.zoomTo) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getZoomTo", details: nil)) + } + } + + func isIconVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_isIconVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.iconVisible) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_isIconVisible", details: nil)) + } + } + + func isLabelVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_isLabelVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.labelVisible) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_isLabelVisible", details: nil)) + } + } + + func isModel2DVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_isModel2DVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.model2DVisible) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_isModel2DVisible", details: nil)) + } + } + + func isPolygonVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_isPolygonVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.polygonVisible) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_isPolygonVisible", details: nil)) + } + } + + func isValid(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_isValid", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(true) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_isValid", details: nil)) + } + } + + func reset(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_reset", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + displayRule!.reset() + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_reset", details: nil)) + } + } + + func setIcon(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setIcon", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let url = arguments?["url"] as? String else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setIcon", details: nil)) + return + } + displayRule?.iconURL = URL(string: url) + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setIcon", details: nil)) + } + } + + func setIconVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setIconVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let visibility = arguments?["visible"] as? Bool else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setIconVisible", details: nil)) + return + } + displayRule?.iconVisible = visibility + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setIconVisible", details: nil)) + } + } + + func setIconSize(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setIconSize", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let iconSizeString = arguments?["size"] as? String else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setIconSize", details: nil)) + return + } + let iconSize = try? JSONDecoder().decode(MPIconSizeCodable.self, from: Data(iconSizeString.utf8)) + if (iconSize != nil) { + displayRule?.iconSize = CGSize(width: iconSize!.width!, height: iconSize!.height!) + result(nil) + }else { + result(FlutterError(code: "Could not parse iconSize", message: "DRU_setIconSize", details: nil)) + } + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setIconSize", details: nil)) + } + } + + func setLabel(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setLabel", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let label = arguments?["label"] as? String else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setLabel", details: nil)) + return + } + displayRule?.label = label + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setLabel", details: nil)) + } + } + + func setLabelMaxWidth(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setLabelMaxWidth", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let maxWidth = arguments?["maxWidth"] as? UInt else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setLabelMaxWidth", details: nil)) + return + } + displayRule?.labelMaxWidth = maxWidth + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setLabelMaxWidth", details: nil)) + } + } + + func setLabelVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setLabelVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let visibility = arguments?["visible"] as? Bool else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setLabelVisible", details: nil)) + return + } + displayRule?.labelVisible = visibility + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setLabelVisible", details: nil)) + } + } + + func setLabelZoomFrom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setLabelZoomFrom", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let zoomFrom = arguments?["zoomFrom"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setLabelZoomFrom", details: nil)) + return + } + displayRule?.labelZoomFrom = zoomFrom + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setLabelZoomFrom", details: nil)) + } + } + + func setLabelZoomTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setLabelZoomTo", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let zoomTo = arguments?["zoomTo"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setLabelZoomTo", details: nil)) + return + } + displayRule?.labelZoomTo = zoomTo + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setLabelZoomTo", details: nil)) + } + } + + func setModel2DBearing(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setModel2DBearing", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let bearing = arguments?["bearing"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setModel2DBearing", details: nil)) + return + } + displayRule?.model2DBearing = bearing + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setModel2DBearing", details: nil)) + } + } + + func setModel2DModel(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setModel2DModel", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let model = arguments?["model"] as? String else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setModel2DModel", details: nil)) + return + } + displayRule?.model2DModel = model + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setModel2DModel", details: nil)) + } + } + + func setModel2DHeightMeters(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setModel2DHeightMeters", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let height = arguments?["height"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setModel2DHeightMeters", details: nil)) + return + } + displayRule?.model2DHeightMeters = height + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setModel2DHeightMeters", details: nil)) + } + } + + func setModel2DVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "setModel2DVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let visibility = arguments?["visible"] as? Bool else { + result(FlutterError(code: "Could not find value for setter", message: "setModel2DVisible", details: nil)) + return + } + displayRule?.model2DVisible = visibility + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setModel2DVisible", details: nil)) + } + } + + func setModel2DWidthMeters(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setModel2DWidthMeters", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let width = arguments?["width"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setModel2DWidthMeters", details: nil)) + return + } + displayRule?.model2DWidthMeters = width + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setModel2DWidthMeters", details: nil)) + } + } + + func setModel2DZoomFrom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: nil, details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let zoomFrom = arguments?["zoomFrom"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "setModel2DZoomFrom", details: nil)) + return + } + displayRule?.model2DZoomFrom = zoomFrom + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "setModel2DZoomFrom", details: nil)) + } + } + + func setModel2DZoomTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setModel2DZoomTo", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let zoomTo = arguments?["zoomTo"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setModel2DZoomTo", details: nil)) + return + } + displayRule?.model2DZoomTo = zoomTo + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setModel2DZoomTo", details: nil)) + } + } + + func setPolygonFillColor(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setPolygonFillColor", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let color = arguments?["color"] as? String else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setPolygonFillColor", details: nil)) + return + } + displayRule?.polygonFillColor = UIColor(fromHexString: color) + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setPolygonFillColor", details: nil)) + } + } + + func setPolygonFillOpacity(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setPolygonFillOpacity", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let opacity = arguments?["opacity"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setPolygonFillOpacity", details: nil)) + return + } + displayRule?.polygonFillOpacity = opacity + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setPolygonFillOpacity", details: nil)) + } + } + + func setPolygonStrokeColor(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setPolygonStrokeColor", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let color = arguments?["color"] as? String else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setPolygonStrokeColor", details: nil)) + return + } + displayRule?.polygonStrokeColor = UIColor(fromHexString: color) + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setPolygonStrokeColor", details: nil)) + } + } + + func setPolygonStrokeOpacity(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setPolygonStrokeOpacity", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let opacity = arguments?["opacity"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setPolygonStrokeOpacity", details: nil)) + return + } + displayRule?.polygonStrokeOpacity = opacity + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setPolygonStrokeOpacity", details: nil)) + } + } + + func setPolygonStrokeWidth(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setPolygonStrokeWidth", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let width = arguments?["width"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setPolygonStrokeWidth", details: nil)) + return + } + displayRule?.polygonStrokeWidth = width + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setPolygonStrokeWidth", details: nil)) + } + } + + func setPolygonVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setPolygonVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let visibility = arguments?["visible"] as? Bool else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setPolygonVisible", details: nil)) + return + } + displayRule?.polygonVisible = visibility + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setPolygonVisible", details: nil)) + } + } + + func setPolygonZoomFrom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setPolygonZoomFrom", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let zoomFrom = arguments?["zoomFrom"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setPolygonZoomFrom", details: nil)) + return + } + displayRule?.polygonZoomFrom = zoomFrom + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setPolygonZoomFrom", details: nil)) + } + } + + func setPolygonZoomTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setPolygonZoomTo", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let zoomTo = arguments?["zoomTo"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setPolygonZoomTo", details: nil)) + return + } + displayRule?.polygonZoomTo = zoomTo + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setPolygonZoomTo", details: nil)) + } + } + + func getExtrusionColor(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getExtrusionColor", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(hexStringFromColor(color: displayRule!.extrusionColor!)) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getExtrusionColor", details: nil)) + } + } + + func getExtrusionHeight(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getExtrusionHeight", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.extrusionHeight) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getExtrusionHeight", details: nil)) + } + } + + func getExtrusionZoomFrom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getExtrusionZoomFrom", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.extrusionZoomFrom) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getExtrusionZoomFrom", details: nil)) + } + } + + func getExtrusionZoomTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getExtrusionZoomTo", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.extrusionZoomTo) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getExtrusionZoomTo", details: nil)) + } + } + + func getWallColor(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getWallColor", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(hexStringFromColor(color: displayRule!.wallsColor!)) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getWallColor", details: nil)) + } + } + + func getWallHeight(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getWallHeight", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.wallsHeight) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getWallHeight", details: nil)) + } + } + + func getWallZoomFrom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getWallZoomFrom", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.wallsZoomFrom) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getWallZoomFrom", details: nil)) + } + } + + func getWallZoomTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_getWallZoomTo", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.wallsZoomTo) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_getWallZoomTo", details: nil)) + } + } + + func isExtrusionVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_isExtrusionVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.extrusionVisible) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_isExtrusionVisible", details: nil)) + } + } + + func isWallVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_isWallVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + result(displayRule!.wallsVisible) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_isWallVisible", details: nil)) + } + } + + func setExtrusionColor(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setExtrusionColor", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let color = arguments?["color"] as? String else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setExtrusionColor", details: nil)) + return + } + displayRule?.extrusionColor = UIColor(fromHexString: color) + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setExtrusionColor", details: nil)) + } + } + + func setExtrusionHeight(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setExtrusionHeight", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let height = arguments?["height"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setExtrusionHeight", details: nil)) + return + } + displayRule?.extrusionHeight = height + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setExtrusionHeight", details: nil)) + } + } + + func setExtrusionVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setExtrusionVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let visibility = arguments?["visible"] as? Bool else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setExtrusionVisible", details: nil)) + return + } + displayRule?.extrusionVisible = visibility + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setExtrusionVisible", details: nil)) + } + } + + func setExtrusionZoomFrom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setExtrusionZoomFrom", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let zoomFrom = arguments?["zoomFrom"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setExtrusionZoomFrom", details: nil)) + return + } + displayRule?.extrusionZoomFrom = zoomFrom + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setExtrusionZoomFrom", details: nil)) + } + } + + func setExtrusionZoomTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setExtrusionZoomTo", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let zoomTo = arguments?["zoomTo"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setExtrusionZoomTo", details: nil)) + return + } + displayRule?.extrusionZoomTo = zoomTo + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setExtrusionZoomTo", details: nil)) + } + } + + func setWallColor(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setWallColor", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let color = arguments?["color"] as? String else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setWallColor", details: nil)) + return + } + displayRule?.wallsColor = UIColor(fromHexString: color) + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setWallColor", details: nil)) + } + } + + func setWallHeight(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setWallHeight", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let height = arguments?["height"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setWallHeight", details: nil)) + return + } + displayRule?.wallsHeight = height + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setWallHeight", details: nil)) + } + } + + func setWallVisible(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setWallVisible", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let visibility = arguments?["visible"] as? Bool else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setWallVisible", details: nil)) + return + } + displayRule?.wallsVisible = visibility + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setWallVisible", details: nil)) + } + } + + func setWallZoomFrom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setWallZoomFrom", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let zoomFrom = arguments?["zoomFrom"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setWallZoomFrom", details: nil)) + return + } + displayRule?.wallsZoomFrom = zoomFrom + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setWallZoomFrom", details: nil)) + } + } + + func setWallZoomTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setWallZoomTo", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let zoomTo = arguments?["zoomTo"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setWallZoomTo", details: nil)) + return + } + displayRule?.wallsZoomTo = zoomTo + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setWallZoomTo", details: nil)) + } + } + + func setZoomFrom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setZoomFrom", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let zoomFrom = arguments?["zoomFrom"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setZoomFrom", details: nil)) + return + } + displayRule?.zoomFrom = zoomFrom + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setZoomFrom", details: nil)) + } + } + + func setZoomTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let displayRuleId = arguments?["id"] as? String else { + result(FlutterError(code: "Could not find id for DisplayRule", message: "DRU_setZoomTo", details: nil)) + return + } + + let displayRule = getDisplayRule(id: displayRuleId) + if (displayRule != nil) { + guard let zoomTo = arguments?["zoomTo"] as? Double else { + result(FlutterError(code: "Could not find value for setter", message: "DRU_setZoomTo", details: nil)) + return + } + displayRule?.zoomTo = zoomTo + result(nil) + }else { + result(FlutterError(code: "No DisplayRule existing for this Id", message: "DRU_setZoomTo", details: nil)) + } + } + } +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapControl/MapControlFloorSelectorChannel.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapControl/MapControlFloorSelectorChannel.swift new file mode 100644 index 0000000..d0580aa --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapControl/MapControlFloorSelectorChannel.swift @@ -0,0 +1,48 @@ +// +// MapControlFloorSelectorChannel.swift +// mapsindoors_ios +// +// Created by Martin Hansen on 21/02/2023. +// + +import Foundation +import Flutter +import UIKit +import MapsIndoors +import MapsIndoorsCore +import MapsIndoorsCodable + +public class MapControlFloorSelectorChannel: NSObject { + enum Methods: String { + case FSE_onFloorChanged + + func call(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel?) { + if (methodChannel == nil) { + return + } + + let runner: (_ arguments: [String: Any]?, _ mapsIndoorsData: MapsIndoorsData, _ result: @escaping FlutterResult, _ methodChannel: FlutterMethodChannel) -> Void + + switch self { + case .FSE_onFloorChanged: runner = onFloorChanged + } + + runner(arguments, mapsIndoorsData, result, methodChannel!) + } + + func onFloorChanged(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let floorJson = arguments?["floor"] as? String else { + result(FlutterError(code: "Could not read floor", message: "FSE_onFloorChanged", details: nil)) + return + } + + guard let floor = try? JSONDecoder().decode(MPFloorCodable.self, from: floorJson.data(using: .utf8)!) else { + result(FlutterError(code: "Could not parse floor", message: "FSE_onFloorChanged", details: nil)) + return + } + + let floorIndex = floor.floorIndex?.intValue ?? 0 + mapsIndoorsData.mapControl?.select(floorIndex: floorIndex) + } + } +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapControl/MapControlListenerMethodChannel.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapControl/MapControlListenerMethodChannel.swift new file mode 100644 index 0000000..eb77c22 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapControl/MapControlListenerMethodChannel.swift @@ -0,0 +1,181 @@ +// +// MapControlListenerMethodChannel.swift +// mapsindoors_ios +// +// Created by Martin Hansen on 21/02/2023. +// + +import Foundation +import Flutter +import UIKit +import MapsIndoors +import MapsIndoorsCore +import MapsIndoorsCodable + +public class MapControlListenerMethodChannel: NSObject { + + + enum Methods: String { + case MPL_cameraEventListener + case MPL_floorUpdateListener + case MPL_buildingFoundAtCameraTargetListener + case MPL_venueFoundAtCameraTargetListener + case MPL_locationClusterClickListener + case MPL_locationSelectedListener + case MPL_mapClickListener + case MPL_markerClickListener + case MPL_markerInfoWindowClickListener + case MPL_markerInfoWindowCloseListener + case MPL_willUpdateLocationsOnMap + + func call(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel?) { + if (methodChannel == nil && mapsIndoorsData.mapControl == nil) { + return + } + + if (mapsIndoorsData.mapControlListenerDelegate == nil) { + mapsIndoorsData.mapControlListenerDelegate = MapControlDelegate(methodChannel: methodChannel!) + } + mapsIndoorsData.mapControl?.delegate = mapsIndoorsData.mapControlListenerDelegate + + let runner: (_ arguments: [String: Any]?, _ mapsIndoorsData: MapsIndoorsData, _ result: @escaping FlutterResult, _ methodChannel: FlutterMethodChannel) -> Void + + switch self { + case .MPL_cameraEventListener: runner = cameraEventListener + case .MPL_floorUpdateListener: runner = floorUpdateListener + case .MPL_buildingFoundAtCameraTargetListener: runner = buildingFoundAtCameraTargetListener + case .MPL_venueFoundAtCameraTargetListener: runner = venueFoundAtCameraTargetListener + case .MPL_locationClusterClickListener: runner = locationClusterClickListener + case .MPL_locationSelectedListener: runner = locationSelectedListener + case .MPL_mapClickListener: runner = mapClickListener + case .MPL_markerClickListener: runner = markerClickListener + case .MPL_markerInfoWindowClickListener: runner = markerInfoWindowClickListener + case .MPL_markerInfoWindowCloseListener: runner = markerInfoWindowCloseListener + case .MPL_willUpdateLocationsOnMap: runner = willUpdateLocationsOnMap + } + + runner(arguments, mapsIndoorsData, result, methodChannel!) + } + + func cameraEventListener(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let setupListener = arguments?["setupListener"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_cameraEventListener", details: nil)) + return + } + + //TODO: Not implemented + //result(FlutterError(code: "Not implemented on v4", message: nil, details: nil)) + } + + func floorUpdateListener(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let setupListener = arguments?["setupListener"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_floorUpdateListener", details: nil)) + return + } + + mapsIndoorsData.mapControlListenerDelegate?.respondToDidChangeFloorIndex = setupListener + } + + func buildingFoundAtCameraTargetListener(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let setupListener = arguments?["setupListener"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_buildingFoundAtCameraTargetListener", details: nil)) + return + } + + mapsIndoorsData.mapControlListenerDelegate?.respondToDidChangeBuilding = setupListener + } + + func venueFoundAtCameraTargetListener(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let setupListener = arguments?["setupListener"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_venueFoundAtCameraTargetListener", details: nil)) + return + } + + mapsIndoorsData.mapControlListenerDelegate?.respondToDidChangeVenue = setupListener + } + + func locationClusterClickListener(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let setupListener = arguments?["setupListener"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_locationClusterClickListener", details: nil)) + return + } + + //TODO: Not implemented + //result(FlutterError(code: "Not implemented on v4", message: nil, details: nil)) + } + + func locationSelectedListener(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let setupListener = arguments?["setupListener"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_locationSelectedListener", details: nil)) + return + } + + guard let consumeEvent = arguments?["consumeEvent"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_locationSelectedListener", details: nil)) + return + } + + mapsIndoorsData.mapControlListenerDelegate?.respondToDidChangeLocation = setupListener + mapsIndoorsData.mapControlListenerDelegate?.consumeChangeLocation = consumeEvent + } + + func mapClickListener(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let setupListener = arguments?["setupListener"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_mapClickListener", details: nil)) + return + } + + guard let consumeEvent = arguments?["consumeEvent"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_mapClickListener", details: nil)) + return + } + + mapsIndoorsData.mapControlListenerDelegate?.respondToTap = setupListener + mapsIndoorsData.mapControlListenerDelegate?.consumeTap = consumeEvent + } + + func markerClickListener(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let setupListener = arguments?["setupListener"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_markerClickListener", details: nil)) + return + } + + guard let consumeEvent = arguments?["consumeEvent"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_markerClickListener", details: nil)) + return + } + + mapsIndoorsData.mapControlListenerDelegate?.respondToTapIcon = setupListener + mapsIndoorsData.mapControlListenerDelegate?.consumeTapIcon = consumeEvent + } + + func markerInfoWindowClickListener(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let setupListener = arguments?["setupListener"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_markerInfoWindowClickListener", details: nil)) + return + } + + mapsIndoorsData.mapControlListenerDelegate?.respondToDidTapInfoWindow = setupListener + } + + func markerInfoWindowCloseListener(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let setupListener = arguments?["setupListener"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_markerInfoWindowCloseListener", details: nil)) + return + } + + //TODO: Not implemented + //result(FlutterError(code: "Not implemented on v4", message: nil, details: nil)) + } + + func willUpdateLocationsOnMap(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let setupListener = arguments?["setupListener"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MPL_willUpdateLocationsOnMap", details: nil)) + return + } + + //TODO: Not implemented + //result(FlutterError(code: "Not implemented on v4", message: nil, details: nil)) + } + } +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapControl/MapControlMethodChannel.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapControl/MapControlMethodChannel.swift new file mode 100644 index 0000000..d2429e7 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapControl/MapControlMethodChannel.swift @@ -0,0 +1,664 @@ +// +// MapControlMethodChannel.swift +// mapsindoors_ios +// +// Created by Martin Hansen on 21/02/2023. +// + +import Foundation +import Flutter +import UIKit +import MapsIndoors +import MapsIndoorsCore +import MapsIndoorsCodable + +public class MapControlMethodChannel: NSObject { + + enum Methods: String { + case MPC_selectFloor + case MPC_clearFilter + case MPC_deSelectLocation + case MPC_getCurrentBuilding + case MPC_getCurrentBuildingFloor + case MPC_getCurrentFloorIndex + case MPC_setFloorSelector + case MPC_getCurrentMapsIndoorsZoom + case MPC_getCurrentVenue + case MPC_getMapStyle + case MPC_getMapViewPaddingBottom + case MPC_getMapViewPaddingEnd + case MPC_getMapViewPaddingStart + case MPC_getMapViewPaddingTop + case MPC_goTo + case MPC_hideFloorSelector + case MPC_isFloorSelectorHidden + case MPC_isUserPositionShown + case MPC_selectBuilding + case MPC_selectLocation + case MPC_selectLocationById + case MPC_selectVenue + case MPC_setFilter + case MPC_setFilterWithLocations + case MPC_setMapPadding + case MPC_setMapStyle + case MPC_showInfoWindowOnClickedLocation + case MPC_showUserPosition + case MPC_enableLiveData + case MPC_disableLiveData + case MPC_moveCamera + case MPC_animateCamera + case MPC_getCurrentCameraPosition + + func call(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + let runner: (_ arguments: [String: Any]?, _ mapsIndoorsData: MapsIndoorsData, _ result: @escaping FlutterResult) -> Void + + switch self { + case .MPC_selectFloor: runner = selectFloor + case .MPC_clearFilter: runner = clearFilter + case .MPC_deSelectLocation: runner = deSelectLocation + case .MPC_getCurrentBuilding: runner = getCurrentBuilding + case .MPC_getCurrentBuildingFloor: runner = getCurrentBuildingFloor + case .MPC_getCurrentFloorIndex: runner = getCurrentFloorIndex + case .MPC_setFloorSelector: runner = setFloorSelector + case .MPC_getCurrentMapsIndoorsZoom: runner = getCurrentMapsIndoorsZoom + case .MPC_getCurrentVenue: runner = getCurrentVenue + case .MPC_getMapStyle: runner = getMapStyle + case .MPC_getMapViewPaddingBottom: runner = getMapViewPaddingBottom + case .MPC_getMapViewPaddingEnd: runner = getMapViewPaddingEnd + case .MPC_getMapViewPaddingStart: runner = getMapViewPaddingStart + case .MPC_getMapViewPaddingTop: runner = getMapViewPaddingTop + case .MPC_goTo: runner = goTo + case .MPC_hideFloorSelector: runner = hideFloorSelector + case .MPC_isFloorSelectorHidden: runner = isFloorSelectorHidden + case .MPC_isUserPositionShown: runner = isUserPositionShown + case .MPC_selectBuilding: runner = selectBuilding + case .MPC_selectLocation: runner = selectLocation + case .MPC_selectLocationById: runner = selectLocationById + case .MPC_selectVenue: runner = selectVenue + case .MPC_setFilter: runner = setFilter + case .MPC_setFilterWithLocations: runner = setFilterWithLocations + case .MPC_setMapPadding: runner = setMapPadding + case .MPC_setMapStyle: runner = setMapStyle + case .MPC_showInfoWindowOnClickedLocation: runner = showInfoWindowOnClickedLocation + case .MPC_showUserPosition: runner = showUserPosition + case .MPC_enableLiveData: runner = enableLiveData + case .MPC_disableLiveData: runner = disableLiveData + case .MPC_moveCamera: runner = moveCamera + case .MPC_animateCamera: runner = moveCamera + case .MPC_getCurrentCameraPosition: runner = getCurrentCameraPosition + } + + runner(arguments, mapsIndoorsData, result) + } + + func selectFloor(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let floorIndex = arguments?["floorIndex"] as? Int else { + result(FlutterError(code: "Could not read floorIndex", message: "MPC_selectFloor", details: nil)) + return + } + + mapsIndoorsData.mapControl?.select(floorIndex: floorIndex) + result(nil) + } + + + func clearFilter(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + mapsIndoorsData.mapControl?.clearFilter() + result(nil) + } + + func deSelectLocation(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + mapsIndoorsData.mapControl?.select(location: nil, behavior: MPSelectionBehavior()) + result(nil) + } + + func getCurrentBuilding(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let currentBuilding = mapsIndoorsData.mapControl?.currentBuilding else { + result(nil) + return + } + + do { + let jsonData = try JSONEncoder().encode(MPBuildingCodable(withBuilding: currentBuilding)) + let resultJson = String(data: jsonData, encoding: String.Encoding.utf8) + result(resultJson) + } catch { + result(FlutterError(code: "Could not encode building", message: "MPC_getCurrentBuilding", details: nil)) + } + } + + func getCurrentBuildingFloor(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let currentFloorIndex = mapsIndoorsData.mapControl?.currentFloorIndex else { + result(nil) + return + } + + guard let currentFloor = mapsIndoorsData.mapControl?.currentBuilding?.floors?[String(currentFloorIndex)] else { + result(FlutterError(code: "Could not find currentFloor", message: "MPC_getCurrentBuildingFloor", details: nil)) + return + } + + let floorCodable = MPFloorCodable(withFloor: currentFloor) + + do { + let jsonData = try JSONEncoder().encode(floorCodable) + let resultJson = String(data: jsonData, encoding: String.Encoding.utf8) + result(resultJson) + } catch { + result(FlutterError(code: "Could not encode floor", message: "MPC_getCurrentBuildingFloor", details: nil)) + } + } + + func getCurrentFloorIndex(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let currentFloorIndex = mapsIndoorsData.mapControl?.currentFloorIndex else { + result(FlutterError(code: "Could not find currentFloor", message: nil, details: nil)) + return + } + result(currentFloorIndex) + } + + func setFloorSelector(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + + guard let isAutoFloorChangeEnabled = arguments?["isAutoFloorChangeEnabled"] as? Bool else { + result(FlutterError(code: "Could not read isAutoFloorChangeEnabled", message: "MPC_setFloorSelector", details: nil)) + return + } + + mapsIndoorsData.floorSelector = CustomFloorSelector(isAutoFloorChangeEnabled: isAutoFloorChangeEnabled, methodChannel: mapsIndoorsData.mapControlFloorSelector!) + mapsIndoorsData.mapControl?.floorSelector = mapsIndoorsData.floorSelector + + result(nil) + } + + func getCurrentMapsIndoorsZoom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let currentZoom = mapsIndoorsData.mapControl?.cameraPosition.zoom else { + result(FlutterError(code: "Could not find currentZoom", message: "MPC_getCurrentMapsIndoorsZoom", details: nil)) + return + } + result(currentZoom) + } + + func getCurrentVenue(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let currentVenue = mapsIndoorsData.mapControl?.currentVenue else { + result(nil) + return + } + do { + let jsonData = try JSONEncoder().encode(MPVenueCodable(withVenue: currentVenue)) + let resultJson = String(data: jsonData, encoding: String.Encoding.utf8) + result(resultJson) + } catch { + result(FlutterError(code: "Could not encode currentVenue", message: "MPC_getCurrentVenue", details: nil)) + } + } + + func getMapStyle(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + var mapStyle = mapsIndoorsData.mapControl?.mapStyle + //If the mapstyle is not set, use the default style for the current venue instead + if(mapStyle == nil) { + guard let currentVenue = mapsIndoorsData.mapControl?.currentVenue else { + result(nil) + return + } + mapStyle = currentVenue.defaultStyle + if(mapStyle == nil) { + result(nil) + return + } + } + + do { + let jsonData = try JSONEncoder().encode(MPMapStyleCodable(withMapStyle: mapStyle!)) + let resultJson = String(data: jsonData, encoding: String.Encoding.utf8) + result(resultJson) + } catch { + result(FlutterError(code: "Could not encode mapStyle", message: "MPC_getMapStyle", details: nil)) + } + } + + func getMapViewPaddingBottom(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + let bottom = NSNumber(value: Float(mapsIndoorsData.mapControl?.mapPadding.bottom ?? 0.0)) + result(Int.init(truncating: bottom)) + } + + + func getMapViewPaddingEnd(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + let end = NSNumber(value: Float(mapsIndoorsData.mapControl?.mapPadding.right ?? 0.0)) + result(Int.init(truncating: end)) + } + + func getMapViewPaddingStart(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + let start = NSNumber(value: Float(mapsIndoorsData.mapControl?.mapPadding.left ?? 0.0)) + result(Int.init(truncating: start)) + } + + func getMapViewPaddingTop(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + let top = NSNumber(value: Float(mapsIndoorsData.mapControl?.mapPadding.top ?? 0.0)) + result(Int.init(truncating: top)) + } + + func goTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments as? [String: String] else { + result(nil) + return + } + + guard let entityJson = args["entity"] else { + result(nil) + return + } + + guard let type = args["type"] else { + result(FlutterError(code: "Could not read type", message: "MPC_goTo", details: nil)) + return + } + + var entity: MPEntity + + switch type { + case "MPLocation": + guard let LocationId = try? JSONDecoder().decode(MPLocationIdCodable.self, from: entityJson.data(using: .utf8)!) else { + result(FlutterError(code: "Could not parse entity as a location", message: "MPC_goTo", details: nil)) + return + } + guard let location = MPMapsIndoors.shared.locationWith(locationId: LocationId.id) else { + result(FlutterError(code: "Could find location with id", message: "MPC_goTo", details: nil)) + return + } + entity = location + case "MPFloor": + guard let floor = try? JSONDecoder().decode(MPFloorCodable.self, from: entityJson.data(using: .utf8)!) else { + result(FlutterError(code: "Could not parse entity as a floor", message: "MPC_goTo", details: nil)) + return + } + entity = floor + + case "MPBuilding": + guard let building = try? JSONDecoder().decode(MPBuildingCodable.self, from: entityJson.data(using: .utf8)!) else { + result(FlutterError(code: "Could not parse entity as a building", message: "MPC_goTo", details: nil)) + return + } + entity = building + + case "MPVenue": + guard let venue = try? JSONDecoder().decode(MPVenueCodable.self, from: entityJson.data(using: .utf8)!) else { + result(FlutterError(code: "Could not parse venue", message: "MPC_goTo", details: nil)) + return + } + entity = venue + + default: + result(FlutterError(code: "Unknown type", message: "MPC_goTo", details: nil)) + return + } + + mapsIndoorsData.mapControl?.goTo(entity: MPEntityCodable(withEntity: entity)) + result(nil) + } + + func hideFloorSelector(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let hide = arguments?["hide"] as? Bool else { + result(nil) + return + } + + mapsIndoorsData.mapControl?.hideFloorSelector = hide + result(nil) + } + + func isFloorSelectorHidden(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + if (mapsIndoorsData.mapControl != nil) { + result(mapsIndoorsData.mapControl!.hideFloorSelector) + }else { + result(FlutterError(code: "No existing mapcontrol", message: "MPC_isFloorSelectorHidden", details: nil)) + } + } + + func isUserPositionShown(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + result(mapsIndoorsData.mapControl?.showUserPosition) + } + + func selectBuilding(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + Task { + + guard let buildingJson = arguments?["building"] as? String else { + result(FlutterError(code: "Could not read building", message: "MPC_selectBuilding", details: nil)) + return + } + + guard let moveCamera = arguments?["moveCamera"] as? Bool else { + result(FlutterError(code: "Could not read moveCamera", message: "MPC_selectBuilding", details: nil)) + return + } + + var buildingId: String? + do { + if let json = try JSONSerialization.jsonObject(with: Data(buildingJson.utf8), options: []) as? [String: Any] { + buildingId = json["id"] as! String? + } + } catch { + result(FlutterError(code: "Could not read building id", message: "MPC_selectBuilding", details: nil)) + return + } + + //Create a selectionBehavior with the moveCamera set as desired + let selectionBehavior = MPSelectionBehavior() + selectionBehavior.moveCamera = moveCamera + + //Find the building if possible + guard let building = await MPMapsIndoors.shared.buildingWith(id: buildingId!) else { + result(FlutterError(code: "Could not find a building with the given buildingId", message: "MPC_selectBuilding", details: nil)) + return + } + + DispatchQueue.main.async { + mapsIndoorsData.mapControl?.select(building: building, behavior: selectionBehavior) + } + result(nil) + } + } + + func selectLocation(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + + guard let LocationId = arguments?["location"] as? String else { + result(nil) + return + } + + guard let behaviorJson = arguments?["behavior"] as? String else { + result(FlutterError(code: "Could not read behavior", message: "MPC_selectLocation", details: nil)) + return + } + + guard let selectionBehavior = try? JSONDecoder().decode(MPSelectionBehavior.self, from: behaviorJson.data(using: .utf8)!) else { + result(FlutterError(code: "Could not parse behavior", message: "MPC_selectLocation", details: nil)) + return + } + + //Find the location if possible + guard let location = MPMapsIndoors.shared.locationWith(locationId: LocationId) else { + result(FlutterError(code: "Could not find a location with the given locationId", message: "MPC_selectLocation", details: nil)) + return + } + + DispatchQueue.main.async { + mapsIndoorsData.mapControl?.select(location: location, behavior: selectionBehavior) + } + result(nil) + } + + func selectLocationById(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments as? [String: String] else { + result(FlutterError(code: "Could not read arguments", message: "MPC_selectLocationById", details: nil)) + return + } + + guard let locationId = args["id"] else { + result(FlutterError(code: "Could not read locationId", message: "MPC_selectLocationById", details: nil)) + return + } + + guard let behaviorJson = args["behavior"] else { + result(FlutterError(code: "Could not read behavior", message: "MPC_selectLocationById", details: nil)) + return + } + + guard let selectionBehavior = try? JSONDecoder().decode(MPSelectionBehavior.self, from: behaviorJson.data(using: .utf8)!) else { + result(FlutterError(code: "Could not parse behavior", message: "MPC_selectLocationById", details: nil)) + return + } + + let location = MPMapsIndoors.shared.locationWith(locationId: locationId) + if (location == nil) { + result(FlutterError(code: "Could not find a location with the given locationId", message: "MPC_selectLocationById", details: nil)) + } + + DispatchQueue.main.async { + mapsIndoorsData.mapControl?.select(location: location, behavior: selectionBehavior) + } + + result(nil) + } + + func selectVenue(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + Task { + + guard let venueJson = arguments?["venue"] as? String else { + result(FlutterError(code: "Could not read venue", message: "MPC_selectVenue", details: nil)) + return + } + + guard let moveCamera = arguments?["moveCamera"] as? Bool else { + result(FlutterError(code: "Could not read moveCamera", message: "MPC_selectVenue", details: nil)) + return + } + + var venueId: String? + do { + if let json = try JSONSerialization.jsonObject(with: Data(venueJson.utf8), options: []) as? [String: Any] { + venueId = json["id"] as! String? + } + } catch { + result(FlutterError(code: "Could not read venue id", message: "MPC_selectVenue", details: nil)) + return + } + + //Create a selectionBehavior + let selectionBehavior = MPSelectionBehavior() + selectionBehavior.moveCamera = moveCamera + + //Find the venue if possible + guard let venue = await MPMapsIndoors.shared.venueWith(id: venueId!) else { + result(FlutterError(code: "Could not find a venue with the given venueId", message: "MPC_selectVenue", details: nil)) + return + } + + DispatchQueue.main.async { + mapsIndoorsData.mapControl?.select(venue: venue, behavior: selectionBehavior) + } + result(nil) + } + } + + func setFilter(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments as? [String: String] else { + result(FlutterError(code: "Could not read arguments", message: "MPC_setFilter", details: nil)) + return + } + + guard var filterJson = args["filter"] else { + result(FlutterError(code: "Could not read filter", message: "MPC_setFilter", details: nil)) + return + } + + guard let behaviorJson = args["behavior"] else { + result(FlutterError(code: "Could not read behavior", message: "MPC_setFilter", details: nil)) + return + } + +// let filter = Filter().toMPFilter(jsonString: filterJson) + guard let filter = try? JSONDecoder().decode(MPFilter.self, from: filterJson.data(using: .utf8)!) else { + result(FlutterError(code: "Could not parse filter", message: "MPC_setFilter", details: nil)) + return + } + + + + guard let filterBehavior = try? JSONDecoder().decode(MPFilterBehavior.self, from: behaviorJson.data(using: .utf8)!) else { + result(FlutterError(code: "Could not parse filterBehavior", message: "MPC_setFilter", details: nil)) + return + } + + mapsIndoorsData.mapControl?.setFilter(filter: filter, behavior: filterBehavior) + result(nil) + } + + func setFilterWithLocations(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let locationIds = arguments?["locations"] as? [String] else { + result(FlutterError(code: "Could not read locations", message: "MPC_setFilterWithLocations", details: nil)) + return + } + + guard let behaviorJson = arguments?["behavior"] as? String else { + result(FlutterError(code: "Could not read behavior", message: "MPC_setFilterWithLocations", details: nil)) + return + } + + guard let filterBehavior = try? JSONDecoder().decode(MPFilterBehavior.self, from: behaviorJson.data(using: .utf8)!) else { + result(FlutterError(code: "Could not parse filterBehavior", message: "MPC_setFilterWithLocations", details: nil)) + return + } + + let locations = locationIds.compactMap { MPMapsIndoors.shared.locationWith(locationId: $0) } + mapsIndoorsData.mapControl?.setFilter(locations: locations, behavior: filterBehavior) + result(nil) + } + + func setMapPadding(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let start = arguments?["start"] as? Int else { + result(FlutterError(code: "Could not read start", message: "MPC_setMapPadding", details: nil)) + return + } + guard let top = arguments?["top"] as? Int else { + result(FlutterError(code: "Could not read top", message: "MPC_setMapPadding", details: nil)) + return + } + guard let end = arguments?["end"] as? Int else { + result(FlutterError(code: "Could not read end", message: "MPC_setMapPadding", details: nil)) + return + } + guard let bottom = arguments?["bottom"] as? Int else { + result(FlutterError(code: "Could not read bottom", message: "MPC_setMapPadding", details: nil)) + return + } + + mapsIndoorsData.mapControl?.mapPadding.top = CGFloat(top) + mapsIndoorsData.mapControl?.mapPadding.bottom = CGFloat(bottom) + mapsIndoorsData.mapControl?.mapPadding.left = CGFloat(start) + mapsIndoorsData.mapControl?.mapPadding.right = CGFloat(end) + + result(nil) + } + + func setMapStyle(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + + guard let args = arguments as? [String: String] else { + result(FlutterError(code: "Could not read arguments", message: "MPC_setMapStyle", details: nil)) + return + } + + guard let mapStyleJson = args["mapStyle"] else { + result(FlutterError(code: "Could not read mapStyle", message: "MPC_setMapStyle", details: nil)) + return + } + + guard let mapStyle = try? JSONDecoder().decode(MPMapStyleCodable.self, from: mapStyleJson.data(using: .utf8)!) else { + result(FlutterError(code: "Could not parse mapStyle", message: "MPC_setMapStyle", details: nil)) + return + } + + mapsIndoorsData.mapControl?.mapStyle = mapStyle + result(nil) + } + + func showInfoWindowOnClickedLocation(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let show = arguments?["show"] as? Bool else { + result(FlutterError(code: "Could not read show", message: "MPC_showInfoWindowOnClickedLocation", details: nil)) + return + } + + mapsIndoorsData.mapControl?.showInfoWindowOnClickedLocation = show + result(nil) + } + + func showUserPosition(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let show = arguments?["show"] as? Bool else { + result(FlutterError(code: "Could not read show", message: nil, details: nil)) + return + } + mapsIndoorsData.mapControl?.showUserPosition = show + result(nil) + } + + func enableLiveData(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let hasListener = arguments?["listener"] as? Bool else { + result(FlutterError(code: "Could not read listener", message: "MPC_enableLiveData", details: nil)) + return + } + guard let domainType = arguments?["domainType"] as? String else { + result(FlutterError(code: "Could not read domainType", message: "MPC_enableLiveData", details: nil)) + return + } + + if(hasListener) { + if (mapsIndoorsData.liveDataDelegate == nil && mapsIndoorsData.mapControlMethodChannel != nil) { + mapsIndoorsData.liveDataDelegate = MapControlLiveDataDelegate(methodChannel: mapsIndoorsData.mapControlMethodChannel!) + } + mapsIndoorsData.mapControl?.enableLiveData(domain: domainType, listener: { liveUpdate in + mapsIndoorsData.liveDataDelegate?.dataReceived(liveUpdate: liveUpdate) + }) + } else { + mapsIndoorsData.mapControl?.enableLiveData(domain: domainType, listener: nil) + } + result(nil) + } + + func disableLiveData(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments as? [String: String] else { + result(FlutterError(code: "Could not read arguments", message: "MPC_disableLiveData", details: nil)) + return + } + + guard let domainType = args["domainType"] else { + result(FlutterError(code: "Could not read domainType", message: "MPC_disableLiveData", details: nil)) + return + } + mapsIndoorsData.mapControl?.disableLiveData(domain: domainType) + result(nil) + } + + func moveCamera(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let updateJson = arguments?["update"] as? String else { + result(FlutterError(code: "Could not read update", message: "MPC_moveCamera", details: nil)) + return + } + + guard let cameraUpdate = try? JSONDecoder().decode(CameraUpdate.self, from: updateJson.data(using: .utf8)!) else { + result(FlutterError(code: "Could not parse cameraUpdate", message: "MPC_moveCamera", details: nil)) + return + } + + let duration = arguments?["duration"] as? Int + + do { + if( duration != nil ) { + //Animate the camera + try mapsIndoorsData.mapView?.animateCamera(cameraUpdate: cameraUpdate, duration: duration!) + } else { + //Move the camera + try mapsIndoorsData.mapView?.moveCamera(cameraUpdate: cameraUpdate) + } + }catch { + result(FlutterError(code: "Could not make camera update", message: "MPC_moveCamera", details: nil)) + } + + result(nil) + } + + func getCurrentCameraPosition(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let cameraPosition = mapsIndoorsData.mapControl?.cameraPosition else { + result(FlutterError(code: "Could not read cameraPosition", message: "MPC_getCurrentCameraPosition", details: nil)) + return; + } + + do { + let jsonData = try JSONEncoder().encode(MPCameraPositionCodable(withCameraPosition: cameraPosition)) + let resultJson = String(data: jsonData, encoding: String.Encoding.utf8) + result(resultJson) + } catch { + result(FlutterError(code: "Could not encode cameraPosition", message: "MPC_getCurrentCameraPosition", details: nil)) + } + } + } +} + diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapsIndoors/MapsIndoorsListenerChannel.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapsIndoors/MapsIndoorsListenerChannel.swift new file mode 100644 index 0000000..cda8aa6 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapsIndoors/MapsIndoorsListenerChannel.swift @@ -0,0 +1,76 @@ +// +// MapsIndoorsListenerChannel.swift +// mapsindoors_ios +// +// Created by Martin Hansen on 21/02/2023. +// + +import Foundation +import Flutter +import UIKit +import MapsIndoors + +public class MapsIndoorsListenerChannel: NSObject { + static var delegate: ReadyDelegate? = nil + + enum Methods: String { + case MIL_onMapsIndoorsReadyListener + case MIL_onPositionUpdate + + func call(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel?) { + if (methodChannel == nil) { + return + } + + let runner: (_ arguments: [String: Any]?, _ mapsIndoorsData: MapsIndoorsData, _ result: @escaping FlutterResult, _ methodChannel: FlutterMethodChannel) -> Void + + switch self { + case .MIL_onMapsIndoorsReadyListener: runner = onMapsIndoorsReadyListener + case .MIL_onPositionUpdate: runner = onPositionUpdate + } + + runner(arguments, mapsIndoorsData, result, methodChannel!) + } + + func onMapsIndoorsReadyListener(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let setupListener = arguments?["addListener"] as? Bool else { + result(FlutterError(code: "Missing parameter to set listener", message: "MIL_onMapsIndoorsReadyListener", details: nil)) + return + } + if (setupListener) { + MapsIndoorsListenerChannel.delegate = ReadyDelegate(methodChannel: methodChannel) + mapsIndoorsData.delegate.append(MapsIndoorsListenerChannel.delegate!) + }else { + mapsIndoorsData.delegate = mapsIndoorsData.delegate.filter { !($0 is ReadyDelegate) } + } + } + + func onPositionUpdate(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult, methodChannel: FlutterMethodChannel) { + guard let position = arguments?["position"] as? String else { + result(FlutterError(code: "Missing parameter to set listener", message: "MIL_onPositionUpdate", details: nil)) + return + } + + let positionResult = try? JSONDecoder().decode(MPPositionResult.self, from: Data(position.utf8)) + if (positionResult != nil) { + mapsIndoorsData.positionProvider?.setLatestPosition(positionResult: positionResult!) + } + } + } + + class ReadyDelegate: MapsIndoorsReadyDelegate { + let methodChannel: FlutterMethodChannel + + init(methodChannel: FlutterMethodChannel) { + self.methodChannel = methodChannel + } + + func isReady(error: MPError?) { + if (error == MPError.invalidApiKey || error == MPError.networkError || error == MPError.unknownError ) { + methodChannel.invokeMethod("onMapsIndoorsReady", arguments: error) + }else { + methodChannel.invokeMethod("onMapsIndoorsReady", arguments: nil) + } + } + } +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapsIndoors/MapsIndoorsMethodChannel.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapsIndoors/MapsIndoorsMethodChannel.swift new file mode 100644 index 0000000..7411b74 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapsIndoors/MapsIndoorsMethodChannel.swift @@ -0,0 +1,485 @@ +// +// MapsIndoorsMethodChannel.swift +// mapsindoors_ios +// +// Created by Martin Hansen on 21/02/2023. +// + +import Foundation +import Flutter +import UIKit +import MapsIndoors +import MapsIndoorsCore +import MapsIndoorsCodable + + +public class MapsIndoorsMethodChannel: NSObject { + + enum Methods: String { + case MIN_initialize + case MIN_locationDisplayRuleExists + case MIN_displayRuleNameExists + case MIN_checkOfflineDataAvailability + case MIN_destroy + case MIN_disableEventLogging + case MIN_getAPIKey + case MIN_getAvailableLanguages + case MIN_getBuildings + case MIN_getCategories + case MIN_getDataSet + case MIN_getDefaultLanguage + case MIN_getLanguage + case MIN_getLocationById + case MIN_getLocations + case MIN_getLocationsByExternalIds + case MIN_getLocationsByQuery + case MIN_getMapStyles + case MIN_getSolution + case MIN_getVenues + case MIN_isAPIKeyValid + case MIN_isInitialized + case MIN_isReady + case MIN_setLanguage + case MIN_synchronizeContent + case MIN_applyUserRoles + case MIN_getAppliedUserRoles + case MIN_getUserRoles + case MIN_reverseGeoCode + case MIN_setPositionProvider + case MIN_getDefaultVenue + + func call(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + let runner: (_ arguments: [String: Any]?, _ mapsIndoorsData: MapsIndoorsData, _ result: @escaping FlutterResult) -> Void + + switch self { + case .MIN_initialize: runner = initialize + case .MIN_locationDisplayRuleExists: runner = locationDisplayRuleExists + case .MIN_displayRuleNameExists: runner = displayRuleNameExists + case .MIN_checkOfflineDataAvailability: runner = checkOfflineDataAvailability + case .MIN_destroy: runner = destroy + case .MIN_disableEventLogging: runner = disableEventLogging + case .MIN_getAPIKey: runner = getAPIKey + case .MIN_getAvailableLanguages: runner = getAvailableLanguages + case .MIN_getBuildings: runner = getBuildings + case .MIN_getCategories: runner = getCategories + case .MIN_getDataSet: runner = getDataSet + case .MIN_getDefaultLanguage: runner = getDefaultLanguage + case .MIN_getLanguage: runner = getLanguage + case .MIN_getLocationById: runner = getLocationById + case .MIN_getLocations: runner = getLocations + case .MIN_getLocationsByExternalIds: runner = getLocationsByExternalIds + case .MIN_getLocationsByQuery: runner = getLocationsByQuery + case .MIN_getMapStyles: runner = getMapStyles + case .MIN_getSolution: runner = getSolution + case .MIN_getVenues: runner = getVenues + case .MIN_isAPIKeyValid: runner = isAPIKeyValid + case .MIN_isInitialized: runner = isInitialized + case .MIN_isReady: runner = isReady + case .MIN_setLanguage: runner = setLanguage + case .MIN_synchronizeContent: runner = synchronizeContent + case .MIN_applyUserRoles: runner = applyUserRoles + case .MIN_getAppliedUserRoles: runner = getAppliedUserRoles + case .MIN_getUserRoles: runner = getUserRoles + case .MIN_reverseGeoCode: runner = reverseGeoCode + case .MIN_setPositionProvider: runner = setPositionProvider + case .MIN_getDefaultVenue: runner = getDefaultVenue + } + + runner(arguments, mapsIndoorsData, result) + } + + func initialize(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + + guard let args = arguments else { + result(FlutterError(code: "Initialized called without arguments", message: "MIN_initialize", details: nil)) + return + } + + guard let apiKey = args["key"] as? String else { + result(FlutterError(code: "Could not initialise MapsIndoors", message: "MIN_initialize", details: nil)) + return + } + + Task { + do { + try await MPMapsIndoors.shared.load(apiKey: apiKey) + mapsIndoorsData.mapsIndoorsReady(error: nil) + result(nil) + } + catch { + result("{\"code\": 1000,\"message\": " + "\"" + error.localizedDescription + "\"" + "}") + } + } + } + + func locationDisplayRuleExists(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments as? [String: String] else { + result(FlutterError(code: "Could not read arguments", message: "MIN_locationDisplayRuleExists", details: nil)) + return + } + + guard let locationId = args["id"] else { + result(FlutterError(code: "Could not read locationId", message: "MIN_locationDisplayRuleExists", details: nil)) + return + } + + guard let loc = MPMapsIndoors.shared.locationWith(locationId: locationId) else { + result(FlutterError(code: "Could not find any location with the given locationId", message: "MIN_locationDisplayRuleExists", details: nil)) + return + } + + result ( MPMapsIndoors.shared.displayRuleFor(location: loc) != nil ) + } + + func displayRuleNameExists(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments as? [String: String] else { + result(FlutterError(code: "Could not read arguments", message: "MIN_displayRuleNameExists", details: nil)) + return + } + + guard let typeName = args["name"] else { + result(FlutterError(code: "Could not read name", message: "MIN_displayRuleNameExists", details: nil)) + return + } + //Return true if getdisplayRuleType can find a type with the given name + result ( getdisplayRuleType(name: typeName) != nil || MPMapsIndoors.shared.displayRuleFor(type: typeName) != nil) + } + + func getdisplayRuleType(name: String) -> MPDisplayRuleType? { + switch name { + case "buildingOutline": + return MPDisplayRuleType.buildingOutline + case "selectionHighlight": + return MPDisplayRuleType.selectionHighlight + case "positionIndicator": + return MPDisplayRuleType.blueDot + case "main": + return MPDisplayRuleType.main + case "default": + return MPDisplayRuleType.default + default: + return nil + } + } + + func checkOfflineDataAvailability(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + Task { + guard let apikey = MPMapsIndoors.shared.apiKey else { + result(FlutterError(code: "APIKey not set", message: "MIN_checkOfflineDataAvailability", details: nil)) + return + } + result(await MPMapsIndoors.shared.isOfflineDataAvailable(apiKey: apikey) ? "true" : "false") + } + } + + func destroy(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + MPMapsIndoors.shared.shutdown() + } + + func disableEventLogging(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let disable = arguments?["disable"] as? Bool else { + result(FlutterError(code: "Could not read disable", message: "MIN_disableEventLogging", details: nil)) + return + } + MPMapsIndoors.shared.eventLoggingDisabled = disable + } + + func getAPIKey(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + result(MPMapsIndoors.shared.apiKey) + } + + func getAvailableLanguages(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + result(MPMapsIndoors.shared.solution?.availableLanguages) + } + + func getBuildings(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + Task { + let buildings = await MPMapsIndoors.shared.buildings(); + let jsonData = try JSONEncoder().encode(buildings.map {MPBuildingCodable(withBuilding: $0) }) + result(String(data: jsonData, encoding: String.Encoding.utf8)); + } + } + + func getCategories(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + Task { + let categories = await MPMapsIndoors.shared.categories(); + let jsonData = try JSONEncoder().encode(categories.map {MPDataFieldCodable(withDataField: $0) }) + result(String(data: jsonData, encoding: String.Encoding.utf8)); + } + } + + func getDataSet(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let solution = MPMapsIndoors.shared.solution else { + result(FlutterError(code: "No dataset found", message: "MIN_getDataSet", details: nil)) + return + } + + let jsonData = try! JSONEncoder().encode(MPSolutionCodable(withSolution: solution)) + result(String(data: jsonData, encoding: String.Encoding.utf8)); + } + + func getDefaultLanguage(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + result(MPMapsIndoors.shared.solution?.defaultLanguage) + } + + func getLanguage(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + result(MPMapsIndoors.shared.language) + } + + func getLocationById(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments as? [String: String] else { + result(FlutterError(code: "Could not read arguments", message: "MIN_getLocationById", details: nil)) + return + } + + guard let locationId = args["id"] else { + result(FlutterError(code: "Could not read locationId", message: "MIN_getLocationById", details: nil)) + return + } + + guard let location = MPMapsIndoors.shared.locationWith(locationId: locationId) else { + result(FlutterError(code: "Could not find location with the given locationId", message: "MIN_getLocationById", details: nil)) + return + } + + let jsonData = try! JSONEncoder().encode(MPLocationCodable(withLocation: location)) + + let resultJson = String(data: jsonData, encoding: String.Encoding.utf8) + result(resultJson); + } + + func getLocations(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + Task + { + let query = MPQuery() + let filter = MPFilter() + + let locations = await MPMapsIndoors.shared.locationsWith(query: query, filter: filter); + let locationsCodable = locations.map {MPLocationCodable(withLocation: $0) } + + let jsonData = try JSONEncoder().encode(locationsCodable) + let resultJson = String(data: jsonData, encoding: String.Encoding.utf8) + + result(resultJson); + } + } + + func getLocationsByExternalIds(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let locationIds = arguments?["ids"] as? [String] else { + result(FlutterError(code: "Could not read locationId", message: "MIN_getLocationsByExternalIds", details: nil)) + return + } + + let locations = MPMapsIndoors.shared.locationsWith(externalIds: locationIds) + let locationsCodable = locations.map {MPLocationCodable(withLocation: $0) } + + let jsonData = try! JSONEncoder().encode(locationsCodable) + let resultJson = String(data: jsonData, encoding: String.Encoding.utf8) + + result(resultJson); + } + + func getLocationsByQuery(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + Task + { + guard let args = arguments as? [String: String] else { + result(FlutterError(code: "Could not read arguments", message: "MIN_getLocationsByQuery", details: nil)) + return + } + + guard let queryJson = args["query"] else { + result(FlutterError(code: "Could not read query", message: "MIN_getLocationsByQuery", details: nil)) + return + } + + guard let filterJson = args["filter"] else { + result(FlutterError(code: "Could not read filter", message: "MIN_getLocationsByQuery", details: nil)) + return + } + + let query = Query().toMPQuery(jsonString: queryJson) + let filter = Filter().toMPFilter(jsonString: filterJson) + + let locations = await MPMapsIndoors.shared.locationsWith(query: query, filter: filter); + let locationsCodable = locations.map {MPLocationCodable(withLocation: $0) } + + let jsonData = try JSONEncoder().encode(locationsCodable) + let resultJson = String(data: jsonData, encoding: String.Encoding.utf8) + + result(resultJson); + } + } + + func getMapStyles(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + Task { + guard let defaultVenue = await MPMapsIndoors.shared.venues().first else { + result(FlutterError(code: "Could not get default venue", message: "MIN_getMapStyles", details: nil)) + return + } + + //Use the current venue (if available) or fallback to the default venue + let venue = mapsIndoorsData.mapControl?.currentVenue ?? defaultVenue + + //If there are no mapstyles just return an empty array + guard let mapStyles = venue.styles else { + result("[]") + return + } + + let jsonData = try! JSONEncoder().encode( mapStyles.map { MPMapStyleCodable(withMapStyle: $0)} ) + result(String(data: jsonData, encoding: String.Encoding.utf8)); + } + } + + func getSolution(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let solution = MPMapsIndoors.shared.solution else { + result(FlutterError(code: "Solution not found", message: "MIN_getSolution", details: nil)) + return + } + + let jsonData = try! JSONEncoder().encode(MPSolutionCodable(withSolution: solution)) + result(String(data: jsonData, encoding: String.Encoding.utf8)); + } + + func getVenues(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + Task { + let venues = await MPMapsIndoors.shared.venues(); + let jsonData = try JSONEncoder().encode(venues.map { MPVenueCodable(withVenue: $0) }) + let venueJson = String(data: jsonData, encoding: String.Encoding.utf8) + result(venueJson); + } + } + + func isAPIKeyValid(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + Task { + guard let apiKey = MPMapsIndoors.shared.apiKey else { + result(FlutterError(code: "Apikey not set", message: "MIN_isAPIKeyValid", details: nil)) + return + } + let isValid = await MPMapsIndoors.shared.isApiKeyValid(apiKey: apiKey) + result(isValid) + } + } + + func isInitialized(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + result(nil) + } + + func isReady(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + result(MPMapsIndoors.shared.ready) + } + + func setLanguage(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments as? [String: String] else { + result(FlutterError(code: "Could not read arguments", message: "MIN_setLanguage", details: nil)) + return + } + + guard let language = args["language"] else { + result(FlutterError(code: "Could not read language argument", message: "MIN_setLanguage", details: nil)) + return + } + MPMapsIndoors.shared.language = language + result(nil) + } + + func synchronizeContent(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + Task { + let _ = try await MPMapsIndoors.shared.synchronize() + result(nil) + } + } + + func applyUserRoles(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments as? [String: String] else { + result(FlutterError(code: "Could not read arguments", message: "MIN_applyUserRoles", details: nil)) + return + } + + guard let userRolesJson = args["userRoles"] else { + result(FlutterError(code: "Could not read userRoles argument", message: "MIN_applyUserRoles", details: nil)) + return + } + + do { + let userRoles = try JSONDecoder().decode([MPUserRole].self, from: Data(userRolesJson.utf8)) + MPMapsIndoors.shared.userRoles = userRoles + result(nil) + } catch { + result(FlutterError(code: "Unable to read userroles", message: "MIN_applyUserRoles", details: nil)) + } + } + + func getAppliedUserRoles(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + do { + let jsonData = try JSONEncoder().encode([MPUserRole](MPMapsIndoors.shared.userRoles)) + let resultJson = String(data: jsonData, encoding: String.Encoding.utf8) + result(resultJson) + } catch { + result(FlutterError(code: "Could not encode userroles", message: "MIN_getAppliedUserRoles", details: nil)) + } + } + + func getUserRoles(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + do { + let jsonData = try JSONEncoder().encode([MPUserRole](MPMapsIndoors.shared.availableUserRoles)) + let resultJson = String(data: jsonData, encoding: String.Encoding.utf8) + result(resultJson) + } catch { + result(FlutterError(code: "Could not encode userroles", message: "MIN_getUserRoles", details: nil)) + } + } + + func reverseGeoCode(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let pointJson = arguments?["point"] as? String else { + result(FlutterError(code: "Could not read point", message: "MIN_reverseGeoCode", details: nil)) + return + } + + do { + let point = try JSONDecoder().decode(MPPoint.self, from: Data(pointJson.utf8)) + } catch { + result(FlutterError(code: "Unable to parse point", message: "MIN_reverseGeoCode", details: nil)) + } + + //TODO: reverseGeoCode does not exists on IOS. Sorry. + result(FlutterError(code: "Not implemented as it's not available in IOS", message: nil, details: nil)) + } + + func setPositionProvider(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + + guard let remove = arguments?["remove"] as? Bool else { + result(FlutterError(code: "Could not read arguments", message: "MIN_setPositionProvider", details: nil)) + return + } + + guard let name = arguments?["name"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "MIN_setPositionProvider", details: nil)) + return + } + + if(remove) { + mapsIndoorsData.positionProvider = nil + mapsIndoorsData.mapControl?.positionProvider = nil + } else { + let provider = FlutterPositionProvider() + provider.name = name + provider.mapsIndoorsData = mapsIndoorsData + mapsIndoorsData.mapControl?.showUserPosition = true + mapsIndoorsData.positionProvider = provider + mapsIndoorsData.mapControl?.positionProvider = provider + } + } + + func getDefaultVenue(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + Task { + guard let defaultVenue = await MPMapsIndoors.shared.venues().first else { + result(FlutterError(code: "Could not get default venue", message: "MIN_getDefaultVenue", details: nil)) + return + } + let jsonData = try! JSONEncoder().encode( MPVenueCodable(withVenue: defaultVenue) ) + result(String(data: jsonData, encoding: String.Encoding.utf8)); + } + } + } +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapsIndoors/UtilMethodChannel.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapsIndoors/UtilMethodChannel.swift new file mode 100644 index 0000000..14860db --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Channels/MapsIndoors/UtilMethodChannel.swift @@ -0,0 +1,323 @@ +// +// MapsIndoorsMethodChannel.swift +// mapsindoors_ios +// +// Created by Martin Hansen on 21/02/2023. +// + +import Foundation +import Flutter +import UIKit +import MapsIndoors +import MapsIndoorsCore + +public class UtilMethodChannel: NSObject { + enum Methods: String { + case UTL_getPlatformVersion + case UTL_venueHasGraph + case UTL_pointAngleBetween + case UTL_pointDistanceTo + case UTL_geometryIsInside + case UTL_geometryArea + case UTL_polygonDistToClosestEdge + case UTL_parseMapClientUrl + case UTL_setCollisionHandling + case UTL_setEnableClustering + case UTL_setExtrusionOpacity + case UTL_setWallOpacity + + func call(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + let runner: (_ arguments: [String: Any]?, _ mapsIndoorsData: MapsIndoorsData, _ result: @escaping FlutterResult) -> Void + + switch self { + case .UTL_getPlatformVersion: runner = getPlatformVersion + case .UTL_venueHasGraph: runner = venueHasGraph + case .UTL_pointAngleBetween: runner = pointAngleBetween + case .UTL_pointDistanceTo: runner = pointDistanceTo + case .UTL_geometryIsInside: runner = geometryIsInside + case .UTL_geometryArea: runner = geometryArea + case .UTL_polygonDistToClosestEdge: runner = polygonDistToClosestEdge + case .UTL_parseMapClientUrl: runner = parseMapClientUrl + case .UTL_setCollisionHandling: runner = setCollisionHandling + case .UTL_setEnableClustering: runner = setEnableClustering + case .UTL_setExtrusionOpacity: runner = setExtrusionOpacity + case .UTL_setWallOpacity: runner = setWallOpacity + } + + runner(arguments, mapsIndoorsData, result) + } + + func getPlatformVersion(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + result("iOS " + UIDevice.current.systemVersion) + } + + func venueHasGraph(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + Task{ + guard let args = arguments else { + result(FlutterError(code: "venueHasGraph called without arguments", message: "UTL_venueHasGraph", details: nil)) + return + } + + guard let venueId = args["venueId"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_venueHasGraph", details: nil)) + return + } + + let venue = await MPMapsIndoors.shared.venueWith(id: venueId) + result(venue?.hasGraph) + } + } + + func pointAngleBetween(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "pointAngleBetween called without arguments", message: "UTL_pointAngleBetween", details: nil)) + return + } + + guard let it = args["it"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_pointAngleBetween", details: nil)) + return + } + + guard let other = args["other"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_pointAngleBetween", details: nil)) + return + } + + let decoder = JSONDecoder() + let fromPoint = try! decoder.decode(MPPoint.self, from: Data(it.utf8)) + let toPoint = try! decoder.decode(MPPoint.self, from: Data(other.utf8)) + + let fromCoordinate = fromPoint.coordinate; + let toCoordinate = toPoint.coordinate; + + let angle = MPGeometryUtils.bearingBetweenPoints(from: fromCoordinate, to: toCoordinate) + result(angle) + } + + func pointDistanceTo(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "pointDistanceTo called without arguments", message: "UTL_pointDistanceTo", details: nil)) + return + } + + guard let it = args["it"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_pointDistanceTo", details: nil)) + return + } + + guard let other = args["other"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_pointDistanceTo", details: nil)) + return + } + + let decoder = JSONDecoder() + let itObj = try! decoder.decode(MPPoint.self, from: Data(it.utf8)) + let otherObj = try! decoder.decode(MPPoint.self, from: Data(other.utf8)) + + let distance = itObj.distanceTo(otherObj) + result(distance) + } + + func geometryIsInside(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "geometryIsInside called without arguments", message: "UTL_geometryIsInside", details: nil)) + return + } + + guard let geo = args["it"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_geometryIsInside", details: nil)) + return + } + + guard let point = args["point"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_geometryIsInside", details: nil)) + return + } + + guard let it = try? JSONDecoder().decode(MPPoint.self, from: Data(point.utf8)) as MPPoint else { + result(FlutterError(code: "Could not parse point", message: "UTL_geometryIsInside", details: nil)) + return + } + + guard let geom = try? JSONDecoder().decode(MPGeometry.self, from: Data(geo.utf8)) as MPGeometry else { + result(FlutterError(code: "Could not parse geometry", message: "UTL_geometryIsInside", details: nil)) + return + } + + //Determine of the point is inside the polygon + if (geom is MPPolygonGeometry) { + let poly = geom.mp_polygon + result(poly?.containsCoordinate(it.coordinate)) + }else if (geom is MPMultiPolygonGeometry) { + let multiPoly = geom.mp_multiPolygon + result(multiPoly?.containsCoordinate(it.coordinate)) + }else { + result(FlutterError(code: "The given geometry needs to be a polygon", message: "UTL_geometryIsInside", details: nil)) + } + } + + func geometryArea(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "parseMapClientUrl called without arguments", message: "UTL_geometryArea", details: nil)) + return + } + + guard let geo = args["geometry"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_geometryArea", details: nil)) + return + } + + let decoder = JSONDecoder() + let geoObj = try! decoder.decode(MPGeometry.self, from: Data(geo.utf8)) + + if(geoObj is MPPoint) + { + result(0) + return + } + if(geoObj is MPPolygonGeometry) + { + result(geoObj.mp_polygon?.area) + return + } + + result(nil) + } + + func polygonDistToClosestEdge(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "parseMapClientUrl called without arguments", message: "UTL_polygonDistToClosestEdge", details: nil)) + return + } + + guard let pointJson = args["point"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_polygonDistToClosestEdge", details: nil)) + return + } + + guard let geo = args["it"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_polygonDistToClosestEdge", details: nil)) + return + } + + let decoder = JSONDecoder() + let geoObj = try! decoder.decode(MPGeometry.self, from: Data(geo.utf8)) + if( !(geoObj is MPPolygonGeometry)) { + result(FlutterError(code: "Could not read polygon data", message: "UTL_polygonDistToClosestEdge", details: nil)) + } + + let point = try! decoder.decode(MPPoint.self, from: Data(pointJson.utf8)) + let geoPoint = CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude) + + if(geoObj is MPPolygonGeometry) + { + + guard let outerRing = (geoObj.mp_polygon.coordinates.first?.map {CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude)}) else + { + result(FlutterError(code: "Could not read polygon data", message: "UTL_polygonDistToClosestEdge", details: nil)) + return + } + + var shortestDistance = Double.greatestFiniteMagnitude + for i in 1 ..< outerRing.count { + let p1 = outerRing[i-1] + let p2 = outerRing[i] + + let distanceToLine = MPGeometryUtils.distancePointToLine(point: geoPoint, lineStart: p1, lineEnd: p2) + if( distanceToLine < shortestDistance) { + shortestDistance = distanceToLine + } + } + + result(shortestDistance) + return + } + result(nil) + } + + func parseMapClientUrl(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "parseMapClientUrl called without arguments", message: "UTL_parseMapClientUrl", details: nil)) + return + } + + guard let venueId = args["venueId"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_parseMapClientUrl", details: nil)) + return + } + + guard let locationId = args["locationId"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_parseMapClientUrl", details: nil)) + return + } + + result(MPMapsIndoors.shared.solution?.getMapClientUrlFor(venueId: venueId, locationId: locationId)) + } + + func setCollisionHandling(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "setCollisionHandling called without arguments", message: "UTL_setCollisionHandling", details: nil)) + return + } + + guard let handling = args["handling"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_setCollisionHandling", details: nil)) + return + } + + let decoder = JSONDecoder() + let collisionHandling = try! decoder.decode(MPCollisionHandling.self, from: Data(handling.utf8)) + + MPMapsIndoors.shared.solution?.config.collisionHandling = collisionHandling + } + + func setEnableClustering(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "setEnableClustering called without arguments", message: "UTL_setEnableClustering", details: nil)) + return + } + + guard let enable = args["enable"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_setEnableClustering", details: nil)) + return + } + + MPMapsIndoors.shared.solution?.config.enableClustering = enable == "true" + } + + func setExtrusionOpacity(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "setExtrusionOpacity called without arguments", message: nil, details: nil)) + return + } + + guard let opacityString = args["opacity"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_setExtrusionOpacity", details: nil)) + return + } + + let opacity = Int(opacityString) + + //Note: 3d settings is not available in the V4 SDK yet +// MPMapsIndoors.shared.solution?.config.settings3D["ExtrusionOpacity"] = opacity + } + + func setWallOpacity(arguments: [String: Any]?, mapsIndoorsData: MapsIndoorsData, result: @escaping FlutterResult) { + guard let args = arguments else { + result(FlutterError(code: "setWallOpacity called without arguments", message: "UTL_setWallOpacity", details: nil)) + return + } + + guard let opacityString = args["opacity"] as? String else { + result(FlutterError(code: "Could not read arguments", message: "UTL_setWallOpacity", details: nil)) + return + } + + let opacity = Int(opacityString) + + //Note: 3d settings is not available in the V4 SDK yet +// MPMapsIndoors.shared.solution?.config.settings3D["WallOpacity"] = opacity + } + } +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/FlutterMapView.swift b/mapsindoors_mapbox_ios/ios/Classes/core/FlutterMapView.swift new file mode 100644 index 0000000..601d587 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/FlutterMapView.swift @@ -0,0 +1,14 @@ +// +// FlutterMapView.swift +// mapsindoors_googlemaps_ios +// +// Created by Tim Mikkelsen on 26/07/2023. +// + +import MapsIndoors + + +protocol FlutterMapView { + func animateCamera(cameraUpdate: CameraUpdate, duration: Int) throws + func moveCamera(cameraUpdate: CameraUpdate) throws +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/MapsIndoorsData.swift b/mapsindoors_mapbox_ios/ios/Classes/core/MapsIndoorsData.swift new file mode 100644 index 0000000..0b42691 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/MapsIndoorsData.swift @@ -0,0 +1,301 @@ +// +// MapsIndoorsData.swift +// mapsindoors_ios +// +// Created by Martin Hansen on 21/02/2023. + +import Foundation +import UIKit +import MapsIndoors +import MapsIndoorsCore +import Flutter +import MapsIndoorsCodable + +protocol MapsIndoorsReadyDelegate: AnyObject { + func isReady(error: MPError?) +} + +protocol LiveDataDelegate: AnyObject { + func dataReceived(liveUpdate: MPLiveUpdate) +} + +public class MapsIndoorsData: NSObject { + + private var _mapView: FlutterMapView? = nil + private var _mapControl: MPMapControl? = nil + private var _mapControlMethodChannel: FlutterMethodChannel? = nil + private var _mapControlFloorSelector: FlutterMethodChannel? = nil + private var _mapsIndoorsMethodChannel: FlutterMethodChannel? = nil + private var _directionsRendererMethodChannel: FlutterMethodChannel? = nil + private var _positionProvider: FlutterPositionProvider? = nil + private var _directionsRenderer: MPDirectionsRenderer? = nil + private var _floorSelector: CustomFloorSelector? = nil + + var delegate: [MapsIndoorsReadyDelegate] = [] + + var liveDataDelegate: LiveDataDelegate? + + var mapControlListenerDelegate: MapControlDelegate? + + var mapView: FlutterMapView? { + set { _mapView = newValue } + get { return _mapView } + } + + var mapControl: MPMapControl? { + set { _mapControl = newValue } + get { return _mapControl } + } + + var isMapControlInitialized: Bool { + get { return _mapControl == nil } + } + + var isMapsIndoorsReady: Bool { + get { return MPMapsIndoors.shared.ready } + } + + func mapsIndoorsReady(error: MPError?) { + delegate.forEach { onReady in + onReady.isReady(error: error) + } + } + + var mapControlMethodChannel: FlutterMethodChannel? { + set { _mapControlMethodChannel = newValue } + get { return _mapControlMethodChannel } + } + + var directionsRendererMethodChannel: FlutterMethodChannel? { + set { _directionsRendererMethodChannel = newValue } + get { return _directionsRendererMethodChannel } + } + + var mapsIndoorsMethodChannel: FlutterMethodChannel? { + set { _mapsIndoorsMethodChannel = newValue } + get { return _mapsIndoorsMethodChannel } + } + + var mapControlFloorSelector: FlutterMethodChannel? { + set { _mapControlFloorSelector = newValue } + get { return _mapControlFloorSelector } + } + + var positionProvider: FlutterPositionProvider? { + set { _positionProvider = newValue } + get { return _positionProvider } + } + + var directionsRenderer: MPDirectionsRenderer? { + set { _directionsRenderer = newValue } + get { return _directionsRenderer } + } + + var floorSelector: MPCustomFloorSelector? { + set { _floorSelector = newValue as? CustomFloorSelector } + get { return _floorSelector } + } +} + +public class CustomFloorSelector: UIView, MPCustomFloorSelector { + public var isAutoFloorChangeEnabled = true + + public var methodChannel: FlutterMethodChannel? = nil + + public var building: MapsIndoors.MPBuilding? + + public var delegate: MapsIndoors.MPFloorSelectorDelegate? + + public var floorIndex: NSNumber? + + init(isAutoFloorChangeEnabled: Bool = true, methodChannel: FlutterMethodChannel) { + super.init(frame: CGRect()) + self.methodChannel = methodChannel + self.isAutoFloorChangeEnabled = isAutoFloorChangeEnabled + self.delegate = FloorSelectorDelegate(floorSelector: self) + } + + required init?(coder: NSCoder) { + super.init(frame: CGRect()) + } + + public func onShow() { + if (building != nil) { + var floors = building?.floors?.values.map {MPFloorCodable(withFloor: $0)} + let jsonData = try! JSONEncoder().encode(floors) + let resultJson = String(data: jsonData, encoding: String.Encoding.utf8) + + methodChannel?.invokeMethod("setList", arguments: resultJson) + } + let map = ["show": true] + methodChannel?.invokeMethod("show", arguments: map) + } + + public func onHide() { + let map = ["show": false] + methodChannel?.invokeMethod("show", arguments: map) + } + + public func onUserPositionFloorChange(floorIndex: Int) { + methodChannel?.invokeMethod("setUserPositionFloor", arguments: floorIndex) + } +} + +public class FloorSelectorDelegate: MPFloorSelectorDelegate { + private var customFloorSelector: CustomFloorSelector + + init(floorSelector: CustomFloorSelector) { + customFloorSelector = floorSelector + } + + public func onFloorIndexChanged(_ floorIndex: NSNumber) { + if (customFloorSelector.isAutoFloorChangeEnabled) { + customFloorSelector.floorIndex = floorIndex + } + } +} + +public class FlutterPositionProvider: MPPositionProvider { + public var latestPosition: MapsIndoors.MPPositionResult? + + public var name = "default" + public var delegate: MapsIndoors.MPPositionProviderDelegate? + public var mapsIndoorsData: MapsIndoorsData? + + public func setLatestPosition(positionResult: MPPositionResult) { + if (latestPosition?.floorIndex != positionResult.floorIndex) { + let floorSelector = mapsIndoorsData?.floorSelector as? CustomFloorSelector + floorSelector?.onUserPositionFloorChange(floorIndex: positionResult.floorIndex) + } + + delegate?.onPositionUpdate(position: positionResult) + latestPosition = positionResult + } +} + +public class MapControlLiveDataDelegate: LiveDataDelegate { + var methodChannel: FlutterMethodChannel? = nil + + init(methodChannel: FlutterMethodChannel) { + self.methodChannel = methodChannel + } + + func dataReceived(liveUpdate: MapsIndoors.MPLiveUpdate) { + let domain = liveUpdate.topic.domainType + let location = MPMapsIndoors.shared.locationWith(locationId: liveUpdate.itemId) + if (location != nil && domain != nil) { + let locationData = try? JSONEncoder().encode(MPLocationCodable(withLocation: location!)) + if (locationData != nil) { + let locationString = String(data: locationData!, encoding: String.Encoding.utf8) + let map = ["location": locationString, "domainType": domain] + methodChannel?.invokeMethod("onLiveLocationUpdate", arguments: map) + } + } + } +} + +public class MapControlDelegate: MPMapControlDelegate { + var methodChannel: FlutterMethodChannel + + var respondToTap: Bool = false + var consumeTap: Bool = false + + var respondToTapIcon: Bool = false + var consumeTapIcon: Bool = false + + var respondToDidChangeFloorIndex: Bool = false + + var respondToDidChangeBuilding: Bool = false + + var respondToDidChangeVenue: Bool = false + + var respondToDidChangeLocation: Bool = false + var consumeChangeLocation: Bool = false + + var respondToDidTapInfoWindow: Bool = false + + init(methodChannel: FlutterMethodChannel) { + self.methodChannel = methodChannel + } + + public func didTap(coordinate: MPPoint) -> Bool { + if (respondToTap) { + var map = [String: Any?]() + let pointData = try? JSONEncoder().encode(coordinate) + if (pointData != nil) { + map["point"] = String(data: pointData!, encoding: String.Encoding.utf8) + } + methodChannel.invokeMethod("onMapClick", arguments: map) + return consumeTap + }else { + return false + } + } + + public func didTapIcon(location: MPLocation) -> Bool { + if (respondToTapIcon) { + methodChannel.invokeMethod("onMarkerClick", arguments: location.locationId) + return consumeTapIcon + }else { + return false + } + } + + public func didChange(floorIndex: Int) -> Bool { + if (respondToDidChangeFloorIndex) { + var map = [String: Any]() + map["floor"] = floorIndex + methodChannel.invokeMethod("onFloorUpdate", arguments: map) + } + return false + } + + public func didChange(selectedVenue: MPVenue?) -> Bool { + if (respondToDidChangeVenue) { + var venueString: String? = nil + if (selectedVenue != nil) { + let venueData = try? JSONEncoder().encode(MPVenueCodable(withVenue: selectedVenue!)) + if (venueData != nil) { + venueString = String(data: venueData!, encoding: String.Encoding.utf8) + } + } + methodChannel.invokeMethod("onVenueFoundAtCameraTarget", arguments: venueString) + } + return false + } + + public func didChange(selectedBuilding: MPBuilding?) -> Bool { + if (respondToDidChangeBuilding) { + var buildingString: String? = nil + if (selectedBuilding != nil) { + let buildingData = try? JSONEncoder().encode(MPBuildingCodable(withBuilding: selectedBuilding!)) + if (buildingData != nil) { + buildingString = String(data: buildingData!, encoding: String.Encoding.utf8) + } + } + methodChannel.invokeMethod("onBuildingFoundAtCameraTarget", arguments: buildingString) + } + return false + } + + public func didChange(selectedLocation: MPLocation?) -> Bool { + if (respondToDidChangeLocation) { + var locationString: String? = nil + if (selectedLocation != nil) { + let locationData = try? JSONEncoder().encode(MPLocationCodable(withLocation: selectedLocation!)) + if (locationData != nil) { + locationString = String(data: locationData!, encoding: String.Encoding.utf8) + } + } + methodChannel.invokeMethod("onLocationSelected", arguments: locationString) + } + return false + } + + public func didTapInfoWindow(location: MPLocation) -> Bool { + if (respondToDidTapInfoWindow) { + methodChannel.invokeMethod("onInfoWindowClick", arguments: location.locationId) + } + return false + } +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Models/CameraPosition.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Models/CameraPosition.swift new file mode 100644 index 0000000..9fe8446 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Models/CameraPosition.swift @@ -0,0 +1,14 @@ +// +// CameraPosition.swift +// mapsindoors_googlemaps_ios +// +// Created by Tim Mikkelsen on 26/07/2023. +// +import MapsIndoors + +struct CameraPosition: Codable { + let zoom: Float + let tilt: Float + let bearing: Float + let target: MPPoint +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Models/CameraUpdate.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Models/CameraUpdate.swift new file mode 100644 index 0000000..a4ea7a2 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Models/CameraUpdate.swift @@ -0,0 +1,20 @@ +// +// CameraUpdate.swift +// mapsindoors_googlemaps_ios +// +// Created by Tim Mikkelsen on 26/07/2023. +// + +import MapsIndoors + +//Internal structs used to decode the camera json above +struct CameraUpdate: Codable { + let position: CameraPosition? + let mode: String + let point: MPPoint? + let bounds: MPGeoBounds? + let padding: Int? + let width: Int? + let height: Int? + let zoom: Float? +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Models/Filter.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Models/Filter.swift new file mode 100644 index 0000000..0d52bfc --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Models/Filter.swift @@ -0,0 +1,96 @@ +// +// Filter.swift +// mapsindoors_ios +// +// Created by Martin Hansen on 20/02/2023. +// + +import Foundation +import MapsIndoors +import MapsIndoorsCore + +public class Filter: Codable { + public func toMPFilter( jsonString: String ) -> (MPFilter) + { + let queryObj: Filter = try! JSONDecoder().decode(Filter.self, from: Data(jsonString.utf8)) + + let result = MPFilter() + + if(queryObj.take != nil) + { + result.take = queryObj.take! + } + if(queryObj.skip != nil) + { + result.skip = queryObj.skip! + } + if(queryObj.floorIndex != nil) + { + result.floorIndex = NSNumber(integerLiteral: queryObj.floorIndex!) + } + if(queryObj.categories != nil) + { + result.categories = queryObj.categories! + } + if(queryObj.locations != nil) + { + result.locations = queryObj.locations! + } + if(queryObj.types != nil) + { + result.types = queryObj.types! + } + if(queryObj.parents != nil) + { + result.parents = queryObj.parents! + } + if(queryObj.mapExtend != nil) + { + let sw = CLLocationCoordinate2D.init(latitude: CLLocationDegrees(floatLiteral: queryObj.mapExtend!.southwest.latitude), + longitude: CLLocationDegrees(floatLiteral: queryObj.mapExtend!.southwest.longitude)) + let ne = CLLocationCoordinate2D.init(latitude: CLLocationDegrees(floatLiteral: queryObj.mapExtend!.northeast.latitude), + longitude: CLLocationDegrees(floatLiteral: queryObj.mapExtend!.northeast.longitude)) + result.bounds = MPGeoBounds(southWest: sw, northEast: ne) + } +// if(queryObj.geometry != nil) +// { +// result.geometry = queryObj.geometry! +// } + if(queryObj.floorIndex != nil) + { + result.floorIndex = NSNumber(integerLiteral: queryObj.floorIndex!) + } + if(queryObj.depth != nil) + { + result.depth = queryObj.depth! + } + if(queryObj.ignoreLocationSearchableStatus != nil) + { + result.ignoreSearchableStatus = queryObj.ignoreLocationSearchableStatus! + } + if(queryObj.ignoreLocationActiveStatus != nil) + { + result.ignoreActiveStatus = queryObj.ignoreLocationActiveStatus! + } + return result + } + + public var take: Int? + public var skip: Int? + public var categories: [String]? + public var locations: [String]? + public var types: [String]? + public var parents: [String]? + + public var mapExtend: geoBounds? + public var geometry: geoBounds? + public var floorIndex: Int? + public var depth: Int? + public var ignoreLocationSearchableStatus: Bool? + public var ignoreLocationActiveStatus: Bool? + + public struct geoBounds : Codable { + var northeast: MPPoint + var southwest: MPPoint + } +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Models/MPIconSizeCodable.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Models/MPIconSizeCodable.swift new file mode 100644 index 0000000..23821e7 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Models/MPIconSizeCodable.swift @@ -0,0 +1,36 @@ +// +// MPIconSizeCodable.swift +// MapsIndoorsCodable +// +// Created by Tim Mikkelsen on 21/03/2023. +// Copyright © 2023 MapsPeople A/S. All rights reserved. +// + +import MapsIndoors + +public class MPIconSizeCodable: Codable { + public var height: Int? + public var width: Int? + + public enum CodingKeys: String, CodingKey { + case height + case width + } + + public init(withCGSize: CGSize) { + height = Int(withCGSize.height) + width = Int(withCGSize.width) + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + height = try container.decode(Int?.self, forKey: .height) + width = try container.decode(Int?.self, forKey: .width) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(height, forKey: .height) + try container.encode(width, forKey: .width) + } +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Models/MPLocationIdCodable.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Models/MPLocationIdCodable.swift new file mode 100644 index 0000000..91febf4 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Models/MPLocationIdCodable.swift @@ -0,0 +1,26 @@ +// +// MPLocationIdCodable.swift +// mapsindoors_googlemaps_ios +// +// Created by Tim Mikkelsen on 21/06/2023. +// + +import MapsIndoors + +public class MPLocationIdCodable: Codable { + public var id: String + + public enum CodingKeys: String, CodingKey { + case id + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + } +} diff --git a/mapsindoors_mapbox_ios/ios/Classes/core/Models/Query.swift b/mapsindoors_mapbox_ios/ios/Classes/core/Models/Query.swift new file mode 100644 index 0000000..6c98eb2 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/Classes/core/Models/Query.swift @@ -0,0 +1,27 @@ +// +// Query.swift +// mapsindoors_ios +// +// Created by Martin Hansen on 20/02/2023. +// + +import Foundation +import MapsIndoors + +public class Query: Codable { + public func toMPQuery( jsonString: String ) -> (MPQuery) + { + let queryObj: Query = try! JSONDecoder().decode(Query.self, from: Data(jsonString.utf8)) + + let result = MPQuery() + if(queryObj.query != nil) + { + result.query = queryObj.query! + } + result.near = MPPoint() + return result + } + public var query: String? + public var near: MPPoint? + public var queryProperties: [String]? +} diff --git a/mapsindoors_mapbox_ios/ios/mapsindoors_mapbox_ios.podspec b/mapsindoors_mapbox_ios/ios/mapsindoors_mapbox_ios.podspec new file mode 100644 index 0000000..98f1a92 --- /dev/null +++ b/mapsindoors_mapbox_ios/ios/mapsindoors_mapbox_ios.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint mapsindoors.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'mapsindoors_mapbox_ios' + s.version = '4.0.0' + s.summary = 'Mapsindoors flutter plugin' + s.homepage = 'http://mapspeople.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Mapspeople' => 'info@mapspeople.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '13.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' + + s.dependency 'MapsIndoorsCodable', "4.2.2" + s.dependency 'MapsIndoorsMapbox', "4.2.2" +end diff --git a/mapsindoors_mapbox_ios/pubspec.yaml b/mapsindoors_mapbox_ios/pubspec.yaml new file mode 100644 index 0000000..1b7774d --- /dev/null +++ b/mapsindoors_mapbox_ios/pubspec.yaml @@ -0,0 +1,29 @@ +name: mapsindoors_mapbox_ios +description: iOS implementation of the mapsindoors plugin using the Mapbox platform +version: 2.0.0 +repository: https://github.com/MapsPeople/MapsIndoors-sdk-flutter +homepage: https://www.mapsindoors.com/ + +environment: + sdk: ">=2.18.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: mapsindoors + platforms: + ios: + pluginClass: MapsIndoorsPlugin + +dependencies: + flutter: + sdk: flutter + mapsindoors_platform_interface: ^2.0.0 +dependency_overrides: + mapsindoors_platform_interface: + path: ../mapsindoors_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 \ No newline at end of file diff --git a/mapsindoors_platform_interface/CHANGELOG.md b/mapsindoors_platform_interface/CHANGELOG.md new file mode 100644 index 0000000..5edb9b9 --- /dev/null +++ b/mapsindoors_platform_interface/CHANGELOG.md @@ -0,0 +1,7 @@ +# 2.0.0 + +* Changed + +# 1.0.0 + +* Release diff --git a/mapsindoors_platform_interface/LICENSE b/mapsindoors_platform_interface/LICENSE new file mode 100644 index 0000000..75757d5 --- /dev/null +++ b/mapsindoors_platform_interface/LICENSE @@ -0,0 +1,11 @@ +Copyright 2023 Mapspeople + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/mapsindoors_platform_interface/README.md b/mapsindoors_platform_interface/README.md new file mode 100644 index 0000000..fd5172a --- /dev/null +++ b/mapsindoors_platform_interface/README.md @@ -0,0 +1,3 @@ +A federated Flutter plugin for integrating with the native MapsIndoors SDK. + +This plugin contains the platform interface for the [mapsindoors](pub.dev/packages/mapsindoors) plugin. diff --git a/mapsindoors_platform_interface/lib/directionsrenderer_platform_interface.dart b/mapsindoors_platform_interface/lib/directionsrenderer_platform_interface.dart new file mode 100644 index 0000000..794cfb9 --- /dev/null +++ b/mapsindoors_platform_interface/lib/directionsrenderer_platform_interface.dart @@ -0,0 +1,40 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +abstract class DirectionsRendererPlatform extends PlatformInterface { + DirectionsRendererPlatform() : super(token: _token); + + static final Object _token = Object(); + + static DirectionsRendererPlatform _instance = MethodChannelDirectionsRenderer(); + + static DirectionsRendererPlatform get instance => _instance; + + static set instance(DirectionsRendererPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future clear(); + + Future getSelectedLegFloorIndex(); + + Future nextLeg(); + + Future previousLeg(); + + Future selectLegIndex(int legIndex); + + Future setAnimatedPolyline(bool animated, bool repeating, int durationMs); + + Future setCameraAnimationDuration(int durationMs); + + Future setCameraViewFitMode(MPCameraViewFitMode mpCameraViewFitMode); + + Future setOnLegSelectedListener(OnLegSelectedListener? onLegSelectedListener); + + Future setPolyLineColors(String foreground, String background); + + Future setRoute(MPRoute? route); + + Future useContentOfNearbyLocations(); +} diff --git a/mapsindoors_platform_interface/lib/directionsservice_platform_interface.dart b/mapsindoors_platform_interface/lib/directionsservice_platform_interface.dart new file mode 100644 index 0000000..6985533 --- /dev/null +++ b/mapsindoors_platform_interface/lib/directionsservice_platform_interface.dart @@ -0,0 +1,30 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +abstract class DirectionsServicePlatform extends PlatformInterface { + DirectionsServicePlatform() : super(token: _token); + + static final Object _token = Object(); + + static DirectionsServicePlatform _instance = MethodChannelDirectionsService(); + + static DirectionsServicePlatform get instance => _instance; + + static set instance(DirectionsServicePlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future create(); + + Future addAvoidWayType(String wayType); + + Future clearWayType(); + + Future setIsDeparture(bool isDeparture); + + Future getRoute(MPPoint originJson, MPPoint destinationJson); + + Future setTravelMode(String travelMode); + + Future setTime(int time); +} diff --git a/mapsindoors_platform_interface/lib/display_rule_platform_interface.dart b/mapsindoors_platform_interface/lib/display_rule_platform_interface.dart new file mode 100644 index 0000000..47d2780 --- /dev/null +++ b/mapsindoors_platform_interface/lib/display_rule_platform_interface.dart @@ -0,0 +1,103 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +abstract class DisplayRulePlatform extends PlatformInterface { + /// Constructs a MapsindoorsPlatform. + DisplayRulePlatform() : super(token: _token); + + static final Object _token = Object(); + + static DisplayRulePlatform _instance = MethodChannelDisplayRule(); + + /// The default instance of [DisplayRulePlatform] to use. + /// + /// Defaults to [MethodChannelDisplayRule]. + static DisplayRulePlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [DisplayRulePlatform] when + /// they register themselves. + static set instance(DisplayRulePlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + // poi + Future isVisible(MPDisplayRuleId id); + Future setVisible(MPDisplayRuleId id, bool visible); + Future isIconVisible(MPDisplayRuleId id); + Future setIconVisible(MPDisplayRuleId id, bool visible); + Future getZoomFrom(MPDisplayRuleId id); + Future setZoomFrom(MPDisplayRuleId id, num from); + Future getZoomTo(MPDisplayRuleId id); + Future setZoomTo(MPDisplayRuleId id, num to); + Future getIconUrl(MPDisplayRuleId id); + Future setIcon(MPDisplayRuleId id, String url); + Future getIconSize(MPDisplayRuleId id); + Future setIconSize(MPDisplayRuleId id, MPIconSize size); + Future isLabelVisible(MPDisplayRuleId id); + Future setLabelVisible(MPDisplayRuleId id, bool visible); + Future getLabel(MPDisplayRuleId id); + Future setLabel(MPDisplayRuleId id, String label); + Future getLabelZoomFrom(MPDisplayRuleId id); + Future setLabelZoomFrom(MPDisplayRuleId id, num from); + Future getLabelZoomTo(MPDisplayRuleId id); + Future setLabelZoomTo(MPDisplayRuleId id, num to); + Future getLabelMaxWidth(MPDisplayRuleId id); + Future setLabelMaxWidth(MPDisplayRuleId id, int max); + // polygon + Future isPolygonVisible(MPDisplayRuleId id); + Future setPolygonVisible(MPDisplayRuleId id, bool visible); + Future getPolygonZoomFrom(MPDisplayRuleId id); + Future setPolygonZoomFrom(MPDisplayRuleId id, num from); + Future getPolygonZoomTo(MPDisplayRuleId id); + Future setPolygonZoomTo(MPDisplayRuleId id, num to); + Future getPolygonStrokeWidth(MPDisplayRuleId id); + Future setPolygonStrokeWidth(MPDisplayRuleId id, num width); + Future getPolygonStrokeColor(MPDisplayRuleId id); + Future setPolygonStrokeColor(MPDisplayRuleId id, String color); + Future getPolygonStrokeOpacity(MPDisplayRuleId id); + Future setPolygonStrokeOpacity(MPDisplayRuleId id, num opacity); + Future getPolygonFillColor(MPDisplayRuleId id); + Future setPolygonFillColor(MPDisplayRuleId id, String color); + Future getPolygonFillOpacity(MPDisplayRuleId id); + Future setPolygonFillOpacity(MPDisplayRuleId id, num opacity); + // wall + Future isWallVisible(MPDisplayRuleId id); + Future setWallVisible(MPDisplayRuleId id, bool visible); + Future getWallColor(MPDisplayRuleId id); + Future setWallColor(MPDisplayRuleId id, String color); + Future getWallHeight(MPDisplayRuleId id); + Future setWallHeight(MPDisplayRuleId id, num height); + Future getWallZoomFrom(MPDisplayRuleId id); + Future setWallZoomFrom(MPDisplayRuleId id, num from); + Future getWallZoomTo(MPDisplayRuleId id); + Future setWallZoomTo(MPDisplayRuleId id, num to); + //extrusion + Future isExtrusionVisible(MPDisplayRuleId id); + Future setExtrusionVisible(MPDisplayRuleId id, bool visible); + Future getExtrusionColor(MPDisplayRuleId id); + Future setExtrusionColor(MPDisplayRuleId id, String color); + Future getExtrusionHeight(MPDisplayRuleId id); + Future setExtrusionHeight(MPDisplayRuleId id, num height); + Future getExtrusionZoomFrom(MPDisplayRuleId id); + Future setExtrusionZoomFrom(MPDisplayRuleId id, num from); + Future getExtrusionZoomTo(MPDisplayRuleId id); + Future setExtrusionZoomTo(MPDisplayRuleId id, num to); + //2d models + Future isModel2DVisible(MPDisplayRuleId id); + Future setModel2DVisible(MPDisplayRuleId id, bool visible); + Future getModel2DZoomFrom(MPDisplayRuleId id); + Future setModel2DZoomFrom(MPDisplayRuleId id, num from); + Future getModel2DZoomTo(MPDisplayRuleId id); + Future setModel2DZoomTo(MPDisplayRuleId id, num to); + Future getModel2DModel(MPDisplayRuleId id); + Future setModel2DModel(MPDisplayRuleId id, String model); + Future getModel2DWidthMeters(MPDisplayRuleId id); + Future setModel2DWidthMeters(MPDisplayRuleId id, num width); + Future getModel2DHeightMeters(MPDisplayRuleId id); + Future setModel2DHeightMeters(MPDisplayRuleId id, num height); + Future getModel2DBearing(MPDisplayRuleId id); + Future setModel2DBearing(MPDisplayRuleId id, num bearing); + + Future reset(MPDisplayRuleId id); +} diff --git a/mapsindoors_platform_interface/lib/mapcontrol_platform_interface.dart b/mapsindoors_platform_interface/lib/mapcontrol_platform_interface.dart new file mode 100644 index 0000000..4ccc3b6 --- /dev/null +++ b/mapsindoors_platform_interface/lib/mapcontrol_platform_interface.dart @@ -0,0 +1,69 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +abstract class MapcontrolPlatform extends PlatformInterface { + /// Constructs a MapsindoorsPlatform. + MapcontrolPlatform() : super(token: _token); + + static final Object _token = Object(); + + static MapcontrolPlatform _instance = MethodChannelMapControl(); + + /// The default instance of [MapcontrolPlatform] to use. + /// + /// Defaults to [MethodChannelMapcontrol]. + static MapcontrolPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [MapcontrolPlatform] when + /// they register themselves. + static set instance(MapcontrolPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getCurrentVenue(); + Future selectVenue(MPVenue venue, bool moveCamera); + Future getCurrentBuilding(); + Future selectBuilding(MPBuilding building, bool moveCamera); + Future clearFilter(); + Future setFilter(MPFilter filter, MPFilterBehavior behavior); + Future setFilterWithLocations(List locations, MPFilterBehavior behavior); + Future hideFloorSelector(bool hide); + Future setMapPadding(int start, int top, int end, int bottom); + Future setMapStyle(MPMapStyle mapstyle); + Future getMapStyle(); + Future getMapViewPaddingBottom(); + Future getMapViewPaddingEnd(); + Future getMapViewPaddingStart(); + Future getMapViewPaddingTop(); + Future showInfoWindowOnClickedLocation(bool show); + Future isFloorSelectorHidden(); + Future deSelectLocation(); + Future selectFloor(int floorIndex); + Future goTo(MPEntity? entity); + Future selectLocation(MPLocation? location, MPSelectionBehavior behavior); + Future selectLocationById(String id, MPSelectionBehavior behavior); + Future getCurrentBuildingFloor(); + Future getCurrentFloorIndex(); + Future getCurrentMapsIndoorsZoom(); + Future setFloorSelector(MPFloorSelectorInterface? floorSelector, bool internal); + MPFloorSelectorInterface? getFloorSelector(); + Future showUserPosition(bool show); + Future isUserPositionShown(); + Future enableLiveData(String domainType, OnLiveLocationUpdateListener? listener); + Future disableLiveData(String donaminType); + void addOnCameraEventListner(MPCameraEventListener listener); + void removeOnCameraEventListner(MPCameraEventListener listener); + void addOnFloorUpdateListener(OnFloorUpdateListener listener); + void removeOnFloorUpdateListener(OnFloorUpdateListener listener); + void setOnCurrentBuildingChangedListener(OnBuildingFoundAtCameraTargetListener? listener); + void setOnCurrentVenueChangedListener(OnVenueFoundAtCameraTargetListener? listener); + void setOnLocationSelectedListener(OnLocationSelectedListener? listener, bool? consumeEvent); + void setOnMapClickListener(OnMapClickListener? listener, bool? consumeEvent); + void setOnMarkerClickListener(OnMarkerClickListener? listener, bool? consumeEvent); + void setOnMarkerInfoWindowClickListener(OnMarkerInfoWindowClickListener? listener); + void setOnMapControlReadyListener(OnMapReadyListener listener); + Future animateCamera(MPCameraUpdate update, [int? duration]); + Future moveCamera(MPCameraUpdate update); + Future currentCameraPosition(); +} diff --git a/mapsindoors_platform_interface/lib/mapsindoors_platform_interface.dart b/mapsindoors_platform_interface/lib/mapsindoors_platform_interface.dart new file mode 100644 index 0000000..4451376 --- /dev/null +++ b/mapsindoors_platform_interface/lib/mapsindoors_platform_interface.dart @@ -0,0 +1,63 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +abstract class MapsindoorsPlatform extends PlatformInterface { + /// Constructs a MapsindoorsPlatform. + MapsindoorsPlatform() : super(token: _token); + + static final Object _token = Object(); + + static MapsindoorsPlatform _instance = MethodChannelMapsindoors(); + + /// The default instance of [MapsindoorsPlatform] to use. + /// + /// Defaults to [MethodChannelMapsindoors]. + static MapsindoorsPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [MapsindoorsPlatform] when + /// they register themselves. + static set instance(MapsindoorsPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + //region MapsIndoors + Future load(String key); + void addOnMapsIndoorsReadyListener(OnMapsIndoorsReadyListener listener); + Future checkOfflineDataAvailability(); + Future destroy(); + Future disableEventLogging(bool disable); + Future getAPIKey(); + Future?> getAvailableLanguages(); + Future getBuildings(); + Future getCategories(); + Future getDefaultLanguage(); + Future getLanguage(); + Future getLocationById(String id); + Future?> getLocations(); + Future?> getLocationsByQuery(MPQuery? query, MPFilter? filter); + Future?> getLocationsByExternalIds(List ids); + Future?> getMapStyles(); + MPPositionProviderInterface? getPositionProvider(); + void setPositionProvider(MPPositionProviderInterface? provider); + Future getSolution(); + Future getDefaultVenue(); + Future getVenues(); + Future isAPIKeyValid(); + Future isInitialized(); + Future isReady(); + void removeOnMapsIndoorsReadyListener(OnMapsIndoorsReadyListener listener); + Future reverseGeoCode(MPPoint point); + Future setLanguage(String language); + Future synchronizeContent(); + Future getUserRoles(); + Future?> getAppliedUserRoles(); + Future applyUserRoles(List userRoles); + //endregion + + //region Display Rules + Future locationDisplayRuleExists(MPLocationId location); + Future displayRuleNameExists(String name); + MPDisplayRule createDisplayRuleWithName(String name); + //endregion +} diff --git a/mapsindoors_platform_interface/lib/models/collections/mp_building_collection.dart b/mapsindoors_platform_interface/lib/models/collections/mp_building_collection.dart new file mode 100644 index 0000000..fb0b63d --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/collections/mp_building_collection.dart @@ -0,0 +1,23 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A collection of building objects fetched from MapsIndoors. Uses [MPBuilding.id] as key +class MPBuildingCollection extends MPCollection { + late Map _adminIdMap; + + /// Attempts to build a [MPBuildingCollection] from a JSON object, this method will decode the object if needed + static MPBuildingCollection? fromJson(json) => json != null && json != "null" ? MPBuildingCollection._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPBuildingCollection._fromJson(data) : super._() { + _adminIdMap = new Map(); + List buildings = convertMIList(data, (p0) => MPBuilding.fromJson(p0)); + buildings.forEach((building) { + _map[building.id.value] = building; + _adminIdMap[building.administrativeId] = building; + }); + } + + /// Fetch a building by its administrative [id] + MPBuilding? getBuildingByAdminId(String id) { + return this._adminIdMap[id]; + } +} diff --git a/mapsindoors_platform_interface/lib/models/collections/mp_category_collection.dart b/mapsindoors_platform_interface/lib/models/collections/mp_category_collection.dart new file mode 100644 index 0000000..9f00c17 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/collections/mp_category_collection.dart @@ -0,0 +1,22 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A collection of category objects fetched from MapsIndoors. Uses [MPCategory.key] as key +class MPCategoryCollection extends MPCollection { + /// Attempts to build a [MPCategoryCollection] from a JSON object, this method will decode the object if needed + static MPCategoryCollection? fromJson(json) => json != null && json != "null" ? MPCategoryCollection._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPCategoryCollection._fromJson(data) : super._() { + List categories = convertMIList(data, (p0) => MPCategory.fromJson(p0)); + categories.forEach((category) => _map[category.key] = category); + } + + /// Fetch the [MPCategory.value] of a [MPCategory] directly from the collection + String? getValue(String key) { + return this.getById(key)?.value; + } + + /// Fetch the [MPCategory.fields] of a [MPCategory] directly from the collection + Map? getFields(String key) { + return this.getById(key)?.fields; + } +} diff --git a/mapsindoors_platform_interface/lib/models/collections/mp_collection.dart b/mapsindoors_platform_interface/lib/models/collections/mp_collection.dart new file mode 100644 index 0000000..cfcff10 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/collections/mp_collection.dart @@ -0,0 +1,23 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A collection that offers simple getter logic +class MPCollection { + late Map _map; + + MPCollection._() { + this._map = new Map(); + } + + /// Fetch all elements from this collection as a list + List getAll() { + return List.from(_map.values); + } + + /// Fetch an element by its identifier + T? getById(String id) { + return _map[id]; + } + + /// Get the number of elements in this collection + int get size => _map.length; +} diff --git a/mapsindoors_platform_interface/lib/models/collections/mp_user_role_collection.dart b/mapsindoors_platform_interface/lib/models/collections/mp_user_role_collection.dart new file mode 100644 index 0000000..1a3f60e --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/collections/mp_user_role_collection.dart @@ -0,0 +1,12 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A collection of user role objects fetched from MapsIndoors. Uses [MPUserRole.id] as key +class MPUserRoleCollection extends MPCollection { + /// Attempts to build a [MPUserRoleCollection] from a JSON object, this method will decode the object if needed + static MPUserRoleCollection? fromJson(json) => json != null && json != "null" ? MPUserRoleCollection._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPUserRoleCollection._fromJson(data) : super._() { + List userroles = convertMIList(data, (p0) => MPUserRole.fromJson(p0)); + userroles.forEach((role) => _map[role.id] = role); + } +} diff --git a/mapsindoors_platform_interface/lib/models/collections/mp_venue_collection.dart b/mapsindoors_platform_interface/lib/models/collections/mp_venue_collection.dart new file mode 100644 index 0000000..e04bceb --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/collections/mp_venue_collection.dart @@ -0,0 +1,23 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A collection of venue objects fetched from MapsIndoors. Uses [MPVenue.id] as key +class MPVenueCollection extends MPCollection { + late Map _adminIdMap; + + /// Attempts to build a [MPVenueCollection] from a JSON object, this method will decode the object if needed + static MPVenueCollection? fromJson(json) => json != null && json != "null" ? MPVenueCollection._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPVenueCollection._fromJson(data) : super._() { + _adminIdMap = new Map(); + List venues = convertMIList(data, (p0) => MPVenue.fromJson(p0)); + venues.forEach((venue) { + _map[venue.id.value] = venue; + _adminIdMap[venue.administrativeId] = venue; + }); + } + + /// Fetch a venue by its administrative [id] + MPVenue? getVenueByAdminId(String id) { + return this._adminIdMap[id]; + } +} diff --git a/mapsindoors_platform_interface/lib/models/entities/mp_building.dart b/mapsindoors_platform_interface/lib/models/entities/mp_building.dart new file mode 100644 index 0000000..e5c3db0 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/entities/mp_building.dart @@ -0,0 +1,124 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A unique identifier for buildings +@immutable +class MPBuildingId extends DynamicObjectId { + const MPBuildingId(String value) : super(value); +} + +/// A MapsIndoors geographical entity. A [MPBuilding] is contained within a [MPVenue] +/// and contains a number of [MPFloor]s. +class MPBuilding extends MPEntity { + final MPBuildingId id; + final String administrativeId; + final String? externalId; + final String venueId; + final MPPoint? _anchor; + final MPBuildingInfo? _buildingInfo; + final MPPolygon geometry; + final Map? _floors; + final int? _defaultFloor; + final String? _address; + + /// Attempts to build a [MPBuilding] from a JSON object, this method will decode the object if needed + static MPBuilding? fromJson(json) => json != null && json != "null" ? MPBuilding._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPBuilding._fromJson(data) + : id = MPBuildingId(data["id"]), + administrativeId = data["administrativeId"], + externalId = data["externalId"], + venueId = data["venueId"], + _anchor = MPPoint.fromJson(data["anchor"]), + _buildingInfo = MPBuildingInfo.fromJson(data["buildingInfo"]), + geometry = MPPolygon.fromJson(data["geometry"])!, + _floors = (data["floors"] as Map).map((key, value) => MapEntry(int.parse(key), MPFloor.fromJson(value)!)), + _defaultFloor = data["defaultFloor"], + _address = data["address"] { + if (_floors != null) { + for (var floor in _floors!.entries) { + floor.value.buildingId = id.value; + } + } + } + + /// Get the building's [bounds] + @override + MPBounds? get bounds => geometry.bounds; + + /// Inherited from [MPEntity], a building's [geometry] is never a [MPPoint] + @override + bool get isPoint => false; + + /// Get the [position] of the building, this will correspond to the building's anchor point + @override + MPPoint get position => _anchor!; + + /// Get the building's [id] value + String get buildingId => id.value; + + /// Get the building's [name] + String get name => _buildingInfo!.name; + + /// Get the building's [address] + String get address => _address ?? ""; + + /// Get a list of [aliases] for the building + List get aliases => _buildingInfo!.aliases!; + + /// Get the number of [MPFloor]s in the building + int get floorCount => _floors!.length; + + /// Get a list of the floors in the building + List get floors { + List list = List.from(_floors!.values); + list.sort((a, b) => a.compareTo(b)); + return list; + } + + /// Get the default floor index + int get initialFloorIndex => _defaultFloor ?? floors.first._floorIndex ?? 0; + + /// Check whether the building contains a floor with the [floorIndex] + bool hasFloorIndex(int floorIndex) => _floors!.containsKey(floorIndex); + + /// Fetch a floor by providing its [floorIndex] + MPFloor? getFloorByIndex(int floorIndex) => _floors?[floorIndex]; + + /// Check whether the building's [geometry] [contains] the [point] + Future contains(MPPoint point) async => await geometry.contains(point); + + /// Fetch a custom field saved on the building + MPDataField? getField(String? key) => _buildingInfo?.fields[key]; + + /// Converts the [MPBuilding] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + return { + "id": id.value, + "administrativeId": administrativeId, + "externalId": externalId, + "venueId": venueId, + "anchor": _anchor, + "buildingInfo": _buildingInfo, + "geometry": geometry, + "floors": _floors?.map((key, value) => MapEntry(key.toString(), value.toJson())), + "defaultFloor": _defaultFloor, + "address": _address, + "boundingBox": bounds?.toJson() + }; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is MPBuilding && id == other.id; + } + + @override + int get hashCode => id.hashCode; +} diff --git a/mapsindoors_platform_interface/lib/models/entities/mp_entity.dart b/mapsindoors_platform_interface/lib/models/entities/mp_entity.dart new file mode 100644 index 0000000..89ca5f3 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/entities/mp_entity.dart @@ -0,0 +1,9 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// An interface describing trivial properties of a MapsIndoors geographical entities +abstract class MPEntity extends MapsIndoorsObject implements DynamicObject { + const MPEntity(); + MPPoint get position; + MPBounds? get bounds; + bool get isPoint; +} diff --git a/mapsindoors_platform_interface/lib/models/entities/mp_floor.dart b/mapsindoors_platform_interface/lib/models/entities/mp_floor.dart new file mode 100644 index 0000000..e5f9b0c --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/entities/mp_floor.dart @@ -0,0 +1,86 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A unique identifier for floors +@immutable +class MPFloorId extends DynamicObjectId { + const MPFloorId(String value) : super(value); +} + +/// A MapsIndoors geographical entity. A [MPFloor] is contained within a [MPBuilding]. +class MPFloor extends MPEntity implements Comparable { + /// The default floor index is 0, as that is the index of ground floors and any point outside a [MPBuilding] + static const defaultGroundFloorIndex = 0; + final MPFloorId _id; + final String _displayName; + final MPMultiPolygon? _geometry; + final List? _aliases; + + /// The building id of the floor + String? buildingId; + final int? _floorIndex; + + /// Attempts to build a [MPFloor] from a JSON object, this method will decode the object if needed + static MPFloor? fromJson(json) => json != null && json != "null" ? MPFloor._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPFloor._fromJson(data) + : _id = MPFloorId(data["id"]), + _displayName = data["name"], + _floorIndex = data["floorIndex"], + _geometry = MPMultiPolygon.fromJson(data["geometry"]), + _aliases = data["aliases"] != null ? convertJsonArray(data["aliases"]) : null; + + /// Get the floor's [geometry] + MPMultiPolygon get geometry => _geometry!; + + /// Get the floor's [id] + @override + MPFloorId get id => _id; + + /// Get the floor's [id] value + String get floorId => _id.value; + + /// Get the floor's [floorIndex] + int get floorIndex => _floorIndex ?? defaultGroundFloorIndex; + + /// Get the floor's [displayName] + String get displayName => _displayName; + + /// Get the floor's [bounds] + @override + MPBounds? get bounds => _geometry?.bounds; + + /// Get a list of [aliases] for the floor + List? get aliases => _aliases; + + /// Inherited from [MPEntity], a floor's [geometry] is never a [MPPoint] + @override + bool get isPoint => false; + + /// Get the floor's [position], this is usually the center of the floor's [geometry] + @override + MPPoint get position => _geometry!.bounds.center; + + /// Converts the [MPFloor] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() => {"id": _id.value, "name": _displayName, "geometry": _geometry, "floorIndex": _floorIndex, "aliases": _aliases, "entityBounds": bounds, "entityIsPoint": false}; + + /// Compares two [MPFloor] objects based on their [floorIndex] + @override + int compareTo(other) { + return _floorIndex != other._floorIndex ? (_floorIndex! < other._floorIndex! ? -1 : 1) : 0; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is MPFloor && id == other.id; + } + + @override + int get hashCode => id.hashCode; +} diff --git a/mapsindoors_platform_interface/lib/models/entities/mp_location.dart b/mapsindoors_platform_interface/lib/models/entities/mp_location.dart new file mode 100644 index 0000000..5be5232 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/entities/mp_location.dart @@ -0,0 +1,249 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A unique identifier for locations +@immutable +class MPLocationId extends DynamicObjectId { + const MPLocationId(String value) : super(value); +} + +/// A MapsIndoors geographical entity. A [MPLocation] can exist anywhere, +/// but it is usually only used inside [MPVenue]s and [MPBuilding]s. +class MPLocation extends MPEntity { + final MPLocationId id; + final MPGeometry? _geometry; + final List? _restrictions; + final MPPropertyData? _properties; + final MPPoint? _point; + + const MPLocation._({required this.id, MPGeometry? geometry, List? restrictions, MPPropertyData? properties, MPPoint? point}) + : _geometry = geometry, + _restrictions = restrictions, + _properties = properties, + _point = point; + + /// Attempts to build a [MPLocation] from a JSON object, this method will decode the object if needed + static MPLocation? fromJson(json) => json != null && json != "null" ? MPLocation._fromJson(json is String ? jsonDecode(json) : json) : null; + + static MPLocation _fromJson(data) { + final id = MPLocationId(data["id"]); + var geometry; + if (data["geometry"] != null) { + final geo = data["geometry"]; + + if (Platform.isIOS) { + switch (data["geometryType"]) { + case MPGeometry.point: + geometry = MPPoint.fromJson(geo); + break; + case MPGeometry.polygon: + geometry = MPPolygon.fromJson(geo); + break; + case MPGeometry.multiPolygon: + geometry = MPMultiPolygon.fromJson(geo); + break; + } + } else { + switch (geo["type"]) { + case MPGeometry.point: + geometry = MPPoint.fromJson(geo); + break; + case MPGeometry.polygon: + geometry = MPPolygon.fromJson(geo); + break; + case MPGeometry.multiPolygon: + geometry = MPMultiPolygon.fromJson(geo); + break; + } + } + } + var point; + if (Platform.isIOS) { + if (data["point"] != null) { + point = MPPoint.fromJson(data["point"]); + } + //TODO: fix bounds not being available on iOS + } + var restrictions; + if (data["restrictions"] != null) { + restrictions = convertJsonArray(data["restrictions"]); + } + final properties = MPPropertyData.fromJson(data["properties"]); + + return MPLocation._(id: id, geometry: geometry, restrictions: restrictions, properties: properties, point: point); + } + + /// Get the location's [bounds] + @override + MPBounds get bounds { + return _calcBounds(); + } + + MPBounds _calcBounds() { + switch (_geometry!.runtimeType) { + case MPPolygon: + return (_geometry as MPPolygon).bounds; + case MPMultiPolygon: + return (_geometry as MPMultiPolygon).bounds; + default: + return MPBounds(northeast: position, southwest: position); + } + } + + /// Inherited from [MPEntity], checks whether the location's [geometry] is a [MPPoint] + @override + bool get isPoint => _geometry is MPPoint; + + /// Get the location's [position], this is usually the location's anchor point + @override + MPPoint get position => _properties?.anchor ?? MPPoint.withCoordinates(latitude: 0, longitude: 0); + + /// Converts the [MPLocation] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() => {"id": id.value}; + + /// Get the location's [id] value + String get locationId => id.value; + + /// Get the location's [point] + MPPoint get point => _point ?? _calcPoint(); + + MPPoint _calcPoint() { + if (_geometry == null) { + return MPPoint(); + } else { + final pt = MPPoint.copy(_geometry!.position); + pt.floorIndex = floorIndex; + return pt; + } + } + + // Get the location's [geometry] + // This is currently unused + //MPGeometry get geometry => _geometry!; + + /// Get the location's [name] + String get name => _properties!.name; + + /// Get a list of [aliases] for the location + List? get aliases => _properties?.aliases; + + /// Get a list of [categories] the location is contained in + List? get categories { + final categoryMap = _properties?.categories?.entries; + return categoryMap != null ? List.from(categoryMap) : null; + } + + /// Get the location's [floorIndex] + int get floorIndex => _properties!.floorIndex!; + + /// Get the name of the [MPFloor] the location is on + String? get floorName => _properties?.floorName; + + /// Get the name of the [MPBuilding] the location is in + String? get buildingName => _properties?.building; + + /// Get the name of the [MPVenue] the location is in + String? get venueName => _properties?.venue; + + /// Get the name of the location's type + String? get typeName => _properties?.type; + + /// Get the location's [description] + String? get description => _properties?.description; + + /// Get the location's external id + String get externalId => _properties!.externalId!; + + /// Get the time (epoch) the location is active from + int? get activeFrom => _properties?.activeFrom; + + /// Get the time (epoch) the location is active to + int? get activeTo => _properties?.activeTo; + + /// Get the URL for the location's image + String? get imageUrl => _properties?.imageUrl; + + /// Get the location's [restrictions] + List? get restrictions => _restrictions; + + /// Chech whether this location is bookable, this only checks if the location is allowed to be booked + bool get isBookable => _properties?.bookable ?? false; + + /// Gets the location's [MPLocationType] [baseType] + MPLocationType get baseType { + switch (_properties?.type) { + case "area": + return MPLocationType.area; + case "venue": + return MPLocationType.venue; + case "building": + return MPLocationType.building; + case "room": + return MPLocationType.room; + case "floor": + return MPLocationType.floor; + case "asset": + return MPLocationType.asset; + case "poi": + default: + return MPLocationType.poi; + } + } + + /// Fetch a property from the location with a [MPLocationPropertyNames] [key] + Object? getProperty(MPLocationPropertyNames key) { + switch (key) { + case MPLocationPropertyNames.name: + return _properties?.name; + case MPLocationPropertyNames.aliases: + return _properties?.aliases; + case MPLocationPropertyNames.categories: + return _properties?.categories; + case MPLocationPropertyNames.floor: + return _properties?.floorIndex; + case MPLocationPropertyNames.floorName: + return _properties?.floorName; + case MPLocationPropertyNames.building: + return _properties?.building; + case MPLocationPropertyNames.venue: + return _properties?.venue; + case MPLocationPropertyNames.type: + return _properties?.type; + case MPLocationPropertyNames.description: + return _properties?.description; + case MPLocationPropertyNames.roomId: + case MPLocationPropertyNames.externalId: + return _properties?.externalId; + case MPLocationPropertyNames.activeFrom: + return _properties?.activeFrom; + case MPLocationPropertyNames.activeTo: + return _properties?.activeTo; + case MPLocationPropertyNames.contact: + return _properties?.contact; + case MPLocationPropertyNames.fields: + return _properties?.fields; + case MPLocationPropertyNames.imageURL: + return _properties?.imageUrl; + case MPLocationPropertyNames.locationType: + return _properties?.locationType; + case MPLocationPropertyNames.anchor: + return _properties?.anchor; + case MPLocationPropertyNames.bookable: + return _properties?.bookable; + } + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is MPLocation && id == other.id; + } + + @override + int get hashCode => id.hashCode; +} diff --git a/mapsindoors_platform_interface/lib/models/entities/mp_venue.dart b/mapsindoors_platform_interface/lib/models/entities/mp_venue.dart new file mode 100644 index 0000000..c546961 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/entities/mp_venue.dart @@ -0,0 +1,123 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A unique identifier for venues +@immutable +class MPVenueId extends DynamicObjectId { + const MPVenueId(String value) : super(value); +} + +/// A MapsIndoors geographical entity. A [MPVenue] can exist anywhere, +/// and it can contain a number of [MPBuilding]s and [MPLocation]s. +class MPVenue extends MPEntity { + final MPVenueId id; + final String? graphId; + final String administrativeId; + final String tilesUrl; + final List? _mapStyles; + final MPPolygon geometry; + final int defaultFloor; + final MPVenueInfo venueInfo; + final MPPoint? _anchor; + final List? _entryPoints; + final String? externalId; + + /// Attempts to build a [MPVenue] from a JSON object, this method will decode the object if needed + static MPVenue? fromJson(json) => json != null && json != "null" ? MPVenue._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPVenue._fromJson(data) + : id = MPVenueId(data["id"]), + graphId = data["graphId"], + administrativeId = data["name"], + tilesUrl = data["tilesUrl"], + geometry = MPPolygon.fromJson(data["geometry"])!, + defaultFloor = data["defaultFloor"], + venueInfo = MPVenueInfo.fromJson(data["venueInfo"])!, + _anchor = MPPoint.fromJson(data["anchor"]), + externalId = data["externalId"], + _mapStyles = convertMIList(data["styles"], (p0) => MPMapStyle.fromJson(p0)), + _entryPoints = convertMIList(data["entryPoints"], ((p0) => MPPoint.fromJson(p0))); + + /// Converts the [MPVenue] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + return { + "id": id.value, + "name": administrativeId, + "graphId": graphId, + "administrativeId": administrativeId, + "tilesUrl": tilesUrl, + "styles": _mapStyles, + "geometry": geometry, + "defaultFloor": defaultFloor, + "venueInfo": venueInfo, + "anchor": _anchor, + "entryPoints": _entryPoints, + "externalId": externalId, + }; + } + + /// Get the [position] of the venue, this will correspond to the venue's anchor point + @override + MPPoint get position => _anchor!; + + /// Get the venue's [bounds] + @override + MPBounds? get bounds => geometry.bounds; + + /// Get the venue's [name] + String? get name => venueInfo._name; + + /// Get the venue's map styles + List get mapStyles => _mapStyles ?? []; + + /// Get the venue's default mapstyle + MPMapStyle? get defaultMapStyle => _mapStyles?[0]; + + /// Get a list of entry points for the venue + List get entryPoints => _entryPoints ?? []; + + /// Fetch a field from the venue + MPDataField? getField(String? key) => venueInfo._fields?[key]; + + /// Inherited from [MPEntity], a venue's [geometry] is never a [MPPoint] + @override + bool get isPoint => false; + + /// Check whether a given [mapstyle] is valid for the venue + bool isMapStyleValid(MPMapStyle mapstyle) { + if (_mapStyles == null) { + return false; + } + for (var style in _mapStyles!) { + if (style == mapstyle) { + return true; + } + } + return false; + } + + /// Check whether the venue has a valid routing graph + Future get hasGraph async { + if (graphId == null) { + return false; + } + return await UtilPlatform.instance.venueHasGraph(id.value); + } + + /// Check whether the venue contains a [point] + Future contains(MPPoint point) async => await UtilPlatform.instance.geometryIsInside(geometry, point); + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is MPVenue && id == other.id; + } + + @override + int get hashCode => id.hashCode; +} diff --git a/mapsindoors_platform_interface/lib/models/enums/live_data_domain_types.dart b/mapsindoors_platform_interface/lib/models/enums/live_data_domain_types.dart new file mode 100644 index 0000000..7fd6fc7 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/enums/live_data_domain_types.dart @@ -0,0 +1,20 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Domain types for livedata subscriptions +/// +/// Used to set which livedata domains are active on MapControl.enableLiveData(String) +enum LiveDataDomainTypes { + occupancy("occupancy"), + availability("availability"), + position("position"), + count("count"), + temperature("temperature"), + co2("co2"), + humidity("humidity"), + any("any"); + + final String name; + const LiveDataDomainTypes(this.name); + + dynamic toJson() => name; +} diff --git a/mapsindoors_platform_interface/lib/models/enums/mp_camera_event.dart b/mapsindoors_platform_interface/lib/models/enums/mp_camera_event.dart new file mode 100644 index 0000000..3fb66f0 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/enums/mp_camera_event.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Possible events that a [MPCameraEventListener] can recieve +enum MPCameraEvent { finished, cancelled, moveStartedApiAnimation, moveStartedDeveloperAnimation, moveStartedGesture, onMove, moveCancelled, idle } diff --git a/mapsindoors_platform_interface/lib/models/enums/mp_camera_view_fit_mode.dart b/mapsindoors_platform_interface/lib/models/enums/mp_camera_view_fit_mode.dart new file mode 100644 index 0000000..84d8148 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/enums/mp_camera_view_fit_mode.dart @@ -0,0 +1,11 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Different ways the camera can fit a route inside the screen +/// +/// [northAligned] : The camera will point north +/// +/// [firstStepAligned] : The camera will be aligned with the direction of the first step +/// +/// [startToEndAligned] : The camera will point in the same direction as a line that +/// goes directly from the origin to the destination +enum MPCameraViewFitMode { northAligned, firstStepAligned, startToEndAligned } diff --git a/mapsindoors_platform_interface/lib/models/enums/mp_collision_handling.dart b/mapsindoors_platform_interface/lib/models/enums/mp_collision_handling.dart new file mode 100644 index 0000000..081147d --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/enums/mp_collision_handling.dart @@ -0,0 +1,31 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Describes how the SDK should handle markers and labels overlapping with other markers and labels +enum MPCollisionHandling { + allowOverlap(0), + removeLabelFirst(1), + removeIconFirst(2), + removeIconAndLabel(3); + + /// The integer representation of this handling preset + final int value; + const MPCollisionHandling(this.value); + + dynamic toJson() => value; + + /// Parses the integer value of the enum + static MPCollisionHandling fromValue(int value) { + switch (value) { + case 0: + return MPCollisionHandling.allowOverlap; + case 1: + return MPCollisionHandling.removeLabelFirst; + case 2: + return MPCollisionHandling.removeIconFirst; + case 3: + return MPCollisionHandling.removeIconAndLabel; + default: + throw ArgumentError("A MPCollisionHandling scheme does not exist for the value $value"); + } + } +} diff --git a/mapsindoors_platform_interface/lib/models/enums/mp_highway.dart b/mapsindoors_platform_interface/lib/models/enums/mp_highway.dart new file mode 100644 index 0000000..3c7973a --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/enums/mp_highway.dart @@ -0,0 +1,22 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// OSM highways used by the MapsIndoors SDK in addition to MapsIndoors specific highways +enum MPHighway { + unclassified("unclassified"), + footway("footway"), + residential("residential"), + service("service"), + ramp("ramp"), + steps("steps"), + escalator("escalator"), + travelator("travelator"), + elevator("elevator"), + wheelChairRamp("wheelchairramp"), + wheelChairLift("wheelchairlift"), + ladder("ladder"); + + final String name; + const MPHighway(this.name); + + dynamic toJson() => name; +} diff --git a/mapsindoors_platform_interface/lib/models/enums/mp_location_propery_names.dart b/mapsindoors_platform_interface/lib/models/enums/mp_location_propery_names.dart new file mode 100644 index 0000000..55a6511 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/enums/mp_location_propery_names.dart @@ -0,0 +1,30 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Names of properties that can be set on a [MPLocation] +enum MPLocationPropertyNames { + name("name"), + aliases("aliases"), + categories("categories"), + floor("floor"), + floorName("floorName"), + building("building"), + venue("venue"), + //DISPLAY_RULE, + type("type"), + description("description"), + roomId("roomId"), + externalId("externalId"), + activeFrom("activeFrom"), + activeTo("activeTo"), + contact("contact"), + fields("fields"), + imageURL("imageURL"), + locationType("locationType"), + anchor("anchor"), + bookable("bookable"); + + final String propertyName; + const MPLocationPropertyNames(this.propertyName); + + dynamic toJson() => name; +} diff --git a/mapsindoors_platform_interface/lib/models/enums/mp_location_type.dart b/mapsindoors_platform_interface/lib/models/enums/mp_location_type.dart new file mode 100644 index 0000000..04358f2 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/enums/mp_location_type.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Base type of a [MPLocation] +enum MPLocationType { room, poi, building, venue, area, asset, floor } diff --git a/mapsindoors_platform_interface/lib/models/enums/mp_solution_display_rule.dart b/mapsindoors_platform_interface/lib/models/enums/mp_solution_display_rule.dart new file mode 100644 index 0000000..a941070 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/enums/mp_solution_display_rule.dart @@ -0,0 +1,13 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Special [MPDisplayRule]s that govern specific issues +enum MPSolutionDisplayRuleEnum { + buildingOutline("buildingOutline"), + selectionHighlight("selectionHighlight"), + positionIndicator("positionIndicator"); + + final String name; + const MPSolutionDisplayRuleEnum(this.name); + + dynamic toJson() => name; +} diff --git a/mapsindoors_platform_interface/lib/models/geometries/mp_bounds.dart b/mapsindoors_platform_interface/lib/models/geometries/mp_bounds.dart new file mode 100644 index 0000000..8529b0b --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/geometries/mp_bounds.dart @@ -0,0 +1,76 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Describes the geographical bounds as a rectangle spanning from its northeastern point to its southwestern point +class MPBounds extends MapsIndoorsObject { + final MPPoint northeast; + final MPPoint southwest; + static MPBoundsBuilder builder() => MPBoundsBuilder(); + + /// Build a [MPBounds] from a pair of coordinates + const MPBounds({required this.northeast, required this.southwest}); + + /// Attempts to build a [MPBounds] from a JSON object, this method will decode the object if needed + static MPBounds? fromJson(json) => json != null && json != "null" ? MPBounds._fromJson(json is String ? jsonDecode(json) : json) : null; + + static MPBounds _fromJson(data) { + final ne = MPPoint.fromJson(data["northeast"])!; + final sw = MPPoint.fromJson(data["southwest"])!; + return MPBounds(northeast: ne, southwest: sw); + } + + /// Calculates the center of the bounds + MPPoint get center { + var latCenter = (northeast.latitude + southwest.latitude) / 2.0; + var lngCenter = (northeast.longitude + southwest.longitude) / 2.0; + return MPPoint.withCoordinates(latitude: latCenter, longitude: lngCenter); + } + + /// Check whether a [point] is inside the bounds + bool contains(MPPoint point) => ((point.latitude <= northeast.latitude && point.latitude >= southwest.latitude) && (point.longitude <= northeast.longitude && point.longitude >= southwest.longitude)); + + /// Converts the [MPBounds] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + if (Platform.isIOS) { + return { + "northeast": [northeast.longitude, northeast.latitude], + "southwest": [southwest.longitude, southwest.latitude] + }; + } else { + return {"northeast": northeast.toJson(), "southwest": southwest.toJson()}; + } + } + + @override + String toString() => "NE: $northeast, SW: $southwest"; +} + +/// Build a [MPBounds] from a collection of [MPPoint]s, +/// note that at least one point has to be supplied before the bounds can be built +class MPBoundsBuilder { + num _north = -double.maxFinite; + num _south = double.maxFinite; + num _east = -double.maxFinite; + num _west = double.maxFinite; + + MPBoundsBuilder(); + + /// Include a [point] in the [MPBounds], if the [point] is outside the bounds, the bounds will be expanded to include the point + MPBoundsBuilder include(MPPoint point) { + _north = max(_north, point.latitude); + _south = min(_south, point.latitude); + _east = max(_east, point.longitude); + _west = min(_west, point.longitude); + return this; + } + + /// Build a [MPBounds] object + /// + /// Throws FormatException if no [MPPoint]s were added in with [include] + MPBounds build() { + if (_north == -double.maxFinite) { + throw const FormatException("Cannot build bounds with no coordinates"); + } + return MPBounds(northeast: MPPoint.withCoordinates(latitude: _north, longitude: _east), southwest: MPPoint.withCoordinates(latitude: _south, longitude: _west)); + } +} diff --git a/mapsindoors_platform_interface/lib/models/geometries/mp_geometry.dart b/mapsindoors_platform_interface/lib/models/geometries/mp_geometry.dart new file mode 100644 index 0000000..25bc878 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/geometries/mp_geometry.dart @@ -0,0 +1,16 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Superclass of all MapsIndoors geometry classes +abstract class MPGeometry extends MapsIndoorsObject { + static const _lat = 1; + static const _lng = 0; + static const point = "Point"; + static const polygon = "Polygon"; + static const multiPolygon = "MultiPolygon"; + String get type; + Future get area; + MPPoint get position; + + /// Check whether a [point] is contained within the geometry + Future contains(MPPoint point) async => await UtilPlatform.instance.geometryIsInside(this, point); +} diff --git a/mapsindoors_platform_interface/lib/models/geometries/mp_multi_polygon.dart b/mapsindoors_platform_interface/lib/models/geometries/mp_multi_polygon.dart new file mode 100644 index 0000000..37dbdaf --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/geometries/mp_multi_polygon.dart @@ -0,0 +1,101 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// [MPMultiPolygon] is a collection of [MPPolygon]s that combine +/// to form a single geographical area with multiple bodies +class MPMultiPolygon extends MPGeometry { + List>>> _coordinates = List.empty(growable: true); + List _bbox = List.empty(growable: true); // 0 = Lng W, 1 = Lat S, 2 = Lng E, 3 = Lat N + num? _area; + MPBounds? _bounds; + + /// Create a empty [MPMultiPolygon], remember to set [coordinates] for proper use + MPMultiPolygon(); + + /// Attempts to build a [MPMultiPolygon] from a JSON object, this method will decode the object if needed + static MPMultiPolygon? fromJson(json) => json != null && json != "null" ? MPMultiPolygon._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPMultiPolygon._fromJson(data) { + if (data is List) { + coordinates = convertJson4dArray(data); + var bounds = _calcBounds(); + bbox = [bounds.southwest.longitude, bounds.southwest.latitude, bounds.northeast.longitude, bounds.northeast.latitude]; + } else { + coordinates = convertJson4dArray(data['coordinates']); + if (data['bbox'] is Map) { + bbox = [data['bbox']["southwest"][0], data['bbox']["southwest"][1], data['bbox']["northeast"][0], data['bbox']["northeast"][1]]; + } else { + bbox = convertJsonArray(data['bbox']); + } + } + } + + /// Update the coordinates list for the polygon, the coordinates must be in + /// [GeoJSON format](https://stevage.github.io/geojson-spec/#section-3.1.7) + set coordinates(List>>> coordinates) { + _coordinates = coordinates; + _clearCache(); + } + + /// Get the coordinates as a collection in [GeoJSON format](https://stevage.github.io/geojson-spec/#section-3.1.7) + List>>> get coordinates => _coordinates; + + /// Update the polygon's bounding box, the bounding box must be in [GeoJson format](https://stevage.github.io/geojson-spec/#section-5) + set bbox(List box) { + _bbox = box; + _clearCache(); + } + + /// Get the polygon's bounding box, the bounding box is in [GeoJson format](https://stevage.github.io/geojson-spec/#section-5) + List get bbox => _bbox; + + /// Get the polygon's bounds. If [bbox] is present then that will be used, otherwise a [MPBounds] + /// will be created from the coordinates of the polygon + MPBounds get bounds => _bounds ??= _calcBounds(); + + MPBounds _calcBounds() { + if (bbox.isNotEmpty) { + return MPBounds(northeast: MPPoint.withCoordinates(latitude: bbox[3], longitude: bbox[2]), southwest: MPPoint.withCoordinates(latitude: bbox[1], longitude: bbox[0])); + } else { + MPBoundsBuilder builder = MPBounds.builder(); + for (var p in coordinates) { + for (var outline in p[0]) { + builder.include(MPPoint.withCoordinates(latitude: outline[MPGeometry._lat], longitude: outline[MPGeometry._lng])); + } + } + return builder.build(); + } + } + + /// Calculates the squared distance from the [point] to the closest edge in the polygon + Future getSquaredDistanceToClosestEdge(MPPoint point) async => await UtilPlatform.instance.geometryGetSquaredDistanceToClosestEdge(this, point); + + /// Converts the [MPMultiPolygon] to a collection of [MPPolygon]s + List toMPPolygons() { + List result = List.empty(growable: true); + for (var coordinate in _coordinates) { + result.add(MPPolygon._fromCoordinates(coordinate)); + } + return result; + } + + /// Get the polygon's area, this is the combined area of all the polygons in the [MPMultiPolygon] + @override + Future get area async => _area ??= await UtilPlatform.instance.geometryArea(this); + + /// Used to determine the proper type of a [MPGeometry] + @override + String get type => MPGeometry.multiPolygon; + + /// Converts the [MPMultiPolygon] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() => {"coordinates": coordinates, "bbox": bbox, "type": type}; + + void _clearCache() { + _area = null; + _bounds = null; + } + + /// Get the [position] of the polygon, which is roughly the center + @override + MPPoint get position => bounds.center; +} diff --git a/mapsindoors_platform_interface/lib/models/geometries/mp_point.dart b/mapsindoors_platform_interface/lib/models/geometries/mp_point.dart new file mode 100644 index 0000000..89fd22e --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/geometries/mp_point.dart @@ -0,0 +1,123 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// [MPPoint] is a representation of [latitude] and [longitude] coordinates packaged with +/// a Z-axis representation in [floorIndex] +class MPPoint extends MPGeometry { + // max int32, same value returned in SDK + /// The index returned when no floor index has been specified + /// + /// == 2147483647 + static const noFloorIndex = 2147483647; + num? _lat; + num? _lng; + int? _floorIndex; + + /// Create a empty [MPPoint], remember to [setCoordinates] for proper use + MPPoint(); + + /// Copy another [MPPoint]'s coordinates + MPPoint.copy(MPPoint other) { + _lat = other.latitude; + _lng = other.longitude; + _floorIndex = other.floorIndex; + } + + /// Createa a [MPPoint] from a set of [latitude]/[longitude] coordinates and an optional [floorIndex] + MPPoint.withCoordinates({required num longitude, required num latitude, int? floorIndex}) { + this.longitude = longitude; + this.latitude = latitude; + _floorIndex = floorIndex; + } + + /// Attempts to build a [MPPoint] from a JSON object, this method will decode the object if needed + static MPPoint? fromJson(json) => json != null && json != "null" ? MPPoint._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPPoint._fromJson(data) { + // iOS will send an array directly, so handle this case + final List coordinates; + if (data is List) + coordinates = convertJsonArray(data); + else + coordinates = convertJsonArray(data["coordinates"]); + + longitude = coordinates[0]; + latitude = coordinates[1]; + if (coordinates.length > 2) { + _floorIndex = coordinates[2].toInt(); + } + } + + /// Update both coordinates for the point + void setCoordinates({required num latitude, required num longitude}) { + longitude = longitude; + latitude = latitude; + } + + /// Set a new [latitude] + set latitude(num lat) { + _lat = lat.abs() > 90.0 ? (90.0 * lat.sign) : lat; + } + + /// Set a new [longitude] + set longitude(num lng) { + _lng = lng.abs() > 180.0 ? (180.0 * lng.sign) : lng; + } + + /// Set a new [floorIndex] + set floorIndex(int floorIndex) => _floorIndex = floorIndex; + + /// Get the [latitude], if none is present returns [double.nan] + num get latitude => _lat ?? double.nan; + + /// Get the [longitude], if none is present returns [double.nan] + num get longitude => _lng ?? double.nan; + + /// Get the [floorIndex], if none is present returns [noFloorIndex] + int get floorIndex => _floorIndex ?? noFloorIndex; + + /// Get the non-zero area of the point + @override + Future get area async => 0.5; + + /// Used to determine the proper type of a [MPGeometry] + @override + String get type => MPGeometry.point; + + /// Converts the [MPPoint] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + if (_lat == null || _lng == null) { + return { + "coordinates": [0.0, 0.0], + "type": type + }; + } + List coords = [longitude, latitude]; + + if (_floorIndex != null) { + coords.add(_floorIndex!); + } + + return {"coordinates": coords, "type": type}; + } + + /// Calculates the shortest distance to another [MPPoint] + Future distanceTo(MPPoint destination) async => await UtilPlatform.instance.pointDistanceTo(this, destination); + + /// Calculates the angle between this point and another [MPPoint] in degrees from north + Future angleBetween(MPPoint other) async => await UtilPlatform.instance.pointAngleBetween(this, other); + + /// Prints this point on the following form: + /// + /// "15.00000001,20.20000014,10" + String get coordinatesAsString { + return "${latitude.toStringAsPrecision(8)},${longitude.toStringAsPrecision(8)}" + ((_floorIndex != null) ? ",$floorIndex" : ""); + } + + @override + String toString() => "Lat: $latitude, Lng: $longitude"; + + /// Gets the position of the point, which is [this] + @override + MPPoint get position => this; +} diff --git a/mapsindoors_platform_interface/lib/models/geometries/mp_polygon.dart b/mapsindoors_platform_interface/lib/models/geometries/mp_polygon.dart new file mode 100644 index 0000000..9bc128e --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/geometries/mp_polygon.dart @@ -0,0 +1,108 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// [MPPolygon] is a collection of [MPPoint]s that combine +/// to form a single geographical area with a single body +class MPPolygon extends MPGeometry { + List>> _coordinates = List.empty(growable: true); + List _bbox = List.empty(growable: true); // 0 = Lng W, 1 = Lat S, 2 = Lng E, 3 = Lat N + num? _area; + List>? _points; + MPBounds? _bounds; + + /// Create a empty [MPPolygon], remember to set [coordinates] for proper use + MPPolygon(); + + /// Attempts to build a [MPPolygon] from a JSON object, this method will decode the object if needed + static MPPolygon? fromJson(json) => json != null && json != "null" ? MPPolygon._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPPolygon._fromJson(data) { + // iOS will send an array directly, so handle this case + if (data is List) + coordinates = convertJson3dArray(data); + else { + coordinates = convertJson3dArray(data["coordinates"]); + bbox = convertJsonArray(data['bbox']); + } + } + + MPPolygon._fromCoordinates(List>> list) { + _coordinates = List.from(list); + var bounds = _calcBounds(); + bbox = [bounds.southwest.longitude, bounds.southwest.latitude, bounds.northeast.longitude, bounds.northeast.latitude]; + } + + /// Update the coordinates list for the polygon, the coordinates must be in + /// [GeoJSON format](https://stevage.github.io/geojson-spec/#section-3.1.6) + set coordinates(List>> coordinates) { + _coordinates = coordinates; + _clearCache(); + } + + /// Get the coordinates as a collection in [GeoJSON format](https://stevage.github.io/geojson-spec/#section-3.1.6) + List>> get coordinates => _coordinates; + + /// Update the polygon' bounding box, the bounding box must be in [GeoJSON format](https://stevage.github.io/geojson-spec/#section-5) + set bbox(List box) { + _bbox = box; + _clearCache(); + } + + /// Get the polygon' bounding box, the bounding box is in [GeoJSON format](https://stevage.github.io/geojson-spec/#section-5) + List get bbox => _bbox; + + /// Get the [points] the polygon consists of in [GeoJSON format](https://stevage.github.io/geojson-spec/#section-3.1.3) + List> get points => _points ??= _calcPoints(); + + List> _calcPoints() { + List> points = List.empty(growable: true); + for (var coordSet in coordinates) { + List pointsList = List.empty(growable: true); + for (var pair in coordSet) { + pointsList.add(MPPoint.withCoordinates(latitude: pair[MPGeometry._lat], longitude: pair[MPGeometry._lng])); + } + points.add(pointsList); + } + return points; + } + + /// Get the polygon' bounds. If [bbox] is present then that will be used, otherwise a [MPBounds] + /// will be created from the coordinates of the polygon + MPBounds get bounds => _bounds ??= _calcBounds(); + + MPBounds _calcBounds() { + if (bbox.isNotEmpty) { + return MPBounds(northeast: MPPoint.withCoordinates(latitude: bbox[3], longitude: bbox[2]), southwest: MPPoint.withCoordinates(latitude: bbox[1], longitude: bbox[0])); + } else { + MPBoundsBuilder builder = MPBounds.builder(); + for (var p in coordinates[0]) { + builder.include(MPPoint.withCoordinates(latitude: p[MPGeometry._lat], longitude: p[MPGeometry._lng])); + } + return builder.build(); + } + } + + /// Calculates the squared distance from the [point] to the closest edge in the polygon + Future getSquaredDistanceToClosestEdge(MPPoint point) async => await UtilPlatform.instance.geometryGetSquaredDistanceToClosestEdge(this, point); + + /// Get the polygon' area + @override + Future get area async => _area ??= await UtilPlatform.instance.geometryArea(this); + + /// Used to determine the proper type of a [MPGeometry] + @override + String get type => MPGeometry.polygon; + + /// Converts the [MPPolygon] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() => {"coordinates": coordinates, "bbox": bbox, "type": type}; + + void _clearCache() { + _area = null; + _points = null; + _bounds = null; + } + + /// Get the [position] of the polygon, which is roughly the center + @override + MPPoint get position => bounds.center; +} diff --git a/mapsindoors_platform_interface/lib/models/listeners/mp_camera_event_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/mp_camera_event_listener.dart new file mode 100644 index 0000000..f3c3e9b --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/mp_camera_event_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listens for camera events, eg. when the Camera starts or stops moving +typedef MPCameraEventListener = void Function(MPCameraEvent event); diff --git a/mapsindoors_platform_interface/lib/models/listeners/on_building_found_at_camera_target_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/on_building_found_at_camera_target_listener.dart new file mode 100644 index 0000000..e50e113 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/on_building_found_at_camera_target_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listener that is invoked when a building is found within the camera bounds +typedef OnBuildingFoundAtCameraTargetListener = void Function(MPBuilding? building); diff --git a/mapsindoors_platform_interface/lib/models/listeners/on_floor_selection_changed_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/on_floor_selection_changed_listener.dart new file mode 100644 index 0000000..1464441 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/on_floor_selection_changed_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listener that is invoked when the active floor is changed +typedef OnFloorSelectionChangedListener = void Function(MPFloor newFloor); diff --git a/mapsindoors_platform_interface/lib/models/listeners/on_floor_update_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/on_floor_update_listener.dart new file mode 100644 index 0000000..87f9836 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/on_floor_update_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listener that is invoked when floor selection changes +typedef OnFloorUpdateListener = void Function(int floor); diff --git a/mapsindoors_platform_interface/lib/models/listeners/on_leg_selected_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/on_leg_selected_listener.dart new file mode 100644 index 0000000..6a3e936 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/on_leg_selected_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listener that is invoked during directions when a [MPRouteLeg] is selected +typedef OnLegSelectedListener = void Function(int legIndex); diff --git a/mapsindoors_platform_interface/lib/models/listeners/on_live_location_update_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/on_live_location_update_listener.dart new file mode 100644 index 0000000..db26166 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/on_live_location_update_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listener that is invoked when a location receives a livedata update +typedef OnLiveLocationUpdateListener = void Function(MPLocation location); diff --git a/mapsindoors_platform_interface/lib/models/listeners/on_location_selected_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/on_location_selected_listener.dart new file mode 100644 index 0000000..7e122a1 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/on_location_selected_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listener that is invoked when a [MPLocation] is selected +typedef OnLocationSelectedListener = void Function(MPLocation? location); diff --git a/mapsindoors_platform_interface/lib/models/listeners/on_map_click_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/on_map_click_listener.dart new file mode 100644 index 0000000..697bfd4 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/on_map_click_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listener that is invoked when the map is clicked +typedef OnMapClickListener = void Function(MPPoint point, List? locations); diff --git a/mapsindoors_platform_interface/lib/models/listeners/on_map_ready_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/on_map_ready_listener.dart new file mode 100644 index 0000000..f7cca55 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/on_map_ready_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listener that is invoked when the map is ready for use. +typedef OnMapReadyListener = void Function(MPError? error); diff --git a/mapsindoors_platform_interface/lib/models/listeners/on_mapsindoors_ready_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/on_mapsindoors_ready_listener.dart new file mode 100644 index 0000000..f05c2c1 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/on_mapsindoors_ready_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listener that is invoked when MapsIndoors is ready for use +typedef OnMapsIndoorsReadyListener = void Function(MPError? error); diff --git a/mapsindoors_platform_interface/lib/models/listeners/on_marker_click_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/on_marker_click_listener.dart new file mode 100644 index 0000000..907651f --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/on_marker_click_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listener that is invoked when a [MPLocation] marker is clicked on the map +typedef OnMarkerClickListener = void Function(String locationId); diff --git a/mapsindoors_platform_interface/lib/models/listeners/on_marker_info_window_click_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/on_marker_info_window_click_listener.dart new file mode 100644 index 0000000..0b9f8f5 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/on_marker_info_window_click_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listener that is invoked when a markers infowindow is clicked +typedef OnMarkerInfoWindowClickListener = void Function(String locationId); diff --git a/mapsindoors_platform_interface/lib/models/listeners/on_position_update_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/on_position_update_listener.dart new file mode 100644 index 0000000..09b1355 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/on_position_update_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listener that is invoked when user positioning is updated +typedef OnPositionUpdateListener = void Function(MPPositionResultInterface position); diff --git a/mapsindoors_platform_interface/lib/models/listeners/on_venue_found_at_camera_target_listener.dart b/mapsindoors_platform_interface/lib/models/listeners/on_venue_found_at_camera_target_listener.dart new file mode 100644 index 0000000..9e75f79 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/listeners/on_venue_found_at_camera_target_listener.dart @@ -0,0 +1,4 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Listener that is invoked when a venue is found within the camera bounds +typedef OnVenueFoundAtCameraTargetListener = void Function(MPVenue? venue); diff --git a/mapsindoors_platform_interface/lib/models/map/mp_camera_position.dart b/mapsindoors_platform_interface/lib/models/map/mp_camera_position.dart new file mode 100644 index 0000000..57c5d8c --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/map/mp_camera_position.dart @@ -0,0 +1,50 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A camera position object, used to move the camera to a specific position. +class MPCameraPosition extends MapsIndoorsObject { + final num zoom; + final MPPoint target; + final num tilt; + final num bearing; + + /// Attempts to build a [MPCameraPosition] from a JSON object, this method will decode the object if needed + static MPCameraPosition? fromJson(json) => json != null && json != "null" ? MPCameraPosition._fromJson(json is String ? jsonDecode(json) : json) : null; + + /// Build a Camera Position, with optional tilt and bearing parameters. + const MPCameraPosition({required this.zoom, required this.target, this.tilt = 0, this.bearing = 0}); + + static MPCameraPosition _fromJson(data) { + final zoom = data["zoom"]; + final tilt = data["tilt"]; + final bearing = data["bearing"]; + final target = MPPoint.fromJson(data["target"])!; + return new MPCameraPosition(zoom: zoom, target: target, bearing: bearing, tilt: tilt); + } + + @override + Map toJson() { + return {"zoom": zoom, "tilt": tilt, "bearing": bearing, "target": target.toJson()}; + } +} + +/// Construct a camera position. +class MPCameraPositionBuilder { + /// Copy another position to use as a reference. + MPCameraPositionBuilder.fromPosition(MPCameraPosition position) + : target = position.target, + zoom = position.zoom, + tilt = position.tilt, + bearing = position.bearing; + + /// Start the builder, a target and zoom is required to build a [MPCameraPosition] + MPCameraPositionBuilder({required this.target, required this.zoom}); + + num zoom; + MPPoint target; + num tilt = 0; + num bearing = 0; + + MPCameraPosition build() { + return MPCameraPosition(zoom: zoom, target: target, tilt: tilt, bearing: bearing); + } +} diff --git a/mapsindoors_platform_interface/lib/models/map/mp_camera_update.dart b/mapsindoors_platform_interface/lib/models/map/mp_camera_update.dart new file mode 100644 index 0000000..2d56452 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/map/mp_camera_update.dart @@ -0,0 +1,28 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// An object that can be used to update the position of the camera by calling MapControl.animateCamera(). +class MPCameraUpdate extends MapsIndoorsObject { + final Map _map; + + /// Construct the update from a point. + MPCameraUpdate.fromPoint(MPPoint point) : _map = {"mode": "fromPoint", "point": point.toJson()}; + + /// Construct the update from a bounding box with some padding. + MPCameraUpdate.fromBounds({required MPBounds bounds, required int padding, int? width, int? height}) : _map = {"mode": "fromBounds", "bounds": bounds.toJson(), "padding": padding, "width": width, "height": height} { + _map.removeWhere((key, value) => value == null); + } + + /// Zooms the camera by the given amount. + MPCameraUpdate.zoomBy(num amount) : _map = {"mode": "zoomBy", "zoom": amount}; + + /// Zooms the camera to the given level. + MPCameraUpdate.zoomTo(num zoom) : _map = {"mode": "zoomTo", "zoom": zoom}; + + /// Constructs the update from the given position. + MPCameraUpdate.fromCameraPosition(MPCameraPosition position) : _map = {"mode": "fromCameraPosition", "position": position.toJson()}; + + @override + Map toJson() { + return _map; + } +} diff --git a/mapsindoors_platform_interface/lib/models/mp_building_info.dart b/mapsindoors_platform_interface/lib/models/mp_building_info.dart new file mode 100644 index 0000000..b54f91e --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_building_info.dart @@ -0,0 +1,35 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A collection of information about a [MPBuilding] +class MPBuildingInfo extends MapsIndoorsObject { + String? _name; + List? _aliases; + Map? _fields; + + /// Attempts to build a [MPBuildingInfo] from a JSON object, this method will decode the object if needed + static MPBuildingInfo? fromJson(json) => json != null && json != "null" ? MPBuildingInfo._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPBuildingInfo._fromJson(data) { + _name = data["name"]; + _aliases = data["aliases"] != null ? convertJsonArray(data["aliases"]) : null; + _fields = data["fields"] != null ? (data?["fields"] as Map).map((key, value) => MapEntry(key, MPDataField.fromJson(value)!)) : null; + } + + /// Converts the [MPBuilding] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() => {"name": _name, "aliases": _aliases, "fields": _fields}; + + /// The [MPBuilding]'s [name] + String get name => _name!; + + /// The [MPBuilding]'s [aliases] + List? get aliases => _aliases; + + /// Fetch a [MPDataField] with its corresponding [key] + MPDataField? getField(String? key) { + return _fields?[key]; + } + + /// Get all [fields] for the [MPBuilding] + Map get fields => _fields ?? {}; +} diff --git a/mapsindoors_platform_interface/lib/models/mp_category.dart b/mapsindoors_platform_interface/lib/models/mp_category.dart new file mode 100644 index 0000000..8dce56a --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_category.dart @@ -0,0 +1,22 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A categorisation for MapsIndoors objects +class MPCategory { + /// The category's [key] + late final String key; + + /// The category's [value] + late final String value; + + /// The category's [fields] + late final Map? fields; + + /// Attempts to build a [MPCategory] from a JSON object, this method will decode the object if needed + static MPCategory? fromJson(json) => json != null && json != "null" ? MPCategory._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPCategory._fromJson(data) { + key = data["key"]; + value = data["value"]; + fields = (data["fields"] as Map?)?.map((key, value) => MapEntry(key, MPDataField.fromJson(value)!)); + } +} diff --git a/mapsindoors_platform_interface/lib/models/mp_data_field.dart b/mapsindoors_platform_interface/lib/models/mp_data_field.dart new file mode 100644 index 0000000..9912066 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_data_field.dart @@ -0,0 +1,26 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A [MPDataField] contains a single field [value] with descriptive +/// [text] and a [type] that describes the type of the [value] (eg. text or number) +class MPDataField extends MapsIndoorsObject { + /// The [value] of the field + final String? value; + + /// The descriptive [text] for the field + final String? text; + + /// The [type] of the [value] + final String? type; + + /// Attempts to build a [MPDataField] from a JSON object, this method will decode the object if needed + static MPDataField? fromJson(json) => json != null && json != "null" ? MPDataField._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPDataField._fromJson(data) + : value = data["value"], + text = data["text"], + type = data["type"]; + + /// Converts the [MPDataField] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() => {"value": value, "text": text, "type": type}; +} diff --git a/mapsindoors_platform_interface/lib/models/mp_display_rule.dart b/mapsindoors_platform_interface/lib/models/mp_display_rule.dart new file mode 100644 index 0000000..51c20cd --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_display_rule.dart @@ -0,0 +1,258 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A unique identifier for display rules +@immutable +class MPDisplayRuleId extends DynamicObjectId { + const MPDisplayRuleId(String value) : super(value); +} + +/// A collection of settings that dictate how MapsIndoors objects are displayed on the map +@immutable +class MPDisplayRule implements DynamicObject { + const MPDisplayRule._(this._id); + + final MPDisplayRuleId _id; + + /// Get the display rule's [id] + @override + MPDisplayRuleId get id => _id; + + /// The name of the display rule + String get dispalyRuleName => _id.value; + + /// Get the general visibility value + Future isVisible() => DisplayRulePlatform.instance.isVisible(id); + + /// Set the general visibility value + Future setVisible(bool visible) => DisplayRulePlatform.instance.setVisible(id, visible); + + /// Get the icon's visibility value + Future isIconVisible() => DisplayRulePlatform.instance.isIconVisible(id); + + /// Set the icon's visibility value + Future setIconVisible(bool visible) => DisplayRulePlatform.instance.setIconVisible(id, visible); + + /// Get the general zoom from value + Future getZoomFrom() => DisplayRulePlatform.instance.getZoomFrom(id); + + /// Set the general zoom from value + Future setZoomFrom(num from) => DisplayRulePlatform.instance.setZoomFrom(id, from); + + /// Get the general zoom to value + Future getZoomTo() => DisplayRulePlatform.instance.getZoomTo(id); + + /// Set the general zoom to value + Future setZoomTo(num to) => DisplayRulePlatform.instance.setZoomTo(id, to); + + /// Get the icon's URL + Future getIconUrl() => DisplayRulePlatform.instance.getIconUrl(id); + + /// Set the icon's URL + Future setIcon(String url) => DisplayRulePlatform.instance.setIcon(id, url); + + /// Get the icon's size + Future getIconSize() => DisplayRulePlatform.instance.getIconSize(id); + + /// Set the icon's size + Future setIconSize(MPIconSize size) => DisplayRulePlatform.instance.setIconSize(id, size); + + /// Get the label's visibility value + Future isLabelVisible() => DisplayRulePlatform.instance.isLabelVisible(id); + + /// Set the label's visibility value + Future setLabelVisible(bool visible) => DisplayRulePlatform.instance.setLabelVisible(id, visible); + + /// Get the label string + Future getLabel() => DisplayRulePlatform.instance.getLabel(id); + + /// Set the label string + Future setLabel(String label) => DisplayRulePlatform.instance.setLabel(id, label); + + /// Get the label's zoom from value + Future getLabelZoomFrom() => DisplayRulePlatform.instance.getLabelZoomFrom(id); + + /// Set the label's zoom from value + Future setLabelZoomFrom(num from) => DisplayRulePlatform.instance.setLabelZoomFrom(id, from); + + /// Get the label's zoom to value + Future getLabelZoomTo() => DisplayRulePlatform.instance.getLabelZoomTo(id); + + /// Set the label's zoom to value + Future setLabelZoomTo(num to) => DisplayRulePlatform.instance.setLabelZoomTo(id, to); + + /// Get the label's max width value + Future getLabelMaxWidth() => DisplayRulePlatform.instance.getLabelMaxWidth(id); + + /// Set the label's max width value + Future setLabelMaxWidth(int max) => DisplayRulePlatform.instance.setLabelMaxWidth(id, max); + + // polygon + /// Get the polygon's visibility value + Future isPolygonVisible() => DisplayRulePlatform.instance.isPolygonVisible(id); + + /// Set the polygon's visibility value + Future setPolygonVisible(bool visible) => DisplayRulePlatform.instance.setPolygonVisible(id, visible); + + /// Get the polygon's zoom from value + Future getPolygonZoomFrom() => DisplayRulePlatform.instance.getPolygonZoomFrom(id); + + /// Set the polygon's zoom from value + Future setPolygonZoomFrom(num from) => DisplayRulePlatform.instance.setPolygonZoomFrom(id, from); + + /// Get the polygon's zoom to value + Future getPolygonZoomTo() => DisplayRulePlatform.instance.getPolygonZoomTo(id); + + /// Set the polygon's zoom to value + Future setPolygonZoomTo(num to) => DisplayRulePlatform.instance.setPolygonZoomTo(id, to); + + /// Get the polygon's stroke width value + Future getPolygonStrokeWidth() => DisplayRulePlatform.instance.getPolygonStrokeWidth(id); + + /// Set the polygon's stroke width value + Future setPolygonStrokeWidth(num width) => DisplayRulePlatform.instance.setPolygonStrokeWidth(id, width); + + /// Get the polygon's stroke color value + Future getPolygonStrokeColor() => DisplayRulePlatform.instance.getPolygonStrokeColor(id); + + /// Set the polygon's stroke color value + Future setPolygonStrokeColor(String color) => DisplayRulePlatform.instance.setPolygonStrokeColor(id, color); + + /// Get the polygon's stroke opacity value + Future getPolygonStrokeOpacity() => DisplayRulePlatform.instance.getPolygonStrokeOpacity(id); + + /// Set the polygon's stroke opacity value + Future setPolygonStrokeOpacity(num opacity) => DisplayRulePlatform.instance.setPolygonStrokeOpacity(id, opacity); + + /// Get the polygon's fill color value + Future getPolygonFillColor() => DisplayRulePlatform.instance.getPolygonFillColor(id); + + /// Set the polygon's fill color value + Future setPolygonFillColor(String color) => DisplayRulePlatform.instance.setPolygonFillColor(id, color); + + /// Get the polygon's fill opacity value + Future getPolygonFillOpacity() => DisplayRulePlatform.instance.getPolygonFillOpacity(id); + + /// Set the polygon's fill opacity value + Future setPolygonFillOpacity(num opacity) => DisplayRulePlatform.instance.setPolygonFillOpacity(id, opacity); + + // wall + /// Get the wall's visibility value + Future isWallVisible() => DisplayRulePlatform.instance.isWallVisible(id); + + /// Set the wall's visibility value + Future setWallVisible(bool visible) => DisplayRulePlatform.instance.setWallVisible(id, visible); + + /// Get the wall's color value + Future getWallColor() => DisplayRulePlatform.instance.getWallColor(id); + + /// Set the wall's color value + Future setWallColor(String color) => DisplayRulePlatform.instance.setWallColor(id, color); + + /// Get the wall's height value + Future getWallHeight() => DisplayRulePlatform.instance.getWallHeight(id); + + /// Set the wall's height value + Future setWallHeight(num height) => DisplayRulePlatform.instance.setWallHeight(id, height); + + /// Get the wall's zoom from value + Future getWallZoomFrom() => DisplayRulePlatform.instance.getWallZoomFrom(id); + + /// Set the wall's zoom from value + Future setWallZoomFrom(num from) => DisplayRulePlatform.instance.setWallZoomFrom(id, from); + + /// Get the wall's zoom to value + Future getWallZoomTo() => DisplayRulePlatform.instance.getWallZoomTo(id); + + /// Set the wall's zoom to value + Future setWallZoomTo(num to) => DisplayRulePlatform.instance.setWallZoomTo(id, to); + + //extrusion + /// Get the extrusion's visibility value + Future isExtrusionVisible() => DisplayRulePlatform.instance.isExtrusionVisible(id); + + /// Set the extrusion's visibility value + Future setExtrusionVisible(bool visible) => DisplayRulePlatform.instance.setExtrusionVisible(id, visible); + + /// Get the extrusion's color value + Future getExtrusionColor() => DisplayRulePlatform.instance.getExtrusionColor(id); + + /// Set the extrusion's color value + Future setExtrusionColor(String color) => DisplayRulePlatform.instance.setExtrusionColor(id, color); + + /// Get the extrusion's height value + Future getExtrusionHeight() => DisplayRulePlatform.instance.getExtrusionHeight(id); + + /// Set the extrusion's height value + Future setExtrusionHeight(num height) => DisplayRulePlatform.instance.setExtrusionHeight(id, height); + + /// Get the extrusion's zoom from value + Future getExtrusionZoomFrom() => DisplayRulePlatform.instance.getExtrusionZoomFrom(id); + + /// Set the extrusion's zoom from value + Future setExtrusionZoomFrom(num from) => DisplayRulePlatform.instance.setExtrusionZoomFrom(id, from); + + /// Get the extrusion's zoom to value + Future getExtrusionZoomTo() => DisplayRulePlatform.instance.getExtrusionZoomTo(id); + + /// Set the extrusion's zoom to value + Future setExtrusionZoomTo(num to) => DisplayRulePlatform.instance.setExtrusionZoomTo(id, to); + + //2d models + /// Get the 2D model's visibility value + Future isModel2DVisible() => DisplayRulePlatform.instance.isModel2DVisible(id); + + /// Set the 2D model's visibility value + Future setModel2DVisible(bool visible) => DisplayRulePlatform.instance.setModel2DVisible(id, visible); + + /// Get the 2D model's zoom from value + Future getModel2DZoomFrom() => DisplayRulePlatform.instance.getModel2DZoomFrom(id); + + /// Set the 2D model's zoom from value + Future setModel2DZoomFrom(num from) => DisplayRulePlatform.instance.setModel2DZoomFrom(id, from); + + /// Get the 2D model's zoom to value + Future getModel2DZoomTo() => DisplayRulePlatform.instance.getModel2DZoomTo(id); + + /// Set the 2D model's zoom to value + Future setModel2DZoomTo(num to) => DisplayRulePlatform.instance.setModel2DZoomTo(id, to); + + /// Get the 2D model's URL + Future getModel2DModel() => DisplayRulePlatform.instance.getModel2DModel(id); + + /// Set the 2D model's URL + Future setModel2DModel(String url) => DisplayRulePlatform.instance.setModel2DModel(id, url); + + /// Get the 2D model's width in meters + Future getModel2DWidthMeters() => DisplayRulePlatform.instance.getModel2DWidthMeters(id); + + /// Set the 2D model's width in meters + Future setModel2DWidthMeters(num width) => DisplayRulePlatform.instance.setModel2DWidthMeters(id, width); + + /// Get the 2D model's height in meters + Future getModel2DHeightMeters() => DisplayRulePlatform.instance.getModel2DHeightMeters(id); + + /// Set the 2D model's height in meters + Future setModel2DHeightMeters(num height) => DisplayRulePlatform.instance.setModel2DHeightMeters(id, height); + + /// Get the 2D model's bearing value + Future getModel2DBearing() => DisplayRulePlatform.instance.getModel2DBearing(id); + + /// Get the 2D model's bearing value + Future setModel2DBearing(num bearing) => DisplayRulePlatform.instance.setModel2DBearing(id, bearing); + + Future reset() => DisplayRulePlatform.instance.reset(id); + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is MPDisplayRule && id == other.id; + } + + @override + int get hashCode => id.hashCode; +} diff --git a/mapsindoors_platform_interface/lib/models/mp_error.dart b/mapsindoors_platform_interface/lib/models/mp_error.dart new file mode 100644 index 0000000..947f778 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_error.dart @@ -0,0 +1,49 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A class of errors that can occur when using the MapsIndoors SDK +class MPError { + /// Occurs when an internet connection is required, or if the content server is unresponsive + static const int networkError = 10; + + /// Occurs if an unknown exception is caught + static const int unknownError = 20; + + /// Occurs if some functions are called before the SDK has been initialized + static const int sdkNotInitialized = 22; + + /// Occurs if the supplied API key is not a valid MapsIndoors key + static const int invalidApiKey = 100; + + /// The error's [code] + final int code; + + /// A [message] that describes the context of the error + final String message; + + /// An optional [status], if the error is a [networkError], this will be the HTTP response code + final int? status; + + /// An optional tag + final Object? tag; + + MPError({required this.code, required this.message, this.status, this.tag}); + + static MPError? fromJson(json) => json != null && json != "null" ? MPError._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPError._fromJson(data) + : code = data["code"], + message = data["message"], + status = data["status"], + tag = data["tag"]; + + @override + bool operator ==(Object other) => (other is MPError && other.runtimeType == runtimeType && other.code == code); + + @override + int get hashCode => code.hashCode; + + @override + String toString() { + return "code: $code, message: \"$message\"" + ((status != null) ? ", status: $status" : "") + ((tag != null) ? ", tag: $tag" : ""); + } +} diff --git a/mapsindoors_platform_interface/lib/models/mp_filter.dart b/mapsindoors_platform_interface/lib/models/mp_filter.dart new file mode 100644 index 0000000..a273682 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_filter.dart @@ -0,0 +1,153 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A filter that can be applied during search, this will limit the returned [MPEntity]s to those that +/// fulfills the filter +class MPFilter extends MapsIndoorsObject { + static MPFilterBuilder builder() => MPFilterBuilder(); + + /// How many of the applicable locations to [take] + final int? take; + + /// How many of the applicable locations to [skip] + final int? skip; + + /// The [depth] property makes it possible to get the n'th descendant of a parent location + /// + /// Thus, the [depth] property only applies to filters that has set one or more parents. + /// The hierarchical tree of data is generally structured as Venue > Building > Floor > Room > POI. + /// + /// For example, this means that a Floor is the 1st descendant of a Building. + /// So to get all locations inside a Building, set the [depth] to 3. + /// + /// The default value is 1, giving you only the immediate descendant of the specified parents. + final int? depth; + + /// The [floorIndex] property makes it possible to get the locations on a specific floor + final int? floorIndex; + + /// A list of categories (keys) from, for example, [MPLocation.categories] + final List? categories; + + /// A list of location ids to search in + final List? locations; + + /// A list of location [types] to search in + final List? types; + + /// A list of parent ids + final List? parents; + + /// The outer bounds of the query + final MPBounds? mapExtend; + + /// A [geometry] to search inside + final MPBounds? geometry; + + /// Allows queries to return results that are marked as non-searchable. + final bool? ignoreLocationSearchableStatus; + + /// Allows queries to return results that are marked as inactive using the active from/to mechanism. + final bool? ignoreLocationActiveStatus; + + MPFilter._(this.take, this.skip, this.depth, this.floorIndex, this.categories, this.locations, this.types, this.parents, this.mapExtend, this.geometry, this.ignoreLocationActiveStatus, this.ignoreLocationSearchableStatus); + + /// Converts the [MPFilter] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + return { + "take": take, + "skip": skip, + "depth": depth, + "floorIndex": floorIndex, + "categories": categories, + "locations": locations, + "types": types, + "parents": parents, + "mapExtend": mapExtend?.toJson(), + "geometry": geometry?.toJson(), + "ignoreLocationSearchableStatus": ignoreLocationSearchableStatus, + "ignoreLocationActiveStatus": ignoreLocationActiveStatus, + }; + } +} + +/// Constructs a [MPFilter] +class MPFilterBuilder { + int? _take; + int? _skip; + int? _depth; + int? _floorIndex; + List? _categories; + List? _locations; + List? _types; + List? _parents; + MPBounds? _mapExtend; + MPBounds? _geometry; + bool? _ignoreLocationSearchableStatus; + bool? _ignoreLocationActiveStatus; + + /// Set the [take] property of the filter, limiting the number of results + void setTake(int take) { + _take = take; + } + + /// Set the [skip] property of the filter, discarding the first [skip] results + void setSkip(int skip) { + _skip = skip; + } + + /// Set the [depth] property of the filter, including the [depth]'th descendant of a parent location + void setDepth(int depth) { + _depth = depth; + } + + /// Set the [floorIndex] property of the filter, limiting the filter to a single floor index + void setFloorIndex(int floorIndex) { + _floorIndex = floorIndex; + } + + /// Set the [categories] property of the filter, limiting the filter to results that are part of the [categories] + void setCategories(List categories) { + _categories = List.from(categories); + } + + /// Set the [locations] property of the filter, limiting the filter to results included in the [locations] + void setLocations(List locations) { + _locations = List.from(locations); + } + + /// Set the [types] property of the filter, limiting the fliter to results with any of the [types] + void setTypes(List types) { + _types = List.from(types); + } + + /// Set the [parents] property of the filter, a list of parent used to search for results + void setParents(List parents) { + _parents = List.from(parents); + } + + /// Set the [mapExtend] property of the filter, limiting the filter's outer bounds + void setMapExtend(MPBounds mapExtend) { + _mapExtend = mapExtend; + } + + /// Set the [geometry] property of the filter, limiting the filter to search inside the [geometry] + void setGeometry(MPBounds geometry) { + _geometry = geometry; + } + + /// Set a [ignore] property of the filter, ignoring non-searchable status for results + void setIgnoreLocationSearchableStatus(bool ignore) { + _ignoreLocationSearchableStatus = ignore; + } + + /// Set a [ignore] property of the filter, ignoring non-active status for results + void setIgnoreLocationActiveStatus(bool ignore) { + _ignoreLocationActiveStatus = ignore; + } + + /// Construct the filter + MPFilter build() { + return MPFilter._(_take, _skip, _depth, _floorIndex, _categories, _locations, _types, _parents, _mapExtend, _geometry, _ignoreLocationActiveStatus, _ignoreLocationSearchableStatus); + } +} diff --git a/mapsindoors_platform_interface/lib/models/mp_floor_selector_interface.dart b/mapsindoors_platform_interface/lib/models/mp_floor_selector_interface.dart new file mode 100644 index 0000000..6b2794f --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_floor_selector_interface.dart @@ -0,0 +1,33 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Interface used by MapsIndoors to communicate with floor selector UIs +abstract class MPFloorSelectorInterface { + /// A floor selector must be a [Widget] + Widget? getWidget(); + + /// Update the floors shown in the selector + set floors(List? floors); + + /// Set a listener that listens for when a floor is selected in the floor selector + /// + /// NB: This is used internally in the SDK, overwriting the listener will result in functionality loss + set onFloorSelectionChangedListener(OnFloorSelectionChangedListener listener); + + /// Show/Hide the floor selector, this is called when a building comes into/out of view + void show(bool show); + + /// Invoked when a floor has been selected programmatically + void setSelectedFloor(MPFloor floor); + + /// Invoked when a floor has been selected programmatically by index + void setSelectedFloorByFloorIndex(int floorIndex); + + /// Invoked when the zoom level changes + void zoomLevelChanged(num newZoomLevel); + + /// Whether to allow the floor to automatically change, eg. when panning to a new building + bool get isAutoFloorChangeEnabled; + + /// Invoked when user positioning changes [floorIndex] + set userPositionFloor(int floorIndex); +} diff --git a/mapsindoors_platform_interface/lib/models/mp_geocode_result.dart b/mapsindoors_platform_interface/lib/models/mp_geocode_result.dart new file mode 100644 index 0000000..3dacb54 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_geocode_result.dart @@ -0,0 +1,33 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// GeoCode class to represent results from a reverse GeoCode. +class MPGeocodeResult { + /// Get the [areas], that the [MPPoint] is inside + final List areas; + + /// Get the [rooms], that the [MPPoint] is inside + final List rooms; + + /// Get the [floors], that the [MPPoint] is inside + final List floors; + + /// Get the [buildings], that the [MPPoint] is inside + final List buildings; + + /// Get the [venues], that the [MPPoint] is inside + final List venues; + + /// Attempts to build a [MPFloor] from a JSON object, this method will decode the object if needed + static MPGeocodeResult? fromJson(json) => json != null && json != "null" ? MPGeocodeResult._fromJson(json is String ? jsonDecode(json) : json) : null; + + static List convert(dynamic data) { + return convertMIList(data, (p0) => MPLocation.fromJson(p0)); + } + + MPGeocodeResult._fromJson(data) + : areas = convert(data["areas"]), + rooms = convert(data["rooms"]), + floors = convert(data["floors"]), + buildings = convert(data["buildings"]), + venues = convert(data["venues"]); +} diff --git a/mapsindoors_platform_interface/lib/models/mp_icon_size.dart b/mapsindoors_platform_interface/lib/models/mp_icon_size.dart new file mode 100644 index 0000000..10ad0dd --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_icon_size.dart @@ -0,0 +1,23 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Class to hold [height] and [width] information for an icon +class MPIconSize extends MapsIndoorsObject { + final int height; + final int width; + + /// Construct a size from a [height] and a [width] + MPIconSize({required this.height, required this.width}); + + /// Attempts to build a [MPIconSize] from a JSON object, this method will decode the object if needed + static MPIconSize? fromJson(json) => json != null && json != "null" ? MPIconSize._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPIconSize._fromJson(data) + : height = data["height"], + width = data["width"]; + + /// Converts the [MPIconSize] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + return {"width": width, "height": height}; + } +} diff --git a/mapsindoors_platform_interface/lib/models/mp_map_behavior.dart b/mapsindoors_platform_interface/lib/models/mp_map_behavior.dart new file mode 100644 index 0000000..3b8dd88 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_map_behavior.dart @@ -0,0 +1,145 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Sets a behavior for the map when calling MapsIndoorsWidget.setFilter +/// +/// Has a [DEFAULT] behavior +@immutable +class MPFilterBehavior extends MapsIndoorsObject { + /// The default behavior for filtering: + /// + /// MoveCamera = false + /// + /// ShowInfoWindow = false + /// + /// AnimationDuration = 0 + /// + /// AllowFloorChange = false + /// + /// ZoomToFit = true + static const MPFilterBehavior DEFAULT = MPFilterBehavior._(false, false, 0, false, true); + + /// Get a builder object + static MPFilterBehaviorBuilder builder() => MPFilterBehaviorBuilder(); + + /// Whether the filtering is allowed to change the floor if no results are visible on the current floor + final bool allowFloorChange; + + /// Whether the filtering should move the camera to encompass the results + final bool moveCamera; + + /// How long the camera movement should be animated for, set to 0 disables animation + final int animationDuration; + + /// Whether to open the info window if a single result is returned + final bool showInfoWindow; + + /// Whether the filtering is allowed to zoom in/out the camera + final bool zoomToFit; + + const MPFilterBehavior._(this.allowFloorChange, this.moveCamera, this.animationDuration, this.showInfoWindow, this.zoomToFit); + + /// Converts the [MPFilterBehavior] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + return {"allowFloorChange": allowFloorChange, "moveCamera": moveCamera, "animationDuration": animationDuration, "showInfoWindow": showInfoWindow, "zoomToFit": zoomToFit}; + } +} + +/// Sets a behavior for the map when calling MapsIndoorsWidget.selectLocation +/// +/// Has a [DEFAULT] behavior +class MPSelectionBehavior extends MapsIndoorsObject { + /// The default behavior for selection: + /// + /// Animate camera = true + /// + /// Show InfoWindow = true + /// + /// Animation duration = 500 + /// + /// Allow floor change = true + /// + /// Zoom to fit = true + static const MPSelectionBehavior DEFAULT = MPSelectionBehavior._(true, true, 500, true, true); + + /// Get a builder object + static MPSelectionBehaviorBuilder builder() => MPSelectionBehaviorBuilder(); + + /// Whether the filtering is allowed to change the floor if no results are visible on the current floor + final bool allowFloorChange; + + /// Whether the filtering should move the camera to encompass the results + final bool moveCamera; + + /// How long the camera movement should be animated for, set to 0 disables animation + final int animationDuration; + + /// Whether to open the info window if a single result is returned + final bool showInfoWindow; + + /// Whether the filtering is allowed to zoom in/out the camera + final bool zoomToFit; + + const MPSelectionBehavior._(this.allowFloorChange, this.moveCamera, this.animationDuration, this.showInfoWindow, this.zoomToFit); + + /// Converts the [MPSelectionBehavior] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + return {"allowFloorChange": allowFloorChange, "moveCamera": moveCamera, "animationDuration": animationDuration, "showInfoWindow": showInfoWindow, "zoomToFit": zoomToFit}; + } +} + +/// Map behavior builder +abstract class Builder { + bool? _allowFloorChange; + bool? _moveCamera; + int? _animationDuration; + bool? _showInfoWindow; + bool? _zoomToFit; + + /// Set whether the filtering is [allow]ed to change the floor if no results are visible on the current floor + void setAllowFloorChange(bool allow) { + _allowFloorChange = allow; + } + + /// Set whether the filtering should [move] the camera to encompass the results + void setMoveCamera(bool move) { + _moveCamera = move; + } + + /// Set the [duration] the camera movement should be animated for, set to 0 to disable animation + void setAnimationDuration(int duration) { + _animationDuration = duration; + } + + /// Set whether to open the info window if a single result is returned + void setShowInfoWindow(bool show) { + _showInfoWindow = show; + } + + /// Set whether the filtering is allowed to zoom the camera in/out + void setZoomToFit(bool doFit) { + _zoomToFit = doFit; + } + + /// Build the behavior object + T build(); +} + +/// Builder for [MPFilterBehavior] +class MPFilterBehaviorBuilder extends Builder { + @override + MPFilterBehavior build() { + return MPFilterBehavior._(_allowFloorChange ?? MPFilterBehavior.DEFAULT.allowFloorChange, _moveCamera ?? MPFilterBehavior.DEFAULT.moveCamera, _animationDuration ?? MPFilterBehavior.DEFAULT.animationDuration, + _showInfoWindow ?? MPFilterBehavior.DEFAULT.showInfoWindow, _zoomToFit ?? MPFilterBehavior.DEFAULT.zoomToFit); + } +} + +/// Builder for [MPSelectionBehavior] +class MPSelectionBehaviorBuilder extends Builder { + @override + MPSelectionBehavior build() { + return MPSelectionBehavior._(_allowFloorChange ?? MPSelectionBehavior.DEFAULT.allowFloorChange, _moveCamera ?? MPSelectionBehavior.DEFAULT.moveCamera, _animationDuration ?? MPSelectionBehavior.DEFAULT.animationDuration, + _showInfoWindow ?? MPSelectionBehavior.DEFAULT.showInfoWindow, _zoomToFit ?? MPSelectionBehavior.DEFAULT.zoomToFit); + } +} diff --git a/mapsindoors_platform_interface/lib/models/mp_map_style.dart b/mapsindoors_platform_interface/lib/models/mp_map_style.dart new file mode 100644 index 0000000..2400b0e --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_map_style.dart @@ -0,0 +1,28 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +class MPMapStyle extends MapsIndoorsObject { + /// The folder the style is saved in + final String folder; + + /// The name of the style + final String displayName; + + /// Attempts to build a [MPMapStyle] from a JSON object, this method will decode the object if needed + static MPMapStyle? fromJson(json) => json != null && json != "null" ? MPMapStyle._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPMapStyle._fromJson(data) + : folder = data["folder"], + displayName = data["displayName"]; + + /// Converts the [MPMapStyle] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + return {"folder": folder, "displayName": displayName}; + } + + @override + bool operator ==(Object other) => (other is MPMapStyle && other.runtimeType == runtimeType && other.folder.toLowerCase() == folder.toLowerCase() && other.displayName.toLowerCase() == displayName.toLowerCase()); + + @override + int get hashCode => (folder.toLowerCase() + displayName.toLowerCase()).hashCode; +} diff --git a/mapsindoors_platform_interface/lib/models/mp_position_provider_interface.dart b/mapsindoors_platform_interface/lib/models/mp_position_provider_interface.dart new file mode 100644 index 0000000..a435738 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_position_provider_interface.dart @@ -0,0 +1,18 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Interface for added a position provider to the SDK +abstract class MPPositionProviderInterface { + /// Add a [listener] to the position provider + /// + /// Invoke the [listener] when a new position has been received + void addOnPositionUpdateListener(OnPositionUpdateListener listener); + + /// Remove a [listener] + void removeOnPositionUpdateListener(OnPositionUpdateListener listener); + + /// Invoked when the latest valid position is needed + MPPositionResultInterface? get latestPosition; + + /// The name of the position provider, used to identify it + String get name; +} diff --git a/mapsindoors_platform_interface/lib/models/mp_position_result_interface.dart b/mapsindoors_platform_interface/lib/models/mp_position_result_interface.dart new file mode 100644 index 0000000..09abe40 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_position_result_interface.dart @@ -0,0 +1,33 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Interface to deliver a position result to the MapsIndoors SDK +abstract class MPPositionResultInterface extends MapsIndoorsObject { + /// The position in world space + MPPoint? get point; + + /// The floor the position is on + int? get floorIndex; + + /// The [bearing] the position is pointing + num? get bearing; + + /// The [accuracy] of the position in meters + num? get accuracy; + + /// What position provider delivered the position result + MPPositionProviderInterface get provider; + + /// Converts the [MPPositionResultInterface] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + if (Platform.isIOS) { + List? coordinate; + if (point != null) { + coordinate = [point!._lng!, point!._lat!]; + } + return {"coordinate": coordinate, "floorIndex": floorIndex, "bearing": bearing, "accuracy": accuracy, "providerName": provider.name}; + } else { + return {"point": point, "floorIndex": floorIndex, "bearing": bearing, "accuracy": accuracy, "providerName": provider.name}; + } + } +} diff --git a/mapsindoors_platform_interface/lib/models/mp_property_data.dart b/mapsindoors_platform_interface/lib/models/mp_property_data.dart new file mode 100644 index 0000000..f46bcac --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_property_data.dart @@ -0,0 +1,77 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +class MPPropertyData extends MapsIndoorsObject { + final String? _name; + final List? aliases; + final Map? categories; + final int? floorIndex; + final String? floorName; + final String? building; + final String? venue; + //final DisplayRule? _displayRule; // has been removed due to the nature of this display rule being an immutable display rule which cannot be viewed + final String? type; + final String? description; + final String? externalId; + final int? activeFrom; + final int? activeTo; + final Map? contact; + final Map? fields; + final String? imageUrl; + final String? locationType; + final MPPoint? anchor; + final bool? bookable; + + static MPPropertyData? fromJson(json) => json != null && json != "null" ? MPPropertyData._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPPropertyData._fromJson(data) + : _name = data["name"], + aliases = data["aliases"] != null ? convertJsonArray(data["aliases"]) : null, + categories = data["categories"] != null ? _convertMap((x) => x.toString(), data["categories"]) : null, + floorIndex = Platform.isIOS ? data["floor"] : int.tryParse(data["floor"]), + floorName = data["floorName"], + building = data["building"], + venue = data["venue"], + type = data["type"], + description = data["description"], + externalId = data["externalId"], + activeFrom = data["activeFrom"], + activeTo = data["activeTo"], + contact = data["contact"] != null ? _convertMap(MPDataField._fromJson, data["contact"]) : null, + fields = data["fields"] != null ? _convertMap(MPDataField._fromJson, data["fields"]) : null, + imageUrl = data["imageURL"], + locationType = data["locationType"], + anchor = MPPoint.fromJson(data["anchor"]), + bookable = data["bookable"]; + + static Map _convertMap(T Function(dynamic value) parser, Map map) { + return map.map((key, value) => MapEntry(key, parser.call(value))); + } + + String get name { + return _name!.replaceAll("\\n", "n"); + } + + @override + Map toJson() { + return { + "name": _name, + "aliases": aliases, + "categories": categories, + "floor": floorIndex.toString(), + "floorName": floorName, + "building": building, + "venue": venue, + "type": type, + "description": description, + "externalId": externalId, + "activeFrom": activeFrom, + "activeTo": activeTo, + "contact": contact, + "fields": fields, + "imageURL": imageUrl, + "locationType": locationType, + "anchor": anchor, + "bookable": bookable, + }; + } +} diff --git a/mapsindoors_platform_interface/lib/models/mp_query.dart b/mapsindoors_platform_interface/lib/models/mp_query.dart new file mode 100644 index 0000000..fabc61e --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_query.dart @@ -0,0 +1,65 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A collection of query parameters used to query MapsIndoors +class MPQuery extends MapsIndoorsObject { + static MPQueryBuilder builder() => MPQueryBuilder(); + + /// The queried text (search text) + final String? query; + + /// The query is restricted to entities near this point + final MPPoint? near; + + /// A list of properties for this query + final List? queryProperties; + + const MPQuery._(this.query, this.near, this.queryProperties); + + /// Converts the [MPQuery] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + return { + "query": query, + "near": near?.toJson(), + "queryProperties": queryProperties, + }; + } +} + +/// A builder for [MPQuery] +class MPQueryBuilder { + String? _query; + MPPoint? _near; + List? _queryProperties; + + /// The [query] text + void setQuery(String query) { + _query = query; + } + + /// The point where the queried target is [near] + void setNear(MPPoint near) { + _near = near; + } + + /// Set the coordinates where the queried target is near + void setNearWithCoordinates({required num longitude, required num latitude}) { + _near = MPPoint.withCoordinates(latitude: latitude, longitude: longitude); + } + + /// A list of query [properties], see [MPLocationPropertyNames] + void setQueryProperties(List properties) { + _queryProperties = properties.toList(growable: true); + } + + /// Add a single query [property], see [MPLocationPropertyNames] + void addQueryProperty(String property) { + _queryProperties ??= List.empty(growable: true); + _queryProperties?.add(property); + } + + /// Construct a [MPQuery] + MPQuery build() { + return MPQuery._(_query, _near, _queryProperties); + } +} diff --git a/mapsindoors_platform_interface/lib/models/mp_route_result.dart b/mapsindoors_platform_interface/lib/models/mp_route_result.dart new file mode 100644 index 0000000..3c6c7b1 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_route_result.dart @@ -0,0 +1,9 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Object that is returned from MPDirectionsSerivce.getRoute +class MPRouteResult { + final MPRoute? route; + final MPError? error; + + const MPRouteResult._(this.route, this.error); +} diff --git a/mapsindoors_platform_interface/lib/models/mp_settings_3d.dart b/mapsindoors_platform_interface/lib/models/mp_settings_3d.dart new file mode 100644 index 0000000..d590609 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_settings_3d.dart @@ -0,0 +1,38 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// An object that governs layer settings for 3D features. +/// +/// Can be aquired with [MPSolutionConfig.settings3D] +class MPSettings3D { + num? _extrusionOpacity; + num? _wallOpacity; + + /// Attempts to build a [MPSettings3D] from a JSON object, this method will decode the object if needed + static MPSettings3D? fromJson(json) => json != null && json != "null" ? MPSettings3D._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPSettings3D._fromJson(data) + : _extrusionOpacity = data["extrusionOpacity"], + _wallOpacity = data["wallOpacity"]; + + /// Get the opacity of extruded rooms + num? get extrusionOpacity => _extrusionOpacity; + + /// Get the opacity of extruded walls + num? get wallOpacity => _wallOpacity; + + /// Set the opacity of extruded rooms + set extrusionOpacity(num? opacity) { + if (opacity != null) { + _extrusionOpacity = opacity; + UtilPlatform.instance.setExtrusionOpacity(opacity); + } + } + + /// Set the opacity of extruded walls + set wallOpacity(num? opacity) { + if (opacity != null) { + _wallOpacity = opacity; + UtilPlatform.instance.setWallOpacity(opacity); + } + } +} diff --git a/mapsindoors_platform_interface/lib/models/mp_solution.dart b/mapsindoors_platform_interface/lib/models/mp_solution.dart new file mode 100644 index 0000000..e79cb41 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_solution.dart @@ -0,0 +1,60 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// Governs the topmost +class MPSolution { + /// The solution's [config]uration object + final MPSolutionConfig config; + + /// The solution's [id] + final String id; + + /// The id of the solution in customer systems + final String? customerId; + + /// The solution's [name] + final String name; + + /// The solution's default language + final String defaultLanguage; + + /// A list of available languages for the solution + final List availableLanguages; + + /// The URL used to fetch data from the CMS + final String? mapClientUrl; + + /// The solution's location template + final String? locationTemplate; + final List _modules; + + /// Attempts to build a [MPSolution] from a JSON object, this method will decode the object if needed + static MPSolution? fromJson(json) => json != null && json != "null" ? MPSolution._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPSolution._fromJson(data) + : config = MPSolutionConfig.fromJson(data["solutionConfig"])!, + id = data["id"], + customerId = data["customerId"], + name = data["name"], + defaultLanguage = data["defaultLanguage"], + availableLanguages = convertJsonArray(data["availableLanguages"]), + mapClientUrl = data["mapClientUrl"], + locationTemplate = data["locationTemplate"], + _modules = data["modules"].cast(); + + /// Parses a [venueId] and a [locationId] to create a [mapClientUrl] + Future parseMapClientUrl(String venueId, String locationId) async { + return await UtilPlatform.instance.parseMapClientUrl(venueId, locationId); + } + + /// Check if the solution has support for a [language] + bool hasLanguage(String? language) => language != null ? availableLanguages.contains(language) : false; + + /// Check whether the 22nd zoom level is available for select map providers + bool isZoom22Enabled() => _modules.contains("z22"); + + /// Check whether wall extrusions are available for select map providers + bool is3DWallsEnabled() => _modules.contains("3dwalls"); + + /// Check whether room extrusions are available for select map providers + bool is3DExtrusionsEnabled() => _modules.contains("3dextrusions"); +} diff --git a/mapsindoors_platform_interface/lib/models/mp_solution_config.dart b/mapsindoors_platform_interface/lib/models/mp_solution_config.dart new file mode 100644 index 0000000..e373710 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_solution_config.dart @@ -0,0 +1,50 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// An object that governs solution level settings such as: +///
    +///
  • Marker clustering
  • +///
  • Marker collision handling
  • +///
  • Main Display Rule
  • +///
  • [MPSettings3D]
  • +///
+class MPSolutionConfig { + /// Settings related to 3D rendering + final MPSettings3D settings3D; + late MPCollisionHandling? _collisionHandling; + late bool? _enableClustering; + + /// Attempts to build a [MPSolutionConfig] from a JSON object, this method will decode the object if needed + static MPSolutionConfig? fromJson(json) => json != null && json != "null" ? MPSolutionConfig._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPSolutionConfig._fromJson(data) + : settings3D = MPSettings3D.fromJson(data["settings3D"])!, + _enableClustering = data["enableClustering"] { + if (data["collisionHandling"] is int) { + _collisionHandling = MPCollisionHandling.fromValue(data["collisionHandling"]); + } else { + _collisionHandling = MPCollisionHandling.fromValue(int.parse(data["collisionHandling"])); + } + } + + /// Get whether clustering is enabled + bool? get enableClustering => _enableClustering; + + /// Set whether clustering is enabled + set enableClustering(bool? enable) { + if (enable != null) { + _enableClustering = enable; + UtilPlatform.instance.setEnableClustering(enable); + } + } + + /// Get the type of [collisionHandling] that is enabled + MPCollisionHandling? get collisionHandling => _collisionHandling; + + /// Set the type of [collisionHandling] that is enabled + set collisionHandling(MPCollisionHandling? handling) { + if (handling != null) { + _collisionHandling = handling; + UtilPlatform.instance.setCollisionHandling(handling); + } + } +} diff --git a/mapsindoors_platform_interface/lib/models/mp_user_role.dart b/mapsindoors_platform_interface/lib/models/mp_user_role.dart new file mode 100644 index 0000000..0ff2e6f --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_user_role.dart @@ -0,0 +1,27 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A User Role that allows certain parts of the data to be viewed +class MPUserRole extends MapsIndoorsObject { + /// The [id] of the user role + late final String id; + + /// The [name] of the user role + late final String name; + + /// Attempts to build a [MPUserRole] from a JSON object, this method will decode the object if needed + static MPUserRole? fromJson(json) => json != null && json != "null" ? MPUserRole._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPUserRole._fromJson(data) { + id = data["id"]; + name = data["name"]; + } + + @override + bool operator ==(Object other) => other is MPUserRole && other.runtimeType == runtimeType && name.toLowerCase() == other.name.toLowerCase(); + + @override + int get hashCode => Object.hash(id, name); + + @override + Map toJson() => {"id": id, "name": name}; +} diff --git a/mapsindoors_platform_interface/lib/models/mp_venue_info.dart b/mapsindoors_platform_interface/lib/models/mp_venue_info.dart new file mode 100644 index 0000000..4069308 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/mp_venue_info.dart @@ -0,0 +1,19 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +class MPVenueInfo extends MapsIndoorsObject { + final String? _name; + final List? _aliases; + final String? _language; + final Map? _fields; + + static MPVenueInfo? fromJson(json) => json != null && json != "null" ? MPVenueInfo._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPVenueInfo._fromJson(data) + : _name = data["name"], + _aliases = data["aliases"] != null ? convertJsonArray(data["aliases"]) : null, + _language = data["language"], + _fields = (data["fields"] as Map?)?.map((key, value) => MapEntry(key, MPDataField.fromJson(value)!)); + + @override + Map toJson() => {"name": _name, "aliases": _aliases, "language": _language, "fields": _fields}; +} diff --git a/mapsindoors_platform_interface/lib/models/routing/mp_route.dart b/mapsindoors_platform_interface/lib/models/routing/mp_route.dart new file mode 100644 index 0000000..3033ff5 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/routing/mp_route.dart @@ -0,0 +1,50 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A route from a origin to a destination broken up into [MPRouteLeg] [legs] +class MPRoute extends MapsIndoorsObject { + /// The [legs] the route consists of + final List? legs; + + /// The [copyrights] for this route, if any + final String? copyrights; + + /// A [summary] of the route + final String? summary; + + /// Any [warnings] issued for any paths on the route + final List? warnings; + + /// All [restrictions] in place for the route + final List? restrictions; + + /// The outer [bounds] of the route + final MPBounds? bounds; + + const MPRoute._({this.legs, this.copyrights, this.summary, this.warnings, this.restrictions, this.bounds}); + + /// Converts the [MPRoute] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + var jsonLegs = legs?.map((e) => e.toJson()).toList(); + return {"legs": jsonLegs, "copyrights": copyrights, "summary": summary, "warnings": warnings, "restrictions": restrictions, "bounds": bounds?.toJson()}; + } + + /// Attempts to build a [MPRoute] from a JSON object, this method will decode the object if needed + static MPRoute? fromJson(json) => json != null && json != "null" ? MPRoute._fromJson(json is String ? jsonDecode(json) : json) : null; + + static MPRoute _fromJson(data) { + final copyrights = data["copyrights"]; + final summary = data["summary"]; + final warnings = convertJsonArray(data["warnings"]); + final restrictions = convertJsonArray(data["restrictions"]); + final list = convertJsonArray(data["legs"]); + final routeLegs = List.generate(list.length, (index) => MPRouteLeg.fromJson(list[index])); + routeLegs.removeWhere((element) => element == null); + final legs = routeLegs.cast(); + var bounds; + if (data["bounds"] != null) { + //TODO: fix bounds? + } + return MPRoute._(legs: legs, copyrights: copyrights, summary: summary, warnings: warnings, restrictions: restrictions, bounds: bounds); + } +} diff --git a/mapsindoors_platform_interface/lib/models/routing/mp_route_coordinate.dart b/mapsindoors_platform_interface/lib/models/routing/mp_route_coordinate.dart new file mode 100644 index 0000000..c01b615 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/routing/mp_route_coordinate.dart @@ -0,0 +1,33 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A coordinate used for routing, it is contained within a [MPRouteStep] +/// +/// Is the smallest building block of a [MPRoute] +class MPRouteCoordinate extends MapsIndoorsObject { + /// The latitude of the coordinate + final num? latitude; + + /// The longitude of the coordinate + final num? longitude; + + /// The index of the floor the coordinate is on + final num? floorIndex; + + /// The name of the floor the coordinate is on + final String? floorname; + + /// Converts the [MPRouteCoordinate] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + return {"lat": latitude, "lng": longitude, "zLevel": floorIndex, "floor_name": floorname}; + } + + /// Attempts to build a [MPRouteCoordinate] from a JSON object, this method will decode the object if needed + static MPRouteCoordinate? fromJson(json) => json != null && json != "null" ? MPRouteCoordinate._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPRouteCoordinate._fromJson(data) + : latitude = data["lat"], + longitude = data["lng"], + floorIndex = data["zLevel"], + floorname = data["floor_name"]; +} diff --git a/mapsindoors_platform_interface/lib/models/routing/mp_route_leg.dart b/mapsindoors_platform_interface/lib/models/routing/mp_route_leg.dart new file mode 100644 index 0000000..3db5eb4 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/routing/mp_route_leg.dart @@ -0,0 +1,61 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A leg of a [MPRoute] is defined as all steps between any context shifts (entering/exiting buildings, changing floors) +/// A leg is comprised of a list of [steps] as well as a [startLocation] and an [endLocation] +class MPRouteLeg extends MapsIndoorsObject { + /// The start address of the leg + final String? startAddress; + + /// The end address of the leg + final String? endAddress; + + /// The start coordinate of the leg + final MPRouteCoordinate? startLocation; + + /// The end coordinate of the lg + final MPRouteCoordinate? endLocation; + + /// The [steps] the leg consists of + final List? steps; + + /// The distance of the leg + final MPRouteProperty? distance; + + /// The expected time it takes to traverse + final MPRouteProperty? duration; + + const MPRouteLeg._({this.startAddress, this.endAddress, this.startLocation, this.endLocation, this.steps, this.distance, this.duration}); + + /// Converts the [MPRouteLeg] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + var jsonMPRouteSteps = steps?.map((e) => e.toJson()).toList(); + return { + "start_address": startAddress, + "end_address": endAddress, + "start_location": startLocation?.toJson(), + "end_location": endLocation?.toJson(), + "steps": jsonMPRouteSteps, + "distance": distance?.toJson(), + "duration": duration?.toJson() + }; + } + + /// Attempts to build a [MPRouteLeg] from a JSON object, this method will decode the object if needed + static MPRouteLeg? fromJson(json) => json != null && json != "null" ? MPRouteLeg._fromJson(json is String ? jsonDecode(json) : json) : null; + + static MPRouteLeg _fromJson(data) { + final endAddress = data["end_address"]; + final startAddress = data["start_address"]; + final distance = (data["distance"] is num) ? MPRouteProperty.fromJson(data["distance"]) : MPRouteProperty.fromJson(data["distance"]); + final duration = (data["duration"] is num) ? MPRouteProperty.fromJson(data["duration"]) : MPRouteProperty.fromJson(data["duration"]); + final startLocation = MPRouteCoordinate.fromJson(data["start_location"]); + final endLocation = MPRouteCoordinate.fromJson(data["end_location"]); + final list = convertJsonArray(data["steps"]); + final routeSteps = List.generate(list.length, (index) => MPRouteStep.fromJson(list[index])); + routeSteps.removeWhere((element) => element == null); + final steps = routeSteps.cast(); + + return MPRouteLeg._(startAddress: startAddress, endAddress: endAddress, startLocation: startLocation, endLocation: endLocation, steps: steps, distance: distance, duration: duration); + } +} diff --git a/mapsindoors_platform_interface/lib/models/routing/mp_route_property.dart b/mapsindoors_platform_interface/lib/models/routing/mp_route_property.dart new file mode 100644 index 0000000..7756400 --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/routing/mp_route_property.dart @@ -0,0 +1,25 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A property of of a [MPRoute] +class MPRouteProperty extends MapsIndoorsObject { + /// The value of the property + final num? value; + + /// Accompanying text to the value + final String? text; + final String? timeZone; + + /// Converts the [MPRouteProperty] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + return {"value": value, "text": text, "time_zone": timeZone}; + } + + /// Attempts to build a [MPRouteProperty] from a JSON object, this method will decode the object if needed + static MPRouteProperty? fromJson(json) => json != null && json != "null" ? MPRouteProperty._fromJson(json is String ? jsonDecode(json) : json) : null; + + MPRouteProperty._fromJson(data) + : value = data["value"], + text = data["text"], + timeZone = data["time_zone"]; +} diff --git a/mapsindoors_platform_interface/lib/models/routing/mp_route_step.dart b/mapsindoors_platform_interface/lib/models/routing/mp_route_step.dart new file mode 100644 index 0000000..68fa25d --- /dev/null +++ b/mapsindoors_platform_interface/lib/models/routing/mp_route_step.dart @@ -0,0 +1,115 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// A step of a [MPRoute], the step is usually contained in a [MPRouteLeg] +class MPRouteStep extends MapsIndoorsObject { + /// The [distance] of the step + final MPRouteProperty? distance; + + /// The estimated [duration] of the step + final MPRouteProperty? duration; + + /// The origin of the step + final MPRouteCoordinate? startLocation; + + /// The destination of the step + final MPRouteCoordinate? endLocation; + + /// The coordinates the step is made up of + final List? geometry; + + /// The type of [MPHighway] of the step + final String? highway; + + /// Some [abutters] set on the step + final String? abutters; + + /// The step [maneuver] embedded in HTML + final String? htmlInstructions; + + /// The steps' [maneuver], eg. "Straight", "Turn lef" + final String? maneuver; + + /// How the step is traversed + final String? travelMode; + + /// A list of substeps if any + final List? steps; + + /// A list of modes it is possible to travel the step with (eg. a bike path can both be walked on, as well as biked on) + final List? availableTravelModes; + + const MPRouteStep._({this.distance, this.duration, this.startLocation, this.endLocation, this.geometry, this.highway, this.abutters, this.htmlInstructions, this.maneuver, this.travelMode, this.steps, this.availableTravelModes}); + + /// Converts the [MPRouteStep] to a JSON representation that can be parsed by the MapsIndoors Platform SDK + @override + Map toJson() { + var jsonRouteSteps = steps?.map((e) => e.toJson()).toList(); + var jsonGeometry = geometry?.map((e) => e.toJson()).toList(); + return { + "distance": distance?.toJson(), + "duration": duration?.toJson(), + "start_location": startLocation?.toJson(), + "end_location": endLocation?.toJson(), + "geometry": jsonGeometry, + "highway": highway, + "abutters": abutters, + "html_instructions": htmlInstructions, + "maneuver": maneuver, + "travel_mode": travelMode, + "steps": jsonRouteSteps, + "available_travel_modes": availableTravelModes + }; + } + + /// Attempts to build a [MPRouteStep] from a JSON object, this method will decode the object if needed + static MPRouteStep? fromJson(json) => json != null && json != "null" ? MPRouteStep._fromJson(json is String ? jsonDecode(json) : json) : null; + + static MPRouteStep _fromJson(data) { + final distance = (data["distance"] is num) ? MPRouteProperty.fromJson(data["distance"]) : MPRouteProperty.fromJson(data["distance"]); + final duration = (data["duration"] is num) ? MPRouteProperty.fromJson(data["duration"]) : MPRouteProperty.fromJson(data["duration"]); + final startLocation = MPRouteCoordinate.fromJson(data["start_location"]); + final endLocation = MPRouteCoordinate.fromJson(data["end_location"]); + var highway; + if (data["highway"] is String) { + highway = data["highway"]; + } else if (data["highway"] != null) { + highway = data["highway"]["value"]; + } + final abutters = data["abutters"]; + final htmlInstructions = data["html_instructions"]; + final maneuver = data["maneuver"]; + final travelMode = data["travel_mode"]; + var availableTravelModes; + if (data["available_travel_modes"] != null) { + availableTravelModes = convertJsonArray(data["available_travel_modes"]); + } + var steps; + if (data["steps"] != null) { + var jsonRouteSteps = convertJsonArray(data["steps"]); + var routeSteps = List.generate(jsonRouteSteps.length, (index) => MPRouteStep.fromJson(jsonRouteSteps[index])); + routeSteps.removeWhere((element) => element == null); + steps = routeSteps.cast(); + } + + var geometry; + if (data["geometry"] != null) { + var jsonGeometry = convertJsonArray(data["geometry"]); + var routeGeometry = List.generate(jsonGeometry.length, (index) => MPRouteCoordinate.fromJson(jsonGeometry[index])); + routeGeometry.removeWhere((element) => element == null); + geometry = routeGeometry.cast(); + } + return MPRouteStep._( + distance: distance, + duration: duration, + startLocation: startLocation, + endLocation: endLocation, + highway: highway, + abutters: abutters, + htmlInstructions: htmlInstructions, + maneuver: maneuver, + travelMode: travelMode, + availableTravelModes: availableTravelModes, + steps: steps, + geometry: geometry); + } +} diff --git a/mapsindoors_platform_interface/lib/platform_library.dart b/mapsindoors_platform_interface/lib/platform_library.dart new file mode 100644 index 0000000..fd1da6b --- /dev/null +++ b/mapsindoors_platform_interface/lib/platform_library.dart @@ -0,0 +1,98 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; +import 'dart:convert'; +import 'dart:math'; +import 'dart:io' show Platform; +import 'package:flutter/foundation.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +part 'directionsrenderer_platform_interface.dart'; +part 'directionsservice_platform_interface.dart'; +part 'display_rule_platform_interface.dart'; +part 'mapcontrol_platform_interface.dart'; +part 'mapsindoors_platform_interface.dart'; +part 'util_platform_interface.dart'; + +part 'src/directionsrenderer_method_channel.dart'; +part 'src/directionsservice_method_channel.dart'; +part 'src/display_rule_method_channel.dart'; +part 'src/mapcontrol_method_channel.dart'; +part 'src/mapsindoors_method_channel.dart'; +part 'src/util_method_channel.dart'; + +part 'src/util.dart'; + +part 'models/geometries/mp_geometry.dart'; +part 'models/geometries/mp_bounds.dart'; +part 'models/geometries/mp_point.dart'; +part 'models/geometries/mp_polygon.dart'; +part 'models/geometries/mp_multi_polygon.dart'; + +part 'models/entities/mp_entity.dart'; +part 'models/entities/mp_venue.dart'; +part 'models/entities/mp_building.dart'; +part 'models/entities/mp_floor.dart'; +part 'models/entities/mp_location.dart'; + +part 'models/enums/mp_collision_handling.dart'; +part 'models/enums/mp_location_propery_names.dart'; +part 'models/enums/mp_location_type.dart'; +part 'models/enums/mp_solution_display_rule.dart'; +part 'models/enums/mp_camera_event.dart'; +part 'models/enums/mp_camera_view_fit_mode.dart'; +part 'models/enums/live_data_domain_types.dart'; +part 'models/enums/mp_highway.dart'; + +part 'models/listeners/mp_camera_event_listener.dart'; +part 'models/listeners/on_building_found_at_camera_target_listener.dart'; +part 'models/listeners/on_floor_selection_changed_listener.dart'; +part 'models/listeners/on_floor_update_listener.dart'; +part 'models/listeners/on_location_selected_listener.dart'; +part 'models/listeners/on_map_click_listener.dart'; +part 'models/listeners/on_mapsindoors_ready_listener.dart'; +part 'models/listeners/on_marker_click_listener.dart'; +part 'models/listeners/on_marker_info_window_click_listener.dart'; +part 'models/listeners/on_venue_found_at_camera_target_listener.dart'; +part 'models/listeners/on_position_update_listener.dart'; +part 'models/listeners/on_leg_selected_listener.dart'; +part 'models/listeners/on_live_location_update_listener.dart'; +part 'models/listeners/on_map_ready_listener.dart'; + +part 'models/routing/mp_route.dart'; +part 'models/routing/mp_route_leg.dart'; +part 'models/routing/mp_route_step.dart'; +part 'models/routing/mp_route_coordinate.dart'; +part 'models/routing/mp_route_property.dart'; + +part 'models/mp_display_rule.dart'; +part 'models/mp_error.dart'; +part 'models/mp_filter.dart'; +part 'models/mp_query.dart'; +part 'models/mp_floor_selector_interface.dart'; +part 'models/mp_map_behavior.dart'; +part 'models/mp_position_provider_interface.dart'; +part 'models/mp_position_result_interface.dart'; +part 'models/mp_building_info.dart'; +part 'models/mp_venue_info.dart'; +part 'models/mp_property_data.dart'; +part 'models/mp_solution.dart'; +part 'models/mp_solution_config.dart'; +part 'models/mp_settings_3d.dart'; +part 'models/mp_category.dart'; +part 'models/mp_data_field.dart'; +part 'models/mp_geocode_result.dart'; +part 'models/mp_user_role.dart'; +part 'models/mp_icon_size.dart'; +part 'models/mp_map_style.dart'; +part 'models/mp_route_result.dart'; + +part 'models/map/mp_camera_update.dart'; +part 'models/map/mp_camera_position.dart'; + +part 'models/collections/mp_collection.dart'; +part 'models/collections/mp_building_collection.dart'; +part 'models/collections/mp_category_collection.dart'; +part 'models/collections/mp_user_role_collection.dart'; +part 'models/collections/mp_venue_collection.dart'; diff --git a/mapsindoors_platform_interface/lib/src/directionsrenderer_method_channel.dart b/mapsindoors_platform_interface/lib/src/directionsrenderer_method_channel.dart new file mode 100644 index 0000000..3b1912e --- /dev/null +++ b/mapsindoors_platform_interface/lib/src/directionsrenderer_method_channel.dart @@ -0,0 +1,77 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +class MethodChannelDirectionsRenderer extends DirectionsRendererPlatform { + final directionsRendererMethodChannel = const MethodChannel('DirectionsRendererMethodChannel'); + + OnLegSelectedListener? _onLegSelectedListener; + + MethodChannelDirectionsRenderer() { + directionsRendererMethodChannel.setMethodCallHandler((call) => _listenerHandler(call)); + } + + Future _listenerHandler(MethodCall call) async { + switch (call.method) { + case "onLegSelected": + { + final int legIndex = call.arguments; + _onLegSelectedListener?.call(legIndex); + } + } + } + + @override + Future clear() => directionsRendererMethodChannel.invokeMethod('DRE_clear'); + + @override + Future nextLeg() => directionsRendererMethodChannel.invokeMethod('DRE_nextLeg'); + + @override + Future previousLeg() => directionsRendererMethodChannel.invokeMethod('DRE_previousLeg'); + + @override + Future setRoute(MPRoute? route) => directionsRendererMethodChannel.invokeMethod('DRE_setRoute', {"route": jsonEncode(route)}); + + @override + Future setAnimatedPolyline(bool animated, bool repeating, int durationMs) => directionsRendererMethodChannel.invokeMethod('DRE_setAnimatedPolyline', {"animated": animated, "repeating": repeating, "durationMs": durationMs}); + + @override + Future setPolyLineColors(String foreground, String background) => directionsRendererMethodChannel.invokeMethod('DRE_setPolyLineColors', {"foreground": foreground, "background": background}); + + @override + Future selectLegIndex(int legIndex) => directionsRendererMethodChannel.invokeMethod('DRE_selectLegIndex', {"legIndex": legIndex}); + + @override + Future getSelectedLegFloorIndex() => directionsRendererMethodChannel.invokeMethod('DRE_getSelectedLegFloorIndex'); + + @override + Future setCameraAnimationDuration(int durationMs) => directionsRendererMethodChannel.invokeMethod('DRE_setCameraAnimationDuration', {"durationMs": durationMs}); + + @override + Future setCameraViewFitMode(MPCameraViewFitMode mpCameraViewFitMode) { + late int cameraFitMode; + switch (mpCameraViewFitMode) { + case MPCameraViewFitMode.northAligned: + cameraFitMode = 0; + break; + case MPCameraViewFitMode.firstStepAligned: + cameraFitMode = 1; + break; + case MPCameraViewFitMode.startToEndAligned: + cameraFitMode = 2; + break; + } + return directionsRendererMethodChannel.invokeMethod('DRE_setCameraViewFitMode', {"cameraFitMode": cameraFitMode}); + } + + @override + Future setOnLegSelectedListener(OnLegSelectedListener? onLegSelectedListener) { + _onLegSelectedListener = onLegSelectedListener; + return directionsRendererMethodChannel.invokeMethod('DRE_setOnLegSelectedListener'); + } + + @override + Future useContentOfNearbyLocations() { + // TODO: implement useContentOfNearbyLocations + return Future(() {}); + } +} diff --git a/mapsindoors_platform_interface/lib/src/directionsservice_method_channel.dart b/mapsindoors_platform_interface/lib/src/directionsservice_method_channel.dart new file mode 100644 index 0000000..1fc5525 --- /dev/null +++ b/mapsindoors_platform_interface/lib/src/directionsservice_method_channel.dart @@ -0,0 +1,51 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +class MethodChannelDirectionsService extends DirectionsServicePlatform { + final directionsServiceMethodChannel = const MethodChannel('DirectionsServiceMethodChannel'); + + @override + Future create() { + return directionsServiceMethodChannel.invokeMethod('DSE_create'); + } + + @override + Future addAvoidWayType(String wayType) { + return directionsServiceMethodChannel.invokeMethod('DSE_addAvoidWayType', {"wayType": wayType}); + } + + @override + Future clearWayType() { + return directionsServiceMethodChannel.invokeMethod('DSE_clearWayType'); + } + + @override + Future setIsDeparture(bool isDeparture) { + return directionsServiceMethodChannel.invokeMethod('DSE_setIsDeparture', {"isDeparture": isDeparture}); + } + + @override + Future getRoute(MPPoint origin, MPPoint destination) async { + Map? result = await directionsServiceMethodChannel.invokeMapMethod('DSE_getRoute', {'origin': origin._jsonEncode(), 'destination': destination._jsonEncode()}); + + final route = MPRoute.fromJson(result?["route"]); + final error = MPError.fromJson(result?["error"]); + + if (route != null) { + return Future.value(route); + } else if (error != null) { + return Future.error(error); + } else { + return Future.error(MPError(code: MPError.unknownError, message: "Did not receieve route object from Platform")); + } + } + + @override + Future setTravelMode(String travelMode) { + return directionsServiceMethodChannel.invokeMethod('DSE_setTravelMode', {"travelMode": travelMode}); + } + + @override + Future setTime(int time) { + return directionsServiceMethodChannel.invokeMethod('DSE_setTime', {"time": time}); + } +} diff --git a/mapsindoors_platform_interface/lib/src/display_rule_method_channel.dart b/mapsindoors_platform_interface/lib/src/display_rule_method_channel.dart new file mode 100644 index 0000000..ad5ba6a --- /dev/null +++ b/mapsindoors_platform_interface/lib/src/display_rule_method_channel.dart @@ -0,0 +1,374 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// An implementation of [MapsindoorsPlatform] that uses method channels. +class MethodChannelDisplayRule extends DisplayRulePlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final displayRuleMethodChannel = const MethodChannel('DisplayRuleMethodChannel'); + + @override + Future isVisible(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_isVisible", {"id": id.value}); + } + + @override + Future setVisible(MPDisplayRuleId id, bool visible) { + return displayRuleMethodChannel.invokeMethod("DRU_setVisible", {"id": id.value, "visible": visible}); + } + + @override + Future getIconSize(MPDisplayRuleId id) async { + final ret = await displayRuleMethodChannel.invokeMethod("DRU_getIconSize", {"id": id.value}); + return MPIconSize.fromJson(ret); + } + + @override + Future getIconUrl(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getIconUrl", {"id": id.value}); + } + + @override + Future getLabel(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getLabel", {"id": id.value}); + } + + @override + Future getLabelMaxWidth(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getLabelMaxWidth", {"id": id.value}); + } + + @override + Future getLabelZoomFrom(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getLabelZoomFrom", {"id": id.value}); + } + + @override + Future getLabelZoomTo(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getLabelZoomTo", {"id": id.value}); + } + + @override + Future getModel2DBearing(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getModel2DBearing", {"id": id.value}); + } + + @override + Future getModel2DHeightMeters(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getModel2DHeightMeters", {"id": id.value}); + } + + @override + Future getModel2DModel(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getModel2DModel", {"id": id.value}); + } + + @override + Future getModel2DZoomTo(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getModel2DZoomTo", {"id": id.value}); + } + + @override + Future getModel2DWidthMeters(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getModel2DWidthMeters", {"id": id.value}); + } + + @override + Future getModel2DZoomFrom(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getModel2DZoomFrom", {"id": id.value}); + } + + @override + Future getPolygonFillColor(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getPolygonFillColor", {"id": id.value}); + } + + @override + Future getPolygonFillOpacity(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getPolygonFillOpacity", {"id": id.value}); + } + + @override + Future getPolygonZoomTo(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getPolygonZoomTo", {"id": id.value}); + } + + @override + Future getPolygonStrokeColor(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getPolygonStrokeColor", {"id": id.value}); + } + + @override + Future getPolygonStrokeOpacity(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getPolygonStrokeOpacity", {"id": id.value}); + } + + @override + Future getPolygonStrokeWidth(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getPolygonStrokeWidth", {"id": id.value}); + } + + @override + Future getPolygonZoomFrom(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getPolygonZoomFrom", {"id": id.value}); + } + + @override + Future getZoomFrom(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getZoomFrom", {"id": id.value}); + } + + @override + Future getZoomTo(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getZoomTo", {"id": id.value}); + } + + @override + Future isIconVisible(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_isIconVisible", {"id": id.value}); + } + + @override + Future isLabelVisible(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_isLabelVisible", {"id": id.value}); + } + + @override + Future isModel2DVisible(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_isModel2DVisible", {"id": id.value}); + } + + @override + Future isPolygonVisible(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_isPolygonVisible", {"id": id.value}); + } + + @override + Future setIcon(MPDisplayRuleId id, String url) { + return displayRuleMethodChannel.invokeMethod("DRU_setIcon", {"id": id.value, "url": url}); + } + + @override + Future setIconSize(MPDisplayRuleId id, MPIconSize size) { + return displayRuleMethodChannel.invokeMethod("DRU_setIconSize", {"id": id.value, "size": size._jsonEncode()}); + } + + @override + Future setIconVisible(MPDisplayRuleId id, bool visible) { + return displayRuleMethodChannel.invokeMethod("DRU_setIconVisible", {"id": id.value, "visible": visible}); + } + + @override + Future setLabel(MPDisplayRuleId id, String label) { + return displayRuleMethodChannel.invokeMethod("DRU_setLabel", {"id": id.value, "label": label}); + } + + @override + Future setLabelMaxWidth(MPDisplayRuleId id, int max) { + return displayRuleMethodChannel.invokeMethod("DRU_setLabelMaxWidth", {"id": id.value, "maxWidth": max}); + } + + @override + Future setLabelVisible(MPDisplayRuleId id, bool visible) { + return displayRuleMethodChannel.invokeMethod("DRU_setLabelVisible", {"id": id.value, "visible": visible}); + } + + @override + Future setLabelZoomFrom(MPDisplayRuleId id, num from) { + return displayRuleMethodChannel.invokeMethod("DRU_setLabelZoomFrom", {"id": id.value, "zoomFrom": from}); + } + + @override + Future setLabelZoomTo(MPDisplayRuleId id, num to) { + return displayRuleMethodChannel.invokeMethod("DRU_setLabelZoomTo", {"id": id.value, "zoomTo": to}); + } + + @override + Future setModel2DBearing(MPDisplayRuleId id, num bearing) { + return displayRuleMethodChannel.invokeMethod("DRU_setModel2DBearing", {"id": id.value, "bearing": bearing}); + } + + @override + Future setModel2DModel(MPDisplayRuleId id, String model) { + return displayRuleMethodChannel.invokeMethod("DRU_setModel2DModel", {"id": id.value, "model": model}); + } + + @override + Future setModel2DHeightMeters(MPDisplayRuleId id, num height) { + return displayRuleMethodChannel.invokeMethod("DRU_setModel2DHeightMeters", {"id": id.value, "height": height}); + } + + @override + Future setModel2DVisible(MPDisplayRuleId id, bool visible) { + return displayRuleMethodChannel.invokeMethod("DRU_setModel2DVisible", {"id": id.value, "visible": visible}); + } + + @override + Future setModel2DWidthMeters(MPDisplayRuleId id, num width) { + return displayRuleMethodChannel.invokeMethod("DRU_setModel2DWidthMeters", {"id": id.value, "width": width}); + } + + @override + Future setModel2DZoomFrom(MPDisplayRuleId id, num from) { + return displayRuleMethodChannel.invokeMethod("DRU_setModel2DZoomFrom", {"id": id.value, "zoomFrom": from}); + } + + @override + Future setModel2DZoomTo(MPDisplayRuleId id, num to) { + return displayRuleMethodChannel.invokeMethod("DRU_setModel2DZoomTo", {"id": id.value, "zoomTo": to}); + } + + @override + Future setPolygonFillColor(MPDisplayRuleId id, String color) { + return displayRuleMethodChannel.invokeMethod("DRU_setPolygonFillColor", {"id": id.value, "color": color}); + } + + @override + Future setPolygonFillOpacity(MPDisplayRuleId id, num opacity) { + return displayRuleMethodChannel.invokeMethod("DRU_setPolygonFillOpacity", {"id": id.value, "opacity": opacity}); + } + + @override + Future setPolygonStrokeColor(MPDisplayRuleId id, String color) { + return displayRuleMethodChannel.invokeMethod("DRU_setPolygonStrokeColor", {"id": id.value, "color": color}); + } + + @override + Future setPolygonStrokeOpacity(MPDisplayRuleId id, num opacity) { + return displayRuleMethodChannel.invokeMethod("DRU_setPolygonStrokeOpacity", {"id": id.value, "opacity": opacity}); + } + + @override + Future setPolygonStrokeWidth(MPDisplayRuleId id, num width) { + return displayRuleMethodChannel.invokeMethod("DRU_setPolygonStrokeWidth", {"id": id.value, "width": width}); + } + + @override + Future setPolygonVisible(MPDisplayRuleId id, bool visible) { + return displayRuleMethodChannel.invokeMethod("DRU_setPolygonVisible", {"id": id.value, "visible": visible}); + } + + @override + Future setPolygonZoomFrom(MPDisplayRuleId id, num from) { + return displayRuleMethodChannel.invokeMethod("DRU_setPolygonZoomFrom", {"id": id.value, "zoomFrom": from}); + } + + @override + Future setPolygonZoomTo(MPDisplayRuleId id, num to) { + return displayRuleMethodChannel.invokeMethod("DRU_setPolygonZoomTo", {"id": id.value, "zoomTo": to}); + } + + @override + Future setZoomFrom(MPDisplayRuleId id, num from) { + return displayRuleMethodChannel.invokeMethod("DRU_setZoomFrom", {"id": id.value, "zoomFrom": from}); + } + + @override + Future setZoomTo(MPDisplayRuleId id, num to) { + return displayRuleMethodChannel.invokeMethod("DRU_setZoomTo", {"id": id.value, "zoomTo": to}); + } + + @override + Future getExtrusionColor(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getExtrusionColor", {"id": id.value}); + } + + @override + Future getExtrusionHeight(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getExtrusionHeight", {"id": id.value}); + } + + @override + Future getExtrusionZoomFrom(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getExtrusionZoomFrom", {"id": id.value}); + } + + @override + Future getExtrusionZoomTo(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getExtrusionZoomTo", {"id": id.value}); + } + + @override + Future getWallColor(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getWallColor", {"id": id.value}); + } + + @override + Future getWallHeight(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getWallHeight", {"id": id.value}); + } + + @override + Future getWallZoomFrom(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getWallZoomFrom", {"id": id.value}); + } + + @override + Future getWallZoomTo(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_getWallZoomTo", {"id": id.value}); + } + + @override + Future isExtrusionVisible(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_isExtrusionVisible", {"id": id.value}); + } + + @override + Future isWallVisible(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_isWallVisible", {"id": id.value}); + } + + @override + Future setExtrusionColor(MPDisplayRuleId id, String color) { + return displayRuleMethodChannel.invokeMethod("DRU_setExtrusionColor", {"id": id.value, "color": color}); + } + + @override + Future setExtrusionHeight(MPDisplayRuleId id, num height) { + return displayRuleMethodChannel.invokeMethod("DRU_setExtrusionHeight", {"id": id.value, "height": height}); + } + + @override + Future setExtrusionVisible(MPDisplayRuleId id, bool visible) { + return displayRuleMethodChannel.invokeMethod("DRU_setExtrusionVisible", {"id": id.value, "visible": visible}); + } + + @override + Future setExtrusionZoomFrom(MPDisplayRuleId id, num from) { + return displayRuleMethodChannel.invokeMethod("DRU_setExtrusionZoomFrom", {"id": id.value, "zoomFrom": from}); + } + + @override + Future setExtrusionZoomTo(MPDisplayRuleId id, num to) { + return displayRuleMethodChannel.invokeMethod("DRU_setExtrusionZoomTo", {"id": id.value, "zoomTo": to}); + } + + @override + Future setWallColor(MPDisplayRuleId id, String color) { + return displayRuleMethodChannel.invokeMethod("DRU_setWallColor", {"id": id.value, "color": color}); + } + + @override + Future setWallHeight(MPDisplayRuleId id, num height) { + return displayRuleMethodChannel.invokeMethod("DRU_setWallHeight", {"id": id.value, "height": height}); + } + + @override + Future setWallVisible(MPDisplayRuleId id, bool visible) { + return displayRuleMethodChannel.invokeMethod("DRU_setWallVisible", {"id": id.value, "visible": visible}); + } + + @override + Future setWallZoomFrom(MPDisplayRuleId id, num from) { + return displayRuleMethodChannel.invokeMethod("DRU_setWallZoomFrom", {"id": id.value, "zoomFrom": from}); + } + + @override + Future setWallZoomTo(MPDisplayRuleId id, num to) { + return displayRuleMethodChannel.invokeMethod("DRU_setWallZoomTo", {"id": id.value, "zoomTo": to}); + } + + @override + Future reset(MPDisplayRuleId id) { + return displayRuleMethodChannel.invokeMethod("DRU_reset", {"id": id.value}); + } +} diff --git a/mapsindoors_platform_interface/lib/src/mapcontrol_method_channel.dart b/mapsindoors_platform_interface/lib/src/mapcontrol_method_channel.dart new file mode 100644 index 0000000..c90314b --- /dev/null +++ b/mapsindoors_platform_interface/lib/src/mapcontrol_method_channel.dart @@ -0,0 +1,385 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// An implementation of [MapcontrolPlatform] that uses method channels. +class MethodChannelMapControl extends MapcontrolPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final mapControlMethodChannel = const MethodChannel('MapControlMethodChannel'); + final listenerChannel = const MethodChannel('MapControlListenerMethodChannel'); + final floorSelectorChannel = const MethodChannel('MapControlFloorSelectorChannel'); + final List _cameraEventListeners = List.empty(growable: true); + final List _floorUpdateListeners = List.empty(growable: true); + + OnBuildingFoundAtCameraTargetListener? _buildingFoundAtCameraTargetListener; + OnVenueFoundAtCameraTargetListener? _venueFoundAtCameraTargetListener; + OnMarkerInfoWindowClickListener? _infoWindowClickListener; + OnLocationSelectedListener? _locationSelectedListener; + OnMarkerClickListener? _markerClickListener; + OnMapClickListener? _mapClickListener; + + OnMapReadyListener? onMapControlReady; + + final Map _liveUpdateListeners = Map(); + + MPFloorSelectorInterface? _floorSelector; + + MethodChannelMapControl() { + listenerChannel.setMethodCallHandler((call) => _listenerHandler(call)); + floorSelectorChannel.setMethodCallHandler((call) => _floorSelectorHandler(call)); + mapControlMethodChannel.setMethodCallHandler((call) => _mapControlHandler(call)); + } + + Future _mapControlHandler(MethodCall call) async { + switch (call.method) { + case "create": + final error = MPError.fromJson(call.arguments); + onMapControlReady?.call(error); + onMapControlReady = null; + break; + default: + throw UnimplementedError(); + } + } + + Future _listenerHandler(MethodCall call) async { + switch (call.method) { + case "onBuildingFoundAtCameraTarget": + final building = MPBuilding.fromJson(call.arguments); + _buildingFoundAtCameraTargetListener?.call(building); + break; + case "onVenueFoundAtCameraTarget": + final venue = MPVenue.fromJson(call.arguments); + _venueFoundAtCameraTargetListener?.call(venue); + break; + case "onInfoWindowClick": + final String locationId = call.arguments; + _infoWindowClickListener?.call(locationId); + break; + case "onLocationSelected": + final MPLocation? location = MPLocation.fromJson(call.arguments); + _locationSelectedListener?.call(location); + break; + + case "onMarkerClick": + final String locationId = call.arguments; + _markerClickListener?.call(locationId); + break; + case "onCameraEvent": + for (final listener in _cameraEventListeners) { + listener(call.arguments); + } + break; + case "onFloorUpdate": + num floor = call.arguments as num; + for (final listener in _floorUpdateListeners) { + listener(floor.toInt()); + } + break; + case "onMapClick": + final point = MPPoint.fromJson(call.arguments["point"]); + if (call.arguments["locations"] != null) { + final locations = (jsonDecode(call.arguments["locations"]) as List).map((e) => MPLocation.fromJson(e)!); + _mapClickListener?.call(point!, locations.toList()); + } else { + _mapClickListener?.call(point!, null); + } + break; + case "onLiveLocationUpdate": + final location = MPLocation.fromJson(call.arguments["location"]); + if (location != null) { + _liveUpdateListeners[call.arguments["type"] as String]?.call(location); + } + break; + default: + throw UnimplementedError(); + } + } + + Future _floorSelectorHandler(MethodCall call) async { + switch (call.method) { + case "setList": + final floors = convertMIList(jsonDecode(call.arguments), (p0) => MPFloor.fromJson(p0)); + _floorSelector?.floors = floors; + break; + case "show": + _floorSelector?.show(call.arguments["show"]); + break; + case "setSelectedFloor": + _floorSelector?.setSelectedFloor(MPFloor.fromJson(jsonDecode(call.arguments))!); + break; + case "setSelectedFloorByZIndex": + _floorSelector?.setSelectedFloorByFloorIndex(call.arguments); + break; + case "zoomLevelChanged": + _floorSelector?.zoomLevelChanged(call.arguments); + break; + case "setUserPositionFloor": + _floorSelector?.userPositionFloor = call.arguments; + break; + default: + throw UnimplementedError(); + } + } + + @override + Future setFloorSelector(MPFloorSelectorInterface? floorSelector, bool internal) { + _floorSelector = floorSelector; + _floorSelector?.onFloorSelectionChangedListener = onFloorSelectionChanged; + if (internal) { + return Future(() => null); + } else { + return mapControlMethodChannel.invokeMethod("MPC_setFloorSelector", {"isAutoFloorChangeEnabled": floorSelector?.isAutoFloorChangeEnabled}); + } + } + + @override + MPFloorSelectorInterface? getFloorSelector() { + return _floorSelector; + } + + void onFloorSelectionChanged(MPFloor newFloor) { + floorSelectorChannel.invokeMethod("FSE_onFloorChanged", {"floor": newFloor._jsonEncode()}); + } + + @override + Future selectFloor(int floorIndex) { + return mapControlMethodChannel.invokeMethod("MPC_selectFloor", {"floorIndex": floorIndex}); + } + + @override + Future clearFilter() { + return mapControlMethodChannel.invokeMethod("MPC_clearFilter"); + } + + @override + Future deSelectLocation() { + return mapControlMethodChannel.invokeMethod("MPC_deSelectLocation"); + } + + @override + Future getCurrentBuilding() async { + final buildingData = await mapControlMethodChannel.invokeMethod("MPC_getCurrentBuilding"); + return MPBuilding.fromJson(buildingData); + } + + @override + Future getCurrentBuildingFloor() async { + final floorData = await mapControlMethodChannel.invokeMethod("MPC_getCurrentBuildingFloor"); + return MPFloor.fromJson(floorData); + } + + @override + Future getCurrentFloorIndex() { + return mapControlMethodChannel.invokeMethod("MPC_getCurrentFloorIndex"); + } + + @override + Future getCurrentMapsIndoorsZoom() { + return mapControlMethodChannel.invokeMethod("MPC_getCurrentMapsIndoorsZoom"); + } + + @override + Future getCurrentVenue() async { + final venueData = await mapControlMethodChannel.invokeMethod("MPC_getCurrentVenue"); + return MPVenue.fromJson(venueData); + } + + @override + Future getMapStyle() async { + final mapStyleData = await mapControlMethodChannel.invokeMethod("MPC_getMapStyle"); + return MPMapStyle.fromJson(mapStyleData); + } + + @override + Future getMapViewPaddingBottom() { + return mapControlMethodChannel.invokeMethod("MPC_getMapViewPaddingBottom"); + } + + @override + Future getMapViewPaddingEnd() { + return mapControlMethodChannel.invokeMethod("MPC_getMapViewPaddingEnd"); + } + + @override + Future getMapViewPaddingStart() { + return mapControlMethodChannel.invokeMethod("MPC_getMapViewPaddingStart"); + } + + @override + Future getMapViewPaddingTop() { + return mapControlMethodChannel.invokeMethod("MPC_getMapViewPaddingTop"); + } + + @override + Future goTo(MPEntity? entity) { + return mapControlMethodChannel.invokeMethod("MPC_goTo", {"entity": entity?._jsonEncode(), "type": entity?.runtimeType.toString()}); + } + + @override + Future hideFloorSelector(bool hide) { + return mapControlMethodChannel.invokeMethod("MPC_hideFloorSelector", {"hide": hide}); + } + + @override + Future isFloorSelectorHidden() { + return mapControlMethodChannel.invokeMethod("MPC_isFloorSelectorHidden"); + } + + @override + Future isUserPositionShown() { + return mapControlMethodChannel.invokeMethod("MPC_isUserPositionShown"); + } + + @override + Future selectBuilding(MPBuilding building, bool moveCamera) { + return mapControlMethodChannel.invokeMethod("MPC_selectBuilding", {"building": building._jsonEncode(), "moveCamera": moveCamera}); + } + + @override + Future selectLocation(MPLocation? location, MPSelectionBehavior behavior) { + return mapControlMethodChannel.invokeMethod("MPC_selectLocation", {"location": location?.id.value, "behavior": behavior._jsonEncode()}); + } + + @override + Future selectLocationById(String id, MPSelectionBehavior behavior) { + return mapControlMethodChannel.invokeMethod("MPC_selectLocationById", {"id": id, "behavior": behavior._jsonEncode()}); + } + + @override + Future selectVenue(MPVenue venue, bool moveCamera) { + return mapControlMethodChannel.invokeMethod("MPC_selectVenue", {"venue": venue._jsonEncode(), "moveCamera": moveCamera}); + } + + @override + Future setFilter(MPFilter filter, MPFilterBehavior behavior) async { + final res = await mapControlMethodChannel.invokeMethod("MPC_setFilter", {"filter": filter._jsonEncode(), "behavior": behavior._jsonEncode()}); + return (res == "success"); + } + + @override + Future setFilterWithLocations(List locations, MPFilterBehavior behavior) { + return mapControlMethodChannel.invokeMethod("MPC_setFilterWithLocations", {"locations": locations.map((e) => e?.id.value).toList(), "behavior": behavior._jsonEncode()}); + } + + @override + Future setMapPadding(int start, int top, int end, int bottom) { + return mapControlMethodChannel.invokeMethod("MPC_setMapPadding", {"start": start, "top": top, "end": end, "bottom": bottom}); + } + + @override + Future setMapStyle(MPMapStyle mapstyle) { + return mapControlMethodChannel.invokeMethod("MPC_setMapStyle", {"mapStyle": mapstyle._jsonEncode()}); + } + + @override + Future showInfoWindowOnClickedLocation(bool show) { + return mapControlMethodChannel.invokeMethod("MPC_showInfoWindowOnClickedLocation", {"show": show}); + } + + @override + Future showUserPosition(bool show) { + return mapControlMethodChannel.invokeMethod("MPC_showUserPosition", {"show": show}); + } + + @override + Future disableLiveData(String domainType) { + return mapControlMethodChannel.invokeMethod("MPC_disableLiveData", {"domainType": domainType}); + } + + @override + Future enableLiveData(String domainType, OnLiveLocationUpdateListener? listener) { + if (listener != null) { + _liveUpdateListeners.putIfAbsent(domainType, () => listener); + } + return mapControlMethodChannel.invokeMethod("MPC_enableLiveData", {"domainType": domainType, "listener": (listener != null)}); + } + + @override + void addOnCameraEventListner(MPCameraEventListener listener) { + if (_cameraEventListeners.isEmpty) { + listenerChannel.invokeMethod("MPL_cameraEventListener", {"setupListener": true}); + } + _cameraEventListeners.add(listener); + } + + @override + void addOnFloorUpdateListener(OnFloorUpdateListener listener) { + if (_floorUpdateListeners.isEmpty) { + listenerChannel.invokeMethod("MPL_floorUpdateListener", {"setupListener": true}); + } + _floorUpdateListeners.add(listener); + } + + @override + void setOnCurrentBuildingChangedListener(OnBuildingFoundAtCameraTargetListener? listener) { + listenerChannel.invokeMethod("MPL_buildingFoundAtCameraTargetListener", {"setupListener": listener != null}); + _buildingFoundAtCameraTargetListener = listener; + } + + @override + void setOnCurrentVenueChangedListener(OnVenueFoundAtCameraTargetListener? listener) { + listenerChannel.invokeMethod("MPL_venueFoundAtCameraTargetListener", {"setupListener": listener != null}); + _venueFoundAtCameraTargetListener = listener; + } + + @override + void removeOnCameraEventListner(MPCameraEventListener listener) { + _cameraEventListeners.remove(listener); + if (_cameraEventListeners.isEmpty) { + listenerChannel.invokeMethod("MPL_cameraEventListener", {"setupListener": false}); + } + } + + @override + void removeOnFloorUpdateListener(OnFloorUpdateListener listener) { + _floorUpdateListeners.remove(listener); + if (_floorUpdateListeners.isEmpty) { + listenerChannel.invokeMethod("MPL_floorUpdateListener", {"setupListener": false}); + } + } + + @override + void setOnLocationSelectedListener(OnLocationSelectedListener? listener, bool? consumeEvent) { + listenerChannel.invokeMethod("MPL_locationSelectedListener", {"setupListener": listener != null, "consumeEvent": consumeEvent}); + _locationSelectedListener = listener; + } + + @override + void setOnMapClickListener(OnMapClickListener? listener, bool? consumeEvent) { + listenerChannel.invokeMethod("MPL_mapClickListener", {"setupListener": listener != null, "consumeEvent": consumeEvent}); + _mapClickListener = listener; + } + + @override + void setOnMarkerClickListener(OnMarkerClickListener? listener, bool? consumeEvent) { + listenerChannel.invokeMethod("MPL_markerClickListener", {"setupListener": listener != null, "consumeEvent": consumeEvent}); + _markerClickListener = listener; + } + + @override + void setOnMarkerInfoWindowClickListener(OnMarkerInfoWindowClickListener? listener) { + listenerChannel.invokeMethod("MPL_markerInfoWindowClickListener", {"setupListener": listener != null}); + _infoWindowClickListener = listener; + } + + @override + Future animateCamera(MPCameraUpdate update, [int? duration]) { + return mapControlMethodChannel.invokeMethod("MPC_animateCamera", {"update": update._jsonEncode(), "duration": duration}); + } + + @override + Future currentCameraPosition() async { + final res = await mapControlMethodChannel.invokeMethod("MPC_getCurrentCameraPosition"); + return MPCameraPosition._fromJson(jsonDecode(res)); + } + + @override + Future moveCamera(MPCameraUpdate update) { + return mapControlMethodChannel.invokeMethod("MPC_moveCamera", {"update": update._jsonEncode()}); + } + + @override + void setOnMapControlReadyListener(OnMapReadyListener listener) { + onMapControlReady = listener; + } +} diff --git a/mapsindoors_platform_interface/lib/src/mapsindoors_method_channel.dart b/mapsindoors_platform_interface/lib/src/mapsindoors_method_channel.dart new file mode 100644 index 0000000..062529e --- /dev/null +++ b/mapsindoors_platform_interface/lib/src/mapsindoors_method_channel.dart @@ -0,0 +1,227 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// An implementation of [MapsindoorsPlatform] that uses method channels. +class MethodChannelMapsindoors extends MapsindoorsPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final mapsIndoorsMethodChannel = const MethodChannel('MapsIndoorsMethodChannel'); + final listenerChannel = const MethodChannel('MapsIndoorsListenerChannel'); + final List _readyListeners = List.empty(growable: true); + + MPPositionProviderInterface? _positionProvider; + + MethodChannelMapsindoors() { + listenerChannel.setMethodCallHandler((call) => _listenerHandler(call)); + } + + Future _listenerHandler(MethodCall call) async { + switch (call.method) { + case "onMapsIndoorsReady": + final MPError? error = MPError.fromJson(call.arguments); + for (var listener in _readyListeners) { + listener.call(error); + } + break; + + default: + throw UnimplementedError(); + } + } + + void onPositionUpdate(MPPositionResultInterface position) { + listenerChannel.invokeMethod('MIL_onPositionUpdate', {"position": position._jsonEncode()}); + } + + @override + Future load(String key) async { + final error = await mapsIndoorsMethodChannel.invokeMethod('MIN_initialize', {"key": key}); + return MPError.fromJson(error); + } + + @override + Future locationDisplayRuleExists(MPLocationId location) async { + return await mapsIndoorsMethodChannel.invokeMethod("MIN_locationDisplayRuleExists", {"id": location.value}); + } + + @override + Future displayRuleNameExists(String name) async { + return await mapsIndoorsMethodChannel.invokeMethod("MIN_displayRuleNameExists", {"name": name}); + } + + @override + void addOnMapsIndoorsReadyListener(OnMapsIndoorsReadyListener listener) { + if (_readyListeners.isEmpty) { + listenerChannel.invokeMethod("MIL_onMapsIndoorsReadyListener", {"addListener": true}); + } + _readyListeners.add(listener); + } + + @override + void removeOnMapsIndoorsReadyListener(OnMapsIndoorsReadyListener listener) { + _readyListeners.remove(listener); + if (_readyListeners.isEmpty) { + listenerChannel.invokeMethod("MIL_onMapsIndoorsReadyListener", {"addListener": false}); + } + } + + @override + Future checkOfflineDataAvailability() { + return mapsIndoorsMethodChannel.invokeMethod("MIN_checkOfflineDataAvailability"); + } + + @override + Future destroy() { + return mapsIndoorsMethodChannel.invokeMethod("MIN_destroy"); + } + + @override + Future disableEventLogging(bool disable) { + return mapsIndoorsMethodChannel.invokeMethod("MIN_disableEventLogging", {"disable": disable}); + } + + @override + Future getAPIKey() async { + return await mapsIndoorsMethodChannel.invokeMethod("MIN_getAPIKey"); + } + + @override + Future?> getAvailableLanguages() { + return mapsIndoorsMethodChannel.invokeListMethod("MIN_getAvailableLanguages"); + } + + @override + Future getBuildings() async { + final buildings = await mapsIndoorsMethodChannel.invokeMethod("MIN_getBuildings"); + return MPBuildingCollection.fromJson(buildings); + } + + @override + Future getCategories() async { + final categories = await mapsIndoorsMethodChannel.invokeMethod("MIN_getCategories"); + return MPCategoryCollection.fromJson(categories); + } + + @override + Future getDefaultLanguage() { + return mapsIndoorsMethodChannel.invokeMethod("MIN_getDefaultLanguage"); + } + + @override + Future getLanguage() { + return mapsIndoorsMethodChannel.invokeMethod("MIN_getLanguage"); + } + + @override + Future getLocationById(String id) async { + final location = await mapsIndoorsMethodChannel.invokeMethod("MIN_getLocationById", {"id": id}); + return MPLocation.fromJson(location); + } + + @override + Future?> getLocations() async { + final locations = await mapsIndoorsMethodChannel.invokeMethod("MIN_getLocations"); + return convertMIList(jsonDecode(locations), (p0) => MPLocation.fromJson(p0)); + } + + @override + Future?> getLocationsByExternalIds(List ids) async { + final locations = await mapsIndoorsMethodChannel.invokeMethod("MIN_getLocationsByExternalIds", {"ids": ids}); + return convertMIList(jsonDecode(locations), (p0) => MPLocation.fromJson(p0)); + } + + @override + Future?> getLocationsByQuery(MPQuery? query, MPFilter? filter) async { + final locations = await mapsIndoorsMethodChannel.invokeMethod("MIN_getLocationsByQuery", {"query": query?._jsonEncode(), "filter": filter?._jsonEncode()}); + return convertMIList(jsonDecode(locations), (p0) => MPLocation.fromJson(p0)); + } + + @override + Future?> getMapStyles() async { + final mapStyles = await mapsIndoorsMethodChannel.invokeMethod("MIN_getMapStyles"); + return convertMIList(jsonDecode(mapStyles), (p0) => MPMapStyle.fromJson(p0)); + } + + @override + MPPositionProviderInterface? getPositionProvider() { + return _positionProvider; + } + + @override + void setPositionProvider(MPPositionProviderInterface? provider) { + _positionProvider = provider; + _positionProvider?.addOnPositionUpdateListener(onPositionUpdate); + mapsIndoorsMethodChannel.invokeMethod("MIN_setPositionProvider", {"remove": provider == null, "name": provider?.name}); + } + + @override + Future getSolution() async { + final solution = await mapsIndoorsMethodChannel.invokeMethod("MIN_getSolution"); + return MPSolution.fromJson(solution); + } + + @override + Future getVenues() async { + final venues = await mapsIndoorsMethodChannel.invokeMethod("MIN_getVenues"); + return MPVenueCollection.fromJson(venues); + } + + @override + Future isAPIKeyValid() { + return mapsIndoorsMethodChannel.invokeMethod("MIN_isAPIKeyValid"); + } + + @override + Future isInitialized() { + return mapsIndoorsMethodChannel.invokeMethod("MIN_isInitialized"); + } + + @override + Future isReady() { + return mapsIndoorsMethodChannel.invokeMethod("MIN_isReady"); + } + + @override + Future setLanguage(String language) { + return mapsIndoorsMethodChannel.invokeMethod("MIN_setLanguage", {"language": language}); + } + + @override + Future synchronizeContent() async { + final error = await mapsIndoorsMethodChannel.invokeMethod("MIN_synchronizeContent"); + return MPError.fromJson(error); + } + + @override + Future applyUserRoles(List userRoles) { + return mapsIndoorsMethodChannel.invokeMethod("MIN_applyUserRoles", {"userRoles": convertMIListToJson(userRoles)}); + } + + @override + Future?> getAppliedUserRoles() async { + final userRoles = await mapsIndoorsMethodChannel.invokeMethod("MIN_getAppliedUserRoles"); + return convertMIList(jsonDecode(userRoles), (p0) => MPUserRole.fromJson(p0)); + } + + @override + Future getUserRoles() async { + final userRoles = await mapsIndoorsMethodChannel.invokeMethod("MIN_getUserRoles"); + return MPUserRoleCollection.fromJson(userRoles); + } + + @override + Future reverseGeoCode(MPPoint point) async { + final geoCode = await mapsIndoorsMethodChannel.invokeMethod("MIN_reverseGeoCode", {"point": point._jsonEncode()}); + return MPGeocodeResult.fromJson(geoCode); + } + + @override + Future getDefaultVenue() async { + final venue = await mapsIndoorsMethodChannel.invokeMethod("MIN_getDefaultVenue"); + return MPVenue.fromJson(venue); + } + + @override + MPDisplayRule createDisplayRuleWithName(String name) { + return MPDisplayRule._(MPDisplayRuleId(name)); + } +} diff --git a/mapsindoors_platform_interface/lib/src/util.dart b/mapsindoors_platform_interface/lib/src/util.dart new file mode 100644 index 0000000..aa4566b --- /dev/null +++ b/mapsindoors_platform_interface/lib/src/util.dart @@ -0,0 +1,108 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +@immutable +class DynamicObjectId { + /// Creates an immutable object in MapsIndoors. + const DynamicObjectId(this.value); + + /// The value of the id. + final String value; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is DynamicObjectId && value == other.value; + } + + @override + int get hashCode => value.hashCode; + + @override + String toString() { + return '${objectRuntimeType(this, 'MapsObjectId')}($value)'; + } +} + +abstract class DynamicObject { + const DynamicObject(); + T get id; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is DynamicObject && id == other.id; + } + + @override + int get hashCode => id.hashCode; +} + +abstract class MapsIndoorsObject { + const MapsIndoorsObject(); + Map toJson(); + String _jsonEncode() => jsonEncode(toJson()); +} + +/// for use with MapsIndoors objects +List convertMIList(List data, T? Function(dynamic) fromJson) { + final List list = List.empty(growable: true); + for (final item in data) { + final T? convert = fromJson(item); + if (convert != null) { + list.add(convert); + } + } + return list; +} + +String convertMIListToJson(List objects) { + return jsonEncode(List.from(objects.map((e) => e.toJson()))); +} + +/// for use with dart default types +List convertJsonArray(List json) { + return json.map((e) => e as T).toList(); +} + +/// for use with dart default types +List> convertJson2dArray(List json) { + return json.map((e) { + if (e is List) { + return convertJsonArray(e); + } else { + return [] as List; + } + }).toList(); +} + +/// for use with dart default types +List>> convertJson3dArray(List json) { + return json.map((e) { + if (e is List) { + return convertJson2dArray(e); + } else { + return [] as List>; + } + }).toList(); +} + +/// for use with dart default types +List>>> convertJson4dArray(List json) { + return json.map((e) { + if (e is List) { + return convertJson3dArray(e); + } else { + return [] as List>>; + } + }).toList(); +} diff --git a/mapsindoors_platform_interface/lib/src/util_method_channel.dart b/mapsindoors_platform_interface/lib/src/util_method_channel.dart new file mode 100644 index 0000000..e65b19e --- /dev/null +++ b/mapsindoors_platform_interface/lib/src/util_method_channel.dart @@ -0,0 +1,68 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +/// An implementation of [UtilPlatform] that uses method channels. +class MethodChannelUtil extends UtilPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final utilMethodChannel = const MethodChannel('UtilMethodChannel'); + + @override + Future getPlatformVersion() async { + return await utilMethodChannel.invokeMethod('UTL_getPlatformVersion'); + } + + @override + Future venueHasGraph(String venueId) async { + return await utilMethodChannel.invokeMethod('UTL_venueHasGraph', {"id": venueId}); + } + + @override + Future pointAngleBetween(MPPoint it, MPPoint other) async { + return await utilMethodChannel.invokeMethod("UTL_pointAngleBetween", {"it": it._jsonEncode(), "other": other._jsonEncode()}); + } + + @override + Future pointDistanceTo(MPPoint it, MPPoint other) async { + return await utilMethodChannel.invokeMethod("UTL_pointDistanceTo", {"it": it._jsonEncode(), "other": other._jsonEncode()}); + } + + @override + Future geometryIsInside(MPGeometry it, MPPoint point) async { + return await utilMethodChannel.invokeMethod("UTL_geometryIsInside", {"it": it._jsonEncode(), "point": point._jsonEncode()}); + } + + @override + Future geometryArea(MPGeometry it) async { + return await utilMethodChannel.invokeMethod("UTL_geometryArea", {"geometry": it._jsonEncode()}); + } + + @override + Future geometryGetSquaredDistanceToClosestEdge(MPGeometry it, MPPoint point) async { + return await utilMethodChannel.invokeMethod("UTL_polygonDistToClosestEdge", {"it": it._jsonEncode(), "point": point._jsonEncode()}); + } + + @override + Future parseMapClientUrl(String venueId, String locationId) async { + return await utilMethodChannel.invokeMethod("UTL_parseMapClientUrl", {"venueId": venueId, "locationId": locationId}); + } + + @override + Future setCollisionHandling(MPCollisionHandling handling) async { + return await utilMethodChannel.invokeMethod("UTL_setCollisionHandling", {"handling": handling.value}); + } + + @override + Future setEnableClustering(bool enable) async { + return await utilMethodChannel.invokeMethod("UTL_setEnableClustering", {"enable": enable}); + } + + @override + Future setExtrusionOpacity(num opacity) async { + return await utilMethodChannel.invokeMethod("UTL_setExtrusionOpacity", {"opacity": opacity}); + } + + @override + Future setWallOpacity(num opacity) async { + return await utilMethodChannel.invokeMethod("UTL_setWallOpacity", {"opacity": opacity}); + } +} diff --git a/mapsindoors_platform_interface/lib/util_platform_interface.dart b/mapsindoors_platform_interface/lib/util_platform_interface.dart new file mode 100644 index 0000000..13c5423 --- /dev/null +++ b/mapsindoors_platform_interface/lib/util_platform_interface.dart @@ -0,0 +1,42 @@ +part of 'package:mapsindoors_platform_interface/platform_library.dart'; + +abstract class UtilPlatform extends PlatformInterface { + /// Constructs a MapsindoorsPlatform. + UtilPlatform() : super(token: _token); + + static final Object _token = Object(); + + static UtilPlatform _instance = MethodChannelUtil(); + + /// The default instance of [MapsindoorsPlatform] to use. + /// + /// Defaults to [MethodChannelMapsindoors]. + static UtilPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [MapsindoorsPlatform] when + /// they register themselves. + static set instance(UtilPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + //region Util + Future getPlatformVersion(); + Future venueHasGraph(String venueId); + //endregion + //region geometry + Future pointAngleBetween(MPPoint it, MPPoint other); + Future pointDistanceTo(MPPoint it, MPPoint other); + Future geometryIsInside(MPGeometry it, MPPoint point); + Future geometryArea(MPGeometry it); + Future geometryGetSquaredDistanceToClosestEdge(MPGeometry it, MPPoint point); + //endregion + //region solution + Future parseMapClientUrl(String venueId, String locationId); + Future setEnableClustering(bool enable); + Future setCollisionHandling(MPCollisionHandling handling); + Future setExtrusionOpacity(num opacity); + Future setWallOpacity(num opacity); + //endregion +} diff --git a/mapsindoors_platform_interface/pubspec.yaml b/mapsindoors_platform_interface/pubspec.yaml new file mode 100644 index 0000000..9eeeda4 --- /dev/null +++ b/mapsindoors_platform_interface/pubspec.yaml @@ -0,0 +1,18 @@ +name: mapsindoors_platform_interface +description: A common platform interface for the mapsindoors plugin. +version: 2.0.0 +repository: https://github.com/MapsPeople/MapsIndoors-sdk-flutter +homepage: https://www.mapsindoors.com/ + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter \ No newline at end of file