Taking some time-off from Swift-programming, I wanted to share an app I created in React Native. The idea was to create an interface to display JavaScript (Google) maps. Even throughout the blog quite a few of these maps have accumulated. They are by design interactive and a user benefits hugely from being able to click on markers to get more information to zooming in and really understanding the location (or perhaps switching to satellite view).

IMG_0011

We could use Dropbox, however this has the advantage of letting you host everything on your server (if the data is sensitive), defining access-control (give some people access to some maps on a project), and a more controlled navigation experience (e.g. grouping maps across different projects). Mainly it has the advantage of teaching me react.

The application is composed of four modules:

  1. ‘Index.ios.js’ -> defines point of entry
  2. ‘SearchPage.js’ -> a log-in page for the user, it loads (searches for) the relevant data for the specified password
  3. ‘SearchResults.js’ -> main navigator; for selecting which map to view
  4. ‘MapView.js’ -> renders the map (url) within the app

Index.ios.js

This is the point of entry into the application (the equivalent file for android is index.android.js) and I simply route it to the log-in page:

/********************************************************************************************************
 * Index.ios.js -> GIS App (iPad Version)
 ********************************************************************************************************/
'use strict';

// Load react-native module
var React  = require('react-native');
var SearchPage = require('./SearchPage');
// Define a style (CSS)
var styles = React.StyleSheet.create ({
  container: {
    flex: 1
  }
})
class GISApp extends React.Component {
  // Our main page (the one we are routed to ) is the 'SearchPage'
  render() {
    return (
      <React.NavigatorIOS
      style={styles.container}
      initialRoute={{
        title: '',
        component: SearchPage,
      }}/>
      );
  }

}
// AppRegistry defines entry point to application and provides root component
React.AppRegistry.registerComponent('GIS', function() { return GISApp })

SearchPage.js

IMG_0006

React uses flex-styling to help you position items on the screen (equivalent is auto-layout in Xcode), however since I am only creating the app for the iPad I haven’t really accounted for different screens-sizes and perhaps hard-coded in too much what I think looks good. Also it looks too small.

I have filled in quite a few properties to make the text-input act more like a proper ‘password’ field. In some sense the “Load Projects” button is unnecessary.

Edit: I decided to change the UI to the below:

IMG_0014

var styles = StyleSheet.create({
	description: {
		marginBottom: 20,
		fontSize: 24,
		textAlign: 'center',
		color: '#656565'
	},
	container: {
		flex:1,
		flexDirection:'column',
		justifyContent: 'center',
		alignItems: 'center',
		marginBottom: 200
	},
	image: {
		alignSelf: 'center',
		marginBottom: 40
	},
  	searchInput: {
  		alignSelf: 'center',
  		width: 500,
  		height: 80,
    	marginBottom: 20,
    	fontSize: 36,
    	borderWidth: 5,
    	borderColor: '#48BBEC',
    	borderRadius: 8,
    	textAlign: 'center',
    	color: '#48BBEC'
  }
})

Basically the app connects to http://exampleserver/myjsonfile.json and pushes the results to the next module (this is done using _executeQuery). I have added some authentication using crypto-js, where the user is required to enter a password which is then hashed into a string which corresponds with the file-name of .json file. The idea is to have several .json files (each with its corresponding password) which link to different projects/maps.

In this example I have hosted my json file on drop-box, however the goal would be to move it a private-server. If the wrong password is entered, then the wrong URL is generated and we get nothing.

IMG_0007

I use the json file a bit like a cheap database, to bring structure and access-control to all the maps. It looks a bit like this (and can be cheaply created with Python):

{
	"response": {
		"application_response_code": "200",
		"projects": [{
			"project_name": "Demo",
			"maps": [
			{
				"map_name": "Maidstone",
				"grouping" : "",
				"map_url": "http://exampleserver/mymapforprojectdemo.html"
			},
			{
				"map_name": "Warlingham",
				"grouping": "",
				"map_url": "....html"
			}, 
			{
				"map_name": "Slough",
				"grouping" : "",
				"map_url": "....html"
			}
			]
		}, 
		{
			"project_name": ...

The “grouping” key is to used to group similar maps within a project (e.g. we may have 5 maps that are different variations of the same thing). The code for this module is:

/********************************************************************************************************
 * SearchPage.js -> GIS App (iPad Version)
 ********************************************************************************************************/
'use_strict';

var React = require('react-native')
var SearchResults = require('./SearchResults');
// Using an additional library for HMAC / hashing
// https://www.npmjs.com/package/crypto-js
var CryptoJS = require('crypto-js')
var {
	StyleSheet,
	Text,
	TextInput,
	View,
	TouchableHighlight,
	ActivityIndicatorIOS,
	Image,
	Component
} = React;

// Should go back and remove the fixed positions and replace with
// flex styling (similar to AutoLayout in Xcode)
var styles = StyleSheet.create({
	description: {
		marginBottom: 20,
		fontSize: 18,
		textAlign: 'center',
		color: '#656565'
	},
	container: {
		padding: 30,
		marginTop: 65,
		justifyContent: 'center',
		alignItems: 'center'
	},
	buttonText: {
		fontSize: 18,
	 	color: 'white',
	 	alignSelf: 'center'
	},
	button: {
	 	height: 36,
	  	flex: 1,
	 	flexDirection: 'row',
	  	backgroundColor: '#48BBEC',
	  	borderColor: '#48BBEC',
	  	borderWidth: 1,
	  	borderRadius: 8,
	  	marginTop: 10,
	  	marginBottom: 10,
	  	alignSelf: 'stretch',
	  	justifyContent: 'center',
	  	alignItems: 'center'
	},
	image: {
		alignSelf: 'center',
		marginBottom: 20,
		width: 257,
		height: 47
	},
  	searchInput: {
    	height: 36,
    	padding: 10,
    	marginRight: 5,
    	flex: 1,
    	fontSize: 18,
    	borderWidth: 1,
    	borderColor: '#48BBEC',
    	borderRadius: 8,
    	textAlign: 'center',
    	color: '#48BBEC'
  }
})

class SearchPage extends Component{

	constructor(props) {
		super(props);
		this.state = {
			isLoading: false,
			password: '',
			message: ''
		};
	}

	__handleResponse(response) {
		this.setState( {isLoading: false, message: '' });
		// Update this at some point to better handle response codes
		if (response.application_response_code.substr(0, 1) === '2') {
			console.log('Projects found: ' + response.projects.length);
			this.props.navigator.push({
				title: '',
				component: SearchResults,
				passProps: {projects: response.projects}
			})
		} else {
			this.setState({ message: 'Connected. However, could not load data.'});
		}
	}

	_executeQuery(query) {
		console.log(query);
		this.setState( {isLoading: true });
		fetch(query)
			.then(response => response.json())
			.then(json => this.__handleResponse(json.response))
			// I think only two errors can be caught -> no internet connection
			// or did not connect (which will group many errors)
			.catch(error =>
				this.setState({
					isLoading: false,
					message: 'Could not load data from Internet: ' + error,
					password: ''
				}));
	}

	onSearchPressed() {
		// Any message used
		// Not sure whether to use hashing or signing with HMAC
		// Ended up with HMAC
		// This helps with version control ... maybe??
		var message = 'RandomstringfdfdStringsds=4394£-whichupdates with the version, v1.0'
		// Password user will remember
		//var key = 'hippo'
		var key = this.state.password
		console.log('Entered:')
		console.log(key)
		// Encrypt using SHA256
		var hash = CryptoJS.HmacSHA256(message, key)
		// Make url-safe
		// Drop-box doesn't like "%"
		var hashInBase64 = encodeURIComponent(CryptoJS.enc.Base64.stringify(hash)).replace('%','_')
		// .json file will be saved with this name on the website
		console.log(hashInBase64)
		// First part of url will be common knowledge but hash will be only known with password
		var first_part = 'https://www.dropbox.com/s/mxieft3nmm907sy/'
		// For Drop-box to show in browser
		var last_part =  '.json?raw=1'
		var query =  first_part + hashInBase64 + last_part
		console.log(query)
		this._executeQuery(query);
	}

	onSearchTextChanged(event) {
	 	this.setState({ password: event.nativeEvent.text });
	}

	render() {
		var spinner = this.state.isLoading?
		( <ActivityIndicatorIOS
			hidden='true'
			size='large'/> ) :
		( <View/>);

		return (
			<View style={styles.container}>
			<Image source={require('image!react')} style={styles.image}/>
			<TextInput
				style = {styles.searchInput}
				value = {this.state.password}
				autoFocus = {true}
				returnKeyType="search"
				enablesReturnKeyAutomatically={true}
				autoCapitalize={'none'}
				autoCorrect={false}
				onChange= {this.onSearchTextChanged.bind(this)}
				onSubmitEditing= {this.onSearchPressed.bind(this)}
				placeholder= 'Demo password is hippo' />
			<TouchableHighlight style={styles.button}
				underlayColor='#99d9f4'
				onPress={this.onSearchPressed.bind(this)}>
				<Text style={styles.buttonText}>Load Projects</Text>
			</TouchableHighlight>
			{spinner}
			<Text style={styles.description}>{this.state.message}</Text>
			</View>
		);
	}
}

// Permit use in other files
module.exports = SearchPage;

SearchResults.js

IMG_0008

This uses the ListView.DataSource component to display the data extracted from the json file (actually I have 3 using flex-styling to imitate the split-screen screen view in Swift).

I use ‘rowMapPressed’ to group maps on the third screen (if they belong to a group), otherwise we go straight to the map. For example the maps below have a grouping key which corresponds to their ID so we get all the versions of 3400 on the third screen.

IMG_0009

I have also found 26 letter icons which I match to a row based on the first-letter. Note: I had to use ‘uri:’ rather than ‘require()’ because the former does not allow me to use variables (just a static string).

/********************************************************************************************************
 * SearchResults.js -> GIS App (iPad Version)
 ********************************************************************************************************/
'use strict';

var React = require('react-native');
var MapView = require('./MapView');
var {
	StyleSheet,
	Image,
	View,
	TouchableHighlight,
	ListView,
	Text,
	Component
} = React;

var styles = StyleSheet.create({
	thumb: {
		width: 80,
		height: 80,
		marginRight: 10
	},
	textContainer: {
		flex: 1
	},
	separator: {
		height: 4,
		backgroundColor: '#dddddd'
	},
	title: {
		marginTop: 25,
		fontSize: 20,
		color: '#656565'
	},
	rowContainer: {
		flexDirection: 'row'
	},
	viewContainer: {
		paddingTop: 75,
		flexDirection: 'row',
		flex: 1
	}
});


class SearchResults extends Component {
	// Class to extract individual maps and projects from main json:
	// this.props.projects

	constructor(props) {
		super(props);
		var dataSource_proj = new ListView.DataSource(
			{rowHasChanged: (r1, r2) => r1 !== r2}
		);
		var dataSource_map = new ListView.DataSource(
			{rowHasChanged: (r1, r2) => r1 !== r2}
		);
		var dataSource_groupped = new ListView.DataSource(
			{rowHasChanged: (r1, r2) => r1 !== r2}
		);

		this.state = {
			// We have the projects loaded but not the maps yet
			dataSource_proj: dataSource_proj.cloneWithRows(this.props.projects),
			// Maps will load once the relevant project is clicked
			// Initiate as empty
			dataSource_map: dataSource_map.cloneWithRows([]),
			dataSource_groupped: dataSource_groupped.cloneWithRows([])
		};

	}

	rowProjPressed(proj) {
		// Load maps for selected projected
		var project_name = proj.project_name
		var project = this.props.projects.filter(prop => prop.project_name === project_name)[0];
		console.log('Maps found: ' + project.maps.length);
		this.setState({
			proj: project_name,
			dataSource_map: this.state.dataSource_map.cloneWithRows(project.maps),
			dataSource_groupped: this.state.dataSource_groupped.cloneWithRows([])
		})
	}

	rowMapPressed(project_map) {
		// Get all maps in project with same grouping
		var project = this.props.projects.filter(prop => prop.project_name === this.state.proj)[0];
		var groupped = project.maps.filter(prop => prop.grouping === project_map.grouping)
		// Conditional
		if ((groupped.length > 1 ) && (project_map.grouping != '')) {
			// show others in group (if more than one and group not empty)
			this.setState({
				dataSource_groupped: this.state.dataSource_groupped.cloneWithRows(groupped)
			})
		} else {
			this.setState({
				dataSource_groupped: this.state.dataSource_groupped.cloneWithRows([])
			})
			// Go straight to map
			this.props.navigator.push({
				title: project_map.map_name,
				component: MapView,
				passProps: {
					map_url: project_map.map_url,
				}
			});
		}
	}

	rowGroupPressed(project_map) {
		// Go straight to map
		this.props.navigator.push({
			title: project_map.map_name,
			component: MapView,
			passProps: {
				map_url: project_map.map_url,
			}
		});
	}

	// Avoiding eval but also avoiding having three render_Row functions
	// Since function is same but calls a different function depending
	// on sender
	renderOnPress(type, dta) {
		switch (type) {
			case 'group':
				this.rowGroupPressed(dta)
				break
			case 'map':
				this.rowMapPressed(dta)
				break
			case 'proj':
				this.rowProjPressed(dta)
				break
			default:
				console.log('Unknown sender - break?')
		}

	}

	renderRow(rowType, rowData) {
		// Decide which name to extract
		// Only projects have 'project_name'
		// Name needed for title and auto icon
		switch (rowType) {
			case 'proj':
				var name = rowData.project_name
				break
			default:
				var name = rowData.map_name
			}

		return (
			<TouchableHighlight onPress={() => this.renderOnPress(rowType, rowData)}
				underlayColor='#dddddd'>
				<View>
					<View style={styles.rowContainer}>
						<Image source={{uri: 'Letter-' + name.substr(0,1).toUpperCase()}} 
							style={styles.thumb}/>
					 	<View style={styles.textContainer}>
					 		<Text style={styles.title}
					 			numberOfLines={1}>{name}</Text>
					 	</View>
					</View>
					<View Style={styles.separator}/>
				</View>
			</TouchableHighlight>
			);
	}

	render() {
		// An attempt to create the equivalent of swift's splitview
		// Projects, Maps, (Groups)
		return (
			<View style={styles.viewContainer}>
					<ListView
  						automaticallyAdjustContentInsets={false}
						dataSource={this.state.dataSource_proj}
						renderRow={this.renderRow.bind(this, 'proj')}/>
					<ListView
  						automaticallyAdjustContentInsets={false}
						dataSource={this.state.dataSource_map}
						renderRow={this.renderRow.bind(this, 'map')}/>
					<ListView
  						automaticallyAdjustContentInsets={false}
						dataSource={this.state.dataSource_groupped}
						renderRow={this.renderRow.bind(this, 'group')}/>
			</View>
		);
	}
}

module.exports = SearchResults;

MapView.js

IMG_0011

Quite a short module which uses the WebView component to render the JavaScript. The end-goal is a map that is interactive, here is an example zooming-in on the NE side of the isochrone.

IMG_0012

Final code:

/********************************************************************************************************
 * MapView.js -> CRA GIS App (iPad Version)
 ********************************************************************************************************/
'use strict';

var React = require('react-native');
var {
	WebView,
	Component
} = React;

class MapView extends Component {
	// Class to render the javascript URL and show full-size 
	constructor(props) {
		super(props);
		this.state = {
			map_url : props.map_url
		}
	}

	render() {
		return (
			<WebView
				source={{uri: this.state.map_url}}
				javaScriptEnabled={true}/>
		);
	}
}

module.exports = MapView;