3. public void showRide(long userId) {
InteractionDialog id = new InteractionDialog(new BorderLayout());
id.setTitle("Loading Ride Details");
id.add(CENTER, new InfiniteProgress());
id.show(getHeight() - id.getPreferredH(), 0, 0, 0);
DriverService.fetchRideDetails(userId, ride -> {
id.setTitle("Building Ride Path");
final Coord[] locations = new Coord[2];
if(ride.from.get() == null) {
id.dispose();
ToastBar.showErrorMessage("Ride no longer available...");
return;
}
SearchService.findLocation(ride.from.get(), fromLocation -> {
locations[0] = fromLocation;
onShowRideResponse(id, ride, locations);
});
SearchService.findLocation(ride.destination.get(), toLocation -> {
locations[1] = toLocation;
onShowRideResponse(id, ride, locations);
});
});
}
showRide
Let's see how this all comes together in the changes made to MapForm. The first method we saw in the callback from the push notification is the showRide method.
We use an InteractionDialog for simplicity, I show it in the bottom of the form. The arguments for position indicate the distance from each edge so it touches all the edges
other than the top edge. As a side note notice that the regular Dialog doesn't work well on top of maps.
4. public void showRide(long userId) {
InteractionDialog id = new InteractionDialog(new BorderLayout());
id.setTitle("Loading Ride Details");
id.add(CENTER, new InfiniteProgress());
id.show(getHeight() - id.getPreferredH(), 0, 0, 0);
DriverService.fetchRideDetails(userId, ride -> {
id.setTitle("Building Ride Path");
final Coord[] locations = new Coord[2];
if(ride.from.get() == null) {
id.dispose();
ToastBar.showErrorMessage("Ride no longer available...");
return;
}
SearchService.findLocation(ride.from.get(), fromLocation -> {
locations[0] = fromLocation;
onShowRideResponse(id, ride, locations);
});
SearchService.findLocation(ride.destination.get(), toLocation -> {
locations[1] = toLocation;
onShowRideResponse(id, ride, locations);
});
});
}
showRide
By the time the callback is returned from fetchRideDetails the ride might no longer be available so we can just dispose the UI and continue as usual
5. public void showRide(long userId) {
InteractionDialog id = new InteractionDialog(new BorderLayout());
id.setTitle("Loading Ride Details");
id.add(CENTER, new InfiniteProgress());
id.show(getHeight() - id.getPreferredH(), 0, 0, 0);
DriverService.fetchRideDetails(userId, ride -> {
id.setTitle("Building Ride Path");
final Coord[] locations = new Coord[2];
if(ride.from.get() == null) {
id.dispose();
ToastBar.showErrorMessage("Ride no longer available...");
return;
}
SearchService.findLocation(ride.from.get(), fromLocation -> {
locations[0] = fromLocation;
onShowRideResponse(id, ride, locations);
});
SearchService.findLocation(ride.destination.get(), toLocation -> {
locations[1] = toLocation;
onShowRideResponse(id, ride, locations);
});
});
}
showRide
We need to find the coordinates on the map of the source and destination. I could have just transferred the map coordinates in the ride data (but I didn't)... If findLocation
was synchronous I could just invoke find on from then on destination and everything would work but it would be slow as it would require that we make the first
WebService call and then the second one. In this way both calls might happen concurrently. The first one to complete will have one location in the locations array and
the second one will have both. The onShowRideResponse doesn't do anything unless both locations are set…
A race condition won’t be possible here since the callbacks are invoked on the EDT so they will always happen in sequence.
6. void onShowRideResponse(InteractionDialog dlg, Ride ride,
Coord[] locations) {
if(locations[0] == null || locations[1] == null) {
return;
}
SearchService.directions(toLocation(locations[0]), toLocation(locations[1]),
(path, duration, distance) -> {
dlg.dispose();
String from = ride.from.get();
String to = ride.destination.get();
Component fromComponent = createNavigationTag(
trimmedString(from), duration / 60);
Component toComponent = createNavigationTag(trimmedString(to), -1);
MapContainer.MapObject pathObject = addPath(path,
fromComponent, toComponent, duration);
InteractionDialog id = new InteractionDialog("Ride", BoxLayout.y());
id.add(new Label(ride.name.get(), "RideTitle"));
Button acceptButton = new Button("Accept", "BlackButton");
Button cancelButton = new Button("Cancel", "BlackButton");
id.setAnimateShow(false);
id.add(acceptButton);
id.add(cancelButton);
cancelButton.addActionListener(e -> {
fromComponent.remove();
toComponent.remove();
onShowRideResponse
The obvious next step is the onShowRideResponse method
As I mentioned before the from/destination values are fetched concurrently so this method will be invoked twice. Only the second time would be valid
7. void onShowRideResponse(InteractionDialog dlg, Ride ride,
Coord[] locations) {
if(locations[0] == null || locations[1] == null) {
return;
}
SearchService.directions(toLocation(locations[0]), toLocation(locations[1]),
(path, duration, distance) -> {
dlg.dispose();
String from = ride.from.get();
String to = ride.destination.get();
Component fromComponent = createNavigationTag(
trimmedString(from), duration / 60);
Component toComponent = createNavigationTag(trimmedString(to), -1);
MapContainer.MapObject pathObject = addPath(path,
fromComponent, toComponent, duration);
InteractionDialog id = new InteractionDialog("Ride", BoxLayout.y());
id.add(new Label(ride.name.get(), "RideTitle"));
Button acceptButton = new Button("Accept", "BlackButton");
Button cancelButton = new Button("Cancel", "BlackButton");
id.setAnimateShow(false);
id.add(acceptButton);
id.add(cancelButton);
cancelButton.addActionListener(e -> {
fromComponent.remove();
toComponent.remove();
onShowRideResponse
I used Coord at some points and Location at others. The reason for both objects is mostly historic where the map API works with one and the Location API's work with
another so the toLocation method just translates Coord to the Location object type
8. void onShowRideResponse(InteractionDialog dlg, Ride ride,
Coord[] locations) {
if(locations[0] == null || locations[1] == null) {
return;
}
SearchService.directions(toLocation(locations[0]), toLocation(locations[1]),
(path, duration, distance) -> {
dlg.dispose();
String from = ride.from.get();
String to = ride.destination.get();
Component fromComponent = createNavigationTag(
trimmedString(from), duration / 60);
Component toComponent = createNavigationTag(trimmedString(to), -1);
MapContainer.MapObject pathObject = addPath(path,
fromComponent, toComponent, duration);
InteractionDialog id = new InteractionDialog("Ride", BoxLayout.y());
id.add(new Label(ride.name.get(), "RideTitle"));
Button acceptButton = new Button("Accept", "BlackButton");
Button cancelButton = new Button("Cancel", "BlackButton");
id.setAnimateShow(false);
id.add(acceptButton);
id.add(cancelButton);
cancelButton.addActionListener(e -> {
fromComponent.remove();
toComponent.remove();
onShowRideResponse
This creates a map path similar to the one the user sees on his phone so the driver and user have a similar view of the trip properties and duration. I'll discuss the
addPath method later, I refactored some code from the enterNavigationMode method into that new method
9. void onShowRideResponse(InteractionDialog dlg, Ride ride,
Coord[] locations) {
if(locations[0] == null || locations[1] == null) {
return;
}
SearchService.directions(toLocation(locations[0]), toLocation(locations[1]),
(path, duration, distance) -> {
dlg.dispose();
String from = ride.from.get();
String to = ride.destination.get();
Component fromComponent = createNavigationTag(
trimmedString(from), duration / 60);
Component toComponent = createNavigationTag(trimmedString(to), -1);
MapContainer.MapObject pathObject = addPath(path,
fromComponent, toComponent, duration);
InteractionDialog id = new InteractionDialog("Ride", BoxLayout.y());
id.add(new Label(ride.name.get(), "RideTitle"));
Button acceptButton = new Button("Accept", "BlackButton");
Button cancelButton = new Button("Cancel", "BlackButton");
id.setAnimateShow(false);
id.add(acceptButton);
id.add(cancelButton);
cancelButton.addActionListener(e -> {
fromComponent.remove();
toComponent.remove();
onShowRideResponse
We create a new InteractionDialog to prompt the driver whether he is interested in accepting this ride or not. It would look roughly like this image
10. id.add(cancelButton);
cancelButton.addActionListener(e -> {
fromComponent.remove();
toComponent.remove();
mc.removeMapObject(pathObject);
id.dispose();
});
acceptButton.addActionListener(e -> {
boolean accept = DriverService.acceptRide(ride.userId.getLong());
callSerially(() -> {
if(accept) {
id.dispose();
pickUpPassenger(pathObject, ride, fromComponent,
toComponent);
} else {
id.dispose();
fromComponent.remove();
toComponent.remove();
mc.removeMapObject(pathObject);
getAnimationManager().flushAnimation(() ->
ToastBar.showErrorMessage("Failed to grab ride"));
}
});
});
id.show(getHeight() - id.getPreferredH(), 0, 0, 0);
onShowRideResponse
In the case he doesn't want to accept the ride the work is pretty easy, we just remove everything we added
11. id.add(cancelButton);
cancelButton.addActionListener(e -> {
fromComponent.remove();
toComponent.remove();
mc.removeMapObject(pathObject);
id.dispose();
});
acceptButton.addActionListener(e -> {
boolean accept = DriverService.acceptRide(ride.userId.getLong());
callSerially(() -> {
if(accept) {
id.dispose();
pickUpPassenger(pathObject, ride, fromComponent,
toComponent);
} else {
id.dispose();
fromComponent.remove();
toComponent.remove();
mc.removeMapObject(pathObject);
getAnimationManager().flushAnimation(() ->
ToastBar.showErrorMessage("Failed to grab ride"));
}
});
});
id.show(getHeight() - id.getPreferredH(), 0, 0, 0);
onShowRideResponse
If he does accept the ride we trigger the accept method. Notice that the accept call is a blocking call so just to be safe I used callSerially afterwards to flush any EDT
related animation before showing the next UI
12. id.add(cancelButton);
cancelButton.addActionListener(e -> {
fromComponent.remove();
toComponent.remove();
mc.removeMapObject(pathObject);
id.dispose();
});
acceptButton.addActionListener(e -> {
boolean accept = DriverService.acceptRide(ride.userId.getLong());
callSerially(() -> {
if(accept) {
id.dispose();
pickUpPassenger(pathObject, ride, fromComponent,
toComponent);
} else {
id.dispose();
fromComponent.remove();
toComponent.remove();
mc.removeMapObject(pathObject);
getAnimationManager().flushAnimation(() ->
ToastBar.showErrorMessage("Failed to grab ride"));
}
});
});
id.show(getHeight() - id.getPreferredH(), 0, 0, 0);
onShowRideResponse
This shows the pickup UI and passes all the components/objects we'll need to eventually "clean up" such as the path and the components on the map
14. private Location toLocation(Coord crd) {
return new Location(crd.getLatitude(), crd.getLongitude());
}
toLocation
For completeness the toLocation method is this. It converts Coord objects to Location objects
15. public void pickUpPassenger(MapContainer.MapObject pathObject,
Ride ride,
Component fromComponent, Component toComponent) {
InteractionDialog id = new InteractionDialog("Pick Up", BoxLayout.y());
id.add(new Label(ride.name.get(), "RideTitle"));
Button acceptButton = new Button("Picked Up", "BlackButton");
Button cancelButton = new Button("Cancel", "BlackButton");
id.add(acceptButton);
id.add(cancelButton);
acceptButton.addActionListener(e -> {
DriverService.startRide();
id.dispose();
InteractionDialog dlg = new InteractionDialog("Driving...", BoxLayout.y());
dlg.add(new Label(ride.name.get(), "RideTitle"));
Button finishButton = new Button("Finished Ride", "BlackButton");
dlg.add(finishButton);
finishButton.addActionListener(ee -> {
DriverService.finishRide();
fromComponent.remove();
toComponent.remove();
mc.removeMapObject(pathObject);
dlg.dispose();
});
dlg.show(getHeight() - dlg.getPreferredH(), 0, 0, 0);
});
cancelButton.addActionListener(e -> {
fromComponent.remove();
pickUpPassenger
Now we can proceed to the pickUpPassenger method where we handle the rest of the ride.
This is again a pretty standard approach by now I'm creating another dialog with the Picked up button as the accept option
16. public void pickUpPassenger(MapContainer.MapObject pathObject,
Ride ride,
Component fromComponent, Component toComponent) {
InteractionDialog id = new InteractionDialog("Pick Up", BoxLayout.y());
id.add(new Label(ride.name.get(), "RideTitle"));
Button acceptButton = new Button("Picked Up", "BlackButton");
Button cancelButton = new Button("Cancel", "BlackButton");
id.add(acceptButton);
id.add(cancelButton);
acceptButton.addActionListener(e -> {
DriverService.startRide();
id.dispose();
InteractionDialog dlg = new InteractionDialog("Driving...", BoxLayout.y());
dlg.add(new Label(ride.name.get(), "RideTitle"));
Button finishButton = new Button("Finished Ride", "BlackButton");
dlg.add(finishButton);
finishButton.addActionListener(ee -> {
DriverService.finishRide();
fromComponent.remove();
toComponent.remove();
mc.removeMapObject(pathObject);
dlg.dispose();
});
dlg.show(getHeight() - dlg.getPreferredH(), 0, 0, 0);
});
cancelButton.addActionListener(e -> {
fromComponent.remove();
pickUpPassenger
The Accept button leads us to the last step which is the Finish button
17. public void pickUpPassenger(MapContainer.MapObject pathObject,
Ride ride,
Component fromComponent, Component toComponent) {
InteractionDialog id = new InteractionDialog("Pick Up", BoxLayout.y());
id.add(new Label(ride.name.get(), "RideTitle"));
Button acceptButton = new Button("Picked Up", "BlackButton");
Button cancelButton = new Button("Cancel", "BlackButton");
id.add(acceptButton);
id.add(cancelButton);
acceptButton.addActionListener(e -> {
DriverService.startRide();
id.dispose();
InteractionDialog dlg = new InteractionDialog("Driving...", BoxLayout.y());
dlg.add(new Label(ride.name.get(), "RideTitle"));
Button finishButton = new Button("Finished Ride", "BlackButton");
dlg.add(finishButton);
finishButton.addActionListener(ee -> {
DriverService.finishRide();
fromComponent.remove();
toComponent.remove();
mc.removeMapObject(pathObject);
dlg.dispose();
});
dlg.show(getHeight() - dlg.getPreferredH(), 0, 0, 0);
});
cancelButton.addActionListener(e -> {
fromComponent.remove();
pickUpPassenger
Once we picked up a passenger we can only finish the ride, not cancel it. Since I didn't integrate any billing I think it makes sense. With billing we'll obviously need a way
to pay and refund a ride
18. private MapContainer.MapObject addPath(List<Coord> path,
Component fromComponent, Component toComponent, int duration) {
Coord[] pathCoords = new Coord[path.size()];
path.toArray(pathCoords);
MapContainer.MapObject pathObject = mc.addPath(pathCoords);
BoundingBox bb = BoundingBox.create(pathCoords).
extend(new BoundingBox(pathCoords[0], 0.01, 0.01)).
extend(new BoundingBox(pathCoords[pathCoords.length-1], 0.01, 0.01));
mc.fitBounds(bb);
MapLayout.setHorizontalAlignment(fromComponent,
MapLayout.HALIGN.RIGHT);
mapLayer.add(pathCoords[0], fromComponent);
mapLayer.add(pathCoords[pathCoords.length - 1], toComponent);
return pathObject;
}
addPath
I used the addPath method in the onShowRideResponse so I’m showing it here for completeness.
The addPath method generalizes some of the common code for adding a path and zooming into the map. I won't go into the details of this method as all the code in here
existed in enterNavigationMode so it should be pretty familiar.
19. private void hailRideImpl(User car, final Container pinLayer) {
pinLayer.getUnselectedStyle().setBgTransparency(0);
pinLayer.removeAll();
String driverName = car.givenName.get();
String carBrand = car.car.get();
SpanLabel driver = new SpanLabel("Driver found " + driverName + "n" + carBrand);
Container stars = new Container(new FlowLayout(CENTER));
for(int iter = 0 ; iter < 5 ; iter++) {
if(iter + 1 >= car.currentRating.getFloat()) {
Label fullStar = new Label("", "Star");
FontImage.setMaterialIcon(fullStar, FontImage.MATERIAL_STAR);
stars.add(fullStar);
} else {
if(iter + 1 >= Math.round(car.currentRating.getFloat())) {
Label halfStar = new Label("", "Star");
FontImage.setMaterialIcon(halfStar, FontImage.MATERIAL_STAR_HALF);
stars.add(halfStar);
} else {
break;
}
}
}
Button ok = new Button("OK", "BlackButton");
Container dialog = BoxLayout.encloseY(driver, stars, ok);
dialog.setUIID("SearchingDialog");
pinLayer.add(SOUTH, dialog);
revalidate();
hailRideImpl
With that lets move back to the “end user experience”. We discussed the perception from the driver side but what does the passenger see during this whole process?
That’s exactly what hailRideImpl does…
Both fields include no actual data currently since we don't have the UI to fill in the driver or user names
20. private void hailRideImpl(User car, final Container pinLayer) {
pinLayer.getUnselectedStyle().setBgTransparency(0);
pinLayer.removeAll();
String driverName = car.givenName.get();
String carBrand = car.car.get();
SpanLabel driver = new SpanLabel("Driver found " + driverName + "n" + carBrand);
Container stars = new Container(new FlowLayout(CENTER));
for(int iter = 0 ; iter < 5 ; iter++) {
if(iter + 1 >= car.currentRating.getFloat()) {
Label fullStar = new Label("", "Star");
FontImage.setMaterialIcon(fullStar, FontImage.MATERIAL_STAR);
stars.add(fullStar);
} else {
if(iter + 1 >= Math.round(car.currentRating.getFloat())) {
Label halfStar = new Label("", "Star");
FontImage.setMaterialIcon(halfStar, FontImage.MATERIAL_STAR_HALF);
stars.add(halfStar);
} else {
break;
}
}
}
Button ok = new Button("OK", "BlackButton");
Container dialog = BoxLayout.encloseY(driver, stars, ok);
dialog.setUIID("SearchingDialog");
pinLayer.add(SOUTH, dialog);
revalidate();
hailRideImpl
We build the text and add stars based on the level of ranking, this is essentially a set of star labels in a container based on the value of the rank field
21. private void hailRideImpl(User car, final Container pinLayer) {
pinLayer.getUnselectedStyle().setBgTransparency(0);
pinLayer.removeAll();
String driverName = car.givenName.get();
String carBrand = car.car.get();
SpanLabel driver = new SpanLabel("Driver found " + driverName + "n" + carBrand);
Container stars = new Container(new FlowLayout(CENTER));
for(int iter = 0 ; iter < 5 ; iter++) {
if(iter + 1 >= car.currentRating.getFloat()) {
Label fullStar = new Label("", "Star");
FontImage.setMaterialIcon(fullStar, FontImage.MATERIAL_STAR);
stars.add(fullStar);
} else {
if(iter + 1 >= Math.round(car.currentRating.getFloat())) {
Label halfStar = new Label("", "Star");
FontImage.setMaterialIcon(halfStar, FontImage.MATERIAL_STAR_HALF);
stars.add(halfStar);
} else {
break;
}
}
}
Button ok = new Button("OK", "BlackButton");
Container dialog = BoxLayout.encloseY(driver, stars, ok);
dialog.setUIID("SearchingDialog");
pinLayer.add(SOUTH, dialog);
revalidate();
hailRideImpl
We call this a "dialog" but it's really just a Container with a special style like before