diff --git a/design/Deighton AR Design.sketch b/design/Deighton AR Design.sketch index bf912ce..29e6330 100644 Binary files a/design/Deighton AR Design.sketch and b/design/Deighton AR Design.sketch differ diff --git a/mobile/src/API.js b/mobile/src/API.js index 4853c49..35408e6 100644 --- a/mobile/src/API.js +++ b/mobile/src/API.js @@ -2,7 +2,7 @@ import EventEmitter from "eventemitter3" import io from "socket.io-client" import { AsyncStorage } from "react-native" import autobind from "autobind-decorator" -import { localIPAddr } from "./config" +import { config } from "./config" const authTokenKeyName = "AuthToken" const backendKeyName = "Backend" @@ -37,7 +37,7 @@ class API extends EventEmitter { static urls = { normal: "https://dar.kss.us.com/api", test: "https://dar-test.kss.us.com/api", - local: `http://${localIPAddr || "localhost"}:3001`, + local: `http://${config.localIPAddr || "localhost"}:3001`, } constructor() { @@ -384,6 +384,45 @@ class API extends EventEmitter { return this.delete("/activities/" + _id) } + getAddress(coord) { + var promise = new Promise((resolve, reject) => { + let fetchOptions = { + method: "GET", + mode: "no-cors", + } + + fetch( + config.googleGeocodeURL + + `?latlng=${coord.latitude},${coord.longitude}&key=${ + config.googleAPIKey + }`, + fetchOptions + ) + .then((res) => { + return Promise.all([Promise.resolve(res), res.json()]) + }) + .then((arr) => { + let [res, responseBody] = arr + + if (res.ok) { + let address = "" + + if (responseBody.results && responseBody.result.length > 0) { + address = responseBody.results[0].formatted_address + } + + resolve(address) + } else { + reject(new APIError(res.status, responseBody.message)) + } + }) + .catch((error) => { + reject(new NetworkError(error.message)) + }) + }) + return promise + } + upload(file, progressCallback) { return new Promise((resolve, reject) => { const chunkSize = 32 * 1024 diff --git a/mobile/src/ARViewer/ARViewer.js b/mobile/src/ARViewer/ARViewer.js index 05ceb61..cfc22fe 100644 --- a/mobile/src/ARViewer/ARViewer.js +++ b/mobile/src/ARViewer/ARViewer.js @@ -14,26 +14,20 @@ import { } from "react-viro" import autobind from "autobind-decorator" import backImage from "./images/back.png" - -const styles = { - buttons: { - height: 80, - width: 80, - }, -} +import { config } from "../config" const shapes = { - hardhat: { - shape: require("./models/hardhat_obj.obj"), - materials: [require("./models/hardhat.mtl")], + order: { + source: require("./models/hardhat_obj.obj"), + resources: [require("./models/hardhat.mtl")], }, - question: { - shape: require("./models/question_obj.obj"), - materials: [require("./models/question.mtl")], + complaint: { + source: require("./models/question_obj.obj"), + resources: [require("./models/question.mtl")], }, - clipboard: { - shape: require("./models/clipboard_obj.obj"), - materials: [require("./models/clipboard.mtl")], + inspection: { + source: require("./models/clipboard_obj.obj"), + resources: [require("./models/clipboard.mtl")], }, } const distance = (vectorA, vectorB) => { @@ -139,11 +133,16 @@ class WorkItemSceneAR extends React.Component { @autobind handleClick(position, source) { - // this.props.history.replace("/activity") + const { workItemId } = this.props + + this.props.history.replace( + `/activity${workItemId ? "?workItemId=" + workItemId : ""}` + ) } render() { const { position, scale, rotation, shouldBillboard } = this.state + const shape = shapes[this.props.workItemType] return ( (this.arScene = ref)}> @@ -165,14 +164,16 @@ class WorkItemSceneAR extends React.Component { shadowFarZ={6} shadowOpacity={0.9} /> - + {shape && ( + + )} diff --git a/mobile/src/Activity/Activity.js b/mobile/src/Activity/Activity.js index 515808d..c98d84f 100644 --- a/mobile/src/Activity/Activity.js +++ b/mobile/src/Activity/Activity.js @@ -15,11 +15,11 @@ import { FormBinder } from "react-form-binder" import { Icon, Header, - PhotoButton, BoundInput, BoundButton, BoundOptionStrip, BoundHeader, + PhotoPanel, } from "../ui" import { MessageModal } from "../Modal" import autobind from "autobind-decorator" @@ -73,6 +73,10 @@ export class Activity extends React.Component { isValid: (r, v) => v !== "", alwaysGet: true, }, + location: { + isValid: (r, v) => v !== "", + isReadOnly: true, + }, } constructor(props) { @@ -218,6 +222,8 @@ export class Activity extends React.Component { + }}> + + - Pictures: - - - - - - - - - - + {isIphoneX ? : null} diff --git a/mobile/src/App.js b/mobile/src/App.js index 25f634d..39fbde5 100644 --- a/mobile/src/App.js +++ b/mobile/src/App.js @@ -71,11 +71,11 @@ export default class App extends React.Component { - + diff --git a/mobile/src/Auth/DefaultRoute.js b/mobile/src/Auth/DefaultRoute.js index 9e21135..1323068 100644 --- a/mobile/src/Auth/DefaultRoute.js +++ b/mobile/src/Auth/DefaultRoute.js @@ -3,5 +3,5 @@ import { Route, Redirect } from "react-router-native" export const DefaultRoute = () => { // NOTE: When working on the app, change this to the page you are working on - return } /> + return } /> } diff --git a/mobile/src/Auth/Login.js b/mobile/src/Auth/Login.js index ce0fdf5..23c76fc 100644 --- a/mobile/src/Auth/Login.js +++ b/mobile/src/Auth/Login.js @@ -10,7 +10,7 @@ import { Button, TouchableWithoutFeedback, } from "react-native" -import { MessageModal, ApiModal } from "../Modal" +import { MessageModal, ApiModal, WaitModal } from "../Modal" import logoImage from "./images/deighton.png" import { FormBinder } from "react-form-binder" import { api } from "../API" @@ -19,7 +19,7 @@ import KeyboardSpacer from "react-native-keyboard-spacer" import { versionInfo } from "../version" import autobind from "autobind-decorator" import { isIphoneX } from "react-native-iphone-x-helper" -import { defaultUser } from "../config" +import { config } from "../config" export class Login extends React.Component { static bindings = { @@ -76,9 +76,10 @@ export class Login extends React.Component { constructor(props) { super(props) this.state = { - binder: new FormBinder({ email: defaultUser }, Login.bindings), + binder: new FormBinder({ email: config.defaultUser }, Login.bindings), messageModal: null, apiModal: null, + waitModal: null, } } @@ -88,6 +89,7 @@ export class Login extends React.Component { let { history } = this.props if (obj) { + this.setState({ waitModal: { message: "Loggin In..." } }) api .login(obj.email.trim(), obj.password, obj.rememberMe) .then((user) => { @@ -95,6 +97,7 @@ export class Login extends React.Component { }) .catch((error) => { this.setState({ + waitModal: null, messageModal: { icon: "hand", message: "Unable to login", @@ -178,6 +181,10 @@ export class Login extends React.Component { {versionInfo.fullVersion} + { @@ -80,9 +88,9 @@ export class Home extends React.Component { icon: "hand", message: "You have denied the app access to phone features it needs to function. " + - "Some parts of the app are disabled. To enable these features in future " + - "please go to Settings.", - detail: "", + "Some parts of the app are disabled.", + detail: + "To enable these features in future " + "please go to Settings.", }, }) } @@ -130,11 +138,37 @@ export class Home extends React.Component { viewOffset: 45, }) } + + if (this.state.positionInfo) { + const coords = this.state.positionInfo.coords + const workItem = sections[sectionIndex] + const [lng, lat] = workItem.location.coordinates + + this.setState({ + workItemDistance: geoDistance( + coords.latitude, + coords.longitude, + lat, + lng, + "K" + ).toFixed(2), + }) + } } @autobind handleWorkItemsListPress() { - this.props.history.push("/workitemlist") + const { positionInfo } = this.state + this.props.history.push( + `/workItemList${ + positionInfo + ? "?latLng=" + + coords.latitude.toString() + + "," + + coords.longitude.toString() + : "" + }` + ) } @autobind @@ -149,7 +183,30 @@ export class Home extends React.Component { @autobind handleGlassesPress() { - this.props.history.push("/arviewer") + const { lat: lat1, lng: lng1 } = this.state.positionInfo.coords + const closestWorkItem = null + const shortestDistance = config.minDistanceToItem + + this.state.sections.forEach((workItem) => { + const [lng2, lat2] = workItem.location.coordinates + const distance = geoDistance(lat1, lng1, lat2, lng2, "K") * 1000 + + if (distance <= shortestDistance) { + closestWorkItem = workItem + shortestDistance = distance + } + }) + + this.props.history.push( + `/arviewer${ + closestWorkItem + ? "?workItemId=" + + closestWorkItem._id + + "&workItemType=" + + closestWorkItem.workItemType + : "" + }` + ) } @autobind @@ -176,7 +233,8 @@ export class Home extends React.Component { region, positionInfo, messageModal, - disableARViewer, + haveCameraPermission, + workItemDistance, } = this.state return ( @@ -191,8 +249,10 @@ export class Home extends React.Component { leftButton={{ icon: "logout", onPress: this.handleLogoutPress }} rightButton={{ icon: "glasses", onPress: this.handleGlassesPress }} disabled={ - !(positionInfo && positionInfo.coords.accuracy <= minGPSAccuracy) || - disableARViewer + !( + positionInfo && + positionInfo.coords.accuracy <= config.minGPSAccuracy + ) || !haveCameraPermission } /> this.handleMarkerPress(e, index)} - /> + image={ + workItem.workItemType === "inspection" + ? clipboardPinImage + : workItem.workItemType === "complaint" + ? questionPinImage + : hardhatPinImage + } + onPress={(e) => this.handleMarkerPress(e, index)}> + + + + {pad(workItem.ticketNumber, 4) + + ": " + + workItemTypeText[workItem.workItemType]} + + + {workItem.address} ({this.workItemDistance > 0 + ? this.workItemDistance.toString() + : "?"}{" "} + km) + + + + ))} - {activity.address || "..."} + {activity.when} + + + + + + + + + + ) + } +} diff --git a/mobile/src/Modal/WaitModal.js b/mobile/src/Modal/WaitModal.js new file mode 100644 index 0000000..1ad69f9 --- /dev/null +++ b/mobile/src/Modal/WaitModal.js @@ -0,0 +1,29 @@ +import React, { Component } from "react" +import Modal from "react-native-modal" +import PropTypes from "prop-types" +import { View, ActivityIndicator } from "react-native" + +export class WaitModal extends Component { + static propTypes = { + open: PropTypes.bool, + message: PropTypes.string.isRequired, + } + + render() { + const { open, icon, message, detail } = this.props + + return ( + + + + {message} + + + ) + } +} diff --git a/mobile/src/Modal/images/back.png b/mobile/src/Modal/images/back.png new file mode 100644 index 0000000..0138b94 Binary files /dev/null and b/mobile/src/Modal/images/back.png differ diff --git a/mobile/src/Modal/index.js b/mobile/src/Modal/index.js index 08b28e2..95f01ab 100644 --- a/mobile/src/Modal/index.js +++ b/mobile/src/Modal/index.js @@ -1,2 +1,4 @@ export { MessageModal } from "./MessageModal" export { ApiModal } from "./ApiModal" +export { WaitModal } from "./WaitModal" +export { ImageViewerModal } from "./ImageViewerModal" diff --git a/mobile/src/WorkItem/WorkItem.js b/mobile/src/WorkItem/WorkItem.js index bd1d1db..b196ef1 100644 --- a/mobile/src/WorkItem/WorkItem.js +++ b/mobile/src/WorkItem/WorkItem.js @@ -59,6 +59,10 @@ export class WorkItem extends React.Component { isValid: (r, v) => v !== "", isReadOnly: true, }, + address: { + isValid: true, + isReadOnly: true, + }, details: { isValid: (r, v) => v !== "", }, @@ -118,6 +122,12 @@ export class WorkItem extends React.Component { } } + componentWillUnmount() { + if (this.geoCodeTimer) { + clearTimeout(this.geoCodeTimer) + } + } + @autobind handleBackPress() { const { history } = this.props @@ -180,11 +190,32 @@ export class WorkItem extends React.Component { @autobind handleRegionChange(region) { + const { latitude, longitude } = region + if (this.latLngInput) { - this.latLngInput.handleChangeText( - formatLatLng(region.latitude, region.longitude) - ) + this.latLngInput.handleChangeText(formatLatLng(latitude, longitude)) } + + if (this.geoCodeTimer) { + clearTimeout(this.geoCodeTimer) + } + + this.geoCodeTimer = setTimeout( + () => this.handleStartAddressLookup({ latitude, longitude }), + config.geocodeDelayMilliseconds + ) + } + + @autobind + handleStartAddressLookup(latLng) { + api + .addressLookup(latLng) + .then((address) => { + this.setState({ address }) + }) + .catch(() => { + this.setState({ address: "" }) + }) } render() { @@ -252,6 +283,12 @@ export class WorkItem extends React.Component { name="location" label="Location:" /> + (this.addressInput = ref)} + binder={binder} + name="address" + label="Address:" + /> diff --git a/mobile/src/WorkItem/WorkItemList.js b/mobile/src/WorkItem/WorkItemList.js index bf8b756..750cdaa 100644 --- a/mobile/src/WorkItem/WorkItemList.js +++ b/mobile/src/WorkItem/WorkItemList.js @@ -19,6 +19,7 @@ import { formatLatLng, parseLatLng, pad, + geoDistance, } from "../util" const styles = StyleSheet.create({ @@ -36,6 +37,25 @@ export class WorkItemList extends React.Component { this.state = { messageModal: null, } + + const { search } = this.props.location + + if (search) { + const params = new URLSearchParams(search) + const latLng = params.get("latLng") + + if (latLng) { + const [lat, lng] = latLng.split(",") + + if (lat && lng) { + this.position = { + latitude: parseFloat(lat), + longitude: parseFloat(lng), + } + } + } + } + api .listWorkItems() .then((list) => { @@ -54,7 +74,7 @@ export class WorkItemList extends React.Component { @autobind handleItemSelect(item, ref) { - this.props.history.push(`/workitem?id=${item._id}`) + this.props.history.push(`/workItem?id=${item._id}`) } @autobind @@ -95,7 +115,7 @@ export class WorkItemList extends React.Component { @autobind handleDonePress() { - this.props.history.push("/workitem") + this.props.history.push("/workItem") } @autobind @@ -159,7 +179,17 @@ export class WorkItemList extends React.Component { {workItemTypeText[item.workItemType]} - {`${item.address || "..."} | ??? mi`} + {`${item.address || "..."} | ${ + this.position + ? geoDistance( + this.position.latitude, + this.position.longitude, + item.location.coordinates[1], + item.location.coordinates[0], + "K" + ).toFixed(2) + : "?" + } km`}