Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: migrate tile rendering from widgets to CustomPainter #1908

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions benchmark/point_in_polygon.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui';

import 'package:flutter_map/src/misc/point_in_polygon.dart';
import 'package:logger/logger.dart';

class NoFilter extends LogFilter {
@override
bool shouldLog(LogEvent event) => true;
}

typedef Result = ({
String name,
Duration duration,
});

Future<Result> timedRun(String name, dynamic Function() body) async {
Logger().i('running $name...');
final watch = Stopwatch()..start();
await body();
watch.stop();

return (name: name, duration: watch.elapsed);
}

List<Offset> makeCircle(int points, double radius, double phase) {
final slice = math.pi * 2 / (points - 1);
return List.generate(points, (i) {
// Note the modulo is only there to deal with floating point imprecision
// and ensure first == last.
final angle = slice * (i % (points - 1)) + phase;
return Offset(radius * math.cos(angle), radius * math.sin(angle));
}, growable: false);
}

// NOTE: to have a more prod like comparison, run with:
// $ dart compile exe benchmark/crs.dart && ./benchmark/crs.exe
//
// If you run in JIT mode, the resulting execution times will be a lot more similar.
Future<void> main() async {
Logger.level = Level.all;
Logger.defaultFilter = NoFilter.new;
Logger.defaultPrinter = SimplePrinter.new;

final results = <Result>[];
const N = 3000000;

final circle = makeCircle(1000, 1, 0);

results.add(await timedRun('In circle', () {
const point = math.Point(0, 0);

bool yesPlease = true;
for (int i = 0; i < N; ++i) {
yesPlease = yesPlease && isPointInPolygon(point, circle);
}

assert(yesPlease, 'should be in circle');
return yesPlease;
}));

results.add(await timedRun('Not in circle', () {
const point = math.Point(4, 4);

bool noSir = false;
for (int i = 0; i < N; ++i) {
noSir = noSir || isPointInPolygon(point, circle);
}

assert(!noSir, 'should not be in circle');
return noSir;
}));

Logger().i('Results:\n${results.map((r) => r.toString()).join('\n')}');
}
17 changes: 11 additions & 6 deletions example/lib/pages/retina.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,17 @@ class _RetinaPageState extends State<RetinaPage> {
null => RetinaMode.isHighDensity(context),
_ => retinaMode!,
},
tileBuilder: (context, tileWidget, _) => DecoratedBox(
decoration: BoxDecoration(
border: Border.all(width: 2, color: Colors.white),
),
position: DecorationPosition.foreground,
child: tileWidget,
tileOverlayPainter: ({
required canvas,
required origin,
required size,
required tile,
}) =>
canvas.drawRect(
origin & size,
Paint()
..color = Colors.white
..style = PaintingStyle.stroke,
),
);

Expand Down
134 changes: 82 additions & 52 deletions example/lib/pages/tile_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,37 @@ class TileBuilderPage extends StatefulWidget {
const TileBuilderPage({super.key});

@override
TileBuilderPageState createState() => TileBuilderPageState();
State<TileBuilderPage> createState() => _TileBuilderPageState();
}

