MapView
API Reference: UI.MapView
MapView is a view to display native maps (Apple Maps on iOS and Google Maps on Android).
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.
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 CodeAs 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 Basicsimport 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('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('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('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+.
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 CodeAs 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 Basicsimport 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('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());
}
}
This can be achieved by calling myMap.removeAllPins();
method. No map reloading is needed.
Pins must be added after onCreate event is triggered.
Zoom Level
Zoom level value behavior is different from the Android and iOS implementations.
- Android
- iOS
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
For example, if the center of the map is at 0 degrees longitude, the map will be zoomed in to the 15th degree of longitude.
Consequently, given zoom value might not be the same as the zoom level on Android.
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);
}
});
}