Skip to main content
Version: Next

MapView

API Reference: UI.MapView

MapView is a view to display native maps (Apple Maps on iOS and Google Maps on Android).

Basic MapView

In this example, lazy loading is also enabled on onShow. Note that this is Android only property. For more information please refer here.

note

The components in the example are added from the code for better showcase purposes. To learn more about the subject you can refer to:

Adding Component From Code

As a best practice, Smartface recommends using the WYSIWYG editor in order to add components and styles to your page or library. To learn how to use UI Editor better, please refer to this documentation

UI Editor Basics
scripts/pages/pageSample.ts
import PageSampleDesign from "generated/pages/pageSample";
import { Route, Router } from "@smartface/router";
import { styleableComponentMixin } from "@smartface/styling-context";
import MapView from "@smartface/native/ui/mapview";
import Application from "@smartface/native/application";
import System from "@smartface/native/device/system";

class StyleableMapView extends styleableComponentMixin(MapView) {}

interface MapPoint {
latitude: number;
longitude: number;
title?: string;
description?: string;
phone?: string;
}

const CenterMapCoordinates: MapPoint = Object.freeze({
latitude: 37.4488259,
longitude: -122.1600047,
description: "2nd Floor, 530 Lytton Ave, Palo Alto, CA 94301",
title: "Smartface Inc.",
});

const DEFAULT_ZOOM_LEVEL = 8;

//You should create new Page from UI-Editor and extend with it.
export default class Sample extends PageSampleDesign {
myMapView: StyleableMapView;

constructor(private router?: Router, private route?: Route) {
super({});
}

initMapView() {
this.myMapView = new StyleableMapView({
flexGrow: 1,
});
this.myMapView.setCenterLocationWithZoomLevel(
CenterMapCoordinates,
DEFAULT_ZOOM_LEVEL,
false
);
this.myMapView.minZoomLevel = DEFAULT_ZOOM_LEVEL - 2;
this.myMapView.maxZoomLevel = DEFAULT_ZOOM_LEVEL + 2;
this.addChild(this.myMapView, "myMapView", ".mapView", {
height: null,
left: 0,
top: 0,
right: 0,
bottom: 0,
flexProps: {
positionType: "ABSOLUTE",
},
});
}

onShow() {
super.onShow();

const { headerBar } =
System.OS === System.OSType.ANDROID ? this : this.parentController;
Application.statusBar.visible = false;
headerBar.visible = false;

// Enable lazy loading on Android
this.myMapView.android.prepareMap();
}

onLoad() {
super.onLoad();
this.initMapView();
}
}

MapView with Pins

note

The components in the example are added from the code for better showcase purposes. To learn more about the subject you can refer to:

Adding Component From Code

As a best practice, Smartface recommends using the WYSIWYG editor in order to add components and styles to your page or library. To learn how to use UI Editor better, please refer to this documentation

UI Editor Basics
scripts/pages/pageSample.ts
import PageSampleDesign from "generated/pages/pageSample";
import { Route, Router } from "@smartface/router";
import { styleableComponentMixin } from "@smartface/styling-context";
import MapView from "@smartface/native/ui/mapview";
import Color from "@smartface/native/ui/color";
import Application from "@smartface/native/application";
import System from "@smartface/native/device/system";

class StyleableMapView extends styleableComponentMixin(MapView) {}

type locationType = { latitude: number; longitude: number };

const DEFAULT_ZOOM_LEVEL = 8;

const CenterMapCoordinates: locationType = Object.freeze({
latitude: 37.4488259,
longitude: -122.1600047,
});

export default class PageSample extends PageSampleDesign {
myMapView: StyleableMapView;
constructor(private router?: Router, private route?: Route) {
super({});
}

initMapView() {
this.myMapView = new StyleableMapView({
flexGrow: 1,
});

this.addChild(this.myMapView, "myMapView", ".sf-mapView", {
height: null,
left: 0,
top: 0,
right: 0,
bottom: 0,
flexProps: {
positionType: "ABSOLUTE",
},
});
}

setPin() {
for (let i = 0; i < 10; i++) {
const myPin = new MapView.Pin({
location: {
latitude: 37.4488259 + i * 0.01,
longitude: -122.1600047,
},
title: `Title ${i}`,
});
myPin.subtitle = "subtitle";
myPin.color = Color.RED;
myPin.on("press", () => {
console.info("Title : ", myPin.title);
});
this.myMapView.addPin(myPin);
}
this.myMapView.setCenterLocationWithZoomLevel(
CenterMapCoordinates,
DEFAULT_ZOOM_LEVEL,
false
);
}

onShow() {
super.onShow();
const { headerBar } =
System.OS === System.OSType.ANDROID ? this : this.parentController;
Application.statusBar.visible = false;
headerBar.visible = false;

// Enable lazy loading on Android
this.myMapView.android.prepareMap();
this.setPin();
}
onLoad() {
super.onLoad();
this.initMapView();
}
}

MapView with Cluster

Cluster that groups two or more distinct pins into a single entity. Cluster works on Android & iOS 11.0+.

note

The components in the example are added from the code for better showcase purposes. To learn more about the subject you can refer to:

Adding Component From Code

As a best practice, Smartface recommends using the WYSIWYG editor in order to add components and styles to your page or library. To learn how to use UI Editor better, please refer to this documentation

UI Editor Basics
scripts/pages/pageSample.ts
import PageSampleDesign from "generated/pages/pageSample";
import Application from "@smartface/native/application";
import MapView from "@smartface/native/ui/mapview";
import Color from "@smartface/native/ui/color";
import Font from "@smartface/native/ui/font";
import { Route, Router } from "@smartface/router";
import { styleableComponentMixin } from "@smartface/styling-context";
import Pin from "@smartface/native/ui/mapview/pin";
import System from "@smartface/native/device/system";

class StyleableMapView extends styleableComponentMixin(MapView) {}

type locationType = { latitude: number; longitude: number };
//You should create new Page from UI-Editor and extend with it.
export default class Sample extends PageSampleDesign {
myMapView: StyleableMapView;
private disposeables: (() => void)[] = [];
constructor(private router?: Router, private route?: Route) {
super({});
}

averageGeolocation(pins: Pin[]): locationType {
if (pins.length === 1) {
return pins[0].location;
}
let x = 0.0;
let y = 0.0;
let z = 0.0;

pins.forEach((pin) => {
let latitude = (pin.location.latitude * Math.PI) / 180;
let longitude = (pin.location.longitude * Math.PI) / 180;

x += Math.cos(latitude) * Math.cos(longitude);
y += Math.cos(latitude) * Math.sin(longitude);
z += Math.sin(latitude);
});

const total = pins.length;

x = x / total;
y = y / total;
z = z / total;

const centralLongitude = Math.atan2(y, x);
const centralSquareRoot = Math.sqrt(x * x + y * y);
const centralLatitude = Math.atan2(z, centralSquareRoot);

return {
latitude: (centralLatitude * 180) / Math.PI,
longitude: (centralLongitude * 180) / Math.PI,
};
}

initMapView() {
this.myMapView = new StyleableMapView({
flexGrow: 1,
onCreate: () => {
for (let i = 0; i < 10; i++) {
const myPin = new MapView.Pin({
location: {
latitude: 37.4488259 + i * 0.01,
longitude: -122.1600047,
},
title: `Title ${i}`,
subtitle: "Subtitle",
color: Color.RED,
});
myPin.subtitle = "subtitle";
myPin.color = Color.RED;
this.disposeables.push(
myPin.on("press", () => {
console.log("Title : " + i);
})
);
this.myMapView.addPin(myPin);
}
},
});
const centerLocation = {
latitude: 37.4488259,
longitude: -122.1600047,
};
this.myMapView.setCenterLocationWithZoomLevel(centerLocation, 12, false);

this.myMapView.clusterEnabled = true;
this.myMapView.clusterFillColor = Color.RED;
this.myMapView.clusterBorderColor = Color.WHITE;
this.myMapView.clusterTextColor = Color.WHITE;
this.myMapView.clusterFont = Font.create(Font.DEFAULT, 20, Font.BOLD);

this.myMapView.onClusterPress = (pins: Pin[]) => {
var centerLocation = this.averageGeolocation(pins);
this.myMapView.setCenterLocationWithZoomLevel(centerLocation, 12, true);
};

this.addChild(this.myMapView, "myMapView", ".mapView", {
height: null,
left: 0,
top: 0,
right: 0,
bottom: 0,
flexProps: {
positionType: "ABSOLUTE",
},
});
}

onShow() {
super.onShow();
const { headerBar } =
System.OS === System.OSType.ANDROID ? this : this.parentController;
Application.statusBar.visible = false;
headerBar.visible = false;

// Enable lazy loading on Android
this.myMapView.android.prepareMap();
}
onLoad() {
super.onLoad();
this.initMapView();
}
}
Removing all pins at once

This can be achieved by calling myMap.removeAllPins(); method. No map reloading is needed.

Adding pins

Pins must be added after onCreate event is triggered.

Zoom Level

Zoom level value behavior is different from the Android and iOS implementations.

On Android, the zoom level is a number between 0 and 20. Default is 10.

The following list shows the approximate level of detail you can expect to see at each zoom level:

  • 1: World
  • 5: Landmass/continent
  • 10: City
  • 15: Streets
  • 20: Buildings

MapView With LazyLoad and Services

When using a service that returns coordinates and relevant information about the location, the common mobile way is to show them on the map. Since returning all of the locations will increase the response time, most of the services will provide partial load according to the coordinates the user is currently at. To achieve that, the services usually follow these two methods:

  • [Recommended] Taking coordinates of 4 corners, return an array of coordinates
  • Taking a coordinate and radius as parameter, returning an array of coordinates

In this documentation, as the mobile side we will see through how to provide those parameters.

Partial MapView Pin Load with 4 Corners

Since this method doesn't care about zoom levels or radius, it will be more robust and provide a better result than taking the radius or giving a hard-coded radius.

In order to accomplish this, we need to prepare a few functions beforehand

  • A function to check if selected coordinate is within the realm
  • A function or mechanism to check for duplicate pins
  • An event to listen when the camera is scrolling or ended scrolling

The idea is simply take x-axis(latitude) and y-axis(longitude) of relevant corners and compare it with our current pin.

import MapView from "@smartface/native/ui/mapview";

function checkIfInsideRegion(pin: MapView.Pin): boolean {
const xCoordinateLower = this.map.visibleRegion.bottomLeft.latitude;
const xCoordinateHigher = this.map.visibleRegion.topRight.latitude;
const yCoordinateLower = this.map.visibleRegion.bottomLeft.longitude;
const yCoordinateHigher = this.map.visibleRegion.topRight.longitude;

const isLatInside =
pin.location.latitude >= xCoordinateLower &&
pin.location.latitude <= xCoordinateHigher;
const isLngInside =
pin.location.longitude >= yCoordinateLower &&
pin.location.longitude <= yCoordinateHigher;

return isLatInside && isLngInside;
}

Then, we can apply the duplicate mechanism as the same as below. Afterwards, our code will look like this:

scripts/pages/pageSample.ts
import PageSampleDesign from "generated/pages/pageSample";
import { Route, Router } from "@smartface/router";
import { styleableComponentMixin } from "@smartface/styling-context";
import MapView from "@smartface/native/ui/mapview";
import Application from "@smartface/native/application";
import System from "@smartface/native/device/system";
import Pin from "@smartface/native/ui/mapview/pin";

class StyleableMapView extends styleableComponentMixin(MapView) {}

const MAP_RANDOM_RANGE = 1;
const DEFAULT_ZOOM_LEVEL = 8;

interface MapPoint {
lat: number;
lng: number;
title?: string;
description?: string;
phone?: string;
}

const CenterMapCoordinates: MapPoint = Object.freeze({
description: "2nd Floor, 530 Lytton Ave, Palo Alto, CA 94301",
lat: 37.4488259,
lng: -122.1600047,
title: "Smartface Inc.",
});

//You should create new Page from UI-Editor and extend with it.
export default class Sample extends PageSampleDesign {
myMapView: StyleableMapView;
allPins: Pin[] = this.generateMockMapData();
addedPins: Pin[] = []; // This is for duplicate prevention
constructor(private router?: Router, private route?: Route) {
super({});
}

randomizeCoordinates(centerPoint: MapPoint): MapPoint {
const randomLatitude =
CenterMapCoordinates.lat +
Randomize.randomSign() * Randomize.randomPositive(MAP_RANDOM_RANGE);
const randomLongitude =
CenterMapCoordinates.lng +
Randomize.randomSign() * Randomize.randomPositive(MAP_RANDOM_RANGE);
return {
...centerPoint,
lat: randomLatitude,
lng: randomLongitude,
};
}

generateMockMapData(): Pin[] {
const randomizedArray = Array.from({ length: 50 }).map(() => {
const randomized = this.randomizeCoordinates(CenterMapCoordinates);
return new MapView.Pin({
location: {
latitude: randomized.lat,
longitude: randomized.lng,
},
title: randomized.title || "",
});
});
return randomizedArray;
}
initMapView() {
this.myMapView = new StyleableMapView({
flexGrow: 1,
});
this.myMapView.setCenterLocationWithZoomLevel(
{
longitude: CenterMapCoordinates.lng,
latitude: CenterMapCoordinates.lat,
},
DEFAULT_ZOOM_LEVEL,
true
);
this.myMapView.minZoomLevel = DEFAULT_ZOOM_LEVEL - 2;
this.myMapView.maxZoomLevel = DEFAULT_ZOOM_LEVEL + 2;
this.myMapView.onCameraMoveEnded = () => this.addPinsWithLazyLoad();
this.addChild(this.myMapView, "myMapView", ".mapView", {
height: null,
left: 0,
top: 0,
right: 0,
bottom: 0,
flexProps: {
positionType: "ABSOLUTE",
},
});
}

checkForDuplicate(pin: Pin): boolean {
const doesCurrentPinAdded = this.addedPins.find((addedPin) => {
return (
pin.location.latitude === addedPin.location.latitude &&
pin.location.longitude === addedPin.location.longitude
);
});
return !!doesCurrentPinAdded;
}

checkIfInsideRegion(pin: Pin): boolean {
const xCoordinateLower = this.myMapView.visibleRegion.bottomLeft.latitude;
const xCoordinateHigher = this.myMapView.visibleRegion.topRight.latitude;
const yCoordinateLower = this.myMapView.visibleRegion.bottomLeft.longitude;
const yCoordinateHigher = this.myMapView.visibleRegion.topRight.longitude;

const isLatInside =
pin.location.latitude >= xCoordinateLower &&
pin.location.latitude <= xCoordinateHigher;
const isLngInside =
pin.location.longitude >= yCoordinateLower &&
pin.location.longitude <= yCoordinateHigher;

return isLatInside && isLngInside;
}

addPinsWithLazyLoad() {
this.allPins.forEach((pin) => {
if (this.checkForDuplicate(pin)) {
return; // Do not add the current pin if it's already there
}
if (this.checkIfInsideRegion(pin)) {
this.myMapView.addPin(pin); // Add the pin if inside of the current pin.
this.addedPins.push(pin);
}
});
}
onShow() {
super.onShow();
const { headerBar } =
System.OS === System.OSType.ANDROID ? this : this.parentController;
Application.statusBar.visible = false;
headerBar.visible = false;

// Enable lazy loading on Android
this.myMapView.android.prepareMap();
}

onLoad() {
super.onLoad();
this.initMapView();
}
}

class Randomize {
static randomSign(): number {
return 10 * Math.random() >= 5 ? 1 : -1;
}
static randomPositive(maxNumber = 1): number {
return Math.random() * maxNumber;
}
}

Partial MapView Pin Load with Radius

Before approaching this method, one should consider the zoom level on Smartface, since it will affect the visible radius range of the map.

In order to accomplish this, we need to prepare a few functions beforehand

  • A function to calculate the distance between two points
  • A function or mechanism to check for duplicate pins
  • An event to listen when the camera is scrolling or ended scrolling

Calculating the distance between two points is fairly easy:

interface MapPoint {
lat: number;
lng: number;
title?: string;
description?: string;
phone?: string;
}

/**
* Use basic geometry
*/
getDistanceFromLatLonInKm(point1: MapPoint, point2: MapPoint): number {
const deg2rad = (deg: number) => deg * (Math.PI / 180);
const R = 6371; // Radius of the earth in km
const dLat = deg2rad(point2.lat - point1.lat);
const dLon = deg2rad(point2.lng - point1.lng);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(deg2rad(point1.lat)) * Math.cos(deg2rad(point2.lng)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const d = R * c; // Distance in km
return d;
}

Checking for duplicate pins is also easy:

  checkForDuplicate(pin: Pin) {
const doesCurrentPinAdded = this.addedPins.find((addedPin) => {
return (
pin.lat === addedPin.location.latitude &&
pin.lng === addedPin.location.longitude
);
});
return !!doesCurrentPinAdded;
}

Our full code will look like this

scripts/pages/pageSample.ts
import PageSampleDesign from "generated/pages/pageSample";
import MapView from "@smartface/native/ui/mapview";
import { Route, Router } from "@smartface/router";
import Pin from "@smartface/native/ui/mapview/pin";
import { styleableComponentMixin } from "@smartface/styling-context";

class StyleableMapView extends styleableComponentMixin(MapView) {}

const MAP_RANDOM_RANGE = 1;
const DEFAULT_ZOOM_LEVEL = 8;

interface MapPoint {
lat: number;
lng: number;
title?: string;
description?: string;
phone?: string;
}

const CenterMapCoordinates: MapPoint = Object.freeze({
description: "2nd Floor, 530 Lytton Ave, Palo Alto, CA 94301",
lat: 37.4488259,
lng: -122.1600047,
title: "Smartface Inc.",
});

export default class Sample extends PageSampleDesign {
myMapView: StyleableMapView;
allPins: Pin[] = this.generateMockMapData();
addedPins: Pin[] = []; // This is for duplicate prevention
constructor(private router?: Router, private route?: Route) {
super({});
}
generateMockMapData(): Pin[] {
const randomizedArray = Array.from({ length: 50 }).map(() => {
const randomized = this.randomizeCoordinates(CenterMapCoordinates);
return new Pin({
location: {
latitude: randomized.lat,
longitude: randomized.lng,
},
title: randomized.title || "",
});
});
return randomizedArray;
}

randomizeCoordinates(centerPoint: MapPoint): MapPoint {
const randomLatitude =
CenterMapCoordinates.lat +
Randomize.randomSign() * Randomize.randomPositive(MAP_RANDOM_RANGE);
const randomLongitude =
CenterMapCoordinates.lng +
Randomize.randomSign() * Randomize.randomPositive(MAP_RANDOM_RANGE);
return {
...centerPoint,
lat: randomLatitude,
lng: randomLongitude,
};
}

initMapView() {
this.myMapView = new StyleableMapView({
flexGrow: 1,
});
this.myMapView.setCenterLocationWithZoomLevel(
{
longitude: CenterMapCoordinates.lng,
latitude: CenterMapCoordinates.lat,
},
DEFAULT_ZOOM_LEVEL,
true
);
this.myMapView.minZoomLevel = DEFAULT_ZOOM_LEVEL - 2;
this.myMapView.maxZoomLevel = DEFAULT_ZOOM_LEVEL + 2;
this.myMapView.onCameraMoveEnded = () => this.addPinsWithLazyLoad();
this.addChild(this.myMapView, "myMapView", ".mapView", {
height: null,
left: 0,
top: 0,
right: 0,
bottom: 0,
flexProps: {
positionType: "ABSOLUTE",
},
});
}

addPinsWithLazyLoad() {
// First, get the current distance between center and the corner of the visible map
const visibleRegions = this.myMapView.visibleRegion;
const bottomLeft: MapPoint = {
lat: visibleRegions.bottomLeft.latitude,
lng: visibleRegions.bottomLeft.longitude,
};
const center: MapPoint = {
lat: this.myMapView.centerLocation.latitude,
lng: this.myMapView.centerLocation.longitude,
};
const visibleDistance = this.getDistanceFromLatLonInKm(center, bottomLeft); //This variable is our 'radius'

this.allPins.forEach((pin) => {
if (this.checkForDuplicate(pin)) {
return; // Do not add the current pin if it's already there
}
const pinPoint: MapPoint = {
lat: pin.location.latitude,
lng: pin.location.longitude,
};
const currentDistance = this.getDistanceFromLatLonInKm(center, pinPoint);
if (currentDistance <= visibleDistance) {
this.myMapView.addPin(pin); // Add the pin if inside of the current pin.
this.addedPins.push(pin);
}
});
}
checkForDuplicate(pin: Pin) {
const doesCurrentPinAdded = this.addedPins.find((addedPin) => {
return (
pin.location.latitude === addedPin.location.latitude &&
pin.location.longitude === addedPin.location.longitude
);
});
return !!doesCurrentPinAdded;
}
/**
* Use basic geometry to calculate if the pins are in the imaginary circle
*/
getDistanceFromLatLonInKm(point1: MapPoint, point2: MapPoint): number {
const deg2rad = (deg: number) => deg * (Math.PI / 180);
const R = 6371; // Radius of the earth in km
const dLat = deg2rad(point2.lat - point1.lat);
const dLon = deg2rad(point2.lng - point1.lng);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(deg2rad(point1.lat)) *
Math.cos(deg2rad(point2.lng)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const d = R * c; // Distance in km
return d;
}
onShow() {
super.onShow();
this.myMapView.android.prepareMap();
}

onLoad() {
super.onLoad();
this.initMapView();
}
}

class Randomize {
static randomSign(): number {
return 10 * Math.random() >= 5 ? 1 : -1;
}
static randomPositive(maxNumber = 1): number {
return Math.random() * maxNumber;
}
}

In these examples, simple pin additions are used. The real world example will use service calls with relevant response times. In those cases, simply wrap this function with async and do the calculations in that instead:

async addPinsWithLazyLoad() {
const pins = await getPins(); // Your service call
pins.forEach((pin) => {
if (this.checkForDuplicate(pin)) {
return; // Do not add the current pin if it's already there
}
if (this.checkIfInsideRegion(pin)) {
this.map.addPin(pin); // Add the pin if inside of the current pin.
this.addedPins.push(pin);
}
});
}

Requirements for Android before publishing the application

Android Manifest

Smartface Android Emulator comes with its own Google Maps API-Keys. Before publishing your project, you must change that key from AndroidManifest.xml. Follow the guide to get a key.

Updating AndroidManifest.xml

  • Go to /config/Android/AndroidManifest.xml
  • Add the following code below under tag
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value=" ADD API-KEY HERE "
/>