package org.activityinfo.server.command.handler;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import org.activityinfo.model.legacy.KeyGenerator;
import org.activityinfo.legacy.shared.command.MatchLocation;
import org.activityinfo.legacy.shared.command.result.CommandResult;
import org.activityinfo.legacy.shared.exception.CommandException;
import org.activityinfo.legacy.shared.model.AdminEntityDTO;
import org.activityinfo.legacy.shared.model.LocationDTO;
import org.activityinfo.server.database.hibernate.entity.AdminEntity;
import org.activityinfo.server.database.hibernate.entity.AdminLevel;
import org.activityinfo.server.database.hibernate.entity.Location;
import org.activityinfo.server.database.hibernate.entity.User;
import org.activityinfo.server.util.Jaro;
import javax.persistence.EntityManager;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
public class MatchLocationHandler implements CommandHandler<MatchLocation> {
private EntityManager em;
private Jaro jaro = new Jaro();
@Inject
public MatchLocationHandler(EntityManager em) {
super();
this.em = em;
}
@Override
public CommandResult execute(MatchLocation cmd, User user) throws CommandException {
Map<Integer, AdminEntity> matched = Maps.newHashMap();
// now try and match against the text
for (Integer levelId : cmd.getAdminLevels().keySet()) {
matchEntity(matched, cmd.getAdminLevels(), em.find(AdminLevel.class, levelId));
}
Location matchedLocation = matchLocation(cmd.getLocationType(), cmd.getName(), matched.values());
LocationDTO location = new LocationDTO();
if (matchedLocation == null) {
// create a new location object
location.setId(new KeyGenerator().generateInt());
location.setName(cmd.getName());
location.setLatitude(cmd.getLatitude());
location.setLongitude(cmd.getLongitude());
location.setNew(true);
location.setLocationTypeId(cmd.getLocationType());
for (AdminEntity entity : matched.values()) {
AdminEntityDTO dto = new AdminEntityDTO();
dto.setId(entity.getId());
dto.setName(entity.getName());
dto.setLevelId(entity.getLevel().getId());
location.setAdminEntity(entity.getLevel().getId(), dto);
}
} else {
location.setNew(false);
location.setId(matchedLocation.getId());
location.setName(matchedLocation.getName());
location.setLatitude(matchedLocation.getY());
location.setLongitude(matchedLocation.getX());
location.setLocationTypeId(matchedLocation.getLocationType().getId());
for (AdminEntity entity : matchedLocation.getAdminEntities()) {
AdminEntityDTO dto = new AdminEntityDTO();
dto.setId(entity.getId());
dto.setName(entity.getName());
dto.setLevelId(entity.getLevel().getId());
location.setAdminEntity(entity.getLevel().getId(), dto);
}
}
return location;
}
/* try to match to an existing location, based on administrative entity
* membership and approximate name match
*/
private Location matchLocation(int locationTypeId, String name, Collection<AdminEntity> entities) {
// find the set of locations that is present in all matched admin
// entities
Set<Location> locations = null;
for (AdminEntity entity : entities) {
if (locations == null) {
locations = Sets.newHashSet(entity.getLocations());
} else {
locations.retainAll(entity.getLocations());
}
}
// anything?
if (locations == null) {
return null;
}
// now find the best matching location by name among this set
Location bestMatch = null;
double bestScore = 0;
for (Location location : locations) {
if (location.getLocationType().getId() == locationTypeId) {
double score = similarity(location.getName(), name);
if (score > bestScore) {
bestScore = score;
bestMatch = location;
}
}
}
return bestMatch;
}
private void matchEntity(Map<Integer, AdminEntity> matched, Map<Integer, String> toMatch, AdminLevel level) {
// match parent level first
if (level.getParent() != null && !matched.containsKey(level.getParentId())) {
matchEntity(matched, toMatch, level.getParent());
}
// match by name
String name = toMatch.get(level.getId());
if (!Strings.isNullOrEmpty(name)) {
AdminEntity parent = null;
AdminEntity matchedEntity = null;
if (level.getParent() != null && matched.containsKey(level.getParentId())) {
parent = matched.get(level.getParentId());
matchedEntity = match(name, parent.getChildren());
} else {
matchedEntity = match(name, level.getEntities());
}
if (matchedEntity != null) {
matched.put(level.getId(), matchedEntity);
// the import may provide child entities without their parents,
// so just be sure to add parents here as well
addParent(matched, matchedEntity);
}
}
}
private void addParent(Map<Integer, AdminEntity> matched, AdminEntity child) {
if (child.getParent() != null) {
matched.put(child.getParent().getLevel().getId(), child.getParent());
addParent(matched, child.getParent());
}
}
private AdminEntity match(String name, Set<AdminEntity> entities) {
// look for an exact match first
for (AdminEntity entity : entities) {
if (entity.getName().equalsIgnoreCase(name)) {
return entity;
}
}
// look for the best approximate match
double bestFit = 0;
AdminEntity bestEntity = null;
for (AdminEntity entity : entities) {
double similarity = similarity(name, entity.getName());
if (similarity > bestFit) {
bestFit = similarity;
bestEntity = entity;
}
}
return bestEntity;
}
protected float similarity(String s1, String s2) {
return jaro.getSimilarity(s1.toLowerCase(), s2.toLowerCase());
}
}