Skip to main content

smap_flutter 1.5.8 copy "smap_flutter: ^1.5.8" to clipboard
smap_flutter: ^1.5.8 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

sMap for Flutter: smap_flutter #

Maps from the sMap service for Flutter apps on Android and iOS. The map is the sMap Android or iOS SDK's own, shown in the widget: it behaves exactly as in a native app.

  • 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
  • Your own widgets as markers (dragged after a long press), clusters as rings of colours or your own widgets, tooltips, labels along lines, heat maps, hillshading, satellite imagery and Google base maps, a layer switcher, synced maps, tiles you paint, glTF 3D models, a drawing tool and area selection

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.2.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, or
  • getToken: a Future<String> Function() that fetches one. It is called at start when no token is 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.

Map tools #

Give the map its tools; it gets a tools control (one button, the last of the map's toolbar) that shows or hides each of them, with Show all and Hide all:

SMap(
  options: SMapOptions(
    apiKey: 'smk_…',
    tools: SMapToolsOptions(hidden: savedHidden, onChange: (change) => save(change.hidden)),
  ),
  tools: [
    SMapTool.kind('zoom', SMapToolKind.zoom),
    SMapTool.kind('compass', SMapToolKind.compass),
    SMapTool.kind('scale', SMapToolKind.scale),
    SMapTool.layers('stops', ['stops', 'stop-labels'], label: 'Stops'),
    SMapTool(id: 'legend', label: 'Legend', position: SMapToolPosition.bottomLeft, child: const Legend()),
    SMapTool(id: 'orders', label: 'Orders list', position: SMapToolPosition.none,
        child: const Positioned(left: 0, top: 0, bottom: 0, child: OrdersList())),
  ],
)

controller.setToolVisible('legend', false);
controller.setToolsVisible(true);
controller.tools(); // [SMapToolState(zoom, Zoom, true), …]
  • SMapTool.kind makes zoom (+ / − buttons), compass (shown while the map is rotated; a tap turns north; it takes the engine compass's place), scale (metric, imperial where miles are used) and locate (centres the map on the device's position; it never asks for the location permission and reports a missing one to onError).
  • Your own widget goes in a corner, or with SMapToolPosition.none it is placed by you in the map's stack (give it a Positioned or an Align). Ids are 1–64 letters, digits, -, _ or .; an id given twice keeps the last.
  • A hidden tool stays built (Offstage) and keeps its state. The attribution, the sMap badge and the tools control are never tools.
  • Data layers as a tool: SMapTool.layers(id, [layer ids], label:) names layers you added (points, lines, polygons, style and external layers) or basemap layers. Hiding the tool hides them all, showing it shows them; it has no widget. A layer that is not on the map is left alone, one added later under the id of a hidden tool starts hidden, and a hidden tool that leaves the list shows its layers again.
  • The tools control sits by default in the toolbar (SMapToolPosition.toolbar): the last button of the corner of your first zoom tool, else of your first compass tool, else the top left, and it stays last when tools join that corner. Its corner is painted above the other corners and your own tool widgets; the panel opens towards the middle of the map. SMapToolsOptions(control: SMapToolPosition.topRight) (or another corner) places it there, as in 1.1.5.
  • SMapToolsOptions(control: null) leaves the tools control out; labels translates it.
  • The SDK keeps no state: give the starting state in hidden and keep onChange's hidden where you like.

Taps and piled points (1.5.5) #

A tap counts a point or custom marker only where it is drawn — a circle within its radius and stroke, an icon within its box, a marker within its widget — plus SMapOptions.tapTolerance (6 logical pixels). The one containing the tap answers first, then the nearest, the top one on a tie. When two or more contain the tap point, a small menu opens at the tap; choosing a row calls that layer's tap handler (onTap of the controller for point layers, the layer's onTap for custom markers) and the camera never moves.

await map.addPoints('stops', stops, SMapPointOptions(
  icon: 'pin',
  overlap: SMapOverlap(label: (f) => 'Stop ${f.properties['n']}'), // row text; default: label property, id, 1, 2, …
));
await map.addCustomMarkers('vans', vans, SMapCustomMarkerOptions(
  builder: (p) => VanPin(p),
  onTap: (f) => openVan(f.key),
  overlap: const SMapOverlap(enabled: false), // the top marker answers, no menu
));

For point layers the native map draws the menu; a label function is worked out in Dart for every feature when the data is sent (it sees the feature's key, properties and position, not the screen). Custom markers drawn as widgets list only among themselves: a pile of a marker and a native point answers the marker.

Many markers #

Custom markers are live widgets laid out over the map, smooth to a few thousand in view. For more, snapshot: true draws each look once into an image and the map draws the images like points (10,000 pan at full frame rate); taps, tooltips, dragging, opacity and zIndex still work, but each marker is a still picture (no animation, no input, no image still loading). Features that look the same share one image; snapshotKey says what makes two look the same (all properties by default). Point layers can do the same with your own images: iconProperty names the property holding each point's image of addImage (pixelRatio for images drawn at the screen's ratio).

await map.addCustomMarkers('stops', stops, SMapCustomMarkerOptions(
  builder: (p) => NumberedPin('${p['n']}'),
  anchor: SMapAnchor.bottom,
  snapshot: true,
  snapshotKey: (p) => '${p['n']}',
));

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, or apksigner verify --print-certs app-release.apk | grep SHA-256 for 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.

Offline regions #

An offline region is an area of the map kept on the device and shown without a network: a bounding box, a zoom range, a scheme or custom style and a label language, downloaded as one file of at most 500 MB. A download continues where it stopped when it is called again for the same region. A region is shown for 30 days from its download; after that its map data is removed until it is refreshed, and while a map is online it downloads again on its own the regions in their last 3 days. Downloads need a token, or a map key whose Allow offline regions switch is on, and are billed as the region's tiles; showing a region costs nothing. Android and iOS only.

final region = SMapOfflineRegion(id: 'downtown', west: -79.42, south: 43.63, east: -79.35, north: 43.68, maxZoom: 15);
final estimate = await controller.offline.estimate(region); // tiles, bytes, fits, allowed
if (estimate.allowed && estimate.fits) {
  await controller.offline.download(region, onProgress: (done, total) {});
  await controller.useOfflineRegion('downtown');            // controller.useOnline() reads the map service again
}

SMapOffline(options) works without a map. SMapOptions(offlineRegion: 'downtown') shows a region from the start; offlineFallback (on by default) shows a downloaded region containing the centre when no node answers, and offlineAutoRefresh (on by default) refreshes regions in their last days. list(), get(id), refresh(id), remove(id) and usage() manage what is kept.

Plan editor (1.1.6) #

A person edits a plan of routes on the map — drags a stop within or between routes, takes it off, places an unassigned job, re-orders a route around locked stops — and every edit is one call of the Open Platform's plan editing (open.superroute.ca/v1/plan/*) with the whole plan; the answer replaces what is drawn. The editor keeps no plan on any server: keep the plan the SMapPlanChange event gives you.

final controller = SMapController();
// /plan-token on your own server calls POST /v1/plan/token with its API token and hands the answer on.
late final editing = controller.planEditor(SMapPlanEditorOptions(
  credentials: SMapPlanCredentials.token(() async => (await http.get(Uri.parse('https://my.server/plan-token'))).body),
  plan: SMapPlan.fromJson(planJson),   // {vehicles, jobs, routes, region?, profile?, departed_at?}
));

SMap(controller: controller, tools: [editing.panelTool()]);   // the panel as the map tool "plan-editor"

editing.editor.addListener((e) {
  if (e is SMapPlanChange) save(e.plan!.toJson());
});

// From code (a superseded call gives null, a refusal throws SMapPlanException):
await editing.editor.move(['job-7'], 'van-2', position: 1);
await editing.editor.insert(['job-9']);   // an unassigned job; editor.choices holds the positions answered
await editing.editor.refine();            // around the locked stops
await editing.editor.undo();
  • The editor (SMapPlanEditor, usable without a map): setPlan, resequence, move, remove, dropStop, insert, choose, refine, recost, lock / unlock, undo / redo, preview, destroy; state() gives everything a panel shows. It is a Dart port of the Web SDK's editor and passes every case of the shared contract (shared/plan-contract.json), so every edit behaves as in the other sMap SDKs.
  • On the map (SMapPlanLayer, made by planEditor): legs (the answer's road lines, straight lines until answered), vehicle starts, numbered stops (dark red border with a violation or an unreachable leg, near black when locked) and unassigned jobs in grey, one colour per vehicle, as the layers plan:legs, plan:starts, plan:unassigned, plan:stop-borders, plan:stops and plan:drag. Long-press a stop and drag it: dropped on another stop it goes before it, on a route's line into it, on a start first; while it is dragged the drop is priced as a preview 250 ms after the target last changed. The map does not pan during the drag.
  • The panel (SMapPlanPanel, or editing.panelTool() for the map's tools) is made of Flutter widgets: Undo, Redo, Refine and the busy status, the error with its code and reason, the totals (and the preview's), each route with its stops (arrival, violations, Lock / Unlock, Remove; long-press a row and drop it before another stop, on another route, or on the unassigned jobs to take it off), the unassigned jobs (Place) and the positions an insert answered (Choose). It is in the map's language, one of 14 (SMapPlanTexts, the texts of shared/plan-texts.json); labels replaces any text. It needs an Overlay above it, which every app's navigator gives.
  • Credentials: SMapPlanCredentials.token(() => …) (the provider returns a sr_pet_… token or the token answer of POST /v1/plan/token, as a map or JSON text; renewed 30 s before it ends and after a 401 SESSION_EXPIRED), or SMapPlanCredentials.key('smk_…') (a map key with Allow plan editing switched on). Every call also names the app (X-SMap-App: android:<package> with X-SMap-App-Cert, or apple:<bundle id>), read from the platform by the plugin. Every other refusal is raised with its code and reason (SESSION_REVOKED, TOKEN_INVALID, NOT_AUTHORIZED …) and never repeated.
  • Fast path (no code change): when the token answer carries fast_path.url (readPlanFastPath), or the map key's configuration on sMap names one (GET <smapUrl>/plan/v1/config, kept 10 minutes; controller.planEditor uses the map's smapUrl, otherwise https://tile.srsr.ca), every call goes to <url>/<same path> with the same credential, headers and body. A 409 USE_STANDARD_PATH (or X-Plan-Path: standard), a 5xx, a network error or no answer within 2 s (fastPathTimeout; refine 5 s, fastPathRefineTimeout) repeats the same call once on the standard path; the next call tries the fast path again; 402, 429 and every other 4xx are final. state().path, SMapPlanChange.path and SMapPlanEditResult.path give SMapPlanPath.fast or .standard; fastPath: false keeps every call on the standard path.
  • Distance snapshot: the editor opens one per region and profile and names it in every call, opening another by itself when it expired; it never makes a call fail. snapshot: false turns it off.
  • Events (addListener or the events stream): SMapPlanChange, SMapPlanBusy, SMapPlanError, SMapPlanSnapshot, SMapPlanPreviewEvent, SMapPlanDestroy.
  • Drop points (OPEN-ROUTE-EDIT E25 / E26): editor.prepareInsert([...]) (points {'lat', 'lng'} or [lng, lat], jobs or ids) sends points about to be inserted to the open snapshot at once, so their legs are measured before the insert is confirmed. Call it when a new stop is dropped on the map (on release) or an address is picked in your own insert form; the editor does it itself when a job of no route is dropped on a route. The call is POST <baseUrl>/v1/plan/matrix/<id>/points, always on the standard path, each point once per snapshot, nothing without a snapshot; it never waits and never reports an error. An insert of such a point prepared less than insertWaitMinAge (300 ms) before is sent at once; later, while the point is not known to be measured, it first asks the server to wait for it (wait_ms = insertWaitBudget, 1100 ms, minus the time since the point was sent, within 100-1000 ms; the call given up after wait_ms + 200 ms, at most insertWaitTimeout, 1.2 s) and is sent then either way.

The camera, events, states and moving features (1.5.0) #

Every call goes over the view's channel to the Android or iOS SDK; values given as Dart functions are worked out in Dart per feature and sent as hidden __smap_… properties. Points on the screen are logical pixels from the map's top-left corner.

await map.fitBounds([-79.6, 43.5, -79.1, 43.9], options: const SMapFitOptions(maxZoom: 14, bearing: 30));
await map.fitLayers(['zones', 'route']);               // an addData group counts as its layers
final center = await map.getCenter();                  // getZoom, getBearing, getPitch, getBounds
await map.easeTo(const SMapCameraOptions(zoom: 12));   // jumpTo, zoomIn, zoomOut
final at = await map.project(const LatLng(43.65, -79.38));
map.on(SMapEventType.doubleClick, (e) => print(e.position), preventDefault: true); // the map does not zoom
map.events(SMapEventType.moveEnd).listen((e) => print(e.zoom));

await map.addPolygons('zones', zones, SMapPolygonOptions(
  idProperty: 'id',
  fillValue: SMapValue.of((p) => p['busy'] == true ? '#e11d48' : '#2563eb'),
  hoverState: true,
  states: const {'hover': SMapPolygonLook(opacity: 0.5), 'selected': SMapPolygonLook(fill: '#f97316')},
  onDoubleClick: (f, e) {}, onContextMenu: (f, e) {}, onHover: (f, e) {},
));
map.setFeatureState('zones', 'z1', {'selected': true});
final hits = await map.featuresAt(const Offset(120, 300), layers: ['zones']);
await map.openTooltip('zones', 'z1');
await map.moveFeature('trucks', 't1', next, duration: const Duration(seconds: 1), easing: Curves.easeInOut);
await map.updateFeature('trucks', 't1', {'heading': 90});
  • preventDefault is given when a listener is added: the native map decides before Dart hears of the event.
  • Custom markers are Flutter widgets: their onHover, onDoubleClick, onContextMenu, hoverState, onStateChange(key, state) (the widget is built again; read featureState in the builder), rotation and moveFeature (eased in Dart) work on them too.
  • featuresAt answers custom markers first, then the drawn layers' features. Before 1.5.0 featuresAt was the engine's own hit test, which is now engineFeaturesAt.
  • An easing curve is sent to the native map as 33 samples.

Images, circles, export, search and more (1.5.0) #

await map.addImage('arrow', pngBytes, pixelRatio: 2, sdf: true);   // shown as the app's, never a sprite's `arrow`
await map.addGlyphImage('p', const SMapGlyph('P', background: '#7c3aed', color: '#ffffff'));
await map.addPoints('places', places, SMapPointOptions(icon: 'arrow', iconColor: SMapValue.property('color'),
    label: 'name', priority: const SMapValue.property('rank'), rotation: const SMapValue.property('heading')));
await map.addCircles('reach', depots, const SMapCircleOptions(radius: SMapValue.property('metres')));
await map.addLines('route', lines, const SMapLineOptions(arrows: SMapLineArrows(share: 0.15)));
await map.addHeatmap('heat', points, const SMapHeatmapOptions(blur: 10, minOpacity: 0.3));
final png = await map.export(options: const SMapExportOptions(format: SMapExportFormat.jpeg));
map.addSearchTool('search', SMapSearchOptions(search: (q) async => myPlaces(q), onPick: (r) {}));
final area = SMapGeo.area(polygon); // centroid, pointOnFeature, circle, union, buffer, simplify, selfIntersects
map.on(SMapEventType.unavailable, (e) => print(e.reason));
  • SMapOptions(cooperativeGestures: true) for a map inside a scrolling screen: one finger scrolls the page (a tap, a double tap and a long press still reach the map), two fingers move it, and a hint shows when one finger tries.
  • SMapOptions(unavailableNotice: false) leaves out the card the native map shows when it cannot be shown.
  • SMapDrawOptions takes onDrawing (or the tool's drawing stream), measure, allowSelfIntersection, snapLayers, and a look per shape as styleProperty + styles (the native tool decides a shape's look while it draws, so the looks are data rather than a Dart function; measureFormat is not offered for the same reason); setProperties(id, properties) changes a shape.
  • export draws the custom markers and widget clusters over the native map's picture; your own widgets around the map are not included.
  • The search tool and the layer switcher are Flutter widgets: the search function runs in Dart, and a SMapLayersBase(label: 'Google', google: …) entry switches Google's map on (another base switches it off).
  • The SDK's own texts (sdkText) come from the native SDK's table in the map's language.

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_…
0
likes
0
points
509
downloads

Publisher

unverified uploader

Weekly Downloads

sMap for Flutter: maps from the sMap service in Flutter apps, with styles, labels in any language, clustered points, lines, areas and 3D buildings.

Homepage

Topics

#map #maps #vector-tiles #smap

License

unknown (license)

Dependencies

archive, clock, flutter, http, path_provider, shared_preferences

More

Packages that depend on smap_flutter

Packages that implement smap_flutter