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

[v2.2.0] Add Rotation Capability To OverlayImage #1315

Merged
merged 2 commits into from
Jul 29, 2022
Merged
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
50 changes: 48 additions & 2 deletions example/lib/pages/overlay_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,23 @@ class OverlayImagePage extends StatelessWidget {

@override
Widget build(BuildContext context) {
var overlayImages = <OverlayImage>[
final topLeftCorner = LatLng(53.377, -2.999);
final bottomRightCorner = LatLng(53.475, 0.275);
final bottomLeftCorner = LatLng(52.503, -1.868);

final overlayImages = [
OverlayImage(
bounds: LatLngBounds(LatLng(51.5, -0.09), LatLng(48.8566, 2.3522)),
opacity: 0.8,
imageProvider: const NetworkImage(
'https://images.pexels.com/photos/231009/pexels-photo-231009.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=300&w=600')),
RotatedOverlayImage(
topLeftCorner: topLeftCorner,
bottomLeftCorner: bottomLeftCorner,
bottomRightCorner: bottomRightCorner,
opacity: 0.8,
imageProvider: const NetworkImage(
'https://images.pexels.com/photos/231009/pexels-photo-231009.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=300&w=600')),
];

return Scaffold(
Expand All @@ -43,7 +54,21 @@ class OverlayImagePage extends StatelessWidget {
subdomains: ['a', 'b', 'c'],
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
),
OverlayImageLayerOptions(overlayImages: overlayImages)
OverlayImageLayerOptions(overlayImages: overlayImages),
MarkerLayerOptions(markers: [
Marker(
point: topLeftCorner,
builder: (context) => const _Circle(
color: Colors.redAccent, label: "TL")),
Marker(
point: bottomLeftCorner,
builder: (context) => const _Circle(
color: Colors.redAccent, label: "BL")),
Marker(
point: bottomRightCorner,
builder: (context) => const _Circle(
color: Colors.redAccent, label: "BR")),
])
],
),
),
Expand All @@ -53,3 +78,24 @@ class OverlayImagePage extends StatelessWidget {
);
}
}

class _Circle extends StatelessWidget {
final String label;
final Color color;

const _Circle({Key? key, required this.label, required this.color})
: super(key: key);

@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
child: Center(
child: Text(
label,
style: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.white),
),
));
}
}
155 changes: 124 additions & 31 deletions lib/src/layer/overlay_image_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/src/map/map.dart';
import 'package:flutter_map/src/core/bounds.dart';
import 'package:latlong2/latlong.dart';

class OverlayImageLayerOptions extends LayerOptions {
final List<OverlayImage> overlayImages;
final List<BaseOverlayImage> overlayImages;

OverlayImageLayerOptions({
Key? key,
Expand All @@ -15,18 +16,132 @@ class OverlayImageLayerOptions extends LayerOptions {
}) : super(key: key, rebuild: rebuild);
}

class OverlayImage {
/// Base class for all overlay images.
abstract class BaseOverlayImage {
ImageProvider get imageProvider;

double get opacity;

bool get gaplessPlayback;

Positioned buildPositionedForOverlay(MapState map);

Image buildImageForOverlay() {
return Image(
image: imageProvider,
fit: BoxFit.fill,
color: Color.fromRGBO(255, 255, 255, opacity),
colorBlendMode: BlendMode.modulate,
gaplessPlayback: gaplessPlayback,
);
}
}

/// Unrotated overlay image that spans between a given bounding box.
///
/// The shortest side of the image will be placed along the shortest side of the
/// bounding box to minimize distortion.
class OverlayImage extends BaseOverlayImage {
final LatLngBounds bounds;
@override
final ImageProvider imageProvider;
@override
final double opacity;
@override
final bool gaplessPlayback;

OverlayImage(
{required this.bounds,
required this.imageProvider,
this.opacity = 1.0,
this.gaplessPlayback = false});

@override
Positioned buildPositionedForOverlay(MapState map) {
final pixelOrigin = map.getPixelOrigin();
// northWest is not necessarily upperLeft depending on projection
final bounds = Bounds<num>(
map.project(this.bounds.northWest) - pixelOrigin,
map.project(this.bounds.southEast) - pixelOrigin,
);
return Positioned(
left: bounds.topLeft.x.toDouble(),
top: bounds.topLeft.y.toDouble(),
width: bounds.size.x.toDouble(),
height: bounds.size.y.toDouble(),
child: buildImageForOverlay());
}
}

/// Spans an image across three corner points.
///
/// Therefore this layer can be used to rotate or skew an image on the map.
///
/// The image is transformed so that its corners touch the [topLeftCorner],
/// [bottomLeftCorner] and [bottomRightCorner] points while the top-right
/// corner point is derived from the other points.
class RotatedOverlayImage extends BaseOverlayImage {
@override
final ImageProvider imageProvider;

final LatLng topLeftCorner, bottomLeftCorner, bottomRightCorner;

@override
final double opacity;

@override
final bool gaplessPlayback;

OverlayImage({
required this.bounds,
required this.imageProvider,
this.opacity = 1.0,
this.gaplessPlayback = false,
});
/// The filter quality when rotating the image.
final FilterQuality? filterQuality;

RotatedOverlayImage(
{required this.imageProvider,
required this.topLeftCorner,
required this.bottomLeftCorner,
required this.bottomRightCorner,
this.opacity = 1.0,
this.gaplessPlayback = false,
this.filterQuality = FilterQuality.medium});

@override
Positioned buildPositionedForOverlay(MapState map) {
final pixelOrigin = map.getPixelOrigin();

final pxTopLeft = map.project(topLeftCorner) - pixelOrigin;
final pxBottomRight = map.project(bottomRightCorner) - pixelOrigin;
final pxBottomLeft = (map.project(bottomLeftCorner) - pixelOrigin);
// calculate pixel coordinate of top-right corner by calculating the
// vector from bottom-left to top-left and adding it to bottom-right
final pxTopRight = (pxTopLeft - pxBottomLeft + pxBottomRight);

// update/enlarge bounds so the new corner points fit within
final bounds = Bounds<num>(pxTopLeft, pxBottomRight)
.extend(pxTopRight)
.extend(pxBottomLeft);

final vectorX = (pxTopRight - pxTopLeft) / bounds.size.x;
final vectorY = (pxBottomLeft - pxTopLeft) / bounds.size.y;
final offset = pxTopLeft - bounds.topLeft;

final a = vectorX.x.toDouble();
final b = vectorX.y.toDouble();
final c = vectorY.x.toDouble();
final d = vectorY.y.toDouble();
final tx = offset.x.toDouble();
final ty = offset.y.toDouble();

return Positioned(
left: bounds.topLeft.x.toDouble(),
top: bounds.topLeft.y.toDouble(),
width: bounds.size.x.toDouble(),
height: bounds.size.y.toDouble(),
child: Transform(
transform:
Matrix4(a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1),
filterQuality: filterQuality,
child: buildImageForOverlay()));
}
}

class OverlayImageLayerWidget extends StatelessWidget {
Expand Down Expand Up @@ -59,33 +174,11 @@ class OverlayImageLayer extends StatelessWidget {
child: Stack(
children: <Widget>[
for (var overlayImage in overlayImageOpts.overlayImages)
_positionedForOverlay(overlayImage),
overlayImage.buildPositionedForOverlay(map),
],
),
);
},
);
}

Positioned _positionedForOverlay(OverlayImage overlayImage) {
// northWest is not necessarily upperLeft depending on projection
final bounds = Bounds<num>(
map.project(overlayImage.bounds.northWest) - map.getPixelOrigin(),
map.project(overlayImage.bounds.southEast) - map.getPixelOrigin(),
);

return Positioned(
left: bounds.topLeft.x.toDouble(),
top: bounds.topLeft.y.toDouble(),
width: bounds.size.x.toDouble(),
height: bounds.size.y.toDouble(),
child: Image(
image: overlayImage.imageProvider,
fit: BoxFit.fill,
color: Color.fromRGBO(255, 255, 255, overlayImage.opacity),
colorBlendMode: BlendMode.modulate,
gaplessPlayback: overlayImage.gaplessPlayback,
),
);
}
}