/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2010, Geomatys
*
* 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;
* version 2.1 of the License.
*
* 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.
*/
package org.geotoolkit.data.osm.xml;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;
import java.util.List;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.xml.stream.XMLStreamException;
import org.geotoolkit.data.osm.model.Api;
import org.geotoolkit.data.osm.model.Bound;
import org.geotoolkit.data.osm.model.ChangeSet;
import org.geotoolkit.data.osm.model.GPXFileMetadata;
import org.geotoolkit.data.osm.model.MemberType;
import org.geotoolkit.data.osm.model.Transaction;
import org.geotoolkit.data.osm.model.TransactionType;
import org.geotoolkit.temporal.object.ISODateParser;
import org.geotoolkit.temporal.object.TemporalUtilities;
import org.geotoolkit.xml.StaxStreamReader;
import org.opengis.geometry.Envelope;
import static javax.xml.stream.XMLStreamReader.*;
import org.geotoolkit.data.osm.model.OSMModelConstants;
import static org.geotoolkit.data.osm.xml.OSMXMLConstants.*;
import org.opengis.feature.Feature;
/**
* Stax reader class for OSM XML planet files.
*
* @author Johann Sorel (Geomatys)
* @module
*/
public class OSMXMLReader extends StaxStreamReader{
private final GeometryFactory GF = new GeometryFactory();
private final ISODateParser dateParser = new ISODateParser();
private Envelope envelope;
/**
* Caches.
*/
private final List<Feature> tags = new ArrayList<>();
private final List<Feature> members = new ArrayList<>();
private final List<Long> nodes = new ArrayList<>();
private final List<Feature> transaction = new ArrayList<>();
private long id = Long.MIN_VALUE;
private int version = Integer.MIN_VALUE;
private int changeset = Integer.MIN_VALUE;
private String user = null;
private int uid = OSMModelConstants.USER_ID_NONE;
private long timestamp = Long.MIN_VALUE;
private Object current;
private long moveToId = -1;
@Override
public void setInput(final Object input) throws IOException, XMLStreamException {
super.setInput(input);
//search for the bound tag to generate the envelope
searchLoop :
while(reader.hasNext()){
final int type = reader.next();
switch (type) {
// Si c'est un début d'elément, on garde son type
case START_ELEMENT:
final String typeName = reader.getLocalName();
if(TAG_BOUNDS.equalsIgnoreCase(typeName)){
envelope = parseBound();
break searchLoop;
}else if( TAG_NODE.equalsIgnoreCase(typeName)
|| TAG_WAY.equalsIgnoreCase(typeName)
|| TAG_REL.equalsIgnoreCase(typeName)
|| TAG_MODIFY.equalsIgnoreCase(typeName)
|| TAG_CREATE.equalsIgnoreCase(typeName)
|| TAG_DELETE.equalsIgnoreCase(typeName)
|| TAG_CHANGESET.equalsIgnoreCase(typeName)
|| TAG_GPX.equalsIgnoreCase(typeName)
|| TAG_API.equalsIgnoreCase(typeName)){
//there is no bounds tag
break searchLoop;
}
}
}
}
public Envelope getEnvelope() {
return envelope;
}
/**
* Iterate in the file until we reach a entity with an id
* that match the given one.
*
* @param id : identifier to move to
*/
public void moveTo(final Long id) throws XMLStreamException{
moveToId = id;
read();
//we have reached the wanted item, reset the search id
moveToId = -1;
}
public boolean hasNext() throws XMLStreamException{
read();
return current != null;
}
/**
*
* @return IdentifiedElement (Way,node,Relation), Transaction (in case of daily update files)
* or GPXFileMetadata
* @throws XMLStreamException
*/
public Object next() throws XMLStreamException{
read();
final Object ele = current;
current = null;
return ele;
}
private void resetCache(){
members.clear();
tags.clear();
nodes.clear();
id = Long.MIN_VALUE;
version = Integer.MIN_VALUE;
changeset = Integer.MIN_VALUE;
user = null;
uid = OSMModelConstants.USER_ID_NONE;
timestamp = Long.MIN_VALUE;
}
private void read() throws XMLStreamException{
if(current != null) return;
boolean first = true;
while ( first || (current == null && reader.hasNext()) ) {
final int type;
if(first){
type = reader.getEventType();
first = false;
}else{
type = reader.next();
}
if(type == START_ELEMENT) {
final String localName = reader.getLocalName();
if(localName.equalsIgnoreCase(TAG_NODE)){
current = parseNode();
}else if(localName.equalsIgnoreCase(TAG_WAY)){
current = parseWay();
}else if(localName.equalsIgnoreCase(TAG_REL)){
current = parseRelation();
}else if(localName.equalsIgnoreCase(TAG_CREATE)){
current = parseTransaction(TransactionType.CREATE);
}else if(localName.equalsIgnoreCase(TAG_MODIFY)){
current = parseTransaction(TransactionType.MODIFY);
}else if(localName.equalsIgnoreCase(TAG_DELETE)){
current = parseTransaction(TransactionType.DELETE);
}else if(localName.equalsIgnoreCase(TAG_CHANGESET)){
current = parseChangeSet();
}else if(localName.equalsIgnoreCase(TAG_API)){
current = parseAPI();
}else if(localName.equalsIgnoreCase(TAG_GPX)){
current = parseGPX();
}else{
System.out.println("Unexpected tag : " + localName);
}
}
}
}
private Long toDateLong(final String str){
return dateParser.parseToMillis(str);
}
private Envelope parseBound() throws XMLStreamException {
final String xmin = reader.getAttributeValue(null, ATT_BOUNDS_MINLON);
final String xmax = reader.getAttributeValue(null, ATT_BOUNDS_MAXLON);
final String ymin = reader.getAttributeValue(null, ATT_BOUNDS_MINLAT);
final String ymax = reader.getAttributeValue(null, ATT_BOUNDS_MAXLAT);
if(xmin == null || xmax == null || ymin == null || ymax == null){
throw new XMLStreamException("Error in xml file, osm bounds not defined correctly");
}
toTagEnd(TAG_BOUNDS);
return Bound.create(
Double.parseDouble(xmin),
Double.parseDouble(xmax),
Double.parseDouble(ymin),
Double.parseDouble(ymax));
}
/**
* Parse attributs
* @param reader
* @return true if this entity Id matches the one searched (if there is one).
* false otherwise.
*
* @throws XMLStreamException
*/
private boolean parseIdentifiedAttributs() throws XMLStreamException{
final String strChangeset = reader.getAttributeValue(null, ATT_CHANGESET);
final String strID = reader.getAttributeValue(null, ATT_ID);
final String strTimestamp = reader.getAttributeValue(null, ATT_TIMESTAMP);
final String strUID = reader.getAttributeValue(null, ATT_UID);
user = reader.getAttributeValue(null, ATT_USER);
final String strVersion = reader.getAttributeValue(null, ATT_VERSION);
id = Long.parseLong(strID);
//check if we are in search mode
if(moveToId > 0 && id != moveToId) return false;
if(id == Long.MIN_VALUE) throw new XMLStreamException("Error in xml file, entity with no id");
changeset = Integer.parseInt(strChangeset);
if(changeset == Integer.MIN_VALUE) throw new XMLStreamException("Error in xml file, change set is null");
timestamp = toDateLong(strTimestamp);
if(timestamp == Long.MIN_VALUE) throw new XMLStreamException("Error in xml file, timestamp is null");
if(strUID != null){
uid = Integer.parseInt(strUID);
}
version = Integer.parseInt(strVersion);
if(version == Integer.MIN_VALUE) throw new XMLStreamException("Error in xml file, version is null");
return true;
}
private Feature parseNode() throws XMLStreamException {
resetCache();
if(!parseIdentifiedAttributs()){
//we dont want this entity
toTagEnd(TAG_NODE);
return null;
}
String lat = reader.getAttributeValue(null, ATT_NODE_LAT);
String lon = reader.getAttributeValue(null, ATT_NODE_LON);
if(lat == null || lon == null){
//throw new XMLStreamException("Error in xml file, osm node lat/lon not defined correctly");
//TODO : recheck the evolution of the specification
//TODO : new attribute visible
//TODO : http://wiki.openstreetmap.org/wiki/API_v0.6/XSD
lat = "NaN";
lon = "NaN";
}
while (reader.hasNext()) {
final int type = reader.next();
switch (type) {
case START_ELEMENT:
if(TAG_TAG.equalsIgnoreCase(reader.getLocalName())){
parseTag(tags);
}
break;
case END_ELEMENT:
if(TAG_NODE.equalsIgnoreCase(reader.getLocalName())){
//end of the node element
final Feature node = OSMModelConstants.TYPE_NODE.newInstance();
final Point pt = GF.createPoint(new Coordinate(Double.parseDouble(lon), Double.parseDouble(lat)));
node.setPropertyValue("point", pt);
if (user!=null || uid != OSMModelConstants.USER_ID_NONE) {
final Feature u = OSMModelConstants.TYPE_USER.newInstance();
u.setPropertyValue(ATT_UID, uid);
u.setPropertyValue(ATT_USER, user);
node.setPropertyValue("user", u);
}
node.setPropertyValue(ATT_ID, id);
node.setPropertyValue(ATT_VERSION, version);
node.setPropertyValue(ATT_CHANGESET, changeset);
node.setPropertyValue(ATT_TIMESTAMP, timestamp);
node.setPropertyValue("tags", tags);
return node;
}
break;
}
}
throw new XMLStreamException("Error in xml file, node tag without end.");
}
private Feature parseWay() throws XMLStreamException {
resetCache();
if(!parseIdentifiedAttributs()){
//we dont want this entity
toTagEnd(TAG_WAY);
return null;
}
while (reader.hasNext()) {
final int type = reader.next();
switch (type) {
case START_ELEMENT:
final String localName = reader.getLocalName();
if(TAG_TAG.equalsIgnoreCase(localName)){
parseTag(tags);
}else if(TAG_WAYND.equalsIgnoreCase(localName)){
nodes.add(parseWayNode());
}
break;
case END_ELEMENT:
if(TAG_WAY.equalsIgnoreCase(reader.getLocalName())){
//end of the way element
final Feature way = OSMModelConstants.TYPE_WAY.newInstance();
if (user!=null || uid != OSMModelConstants.USER_ID_NONE) {
final Feature u = OSMModelConstants.TYPE_USER.newInstance();
u.setPropertyValue(ATT_UID, uid);
u.setPropertyValue(ATT_USER, user);
way.setPropertyValue("user", u);
}
way.setPropertyValue(ATT_ID, id);
way.setPropertyValue(ATT_VERSION, version);
way.setPropertyValue(ATT_CHANGESET, changeset);
way.setPropertyValue(ATT_TIMESTAMP, timestamp);
way.setPropertyValue("tags", tags);
way.setPropertyValue(TAG_WAYND, nodes);
return way;
}
break;
}
}
throw new XMLStreamException("Error in xml file, way tag without end.");
}
private Feature parseRelation() throws XMLStreamException {
resetCache();
if(!parseIdentifiedAttributs()){
//we dont want this entity
toTagEnd(TAG_REL);
return null;
}
while (reader.hasNext()) {
final int type = reader.next();
switch (type) {
case START_ELEMENT:
final String localName = reader.getLocalName();
if(TAG_TAG.equalsIgnoreCase(localName)){
parseTag(tags);
}else if(TAG_RELMB.equalsIgnoreCase(localName)){
members.add(parseRelationMember());
}
break;
case END_ELEMENT:
if(TAG_REL.equalsIgnoreCase(reader.getLocalName())){
//end of the relation element
final Feature relation = OSMModelConstants.TYPE_RELATION.newInstance();
if (user!=null || uid != OSMModelConstants.USER_ID_NONE) {
final Feature u = OSMModelConstants.TYPE_USER.newInstance();
u.setPropertyValue(ATT_UID, uid);
u.setPropertyValue(ATT_USER, user);
relation.setPropertyValue("user", u);
}
relation.setPropertyValue(ATT_ID, id);
relation.setPropertyValue(ATT_VERSION, version);
relation.setPropertyValue(ATT_CHANGESET, changeset);
relation.setPropertyValue(ATT_TIMESTAMP, timestamp);
relation.setPropertyValue("tags", tags);
relation.setPropertyValue("members", members);
return relation;
}
break;
}
}
throw new XMLStreamException("Error in xml file, relation tag without end.");
}
private ChangeSet parseChangeSet() throws XMLStreamException {
resetCache();
final String strID = reader.getAttributeValue(null, ATT_ID);
final String strUser = reader.getAttributeValue(null, ATT_USER);
final String strUID = reader.getAttributeValue(null, ATT_UID);
final String strTime = reader.getAttributeValue(null, ATT_CHANGESET_CREATEDAT);
final String strOpen = reader.getAttributeValue(null, ATT_CHANGESET_OPEN);
final String strMinLon = reader.getAttributeValue(null, ATT_CHANGESET_MINLON);
final String strMinLat = reader.getAttributeValue(null, ATT_CHANGESET_MINLAT);
final String strMaxLon = reader.getAttributeValue(null, ATT_CHANGESET_MAXLON);
final String strMaxLat = reader.getAttributeValue(null, ATT_CHANGESET_MAXLAT);
final Envelope env;
if(strMinLon != null && strMinLat != null && strMaxLat != null && strMaxLon != null){
env = Bound.create(
Double.parseDouble(strMinLon),
Double.parseDouble(strMaxLon),
Double.parseDouble(strMinLat),
Double.parseDouble(strMaxLat));
}else{
env = null;
}
while (reader.hasNext()) {
final int type = reader.next();
switch (type) {
case START_ELEMENT:
final String localName = reader.getLocalName();
if(TAG_TAG.equalsIgnoreCase(localName)){
parseTag(tags);
}
break;
case END_ELEMENT:
if(TAG_CHANGESET.equalsIgnoreCase(reader.getLocalName())){
//end of the changeset element
Feature user = null;
if(strUID!=null){
user = OSMModelConstants.TYPE_USER.newInstance();
user.setPropertyValue(ATT_UID, Integer.parseInt(strUID));
user.setPropertyValue(ATT_USER, strUser);
}
return new ChangeSet(
(strID!=null) ? Integer.parseInt(strID) : null,
user,
(strTime!=null) ? toDateLong(strTime) : null,
(strOpen!=null) ? Boolean.valueOf(strOpen) : null,
env,
tags);
}
break;
}
}
throw new XMLStreamException("Error in xml file, chageset tag without end.");
}
private Transaction parseTransaction(final TransactionType tt) throws XMLStreamException {
transaction.clear();
final String version = reader.getAttributeValue(null, ATT_VERSION);
final String generator = reader.getAttributeValue(null, ATT_GENERATOR);
while (reader.hasNext()) {
final int type = reader.next();
switch (type) {
case START_ELEMENT:
final String localName = reader.getLocalName();
if(TAG_NODE.equalsIgnoreCase(localName)){
transaction.add(parseNode());
}else if(TAG_WAY.equalsIgnoreCase(localName)){
transaction.add(parseWay());
}else if(TAG_REL.equalsIgnoreCase(localName)){
transaction.add(parseRelation());
}
break;
case END_ELEMENT:
if(reader.getLocalName().equalsIgnoreCase(tt.getTagName())){
//end of the transaction element
return new Transaction(tt,transaction,version,generator);
}
break;
}
}
throw new XMLStreamException("Error in xml file, modify tag without end.");
}
private Api parseAPI() throws XMLStreamException {
String versionMinimum = null;
String versionMaximum = null;
String areaMaximum = null;
String tracePointsPerPage = null;
String wayNodeMaximum = null;
String changesetMaximum = null;
String timeout = null;
while (reader.hasNext()) {
final int type = reader.next();
switch (type) {
case START_ELEMENT:
final String localName = reader.getLocalName();
if(localName.equalsIgnoreCase(TAG_API_AREA)){
areaMaximum = reader.getAttributeValue(null, ATT_API_MAXIMUM);
}else if(localName.equalsIgnoreCase(TAG_API_CHANGESETS)){
changesetMaximum = reader.getAttributeValue(null, ATT_API_MAXIMUM_ELEMENTS);
}else if(localName.equalsIgnoreCase(TAG_API_TIMEOUT)){
timeout = reader.getAttributeValue(null, ATT_API_SECONDS);
}else if(localName.equalsIgnoreCase(TAG_API_TRACEPOINTS)){
tracePointsPerPage = reader.getAttributeValue(null, ATT_API_PER_PAGE);
}else if(localName.equalsIgnoreCase(TAG_API_VERSION)){
versionMinimum = reader.getAttributeValue(null, ATT_API_MINIMUM);
versionMaximum = reader.getAttributeValue(null, ATT_API_MAXIMUM);
}else if(localName.equalsIgnoreCase(TAG_API_WAYNODES)){
wayNodeMaximum = reader.getAttributeValue(null, ATT_API_MAXIMUM);
}
break;
case END_ELEMENT:
if(reader.getLocalName().equalsIgnoreCase(TAG_API)){
if(versionMinimum == null){
throw new XMLStreamException("Invalid Api element, missing parameter : versionMinimum");
}
if(versionMaximum == null ){
throw new XMLStreamException("Invalid Api element, missing parameter : versionMaximum");
}
if(areaMaximum == null){
throw new XMLStreamException("Invalid Api element, missing parameter : areaMaximum");
}
if(tracePointsPerPage == null){
throw new XMLStreamException("Invalid Api element, missing parameter : tracePointsPerPage");
}
if(wayNodeMaximum == null){
throw new XMLStreamException("Invalid Api element, missing parameter : wayNodeMaximum");
}
if(changesetMaximum == null){
throw new XMLStreamException("Invalid Api element, missing parameter : changesetMaximum");
}
if(timeout == null){
throw new XMLStreamException("Invalid Api element, missing parameter : timeout");
}
//end of the api element
return new Api(
versionMinimum,
versionMaximum,
Double.parseDouble(areaMaximum),
Integer.parseInt(tracePointsPerPage),
Integer.parseInt(wayNodeMaximum),
Integer.parseInt(changesetMaximum),
Integer.parseInt(timeout));
}
break;
}
}
throw new XMLStreamException("Error in xml file, modify tag without end.");
}
private GPXFileMetadata parseGPX() throws XMLStreamException{
final String id = reader.getAttributeValue(null, ATT_ID);
final String name = reader.getAttributeValue(null, ATT_GPX_NAME);
final String lat = reader.getAttributeValue(null, ATT_GPX_LAT);
final String lon = reader.getAttributeValue(null, ATT_GPX_LON);
final String user = reader.getAttributeValue(null, ATT_USER);
final String time = reader.getAttributeValue(null, ATT_TIMESTAMP);
final String publik = reader.getAttributeValue(null, ATT_GPX_PUBLIC);
final String pending = reader.getAttributeValue(null, ATT_GPX_PENDING);
toTagEnd(TAG_GPX);
return new GPXFileMetadata(Long.parseLong(id),name, user,
Boolean.parseBoolean(publik), Boolean.parseBoolean(pending),
TemporalUtilities.parseDateSafe(time,false),
Double.parseDouble(lat),
Double.parseDouble(lon));
}
private void parseTag(final List<Feature> tags) throws XMLStreamException{
final String key = reader.getAttributeValue(null, ATT_TAG_KEY);
final String value = reader.getAttributeValue(null, ATT_TAG_VALUE);
if(key == null || value == null){
throw new XMLStreamException("Error in xml file, tag has no proper key value pair.");
}
toTagEnd(TAG_TAG);
final Feature tag = OSMModelConstants.TYPE_TAG.newInstance();
tag.setPropertyValue("k", key);
tag.setPropertyValue("v", value);
tags.add(tag);
}
private Long parseWayNode() throws XMLStreamException{
final String ref = reader.getAttributeValue(null, ATT_WAYND_REF);
if(ref == null){
throw new XMLStreamException("Error in xml file, way node has no reference attribut.");
}
toTagEnd(TAG_WAYND);
return Long.parseLong(ref);
}
private Feature parseRelationMember() throws XMLStreamException{
final String ref = reader.getAttributeValue(null, ATT_RELMB_REF);
final String role = reader.getAttributeValue(null, ATT_RELMB_ROLE);
final String type = reader.getAttributeValue(null, ATT_RELMB_TYPE);
if(ref == null){
throw new XMLStreamException("Error in xml file, relation member node has no reference attribut.");
}
toTagEnd(TAG_RELMB);
final Feature member = OSMModelConstants.TYPE_RELATION_MEMBER.newInstance();
member.setPropertyValue("ref", Long.parseLong(ref));
member.setPropertyValue("role", role);
member.setPropertyValue("type", MemberType.valueOfIgnoreCase(type));
return member;
}
}