Skip to content
Open
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
14 changes: 10 additions & 4 deletions example/lib/pages/tile_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ class TileBuilderPageState extends State<TileBuilderPage> {
onChanged: (v) => setState(() => enableGrid = v),
),
const SizedBox.square(dimension: 12),
const Tooltip(
message: 'Show Background Grid While Tiles Load',
child: Icon(Icons.grid_on),
),
const Tooltip(
message: 'Show Coordinates',
child: Icon(Icons.location_on),
Expand Down Expand Up @@ -113,16 +117,19 @@ class TileBuilderPageState extends State<TileBuilderPage> {
),
Expanded(
child: FlutterMap(
options: const MapOptions(
initialCenter: LatLng(51.5, -0.09),
options: MapOptions(
initialCenter: const LatLng(51.5, -0.09),
initialZoom: 5,
backgroundColor:
darkMode ? Colors.grey[900]! : Colors.grey[200]!,
),
children: [
_darkModeContainerIfEnabled(
TileLayer(
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
userAgentPackageName: 'dev.fleaflet.flutter_map.demo',
maxNativeZoom: 19,
tileBuilder: tileBuilder,
),
),
Expand All @@ -148,7 +155,6 @@ class TileBuilderPageState extends State<TileBuilderPage> {

Widget _darkModeContainerIfEnabled(Widget child) {
if (!darkMode) return child;

return darkModeTilesContainerBuilder(context, child);
}
}
1 change: 1 addition & 0 deletions lib/flutter_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export 'package:flutter_map/src/layer/attribution_layer/simple.dart';
export 'package:flutter_map/src/layer/circle_layer/circle_layer.dart';
export 'package:flutter_map/src/layer/marker_layer/marker_layer.dart';
export 'package:flutter_map/src/layer/overlay_image_layer/overlay_image_layer.dart';
export 'package:flutter_map/src/layer/placeholder_grid_layer/placeholder_grid_layer.dart';
export 'package:flutter_map/src/layer/polygon_layer/label/deprecated_placements.dart';
export 'package:flutter_map/src/layer/polygon_layer/label/placement_calculators/placement_calculator.dart';
export 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart';
Expand Down
76 changes: 76 additions & 0 deletions lib/src/layer/placeholder_grid_layer/painter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
part of 'placeholder_grid_layer.dart';

class _BackgroundGridPainter extends CustomPainter {
const _BackgroundGridPainter({
required this.color,
required this.spacing,
required this.pixelOrigin,
required this.numOfMinors,
required this.majorStrokeWidth,
required this.minorStrokeWidth,
});

final Color color;
final double spacing;
final Offset? pixelOrigin;
final int numOfMinors;
final double majorStrokeWidth;
final double minorStrokeWidth;

@override
void paint(Canvas canvas, Size size) {
if (size.isEmpty || !spacing.isFinite || spacing < 1) return;

final paint = Paint()
..color = color
..strokeWidth = majorStrokeWidth
..style = PaintingStyle.stroke
..isAntiAlias = false;
final path = Path();

double x = -(pixelOrigin == null ? size.width : pixelOrigin!.dx % spacing);
for (; x < size.width; x += spacing) {
path
..moveTo(x, 0)
..lineTo(x, size.height);
}
double y = -(pixelOrigin == null ? size.height : pixelOrigin!.dy % spacing);
for (; y < size.height; y += spacing) {
path
..moveTo(0, y)
..lineTo(size.width, y);
}

canvas.drawPath(path, paint);

if (numOfMinors == 0) return;

path.reset();

final minorSpacing = spacing / (numOfMinors + 1);

x = -(pixelOrigin == null ? size.width : pixelOrigin!.dx % minorSpacing);
for (; x < size.width; x += minorSpacing) {
path
..moveTo(x, 0)
..lineTo(x, size.height);
}
y = -(pixelOrigin == null ? size.height : pixelOrigin!.dy % minorSpacing);
for (; y < size.height; y += minorSpacing) {
path
..moveTo(0, y)
..lineTo(size.width, y);
}

canvas.drawPath(path, paint..strokeWidth = minorStrokeWidth);
}

@override
bool shouldRepaint(_BackgroundGridPainter oldDelegate) =>
color != oldDelegate.color ||
spacing != oldDelegate.spacing ||
pixelOrigin != oldDelegate.pixelOrigin ||
numOfMinors != oldDelegate.numOfMinors ||
majorStrokeWidth != oldDelegate.majorStrokeWidth ||
minorStrokeWidth != oldDelegate.majorStrokeWidth;
}
155 changes: 155 additions & 0 deletions lib/src/layer/placeholder_grid_layer/placeholder_grid_layer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_map/flutter_map.dart';

part 'painter.dart';

/// Layer which draws a grid consisting of orthogonal major and minor strokes,
/// configured by a [PlaceholderGridOptions].
///
/// This layer is usually drawn by default on the map background, but can be
/// inserted manually. See [MapOptions.backgroundGridOptions] for info.
///
/// This layer is intended for use in a [FlutterMap]. However, it will work
/// without being placed in one.
class PlaceholderGridLayer extends StatelessWidget {
/// Build a widget which draws a grid consisting of orthogonal major and minor
/// strokes, configured by a [PlaceholderGridOptions].
///
/// The widget does not have to be used within a map context.
const PlaceholderGridLayer({
super.key,
this.gridOptions = const PlaceholderGridOptions(),
});

/// Configuration of the grid drawn.
final PlaceholderGridOptions gridOptions;

@override
Widget build(BuildContext context) {
final camera = MapCamera.maybeOf(context);

late final autoGridColor = () {
const delta = 0.15;

if (MapOptions.maybeOf(context)?.backgroundColor case final color?) {
final hsl = HSLColor.fromColor(color);
return hsl
.withLightness(
((hsl.lightness > 0.5 ? -delta : delta) + hsl.lightness)
.clamp(0, 1),
)
.toColor();
}
}();
final static = gridOptions.static || camera == null;
final spacing = gridOptions.spacing *
(static
? 1
: camera.getZoomScale(camera.zoom, camera.zoom.floorToDouble()));

final painted = CustomPaint(
painter: _BackgroundGridPainter(
color: gridOptions.color ?? autoGridColor ?? const Color(0x14000000),
spacing: spacing,
pixelOrigin: static ? null : camera.pixelOrigin,
numOfMinors: gridOptions.numOfMinors,
majorStrokeWidth: gridOptions.majorStrokeWidth,
minorStrokeWidth: gridOptions.minorStrokeWidth,
),
);
if (static) return painted;
return MobileLayerTransformer(child: painted);
}
}

/// Configuration options for a [PlaceholderGridLayer], which is usually
/// displayed on the map background.
///
/// The grid consists of orthogonal major and minor strokes.
@immutable
class PlaceholderGridOptions {
/// Color of the grid strokes.
///
/// Defaults to a slightly ligher or darker shade of the
/// [MapOptions.backgroundColor], based on if it's dark or light respectively.
///
/// If no inherited [MapOptions] is available, the default color is a
/// translucent black.
final Color? color;

/// Gap between major strokes (at integer zoom levels when [static] is
/// `false`), in logical pixels.
///
/// To ensure a grid is visible while tiles are loading in holes, it's
/// recommended to set this to a smaller value than the tile size.
///
/// Defaults to 64.
final double spacing;

/// Whether the grid should be static and not move or zoom with the map,
/// instead of its default behaviour of moving and scaling with zoom levels.
///
/// If no inherited [MapCamera] is available, the grid is always static.
final bool static;

/// Thickness of major strokes.
///
/// Defaults to 1.5.
final double majorStrokeWidth;

/// Thickness of minor strokes.
///
/// Defaults to 0 (a hairline stroke).
final double minorStrokeWidth;

/// Number of minor strokes to insert between major strokes.
///
/// Defaults to 3. Set to 0 to disable minor strokes.
final int numOfMinors;

/// Configuration options for a grid, usually displayed on the map background.
const PlaceholderGridOptions({
this.color,
this.spacing = 64,
this.static = false,
this.majorStrokeWidth = 1.5,
this.minorStrokeWidth = 0,
this.numOfMinors = 3,
}) : assert(
spacing >= 1 && spacing < double.infinity,
'`spacing` must be equal to or greater than one, and finite',
),
assert(
majorStrokeWidth >= 0,
'`majorStrokeWidth` must be equal to or greater than zero',
),
assert(
minorStrokeWidth >= 0,
'`minorStrokeWidth` must be equal to or greater than zero',
),
assert(
numOfMinors >= 0,
'`numOfMinors` must be equal to or greater than zero',
);

@override
int get hashCode => Object.hash(
color,
spacing,
static,
majorStrokeWidth,
minorStrokeWidth,
numOfMinors,
);

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PlaceholderGridOptions &&
color == other.color &&
spacing == other.spacing &&
static == other.static &&
majorStrokeWidth == other.majorStrokeWidth &&
minorStrokeWidth == other.minorStrokeWidth &&
numOfMinors == other.numOfMinors;
}
24 changes: 22 additions & 2 deletions lib/src/map/options/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,27 @@ class MapOptions {
/// additional zoom level restrictions.
final double? maxZoom;

/// The background color of the [FlutterMap] widget, defaults to a
/// yellow grey-ish color.
/// Background color of the [FlutterMap] widget.
///
/// It is usually recommended to set this to the background color of the app/
/// screen, such as the scaffold background color held within the inherited
/// theme, or at least a dark color if the app and map run in dark mode.
///
/// Defaults to a light yellow-gray-ish color.
final Color backgroundColor;

/// Configuration of an optional grid to display on the map background.
///
/// By default, a grid is drawn on top of the [backgroundColor] with a stroke
/// color which will automatically work on most backgrounds.
///
/// Set to `null` to disable the grid. The grid is automatically disabled if
/// the [backgroundColor] is fully transparent.
///
/// A grid can be inserted anywhere within the map stack (or outside of the
/// map context) using a [PlaceholderGridLayer] manually.
final PlaceholderGridOptions? backgroundGridOptions;

/// Callback that fires when the map gets tapped or clicked with the
/// primary mouse button. This is normally the left mouse button. This
/// callback does not fire if the gesture is recognized as a double click.
Expand Down Expand Up @@ -167,6 +184,7 @@ class MapOptions {
this.minZoom,
this.maxZoom,
this.backgroundColor = const Color(0xFFE0E0E0),
this.backgroundGridOptions = const PlaceholderGridOptions(),
this.onTap,
this.onSecondaryTap,
this.onLongPress,
Expand Down Expand Up @@ -204,6 +222,7 @@ class MapOptions {
minZoom == other.minZoom &&
maxZoom == other.maxZoom &&
backgroundColor == other.backgroundColor &&
backgroundGridOptions == other.backgroundGridOptions &&
onTap == other.onTap &&
onSecondaryTap == other.onSecondaryTap &&
onLongPress == other.onLongPress &&
Expand All @@ -228,6 +247,7 @@ class MapOptions {
minZoom,
maxZoom,
backgroundColor,
backgroundGridOptions,
onTap,
onSecondaryTap,
onLongPress,
Expand Down
11 changes: 7 additions & 4 deletions lib/src/map/widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,15 @@ class _FlutterMapStateContainer extends State<FlutterMap>
Widget build(BuildContext context) {
super.build(context);

final backgroundColor = widget.options.backgroundColor;

final widgets = ClipRect(
child: Stack(
children: <Widget>[
Positioned.fill(
child: ColoredBox(color: widget.options.backgroundColor),
),
children: [
Positioned.fill(child: ColoredBox(color: backgroundColor)),
if (widget.options.backgroundGridOptions case final options?
when backgroundColor.a != 0)
Positioned.fill(child: PlaceholderGridLayer(gridOptions: options)),
...widget.children,
],
),
Expand Down
Loading