feat(pagerenderer): render maps with markers using Flutter Maps

This commit is contained in:
Matyáš Caras 2023-07-04 00:04:06 +02:00
parent 74b795f00b
commit 55bd0e56c4
4 changed files with 231 additions and 63 deletions

View file

@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }
task clean(type: Delete) { tasks.register("clean", Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View file

@ -2,8 +2,11 @@ import 'dart:math';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:html/dom.dart' as dom; 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.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:voyagehandbook/api/classes.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 <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
final _ignoredTags = ["style", "script"]; final _ignoredTags = ["style", "script"];
final logger = Logger(printer: PrettyPrinter());
class PageRenderer { class PageRenderer {
final ColorScheme scheme; final ColorScheme scheme;
final double height; final double height;
final double width; final double width;
final BuildContext context; final BuildContext context;
String? document;
PageRenderer(this.scheme, this.height, this.width, this.context); PageRenderer(this.scheme, this.height, this.width, this.context);
@ -91,6 +96,7 @@ class PageRenderer {
]; ];
var document = parse(page.html); var document = parse(page.html);
var sections = document.body!.getElementsByTagName("section"); var sections = document.body!.getElementsByTagName("section");
this.document = page.html;
for (var sec in sections) { for (var sec in sections) {
if (sec.localName == "section") { if (sec.localName == "section") {
out.addAll(_renderSection(sec)); out.addAll(_renderSection(sec));
@ -329,45 +335,139 @@ class PageRenderer {
), ),
); );
} else if (element.classes.contains("mw-kartographer-container")) { } else if (element.classes.contains("mw-kartographer-container")) {
logger.i("Found map container");
var imgs = element var imgs = element
.getElementsByTagName("div") .getElementsByTagName("div")
.first .first
.getElementsByTagName("a") .getElementsByTagName("a")
.first .first
.getElementsByTagName("img"); .getElementsByTagName("img");
if (imgs.isEmpty) break; // load maps that have a static image if (imgs.isEmpty ||
var img = imgs[0]; (imgs.first.attributes["src"]
var cap = element.getElementsByClassName("thumbcaption")[0]; ?.startsWith("https://maps.wikimedia.org") ??
out.add(const SizedBox( false)) {
height: 10, logger.i("Rendering with FlutterMap");
)); // render map using FlutterMap
out.add( var dataElement = element
SizedBox( .getElementsByClassName("thumbinner")
width: width * 0.8, .first
height: height * 0.3, .getElementsByClassName("mw-kartographer-map")
child: WidgetZoom( .first;
zoomWidget:
CachedNetworkImage(imageUrl: img.attributes["src"]!), var pointsRaw = RegExp(
heroAnimationTag: 'tag', r"""<span class="noprint listing-coordinates".+?><span class="geo"><abbr class="latitude">(.+?)<\/abbr><abbr class="longitude">(.+?)<\/abbr>.+?}'>(\d+)<""")
), .allMatches(document!); // find markers
), logger.i("Found ${pointsRaw.length} markers");
); if (pointsRaw.isEmpty) break;
out.add( var points = <Marker>[];
const SizedBox( for (var point in pointsRaw) {
height: 3, // convert to FlutterMap markers
), if (point.groups([1, 2]).any((element) => element == null)) {
); logger.w("No lat/lon found in pointer");
out.add( continue;
SelectableText( }
cap.text, // assign marker color
textAlign: TextAlign.center, var colorMatch = RegExp(r'background: #(.+?);')
), .firstMatch(point.group(0)!)
); ?.group(1);
out.add( Color bubbleColor = (colorMatch == null)
const SizedBox( ? 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, 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; break;
default: default:
@ -561,16 +661,6 @@ class PageRenderer {
return content; 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) { SingleChildScrollView _renderList(dom.Element element) {
var out = <Widget>[]; var out = <Widget>[];
var i = 0; var i = 0;
@ -580,24 +670,19 @@ class PageRenderer {
? item.getElementsByClassName("listing-name")[0].text ? item.getElementsByClassName("listing-name")[0].text
: null; : null;
Color bubbleColor; var colorMatch =
if (item.innerHtml.contains("background: #0000FF")) { RegExp(r'background: #(.+?);').firstMatch(item.innerHtml)?.group(1);
bubbleColor = Color bubbleColor = (colorMatch == null)
(MediaQuery.of(context).platformBrightness == Brightness.dark) ? scheme.secondary
? _extraColorsDark["blue"]! : Color(int.parse("0xff$colorMatch"));
: _extraColorsLight["blue"]!; if (element.getElementsByClassName("geo").isNotEmpty) {
} else if (item.innerHtml.contains("background: #800000")) { element.getElementsByClassName("geo").first.remove();
bubbleColor = }
(MediaQuery.of(context).platformBrightness == Brightness.dark) var rest =
? _extraColorsDark["red"]! ((title != null) ? item.text.replaceAll(title, "") : item.text);
: _extraColorsLight["red"]!; if (rest.startsWith("1")) {
} else { rest = rest.substring(1); // TODO: figure out how to remove it better?
bubbleColor = scheme.secondary;
} }
var rest = (title != null)
? item.text.replaceAll(item.getElementsByTagName("span")[0].text, "")
: item.text;
out.add(const SizedBox( out.add(const SizedBox(
height: 5, height: 5,
)); ));

View file

@ -318,6 +318,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" 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: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -400,6 +408,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.0" version: "3.3.0"
intl:
dependency: transitive
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.dev"
source: hosted
version: "0.18.1"
io: io:
dependency: transitive dependency: transitive
description: description:
@ -432,6 +448,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.7.1" version: "6.7.1"
latlong2:
dependency: "direct main"
description:
name: latlong2
sha256: "18712164760cee655bc790122b0fd8f3d5b3c36da2cb7bf94b68a197fbb0811b"
url: "https://pub.dev"
source: hosted
version: "0.9.0"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -440,6 +464,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" 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: logging:
dependency: transitive dependency: transitive
description: description:
@ -472,6 +512,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" 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: mime:
dependency: transitive dependency: transitive
description: description:
@ -584,6 +632,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.7.3" version: "3.7.3"
polylabel:
dependency: transitive
description:
name: polylabel
sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -600,6 +656,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.4" version: "4.2.4"
proj4dart:
dependency: transitive
description:
name: proj4dart
sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e
url: "https://pub.dev"
source: hosted
version: "2.1.0"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@ -813,6 +877,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.2"
unicode:
dependency: transitive
description:
name: unicode
sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
url_launcher: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
@ -925,6 +997,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.5" 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: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -951,4 +1031,4 @@ packages:
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.0.0 <3.2.0" dart: ">=3.0.0 <3.2.0"
flutter: ">=3.4.0-17.0.pre" flutter: ">=3.10.0"

View file

@ -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 # 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 # 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. # 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: environment:
sdk: '>=2.19.4 <3.0.0' sdk: '>=3.0.0'
# Dependencies specify other packages that your package needs in order to work. # Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions # To automatically upgrade your package dependencies to the latest versions
@ -46,6 +46,9 @@ dependencies:
cached_network_image: ^3.2.3 cached_network_image: ^3.2.3
html_unescape: ^2.0.0 html_unescape: ^2.0.0
widget_zoom: ^0.0.1 widget_zoom: ^0.0.1
flutter_map: ^5.0.0
latlong2: ^0.9.0
logger: ^1.4.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: