smap_flutter 1.1.0
smap_flutter: ^1.1.0 copied to clipboard
sMap for Flutter: maps from the sMap service in Flutter apps, with styles, labels in any language, clustered points, lines, areas and 3D buildings.
sMap for Flutter: smap_flutter #
Maps from the sMap service for Flutter apps on Android and iOS.
- Five colour schemes:
light,white,grayscale,dark,black - Labels in any language (BCP-47 tags such as
en,fr,zh-Hans); the device language by default - Switching scheme or language keeps every layer you added
- Points at any scale: clustered (tap a cluster to zoom in), circles or your own icons, labels
- Lines (colour, width, dashes) and areas (flat or extruded in 3D)
- 3D buildings
- Draw order, visibility, replacing data, fitting the camera to a layer, tap handlers
- Reads from the nearest service node and moves to the next one when a node fails; keeps working while the service address is down (kept node list, optional seed nodes)
- Short-lived tokens refreshed automatically, or a map key that ships inside the app
- Standard style layers and sources (Style Specification v8) passed through as they are, placed in named basemap slots
- The basemap shown, hidden and recoloured by class (water, parks, roads by level, labels, …), and saved custom styles
- Files of other tools read on the device: KML, KMZ, GPX, encoded polylines, WKT, WKB, CSV, TopoJSON, GeoJSON; any layer written back as GeoJSON or KML
- External layers: raster XYZ, TileJSON, OGC WMS and WMTS, vector tiles and PMTiles, ArcGIS REST services
- One layer description shared with the other sMap SDKs: a map built on the web opens the same in the app
The map's attribution button stays visible: the map data licence requires it. Every map shows a small sMap badge in its bottom-right corner, just left of the attribution button.
Installation #
From pub.dev:
dependencies:
smap_flutter: ^1.1.0
Or straight from the repository:
dependencies:
smap_flutter:
git:
url: https://github.com/Goodneo-Solutions/smap-sdk
path: flutter
ref: 1.1.0
Requirements: Flutter 3.41 or later, Android minSdk 24 (the Flutter default) with the INTERNET permission,
iOS 15 or later.
A minimal map #
import 'package:smap_flutter/smap_flutter.dart';
SMap(
options: SMapOptions(
getToken: () => myServer.mapToken(), // see "Tokens"
style: SMapStyle.light,
center: const LatLng(43.6532, -79.3832),
zoom: 11,
),
onReady: (map) async {
await map.addPoints(
'stores',
[const SMapPoint(43.65, -79.38, properties: {'name': 'Queen St'})],
const SMapPointOptions(cluster: true, label: 'name'),
);
map.onTap('stores', (properties, position) => print(properties['name']));
},
)
Layers may also be added before the map is ready (in onCreated); they are drawn as soon as it is.
Tokens #
The service uses short-lived map tokens issued by your server. Give the map either:
token: a token you already have, orgetToken: aFuture<String> Function()that fetches one. It is called at start when notokenis given, and again two minutes before each token expires (a failed call is retried after 30 seconds).
A new token reloads the style with it; your layers are put back automatically. Never ship the secret that signs tokens inside an app: issue tokens from your server.
Controller #
| Call | What it does |
|---|---|
ready |
Future that completes once the first style has loaded |
setStyle(style) / setLanguage(tag) |
Switch scheme (SMapStyle, its name, or 'custom:<id>') or label language; layers are kept |
setBuildings3D(on, color:, opacity:) |
Buildings in 3D below your layers; tilts a flat map to 55° |
addPoints(id, points, SMapPointOptions) |
GeoJSON or a list of SMapPoint / {lat, lng, properties} maps |
addLines(id, lines, SMapLineOptions) |
GeoJSON or a list of SMapLine |
addPolygons(id, polygons, SMapPolygonOptions) |
GeoJSON or a list of SMapPolygon; extrude draws them in 3D |
addImage(name, bytes) |
Registers an image for the icon of points |
setData(id, data) / setVisible(id, visible) / removeLayer(id) |
Change a layer |
layers() |
Layer ids in draw order, bottom first |
fitLayer(id, padding: 40, maxZoom: 15) |
Show all of a layer's data |
onTap(id, handler) |
handler(properties, LatLng) for a tapped feature |
flyTo(center, zoom:) |
Move the camera |
engine |
The underlying map controller, for anything not wrapped |
Draw order: a higher zIndex draws on top; equal values draw in the order added. A layer with a slot is drawn at
that place in the basemap instead (see "Slots"); a layer without one draws on top, as before. Errors are raised as
SMapException; errors with no caller (token refresh, style reloads) go to the widget's onError.
Style layers and sources #
Layers and sources written for the Style Specification v8 (copied from Mapbox Studio, Maputnik or a web map) are passed through unchanged:
await map.addSource('parcels', {'type': 'vector', 'url': 'https://example.com/parcels.json'});
await map.addStyleLayer({
'id': 'parcels-fill',
'type': 'fill',
'source': 'parcels',
'source-layer': 'parcels',
'paint': {'fill-color': '#f97316', 'fill-opacity': 0.3},
}, slot: SMapSlot.labels);
await map.setPaint('parcels-fill', 'fill-opacity', 0.5);
await map.setFilter('parcels-fill', ['==', ['get', 'zone'], 'R1']);
| Call | What it does |
|---|---|
addSource(id, source) / removeSource(id) |
A source (vector, raster, raster-dem, geojson, image) |
addStyleLayer(layer, slot:, before:, zIndex:) |
A layer, in slot or below before (a layer id), else on top in the order added; source may be inline |
setPaint(layerId, name, value) / setLayout(…) / setFilter(layerId, filter) |
Change a style layer, yours or the basemap's (the look of addPoints / addLines / addPolygons layers is set with their options) |
getStyleLayer(layerId) |
The layer as it stands now |
sources() |
The sources added so far |
removeLayer(id) |
Removes a layer of any kind (with the source an external-layer helper made for it), or a source |
Style layers, sources and every change made with setPaint, setLayout and setFilter (to your layers and to the
basemap's) are put back after every style, language or token change.
Slots #
SMapSlot.bottom (over land and water, under everything else), roads (under the roads), labels (under the
labels) and top. addPoints, addLines and addPolygons take slot (or before) in their options too; inside a
slot a higher zIndex draws on top. A layer without a slot draws as before: on top, in the order added. The slots
follow the basemap when its layers change; slots() says where each one begins.
Basemap #
await map.setBasemap({
'water': {'color': '#9cc0e6'},
'roads': {'color': '#ffffff', 'width': 1.2},
'roads.highway': {'color': '#fde68a'},
'pois': {'visible': false},
'labels': {'halo': '#000000'},
});
Classes: land, landcover, parks, landuse, airports, water, roads.paths, roads.minor, roads.major,
roads.highway, buildings, rail, boundaries, labels.addresses, labels.water, labels.roads,
labels.places, pois; roads and labels set every class below them (a class's own settings win). Each takes
visible, color, casing (road edges), halo (labels), opacity (0–1) and width (a factor, 0.25–4). An
unknown class or setting is refused with an SMapException that names it. setBasemap replaces the previous
settings ({} clears them), basemap() reads them back, and they stay across style and language changes. The
basemap option applies them from the first style on.
Saved custom styles from the Open Platform portal: SMapOptions(customStyle: 'custom:sms_…') or
setStyle('custom:sms_…').
Files #
final collection = formats.read(kmlText); // format guessed; or format: 'gpx', name: 'trip.gpx'
await map.addData('trip', gpxText, lines: const SMapLineOptions(width: 5));
final kml = map.toKML('trip:lines');
| Call | What it does |
|---|---|
formats.read(input, format:, lat:, lng:, name:) |
Text or bytes as a GeoJSON FeatureCollection: kml, kmz, gpx, polyline, polyline6, wkt, wkb, csv, topojson, geojson (from name's extension or the content when not given; polylines are never guessed; lat / lng name CSV columns) |
formats.write(collection, format:) |
geojson or kml text |
addData(id, input, …) |
Reads and draws: areas as id:areas, lines as id:lines, points as id; removeLayer(id) and toGeoJSON(id) take them together |
toGeoJSON(layerId) / toKML(layerId) |
A layer's data out again |
convert(input, format:) |
The service converts the file (up to 20 MB) |
KML styles become simplestyle properties (marker-color, stroke, stroke-width, stroke-opacity, fill,
fill-opacity). Simplestyle properties in any data are drawn when the layer gives no colour, width or opacity of its
own, so files from geojson.io and similar tools look as they do there. marker-symbol is not drawn. Every SDK of the
family reads a file into the same features.
External layers #
| Call | What it does |
|---|---|
addRasterLayer(id, tiles: | url:, tileSize:, attribution:, opacity:, slot:, …) |
Raster {z}/{x}/{y} tiles or a TileJSON |
addWmsLayer(id, url:, layers:, styles:, format:, transparent:, version:, params:, …) |
OGC WMS, one image per tile |
addWmtsLayer(id, url: | capabilities:, layer:, tileMatrixSet:, style:, format:, encoding:, …) |
OGC WMTS from its capabilities or a REST template |
addVectorTiles(id, tiles: | url: | pmtiles:, layers:, attribution:, …) |
Vector tiles or a PMTiles archive, drawn by your style layers |
addArcGisLayer(id, url:, layers:, attribution:, …) |
ArcGIS MapServer, ImageServer or VectorTileServer |
rasterUrl(style, lang:, retina:) |
The service's own raster tiles as a template (with your map key), for other tools |
Each helper adds a source and a style layer, both under id (vector tiles: the source under id, your layers under
their own ids), so setPaint, getStyleLayer and removeLayer work on them as on any style layer. Each source's
attribution is shown with the map data credit. WMTS layers are drawn when their tile matrix set is the Web Mercator
grid with matrices named by zoom level (3, or EPSG:3857:3); other sets are refused. Labels of ArcGIS vector
styles are drawn only in fonts the map service has, and their icons are left out. Google and Bing tiles are refused.
Layer description #
map.toJSON() describes the basemap (style, language, settings), the sources and every layer (with its data, options,
slot, order and visibility); map.loadLayers(json) adds them to another map. The other sMap SDKs read and write the
same description, so a map built on the web opens the same in the app; options this SDK does not draw are kept and
written back.
Map keys #
A map key (smk_…) can ship inside the app instead of a token from your server. Create one in the developer center
and list your app on its allowlist:
- Android: the package name and the SHA-256 of each signing certificate (debug, release, and the store's app signing
key when the store re-signs the app). Print it with
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android | grep SHA256, orapksigner verify --print-certs app-release.apk | grep SHA-256for a built app. - iOS: the bundle id.
SMap(options: const SMapOptions(apiKey: 'smk_…'))
The package reads the app's identity from the platform (package name and signing certificate on Android, bundle id on iOS) and sends it, with the key, on every request to the map service's nodes and to no other host; the app cannot set it itself. With a token too, both are sent and the token is checked first.
The map's tile requests go through one HTTP layer shared by every map in the app, which this package sets when it starts. An app that sets its own HTTP client or request headers for the map afterwards (including the global header functions of the map engine package) replaces it, and the map's tile requests no longer carry the app identity.
Options #
| Option | Default | What it does |
|---|---|---|
baseUrl |
https://tile.srsr.ca |
The service address; its node list is asked for first |
nodes |
none | Node URLs known in advance; their node list is asked for when baseUrl does not answer, so a first start works while it is down |
token / getToken |
none | See "Tokens" |
apiKey |
none | A map key, see "Map keys" |
style |
SMapStyle.light |
Colour scheme |
language |
device language | Label language |
nearest |
true |
Read from the nearest node (the measured order is remembered for an hour) |
center, zoom, bearing, pitch |
0, 0, 1, 0, 0 |
Initial camera |
The last node list received is kept for 30 days. When no node answers for its list (baseUrl, then every other
known node in turn), the kept list and nodes are measured instead, so the map keeps working while the service
address is down. An order found that way is used for the current run only and is not remembered; the next start
asks for the node list again.
Example #
example/ is a one-screen demo: scheme and language switchers, 2,000 clustered points, a line, an extruded block
and a 3D buildings switch.
cd example
flutter run --dart-define=SMAP_TOKEN=sm1.… # or --dart-define=SMAP_API_KEY=smk_…