Commit 6a93e737 authored by Amirjanyan's avatar Amirjanyan
Browse files

Merge branch '14-maprenderer' into 'dev'

Resolve "MapRenderer"

Closes #14

See merge request !23
parents 6759927f aadc5cef
Pipeline #2341 passed with stages
in 2 minutes and 47 seconds
Showing with 609 additions and 14 deletions
+609 -14
......@@ -35,10 +35,11 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "io.memri.memri"
minSdkVersion 16
minSdkVersion 20
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
}
buildTypes {
......@@ -46,6 +47,9 @@ android {
// 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
ndk {
abiFilters 'armeabi-v7a','arm64-v8a','x86_64', 'x86'
}
}
}
}
......
......@@ -296,9 +296,8 @@ class CVULookupController {
break;
case "joinwithcomma":
String joined = (await Future.wait(args.map((element) async =>
(await resolve<String>(expression: element, context: context, db: db))
.toString())))
.where((element) => element.isNotEmpty)
(await resolve<String>(expression: element, context: context, db: db)))))
.where((element) => element != null && element.isNotEmpty)
.join(", ");
currentValue = LookupStepValues([PropertyDatabaseValueString(joined)]);
break;
......
......@@ -230,7 +230,7 @@ class CVUPropertyResolver {
if (type == null) {
return null;
}
return [type(vars: {})];
return [type()];
}
}
if (val is CVUValueArray) {
......
import 'dart:convert';
import 'package:mapbox_gl/mapbox_gl.dart';
import 'package:memri/MemriApp/Controllers/AppController.dart';
import 'package:memri/MemriApp/Controllers/Database/DatabaseController.dart';
import 'package:memri/MemriApp/Controllers/Database/ItemEdgeRecord.dart';
import 'package:memri/MemriApp/Controllers/Database/ItemPropertyRecord.dart';
import 'package:memri/MemriApp/Controllers/Database/ItemRecord.dart';
import 'package:memri/MemriApp/Controllers/Database/PropertyDatabaseValue.dart';
import 'package:memri/MemriApp/Extensions/BaseTypes/Collection.dart';
import 'package:http/http.dart' as http;
class MapHelper {
Map<int, LatLng> addressLookupResults = {};
static var shared = MapHelper();
static String accessToken =
"pk.eyJ1IjoibWtzbGFuYyIsImEiOiJja29pdHJrbW0wNWl4Mm9ud2Fla212Z2ozIn0.tDDonSujiNPa3GiobveWvw";
final String _url = 'https://api.mapbox.com/geocoding/v5/mapbox.places/';
Future<List<MapBoxObject>?> lookupAddress(String string) async {
String finalUrl = '$_url${Uri.encodeFull(string)}.json?';
finalUrl += 'access_token=${MapHelper.accessToken}';
final response = await http.get(Uri.parse(finalUrl));
if (response.body.contains('message')) {
throw Exception(json.decode(response.body)['message']);
}
return (json.decode(response.body)["features"] as List<dynamic>)
.map((el) => MapBoxObject.fromJson(el))
.whereType<MapBoxObject>()
.toList();
}
Future<LatLng?> getLocationForAddress(ItemRecord address, [DatabaseController? db]) async {
db ??= AppController.shared.databaseController;
// Check if the address holds a valid location
var location = await address.edgeItem("location");
if (location == null) {
return null;
}
var latitude = (await location.propertyValue("latitude"))?.asDouble();
var longitude = (await location.propertyValue("longitude"))?.asDouble();
if (latitude != null && longitude != null) {
return LatLng(latitude, longitude);
}
return null;
}
Future<LatLng?> lookupLocationForAddress(ItemRecord address, [DatabaseController? db]) async {
db ??= AppController.shared.databaseController;
// Make new lookup
var inclusions = ["street", "city", "state"];
var addressString = (await Future.wait(
inclusions.compactMap((el) async => (await address.propertyValue(el))?.asString())))
.join("");
var lookupHash = addressString.hashCode;
// Check lookups in progress
var knownLocation = addressLookupResults[lookupHash];
if (knownLocation != null) {
// Successful lookup already completed
return knownLocation;
}
// Check if the address holds a valid location
var location = await address.edgeItem("location");
if (location != null) {
var latitude = (await location.propertyValue("latitude"))?.asDouble();
var longitude = (await location.propertyValue("longitude"))?.asDouble();
if (latitude != null && longitude != null) {
var clLocation = LatLng(latitude, longitude);
var oldLookupHash = (await address.propertyValue("locationAutoLookupHash"))?.asString();
if (oldLookupHash != null) {
// This was an automatic lookup - check it's still current
if (oldLookupHash == lookupHash.toString()) {
return clLocation;
}
}
}
}
var lookup = await lookupAddress(addressString);
if (lookup != null) {
var location = lookup.firstWhereOrNull((element) => element.center != null);
if (location != null) {
var locationItem = ItemRecord(type: "Location");
var itemRowId = await locationItem.insert(db.databasePool);
var locationItemProperties = [
ItemPropertyRecord(
itemRowID: itemRowId,
name: "latitude",
value: PropertyDatabaseValueDouble(location.center![1])),
ItemPropertyRecord(
itemRowID: itemRowId,
name: "longitude",
value: PropertyDatabaseValueDouble(location.center![0]))
];
var locationEdge =
ItemEdgeRecord(sourceRowID: address.rowId, name: "location", targetRowID: itemRowId);
await Future.forEach(locationItemProperties,
((ItemPropertyRecord el) async => await el.save(db!.databasePool)));
await locationEdge.save(db.databasePool);
addressLookupResults[lookupHash] = LatLng(location.center![1], location.center![0]);
return addressLookupResults[lookupHash];
}
}
}
}
class MapBoxObject {
String id;
String addressNumber;
String text;
String placeName;
List<double>? bbox;
List<double>? center;
String matchingText;
String matchingPlaceName;
//TODO: we could use more params
MapBoxObject({
required this.id,
required this.addressNumber,
required this.text,
required this.placeName,
required this.bbox,
required this.center,
required this.matchingText,
required this.matchingPlaceName,
});
factory MapBoxObject.fromJson(Map<String, dynamic> json) => MapBoxObject(
id: json["id"],
addressNumber: json["address"],
text: json["text"],
placeName: json["place_name"],
bbox:
json["bbox"] == null ? null : List<double>.from(json["bbox"].map((x) => x.toDouble())),
center: json["center"] == null
? null
: List<double>.from(json["center"].map((x) => x.toDouble())),
matchingText: json["matching_text"] ?? "",
matchingPlaceName: json["matching_place_name"] ?? "",
);
}
......@@ -19,6 +19,7 @@ import 'CVUElements/CVUFlowStack.dart';
import 'CVUElements/CVUForEach.dart';
import 'CVUElements/CVUHTMLView.dart';
import 'CVUElements/CVUImage.dart';
import 'CVUElements/CVUMap.dart';
import 'CVUElements/CVUMemriButton.dart';
import 'CVUElements/CVUShape.dart';
import 'CVUElements/CVUStacks.dart';
......@@ -52,8 +53,8 @@ class CVUElementView extends StatelessWidget {
);
case CVUUIElementFamily.Image:
return CVUImage(nodeResolver: nodeResolver);
//case CVUUIElementFamily.Map:
// return CVU_Map(nodeResolver: nodeResolver);
case CVUUIElementFamily.Map:
return CVUMap(nodeResolver: nodeResolver);
case CVUUIElementFamily.SmartText:
return CVUSmartText(
nodeResolver: nodeResolver,
......
import 'package:flutter/material.dart';
import 'package:mapbox_gl/mapbox_gl.dart';
import 'package:memri/MemriApp/Controllers/Database/ItemRecord.dart';
import 'package:memri/MemriApp/Helpers/MapHelper.dart';
import 'package:memri/MemriApp/UI/CVUComponents/CVUUINodeResolver.dart';
import 'package:memri/MemriApp/UI/Components/Map/MapView.dart';
/// A CVU element for displaying a Map
class CVUMap extends StatelessWidget {
final CVUUINodeResolver nodeResolver;
CVUMap({required this.nodeResolver});
Future<String?> get content async {
return await nodeResolver.propertyResolver.string("text") ?? "";
}
@override
Widget build(BuildContext context) {
return MapView(
config: config,
);
}
Future<List<ItemRecord>> Function(ItemRecord) get locationResolver {
return (ItemRecord item) => nodeResolver.propertyResolver.items("location");
}
Future<List<ItemRecord>> Function(ItemRecord) get addressResolver {
return (ItemRecord item) async => await nodeResolver.propertyResolver.items("address");
}
Future<String?> Function(ItemRecord) get labelResolver {
return (ItemRecord item) async => await nodeResolver.propertyResolver.string("label");
}
MapViewConfig get config {
var currentItem = nodeResolver.propertyResolver.context.currentItem;
return MapViewConfig(
dataItems: currentItem != null ? [currentItem] : [],
locationResolver: locationResolver,
addressResolver: addressResolver,
labelResolver: labelResolver,
moveable: nodeResolver.propertyResolver.boolean("moveable", true));
}
}
class MapViewConfig {
List<ItemRecord> dataItems;
Future<List<ItemRecord>> Function(ItemRecord) locationResolver;
Future<List<ItemRecord>> Function(ItemRecord) addressResolver;
Future<String?> Function(ItemRecord) labelResolver;
double maxInitialZoom;
Future<bool?> moveable;
void Function(ItemRecord)? onPress;
var colorScheme;
MapViewConfig(
{List<ItemRecord>? dataItems,
required this.locationResolver,
required this.addressResolver,
required this.labelResolver,
this.maxInitialZoom = 16,
Future<bool?>? moveable,
this.onPress})
: this.dataItems = dataItems ?? [],
this.moveable = moveable ?? Future(() => true);
}
class MapModel {
List<ItemRecord> dataItems;
Future<List<ItemRecord>> Function(ItemRecord) locationResolver;
Future<List<ItemRecord>> Function(ItemRecord) addressResolver;
Future<String?> Function(ItemRecord) labelResolver;
late List<MapItem> items;
MapModel(
{required this.dataItems,
required this.locationResolver,
required this.addressResolver,
required this.labelResolver});
updateModel() async {
var newItems = await Future.wait(dataItems.map((item) async {
var locations = await resolveItem(item);
String labelString = await labelResolver(item) ?? "";
return locations.map((el) {
return MapItem(label: labelString, coordinate: el, dataItem: item);
});
}));
items = newItems.expand((element) => element).toList();
}
Future<List<LatLng>> resolveItem(ItemRecord dataItem) async {
List<LatLng> clLocations = [];
List<LatLng> locations =
(await Future.wait((await locationResolver(dataItem)).map((item) async {
var latitude = (await item.propertyValue("latitude"))?.asDouble();
var longitude = (await item.propertyValue("longitude"))?.asDouble();
if (latitude == null || longitude == null) {
return null;
}
return LatLng(latitude, longitude);
})))
.whereType<LatLng>()
.toList();
clLocations.addAll(locations);
var addresses = await addressResolver(dataItem);
var resolvedLocations = await Future.wait(addresses.map((el) async => await lookupAddress(el)));
clLocations.addAll(resolvedLocations.whereType<LatLng>());
return clLocations;
}
Future<LatLng?> lookupAddress(ItemRecord address) async {
var location = await MapHelper.shared.getLocationForAddress(address);
if (location != null) {
return location;
} else {
return await MapHelper.shared.lookupLocationForAddress(address);
}
}
}
class MapItem {
String label;
LatLng coordinate;
ItemRecord? dataItem;
MapItem({required this.label, required this.coordinate, required this.dataItem});
}
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mapbox_gl/mapbox_gl.dart';
class MapMarker extends StatefulWidget {
final Point _initialPosition;
final LatLng _coordinate;
final void Function(MapMarkerState) _addMarkerState;
MapMarker(String key, this._coordinate, this._initialPosition, this._addMarkerState)
: super(key: Key(key));
@override
State<StatefulWidget> createState() {
final state = MapMarkerState(_initialPosition);
_addMarkerState(state);
return state;
}
}
class MapMarkerState extends State<MapMarker> with TickerProviderStateMixin {
var _initialIconSize = 30.0;
var _iconSize;
Point _position;
MapMarkerState(this._position);
@override
void initState() {
_iconSize = _initialIconSize;
super.initState();
}
@override
Widget build(BuildContext context) {
var ratio = 1.0;
if (!kIsWeb) {
ratio = MediaQuery.of(context).devicePixelRatio;
}
return Positioned(
left: _position.x / ratio - _iconSize / 2,
top: _position.y / ratio - _iconSize,
child: IconButton(
onPressed: _onTap,
icon: Icon(
Icons.location_on,
size: _iconSize,
color: Colors.red,
),
));
}
void updatePosition(Point<num> point) {
setState(() {
_position = point;
});
}
LatLng getCoordinate() {
return widget._coordinate;
}
void _onTap() {
setState(() {
_iconSize = _iconSize == _initialIconSize ? _iconSize * 2 : _initialIconSize;
});
}
}
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mapbox_gl/mapbox_gl.dart';
import 'package:memri/MemriApp/Helpers/MapHelper.dart';
import 'package:memri/MemriApp/UI/CVUComponents/CVUElements/CVUMap.dart';
import 'package:memri/MemriApp/UI/Components/Map/MapMarker.dart';
class MapView extends StatelessWidget {
final MapViewConfig config;
final MapModel _mapModel;
late final MapboxMapController mapController;
late final LatLng currentCoords;
final List<MapMarker> _markers = [];
final List<MapMarkerState> _markerStates = [];
late final bool moveable;
MapView({required this.config})
: _mapModel = MapModel(
dataItems: config.dataItems,
locationResolver: config.locationResolver,
addressResolver: config.addressResolver,
labelResolver: config.labelResolver);
Future<void> init() async {
await _mapModel.updateModel();
moveable = (await config.moveable)!;
}
void _addMarkerStates(MapMarkerState markerState) {
_markerStates.add(markerState);
}
void _onMapCreated(MapboxMapController controller) {
mapController = controller;
mapController.addListener(() {
if (mapController.isCameraMoving) {
_updateMarkerPosition();
}
});
}
void _onCameraIdleCallback() {
_updateMarkerPosition();
}
void _updateMarkerPosition() {
final coordinates = <LatLng>[];
for (final markerState in _markerStates) {
coordinates.add(markerState.getCoordinate());
}
mapController.toScreenLocationBatch(coordinates).then((points) {
_markerStates.asMap().forEach((i, value) {
_markerStates[i].updatePosition(points[i]);
});
});
}
void _addMarker(Point<double> point, LatLng coordinates) {
_markers
.add(MapMarker(Random().nextInt(100000).toString(), coordinates, point, _addMarkerStates));
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: init(),
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
var firstItem = _mapModel.items.asMap()[0];
if (firstItem != null) {
currentCoords = firstItem.coordinate;
_addMarker(Point(0, 0), currentCoords);
}
return IgnorePointer(
ignoring: !moveable,
child: Stack(
children: [
MapboxMap(
trackCameraPosition: true,
onMapCreated: _onMapCreated,
onCameraIdle: _onCameraIdleCallback,
accessToken: MapHelper.accessToken,
initialCameraPosition: CameraPosition(
target: (firstItem != null) ? firstItem.coordinate : LatLng(0.0, 0.0),
zoom: config.maxInitialZoom),
),
Stack(
children: _markers,
),
],
),
);
} else {
return Flexible(
child: Center(
child: SizedBox(
child: CircularProgressIndicator(),
width: 30,
height: 30,
),
),
);
}
});
}
}
......@@ -124,9 +124,11 @@ class _GridRendererViewState extends State<GridRendererView> {
var item = viewContext.items.asMap()[index];
if (item != null) {
var press = viewContext.nodePropertyResolver(item)?.action("onPress");
if (press != null) {
press.execute(sceneController, viewContext.getCVUContext(item: item));
var presses = viewContext.rendererDefinitionPropertyResolver.actions("onPress") ??
viewContext.nodePropertyResolver(item)?.actions("onPress");
if (presses != null) {
presses.forEach(
(press) => press.execute(sceneController, viewContext.getCVUContext(item: item)));
}
}
};
......
......@@ -117,9 +117,11 @@ class _ListRendererViewState extends State<ListRendererView> {
var item = viewContext.items.asMap()[index];
if (item != null) {
var press = viewContext.nodePropertyResolver(item)?.action("onPress");
if (press != null) {
press.execute(sceneController, viewContext.getCVUContext(item: item));
var presses = viewContext.rendererDefinitionPropertyResolver.actions("onPress") ??
viewContext.nodePropertyResolver(item)?.actions("onPress");
if (presses != null) {
presses.forEach(
(press) => press.execute(sceneController, viewContext.getCVUContext(item: item)));
}
}
};
......
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:memri/MemriApp/Controllers/Database/ItemRecord.dart';
import 'package:memri/MemriApp/Controllers/SceneController.dart';
import 'package:memri/MemriApp/UI/CVUComponents/CVUElements/CVUMap.dart';
import 'package:memri/MemriApp/UI/Components/Map/MapView.dart';
import '../ViewContextController.dart';
......@@ -15,6 +20,32 @@ class MapRendererView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Expanded(child: Text("MapRendererView"));
return Expanded(child: MapView(config: mapConfig));
}
Future<List<ItemRecord>> Function(ItemRecord) get locationResolver {
return (ItemRecord item) async =>
await viewContext.nodePropertyResolver(item)?.items("location") ?? [];
}
Future<List<ItemRecord>> Function(ItemRecord) get addressResolver {
return (ItemRecord item) async =>
await viewContext.nodePropertyResolver(item)?.items("address") ?? [];
}
Future<String?> Function(ItemRecord) get labelResolver {
return (ItemRecord item) async => await viewContext.nodePropertyResolver(item)?.string("label");
}
MapViewConfig get mapConfig {
var resolver = viewContext.rendererDefinitionPropertyResolver;
var moveable = resolver.boolean("moveable", true);
return MapViewConfig(
dataItems: viewContext.items,
locationResolver: locationResolver,
addressResolver: addressResolver,
labelResolver: labelResolver,
moveable: moveable);
}
}
......@@ -22,6 +22,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.1"
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.2"
args:
dependency: transitive
description:
......@@ -242,6 +249,11 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
......@@ -263,6 +275,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.0"
http:
dependency: "direct main"
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.3"
http_multi_server:
dependency: transitive
description:
......@@ -277,6 +296,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
intl:
dependency: "direct main"
description:
......@@ -319,6 +345,40 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
mapbox_gl:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: "6a907cd29b4ad260fe2744305e39bd0feb3423aa"
url: "https://github.com/tobrun/flutter-mapbox-gl.git"
source: git
version: "0.12.0"
mapbox_gl_dart:
dependency: transitive
description:
name: mapbox_gl_dart
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0-nullsafety.0"
mapbox_gl_platform_interface:
dependency: "direct overridden"
description:
path: mapbox_gl_platform_interface
ref: HEAD
resolved-ref: "6a907cd29b4ad260fe2744305e39bd0feb3423aa"
url: "https://github.com/tobrun/flutter-mapbox-gl.git"
source: git
version: "0.12.0"
mapbox_gl_web:
dependency: "direct overridden"
description:
path: mapbox_gl_web
ref: HEAD
resolved-ref: "6a907cd29b4ad260fe2744305e39bd0feb3423aa"
url: "https://github.com/tobrun/flutter-mapbox-gl.git"
source: git
version: "0.12.0"
matcher:
dependency: transitive
description:
......@@ -410,6 +470,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.0"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
platform:
dependency: transitive
description:
......@@ -618,6 +685,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.0"
yaml:
dependency: transitive
description:
......
......@@ -34,6 +34,8 @@ dependencies:
jiffy: ^4.1.0
intl: ^0.17.0
fl_chart: ^0.35.0
mapbox_gl: ^0.12.0
http: ^0.13.3
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
......@@ -46,6 +48,18 @@ dev_dependencies:
moor_generator: ^4.1.0
build_runner: ^1.11.0
dependency_overrides:
mapbox_gl:
git:
url: https://github.com/tobrun/flutter-mapbox-gl.git
mapbox_gl_platform_interface:
git:
url: https://github.com/tobrun/flutter-mapbox-gl.git
path: mapbox_gl_platform_interface
mapbox_gl_web:
git:
url: https://github.com/tobrun/flutter-mapbox-gl.git
path: mapbox_gl_web
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment