mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
Add bing sat imagery
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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+
|
||||
|
||||
124
test/models/tile_provider_test.dart
Normal file
124
test/models/tile_provider_test.dart
Normal 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user