/*
* Copyright (C) 2008 Steve Ratcliffe
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 or
* version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
package uk.me.parabola.imgfmt.app.net;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.general.CityInfo;
import uk.me.parabola.mkgmap.general.ZipCodeInfo;
/**
* Describes the house numbering from a node in the road.
* @author Steve Ratcliffe
*/
public class Numbers {
private static final Logger log = Logger.getLogger(Numbers.class);
public static final boolean LEFT = true;
public static final boolean RIGHT = false;
private static final int MAX_DELTA = 131071; // see NumberPreparer
// The node in the road where these numbers apply. In the polish notation it is the
// node in the road, whereas in the NET file it is the index of the number node.
private int nodeNumber; // node in road index
private Integer indexNumber; // the position in the list of Numbers (starting with 0)
// the data on side of the road
private RoadSide leftSide,rightSide;
private class RoadSide {
NumDesc numbers;
// to be added
CityInfo cityInfo;
ZipCodeInfo zipCode;
boolean isEmpty(){
return cityInfo == null && zipCode == null && numbers == null;
}
}
private class NumDesc{
NumberStyle numberStyle;
int start,end;
public NumDesc(NumberStyle numberStyle, int start, int end) {
this.numberStyle = numberStyle;
this.start = start;
this.end = end;
}
public boolean contained(int hn){
boolean isEven = (hn % 2 == 0);
if (numberStyle == NumberStyle.BOTH
|| numberStyle == NumberStyle.EVEN && isEven
|| numberStyle == NumberStyle.ODD && !isEven){
if (start <= end) {
if (start <= hn && hn <= end)
return true;
}
else {
if (end <= hn && hn <= start)
return true;
}
}
return false;
}
@Override
public String toString() {
return String.format("%s,%d,%d", numberStyle, start,end);
}
}
public Numbers() {
}
/**
* This constructor takes a comma separated list as in the polish format. Also used in testing as
* it is an easy way to set all common parameters at once.
*
* @param spec Node number, followed by left and then right parameters as in the polish format.
*/
public Numbers(String spec) {
String[] strings = spec.split(",");
nodeNumber = Integer.valueOf(strings[0]);
NumberStyle numberStyle = NumberStyle.fromChar(strings[1]);
int start = Integer.valueOf(strings[2]);
int end = Integer.valueOf(strings[3]);
setNumbers(LEFT, numberStyle, start, end);
numberStyle = NumberStyle.fromChar(strings[4]);
start = Integer.valueOf(strings[5]);
end = Integer.valueOf(strings[6]);
setNumbers(RIGHT, numberStyle, start, end);
if (strings.length > 8){
// zip codes
String zip = strings[7];
if ("-1".equals(zip) == false)
setZipCode(LEFT, new ZipCodeInfo(zip));
zip = strings[8];
if ("-1".equals(zip) == false)
setZipCode(RIGHT, new ZipCodeInfo(zip));
}
if (strings.length > 9){
String city,region,country;
int nextPos = 9;
city = strings[nextPos];
if ("-1".equals(city) == false){
region = strings[nextPos + 1];
country = strings[nextPos + 2];
setCityInfo(LEFT, new CityInfo(city, region, country));
nextPos = 12;
} else
nextPos = 10;
city = strings[nextPos];
if ("-1".equals(city) == false){
region = strings[nextPos + 1];
country = strings[nextPos + 2];
setCityInfo(RIGHT, new CityInfo(city, region, country));
}
}
}
public void setNumbers(boolean left, NumberStyle numberStyle, int start, int end){
if (numberStyle != NumberStyle.NONE || start != -1 || end != -1){
RoadSide rs = assureSideIsAllocated(left);
rs.numbers = new NumDesc(numberStyle, start, end);
} else {
RoadSide rs = (left) ? leftSide : rightSide;
if (rs != null)
rs.numbers = null;
removeIfEmpty(left);
}
}
public void setCityInfo(boolean left, CityInfo ci){
if (ci != null){
RoadSide rs = assureSideIsAllocated(left);
rs.cityInfo = ci;
} else {
RoadSide rs = (left) ? leftSide : rightSide;
if (rs != null)
rs.cityInfo = null;
removeIfEmpty(left);
}
}
public CityInfo getCityInfo(boolean left){
RoadSide rs = (left) ? leftSide : rightSide;
return (rs != null) ? rs.cityInfo : null;
}
public void setZipCode(boolean left, ZipCodeInfo zipCode){
if (zipCode != null){
RoadSide rs = assureSideIsAllocated(left);
rs.zipCode = zipCode;
} else {
RoadSide rs = (left) ? leftSide : rightSide;
if (rs != null)
rs.zipCode= null;
removeIfEmpty(left);
}
}
public ZipCodeInfo getZipCodeInfo (boolean left){
RoadSide rs = (left) ? leftSide : rightSide;
return (rs != null) ? rs.zipCode: null;
}
private void removeIfEmpty(boolean left){
if (left && leftSide != null && leftSide.isEmpty())
leftSide = null;
if (!left && rightSide != null && rightSide.isEmpty())
rightSide = null;
}
// allocate or return allocated RoadSide instance for the given road side
private RoadSide assureSideIsAllocated(boolean left){
if (left && leftSide == null)
leftSide = new RoadSide();
if (!left && rightSide == null)
rightSide = new RoadSide();
return (left) ? leftSide : rightSide;
}
public int getNodeNumber() {
return nodeNumber;
}
public void setNodeNumber(int nodeNumber) {
this.nodeNumber = nodeNumber;
}
public int getIndex() {
if (indexNumber == null) {
log.error("WARNING: index not set!!");
return nodeNumber;
}
return indexNumber;
}
public boolean hasIndex() {
return indexNumber != null;
}
/**
* @param index the nth number node
*/
public void setIndex(int index) {
this.indexNumber = index;
}
private NumDesc getNumbers(boolean left) {
RoadSide rs = (left) ? leftSide : rightSide;
return (rs != null) ? rs.numbers : null;
}
public NumberStyle getNumberStyle(boolean left) {
NumDesc n = getNumbers(left);
return (n == null) ? NumberStyle.NONE : n.numberStyle;
}
public int getStart(boolean left) {
NumDesc n = getNumbers(left);
return (n == null) ? -1 : n.start; // -1 is the default in the polish format
}
public int getEnd(boolean left) {
NumDesc n = getNumbers(left);
return (n == null) ? -1 : n.end; // -1 is the default in the polish format
}
public String toString() {
String nodeStr = "0";
if (nodeNumber > 0)
nodeStr = String.valueOf(nodeNumber);
else if (getIndex() > 0)
nodeStr = String.format("(n%d)", getIndex());
nodeStr = String.format("%s,%s,%d,%d,%s,%d,%d",
nodeStr,
getNumberStyle(LEFT),
getStart(LEFT),
getEnd(LEFT),
getNumberStyle(RIGHT),
getStart(RIGHT),
getEnd(RIGHT));
if (getCityInfo(LEFT) != null || getCityInfo(RIGHT) != null
|| getZipCodeInfo(LEFT) != null || getZipCodeInfo(RIGHT) != null) {
nodeStr = String.format("%s,%s,%s", nodeStr,
getPolishZipCode(LEFT), getPolishZipCode(RIGHT));
if (getCityInfo(LEFT) != null || getCityInfo(RIGHT) != null) {
nodeStr = String.format("%s,%s,%s",nodeStr,
getPolishCityInfo(LEFT),getPolishCityInfo(RIGHT));
}
}
return nodeStr;
}
public NumberStyle getLeftNumberStyle() {
return getNumberStyle(LEFT);
}
public NumberStyle getRightNumberStyle() {
return getNumberStyle(RIGHT);
}
public int getLeftStart(){
return getStart(LEFT);
}
public int getRightStart(){
return getStart(RIGHT);
}
public int getLeftEnd(){
return getEnd(LEFT);
}
public int getRightEnd(){
return getEnd(RIGHT);
}
public boolean equals(Object obj) {
if (!(obj instanceof Numbers))
return false;
Numbers other = (Numbers) obj;
return toString().equals(other.toString());
}
public int hashCode() {
return toString().hashCode();
}
public boolean isPlausible(){
if (!isPlausible(getLeftNumberStyle(), getLeftStart(), getLeftEnd()))
return false;
if (!isPlausible(getRightNumberStyle(), getRightStart(), getRightEnd()))
return false;
if (getLeftNumberStyle() == NumberStyle.NONE
|| getRightNumberStyle() == NumberStyle.NONE)
return true;
if (getCityInfo(LEFT) != null){
if (getCityInfo(LEFT).equals(getCityInfo(RIGHT)) == false)
return true;
} else if (getCityInfo(RIGHT) != null)
return true;
if (getZipCodeInfo(LEFT) != null){
if (getZipCodeInfo(LEFT).equals(getZipCodeInfo(RIGHT)) == false)
return true;
} else if (getCityInfo(RIGHT) != null)
return true;
if (getLeftNumberStyle() == getRightNumberStyle() || getLeftNumberStyle() == NumberStyle.BOTH || getRightNumberStyle()==NumberStyle.BOTH){
// check if intervals are overlapping
int start1, start2,end1,end2;
if (getLeftStart() < getLeftEnd()){
start1 = getLeftStart();
end1 = getLeftEnd();
} else {
start1 = getLeftEnd();
end1 = getLeftStart();
}
if (getRightStart() < getRightEnd()){
start2 = getRightStart();
end2 = getRightEnd();
} else {
start2 = getRightEnd();
end2 = getRightStart();
}
if (start2 > end1 || end2 < start1)
return true;
if (getLeftStart() == getLeftEnd() && getRightStart() == getRightEnd() && getLeftStart() == getRightStart())
return true; // single number on both sides of the road
return false;
}
return true;
}
private static boolean isPlausible(NumberStyle style, int start, int end){
if (Math.abs(start - end) > MAX_DELTA)
return false;
if (style == NumberStyle.EVEN)
return start % 2 == 0 && end % 2 == 0;
if (style == NumberStyle.ODD)
return start % 2 != 0 && end % 2 != 0;
return true;
}
public boolean isContained(int hn, boolean left){
RoadSide rs = left ? leftSide : rightSide;
if (rs == null || rs.numbers == null)
return false;
return rs.numbers.contained(hn);
}
/**
* @param hn a house number
* @param left left or right side
* @return 0 if the number is not within the intervals, 1 if it is on one side, 2 if it on both sides
*/
public int countMatches(int hn) {
int matches = 0;
if (isContained(hn, LEFT))
matches++;
if (isContained(hn, RIGHT))
matches++;
if (matches > 1){
if (getLeftStart() == getLeftEnd() && getRightStart() == getRightEnd())
matches = 1; // single number on both sides of the road
}
return matches;
}
/**
* Compare all fields that describe the interval, but not the position
* @param other
* @return true if these fields are equal
*/
public boolean isSimilar(Numbers other){
if (other == null)
return false;
if (getLeftNumberStyle() != other.getLeftNumberStyle()
|| getLeftStart() != other.getLeftStart() || getLeftEnd() != other.getLeftEnd()
|| getRightNumberStyle() != other.getRightNumberStyle()
|| getRightStart() != other.getRightStart() || getRightEnd() != other.getRightEnd())
return false;
return true;
}
public boolean isEmpty(){
return getLeftNumberStyle() == NumberStyle.NONE && getRightNumberStyle() == NumberStyle.NONE;
}
private String getPolishCityInfo (boolean left){
CityInfo ci = getCityInfo(left);
if (ci == null)
return "-1";
StringBuilder sb = new StringBuilder();
if (ci.getCity() != null)
sb.append(ci.getCity());
sb.append(",");
if (ci.getRegion() != null)
sb.append(ci.getRegion());
sb.append(",");
if (ci.getCountry() != null)
sb.append(ci.getCountry());
return sb.toString();
}
private String getPolishZipCode (boolean left){
ZipCodeInfo zip = getZipCodeInfo(left);
return (zip != null && zip.getZipCode() != null ) ? zip.getZipCode() : "-1";
}
}