new change to use intaleq_map sdk v2 04-17-1

This commit is contained in:
Hamza-Ayed
2026-04-17 14:47:11 +03:00
parent 834c220d31
commit cc62019609
16 changed files with 1436 additions and 357 deletions
@@ -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}}
+21 -5
View File
@@ -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.
+153 -25
View File
@@ -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(
// In your State:
Set<Marker> _markers = {};
// Add a marker — just setState:
setState(() {
_markers = {
Marker(
markerId: MarkerId('driver_1'),
position: LatLng(33.5138, 36.2765),
iconImage: "marker-icon",
));
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).
Binary file not shown.
+16 -4
View File
@@ -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';
IntaleqColors._();
/// Cyan used for remaining route lines.
static const String routeCyan = '#00e5ff';
// ── Flutter Color constants ────────────────────────────────
/// Gray used for traveled paths or alternatives.
static const String softGray = '#BDBDBD';
/// Primary Intaleq blue.
static const Color primaryBlue = Color(0xFF0D47A1);
/// Semi-transparent gray for polygon fills.
static const String fillGray = '#BDBDBD33'; // 20% opacity
/// 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;
// ── Internal object registries ─────────────────────────────
/// 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().
}
}
// ── 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);
}
// 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);
}
}
}
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);
}
}
}
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);
}
}
}
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);
}
}
}
// ── 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();
}
// ── 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,
});
// --- API Integrations ---
/// 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);
}
throw Exception('Failed to search');
final res = await http.get(uri);
if (res.statusCode == 200) return jsonDecode(res.body) as List<dynamic>;
throw Exception('Geocoding request failed: ${res.statusCode}');
}
/// 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);
}
throw Exception('Failed to fetch route');
/// 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}');
}
// --- Friendly Abstractions (Transformation Layer) ---
/// Add a marker (friendly wrapper for addSymbol)
Future<Symbol> addMarker(MarkerOptions options) async {
return await mapController.addSymbol(options.toSymbolOptions());
/// 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}');
}
/// Add multiple markers
Future<List<Symbol>> addMarkers(List<MarkerOptions> optionsList) async {
final symbols = <Symbol>[];
for (var options in optionsList) {
symbols.add(await addMarker(options));
}
return symbols;
}
// ── Bitmap loader helper ───────────────────────────────────
/// Remove a marker
Future<void> removeMarker(Symbol marker) async {
await mapController.removeSymbol(marker);
}
final Set<String> _loadedImages = {};
/// Add a polyline (friendly wrapper for addLine)
Future<Line> addPolyline(PolylineOptions options) async {
return await mapController.addLine(options.toLineOptions());
}
Future<void> _loadBitmapIfNeeded(InlqBitmap bitmap) async {
final id = bitmap.mapLibreImageId;
if (_loadedImages.contains(id)) return;
/// Remove a polyline
Future<void> removePolyline(Line polyline) async {
await mapController.removeLine(polyline);
}
/// Add a circle
Future<Circle> addCircle(CircleOptions options) async {
return await mapController.addCircle(options.toMapLibreOptions());
}
/// Remove a circle
Future<void> removeCircle(Circle circle) async {
await mapController.removeCircle(circle);
}
/// Add a polygon (Fill)
Future<Fill> addPolygon(PolygonOptions options) async {
return await mapController.addFill(options.toFillOptions());
}
/// Remove a polygon
Future<void> removePolygon(Fill polygon) async {
await mapController.removeFill(polygon);
}
/// Clear all map objects
void clearAll() {
mapController.clearSymbols();
mapController.clearLines();
mapController.clearCircles();
mapController.clearFills();
}
// --- Camera Controls ---
void animateCameraTo(LatLng position, {double? zoom}) {
mapController.animateCamera(CameraUpdate.newLatLngZoom(position, zoom ?? mapController.cameraPosition?.zoom ?? 14.0));
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)';
}
+409 -77
View File
@@ -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({
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 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)';
}
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.iconImage,
this.iconSize,
this.iconRotate,
this.textField,
this.textOffset,
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.SymbolOptions toSymbolOptions() {
return maplibre.SymbolOptions(
geometry: position,
iconImage: iconImage,
iconSize: iconSize,
iconRotate: iconRotate,
textField: textField,
textOffset: textOffset != null ? Offset(0, textOffset!) : null,
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,
);
}
class PolylineOptions {
final List<maplibre.LatLng> coordinates;
final String? color;
final double? width;
final double? opacity;
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';
}
PolylineOptions({
required this.coordinates,
this.color,
this.width,
this.opacity,
@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,
});
maplibre.LineOptions toLineOptions() {
return maplibre.LineOptions(
geometry: coordinates,
lineColor: color ?? IntaleqColors.primaryBlue,
lineWidth: width ?? 5.0,
lineOpacity: opacity ?? 1.0,
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,
);
}
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;
@override
bool operator ==(Object o) => o is Circle && o.circleId == circleId;
@override
int get hashCode => circleId.hashCode;
}
PolygonOptions({
required this.rings,
this.fillColor,
this.fillOpacity,
this.outlineColor,
// ─────────────────────────────────────────────────────────────
// Polygon (mirrors google_maps_flutter Polygon)
// ─────────────────────────────────────────────────────────────
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);
+6 -3
View File
@@ -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;
}
-104
View File
@@ -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:
+6 -8
View File
@@ -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: