Skip to content

Commit

Permalink
[v2.2.0] Add Rotation Capability To OverlayImage (#1315)
Browse files Browse the repository at this point in the history
* Add rotation capability to overlay image

* Separate overlay image classes
  • Loading branch information
Robbendebiene authored Jul 29, 2022
1 parent 4fdfb82 commit 2d2a956
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 33 deletions.
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,
),
);
}
}

0 comments on commit 2d2a956

Please sign in to comment.