Refactor and fix compilation
This commit is contained in:
parent
ca5448286a
commit
1a71225c8a
7 changed files with 456 additions and 590 deletions
|
@ -22,8 +22,6 @@ public class TransportStop extends MapObject {
|
||||||
public int y31;
|
public int y31;
|
||||||
private List<TransportStopExit> exits;
|
private List<TransportStopExit> exits;
|
||||||
private List<TransportRoute> routes = null;
|
private List<TransportRoute> routes = null;
|
||||||
private LinkedHashMap<String, int[]> referencesToRoutesMap;
|
|
||||||
|
|
||||||
private TransportStopAggregated transportStopAggregated;
|
private TransportStopAggregated transportStopAggregated;
|
||||||
|
|
||||||
public TransportStop() {}
|
public TransportStop() {}
|
||||||
|
@ -36,19 +34,6 @@ public class TransportStop extends MapObject {
|
||||||
return MISSING_STOP_NAME.equals(getName());
|
return MISSING_STOP_NAME.equals(getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public LinkedHashMap<String, int[]> getReferencesToRoutesMap() {
|
|
||||||
return referencesToRoutesMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void putReferencesToRoutes(String repositoryFileName, int[] referencesToRoutes) {
|
|
||||||
LinkedHashMap<String, int[]> referencesToRoutesMap = this.referencesToRoutesMap;
|
|
||||||
if (referencesToRoutesMap == null) {
|
|
||||||
referencesToRoutesMap = new LinkedHashMap<>();
|
|
||||||
this.referencesToRoutesMap = referencesToRoutesMap;
|
|
||||||
}
|
|
||||||
referencesToRoutesMap.put(repositoryFileName, referencesToRoutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRoutes(List<TransportRoute> routes) {
|
public void setRoutes(List<TransportRoute> routes) {
|
||||||
this.routes = routes;
|
this.routes = routes;
|
||||||
}
|
}
|
||||||
|
@ -115,10 +100,6 @@ public class TransportStop extends MapObject {
|
||||||
return !isDeleted() && referencesToRoutes != null && referencesToRoutes.length > 0;
|
return !isDeleted() && referencesToRoutes != null && referencesToRoutes.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasReferencesToRoutesMap() {
|
|
||||||
return !isDeleted() && referencesToRoutesMap != null && !referencesToRoutesMap.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Amenity getAmenity() {
|
public Amenity getAmenity() {
|
||||||
if (transportStopAggregated != null) {
|
if (transportStopAggregated != null) {
|
||||||
return transportStopAggregated.getAmenity();
|
return transportStopAggregated.getAmenity();
|
||||||
|
|
|
@ -2,6 +2,7 @@ package net.osmand.router;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
@ -30,12 +31,15 @@ import net.osmand.data.TransportStop;
|
||||||
import net.osmand.data.TransportStopExit;
|
import net.osmand.data.TransportStopExit;
|
||||||
import net.osmand.osm.edit.Node;
|
import net.osmand.osm.edit.Node;
|
||||||
import net.osmand.osm.edit.Way;
|
import net.osmand.osm.edit.Way;
|
||||||
|
import net.osmand.router.TransportRoutePlanner.TransportRouteResult;
|
||||||
|
import net.osmand.router.TransportRoutePlanner.TransportRouteResultSegment;
|
||||||
|
import net.osmand.router.TransportRoutePlanner.TransportRouteSegment;
|
||||||
import net.osmand.util.MapUtils;
|
import net.osmand.util.MapUtils;
|
||||||
|
|
||||||
public class TransportRoutePlanner {
|
public class TransportRoutePlanner {
|
||||||
|
|
||||||
private static final boolean MEASURE_TIME = false;
|
private static final boolean MEASURE_TIME = false;
|
||||||
private static final int MISSING_STOP_SEARCH_RADIUS = 15000;
|
|
||||||
private static final int MIN_DIST_STOP_TO_GEOMETRY = 150;
|
private static final int MIN_DIST_STOP_TO_GEOMETRY = 150;
|
||||||
public static final long GEOMETRY_WAY_ID = -1;
|
public static final long GEOMETRY_WAY_ID = -1;
|
||||||
public static final long STOPS_WAY_ID = -2;
|
public static final long STOPS_WAY_ID = -2;
|
||||||
|
@ -440,7 +444,7 @@ public class TransportRoutePlanner {
|
||||||
Node ln = startInd.way.getLastNode();
|
Node ln = startInd.way.getLastNode();
|
||||||
Node fn = endInd.way.getFirstNode();
|
Node fn = endInd.way.getFirstNode();
|
||||||
// HERE we need to check other ways for continuation
|
// HERE we need to check other ways for continuation
|
||||||
if (ln != null && fn != null && MapUtils.getDistance(ln.getLatLon(), fn.getLatLon()) < MISSING_STOP_SEARCH_RADIUS) {
|
if (ln != null && fn != null && MapUtils.getDistance(ln.getLatLon(), fn.getLatLon()) < TransportStopsRouteReader.MISSING_STOP_SEARCH_RADIUS) {
|
||||||
validContinuation = true;
|
validContinuation = true;
|
||||||
} else {
|
} else {
|
||||||
validContinuation = false;
|
validContinuation = false;
|
||||||
|
@ -752,9 +756,7 @@ public class TransportRoutePlanner {
|
||||||
// Here we don't limit files by bbox, so it could be an issue while searching for multiple unused files
|
// Here we don't limit files by bbox, so it could be an issue while searching for multiple unused files
|
||||||
// Incomplete routes usually don't need more files than around Max-BBOX of start/end,
|
// Incomplete routes usually don't need more files than around Max-BBOX of start/end,
|
||||||
// so here an improvement could be introduced
|
// so here an improvement could be introduced
|
||||||
public final Map<BinaryMapIndexReader, TIntObjectHashMap<TransportRoute>> routeMap =
|
final TransportStopsRouteReader transportStopsReader;
|
||||||
new LinkedHashMap<BinaryMapIndexReader, TIntObjectHashMap<TransportRoute>>();
|
|
||||||
|
|
||||||
public int finishTimeSeconds;
|
public int finishTimeSeconds;
|
||||||
|
|
||||||
// stats
|
// stats
|
||||||
|
@ -779,9 +781,7 @@ public class TransportRoutePlanner {
|
||||||
walkChangeRadiusIn31 = (int) (cfg.walkChangeRadius / MapUtils.getTileDistanceWidth(31));
|
walkChangeRadiusIn31 = (int) (cfg.walkChangeRadius / MapUtils.getTileDistanceWidth(31));
|
||||||
quadTree = new TLongObjectHashMap<List<TransportRouteSegment>>();
|
quadTree = new TLongObjectHashMap<List<TransportRouteSegment>>();
|
||||||
this.library = library;
|
this.library = library;
|
||||||
for (BinaryMapIndexReader r : readers) {
|
transportStopsReader = new TransportStopsRouteReader(Arrays.asList(readers));
|
||||||
routeMap.put(r, new TIntObjectHashMap<TransportRoute>());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<TransportRouteSegment> getTransportStops(LatLon loc) throws IOException {
|
public List<TransportRouteSegment> getTransportStops(LatLon loc) throws IOException {
|
||||||
|
@ -831,370 +831,12 @@ public class TransportRoutePlanner {
|
||||||
int pz = (31 - cfg.ZOOM_TO_LOAD_TILES);
|
int pz = (31 - cfg.ZOOM_TO_LOAD_TILES);
|
||||||
SearchRequest<TransportStop> sr = BinaryMapIndexReader.buildSearchTransportRequest(x << pz, (x + 1) << pz,
|
SearchRequest<TransportStop> sr = BinaryMapIndexReader.buildSearchTransportRequest(x << pz, (x + 1) << pz,
|
||||||
y << pz, (y + 1) << pz, -1, null);
|
y << pz, (y + 1) << pz, -1, null);
|
||||||
|
Collection<TransportStop> stops = transportStopsReader.readMergedTransportStops(sr);
|
||||||
// could be global ?
|
loadTransportSegments(stops, lst);
|
||||||
TLongObjectHashMap<TransportStop> loadedTransportStops = new TLongObjectHashMap<TransportStop>();
|
|
||||||
|
|
||||||
for (BinaryMapIndexReader r : routeMap.keySet()) {
|
|
||||||
sr.clearSearchResults();
|
|
||||||
List<TransportStop> stops = r.searchTransportIndex(sr);
|
|
||||||
|
|
||||||
TIntArrayList routesToLoad = mergeTransportStops(r, loadedTransportStops, stops);
|
|
||||||
|
|
||||||
TIntObjectHashMap<TransportRoute> loadedRoutes = routeMap.get(r);
|
|
||||||
// localFileRoutes.clear();
|
|
||||||
TIntObjectHashMap<TransportRoute> localFileRoutes = new TIntObjectHashMap<>(); //reference, route
|
|
||||||
loadRoutes(r, localFileRoutes, loadedRoutes, routesToLoad);
|
|
||||||
|
|
||||||
for (TransportStop stop : stops) {
|
|
||||||
// skip missing stops
|
|
||||||
if (stop.isMissingStop()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
long stopId = stop.getId();
|
|
||||||
TransportStop multifileStop = loadedTransportStops.get(stopId);
|
|
||||||
int[] rrs = stop.getReferencesToRoutes();
|
|
||||||
// clear up so it won't be used because there is multi file stop
|
|
||||||
stop.setReferencesToRoutes(null);
|
|
||||||
if (rrs != null && !multifileStop.isDeleted()) {
|
|
||||||
for (int rr : rrs) {
|
|
||||||
TransportRoute route = localFileRoutes.get(rr);
|
|
||||||
if (route == null) {
|
|
||||||
System.err.println(
|
|
||||||
String.format("Something went wrong by loading combined route %d for stop %s",
|
|
||||||
rr, stop));
|
|
||||||
} else {
|
|
||||||
TransportRoute combinedRoute = getCombinedRoute(route);
|
|
||||||
if (multifileStop == stop || (!multifileStop.hasRoute(combinedRoute.getId()) &&
|
|
||||||
!multifileStop.isRouteDeleted(combinedRoute.getId()))) {
|
|
||||||
// duplicates won't be added
|
|
||||||
multifileStop.addRouteId(combinedRoute.getId());
|
|
||||||
multifileStop.addRoute(combinedRoute);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// There should go stops with complete routes:
|
|
||||||
loadTransportSegments(loadedTransportStops.valueCollection(), lst);
|
|
||||||
|
|
||||||
readTime += System.nanoTime() - nanoTime;
|
readTime += System.nanoTime() - nanoTime;
|
||||||
return lst;
|
return lst;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public TIntArrayList mergeTransportStops(BinaryMapIndexReader reader, TLongObjectHashMap<TransportStop> loadedTransportStops,
|
|
||||||
List<TransportStop> stops) throws IOException {
|
|
||||||
TIntArrayList routesToLoad = new TIntArrayList();
|
|
||||||
Iterator<TransportStop> it = stops.iterator();
|
|
||||||
TIntArrayList localRoutesToLoad = new TIntArrayList();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
TransportStop stop = it.next();
|
|
||||||
long stopId = stop.getId();
|
|
||||||
localRoutesToLoad.clear();
|
|
||||||
TransportStop multifileStop = loadedTransportStops.get(stopId);
|
|
||||||
long[] routesIds = stop.getRoutesIds();
|
|
||||||
long[] delRIds = stop.getDeletedRoutesIds();
|
|
||||||
if (multifileStop == null) {
|
|
||||||
loadedTransportStops.put(stopId, stop);
|
|
||||||
multifileStop = stop;
|
|
||||||
if (!stop.isDeleted()) {
|
|
||||||
localRoutesToLoad.addAll(stop.getReferencesToRoutes());
|
|
||||||
}
|
|
||||||
} else if (multifileStop.isDeleted()){
|
|
||||||
// stop has nothing to load, so not needed
|
|
||||||
it.remove();
|
|
||||||
} else {
|
|
||||||
if (delRIds != null) {
|
|
||||||
for (long deletedRouteId : delRIds) {
|
|
||||||
multifileStop.addDeletedRouteId(deletedRouteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (routesIds != null && routesIds.length > 0) {
|
|
||||||
int[] refs = stop.getReferencesToRoutes();
|
|
||||||
for (int i = 0; i < routesIds.length; i++) {
|
|
||||||
long routeId = routesIds[i];
|
|
||||||
if (!multifileStop.hasRoute(routeId) && !multifileStop.isRouteDeleted(routeId)) {
|
|
||||||
localRoutesToLoad.add(refs[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (stop.hasReferencesToRoutes()) {
|
|
||||||
// old format
|
|
||||||
localRoutesToLoad.addAll(stop.getReferencesToRoutes());
|
|
||||||
} else {
|
|
||||||
// stop has noting to load, so not needed
|
|
||||||
it.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
routesToLoad.addAll(localRoutesToLoad);
|
|
||||||
multifileStop.putReferencesToRoutes(reader.getFile().getName(), localRoutesToLoad.toArray()); //add valid stop and references to routes
|
|
||||||
}
|
|
||||||
return routesToLoad;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadRoutes(BinaryMapIndexReader reader, TIntObjectHashMap<TransportRoute> localFileRoutes,
|
|
||||||
TIntObjectHashMap<TransportRoute> loadedRoutes, TIntArrayList routesToLoad) throws IOException {
|
|
||||||
// load/combine routes
|
|
||||||
if (routesToLoad.size() > 0) {
|
|
||||||
routesToLoad.sort();
|
|
||||||
TIntArrayList referencesToLoad = new TIntArrayList();
|
|
||||||
TIntIterator itr = routesToLoad.iterator();
|
|
||||||
int p = routesToLoad.get(0) + 1; // different
|
|
||||||
while (itr.hasNext()) {
|
|
||||||
int nxt = itr.next();
|
|
||||||
if (p != nxt) {
|
|
||||||
if (localFileRoutes != null && loadedRoutes != null && loadedRoutes.contains(nxt)) { //check if
|
|
||||||
localFileRoutes.put(nxt, loadedRoutes.get(nxt));
|
|
||||||
} else {
|
|
||||||
referencesToLoad.add(nxt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (localFileRoutes != null && loadedRoutes != null) {
|
|
||||||
reader.loadTransportRoutes(referencesToLoad.toArray(), localFileRoutes);
|
|
||||||
loadedRoutes.putAll(localFileRoutes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private TransportRoute getCombinedRoute(TransportRoute route) throws IOException {
|
|
||||||
if (!route.isIncomplete()) {
|
|
||||||
return route;
|
|
||||||
}
|
|
||||||
TransportRoute c = combinedRoutesCache.get(route.getId());
|
|
||||||
if (c == null) {
|
|
||||||
c = combineRoute(route);
|
|
||||||
combinedRoutesCache.put(route.getId(), c);
|
|
||||||
}
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TransportRoute combineRoute(TransportRoute route) throws IOException {
|
|
||||||
// 1. Get all available route parts;
|
|
||||||
List<TransportRoute> incompleteRoutes = findIncompleteRouteParts(route);
|
|
||||||
if (incompleteRoutes == null) {
|
|
||||||
return route;
|
|
||||||
}
|
|
||||||
// here could be multiple overlays between same points
|
|
||||||
// It's better to remove them especially identical segments
|
|
||||||
List<Way> allWays = getAllWays(incompleteRoutes);
|
|
||||||
|
|
||||||
|
|
||||||
// 2. Get array of segments (each array size > 1):
|
|
||||||
LinkedList<List<TransportStop>> stopSegments = parseRoutePartsToSegments(incompleteRoutes);
|
|
||||||
|
|
||||||
// 3. Merge segments and remove excess missingStops (when they are closer then MISSING_STOP_SEARCH_RADIUS):
|
|
||||||
// + Check for missingStops. If they present in the middle/there more then one segment - we have a hole in the map data
|
|
||||||
List<List<TransportStop>> mergedSegments = combineSegmentsOfSameRoute(stopSegments);
|
|
||||||
|
|
||||||
// 4. Now we need to properly sort segments, proper sorting is minimizing distance between stops
|
|
||||||
// So it is salesman problem, we have this solution at TspAnt, but if we know last or first segment we can solve it straightforward
|
|
||||||
List<TransportStop> firstSegment = null;
|
|
||||||
List<TransportStop> lastSegment = null;
|
|
||||||
for(List<TransportStop> l : mergedSegments) {
|
|
||||||
if(!l.get(0).isMissingStop()) {
|
|
||||||
firstSegment = l;
|
|
||||||
}
|
|
||||||
if(!l.get(l.size() - 1).isMissingStop()) {
|
|
||||||
lastSegment = l;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<List<TransportStop>> sortedSegments = new ArrayList<List<TransportStop>>();
|
|
||||||
if(firstSegment != null) {
|
|
||||||
sortedSegments.add(firstSegment);
|
|
||||||
mergedSegments.remove(firstSegment);
|
|
||||||
while(!mergedSegments.isEmpty()) {
|
|
||||||
List<TransportStop> last = sortedSegments.get(sortedSegments.size() - 1);
|
|
||||||
List<TransportStop> add = findAndDeleteMinDistance(last.get(last.size() - 1).getLocation(), mergedSegments, true);
|
|
||||||
sortedSegments.add(add);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if(lastSegment != null) {
|
|
||||||
sortedSegments.add(lastSegment);
|
|
||||||
mergedSegments.remove(lastSegment);
|
|
||||||
while(!mergedSegments.isEmpty()) {
|
|
||||||
List<TransportStop> first = sortedSegments.get(0);
|
|
||||||
List<TransportStop> add = findAndDeleteMinDistance(first.get(0).getLocation(), mergedSegments, false);
|
|
||||||
sortedSegments.add(0, add);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sortedSegments = mergedSegments;
|
|
||||||
}
|
|
||||||
List<TransportStop> finalList = new ArrayList<TransportStop>();
|
|
||||||
for(List<TransportStop> s : sortedSegments) {
|
|
||||||
finalList.addAll(s);
|
|
||||||
}
|
|
||||||
// 5. Create combined TransportRoute and return it
|
|
||||||
return new TransportRoute(route, finalList, allWays);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<TransportStop> findAndDeleteMinDistance(LatLon location, List<List<TransportStop>> mergedSegments,
|
|
||||||
boolean attachToBegin) {
|
|
||||||
int ind = attachToBegin ? 0 : mergedSegments.get(0).size() - 1;
|
|
||||||
double minDist = MapUtils.getDistance(mergedSegments.get(0).get(ind).getLocation(), location);
|
|
||||||
int minInd = 0;
|
|
||||||
for(int i = 1; i < mergedSegments.size(); i++) {
|
|
||||||
ind = attachToBegin ? 0 : mergedSegments.get(i).size() - 1;
|
|
||||||
double dist = MapUtils.getDistance(mergedSegments.get(i).get(ind).getLocation(), location);
|
|
||||||
if(dist < minDist) {
|
|
||||||
minInd = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mergedSegments.remove(minInd);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Way> getAllWays(List<TransportRoute> parts) {
|
|
||||||
List<Way> w = new ArrayList<Way>();
|
|
||||||
for (TransportRoute t : parts) {
|
|
||||||
w.addAll(t.getForwardWays());
|
|
||||||
}
|
|
||||||
return w;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private List<List<TransportStop>> combineSegmentsOfSameRoute(LinkedList<List<TransportStop>> segments) {
|
|
||||||
List<List<TransportStop>> resultSegments = new ArrayList<List<TransportStop>>();
|
|
||||||
while (!segments.isEmpty()) {
|
|
||||||
List<TransportStop> firstSegment = segments.poll();
|
|
||||||
boolean merged = true;
|
|
||||||
while (merged) {
|
|
||||||
merged = false;
|
|
||||||
Iterator<List<TransportStop>> it = segments.iterator();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
List<TransportStop> segmentToMerge = it.next();
|
|
||||||
merged = tryToMerge(firstSegment, segmentToMerge);
|
|
||||||
|
|
||||||
if (merged) {
|
|
||||||
it.remove();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resultSegments.add(firstSegment);
|
|
||||||
}
|
|
||||||
return resultSegments;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean tryToMerge(List<TransportStop> firstSegment, List<TransportStop> segmentToMerge) {
|
|
||||||
if(firstSegment.size() < 2 || segmentToMerge.size() < 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 1st we check that segments overlap by stop
|
|
||||||
int commonStopFirst = 0;
|
|
||||||
int commonStopSecond = 0;
|
|
||||||
boolean found = false;
|
|
||||||
for(;commonStopFirst < firstSegment.size(); commonStopFirst++) {
|
|
||||||
for(commonStopSecond = 0; commonStopSecond < segmentToMerge.size() && !found; commonStopSecond++) {
|
|
||||||
long lid1 = firstSegment.get(commonStopFirst).getId();
|
|
||||||
long lid2 = segmentToMerge.get(commonStopSecond).getId();
|
|
||||||
if(lid1 > 0 && lid2 == lid1) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(found) {
|
|
||||||
// important to increment break inside loop
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(found && commonStopFirst < firstSegment.size()) {
|
|
||||||
// we've found common stop so we can merge based on stops
|
|
||||||
// merge last part first
|
|
||||||
int leftPartFirst = firstSegment.size() - commonStopFirst;
|
|
||||||
int leftPartSecond = segmentToMerge.size() - commonStopSecond;
|
|
||||||
if(leftPartFirst < leftPartSecond || (leftPartFirst == leftPartSecond &&
|
|
||||||
firstSegment.get(firstSegment.size() - 1).isMissingStop())) {
|
|
||||||
while(firstSegment.size() > commonStopFirst) {
|
|
||||||
firstSegment.remove(firstSegment.size() - 1);
|
|
||||||
}
|
|
||||||
for(int i = commonStopSecond; i < segmentToMerge.size(); i++) {
|
|
||||||
firstSegment.add(segmentToMerge.get(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// merge first part
|
|
||||||
if(commonStopFirst < commonStopSecond || (commonStopFirst == commonStopSecond &&
|
|
||||||
firstSegment.get(0).isMissingStop())) {
|
|
||||||
for(int i = 0; i < commonStopFirst; i++) {
|
|
||||||
firstSegment.remove(0);
|
|
||||||
}
|
|
||||||
for(int i = commonStopSecond; i >= 0; i--) {
|
|
||||||
firstSegment.add(0, segmentToMerge.get(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
// no common stops, so try to connect to the end or beginning
|
|
||||||
// beginning
|
|
||||||
boolean merged = false;
|
|
||||||
if (MapUtils.getDistance(firstSegment.get(0).getLocation(),
|
|
||||||
segmentToMerge.get(segmentToMerge.size() - 1).getLocation()) < MISSING_STOP_SEARCH_RADIUS) {
|
|
||||||
firstSegment.remove(0);
|
|
||||||
for(int i = segmentToMerge.size() - 2; i >= 0; i--) {
|
|
||||||
firstSegment.add(0, segmentToMerge.get(i));
|
|
||||||
}
|
|
||||||
merged = true;
|
|
||||||
} else if(MapUtils.getDistance(firstSegment.get(firstSegment.size() - 1).getLocation(),
|
|
||||||
segmentToMerge.get(0).getLocation()) < MISSING_STOP_SEARCH_RADIUS) {
|
|
||||||
firstSegment.remove(firstSegment.size() - 1);
|
|
||||||
for(int i = 1; i < segmentToMerge.size(); i++) {
|
|
||||||
firstSegment.add(segmentToMerge.get(i));
|
|
||||||
}
|
|
||||||
merged = true;
|
|
||||||
}
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private LinkedList<List<TransportStop>> parseRoutePartsToSegments(List<TransportRoute> routeParts) {
|
|
||||||
LinkedList<List<TransportStop>> segs = new LinkedList<List<TransportStop>>();
|
|
||||||
// here we assume that missing stops come in pairs <A, B, C, MISSING, MISSING, D, E...>
|
|
||||||
// we don't add segments with 1 stop cause they are irrelevant further
|
|
||||||
for (TransportRoute part : routeParts) {
|
|
||||||
List<TransportStop> newSeg = new ArrayList<TransportStop>();
|
|
||||||
for (TransportStop s : part.getForwardStops()) {
|
|
||||||
newSeg.add(s);
|
|
||||||
if (s.isMissingStop()) {
|
|
||||||
if (newSeg.size() > 1) {
|
|
||||||
segs.add(newSeg);
|
|
||||||
newSeg = new ArrayList<TransportStop>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (newSeg.size() > 1) {
|
|
||||||
segs.add(newSeg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return segs;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<TransportRoute> findIncompleteRouteParts(TransportRoute baseRoute) throws IOException {
|
|
||||||
List<TransportRoute> allRoutes = null;
|
|
||||||
for (BinaryMapIndexReader bmir : routeMap.keySet()) {
|
|
||||||
// here we could limit routeMap indexes by only certain bbox around start / end (check comment on field)
|
|
||||||
IncompleteTransportRoute ptr = bmir.getIncompleteTransportRoutes().get(baseRoute.getId());
|
|
||||||
if (ptr != null) {
|
|
||||||
TIntArrayList lst = new TIntArrayList();
|
|
||||||
while(ptr != null) {
|
|
||||||
lst.add(ptr.getRouteOffset());
|
|
||||||
ptr = ptr.getNextLinkedRoute();
|
|
||||||
}
|
|
||||||
if(lst.size() > 0) {
|
|
||||||
if(allRoutes == null) {
|
|
||||||
allRoutes = new ArrayList<TransportRoute>();
|
|
||||||
}
|
|
||||||
allRoutes.addAll(bmir.getTransportRoutes(lst.toArray()).valueCollection());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return allRoutes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadTransportSegments(Collection<TransportStop> stops, List<TransportRouteSegment> lst) throws IOException {
|
private void loadTransportSegments(Collection<TransportStop> stops, List<TransportRouteSegment> lst) throws IOException {
|
||||||
for(TransportStop s : stops) {
|
for(TransportStop s : stops) {
|
||||||
if (s.isDeleted() || s.getRoutes() == null) {
|
if (s.isDeleted() || s.getRoutes() == null) {
|
||||||
|
@ -1216,7 +858,7 @@ public class TransportRoutePlanner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (stopIndex != -1) {
|
if (stopIndex != -1) {
|
||||||
if (cfg.useSchedule) {
|
if (cfg != null && cfg.useSchedule) {
|
||||||
loadScheduleRouteSegment(lst, route, stopIndex);
|
loadScheduleRouteSegment(lst, route, stopIndex);
|
||||||
} else {
|
} else {
|
||||||
TransportRouteSegment segment = new TransportRouteSegment(route, stopIndex);
|
TransportRouteSegment segment = new TransportRouteSegment(route, stopIndex);
|
||||||
|
@ -1314,8 +956,8 @@ public class TransportRoutePlanner {
|
||||||
|
|
||||||
if (nr.intervals != null && nr.intervals.length > 0 && nr.avgStopIntervals != null
|
if (nr.intervals != null && nr.intervals.length > 0 && nr.avgStopIntervals != null
|
||||||
&& nr.avgStopIntervals.length > 0 && nr.avgWaitIntervals != null && nr.avgWaitIntervals.length > 0) {
|
&& nr.avgStopIntervals.length > 0 && nr.avgWaitIntervals != null && nr.avgWaitIntervals.length > 0) {
|
||||||
r.setSchedule(new TransportSchedule(new TIntArrayList(nr.intervals),
|
r.setSchedule(new TransportSchedule(new TIntArrayList(nr.intervals), new TIntArrayList(nr.avgStopIntervals),
|
||||||
new TIntArrayList(nr.avgStopIntervals), new TIntArrayList(nr.avgWaitIntervals)));
|
new TIntArrayList(nr.avgWaitIntervals)));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < nr.waysIds.length; i++) {
|
for (int i = 0; i < nr.waysIds.length; i++) {
|
||||||
|
@ -1360,16 +1002,11 @@ public class TransportRoutePlanner {
|
||||||
|
|
||||||
if (ns.pTStopExit_refs != null && ns.pTStopExit_refs.length > 0) {
|
if (ns.pTStopExit_refs != null && ns.pTStopExit_refs.length > 0) {
|
||||||
for (int i = 0; i < ns.pTStopExit_refs.length; i++) {
|
for (int i = 0; i < ns.pTStopExit_refs.length; i++) {
|
||||||
s.addExit(new TransportStopExit(ns.pTStopExit_x31s[i],
|
s.addExit(
|
||||||
ns.pTStopExit_y31s[i], ns.pTStopExit_refs[i]));
|
new TransportStopExit(ns.pTStopExit_x31s[i], ns.pTStopExit_y31s[i], ns.pTStopExit_refs[i]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ns.referenceToRoutesKeys != null && ns.referenceToRoutesKeys.length > 0) {
|
|
||||||
for (int i = 0; i < ns.referenceToRoutesKeys.length; i++) {
|
|
||||||
s.putReferencesToRoutes(ns.referenceToRoutesKeys[i], ns.referenceToRoutesVals[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (convertedStopsCache == null) {
|
if (convertedStopsCache == null) {
|
||||||
convertedStopsCache = new TLongObjectHashMap<>();
|
convertedStopsCache = new TLongObjectHashMap<>();
|
||||||
}
|
}
|
||||||
|
@ -1380,4 +1017,7 @@ public class TransportRoutePlanner {
|
||||||
}
|
}
|
||||||
return stops;
|
return stops;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,409 @@
|
||||||
|
package net.osmand.router;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import gnu.trove.iterator.TIntIterator;
|
||||||
|
import gnu.trove.list.array.TIntArrayList;
|
||||||
|
import gnu.trove.map.hash.TIntObjectHashMap;
|
||||||
|
import gnu.trove.map.hash.TLongObjectHashMap;
|
||||||
|
import net.osmand.binary.BinaryMapIndexReader;
|
||||||
|
import net.osmand.binary.BinaryMapIndexReader.SearchRequest;
|
||||||
|
import net.osmand.data.IncompleteTransportRoute;
|
||||||
|
import net.osmand.data.LatLon;
|
||||||
|
import net.osmand.data.TransportRoute;
|
||||||
|
import net.osmand.data.TransportStop;
|
||||||
|
import net.osmand.osm.edit.Way;
|
||||||
|
import net.osmand.util.MapUtils;
|
||||||
|
|
||||||
|
public class TransportStopsRouteReader {
|
||||||
|
public static final int MISSING_STOP_SEARCH_RADIUS = 15000;
|
||||||
|
TLongObjectHashMap<TransportRoute> combinedRoutesCache = new TLongObjectHashMap<TransportRoute>();
|
||||||
|
Map<BinaryMapIndexReader, TIntObjectHashMap<TransportRoute>> routesFilesCache = new LinkedHashMap<BinaryMapIndexReader,
|
||||||
|
TIntObjectHashMap<TransportRoute>>();
|
||||||
|
|
||||||
|
|
||||||
|
public TransportStopsRouteReader(Collection<BinaryMapIndexReader> fls) {
|
||||||
|
for(BinaryMapIndexReader r : fls) {
|
||||||
|
routesFilesCache.put(r, new TIntObjectHashMap<TransportRoute>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<TransportStop> readMergedTransportStops(SearchRequest<TransportStop> sr) throws IOException {
|
||||||
|
// TODO could be global ?
|
||||||
|
TLongObjectHashMap<TransportStop> loadedTransportStops = new TLongObjectHashMap<TransportStop>();
|
||||||
|
|
||||||
|
for (BinaryMapIndexReader r : routesFilesCache.keySet()) {
|
||||||
|
sr.clearSearchResults();
|
||||||
|
List<TransportStop> stops = r.searchTransportIndex(sr);
|
||||||
|
|
||||||
|
TIntArrayList routesToLoad = mergeTransportStops(r, loadedTransportStops, stops);
|
||||||
|
|
||||||
|
TIntObjectHashMap<TransportRoute> loadedRoutes = routesFilesCache.get(r);
|
||||||
|
// TODO localFileRoutes.clear();
|
||||||
|
TIntObjectHashMap<TransportRoute> localFileRoutes = new TIntObjectHashMap<>(); //reference, route
|
||||||
|
loadRoutes(r, localFileRoutes, loadedRoutes, routesToLoad);
|
||||||
|
|
||||||
|
for (TransportStop stop : stops) {
|
||||||
|
// skip missing stops
|
||||||
|
if (stop.isMissingStop()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long stopId = stop.getId();
|
||||||
|
TransportStop multifileStop = loadedTransportStops.get(stopId);
|
||||||
|
int[] rrs = stop.getReferencesToRoutes();
|
||||||
|
// clear up so it won't be used because there is multi file stop
|
||||||
|
stop.setReferencesToRoutes(null);
|
||||||
|
if (rrs != null && !multifileStop.isDeleted()) {
|
||||||
|
for (int rr : rrs) {
|
||||||
|
TransportRoute route = localFileRoutes.get(rr);
|
||||||
|
if (route == null) {
|
||||||
|
System.err.println(
|
||||||
|
String.format("Something went wrong by loading combined route %d for stop %s",
|
||||||
|
rr, stop));
|
||||||
|
} else {
|
||||||
|
TransportRoute combinedRoute = getCombinedRoute(route);
|
||||||
|
if (multifileStop == stop || (!multifileStop.hasRoute(combinedRoute.getId()) &&
|
||||||
|
!multifileStop.isRouteDeleted(combinedRoute.getId()))) {
|
||||||
|
// duplicates won't be added
|
||||||
|
multifileStop.addRouteId(combinedRoute.getId());
|
||||||
|
multifileStop.addRoute(combinedRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// There should go stops with complete routes:
|
||||||
|
return loadedTransportStops.valueCollection();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public TIntArrayList mergeTransportStops(BinaryMapIndexReader reader,
|
||||||
|
TLongObjectHashMap<TransportStop> loadedTransportStops, List<TransportStop> stops) throws IOException {
|
||||||
|
TIntArrayList routesToLoad = new TIntArrayList();
|
||||||
|
Iterator<TransportStop> it = stops.iterator();
|
||||||
|
TIntArrayList localRoutesToLoad = new TIntArrayList();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
TransportStop stop = it.next();
|
||||||
|
long stopId = stop.getId();
|
||||||
|
localRoutesToLoad.clear();
|
||||||
|
TransportStop multifileStop = loadedTransportStops.get(stopId);
|
||||||
|
long[] routesIds = stop.getRoutesIds();
|
||||||
|
long[] delRIds = stop.getDeletedRoutesIds();
|
||||||
|
if (multifileStop == null) {
|
||||||
|
loadedTransportStops.put(stopId, stop);
|
||||||
|
multifileStop = stop;
|
||||||
|
if (!stop.isDeleted()) {
|
||||||
|
localRoutesToLoad.addAll(stop.getReferencesToRoutes());
|
||||||
|
}
|
||||||
|
} else if (multifileStop.isDeleted()) {
|
||||||
|
// stop has nothing to load, so not needed
|
||||||
|
it.remove();
|
||||||
|
} else {
|
||||||
|
if (delRIds != null) {
|
||||||
|
for (long deletedRouteId : delRIds) {
|
||||||
|
multifileStop.addDeletedRouteId(deletedRouteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (routesIds != null && routesIds.length > 0) {
|
||||||
|
int[] refs = stop.getReferencesToRoutes();
|
||||||
|
for (int i = 0; i < routesIds.length; i++) {
|
||||||
|
long routeId = routesIds[i];
|
||||||
|
if (!multifileStop.hasRoute(routeId) && !multifileStop.isRouteDeleted(routeId)) {
|
||||||
|
localRoutesToLoad.add(refs[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (stop.hasReferencesToRoutes()) {
|
||||||
|
// old format
|
||||||
|
localRoutesToLoad.addAll(stop.getReferencesToRoutes());
|
||||||
|
} else {
|
||||||
|
// stop has noting to load, so not needed
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
routesToLoad.addAll(localRoutesToLoad);
|
||||||
|
// // add valid
|
||||||
|
// stop and
|
||||||
|
// references
|
||||||
|
// to routes
|
||||||
|
// TODO should be here add route not references
|
||||||
|
// multifileStop.putReferencesToRoutes(reader.getFile().getName(), localRoutesToLoad.toArray());
|
||||||
|
}
|
||||||
|
return routesToLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadRoutes(BinaryMapIndexReader reader, TIntObjectHashMap<TransportRoute> localFileRoutes,
|
||||||
|
TIntObjectHashMap<TransportRoute> loadedRoutes, TIntArrayList routesToLoad) throws IOException {
|
||||||
|
// load/combine routes
|
||||||
|
if (routesToLoad.size() > 0) {
|
||||||
|
routesToLoad.sort();
|
||||||
|
TIntArrayList referencesToLoad = new TIntArrayList();
|
||||||
|
TIntIterator itr = routesToLoad.iterator();
|
||||||
|
int prev = routesToLoad.get(0) - 1; // different
|
||||||
|
while (itr.hasNext()) {
|
||||||
|
int nxt = itr.next();
|
||||||
|
if (prev != nxt) {
|
||||||
|
if (localFileRoutes != null && loadedRoutes != null && loadedRoutes.contains(nxt)) { // check if
|
||||||
|
localFileRoutes.put(nxt, loadedRoutes.get(nxt));
|
||||||
|
} else {
|
||||||
|
referencesToLoad.add(nxt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (localFileRoutes != null && loadedRoutes != null) {
|
||||||
|
reader.loadTransportRoutes(referencesToLoad.toArray(), localFileRoutes);
|
||||||
|
loadedRoutes.putAll(localFileRoutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransportRoute getCombinedRoute(TransportRoute route) throws IOException {
|
||||||
|
if (!route.isIncomplete()) {
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
TransportRoute c = combinedRoutesCache.get(route.getId());
|
||||||
|
if (c == null) {
|
||||||
|
c = combineRoute(route);
|
||||||
|
combinedRoutesCache.put(route.getId(), c);
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransportRoute combineRoute(TransportRoute route) throws IOException {
|
||||||
|
// 1. Get all available route parts;
|
||||||
|
List<TransportRoute> incompleteRoutes = findIncompleteRouteParts(route);
|
||||||
|
if (incompleteRoutes == null) {
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
// here could be multiple overlays between same points
|
||||||
|
// It's better to remove them especially identical segments
|
||||||
|
List<Way> allWays = getAllWays(incompleteRoutes);
|
||||||
|
|
||||||
|
// 2. Get array of segments (each array size > 1):
|
||||||
|
LinkedList<List<TransportStop>> stopSegments = parseRoutePartsToSegments(incompleteRoutes);
|
||||||
|
|
||||||
|
// 3. Merge segments and remove excess missingStops (when they are closer then MISSING_STOP_SEARCH_RADIUS):
|
||||||
|
// + Check for missingStops. If they present in the middle/there more then one segment - we have a hole in the
|
||||||
|
// map data
|
||||||
|
List<List<TransportStop>> mergedSegments = combineSegmentsOfSameRoute(stopSegments);
|
||||||
|
|
||||||
|
// 4. Now we need to properly sort segments, proper sorting is minimizing distance between stops
|
||||||
|
// So it is salesman problem, we have this solution at TspAnt, but if we know last or first segment we can solve
|
||||||
|
// it straightforward
|
||||||
|
List<TransportStop> firstSegment = null;
|
||||||
|
List<TransportStop> lastSegment = null;
|
||||||
|
for (List<TransportStop> l : mergedSegments) {
|
||||||
|
if (!l.get(0).isMissingStop()) {
|
||||||
|
firstSegment = l;
|
||||||
|
}
|
||||||
|
if (!l.get(l.size() - 1).isMissingStop()) {
|
||||||
|
lastSegment = l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<List<TransportStop>> sortedSegments = new ArrayList<List<TransportStop>>();
|
||||||
|
if (firstSegment != null) {
|
||||||
|
sortedSegments.add(firstSegment);
|
||||||
|
mergedSegments.remove(firstSegment);
|
||||||
|
while (!mergedSegments.isEmpty()) {
|
||||||
|
List<TransportStop> last = sortedSegments.get(sortedSegments.size() - 1);
|
||||||
|
List<TransportStop> add = findAndDeleteMinDistance(last.get(last.size() - 1).getLocation(),
|
||||||
|
mergedSegments, true);
|
||||||
|
sortedSegments.add(add);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (lastSegment != null) {
|
||||||
|
sortedSegments.add(lastSegment);
|
||||||
|
mergedSegments.remove(lastSegment);
|
||||||
|
while (!mergedSegments.isEmpty()) {
|
||||||
|
List<TransportStop> first = sortedSegments.get(0);
|
||||||
|
List<TransportStop> add = findAndDeleteMinDistance(first.get(0).getLocation(), mergedSegments, false);
|
||||||
|
sortedSegments.add(0, add);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sortedSegments = mergedSegments;
|
||||||
|
}
|
||||||
|
List<TransportStop> finalList = new ArrayList<TransportStop>();
|
||||||
|
for (List<TransportStop> s : sortedSegments) {
|
||||||
|
finalList.addAll(s);
|
||||||
|
}
|
||||||
|
// 5. Create combined TransportRoute and return it
|
||||||
|
return new TransportRoute(route, finalList, allWays);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TransportStop> findAndDeleteMinDistance(LatLon location, List<List<TransportStop>> mergedSegments,
|
||||||
|
boolean attachToBegin) {
|
||||||
|
int ind = attachToBegin ? 0 : mergedSegments.get(0).size() - 1;
|
||||||
|
double minDist = MapUtils.getDistance(mergedSegments.get(0).get(ind).getLocation(), location);
|
||||||
|
int minInd = 0;
|
||||||
|
for (int i = 1; i < mergedSegments.size(); i++) {
|
||||||
|
ind = attachToBegin ? 0 : mergedSegments.get(i).size() - 1;
|
||||||
|
double dist = MapUtils.getDistance(mergedSegments.get(i).get(ind).getLocation(), location);
|
||||||
|
if (dist < minDist) {
|
||||||
|
minInd = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mergedSegments.remove(minInd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Way> getAllWays(List<TransportRoute> parts) {
|
||||||
|
List<Way> w = new ArrayList<Way>();
|
||||||
|
for (TransportRoute t : parts) {
|
||||||
|
w.addAll(t.getForwardWays());
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<List<TransportStop>> combineSegmentsOfSameRoute(LinkedList<List<TransportStop>> segments) {
|
||||||
|
List<List<TransportStop>> resultSegments = new ArrayList<List<TransportStop>>();
|
||||||
|
while (!segments.isEmpty()) {
|
||||||
|
List<TransportStop> firstSegment = segments.poll();
|
||||||
|
boolean merged = true;
|
||||||
|
while (merged) {
|
||||||
|
merged = false;
|
||||||
|
Iterator<List<TransportStop>> it = segments.iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
List<TransportStop> segmentToMerge = it.next();
|
||||||
|
merged = tryToMerge(firstSegment, segmentToMerge);
|
||||||
|
|
||||||
|
if (merged) {
|
||||||
|
it.remove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultSegments.add(firstSegment);
|
||||||
|
}
|
||||||
|
return resultSegments;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean tryToMerge(List<TransportStop> firstSegment, List<TransportStop> segmentToMerge) {
|
||||||
|
if (firstSegment.size() < 2 || segmentToMerge.size() < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 1st we check that segments overlap by stop
|
||||||
|
int commonStopFirst = 0;
|
||||||
|
int commonStopSecond = 0;
|
||||||
|
boolean found = false;
|
||||||
|
for (; commonStopFirst < firstSegment.size(); commonStopFirst++) {
|
||||||
|
for (commonStopSecond = 0; commonStopSecond < segmentToMerge.size() && !found; commonStopSecond++) {
|
||||||
|
long lid1 = firstSegment.get(commonStopFirst).getId();
|
||||||
|
long lid2 = segmentToMerge.get(commonStopSecond).getId();
|
||||||
|
if (lid1 > 0 && lid2 == lid1) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) {
|
||||||
|
// important to increment break inside loop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found && commonStopFirst < firstSegment.size()) {
|
||||||
|
// we've found common stop so we can merge based on stops
|
||||||
|
// merge last part first
|
||||||
|
int leftPartFirst = firstSegment.size() - commonStopFirst;
|
||||||
|
int leftPartSecond = segmentToMerge.size() - commonStopSecond;
|
||||||
|
if (leftPartFirst < leftPartSecond
|
||||||
|
|| (leftPartFirst == leftPartSecond && firstSegment.get(firstSegment.size() - 1).isMissingStop())) {
|
||||||
|
while (firstSegment.size() > commonStopFirst) {
|
||||||
|
firstSegment.remove(firstSegment.size() - 1);
|
||||||
|
}
|
||||||
|
for (int i = commonStopSecond; i < segmentToMerge.size(); i++) {
|
||||||
|
firstSegment.add(segmentToMerge.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// merge first part
|
||||||
|
if (commonStopFirst < commonStopSecond
|
||||||
|
|| (commonStopFirst == commonStopSecond && firstSegment.get(0).isMissingStop())) {
|
||||||
|
for (int i = 0; i < commonStopFirst; i++) {
|
||||||
|
firstSegment.remove(0);
|
||||||
|
}
|
||||||
|
for (int i = commonStopSecond; i >= 0; i--) {
|
||||||
|
firstSegment.add(0, segmentToMerge.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
// no common stops, so try to connect to the end or beginning
|
||||||
|
// beginning
|
||||||
|
boolean merged = false;
|
||||||
|
if (MapUtils.getDistance(firstSegment.get(0).getLocation(),
|
||||||
|
segmentToMerge.get(segmentToMerge.size() - 1).getLocation()) < MISSING_STOP_SEARCH_RADIUS) {
|
||||||
|
firstSegment.remove(0);
|
||||||
|
for (int i = segmentToMerge.size() - 2; i >= 0; i--) {
|
||||||
|
firstSegment.add(0, segmentToMerge.get(i));
|
||||||
|
}
|
||||||
|
merged = true;
|
||||||
|
} else if (MapUtils.getDistance(firstSegment.get(firstSegment.size() - 1).getLocation(),
|
||||||
|
segmentToMerge.get(0).getLocation()) < MISSING_STOP_SEARCH_RADIUS) {
|
||||||
|
firstSegment.remove(firstSegment.size() - 1);
|
||||||
|
for (int i = 1; i < segmentToMerge.size(); i++) {
|
||||||
|
firstSegment.add(segmentToMerge.get(i));
|
||||||
|
}
|
||||||
|
merged = true;
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinkedList<List<TransportStop>> parseRoutePartsToSegments(List<TransportRoute> routeParts) {
|
||||||
|
LinkedList<List<TransportStop>> segs = new LinkedList<List<TransportStop>>();
|
||||||
|
// here we assume that missing stops come in pairs <A, B, C, MISSING, MISSING, D, E...>
|
||||||
|
// we don't add segments with 1 stop cause they are irrelevant further
|
||||||
|
for (TransportRoute part : routeParts) {
|
||||||
|
List<TransportStop> newSeg = new ArrayList<TransportStop>();
|
||||||
|
for (TransportStop s : part.getForwardStops()) {
|
||||||
|
newSeg.add(s);
|
||||||
|
if (s.isMissingStop()) {
|
||||||
|
if (newSeg.size() > 1) {
|
||||||
|
segs.add(newSeg);
|
||||||
|
newSeg = new ArrayList<TransportStop>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newSeg.size() > 1) {
|
||||||
|
segs.add(newSeg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TransportRoute> findIncompleteRouteParts(TransportRoute baseRoute) throws IOException {
|
||||||
|
List<TransportRoute> allRoutes = null;
|
||||||
|
for (BinaryMapIndexReader bmir : routesFilesCache.keySet()) {
|
||||||
|
// here we could limit routeMap indexes by only certain bbox around start / end (check comment on field)
|
||||||
|
IncompleteTransportRoute ptr = bmir.getIncompleteTransportRoutes().get(baseRoute.getId());
|
||||||
|
if (ptr != null) {
|
||||||
|
TIntArrayList lst = new TIntArrayList();
|
||||||
|
while (ptr != null) {
|
||||||
|
lst.add(ptr.getRouteOffset());
|
||||||
|
ptr = ptr.getNextLinkedRoute();
|
||||||
|
}
|
||||||
|
if (lst.size() > 0) {
|
||||||
|
if (allRoutes == null) {
|
||||||
|
allRoutes = new ArrayList<TransportRoute>();
|
||||||
|
}
|
||||||
|
allRoutes.addAll(bmir.getTransportRoutes(lst.toArray()).valueCollection());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allRoutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -159,7 +159,7 @@ public class TransportStopController extends MenuController {
|
||||||
|
|
||||||
private void addTransportStopRoutes(OsmandApplication app, List<TransportStop> stops, List<TransportStopRoute> routes, boolean useEnglishNames) {
|
private void addTransportStopRoutes(OsmandApplication app, List<TransportStop> stops, List<TransportStopRoute> routes, boolean useEnglishNames) {
|
||||||
for (TransportStop tstop : stops) {
|
for (TransportStop tstop : stops) {
|
||||||
if (tstop.hasReferencesToRoutesMap()) {
|
if (!tstop.isDeleted()) {
|
||||||
addRoutes(app, routes, useEnglishNames, tstop, transportStop, (int) MapUtils.getDistance(tstop.getLocation(), transportStop.getLocation()));
|
addRoutes(app, routes, useEnglishNames, tstop, transportStop, (int) MapUtils.getDistance(tstop.getLocation(), transportStop.getLocation()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ import net.osmand.plus.resources.AsyncLoadingThread.TileLoadDownloadRequest;
|
||||||
import net.osmand.plus.srtmplugin.SRTMPlugin;
|
import net.osmand.plus.srtmplugin.SRTMPlugin;
|
||||||
import net.osmand.plus.views.OsmandMapLayer.DrawSettings;
|
import net.osmand.plus.views.OsmandMapLayer.DrawSettings;
|
||||||
import net.osmand.router.TransportRoutePlanner.TransportRoutingContext;
|
import net.osmand.router.TransportRoutePlanner.TransportRoutingContext;
|
||||||
|
import net.osmand.router.TransportStopsRouteReader;
|
||||||
import net.osmand.util.Algorithms;
|
import net.osmand.util.Algorithms;
|
||||||
import net.osmand.util.MapUtils;
|
import net.osmand.util.MapUtils;
|
||||||
|
|
||||||
|
@ -208,7 +209,7 @@ public class ResourceManager {
|
||||||
private final Map<String, RegionAddressRepository> addressMap = new ConcurrentHashMap<String, RegionAddressRepository>();
|
private final Map<String, RegionAddressRepository> addressMap = new ConcurrentHashMap<String, RegionAddressRepository>();
|
||||||
protected final Map<String, AmenityIndexRepository> amenityRepositories = new ConcurrentHashMap<String, AmenityIndexRepository>();
|
protected final Map<String, AmenityIndexRepository> amenityRepositories = new ConcurrentHashMap<String, AmenityIndexRepository>();
|
||||||
// protected final Map<String, BinaryMapIndexReader> routingMapFiles = new ConcurrentHashMap<String, BinaryMapIndexReader>();
|
// protected final Map<String, BinaryMapIndexReader> routingMapFiles = new ConcurrentHashMap<String, BinaryMapIndexReader>();
|
||||||
protected final Map<String, TransportIndexRepository> transportRepositories = new ConcurrentHashMap<String, TransportIndexRepository>();
|
protected final Map<String, BinaryMapReaderResource> transportRepositories = new ConcurrentHashMap<String, BinaryMapReaderResource>();
|
||||||
|
|
||||||
protected final Map<String, String> indexFileNames = new ConcurrentHashMap<String, String>();
|
protected final Map<String, String> indexFileNames = new ConcurrentHashMap<String, String>();
|
||||||
protected final Map<String, String> basemapFileNames = new ConcurrentHashMap<String, String>();
|
protected final Map<String, String> basemapFileNames = new ConcurrentHashMap<String, String>();
|
||||||
|
@ -742,7 +743,7 @@ public class ResourceManager {
|
||||||
addressMap.put(f.getName(), rarb);
|
addressMap.put(f.getName(), rarb);
|
||||||
}
|
}
|
||||||
if (mapReader.hasTransportData()) {
|
if (mapReader.hasTransportData()) {
|
||||||
transportRepositories.put(f.getName(), new TransportIndexRepositoryBinary(resource));
|
transportRepositories.put(f.getName(), resource);
|
||||||
}
|
}
|
||||||
// disable osmc for routing temporarily due to some bugs
|
// disable osmc for routing temporarily due to some bugs
|
||||||
if (mapReader.containsRouteData() && (!f.getParentFile().equals(liveDir) ||
|
if (mapReader.containsRouteData() && (!f.getParentFile().equals(liveDir) ||
|
||||||
|
@ -989,51 +990,31 @@ public class ResourceManager {
|
||||||
|
|
||||||
////////////////////////////////////////////// Working with transport ////////////////////////////////////////////////
|
////////////////////////////////////////////// Working with transport ////////////////////////////////////////////////
|
||||||
|
|
||||||
public LinkedHashMap<String, TransportIndexRepository> getTransportRepositories() {
|
private List<BinaryMapIndexReader> getTransportRepositories(double topLat, double leftLon, double bottomLat, double rightLon) {
|
||||||
List<String> fileNames = new ArrayList<>(transportRepositories.keySet());
|
List<String> fileNames = new ArrayList<>(transportRepositories.keySet());
|
||||||
Collections.sort(fileNames, Algorithms.getStringVersionComparator());
|
Collections.sort(fileNames, Algorithms.getStringVersionComparator());
|
||||||
LinkedHashMap<String, TransportIndexRepository> res = new LinkedHashMap<>();
|
List<BinaryMapIndexReader> res = new ArrayList<>();
|
||||||
for (String fileName : fileNames) {
|
for (String fileName : fileNames) {
|
||||||
TransportIndexRepository r = transportRepositories.get(fileName);
|
BinaryMapReaderResource r = transportRepositories.get(fileName);
|
||||||
if (r != null) {
|
if (r != null && r.isUseForPublicTransport() &&
|
||||||
res.put(fileName, r);
|
r.getShallowReader().containTransportData(topLat, leftLon, bottomLat, rightLon)) {
|
||||||
|
res.add(r.getReader(BinaryMapReaderResourceType.TRANSPORT));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<TransportIndexRepository> searchTransportRepositories(double latitude, double longitude) {
|
|
||||||
List<TransportIndexRepository> repos = new ArrayList<>();
|
|
||||||
for (TransportIndexRepository index : getTransportRepositories().values()) {
|
|
||||||
if (index.isUseForPublicTransport() && index.checkContains(latitude,longitude)) {
|
|
||||||
repos.add(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return repos;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<TransportStop> searchTransportSync(double topLat, double leftLon, double bottomLat, double rightLon,
|
public List<TransportStop> searchTransportSync(double topLat, double leftLon, double bottomLat, double rightLon,
|
||||||
ResultMatcher<TransportStop> matcher) throws IOException {
|
ResultMatcher<TransportStop> matcher) throws IOException {
|
||||||
List<TransportIndexRepository> repos = new ArrayList<>();
|
TransportStopsRouteReader readers =
|
||||||
TLongObjectHashMap<TransportStop> loadedTransportStops = new TLongObjectHashMap<>();
|
new TransportStopsRouteReader(getTransportRepositories(topLat, leftLon, bottomLat, rightLon));
|
||||||
for (TransportIndexRepository index : getTransportRepositories().values()) {
|
|
||||||
if (index.isUseForPublicTransport() && index.checkContains(topLat, leftLon, bottomLat, rightLon)) {
|
|
||||||
repos.add(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!repos.isEmpty()) {
|
|
||||||
for (TransportIndexRepository r : repos) {
|
|
||||||
List<TransportStop> stops = new ArrayList<>();
|
List<TransportStop> stops = new ArrayList<>();
|
||||||
r.searchTransportStops(topLat, leftLon, bottomLat, rightLon, -1, stops, matcher);
|
BinaryMapIndexReader.SearchRequest<TransportStop> req = BinaryMapIndexReader.buildSearchTransportRequest(MapUtils.get31TileNumberX(leftLon),
|
||||||
BinaryMapIndexReader reader = ((TransportIndexRepositoryBinary) r).getOpenFile();
|
MapUtils.get31TileNumberX(rightLon), MapUtils.get31TileNumberY(topLat),
|
||||||
if (reader != null) {
|
MapUtils.get31TileNumberY(bottomLat), -1, stops);
|
||||||
TransportRoutingContext.mergeTransportStops(reader, loadedTransportStops, stops, null, null);
|
for (TransportStop s : readers.readMergedTransportStops(req)) {
|
||||||
}
|
if (!s.isDeleted() && !s.isMissingStop()) {
|
||||||
}
|
|
||||||
}
|
|
||||||
List<TransportStop> stops = new ArrayList<>();
|
|
||||||
for (TransportStop s : loadedTransportStops.valueCollection()) {
|
|
||||||
if (!s.isDeleted()) {
|
|
||||||
stops.add(s);
|
stops.add(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1041,19 +1022,11 @@ public class ResourceManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<TransportRoute> getRoutesForStop(TransportStop stop) {
|
public List<TransportRoute> getRoutesForStop(TransportStop stop) {
|
||||||
List<TransportRoute> routes = new ArrayList<>();
|
List<TransportRoute> rts = stop.getRoutes();
|
||||||
LinkedHashMap<String, TransportIndexRepository> repositories = getTransportRepositories();
|
if(rts != null) {
|
||||||
LinkedHashMap<String, int[]> referencesToRoutes = stop.getReferencesToRoutesMap();
|
return rts;
|
||||||
if (referencesToRoutes != null) {
|
|
||||||
for (Entry<String, int[]> refs : referencesToRoutes.entrySet()) {
|
|
||||||
TransportIndexRepository r = repositories.get(refs.getKey());
|
|
||||||
if (r != null) {
|
|
||||||
List<TransportRoute> rr = r.getRoutesForReferences(refs.getValue());
|
|
||||||
routes.addAll(rr);
|
|
||||||
}
|
}
|
||||||
}
|
return Collections.emptyList();
|
||||||
}
|
|
||||||
return routes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////// Working with map ////////////////////////////////////////////////
|
////////////////////////////////////////////// Working with map ////////////////////////////////////////////////
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
package net.osmand.plus.resources;
|
|
||||||
|
|
||||||
import net.osmand.ResultMatcher;
|
|
||||||
import net.osmand.data.TransportRoute;
|
|
||||||
import net.osmand.data.TransportStop;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface TransportIndexRepository {
|
|
||||||
|
|
||||||
public boolean checkContains(double latitude, double longitude);
|
|
||||||
|
|
||||||
public boolean checkContains(double topLatitude, double leftLongitude, double bottomLatitude, double rightLongitude);
|
|
||||||
|
|
||||||
public boolean acceptTransportStop(TransportStop stop);
|
|
||||||
|
|
||||||
public void searchTransportStops(double topLatitude, double leftLongitude, double bottomLatitude, double rightLongitude,
|
|
||||||
int limit, List<TransportStop> stops, ResultMatcher<TransportStop> matcher);
|
|
||||||
|
|
||||||
public List<TransportRoute> getRoutesForStop(TransportStop stop);
|
|
||||||
|
|
||||||
public List<TransportRoute> getRoutesForReferences(int[] referencesToRoutes);
|
|
||||||
|
|
||||||
public boolean isUseForPublicTransport();
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
package net.osmand.plus.resources;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import net.osmand.PlatformUtil;
|
|
||||||
import net.osmand.ResultMatcher;
|
|
||||||
import net.osmand.binary.BinaryMapIndexReader;
|
|
||||||
import net.osmand.data.TransportRoute;
|
|
||||||
import net.osmand.data.TransportStop;
|
|
||||||
import net.osmand.plus.resources.ResourceManager.BinaryMapReaderResource;
|
|
||||||
import net.osmand.plus.resources.ResourceManager.BinaryMapReaderResourceType;
|
|
||||||
import net.osmand.util.Algorithms;
|
|
||||||
import net.osmand.util.MapUtils;
|
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class TransportIndexRepositoryBinary implements TransportIndexRepository {
|
|
||||||
private static final Log log = PlatformUtil.getLog(TransportIndexRepositoryBinary.class);
|
|
||||||
private BinaryMapReaderResource resource;
|
|
||||||
|
|
||||||
public TransportIndexRepositoryBinary(BinaryMapReaderResource resource) {
|
|
||||||
this.resource = resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public BinaryMapIndexReader getOpenFile() {
|
|
||||||
return resource.getReader(BinaryMapReaderResourceType.TRANSPORT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean checkContains(double latitude, double longitude) {
|
|
||||||
BinaryMapIndexReader shallowReader = resource.getShallowReader();
|
|
||||||
return shallowReader != null && shallowReader.containTransportData(latitude, longitude);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public boolean checkContains(double topLatitude, double leftLongitude, double bottomLatitude, double rightLongitude) {
|
|
||||||
BinaryMapIndexReader shallowReader = resource.getShallowReader();
|
|
||||||
return shallowReader != null && shallowReader.containTransportData(topLatitude, leftLongitude, bottomLatitude, rightLongitude);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void searchTransportStops(double topLatitude, double leftLongitude, double bottomLatitude, double rightLongitude,
|
|
||||||
int limit, List<TransportStop> stops, ResultMatcher<TransportStop> matcher) {
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
try {
|
|
||||||
BinaryMapIndexReader reader = getOpenFile();
|
|
||||||
if (reader != null) {
|
|
||||||
reader.searchTransportIndex(BinaryMapIndexReader.buildSearchTransportRequest(MapUtils.get31TileNumberX(leftLongitude),
|
|
||||||
MapUtils.get31TileNumberX(rightLongitude), MapUtils.get31TileNumberY(topLatitude),
|
|
||||||
MapUtils.get31TileNumberY(bottomLatitude), limit, stops));
|
|
||||||
if (log.isDebugEnabled()) {
|
|
||||||
log.debug(String.format("Search for %s done in %s ms found %s.", //$NON-NLS-1$
|
|
||||||
topLatitude + " " + leftLongitude, System.currentTimeMillis() - now, stops.size())); //$NON-NLS-1$
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Disk error ", e); //$NON-NLS-1$
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized List<TransportRoute> getRoutesForStop(TransportStop stop) {
|
|
||||||
return getRoutesForReferences(stop.getReferencesToRoutes());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<TransportRoute> getRoutesForReferences(int[] referencesToRoutes) {
|
|
||||||
try {
|
|
||||||
BinaryMapIndexReader reader = getOpenFile();
|
|
||||||
if (reader != null) {
|
|
||||||
Collection<TransportRoute> res = reader.getTransportRoutes(referencesToRoutes).valueCollection();
|
|
||||||
if (res != null) {
|
|
||||||
List<TransportRoute> lst = new ArrayList<>(res);
|
|
||||||
Collections.sort(lst, new Comparator<TransportRoute>() {
|
|
||||||
@Override
|
|
||||||
public int compare(TransportRoute o1, TransportRoute o2) {
|
|
||||||
int i1 = Algorithms.extractFirstIntegerNumber(o1.getRef());
|
|
||||||
int i2 = Algorithms.extractFirstIntegerNumber(o2.getRef());
|
|
||||||
int r = Algorithms.compare(i1, i2);
|
|
||||||
if (r == 0) {
|
|
||||||
r = Algorithms.compare(o1.getName(), o2.getName());
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return lst;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Disk error ", e); //$NON-NLS-1$
|
|
||||||
}
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean acceptTransportStop(TransportStop stop) {
|
|
||||||
BinaryMapIndexReader shallowReader = resource.getShallowReader();
|
|
||||||
return shallowReader != null && shallowReader.transportStopBelongsTo(stop);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isUseForPublicTransport() {
|
|
||||||
return resource.isUseForPublicTransport();
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue