/*******************************************************************************
* Gisgraphy Project
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA
*
* Copyright 2008 Gisgraphy project
* David Masclet <davidmasclet@gisgraphy.com>
*
*
*******************************************************************************/
package com.gisgraphy.domain.geoloc.entity;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Transient;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.Index;
import org.hibernate.annotations.Sort;
import org.hibernate.annotations.SortType;
import org.hibernate.annotations.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gisgraphy.domain.valueobject.GISSource;
import com.gisgraphy.domain.valueobject.SRID;
import com.gisgraphy.domain.valueobject.SpeedMode;
import com.gisgraphy.helper.IntrospectionIgnoredField;
import com.gisgraphy.street.HouseNumberComparator;
import com.gisgraphy.street.StreetSearchMode;
import com.gisgraphy.street.StreetType;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.Point;
/**
* Represents a street in OpenStreetMap. it is different from {@link Street} that represent a street in Geonames.
*
* @author <a href="mailto:david.masclet@gisgraphy.com">David Masclet</a>
*/
@Entity
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@SequenceGenerator(name = "streetosmsequence", sequenceName = "street_osm_sequence")
public class OpenStreetMap {
protected static final Logger logger = LoggerFactory
.getLogger(OpenStreetMap.class);
@Transient
private static final HouseNumberComparator houseNumberComparator = new HouseNumberComparator();
public static final String SHAPE_COLUMN_NAME = "shape";
public static final int MAX_ALTERNATENAME_SIZE = 200;
/**
* Name of the field property in hibernate. This is a string that is used
* for fulltext and contains search without postgres fulltext engine. this
* fields will have the name without accent and special char This value
* should be changed if the getter and the setter of the
* {@link #getTextsearchVector()} change
*
* @see StreetSearchMode#FULLTEXT
*/
public static final String FULLTEXTSEARCH_PROPERTY_NAME = "textSearchName";
/**
* Name of the column that is equals to store a string that is used for
* fulltext search. it deffer form the @{@link #FULLTEXTSEARCH_COLUMN_NAME}
* because Hibernate, by default, lowercase the property to get the column
* name This value should be change if the getter and the setter of the
* {@link #getTextsearchVector()} change
*
* @see StreetSearchMode#FULLTEXT
*/
public static final String FULLTEXTSEARCH_COLUMN_NAME = FULLTEXTSEARCH_PROPERTY_NAME.toLowerCase();
public static final String LOCATION_COLUMN_NAME = "location";
/**
* Needed by CGLib
*/
public OpenStreetMap() {
}
@IntrospectionIgnoredField
private Long id;
private Long gid;
private Long openstreetmapId;
private String name;
private StreetType streetType;
private boolean oneWay = false;
private Point location;
@IntrospectionIgnoredField
private LineString shape;
@IntrospectionIgnoredField
private Integer population;
private String isIn;
private String isInPlace;
private String isInAdm;
private String streetRef;
private GISSource source;
/**
* @return the streetRef
*/
public String getStreetRef() {
return streetRef;
}
/**
* @param streetRef the streetRef to set
*/
public void setStreetRef(String streetRef) {
this.streetRef = streetRef;
}
/**
* This field is only for relevance and allow to search for street<->cities in
* many alternateNames. It is not in stored
*/
@IntrospectionIgnoredField
private Set<String> isInCityAlternateNames;
private String fullyQualifiedName;
private String countryCode;
private Double length;
//@Sort(comparator=HouseNumberComparator.class,type=SortType.COMPARATOR)
private SortedSet<HouseNumber> houseNumbers;
@IntrospectionIgnoredField
private String partialSearchName;
@IntrospectionIgnoredField
private String textSearchName;
private List<AlternateOsmName> alternateNames;
@IntrospectionIgnoredField
private Long cityId;
/**
* if the associated city has been found by shape
* (only for sttistics and relevance purpose
*/
@IntrospectionIgnoredField
private boolean cityConfident = false;
private String adm1Name;
private String adm2Name;
private String adm3Name;
private String adm4Name;
private String adm5Name;
private Integer lanes;
private Boolean toll;
private String surface;
private SpeedMode speedMode;
private String maxSpeed;
private String maxSpeedBackward;
private Integer azimuthStart;
private Integer azimuthEnd;
private String label;
private String labelPostal;
//the zip often one (often from the city), we don't want to have an associated database so we stock this here as transcient field
// and populate the fulltext engine with that.
@IntrospectionIgnoredField
private Set<String> isInZip;
//the zipcode, it is the best choice between all isInZip
private String zipCode;
@IntrospectionIgnoredField
private Set<String> alternateLabels;
/**
* @return the label that represent the Postal address
*/
@Column(length = 500)
public String getLabelPostal() {
return labelPostal;
}
/**
* @param labelPostal the labelPostal to set
*/
public void setLabelPostal(String labelPostal) {
this.labelPostal = labelPostal;
}
/**
* @return the label that represent the street
*/
@Column(length = 500)
public String getLabel() {
return label;
}
/**
* @param label the label to set
*/
public void setLabel(String label) {
this.label = label;
}
/**
* @return the alternate Labels of the streets
*/
@Transient
public Set<String> getAlternateLabels() {
return alternateLabels;
}
/**
* @param alternateLabels the alternate Labels to set
*/
public void setAlternateLabels(Set<String> alternateLabels) {
this.alternateLabels = alternateLabels;
}
/**
* @return the number of lanes
*/
public Integer getLanes() {
return lanes;
}
/**
* @param lanes the number of lanes to set
*/
public void setLanes(Integer lanes) {
this.lanes = lanes;
}
/**
* @return whether the street is toll or free
*/
public Boolean isToll() {
return toll;
}
/**
* @param toll whether the street is toll or free
*/
public void setToll(Boolean toll) {
this.toll = toll;
}
/**
* @return the physical surface of the street
*/
public String getSurface() {
return surface;
}
/**
* @param surface the surface to set
*/
public void setSurface(String surface) {
this.surface = surface;
}
/**
* @return the maxSpeed of the street
*/
public String getMaxSpeed() {
return maxSpeed;
}
/**
* @param maxSpeed the max speed to set
*/
public void setMaxSpeed(String maxSpeed) {
this.maxSpeed = maxSpeed;
}
/**
* @return the max Speed in the backward direction
*/
public String getMaxSpeedBackward() {
return maxSpeedBackward;
}
/**
* @param maxSpeedBackward the max Speed in the Backward to set
*/
public void setMaxSpeedBackward(String maxSpeedBackward) {
this.maxSpeedBackward = maxSpeedBackward;
}
/**
* @return the azimuth at the start of the street
*/
public Integer getAzimuthStart() {
return azimuthStart;
}
/**
* @param azimuthStart the azimuth to set
*/
public void setAzimuthStart(Integer azimuthStart) {
this.azimuthStart = azimuthStart;
}
/**
* @return the azimuth at the end of the street
*/
public Integer getAzimuthEnd() {
return azimuthEnd;
}
/**
* @param azimuthEnd the azimuth to set
*/
public void setAzimuthEnd(Integer azimuthEnd) {
this.azimuthEnd = azimuthEnd;
}
/**
* @return the id of the city, we don't use the object because we don't want to link objects
* for performance reasons
*/
public Long getCityId() {
return cityId;
}
/**
* @param set the id of the city of this street
*/
public void setCityId(Long cityId) {
this.cityId = cityId;
}
/**
* @return A list of the {@link AlternateName}s for this street
*/
@OneToMany(cascade = { CascadeType.ALL }, mappedBy = "street")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Fetch(FetchMode.SELECT)
public List<AlternateOsmName> getAlternateNames() {
return alternateNames;
}
/**
* @param alternateNames
* The {@link AlternateName}s for this street
*/
public void setAlternateNames(List<AlternateOsmName> alternateNames) {
this.alternateNames = alternateNames;
}
/**
* Do a double set : add the alternate name to the current GisFeature and set
* this GisFeature as the GisFeature of the specified AlternateName
*
* @param alternateName
* the alternateName to add
*/
public void addAlternateName(AlternateOsmName alternateName) {
if (alternateName!=null){
if (alternateName.getName() != null && alternateName.getName().length() > MAX_ALTERNATENAME_SIZE){
logger.warn("alternate name "+ alternateName.getName()+" is too long");
} else {
List<AlternateOsmName> currentAlternateNames = getAlternateNames();
if (currentAlternateNames == null) {
currentAlternateNames = new ArrayList<AlternateOsmName>();
}
currentAlternateNames.add(alternateName);
this.setAlternateNames(currentAlternateNames);
alternateName.setStreet(this);
}
}
}
/**
* Do a double set : add (not replace !) the AlternateNames to the current
* GisFeature and for each alternatenames : set the current GisFeature as
* the GisFeature of the Alternate Names
*
* @param alternateNames
* The alternateNames list to add
*/
public void addAlternateNames(List<AlternateOsmName> alternateNames) {
if (alternateNames != null) {
for (AlternateOsmName alternateName : alternateNames) {
addAlternateName(alternateName);
}
}
}
public void addAlternateLabel(String alternateLabel) {
if (alternateLabel!=null){
Set<String> currentLabels = getAlternateLabels();
if (currentLabels == null) {
currentLabels = new HashSet<String>();
}
currentLabels.add(alternateLabel);
this.setAlternateLabels(currentLabels);
}
}
public void addAlternateLabels(Collection<String> alternateLabels) {
if (alternateLabels != null) {
for (String label : alternateLabels) {
addAlternateLabel(label);
}
}
}
/**
* (Experimental) This String is used to search for a part of a street name
*
* @see StreetSearchMode#CONTAINS
* @return the partialSearchName
*/
@Column(unique = false, nullable = true, columnDefinition = "text")
public String getPartialSearchName() {
return partialSearchName;
}
/**
* @param partialSearchName
* the partialSearchName to set
*/
public void setPartialSearchName(String partialSearchName) {
this.partialSearchName = partialSearchName;
}
/**
* This value is use to do a Fulltext search for a street name with index
*
* @return the textSearchName
*/
@Column(unique = false, nullable = true, columnDefinition = "text")
public String getTextSearchName() {
return textSearchName;
}
/**
* @param textSearchName
* the textSearchName to set
*/
public void setTextSearchName(String textSearchName) {
this.textSearchName = textSearchName;
}
/**
* IT DOES NOTHING. ONLY USE BY HIBERNATE This field is only use for the
* text search to improve performance, you should not set / get a value, it
* is declared here, to create the column
*
* @return null ALWAYS
*/
@Column(unique = false, nullable = true, insertable = false, updatable = true, columnDefinition = "tsvector")
@Type(type = "com.gisgraphy.hibernate.type.TsVectorStringType")
public String getTextsearchVector() {
return null;
}
/**
* IT DOES NOTHING. ONLY USE BY HIBERNATE
*
* @param textsearchVector
* the textsearchVector to set
*
*/
public void setTextsearchVector(String textsearchVector) {
}
/**
* IT DOES NOTHING. ONLY USE BY HIBERNATE This field is only use for the
* autocomplete search to improve performance, you should not set / get a
* value, it is declared here, to create the column
*
* @return null ALWAYS
*/
@Column(unique = false, nullable = true, insertable = false, updatable = true, columnDefinition = "tsvector")
@Type(type = "com.gisgraphy.hibernate.type.TsVectorStringType")
public String getPartialsearchVector() {
return null;
}
/**
* IT DOES NOTHING. ONLY USE BY HIBERNATE
*
* @param partialsearchVector
* the ilikesearch to set
*/
public void setPartialsearchVector(String partialsearchVector) {
}
/**
* @return the id
*/
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "streetosmsequence")
public Long getId() {
return id;
}
/**
* @param id
* the id to set
*/
public void setId(Long id) {
this.id = id;
}
/**
* @return an uniqueid that identify the street, it differs from {@link OpenStreetMap#openstreetmapId}
* because the value can not be in conflict between geonames and openstreetmap
*/
@Index(name = "streetosmgidindex")
@Column(unique = true, nullable = false)
public Long getGid() {
return gid;
}
/**
* @param gid
* the gid to set
*/
public void setGid(Long gid) {
this.gid = gid;
}
/**
* @return the openstreetmap internal id
*/
@Index(name = "streetosmopenstreetmapidindex")
@Column(unique = false, nullable = true)
public Long getOpenstreetmapId() {
return openstreetmapId;
}
/**
* @param openstreetmapId the openstreetmap internal id
*/
public void setOpenstreetmapId(Long openstreetmapId) {
this.openstreetmapId = openstreetmapId;
}
/**
* @return the name
*/
@Column(length = 255)
public String getName() {
return name;
}
/**
* @param name
* the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the type of the street
*/
@Index(name = "streetosmtypeIndex")
@Enumerated(EnumType.STRING)
public StreetType getStreetType() {
return streetType;
}
/**
* @param streetType
* the streetType to set
*/
public void setStreetType(StreetType streetType) {
this.streetType = streetType;
}
/**
* @return the speedMode
*/
@Enumerated(EnumType.STRING)
public SpeedMode getSpeedMode() {
return speedMode;
}
/**
* @param speedMode the speedMode to set
*/
public void setSpeedMode(SpeedMode speedMode) {
this.speedMode = speedMode;
}
/**
* @return the oneway
*/
@Index(name = "streetosmonewayIndex")
@Column(length = 9)
public boolean isOneWay() {
return oneWay;
}
/**
* @param oneWay
* the oneWay to set
*/
public void setOneWay(boolean oneWay) {
this.oneWay = oneWay;
}
/**
* Returns The JTS location point of the current street : The Geometry
* representation for the latitude, longitude. The Return type is a JTS
* point. The Location is calculate from the 4326 {@link SRID}
*
* @see SRID
* @return The JTS Point
*/
@Type(type = "org.hibernatespatial.GeometryUserType")
@Column(name = OpenStreetMap.LOCATION_COLUMN_NAME)
public Point getLocation() {
return location;
}
/**
* @return Returns the latitude (north-south) from the Location
* {@link #getLocation()}.
* @see #getLongitude()
* @see #getLocation()
*/
@Transient
public Double getLatitude() {
Double latitude = null;
if (this.location != null) {
latitude = this.location.getY();
}
return latitude;
}
/**
* @return Returns the longitude (east-west) from the Location
* {@link #getLocation()}.
* @see #getLongitude()
* @see #getLocation()
*/
@Transient
public Double getLongitude() {
Double longitude = null;
if (this.location != null) {
longitude = this.location.getX();
}
return longitude;
}
/**
* @param location
* the location to set
*/
public void setLocation(Point location) {
this.location = location;
}
/**
* @return the shape
*/
@Type(type = "org.hibernatespatial.GeometryUserType")
@Column(nullable = false)
public LineString getShape() {
return shape;
}
/**
* @param shape
* the shape to set
*/
public void setShape(LineString shape) {
this.shape = shape;
}
/**
* @return The ISO 3166 alpha-2 letter code.
*/
@Index(name = "openstreetmapcountryindex")
@Column(length = 3)
public String getCountryCode() {
return countryCode;
}
/**
* @param countryCode
* the countryCode to set
*/
public void setCountryCode(String countryCode) {
this.countryCode = countryCode;
}
/**
* @return the length of the street in meters
*/
public Double getLength() {
return length;
}
/**
* @param length
* the length to set
*/
public void setLength(Double length) {
this.length = length;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
//TODO use business key
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
OpenStreetMap other = (OpenStreetMap) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
return true;
}
/**
* @return The city or state or any information where the street is located
*/
public String getIsIn() {
return isIn;
}
/**
* @param isIn
* The city or state or any information where the street is
* located
*/
public void setIsIn(String isIn) {
this.isIn = isIn;
}
/**
* @return the place where the street is located,
* this field is filled when {@link OpenStreetMap#isIn}
* is filled and we got more specific details (generally quarter, neighborhood)
*/
public String getIsInPlace() {
return isInPlace;
}
/**
* @param isInPlace the most precise information on where the street is located,
* generally quarter neighborhood
*/
public void setIsInPlace(String isInPlace) {
this.isInPlace = isInPlace;
}
/**
* @return the adm (aka administrative division) where the street is located.
*/
public String getIsInAdm() {
return isInAdm;
}
/**
* @param isInAdm the adm (aka administrative division) where the street is located
*/
public void setIsInAdm(String isInAdm) {
this.isInAdm = isInAdm;
}
/**
* @return the zipcode where the street is located
*/
//don't want to store it, just for fulltext purpose
@Transient
public Set<String> getIsInZip() {
return isInZip;
}
/**
* @param isInZip the zipcode where the street is located.
*/
public void setIsInZip(Set<String> isInZip) {
this.isInZip = isInZip;
}
/**
* add a zip
*/
public void addIsInZip(String zip) {
Set<String> currentZips = getIsInZip();
if (currentZips == null) {
currentZips = new HashSet<String>();
}
if (zip != null && zip.length()>=3){
//there is some error in zip code and we avoid sip if it is less than 2 character
currentZips.add(zip);
}
this.setIsInZip(currentZips);
}
/**
* add zips
*/
public void addIsInZips(Collection<String> zips) {
if (zips != null) {
for (String zip : zips) {
addIsInZip(zip);
}
}
}
@Column(length = 500)
public String getFullyQualifiedName() {
return fullyQualifiedName;
}
public void setFullyQualifiedName(String fullyQualifiedName) {
this.fullyQualifiedName = fullyQualifiedName;
}
public Integer getPopulation() {
return population;
}
public void setPopulation(Integer population) {
this.population = population;
}
public String getAdm1Name() {
return adm1Name;
}
public void setAdm1Name(String adm1Name) {
this.adm1Name = adm1Name;
}
public String getAdm2Name() {
return adm2Name;
}
public void setAdm2Name(String adm2Name) {
this.adm2Name = adm2Name;
}
public String getAdm3Name() {
return adm3Name;
}
public void setAdm3Name(String adm3Name) {
this.adm3Name = adm3Name;
}
public String getAdm4Name() {
return adm4Name;
}
public void setAdm4Name(String adm4Name) {
this.adm4Name = adm4Name;
}
public String getAdm5Name() {
return adm5Name;
}
public void setAdm5Name(String adm5Name) {
this.adm5Name = adm5Name;
}
/**
* @return the zipCode
*/
public String getZipCode() {
return zipCode;
}
/**
* @param zipCode the zipCode to set
*/
public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}
/**
* @return the houseNumbers associated to that street
*/
@OneToMany(cascade = { CascadeType.ALL }, mappedBy = "street")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Fetch(FetchMode.SELECT)
@Sort(comparator=HouseNumberComparator.class,type=SortType.COMPARATOR)
public SortedSet<HouseNumber> getHouseNumbers() {
return houseNumbers;
}
/**
* @param houseNumbers the houseNumbers to set
*/
public void setHouseNumbers(SortedSet<HouseNumber> houseNumbers) {
this.houseNumbers = houseNumbers;
}
/**
* Do a double set : add the house number to the current street and set
* this street as the street of the specified AlternateName
*
* @param houseNumber
* the houseNumber to add
*/
public void addHouseNumber(HouseNumber houseNumber) {
if (houseNumber!=null){
SortedSet<HouseNumber> currentHouseNumbers = getHouseNumbers();
if (currentHouseNumbers == null) {
currentHouseNumbers = new TreeSet<HouseNumber>(houseNumberComparator);
}
currentHouseNumbers.add(houseNumber);
this.setHouseNumbers(currentHouseNumbers);
houseNumber.setStreet(this);
}
}
/**
* Do a double set : add (not replace !) the House Numbers to the current
* street and for each House numbers : set the current street as
* the street of the House Numbers
*
* @param HouseNumbers
* The House Numbers list to add
*/
public void addHouseNumbers(List<HouseNumber> HouseNumbers) {
if (HouseNumbers != null) {
for (HouseNumber houseNumber : HouseNumbers) {
addHouseNumber(houseNumber);
}
}
}
/**
* @return the source
*/
@Enumerated(EnumType.STRING)
public GISSource getSource() {
return source;
}
/**
* @param source the source to set
*/
public void setSource(GISSource source) {
this.source = source;
}
public boolean isCityConfident() {
return cityConfident;
}
public void setCityConfident(boolean cityConfident) {
this.cityConfident = cityConfident;
}
/**
* This field is only for relevance and allow to search for street<->cities in
* many alternateNames. It is not in stored
*
*/
@Transient
public Set<String> getIsInCityAlternateNames() {
return isInCityAlternateNames;
}
public void setIsInCityAlternateNames(Set<String> isInCityAlternateNames) {
this.isInCityAlternateNames = isInCityAlternateNames;
}
public void addIsInCitiesAlternateName(String isInCityAlternateName) {
if (isInCityAlternateName!=null){
Set<String> currentCitiesAlternateNames = getIsInCityAlternateNames();
if (currentCitiesAlternateNames == null) {
currentCitiesAlternateNames = new HashSet<String>();
}
currentCitiesAlternateNames.add(isInCityAlternateName);
this.setIsInCityAlternateNames(currentCitiesAlternateNames);
}
}
public void addIsInCitiesAlternateNames(Collection<String> isInCityAlternateNames) {
if (isInCityAlternateNames != null) {
for (String isInCityAlternateName : isInCityAlternateNames) {
addIsInCitiesAlternateName(isInCityAlternateName);
}
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("OpenStreetMap [");
if (openstreetmapId != null)
builder.append("openstreetmapId=").append(openstreetmapId)
.append(", ");
if (name != null)
builder.append("name=").append(name).append(", ");
if (location != null)
builder.append("location=").append(location);
builder.append("]");
return builder.toString();
}
}