class TileBuilderPageState extends State<TileBuilderPage> {
class _TileBuilderPageState extends State<TileBuilderPage> {
bool enableGrid = true;
bool showCoordinates = true;
bool showLoadingTime = true;
bool darkMode = true;
bool enableDarkMode = true;

// mix of [coordinateDebugTileBuilder] and [loadingTimeDebugTileBuilder] from tile_builder.dart
Widget tileBuilder(BuildContext context, Widget tileWidget, TileImage tile) {
final coords = tile.coordinates;

return DecoratedBox(
decoration: BoxDecoration(
border: enableGrid ? Border.all(width: 2, color: Colors.white) : null,
),
position: DecorationPosition.foreground,
child: Stack(
fit: StackFit.passthrough,
children: [
tileWidget,
if (showLoadingTime || showCoordinates)
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (showCoordinates)
Text(
'${coords.x} : ${coords.y} : ${coords.z}',
style: Theme.of(context).textTheme.headlineSmall,
),
if (showLoadingTime)
Text(
tile.loadFinishedAt == null
? 'Loading'
// sometimes result is negative which shouldn't happen, abs() corrects it
: '${(tile.loadFinishedAt!.millisecond - tile.loadStarted!.millisecond).abs()} ms',
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
],
),
);
}
final _darkModeColorFilter = const ColorFilter.matrix([
-1,
0,
0,
0,
255,
0,
-1,
0,
0,
255,
0,
0,
-1,
0,
255,
0,
0,
0,
1,
0,
]);

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -101,8 +87,8 @@ class TileBuilderPageState extends State<TileBuilderPage> {
child: Icon(Icons.dark_mode),
),
Switch.adaptive(
value: darkMode,
onChanged: (v) => setState(() => darkMode = v),
value: enableDarkMode,
onChanged: (v) => setState(() => enableDarkMode = v),
),
],
),
Expand All @@ -115,14 +101,14 @@ class TileBuilderPageState extends State<TileBuilderPage> {
initialZoom: 5,
),
children: [
_darkModeContainerIfEnabled(
TileLayer(
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
tileProvider: CancellableNetworkTileProvider(),
tileBuilder: tileBuilder,
),
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
tileProvider: CancellableNetworkTileProvider(),
tilePaint: enableDarkMode
? (Paint()..colorFilter = _darkModeColorFilter)
: null,
tileOverlayPainter: tileOverlayPainter,
),
const MarkerLayer(
markers: [
Expand All @@ -144,9 +130,53 @@ class TileBuilderPageState extends State<TileBuilderPage> {
);
}

Widget _darkModeContainerIfEnabled(Widget child) {
if (!darkMode) return child;
void tileOverlayPainter({
required Canvas canvas,
required Offset origin,
required Size size,
required TileImage tile,
}) {
final rect = origin & size;

if (enableGrid) {
canvas.drawRect(
rect,
Paint()
..color = enableDarkMode ? Colors.white : Colors.black
..style = PaintingStyle.stroke,
);
}

if (showCoordinates || showLoadingTime) {
final textStyle = TextStyle(
color: enableDarkMode ? Colors.white : Colors.black,
fontSize: 18,
);

final textSpan = TextSpan(
text: (showCoordinates ? tile.coordinates.toString() : '') +
(showCoordinates && showLoadingTime ? '\n' : '') +
(showLoadingTime
? tile.loadFinishedAt == null
? 'Loading'
// sometimes result is negative which shouldn't happen, abs() corrects it
: '${(tile.loadFinishedAt!.millisecond - tile.loadStarted!.millisecond).abs()} ms'
: ''),
style: textStyle,
);
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
);
textPainter.layout(
minWidth: 0,
maxWidth: size.width,
);
final xCenter = (size.width - textPainter.width) / 2;
final yCenter = (size.height - textPainter.height) / 2;

return darkModeTilesContainerBuilder(context, child);
textPainter.paint(canvas, origin + Offset(xCenter, yCenter));
}
}
}
2 changes: 1 addition & 1 deletion lib/flutter_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ export 'package:flutter_map/src/layer/shared/layer_interactivity/layer_hit_resul
export 'package:flutter_map/src/layer/shared/line_patterns/stroke_pattern.dart';
export 'package:flutter_map/src/layer/shared/mobile_layer_transformer.dart';
export 'package:flutter_map/src/layer/shared/translucent_pointer.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_builder.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_display.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_image.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_layer.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_overlay_painter.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_provider/asset_tile_provider.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_provider/base_tile_provider.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_provider/file_providers/tile_provider_stub.dart'
Expand Down
2 changes: 1 addition & 1 deletion lib/src/geo/crs.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:math' as math hide Point;
import 'dart:math' show Point;

import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/src/misc/bounds.dart';
import 'package:latlong2/latlong.dart';
import 'package:meta/meta.dart';
import 'package:proj4dart/proj4dart.dart' as proj4;
Expand Down
57 changes: 19 additions & 38 deletions lib/src/layer/polygon_layer/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,42 +39,41 @@ base class _PolygonPainter<R extends Object>
required LatLng coordinate,
}) {
final polygon = projectedPolygon.polygon;

if (!polygon.boundingBox.contains(coordinate)) return false;
if (!polygon.boundingBox.contains(coordinate)) {
return false;
}

final projectedCoords = getOffsetsXY(
camera: camera,
origin: hitTestCameraOrigin,
points: projectedPolygon.points,
).toList();
);

if (projectedCoords.first != projectedCoords.last) {
projectedCoords.add(projectedCoords.first);
}
final isInPolygon = isPointInPolygon(point, projectedCoords);

final hasHoles = projectedPolygon.holePoints.isNotEmpty;
late final List<List<Offset>> projectedHoleCoords;
if (hasHoles) {
projectedHoleCoords = projectedPolygon.holePoints
.map(
(points) => getOffsetsXY(
final isInHole = hasHoles &&
() {
for (final points in projectedPolygon.holePoints) {
final projectedHoleCoords = getOffsetsXY(
camera: camera,
origin: hitTestCameraOrigin,
points: points,
).toList(),
)
.toList();
);

if (projectedHoleCoords.firstOrNull != projectedHoleCoords.lastOrNull) {
projectedHoleCoords.add(projectedHoleCoords.first);
}
}
if (projectedHoleCoords.first != projectedHoleCoords.last) {
projectedHoleCoords.add(projectedHoleCoords.first);
}

final isInPolygon = _isPointInPolygon(point, projectedCoords);
final isInHole = hasHoles &&
projectedHoleCoords
.map((c) => _isPointInPolygon(point, c))
.any((e) => e);
if (isPointInPolygon(point, projectedHoleCoords)) {
return true;
}
}
return false;
}();

// Second check handles case where polygon outline intersects a hole,
// ensuring that the hit matches with the visual representation
Expand Down Expand Up @@ -361,24 +360,6 @@ base class _PolygonPainter<R extends Object>
);
}

/// Checks whether point [p] is within the specified closed [polygon]
///
/// Uses the even-odd algorithm.
static bool _isPointInPolygon(math.Point p, List<Offset> polygon) {
bool isInPolygon = false;

for (int i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
if ((((polygon[i].dy <= p.y) && (p.y < polygon[j].dy)) ||
((polygon[j].dy <= p.y) && (p.y < polygon[i].dy))) &&
(p.x <
(polygon[j].dx - polygon[i].dx) *
(p.y - polygon[i].dy) /
(polygon[j].dy - polygon[i].dy) +
polygon[i].dx)) isInPolygon = !isInPolygon;
}
return isInPolygon;
}

@override
bool shouldRepaint(_PolygonPainter<R> oldDelegate) =>
polygons != oldDelegate.polygons ||
Expand Down
1 change: 1 addition & 0 deletions lib/src/layer/polygon_layer/polygon_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart';
import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart';
import 'package:flutter_map/src/misc/offsets.dart';
import 'package:flutter_map/src/misc/point_in_polygon.dart';
import 'package:flutter_map/src/misc/simplify.dart';
import 'package:latlong2/latlong.dart' hide Path;
import 'package:polylabel/polylabel.dart';
Expand Down
Loading
Loading