feat(pagerenderer): ✨ render maps with markers using Flutter Maps
This commit is contained in:
parent
74b795f00b
commit
55bd0e56c4
4 changed files with 231 additions and 63 deletions
|
@ -26,6 +26,6 @@ subprojects {
|
|||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
|
|
@ -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,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"""<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(
|
||||
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 = <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,
|
||||
));
|
||||
|
|
82
pubspec.lock
82
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"
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue