Skip to main content
Version: 7.0.0

MapView

API Reference: UI.MapView

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

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 "
/>

Basic MapView

In this example, lazy loading is also enabled on Line 73. 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
import PageSampleDesign from "generated/pages/page3";
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 Font from "@smartface/native/ui/font";
import Application from "@smartface/native/application";


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: MapView.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
});
this.disposeables.push(
this.myMapView.on(MapView.Events.Create, () => {
const centerLocation = {
latitude: 37.4488259,
longitude: -122.1600047
};
this.myMapView.setCenterLocationWithZoomLevel(centerLocation, 12, false);
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;
this.disposeables.push(
myPin.on(MapView.Pin.Events.Press, () => {
console.info('Title : ', myPin.title);
})
);
this.myMapView.addPin(myPin);
}
})
);
this.myMapView.clusterEnabled = true;
this.myMapView.clusterFillColor = Color.RED;
this.myMapView.clusterBorderColor = Color.WHITE;
this.myMapView.ios.clusterBorderWidth = 3;
this.myMapView.clusterTextColor = Color.WHITE;
this.myMapView.clusterFont = Font.create(Font.DEFAULT, 20, Font.BOLD);
this.myMapView.ios.clusterPadding = 15;

this.disposeables.push(
this.myMapView.on(MapView.Events.ClusterPress, (pins) => {
const centerLocation = this.averageGeolocation(pins);
this.myMapView.setCenterLocationWithZoomLevel(centerLocation, 12, true);
})
);

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



// The page design has been made from the code for better
// showcase purposes. As a best practice, remove this and
// use WYSIWYG editor to style your pages.
centerizeTheChildrenLayout() {
this.dispatch({
type: "updateUserStyle",
userStyle: {
flexProps: {
flexDirection: 'ROW',
justifyContent: 'CENTER',
alignItems: 'CENTER'
}
}
})
}

onShow() {
super.onShow();
Application.statusBar.visible = false;
this.headerBar.visible = false;

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

onLoad() {
super.onLoad();
this.centerizeTheChildrenLayout();

this.initMapView();
}

onHide(): void {
this.dispose();
}
dispose(): void {
this.disposeables.forEach((item) => item());
}
}

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
import PageSampleDesign from 'generated/pages/pageSample';
import FlexLayout from '@smartface/native/ui/flexlayout';
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';

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: MapView.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: () => {
const centerLocation = {
latitude: 37.4488259,
longitude: -122.1600047
};
this.myMapView.setCenterLocationWithZoomLevel(centerLocation, 12, false);
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(MapView.Pin.Events.Press, () => {
console.log('Title : ' + myPin.index);
});)
this.myMapView.addPin(myPin);
}
}
});
this.myMapView.clusterEnabled = true;
this.myMapView.clusterFillColor = Color.RED;
this.myMapView.clusterBorderColor = Color.WHITE;
this.myMapView.ios.clusterBorderWidth = 3;
this.myMapView.clusterTextColor = Color.WHITE;
this.myMapView.clusterFont = Font.create(Font.DEFAULT, 20, Font.BOLD);
this.myMapView.ios.clusterPadding = 15;

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

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

onShow() {
super.onShow();
Application.statusBar.visible = false;
this.headerBar.visible = false;

// Enable lazy loading on Android
this.myMapView.android.prepareMap();
}
onLoad() {
super.onLoad();
this.initMapView();
}
onHide(): void {
this.dispose();
}
dispose(): void {
this.disposeables.forEach((item) => item());
}
}

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:

import PgMapViewRegionDesign from 'generated/pages/pgMapViewRegion';
import MapView from '@smartface/native/ui/mapview';
import { Route, Router } from '@smartface/router';

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 PgMapViewRegion extends PgMapViewRegionDesign {
allPins: MapView.Pin[] = this.generateMockMapData();
addedPins: MapView.Pin[] = []; // This is for duplicate prevention
private disposeables: (() => void)[] = [];
constructor(private router?: Router, private route?: Route) {
super({});
}
generateMockMapData(): MapView.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;
}

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.map.setCenterLocationWithZoomLevel(
{
longitude: CenterMapCoordinates.lng,
latitude: CenterMapCoordinates.lat
},
DEFAULT_ZOOM_LEVEL,
true
);
this.map.onCameraMoveEnded = () => this.addPinsWithLazyLoad();
this.map.minZoomLevel = DEFAULT_ZOOM_LEVEL - 1;
this.map.maxZoomLevel = DEFAULT_ZOOM_LEVEL + 1;
}

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.map.addPin(pin); // Add the pin if inside of the current pin.
this.addedPins.push(pin);
}
});
}
checkForDuplicate(pin: MapView.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: 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;
}

onShow() {
super.onShow();
}

onLoad() {
super.OnLoad();
this.initMapView();
}
onHide(): void {
this.dispose();
}
dispose(): void {
this.disposeables.forEach((item) => item());
}
}
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;
}

For duplicate checking, we can hold another array to track the added pins to the map like this. Let's implement the function. We will use checkForDuplicates function later.

import MapView from '@smartface/native/ui/mapview';
import PgMapViewDesign from 'generated/pages/pgMapView';
import { Route, Router } from '@smartface/router';

const MAP_RANDOM_RANGE = 1;
const DEFAULT_ZOOM_LEVEL = 8;

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

export default class PgMapView extends PgMapViewDesign {
allPins: MapView.Pin[] = this.generateMockMapData();
addedPins: MapView.Pin[] = []; // This is for duplicate prevention
private disposeables: (() => void)[] = [];
constructor(private router?: Router, private route?: Route) {
super({});
}

initMapView() {
this.map.setCenterLocationWithZoomLevel(
{
longitude: CenterMapCoordinates.lng,
latitude: CenterMapCoordinates.lat
},
DEFAULT_ZOOM_LEVEL,
true
);
this.map.onCameraMoveEnded = () => this.addPinsWithLazyLoad(this.currentMapViewStyle);
this.map.minZoomLevel = DEFAULT_ZOOM_LEVEL - 1;
this.map.maxZoomLevel = DEFAULT_ZOOM_LEVEL + 1;
}

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

onHide(): void {
this.dispose();
}
dispose(): void {
this.disposeables.forEach((item) => item());
}
}

Our full code will look like this( assume that the map variable is added as MapView via UI Editor)

import PgMapViewRadiusDesign from 'generated/pages/pgMapViewRadius';
import MapView from '@smartface/native/ui/mapview';
import { Route, Router } from '@smartface/router';

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 PgMapViewRadius extends PgMapViewRadiusDesign {
allPins: MapView.Pin[] = this.generateMockMapData();
addedPins: MapView.Pin[] = []; // This is for duplicate prevention
private disposeables: (() => void)[] = [];
constructor(private router?: Router, private route?: Route) {
super({});
}
generateMockMapData(): MapView.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;
}

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.map.setCenterLocationWithZoomLevel(
{
longitude: CenterMapCoordinates.lng,
latitude: CenterMapCoordinates.lat
},
DEFAULT_ZOOM_LEVEL,
true
);
this.map.onCameraMoveEnded = () => this.addPinsWithLazyLoad();
this.map.minZoomLevel = DEFAULT_ZOOM_LEVEL - 1;
this.map.maxZoomLevel = DEFAULT_ZOOM_LEVEL + 1;
}

addPinsWithLazyLoad() {
// First, get the current distance between center and the corner of the visible map
const visibleRegions = this.map.visibleRegion;
const bottomLeft: MapPoint = {
lat: visibleRegions.bottomLeft.latitude,
lng: visibleRegions.bottomLeft.longitude
};
const center: MapPoint = {
lat: this.map.centerLocation.latitude,
lng: this.map.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.map.addPin(pin); // Add the pin if inside of the current pin.
this.addedPins.push(pin);
}
});
}
checkForDuplicate(pin: MapView.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();
}

onLoad() {
super.onLoad();
this.initMapView();
}
onHide(): void {
this.dispose();
}
dispose(): void {
this.disposeables.forEach((item) => item());
}
}

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);
}
});
}