diff --git a/example/pubspec.lock b/example/pubspec.lock index 6807a1e..353a467 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -8,6 +8,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.6.1" + bezier: + dependency: transitive + description: + name: bezier + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" boolean_selector: dependency: transitive description: diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 028cc18..c1a6a96 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1 +1 @@ -final int tileSize = 256; +const int tileSize = 256; diff --git a/lib/src/expressions/argument_expression.dart b/lib/src/expressions/argument_expression.dart new file mode 100644 index 0000000..8101a3c --- /dev/null +++ b/lib/src/expressions/argument_expression.dart @@ -0,0 +1,32 @@ +import 'package:vector_tile/vector_tile_value.dart'; + +import 'expression.dart'; + +class ArgumentExpression extends Expression { + final String key; + ArgumentExpression(this.key); + + @override + T? evaluate(Map args) { + final value = args[key]; + + if (value is T?) return value; + + if (T == double && value is num) { + return value.toDouble() as T; + } + + if (value is VectorTileValue) { + switch (T) { + case double: + return value.doubleValue as T?; + case bool: + return value.boolValue as T?; + case String: + return value.stringValue as T?; + } + } + + return null; + } +} diff --git a/lib/src/expressions/case_expression.dart b/lib/src/expressions/case_expression.dart new file mode 100644 index 0000000..a54ef6c --- /dev/null +++ b/lib/src/expressions/case_expression.dart @@ -0,0 +1,27 @@ +import 'expression.dart'; + +class Case { + final Expression condition; + final Expression? value; + + Case(this.condition, this.value); +} + +class CaseExpression extends Expression { + final Iterable> _cases; + final Expression? _fallback; + + CaseExpression(this._cases, this._fallback); + + @override + T? evaluate(Map args) { + for (final $case in _cases) { + final boolExpression = $case.condition.evaluate(args) ?? false; + if (boolExpression) { + return $case.value?.evaluate(args); + } + } + + return _fallback?.evaluate(args); + } +} diff --git a/lib/src/expressions/coalesce_expression.dart b/lib/src/expressions/coalesce_expression.dart new file mode 100644 index 0000000..256b2ae --- /dev/null +++ b/lib/src/expressions/coalesce_expression.dart @@ -0,0 +1,17 @@ +import 'expression.dart'; + +class CoalesceExpression extends Expression { + final Iterable> _delegates; + + CoalesceExpression(this._delegates); + + @override + T? evaluate(Map args) { + for (final delegate in _delegates) { + final result = delegate.evaluate(args); + if (result != null) return result; + } + + return null; + } +} diff --git a/lib/src/expressions/expression.dart b/lib/src/expressions/expression.dart new file mode 100644 index 0000000..39f9db0 --- /dev/null +++ b/lib/src/expressions/expression.dart @@ -0,0 +1,3 @@ +abstract class Expression { + T? evaluate(Map args); +} diff --git a/lib/src/expressions/function_expression.dart b/lib/src/expressions/function_expression.dart new file mode 100644 index 0000000..57698ec --- /dev/null +++ b/lib/src/expressions/function_expression.dart @@ -0,0 +1,12 @@ +import 'expression.dart'; + +// This expression mostly exists to make the refactoring to Expressions easier. +// It's probably desirable to work towards removing this eventually. +class FunctionExpression extends Expression { + final T? Function(Map args) _evaluate; + + FunctionExpression(this._evaluate); + + @override + T? evaluate(Map args) => _evaluate(args); +} diff --git a/lib/src/expressions/interpolate/cubic_bezier_interpolation_expression.dart b/lib/src/expressions/interpolate/cubic_bezier_interpolation_expression.dart new file mode 100644 index 0000000..90c3c7b --- /dev/null +++ b/lib/src/expressions/interpolate/cubic_bezier_interpolation_expression.dart @@ -0,0 +1,26 @@ +import 'package:bezier/bezier.dart'; +import 'package:vector_math/vector_math.dart'; +import 'package:vector_tile_renderer/src/themes/theme_function_model.dart'; + +import '../expression.dart'; +import 'interpolation_expression.dart'; + +class CubicBezierInterpolationExpression extends InterpolationExpression { + final Vector2 _c1; + final Vector2 _c2; + + CubicBezierInterpolationExpression( + this._c1, + this._c2, + Expression input, + List> stops, + ) : super(input, stops); + + @override + double getInterpolationFactor( + double input, double lowerValue, double upperValue) { + final t = exponentialInterpolation(input, 1, lowerValue, upperValue); + final curve = CubicBezier([Vector2(0, 0), _c1, _c2, Vector2(1, 1)]); + return curve.pointAt(t).y; + } +} diff --git a/lib/src/expressions/interpolate/exponential_interpolation_expression.dart b/lib/src/expressions/interpolate/exponential_interpolation_expression.dart new file mode 100644 index 0000000..03e0181 --- /dev/null +++ b/lib/src/expressions/interpolate/exponential_interpolation_expression.dart @@ -0,0 +1,16 @@ +import 'package:vector_tile_renderer/src/themes/theme_function_model.dart'; + +import '../expression.dart'; +import 'interpolation_expression.dart'; + +class ExponentialInterpolationExpression extends InterpolationExpression { + final double _base; + ExponentialInterpolationExpression( + this._base, Expression input, List> stops) + : super(input, stops); + + @override + double getInterpolationFactor( + double input, double lowerValue, double upperValue) => + exponentialInterpolation(input, _base, lowerValue, upperValue); +} diff --git a/lib/src/expressions/interpolate/interpolation_expression.dart b/lib/src/expressions/interpolate/interpolation_expression.dart new file mode 100644 index 0000000..0482165 --- /dev/null +++ b/lib/src/expressions/interpolate/interpolation_expression.dart @@ -0,0 +1,87 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:vector_tile_renderer/src/themes/theme_function_model.dart'; + +import '../expression.dart'; + +abstract class InterpolationExpression extends Expression { + final Expression input; + final List> stops; + + InterpolationExpression(this.input, this.stops); + + double getInterpolationFactor( + double input, + double lowerValue, + double upperValue, + ); + + exponentialInterpolation( + double input, double base, double lowerValue, double upperValue) { + final difference = upperValue - lowerValue; + final progress = input - lowerValue; + if (difference == 0) { + return 0; + } else if (base == 1) { + return progress / difference; + } else { + return (pow(base, progress) - 1) / (pow(base, difference) - 1); + } + } + + double? _interpolateDouble(double begin, double end, double t) { + final diff = end - begin; + return begin + diff * t; + } + + Color? _interpolateColor(Color? begin, Color? end, double t) { + final tween = ColorTween(begin: begin, end: end); + return tween.transform(t); + } + + @override + T? evaluate(Map args) { + final functionInput = input.evaluate(args)!; + + final firstStop = stops.first; + if (functionInput <= firstStop.zoom.evaluate(args)!) + return firstStop.value.evaluate(args); + + final lastStop = stops.last; + if (functionInput > lastStop.zoom.evaluate(args)!) + return lastStop.value.evaluate(args); + + final firstSmallerStopIndex = stops.lastIndexWhere( + (stop) => stop.zoom.evaluate(args)! < functionInput, + ); + final index = max(0, firstSmallerStopIndex); + final smallerStop = stops[index]; + final largerStop = stops[index + 1]; + + final smallerZoom = smallerStop.zoom.evaluate(args)!; + final largerZoom = largerStop.zoom.evaluate(args)!; + + final smallerValue = smallerStop.value.evaluate(args)!; + final largerValue = largerStop.value.evaluate(args)!; + + final t = getInterpolationFactor(functionInput, smallerZoom, largerZoom); + + if (T == double) { + return _interpolateDouble( + smallerValue as double, + largerValue as double, + t, + ) as T?; + } + + if (T == Color) { + return _interpolateColor( + smallerValue as Color?, + largerValue as Color?, + t, + ) as T?; + } + } +} diff --git a/lib/src/expressions/interpolate/linear_interpolation_expression.dart b/lib/src/expressions/interpolate/linear_interpolation_expression.dart new file mode 100644 index 0000000..abd76eb --- /dev/null +++ b/lib/src/expressions/interpolate/linear_interpolation_expression.dart @@ -0,0 +1,15 @@ +import 'package:vector_tile_renderer/src/themes/theme_function_model.dart'; + +import '../expression.dart'; +import 'interpolation_expression.dart'; + +class LinearInterpolationExpression extends InterpolationExpression { + LinearInterpolationExpression( + Expression input, List> stops) + : super(input, stops); + + @override + double getInterpolationFactor( + double input, double lowerValue, double upperValue) => + exponentialInterpolation(input, 1, lowerValue, upperValue); +} diff --git a/lib/src/expressions/match_expression.dart b/lib/src/expressions/match_expression.dart new file mode 100644 index 0000000..670bb81 --- /dev/null +++ b/lib/src/expressions/match_expression.dart @@ -0,0 +1,35 @@ +import 'package:vector_tile_renderer/src/expressions/expression.dart'; + +class Match { + final dynamic input; + final Expression? output; + + Match(this.input, this.output) + : assert(input is List || input is Input); +} + +class MatchExpression extends Expression { + final Expression? _compare; + final Iterable> _matches; + final Expression? _fallback; + + MatchExpression(this._compare, this._matches, this._fallback); + + @override + Output? evaluate(Map args) { + final compare = _compare?.evaluate(args); + if (compare == null) { + return _fallback?.evaluate(args); + } + + for (final match in _matches) { + final input = match.input; + if (input == compare || + (input is List && match.input.contains(compare))) { + return match.output?.evaluate(args); + } + } + + return _fallback?.evaluate(args); + } +} diff --git a/lib/src/expressions/step_expression.dart b/lib/src/expressions/step_expression.dart new file mode 100644 index 0000000..9515f0d --- /dev/null +++ b/lib/src/expressions/step_expression.dart @@ -0,0 +1,32 @@ +import 'expression.dart'; + +class Step { + final num step; + final Expression? value; + + Step(this.step, this.value); +} + +class StepExpression extends Expression { + final Expression _input; + final Expression _base; + final Iterable> _steps; + + StepExpression(this._input, this._base, this._steps) + : assert(_steps.isNotEmpty); + + @override + T? evaluate(Map args) { + final input = _input.evaluate(args)!; + + if (input < _steps.first.step) { + return _base.evaluate(args); + } + + final lastLessThanStop = _steps.lastWhere( + (stop) => stop.step < input, + ); + + return lastLessThanStop.value?.evaluate(args); + } +} diff --git a/lib/src/expressions/text_halo_expression.dart b/lib/src/expressions/text_halo_expression.dart new file mode 100644 index 0000000..309be84 --- /dev/null +++ b/lib/src/expressions/text_halo_expression.dart @@ -0,0 +1,45 @@ +import 'dart:ui'; + +import 'package:vector_tile_renderer/src/expressions/expression.dart'; + +class TextHaloExpression extends Expression> { + final Expression _color; + final double _haloWidth; + + TextHaloExpression(this._color, this._haloWidth); + + List? evaluate(Map args) { + final color = _color.evaluate(args); + if (color == null) { + return null; + } + + final zoom = args['zoom']; + + double offset = _haloWidth / zoom; + double radius = _haloWidth; + + return [ + Shadow( + offset: Offset(-offset, -offset), + blurRadius: radius, + color: color, + ), + Shadow( + offset: Offset(offset, offset), + blurRadius: radius, + color: color, + ), + Shadow( + offset: Offset(offset, -offset), + blurRadius: radius, + color: color, + ), + Shadow( + offset: Offset(-offset, offset), + blurRadius: radius, + color: color, + ), + ]; + } +} diff --git a/lib/src/expressions/value_expression.dart b/lib/src/expressions/value_expression.dart new file mode 100644 index 0000000..a60f57f --- /dev/null +++ b/lib/src/expressions/value_expression.dart @@ -0,0 +1,8 @@ +import 'expression.dart'; + +class ValueExpression extends Expression { + final T? _value; + ValueExpression(this._value); + + T? evaluate(Map values) => _value; +} diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index 7911493..1fb07e1 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -2,6 +2,21 @@ import 'dart:ui'; extension IterableExtension on Iterable { T? firstOrNull() => isEmpty ? null : first; + + List> chunk(int chunkSize) { + final List> chunks = []; + final list = toList(); + + for (var i = 0; i < list.length; i += chunkSize) { + final chunk = list.sublist( + i, + i + chunkSize > list.length ? list.length : i + chunkSize, + ); + chunks.add(chunk); + } + + return chunks; + } } extension PaintExtension on Paint { diff --git a/lib/src/features/line_renderer.dart b/lib/src/features/line_renderer.dart index 1fcdff3..6a9f5d3 100644 --- a/lib/src/features/line_renderer.dart +++ b/lib/src/features/line_renderer.dart @@ -10,6 +10,7 @@ import '../logger.dart'; import '../themes/style.dart'; import 'feature_geometry.dart'; import 'feature_renderer.dart'; +import 'to_args_map.dart'; class LineRenderer extends FeatureRenderer { final Logger logger; @@ -46,7 +47,7 @@ class LineRenderer extends FeatureRenderer { if (!_isWithinClip(context, path)) { return; } - var effectivePaint = style.linePaint!.paint(zoom: context.zoom); + var effectivePaint = style.linePaint!.paint(toArgsMap(context, feature)); if (effectivePaint != null) { if (context.zoomScaleFactor > 1.0) { effectivePaint.strokeWidth = diff --git a/lib/src/features/polygon_renderer.dart b/lib/src/features/polygon_renderer.dart index 9259d2e..64399a9 100644 --- a/lib/src/features/polygon_renderer.dart +++ b/lib/src/features/polygon_renderer.dart @@ -1,13 +1,14 @@ +import 'dart:ui'; + import 'package:vector_tile/vector_tile.dart'; import 'package:vector_tile/vector_tile_feature.dart'; - -import 'dart:ui'; +import 'package:vector_tile_renderer/src/features/to_args_map.dart'; import '../../vector_tile_renderer.dart'; +import '../constants.dart'; import '../context.dart'; import '../logger.dart'; import '../themes/style.dart'; -import '../constants.dart'; import 'feature_renderer.dart'; class PolygonRenderer extends FeatureRenderer { @@ -28,13 +29,13 @@ class PolygonRenderer extends FeatureRenderer { final polygon = geometry as GeometryPolygon; logger.log(() => 'rendering polygon'); final coordinates = polygon.coordinates; - _renderPolygon(context, style, layer, coordinates); + _renderPolygon(context, style, layer, feature, coordinates); } else if (geometry.type == GeometryType.MultiPolygon) { final multiPolygon = geometry as GeometryMultiPolygon; logger.log(() => 'rendering multi-polygon'); final polygons = multiPolygon.coordinates; polygons?.forEach((coordinates) { - _renderPolygon(context, style, layer, coordinates); + _renderPolygon(context, style, layer, feature, coordinates); }); } else { logger.warn( @@ -44,7 +45,7 @@ class PolygonRenderer extends FeatureRenderer { } void _renderPolygon(Context context, Style style, VectorTileLayer layer, - List>> coordinates) { + VectorTileFeature feature, List>> coordinates) { final path = Path(); coordinates.forEach((ring) { ring.asMap().forEach((index, point) { @@ -66,15 +67,15 @@ class PolygonRenderer extends FeatureRenderer { if (!_isWithinClip(context, path)) { return; } - final fillPaint = style.fillPaint == null - ? null - : style.fillPaint!.paint(zoom: context.zoom); + final args = toArgsMap(context, feature); + + final fillPaint = + style.fillPaint == null ? null : style.fillPaint!.paint(args); if (fillPaint != null) { context.canvas.drawPath(path, fillPaint); } - final outlinePaint = style.outlinePaint == null - ? null - : style.outlinePaint!.paint(zoom: context.zoom); + final outlinePaint = + style.outlinePaint == null ? null : style.outlinePaint!.paint(args); if (outlinePaint != null) { context.canvas.drawPath(path, outlinePaint); } diff --git a/lib/src/features/symbol_line_renderer.dart b/lib/src/features/symbol_line_renderer.dart index 5491d5d..6523bb0 100644 --- a/lib/src/features/symbol_line_renderer.dart +++ b/lib/src/features/symbol_line_renderer.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:vector_tile/vector_tile.dart'; import 'package:vector_tile/vector_tile_feature.dart'; +import 'package:vector_tile_renderer/src/features/to_args_map.dart'; import '../../vector_tile_renderer.dart'; import '../constants.dart'; @@ -33,7 +34,7 @@ class SymbolLineRenderer extends FeatureRenderer { final lines = geometry.decodeLines(feature); if (lines != null) { logger.log(() => 'rendering linestring symbol'); - final text = textLayout.text(feature); + final text = textLayout.text.evaluate(toArgsMap(context, feature)); if (text != null) { final path = Path(); lines.forEach((line) { @@ -53,7 +54,7 @@ class SymbolLineRenderer extends FeatureRenderer { final metrics = path.computeMetrics().toList(); if (metrics.length > 0) { final abbreviated = TextAbbreviator().abbreviate(text); - final renderer = TextRenderer(context, style, abbreviated); + final renderer = TextRenderer(context, style, abbreviated, feature); final renderBox = _findMiddleMetric(context, metrics, renderer); if (renderBox != null) { final tangent = renderBox.tangent; diff --git a/lib/src/features/symbol_point_renderer.dart b/lib/src/features/symbol_point_renderer.dart index aca1ded..ca6bd23 100644 --- a/lib/src/features/symbol_point_renderer.dart +++ b/lib/src/features/symbol_point_renderer.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter/rendering.dart'; import 'package:vector_tile/vector_tile.dart'; import 'package:vector_tile/vector_tile_feature.dart'; +import 'package:vector_tile_renderer/src/features/to_args_map.dart'; import '../../vector_tile_renderer.dart'; import '../constants.dart'; @@ -31,10 +32,10 @@ class SymbolPointRenderer extends FeatureRenderer { final points = geometry.decodePoints(feature); if (points != null) { logger.log(() => 'rendering points'); - final text = textLayout.text(feature); + final text = textLayout.text.evaluate(toArgsMap(context, feature)); if (text != null) { final abbreviated = TextAbbreviator().abbreviate(text); - final textRenderer = TextRenderer(context, style, abbreviated); + final textRenderer = TextRenderer(context, style, abbreviated, feature); points.forEach((point) { points.forEach((point) { if (point.length < 2) { diff --git a/lib/src/features/text_renderer.dart b/lib/src/features/text_renderer.dart index ddd218a..92886bf 100644 --- a/lib/src/features/text_renderer.dart +++ b/lib/src/features/text_renderer.dart @@ -1,17 +1,23 @@ import 'dart:ui'; import 'package:flutter/widgets.dart'; +import 'package:vector_tile/vector_tile_feature.dart'; +import 'package:vector_tile_renderer/src/expressions/expression.dart'; -import '../themes/style.dart'; import '../context.dart'; +import '../themes/style.dart'; +import 'to_args_map.dart'; class TextRenderer { final Context context; final Style style; final String text; + final VectorTileFeature feature; + late final TextPainter? _painter; late final Offset? _translation; - TextRenderer(this.context, this.style, this.text) { + + TextRenderer(this.context, this.style, this.text, this.feature) { _painter = _createTextPainter(context, style, text); _translation = _layout(); } @@ -49,21 +55,23 @@ class TextRenderer { } TextPainter? _createTextPainter(Context context, Style style, String text) { - final foreground = style.textPaint!.paint(zoom: context.zoom); + final args = toArgsMap(context, feature); + + final foreground = style.textPaint!.paint(args); if (foreground == null) { return null; } - double? textSize = style.textLayout!.textSize(context.zoom); + double? textSize = style.textLayout!.textSize.evaluate(args); if (textSize != null) { if (context.zoomScaleFactor > 1.0) { textSize = textSize / context.zoomScaleFactor; } double? spacing; - DoubleZoomFunction? spacingFunction = style.textLayout!.textLetterSpacing; + Expression? spacingFunction = style.textLayout!.textLetterSpacing; if (spacingFunction != null) { - spacing = spacingFunction(context.zoom); + spacing = spacingFunction.evaluate(args); } - final shadows = style.textHalo?.call(context.zoom); + final shadows = style.textHalo?.evaluate(args); final textStyle = TextStyle( foreground: foreground, fontSize: textSize, @@ -86,7 +94,10 @@ class TextRenderer { if (_painter == null) { return null; } - final anchor = style.textLayout!.anchor; + final anchorString = style.textLayout!.anchor?.evaluate( + toArgsMap(context, feature), + ); + final anchor = LayoutAnchor.fromName(anchorString); final size = _painter!.size; switch (anchor) { case LayoutAnchor.center: diff --git a/lib/src/features/to_args_map.dart b/lib/src/features/to_args_map.dart new file mode 100644 index 0000000..8afdf3f --- /dev/null +++ b/lib/src/features/to_args_map.dart @@ -0,0 +1,9 @@ +import 'package:vector_tile/vector_tile_feature.dart'; +import 'package:vector_tile_renderer/src/vector_tile_extensions.dart'; + +import '../context.dart'; + +toArgsMap(Context context, VectorTileFeature feature) => { + ...feature.collectedProperties, + 'zoom': context.zoom, + }; diff --git a/lib/src/parsers/boolean.dart b/lib/src/parsers/boolean.dart new file mode 100644 index 0000000..742f11e --- /dev/null +++ b/lib/src/parsers/boolean.dart @@ -0,0 +1,12 @@ +import 'package:vector_tile_renderer/src/expressions/expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; + +import 'parser.dart'; + +class BooleanParser extends ExpressionParser { + @override + Expression? parse(data) { + if (data == 'true') return ValueExpression(true); + if (data == 'false') return ValueExpression(false); + } +} diff --git a/lib/src/parsers/case.dart b/lib/src/parsers/case.dart new file mode 100644 index 0000000..7acab81 --- /dev/null +++ b/lib/src/parsers/case.dart @@ -0,0 +1,37 @@ +import 'package:vector_tile_renderer/src/expressions/case_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/expression.dart'; +import 'package:vector_tile_renderer/src/extensions.dart'; + +import 'parser.dart'; +import 'parsers.dart' as Parsers; + +class CaseParser extends ExpressionParser { + @override + Expression? parse(data) { + if (data is! List) { + return null; + } + + final copy = [...data]; + + assert( + copy.length.isEven && copy.length >= 4, + 'Case expressions must have an even amount of fields: The string literal ' + '"case" followed by pairs of condition and value and finally the default ' + 'value.\n' + 'See https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#case for more information.\n' + 'Failed parsing on expression $data', + ); + + final fallback = Parsers.parse(copy.removeLast()); + + final cases = copy.skip(1).chunk(2).map((chunk) { + final condition = Parsers.parse(chunk[0])!; + final output = Parsers.parse(chunk[1]); + + return Case(condition, output); + }); + + return CaseExpression(cases, fallback); + } +} diff --git a/lib/src/parsers/coalesce.dart b/lib/src/parsers/coalesce.dart new file mode 100644 index 0000000..023224e --- /dev/null +++ b/lib/src/parsers/coalesce.dart @@ -0,0 +1,16 @@ +import 'package:vector_tile_renderer/src/expressions/coalesce_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/expression.dart'; + +import 'parser.dart'; +import 'parsers.dart' as Parsers; + +class CoalesceParser extends ExpressionParser { + @override + Expression? parse(data) { + if (data is! List) return null; + + final delegates = + data.skip(1).map((i) => Parsers.parse(i)).whereType>(); + return CoalesceExpression(delegates); + } +} diff --git a/lib/src/themes/color_parser.dart b/lib/src/parsers/color.dart similarity index 61% rename from lib/src/themes/color_parser.dart rename to lib/src/parsers/color.dart index 2a4114b..3b321ef 100644 --- a/lib/src/themes/color_parser.dart +++ b/lib/src/parsers/color.dart @@ -1,38 +1,53 @@ import 'dart:ui'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:vector_tile_renderer/src/expressions/expression.dart'; +import 'package:vector_tile_renderer/src/expressions/function_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; +import 'package:vector_tile_renderer/src/themes/theme_function.dart'; +import 'package:vector_tile_renderer/src/themes/theme_function_model.dart'; -import 'style.dart'; -import 'theme_function.dart'; -import 'theme_function_model.dart'; +import 'interpolation.dart'; +import 'parser.dart'; -class ColorParser { - static ColorZoomFunction? parse(colorSpec) { - if (colorSpec is String) { - Color? color = toColor(colorSpec); - if (color != null) { - return (zoom) => color; +class ColorParser extends ExpressionParser { + @override + Expression? parse(data) { + if (data is String) { + return ValueExpression(parseString(data)); + } + + if (data is List) { + switch (data[0]) { + case 'interpolate': + return InterpolationParser().parse(data); + default: + return null; } - } else if (colorSpec is Map) { - final model = ColorFunctionModelFactory().create(colorSpec); + } + + if (data is Map) { + final model = ColorFunctionModelFactory().create(data); if (model != null) { - return (zoom) => ColorThemeFunction().exponential(model, zoom); + return FunctionExpression( + (args) => ColorThemeFunction().exponential(model, args), + ); } } + + return null; } - static Color? toColor(String? color) { - if (color == null) { - return null; - } - if (color.startsWith("#") && color.length == 7) { + @visibleForTesting + Color parseString(String color) { + if (color.startsWith('#') && color.length == 7) { return Color.fromARGB( 0xff, int.parse(color.substring(1, 3), radix: 16), int.parse(color.substring(3, 5), radix: 16), int.parse(color.substring(5, 7), radix: 16)); } - if (color.startsWith("#") && color.length == 4) { + if (color.startsWith('#') && color.length == 4) { String r = color.substring(1, 2) + color.substring(1, 2); String g = color.substring(2, 3) + color.substring(2, 3); String b = color.substring(3, 4) + color.substring(3, 4); @@ -43,8 +58,8 @@ class ColorParser { if ((color.startsWith('hsla(') || color.startsWith('hsl(')) && color.endsWith(')')) { final components = color - .replaceAll(RegExp(r"hsla?\("), '') - .replaceAll(RegExp(r"\)"), '') + .replaceAll(RegExp(r'hsla?\('), '') + .replaceAll(RegExp(r'\)'), '') .split(',') .map((s) => s.trim().replaceAll(RegExp(r'%'), '')) .toList(); @@ -69,8 +84,8 @@ class ColorParser { if ((color.startsWith('rgba(') || color.startsWith('rgb(')) && color.endsWith(')')) { final components = color - .replaceAll(RegExp(r"rgba?\("), '') - .replaceAll(RegExp(r"\)"), '') + .replaceAll(RegExp(r'rgba?\('), '') + .replaceAll(RegExp(r'\)'), '') .split(',') .map((s) => s.trim()) .toList(); diff --git a/lib/src/parsers/double.dart b/lib/src/parsers/double.dart new file mode 100644 index 0000000..7585b16 --- /dev/null +++ b/lib/src/parsers/double.dart @@ -0,0 +1,40 @@ +import 'package:vector_tile_renderer/src/expressions/expression.dart'; +import 'package:vector_tile_renderer/src/expressions/function_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; +import 'package:vector_tile_renderer/src/themes/theme_function.dart'; +import 'package:vector_tile_renderer/src/themes/theme_function_model.dart'; + +import 'interpolation.dart'; +import 'parser.dart'; + +class DoubleParser extends ExpressionParser { + @override + Expression? parse(data) { + if (data is num) { + return ValueExpression(data.toDouble()); + } + + if (data is String) { + final parsed = double.tryParse(data); + return parsed == null ? null : ValueExpression(parsed); + } + + if (data is List) { + switch (data[0]) { + case 'interpolate': + return InterpolationParser().parse(data); + default: + return null; + } + } + + if (data is Map) { + final model = DoubleFunctionModelFactory().create(data); + if (model != null) { + return FunctionExpression( + (args) => DoubleThemeFunction().exponential(model, args), + ); + } + } + } +} diff --git a/lib/src/parsers/interpolation.dart b/lib/src/parsers/interpolation.dart new file mode 100644 index 0000000..0ba3eb8 --- /dev/null +++ b/lib/src/parsers/interpolation.dart @@ -0,0 +1,107 @@ +import 'dart:ui'; + +import 'package:vector_math/vector_math.dart'; +import 'package:vector_tile_renderer/src/expressions/expression.dart'; +import 'package:vector_tile_renderer/src/expressions/interpolate/cubic_bezier_interpolation_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/interpolate/exponential_interpolation_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/interpolate/linear_interpolation_expression.dart'; +import 'package:vector_tile_renderer/src/extensions.dart'; +import 'package:vector_tile_renderer/src/themes/theme_function_model.dart'; + +import 'parser.dart'; +import 'parsers.dart' as Parsers; + +class InterpolationParser extends ExpressionParser { + @override + Expression? parse(data) { + if (data is! List) { + return null; + } + + assert( + T == double || T == Color, + 'Linear interpolation is only supported for double and color values!', + ); + + assert( + data.length >= 5 && data.length.isOdd, + 'Interpolation expressions must have an odd amount >= 5 of fields: The ' + 'string literal "interpolate", followed by the interpolation type, ' + 'followed by a numeric input expression, followed by pairs of input and ' + 'output values to be used for interpolation.\n' + 'See https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#interpolate for more information.\n' + 'Failed parsing on expression $data', + ); + + switch (data[1][0]) { + case 'linear': + return _parseLinear(data); + case 'exponential': + return _parseExponential(data); + case 'cubic-bezier': + return _parseCubicBezier(data); + default: + return null; + } + } + + double _toDouble(dynamic d) => (d as num).toDouble(); + Expression _parseInput(List data) => Parsers.parse(data[2])!; + List> _parseStops(List data) => data.skip(3).chunk(2).map( + (chunk) { + var stop = Parsers.parse(chunk[0]); + var output = Parsers.parse(chunk[1]); + return FunctionStop( + stop!, + output!, + ); + }, + ).toList(); + + _parseLinear(List data) { + final input = _parseInput(data); + final stops = _parseStops(data); + return LinearInterpolationExpression(input, stops); + } + + _parseExponential(List data) { + assert( + data[1].length == 2, + 'The interpolation type "expression" must be defined by a list of two ' + 'values: The string literal "expression" and the numeric base.\n' + 'See https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#interpolate for more information.\n' + 'Failed parsing on interpolation type ${data[1]}', + ); + + final base = _toDouble(data[1][1]); + final input = _parseInput(data); + final stops = _parseStops(data); + + return ExponentialInterpolationExpression(base, input, stops); + } + + _parseCubicBezier(List data) { + assert( + data[1].length == 5, + 'The interpolation type "cubic-bezier" must be defined by a list of five ' + 'values: The string literal "cubic-bezier" followed by the x and y ' + 'values of the first control point and lastly the x and y values of the ' + 'second control point.\n' + 'See https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#interpolate for more information.\n' + 'Failed parsing on interpolation type ${data[1]}', + ); + + final x1 = _toDouble(data[1][1]); + final y1 = _toDouble(data[1][2]); + final x2 = _toDouble(data[1][3]); + final y2 = _toDouble(data[1][4]); + + final c1 = Vector2(x1, y1); + final c2 = Vector2(x2, y2); + + final input = _parseInput(data); + final stops = _parseStops(data); + + return CubicBezierInterpolationExpression(c1, c2, input, stops); + } +} diff --git a/lib/src/parsers/match.dart b/lib/src/parsers/match.dart new file mode 100644 index 0000000..7a313d5 --- /dev/null +++ b/lib/src/parsers/match.dart @@ -0,0 +1,78 @@ +import 'package:vector_tile_renderer/src/expressions/expression.dart'; +import 'package:vector_tile_renderer/src/expressions/match_expression.dart'; +import 'package:vector_tile_renderer/src/extensions.dart'; + +import 'parser.dart'; +import 'parsers.dart' as Parsers; + +class MatchParser extends ExpressionParser { + @override + Expression? parse(data) { + if (data is! List) { + return null; + } + + final copy = [...data]; + + assert( + copy.length.isOdd && copy.length >= 5, + 'Match expressions must have an odd amount of fields: The string ' + 'literal "match" followed by an expression describing the input value, ' + 'followed by pairs of input (or list of inputs) to be matched against ' + 'and their respective output and lastly the default return value.\n' + 'See https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#match for more information.\n' + 'Failed parsing on expression $data', + ); + + final inputType = _findInputType(copy); + + if (inputType == double) { + return _parse(copy); + } + + if (inputType == String) { + return _parse(copy); + } + + return null; + } + + Expression? _parse(List data) { + final input = Parsers.parse(data[1]); + final fallback = Parsers.parse(data.removeLast()); + + final matchChunks = data.skip(2).chunk(2); + final matches = matchChunks.map((chunk) { + final matchInput = chunk[0]; + final compareExpression = Parsers.parse(chunk[1]); + + return Match(matchInput, compareExpression); + }); + + return MatchExpression(input, matches, fallback); + } + + Type _findInputType(List data) { + final referenceField = data[2]; + + var type = referenceField.runtimeType; + if (referenceField is List) { + type = referenceField[0].runtimeType; + } + + if (type == num) { + type = double; + } + + if (type == String || type == double) { + return type; + } + + throw ArgumentError( + '$data does not appear to contain a valid match ' + 'expression. Make sure that the input labels are either literal numbers ' + 'or strings or lists of one of the two. ' + 'See https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#match', + ); + } +} diff --git a/lib/src/parsers/parser.dart b/lib/src/parsers/parser.dart new file mode 100644 index 0000000..af9c278 --- /dev/null +++ b/lib/src/parsers/parser.dart @@ -0,0 +1,52 @@ +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; +import 'package:vector_tile_renderer/src/expressions/argument_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; +import 'package:vector_tile_renderer/src/parsers/match.dart'; + +import 'case.dart'; +import 'coalesce.dart'; +import 'step.dart'; + +abstract class ExpressionParser { + @protected + Expression? parse(dynamic data); + + @nonVirtual + Expression? parseExpression(dynamic data) { + if (data == null) return null; + + final common = _parseCommonExpressions(data); + + final result = common ?? parse(data); + assert( + result != null, + '[dart_vector_tile_renderer] Could not parse $data to an expression', + ); + + return result; + } + + Expression? _parseCommonExpressions(dynamic data) { + if (data is T && T != dynamic) return ValueExpression(data); + + if (data is! List) { + return null; + } + + assert( + data.length > 0, + 'Failed to parse expression $data; expected at least one element', + ); + + if (data[0] == 'case') return CaseParser().parse(data); + if (data[0] == 'coalesce') return CoalesceParser().parse(data); + if (data[0] == 'get') return ArgumentExpression(data[1]); + if (data[0] == 'match') return MatchParser().parse(data); + if (data[0] == 'step') return StepParser().parse(data); + if (data[0] == 'zoom') return ArgumentExpression('zoom'); + + return null; + } +} diff --git a/lib/src/parsers/parsers.dart b/lib/src/parsers/parsers.dart new file mode 100644 index 0000000..d70dc81 --- /dev/null +++ b/lib/src/parsers/parsers.dart @@ -0,0 +1,30 @@ +import 'dart:ui'; + +import 'package:vector_tile_renderer/src/expressions/expression.dart'; + +import 'boolean.dart'; +import 'color.dart'; +import 'double.dart'; +import 'parser.dart'; + +final Map _parsers = { + double: DoubleParser(), + bool: BooleanParser(), + Color: ColorParser(), +}; + +/// The common parser has no logic for parsing any special cases and can serve +/// as a parser for everything not specified in _parsers. +class CommonParser extends ExpressionParser { + parse(data) => null; +} + +ExpressionParser parserFor() { + if (!_parsers.containsKey(T)) { + return CommonParser(); + } + + return _parsers[T] as ExpressionParser; +} + +Expression? parse(dynamic data) => parserFor().parseExpression(data); diff --git a/lib/src/parsers/step.dart b/lib/src/parsers/step.dart new file mode 100644 index 0000000..134a7e5 --- /dev/null +++ b/lib/src/parsers/step.dart @@ -0,0 +1,36 @@ +import 'package:vector_tile_renderer/src/expressions/expression.dart'; +import 'package:vector_tile_renderer/src/expressions/step_expression.dart'; +import 'package:vector_tile_renderer/src/extensions.dart'; + +import 'parser.dart'; +import 'parsers.dart' as Parsers; + +class StepParser extends ExpressionParser { + @override + Expression? parse(data) { + if (data is! List) return null; + + assert( + data.length >= 3 && data.length.isOdd, + 'Case expressions must have an odd amount of fields and be at least 3 ' + 'fields long: The string literal "step", followed by a numeric value ' + 'expression as input, followed by the baseline output, followed by pairs ' + 'of input-steps and their respective output values.\n' + 'See https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#step for more information.\n' + 'Failed parsing on expression $data', + ); + + final input = Parsers.parse(data[1])!; + + final base = Parsers.parse(data[2])!; + + final chunks = data.skip(3).chunk(2).map((chunk) { + final step = chunk[0]; + final value = Parsers.parse(chunk[1]); + + return Step(step, value); + }); + + return StepExpression(input, base, chunks); + } +} diff --git a/lib/src/renderer.dart b/lib/src/renderer.dart index 925c7a1..efa5ccd 100644 --- a/lib/src/renderer.dart +++ b/lib/src/renderer.dart @@ -1,12 +1,12 @@ import 'dart:ui'; import 'package:vector_tile/vector_tile.dart'; -import 'constants.dart'; +import 'constants.dart'; import 'context.dart'; import 'features/feature_renderer.dart'; -import 'themes/theme.dart'; import 'logger.dart'; +import 'themes/theme.dart'; class Renderer { final Theme theme; diff --git a/lib/src/themes/paint_factory.dart b/lib/src/themes/paint_factory.dart index a8ee727..9833e82 100644 --- a/lib/src/themes/paint_factory.dart +++ b/lib/src/themes/paint_factory.dart @@ -1,17 +1,18 @@ import 'dart:ui'; +import 'package:vector_tile_renderer/src/expressions/expression.dart'; +import 'package:vector_tile_renderer/src/expressions/function_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; +import 'package:vector_tile_renderer/src/parsers/parsers.dart'; + import '../logger.dart'; -import 'color_parser.dart'; -import 'style.dart'; -import 'theme_function.dart'; -import 'theme_function_model.dart'; class PaintStyle { final String id; final PaintingStyle paintingStyle; - final DoubleZoomFunction opacity; - final DoubleZoomFunction strokeWidth; - final ColorZoomFunction color; + final Expression? opacity; + final Expression? strokeWidth; + final Expression color; PaintStyle( {required this.id, @@ -20,12 +21,12 @@ class PaintStyle { required this.strokeWidth, required this.color}); - Paint? paint({required double zoom}) { - final color = this.color(zoom); + Paint? paint(Map args) { + final color = this.color.evaluate(args); if (color == null) { return null; } - final opacity = this.opacity(zoom); + final opacity = this.opacity?.evaluate(args); if (opacity != null && opacity <= 0) { return null; } @@ -36,7 +37,7 @@ class PaintStyle { paint.color = color.withOpacity(opacity); } if (paintingStyle == PaintingStyle.stroke) { - final strokeWidth = this.strokeWidth(zoom); + final strokeWidth = this.strokeWidth?.evaluate(args); if (strokeWidth == null) { return null; } @@ -55,31 +56,23 @@ class PaintFactory { if (paint == null) { return null; } - final color = ColorParser.parse(paint['$prefix-color']); + final color = parse(paint['$prefix-color']); + if (color == null) { return null; } - final opacity = _toDouble(paint['$prefix-opacity']); - final strokeWidth = _toDouble(paint['$prefix-width']); - return PaintStyle( - id: id, - paintingStyle: style, - opacity: opacity, - strokeWidth: (zoom) => strokeWidth(zoom) ?? defaultStrokeWidth, - color: color); - } + final opacity = parse(paint['$prefix-opacity']); + final strokeWidth = parse(paint['$prefix-width']) ?? + ValueExpression(defaultStrokeWidth); - DoubleZoomFunction _toDouble(doubleSpec) { - if (doubleSpec is num) { - final value = doubleSpec.toDouble(); - return (zoom) => value; - } - if (doubleSpec is Map) { - final model = DoubleFunctionModelFactory().create(doubleSpec); - if (model != null) { - return (zoom) => DoubleThemeFunction().exponential(model, zoom); - } - } - return (_) => null; + return PaintStyle( + id: id, + paintingStyle: style, + opacity: opacity, + strokeWidth: FunctionExpression( + (args) => strokeWidth.evaluate(args) ?? defaultStrokeWidth, + ), + color: color, + ); } } diff --git a/lib/src/themes/selector_factory.dart b/lib/src/themes/selector_factory.dart index 8db4970..8d0a00d 100644 --- a/lib/src/themes/selector_factory.dart +++ b/lib/src/themes/selector_factory.dart @@ -1,6 +1,5 @@ -import 'selector.dart'; - import '../logger.dart'; +import 'selector.dart'; class SelectorFactory { final Logger logger; @@ -16,7 +15,7 @@ class SelectorFactory { } return LayerSelector.composite([selector, _createFilter(filter)]); } - logger.warn(() => 'theme layer has no source-layer: ${themeLayer["id"]}'); + logger.warn(() => 'theme layer has no source-layer: ${themeLayer['id']}'); return LayerSelector.none(); } @@ -51,7 +50,7 @@ class SelectorFactory { } throw Exception('unexpected all filter: $f'); }).toList()); - } else if ((op == ">=" || op == "<=" || op == "<" || op == ">") && + } else if ((op == '>=' || op == '<=' || op == '<' || op == '>') && filter.length == 3 && filter[2] is num) { return _numericComparisonSelector(filter, op); @@ -65,7 +64,9 @@ class SelectorFactory { if (filter.length != 3) { throw Exception('unexpected filter $filter'); } - final propertyName = filter[1]; + final propertyNameField = filter[1]; + final propertyName = + propertyNameField is List ? propertyNameField.last : propertyNameField; final propertyValue = filter[2]; return LayerSelector.withProperty(propertyName, values: [propertyValue], negated: negated); @@ -85,7 +86,9 @@ class SelectorFactory { LayerSelector _numericComparisonSelector(List filter, String op) { ComparisonOperator comparison = _toComparisonOperator(op); - final propertyName = filter[1] as String; + final propertyNameField = filter[1]; + final propertyName = + propertyNameField is List ? propertyNameField.last : propertyNameField; final propertyValue = filter[2] as num; return LayerSelector.comparingProperty( propertyName, comparison, propertyValue); @@ -94,13 +97,13 @@ class SelectorFactory { ComparisonOperator _toComparisonOperator(String op) { switch (op) { - case "<=": + case '<=': return ComparisonOperator.LESS_THAN_OR_EQUAL_TO; - case ">=": + case '>=': return ComparisonOperator.GREATER_THAN_OR_EQUAL_TO; - case "<": + case '<': return ComparisonOperator.LESS_THAN; - case ">": + case '>': return ComparisonOperator.GREATER_THAN; default: throw Exception(op); diff --git a/lib/src/themes/style.dart b/lib/src/themes/style.dart index c6cc9e2..102ca09 100644 --- a/lib/src/themes/style.dart +++ b/lib/src/themes/style.dart @@ -1,14 +1,12 @@ import 'dart:ui'; import 'package:flutter/widgets.dart'; +import 'package:vector_tile_renderer/src/expressions/expression.dart'; import '../../vector_tile_renderer.dart'; import '../extensions.dart'; import 'paint_factory.dart'; -typedef DoubleZoomFunction = double? Function(double zoom); -typedef ColorZoomFunction = Color? Function(double zoom); -typedef TextHaloFunction = List? Function(double zoom); typedef TextTransformFunction = String? Function(String? text); class Style { @@ -16,7 +14,7 @@ class Style { final PaintStyle? linePaint; final PaintStyle? textPaint; final TextLayout? textLayout; - final TextHaloFunction? textHalo; + final Expression>? textHalo; final PaintStyle? outlinePaint; Style( @@ -55,10 +53,10 @@ class LayoutAnchor { class TextLayout { final LayoutPlacement placement; - final LayoutAnchor anchor; - final FeatureTextFunction text; - final DoubleZoomFunction textSize; - final DoubleZoomFunction? textLetterSpacing; + final Expression? anchor; + final Expression text; + final Expression textSize; + final Expression? textLetterSpacing; final FontStyle? fontStyle; final String? fontFamily; final TextTransformFunction? textTransform; diff --git a/lib/src/themes/text_halo_factory.dart b/lib/src/themes/text_halo_factory.dart deleted file mode 100644 index 343df70..0000000 --- a/lib/src/themes/text_halo_factory.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:ui'; - -import 'style.dart'; - -class TextHaloFactory { - static TextHaloFunction? toHaloFunction( - ColorZoomFunction colorFunction, double haloWidth) { - return (zoom) { - final color = colorFunction(zoom); - if (color == null) { - return null; - } - double offset = haloWidth / zoom; - double radius = haloWidth; - return [ - Shadow( - offset: Offset(-offset, -offset), - blurRadius: radius, - color: color, - ), - Shadow( - offset: Offset(offset, offset), - blurRadius: radius, - color: color, - ), - Shadow( - offset: Offset(offset, -offset), - blurRadius: radius, - color: color, - ), - Shadow( - offset: Offset(-offset, offset), - blurRadius: radius, - color: color, - ), - ]; - }; - } -} diff --git a/lib/src/themes/theme_function.dart b/lib/src/themes/theme_function.dart index 856f83a..aa9a98a 100644 --- a/lib/src/themes/theme_function.dart +++ b/lib/src/themes/theme_function.dart @@ -1,23 +1,30 @@ import 'dart:math'; import 'dart:ui'; +import 'package:vector_tile_renderer/src/expressions/expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; + import 'theme_function_model.dart'; abstract class ThemeFunction { Map _cache = {}; - T? exponential(FunctionModel model, double zoom) { + T? exponential(FunctionModel model, Map args) { + final zoom = args['zoom'] as double; + _ZoomValue? cached = _cache[model]; if (cached != null && cached.isCloseTo(zoom)) { return cached.value; } - FunctionStop? lower; - FunctionStop? upper; + FunctionStop? lower; + FunctionStop? upper; for (var stop in model.stops) { - if (stop.zoom > zoom && lower == null) { + final stopZoom = stop.zoom.evaluate(args)!; + + if (stopZoom > zoom && lower == null) { return null; } - if (stop.zoom <= zoom) { + if (stopZoom <= zoom) { lower = stop; upper = stop; } else { @@ -28,12 +35,17 @@ abstract class ThemeFunction { if (lower == null) { return null; } - cached = _ZoomValue(zoom, interpolate(model.base, lower, upper!, zoom)); + cached = _ZoomValue(zoom, interpolate(model.base, lower, upper!, args)); _cache[model] = cached; return cached.value; } - T? interpolate(T? base, FunctionStop lower, FunctionStop upper, double zoom); + T? interpolate( + Expression? base, + FunctionStop lower, + FunctionStop upper, + Map args, + ); } class _ZoomValue { @@ -59,16 +71,35 @@ class DoubleThemeFunction extends ThemeFunction { @override double? interpolate( - double? base, FunctionStop lower, FunctionStop upper, double zoom) { + Expression? base, + FunctionStop lower, + FunctionStop upper, + Map args, + ) { if (base == null) { - base = 1.0; + base = ValueExpression(1.0); } - final factor = interpolationFactor(base, lower.zoom, upper.zoom, zoom); - return (lower.value * (1 - factor)) + (upper.value * factor); + + final zoom = args['zoom']; + final factor = interpolationFactor( + base.evaluate(args) ?? 1.0, + lower.zoom.evaluate(args) ?? 1.0, + upper.zoom.evaluate(args) ?? 1.0, + zoom, + ); + + final lowerValue = lower.value.evaluate(args) ?? 0.0; + final upperValue = upper.value.evaluate(args) ?? 0.0; + + return (lowerValue * (1 - factor)) + (upperValue * factor); } double interpolationFactor( - double base, double lower, double upper, double input) { + double base, + double lower, + double upper, + double input, + ) { final difference = upper - lower; if (difference <= 1.0) { return 0; @@ -89,15 +120,24 @@ class ColorThemeFunction extends ThemeFunction { @override Color? interpolate( - Color? base, FunctionStop lower, FunctionStop upper, double zoom) { - final difference = lower.zoom - upper.zoom; + Expression? base, + FunctionStop lower, + FunctionStop upper, + Map args, + ) { + final zoom = args['zoom'] as double; + + final lowerZoom = lower.zoom.evaluate(args)!; + final upperZoom = upper.zoom.evaluate(args)!; + + final difference = lowerZoom - upperZoom; if (difference < 1.0) { - return lower.value; + return lower.value.evaluate(args); } - final progress = zoom - lower.zoom; + final progress = zoom - lowerZoom; if (progress / difference < 0.5) { - return lower.value; + return lower.value.evaluate(args); } - return upper.value; + return upper.value.evaluate(args); } } diff --git a/lib/src/themes/theme_function_model.dart b/lib/src/themes/theme_function_model.dart index a9c7b7f..945a6b3 100644 --- a/lib/src/themes/theme_function_model.dart +++ b/lib/src/themes/theme_function_model.dart @@ -1,46 +1,50 @@ import 'dart:ui'; -import 'color_parser.dart'; +import 'package:vector_tile_renderer/src/expressions/expression.dart'; +import 'package:vector_tile_renderer/src/parsers/parsers.dart'; class FunctionModel { - final T? base; + final Expression? base; final List> stops; FunctionModel(this.base, this.stops); } class FunctionStop { - final double zoom; - final T value; + final Expression zoom; + final Expression value; FunctionStop(this.zoom, this.value); } class DoubleFunctionModelFactory { FunctionModel? create(json) { - double? base = (json['base'] as num?)?.toDouble(); + final base = parse(json['base']); final stops = json['stops'] as List?; + if (stops == null) { if (base != null) { return FunctionModel(base, []); } return null; } + final modelStops = >[]; for (final stop in stops) { - final stopZoom = (stop[0] as num).toDouble(); - final stopValue = (stop[1] as num).toDouble(); + final stopZoom = parse(stop[0])!; + final stopValue = parse(stop[1])!; + modelStops.add(FunctionStop(stopZoom, stopValue)); } + return FunctionModel(base, modelStops); } } class ColorFunctionModelFactory { FunctionModel? create(json) { - Color? base = json['base'] is String - ? ColorParser.toColor(json['base'] as String?) - : null; + Expression? base = parse(json['base']); + final stops = json['stops'] as List?; if (stops == null) { if (base != null) { @@ -50,8 +54,9 @@ class ColorFunctionModelFactory { } final modelStops = >[]; for (final stop in stops) { - final stopZoom = (stop[0] as num).toDouble(); - final stopValue = ColorParser.toColor(stop[1] as String); + final stopZoom = parse(stop[0])!; + final stopValue = parse(stop[1]); + if (stopValue != null) { modelStops.add(FunctionStop(stopZoom, stopValue)); } diff --git a/lib/src/themes/theme_layers.dart b/lib/src/themes/theme_layers.dart index 352c079..6b68e40 100644 --- a/lib/src/themes/theme_layers.dart +++ b/lib/src/themes/theme_layers.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:vector_tile/vector_tile_feature.dart'; +import 'package:vector_tile_renderer/src/expressions/expression.dart'; import '../constants.dart'; import '../context.dart'; @@ -36,7 +37,7 @@ class DefaultLayer extends ThemeLayer { } class BackgroundLayer extends ThemeLayer { - final Color fillColor; + final Expression fillColor; BackgroundLayer(String id, this.fillColor) : super(id, ThemeLayerType.background, minzoom: 0, maxzoom: 24); @@ -44,9 +45,12 @@ class BackgroundLayer extends ThemeLayer { @override void render(Context context) { context.logger.log(() => 'rendering $id'); + final effectiveColor = + fillColor.evaluate({'zoom': context.zoom}) ?? Color(0x00000000); + final paint = Paint() ..style = PaintingStyle.fill - ..color = fillColor; + ..color = effectiveColor; context.canvas.drawRect( Rect.fromLTRB(0, 0, tileSize.toDouble(), tileSize.toDouble()), paint); } diff --git a/lib/src/themes/theme_reader.dart b/lib/src/themes/theme_reader.dart index b49835b..c9e834f 100644 --- a/lib/src/themes/theme_reader.dart +++ b/lib/src/themes/theme_reader.dart @@ -1,17 +1,16 @@ import 'dart:core'; import 'package:flutter/painting.dart'; +import 'package:vector_tile_renderer/src/expressions/expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; +import 'package:vector_tile_renderer/src/parsers/parsers.dart'; +import '../expressions/text_halo_expression.dart'; import '../logger.dart'; -import '../vector_tile_extensions.dart'; -import 'color_parser.dart'; import 'paint_factory.dart'; import 'selector_factory.dart'; import 'style.dart'; -import 'text_halo_factory.dart'; import 'theme.dart'; -import 'theme_function.dart'; -import 'theme_function_model.dart'; import 'theme_layers.dart'; class ThemeReader { @@ -55,7 +54,7 @@ class ThemeReader { ThemeLayer? _toBackgroundTheme(jsonLayer) { final backgroundColor = - ColorParser.toColor(jsonLayer['paint']?['background-color']); + parse(jsonLayer['paint']?['background-color']); if (backgroundColor != null) { return BackgroundLayer(jsonLayer['id'] ?? _unknownId, backgroundColor); } @@ -122,18 +121,18 @@ class ThemeReader { TextLayout _toTextLayout(jsonLayer) { final layout = jsonLayer['layout']; final textSize = _toTextSize(layout); - final textLetterSpacing = - _toDoubleZoomFunction(layout?['text-letter-spacing']); + final textLetterSpacing = parse(layout?['text-letter-spacing']); final placement = LayoutPlacement.fromName(layout?['symbol-placement'] as String?); - final anchor = LayoutAnchor.fromName(layout?['text-anchor'] as String?); - final textFunction = _toTextFunction(layout?['text-field']); + final anchor = parse(layout?['text-anchor']); + final textFunction = parse(layout?['text-field'])!; + final font = layout?['text-font']; String? fontFamily; FontStyle? fontStyle; if (font is List) { fontFamily = font[0]; - if (fontFamily != null && fontFamily.toLowerCase().contains("italic")) { + if (fontFamily != null && fontFamily.toLowerCase().contains('italic')) { fontStyle = FontStyle.italic; } } @@ -145,61 +144,31 @@ class ThemeReader { textTransform = (s) => s?.toLowerCase(); } return TextLayout( - placement: placement, - anchor: anchor, - text: textFunction, - textSize: textSize, - textLetterSpacing: textLetterSpacing, - fontFamily: fontFamily, - fontStyle: fontStyle, - textTransform: textTransform); + placement: placement, + anchor: anchor, + text: textFunction, + textSize: textSize, + textLetterSpacing: textLetterSpacing, + fontFamily: fontFamily, + fontStyle: fontStyle, + textTransform: textTransform, + ); } - TextHaloFunction? _toTextHalo(jsonLayer) { + TextHaloExpression? _toTextHalo(jsonLayer) { final paint = jsonLayer['paint']; if (paint != null) { final haloWidth = (paint['text-halo-width'] as num?)?.toDouble(); - final colorFunction = ColorParser.parse(paint['text-halo-color']); + final colorFunction = parse(paint['text-halo-color']); if (haloWidth != null && colorFunction != null) { - return TextHaloFactory.toHaloFunction(colorFunction, haloWidth); - } - } - } - - FeatureTextFunction _toTextFunction(String? textField) { - if (textField != null) { - final match = RegExp(r'\{(.+?)\}').firstMatch(textField); - if (match != null) { - final fieldName = match.group(1); - if (fieldName != null) { - return (feature) => feature.stringProperty(fieldName); - } + return TextHaloExpression(colorFunction, haloWidth); } } - return (feature) => feature.stringProperty('name'); } } -DoubleZoomFunction _toTextSize(layout) { - final function = _toDoubleZoomFunction(layout?['text-size']); - - return (function != null) ? function : (zoom) => 16.0; -} - -DoubleZoomFunction? _toDoubleZoomFunction(layoutProperty) { - if (layoutProperty == null) { - return null; - } - if (layoutProperty is Map) { - final model = DoubleFunctionModelFactory().create(layoutProperty); - if (model != null) { - return (zoom) => DoubleThemeFunction().exponential(model, zoom); - } - } else if (layoutProperty is num) { - final size = layoutProperty.toDouble(); - return (zoom) => size; - } - return null; +Expression _toTextSize(layout) { + return parse(layout?['text-size']) ?? ValueExpression(16.0); } ThemeLayerType _toLayerType(jsonLayer) { diff --git a/lib/src/vector_tile_extensions.dart b/lib/src/vector_tile_extensions.dart index c6e769f..664e6ff 100644 --- a/lib/src/vector_tile_extensions.dart +++ b/lib/src/vector_tile_extensions.dart @@ -1,5 +1,4 @@ import '../vector_tile_renderer.dart'; - import 'extensions.dart'; extension VectorTileFeatureExtension on VectorTileFeature { @@ -11,4 +10,14 @@ extension VectorTileFeatureExtension on VectorTileFeature { .firstOrNull() ?.stringValue; } + + Map get collectedProperties { + final properties = decodeProperties(); + Map result = {}; + for (final propertyList in properties) { + result = {...result, ...propertyList}; + } + + return result; + } } diff --git a/pubspec.lock b/pubspec.lock index 28c3a5e..8d8c46c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,6 +29,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.7.0" + bezier: + dependency: "direct main" + description: + name: bezier + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" boolean_selector: dependency: transitive description: @@ -334,7 +341,7 @@ packages: source: hosted version: "1.3.0" vector_math: - dependency: transitive + dependency: "direct main" description: name: vector_math url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index adf56ee..057c025 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,8 @@ environment: dependencies: flutter: sdk: flutter + bezier: ^1.2.0 + vector_math: ^2.1.0 vector_tile: ^0.2.2 dev_dependencies: diff --git a/test/src/expressions/case.dart b/test/src/expressions/case.dart new file mode 100644 index 0000000..1b27998 --- /dev/null +++ b/test/src/expressions/case.dart @@ -0,0 +1,31 @@ +import 'package:test/test.dart'; +import 'package:vector_tile_renderer/src/expressions/argument_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/case_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; + +import 'helpers.dart'; + +void main() { + test('case expression evaluates value of first true expression', () { + final expression = CaseExpression( + [ + Case(ValueExpression(false), ValueExpression('false')), + Case( + ArgumentExpression('true-value'), ValueExpression('result')), + Case(ValueExpression(true), ValueExpression('wrong result')), + ], + ValueExpression('fallback'), + ); + + expectEvaluated(expression, 'result', {'true-value': true}); + }); + + test('case expression evaluates fallback in case of no true expression', () { + final expression = CaseExpression( + [Case(ValueExpression(false), ValueExpression('false'))], + ValueExpression('fallback'), + ); + + expectEvaluated(expression, 'fallback'); + }); +} diff --git a/test/src/expressions/coalesce.dart b/test/src/expressions/coalesce.dart new file mode 100644 index 0000000..0497971 --- /dev/null +++ b/test/src/expressions/coalesce.dart @@ -0,0 +1,26 @@ +import 'package:test/test.dart'; +import 'package:vector_tile_renderer/src/expressions/argument_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/coalesce_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; + +import 'helpers.dart'; + +void main() { + test('coalesce returns the first non-null value', () { + final expression = CoalesceExpression([ + ValueExpression(null), + ArgumentExpression('doesnt-exist'), + ArgumentExpression('result'), + ArgumentExpression('also-exists') + ]); + + final args = {'result': 'result', 'also-exists': 'wrong'}; + + expectEvaluated(expression, 'result', args); + }); + + test('coalesce returns null if no expression is non-null', () { + final expression = CoalesceExpression([ValueExpression(null)]); + expectEvaluated(expression, null); + }); +} diff --git a/test/src/expressions/helpers.dart b/test/src/expressions/helpers.dart new file mode 100644 index 0000000..9163e19 --- /dev/null +++ b/test/src/expressions/helpers.dart @@ -0,0 +1,8 @@ +import 'package:test/test.dart'; +import 'package:vector_tile_renderer/src/expressions/expression.dart'; + +void expectEvaluated(Expression actual, dynamic expected, + [Map args = const {}]) { + final evaluated = actual.evaluate(args); + expect(evaluated, expected); +} diff --git a/test/src/expressions/interpolations/cubic_bezier.dart b/test/src/expressions/interpolations/cubic_bezier.dart new file mode 100644 index 0000000..0b8d7fe --- /dev/null +++ b/test/src/expressions/interpolations/cubic_bezier.dart @@ -0,0 +1,34 @@ +import 'package:test/test.dart'; +import 'package:vector_math/vector_math.dart'; +import 'package:vector_tile_renderer/src/expressions/argument_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/interpolate/cubic_bezier_interpolation_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; +import 'package:vector_tile_renderer/src/themes/theme_function_model.dart'; + +import '../helpers.dart'; + +void main() { + final expression = CubicBezierInterpolationExpression( + Vector2(0.5, 0), + Vector2(1, 1), + ArgumentExpression('zoom'), + [ + FunctionStop(ValueExpression(0), ValueExpression(0)), + FunctionStop(ValueExpression(100), ValueExpression(10)), + ], + ); + + test('Linear interpolation uses values based on stops', () { + const delta = 0.00001; + + expectEvaluated(expression, 0, {'zoom': 0}); + expectEvaluated(expression, closeTo(1.27778, delta), {'zoom': 1}); + expectEvaluated(expression, closeTo(81.98363, delta), {'zoom': 9}); + expectEvaluated(expression, 100, {'zoom': 10}); + }); + + test('Linear interpolation uses last values when outside of bounds', () { + expectEvaluated(expression, 0, {'zoom': -1}); + expectEvaluated(expression, 100, {'zoom': 100}); + }); +} diff --git a/test/src/expressions/interpolations/exponential.dart b/test/src/expressions/interpolations/exponential.dart new file mode 100644 index 0000000..428beac --- /dev/null +++ b/test/src/expressions/interpolations/exponential.dart @@ -0,0 +1,32 @@ +import 'package:test/test.dart'; +import 'package:vector_tile_renderer/src/expressions/argument_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/interpolate/exponential_interpolation_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; +import 'package:vector_tile_renderer/src/themes/theme_function_model.dart'; + +import '../helpers.dart'; + +void main() { + final expression = ExponentialInterpolationExpression( + 2, + ArgumentExpression('zoom'), + [ + FunctionStop(ValueExpression(0), ValueExpression(0)), + FunctionStop(ValueExpression(10), ValueExpression(100)), + ], + ); + + test('Exponential interpolation uses values based on stops', () { + const delta = 0.00001; + + expectEvaluated(expression, 0, {'zoom': 0}); + expectEvaluated(expression, closeTo(0.09775, delta), {'zoom': 1}); + expectEvaluated(expression, closeTo(49.95112, delta), {'zoom': 9}); + expectEvaluated(expression, 100, {'zoom': 10}); + }); + + test('Exponential interpolation uses last values when outside of bounds', () { + expectEvaluated(expression, 0, {'zoom': -1}); + expectEvaluated(expression, 100, {'zoom': 100}); + }); +} diff --git a/test/src/expressions/interpolations/linear.dart b/test/src/expressions/interpolations/linear.dart new file mode 100644 index 0000000..21d3955 --- /dev/null +++ b/test/src/expressions/interpolations/linear.dart @@ -0,0 +1,28 @@ +import 'package:test/test.dart'; +import 'package:vector_tile_renderer/src/expressions/argument_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/interpolate/linear_interpolation_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; +import 'package:vector_tile_renderer/src/themes/theme_function_model.dart'; + +import '../helpers.dart'; + +void main() { + final expression = LinearInterpolationExpression( + ArgumentExpression('zoom'), + [ + FunctionStop(ValueExpression(0), ValueExpression(0)), + FunctionStop(ValueExpression(10), ValueExpression(100)), + ], + ); + + test('Linear interpolation uses values based on stops', () { + expectEvaluated(expression, 0, {'zoom': 0}); + expectEvaluated(expression, 50, {'zoom': 5}); + expectEvaluated(expression, 100, {'zoom': 10}); + }); + + test('Linear interpolation uses last values when outside of bounds', () { + expectEvaluated(expression, 0, {'zoom': -1}); + expectEvaluated(expression, 100, {'zoom': 100}); + }); +} diff --git a/test/src/expressions/match.dart b/test/src/expressions/match.dart new file mode 100644 index 0000000..1657bb5 --- /dev/null +++ b/test/src/expressions/match.dart @@ -0,0 +1,45 @@ +import 'package:test/test.dart'; +import 'package:vector_tile_renderer/src/expressions/match_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; + +import 'helpers.dart'; + +void main() { + test('match expression evaluates first output matching input', () { + final expression = MatchExpression( + ValueExpression('input'), + [ + Match( + ValueExpression('not-input'), + ValueExpression('not-input'), + ), + Match( + ValueExpression('input'), + ValueExpression('result'), + ), + Match( + ValueExpression('input'), + ValueExpression('not-result'), + ), + ], + ValueExpression('fallback'), + ); + + expectEvaluated(expression, 'result'); + }); + + test('match expression evaluates fallback if no match matches', () { + final expression = MatchExpression( + ValueExpression('input'), + [ + Match( + ValueExpression('not-input'), + ValueExpression('not-input'), + ), + ], + ValueExpression('fallback'), + ); + + expectEvaluated(expression, 'fallback'); + }); +} diff --git a/test/src/expressions/step.dart b/test/src/expressions/step.dart new file mode 100644 index 0000000..b963a28 --- /dev/null +++ b/test/src/expressions/step.dart @@ -0,0 +1,27 @@ +import 'package:test/test.dart'; +import 'package:vector_tile_renderer/src/expressions/argument_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/step_expression.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; + +import 'helpers.dart'; + +void main() { + test('step expression picks values based on steps', () { + final expression = StepExpression( + ArgumentExpression('zoom'), + ValueExpression('base'), + [ + Step(1, ValueExpression('1')), + Step(3, ValueExpression('3')), + Step(7, ValueExpression('7')), + Step(20, ValueExpression('20')), + ], + ); + + expectEvaluated(expression, 'base', {'zoom': 0}); + expectEvaluated(expression, '1', {'zoom': 1}); + expectEvaluated(expression, '1', {'zoom': 2.99999}); + expectEvaluated(expression, '3', {'zoom': 3}); + expectEvaluated(expression, '20', {'zoom': 200}); + }); +} diff --git a/test/src/themes/color_parser_test.dart b/test/src/themes/color_parser_test.dart index 826879d..037fc45 100644 --- a/test/src/themes/color_parser_test.dart +++ b/test/src/themes/color_parser_test.dart @@ -1,36 +1,36 @@ import 'package:test/test.dart'; -import 'package:vector_tile_renderer/src/themes/color_parser.dart'; +import 'package:vector_tile_renderer/src/parsers/color.dart'; void main() { test('parses a hex RGB color', () { - final color = ColorParser.toColor('#90d86c'); + final color = ColorParser().parseString('#90d86c'); expect(color, isNotNull); - expect(color!.alpha, 0xff); + expect(color.alpha, 0xff); expect(color.red, 0x90); expect(color.green, 0xd8); expect(color.blue, 0x6c); }); test('parses an RGB color', () { - final color = ColorParser.toColor('rgb(239, 238,12)'); + final color = ColorParser().parseString('rgb(239, 238,12)'); expect(color, isNotNull); - expect(color!.alpha, 0xff); + expect(color.alpha, 0xff); expect(color.red, 239); expect(color.green, 238); expect(color.blue, 12); }); test('parses an hsl color', () { - final color = ColorParser.toColor('hsl(248, 7%, 66%)'); + final color = ColorParser().parseString('hsl(248, 7%, 66%)'); expect(color, isNotNull); - expect(color!.alpha, 0xff); + expect(color.alpha, 0xff); expect(color.red, 0xA4); expect(color.green, 0xA2); expect(color.blue, 0xAE); }); test('parses an hsla color', () { - final color = ColorParser.toColor('hsla(96, 40%, 49%, 0.36)'); + final color = ColorParser().parseString('hsla(96, 40%, 49%, 0.36)'); expect(color, isNotNull); - expect(color!.alpha, 92); + expect(color.alpha, 92); expect(color.red, 0x73); expect(color.green, 0xAF); expect(color.blue, 0x4B); diff --git a/test/src/themes/theme_function_model_test.dart b/test/src/themes/theme_function_model_test.dart index eaebeeb..fd2fdee 100644 --- a/test/src/themes/theme_function_model_test.dart +++ b/test/src/themes/theme_function_model_test.dart @@ -27,7 +27,7 @@ void main() { }; final model = DoubleFunctionModelFactory().create(definition); expect(model, isNotNull); - expect(model!.base, 1.4); + expect(model!.base!.evaluate({}), 1.4); expect(model.stops, hasLength(2)); expect(model.stops[0].zoom, 8); expect(model.stops[0].value, 1); diff --git a/test/src/themes/theme_function_test.dart b/test/src/themes/theme_function_test.dart index 540d04a..f912e2e 100644 --- a/test/src/themes/theme_function_test.dart +++ b/test/src/themes/theme_function_test.dart @@ -1,31 +1,53 @@ import 'package:test/test.dart'; +import 'package:vector_tile_renderer/src/expressions/value_expression.dart'; import 'package:vector_tile_renderer/src/themes/theme_function.dart'; import 'package:vector_tile_renderer/src/themes/theme_function_model.dart'; void main() { - final definition = FunctionModel( - 1.2, [FunctionStop(14, 0.5), FunctionStop(20, 10)]); + ValueExpression v(double value) => ValueExpression(value); + Map withZoom(double zoom) => {'zoom': zoom}; + + final definition = FunctionModel(v(1.2), [ + FunctionStop(v(14), v(1.09)), + FunctionStop(v(20), v(6.19)), + ]); final function = DoubleThemeFunction(); final epsilon = 0.006; test('provides null for zoom values less than the lowest stop', () { - expect(function.exponential(definition, 12), isNull); - expect(function.exponential(definition, 13), isNull); - expect(function.exponential(definition, 13.9), isNull); + expect(function.exponential(definition, withZoom(12)), isNull); + expect(function.exponential(definition, withZoom(13)), isNull); + expect(function.exponential(definition, withZoom(13.9)), isNull); }); test('produces an exponent based on stops', () { - expect(function.exponential(definition, 14), closeTo(1.09, epsilon)); - expect(function.exponential(definition, 20), closeTo(6.19, epsilon)); + expect( + function.exponential(definition, withZoom(14)), + closeTo(1.09, epsilon), + ); + + expect( + function.exponential(definition, withZoom(20)), + closeTo(6.19, epsilon), + ); }); + test('produces an exponent with zoom greater than top stop', () { - expect(function.exponential(definition, 100), closeTo(6.19, epsilon)); - }); - test('produces null with zoom less than bottom stop', () { - expect(function.exponential(definition, 0), isNull); + expect( + function.exponential(definition, withZoom(100)), + closeTo(6.19, epsilon), + ); }); + test('produces an exponent with zoom between stops', () { - expect(function.exponential(definition, 15), closeTo(1.46, epsilon)); - expect(function.exponential(definition, 19), closeTo(4.64, epsilon)); + expect( + function.exponential(definition, withZoom(15)), + closeTo(1.46, epsilon), + ); + + expect( + function.exponential(definition, withZoom(19)), + closeTo(4.64, epsilon), + ); }); }