tune the app
This commit is contained in:
parent
8b53075a92
commit
58f1f3bbbc
11 changed files with 458 additions and 47 deletions
1
.claude/worktrees/relaxed-wright
Submodule
1
.claude/worktrees/relaxed-wright
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit a3aaf1b6de30f5134d8180dbeee145d598e6e9a8
|
||||||
216
backend/initdb/02_tile_functions.sql
Normal file
216
backend/initdb/02_tile_functions.sql
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
-- Zoom-dependent tile functions for Martin.
|
||||||
|
-- These apply geometry simplification and feature filtering so that
|
||||||
|
-- low-zoom tiles contain fewer, simpler features.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- planet_osm_polygon
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
CREATE OR REPLACE FUNCTION tile_polygon(z integer, x integer, y integer)
|
||||||
|
RETURNS bytea AS $$
|
||||||
|
DECLARE
|
||||||
|
bounds geometry;
|
||||||
|
tolerance double precision;
|
||||||
|
result bytea;
|
||||||
|
BEGIN
|
||||||
|
bounds := ST_TileEnvelope(z, x, y);
|
||||||
|
|
||||||
|
-- Simplification tolerance scales with zoom: lower zoom = more aggressive
|
||||||
|
tolerance := CASE
|
||||||
|
WHEN z >= 13 THEN 0 -- full detail
|
||||||
|
WHEN z >= 11 THEN 1 -- light simplification
|
||||||
|
WHEN z >= 9 THEN 10 -- moderate
|
||||||
|
ELSE 50 -- heavy
|
||||||
|
END;
|
||||||
|
|
||||||
|
WITH features AS (
|
||||||
|
SELECT
|
||||||
|
osm_id,
|
||||||
|
name,
|
||||||
|
building,
|
||||||
|
landuse,
|
||||||
|
"natural",
|
||||||
|
amenity,
|
||||||
|
leisure,
|
||||||
|
water,
|
||||||
|
waterway,
|
||||||
|
CASE
|
||||||
|
WHEN tolerance = 0 THEN ST_AsMVTGeom(way, bounds)
|
||||||
|
ELSE ST_AsMVTGeom(
|
||||||
|
ST_Simplify(way, tolerance, true),
|
||||||
|
bounds
|
||||||
|
)
|
||||||
|
END AS geom
|
||||||
|
FROM planet_osm_polygon
|
||||||
|
WHERE way && bounds
|
||||||
|
-- Feature filtering by zoom level:
|
||||||
|
AND CASE
|
||||||
|
-- zoom 8-9: only water, large forests/parks
|
||||||
|
WHEN z < 10 THEN
|
||||||
|
"natural" IN ('water', 'wood') OR landuse IN ('forest', 'reservoir')
|
||||||
|
-- zoom 10-11: add more landuse
|
||||||
|
WHEN z < 12 THEN
|
||||||
|
"natural" IN ('water', 'wood', 'wetland', 'heath', 'scrub')
|
||||||
|
OR landuse IN ('forest', 'reservoir', 'residential', 'industrial',
|
||||||
|
'commercial', 'farmland', 'meadow', 'grass')
|
||||||
|
OR leisure IN ('park', 'nature_reserve', 'garden')
|
||||||
|
-- zoom 12: add buildings (large only)
|
||||||
|
WHEN z < 13 THEN
|
||||||
|
"natural" IS NOT NULL OR landuse IS NOT NULL OR leisure IS NOT NULL
|
||||||
|
OR water IS NOT NULL
|
||||||
|
OR (building IS NOT NULL AND ST_Area(way) > 5000)
|
||||||
|
-- zoom 13+: everything
|
||||||
|
ELSE true
|
||||||
|
END
|
||||||
|
)
|
||||||
|
SELECT ST_AsMVT(features, 'planet_osm_polygon') INTO result FROM features;
|
||||||
|
|
||||||
|
RETURN result;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- planet_osm_line
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
CREATE OR REPLACE FUNCTION tile_line(z integer, x integer, y integer)
|
||||||
|
RETURNS bytea AS $$
|
||||||
|
DECLARE
|
||||||
|
bounds geometry;
|
||||||
|
tolerance double precision;
|
||||||
|
result bytea;
|
||||||
|
BEGIN
|
||||||
|
bounds := ST_TileEnvelope(z, x, y);
|
||||||
|
|
||||||
|
tolerance := CASE
|
||||||
|
WHEN z >= 13 THEN 0
|
||||||
|
WHEN z >= 11 THEN 5
|
||||||
|
ELSE 20
|
||||||
|
END;
|
||||||
|
|
||||||
|
WITH features AS (
|
||||||
|
SELECT
|
||||||
|
osm_id,
|
||||||
|
name,
|
||||||
|
highway,
|
||||||
|
railway,
|
||||||
|
waterway,
|
||||||
|
CASE
|
||||||
|
WHEN tolerance = 0 THEN ST_AsMVTGeom(way, bounds)
|
||||||
|
ELSE ST_AsMVTGeom(
|
||||||
|
ST_Simplify(way, tolerance, true),
|
||||||
|
bounds
|
||||||
|
)
|
||||||
|
END AS geom
|
||||||
|
FROM planet_osm_line
|
||||||
|
WHERE way && bounds
|
||||||
|
AND CASE
|
||||||
|
-- zoom 10-11: only major roads and rivers
|
||||||
|
WHEN z < 12 THEN
|
||||||
|
highway IN ('motorway', 'trunk', 'primary', 'secondary')
|
||||||
|
OR railway IN ('rail')
|
||||||
|
OR waterway IN ('river', 'canal')
|
||||||
|
-- zoom 12: add tertiary roads
|
||||||
|
WHEN z < 13 THEN
|
||||||
|
highway IN ('motorway', 'trunk', 'primary', 'secondary',
|
||||||
|
'tertiary', 'motorway_link', 'trunk_link')
|
||||||
|
OR railway IS NOT NULL
|
||||||
|
OR waterway IS NOT NULL
|
||||||
|
-- zoom 13+: everything
|
||||||
|
ELSE true
|
||||||
|
END
|
||||||
|
)
|
||||||
|
SELECT ST_AsMVT(features, 'planet_osm_line') INTO result FROM features;
|
||||||
|
|
||||||
|
RETURN result;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- planet_osm_point
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
CREATE OR REPLACE FUNCTION tile_point(z integer, x integer, y integer)
|
||||||
|
RETURNS bytea AS $$
|
||||||
|
DECLARE
|
||||||
|
bounds geometry;
|
||||||
|
result bytea;
|
||||||
|
BEGIN
|
||||||
|
bounds := ST_TileEnvelope(z, x, y);
|
||||||
|
|
||||||
|
WITH features AS (
|
||||||
|
SELECT
|
||||||
|
osm_id,
|
||||||
|
name,
|
||||||
|
place,
|
||||||
|
amenity,
|
||||||
|
shop,
|
||||||
|
tourism,
|
||||||
|
"natural",
|
||||||
|
ST_AsMVTGeom(way, bounds) AS geom
|
||||||
|
FROM planet_osm_point
|
||||||
|
WHERE way && bounds
|
||||||
|
AND CASE
|
||||||
|
-- zoom 12: only cities/towns/villages and major POIs
|
||||||
|
WHEN z < 13 THEN
|
||||||
|
place IN ('city', 'town', 'village')
|
||||||
|
-- zoom 13: add hamlets and named POIs
|
||||||
|
WHEN z < 14 THEN
|
||||||
|
place IS NOT NULL
|
||||||
|
OR (name IS NOT NULL AND (amenity IS NOT NULL OR tourism IS NOT NULL))
|
||||||
|
-- zoom 14+: everything
|
||||||
|
ELSE true
|
||||||
|
END
|
||||||
|
)
|
||||||
|
SELECT ST_AsMVT(features, 'planet_osm_point') INTO result FROM features;
|
||||||
|
|
||||||
|
RETURN result;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- planet_osm_roads
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
CREATE OR REPLACE FUNCTION tile_roads(z integer, x integer, y integer)
|
||||||
|
RETURNS bytea AS $$
|
||||||
|
DECLARE
|
||||||
|
bounds geometry;
|
||||||
|
tolerance double precision;
|
||||||
|
result bytea;
|
||||||
|
BEGIN
|
||||||
|
bounds := ST_TileEnvelope(z, x, y);
|
||||||
|
|
||||||
|
tolerance := CASE
|
||||||
|
WHEN z >= 10 THEN 0
|
||||||
|
WHEN z >= 8 THEN 10
|
||||||
|
ELSE 50
|
||||||
|
END;
|
||||||
|
|
||||||
|
WITH features AS (
|
||||||
|
SELECT
|
||||||
|
osm_id,
|
||||||
|
name,
|
||||||
|
highway,
|
||||||
|
ref,
|
||||||
|
CASE
|
||||||
|
WHEN tolerance = 0 THEN ST_AsMVTGeom(way, bounds)
|
||||||
|
ELSE ST_AsMVTGeom(
|
||||||
|
ST_Simplify(way, tolerance, true),
|
||||||
|
bounds
|
||||||
|
)
|
||||||
|
END AS geom
|
||||||
|
FROM planet_osm_roads
|
||||||
|
WHERE way && bounds
|
||||||
|
AND CASE
|
||||||
|
-- zoom 6-7: motorways only
|
||||||
|
WHEN z < 8 THEN
|
||||||
|
highway IN ('motorway', 'trunk')
|
||||||
|
-- zoom 8-9: add primary roads
|
||||||
|
WHEN z < 10 THEN
|
||||||
|
highway IN ('motorway', 'trunk', 'primary')
|
||||||
|
-- zoom 10+: everything in the roads table
|
||||||
|
ELSE true
|
||||||
|
END
|
||||||
|
)
|
||||||
|
SELECT ST_AsMVT(features, 'planet_osm_roads') INTO result FROM features;
|
||||||
|
|
||||||
|
RETURN result;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;
|
||||||
|
|
@ -3,43 +3,31 @@ listen_addresses: '0.0.0.0:3001'
|
||||||
postgres:
|
postgres:
|
||||||
connection_string: ${DATABASE_URL}
|
connection_string: ${DATABASE_URL}
|
||||||
|
|
||||||
tables:
|
functions:
|
||||||
planet_osm_polygon:
|
planet_osm_polygon:
|
||||||
schema: public
|
schema: public
|
||||||
table: planet_osm_polygon
|
function: tile_polygon
|
||||||
srid: 3857
|
minzoom: 8
|
||||||
geometry_column: way
|
|
||||||
geometry_type: GEOMETRY
|
|
||||||
minzoom: 4
|
|
||||||
maxzoom: 14
|
maxzoom: 14
|
||||||
bounds: [-180.0, -85.0511, 180.0, 85.0511]
|
bounds: [-180.0, -85.0511, 180.0, 85.0511]
|
||||||
|
|
||||||
planet_osm_line:
|
planet_osm_line:
|
||||||
schema: public
|
schema: public
|
||||||
table: planet_osm_line
|
function: tile_line
|
||||||
srid: 3857
|
minzoom: 10
|
||||||
geometry_column: way
|
|
||||||
geometry_type: GEOMETRY
|
|
||||||
minzoom: 8
|
|
||||||
maxzoom: 14
|
maxzoom: 14
|
||||||
bounds: [-180.0, -85.0511, 180.0, 85.0511]
|
bounds: [-180.0, -85.0511, 180.0, 85.0511]
|
||||||
|
|
||||||
planet_osm_point:
|
planet_osm_point:
|
||||||
schema: public
|
schema: public
|
||||||
table: planet_osm_point
|
function: tile_point
|
||||||
srid: 3857
|
minzoom: 12
|
||||||
geometry_column: way
|
|
||||||
geometry_type: GEOMETRY
|
|
||||||
minzoom: 10
|
|
||||||
maxzoom: 14
|
maxzoom: 14
|
||||||
bounds: [-180.0, -85.0511, 180.0, 85.0511]
|
bounds: [-180.0, -85.0511, 180.0, 85.0511]
|
||||||
|
|
||||||
planet_osm_roads:
|
planet_osm_roads:
|
||||||
schema: public
|
schema: public
|
||||||
table: planet_osm_roads
|
function: tile_roads
|
||||||
srid: 3857
|
minzoom: 6
|
||||||
geometry_column: way
|
|
||||||
geometry_type: GEOMETRY
|
|
||||||
minzoom: 4
|
|
||||||
maxzoom: 14
|
maxzoom: 14
|
||||||
bounds: [-180.0, -85.0511, 180.0, 85.0511]
|
bounds: [-180.0, -85.0511, 180.0, 85.0511]
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,16 @@ class AppConstants {
|
||||||
/// Default backend URL when none is configured.
|
/// Default backend URL when none is configured.
|
||||||
static const String defaultBackendUrl = 'http://192.168.2.59:8080';
|
static const String defaultBackendUrl = 'http://192.168.2.59:8080';
|
||||||
|
|
||||||
|
/// Default Martin tile server URL when none is configured.
|
||||||
|
static const String defaultMartinUrl = 'http://192.168.2.59:3001';
|
||||||
|
|
||||||
/// Default map center: Amsterdam.
|
/// Default map center: Amsterdam.
|
||||||
static const double defaultLat = 52.3676;
|
static const double defaultLat = 52.3676;
|
||||||
static const double defaultLon = 4.9041;
|
static const double defaultLon = 4.9041;
|
||||||
static final LatLng defaultCenter = LatLng(defaultLat, defaultLon);
|
static final LatLng defaultCenter = LatLng(defaultLat, defaultLon);
|
||||||
|
|
||||||
/// Zoom levels.
|
/// Zoom levels.
|
||||||
static const double minZoom = 0;
|
static const double minZoom = 6;
|
||||||
static const double maxZoom = 18;
|
static const double maxZoom = 18;
|
||||||
static const double defaultZoom = 13;
|
static const double defaultZoom = 13;
|
||||||
static const double poiZoom = 16;
|
static const double poiZoom = 16;
|
||||||
|
|
@ -26,5 +29,6 @@ class AppConstants {
|
||||||
|
|
||||||
/// Settings keys.
|
/// Settings keys.
|
||||||
static const String settingBackendUrl = 'backend_url';
|
static const String settingBackendUrl = 'backend_url';
|
||||||
|
static const String settingMartinUrl = 'martin_url';
|
||||||
static const String settingThemeMode = 'theme_mode';
|
static const String settingThemeMode = 'theme_mode';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:vector_map_tiles/vector_map_tiles.dart';
|
import 'package:vector_map_tiles/vector_map_tiles.dart';
|
||||||
import '../../../../core/api/api_client.dart';
|
|
||||||
import '../../providers/map_provider.dart';
|
import '../../providers/map_provider.dart';
|
||||||
import '../../providers/map_style_provider.dart';
|
import '../../providers/map_style_provider.dart';
|
||||||
import '../widgets/map_controls.dart';
|
import '../widgets/map_controls.dart';
|
||||||
|
|
@ -38,8 +37,8 @@ class _MapScreenState extends ConsumerState<MapScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final mapState = ref.watch(mapProvider);
|
final mapState = ref.watch(mapProvider);
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final martinUrl = ref.watch(martinUrlProvider);
|
||||||
final styleAsync = ref.watch(mapStyleProvider(apiClient.baseUrl));
|
final styleAsync = ref.watch(mapStyleProvider(martinUrl));
|
||||||
|
|
||||||
// Move the map only for programmatic state changes (locateUser,
|
// Move the map only for programmatic state changes (locateUser,
|
||||||
// zoomIn/zoomOut). Gesture-driven pans no longer call updateCamera(),
|
// zoomIn/zoomOut). Gesture-driven pans no longer call updateCamera(),
|
||||||
|
|
@ -60,7 +59,7 @@ class _MapScreenState extends ConsumerState<MapScreen> {
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
initialCenter: mapState.center,
|
initialCenter: mapState.center,
|
||||||
initialZoom: mapState.zoom,
|
initialZoom: mapState.zoom,
|
||||||
minZoom: 0,
|
minZoom: 6,
|
||||||
maxZoom: 18,
|
maxZoom: 18,
|
||||||
onPositionChanged: (position, hasGesture) {
|
onPositionChanged: (position, hasGesture) {
|
||||||
// Intentionally not syncing gesture-driven position changes
|
// Intentionally not syncing gesture-driven position changes
|
||||||
|
|
@ -75,19 +74,26 @@ class _MapScreenState extends ConsumerState<MapScreen> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
styleAsync.when(
|
// Defer tile loading until the initial location is known so
|
||||||
data: (style) => VectorTileLayer(
|
// tiles are never fetched for the default center and then
|
||||||
tileProviders: style.providers,
|
// mass-cancelled (CancellationException crash) when the map
|
||||||
theme: style.theme,
|
// jumps to the GPS position.
|
||||||
concurrency: 4,
|
if (!mapState.isLocating)
|
||||||
memoryTileCacheMaxSize: 20 * 1024 * 1024, // 20 MB raw tiles
|
styleAsync.when(
|
||||||
memoryTileDataCacheMaxSize: 50, // 50 parsed tiles
|
data: (style) => VectorTileLayer(
|
||||||
fileCacheMaximumSizeInBytes: 50 * 1024 * 1024, // 50 MB on disk
|
tileProviders: style.providers,
|
||||||
cacheFolder: () => getApplicationCacheDirectory(),
|
theme: style.theme,
|
||||||
|
layerMode: VectorTileLayerMode.vector,
|
||||||
|
concurrency: 4,
|
||||||
|
maximumTileSubstitutionDifference: 4,
|
||||||
|
memoryTileCacheMaxSize: 40 * 1024 * 1024,
|
||||||
|
memoryTileDataCacheMaxSize: 100,
|
||||||
|
fileCacheMaximumSizeInBytes: 150 * 1024 * 1024,
|
||||||
|
cacheFolder: () => getApplicationCacheDirectory(),
|
||||||
|
),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
error: (e, _) => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
loading: () => const SizedBox.shrink(),
|
|
||||||
error: (e, _) => const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
if (mapState.currentLocation != null)
|
if (mapState.currentLocation != null)
|
||||||
MarkerLayer(
|
MarkerLayer(
|
||||||
markers: [
|
markers: [
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ class MapNotifier extends Notifier<MapState> {
|
||||||
MapState build() => MapState(
|
MapState build() => MapState(
|
||||||
center: AppConstants.defaultCenter,
|
center: AppConstants.defaultCenter,
|
||||||
zoom: AppConstants.defaultZoom,
|
zoom: AppConstants.defaultZoom,
|
||||||
|
isLocating: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
void updateCamera(LatLng center, double zoom) {
|
void updateCamera(LatLng center, double zoom) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,141 @@
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:vector_map_tiles/vector_map_tiles.dart';
|
import 'package:vector_map_tiles/vector_map_tiles.dart';
|
||||||
|
import 'package:vector_tile_renderer/vector_tile_renderer.dart';
|
||||||
|
|
||||||
/// Loads and caches the MapLibre style from the backend.
|
/// Martin URL provider — overridden in main() with the saved value.
|
||||||
/// Keyed by base URL so a URL change triggers a reload.
|
final martinUrlProvider = Provider<String>((ref) => 'http://localhost:3001');
|
||||||
|
|
||||||
|
/// Builds a Style that fetches tiles directly from Martin.
|
||||||
|
/// Keyed by Martin URL so a URL change triggers a reload.
|
||||||
final mapStyleProvider =
|
final mapStyleProvider =
|
||||||
FutureProvider.family<Style, String>((ref, baseUrl) async {
|
FutureProvider.family<Style, String>((ref, martinUrl) async {
|
||||||
final styleUrl = '$baseUrl/tiles/style.json';
|
final urlTemplate =
|
||||||
return StyleReader(uri: styleUrl).read();
|
'$martinUrl/planet_osm_polygon,planet_osm_line,planet_osm_point,planet_osm_roads/{z}/{x}/{y}';
|
||||||
|
|
||||||
|
final tileProvider = NetworkVectorTileProvider(
|
||||||
|
urlTemplate: urlTemplate,
|
||||||
|
maximumZoom: 14,
|
||||||
|
minimumZoom: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
final providers = TileProviders({
|
||||||
|
'osm_all': tileProvider,
|
||||||
|
});
|
||||||
|
|
||||||
|
final styleJson = <String, dynamic>{
|
||||||
|
"version": 8,
|
||||||
|
"name": "Privacy Maps",
|
||||||
|
"sources": {
|
||||||
|
"osm_all": {
|
||||||
|
"type": "vector",
|
||||||
|
"tiles": [urlTemplate],
|
||||||
|
"minzoom": 0,
|
||||||
|
"maxzoom": 14,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"id": "background",
|
||||||
|
"type": "background",
|
||||||
|
"paint": {"background-color": "#f0ebe3"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "landuse",
|
||||||
|
"type": "fill",
|
||||||
|
"source": "osm_all",
|
||||||
|
"source-layer": "planet_osm_polygon",
|
||||||
|
"paint": {"fill-color": "#d4e5c9", "fill-opacity": 0.6}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "water",
|
||||||
|
"type": "fill",
|
||||||
|
"source": "osm_all",
|
||||||
|
"source-layer": "planet_osm_polygon",
|
||||||
|
"filter": ["==", "natural", "water"],
|
||||||
|
"paint": {"fill-color": "#a0c8f0"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "roads-minor",
|
||||||
|
"type": "line",
|
||||||
|
"source": "osm_all",
|
||||||
|
"source-layer": "planet_osm_line",
|
||||||
|
"paint": {"line-color": "#ccc", "line-width": 1}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "roads-main",
|
||||||
|
"type": "line",
|
||||||
|
"source": "osm_all",
|
||||||
|
"source-layer": "planet_osm_roads",
|
||||||
|
"paint": {"line-color": "#f5a623", "line-width": 2}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "buildings",
|
||||||
|
"type": "fill",
|
||||||
|
"source": "osm_all",
|
||||||
|
"source-layer": "planet_osm_polygon",
|
||||||
|
"filter": ["has", "building"],
|
||||||
|
"paint": {"fill-color": "#d9d0c7", "fill-outline-color": "#bbb"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "road-names",
|
||||||
|
"type": "symbol",
|
||||||
|
"source": "osm_all",
|
||||||
|
"source-layer": "planet_osm_line",
|
||||||
|
"minzoom": 13,
|
||||||
|
"filter": ["has", "name"],
|
||||||
|
"layout": {
|
||||||
|
"text-field": ["get", "name"],
|
||||||
|
"text-font": ["Noto Sans Regular", "Open Sans Regular"],
|
||||||
|
"text-size": 11,
|
||||||
|
"symbol-placement": "line",
|
||||||
|
"text-max-angle": 30,
|
||||||
|
"text-padding": 10
|
||||||
|
},
|
||||||
|
"paint": {
|
||||||
|
"text-color": "#444",
|
||||||
|
"text-halo-color": "#fff",
|
||||||
|
"text-halo-width": 1.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "place-names",
|
||||||
|
"type": "symbol",
|
||||||
|
"source": "osm_all",
|
||||||
|
"source-layer": "planet_osm_point",
|
||||||
|
"minzoom": 10,
|
||||||
|
"filter": [
|
||||||
|
"all",
|
||||||
|
["has", "name"],
|
||||||
|
["has", "place"]
|
||||||
|
],
|
||||||
|
"layout": {
|
||||||
|
"text-field": ["get", "name"],
|
||||||
|
"text-font": ["Noto Sans Regular", "Open Sans Regular"],
|
||||||
|
"text-size": [
|
||||||
|
"interpolate",
|
||||||
|
["linear"],
|
||||||
|
["zoom"],
|
||||||
|
10,
|
||||||
|
11,
|
||||||
|
14,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"text-anchor": "center",
|
||||||
|
"text-padding": 5
|
||||||
|
},
|
||||||
|
"paint": {
|
||||||
|
"text-color": "#333",
|
||||||
|
"text-halo-color": "#fff",
|
||||||
|
"text-halo-width": 1.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
final theme = ThemeReader().read(styleJson);
|
||||||
|
|
||||||
|
return Style(
|
||||||
|
theme: theme,
|
||||||
|
providers: providers,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -51,24 +51,30 @@ class SettingsScreen extends ConsumerStatefulWidget {
|
||||||
|
|
||||||
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
late final TextEditingController _backendUrlController;
|
late final TextEditingController _backendUrlController;
|
||||||
|
late final TextEditingController _martinUrlController;
|
||||||
bool _urlSaved = false;
|
bool _urlSaved = false;
|
||||||
|
bool _martinUrlSaved = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_backendUrlController = TextEditingController();
|
_backendUrlController = TextEditingController();
|
||||||
_loadBackendUrl();
|
_martinUrlController = TextEditingController();
|
||||||
|
_loadUrls();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadBackendUrl() async {
|
Future<void> _loadUrls() async {
|
||||||
final db = ref.read(appDatabaseProvider);
|
final db = ref.read(appDatabaseProvider);
|
||||||
final url = await db.getSetting(AppConstants.settingBackendUrl);
|
final url = await db.getSetting(AppConstants.settingBackendUrl);
|
||||||
_backendUrlController.text = url ?? AppConstants.defaultBackendUrl;
|
_backendUrlController.text = url ?? AppConstants.defaultBackendUrl;
|
||||||
|
final martinUrl = await db.getSetting(AppConstants.settingMartinUrl);
|
||||||
|
_martinUrlController.text = martinUrl ?? AppConstants.defaultMartinUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_backendUrlController.dispose();
|
_backendUrlController.dispose();
|
||||||
|
_martinUrlController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,6 +123,41 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
|
||||||
|
// Martin tile server URL
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Tile Server (Martin)',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: _martinUrlController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Martin URL',
|
||||||
|
hintText: AppConstants.defaultMartinUrl,
|
||||||
|
suffixIcon: _martinUrlSaved
|
||||||
|
? const Icon(Icons.check, color: Colors.green)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
onChanged: (_) {
|
||||||
|
if (_martinUrlSaved) setState(() => _martinUrlSaved = false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: _saveMartinUrl,
|
||||||
|
child: const Text('Save'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
|
||||||
// Theme
|
// Theme
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
|
@ -222,6 +263,24 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _saveMartinUrl() async {
|
||||||
|
final url = _martinUrlController.text.trim();
|
||||||
|
if (url.isEmpty) return;
|
||||||
|
|
||||||
|
final db = ref.read(appDatabaseProvider);
|
||||||
|
await db.setSetting(AppConstants.settingMartinUrl, url);
|
||||||
|
|
||||||
|
setState(() => _martinUrlSaved = true);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Martin URL saved. Restart the app to apply.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AttributionTile extends StatelessWidget {
|
class _AttributionTile extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'app/app.dart';
|
||||||
import 'core/api/api_client.dart';
|
import 'core/api/api_client.dart';
|
||||||
import 'core/constants.dart';
|
import 'core/constants.dart';
|
||||||
import 'core/database/app_database.dart';
|
import 'core/database/app_database.dart';
|
||||||
|
import 'features/map/providers/map_style_provider.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
@ -12,12 +13,15 @@ void main() async {
|
||||||
final db = AppDatabase();
|
final db = AppDatabase();
|
||||||
final savedUrl = await db.getSetting(AppConstants.settingBackendUrl);
|
final savedUrl = await db.getSetting(AppConstants.settingBackendUrl);
|
||||||
final backendUrl = savedUrl ?? AppConstants.defaultBackendUrl;
|
final backendUrl = savedUrl ?? AppConstants.defaultBackendUrl;
|
||||||
|
final savedMartinUrl = await db.getSetting(AppConstants.settingMartinUrl);
|
||||||
|
final martinUrl = savedMartinUrl ?? AppConstants.defaultMartinUrl;
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
appDatabaseProvider.overrideWithValue(db),
|
appDatabaseProvider.overrideWithValue(db),
|
||||||
apiClientProvider.overrideWithValue(ApiClient(baseUrl: backendUrl)),
|
apiClientProvider.overrideWithValue(ApiClient(baseUrl: backendUrl)),
|
||||||
|
martinUrlProvider.overrideWithValue(martinUrl),
|
||||||
],
|
],
|
||||||
child: const PrivacyMapsApp(),
|
child: const PrivacyMapsApp(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1062,7 +1062,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
vector_tile_renderer:
|
vector_tile_renderer:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: vector_tile_renderer
|
name: vector_tile_renderer
|
||||||
sha256: "89746f1108eccbc0b6f33fbbef3fcf394cda3733fc0d5064ea03d53a459b56d3"
|
sha256: "89746f1108eccbc0b6f33fbbef3fcf394cda3733fc0d5064ea03d53a459b56d3"
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_map: ^7.0.0
|
flutter_map: ^7.0.0
|
||||||
vector_map_tiles: ^8.0.0
|
vector_map_tiles: ^8.0.0
|
||||||
|
vector_tile_renderer: ^5.2.0
|
||||||
flutter_riverpod: ^3.0.0
|
flutter_riverpod: ^3.0.0
|
||||||
riverpod_annotation: ^4.0.0
|
riverpod_annotation: ^4.0.0
|
||||||
dio: ^5.4.0
|
dio: ^5.4.0
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue