Add bing sat imagery

This commit is contained in:
stopflock
2025-11-22 22:00:56 -06:00
parent c150e3ccee
commit 52af77e1ed
5 changed files with 175 additions and 5 deletions

View File

@@ -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
- **Upload to OpenStreetMap** with OAuth2 integration (live or sandbox modes)
- **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
- **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
### 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
- **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)
@@ -110,7 +110,6 @@ cp lib/keys.dart.example lib/keys.dart
- Delete the old one (also wrong answer unless user chooses intentionally)
- Give multiple of these options??
- 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
- 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

View File

@@ -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": {
"content": [
"• NEW: First-submission guide popup - provides essential guidance and links before your first device submission",

View File

@@ -21,7 +21,22 @@ class TileType {
/// Create URL for a specific tile, replacing template variables
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('{x}', x.toString())
.replaceAll('{y}', y.toString());
@@ -33,6 +48,19 @@ class TileType {
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
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(
id: 'mapbox',
name: 'Mapbox',

View File

@@ -1,7 +1,7 @@
name: deflockapp
description: Map public surveillance infrastructure with OpenStreetMap
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:
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+

View File

@@ -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');
}
});
});
}