new change to use intaleq_map sdk v2 04-17-1
This commit is contained in:
@@ -1 +1 @@
|
||||
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"geolocator_apple","path":"/Users/hamzaaleghwairyeen/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"maplibre_gl","path":"/Users/hamzaaleghwairyeen/.pub-cache/hosted/pub.dev/maplibre_gl-0.19.0+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"geolocator_android","path":"/Users/hamzaaleghwairyeen/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"maplibre_gl","path":"/Users/hamzaaleghwairyeen/.pub-cache/hosted/pub.dev/maplibre_gl-0.19.0+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"geolocator_apple","path":"/Users/hamzaaleghwairyeen/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[],"windows":[{"name":"geolocator_windows","path":"/Users/hamzaaleghwairyeen/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"geolocator_web","path":"/Users/hamzaaleghwairyeen/.pub-cache/hosted/pub.dev/geolocator_web-2.2.1/","dependencies":[],"dev_dependency":false},{"name":"maplibre_gl_web","path":"/Users/hamzaaleghwairyeen/.pub-cache/hosted/pub.dev/maplibre_gl_web-0.19.0+2/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"maplibre_gl","dependencies":["maplibre_gl_web"]},{"name":"maplibre_gl_web","dependencies":[]}],"date_created":"2026-04-16 16:50:12.143766","version":"3.32.8","swift_package_manager_enabled":{"ios":false,"macos":false}}
|
||||
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"maplibre_gl","path":"/Users/hamzaaleghwairyeen/.pub-cache/hosted/pub.dev/maplibre_gl-0.19.0+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"maplibre_gl","path":"/Users/hamzaaleghwairyeen/.pub-cache/hosted/pub.dev/maplibre_gl-0.19.0+2/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[],"linux":[],"windows":[],"web":[{"name":"maplibre_gl_web","path":"/Users/hamzaaleghwairyeen/.pub-cache/hosted/pub.dev/maplibre_gl_web-0.19.0+2/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"maplibre_gl","dependencies":["maplibre_gl_web"]},{"name":"maplibre_gl_web","dependencies":[]}],"date_created":"2026-04-17 14:04:31.920711","version":"3.41.2","swift_package_manager_enabled":{"ios":false,"macos":false}}
|
||||
@@ -1,7 +1,23 @@
|
||||
## 2.0.0
|
||||
|
||||
**Breaking redesign — full Google Maps Flutter API parity.**
|
||||
|
||||
* Replaced imperative `add/removeMarker()` controller methods with a
|
||||
**declarative `Set<Marker>` / `Set<Polyline>` / `Set<Circle>` / `Set<Polygon>`**
|
||||
API on the widget, identical to `google_maps_flutter`.
|
||||
* Added `MarkerId`, `PolylineId`, `CircleId`, `PolygonId` typed ID classes.
|
||||
* Added per-object callbacks: `Marker.onTap`, `Marker.onDragEnd`, `Polyline.onTap`, etc.
|
||||
* Added `InlqBitmap` (mirrors `BitmapDescriptor`): `defaultMarker`,
|
||||
`defaultMarkerWithHue`, `fromAsset`, `fromBytes`, `fromStyleImage`.
|
||||
* Added `InfoWindow` model with `title`, `snippet`, and `onTap`.
|
||||
* Added `IntaleqMapType` enum (`normal`, `light`, `satellite`, `none`).
|
||||
* Added `MinMaxZoomPreference` and `CameraTargetBounds`.
|
||||
* Renamed `getRoute()` → `getDirections()` for API clarity.
|
||||
* Added `reverseGeocode()` to controller.
|
||||
* Added `PolylineUtils.encode()` and `PolylineUtils.totalLength()`.
|
||||
* Added `IntaleqColors` Flutter `Color` constants alongside hex strings.
|
||||
* Removed `geolocator` and `get`/`latlong2` dependencies (no longer needed).
|
||||
|
||||
## 1.0.0
|
||||
|
||||
* Initial release of the Intaleq Maps Foundation SDK.
|
||||
* Support for Markers, Polylines, Circles, and Polygons (with holes).
|
||||
* Integrated Intaleq Routing and Geocoding APIs.
|
||||
* Built-in premium map styles (Obsidian, Light, Satellite).
|
||||
* Integrated Polyline decoding utility.
|
||||
* Initial release. Imperative add/remove API over MapLibre GL.
|
||||
+154
-26
@@ -1,59 +1,187 @@
|
||||
# Intaleq Maps SDK for Flutter
|
||||
|
||||
A premium Flutter SDK for the Intaleq Map Platform, optimized for Jordan, Syria, and the broader region. This SDK provides a simplified, developer-friendly interface over MapLibre GL, with built-in support for Intaleq's Routing and Geocoding APIs.
|
||||
A **drop-in replacement for `google_maps_flutter`** backed by MapLibre GL, optimized for Jordan, Syria, and the broader MENA region.
|
||||
|
||||
## Features
|
||||
---
|
||||
|
||||
- **Simplified Geometry**: Use `Marker`, `Polyline`, `Circle`, and `Polygon` (with hole support) instead of complex low-level types.
|
||||
- **Intaleq Routing**: Integrated multi-profile routing API.
|
||||
- **Intaleq Geocoding**: High-performance place search and reverse geocoding.
|
||||
- **Branding-Ready**: Built-in Intaleq branding colors and premium map styles (Obsidian, Light, Satellite).
|
||||
- **Standalone**: No external dependencies required for route decoding.
|
||||
## Why Intaleq Maps?
|
||||
|
||||
## Getting Started
|
||||
| Feature | `google_maps_flutter` | `intaleq_maps` |
|
||||
|---|---|---|
|
||||
| Works in Syria (sanctions) | ❌ | ✅ |
|
||||
| Self-hosted tiles | ❌ | ✅ |
|
||||
| MENA-optimised data | ❌ | ✅ |
|
||||
| Same Flutter API | — | ✅ |
|
||||
| Offline-ready styles | ❌ | ✅ |
|
||||
|
||||
Add the following to your `pubspec.yaml`:
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
intaleq_maps: ^1.0.0
|
||||
intaleq_maps: ^2.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration from `google_maps_flutter`
|
||||
|
||||
The API is intentionally identical. Most migrations are a find-and-replace:
|
||||
|
||||
| Google | Intaleq |
|
||||
|---|---|
|
||||
| `GoogleMap(…)` | `IntaleqMap(apiKey: key, …)` |
|
||||
| `GoogleMapController` | `IntaleqMapController` |
|
||||
| `BitmapDescriptor` | `InlqBitmap` |
|
||||
| `MapType.normal` | `IntaleqMapType.normal` |
|
||||
|
||||
Everything else — `Marker`, `Polyline`, `Circle`, `Polygon`, `MarkerId`, `PolylineId`, `InfoWindow`, `CameraUpdate`, `LatLng`, `LatLngBounds` — is **identical**.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Initialize the Map
|
||||
### Basic map
|
||||
|
||||
```dart
|
||||
IntaleqMap(
|
||||
apiKey: "YOUR_INTALEQ_API_KEY",
|
||||
onMapCreated: (controller) {
|
||||
// Access the controller here
|
||||
apiKey: 'YOUR_INTALEQ_API_KEY',
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: LatLng(33.5138, 36.2765), // Damascus
|
||||
zoom: 14,
|
||||
),
|
||||
onMapCreated: (IntaleqMapController controller) {
|
||||
_controller = controller;
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Add a Marker
|
||||
### Declarative markers (exactly like Google Maps Flutter)
|
||||
|
||||
```dart
|
||||
await controller.addMarker(MarkerOptions(
|
||||
position: LatLng(33.5138, 36.2765),
|
||||
iconImage: "marker-icon",
|
||||
));
|
||||
// In your State:
|
||||
Set<Marker> _markers = {};
|
||||
|
||||
// Add a marker — just setState:
|
||||
setState(() {
|
||||
_markers = {
|
||||
Marker(
|
||||
markerId: MarkerId('driver_1'),
|
||||
position: LatLng(33.5138, 36.2765),
|
||||
icon: InlqBitmap.fromAsset('assets/icons/car.png'),
|
||||
infoWindow: InfoWindow(
|
||||
title: 'Ahmad',
|
||||
snippet: 'Toyota Corolla · 3 min away',
|
||||
),
|
||||
onTap: () => print('driver tapped'),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Pass to widget:
|
||||
IntaleqMap(
|
||||
apiKey: _apiKey,
|
||||
initialCameraPosition: _camera,
|
||||
markers: _markers, // ← declarative
|
||||
polylines: _polylines, // ← declarative
|
||||
circles: _circles, // ← declarative
|
||||
polygons: _polygons, // ← declarative
|
||||
)
|
||||
```
|
||||
|
||||
### Get a Route
|
||||
### Camera control
|
||||
|
||||
```dart
|
||||
final routeData = await controller.getRoute(
|
||||
LatLng(33.5138, 36.2765),
|
||||
LatLng(33.5, 36.3),
|
||||
// Animate to a position
|
||||
_controller.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(LatLng(33.5, 36.2), 15),
|
||||
);
|
||||
|
||||
// Fit bounds
|
||||
_controller.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(bounds, left: 50, right: 50, top: 100, bottom: 100),
|
||||
);
|
||||
```
|
||||
|
||||
## Documentation
|
||||
### Get a route
|
||||
|
||||
For full documentation and API reference, visit the [Intaleq Documentation](https://map-dashbord.intaleqapp.com/dashboard.html#docs).
|
||||
```dart
|
||||
final route = await _controller.getDirections(
|
||||
LatLng(33.5138, 36.2765),
|
||||
LatLng(33.49, 36.30),
|
||||
profile: 'driving',
|
||||
);
|
||||
|
||||
// Decode the encoded polyline from the response
|
||||
final points = PolylineUtils.decode(route['geometry']);
|
||||
|
||||
setState(() {
|
||||
_polylines = {
|
||||
Polyline(
|
||||
polylineId: PolylineId('route'),
|
||||
points: points,
|
||||
color: IntaleqColors.routeCyan,
|
||||
width: 5,
|
||||
),
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Search for places
|
||||
|
||||
```dart
|
||||
final results = await _controller.searchPlaces('مطار دمشق');
|
||||
```
|
||||
|
||||
### Map types
|
||||
|
||||
```dart
|
||||
IntaleqMap(
|
||||
mapType: IntaleqMapType.normal, // Dark Obsidian (default)
|
||||
// mapType: IntaleqMapType.light,
|
||||
// mapType: IntaleqMapType.satellite,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### `IntaleqMap` widget
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `apiKey` | `String` | required | Your Intaleq Maps API key |
|
||||
| `initialCameraPosition` | `CameraPosition` | required | Starting camera |
|
||||
| `markers` | `Set<Marker>` | `{}` | Declarative marker set |
|
||||
| `polylines` | `Set<Polyline>` | `{}` | Declarative polyline set |
|
||||
| `circles` | `Set<Circle>` | `{}` | Declarative circle set |
|
||||
| `polygons` | `Set<Polygon>` | `{}` | Declarative polygon set |
|
||||
| `onMapCreated` | `MapCreatedCallback?` | — | Fires once map is ready |
|
||||
| `onTap` | `ArgumentCallback<LatLng>?` | — | Map tap |
|
||||
| `onLongPress` | `ArgumentCallback<LatLng>?` | — | Map long press |
|
||||
| `onCameraMove` | `CameraPositionCallback?` | — | Camera movement |
|
||||
| `mapType` | `IntaleqMapType` | `.normal` | Tile style |
|
||||
| `myLocationEnabled` | `bool` | `false` | Show user location dot |
|
||||
|
||||
### `IntaleqMapController`
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `animateCamera(update)` | Animated camera move |
|
||||
| `moveCamera(update)` | Instant camera move |
|
||||
| `getZoomLevel()` | Current zoom level |
|
||||
| `getVisibleRegion()` | Current `LatLngBounds` |
|
||||
| `clearOverlays()` | Remove all markers/lines/circles/polygons |
|
||||
| `getDirections(origin, dest)` | Intaleq Routing API |
|
||||
| `searchPlaces(query)` | Intaleq Geocoding API |
|
||||
| `reverseGeocode(position)` | Reverse geocoding |
|
||||
| `addImage(id, bytes)` | Register custom marker image |
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
Vendored
BIN
Binary file not shown.
@@ -1,10 +1,22 @@
|
||||
/// Intaleq Maps SDK for Flutter.
|
||||
///
|
||||
/// A Google Maps Flutter-compatible API backed by MapLibre GL,
|
||||
/// optimized for Jordan, Syria, and the broader MENA region.
|
||||
library intaleq_maps;
|
||||
|
||||
export 'package:maplibre_gl/maplibre_gl.dart' show LatLng, LatLngBounds, CameraUpdate, CameraPosition, Symbol, Line, Circle, Fill;
|
||||
// Re-export core MapLibre primitives under familiar names
|
||||
export 'package:maplibre_gl/maplibre_gl.dart'
|
||||
show LatLng, LatLngBounds, CameraUpdate, CameraPosition;
|
||||
|
||||
export 'src/intaleq_map_controller.dart';
|
||||
// Public SDK surface
|
||||
export 'src/intaleq_map_widget.dart';
|
||||
export 'src/intaleq_map_controller.dart';
|
||||
export 'src/styles.dart';
|
||||
export 'src/models/geometry.dart';
|
||||
export 'src/utils/polyline_utils.dart';
|
||||
export 'src/constants/colors.dart';
|
||||
export 'src/utils/polyline_utils.dart';
|
||||
|
||||
// Models — mirrors google_maps_flutter public types
|
||||
export 'src/models/geometry.dart';
|
||||
export 'src/models/bitmap.dart';
|
||||
export 'src/models/info_window.dart';
|
||||
export 'src/models/types.dart';
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
import 'dart:ui' show Color;
|
||||
|
||||
/// Intaleq brand color palette.
|
||||
///
|
||||
/// CSS hex strings are provided for use with MapLibre layer properties.
|
||||
/// Flutter [Color] constants are provided for use in Dart/Flutter code.
|
||||
class IntaleqColors {
|
||||
/// The signature Intaleq Blue color.
|
||||
static const String primaryBlue = '#0D47A1';
|
||||
|
||||
/// Cyan used for remaining route lines.
|
||||
static const String routeCyan = '#00e5ff';
|
||||
|
||||
/// Gray used for traveled paths or alternatives.
|
||||
static const String softGray = '#BDBDBD';
|
||||
|
||||
/// Semi-transparent gray for polygon fills.
|
||||
static const String fillGray = '#BDBDBD33'; // 20% opacity
|
||||
|
||||
IntaleqColors._();
|
||||
|
||||
// ── Flutter Color constants ────────────────────────────────
|
||||
|
||||
/// Primary Intaleq blue.
|
||||
static const Color primaryBlue = Color(0xFF0D47A1);
|
||||
|
||||
/// Cyan for active / remaining route lines.
|
||||
static const Color routeCyan = Color(0xFF00E5FF);
|
||||
|
||||
/// Soft gray for traveled paths or alternative routes.
|
||||
static const Color softGray = Color(0xFFBDBDBD);
|
||||
|
||||
/// Semi-transparent gray for polygon fills (20 % opacity).
|
||||
static const Color fillGray = Color(0x33BDBDBD);
|
||||
|
||||
/// Red for error or stop states.
|
||||
static const String alertRed = '#ba1a1a';
|
||||
static const Color alertRed = Color(0xFFBA1A1A);
|
||||
|
||||
// ── MapLibre hex strings ───────────────────────────────────
|
||||
|
||||
static const String primaryBlueHex = '#0D47A1';
|
||||
static const String routeCyanHex = '#00e5ff';
|
||||
static const String softGrayHex = '#BDBDBD';
|
||||
static const String fillGrayHex = '#BDBDBD33';
|
||||
static const String alertRedHex = '#ba1a1a';
|
||||
}
|
||||
|
||||
@@ -1,105 +1,363 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:maplibre_gl/maplibre_gl.dart' hide CircleOptions;
|
||||
import 'package:maplibre_gl/maplibre_gl.dart' as mgl;
|
||||
import 'models/geometry.dart';
|
||||
import 'models/bitmap.dart';
|
||||
|
||||
/// Controls a live [IntaleqMap] widget.
|
||||
///
|
||||
/// Obtain a reference via the [IntaleqMap.onMapCreated] callback.
|
||||
/// This API mirrors [GoogleMapController] from `google_maps_flutter`.
|
||||
class IntaleqMapController {
|
||||
final MaplibreMapController mapController;
|
||||
final String apiKey;
|
||||
IntaleqMapController._({
|
||||
required mgl.MaplibreMapController raw,
|
||||
required String apiKey,
|
||||
}) : _raw = raw,
|
||||
_apiKey = apiKey;
|
||||
|
||||
IntaleqMapController({
|
||||
required this.mapController,
|
||||
required this.apiKey,
|
||||
});
|
||||
final mgl.MaplibreMapController _raw;
|
||||
final String _apiKey;
|
||||
|
||||
// --- API Integrations ---
|
||||
// ── Internal object registries ─────────────────────────────
|
||||
|
||||
/// Search for places using Intaleq Geocoding API
|
||||
Future<List<dynamic>> search(String query) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('https://map-saas.intaleq.com/v1/geocoding/search?q=$query&key=$apiKey'),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
/// Maps our [MarkerId] → the live MapLibre [mgl.Symbol].
|
||||
final Map<MarkerId, mgl.Symbol> _symbols = {};
|
||||
|
||||
/// Reverse map: MapLibre symbol id → our [Marker] (for tap routing).
|
||||
final Map<String, Marker> _symbolToMarker = {};
|
||||
|
||||
/// Maps our [PolylineId] → live MapLibre [mgl.Line].
|
||||
final Map<PolylineId, mgl.Line> _lines = {};
|
||||
final Map<String, Polyline> _lineToPolyline = {};
|
||||
|
||||
/// Maps our [CircleId] → live MapLibre [mgl.Circle].
|
||||
final Map<CircleId, mgl.Circle> _circles = {};
|
||||
|
||||
/// Maps our [PolygonId] → live MapLibre [mgl.Fill].
|
||||
final Map<PolygonId, mgl.Fill> _fills = {};
|
||||
|
||||
// ── Factory / init ─────────────────────────────────────────
|
||||
|
||||
static Future<IntaleqMapController> create({
|
||||
required mgl.MaplibreMapController raw,
|
||||
required String apiKey,
|
||||
}) async {
|
||||
final ctrl = IntaleqMapController._(raw: raw, apiKey: apiKey);
|
||||
await ctrl._registerDefaultImages();
|
||||
return ctrl;
|
||||
}
|
||||
|
||||
/// Pre-loads built-in Intaleq marker images into the map style.
|
||||
Future<void> _registerDefaultImages() async {
|
||||
try {
|
||||
final bytes = await rootBundle
|
||||
.load('packages/intaleq_maps/assets/markers/default.png');
|
||||
await _raw.addImage('intaleq-marker-default', bytes.buffer.asUint8List());
|
||||
} catch (_) {
|
||||
// Asset not found — developer can register their own via addImage().
|
||||
}
|
||||
throw Exception('Failed to search');
|
||||
}
|
||||
|
||||
/// Get route using Intaleq Routing API
|
||||
Future<Map<String, dynamic>> getRoute(LatLng start, LatLng end, {String profile = 'driving'}) async {
|
||||
final response = await http.get(
|
||||
Uri.parse(
|
||||
'https://map-saas.intaleq.com/v1/routing/route?start=${start.longitude},${start.latitude}&end=${end.longitude},${end.latitude}&profile=$profile&key=$apiKey',
|
||||
),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
// ── Camera (same API as GoogleMapController) ──────────────
|
||||
|
||||
/// Animates the camera to the given [update].
|
||||
Future<bool?> animateCamera(mgl.CameraUpdate update) =>
|
||||
_raw.animateCamera(update);
|
||||
|
||||
/// Instantly moves the camera to the given [update].
|
||||
Future<bool?> moveCamera(mgl.CameraUpdate update) => _raw.moveCamera(update);
|
||||
|
||||
/// Returns the current [mgl.CameraPosition] of the map.
|
||||
mgl.CameraPosition? get cameraPosition => _raw.cameraPosition;
|
||||
|
||||
/// Returns the current zoom level.
|
||||
Future<double> getZoomLevel() async => _raw.cameraPosition?.zoom ?? 14.0;
|
||||
|
||||
/// Returns the [mgl.LatLng] visible at screen coordinate [point].
|
||||
Future<mgl.LatLng> getLatLng(Point<double> point) =>
|
||||
_raw.toLatLng(Point(point.x, point.y));
|
||||
|
||||
/// Returns the screen coordinate of the given [latLng].
|
||||
Future<Point<num>> getScreenCoordinate(mgl.LatLng latLng) =>
|
||||
_raw.toScreenLocation(latLng);
|
||||
|
||||
/// Returns the current visible region as [mgl.LatLngBounds].
|
||||
Future<mgl.LatLngBounds> getVisibleRegion() => _raw.getVisibleRegion();
|
||||
|
||||
// ── Map images ─────────────────────────────────────────────
|
||||
|
||||
/// Register a custom image into the map.
|
||||
/// Required before using [InlqBitmap.fromBytes] or custom style images.
|
||||
Future<void> addImage(String imageId, Uint8List bytes) =>
|
||||
_raw.addImage(imageId, bytes);
|
||||
|
||||
// ── Markers ────────────────────────────────────────────────
|
||||
|
||||
/// Adds a single [Marker] to the map and returns its MapLibre handle.
|
||||
Future<mgl.Symbol> addMarker(Marker marker) async {
|
||||
await _loadBitmapIfNeeded(marker.icon);
|
||||
final symbol = await _raw.addSymbol(marker.toSymbolOptions());
|
||||
_symbols[marker.markerId] = symbol;
|
||||
_symbolToMarker[symbol.id] = marker;
|
||||
return symbol;
|
||||
}
|
||||
|
||||
/// Updates an existing marker's position / appearance.
|
||||
Future<void> _updateMarker(Marker marker) async {
|
||||
final symbol = _symbols[marker.markerId];
|
||||
if (symbol == null) return;
|
||||
await _raw.updateSymbol(symbol, marker.toSymbolOptions());
|
||||
_symbolToMarker[symbol.id] = marker;
|
||||
}
|
||||
|
||||
/// Removes a marker by its [MarkerId].
|
||||
Future<void> _removeMarker(MarkerId id) async {
|
||||
final symbol = _symbols.remove(id);
|
||||
if (symbol == null) return;
|
||||
_symbolToMarker.remove(symbol.id);
|
||||
await _raw.removeSymbol(symbol);
|
||||
}
|
||||
|
||||
// ── Polylines ──────────────────────────────────────────────
|
||||
|
||||
Future<mgl.Line> addPolyline(Polyline polyline) async {
|
||||
final line = await _raw.addLine(polyline.toLineOptions());
|
||||
_lines[polyline.polylineId] = line;
|
||||
_lineToPolyline[line.id] = polyline;
|
||||
return line;
|
||||
}
|
||||
|
||||
Future<void> _updatePolyline(Polyline polyline) async {
|
||||
final line = _lines[polyline.polylineId];
|
||||
if (line == null) return;
|
||||
await _raw.updateLine(line, polyline.toLineOptions());
|
||||
_lineToPolyline[line.id] = polyline;
|
||||
}
|
||||
|
||||
Future<void> _removePolyline(PolylineId id) async {
|
||||
final line = _lines.remove(id);
|
||||
if (line == null) return;
|
||||
_lineToPolyline.remove(line.id);
|
||||
await _raw.removeLine(line);
|
||||
}
|
||||
|
||||
// ── Circles ────────────────────────────────────────────────
|
||||
|
||||
Future<void> addCircle(Circle circle) async {
|
||||
final c = await _raw.addCircle(circle.toCircleOptions());
|
||||
_circles[circle.circleId] = c;
|
||||
}
|
||||
|
||||
Future<void> _updateCircle(Circle circle) async {
|
||||
final c = _circles[circle.circleId];
|
||||
if (c == null) return;
|
||||
await _raw.updateCircle(c, circle.toCircleOptions());
|
||||
}
|
||||
|
||||
Future<void> _removeCircle(CircleId id) async {
|
||||
final c = _circles.remove(id);
|
||||
if (c == null) return;
|
||||
await _raw.removeCircle(c);
|
||||
}
|
||||
|
||||
// ── Polygons ───────────────────────────────────────────────
|
||||
|
||||
Future<void> addPolygon(Polygon polygon) async {
|
||||
final fill = await _raw.addFill(polygon.toFillOptions());
|
||||
_fills[polygon.polygonId] = fill;
|
||||
}
|
||||
|
||||
Future<void> _updatePolygon(Polygon polygon) async {
|
||||
final fill = _fills[polygon.polygonId];
|
||||
if (fill == null) return;
|
||||
await _raw.updateFill(fill, polygon.toFillOptions());
|
||||
}
|
||||
|
||||
Future<void> _removePolygon(PolygonId id) async {
|
||||
final fill = _fills.remove(id);
|
||||
if (fill == null) return;
|
||||
await _raw.removeFill(fill);
|
||||
}
|
||||
|
||||
// ── Tap routing ────────────────────────────────────────────
|
||||
|
||||
void onSymbolTapped(mgl.Symbol symbol) {
|
||||
final marker = _symbolToMarker[symbol.id];
|
||||
if (marker == null) return;
|
||||
marker.onTap?.call();
|
||||
}
|
||||
|
||||
void onLineTapped(mgl.Line line) {
|
||||
_lineToPolyline[line.id]?.onTap?.call();
|
||||
}
|
||||
|
||||
// ── Diff helpers (called from widget on setState) ──────────
|
||||
|
||||
Future<void> diffMarkers(
|
||||
Set<Marker> oldSet,
|
||||
Set<Marker> newSet,
|
||||
) async {
|
||||
final oldMap = {for (final m in oldSet) m.markerId: m};
|
||||
final newMap = {for (final m in newSet) m.markerId: m};
|
||||
|
||||
// Remove deleted
|
||||
for (final id in oldMap.keys) {
|
||||
if (!newMap.containsKey(id)) await _removeMarker(id);
|
||||
}
|
||||
throw Exception('Failed to fetch route');
|
||||
}
|
||||
|
||||
// --- Friendly Abstractions (Transformation Layer) ---
|
||||
|
||||
/// Add a marker (friendly wrapper for addSymbol)
|
||||
Future<Symbol> addMarker(MarkerOptions options) async {
|
||||
return await mapController.addSymbol(options.toSymbolOptions());
|
||||
}
|
||||
|
||||
/// Add multiple markers
|
||||
Future<List<Symbol>> addMarkers(List<MarkerOptions> optionsList) async {
|
||||
final symbols = <Symbol>[];
|
||||
for (var options in optionsList) {
|
||||
symbols.add(await addMarker(options));
|
||||
// Add new
|
||||
for (final entry in newMap.entries) {
|
||||
if (!oldMap.containsKey(entry.key)) {
|
||||
await addMarker(entry.value);
|
||||
} else if (oldMap[entry.key] != entry.value) {
|
||||
// Update changed (position, icon, etc.)
|
||||
await _updateMarker(entry.value);
|
||||
}
|
||||
}
|
||||
return symbols;
|
||||
}
|
||||
|
||||
/// Remove a marker
|
||||
Future<void> removeMarker(Symbol marker) async {
|
||||
await mapController.removeSymbol(marker);
|
||||
Future<void> diffPolylines(
|
||||
Set<Polyline> oldSet,
|
||||
Set<Polyline> newSet,
|
||||
) async {
|
||||
final oldMap = {for (final p in oldSet) p.polylineId: p};
|
||||
final newMap = {for (final p in newSet) p.polylineId: p};
|
||||
|
||||
for (final id in oldMap.keys) {
|
||||
if (!newMap.containsKey(id)) await _removePolyline(id);
|
||||
}
|
||||
for (final entry in newMap.entries) {
|
||||
if (!oldMap.containsKey(entry.key)) {
|
||||
await addPolyline(entry.value);
|
||||
} else if (oldMap[entry.key] != entry.value) {
|
||||
await _updatePolyline(entry.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a polyline (friendly wrapper for addLine)
|
||||
Future<Line> addPolyline(PolylineOptions options) async {
|
||||
return await mapController.addLine(options.toLineOptions());
|
||||
Future<void> diffCircles(
|
||||
Set<Circle> oldSet,
|
||||
Set<Circle> newSet,
|
||||
) async {
|
||||
final oldMap = {for (final c in oldSet) c.circleId: c};
|
||||
final newMap = {for (final c in newSet) c.circleId: c};
|
||||
|
||||
for (final id in oldMap.keys) {
|
||||
if (!newMap.containsKey(id)) await _removeCircle(id);
|
||||
}
|
||||
for (final entry in newMap.entries) {
|
||||
if (!oldMap.containsKey(entry.key)) {
|
||||
await addCircle(entry.value);
|
||||
} else if (oldMap[entry.key] != entry.value) {
|
||||
await _updateCircle(entry.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a polyline
|
||||
Future<void> removePolyline(Line polyline) async {
|
||||
await mapController.removeLine(polyline);
|
||||
Future<void> diffPolygons(
|
||||
Set<Polygon> oldSet,
|
||||
Set<Polygon> newSet,
|
||||
) async {
|
||||
final oldMap = {for (final p in oldSet) p.polygonId: p};
|
||||
final newMap = {for (final p in newSet) p.polygonId: p};
|
||||
|
||||
for (final id in oldMap.keys) {
|
||||
if (!newMap.containsKey(id)) await _removePolygon(id);
|
||||
}
|
||||
for (final entry in newMap.entries) {
|
||||
if (!oldMap.containsKey(entry.key)) {
|
||||
await addPolygon(entry.value);
|
||||
} else if (oldMap[entry.key] != entry.value) {
|
||||
await _updatePolygon(entry.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a circle
|
||||
Future<Circle> addCircle(CircleOptions options) async {
|
||||
return await mapController.addCircle(options.toMapLibreOptions());
|
||||
// ── Clear all ──────────────────────────────────────────────
|
||||
|
||||
/// Removes every marker, polyline, circle, and polygon from the map.
|
||||
Future<void> clearOverlays() async {
|
||||
await _raw.clearSymbols();
|
||||
await _raw.clearLines();
|
||||
await _raw.clearCircles();
|
||||
await _raw.clearFills();
|
||||
_symbols.clear();
|
||||
_symbolToMarker.clear();
|
||||
_lines.clear();
|
||||
_lineToPolyline.clear();
|
||||
_circles.clear();
|
||||
_fills.clear();
|
||||
}
|
||||
|
||||
/// Remove a circle
|
||||
Future<void> removeCircle(Circle circle) async {
|
||||
await mapController.removeCircle(circle);
|
||||
// ── Intaleq API integrations ───────────────────────────────
|
||||
|
||||
/// Searches for places using the Intaleq Geocoding API.
|
||||
Future<List<dynamic>> searchPlaces(String query) async {
|
||||
final uri = Uri.https('map-saas.intaleq.com', '/v1/geocoding/search', {
|
||||
'q': query,
|
||||
'key': _apiKey,
|
||||
});
|
||||
final res = await http.get(uri);
|
||||
if (res.statusCode == 200) return jsonDecode(res.body) as List<dynamic>;
|
||||
throw Exception('Geocoding request failed: ${res.statusCode}');
|
||||
}
|
||||
|
||||
/// Add a polygon (Fill)
|
||||
Future<Fill> addPolygon(PolygonOptions options) async {
|
||||
return await mapController.addFill(options.toFillOptions());
|
||||
/// Reverse geocodes a [LatLng] to a place description.
|
||||
Future<Map<String, dynamic>> reverseGeocode(mgl.LatLng position) async {
|
||||
final uri = Uri.https('map-saas.intaleq.com', '/v1/geocoding/reverse', {
|
||||
'lat': position.latitude.toString(),
|
||||
'lng': position.longitude.toString(),
|
||||
'key': _apiKey,
|
||||
});
|
||||
final res = await http.get(uri);
|
||||
if (res.statusCode == 200)
|
||||
return jsonDecode(res.body) as Map<String, dynamic>;
|
||||
throw Exception('Reverse geocoding failed: ${res.statusCode}');
|
||||
}
|
||||
|
||||
/// Remove a polygon
|
||||
Future<void> removePolygon(Fill polygon) async {
|
||||
await mapController.removeFill(polygon);
|
||||
/// Fetches a route between [origin] and [destination].
|
||||
///
|
||||
/// [profile] is one of: `driving`, `cycling`, `walking`.
|
||||
/// Returns the raw Intaleq Routing API response.
|
||||
Future<Map<String, dynamic>> getDirections(
|
||||
mgl.LatLng origin,
|
||||
mgl.LatLng destination, {
|
||||
String profile = 'driving',
|
||||
}) async {
|
||||
final uri = Uri.https('map-saas.intaleq.com', '/v1/routing/route', {
|
||||
'start': '${origin.longitude},${origin.latitude}',
|
||||
'end': '${destination.longitude},${destination.latitude}',
|
||||
'profile': profile,
|
||||
'key': _apiKey,
|
||||
});
|
||||
final res = await http.get(uri);
|
||||
if (res.statusCode == 200)
|
||||
return jsonDecode(res.body) as Map<String, dynamic>;
|
||||
throw Exception('Routing request failed: ${res.statusCode}');
|
||||
}
|
||||
|
||||
/// Clear all map objects
|
||||
void clearAll() {
|
||||
mapController.clearSymbols();
|
||||
mapController.clearLines();
|
||||
mapController.clearCircles();
|
||||
mapController.clearFills();
|
||||
}
|
||||
// ── Bitmap loader helper ───────────────────────────────────
|
||||
|
||||
// --- Camera Controls ---
|
||||
final Set<String> _loadedImages = {};
|
||||
|
||||
void animateCameraTo(LatLng position, {double? zoom}) {
|
||||
mapController.animateCamera(CameraUpdate.newLatLngZoom(position, zoom ?? mapController.cameraPosition?.zoom ?? 14.0));
|
||||
Future<void> _loadBitmapIfNeeded(InlqBitmap bitmap) async {
|
||||
final id = bitmap.mapLibreImageId;
|
||||
if (_loadedImages.contains(id)) return;
|
||||
|
||||
if (bitmap.bytes != null) {
|
||||
await _raw.addImage(id, bitmap.bytes!);
|
||||
_loadedImages.add(id);
|
||||
} else if (bitmap.assetName != null) {
|
||||
try {
|
||||
final data = await rootBundle.load(bitmap.assetName!);
|
||||
await _raw.addImage(id, data.buffer.asUint8List());
|
||||
_loadedImages.add(id);
|
||||
} catch (_) {
|
||||
// Asset not found — marker will fall back to default.
|
||||
}
|
||||
}
|
||||
// Style-registered images need no loading.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,215 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart' as mgl;
|
||||
import 'intaleq_map_controller.dart';
|
||||
import 'styles.dart';
|
||||
import 'models/geometry.dart';
|
||||
import 'models/types.dart';
|
||||
|
||||
typedef MapCreatedCallback = void Function(IntaleqMapController controller);
|
||||
|
||||
/// A widget displaying an Intaleq map.
|
||||
///
|
||||
/// ## Drop-in replacement for `GoogleMap`
|
||||
///
|
||||
/// ```dart
|
||||
/// // Before (Google Maps Flutter)
|
||||
/// GoogleMap(
|
||||
/// initialCameraPosition: CameraPosition(target: LatLng(33.5, 36.2), zoom: 14),
|
||||
/// markers: _markers,
|
||||
/// onMapCreated: (ctrl) => _controller = ctrl,
|
||||
/// )
|
||||
///
|
||||
/// // After (Intaleq Maps)
|
||||
/// IntaleqMap(
|
||||
/// apiKey: 'YOUR_KEY',
|
||||
/// initialCameraPosition: CameraPosition(target: LatLng(33.5, 36.2), zoom: 14),
|
||||
/// markers: _markers,
|
||||
/// onMapCreated: (ctrl) => _controller = ctrl,
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// All overlay collections ([markers], [polylines], [circles], [polygons])
|
||||
/// are **declarative**: just call `setState` with a new `Set` and the map
|
||||
/// will reconcile additions, updates, and removals automatically.
|
||||
class IntaleqMap extends StatefulWidget {
|
||||
final String apiKey;
|
||||
final String? styleUrl;
|
||||
final LatLng initialPosition;
|
||||
final double initialZoom;
|
||||
final MapCreatedCallback? onMapCreated;
|
||||
final bool myLocationEnabled;
|
||||
|
||||
const IntaleqMap({
|
||||
super.key,
|
||||
required this.apiKey,
|
||||
required this.initialCameraPosition,
|
||||
this.mapType = IntaleqMapType.normal,
|
||||
this.styleUrl,
|
||||
this.initialPosition = const LatLng(33.5138, 36.2765),
|
||||
this.initialZoom = 14.0,
|
||||
this.markers = const {},
|
||||
this.polylines = const {},
|
||||
this.circles = const {},
|
||||
this.polygons = const {},
|
||||
this.onMapCreated,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onCameraMove,
|
||||
this.onCameraMoveStarted,
|
||||
this.onCameraIdle,
|
||||
this.myLocationEnabled = false,
|
||||
this.myLocationButtonEnabled = false,
|
||||
this.zoomControlsEnabled = false,
|
||||
this.compassEnabled = false,
|
||||
this.rotateGesturesEnabled = true,
|
||||
this.scrollGesturesEnabled = true,
|
||||
this.tiltGesturesEnabled = true,
|
||||
this.zoomGesturesEnabled = true,
|
||||
this.minMaxZoomPreference = MinMaxZoomPreference.unbounded,
|
||||
this.cameraTargetBounds = CameraTargetBounds.unbounded,
|
||||
});
|
||||
|
||||
// ── Required ───────────────────────────────────────────────
|
||||
|
||||
/// Your Intaleq Maps API key.
|
||||
final String apiKey;
|
||||
|
||||
/// Starting camera position (target + zoom).
|
||||
final mgl.CameraPosition initialCameraPosition;
|
||||
|
||||
// ── Map style ──────────────────────────────────────────────
|
||||
|
||||
/// Base tile style. Ignored when [styleUrl] is provided.
|
||||
final IntaleqMapType mapType;
|
||||
|
||||
/// Override with a custom MapLibre style URL.
|
||||
final String? styleUrl;
|
||||
|
||||
// ── Overlays (declarative — mirrors GoogleMap) ─────────────
|
||||
|
||||
final Set<Marker> markers;
|
||||
final Set<Polyline> polylines;
|
||||
final Set<Circle> circles;
|
||||
final Set<Polygon> polygons;
|
||||
|
||||
// ── Lifecycle callbacks ────────────────────────────────────
|
||||
|
||||
/// Called once the map is ready. Use [IntaleqMapController] for all operations.
|
||||
final MapCreatedCallback? onMapCreated;
|
||||
|
||||
// ── Interaction callbacks ──────────────────────────────────
|
||||
|
||||
/// Called when the user taps the map (not on a marker).
|
||||
final ArgumentCallback<mgl.LatLng>? onTap;
|
||||
|
||||
/// Called on a long-press on the map.
|
||||
final ArgumentCallback<mgl.LatLng>? onLongPress;
|
||||
|
||||
/// Called repeatedly while the camera is moving.
|
||||
final CameraPositionCallback? onCameraMove;
|
||||
|
||||
/// Called when the camera starts moving.
|
||||
final VoidCallback? onCameraMoveStarted;
|
||||
|
||||
/// Called when the camera becomes idle.
|
||||
final VoidCallback? onCameraIdle;
|
||||
|
||||
// ── UI controls ────────────────────────────────────────────
|
||||
|
||||
final bool myLocationEnabled;
|
||||
final bool myLocationButtonEnabled;
|
||||
final bool zoomControlsEnabled;
|
||||
final bool compassEnabled;
|
||||
|
||||
// ── Gesture controls ───────────────────────────────────────
|
||||
|
||||
final bool rotateGesturesEnabled;
|
||||
final bool scrollGesturesEnabled;
|
||||
final bool tiltGesturesEnabled;
|
||||
final bool zoomGesturesEnabled;
|
||||
|
||||
// ── Camera constraints ─────────────────────────────────────
|
||||
|
||||
final MinMaxZoomPreference minMaxZoomPreference;
|
||||
final CameraTargetBounds cameraTargetBounds;
|
||||
|
||||
@override
|
||||
State<IntaleqMap> createState() => _IntaleqMapState();
|
||||
}
|
||||
|
||||
class _IntaleqMapState extends State<IntaleqMap> {
|
||||
IntaleqMapController? _intaleqController;
|
||||
IntaleqMapController? _controller;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(IntaleqMap oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final ctrl = _controller;
|
||||
if (ctrl == null) return;
|
||||
|
||||
// Reconcile each overlay set when widget rebuilds.
|
||||
ctrl.diffMarkers(oldWidget.markers, widget.markers);
|
||||
ctrl.diffPolylines(oldWidget.polylines, widget.polylines);
|
||||
ctrl.diffCircles(oldWidget.circles, widget.circles);
|
||||
ctrl.diffPolygons(oldWidget.polygons, widget.polygons);
|
||||
}
|
||||
|
||||
String get _resolvedStyleUrl {
|
||||
if (widget.styleUrl != null) return widget.styleUrl!;
|
||||
return switch (widget.mapType) {
|
||||
IntaleqMapType.normal => IntaleqStyles.obsidian(widget.apiKey),
|
||||
IntaleqMapType.light => IntaleqStyles.light(widget.apiKey),
|
||||
IntaleqMapType.satellite => IntaleqStyles.satellite(widget.apiKey),
|
||||
IntaleqMapType.none => 'about:blank',
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _onMapCreated(mgl.MaplibreMapController rawCtrl) async {
|
||||
// Wire up tap routing before handing the controller to the caller.
|
||||
rawCtrl.onSymbolTapped.add(_onSymbolTapped);
|
||||
rawCtrl.onLineTapped.add(_onLineTapped);
|
||||
|
||||
final ctrl = await IntaleqMapController.create(
|
||||
raw: rawCtrl,
|
||||
apiKey: widget.apiKey,
|
||||
);
|
||||
_controller = ctrl;
|
||||
|
||||
// Render the initial overlay sets.
|
||||
for (final m in widget.markers) await ctrl.addMarker(m);
|
||||
for (final p in widget.polylines) await ctrl.addPolyline(p);
|
||||
for (final c in widget.circles) await ctrl.addCircle(c);
|
||||
for (final g in widget.polygons) await ctrl.addPolygon(g);
|
||||
|
||||
widget.onMapCreated?.call(ctrl);
|
||||
}
|
||||
|
||||
void _onSymbolTapped(mgl.Symbol symbol) =>
|
||||
_controller?.onSymbolTapped(symbol);
|
||||
|
||||
void _onLineTapped(mgl.Line line) => _controller?.onLineTapped(line);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = widget.styleUrl ?? IntaleqStyles.obsidian(widget.apiKey);
|
||||
|
||||
return MaplibreMap(
|
||||
onMapCreated: (MaplibreMapController controller) {
|
||||
_intaleqController = IntaleqMapController(
|
||||
mapController: controller,
|
||||
apiKey: widget.apiKey,
|
||||
);
|
||||
if (widget.onMapCreated != null) {
|
||||
widget.onMapCreated!(_intaleqController!);
|
||||
}
|
||||
},
|
||||
styleString: style,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: widget.initialPosition,
|
||||
zoom: widget.initialZoom,
|
||||
),
|
||||
return mgl.MaplibreMap(
|
||||
styleString: _resolvedStyleUrl,
|
||||
initialCameraPosition: widget.initialCameraPosition,
|
||||
onMapCreated: _onMapCreated,
|
||||
onMapClick: widget.onTap != null
|
||||
? (point, latlng) => widget.onTap!(latlng)
|
||||
: null,
|
||||
onMapLongClick: widget.onLongPress != null
|
||||
? (point, latlng) => widget.onLongPress!(latlng)
|
||||
: null,
|
||||
onCameraIdle: widget.onCameraIdle,
|
||||
onCameraTrackingChanged: null,
|
||||
myLocationEnabled: widget.myLocationEnabled,
|
||||
compassEnabled: false,
|
||||
trackCameraPosition: true,
|
||||
myLocationRenderMode: widget.myLocationEnabled
|
||||
? mgl.MyLocationRenderMode.NORMAL
|
||||
: mgl.MyLocationRenderMode.NORMAL,
|
||||
myLocationTrackingMode: mgl.MyLocationTrackingMode.None,
|
||||
compassEnabled: widget.compassEnabled,
|
||||
rotateGesturesEnabled: widget.rotateGesturesEnabled,
|
||||
scrollGesturesEnabled: widget.scrollGesturesEnabled,
|
||||
tiltGesturesEnabled: widget.tiltGesturesEnabled,
|
||||
zoomGesturesEnabled: widget.zoomGesturesEnabled,
|
||||
minMaxZoomPreference: mgl.MinMaxZoomPreference(
|
||||
widget.minMaxZoomPreference.minZoom,
|
||||
widget.minMaxZoomPreference.maxZoom,
|
||||
),
|
||||
cameraTargetBounds: widget.cameraTargetBounds.bounds != null
|
||||
? mgl.CameraTargetBounds(widget.cameraTargetBounds.bounds!)
|
||||
: mgl.CameraTargetBounds.unbounded,
|
||||
trackCameraPosition: widget.onCameraMove != null,
|
||||
onCameraTrackingDismissed: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'dart:typed_data' show Uint8List;
|
||||
import 'dart:ui' show Offset;
|
||||
|
||||
/// Defines a bitmap image for use as a [Marker] icon.
|
||||
///
|
||||
/// This class mirrors `BitmapDescriptor` from `google_maps_flutter`.
|
||||
/// Internally it resolves to a MapLibre image name or inline bytes.
|
||||
class InlqBitmap {
|
||||
const InlqBitmap._({
|
||||
required this.mapLibreImageId,
|
||||
this.size,
|
||||
this.offset,
|
||||
this.bytes,
|
||||
this.assetName,
|
||||
});
|
||||
|
||||
/// The image name used in MapLibre style expressions.
|
||||
final String mapLibreImageId;
|
||||
|
||||
/// Optional size multiplier (MapLibre iconSize).
|
||||
final double? size;
|
||||
|
||||
/// Optional pixel offset from the anchor point.
|
||||
final Offset? offset;
|
||||
|
||||
/// Raw PNG/JPEG bytes (used with [fromBytes]).
|
||||
final Uint8List? bytes;
|
||||
|
||||
/// Flutter asset path (used with [fromAsset]).
|
||||
final String? assetName;
|
||||
|
||||
// ── Built-in defaults ──────────────────────────────────────
|
||||
|
||||
/// The standard blue pin marker (default).
|
||||
static const InlqBitmap defaultMarker = InlqBitmap._(
|
||||
mapLibreImageId: 'intaleq-marker-default',
|
||||
);
|
||||
|
||||
// Hue constants matching google_maps_flutter
|
||||
static const double hueRed = 0.0;
|
||||
static const double hueOrange = 30.0;
|
||||
static const double hueYellow = 60.0;
|
||||
static const double hueGreen = 120.0;
|
||||
static const double hueCyan = 180.0;
|
||||
static const double hueAzure = 210.0;
|
||||
static const double hueBlue = 240.0;
|
||||
static const double hueViolet = 270.0;
|
||||
static const double hueMagenta = 300.0;
|
||||
static const double hueRose = 330.0;
|
||||
|
||||
/// A colored pin marker. [hue] is a value from 0–360 (HSV hue).
|
||||
///
|
||||
/// Use the `hue*` constants (e.g. [InlqBitmap.hueBlue]) for clarity.
|
||||
static InlqBitmap defaultMarkerWithHue(double hue) {
|
||||
return InlqBitmap._(
|
||||
mapLibreImageId: 'intaleq-marker-hue-${hue.toInt()}',
|
||||
size: 1.0,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Custom bitmaps ─────────────────────────────────────────
|
||||
|
||||
/// Load an icon from a Flutter asset bundle.
|
||||
///
|
||||
/// ```dart
|
||||
/// icon: InlqBitmap.fromAsset('assets/icons/car.png')
|
||||
/// ```
|
||||
static InlqBitmap fromAsset(String assetName, {double? size}) {
|
||||
return InlqBitmap._(
|
||||
mapLibreImageId: 'asset:$assetName',
|
||||
assetName: assetName,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
/// Load an icon from raw image bytes (e.g. a network image you decoded).
|
||||
static InlqBitmap fromBytes(Uint8List bytes, {double? size}) {
|
||||
return InlqBitmap._(
|
||||
mapLibreImageId: 'bytes:${bytes.hashCode}',
|
||||
bytes: bytes,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
/// Reference an image already registered in the MapLibre style.
|
||||
///
|
||||
/// Use this when your map style JSON already defines the sprite image.
|
||||
static InlqBitmap fromStyleImage(String imageId, {double? size}) {
|
||||
return InlqBitmap._(
|
||||
mapLibreImageId: imageId,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'InlqBitmap($mapLibreImageId)';
|
||||
}
|
||||
@@ -1,110 +1,442 @@
|
||||
import 'dart:ui';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart' as maplibre;
|
||||
import 'dart:ui' show Offset, Color;
|
||||
import 'package:flutter/foundation.dart' show VoidCallback, ValueChanged;
|
||||
import 'package:maplibre_gl/maplibre_gl.dart' as mgl;
|
||||
import '../constants/colors.dart';
|
||||
import 'bitmap.dart';
|
||||
import 'info_window.dart';
|
||||
|
||||
class MarkerOptions {
|
||||
final maplibre.LatLng position;
|
||||
final String? iconImage;
|
||||
final double? iconSize;
|
||||
final double? iconRotate;
|
||||
final String? textField;
|
||||
final double? textOffset;
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ID types (identical contract to google_maps_flutter)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
MarkerOptions({
|
||||
required this.position,
|
||||
this.iconImage,
|
||||
this.iconSize,
|
||||
this.iconRotate,
|
||||
this.textField,
|
||||
this.textOffset,
|
||||
});
|
||||
|
||||
maplibre.SymbolOptions toSymbolOptions() {
|
||||
return maplibre.SymbolOptions(
|
||||
geometry: position,
|
||||
iconImage: iconImage,
|
||||
iconSize: iconSize,
|
||||
iconRotate: iconRotate,
|
||||
textField: textField,
|
||||
textOffset: textOffset != null ? Offset(0, textOffset!) : null,
|
||||
);
|
||||
}
|
||||
class MarkerId {
|
||||
const MarkerId(this.value);
|
||||
final String value;
|
||||
@override
|
||||
bool operator ==(Object o) => o is MarkerId && o.value == value;
|
||||
@override
|
||||
int get hashCode => value.hashCode;
|
||||
@override
|
||||
String toString() => 'MarkerId($value)';
|
||||
}
|
||||
|
||||
class PolylineOptions {
|
||||
final List<maplibre.LatLng> coordinates;
|
||||
final String? color;
|
||||
final double? width;
|
||||
final double? opacity;
|
||||
class PolylineId {
|
||||
const PolylineId(this.value);
|
||||
final String value;
|
||||
@override
|
||||
bool operator ==(Object o) => o is PolylineId && o.value == value;
|
||||
@override
|
||||
int get hashCode => value.hashCode;
|
||||
@override
|
||||
String toString() => 'PolylineId($value)';
|
||||
}
|
||||
|
||||
PolylineOptions({
|
||||
required this.coordinates,
|
||||
this.color,
|
||||
this.width,
|
||||
this.opacity,
|
||||
class CircleId {
|
||||
const CircleId(this.value);
|
||||
final String value;
|
||||
@override
|
||||
bool operator ==(Object o) => o is CircleId && o.value == value;
|
||||
@override
|
||||
int get hashCode => value.hashCode;
|
||||
@override
|
||||
String toString() => 'CircleId($value)';
|
||||
}
|
||||
|
||||
class PolygonId {
|
||||
const PolygonId(this.value);
|
||||
final String value;
|
||||
@override
|
||||
bool operator ==(Object o) => o is PolygonId && o.value == value;
|
||||
@override
|
||||
int get hashCode => value.hashCode;
|
||||
@override
|
||||
String toString() => 'PolygonId($value)';
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Marker (mirrors google_maps_flutter Marker)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
class Marker {
|
||||
const Marker({
|
||||
required this.markerId,
|
||||
required this.position,
|
||||
this.alpha = 1.0,
|
||||
this.anchor = const Offset(0.5, 1.0),
|
||||
this.draggable = false,
|
||||
this.flat = false,
|
||||
this.icon = InlqBitmap.defaultMarker,
|
||||
this.infoWindow = InfoWindow.noText,
|
||||
this.rotation = 0.0,
|
||||
this.visible = true,
|
||||
this.zIndex = 0.0,
|
||||
this.onTap,
|
||||
this.onDragStart,
|
||||
this.onDrag,
|
||||
this.onDragEnd,
|
||||
});
|
||||
|
||||
maplibre.LineOptions toLineOptions() {
|
||||
return maplibre.LineOptions(
|
||||
geometry: coordinates,
|
||||
lineColor: color ?? IntaleqColors.primaryBlue,
|
||||
lineWidth: width ?? 5.0,
|
||||
lineOpacity: opacity ?? 1.0,
|
||||
final MarkerId markerId;
|
||||
final mgl.LatLng position;
|
||||
|
||||
/// Opacity of the marker icon, from 0.0 (transparent) to 1.0 (opaque).
|
||||
final double alpha;
|
||||
|
||||
/// Icon anchor point as a fraction of icon size. (0.5, 1.0) = bottom-center.
|
||||
final Offset anchor;
|
||||
|
||||
/// Whether the marker can be dragged by the user.
|
||||
final bool draggable;
|
||||
|
||||
/// When true the marker is drawn flat against the map surface.
|
||||
final bool flat;
|
||||
|
||||
/// The icon image for the marker.
|
||||
final InlqBitmap icon;
|
||||
|
||||
/// Info window shown when the marker is tapped.
|
||||
final InfoWindow infoWindow;
|
||||
|
||||
/// Rotation of the marker in degrees clockwise from north.
|
||||
final double rotation;
|
||||
|
||||
/// Whether the marker is visible on the map.
|
||||
final bool visible;
|
||||
|
||||
/// Z-index for draw ordering among markers.
|
||||
final double zIndex;
|
||||
|
||||
/// Called when the marker is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Called when a drag starts.
|
||||
final ValueChanged<mgl.LatLng>? onDragStart;
|
||||
|
||||
/// Called repeatedly during drag.
|
||||
final ValueChanged<mgl.LatLng>? onDrag;
|
||||
|
||||
/// Called when drag ends.
|
||||
final ValueChanged<mgl.LatLng>? onDragEnd;
|
||||
|
||||
Marker copyWith({
|
||||
mgl.LatLng? position,
|
||||
double? alpha,
|
||||
Offset? anchor,
|
||||
bool? draggable,
|
||||
bool? flat,
|
||||
InlqBitmap? icon,
|
||||
InfoWindow? infoWindow,
|
||||
double? rotation,
|
||||
bool? visible,
|
||||
double? zIndex,
|
||||
VoidCallback? onTap,
|
||||
ValueChanged<mgl.LatLng>? onDragStart,
|
||||
ValueChanged<mgl.LatLng>? onDrag,
|
||||
ValueChanged<mgl.LatLng>? onDragEnd,
|
||||
}) {
|
||||
return Marker(
|
||||
markerId: markerId,
|
||||
position: position ?? this.position,
|
||||
alpha: alpha ?? this.alpha,
|
||||
anchor: anchor ?? this.anchor,
|
||||
draggable: draggable ?? this.draggable,
|
||||
flat: flat ?? this.flat,
|
||||
icon: icon ?? this.icon,
|
||||
infoWindow: infoWindow ?? this.infoWindow,
|
||||
rotation: rotation ?? this.rotation,
|
||||
visible: visible ?? this.visible,
|
||||
zIndex: zIndex ?? this.zIndex,
|
||||
onTap: onTap ?? this.onTap,
|
||||
onDragStart: onDragStart ?? this.onDragStart,
|
||||
onDrag: onDrag ?? this.onDrag,
|
||||
onDragEnd: onDragEnd ?? this.onDragEnd,
|
||||
);
|
||||
}
|
||||
|
||||
/// Internal: converts to MapLibre SymbolOptions.
|
||||
mgl.SymbolOptions toSymbolOptions() {
|
||||
return mgl.SymbolOptions(
|
||||
geometry: position,
|
||||
iconImage: icon.mapLibreImageId,
|
||||
iconSize: icon.size,
|
||||
iconRotate: rotation,
|
||||
iconOpacity: alpha,
|
||||
iconAnchor: _anchorToString(anchor),
|
||||
draggable: draggable,
|
||||
zIndex: zIndex.toInt(),
|
||||
textField: infoWindow.title,
|
||||
iconOffset: icon.offset,
|
||||
);
|
||||
}
|
||||
|
||||
static String _anchorToString(Offset anchor) {
|
||||
if (anchor.dx == 0.5 && anchor.dy == 1.0) return 'bottom';
|
||||
if (anchor.dx == 0.5 && anchor.dy == 0.0) return 'top';
|
||||
if (anchor.dx == 0.0 && anchor.dy == 0.5) return 'left';
|
||||
if (anchor.dx == 1.0 && anchor.dy == 0.5) return 'right';
|
||||
if (anchor.dx == 0.5 && anchor.dy == 0.5) return 'center';
|
||||
return 'bottom';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) => o is Marker && o.markerId == markerId;
|
||||
@override
|
||||
int get hashCode => markerId.hashCode;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Polyline (mirrors google_maps_flutter Polyline)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
class Polyline {
|
||||
const Polyline({
|
||||
required this.polylineId,
|
||||
required this.points,
|
||||
this.color = const Color(0xFF0D47A1),
|
||||
this.width = 5,
|
||||
this.visible = true,
|
||||
this.zIndex = 0,
|
||||
this.geodesic = false,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final PolylineId polylineId;
|
||||
|
||||
/// The ordered list of points that make up the polyline.
|
||||
final List<mgl.LatLng> points;
|
||||
|
||||
/// Line stroke color.
|
||||
final Color color;
|
||||
|
||||
/// Line stroke width in screen pixels.
|
||||
final int width;
|
||||
|
||||
/// Whether the polyline is visible on the map.
|
||||
final bool visible;
|
||||
|
||||
final int zIndex;
|
||||
|
||||
/// When true, the polyline follows the curvature of the Earth.
|
||||
final bool geodesic;
|
||||
|
||||
/// Called when the polyline is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
Polyline copyWith({
|
||||
List<mgl.LatLng>? points,
|
||||
Color? color,
|
||||
int? width,
|
||||
bool? visible,
|
||||
int? zIndex,
|
||||
bool? geodesic,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
return Polyline(
|
||||
polylineId: polylineId,
|
||||
points: points ?? this.points,
|
||||
color: color ?? this.color,
|
||||
width: width ?? this.width,
|
||||
visible: visible ?? this.visible,
|
||||
zIndex: zIndex ?? this.zIndex,
|
||||
geodesic: geodesic ?? this.geodesic,
|
||||
onTap: onTap ?? this.onTap,
|
||||
);
|
||||
}
|
||||
|
||||
/// Internal: converts to MapLibre LineOptions.
|
||||
mgl.LineOptions toLineOptions() {
|
||||
return mgl.LineOptions(
|
||||
geometry: points,
|
||||
lineColor: _colorToHex(color),
|
||||
lineWidth: width.toDouble(),
|
||||
lineOpacity: visible ? color.opacity : 0.0,
|
||||
lineJoin: 'round',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) => o is Polyline && o.polylineId == polylineId;
|
||||
@override
|
||||
int get hashCode => polylineId.hashCode;
|
||||
}
|
||||
|
||||
class CircleOptions {
|
||||
final maplibre.LatLng position;
|
||||
final double radius;
|
||||
final String? color;
|
||||
final double? opacity;
|
||||
final String? strokeColor;
|
||||
final double? strokeWidth;
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Circle (mirrors google_maps_flutter Circle)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
CircleOptions({
|
||||
required this.position,
|
||||
class Circle {
|
||||
const Circle({
|
||||
required this.circleId,
|
||||
required this.center,
|
||||
required this.radius,
|
||||
this.color,
|
||||
this.opacity,
|
||||
this.strokeColor,
|
||||
this.strokeWidth,
|
||||
this.fillColor = const Color(0x1A0D47A1),
|
||||
this.strokeColor = const Color(0xFF0D47A1),
|
||||
this.strokeWidth = 2,
|
||||
this.visible = true,
|
||||
this.zIndex = 0,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
maplibre.CircleOptions toMapLibreOptions() {
|
||||
return maplibre.CircleOptions(
|
||||
geometry: position,
|
||||
circleRadius: radius,
|
||||
circleColor: color ?? IntaleqColors.primaryBlue,
|
||||
circleOpacity: opacity ?? 0.6,
|
||||
circleStrokeColor: strokeColor,
|
||||
circleStrokeWidth: strokeWidth,
|
||||
final CircleId circleId;
|
||||
|
||||
/// Center of the circle.
|
||||
final mgl.LatLng center;
|
||||
|
||||
/// Radius in meters.
|
||||
final double radius;
|
||||
|
||||
/// Fill color (with alpha for opacity).
|
||||
final Color fillColor;
|
||||
|
||||
/// Stroke color.
|
||||
final Color strokeColor;
|
||||
|
||||
/// Stroke width in pixels.
|
||||
final int strokeWidth;
|
||||
|
||||
final bool visible;
|
||||
final int zIndex;
|
||||
|
||||
/// Called when the circle is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
Circle copyWith({
|
||||
mgl.LatLng? center,
|
||||
double? radius,
|
||||
Color? fillColor,
|
||||
Color? strokeColor,
|
||||
int? strokeWidth,
|
||||
bool? visible,
|
||||
int? zIndex,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
return Circle(
|
||||
circleId: circleId,
|
||||
center: center ?? this.center,
|
||||
radius: radius ?? this.radius,
|
||||
fillColor: fillColor ?? this.fillColor,
|
||||
strokeColor: strokeColor ?? this.strokeColor,
|
||||
strokeWidth: strokeWidth ?? this.strokeWidth,
|
||||
visible: visible ?? this.visible,
|
||||
zIndex: zIndex ?? this.zIndex,
|
||||
onTap: onTap ?? this.onTap,
|
||||
);
|
||||
}
|
||||
|
||||
/// Internal: converts to MapLibre CircleOptions.
|
||||
/// Note: MapLibre circle radius is in pixels, not meters. For a true
|
||||
/// meter-accurate circle, use [IntaleqMapController.addCircleAccurate].
|
||||
mgl.CircleOptions toCircleOptions() {
|
||||
return mgl.CircleOptions(
|
||||
geometry: center,
|
||||
circleRadius:
|
||||
radius / 10, // approximate; use addCircleAccurate for precision
|
||||
circleColor: _colorToHex(fillColor),
|
||||
circleOpacity: visible ? fillColor.opacity : 0.0,
|
||||
circleStrokeColor: _colorToHex(strokeColor),
|
||||
circleStrokeWidth: strokeWidth.toDouble(),
|
||||
circleStrokeOpacity: visible ? strokeColor.opacity : 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) => o is Circle && o.circleId == circleId;
|
||||
@override
|
||||
int get hashCode => circleId.hashCode;
|
||||
}
|
||||
|
||||
class PolygonOptions {
|
||||
/// The list of rings. The first ring is the outer boundary,
|
||||
/// and subsequent rings are holes.
|
||||
final List<List<maplibre.LatLng>> rings;
|
||||
final String? fillColor;
|
||||
final double? fillOpacity;
|
||||
final String? outlineColor;
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Polygon (mirrors google_maps_flutter Polygon)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
PolygonOptions({
|
||||
required this.rings,
|
||||
this.fillColor,
|
||||
this.fillOpacity,
|
||||
this.outlineColor,
|
||||
class Polygon {
|
||||
const Polygon({
|
||||
required this.polygonId,
|
||||
required this.points,
|
||||
this.holes = const [],
|
||||
this.fillColor = const Color(0x1ABDBDBD),
|
||||
this.strokeColor = const Color(0xFF0D47A1),
|
||||
this.strokeWidth = 2,
|
||||
this.visible = true,
|
||||
this.zIndex = 0,
|
||||
this.geodesic = false,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
maplibre.FillOptions toFillOptions() {
|
||||
return maplibre.FillOptions(
|
||||
final PolygonId polygonId;
|
||||
|
||||
/// Outer boundary of the polygon.
|
||||
final List<mgl.LatLng> points;
|
||||
|
||||
/// Holes inside the polygon (each is a list of LatLng).
|
||||
final List<List<mgl.LatLng>> holes;
|
||||
|
||||
/// Fill color.
|
||||
final Color fillColor;
|
||||
|
||||
/// Outline stroke color.
|
||||
final Color strokeColor;
|
||||
|
||||
/// Outline stroke width in pixels.
|
||||
final int strokeWidth;
|
||||
|
||||
final bool visible;
|
||||
final int zIndex;
|
||||
final bool geodesic;
|
||||
|
||||
/// Called when the polygon is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
Polygon copyWith({
|
||||
List<mgl.LatLng>? points,
|
||||
List<List<mgl.LatLng>>? holes,
|
||||
Color? fillColor,
|
||||
Color? strokeColor,
|
||||
int? strokeWidth,
|
||||
bool? visible,
|
||||
int? zIndex,
|
||||
bool? geodesic,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
return Polygon(
|
||||
polygonId: polygonId,
|
||||
points: points ?? this.points,
|
||||
holes: holes ?? this.holes,
|
||||
fillColor: fillColor ?? this.fillColor,
|
||||
strokeColor: strokeColor ?? this.strokeColor,
|
||||
strokeWidth: strokeWidth ?? this.strokeWidth,
|
||||
visible: visible ?? this.visible,
|
||||
zIndex: zIndex ?? this.zIndex,
|
||||
geodesic: geodesic ?? this.geodesic,
|
||||
onTap: onTap ?? this.onTap,
|
||||
);
|
||||
}
|
||||
|
||||
/// Internal: converts to MapLibre FillOptions.
|
||||
/// The first ring is the outer boundary; subsequent rings are holes.
|
||||
mgl.FillOptions toFillOptions() {
|
||||
final rings = [points, ...holes];
|
||||
return mgl.FillOptions(
|
||||
geometry: rings,
|
||||
fillColor: fillColor ?? IntaleqColors.fillGray,
|
||||
fillOpacity: fillOpacity ?? 0.4,
|
||||
fillOutlineColor: outlineColor ?? IntaleqColors.primaryBlue,
|
||||
fillColor: _colorToHex(fillColor),
|
||||
fillOpacity: visible ? fillColor.opacity : 0.0,
|
||||
fillOutlineColor: _colorToHex(strokeColor),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) => o is Polygon && o.polygonId == polygonId;
|
||||
@override
|
||||
int get hashCode => polygonId.hashCode;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Converts a Flutter [Color] to a CSS hex string (e.g. '#0D47A1').
|
||||
String _colorToHex(Color color) {
|
||||
return '#${color.red.toRadixString(16).padLeft(2, '0')}'
|
||||
'${color.green.toRadixString(16).padLeft(2, '0')}'
|
||||
'${color.blue.toRadixString(16).padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/foundation.dart' show VoidCallback;
|
||||
|
||||
/// Text labels and a tap callback shown when a [Marker] is tapped.
|
||||
///
|
||||
/// Mirrors `InfoWindow` from `google_maps_flutter`.
|
||||
class InfoWindow {
|
||||
const InfoWindow({
|
||||
this.title,
|
||||
this.snippet,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
/// Displayed as the window heading.
|
||||
final String? title;
|
||||
|
||||
/// Displayed below the title as secondary text.
|
||||
final String? snippet;
|
||||
|
||||
/// Called when the info window itself is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// An [InfoWindow] with no text (the default).
|
||||
static const InfoWindow noText = InfoWindow();
|
||||
|
||||
InfoWindow copyWith({
|
||||
String? title,
|
||||
String? snippet,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
return InfoWindow(
|
||||
title: title ?? this.title,
|
||||
snippet: snippet ?? this.snippet,
|
||||
onTap: onTap ?? this.onTap,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'InfoWindow(title: $title, snippet: $snippet)';
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:maplibre_gl/maplibre_gl.dart'
|
||||
show LatLngBounds, LatLng, CameraPosition;
|
||||
import '../intaleq_map_controller.dart';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// IntaleqMapType (mirrors MapType in google_maps_flutter)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// The visual style of the base map tiles.
|
||||
enum IntaleqMapType {
|
||||
/// No base map tiles.
|
||||
none,
|
||||
|
||||
/// Dark premium Obsidian style — the Intaleq default.
|
||||
normal,
|
||||
|
||||
/// High-contrast light style.
|
||||
light,
|
||||
|
||||
/// Satellite imagery with road labels overlaid.
|
||||
satellite,
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// MinMaxZoomPreference (identical to google_maps_flutter)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Preferred zoom level constraints for the camera.
|
||||
class MinMaxZoomPreference {
|
||||
const MinMaxZoomPreference(this.minZoom, this.maxZoom);
|
||||
|
||||
final double? minZoom;
|
||||
final double? maxZoom;
|
||||
|
||||
/// Unbounded zoom (default).
|
||||
static const MinMaxZoomPreference unbounded =
|
||||
MinMaxZoomPreference(null, null);
|
||||
|
||||
@override
|
||||
String toString() => 'MinMaxZoomPreference($minZoom, $maxZoom)';
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// CameraTargetBounds (identical to google_maps_flutter)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Constrains the camera target to a [LatLngBounds] region.
|
||||
class CameraTargetBounds {
|
||||
const CameraTargetBounds(this.bounds);
|
||||
|
||||
final LatLngBounds? bounds;
|
||||
|
||||
/// Unbounded (default).
|
||||
static const CameraTargetBounds unbounded = CameraTargetBounds(null);
|
||||
|
||||
@override
|
||||
String toString() => 'CameraTargetBounds($bounds)';
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Typedefs (same names as google_maps_flutter)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
typedef MapCreatedCallback = void Function(IntaleqMapController controller);
|
||||
typedef ArgumentCallback<T> = void Function(T argument);
|
||||
typedef CameraPositionCallback = void Function(CameraPosition position);
|
||||
@@ -1,13 +1,16 @@
|
||||
/// Predefined Intaleq Map tile style URLs.
|
||||
class IntaleqStyles {
|
||||
/// Dark premium map style (The default for Intaleq)
|
||||
IntaleqStyles._();
|
||||
|
||||
/// Dark premium Obsidian style — the Intaleq default.
|
||||
static String obsidian(String apiKey) =>
|
||||
'https://maps.intaleq.com/styles/obsidian/style.json?key=$apiKey';
|
||||
|
||||
/// Light premium map style
|
||||
/// High-contrast light style.
|
||||
static String light(String apiKey) =>
|
||||
'https://maps.intaleq.com/styles/light/style.json?key=$apiKey';
|
||||
|
||||
/// Satellite hybrid style
|
||||
/// Satellite imagery with road labels.
|
||||
static String satellite(String apiKey) =>
|
||||
'https://maps.intaleq.com/styles/satellite/style.json?key=$apiKey';
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'dart:math' show sin, cos, sqrt, atan2, pi;
|
||||
import 'package:maplibre_gl/maplibre_gl.dart' show LatLng;
|
||||
|
||||
/// Utility functions for working with encoded polylines and coordinates.
|
||||
class PolylineUtils {
|
||||
/// Decode a Google-encoded polyline string into a list of LatLng coordinates.
|
||||
PolylineUtils._();
|
||||
|
||||
// ── Encoding / Decoding ────────────────────────────────────
|
||||
|
||||
/// Decodes a [Google-encoded polyline](https://developers.google.com/maps/documentation/utilities/polylinealgorithm)
|
||||
/// string into a list of [LatLng] coordinates.
|
||||
static List<LatLng> decode(String encoded) {
|
||||
List<LatLng> points = [];
|
||||
int index = 0, len = encoded.length;
|
||||
final points = <LatLng>[];
|
||||
int index = 0;
|
||||
final len = encoded.length;
|
||||
int lat = 0, lng = 0;
|
||||
|
||||
while (index < len) {
|
||||
@@ -14,7 +22,7 @@ class PolylineUtils {
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
|
||||
final dlat = (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
|
||||
lat += dlat;
|
||||
|
||||
shift = 0;
|
||||
@@ -24,11 +32,61 @@ class PolylineUtils {
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
|
||||
final dlng = (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
|
||||
lng += dlng;
|
||||
|
||||
points.add(LatLng(lat / 1E5, lng / 1E5));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
/// Encodes a list of [LatLng] points to a Google-encoded polyline string.
|
||||
static String encode(List<LatLng> points) {
|
||||
final result = StringBuffer();
|
||||
int prevLat = 0, prevLng = 0;
|
||||
|
||||
for (final point in points) {
|
||||
final lat = (point.latitude * 1E5).round();
|
||||
final lng = (point.longitude * 1E5).round();
|
||||
_encodeValue(result, lat - prevLat);
|
||||
_encodeValue(result, lng - prevLng);
|
||||
prevLat = lat;
|
||||
prevLng = lng;
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
static void _encodeValue(StringBuffer buf, int value) {
|
||||
var v = value < 0 ? ~(value << 1) : (value << 1);
|
||||
while (v >= 0x20) {
|
||||
buf.writeCharCode(((0x20 | (v & 0x1f)) + 63));
|
||||
v >>= 5;
|
||||
}
|
||||
buf.writeCharCode((v + 63));
|
||||
}
|
||||
|
||||
// ── Distance ───────────────────────────────────────────────
|
||||
|
||||
/// Returns the Haversine distance in **metres** between two [LatLng] points.
|
||||
static double distanceBetween(LatLng a, LatLng b) {
|
||||
const earthRadius = 6371000.0; // metres
|
||||
final dLat = _toRad(b.latitude - a.latitude);
|
||||
final dLng = _toRad(b.longitude - a.longitude);
|
||||
final sinDLat = sin(dLat / 2);
|
||||
final sinDLng = sin(dLng / 2);
|
||||
final h = sinDLat * sinDLat +
|
||||
cos(_toRad(a.latitude)) * cos(_toRad(b.latitude)) * sinDLng * sinDLng;
|
||||
return 2 * earthRadius * atan2(sqrt(h), sqrt(1 - h));
|
||||
}
|
||||
|
||||
/// Returns the total length of a polyline in metres.
|
||||
static double totalLength(List<LatLng> points) {
|
||||
double total = 0;
|
||||
for (int i = 0; i < points.length - 1; i++) {
|
||||
total += distanceBetween(points[i], points[i + 1]);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
static double _toRad(double deg) => deg * pi / 180.0;
|
||||
}
|
||||
|
||||
@@ -49,14 +49,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -73,14 +65,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -96,62 +80,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
geolocator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: geolocator
|
||||
sha256: f4efb8d3c4cdcad2e226af9661eb1a0dd38c71a9494b22526f9da80ab79520e5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.1.1"
|
||||
geolocator_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_android
|
||||
sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.2"
|
||||
geolocator_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_apple
|
||||
sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.13"
|
||||
geolocator_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_platform_interface
|
||||
sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.6"
|
||||
geolocator_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_web
|
||||
sha256: "102e7da05b48ca6bf0a5bda0010f886b171d1a08059f01bfe02addd0175ebece"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
geolocator_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_windows
|
||||
sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.5"
|
||||
get:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: get
|
||||
sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.3"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -176,14 +104,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -192,14 +112,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
latlong2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: latlong2
|
||||
sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.1"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -296,14 +208,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -373,14 +277,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.3"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
name: intaleq_maps
|
||||
description: Premium Flutter SDK for Intaleq Map Platform (Jordan & Syria).
|
||||
version: 1.0.0
|
||||
description: >
|
||||
Premium Flutter SDK for the Intaleq Map Platform (Jordan & Syria).
|
||||
A drop-in Google Maps Flutter replacement backed by MapLibre GL.
|
||||
version: 2.0.0
|
||||
homepage: https://intaleqapp.com
|
||||
repository: https://github.com/Hamza-Ayed/intaleq_maps
|
||||
issue_tracker: https://github.com/Hamza-Ayed/intaleq_maps/issues
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
@@ -12,12 +16,6 @@ dependencies:
|
||||
sdk: flutter
|
||||
maplibre_gl: ^0.19.0
|
||||
http: ^1.1.0
|
||||
geolocator: ^10.1.0
|
||||
get: ^4.6.6
|
||||
latlong2: ^0.9.0
|
||||
|
||||
repository: https://github.com/Hamza-Ayed/intaleq_maps
|
||||
issue_tracker: https://github.com/Hamza-Ayed/intaleq_maps/issues
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -25,4 +23,4 @@ dev_dependencies:
|
||||
lints: ^2.1.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
uses-material-design: true
|
||||
Reference in New Issue
Block a user