diff --git a/example/assets/map/epsg3413/amsr2.png b/example/assets/map/epsg3413/amsr2.png new file mode 100644 index 000000000..ad24c5e91 Binary files /dev/null and b/example/assets/map/epsg3413/amsr2.png differ diff --git a/example/lib/main.dart b/example/lib/main.dart index 961652307..5e874f338 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -33,6 +33,7 @@ import './pages/tile_builder_example.dart'; import './pages/tile_loading_error_handle.dart'; import './pages/widgets.dart'; import './pages/wms_tile_layer.dart'; +import './pages/epsg3413_crs.dart'; void main() => runApp(const MyApp()); @@ -82,6 +83,7 @@ class MyApp extends StatelessWidget { MapInsideListViewPage.route: (context) => const MapInsideListViewPage(), ResetTileLayerPage.route: (context) => const ResetTileLayerPage(), EPSG4326Page.route: (context) => const EPSG4326Page(), + EPSG3413Page.route: (context) => const EPSG3413Page(), MaxBoundsPage.route: (context) => const MaxBoundsPage(), PointToLatLngPage.route: (context) => const PointToLatLngPage(), }, diff --git a/example/lib/pages/epsg3413_crs.dart b/example/lib/pages/epsg3413_crs.dart new file mode 100644 index 000000000..e12d4f534 --- /dev/null +++ b/example/lib/pages/epsg3413_crs.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/plugin_api.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:proj4dart/proj4dart.dart' as proj4; + +import '../../widgets/drawer.dart'; + +class EPSG3413Page extends StatefulWidget { + static const String route = 'EPSG3413 Page'; + + const EPSG3413Page({Key? key}) : super(key: key); + + @override + _EPSG3413PageState createState() => _EPSG3413PageState(); +} + +class _EPSG3413PageState extends State { + late final Proj4Crs epsg3413CRS; + + double? maxZoom; + + @override + void initState() { + super.initState(); + + // 9 example zoom level resolutions + final resolutions = [ + 32768, + 16384, + 8192, + 4096, + 2048, + 1024, + 512, + 256, + 128, + ]; + + final epsg3413Bounds = Bounds( + const CustomPoint(-4511619.0, -4511336.0), + const CustomPoint(4510883.0, 4510996.0), + ); + + maxZoom = (resolutions.length - 1).toDouble(); + + // EPSG:3413 is a user-defined projection from a valid Proj4 definition string + // From: http://epsg.io/3413, proj definition: http://epsg.io/3413.proj4 + // Find Projection by name or define it if not exists + final proj4.Projection epsg3413 = proj4.Projection.get('EPSG:3413') ?? + proj4.Projection.add('EPSG:3413', + '+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'); + + epsg3413CRS = Proj4Crs.fromFactory( + code: 'EPSG:3413', + proj4Projection: epsg3413, + resolutions: resolutions, + bounds: epsg3413Bounds, + origins: [const CustomPoint(0, 0)], + scales: null, + transformation: null, + ); + } + + @override + Widget build(BuildContext context) { + // These circles should have the same pixel radius on the map + final circles = [ + CircleMarker( + point: LatLng(90, 0), + radius: 20000, + useRadiusInMeter: true, + color: Colors.yellow, + ) + ]; + for (final lon in [-90.0, 0.0, 90.0, 180.0]) { + circles.add(CircleMarker( + point: LatLng(80, lon), + radius: 20000, + useRadiusInMeter: true, + color: Colors.red, + )); + } + + // Add latitude line at 80 degrees + final distancePoleToLat80 = + const Distance().distance(LatLng(90, 0), LatLng(80, 0)); + circles.add(CircleMarker( + point: LatLng(90, 0), + radius: distancePoleToLat80, + useRadiusInMeter: true, + color: Colors.transparent, + borderColor: Colors.black, + borderStrokeWidth: 1.0, + )); + + return Scaffold( + appBar: AppBar(title: const Text('EPSG:3413 CRS')), + drawer: buildDrawer(context, EPSG3413Page.route), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const Padding( + padding: EdgeInsets.only(top: 8.0, bottom: 2.0), + child: Text( + 'Tricky edge-cases with polar projections', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.blue, + fontSize: 16.0, + ), + ), + ), + const Text( + 'Details: https://github.com/fleaflet/flutter_map/pull/1295'), + const Padding( + padding: EdgeInsets.only(top: 8.0, bottom: 2.0), + child: SizedBox( + width: 500, + child: Text( + '• Northern and eastern directions are relative to where you are on the map:\n' + ' • A red dot moves north toward the yellow dot (North Pole).\n' + ' • A red dot moves east counter-clockwise along the black latitude line (80°).\n' + '• The lower left and right corners of the overlay image are the northern corners.' + //textAlign: TextAlign.center, + ), + ), + ), + Flexible( + child: FlutterMap( + options: MapOptions( + crs: epsg3413CRS, + center: LatLng(90, 0), + zoom: 3.0, + maxZoom: maxZoom, + ), + layers: [ + TileLayerOptions( + opacity: 1, + backgroundColor: Colors.transparent, + wmsOptions: WMSTileLayerOptions( + crs: epsg3413CRS, + transparent: true, + format: 'image/jpeg', + baseUrl: + 'https://www.gebco.net/data_and_products/gebco_web_services/north_polar_view_wms/mapserv?', + layers: ['gebco_north_polar_view'], + ), + ), + OverlayImageLayerOptions( + overlayImages: [ + OverlayImage( + bounds: LatLngBounds( + LatLng(72.7911372, 162.6196478), + LatLng(85.2802493, 79.794166), + ), + imageProvider: Image.asset( + 'map/epsg3413/amsr2.png', + ).image, + ) + ], + ), + CircleLayerOptions( + circles: circles, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/widgets/drawer.dart b/example/lib/widgets/drawer.dart index be7c38e74..e2c5925df 100644 --- a/example/lib/widgets/drawer.dart +++ b/example/lib/widgets/drawer.dart @@ -8,6 +8,7 @@ import 'package:flutter_map_example/pages/point_to_latlng.dart'; import '../pages/animated_map_controller.dart'; import '../pages/circle.dart'; import '../pages/custom_crs/custom_crs.dart'; +import '../pages/epsg3413_crs.dart'; import '../pages/esri.dart'; import '../pages/home.dart'; import '../pages/interactive_test_page.dart'; @@ -237,12 +238,19 @@ Drawer buildDrawer(BuildContext context, String currentRoute) { }, ), ListTile( - title: const Text('EPSG4326 Crs'), + title: const Text('EPSG4326 CRS'), selected: currentRoute == EPSG4326Page.route, onTap: () { Navigator.pushReplacementNamed(context, EPSG4326Page.route); }, ), + ListTile( + title: const Text('EPSG3413 CRS'), + selected: currentRoute == EPSG3413Page.route, + onTap: () { + Navigator.pushReplacementNamed(context, EPSG3413Page.route); + }, + ), _buildMenuItem( context, const Text('Stateful markers'), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index afbcf07a2..5f478c080 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -59,3 +59,4 @@ flutter: - assets/map/anholt_osmbright/14/8725/ - assets/map/anholt_osmbright/14/8726/ - assets/map/anholt_osmbright/14/8727/ + - assets/map/epsg3413/amsr2.png diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index 9ab7b11c5..bf790980f 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -66,18 +66,12 @@ class CircleLayer extends StatelessWidget { builder: (BuildContext context, _) { final circleWidgets = []; for (final circle in circleOpts.circles) { - var pos = map.project(circle.point); - pos = pos.multiplyBy(map.getZoomScale(map.zoom, map.zoom)) - - map.getPixelOrigin(); - circle.offset = Offset(pos.x.toDouble(), pos.y.toDouble()); + circle.offset = map.getOffsetFromOrigin(circle.point); if (circle.useRadiusInMeter) { final r = const Distance().offset(circle.point, circle.radius, 180); - var rpos = map.project(r); - rpos = rpos.multiplyBy(map.getZoomScale(map.zoom, map.zoom)) - - map.getPixelOrigin(); - - circle.realRadius = rpos.y - pos.y; + final delta = circle.offset - map.getOffsetFromOrigin(r); + circle.realRadius = delta.distance; } circleWidgets.add( diff --git a/lib/src/layer/overlay_image_layer.dart b/lib/src/layer/overlay_image_layer.dart index bf1265081..5bfd53b17 100644 --- a/lib/src/layer/overlay_image_layer.dart +++ b/lib/src/layer/overlay_image_layer.dart @@ -3,6 +3,7 @@ import 'dart:async'; 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'; class OverlayImageLayerOptions extends LayerOptions { final List overlayImages; @@ -67,16 +68,17 @@ class OverlayImageLayer extends StatelessWidget { } Positioned _positionedForOverlay(OverlayImage overlayImage) { - final pixelOrigin = map.getPixelOrigin(); - final upperLeftPixel = - map.project(overlayImage.bounds.northWest) - pixelOrigin; - final bottomRightPixel = - map.project(overlayImage.bounds.southEast) - pixelOrigin; + // northWest is not necessarily upperLeft depending on projection + final bounds = Bounds( + map.project(overlayImage.bounds.northWest) - map.getPixelOrigin(), + map.project(overlayImage.bounds.southEast) - map.getPixelOrigin(), + ); + return Positioned( - left: upperLeftPixel.x.toDouble(), - top: upperLeftPixel.y.toDouble(), - width: (bottomRightPixel.x - upperLeftPixel.x).toDouble(), - height: (bottomRightPixel.y - upperLeftPixel.y).toDouble(), + 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, diff --git a/lib/src/layer/polygon_layer.dart b/lib/src/layer/polygon_layer.dart index 4a70a0085..daaccb812 100644 --- a/lib/src/layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer.dart @@ -1,5 +1,4 @@ import 'dart:math'; -import 'dart:ui'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -42,6 +41,8 @@ class Polygon { final bool disableHolesBorder; final bool isDotted; final bool isFilled; + final StrokeCap strokeCap; + final StrokeJoin strokeJoin; late final LatLngBounds boundingBox; final String? label; final TextStyle labelStyle; @@ -56,6 +57,8 @@ class Polygon { this.disableHolesBorder = false, this.isDotted = false, this.isFilled = false, + this.strokeCap = StrokeCap.round, + this.strokeJoin = StrokeJoin.round, this.label, this.labelStyle = const TextStyle(), this.labelPlacement = PolygonLabelPlacement.centroid, @@ -143,14 +146,8 @@ class PolygonLayer extends StatelessWidget { final len = points.length; for (var i = 0; i < len; ++i) { final point = points[i]; - - var pos = map.project(point); - pos = pos.multiplyBy(map.getZoomScale(map.zoom, map.zoom)) - - map.getPixelOrigin(); - offsets.add(Offset(pos.x.toDouble(), pos.y.toDouble())); - if (i > 0) { - offsets.add(Offset(pos.x.toDouble(), pos.y.toDouble())); - } + final offset = map.getOffsetFromOrigin(point); + offsets.add(offset); } } } @@ -171,13 +168,13 @@ class PolygonPainter extends CustomPainter { void _paintBorder(Canvas canvas) { if (polygonOpt.borderStrokeWidth > 0.0) { - final borderRadius = (polygonOpt.borderStrokeWidth / 2); - final borderPaint = Paint() ..color = polygonOpt.borderColor ..strokeWidth = polygonOpt.borderStrokeWidth; if (polygonOpt.isDotted) { + final borderRadius = (polygonOpt.borderStrokeWidth / 2); + final spacing = polygonOpt.borderStrokeWidth * 1.5; _paintDottedLine( canvas, polygonOpt.offsets, borderRadius, spacing, borderPaint); @@ -190,12 +187,17 @@ class PolygonPainter extends CustomPainter { } } } else { - _paintLine(canvas, polygonOpt.offsets, borderRadius, borderPaint); + borderPaint + ..style = PaintingStyle.stroke + ..strokeCap = polygonOpt.strokeCap + ..strokeJoin = polygonOpt.strokeJoin; + + _paintLine(canvas, polygonOpt.offsets, borderPaint); if (!polygonOpt.disableHolesBorder && null != polygonOpt.holeOffsetsList) { for (final offsets in polygonOpt.holeOffsetsList!) { - _paintLine(canvas, offsets, borderRadius, borderPaint); + _paintLine(canvas, offsets, borderPaint); } } } @@ -224,12 +226,12 @@ class PolygonPainter extends CustomPainter { canvas.drawCircle(offsets.last, radius, paint); } - void _paintLine( - Canvas canvas, List offsets, double radius, Paint paint) { - canvas.drawPoints(PointMode.lines, [...offsets, offsets[0]], paint); - for (final offset in offsets) { - canvas.drawCircle(offset, radius, paint); + void _paintLine(Canvas canvas, List offsets, Paint paint) { + if (offsets.isEmpty) { + return; } + final path = Path()..addPolygon(offsets, true); + canvas.drawPath(path, paint); } void _paintPolygon(Canvas canvas, Rect rect) { diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 27631f7f6..e925d8762 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -130,14 +130,8 @@ class PolylineLayer extends StatelessWidget { final len = points.length; for (var i = 0; i < len; ++i) { final point = points[i]; - - var pos = map.project(point); - pos = pos.multiplyBy(map.getZoomScale(map.zoom, map.zoom)) - - map.getPixelOrigin(); - offsets.add(Offset(pos.x.toDouble(), pos.y.toDouble())); - if (i > 0) { - offsets.add(Offset(pos.x.toDouble(), pos.y.toDouble())); - } + final offset = map.getOffsetFromOrigin(point); + offsets.add(offset); } } } @@ -246,10 +240,11 @@ class PolylinePainter extends CustomPainter { } void _paintLine(Canvas canvas, List offsets, Paint paint) { - if (offsets.isNotEmpty) { - final path = ui.Path()..addPolygon(offsets, false); - canvas.drawPath(path, paint); + if (offsets.isEmpty) { + return; } + final path = ui.Path()..addPolygon(offsets, false); + canvas.drawPath(path, paint); } ui.Gradient _paintGradient() => ui.Gradient.linear(polylineOpt.offsets.first, diff --git a/lib/src/map/map.dart b/lib/src/map/map.dart index d57d4314e..ea5b9b390 100644 --- a/lib/src/map/map.dart +++ b/lib/src/map/map.dart @@ -567,6 +567,11 @@ class MapState { return _pixelOrigin; } + Offset getOffsetFromOrigin(LatLng pos) { + final delta = project(pos) - getPixelOrigin(); + return Offset(delta.x.toDouble(), delta.y.toDouble()); + } + CustomPoint getNewPixelOrigin(LatLng center, [double? zoom]) { final viewHalf = size / 2.0; return (project(center, zoom) - viewHalf).round();