add address index to binary format
git-svn-id: https://osmand.googlecode.com/svn/trunk@620 e29c36b1-1cfa-d876-8d93-3434fc2bb7b8
This commit is contained in:
parent
45c33e0f07
commit
dda3b6ec80
5 changed files with 3011 additions and 144 deletions
|
@ -7,15 +7,12 @@ package net.osmand;
|
||||||
*/
|
*/
|
||||||
public class ToDoConstants {
|
public class ToDoConstants {
|
||||||
|
|
||||||
// TODO max 100
|
// TODO max 101
|
||||||
// FOR 0.4 beta RELEASE
|
|
||||||
// Profile vector rendering
|
|
||||||
|
|
||||||
|
|
||||||
// Outside base 0.4 release
|
// Outside base 0.4 release
|
||||||
// 69. Add phone and site information to POI (enable call to POI and open site)
|
// 69. Add phone and site information to POI (enable call to POI and open site)
|
||||||
// 86. Allow to add/edit custom tags to POI objects (Issue)
|
// 86. Allow to add/edit custom tags to POI objects (Issue)
|
||||||
// 91. Invent binary format (minimize disk space, maximize speed)
|
|
||||||
// 92. Replace poi index with standard map index and unify POI categories
|
// 92. Replace poi index with standard map index and unify POI categories
|
||||||
// 94. Revise index to decrease their size (especially address) - replace to float lat/lon and remove for POI
|
// 94. Revise index to decrease their size (especially address) - replace to float lat/lon and remove for POI
|
||||||
// remove en_names from POI (possibly from address)
|
// remove en_names from POI (possibly from address)
|
||||||
|
@ -42,6 +39,7 @@ public class ToDoConstants {
|
||||||
// DONE ANDROID :
|
// DONE ANDROID :
|
||||||
// 99. Implement better file downloader for big files
|
// 99. Implement better file downloader for big files
|
||||||
// 100. Show impoted gpx points (as favorites), sort the by distance
|
// 100. Show impoted gpx points (as favorites), sort the by distance
|
||||||
|
// 91. Invent binary format (minimize disk space, maximize speed)
|
||||||
|
|
||||||
// DONE SWING
|
// DONE SWING
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,14 @@ import java.util.Map;
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
|
|
||||||
import net.osmand.Algoritms;
|
import net.osmand.Algoritms;
|
||||||
|
import net.osmand.binary.OsmandOdb.StreetIndex;
|
||||||
|
import net.osmand.data.Building;
|
||||||
|
import net.osmand.data.City;
|
||||||
|
import net.osmand.data.MapObject;
|
||||||
|
import net.osmand.data.Street;
|
||||||
import net.osmand.data.index.IndexConstants;
|
import net.osmand.data.index.IndexConstants;
|
||||||
|
import net.osmand.osm.LatLon;
|
||||||
|
import net.osmand.osm.MapUtils;
|
||||||
|
|
||||||
import com.google.protobuf.CodedOutputStream;
|
import com.google.protobuf.CodedOutputStream;
|
||||||
import com.google.protobuf.WireFormat;
|
import com.google.protobuf.WireFormat;
|
||||||
|
@ -39,6 +46,9 @@ public class BinaryMapIndexWriter {
|
||||||
private Stack<Long> stackBaseIds = new Stack<Long>();
|
private Stack<Long> stackBaseIds = new Stack<Long>();
|
||||||
private Stack<Map<String, Integer>> stackStringTable = new Stack<Map<String, Integer>>();
|
private Stack<Map<String, Integer>> stackStringTable = new Stack<Map<String, Integer>>();
|
||||||
|
|
||||||
|
// needed for address index
|
||||||
|
private MapObject cityOrPostcode = null;
|
||||||
|
|
||||||
// internal constants to track state of index writing
|
// internal constants to track state of index writing
|
||||||
private Stack<Integer> state = new Stack<Integer>();
|
private Stack<Integer> state = new Stack<Integer>();
|
||||||
private Stack<Long> stackSizes = new Stack<Long>();
|
private Stack<Long> stackSizes = new Stack<Long>();
|
||||||
|
@ -48,6 +58,10 @@ public class BinaryMapIndexWriter {
|
||||||
private final static int MAP_ROOT_LEVEL_INIT = 3;
|
private final static int MAP_ROOT_LEVEL_INIT = 3;
|
||||||
private final static int MAP_TREE = 4;
|
private final static int MAP_TREE = 4;
|
||||||
|
|
||||||
|
private final static int ADDRESS_INDEX_INIT = 5;
|
||||||
|
private final static int CITY_INDEX_INIT = 6;
|
||||||
|
private final static int POSTCODES_INDEX_INIT = 7;
|
||||||
|
|
||||||
public BinaryMapIndexWriter(final RandomAccessFile raf) throws IOException{
|
public BinaryMapIndexWriter(final RandomAccessFile raf) throws IOException{
|
||||||
this.raf = raf;
|
this.raf = raf;
|
||||||
codedOutStream = CodedOutputStream.newInstance(new OutputStream() {
|
codedOutStream = CodedOutputStream.newInstance(new OutputStream() {
|
||||||
|
@ -88,22 +102,19 @@ public class BinaryMapIndexWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startWriteMapIndex() throws IOException{
|
public void startWriteMapIndex() throws IOException{
|
||||||
assert state.peek() == OSMAND_STRUCTURE_INIT;
|
pushState(MAP_INDEX_INIT, OSMAND_STRUCTURE_INIT);
|
||||||
state.push(MAP_INDEX_INIT);
|
|
||||||
codedOutStream.writeTag(OsmandOdb.OsmAndStructure.MAPINDEX_FIELD_NUMBER, WireFormat.WIRETYPE_FIXED32_LENGTH_DELIMITED);
|
codedOutStream.writeTag(OsmandOdb.OsmAndStructure.MAPINDEX_FIELD_NUMBER, WireFormat.WIRETYPE_FIXED32_LENGTH_DELIMITED);
|
||||||
preserveInt32Size();
|
preserveInt32Size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void endWriteMapIndex() throws IOException{
|
public void endWriteMapIndex() throws IOException{
|
||||||
Integer st = state.pop();
|
popState(MAP_INDEX_INIT);
|
||||||
assert st == MAP_INDEX_INIT;
|
|
||||||
writeInt32Size();
|
writeInt32Size();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startWriteMapLevelIndex(int minZoom, int maxZoom, int leftX, int rightX, int topY, int bottomY) throws IOException{
|
public void startWriteMapLevelIndex(int minZoom, int maxZoom, int leftX, int rightX, int topY, int bottomY) throws IOException{
|
||||||
assert state.peek() == MAP_INDEX_INIT;
|
pushState(MAP_ROOT_LEVEL_INIT, MAP_INDEX_INIT);
|
||||||
state.push(MAP_ROOT_LEVEL_INIT);
|
|
||||||
|
|
||||||
codedOutStream.writeTag(OsmandOdb.OsmAndMapIndex.LEVELS_FIELD_NUMBER, WireFormat.WIRETYPE_FIXED32_LENGTH_DELIMITED);
|
codedOutStream.writeTag(OsmandOdb.OsmAndMapIndex.LEVELS_FIELD_NUMBER, WireFormat.WIRETYPE_FIXED32_LENGTH_DELIMITED);
|
||||||
preserveInt32Size();
|
preserveInt32Size();
|
||||||
|
@ -119,8 +130,7 @@ public class BinaryMapIndexWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void endWriteMapLevelIndex() throws IOException{
|
public void endWriteMapLevelIndex() throws IOException{
|
||||||
assert state.peek() == MAP_ROOT_LEVEL_INIT;
|
popState(MAP_ROOT_LEVEL_INIT);
|
||||||
state.pop();
|
|
||||||
stackBounds.pop();
|
stackBounds.pop();
|
||||||
writeInt32Size();
|
writeInt32Size();
|
||||||
}
|
}
|
||||||
|
@ -130,14 +140,14 @@ public class BinaryMapIndexWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startMapTreeElement(long baseId, int leftX, int rightX, int topY, int bottomY) throws IOException{
|
public void startMapTreeElement(long baseId, int leftX, int rightX, int topY, int bottomY) throws IOException{
|
||||||
assert state.peek() == MAP_ROOT_LEVEL_INIT || state.peek() == MAP_TREE;
|
checkPeekState(MAP_ROOT_LEVEL_INIT, MAP_TREE);
|
||||||
if(state.peek() == MAP_ROOT_LEVEL_INIT){
|
if(state.peek() == MAP_ROOT_LEVEL_INIT){
|
||||||
codedOutStream.writeTag(OsmandOdb.MapRootLevel.ROOT_FIELD_NUMBER, WireFormat.WIRETYPE_FIXED32_LENGTH_DELIMITED);
|
codedOutStream.writeTag(OsmandOdb.MapRootLevel.ROOT_FIELD_NUMBER, WireFormat.WIRETYPE_FIXED32_LENGTH_DELIMITED);
|
||||||
} else {
|
} else {
|
||||||
codedOutStream.writeTag(OsmandOdb.MapTree.SUBTREES_FIELD_NUMBER, WireFormat.WIRETYPE_FIXED32_LENGTH_DELIMITED);
|
codedOutStream.writeTag(OsmandOdb.MapTree.SUBTREES_FIELD_NUMBER, WireFormat.WIRETYPE_FIXED32_LENGTH_DELIMITED);
|
||||||
}
|
}
|
||||||
preserveInt32Size();
|
|
||||||
state.push(MAP_TREE);
|
state.push(MAP_TREE);
|
||||||
|
preserveInt32Size();
|
||||||
|
|
||||||
|
|
||||||
Bounds bounds = stackBounds.peek();
|
Bounds bounds = stackBounds.peek();
|
||||||
|
@ -150,9 +160,9 @@ public class BinaryMapIndexWriter {
|
||||||
stackStringTable.push(null);
|
stackStringTable.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void endWriteMapTreeElement() throws IOException{
|
public void endWriteMapTreeElement() throws IOException{
|
||||||
assert state.peek() == MAP_TREE;
|
popState(MAP_TREE);
|
||||||
state.pop();
|
|
||||||
|
|
||||||
stackBounds.pop();
|
stackBounds.pop();
|
||||||
Long l = stackBaseIds.pop();
|
Long l = stackBaseIds.pop();
|
||||||
|
@ -319,6 +329,108 @@ public class BinaryMapIndexWriter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void startWriteAddressIndex(String name) throws IOException {
|
||||||
|
pushState(OSMAND_STRUCTURE_INIT, ADDRESS_INDEX_INIT);
|
||||||
|
codedOutStream.writeTag(OsmandOdb.OsmAndStructure.ADDRESSINDEX_FIELD_NUMBER, WireFormat.WIRETYPE_FIXED32_LENGTH_DELIMITED);
|
||||||
|
preserveInt32Size();
|
||||||
|
|
||||||
|
codedOutStream.writeString(OsmandOdb.OsmAndAddressIndex.NAME_FIELD_NUMBER, name);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void endWriteAddressIndex() throws IOException {
|
||||||
|
popState(ADDRESS_INDEX_INIT);
|
||||||
|
writeInt32Size();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void startWriteCityIndex(City city) throws IOException {
|
||||||
|
pushState(CITY_INDEX_INIT, ADDRESS_INDEX_INIT);
|
||||||
|
codedOutStream.writeTag(OsmandOdb.OsmAndStructure.ADDRESSINDEX_FIELD_NUMBER, WireFormat.WIRETYPE_FIXED32_LENGTH_DELIMITED);
|
||||||
|
preserveInt32Size();
|
||||||
|
|
||||||
|
codedOutStream.writeUInt32(OsmandOdb.CityIndex.CITY_TYPE_FIELD_NUMBER, city.getType().ordinal());
|
||||||
|
codedOutStream.writeUInt64(OsmandOdb.CityIndex.ID_FIELD_NUMBER, city.getId());
|
||||||
|
codedOutStream.writeString(OsmandOdb.CityIndex.NAME_FIELD_NUMBER, city.getName());
|
||||||
|
if(city.getEnName() != null){
|
||||||
|
codedOutStream.writeString(OsmandOdb.CityIndex.NAME_EN_FIELD_NUMBER, city.getEnName());
|
||||||
|
}
|
||||||
|
codedOutStream.writeFixed32(OsmandOdb.CityIndex.X_FIELD_NUMBER, MapUtils.get31TileNumberX(city.getLocation().getLongitude()));
|
||||||
|
codedOutStream.writeFixed32(OsmandOdb.CityIndex.Y_FIELD_NUMBER, MapUtils.get31TileNumberY(city.getLocation().getLatitude()));
|
||||||
|
cityOrPostcode = city;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeStreetAndBuildings(Street street) throws IOException {
|
||||||
|
checkPeekState(CITY_INDEX_INIT, POSTCODES_INDEX_INIT);
|
||||||
|
StreetIndex.Builder streetBuilder = OsmandOdb.StreetIndex.newBuilder();
|
||||||
|
streetBuilder.setName(street.getName());
|
||||||
|
if(street.getEnName() != null){
|
||||||
|
streetBuilder.setNameEn(street.getEnName());
|
||||||
|
}
|
||||||
|
streetBuilder.setId(street.getId());
|
||||||
|
|
||||||
|
|
||||||
|
LatLon location = cityOrPostcode.getLocation();
|
||||||
|
int cx = MapUtils.get31TileNumberX(location.getLongitude());
|
||||||
|
int cy = MapUtils.get31TileNumberY(location.getLatitude());
|
||||||
|
int sx = MapUtils.get31TileNumberX(street.getLocation().getLongitude());
|
||||||
|
int sy = MapUtils.get31TileNumberY(street.getLocation().getLatitude());
|
||||||
|
streetBuilder.setX((sx - cx) >> 7);
|
||||||
|
streetBuilder.setY((sy - cy) >> 7);
|
||||||
|
|
||||||
|
for(Building b : street.getBuildings()){
|
||||||
|
OsmandOdb.BuildingIndex.Builder bbuilder= OsmandOdb.BuildingIndex.newBuilder();
|
||||||
|
int bx = MapUtils.get31TileNumberX(b.getLocation().getLongitude());
|
||||||
|
int by = MapUtils.get31TileNumberY(b.getLocation().getLatitude());
|
||||||
|
bbuilder.setX((bx - sx) >> 7);
|
||||||
|
bbuilder.setY((by - sy) >> 7);
|
||||||
|
bbuilder.setId(b.getId());
|
||||||
|
bbuilder.setName(b.getName());
|
||||||
|
if(b.getEnName() != null){
|
||||||
|
bbuilder.setNameEn(b.getEnName());
|
||||||
|
}
|
||||||
|
streetBuilder.addBuildings(bbuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(state.peek() == CITY_INDEX_INIT){
|
||||||
|
codedOutStream.writeMessage(OsmandOdb.CityIndex.STREETS_FIELD_NUMBER, streetBuilder.build());
|
||||||
|
} else {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void endWriteCityIndex() throws IOException {
|
||||||
|
popState(CITY_INDEX_INIT);
|
||||||
|
writeInt32Size();
|
||||||
|
cityOrPostcode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushState(int push, int peek){
|
||||||
|
if(state.peek() != peek){
|
||||||
|
throw new IllegalStateException("expected " + peek+ " != "+ state.peek());
|
||||||
|
}
|
||||||
|
state.push(push);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkPeekState(int... states) {
|
||||||
|
for(int i=0;i<states.length; i++){
|
||||||
|
if(states[i] == state.peek()){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Note expected state : " + state.peek());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void popState(int state){
|
||||||
|
Integer st = this.state.pop();
|
||||||
|
if(st != state){
|
||||||
|
throw new IllegalStateException("expected " + state + " != "+ st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void close() throws IOException{
|
public void close() throws IOException{
|
||||||
assert state.peek() == OSMAND_STRUCTURE_INIT;
|
assert state.peek() == OSMAND_STRUCTURE_INIT;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1484,14 +1484,15 @@ public class IndexCreator {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writeBinaryMapIndex() throws IOException, SQLException {
|
public void writeBinaryAddressIndex(BinaryMapIndexWriter writer) throws IOException, SQLException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeBinaryMapIndex(BinaryMapIndexWriter writer) throws IOException, SQLException {
|
||||||
try {
|
try {
|
||||||
assert IndexConstants.IndexBinaryMapRenderObject.values().length == 6;
|
assert IndexConstants.IndexBinaryMapRenderObject.values().length == 6;
|
||||||
PreparedStatement selectData = mapConnection.prepareStatement("SELECT * FROM " + IndexBinaryMapRenderObject.getTable() + " WHERE id = ?");
|
PreparedStatement selectData = mapConnection.prepareStatement("SELECT * FROM " + IndexBinaryMapRenderObject.getTable() + " WHERE id = ?");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BinaryMapIndexWriter writer = new BinaryMapIndexWriter(mapRAFile);
|
|
||||||
writer.startWriteMapIndex();
|
writer.startWriteMapIndex();
|
||||||
|
|
||||||
for (int i = 0; i < MAP_ZOOMS.length - 1; i++) {
|
for (int i = 0; i < MAP_ZOOMS.length - 1; i++) {
|
||||||
|
@ -1897,12 +1898,21 @@ public class IndexCreator {
|
||||||
pStatements.remove(mapBinaryStat);
|
pStatements.remove(mapBinaryStat);
|
||||||
mapConnection.commit();
|
mapConnection.commit();
|
||||||
|
|
||||||
|
log.info("Finish packing RTree files");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(indexMap || indexAddress){
|
||||||
if(mapFile.exists()){
|
if(mapFile.exists()){
|
||||||
mapFile.delete();
|
mapFile.delete();
|
||||||
}
|
}
|
||||||
mapRAFile = new RandomAccessFile(mapFile, "rw");
|
mapRAFile = new RandomAccessFile(mapFile, "rw");
|
||||||
log.info("Finish packing RTree files");
|
BinaryMapIndexWriter writer = new BinaryMapIndexWriter(mapRAFile);
|
||||||
writeBinaryMapIndex();
|
if(indexMap){
|
||||||
|
writeBinaryMapIndex(writer);
|
||||||
|
}
|
||||||
|
if(indexAddress){
|
||||||
|
writeBinaryAddressIndex(writer);
|
||||||
|
}
|
||||||
log.info("Finish writing binary file");
|
log.info("Finish writing binary file");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2020,7 +2030,6 @@ public class IndexCreator {
|
||||||
// creator.setNodesDBFile(new File("e:/Information/OSM maps/osmand/belarus_nodes.tmp.odb"));
|
// creator.setNodesDBFile(new File("e:/Information/OSM maps/osmand/belarus_nodes.tmp.odb"));
|
||||||
// creator.generateIndexes(new File("e:/Information/OSM maps/belarus osm/belarus.osm.bz2"), new ConsoleProgressImplementation(3), null);
|
// creator.generateIndexes(new File("e:/Information/OSM maps/belarus osm/belarus.osm.bz2"), new ConsoleProgressImplementation(3), null);
|
||||||
|
|
||||||
creator.generateIndexes(new File("e:/Information/OSM maps/osm_map/zimbabwe.osm.bz2"), new ConsoleProgressImplementation(3), null);
|
|
||||||
|
|
||||||
|
|
||||||
// creator.generateIndexes(new File("e:/Information/OSM maps/belarus osm/forest.osm"), new ConsoleProgressImplementation(3), null);
|
// creator.generateIndexes(new File("e:/Information/OSM maps/belarus osm/forest.osm"), new ConsoleProgressImplementation(3), null);
|
||||||
|
|
|
@ -12,8 +12,11 @@ message OsmAndStructure {
|
||||||
required uint32 version = 1;
|
required uint32 version = 1;
|
||||||
// encoded as fixed32 length delimited
|
// encoded as fixed32 length delimited
|
||||||
repeated OsmAndMapIndex mapIndex = 2;
|
repeated OsmAndMapIndex mapIndex = 2;
|
||||||
|
// encoded as fixed32 length delimited
|
||||||
|
repeated OsmAndAddressIndex addressIndex = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
message OsmAndMapIndex {
|
message OsmAndMapIndex {
|
||||||
// encoded as fixed32 length delimited
|
// encoded as fixed32 length delimited
|
||||||
repeated MapRootLevel levels = 1;
|
repeated MapRootLevel levels = 1;
|
||||||
|
@ -70,3 +73,57 @@ message MapData {
|
||||||
optional int32 highwayMeta = 6;
|
optional int32 highwayMeta = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// ADDRESS INFORMATION TEST -----
|
||||||
|
|
||||||
|
message OsmAndAddressIndex {
|
||||||
|
|
||||||
|
required string name = 1;
|
||||||
|
optional string name_en = 2;
|
||||||
|
|
||||||
|
// encoded as fixed32 length delimited
|
||||||
|
repeated CityIndex cityIndex= 5; // cities and towns
|
||||||
|
|
||||||
|
repeated PostcodeIndex postcodes= 6;
|
||||||
|
|
||||||
|
repeated CityIndex villages = 7; // suburbs and villages
|
||||||
|
}
|
||||||
|
|
||||||
|
message CityIndex {
|
||||||
|
required uint32 city_type = 1; // 0-5 enum CityType
|
||||||
|
required string name = 2;
|
||||||
|
optional string name_en = 3;
|
||||||
|
optional uint64 id = 4;
|
||||||
|
|
||||||
|
required fixed32 x = 5; // x tile of 31 zoom
|
||||||
|
required fixed32 y = 6; // y tile of 31 zoom
|
||||||
|
|
||||||
|
repeated StreetIndex streets = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PostcodeIndex {
|
||||||
|
required string postcode = 1;
|
||||||
|
repeated StreetIndex streets = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StreetIndex {
|
||||||
|
required string name = 1;
|
||||||
|
optional string name_en = 2;
|
||||||
|
|
||||||
|
optional uint64 id = 6;
|
||||||
|
|
||||||
|
required sint32 x = 3; // delta encoded to parent 24 zoom
|
||||||
|
required sint32 y = 4; // delta encoded to parent 24 zoom
|
||||||
|
|
||||||
|
repeated BuildingIndex buildings = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BuildingIndex {
|
||||||
|
required string name = 1;
|
||||||
|
optional string name_en = 2;
|
||||||
|
optional uint64 id = 5;
|
||||||
|
|
||||||
|
required sint32 x = 3; // delta encoded to street 24 zoom
|
||||||
|
required sint32 y = 4; // delta encoded to street 24 zoom
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue