diff options
| -rw-r--r-- | mobile/App.js | 29 | ||||
| -rw-r--r-- | mobile/components/FormPage.js | 20 | ||||
| -rw-r--r-- | mobile/components/GreetingPage.js | 151 | ||||
| -rw-r--r-- | mobile/package.json | 1 | ||||
| -rw-r--r-- | mobile/screens/Form1.js | 86 | ||||
| -rw-r--r-- | web/app.js | 2 | ||||
| -rw-r--r-- | web/public/javascripts/map.js | 119 | ||||
| -rw-r--r-- | web/public/stylesheets/style.css | 53 | ||||
| -rw-r--r-- | web/routes/index.js | 16 | ||||
| -rw-r--r-- | web/routes/sse.js | 15 | ||||
| -rw-r--r-- | web/views/map.pug | 14 |
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> + ); + } +} @@ -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: '© <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> — Map data © <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'). |
