// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.dialogs.relation.sort;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.gui.DefaultNameFormatter;
import org.openstreetmap.josm.tools.AlphanumComparator;
import org.openstreetmap.josm.tools.Utils;
public class RelationSorter {
private interface AdditionalSorter {
boolean acceptsMember(RelationMember m);
List<RelationMember> sortMembers(List<RelationMember> list);
}
private static final Collection<AdditionalSorter> additionalSorters = new ArrayList<>();
static {
// first adequate sorter is used, so order matters
additionalSorters.add(new AssociatedStreetRoleStreetSorter());
additionalSorters.add(new AssociatedStreetRoleAddressHouseSorter());
additionalSorters.add(new PublicTransportRoleStopPlatformSorter());
}
/**
* Class that sorts the {@code street} members of
* {@code type=associatedStreet} and {@code type=street} relations.
*/
private static class AssociatedStreetRoleStreetSorter implements AdditionalSorter {
@Override
public boolean acceptsMember(RelationMember m) {
return "street".equals(m.getRole());
}
@Override
public List<RelationMember> sortMembers(List<RelationMember> list) {
return sortMembersByConnectivity(list);
}
}
/**
* Class that sorts the {@code address} and {@code house} members of
* {@code type=associatedStreet} and {@code type=street} relations.
*/
private static class AssociatedStreetRoleAddressHouseSorter implements AdditionalSorter {
@Override
public boolean acceptsMember(RelationMember m) {
return "address".equals(m.getRole()) || "house".equals(m.getRole());
}
@Override
public List<RelationMember> sortMembers(List<RelationMember> list) {
list.sort((a, b) -> {
final int houseNumber = AlphanumComparator.getInstance().compare(
a.getMember().get("addr:housenumber"),
b.getMember().get("addr:housenumber"));
if (houseNumber != 0) {
return houseNumber;
}
final String aDisplayName = a.getMember().getDisplayName(DefaultNameFormatter.getInstance());
final String bDisplayName = b.getMember().getDisplayName(DefaultNameFormatter.getInstance());
return AlphanumComparator.getInstance().compare(aDisplayName, bDisplayName);
});
return list;
}
}
/**
* Class that sorts the {@code platform} and {@code stop} members of
* {@code type=public_transport} relations.
*/
private static class PublicTransportRoleStopPlatformSorter implements AdditionalSorter {
@Override
public boolean acceptsMember(RelationMember m) {
return m.getRole() != null && (m.getRole().startsWith("platform") || m.getRole().startsWith("stop"));
}
private static String getStopName(OsmPrimitive p) {
for (Relation ref : Utils.filteredCollection(p.getReferrers(), Relation.class)) {
if (ref.hasTag("type", "public_transport") && ref.hasTag("public_transport", "stop_area") && ref.getName() != null) {
return ref.getName();
}
}
return p.getName();
}
@Override
public List<RelationMember> sortMembers(List<RelationMember> list) {
final Map<String, RelationMember> platformByName = new HashMap<>();
for (RelationMember i : list) {
if (i.getRole().startsWith("platform")) {
final RelationMember old = platformByName.put(getStopName(i.getMember()), i);
if (old != null) {
// Platform with same name present. Stop to avoid damaging complicated relations.
// This case can happily be handled differently.
return list;
}
}
}
final List<RelationMember> sorted = new ArrayList<>(list.size());
for (RelationMember i : list) {
if (i.getRole().startsWith("stop")) {
sorted.add(i);
final RelationMember platform = platformByName.remove(getStopName(i.getMember()));
if (platform != null) {
sorted.add(platform);
}
}
}
sorted.addAll(platformByName.values());
return sorted;
}
}
/**
* Sort a collection of relation members by the way they are linked.
*
* @param relationMembers collection of relation members
* @return sorted collection of relation members
*/
public List<RelationMember> sortMembers(List<RelationMember> relationMembers) {
List<RelationMember> newMembers = new ArrayList<>();
// Sort members with custom mechanisms (relation-dependent)
List<RelationMember> defaultMembers = new ArrayList<>(relationMembers.size());
// Maps sorter to assigned members for sorting. Use LinkedHashMap to retain order.
Map<AdditionalSorter, List<RelationMember>> customMap = new LinkedHashMap<>();
// Dispatch members to the first adequate sorter
for (RelationMember m : relationMembers) {
boolean wasAdded = false;
for (AdditionalSorter sorter : additionalSorters) {
if (sorter.acceptsMember(m)) {
List<RelationMember> list;
list = customMap.get(sorter);
if (list == null) {
list = new LinkedList<>();
customMap.put(sorter, list);
}
list.add(m);
wasAdded = true;
break;
}
}
if (!wasAdded) {
defaultMembers.add(m);
}
}
// Sort members and add them to result
for (Entry<AdditionalSorter, List<RelationMember>> entry : customMap.entrySet()) {
newMembers.addAll(entry.getKey().sortMembers(entry.getValue()));
}
newMembers.addAll(sortMembersByConnectivity(defaultMembers));
return newMembers;
}
public static List<RelationMember> sortMembersByConnectivity(List<RelationMember> defaultMembers) {
List<RelationMember> newMembers = new ArrayList<>();
RelationNodeMap map = new RelationNodeMap(defaultMembers);
// List of groups of linked members
//
List<LinkedList<Integer>> allGroups = new ArrayList<>();
// current group of members that are linked among each other
// Two successive members are always linked i.e. have a common node.
//
LinkedList<Integer> group;
Integer first;
while ((first = map.pop()) != null) {
group = new LinkedList<>();
group.add(first);
allGroups.add(group);
Integer next = first;
while ((next = map.popAdjacent(next)) != null) {
group.addLast(next);
}
// The first element need not be in front of the list.
// So the search goes in both directions
//
next = first;
while ((next = map.popAdjacent(next)) != null) {
group.addFirst(next);
}
}
for (List<Integer> tmpGroup : allGroups) {
for (Integer p : tmpGroup) {
newMembers.add(defaultMembers.get(p));
}
}
// Finally, add members that have not been sorted at all
for (Integer i : map.getNotSortableMembers()) {
newMembers.add(defaultMembers.get(i));
}
return newMembers;
}
}