Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter_map_example/pages/cancellable_tile_provider.dart';
import 'package:flutter_map_example/pages/circle.dart';
import 'package:flutter_map_example/pages/custom_crs/custom_crs.dart';
import 'package:flutter_map_example/pages/debouncing_tile_update_transformer.dart';
import 'package:flutter_map_example/pages/epsg3006_crs.dart';
import 'package:flutter_map_example/pages/epsg3413_crs.dart';
import 'package:flutter_map_example/pages/epsg4326_crs.dart';
import 'package:flutter_map_example/pages/fallback_url_page.dart';
Expand Down Expand Up @@ -67,6 +68,7 @@ class MyApp extends StatelessWidget {
CirclePage.route: (context) => const CirclePage(),
OverlayImagePage.route: (context) => const OverlayImagePage(),
PolygonPage.route: (context) => const PolygonPage(),
EPSG3006Page.route: (context) => const EPSG3006Page(),
PolygonPerfStressPage.route: (context) => const PolygonPerfStressPage(),
SlidingMapPage.route: (_) => const SlidingMapPage(),
WMSLayerPage.route: (context) => const WMSLayerPage(),
Expand Down
76 changes: 76 additions & 0 deletions example/lib/pages/epsg3006_crs.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart';
import 'package:latlong2/latlong.dart';
import 'package:proj4dart/proj4dart.dart' as proj4;

typedef HitValue = ({String title, String subtitle});

class EPSG3006Page extends StatefulWidget {
static const String route = '/crs_epsg3006';

const EPSG3006Page({super.key});

@override
State<EPSG3006Page> createState() => _EPSG3006PageState();
}

class _EPSG3006PageState extends State<EPSG3006Page> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('EPSG:3006 CRS')),
drawer: const MenuDrawer(EPSG3006Page.route),
body: Stack(
children: [
// OSM based, uses somewhat strange bounds.
// No access restrictions or fees in GetCapabilities as of 2024-10-15
FlutterMap(
options: MapOptions(
initialCameraFit: CameraFit.bounds(
bounds: LatLngBounds(
const LatLng(61.285991891313344, 17.006922572652666),
const LatLng(61.279648385340494, 17.018309853620366),
),
),
crs: Proj4Crs.fromFactory(
code: 'EPSG:3006',
proj4Projection: proj4.Projection.add(
'EPSG:3006',
'+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs',
),
origins: const [
Point(-58122915.354077406, 19995929.885879364),
],
resolutions: const <double>[
4096,
2048,
1024,
512,
256,
128,
64,
32,
16,
8,
4,
2,
1,
0.5,
0.25,
0.125
],
)),
children: [
TileLayer(
tileBuilder: coordinateDebugTileBuilder,
urlTemplate:
'https://maps.trafikinfo.trafikverket.se/MapService/wmts.axd/BakgrundskartaNorden.gpkg?layer=Background&style=default&tilematrixset=Default.256.3006&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image%2Fpng&TileMatrix={z}&TileCol={x}&TileRow={y}')
],
),
],
),
);
}
}
6 changes: 6 additions & 0 deletions example/lib/widgets/drawer/menu_drawer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flutter_map_example/pages/cancellable_tile_provider.dart';
import 'package:flutter_map_example/pages/circle.dart';
import 'package:flutter_map_example/pages/custom_crs/custom_crs.dart';
import 'package:flutter_map_example/pages/debouncing_tile_update_transformer.dart';
import 'package:flutter_map_example/pages/epsg3006_crs.dart';
import 'package:flutter_map_example/pages/epsg3413_crs.dart';
import 'package:flutter_map_example/pages/epsg4326_crs.dart';
import 'package:flutter_map_example/pages/fallback_url_page.dart';
Expand Down Expand Up @@ -195,6 +196,11 @@ class MenuDrawer extends StatelessWidget {
currentRoute: currentRoute,
routeName: EPSG3413Page.route,
),
MenuItemWidget(
caption: 'EPSG3006 CRS',
routeName: EPSG3006Page.route,
currentRoute: currentRoute,
),
const Divider(),
MenuItemWidget(
caption: 'Sliding Map',
Expand Down
6 changes: 6 additions & 0 deletions lib/src/geo/crs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ abstract class Crs {

/// Rescales the bounds to a given zoom value.
Bounds<double>? getProjectedBounds(double zoom);

/// Returns true if we want the world to be replicated, longitude-wise.
bool get replicatesWorldLongitude => false;
}

/// Internal base class for CRS with a single zoom-level independent transformation.
Expand Down Expand Up @@ -175,6 +178,9 @@ class Epsg3857 extends CrsWithStaticTransformation {
);
return Point<double>(x, y);
}

@override
bool get replicatesWorldLongitude => true;
}

/// EPSG:4326, A common CRS among GIS enthusiasts.
Expand Down
25 changes: 15 additions & 10 deletions lib/src/gestures/map_interactive_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -883,18 +883,23 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>

final newCenterPoint = _camera.project(_mapCenterStart) +
_flingAnimation.value.toPoint().rotate(_camera.rotationRad);
final math.Point<double> bestCenterPoint;
final double worldSize = _camera.crs.scale(_camera.zoom);
if (newCenterPoint.x > worldSize) {
bestCenterPoint =
math.Point(newCenterPoint.x - worldSize, newCenterPoint.y);
} else if (newCenterPoint.x < 0) {
bestCenterPoint =
math.Point(newCenterPoint.x + worldSize, newCenterPoint.y);
final LatLng newCenter;
if (!_camera.crs.replicatesWorldLongitude) {
newCenter = _camera.unproject(newCenterPoint);
} else {
bestCenterPoint = newCenterPoint;
final math.Point<double> bestCenterPoint;
final double worldSize = _camera.crs.scale(_camera.zoom);
if (newCenterPoint.x > worldSize) {
bestCenterPoint =
math.Point(newCenterPoint.x - worldSize, newCenterPoint.y);
} else if (newCenterPoint.x < 0) {
bestCenterPoint =
math.Point(newCenterPoint.x + worldSize, newCenterPoint.y);
} else {
bestCenterPoint = newCenterPoint;
}
newCenter = _camera.unproject(bestCenterPoint);
}
final newCenter = _camera.unproject(bestCenterPoint);

widget.controller.moveRaw(
newCenter,
Expand Down
65 changes: 42 additions & 23 deletions lib/src/layer/tile_layer/tile_coordinates.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,6 @@ class TileCoordinates extends Point<int> {
/// Create a new [TileCoordinates] instance.
const TileCoordinates(super.x, super.y, this.z);

/// Returns a unique value for the same tile on all world replications.
factory TileCoordinates.key(TileCoordinates coordinates) {
if (coordinates.z < 0) {
return coordinates;
}
final modulo = 1 << coordinates.z;
int x = coordinates.x;
while (x < 0) {
x += modulo;
}
while (x >= modulo) {
x -= modulo;
}
int y = coordinates.y;
while (y < 0) {
y += modulo;
}
while (y >= modulo) {
y -= modulo;
}
return TileCoordinates(x, y, coordinates.z);
}

@override
String toString() => 'TileCoordinate($x, $y, $z)';

Expand Down Expand Up @@ -70,3 +47,45 @@ class TileCoordinates extends Point<int> {
return x ^ y << 24 ^ z << 48;
}
}

/// Simplifies coordinates in the context of world replications.
///
/// On maps with world replications, different tile coordinates may actually
/// refer to the same "simplified" tile coordinate - the coordinate that starts
/// from 0.
/// For instance, on zoom level 0, all tile coordinates can be simplified to
/// (0,0), which is the only tile.
/// On zoom level 1, (0, 1) and (2, 1) can be simplified to (0, 1), as they both
/// mean the bottom left tile.
/// And when we're not in the context of world replications, we don't have to
/// simplify the tile coordinates: we just return the same value.
class TileCoordinatesSimplifier {
/// True if we simplify the coordinates according to the world replications.
bool replicatesWorldLongitude = false;

/// Returns the simplification of the coordinates.
TileCoordinates get(TileCoordinates positionCoordinates) {
if (!replicatesWorldLongitude) {
return positionCoordinates;
}
if (positionCoordinates.z < 0) {
return positionCoordinates;
}
final modulo = 1 << positionCoordinates.z;
int x = positionCoordinates.x;
while (x < 0) {
x += modulo;
}
while (x >= modulo) {
x -= modulo;
}
int y = positionCoordinates.y;
while (y < 0) {
y += modulo;
}
while (y >= modulo) {
y -= modulo;
}
return TileCoordinates(x, y, positionCoordinates.z);
}
}
16 changes: 12 additions & 4 deletions lib/src/layer/tile_layer/tile_image_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ class TileImageManager {
bool get allLoaded =>
_tiles.values.none((tile) => tile.loadFinishedAt == null);

/// Coordinates simplifier.
final TileCoordinatesSimplifier tileCoordinatesSimplifier =
TileCoordinatesSimplifier();

/// Filter tiles to only tiles that would be visible on screen. Specifically:
/// 1. Tiles in the visible range at the target zoom level.
/// 2. Tiles at non-target zoom level that would cover up holes that would
Expand All @@ -42,10 +46,12 @@ class TileImageManager {
// `keepRange` is irrelevant here since we're not using the output for
// pruning storage but rather to decide on what to put on screen.
keepRange: visibleRange,
tileCoordinatesSimplifier: tileCoordinatesSimplifier,
).renderTiles;
final List<TileRenderer> tileRenderers = <TileRenderer>[];
for (final position in positionCoordinates) {
final TileImage? tileImage = _tiles[TileCoordinates.key(position)];
final TileImage? tileImage =
_tiles[tileCoordinatesSimplifier.get(position)];
if (tileImage != null) {
tileRenderers.add(TileRenderer(tileImage, position));
}
Expand All @@ -68,7 +74,7 @@ class TileImageManager {
final notLoaded = <TileImage>[];

for (final coordinates in tileBoundsAtZoom.validCoordinatesIn(tileRange)) {
final cleanCoordinates = TileCoordinates.key(coordinates);
final cleanCoordinates = tileCoordinatesSimplifier.get(coordinates);
TileImage? tile = _tiles[cleanCoordinates];
if (tile == null) {
tile = createTile(cleanCoordinates);
Expand Down Expand Up @@ -97,11 +103,11 @@ class TileImageManager {
required bool Function(TileImage tileImage) evictImageFromCache,
}) {
_positionCoordinates.remove(key);
final cleanKey = TileCoordinates.key(key);
final cleanKey = tileCoordinatesSimplifier.get(key);

// guard if positionCoordinates with the same tileImage.
for (final positionCoordinates in _positionCoordinates) {
if (TileCoordinates.key(positionCoordinates) == cleanKey) {
if (tileCoordinatesSimplifier.get(positionCoordinates) == cleanKey) {
return;
}
}
Expand Down Expand Up @@ -172,6 +178,7 @@ class TileImageManager {
positionCoordinates: _positionCoordinates,
visibleRange: visibleRange,
keepRange: visibleRange.expand(pruneBuffer),
tileCoordinatesSimplifier: tileCoordinatesSimplifier,
);

_evictErrorTiles(pruningState, evictStrategy);
Expand Down Expand Up @@ -210,6 +217,7 @@ class TileImageManager {
positionCoordinates: _positionCoordinates,
visibleRange: visibleRange,
keepRange: visibleRange.expand(pruneBuffer),
tileCoordinatesSimplifier: tileCoordinatesSimplifier,
),
evictStrategy,
);
Expand Down
35 changes: 27 additions & 8 deletions lib/src/layer/tile_layer/tile_image_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ final class TileImageView {
required Set<TileCoordinates> positionCoordinates,
required DiscreteTileRange visibleRange,
required DiscreteTileRange keepRange,
final TileCoordinatesSimplifier? tileCoordinatesSimplifier,
}) : _tileImages = tileImages,
_positionCoordinates = positionCoordinates,
_visibleRange = visibleRange,
_keepRange = keepRange;
_keepRange = keepRange,
__tileCoordinatesSimplifier = tileCoordinatesSimplifier;

final TileCoordinatesSimplifier? __tileCoordinatesSimplifier;

TileCoordinatesSimplifier get _tileCoordinatesSimplifier =>
__tileCoordinatesSimplifier ?? TileCoordinatesSimplifier();

/// Get a list with all tiles that have an error and are outside of the
/// margin that should get kept.
Expand All @@ -37,11 +44,15 @@ final class TileImageView {
List<TileCoordinates> _errorTilesWithinRange(DiscreteTileRange range) {
final List<TileCoordinates> result = <TileCoordinates>[];
for (final positionCoordinates in _positionCoordinates) {
if (range.contains(positionCoordinates)) {
if (range.contains(
positionCoordinates,
replicatesWorldLongitude:
_tileCoordinatesSimplifier.replicatesWorldLongitude,
)) {
continue;
}
final TileImage? tileImage =
_tileImages[TileCoordinates.key(positionCoordinates)];
_tileImages[_tileCoordinatesSimplifier.get(positionCoordinates)];
if (tileImage?.loadError ?? false) {
result.add(positionCoordinates);
}
Expand All @@ -55,7 +66,11 @@ final class TileImageView {
final retain = HashSet<TileCoordinates>();

for (final positionCoordinates in _positionCoordinates) {
if (!_keepRange.contains(positionCoordinates)) {
if (!_keepRange.contains(
positionCoordinates,
replicatesWorldLongitude:
_tileCoordinatesSimplifier.replicatesWorldLongitude,
)) {
stale.add(positionCoordinates);
continue;
}
Expand Down Expand Up @@ -86,14 +101,18 @@ final class TileImageView {
final retain = HashSet<TileCoordinates>();

for (final positionCoordinates in _positionCoordinates) {
if (!_visibleRange.contains(positionCoordinates)) {
if (!_visibleRange.contains(
positionCoordinates,
replicatesWorldLongitude:
_tileCoordinatesSimplifier.replicatesWorldLongitude,
)) {
continue;
}

retain.add(positionCoordinates);

final TileImage? tile =
_tileImages[TileCoordinates.key(positionCoordinates)];
_tileImages[_tileCoordinatesSimplifier.get(positionCoordinates)];
if (tile == null || !tile.readyToDisplay) {
final retainedAncestor = _retainAncestor(
retain,
Expand Down Expand Up @@ -131,7 +150,7 @@ final class TileImageView {
final z2 = z - 1;
final coords2 = TileCoordinates(x2, y2, z2);

final tile = _tileImages[TileCoordinates.key(coords2)];
final tile = _tileImages[_tileCoordinatesSimplifier.get(coords2)];
if (tile != null) {
if (tile.readyToDisplay) {
retain.add(coords2);
Expand Down Expand Up @@ -160,7 +179,7 @@ final class TileImageView {
for (final (i, j) in const [(0, 0), (0, 1), (1, 0), (1, 1)]) {
final coords = TileCoordinates(2 * x + i, 2 * y + j, z + 1);

final tile = _tileImages[TileCoordinates.key(coords)];
final tile = _tileImages[_tileCoordinatesSimplifier.get(coords)];
if (tile != null) {
if (tile.readyToDisplay || tile.loadFinishedAt != null) {
retain.add(coords);
Expand Down
Loading