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')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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,13 +335,106 @@ 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
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"""<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;
var points = <Marker>[];
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(
@ -369,6 +468,7 @@ class PageRenderer {
),
);
}
}
break;
default:
break;
@ -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 = <Widget>[];
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,
));

View file

@ -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"

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
# 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: