Responsive OneMap UI using media query and flex box

In this tutorial, I will show you how to make responsive UI for web page using flex boxes and media query. I will redesign one of the examples available on OneMap API Help Web Site. We will see how to make pages using OneMap JavaScript API responsive. Please visit the OneMap API address search example  to check the original web page.

onemap_add_eg 1) Inspect the source code

<table style="width: 436px">
<tr>
<th colspan="2">
Address Search API Usage (WGS84 Coordinate System)
</th>
</tr>
<tr>
<td>
Search Text :
</td>
<td>
<input type="text" id="txtSearchText" value='City Hall' />
</td>
</tr>
<tr>
<td>
<input type="button" onclick="GetSearchData();" value="Search" />
</td>
</tr>
</table>
<table>
<tr>
<td>
<div id="divMain" style='width: 500px; height: 500px;'>
</div>
</td>
<td style="vertical-align: top">
<div id="divResults">
</div>
</td>
</tr>
</table>

view raw
gistfile1.html
hosted with ❤ by GitHub

By inspecting the source code, I found that the page is designed using HTML Table. It is quite a simple page with only a few sections, each section embedded in  table cell. I am going to use media query and flex boxes to make the page responsive.

2) Put viewport meta tag in the head of the document.

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

view raw
gistfile1.html
hosted with ❤ by GitHub

width=device-width” indicates the screen’s width in device independent pixels. “initial-scale=1.0” to indicates one to one relationship between CSS and device independent pixels.

3) Convert the HTML Table structure into separate sections as follow.

<div class="container">
<div class="box heading">
<h2>Address Search API Usage (WGS84 Coordinate System)</h2>
</div>
<div class="box container2">
<div class="box searchbox">
<input type="text" id="txtSearchText" value='City Hall' />
<input type="button" onclick="GetSearchData();" value="Search" />
</div>
<div class="box results">
<div id="divResults">
</div>
</div>
</div>
<div class="box map">
<div id="divMain" >
</div>
</div>
</div>

view raw
gistfile1.html
hosted with ❤ by GitHub

.searchbox and .results are put into .container2 so that they could be moved around together when the screen size becomes bigger.

4) Design smallest view first and use media query for different break points.

/* set padding and height to 100% */
html, body, #mapDiv {
padding: 0;
margin: 0;
height: 100%;
font-family: sans-serif;
}
/* use flex and flex-wrap */
.container {
display: flex;
flex-wrap: wrap;
height: 100%;
}
/* set all the section stacked vertically in mobile view*/
.box {
width: 100%;
}
.heading {
font-size: 1.0em;
text-align: center;
min-height: 50px;
}
.heading h2{
font-size: smaller;
}
.map {
height: 80%;
max-height: 700px;
}
#txtSearchText {
width: 74%;
}
#btnSearch {
width: 20%;
}
#divMain{
width: 100%;
height: 100%;
}
/* results will be off the screen at first
when the search button is pressed,
it will slide in from the left.
*/
#divResults{
overflow:scroll;
width: 300px;
height: 100%;
position: absolute;
transform: translate(-300px, 0);
transition: transform 0.3s ease;
background-color: lightyellow;
}
/* Set this property from javascript to make the results
to slide into view */
#divResults.open {
transform: translate(0,0);
z-index: 101;
}
/* adjust the logo at the bottom of map */
#OneMapLogo {
bottom: 5px;
}

view raw
gistfile1.css
hosted with ❤ by GitHub

onemap_min

5) Slide in the results on the screen when the user clicked the search button.

function displayData(resultData) {
var results = resultData.results;
if (results == 'No results') {
document.geElementById('divResults').innerHTML = "No result(s) found";
return false
}
else {
// Set the divResults class as open to slide in from off screen
var drawer = document.getElementById("divResults");
drawer.classList.toggle('open');
/////
var htmlStr = "<table>";
htmlStr = htmlStr + "<tr><th>Search Value </th></tr>";
for (var i = 0; i < results.length; i++) {
var row = results[i];
htmlStr = htmlStr + "<tr>";
htmlStr = htmlStr + "<td>";
htmlStr = htmlStr + "<a href='JavaScript:ZoomTo(" + row.Y + "," + row.X + ")'>" + row.SEARCHVAL + " (" + parseFloat(row.X).toFixed(4) + " , " + parseFloat(row.Y).toFixed(4) + ")" + "</a>";
htmlStr = htmlStr + "</td>";
htmlStr = htmlStr + "</tr>";
}
htmlStr = htmlStr + "</table>";
document.getElementById("divResults").innerHTML = htmlStr;
}
}

view raw
gistfile1.js
hosted with ❤ by GitHub

Edit the displayData( ) function as above to slide in the divResults into the view.

6) Slide back and hide the results list when the user clicked on one of the result, and then show the clicked location on the map. 

Add the following code to ZoomTo( ) function to hide the list.

function ZoomTo(xVal, yVal) {
// Remove the class open from divResults,
// so that it will slide back out of view
var drawer = document.getElementById("divResults");
drawer.classList.toggle('open');
///
OneMap.showLocation(xVal, yVal);
}

view raw
gistfile1.js
hosted with ❤ by GitHub

7) Add media query and styles for break point with minimum width 800px.

@media screen and (minwidth: 800px){
.container2 {
width :40%;
}
.map {
width : 60%;
}
#divResults{
position: relative;
transform: translate(0,0);
}
}

view raw
gistfile1.js
hosted with ❤ by GitHub

In a wider screen, container2, which contains search input box and result list, and map area are placed side by side. Result list is put back on screen by setting transform to 0,0.

onemap_800_min

8) Add media query and styles for break point with minimum width 1000px.

After minimum width 1000px is reached, the contents will stop growing, and left and right margins will be added automatically.

@media screen and (min-width: 1000px){
.container {
width: 1000px;
margin-left: auto;
margin-right: auto;
height: 800px;
}
.map {
height: 700px;
}
.heading {
height: 20px;
}
}

view raw
gistfile1.css
hosted with ❤ by GitHub

onemap_maximum

That is! We have a responsive page using OneMap as base-map. This is just a very simple rework on the existing code and by no means a complete work. But you get the basic idea of how to make your page responsive if you are using onemap api in it.

Using OneMap Address Search in iOS App

In this tutorial, we are going to implement the address search feature using OneMap REST API. If you want to know more about OneMap, please visit to OneMap API Help page.

We will continue our tutorial from my post how to add OneMap to iOS App, in which I’ve shown how to use ArcGIS iOS SDK and adding OneMap base map and setting map extent. Complete the steps in it before attempting this tutorial.

We are going to call the Address Search REST API from OneMap form our iOS app. I’ve tried out the API call and explored the data returned from the service in previous post Exploring OneMap REST API with Swift Playgrounds.

Add Navigation Controller
First, we need to add Navigation controller to our View Controller. Go to Editor > Embed In > Navigation Controller. A Navigation Controller is added to our View Controller.

Embed in navigation

Add UISearchController And UITableViewController
We are going to use the UISearchController to allow users to key in address. The results will be shown in UITableViewController as the user type in the key words. Add UISearchController property and UITableViewController property in the View Controller.

var resultTableViewController = UITableViewController()
var searchController = UISearchController()

view raw
gistfile1.swift
hosted with ❤ by GitHub

In the viewDidLoad( ) method, add following setup code for UISearchController.

// UISearchController Setup
searchController = UISearchController(searchResultsController: resultTableViewController)
searchController.searchResultsUpdater = self
searchController.searchBar.searchBarStyle = UISearchBarStyle.Minimal
searchController.hidesNavigationBarDuringPresentation = false
navigationItem.titleView = searchController.searchBar
definesPresentationContext = true
resultTableViewController.tableView.dataSource = self
resultTableViewController.tableView.delegate = self
searchController.dimsBackgroundDuringPresentation = false
searchController.delegate = self
searchController.searchBar.delegate = self

view raw
gistfile1.swift
hosted with ❤ by GitHub

Next, add the necessary delegates to View Controller as follow:

// delegates for AGSMapViewLayer, UISearchController, UITableView
class ViewController: UIViewController, AGSMapViewLayerDelegate,
UISearchResultsUpdating, UITableViewDelegate,
UITableViewDataSource, UISearchControllerDelegate,
UISearchBarDelegate {

view raw
gistfile1.swift
hosted with ❤ by GitHub

Create a data structure to hold the address data
Create a structure named Place to hold address name, category, X and Y.

struct Place {
var placeName = ""
var Category = ""
var X = 0.0
var Y = 0.0
}

view raw
gistfile1.swift
hosted with ❤ by GitHub

In ViewController, declare an array of Places. This array will be our data model to store the search results.

var searchResults = [Place]()

view raw
gistfile1.swift
hosted with ❤ by GitHub

Implement UITableView delegate protocol methods 
There are three protocol methods we need to implement to adopt the UITableView delegate protocol. Add the following methods to ViewController Class.

// UITableView delegate protocol methods
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
searchController.searchBar.text = searchResults[indexPath.row].placeName
showSelectedSearchPlace(searchResults[indexPath.row])
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let rowData = searchResults[indexPath.row].placeName
let cell = UITableViewCell()
cell.textLabel?.text = rowData
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchResults.count
}

view raw
gistfile1.swift
hosted with ❤ by GitHub

Implement UISearchResultsUpdating delegate protocol method
UISearchResultsUpdating protocol requires to implement the following method. This allow us to update search results based on the information users enters in the search bar.

func updateSearchResultsForSearchController(searchController: UISearchController) {
getAddresses(searchController.searchBar.text)
}

view raw
gistfile1.swift
hosted with ❤ by GitHub

Next, we will implement the function to call OneMap REST API to search for the address.

// Call OneMap's Address Search API to search a location in Singapore
func getAddresses(keyWord: String ){
if keyWord != "" {
let keyWordEscaped = keyWord.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
let urlString = "http://www.onemap.sg/API/services.svc/basicSearch?token=qo/s2TnSUmfLz+32CvLC4RMVkzEFYjxqyti1KhByvEacEdMWBpCuSSQ+IFRT84QjGPBCuz/cBom8PfSm3GjEsGc8PkdEEOEr&wc=SEARCHVAL%20LIKE%20%27\(keyWordEscaped!)$%27&otptFlds=CATEGORY&returnGeom=0&nohaxr=10"
session.dataTaskWithURL(NSURL(string: urlString)!, completionHandler: {
(taskData, taskResponse, taskError) -> Void in
var jsonReadError : NSError?
var jsonResult = NSJSONSerialization.JSONObjectWithData(taskData, options: NSJSONReadingOptions.MutableContainers, error: &jsonReadError) as NSDictionary
let resultsArray = jsonResult["SearchResults"] as NSArray
self.searchResults = []
for (index, result) in enumerate(resultsArray){
if index > 0 {
var place = Place()
if let placeName = result["SEARCHVAL"] as String? {
place.placeName = placeName
}
if let category = result["CATEGORY"] as String? {
place.Category = category
}
if let x = result["X"] as NSString? {
place.X = x.doubleValue
}
if let y = result["Y"] as NSString? {
place.Y = y.doubleValue
}
self.searchResults.append(place)
}
}
self.resultTableViewController.tableView.reloadData()
}).resume()
}
}

view raw
gistfile1.swift
hosted with ❤ by GitHub

Run the application
If everything goes well, a search bar will be displayed on the navigation controller title place.

searchbar

When the user type in the search keywords in the search bar, getAddresses function is called and a list of addresses started by the keyword is shown in the table view as suggestion.

Auto suggestion

Show the selected one on the map view
We need to implement one more function to show the search result when the user clicks on one of the suggested places. This function is called in tableView:didSelectRowAtIndexPath: delegate method.

func showSelectedSearchPlace(selectedPlace :Place){
// to close suggested list when the user selected one
searchController.active = false
searchController.searchBar.text = selectedPlace.placeName
// a red color symbol to mark the place
let myMarkerSymbol = AGSSimpleMarkerSymbol()
myMarkerSymbol.color = UIColor.redColor()
// a point representing x,y data from selctedPlace
let point = AGSPoint(x: selectedPlace.X, y: selectedPlace.Y, spatialReference: mapView.spatialReference)
// make the selectedPlace at the center of the map
mapView.centerAtPoint(point, animated: false)
// make a graphic with symbol and point to add to the map
var pointGraphic = AGSGraphic(geometry: point, symbol: myMarkerSymbol, attributes: nil)
// remove previously searched places
graphicLayer.removeAllGraphics()
// display the graphic on the map
graphicLayer.addGraphic(pointGraphic)
}

view raw
gistfile1.swift
hosted with ❤ by GitHub

showaddress

Above screen is shown when the user selects City Hall from suggested addresses. The project is available on github. Please leave comments if you have any questions and suggestion.