Skip to content

Commit

Permalink
Merge branch 'master' into fix-1500
Browse files Browse the repository at this point in the history
  • Loading branch information
JaffaKetchup authored Sep 8, 2023
2 parents f7721e2 + d65881c commit ca2ca75
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 89 deletions.
1 change: 1 addition & 0 deletions example/lib/pages/polygon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class PolygonPage extends StatelessWidget {
borderColor: Colors.purple,
label: "Rotated!",
rotateLabel: true,
labelPlacement: PolygonLabelPlacement.polylabel,
),
Polygon(
points: holeOuterPoints,
Expand Down
2 changes: 1 addition & 1 deletion lib/src/gestures/flutter_map_interactive_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ class FlutterMapInteractiveViewerState
newCenter,
newZoom,
offset: Offset.zero,
hasGesture: false,
hasGesture: true,
source: MapEventSource.scrollWheel,
id: null,
);
Expand Down
58 changes: 38 additions & 20 deletions lib/src/layer/label.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@ import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:latlong2/latlong.dart';
import 'package:polylabel/polylabel.dart';

void Function(Canvas canvas)? buildLabelTextPainter({
required String labelText,
void Function(Canvas canvas)? buildLabelTextPainter(
List<LatLng> locs,
LatLng labelLoc, {
required Offset placementPoint,
required List<Offset> points,
required String labelText,
required double rotationRad,
bool rotate = false,
TextStyle? labelStyle,
PolygonLabelPlacement labelPlacement = PolygonLabelPlacement.polylabel,
double padding = 0,
}) {
final placementPoint = switch (labelPlacement) {
PolygonLabelPlacement.centroid => _computeCentroid(points),
PolygonLabelPlacement.polylabel => _computePolylabel(points),
};

var dx = placementPoint.dx;
var dy = placementPoint.dy;
double dx = placementPoint.dx;
double dy = placementPoint.dy;

if (dx > 0) {
final textSpan = TextSpan(text: labelText, style: labelStyle);
Expand Down Expand Up @@ -63,17 +61,37 @@ void Function(Canvas canvas)? buildLabelTextPainter({
return null;
}

Offset _computeCentroid(List<Offset> points) {
return Offset(
points.map((e) => e.dx).average,
points.map((e) => e.dy).average,
LatLng computeLabelPosition(
PolygonLabelPlacement labelPlacement, List<LatLng> points) {
return switch (labelPlacement) {
PolygonLabelPlacement.centroid => _computeCentroid(points),
PolygonLabelPlacement.polylabel => _computePolylabel(points),
};
}

LatLng _computeCentroid(List<LatLng> points) {
return LatLng(
points.map((e) => e.latitude).average,
points.map((e) => e.longitude).average,
);
}

Offset _computePolylabel(List<Offset> points) {
final labelPosition = polylabel([
List<math.Point>.generate(
points.length, (i) => math.Point(points[i].dx, points[i].dy)),
]);
return labelPosition.point.toOffset();
LatLng _computePolylabel(List<LatLng> points) {
final labelPosition = polylabel(
[
List<math.Point>.generate(points.length,
(i) => math.Point(points[i].longitude, points[i].latitude)),
],
// "precision" is a bit of a misnomer. It's a threshold for when to stop
// dividing-and-conquering the polygon in the hopes of finding a better
// point with more distance to the polygon's outline. It's given in
// point-units, i.e. degrees here. A bigger number means less precision,
// i.e. cheaper at the expense off less optimal label placement.
precision: 0.000001,
);
final latlng = LatLng(
labelPosition.point.y.toDouble(),
labelPosition.point.x.toDouble(),
);
return latlng;
}
10 changes: 8 additions & 2 deletions lib/src/layer/polygon_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ class Polygon {
// of opposing clock-directions cut holes into each other leading to a leaky optimization.
final bool _filledAndClockwise;

LatLngBounds? _boundingBox;
// Location where to place the label if `label` is set.
LatLng? _labelPosition;
LatLng get labelPosition =>
_labelPosition ??= computeLabelPosition(labelPlacement, points);

LatLngBounds? _boundingBox;
LatLngBounds get boundingBox =>
_boundingBox ??= LatLngBounds.fromPoints(points);

Expand Down Expand Up @@ -240,12 +244,14 @@ class PolygonPainter extends CustomPainter {
// The painter will be null if the layouting algorithm determined that
// there isn't enough space.
final painter = buildLabelTextPainter(
polygon.points,
polygon.labelPosition,
placementPoint: map.getOffsetFromOrigin(polygon.labelPosition),
points: offsets,
labelText: polygon.label!,
labelStyle: polygon.labelStyle,
rotationRad: map.rotationRad,
rotate: polygon.rotateLabel,
labelPlacement: polygon.labelPlacement,
padding: 10,
);

Expand Down
85 changes: 44 additions & 41 deletions lib/src/layer/tile_layer/tile_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,18 @@ part 'wms_tile_layer_options.dart';
/// avoid issues.
@immutable
class TileLayer extends StatefulWidget {
/// Defines the structure to create the URLs for the tiles. `{s}` means one of
/// the available subdomains (can be omitted) `{z}` zoom level `{x}` and `{y}`
/// — tile coordinates `{r}` can be used to add "&commat;2x" to the URL to
/// load retina tiles (can be omitted)
/// The URL template is a string that contains placeholders, which, when filled
/// in, create a URL/URI to a specific tile.
///
/// Example:
///
/// https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
///
/// Is translated to this:
///
/// https://a.tile.openstreetmap.org/12/2177/1259.png
/// For more information, see <https://docs.fleaflet.dev/layers/tile-layer>.
final String? urlTemplate;

/// Follows the same structure as [urlTemplate]. If specified, this URL is
/// used only if an error occurs when loading the [urlTemplate].
/// Fallback URL template, used if an error occurs when fetching tiles from
/// the [urlTemplate].
///
/// Note that specifying this (non-null) will result in tiles not being cached
/// in memory. This is to avoid issues where the [urlTemplate] is flaky, to
/// prevent different tilesets being displayed at the same time.
///
/// Avoid specifying this when using [AssetTileProvider] or [FileTileProvider],
/// as these providers are less performant and efficient when this is
Expand All @@ -57,23 +53,40 @@ class TileLayer extends StatefulWidget {
/// Default is 256
final double tileSize;

// The minimum zoom level down to which this layer will be
// displayed (inclusive).
/// The minimum zoom level down to which this layer will be displayed
/// (inclusive)
///
/// This should usually be 0 (as default).
final double minZoom;

/// The maximum zoom level up to which this layer will be displayed
/// (inclusive). In most tile providers goes from 0 to 19.
/// (inclusive).
///
/// Prefer [maxNativeZoom] for setting the maximum zoom level supported by the
/// tile source. The main usage for this is to display a different [TileLayer]
/// when zoomed far in.
///
/// Otherwise, this should usually be infinite (as default), so that there are
/// tiles always displayed.
final double maxZoom;

/// Minimum zoom number the tile source has available. If it is specified, the
/// tiles on all zoom levels lower than minNativeZoom will be loaded from
/// minNativeZoom level and auto-scaled.
final int? minNativeZoom;
/// Minimum zoom level supported by the tile source
///
/// Tiles from below this zoom level will not be displayed, instead tiles at
/// this zoom level will be displayed and scaled.
///
/// This should usually be 0 (as default), as most tile sources will support
/// zoom levels onwards from this.
final int minNativeZoom;

/// Maximum zoom number the tile source has available. If it is specified, the
/// tiles on all zoom levels higher than maxNativeZoom will be loaded from
/// maxNativeZoom level and auto-scaled.
final int? maxNativeZoom;
/// Maximum zoom number supported by the tile source has available.
///
/// Tiles from above this zoom level will not be displayed, instead tiles at
/// this zoom level will be displayed and scaled.
///
/// Most tile servers support up to zoom level 19, which is the default.
/// Otherwise, this should be specified.
final int maxNativeZoom;

/// If set to true, the zoom number used in tile URLs will be reversed
/// (`maxZoom - zoom` instead of `zoom`)
Expand Down Expand Up @@ -231,11 +244,11 @@ class TileLayer extends StatefulWidget {
super.key,
this.urlTemplate,
this.fallbackUrl,
double tileSize = 256.0,
double minZoom = 0.0,
double maxZoom = 18.0,
this.minNativeZoom,
this.maxNativeZoom,
double tileSize = 256,
double minZoom = 0,
double maxZoom = double.infinity,
this.minNativeZoom = 0,
this.maxNativeZoom = 19,
this.zoomReverse = false,
double zoomOffset = 0.0,
this.additionalOptions = const {},
Expand Down Expand Up @@ -639,18 +652,8 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {

/// Rounds the zoom to the nearest int and clamps it to the native zoom limits
/// if there are any.
int _clampToNativeZoom(double zoom) {
var result = zoom.round();

if (widget.minNativeZoom != null) {
result = max(result, widget.minNativeZoom!);
}
if (widget.maxNativeZoom != null) {
result = min(result, widget.maxNativeZoom!);
}

return result;
}
int _clampToNativeZoom(double zoom) =>
zoom.round().clamp(widget.minNativeZoom, widget.maxNativeZoom);

void _onTileLoadError(TileImage tile, Object error, StackTrace? stackTrace) {
debugPrint(error.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import 'package:flutter/painting.dart';
import 'package:http/http.dart';

/// Dedicated [ImageProvider] to fetch tiles from the network
///
/// Supports falling back to a secondary URL, if the primary URL fetch fails.
/// Note that specifying a [fallbackUrl] will prevent this image provider from
/// being cached.
@immutable
class FlutterMapNetworkImageProvider
extends ImageProvider<FlutterMapNetworkImageProvider> {
Expand All @@ -14,15 +18,27 @@ class FlutterMapNetworkImageProvider

/// The URL to fetch the tile from (GET request), in the event the original
/// [url] request fails
///
/// If this is non-null, [operator==] will always return `false` (except if
/// the two objects are [identical]). Therefore, if this is non-null, this
/// image provider will not be cached in memory.
final String? fallbackUrl;

/// The HTTP client to use to make network requests
///
/// Not included in [operator==].
final BaseClient httpClient;

/// The headers to include with the tile fetch request
///
/// Not included in [operator==].
final Map<String, String> headers;

/// Dedicated [ImageProvider] to fetch tiles from the network
/// Create a dedicated [ImageProvider] to fetch tiles from the network
///
/// Supports falling back to a secondary URL, if the primary URL fetch fails.
/// Note that specifying a [fallbackUrl] will prevent this image provider from
/// being cached.
const FlutterMapNetworkImageProvider({
required this.url,
required this.fallbackUrl,
Expand Down Expand Up @@ -75,4 +91,15 @@ class FlutterMapNetworkImageProvider

return decode(await ImmutableBuffer.fromUint8List(bytes));
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is FlutterMapNetworkImageProvider &&
fallbackUrl == null &&
url == other.url);

@override
int get hashCode =>
Object.hashAll([url, if (fallbackUrl != null) fallbackUrl]);
}
23 changes: 12 additions & 11 deletions lib/src/map/internal_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,13 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> {
}

if (degree == camera.rotation) {
return const MoveAndRotateResult(false, false);
return const (moveSuccess: false, rotateSuccess: false);
}

if (offset == Offset.zero) {
return MoveAndRotateResult(
true,
rotate(
return (
moveSuccess: true,
rotateSuccess: rotate(
degree,
hasGesture: hasGesture,
source: source,
Expand All @@ -178,8 +178,8 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> {
: Point(offset!.dx, offset.dy))
.rotate(camera.rotationRad);

return MoveAndRotateResult(
move(
return (
moveSuccess: move(
camera.unproject(
rotationCenter +
(camera.project(camera.center) - rotationCenter)
Expand All @@ -191,7 +191,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> {
source: source,
id: id,
),
rotate(
rotateSuccess: rotate(
camera.rotation + rotationDiff,
hasGesture: hasGesture,
source: source,
Expand All @@ -212,16 +212,17 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> {
required MapEventSource source,
required String? id,
}) =>
MoveAndRotateResult(
move(
(
moveSuccess: move(
newCenter,
newZoom,
offset: offset,
hasGesture: hasGesture,
source: source,
id: id,
),
rotate(newRotation, id: id, source: source, hasGesture: hasGesture),
rotateSuccess:
rotate(newRotation, id: id, source: source, hasGesture: hasGesture),
);

/// Note: All named parameters are required to prevent inconsistent default
Expand All @@ -238,7 +239,7 @@ class FlutterMapInternalController extends ValueNotifier<_InternalState> {
fitted.zoom,
offset: offset,
hasGesture: false,
source: MapEventSource.fitCamera,
source: MapEventSource.mapController,
id: null,
);
}
Expand Down
8 changes: 4 additions & 4 deletions lib/src/map/map_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ abstract class MapController {
/// The emitted [MapEventRotate.source]/[MapEventMove.source] properties will
/// be [MapEventSource.mapController].
///
/// The operation was successful if [MoveAndRotateResult.moveSuccess] and
/// [MoveAndRotateResult.rotateSuccess] are `true`.
/// The operation was successful if both fields of the resulting record are
/// `true`.
MoveAndRotateResult rotateAroundPoint(
double degree, {
Point<double>? point,
Expand All @@ -125,8 +125,8 @@ abstract class MapController {
///
/// See documentation on those methods for more details.
///
/// The operation was successful if [MoveAndRotateResult.moveSuccess] and
/// [MoveAndRotateResult.rotateSuccess] are `true`.
/// The operation was successful if both fields of the resulting record are
/// `true`.
MoveAndRotateResult moveAndRotate(
LatLng center,
double zoom,
Expand Down
Loading

0 comments on commit ca2ca75

Please sign in to comment.