Narrow scope to radar, and put the app on a real OpenStreetMap base map
Drops forecasts, lightning, the home-screen widget and advertising. What remains is radar on a map, the official ARPA alert bulletin, and rain notifications. The base map is now OpenFreeMap's Positron style: real OpenStreetMap vector tiles with no API key, no registration, no request limits and commercial use permitted. Every other free tier — MapTiler, Stadia, Jawg, Thunderforest — needs a key, which is a secret to manage, a quota to outgrow and a signup to complete before anyone can build the project, and the map is the one thing the app cannot work without. Positron rather than Liberty or Bright because the radar overlay has to be the loudest thing on screen, and a desaturated grey base is built to sit under data. Its style JSON carries no `attribution` field, so MapLibre displays no credits by itself. The app renders them from the region config instead: the two mandatory credits, OpenStreetMap and OpenMapTiles, go in the always-visible bar, and OpenFreeMap's own credit — optional by their terms — is listed on the Sources screen with the rest. The bundled offline style is still reachable with MAP_STYLE_URL=offline, and still claims no base map attribution, because crediting OpenStreetMap while showing it would be a false claim. Radar-DPC stays the source. ARPA Piemonte's own radar remains a disabled stub for two reasons that belong to the project owner, not to the code: the real-time access link is only issued by email, and the open-data page states the data is "gratuiti" and nothing else. Free of charge is not a licence, and rendering those volumes into frames served from a CDN is redistribution. Both questions go in the same email. An earlier draft of the docs recorded ARPA radar as CC BY 4.0; the source page does not support that, so the claim is removed rather than carried forward. The documentation is updated throughout rather than annotated: CLAUDE.md gains an explicit scope boundary, data-sources drops MET Norway and ISTAT and gains the base map, licenses records that free of charge is not a licence, privacy loses the whole advertising section, and the roadmap is renumbered so the backend worker is next — until it exists, DpcRadarSource has nothing to read. licenses.md keeps Open-Meteo and Blitzortung listed as excluded even though the features that would have used them are gone: both are non-commercial-only, ads are a plausible future, and neither should be adopted on the grounds that there are none today. Verified: analyze clean, 130 tests passing, and on the emulator the radar overlay sits correctly over Piedmont on real OSM tiles with Turin, Milan and Genoa labelled, the age reads "Aggiornato 4 minuti fa", and the Sources screen lists all five credits with their licences. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
/// Build-time configuration, supplied with `--dart-define-from-file=env.json`.
|
||||
///
|
||||
/// Every value has a working default, so the app builds and runs with no
|
||||
/// `env.json` at all: it starts in demo mode against bundled mock frames, with
|
||||
/// the offline map style and Google's test ad units. That keeps the repository
|
||||
/// free of secrets and keeps a fresh clone runnable.
|
||||
/// `env.json` at all: it starts in demo mode against bundled mock frames, on
|
||||
/// the free OpenFreeMap base map, with no credentials anywhere. That keeps the
|
||||
/// repository free of secrets and keeps a fresh clone runnable.
|
||||
///
|
||||
/// Values are `const` reads of `String.fromEnvironment` so they are inlined and
|
||||
/// tree-shaken; they cannot be read from a file at run time.
|
||||
@@ -20,12 +20,18 @@ class Env {
|
||||
|
||||
/// MapLibre style URL for the base map.
|
||||
///
|
||||
/// Empty means no key is configured, and [hasMapStyle] is false: the app
|
||||
/// falls back to a minimal style bundled in assets so development and tests
|
||||
/// work offline.
|
||||
/// Empty means the default OpenFreeMap style, which needs no key. Set it to
|
||||
/// point at another provider, or to [offlineStyle] to force the bundled
|
||||
/// no-network style.
|
||||
static const String mapStyleUrl = String.fromEnvironment('MAP_STYLE_URL');
|
||||
|
||||
static bool get hasMapStyle => mapStyleUrl.isNotEmpty;
|
||||
/// Sentinel for `MAP_STYLE_URL` that selects the bundled offline style.
|
||||
static const String offlineStyle = 'offline';
|
||||
|
||||
static bool get wantsOfflineStyle => mapStyleUrl == offlineStyle;
|
||||
|
||||
static bool get hasCustomMapStyle =>
|
||||
mapStyleUrl.isNotEmpty && !wantsOfflineStyle;
|
||||
|
||||
/// Base URL of the radar `manifest.json` published by our worker.
|
||||
///
|
||||
@@ -39,27 +45,8 @@ class Env {
|
||||
/// Empty means "use whatever the region config says".
|
||||
static const String radarSource = String.fromEnvironment('RADAR_SOURCE');
|
||||
|
||||
/// Contact address embedded in the MET Norway User-Agent.
|
||||
///
|
||||
/// MET Norway blocks generic User-Agent strings, so forecasts are disabled
|
||||
/// rather than attempted when this is empty — see [hasForecastContact].
|
||||
static const String metnoUserAgentContact = String.fromEnvironment(
|
||||
'METNO_USER_AGENT_CONTACT',
|
||||
);
|
||||
|
||||
static bool get hasForecastContact => metnoUserAgentContact.isNotEmpty;
|
||||
|
||||
/// AdMob identifiers. Empty means Google's test units are used.
|
||||
static const String admobAppId = String.fromEnvironment('ADMOB_APP_ID');
|
||||
static const String admobBannerUnitId = String.fromEnvironment(
|
||||
'ADMOB_BANNER_UNIT_ID',
|
||||
);
|
||||
|
||||
static bool get hasAdMobConfig =>
|
||||
admobAppId.isNotEmpty && admobBannerUnitId.isNotEmpty;
|
||||
|
||||
/// True when nothing is configured and the app is running entirely on
|
||||
/// True when nothing is configured and the radar is running entirely on
|
||||
/// bundled data. Surfaced in the UI so demo frames are never mistaken for a
|
||||
/// real forecast.
|
||||
/// real observation.
|
||||
static bool get isDemoMode => radarManifestUrl.isEmpty;
|
||||
}
|
||||
|
||||
@@ -23,20 +23,6 @@ enum RadarAdapter {
|
||||
);
|
||||
}
|
||||
|
||||
enum ForecastAdapter {
|
||||
/// MET Norway locationforecast.
|
||||
metno,
|
||||
|
||||
/// ItaliaMeteo ICON-2I via MeteoHub.
|
||||
iconIt2;
|
||||
|
||||
static ForecastAdapter parse(String value) =>
|
||||
ForecastAdapter.values.firstWhere(
|
||||
(adapter) => adapter.name == value,
|
||||
orElse: () => throw FormatException('unknown forecast adapter: $value'),
|
||||
);
|
||||
}
|
||||
|
||||
enum AlertAdapter {
|
||||
/// ARPA Piemonte XML-CAP bulletin, fetched through our backend.
|
||||
arpaCap;
|
||||
@@ -143,36 +129,6 @@ class RadarSourceConfig {
|
||||
final Duration frameInterval;
|
||||
}
|
||||
|
||||
class ForecastSourceConfig {
|
||||
const ForecastSourceConfig({
|
||||
required this.defaultAdapter,
|
||||
required this.availableAdapters,
|
||||
});
|
||||
|
||||
factory ForecastSourceConfig.fromJson(Map<String, Object?> json) {
|
||||
final available = _requireStringList(
|
||||
json,
|
||||
'availableAdapters',
|
||||
).map(ForecastAdapter.parse).toList(growable: false);
|
||||
final defaultAdapter = ForecastAdapter.parse(
|
||||
_requireString(json, 'defaultAdapter'),
|
||||
);
|
||||
if (!available.contains(defaultAdapter)) {
|
||||
throw FormatException(
|
||||
'forecast defaultAdapter ${defaultAdapter.name} is not in '
|
||||
'availableAdapters',
|
||||
);
|
||||
}
|
||||
return ForecastSourceConfig(
|
||||
defaultAdapter: defaultAdapter,
|
||||
availableAdapters: available,
|
||||
);
|
||||
}
|
||||
|
||||
final ForecastAdapter defaultAdapter;
|
||||
final List<ForecastAdapter> availableAdapters;
|
||||
}
|
||||
|
||||
class AlertSourceConfig {
|
||||
const AlertSourceConfig({
|
||||
required this.defaultAdapter,
|
||||
@@ -274,7 +230,6 @@ class RegionConfig {
|
||||
required this.bounds,
|
||||
required this.map,
|
||||
required this.radar,
|
||||
required this.forecast,
|
||||
required this.alerts,
|
||||
required this.alertZones,
|
||||
required this.attributions,
|
||||
@@ -328,7 +283,6 @@ class RegionConfig {
|
||||
bounds: bounds,
|
||||
map: map,
|
||||
radar: RadarSourceConfig.fromJson(_requireMap(sources, 'radar')),
|
||||
forecast: ForecastSourceConfig.fromJson(_requireMap(sources, 'forecast')),
|
||||
alerts: AlertSourceConfig.fromJson(_requireMap(sources, 'alerts')),
|
||||
alertZones: zones,
|
||||
attributions: attributions,
|
||||
@@ -347,13 +301,12 @@ class RegionConfig {
|
||||
final String id;
|
||||
final String displayName;
|
||||
|
||||
/// IANA time zone the region's forecasts and alerts are expressed in.
|
||||
/// IANA time zone the region's alert bulletins are expressed in.
|
||||
final String timeZone;
|
||||
|
||||
final GeoBounds bounds;
|
||||
final MapConfig map;
|
||||
final RadarSourceConfig radar;
|
||||
final ForecastSourceConfig forecast;
|
||||
final AlertSourceConfig alerts;
|
||||
final List<AlertZone> alertZones;
|
||||
final List<Attribution> attributions;
|
||||
|
||||
Reference in New Issue
Block a user