From 55bd0e56c4c03285336c1af0bc0e1d2c61bcd0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Tue, 4 Jul 2023 00:04:06 +0200 Subject: [PATCH] feat(pagerenderer): :sparkles: render maps with markers using Flutter Maps --- android/build.gradle | 2 +- lib/util/render.dart | 203 ++++++++++++++++++++++++++++++------------- pubspec.lock | 82 ++++++++++++++++- pubspec.yaml | 7 +- 4 files changed, 231 insertions(+), 63 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 58a8c74..713d7f6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/lib/util/render.dart b/lib/util/render.dart index e0ed7fc..20e351d 100644 --- a/lib/util/render.dart +++ b/lib/util/render.dart @@ -2,8 +2,11 @@ import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:html/parser.dart'; import 'package:html/dom.dart' as dom; +import 'package:latlong2/latlong.dart'; +import 'package:logger/logger.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:voyagehandbook/api/classes.dart'; @@ -30,12 +33,14 @@ import 'package:widget_zoom/widget_zoom.dart'; along with this program. If not, see . */ final _ignoredTags = ["style", "script"]; +final logger = Logger(printer: PrettyPrinter()); class PageRenderer { final ColorScheme scheme; final double height; final double width; final BuildContext context; + String? document; PageRenderer(this.scheme, this.height, this.width, this.context); @@ -91,6 +96,7 @@ class PageRenderer { ]; var document = parse(page.html); var sections = document.body!.getElementsByTagName("section"); + this.document = page.html; for (var sec in sections) { if (sec.localName == "section") { out.addAll(_renderSection(sec)); @@ -329,45 +335,139 @@ class PageRenderer { ), ); } else if (element.classes.contains("mw-kartographer-container")) { + logger.i("Found map container"); var imgs = element .getElementsByTagName("div") .first .getElementsByTagName("a") .first .getElementsByTagName("img"); - if (imgs.isEmpty) break; // load maps that have a static image - var img = imgs[0]; - var cap = element.getElementsByClassName("thumbcaption")[0]; - out.add(const SizedBox( - height: 10, - )); - out.add( - SizedBox( - width: width * 0.8, - height: height * 0.3, - child: WidgetZoom( - zoomWidget: - CachedNetworkImage(imageUrl: img.attributes["src"]!), - heroAnimationTag: 'tag', - ), - ), - ); - out.add( - const SizedBox( - height: 3, - ), - ); - out.add( - SelectableText( - cap.text, - textAlign: TextAlign.center, - ), - ); - out.add( - const SizedBox( + if (imgs.isEmpty || + (imgs.first.attributes["src"] + ?.startsWith("https://maps.wikimedia.org") ?? + false)) { + logger.i("Rendering with FlutterMap"); + // render map using FlutterMap + var dataElement = element + .getElementsByClassName("thumbinner") + .first + .getElementsByClassName("mw-kartographer-map") + .first; + + var pointsRaw = RegExp( + r"""(.+?)<\/abbr>(.+?)<\/abbr>.+?}'>(\d+)<""") + .allMatches(document!); // find markers + logger.i("Found ${pointsRaw.length} markers"); + if (pointsRaw.isEmpty) break; + var points = []; + for (var point in pointsRaw) { + // convert to FlutterMap markers + if (point.groups([1, 2]).any((element) => element == null)) { + logger.w("No lat/lon found in pointer"); + continue; + } + // assign marker color + var colorMatch = RegExp(r'background: #(.+?);') + .firstMatch(point.group(0)!) + ?.group(1); + Color bubbleColor = (colorMatch == null) + ? scheme.secondary + : Color(int.parse("0xff$colorMatch")); + + points.add( + Marker( + builder: (c) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: bubbleColor), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(point.group(3) ?? "X"), + ), + ), + point: LatLng( + double.parse(point.group(1)!), + double.parse(point.group(2)!), + ), + ), + ); + } + out.add(const SizedBox( height: 10, - ), - ); + )); + out.add( + SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: (MediaQuery.of(context).size.width * 0.8 / 4) * 3, + child: FlutterMap( + options: MapOptions( + center: LatLng( + double.parse( + dataElement.attributes["data-lat"] ?? "00.0"), + double.parse( + dataElement.attributes["data-lon"] ?? "00.0"), + ), + zoom: double.parse( + dataElement.attributes["data-zoom"] ?? "1"), + ), + nonRotatedChildren: [ + RichAttributionWidget( + attributions: [ + TextSourceAttribution( + "OpenStreetMap contributors", + onTap: () => launchUrlString( + "https://openstreetmap.org/copyright"), + ) + ], + ) + ], + children: [ + TileLayer( + urlTemplate: + "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + userAgentPackageName: "cafe.caras.voyagehandbook", + ), + MarkerLayer( + markers: points, + ) + ], + ), + ), + ); + } else { + var img = imgs[0]; + var cap = element.getElementsByClassName("thumbcaption")[0]; + out.add(const SizedBox( + height: 10, + )); + out.add( + SizedBox( + width: width * 0.8, + height: height * 0.3, + child: WidgetZoom( + zoomWidget: + CachedNetworkImage(imageUrl: img.attributes["src"]!), + heroAnimationTag: 'tag', + ), + ), + ); + out.add( + const SizedBox( + height: 3, + ), + ); + out.add( + SelectableText( + cap.text, + textAlign: TextAlign.center, + ), + ); + out.add( + const SizedBox( + height: 10, + ), + ); + } } break; default: @@ -561,16 +661,6 @@ class PageRenderer { return content; } - final _extraColorsLight = { - "blue": const Color.fromARGB(255, 34, 34, 157), - "red": const Color.fromARGB(255, 152, 33, 33) - }; - - final _extraColorsDark = { - "blue": const Color.fromARGB(255, 84, 95, 247), - "red": const Color.fromARGB(255, 242, 69, 69) - }; - SingleChildScrollView _renderList(dom.Element element) { var out = []; var i = 0; @@ -580,24 +670,19 @@ class PageRenderer { ? item.getElementsByClassName("listing-name")[0].text : null; - Color bubbleColor; - if (item.innerHtml.contains("background: #0000FF")) { - bubbleColor = - (MediaQuery.of(context).platformBrightness == Brightness.dark) - ? _extraColorsDark["blue"]! - : _extraColorsLight["blue"]!; - } else if (item.innerHtml.contains("background: #800000")) { - bubbleColor = - (MediaQuery.of(context).platformBrightness == Brightness.dark) - ? _extraColorsDark["red"]! - : _extraColorsLight["red"]!; - } else { - bubbleColor = scheme.secondary; + var colorMatch = + RegExp(r'background: #(.+?);').firstMatch(item.innerHtml)?.group(1); + Color bubbleColor = (colorMatch == null) + ? scheme.secondary + : Color(int.parse("0xff$colorMatch")); + if (element.getElementsByClassName("geo").isNotEmpty) { + element.getElementsByClassName("geo").first.remove(); + } + var rest = + ((title != null) ? item.text.replaceAll(title, "") : item.text); + if (rest.startsWith("1")) { + rest = rest.substring(1); // TODO: figure out how to remove it better? } - - var rest = (title != null) - ? item.text.replaceAll(item.getElementsByTagName("span")[0].text, "") - : item.text; out.add(const SizedBox( height: 5, )); diff --git a/pubspec.lock b/pubspec.lock index a775269..9f0ff98 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -318,6 +318,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + sha256: "5286f72f87deb132daa1489442d6cc46e986fc105cb727d9ae1b602b35b1d1f3" + url: "https://pub.dev" + source: hosted + version: "5.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -400,6 +408,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.0" + intl: + dependency: transitive + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" io: dependency: transitive description: @@ -432,6 +448,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.1" + latlong2: + dependency: "direct main" + description: + name: latlong2 + sha256: "18712164760cee655bc790122b0fd8f3d5b3c36da2cb7bf94b68a197fbb0811b" + url: "https://pub.dev" + source: hosted + version: "0.9.0" lints: dependency: transitive description: @@ -440,6 +464,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: "7ad7215c15420a102ec687bb320a7312afd449bac63bfb1c60d9787c27b9767f" + url: "https://pub.dev" + source: hosted + version: "1.4.0" logging: dependency: transitive description: @@ -472,6 +512,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" mime: dependency: transitive description: @@ -584,6 +632,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.3" + polylabel: + dependency: transitive + description: + name: polylabel + sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" + url: "https://pub.dev" + source: hosted + version: "1.0.1" pool: dependency: transitive description: @@ -600,6 +656,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" + source: hosted + version: "2.1.0" pub_semver: dependency: transitive description: @@ -813,6 +877,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" url_launcher: dependency: "direct main" description: @@ -925,6 +997,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.5" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" xdg_directories: dependency: transitive description: @@ -951,4 +1031,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.0.0 <3.2.0" - flutter: ">=3.4.0-17.0.pre" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index eb48449..44d8872 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,10 +16,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-alpha.1+1 +version: 1.0.0-alpha.2+2 environment: - sdk: '>=2.19.4 <3.0.0' + sdk: '>=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 @@ -46,6 +46,9 @@ dependencies: cached_network_image: ^3.2.3 html_unescape: ^2.0.0 widget_zoom: ^0.0.1 + flutter_map: ^5.0.0 + latlong2: ^0.9.0 + logger: ^1.4.0 dev_dependencies: flutter_test: