URL: www.cardiocarto.com
Cardiovascular Cartography draws inspiration from MapMyRun and allows users to map and document their running routes. When a route is documented it allows the user to keep track of the route's distance and duration and for created maps that have not been logged as complete, provides an estimated duration. Each user also has a dashboard that displays their most recent completed routes and provides their total distance and duration across all completed routes.
Included are social features which allow users to upload and display an avatar, search for friends, create friendships, and view a feed of recent activity from friends.
This project was developed in two weeks utilizing Ruby on Rails, React, Redux, Google Maps, and Amazon S3.
Using Google Maps JavaScript API users are able to draw their route by placing markers and receiving real-time rendering and feedback on their route's attributes.
In the process of creating a route, a user would expect tools that showed them useful information based on their current map, their current location, as well as the ability to reposition placed markers, undo markers, and to clear the entire map without requiring a reload or losing their current view.
When a marker is created, it is designated as being draggable and an event listener is added to direct the map reprocess the directions when a marker is dragged.
addMarker(position){
const marker = new google.maps.Marker({
//... other marker creation code
draggable: true
});
marker.addListener('dragend', () => this.directions());
//... rest of code
}
The user enters their completion time of the route in HH:MM:SS format, the Google Maps API returns time not as a total, but for each leg segment, and the db schema stores time in seconds. The route creator uses a utility to keep all these values in sync.
First I process each leg of the route Google Maps returns as a response to the directions query to cumulative estimated duration for the route.
export const duration = (routes) => {
const time = routes.legs.map((leg) => leg.duration.value);
return Math.floor(time.reduce((a, b) => a + b);
};
Now to present this in a useful format to the user, it's processed again to display in HH:MM:SS.
export const formatTime = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
return [
h,
m > 9 ? m : '0' + m,
s > 9 ? s : '0' + s
].filter(s => s).join(':');
};
The user may mark the route as completed, and enter their own time that differs from the walking estimate by Google Maps, the user enters this as HH:MM:SS so I needed to update the state (which stores duration in seconds) in real time to prepare for form submission, for this I wrote a small helper to make this conversion easier:
export const completionTime = (hh, mm, ss) => (
(hh*3600) + (mm*60) + Number(ss)
);
These tools are used throughout the site the keep backend values and presentational values synced to provide the user with a meaningful format and minimize the number of columns on the backend.
I keep track of the markers and the appropriate order for the directions service by labeling them alphabetically, and storing them as key value pairs.
addMarker(position){
const marker = new google.maps.Marker({
position: position,
label: this.labels[this.labelIndex++ % this.labels.length],
map: this.map,
draggable: true
});
marker.addListener('dragend', () => this.directions());
this.routeMarkers[marker.label] = marker;
}
This method was chosen as in addition to requiring a start and end position, Google Maps Directions service accepts ordered waypoints. By tracking the label index, it becomes quicker to generate ordered requests.
The route index shows all the user's created and completed routes. It provides view filtering to show the specific route types and display method the user selects.
To facilitate DRY coding practices, I integrated dynamic index generation dependent on user selected filters. DetailIndexItem
or ThumbnailIndexItem
is provided as a conditional parameter to createIndexItems()
where it is then used to assign the component type to be used in building the index.
createIndexItems(type, routes) {
const DetailItem = ({ component: Component, route }) => (
<Component route={ route }/>
);
return routes.map((route) => (<
DetailItem
component={ type }
key={`${type}` + route.id}
route={ route }
/>)
);
}
To allow users to find and send friend requests without needing a direct link to that user's page. I implemented a search of users that are not currently in a friendship stage (sent / pending / active) with the user.
In order to filter down to the eligible users that the current user can send a request to, a backend db query is necessary:
def find_friends(query)
User.all.where
.not(id: self
.friends
.push(self)
.push(self.pending_friends))
.where("first_name LIKE :query OR last_name LIKE :query", query: "%#{query}%")
end
Below is a list of other noteworthy features completed during the two week project not demonstrated above.
- Route Show
- Generated paragraph description of the route based on captured data.
- User submitted comments
- Mark route as completed / incomplete and update route completion time through modal.
- User Dashboard
- Total distance of all completed routes.
- Total duration of all completed routes.
- Most recent completed routes in detail, pending route thumbnails.
- Friendship dashboard
- Active friendships.
- Friendship requests awaiting their response.
- Sent requests that can be canceled.
- User
- Modal with stylized image upload to Amazon S3 in order to change avatar.
I have really enjoyed working on this project for two weeks and during the course of development I was able to identify opportunities to continue working on and expand the project. I have listed the most viable of those opportunities below and genuinely look forward to tackling them.
Re-purpose the logged in user's dashboard into a more generalized component to allow users to view other users' routes and statistics.
Use user submissions to provide a heat map of popular route locations.
The current search method sends a request to the API to return a list of all potential friends matching a query string. Bright minds have invested a lot of time developing strong search utilities and I would like to implement a 3rd party tool for search.
Recreate the functionality of the app utilizing React Native.
I currently implemented a current location tool to center the map on the user's current location. I would like to explore expanding this to allow a user to track runs in real time. Pressing start at the beginning would drop the first marker and continue to check the user's current location at a set interval, dropping more markers as the user moves away from the last marker.