/*##################################################################### * * CLAVIN (Cartographic Location And Vicinity INdexer) * --------------------------------------------------- * * Copyright (C) 2012-2013 Berico Technologies * http://clavin.bericotechnologies.com * * ==================================================================== * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. * * ==================================================================== * * MatchedLocation.java * *###################################################################*/ package com.bericotech.clavin.resolver.multipart; import com.bericotech.clavin.gazetteer.GeoName; import com.bericotech.clavin.resolver.ResolvedLocation; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.EnumMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A candidate match for a multi-level search. */ public class MatchedLocation { private static final Logger LOG = LoggerFactory.getLogger(MatchedLocation.class); private final Map<SearchLevel, Match> matches; public MatchedLocation(final Deque<SearchResult> results) { matches = new EnumMap<SearchLevel, Match>(SearchLevel.class); Map<SearchLevel, List<ResolvedLocation>> resultsMap = new EnumMap<SearchLevel, List<ResolvedLocation>>(SearchLevel.class); for (SearchResult result : results) { resultsMap.put(result.level, result.locations); } ResolvedLocation bestMatch = results.peek().getBestLocation(); matches.put(results.peek().level, new Match(results.peek().level, bestMatch, 0)); // if the geoname's ancestry is fully resolved, find the // matched ancestors in the search results and populate the map; // otherwise, we cannot populate the map with anything other than // the best result because we cannot verify which search result // is the parent of the selected location if (bestMatch.getGeoname().isAncestryResolved()) { GeoName parent = bestMatch.getGeoname().getParent(); while (parent != null) { SearchLevel level = SearchLevel.forGeoName(parent); if (resultsMap.containsKey(level)) { // find parent GeoName in the results; this should exist because // searches are filtered by ancestry from prior results ResolvedLocation parentLoc = null; List<ResolvedLocation> searchResults = resultsMap.get(level); int depth; for (depth = 0; depth < searchResults.size(); depth++) { ResolvedLocation loc = searchResults.get(depth); if (parent.getGeonameID() == loc.getGeoname().getGeonameID()) { parentLoc = loc; break; } } if (parentLoc == null) { // log this as an error condition; it indicates a problem with either // gazetteer construction or ancestry indexing; this has been noticed // with certain historical locations, specifically Netherlands Antilles (PCLH), // which lists Curacao (PCLIX), another country, as a parent. We shouldn't // fail the entire matching algorithm at this point; just log and ignore LOG.error(String.format("Missing parent [%s] in search results for match: %s.", parent, bestMatch)); } else { // found a match, add it to the list matches.put(level, new Match(level, parentLoc, depth)); } } parent = parent.getParent(); } } } public Match getMatch(final SearchLevel level) { return matches.get(level); } public Match getMostSpecificMatch() { Match match = null; for (SearchLevel level = SearchLevel.CITY; match == null && level != null; level = level.broaden()) { match = matches.get(level); } return match; } public Collection<Match> getMatches() { return Collections.unmodifiableCollection(matches.values()); } public int getMatchCount() { return matches.size(); } public boolean isFullySpecified() { return getMatchCount() == SearchLevel.values().length; } @Override public int hashCode() { int hash = 7; for (SearchLevel level : SearchLevel.values()) { hash = 89 * hash + (matches.containsKey(level) ? matches.get(level).hashCode() : 0); } return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final MatchedLocation other = (MatchedLocation) obj; for (SearchLevel level : SearchLevel.values()) { Match mine = getMatch(level); Match theirs = other.getMatch(level); if (mine != theirs && (mine == null || !mine.equals(theirs))) { return false; } } return true; } @Override public String toString() { StringBuilder builder = new StringBuilder("Match: { "); for (SearchLevel level : SearchLevel.values()) { builder.append(level).append(": "); Match match = matches.get(level); if (match != null) { builder.append(String.format("[%d] %s (d:%d)", match.getLocation().getGeoname().getGeonameID(), match.getLocation().getGeoname().getName(), match.getDepth())); } else { builder.append("NULL"); } if (level.canNarrow()) { builder.append(", "); } } return builder.append(" }").toString(); } public static class Match { private final SearchLevel level; private final ResolvedLocation location; private final int depth; public Match(final SearchLevel level, final ResolvedLocation location, final int depth) { this.level = level; this.location = location; this.depth = depth; } public SearchLevel getLevel() { return level; } public ResolvedLocation getLocation() { return location; } public int getDepth() { return depth; } @Override public int hashCode() { int hash = 3; hash = 43 * hash + (this.level != null ? this.level.hashCode() : 0); hash = 43 * hash + (this.location != null ? this.location.hashCode() : 0); hash = 43 * hash + this.depth; return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Match other = (Match) obj; if (this.level != other.level) { return false; } if (this.location != other.location && (this.location == null || !this.location.equals(other.location))) { return false; } if (this.depth != other.depth) { return false; } return true; } } }