605 lines
22 KiB
Java
605 lines
22 KiB
Java
package pl.wat.ms4ds.terrain.konwersja;
|
|
|
|
import pl.wat.ms4ds.common.EGeoDirection;
|
|
import pl.wat.ms4ds.terrain.*;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
/**
|
|
* Bazowa klasa dla obiektów OpenStreetMap.
|
|
*/
|
|
public class Way {
|
|
private static final Logger logger = LoggerFactory.getLogger(Way.class);
|
|
|
|
public String id;
|
|
|
|
ArrayList<Node> nodes = new ArrayList<Node>();
|
|
|
|
/**
|
|
* k="highway" - v="motorway" | v="trunk" | v="primary" | v="secondary" | v="tertiary" | v="unclassified"
|
|
* | v="residential" | v="pedestrian" | v="track"
|
|
* | v="motorway_link" | v="trunk_link" | v="primary_link" | v="secondary_link" | v="tertiary_link"
|
|
*/
|
|
public EOSMHighway highway;
|
|
|
|
/**
|
|
* Tag oznacza obiekt obszarowy. <p>
|
|
* k="natural" - v="water" | v="reservoir" | v="wetland" | v="wood"
|
|
*/
|
|
public EOSMNatural natural;
|
|
|
|
/**
|
|
* Tag ten oznacza rzekę lub ciek wodny opisywany liniowo.
|
|
* <p> Wyjątek: waterway=riverbank, który opisuje obszar.
|
|
* <p> k="waterway" - v="river" | v="riverbank" | v="stream" | v="canal" | v="drain" | v="ditch"
|
|
*/
|
|
public EOSMWaterway waterway;
|
|
|
|
/**
|
|
* Tag stosowany w połączeniu z tagiem obszarowym: natural=water. <p>
|
|
* k="water" - v="lake" | v="reservoir" | v="river" | v="canal" | v="cove" | v="lagoon" | v="pond"
|
|
*/
|
|
public EOSMWater water;
|
|
|
|
/**
|
|
* Tag oznacza obiekt obszarowy. <p>
|
|
* k="landuse" - v="forest" | v="residential" | v="commercial"
|
|
*/
|
|
public EOSMLanduse landuse;
|
|
|
|
/**
|
|
* k="amenity" - v="hospital" | v="school" | v="post_office" | v="police" | v="marketplace" | v="fire_station" |
|
|
* v="theatre" | v="cinema" | v="pharmacy" | v="nursing_home" | v="bank" | v="fuel"
|
|
* v="university" | v="library"
|
|
*/
|
|
public EOSMAmenity amenity;
|
|
|
|
/**
|
|
* k="building" - v="yes" | v="chapel" | v="church" | v="house" | v="office" | v="manufacture" | v="industrial"
|
|
*/
|
|
public EOSMBuilding building;
|
|
|
|
/**
|
|
* k="bridge" - v="yes" | "viaduct"
|
|
*/
|
|
public EOSMBridge bridge;
|
|
|
|
/**
|
|
* Tag oznacza obiekt obszarowy. <p>
|
|
* k="landcover" - v="trees" | v="water" | v="grass"
|
|
*/
|
|
public EOSMLandcover landcover;
|
|
|
|
public Way(String id) {
|
|
this.id = id;
|
|
}
|
|
|
|
public Way(Way oryg) {
|
|
this.id = oryg.id;
|
|
}
|
|
|
|
public void copyTags(Way oryg) {
|
|
this.highway = oryg.highway;
|
|
this.natural = oryg.natural;
|
|
this.waterway = oryg.waterway;
|
|
this.water = oryg.water;
|
|
this.landuse = oryg.landuse;
|
|
this.amenity = oryg.amenity;
|
|
this.building = oryg.building;
|
|
this.bridge = oryg.bridge;
|
|
this.landcover = oryg.landcover;
|
|
}
|
|
|
|
public void copyTags(Relation oryg) {
|
|
this.natural = oryg.natural;
|
|
this.waterway = oryg.waterway;
|
|
this.water = oryg.water;
|
|
this.landuse = oryg.landuse;
|
|
this.landcover = oryg.landcover;
|
|
}
|
|
|
|
public boolean tagged() {
|
|
if (landuse != null || natural != null || landcover != null
|
|
|| highway != null || waterway != null
|
|
|| building != null || amenity != null || bridge != null) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void removeSimilarNodes() {
|
|
if (nodes.size() == 0) {
|
|
return;
|
|
}
|
|
Node node_i;
|
|
Node node_j;
|
|
Node similarToNode[] = new Node[nodes.size()];
|
|
boolean similarFound = false;
|
|
for (int i = 0; i < nodes.size() - 1; i++) {
|
|
node_i = nodes.get(i);
|
|
if (similarToNode[i] != null) {
|
|
continue;
|
|
}
|
|
for (int j = i + 1; j < nodes.size(); j++) {
|
|
node_j = nodes.get(j);
|
|
if (node_i.idX == node_j.idX && node_i.idY == node_j.idY) {
|
|
similarToNode[j] = node_i;
|
|
node_i.buildingsCount += node_j.buildingsCount;
|
|
similarFound = true;
|
|
}
|
|
}
|
|
}
|
|
if (similarFound) {
|
|
ArrayList<Node> newList = new ArrayList<>();
|
|
for (int i = 0; i < nodes.size(); i++) {
|
|
node_i = nodes.get(i);
|
|
if (similarToNode[i] == null) {
|
|
newList.add(node_i);
|
|
}
|
|
}
|
|
nodes = newList;
|
|
removeSimilarNodes();
|
|
}
|
|
}
|
|
|
|
void removeCollinearNodes(boolean isPolygon) {
|
|
if (nodes.size() < 3) {
|
|
return;
|
|
}
|
|
boolean toDelete[] = new boolean[nodes.size()];
|
|
Node node_i;
|
|
Node node_i_next;
|
|
Node node_i_prev;
|
|
boolean collinearFound = false;
|
|
int dlMax = nodes.size();
|
|
int iStart = 0;
|
|
if (!isPolygon) {
|
|
// łamana otwarta
|
|
dlMax = nodes.size() - 1;
|
|
iStart = 1;
|
|
}
|
|
for (int i = iStart; i < dlMax; i++) {
|
|
int i_plus_1 = (i + 1) % nodes.size();
|
|
int i_minus_1 = (i + nodes.size() - 1) % nodes.size();
|
|
node_i = nodes.get(i);
|
|
node_i_next = nodes.get(i_plus_1);
|
|
node_i_prev = nodes.get(i_minus_1);
|
|
if (GeomUtils.include(node_i_prev.idX, node_i_prev.idY, node_i_next.idX, node_i_next.idY,
|
|
node_i.idX, node_i.idY)) {
|
|
// i-ty do usuniecia
|
|
toDelete[i] = true;
|
|
collinearFound = true;
|
|
} else if (GeomUtils.include(node_i.idX, node_i.idY, node_i_next.idX, node_i_next.idY,
|
|
node_i_prev.idX, node_i_prev.idY)) {
|
|
// i-1-ty do usuniecia
|
|
toDelete[i_minus_1] = true;
|
|
collinearFound = true;
|
|
} else if (GeomUtils.include(node_i_prev.idX, node_i_prev.idY, node_i.idX, node_i.idY,
|
|
node_i_next.idX, node_i_next.idY)) {
|
|
// i+1-ty do usuniecia
|
|
toDelete[i_plus_1] = true;
|
|
collinearFound = true;
|
|
}
|
|
}
|
|
if (collinearFound) {
|
|
ArrayList<Node> newList = new ArrayList<Node>();
|
|
for (int i = 0; i < nodes.size(); i++) {
|
|
node_i = nodes.get(i);
|
|
if (!toDelete[i]) {
|
|
newList.add(node_i);
|
|
}
|
|
}
|
|
// logger.trace("Liczba oryg. wezlow= {}, liczba niewspolliniowych wezlow= {}, roznica= {}", nodes.size(), newList.size(), nodes.size() - newList.size());
|
|
nodes = newList;
|
|
removeCollinearNodes(isPolygon);
|
|
}
|
|
}
|
|
|
|
void removeSteps() {
|
|
if (nodes.size() < 3) {
|
|
return;
|
|
}
|
|
boolean toDelete[] = new boolean[nodes.size()];
|
|
Node node_i;
|
|
Node node_j;
|
|
Node node_k;
|
|
boolean bylSchodek = false;
|
|
for (int i = 0; i < nodes.size() - 2; i++) {
|
|
node_i = nodes.get(i);
|
|
node_j = nodes.get(i + 1);
|
|
node_k = nodes.get(i + 2);
|
|
int absX_i_j = Math.abs(node_j.idX - node_i.idX);
|
|
int absY_i_j = Math.abs(node_j.idY - node_i.idY);
|
|
int absX_j_k = Math.abs(node_k.idX - node_j.idX);
|
|
int absY_j_k = Math.abs(node_k.idY - node_j.idY);
|
|
if (absX_i_j + absY_i_j + absX_j_k + absY_j_k == 2) {
|
|
// wezly moga tworzyc schodek
|
|
if (absX_i_j + absX_j_k == 1) {
|
|
// wezly tworza schodek, zatem srodkowy wezel schodka do usuniecia
|
|
toDelete[i + 1] = true;
|
|
bylSchodek = true;
|
|
// przeskok o usuwany wezel
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
if (bylSchodek) {
|
|
ArrayList<Node> newList = new ArrayList<Node>();
|
|
for (int i = 0; i < nodes.size(); i++) {
|
|
if (!toDelete[i]) {
|
|
newList.add(nodes.get(i));
|
|
}
|
|
}
|
|
if (nodes.size() > newList.size()) {
|
|
// logger.trace("Liczba oryg. wezlow= {}, liczba roznych wezlow= {}, roznica= {}", nodes.size(), newList.size(), nodes.size() - newList.size());
|
|
}
|
|
nodes = newList;
|
|
removeSteps();
|
|
}
|
|
}
|
|
|
|
void writeLinearFeatureIntoSquares(ELinearFeature type) {
|
|
if (nodes.size() == 0) {
|
|
return;
|
|
}
|
|
Coord.Grid[] punktyLamanej = new Coord.Grid[nodes.size()];
|
|
for (int i = 0; i < nodes.size(); i++) {
|
|
punktyLamanej[i] = new Coord.Grid(nodes.get(i).idX, nodes.get(i).idY);
|
|
}
|
|
Square kw0;
|
|
Square kw1;
|
|
Coord.Grid id0;
|
|
Coord.Grid id1;
|
|
EGeoDirection kier;
|
|
Coord.Grid[] kwadraty = GeomUtils.kwadratyLamanej2(punktyLamanej);
|
|
// float dlug = GeomUtils.dlugoscDrogiPoKwadratch(kwadraty);
|
|
for (int i = 0; i < kwadraty.length - 1; i++) {
|
|
try {
|
|
id0 = kwadraty[i];
|
|
kw0 = Teren.getSquare(id0.x, id0.y);
|
|
id1 = kwadraty[i + 1];
|
|
kw1 = Teren.getSquare(id1.x, id1.y);
|
|
kier = GeomUtils.kierunekDlaSasiada(id0, id1);
|
|
switch (type) {
|
|
case ROAD:
|
|
kw0.jestDroga[kier.id] = true;
|
|
kw0.jestDroga[kier.oppositeDirect().id] = true;
|
|
break;
|
|
case WATER_WAY:
|
|
kw0.jestPrzeszkodaWodna[kier.id] = true;
|
|
kw0.jestPrzeszkodaWodna[kier.oppositeDirect().id] = true;
|
|
break;
|
|
case DITCH:
|
|
kw0.jestRow[kier.id] = true;
|
|
kw0.jestRow[kier.oppositeDirect().id] = true;
|
|
break;
|
|
default:
|
|
}
|
|
} catch (Exception e) {
|
|
logger.warn(e.toString());
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void writeAreaFeatureIntoSquares2(EAreaFeature type, boolean clearFeature, Coord.Grid[] polygon) {
|
|
if (polygon.length > 20) {
|
|
// podział wielokata na dwa mniejsze
|
|
int m = 2;
|
|
Coord.Grid pocz = polygon[0];
|
|
boolean poprawnyPodzial = false;
|
|
while (m < polygon.length - 1 && !poprawnyPodzial) {
|
|
// sprawdzenie, czy punkty wielokata po podziale sa po jednej stronie wektora podzialu
|
|
poprawnyPodzial = true;
|
|
Coord.Grid kon = polygon[m];
|
|
for (int i = 0; i < polygon.length; i++) {
|
|
int i_puls_1 = (i + 1) % polygon.length;
|
|
boolean przeciecie = GeomUtils.intersection(pocz, kon, polygon[i], polygon[i_puls_1]);
|
|
if (przeciecie) {
|
|
// sprawdzenie, czy jakiś koniec jednego odcinka jest równy końcowi drugiego odcinka
|
|
boolean b = pocz.equals(polygon[i]) ||
|
|
pocz.equals(polygon[i_puls_1]) ||
|
|
kon.equals(polygon[i]) ||
|
|
kon.equals(polygon[i_puls_1]);
|
|
if (!b) {
|
|
poprawnyPodzial = false;
|
|
m++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (poprawnyPodzial) {
|
|
// punkt podziału wielokąta jest poprawny, zatem dzielę wielokąt na dwa
|
|
Coord.Grid[] polygon1 = new Coord.Grid[m + 1];
|
|
for (int i = 0; i < polygon1.length; i++) {
|
|
polygon1[i] = polygon[i];
|
|
}
|
|
writeAreaFeatureIntoSquares2(type, clearFeature, polygon1);
|
|
|
|
Coord.Grid[] polygon2 = new Coord.Grid[polygon.length - m + 1];
|
|
polygon2[0] = polygon[0];
|
|
for (int i = m; i < polygon.length; i++) {
|
|
polygon2[i - m + 1] = polygon[i];
|
|
}
|
|
writeAreaFeatureIntoSquares2(type, clearFeature, polygon2);
|
|
} else {
|
|
// nie udało się poprawnie podzielić wielokąta, zatem przesuwam wierzchołki, aby zmienić wierzchołek
|
|
// startowy, który jest wierchołkiem referencyjnym podziału (drugi wierzchołek podziału jest szukany)
|
|
Coord.Grid temp = polygon[0];
|
|
for (int i = 0; i < polygon.length - 1; i++) {
|
|
polygon[i] = polygon[i + 1];
|
|
}
|
|
polygon[polygon.length - 1] = temp;
|
|
writeAreaFeatureIntoSquares2(type, clearFeature, polygon);
|
|
}
|
|
return;
|
|
}
|
|
float val = (clearFeature) ? 0.0f : 1.0f;
|
|
int minX = polygon[0].x;
|
|
int minY = polygon[0].y;
|
|
int maxX = polygon[0].x;
|
|
int maxY = polygon[0].y;
|
|
for (int i = 1; i < polygon.length; i++) {
|
|
minX = Math.min(polygon[i].x, minX);
|
|
minY = Math.min(polygon[i].y, minY);
|
|
maxX = Math.max(polygon[i].x, maxX);
|
|
maxY = Math.max(polygon[i].y, maxY);
|
|
}
|
|
Coord.Grid idTest = new Coord.Grid();
|
|
boolean inside;
|
|
for (int j = maxY; j >= minY; j--) {
|
|
for (int i = minX; i <= maxX; i++) {
|
|
idTest.x = i;
|
|
idTest.y = j;
|
|
Square kw = Teren.getSquare(idTest.x, idTest.y);
|
|
if (kw == Square.EMPTY) {
|
|
continue;
|
|
}
|
|
inside = GeomUtils.insidePolygon(polygon, idTest);
|
|
if (inside) {
|
|
switch (type) {
|
|
case FOREST:
|
|
kw.stopienZalesienia = val;
|
|
break;
|
|
case WATER:
|
|
kw.stopienZawodnienia = val;
|
|
break;
|
|
case SWAMP:
|
|
kw.stopienZabagnienia = val;
|
|
break;
|
|
case BUILDINGS:
|
|
kw.stopienZabudowy = val;
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void writeAreaFeatureIntoSquares(EAreaFeature type, boolean clearFeature, Coord.Grid[] polygon,
|
|
int minX, int maxX, int minY, int maxY) {
|
|
float val = (clearFeature) ? 0.0f : 1.0f;
|
|
Coord.Grid idTest = new Coord.Grid();
|
|
boolean inside;
|
|
for (int i = minX; i <= maxX; i++) {
|
|
for (int j = minY; j <= maxY; j++) {
|
|
idTest.x = i;
|
|
idTest.y = j;
|
|
Square kw = Teren.getSquare(idTest.x, idTest.y);
|
|
if (kw == Square.EMPTY) {
|
|
continue;
|
|
}
|
|
inside = GeomUtils.insidePolygon(polygon, idTest);
|
|
if (inside) {
|
|
switch (type) {
|
|
case FOREST:
|
|
kw.stopienZalesienia = val;
|
|
break;
|
|
case WATER:
|
|
kw.stopienZawodnienia = val;
|
|
break;
|
|
case SWAMP:
|
|
kw.stopienZabagnienia = val;
|
|
break;
|
|
case BUILDINGS:
|
|
kw.stopienZabudowy = val;
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void writeAreaFeatureIntoSquares2(EAreaFeature type, boolean clearFeature) {
|
|
if (nodes.size() == 0) {
|
|
return;
|
|
}
|
|
Square kw;
|
|
float val = (clearFeature) ? 0.0f : 1.0f;
|
|
if (nodes.size() == 1) {
|
|
kw = Teren.getSquare(nodes.get(0).idX, nodes.get(0).idY);
|
|
switch (type) {
|
|
case FOREST:
|
|
kw.stopienZalesienia = val;
|
|
break;
|
|
case WATER:
|
|
kw.stopienZawodnienia = val;
|
|
break;
|
|
case SWAMP:
|
|
kw.stopienZabagnienia = val;
|
|
break;
|
|
case BUILDINGS:
|
|
kw.stopienZabudowy = val;
|
|
break;
|
|
default:
|
|
}
|
|
return;
|
|
}
|
|
if (nodes.size() == 2) {
|
|
Coord.Grid[] kwadraty = GeomUtils.kwadratyOdcinka(nodes.get(0).idX, nodes.get(0).idY,
|
|
nodes.get(1).idX, nodes.get(1).idY);
|
|
for (int i = 0; i < kwadraty.length; i++) {
|
|
kw = Teren.getSquare(kwadraty[i].x, kwadraty[i].y);
|
|
switch (type) {
|
|
case FOREST:
|
|
kw.stopienZalesienia = val;
|
|
break;
|
|
case WATER:
|
|
kw.stopienZawodnienia = val;
|
|
break;
|
|
case SWAMP:
|
|
kw.stopienZabagnienia = val;
|
|
break;
|
|
case BUILDINGS:
|
|
kw.stopienZabudowy = val;
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
Coord.Grid[] wielokat = new Coord.Grid[nodes.size()];
|
|
for (int i = 0; i < nodes.size(); i++) {
|
|
wielokat[i] = new Coord.Grid(nodes.get(i).idX, nodes.get(i).idY);
|
|
}
|
|
writeAreaFeatureIntoSquares2(type, clearFeature, wielokat);
|
|
}
|
|
|
|
void writeAreaFeatureIntoSquares(EAreaFeature type, boolean clearFeature) {
|
|
if (nodes.size() == 0) {
|
|
return;
|
|
}
|
|
Square kw;
|
|
float val = (clearFeature) ? 0.0f : 1.0f;
|
|
if (nodes.size() == 1) {
|
|
kw = Teren.getSquare(nodes.get(0).idX, nodes.get(0).idY);
|
|
switch (type) {
|
|
case FOREST:
|
|
kw.stopienZalesienia = val;
|
|
break;
|
|
case WATER:
|
|
kw.stopienZawodnienia = val;
|
|
break;
|
|
case SWAMP:
|
|
kw.stopienZabagnienia = val;
|
|
break;
|
|
case BUILDINGS:
|
|
kw.stopienZabudowy = val;
|
|
break;
|
|
default:
|
|
}
|
|
return;
|
|
}
|
|
if (nodes.size() == 2) {
|
|
Coord.Grid[] kwadraty = GeomUtils.kwadratyOdcinka(nodes.get(0).idX, nodes.get(0).idY,
|
|
nodes.get(1).idX, nodes.get(1).idY);
|
|
for (int i = 0; i < kwadraty.length; i++) {
|
|
kw = Teren.getSquare(kwadraty[i].x, kwadraty[i].y);
|
|
switch (type) {
|
|
case FOREST:
|
|
kw.stopienZalesienia = val;
|
|
break;
|
|
case WATER:
|
|
kw.stopienZawodnienia = val;
|
|
break;
|
|
case SWAMP:
|
|
kw.stopienZabagnienia = val;
|
|
break;
|
|
case BUILDINGS:
|
|
kw.stopienZabudowy = val;
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
Coord.Grid[] wielokat = new Coord.Grid[nodes.size()];
|
|
int minX = nodes.get(0).idX;
|
|
int minY = nodes.get(0).idY;
|
|
int maxX = nodes.get(0).idX;
|
|
int maxY = nodes.get(0).idY;
|
|
for (int i = 0; i < nodes.size(); i++) {
|
|
wielokat[i] = new Coord.Grid(nodes.get(i).idX, nodes.get(i).idY);
|
|
minX = Math.min(wielokat[i].x, minX);
|
|
minY = Math.min(wielokat[i].y, minY);
|
|
maxX = Math.max(wielokat[i].x, maxX);
|
|
maxY = Math.max(wielokat[i].y, maxY);
|
|
}
|
|
int ileKwTest = (maxX - minX) * (maxY - minY);
|
|
if (ileKwTest > 100000) {
|
|
int dx = maxX - minX;
|
|
int dy = maxY - minY;
|
|
Worker[] workers = new Worker[4];
|
|
workers[0] = new Worker(type, clearFeature, wielokat, minX, minX + dx / 2, minY, minY + dy / 2);
|
|
workers[1] = new Worker(type, clearFeature, wielokat, minX + dx / 2 + 1, maxX, minY, minY + dy / 2);
|
|
workers[2] = new Worker(type, clearFeature, wielokat, minX, minX + dx / 2, minY + dy / 2 + 1, maxY);
|
|
workers[3] = new Worker(type, clearFeature, wielokat, minX + dx / 2 + 1, maxX, minY + dy / 2 + 1, maxY);
|
|
for (int i = 0; i < workers.length; i++) {
|
|
workers[i].start();
|
|
}
|
|
return;
|
|
}
|
|
Coord.Grid idTest = new Coord.Grid();
|
|
boolean nalezyDoWielokata;
|
|
// int liczKw = 0;
|
|
// int liczKwObszaru = 0;
|
|
// for (int j = maxY; j >= minY; j--) {
|
|
// for (int i = minX; i <= maxX; i++) {
|
|
for (int i = minX; i <= maxX; i++) {
|
|
for (int j = minY; j <= maxY; j++) {
|
|
idTest.x = i;
|
|
idTest.y = j;
|
|
// char c = ' ';
|
|
// liczKw++;
|
|
kw = Teren.getSquare(idTest.x, idTest.y);
|
|
if (kw == Square.EMPTY) {
|
|
continue;
|
|
}
|
|
nalezyDoWielokata = GeomUtils.insidePolygon(wielokat, idTest);
|
|
if (nalezyDoWielokata) {
|
|
// c = 'O';
|
|
switch (type) {
|
|
case FOREST:
|
|
kw.stopienZalesienia = val;
|
|
break;
|
|
case WATER:
|
|
kw.stopienZawodnienia = val;
|
|
break;
|
|
case SWAMP:
|
|
kw.stopienZabagnienia = val;
|
|
break;
|
|
case BUILDINGS:
|
|
kw.stopienZabudowy = val;
|
|
break;
|
|
default:
|
|
}
|
|
// liczKwObszaru++;
|
|
}
|
|
// rysowanie wielokata
|
|
// for (int k = 0; k < wielokat.length; k++) {
|
|
// if (wielokat[k].equals(idTest)) {
|
|
// c = 'X';
|
|
// break;
|
|
// }
|
|
// }
|
|
// System.out.print(c);
|
|
// System.out.print(' ');
|
|
}
|
|
// System.out.println();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "Way{id=" + id +
|
|
", len=" + nodes.size() +
|
|
", nodes=" + nodes +
|
|
'}';
|
|
}
|
|
|
|
}
|