aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKumar Damani <damani.kumar@gmail.com>2019-03-08 21:50:43 +0000
committerGitHub <noreply@github.com>2019-03-08 21:50:43 +0000
commita51529eccd60120c34aafe9da9aea0d48750a325 (patch)
treea553e2619100942911e8a1235a63086cc25c091d
parentb254c15754fbcf00457167e6c51dc0e3bfcc083f (diff)
parent032fc2bea5f5c00d4147e4077107f52888b302c4 (diff)
Merge pull request #31 from csc301-winter-2019/qa
Qa
-rw-r--r--mobile/App.js29
-rw-r--r--mobile/components/FormPage.js20
-rw-r--r--mobile/components/GreetingPage.js151
-rw-r--r--mobile/package.json1
-rw-r--r--mobile/screens/Form1.js86
-rw-r--r--web/app.js2
-rw-r--r--web/public/javascripts/map.js119
-rw-r--r--web/public/stylesheets/style.css53
-rw-r--r--web/routes/index.js16
-rw-r--r--web/routes/sse.js15
-rw-r--r--web/views/map.pug14
11 files changed, 473 insertions, 33 deletions
diff --git a/mobile/App.js b/mobile/App.js
index 7271bc2..f36b696 100644
--- a/mobile/App.js
+++ b/mobile/App.js
@@ -1,17 +1,16 @@
import React, { Component } from 'react';
-import { Text, View } from 'react-native';
+import { Text, View, Alert, StyleSheet, Button, AppRegistry, Navigator, TouchableHighlight} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { createStackNavigator, createAppContainer } from 'react-navigation';
-export default class HelloWorldApp extends Component {
- //Every component needs a render() function
- render() {
- return (
- //<View> acts like the way <div> does in JavaScript
- //style={...} stores properties about where you want the component to be placed
- //and how you want it to look. It's similar to adding CSS styles to HTML elements
- //when creating web pages
- <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
- <Text>Hello world!</Text>
- </View>
- );
- }
-}
+import GreetingPage from './components/GreetingPage';
+import FormPage from './screens/Form1';
+
+
+const RootStack = createStackNavigator( {GreetingPage: GreetingPage, FormPage: FormPage,},
+ {headerMode: 'none'})
+
+const App = createAppContainer(RootStack)
+
+export default App;
+
diff --git a/mobile/components/FormPage.js b/mobile/components/FormPage.js
new file mode 100644
index 0000000..9ea3dce
--- /dev/null
+++ b/mobile/components/FormPage.js
@@ -0,0 +1,20 @@
+import React, { Component } from 'react';
+import { Text, View, Alert, StyleSheet, Button, AppRegistry, Navigator, TouchableHighlight} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { createStackNavigator, createAppContainer } from 'react-navigation';
+
+export default class FormPage extends Component {
+ render() {
+ return (
+ <View marginTop = {50}>
+ <Text>
+ Put the form stuff here
+ </Text>
+ <Button
+ onPress={() => this.props.navigation.navigate('GreetingPage')}
+ title='Go Back'
+ />
+ </View>
+ );
+ }
+}
diff --git a/mobile/components/GreetingPage.js b/mobile/components/GreetingPage.js
new file mode 100644
index 0000000..e8da92e
--- /dev/null
+++ b/mobile/components/GreetingPage.js
@@ -0,0 +1,151 @@
+import React, { Component } from 'react';
+import { Text, View, Alert, StyleSheet, Button, AppRegistry, Navigator, TouchableHighlight, Platform} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { createStackNavigator, createAppContainer } from 'react-navigation';
+import { Location, Permissions, Constants } from 'expo';
+
+export default class GreetingPage extends Component {
+ //Every component needs a render() function
+ state = {
+ location: null,
+ errorMessage: null,
+ };
+
+ componentWillMount() {
+ if (Platform.OS === 'android' && !Constants.isDevice) {
+ this.setState({
+ errorMessage: 'This will not work on an android device',
+ });
+ } else {
+ this._getLocationAsync();
+ }
+ }
+
+ _getLocationAsync = async () => {
+ let { status } = await Permissions.askAsync(Permissions.LOCATION);
+ if (status !== 'granted') {
+ this.setState({
+ errorMessage: 'Permission to access location was denied',
+ });
+ }
+
+ let location = await Location.getCurrentPositionAsync({});
+ this.setState({ location });
+ };
+
+ moveToFormPage = () => {
+ this._getLocationAsync;
+ if (this.state.location === null) {
+ Alert.alert("There was a problem accessing your location");
+ return;
+ }
+ const coordinates = [this.state.location.coords.longitude, this.state.location.coords.latitude];
+ this.props.navigation.navigate('FormPage', {coordinates : coordinates});
+ }
+
+
+ render() {
+ return (
+ //<View> acts like the way <div> does in JavaScript
+ //style={...} stores properties about where you want the component to be placed
+ //and how you want it to look. It's similar to adding CSS styles to HTML elements
+ //when creating web pages
+
+ <View style={styles.container}>
+
+ <View style={styles.titleContainer}>
+ <Text style={styles.title}>
+ Ensure that location services are enabled!
+ </Text>
+ </View>
+
+ <TouchableHighlight
+ style={styles.first_button_container}
+ onPress={() => { this.moveToFormPage()
+ }}>
+
+ <View style={styles.press_button}>
+
+ <View>
+ <Text style={styles.current_location_title}>
+ Use Current Location
+ </Text>
+ </View>
+
+ <View marginTop = {20}>
+ <Ionicons name="md-locate" size={40} color="white"/>
+ </View>
+
+ </View>
+
+ </TouchableHighlight>
+
+ <TouchableHighlight style={styles.second_button_container}>
+ <View style={styles.press_button}>
+
+ <View>
+ <Text style={styles.current_location_title}>
+ Drop Pin
+ </Text>
+ </View>
+
+ <View marginTop = {20}>
+ <Ionicons name="md-pin" size={40} color="white"/>
+ </View>
+
+ </View>
+ </TouchableHighlight>
+
+ </View>
+ );
+ }
+}
+
+
+const styles = StyleSheet.create( {
+ container: {
+ flex: 1,
+ flexDirection: 'column',
+ backgroundColor: '#40739e',
+ alignItems: 'stretch'
+ },
+ titleContainer: {
+ flex: 3,
+ //position: 'absolute',
+ marginTop: 205,
+ //flexGrow: 1
+ },
+ title: {
+ color: '#FFF',
+ marginTop: 10,
+ textAlign: 'center',
+ opacity: 0.9,
+ fontSize: 18,
+ fontStyle: 'italic'
+ },
+ current_location_title: {
+ color: '#FFF',
+ marginTop: 15,
+ textAlign: 'center',
+ opacity: 0.9,
+ fontSize: 20
+ },
+ first_button_container: {
+ flex: 1,
+ //position: 'absolute',
+ //marginTop: 360,
+ backgroundColor: '#c0392b',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'stretch'
+ },
+ press_button: {
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ second_button_container: {
+ flex: 1,
+ backgroundColor: '#535c68'
+ }
+})
diff --git a/mobile/package.json b/mobile/package.json
index 0c3b6c2..488e885 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -15,6 +15,7 @@
"expo": "^32.0.0",
"react": "16.5.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz",
+ "react-native-elements": "^1.1.0",
"react-navigation": "^3.0.9"
},
"devDependencies": {
diff --git a/mobile/screens/Form1.js b/mobile/screens/Form1.js
new file mode 100644
index 0000000..16ced64
--- /dev/null
+++ b/mobile/screens/Form1.js
@@ -0,0 +1,86 @@
+import React, { Component } from 'react';
+import {Text, Alert, AppRegistry, StyleSheet, View } from 'react-native';
+import { Button, Input, CheckBox } from 'react-native-elements';
+
+export default class Form1 extends Component {
+ _onPressButton1() {
+ const { navigation } = this.props;
+ const coordinates = navigation.getParam('coordinates');
+ //var coordinates = this.coordinates ? coordinates : [];
+ fetch('https://helpthehome-qa.herokuapp.com/mobilerequest', {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ 'coordinates': coordinates,
+ 'type': "Point",
+ 'isInjured': this.state.injured,
+ 'reasonForHelp': this.state.why,
+ 'ageRange': this.state.age,
+ 'clothingDescription': this.state.appearance
+ }),
+ });
+ // TODO: handle the response from the serve and decide what to display
+ // based on that
+ // for now just to back to the home page
+ Alert.alert("You will now be redirected to the main page");
+ this.props.navigation.navigate('GreetingPage');
+}
+ _onPressButton2() {
+ Alert.alert('Continue:')
+ }
+ constructor(props) {
+ super(props);
+ this.state = {
+ age: '',
+ appearance: '',
+ injured: false,
+ why: ''
+ };
+ }
+
+ render() {
+ return (
+ <View style={{padding: 40}}>
+ <Text>Age:</Text>
+ <Input
+ style={{height: 40}}
+ placeholder="Estimate is fine"
+ onChangeText={(age) => this.setState({age})}
+ />
+ <Text>Appearance:</Text>
+ <Input
+ style={{height: 40}}
+ placeholder="What are they wearing?"
+ onChangeText={(appearance) => this.setState({appearance})}
+ />
+ <CheckBox
+ center
+ title="Are they injured?"
+ checked={this.state.injured}
+ onPress={() => this.setState({ injured: !this.state.injured })}
+ />
+ <Text>Reason for help?</Text>
+ <Input
+ style={{height: 40}}
+ placeholder="Please keep it short"
+ onChangeText={(why) => this.setState({why})}
+ />
+ <View style={{padding: 40}}>
+ <Button
+ buttonStyle={{backgroundColor:"green"}}
+ onPress={() => this._onPressButton1()}
+ title="Submit!"
+ />
+ <Button
+ containerStyle={{paddingTop: 10}}
+ onPress={() => this.props.navigation.navigate('GreetingPage')}
+ title="Go Back"
+ />
+ </View>
+ </View>
+ );
+ }
+}
diff --git a/web/app.js b/web/app.js
index f0ae3a5..9d6e9d0 100644
--- a/web/app.js
+++ b/web/app.js
@@ -19,6 +19,7 @@ app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
+app.use(require('./routes/sse'));
var env = process.env.NODE_ENV || 'development';
var uri;
@@ -45,7 +46,6 @@ mongoose.connect(uri, { useNewUrlParser: true }, function (error) {
app.use('/', indexRouter);
app.use('/users', usersRouter);
-
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
diff --git a/web/public/javascripts/map.js b/web/public/javascripts/map.js
index 190307d..3f33bcf 100644
--- a/web/public/javascripts/map.js
+++ b/web/public/javascripts/map.js
@@ -3,6 +3,88 @@
// attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
// }).addTo(map);
+function plotPointsOnMap(points) {
+ L.geoJson(points, {
+ pointToLayer: function (feature, latlng) {
+ //return L.circleMarker(latlng);
+ latlngbounds.extend(latlng);
+ return L.marker(latlng);
+ }
+ }).on('click', showDetails).addTo(map)
+
+ // rezoom the map so that all the markers fit in the view, add 20% padding so
+ // that marker dont cut off
+ map.fitBounds(latlngbounds.pad(0.20));
+}
+
+// show details about point
+// e is the event info
+function showDetails(e) {
+ // layer.feature.geometry gives you access to all the fields
+ let layer = e.layer
+
+ let sideBar = document.getElementById('sidebar')
+
+ if (getComputedStyle(sideBar).visibility === 'hidden') {
+ sideBar.style.visibility = 'visible'
+ }
+
+ let point = document.getElementById('point')
+
+ //get the previous input text from the previous point
+ let prevUserInput = point.getElementsByClassName('user-input')
+
+ // remove the previous point text
+ while (prevUserInput.length !== 0) {
+ prevUserInput[0].parentNode.removeChild(prevUserInput[0])
+ }
+
+ let pointBreaks = point.getElementsByClassName('point-break')
+
+ // put age range of person
+ let ageRangeTextSpan = document.createElement('span')
+ ageRangeTextSpan.className = 'user-input'
+ let ageRangeText = document.createTextNode(layer.feature.geometry['ageRange'])
+ ageRangeTextSpan.appendChild(ageRangeText)
+ pointBreaks[0].parentNode.insertBefore(ageRangeTextSpan, pointBreaks[0])
+
+ // put clothing description of person
+ let clothingDescTextSpan = document.createElement('span')
+ clothingDescTextSpan.className = 'user-input'
+ let clothingDescText = document.createTextNode(layer.feature.geometry['clothingDescription'])
+ clothingDescTextSpan.appendChild(clothingDescText)
+ pointBreaks[1].parentNode.insertBefore(clothingDescTextSpan, pointBreaks[1])
+
+ // put whether person is injured or not
+ let isInjured = layer.feature.geometry['isInjured']
+ let injurySpan = document.createElement('span')
+ injurySpan.className = 'user-input'
+ if (isInjured) {
+ injurySpan.appendChild(document.createTextNode('Injured'))
+ injurySpan.style.color = 'red'
+ }
+ else {
+ injurySpan.appendChild(document.createTextNode('Not injured'))
+ injurySpan.style.color = 'green'
+ }
+ pointBreaks[2].parentNode.insertBefore(injurySpan, pointBreaks[2])
+
+ // put reason for help
+ let helpReasonTextSpan = document.createElement('span')
+ helpReasonTextSpan.className = 'user-input'
+ let helpReasonText = document.createTextNode(layer.feature.geometry['reasonForHelp'])
+ helpReasonTextSpan.appendChild(helpReasonText)
+ point.appendChild(helpReasonTextSpan)
+
+ let closeBtn = document.getElementById('close-btn')
+ closeBtn.addEventListener('click', closeDetails)
+}
+
+function closeDetails(e) {
+ let details = document.getElementById('sidebar')
+ details.style.visibility = 'hidden'
+}
+
// different basemap
L.tileLayer('https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.{ext}', {
attribution: 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a> &mdash; Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
@@ -12,12 +94,31 @@ L.tileLayer('https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}
ext: 'png'
}).addTo(map);
-L.geoJson(points, {
- pointToLayer: function (feature, latlng) {
- //return L.circleMarker(latlng);
- return L.marker(latlng);
- }
-}).bindPopup(function (layer) {
- // layer.feature.geometry gives you access to all the fields
- return "<p>" + JSON.stringify(layer.feature.geometry) + "</p>";
-}).addTo(map)
+// keep track of the boundary of the markers so that we can update the
+// map to fit them all
+var latlngbounds = new L.latLngBounds();
+
+plotPointsOnMap(points);
+if (window.EventSource) {
+ var source = new EventSource('/stream');
+
+ source.addEventListener('message', function(e) {
+ var data = JSON.parse(e.data);
+ if (data.coordinates) {
+ points.push(data);
+ plotPointsOnMap(points);
+ }
+ }, false)
+
+ source.addEventListener('open', function(e) {
+ console.log("Connection was opened")
+ }, false)
+
+ source.addEventListener('error', function(e) {
+ if (e.readyState == EventSource.CLOSED) {
+ console.log("Connection was closed")
+ }
+ }, false)
+} else {
+ console.log("sse not supported.");
+}
diff --git a/web/public/stylesheets/style.css b/web/public/stylesheets/style.css
index c3bd05c..d8618cd 100644
--- a/web/public/stylesheets/style.css
+++ b/web/public/stylesheets/style.css
@@ -3,9 +3,10 @@ html, body {
overflow: hidden;
}
#map {
- width: 65%;
+ width: 100%;
height: 100%;
- float: right;
+ position: absolute;
+ z-index: 1;
}
body {
margin:0;
@@ -16,11 +17,51 @@ a {
color:#00B7FF;
}
#sidebar {
- width: 35%;
- height: 100%;
+ top: 5px;
+ left: 5px;
+ z-index: 2;
+ width: 350px;
+ height: 275px;
+ max-height: 275px;
+ position: relative;
float: left;
- background-color: #e70000;
+ background-color: lightgrey;
+ border-radius: 10px;
+ visibility: hidden;
}
-h1 {
+#details-header {
text-align: center;
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+#point-container {
+ overflow: auto;
+ max-height: 195px;
}
+#point {
+ background-color: #0CC5EA;
+ width: 90%;
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 5px;
+ padding: 5px;
+ border-radius: 10px;
+}
+#close-btn-container {
+ position: fixed;
+ height: 30px;
+ top: 250px;
+ background-color: lightgrey;
+ width: 350px;
+ border-bottom-left-radius: 10px;
+ border-bottom-right-radius: 10px;
+}
+#close-btn {
+ background-color: red;
+ position: relative;
+ margin-top: 4px;
+ float: right;
+ margin-right: 20px;
+ border-radius: 10px;
+ width: 100px;
+} \ No newline at end of file
diff --git a/web/routes/index.js b/web/routes/index.js
index b2d0796..b2ba91f 100644
--- a/web/routes/index.js
+++ b/web/routes/index.js
@@ -2,7 +2,7 @@ var express = require('express');
var router = express.Router();
var Mockpoints = require('../models/mockpoints');
var Points = require('../models/points');
-
+const connections = [];
/* GET home page. */
router.get('/', function(req, res, next) {
// for now just redirect to /map
@@ -36,10 +36,24 @@ router.post('/mobilerequest', function(req, res) {
console.log(`err inserting mobile request into db: ${err}`);
res.status(500).send({ error: "boo:(" });
} else {
+ // send event to all connections
+ for(var i = 0; i < connections.length; i++) {
+ connections[i].sseSend(data);
+ }
console.log(result);
res.status(200).send({'status': 'success', 'data': data});
}
});
+
});
+
+router.get('/stream', function(req, res){
+ // set up server side event (communication line between front end and server)
+ res.sseSetup();
+ // send an event to the front end to open the connection
+ res.sseSend('ok');
+ // push connection to connection array
+ connections.push(res);
+});
module.exports = router;
diff --git a/web/routes/sse.js b/web/routes/sse.js
new file mode 100644
index 0000000..5cd9a3f
--- /dev/null
+++ b/web/routes/sse.js
@@ -0,0 +1,15 @@
+module.exports = function (req, res, next) {
+ res.sseSetup = function() {
+ res.writeHead(200, {
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ 'Connection': 'keep-alive'
+ })
+ }
+
+ res.sseSend = function(data) {
+ res.write("data: " + JSON.stringify(data) + "\n\n");
+ }
+
+ next()
+} \ No newline at end of file
diff --git a/web/views/map.pug b/web/views/map.pug
index 725c8a8..f5837b6 100644
--- a/web/views/map.pug
+++ b/web/views/map.pug
@@ -1,7 +1,19 @@
extends layout.pug
block content
#sidebar
- h1 This is a sidebar
+ h1#details-header Details
+ #point-container
+ #point
+ strong Age range:
+ br.point-break
+ strong Clothing description:
+ br.point-break
+ strong Injury status:
+ br.point-break
+ strong Reason for help:
+ #close-btn-container
+ button#close-btn Close
+
#map
script(type='text/javascript').