Skip to content
This repository has been archived by the owner on Jul 13, 2019. It is now read-only.

Commit

Permalink
Add multi-threaded data fetch from Strava
Browse files Browse the repository at this point in the history
Given the region submitted by the user, split it into smaller regions, and call Strava segments API for each smaller region.

This allows for better coverage of Strava segments, and better performance.
  • Loading branch information
George Black committed May 12, 2019
1 parent 03c54a9 commit 0924d3c
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ public class Coordinate {
private double lat;
private double lng;

/**
* Constructs new coordinate by cloning existing coordinate.
*
* @param existingCoordinate
*/
public Coordinate(Coordinate existingCoordinate) {
this.lat = existingCoordinate.getLat();
this.lng = existingCoordinate.getLng();
}

@Override
public String toString() {
return "(" + lat + ", " + lng + ")";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ public String toString() {
+ ", "
+ southwest.getLng()
+ "), ("
+ southwest.getLat()
+ northeast.getLat()
+ ", "
+ southwest.getLng()
+ northeast.getLng()
+ ")]";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import dev.georgeblack.pathtorun.model.Route;
import dev.georgeblack.pathtorun.model.api.RoutesRequest;
import dev.georgeblack.pathtorun.model.api.RoutesResponse;
import dev.georgeblack.pathtorun.model.strava.StravaSegment;
import dev.georgeblack.pathtorun.model.strava.StravaSegments;
import dev.georgeblack.pathtorun.repository.StravaSegmentRepository;
import dev.georgeblack.pathtorun.util.PolylineUtil;
Expand All @@ -15,43 +16,78 @@
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

@Service
public class PathToRunService {
Logger logger = LoggerFactory.getLogger(PathToRunService.class);
private Logger logger = LoggerFactory.getLogger(PathToRunService.class);

@Autowired StravaService stravaService;
@Autowired private StravaService stravaService;

@Autowired StravaSegmentRepository stravaSegmentRepository;
@Autowired private StravaSegmentRepository stravaSegmentRepository;

public RoutesResponse getRoutes(RoutesRequest routesRequest) {
logger.info(String.format("Started new Path to Run request: %s", routesRequest));
long start = System.currentTimeMillis();

List<Route> routes = new ArrayList<>();
List<StravaSegment> segments = new ArrayList<>();

Coordinate startingPoint =
new Coordinate(routesRequest.getStartLat(), routesRequest.getStartLng());
Region region =
Region initialRegion =
RegionUtil.buildRegionFromStartingPoint(startingPoint, routesRequest.getDistance());
StravaSegments segments = stravaService.getSegmentsInRegion(region);

List<Region> splitRegions =
RegionUtil.splitRegion(
initialRegion, routesRequest.getDistance(), routesRequest.getDistance());

// for each split region, get segments
List<Callable<StravaSegments>> callables = new LinkedList<>();
for (Region splitRegion : splitRegions) {
callables.add(() -> stravaService.getSegmentsInRegion(splitRegion));
}

logger.info(String.format("Submitting %d tasks to Strava service...", callables.size()));
ExecutorService executorService = Executors.newFixedThreadPool(callables.size());

try {
List<Future<StravaSegments>> futures = executorService.invokeAll(callables);
for (Future<StravaSegments> future : futures) {
StravaSegments stravaSegments = future.get();
segments.addAll(stravaSegments.getSegments());
}
} catch (Exception e) {
logger.error(
String.format("Error with task submitted to Strava service: %s", e.getMessage()));
}

executorService.shutdown();

// save to db in new thread
new Thread(() -> saveStravaSegments(stravaSegmentRepository, segments)).start();

segments
.getSegments()
.forEach(
segment -> {
List<Coordinate> coordinates =
PolylineUtil.decodePolyline(segment.getEncodedPolyline());
String id = Integer.toString(segment.getId());
routes.add(new Route(id, coordinates));
});

return new RoutesResponse(region, routes);
// build route from each segment
segments.forEach(
segment -> {
List<Coordinate> coordinates = PolylineUtil.decodePolyline(segment.getEncodedPolyline());
String id = Integer.toString(segment.getId());
routes.add(new Route(id, coordinates));
});

long elapsed = System.currentTimeMillis() - start;
logger.info(String.format("Path to Run service completed in %d milliseconds", elapsed));

return new RoutesResponse(initialRegion, routes);
}

private void saveStravaSegments(StravaSegmentRepository repository, StravaSegments segments) {
segments.getSegments().forEach(segment -> repository.save(segment));
private void saveStravaSegments(
StravaSegmentRepository repository, List<StravaSegment> segments) {
segments.forEach(repository::save);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,26 @@
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.LinkedList;
import java.util.List;

@Service
public class RegionUtil {
static Logger logger = LoggerFactory.getLogger(RegionUtil.class);

public static final double ONE_MILE_IN_LATITUDE_DEGREES = (1.0 / 69.0);
public static final double HALF_MILE_IN_LATITUDE_DEGREES = (ONE_MILE_IN_LATITUDE_DEGREES / 2.0);
public static final double ONE_DEGREE_LONGITUDE_IN_MILES_AT_EQUATOR = 69.172;
private static Logger logger = LoggerFactory.getLogger(RegionUtil.class);

/**
* Given latitude/longitude of a starting point, build a bounding box that is X miles
* North/South/East/West of point. Region is defined by two coordinates: the South-West and
* North-East corner of bounding box.
*/
public static Region buildRegionFromStartingPoint(
Coordinate start, int distance) {
public static Region buildRegionFromStartingPoint(Coordinate start, int distance) {
double oneMileInLongitudeDegrees =
(1.0 / (Math.cos(Math.toRadians(start.getLat())) * ONE_DEGREE_LONGITUDE_IN_MILES_AT_EQUATOR));
(1.0
/ (Math.cos(Math.toRadians(start.getLat()))
* ONE_DEGREE_LONGITUDE_IN_MILES_AT_EQUATOR));

double northLat = start.getLat() + (distance * ONE_MILE_IN_LATITUDE_DEGREES);
double southLat = start.getLat() - (distance * ONE_MILE_IN_LATITUDE_DEGREES);
Expand All @@ -38,4 +40,69 @@ public static Region buildRegionFromStartingPoint(
logger.info("Generated region: " + region);
return region;
}

/**
* Given a single region, split it into smaller regions that are one square mile, with 1/2 mile
* overlap. This is so we can call the Strava Explore Segments API on each smaller region, getting
* better coverage of segments.
*
* <p>If the region crosses the Prime Meridian, I'm not sure this code will work. That's fine.
*/
public static List<Region> splitRegion(Region initialRegion, int regionWidth, int regionHeight) {
List<Region> splitRegions = new LinkedList<>();

// break region into overlapping cols/rows
int numColumns = (regionWidth * 2) - 1;
int numRows = (regionHeight * 2) - 1;

// start in southwest corner
// create rows of new regions, each row 1 mile tall
// each new row starts 1/2 mile north of the previous row, so they will overlap
Coordinate rowSouthwestCorner = new Coordinate(initialRegion.getSouthwest());
for (int i = 0; i < numRows; i++) {

// start in southwest corner of row
// create 1x1 mile regions, each region shifted 1/2 east for overlap
Coordinate columnSouthwestCorner = new Coordinate(rowSouthwestCorner);
for (int j = 0; j < numColumns; j++) {
// calculate southwest corner
Coordinate newSouthwestCorner = new Coordinate(columnSouthwestCorner);

// calculate northeast corner
double northLat = newSouthwestCorner.getLat() + ONE_MILE_IN_LATITUDE_DEGREES;
double eastLng =
newSouthwestCorner.getLng() + calcOneMileDegreesLng(newSouthwestCorner.getLat());
Coordinate newNortheastCorner = new Coordinate(northLat, eastLng);

// calculate center
double centerLat = newSouthwestCorner.getLat() + HALF_MILE_IN_LATITUDE_DEGREES;
double centerLng =
newSouthwestCorner.getLng() + calcHalfMileDegreesLng(newSouthwestCorner.getLat());
Coordinate center = new Coordinate(centerLat, centerLng);

splitRegions.add(new Region(newSouthwestCorner, newNortheastCorner, center));

// shift southwest corner east by 1/2 mile
columnSouthwestCorner.setLng(
columnSouthwestCorner.getLng()
+ calcHalfMileDegreesLng(columnSouthwestCorner.getLat()));
}

// shift southwest corner north by 1/2 mile
rowSouthwestCorner.setLat(rowSouthwestCorner.getLat() + HALF_MILE_IN_LATITUDE_DEGREES);
}

return splitRegions;
}

/** Given the current latitude, calculate one mile in degrees longitude. */
private static double calcOneMileDegreesLng(double currentLat) {
return (1.0
/ (Math.cos(Math.toRadians(currentLat)) * ONE_DEGREE_LONGITUDE_IN_MILES_AT_EQUATOR));
}

/** Given the current latitude, calculate 1/2 mile in degrees longitude. */
private static double calcHalfMileDegreesLng(double currentLat) {
return calcOneMileDegreesLng(currentLat) / 2.0;
}
}

0 comments on commit 0924d3c

Please sign in to comment.