Skip to content

Map Floorplan

Overview

Use SAP Fiori Map Floorplan to extend Apple's MapKit and Esri's ArcGIS frameworks. The floorplan supports native map APIs and adds additional components to enhance the map experience. Maps are built upon layers of geometries (points, polylines, and polygons). Floorplans provide foundation for functionality including, showing search results, geometry details, and geometry creation. Floorplans currently only support iPad devices.

Components

Floorplan Image

Display Geometries

Layers

The map view may consist of multiple display layers. Each layer is represented by an FUIGeometryLayer object, which has the displayName property. By assigning geometries to distinct layers, geometries can be organized by their business context and then consumed by the map display. The state or behavior of all geometries can then be easily controlled layer by layer. For example, developer can show or hide all geometries on a specified layer using setLayerHidden(_:hidden:) method.

  • Initialization of FUIGeometryLayers on map view

An FUIGeometryLayer object can be initialized by setting its displayName. In the example App, there are three geometry layers defined: special zones, NYC ferry, and NYC MTA.

enum Layer {
    static let zones = FUIGeometryLayer(displayName: "Special Zones")
    static let stops = FUIGeometryLayer(displayName: "NYC Ferry")
    static let mtaStops = FUIGeometryLayer(displayName: "NYC MTA")
}

Display Objects

In the example App, we added annotations and overlays to the FUIMKMapView. By default FUIMKMapView has its own data source implementation and all added annotations and overlays will be automatically managed when calling reloadData().

  • Add annotation objects to the internally managed map data source

Developer can add annotations to the data source of FUIMKMapView directly by calling reloadData(). In such way, the added annotations will also be automatically appended to the corresponding geometry layer. Below code snippet demonstrates adding an array of stop annotations to the data source.

var stopAnnotations: [MKPointAnnotation] = [] {
    didSet {
        reloadData()
    }
}
  • Add overlay objects to the internally managed map data source

Likewise, developer can add overlays to the data source of FUIMKMapView directly by calling reloadData(). In such way, the added overlays will also be automatically appended to the corresponding geometry layer. Below code snippet demonstrates adding an array of zone overlays to the data source.

var zonesOverlays: [MKPolygon] = [] {
    didSet {
        reloadData()
    }
}
  • Working with native MKMapView instance methods

Developer may still use native MKMapView methods to manage annotations and overlays on the map view. Native methods like addAnnotation(_:), addOverlay(_:) works the same on FUIMKMapView. However, for annotation and overlay objects added without geometry layer information, they cannot be configured using methods specialized for wrapper display geometries.

Additionally, developer should manage the display of their own annotations and overlays objects. This can be achieved by implementing your own mapView:viewForAnnotation: and renderer(for:) delegate methods. Below code snippet demonstrates the implementation of custom annotation view for MTA stops.

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    ...
    if mtaStopAnnotations.contains(pointAnnotation) {
        let view = mapView.dequeueReusableAnnotationView(withIdentifier: "FUIMarkerAnnotationView", for: annotation) as! FUIMarkerAnnotationView
        view.markerTintColor = UIColor.preferredFioriColor(forStyle: .map6)
        view.glyphImage = FUIIconLibrary.map.marker.bus
        view.clusteringIdentifier = nil
        return view
    } else if stopAnnotations.contains(pointAnnotation) {
        return nil
    } else if editAnnotations.contains(where: { return $0 as? MKPointAnnotation != nil }) {
        let view = mapView.dequeueReusableAnnotationView(withIdentifier: "FUIMarkerAnnotationView", for: annotation) as! FUIMarkerAnnotationView
        view.markerTintColor = UIColor.preferredFioriColor(forStyle: .map5)
        view.glyphImage = FUIIconLibrary.map.marker.venue
        view.clusteringIdentifier = nil
        return view
    }
    return nil
}

Clustering

A clustering annotation groups two or more distinct annotations into a single entity. FUIMKMapView utilize the same mechanism from MapKit to generate clustered annotations. Depends on the global setting of property isClusteringEnabled on the floorplan view controller as well as the specific setting of property clusteringIdentifier for each annotation view, the map view automatically creates cluster annotations when two or more annotation views become grouped too closely together on the map surface.

In the example App, only stop annotations are set to enable clustering. For MTA stop annotations, clustering is disable. The screenshot below illustrates their different behavior.

Clustering Annotations

By default, clustering is enable for your map view controller once it is inherited from FUIMapFloorplanViewController. Therefore, in default state all annotations added to the map view will be clustered automatically depending on the zoom level. Two ways of customizations on specific annotation view can be made on the map view:

  • Determine if an annotation view should be clustered or not:

If clusteringIdentifier is set to nil, the annotation view will not be clustered. You may also define distinct clusteringIdentifier value for annotation views that should not be clustered. The settings of clusteringIdentifier is commonly done by implementing the delegate method mapView(_:viewFor:).

public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    ...
    guard let _ = annotation as? FUIAnnotation else {
        let view = mapView.dequeueReusableAnnotationView(withIdentifier: "FUIMarkerAnnotationView", for: annotation) as! FUIMarkerAnnotationView
        view.markerTintColor = UIColor.preferredFioriColor(forStyle: .map6)
        view.glyphImage = FUIIconLibrary.map.marker.bus
        view.clusteringIdentifier = nil
        return view
    }
    return nil
}
  • Customize the cluster annotation:

To customize the cluster annotation for the specified set of annotations, implement the mapView(_:clusterAnnotationForMemberAnnotations:) method in your map's delegate. This delegate will not be called if the clusteringIdentifier for the annotation view is set to nil.

Map Interactions

  1. Overlay Selection and Deselection

Native MapKit provides some handy property and methods to manage interaction with annotations: selectedAnnotations to get an array of selected annotations, selectAnnotation(_:animated:) to select the specified annotation and displays a callout view for it, and deselectAnnotation(_:animated:) to deselect the specified annotation and hides its callout view.

Likewise, FUIMKMapView provides equivalent property and methods to manage interaction with overlays:

selectedOverlays: get an array of selected overlays. selectOverlay(_:animated:): select the specified overlay and change its display to selected state. deselectOverlay(_:animated:): deselect the specified overlay and restore its display to default state.

  • Map view delegate setting

Before using the overlay selection functionality, developer should make sure their own map view controller has been sub-classed from FUIMKMapFloorplanViewController. Also developer should set map view's delegate to their own map view delegate implementation:

let mapDelegate = MKMapViewDelegateImpl()
mapView.delegate = mapDelegate
  • Selection/Deselection behavior of overlay on map view

At this point, once the custom map view's delegate is correctly set, the interaction with overlays through tap gesture is supported right out of box. Please note that at one time only one overlay object on map can be selected. Also if an annotation call out is highlighted, all other selected overlay reset to default state.

Overlay selection and deselection can also be implemented using the detail panel as below.

Overlay Selection

  • Link selection behavior of overlay to detail panel

Detail panel is implemented by a table view. So to define the selection behavior from the detail panel, developer has to implement the tableView(_didSelectRowAt:) method for the detail panel.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    ...
    if let zone = feature.zone {
        self.pushContent(for: zone)
    }
    ...
}

After that, the pushContent(for:) method needs to be implemented to trigger the selection behavior for the overlay. Developer needs to get selected overlay through table view index value, and then initiate selectOverlay(_:animated:) to enable selection state on the overlay.

func pushContent(for zone: Feature<BetaNYC.Zone>) {
    ...  
    if let index = zonesFeatureCollection.features.firstIndex(where: { $0.properties.id == zone.properties.id }) {
        self.mapView.selectOverlay(zonesOverlays[index], animated: true)
    }
    ...
}
  • Link deselection behavior of overlay to detail panel

Deselection behavior from detail panel may be implemented through the completion handler on the close button: detailPanel?.content.closeButton.didSelectHandler. Developer may get a list of selected overlays from the selectedOverlays property, and then initiate deselectOverlay(_:animated:) to unselect all.

self.detailPanel?.content.closeButton.didSelectHandler = { [unowned self] _ in
    ...
    for selectedOverlay in self.mapView.selectedOverlays {
        self.mapView.deselectOverlay(selectedOverlay, animated: true)
    }
    ...
}

Map Components

Default Toolbar

The toolbar contains buttons for common map functionalities. By default the toolbar has button for presenting settings, showing user location, presenting a map legend, and showing all annotations. The view is anchored to the right top right side of the map.

Default Toolbar

Settings

The settingsController is a UIViewController for the developer to set additional configurations for his/her application. The controller is presented modally as a formsheet over the map. Tapping on the close button will dismiss the controller and return to the map. In the example project, the settings acts as a placeholder and does not serve a functional purpose.

Settings Image

  • Settings Data Binding

Settings is set below in viewDidLoad():

// Components: Settings
self.settingsController = SettingsTableViewController.shared

User Location

This button zooms the map to center on the coordinates of the user's location.

User Location On

Privacy settings must be changed in the Info.plist file to request permission to track location. Modify the file by adding the Privacy – Location When In Use Usage Description key. Check if permissions have been authorized in the viewDidAppear(_:) method.

Legend

The legend shows additional information about the MKAnnotationViews, MKPolylines, and MKPolygons that appear on the map. The legend is presented as a table in a UIPopoverPresentationController. The popover itself is anchored to the legend button within the toolbar. In the example, legend information about the Ferry Stops & Special Zones is provided.

Legend Image

  • Legend Items

The legend contains a list of FUIMapLegendItems. Items contain icon, line, or fillItem, to represent the MKAnnotationView, MKPolyline, or MKPolygon. In the example app the legend items are defined according to their layer. The icon and fillItem variants are shown.

enum Layer {
   ...

    static var zonesLegendItem: FUIMapLegendItem = {
        var item = FUIMapLegendItem(title: Layer.zones.displayName)
        item.fillItem = FUIMapLegendFillItem()
        item.fillItem?.backgroundColor = UIColor.preferredFioriColor(forStyle: .map1)
        item.fillItem?.borderColor = UIColor.preferredFioriColor(forStyle: .map1)
        return item
    }()

    static var stopsLegendItem: FUIMapLegendItem = {
        var item = FUIMapLegendItem(title: Layer.stops.displayName)
        let image = FUIAttributedImage(image: FUIIconLibrary.map.marker.bus.withRenderingMode(.alwaysTemplate))
        image.tintColor = .white
        item.icon = FUIMapLegendIcon(glyphImage: image) //FUIMapLegendIcon(glyphImage: image)
        item.backgroundColor = UIColor.preferredFioriColor(forStyle: .map6)
        return item
    }()
}
  • Legend Title

A title to the legend by accessing the headerTextView and settings it's text. Titles are truncated after reaching the legend maximum width.

  • Legend passthrough views

Passthrough views allow for the legend to remain open while tapping separate views. By default both the detailPanel and toolbar are passthrough views. Add additional passthrough views by appending to the list,

  • Legend Data Binding

Legend configurations are set in viewDidLoad():

// Components: Legend
self.legend.headerTextView.text = "New York Legend"
self.legend.items = [Layer.zonesLegendItem, Layer.stopsLegendItem]
self.legend.passThroughViews.append(mapView)

Zoom Extents

This button zooms the map to the region containing all annotations.

Note: Zoom Extents does not include the user's location

Zoom Extents Image

Detail Panel

The detailPanel is a view that shows searchResults of map features and additional details about map components. The panel resizes to fit its content based on the controllers preferredContentSize and is anchored to the top left of the map.

Search Results

The searchResults control allows the user to filter map information when typing in the searchbar.

Search Results Unfiltered

Search Results Filtered

  • Enable Search

Search Results is enabled by setting the isSearchEnabled Boolean value.

  • SearchResults tableView dataSource

Setting the dataSource to populates the searchResults tableView. The tableView can also be configured by registering cells, adding estimated row heights, and enabling automatic dimensions.

  • SearchResults tableView delegate

The developer can set the delegate to respond to events. In the example project, row selection will push the content controller. (See Content Section below)

  • SearchResults searchbar``delegate

The developer can set the delegate to respond to events. In the example project, row selection will push the content controller. (See Content Section below)

  • SearchResults Data Binding

In the example project, the ViewController implements the UITableViewDataSource, UITableViewDelegate, and UISearchBarDelegate.

class ViewController: FUIMKMapFloorplanViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate, EditingGeometryProviding {
  ...

  // MARK: UITableViewDataSource

  func numberOfSections(in tableView: UITableView) -> Int { ... }

  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { ... }

  // MARK: UITableViewDelegate

  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { ... }

  // MARK: UISearchBarDelegate

  func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { ... }

  func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { ... }

  func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { ... }
}

SearchResults configurations are set in the viewDidLoad().

// Components: Detail Panel - SearchResults
self.detailPanel.isSearchEnabled = true
self.detailPanel.searchResults.tableView.dataSource = self
self.detailPanel.searchResults.tableView.delegate = self
self.detailPanel.searchResults.tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier)
self.detailPanel.searchResults.tableView.estimatedRowHeight = 60
self.detailPanel.searchResults.tableView.rowHeight = UITableView.automaticDimension
self.detailPanel.searchResults.searchBar.delegate = self

Content

The content control presents additional information about a map component (MKAnnotationView, MKPolyline, MKPolygon, etc.). This controller is presented and dismissed by using the pushChildViewController() and popChildViewController() API respectively.

Content Image

  • Content Headline and Subheadline

The headlineText provides the title of the content and the subheadlineText provides additional title information. The headline is non-scrollable and will always remain visible while the subheadline will be hidden while scrolling. A didSelectTitleHandler is that is called when the headlineText is tapped.

  • Content DataSource and Delegate

Configure the tableView and set the dataSource and delegate to the tableView similar to the searchResults.

  • Content Present

Calling pushChildViewController() will transition the content into view. It is up to the developer to determine when to present the content. In the example project, the controller is presented when a tableView row is selected. Update the controller properties and reloadData immediately prior to calling pushChildViewController().

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    ...
    self.pushContent(for: stop)
}

func pushContent(for stop: Feature<TransitLand.Stop>) {
    ...
    detailPanel.content.headlineText = stop.properties.name
    detailPanel.content.subheadlineText = stop.properties.operators.first?.values.first
    detailPanel.content.didSelectTitleHandler = {
        print(self.detailPanel.content.headlineText)
    }
    detailPanel.content.tableView.reloadData()
    detailPanel.pushChildViewController()
}
1
Only one `content` can be displayed at a time.  To update a `contentViewController`, reload the table then resize the panel by calling the `fitToContent()` method.
  • Content Dismiss

Calling popChildViewController() dismisses the content and returns to searchResults.

This controller has a closeButton intended to dismiss itself. The developer can make additional changes prior to calling popChildViewController().

  • Content Data Binding

As stated in searchResults, example project's ViewController already implements UITableViewDataSource and UITableViewDelegate

Initial content configurations are set in the viewDidLoad().

// Components: Detail Panel - Content
self.detailPanel.content.tableView.dataSource = self
self.detailPanel.content.tableView.delegate = self
self.detailPanel.content.tableView.register(FUIObjectTableViewCell.self, forCellReuseIdentifier: FUIObjectTableViewCell.reuseIdentifier)
self.detailPanel.content.tableView.register(FUIMapDetailTagObjectTableViewCell.self, forCellReuseIdentifier: FUIMapDetailTagObjectTableViewCell.reuseIdentifier)
self.detailPanel.content.tableView.estimatedRowHeight = 44
self.detailPanel.content.tableView.rowHeight = UITableView.automaticDimension
self.detailPanel.content.closeButton.didSelectHandler = { [unowned self] _ in
    ...
    self.detailPanel.popChildViewController()
}

Editing

The floorplan provides an editing interface to add geometries. Users can draw directly onto the map or input addresses to create their shape (point, polyline, or polygon)

Editing Image

  • Set Editable Floorplan

To allow editing of the panel set the isEditable property to true.

  • Set Create Geometry Items

The developer chooses what types of items can be created and placed on the map. In the example project, the user can create items that appear in the legend.

Tapping the + right bar button presents a popover to select the create geometry item. Once selected, the view controller will transition to an editing state by setting all other annotations to disabled and non-interactive, showing editing toolbar buttons, and presenting the editing panel. In the example project, the item that can be created is an editLegendItem.

  • Set Creation Results Controller

This controller is available for the user to make additional changes to the work order. In the example project, a placeholder controller is provided but does not have additional functionality.

  • Save the Geometry

The developer is responsible for updating the model with the returned geometry. Use the didSaveResults closure to update the model. The first argument is the editing shape and the second argument is the type of work order created. This closure is called after tapping the save bar button item on the createGeometryResultsController.

The Floorplan sets geometry creation and editing in viewDidLoad().

// Components: Editing Panel
self.isEditable = true
self.editingPanel.createGeometryItems = [Layer.editLegendItem]
self.editingPanel.createGeometryResultsController = CreateGeometryResultsController(provider: self)
self.editingPanel.didSaveResults = { [unowned self] shape, createObject in
    if let point = shape as? MKPointAnnotation {
        self.editAnnotations.append(point)
    } else if let polyline = shape as? MKPolyline {
        self.editAnnotations.append(polyline)
    } else if let polygon = shape as? MKPolygon {
        self.editAnnotations.append(polygon)
    }
}

Editing Toolbar

The editing toolbar shows buttons to assist in geometry creation.

Editing Image

User Location Button

When tapped, the map will zoom to the user's location. This button has the same functionality as the Default Toolbar User Location Button.

Zoom Extents Button

When tapped, the map will zoom to show all map annotations. This button has the same functionality as the Default Toolbar Zoom Extents Button.

Add Button

When selected, tapping on the map will add points to the map.

Delete Button

When selected, tapping on points of the current editing shape will delete the points.

Undo Button

Add and delete actions can be undone by tapping on the undo button.

Redo Button

Undo actions can be redone by tapping on the redo button.

Branch Button

To create a branch off of a polyline or polygon, select the branch button.

Note: The branch button is disabled when creating a point and when the delete button is selected.

Editing Panel

The Editing Panel shows editing configurations while creating a geometry. A user can select the type of geometry to create (point, polyline, or polygon) as well as add addresses directly to the shape.

Editing Image

Geometry Segmented Control

A user can switch between the geometries (point, polyline, or polygon) by tapping on the segment.

Note: Switching from polyline or polygon to point geometries will clear the stored points. The user will be prompted with an alert if they choose to continue.

Clear All Button

A user can clear all points by tapping the clear all button. The user will be prompted with an alert if they wish to continue.

Add New Point Field

This field allows the user to add addresses directly to the editing shape. Tapping the field will launch the keyboard.

Save Button

The save button will prepare to commit the changes and launch a create results controller. (See Save Geometries)

Drawing Interaction

Tap on the + button to open the create geometry popover. The example project allows the user to create "Edit Annotations". Tap the cell to start.

edit annotation popover

Select the polyline image to create a polyline.

polyline segment selected

When the add button is selected points are added when the map is tapped

tap point 1

tap point 2

tap point 3

tap point 4

To add a point on a segment, tap directly on the line.

tap point on segment

To move the point, long press and drag on the point to a new location.

long press picture

To delete a point first select the delete button. The add button will become deselected.

delete button selected

Tap on a point to delete.

delete point

To undo the delete, tap the undo button.

tap undo

To redo the delete, tap the redo button.

tap redo

To add a branch select the add button,

select add button

then select the branch button.

select branch button

Select a point on the segment then tap away.

select new point

tap away from branch

Edit Panel Interaction

A user can directly add an address by tapping the Add New Point field. This launches the keyboard and shows suggestions.

show suggestions

Typing into the field provides suggestions for addresses.

show suggestions with entries

Selecting the cell adds the address to the model.

show added suggestion

Cells can be rearranged by dragging the cell with the hamburger icon.

show dragged cell

To delete a cell from the panel, tap the red circle to show the delete button.

show delete button

Accept the prompt to delete the point.

show delete prompt

Switching between geometries is possible by changing the segmented control. First a prompt will appear.

show change polygon prompt

Accept the prompt to change to polygon.

show polygon

Changing from a polyline or polygon to a point will delete all the points. This action cannot be undone!

show change geometry point

Points can be wiped by tapping the clear all button. This action cannot be undone!

show clear all alert

Cancel the alert and return to the editing line. Tap the save button in the panel to launch the create results page.

show create result page

The user can tap the cancel button to return back to editing. Tap the save bar button item to return back to the map and the editing geometry will be saved.

show geometry saved


Last update: April 14, 2021