mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-09-30 21:19:42 +02:00
Add bing sat imagery
This commit is contained in:
1 parent
c150e3ccee
commit
52af77e1ed
5 files changed
+175
-5
No files matched your search
@@ -21,7 +21,7 @@ A comprehensive Flutter app for mapping public surveillance infrastructure with
|
|||||||
- **Map surveillance infrastructure** including cameras, ALPRs, gunshot detectors, and more with precise location, direction, and manufacturer details
|
- **Map surveillance infrastructure** including cameras, ALPRs, gunshot detectors, and more with precise location, direction, and manufacturer details
|
||||||
- **Upload to OpenStreetMap** with OAuth2 integration (live or sandbox modes)
|
- **Upload to OpenStreetMap** with OAuth2 integration (live or sandbox modes)
|
||||||
- **Work completely offline** with downloadable map areas and device data, plus upload queue
|
- **Work completely offline** with downloadable map areas and device data, plus upload queue
|
||||||
- **Multiple map types** including satellite imagery from USGS, Esri, Mapbox, and topographic maps from OpenTopoMap, plus custom map tile provider support
|
- **Multiple map types** including satellite imagery from Bing Maps, USGS, Esri, Mapbox, and topographic maps from OpenTopoMap, plus custom map tile provider support
|
||||||
- **Editing Ability** to update existing device locations and properties
|
- **Editing Ability** to update existing device locations and properties
|
||||||
- **Built-in device profiles** for Flock Safety, Motorola, Genetec, Leonardo, and other major manufacturers, plus custom profiles for more specific tag sets
|
- **Built-in device profiles** for Flock Safety, Motorola, Genetec, Leonardo, and other major manufacturers, plus custom profiles for more specific tag sets
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ A comprehensive Flutter app for mapping public surveillance infrastructure with
|
|||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
### Map & Navigation
|
### Map & Navigation
|
||||||
- **Multi-source tiles**: Switch between OpenStreetMap, USGS imagery, Esri imagery, Mapbox, OpenTopoMap, and any custom providers
|
- **Multi-source tiles**: Switch between OpenStreetMap, Bing satellite imagery, USGS imagery, Esri imagery, Mapbox, OpenTopoMap, and any custom providers
|
||||||
- **Offline-first design**: Download a region for complete offline operation
|
- **Offline-first design**: Download a region for complete offline operation
|
||||||
- **Smooth UX**: Intuitive controls, follow-me mode with GPS rotation, compass indicator with north-lock, and gesture-friendly interactions
|
- **Smooth UX**: Intuitive controls, follow-me mode with GPS rotation, compass indicator with north-lock, and gesture-friendly interactions
|
||||||
- **Device visualization**: Color-coded markers showing real devices (blue), pending uploads (purple), pending edits (grey), devices being edited (orange), and pending deletions (red)
|
- **Device visualization**: Color-coded markers showing real devices (blue), pending uploads (purple), pending edits (grey), devices being edited (orange), and pending deletions (red)
|
||||||
@@ -110,7 +110,6 @@ cp lib/keys.dart.example lib/keys.dart
|
|||||||
- Delete the old one (also wrong answer unless user chooses intentionally)
|
- Delete the old one (also wrong answer unless user chooses intentionally)
|
||||||
- Give multiple of these options??
|
- Give multiple of these options??
|
||||||
- Nav start+end too close together error (warning + disable submit button?)
|
- Nav start+end too close together error (warning + disable submit button?)
|
||||||
- Add some builtin satellite tile provider
|
|
||||||
- Persistent cache for MY submissions: assume submissions worked, cache,clean up when we see that node appear in overpass/OSM results or when older than 24h
|
- Persistent cache for MY submissions: assume submissions worked, cache,clean up when we see that node appear in overpass/OSM results or when older than 24h
|
||||||
- Dropdown on "refine tags" page to select acceptable options for camera:mount= (is this a boolean property of a profile?)
|
- Dropdown on "refine tags" page to select acceptable options for camera:mount= (is this a boolean property of a profile?)
|
||||||
- Tutorial / info guide before submitting first node, info and links before creating first profile
|
- Tutorial / info guide before submitting first node, info and links before creating first profile
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
|
"1.5.1": {
|
||||||
|
"content": [
|
||||||
|
"• NEW: Bing satellite imagery - high-quality satellite tiles used by the iD editor, no API key required",
|
||||||
|
"• IMPROVED: Added support for quadkey tile formats and multi-subdomain providers"
|
||||||
|
]
|
||||||
|
},
|
||||||
"1.5.0": {
|
"1.5.0": {
|
||||||
"content": [
|
"content": [
|
||||||
"• NEW: First-submission guide popup - provides essential guidance and links before your first device submission",
|
"• NEW: First-submission guide popup - provides essential guidance and links before your first device submission",
|
||||||
|
|||||||
@@ -21,7 +21,22 @@ class TileType {
|
|||||||
|
|
||||||
/// Create URL for a specific tile, replacing template variables
|
/// Create URL for a specific tile, replacing template variables
|
||||||
String getTileUrl(int z, int x, int y, {String? apiKey}) {
|
String getTileUrl(int z, int x, int y, {String? apiKey}) {
|
||||||
String url = urlTemplate
|
String url = urlTemplate;
|
||||||
|
|
||||||
|
// Handle Bing Maps quadkey conversion
|
||||||
|
if (url.contains('{quadkey}')) {
|
||||||
|
final quadkey = _convertToQuadkey(x, y, z);
|
||||||
|
url = url.replaceAll('{quadkey}', quadkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle subdomains (for Bing Maps and other multi-subdomain providers)
|
||||||
|
if (url.contains('{subdomain}')) {
|
||||||
|
final subdomain = (x + y) % 4; // Distribute across 0-3 subdomains
|
||||||
|
url = url.replaceAll('{subdomain}', subdomain.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard x/y/z replacement
|
||||||
|
url = url
|
||||||
.replaceAll('{z}', z.toString())
|
.replaceAll('{z}', z.toString())
|
||||||
.replaceAll('{x}', x.toString())
|
.replaceAll('{x}', x.toString())
|
||||||
.replaceAll('{y}', y.toString());
|
.replaceAll('{y}', y.toString());
|
||||||
@@ -33,6 +48,19 @@ class TileType {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert x, y, z to Bing Maps quadkey format
|
||||||
|
String _convertToQuadkey(int x, int y, int z) {
|
||||||
|
final quadkey = StringBuffer();
|
||||||
|
for (int i = z; i > 0; i--) {
|
||||||
|
int digit = 0;
|
||||||
|
final mask = 1 << (i - 1);
|
||||||
|
if ((x & mask) != 0) digit++;
|
||||||
|
if ((y & mask) != 0) digit += 2;
|
||||||
|
quadkey.write(digit);
|
||||||
|
}
|
||||||
|
return quadkey.toString();
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if this tile type needs an API key
|
/// Check if this tile type needs an API key
|
||||||
bool get requiresApiKey => urlTemplate.contains('{api_key}');
|
bool get requiresApiKey => urlTemplate.contains('{api_key}');
|
||||||
|
|
||||||
@@ -161,6 +189,19 @@ class DefaultTileProviders {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
TileProvider(
|
||||||
|
id: 'bing',
|
||||||
|
name: 'Bing Maps',
|
||||||
|
tileTypes: [
|
||||||
|
TileType(
|
||||||
|
id: 'bing_satellite',
|
||||||
|
name: 'Satellite',
|
||||||
|
urlTemplate: 'https://ecn.t{subdomain}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=1&n=z',
|
||||||
|
attribution: '© Microsoft Corporation',
|
||||||
|
maxZoom: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
TileProvider(
|
TileProvider(
|
||||||
id: 'mapbox',
|
id: 'mapbox',
|
||||||
name: 'Mapbox',
|
name: 'Mapbox',
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: deflockapp
|
name: deflockapp
|
||||||
description: Map public surveillance infrastructure with OpenStreetMap
|
description: Map public surveillance infrastructure with OpenStreetMap
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.5.0+18 # The thing after the + is the version code, incremented with each release
|
version: 1.5.1+19 # The thing after the + is the version code, incremented with each release
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+
|
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flock_map_app/models/tile_provider.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('TileType', () {
|
||||||
|
test('getTileUrl handles standard x/y/z replacement', () {
|
||||||
|
final tileType = TileType(
|
||||||
|
id: 'test',
|
||||||
|
name: 'Test',
|
||||||
|
urlTemplate: 'https://example.com/{z}/{x}/{y}.png',
|
||||||
|
attribution: 'Test',
|
||||||
|
);
|
||||||
|
|
||||||
|
final url = tileType.getTileUrl(3, 2, 1);
|
||||||
|
expect(url, 'https://example.com/3/2/1.png');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getTileUrl handles subdomain replacement', () {
|
||||||
|
final tileType = TileType(
|
||||||
|
id: 'test',
|
||||||
|
name: 'Test',
|
||||||
|
urlTemplate: 'https://a{subdomain}.example.com/{z}/{x}/{y}.png',
|
||||||
|
attribution: 'Test',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test subdomain distribution (should be consistent)
|
||||||
|
final url1 = tileType.getTileUrl(1, 2, 3);
|
||||||
|
final url2 = tileType.getTileUrl(1, 2, 3);
|
||||||
|
expect(url1, url2); // Same input should give same output
|
||||||
|
|
||||||
|
// Test that different tiles can get different subdomains
|
||||||
|
final url3 = tileType.getTileUrl(1, 0, 0);
|
||||||
|
final url4 = tileType.getTileUrl(1, 1, 1);
|
||||||
|
expect(url3, contains('a0.example.com'));
|
||||||
|
expect(url4, contains('a2.example.com'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getTileUrl handles Bing Maps quadkey conversion', () {
|
||||||
|
final tileType = TileType(
|
||||||
|
id: 'bing_test',
|
||||||
|
name: 'Bing Test',
|
||||||
|
urlTemplate: 'https://ecn.t{subdomain}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=1&n=z',
|
||||||
|
attribution: 'Microsoft',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test some known quadkey conversions
|
||||||
|
// x=0, y=0, z=1 should give quadkey "0"
|
||||||
|
final url1 = tileType.getTileUrl(1, 0, 0);
|
||||||
|
expect(url1, contains('a0.jpeg'));
|
||||||
|
|
||||||
|
// x=1, y=0, z=1 should give quadkey "1"
|
||||||
|
final url2 = tileType.getTileUrl(1, 1, 0);
|
||||||
|
expect(url2, contains('a1.jpeg'));
|
||||||
|
|
||||||
|
// x=0, y=1, z=1 should give quadkey "2"
|
||||||
|
final url3 = tileType.getTileUrl(1, 0, 1);
|
||||||
|
expect(url3, contains('a2.jpeg'));
|
||||||
|
|
||||||
|
// x=1, y=1, z=1 should give quadkey "3"
|
||||||
|
final url4 = tileType.getTileUrl(1, 1, 1);
|
||||||
|
expect(url4, contains('a3.jpeg'));
|
||||||
|
|
||||||
|
// More complex example: x=3, y=5, z=3 should give quadkey "213"
|
||||||
|
final url5 = tileType.getTileUrl(3, 3, 5);
|
||||||
|
expect(url5, contains('a213.jpeg'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getTileUrl handles API key replacement', () {
|
||||||
|
final tileType = TileType(
|
||||||
|
id: 'test',
|
||||||
|
name: 'Test',
|
||||||
|
urlTemplate: 'https://api.example.com/{z}/{x}/{y}?key={api_key}',
|
||||||
|
attribution: 'Test',
|
||||||
|
);
|
||||||
|
|
||||||
|
final url = tileType.getTileUrl(1, 2, 3, apiKey: 'mykey123');
|
||||||
|
expect(url, 'https://api.example.com/1/2/3?key=mykey123');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requiresApiKey detects API key requirement correctly', () {
|
||||||
|
final tileTypeWithKey = TileType(
|
||||||
|
id: 'test1',
|
||||||
|
name: 'Test 1',
|
||||||
|
urlTemplate: 'https://api.example.com/{z}/{x}/{y}?key={api_key}',
|
||||||
|
attribution: 'Test',
|
||||||
|
);
|
||||||
|
|
||||||
|
final tileTypeWithoutKey = TileType(
|
||||||
|
id: 'test2',
|
||||||
|
name: 'Test 2',
|
||||||
|
urlTemplate: 'https://example.com/{z}/{x}/{y}.png',
|
||||||
|
attribution: 'Test',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tileTypeWithKey.requiresApiKey, isTrue);
|
||||||
|
expect(tileTypeWithoutKey.requiresApiKey, isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('DefaultTileProviders', () {
|
||||||
|
test('contains Bing satellite provider', () {
|
||||||
|
final providers = DefaultTileProviders.createDefaults();
|
||||||
|
final bingProvider = providers.firstWhere((p) => p.id == 'bing');
|
||||||
|
|
||||||
|
expect(bingProvider.name, 'Bing Maps');
|
||||||
|
expect(bingProvider.tileTypes, hasLength(1));
|
||||||
|
|
||||||
|
final satelliteType = bingProvider.tileTypes.first;
|
||||||
|
expect(satelliteType.id, 'bing_satellite');
|
||||||
|
expect(satelliteType.name, 'Satellite');
|
||||||
|
expect(satelliteType.urlTemplate, contains('quadkey'));
|
||||||
|
expect(satelliteType.urlTemplate, contains('subdomain'));
|
||||||
|
expect(satelliteType.requiresApiKey, isFalse);
|
||||||
|
expect(satelliteType.attribution, '© Microsoft Corporation');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('all default providers are usable', () {
|
||||||
|
final providers = DefaultTileProviders.createDefaults();
|
||||||
|
for (final provider in providers) {
|
||||||
|
expect(provider.isUsable, isTrue, reason: '${provider.name} should be usable');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user