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

fix: make horizontal repetition CRS dependent #1978

Merged
merged 7 commits into from
Nov 17, 2024
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}')
JaffaKetchup marked this conversation as resolved.
Show resolved Hide resolved
],
),
],
),
);
}
}
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 {
josxha marked this conversation as resolved.
Show resolved Hide resolved
/// 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
Loading