Add rendering route data objects from java/ change download button behavior

This commit is contained in:
vshcherb 2014-03-01 13:30:25 +02:00
parent 92dad044ec
commit 2cc5409044
7 changed files with 231 additions and 129 deletions

View file

@ -33,6 +33,7 @@ public class BinaryMapDataObject {
this.coordinates = coordinates;
}
public String getName(){
if(objectNames == null){
return "";

View file

@ -1541,6 +1541,10 @@ public class BinaryMapIndexReader {
return top;
}
public int getZoom() {
return zoom;
}
public void clearSearchResults(){
// recreate whole list to allow GC collect old data
searchResults = new ArrayList<T>();
@ -1598,6 +1602,10 @@ public class BinaryMapIndexReader {
}
}
public boolean isRegisteredRule(int id) {
return decodingRules.containsKey(id);
}
public void initMapEncodingRule(int type, int id, String tag, String val) {
if(!encodingRules.containsKey(tag)){
encodingRules.put(tag, new HashMap<String, Integer>());
@ -2070,7 +2078,7 @@ public class BinaryMapIndexReader {
}
public List<RouteSubregion> searchRouteIndexTree(SearchRequest<RouteDataObject> req, List<RouteSubregion> list) throws IOException {
public List<RouteSubregion> searchRouteIndexTree(SearchRequest<?> req, List<RouteSubregion> list) throws IOException {
req.numberOfVisitedObjects = 0;
req.numberOfAcceptedObjects = 0;
req.numberOfAcceptedSubtrees = 0;

View file

@ -674,7 +674,7 @@ public class BinaryMapRouteReaderAdapter {
}
}
public void initRouteTypesIfNeeded(SearchRequest<RouteDataObject> req, List<RouteSubregion> list) throws IOException {
public void initRouteTypesIfNeeded(SearchRequest<?> req, List<RouteSubregion> list) throws IOException {
for (RouteSubregion rs : list) {
if (req.intersects(rs.left, rs.top, rs.right, rs.bottom)) {
initRouteRegion(rs.routeReg);
@ -736,7 +736,7 @@ public class BinaryMapRouteReaderAdapter {
}
}
public List<RouteSubregion> searchRouteRegionTree(SearchRequest<RouteDataObject> req, List<RouteSubregion> list,
public List<RouteSubregion> searchRouteRegionTree(SearchRequest<?> req, List<RouteSubregion> list,
List<RouteSubregion> toLoad) throws IOException {
for (RouteSubregion rs : list) {
if (req.intersects(rs.left, rs.top, rs.right, rs.bottom)) {

View file

@ -61,6 +61,10 @@ public class RouteDataObject {
return null;
}
public TIntObjectHashMap<String> getNames() {
return names;
}
public String getRef(){
if(names != null ) {
return names.get(region.refTypeRule);

View file

@ -9,6 +9,7 @@ import java.util.Iterator;
import java.util.List;
import net.osmand.binary.BinaryMapIndexReader;
import net.osmand.binary.BinaryMapRouteReaderAdapter.RouteTypeRule;
import net.osmand.binary.RouteDataObject;
import net.osmand.data.LatLon;
import net.osmand.router.BinaryRoutePlanner.FinalRouteSegment;
@ -295,11 +296,30 @@ public class RouteResultPreparation {
additional.append("description = \"").append(res.getDescription()).append("\" ");
println(MessageFormat.format("\t<segment id=\"{0}\" start=\"{1}\" end=\"{2}\" {3}/>", (res.getObject().getId()) + "",
res.getStartPointIndex() + "", res.getEndPointIndex() + "", additional.toString()));
printAdditionalPointInfo(res);
}
}
println("</test>");
}
private void printAdditionalPointInfo(RouteSegmentResult res) {
boolean plus = res.getStartPointIndex() < res.getEndPointIndex();
for(int k = res.getStartPointIndex(); k != res.getEndPointIndex(); ) {
int[] tp = res.getObject().getPointTypes(k);
if(tp != null) {
for(int t = 0; t < tp.length; t++) {
RouteTypeRule rr = res.getObject().region.quickGetEncodingRule(tp[t]);
println("\t<point tag=\""+rr.getTag()+"\"" + " value=\""+rr.getValue()+"\"/>");
}
}
if(plus) {
k++;
} else {
k--;
}
}
}
private void addTurnInfo(boolean leftside, List<RouteSegmentResult> result) {
int prevSegment = -1;

View file

@ -1,9 +1,11 @@
package net.osmand.plus.render;
import gnu.trove.iterator.TIntObjectIterator;
import gnu.trove.list.TLongList;
import gnu.trove.list.array.TIntArrayList;
import gnu.trove.list.array.TLongArrayList;
import gnu.trove.map.hash.TIntObjectHashMap;
import gnu.trove.set.TLongSet;
import gnu.trove.set.hash.TLongHashSet;
@ -20,18 +22,19 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import net.osmand.IProgress;
import net.osmand.ResultMatcher;
import net.osmand.NativeLibrary.NativeSearchResult;
import net.osmand.PlatformUtil;
import net.osmand.ResultMatcher;
import net.osmand.access.AccessibleToast;
import net.osmand.binary.BinaryMapDataObject;
import net.osmand.binary.BinaryMapIndexReader;
import net.osmand.binary.BinaryMapRouteReaderAdapter.RouteTypeRule;
import net.osmand.binary.RouteDataObject;
import net.osmand.binary.BinaryMapIndexReader.MapIndex;
import net.osmand.binary.BinaryMapIndexReader.SearchRequest;
import net.osmand.binary.BinaryMapIndexReader.TagValuePair;
import net.osmand.binary.BinaryMapRouteReaderAdapter.RouteRegion;
import net.osmand.binary.BinaryMapRouteReaderAdapter.RouteSubregion;
import net.osmand.binary.RouteDataObject;
import net.osmand.data.QuadPointDouble;
import net.osmand.data.QuadRect;
import net.osmand.data.RotatedTileBox;
@ -66,10 +69,10 @@ public class MapRenderRepositories {
// It is needed to not draw object twice if user have map index that intersects by boundaries
public static boolean checkForDuplicateObjectIds = true;
private final static Log log = PlatformUtil.getLog(MapRenderRepositories.class);
private final OsmandApplication context;
private final static int BASEMAP_ZOOM = 11;
static int zoomForBaseRouteRendering = 14;
private Handler handler;
private Map<String, BinaryMapIndexReader> files = new ConcurrentHashMap<String, BinaryMapIndexReader>();
private Set<String> nativeFiles = new HashSet<String>();
@ -91,6 +94,8 @@ public class MapRenderRepositories {
private RotatedTileBox prevBmpLocation = null;
// already rendered bitmap
private Bitmap prevBmp;
// to track necessity of map download (1 (if basemap) + 2 (if normal map)
private int previousRenderedState;
// location of rendered bitmap
private RotatedTileBox bmpLocation = null;
@ -98,6 +103,7 @@ public class MapRenderRepositories {
private Bitmap bmp;
// Field used in C++
private boolean interrupted = false;
private int renderedState = 0; // (1 (if basemap) + 2 (if normal map)
private RenderingContext currentRenderingContext;
private SearchRequest<BinaryMapDataObject> searchRequest;
private OsmandSettings prefs;
@ -252,7 +258,7 @@ public class MapRenderRepositories {
}
NativeSearchResult resultHandler = library.searchObjectsForRendering(leftX, rightX, topY, bottomY, zoom, renderingReq,
checkForDuplicateObjectIds, this, /*context.getString(R.string.switch_to_raster_map_to_see)*/ "");
checkForDuplicateObjectIds, this, "");
if (checkWhetherInterrupted()) {
resultHandler.deleteNativeResult();
return false;
@ -268,6 +274,77 @@ public class MapRenderRepositories {
return true;
}
private void readRouteDataAsMapObjects(SearchRequest<BinaryMapDataObject> sr, BinaryMapIndexReader c,
final ArrayList<BinaryMapDataObject> tempResult, final TLongSet ids) {
final boolean basemap = c.isBasemap();
try {
for (RouteRegion reg : c.getRoutingIndexes()) {
List<RouteSubregion> parent = sr.getZoom() < 15 ? reg.getBaseSubregions() : reg.getSubregions();
List<RouteSubregion> searchRouteIndexTree = c.searchRouteIndexTree(sr, parent);
final MapIndex nmi = new MapIndex();
c.loadRouteIndexData(searchRouteIndexTree, new ResultMatcher<RouteDataObject>() {
@Override
public boolean publish(RouteDataObject r) {
if (basemap) {
renderedState |= 1;
} else {
renderedState |= 2;
}
if (checkForDuplicateObjectIds && !basemap) {
if (ids.contains(r.getId()) && r.getId() > 0) {
// do not add object twice
return false;
}
ids.add(r.getId());
}
int[] coordinantes = new int[r.getPointsLength() * 2];
int[] roTypes = r.getTypes();
for(int k = 0; k < roTypes.length; k++) {
int type = roTypes[k];
registerMissingType(nmi, r, type);
}
for(int k = 0; k < coordinantes.length/2; k++ ) {
coordinantes[2 * k] = r.getPoint31XTile(k);
coordinantes[2 * k + 1] = r.getPoint31YTile(k);
}
BinaryMapDataObject mo = new BinaryMapDataObject(coordinantes, roTypes, new int[0][], r.getId());
TIntObjectHashMap<String> names = r.getNames();
if(names != null) {
TIntObjectIterator<String> it = names.iterator();
while(it.hasNext()) {
it.advance();
registerMissingType(nmi, r, it.key());
mo.putObjectName(it.key(), it.value());
}
}
mo.setMapIndex(nmi);
tempResult.add(mo);
return false;
}
private void registerMissingType(final MapIndex nmi, RouteDataObject r, int type) {
if (!nmi.isRegisteredRule(type)) {
RouteTypeRule rr = r.region.quickGetEncodingRule(type);
String tag = rr.getTag();
int additional = ("highway".equals(tag) || "route".equals(tag) || "railway".equals(tag)
|| "aeroway".equals(tag) || "aerialway".equals(tag)) ? 0 : 1;
nmi.initMapEncodingRule(additional, type, rr.getTag(), rr.getValue());
}
}
@Override
public boolean isCancelled() {
return !interrupted;
}
});
}
} catch (IOException e) {
log.debug("Search failed " + c.getRegionNames(), e); //$NON-NLS-1$
}
}
private boolean loadVectorData(QuadRect dataBox, final int zoom, final RenderingRuleSearchRequest renderingReq) {
double cBottomLatitude = dataBox.bottom;
double cTopLatitude = dataBox.top;
@ -277,18 +354,102 @@ public class MapRenderRepositories {
long now = System.currentTimeMillis();
System.gc(); // to clear previous objects
int count = 0;
ArrayList<BinaryMapDataObject> tempResult = new ArrayList<BinaryMapDataObject>();
ArrayList<BinaryMapDataObject> basemapResult = new ArrayList<BinaryMapDataObject>();
TLongSet ids = new TLongHashSet();
int[] count = new int[]{0};
boolean[] ocean = new boolean[]{false};
boolean[] land = new boolean[]{false};
List<BinaryMapDataObject> coastLines = new ArrayList<BinaryMapDataObject>();
List<BinaryMapDataObject> basemapCoastLines = new ArrayList<BinaryMapDataObject>();
int leftX = MapUtils.get31TileNumberX(cLeftLongitude);
int rightX = MapUtils.get31TileNumberX(cRightLongitude);
int bottomY = MapUtils.get31TileNumberY(cBottomLatitude);
int topY = MapUtils.get31TileNumberY(cTopLatitude);
BinaryMapIndexReader.SearchFilter searchFilter = new BinaryMapIndexReader.SearchFilter() {
TLongSet ids = new TLongHashSet();
MapIndex mi = readMapObjectsForRendering(zoom, renderingReq, tempResult, basemapResult, ids, count, ocean,
land, coastLines, basemapCoastLines, leftX, rightX, bottomY, topY);
int renderRouteDataFile = 0;
if (renderingReq.searchRenderingAttribute("showRoadMapsAttribute")) {
renderRouteDataFile = renderingReq.getIntPropertyValue(renderingReq.ALL.R_ATTR_INT_VALUE);
}
if (checkWhetherInterrupted()) {
return false;
}
if (renderRouteDataFile >= 0 && zoom >= BASEMAP_ZOOM ) {
searchRequest = BinaryMapIndexReader.buildSearchRequest(leftX, rightX, topY, bottomY, zoom, null);
for (BinaryMapIndexReader c : files.values()) {
// false positive case when we have 2 sep maps Country-roads & Country
if(c.getMapIndexes().size() == 0 || renderRouteDataFile == 1) {
readRouteDataAsMapObjects(searchRequest, c, tempResult, ids);
}
}
log.info(String.format("Route objects %s", tempResult.size() +""));
}
String coastlineTime = "";
boolean addBasemapCoastlines = true;
boolean emptyData = zoom > BASEMAP_ZOOM && tempResult.isEmpty() && coastLines.isEmpty();
boolean basemapMissing = zoom <= BASEMAP_ZOOM && basemapCoastLines.isEmpty() && mi == null;
boolean detailedLandData = zoom >= zoomForBaseRouteRendering && tempResult.size() > 0 && renderRouteDataFile < 0;
if (!coastLines.isEmpty()) {
long ms = System.currentTimeMillis();
boolean coastlinesWereAdded = processCoastlines(coastLines, leftX, rightX, bottomY, topY, zoom,
basemapCoastLines.isEmpty(), true, tempResult);
addBasemapCoastlines = (!coastlinesWereAdded && !detailedLandData) || zoom <= BASEMAP_ZOOM;
coastlineTime = "(coastline " + (System.currentTimeMillis() - ms) + " ms )";
} else {
addBasemapCoastlines = !detailedLandData;
}
if (addBasemapCoastlines) {
long ms = System.currentTimeMillis();
boolean coastlinesWereAdded = processCoastlines(basemapCoastLines, leftX, rightX, bottomY, topY, zoom,
true, true, tempResult);
addBasemapCoastlines = !coastlinesWereAdded;
coastlineTime = "(coastline " + (System.currentTimeMillis() - ms) + " ms )";
}
if (addBasemapCoastlines && mi != null) {
BinaryMapDataObject o = new BinaryMapDataObject(new int[]{leftX, topY, rightX, topY, rightX, bottomY, leftX, bottomY, leftX,
topY}, new int[]{ocean[0] && !land[0] ? mi.coastlineEncodingType : (mi.landEncodingType)}, null, -1);
o.setMapIndex(mi);
tempResult.add(o);
}
if (emptyData || basemapMissing) {
// message
MapIndex mapIndex;
if (!tempResult.isEmpty()) {
mapIndex = tempResult.get(0).getMapIndex();
} else {
mapIndex = new MapIndex();
mapIndex.initMapEncodingRule(0, 1, "natural", "coastline");
mapIndex.initMapEncodingRule(0, 2, "name", "");
}
}
if (zoom <= BASEMAP_ZOOM || emptyData) {
tempResult.addAll(basemapResult);
}
if (count[0] > 0) {
log.info(String.format("BLat=%s, TLat=%s, LLong=%s, RLong=%s, zoom=%s", //$NON-NLS-1$
cBottomLatitude, cTopLatitude, cLeftLongitude, cRightLongitude, zoom));
log.info(String.format("Searching: %s ms %s (%s results found)", System.currentTimeMillis() - now, coastlineTime, count[0])); //$NON-NLS-1$
}
cObjects = tempResult;
cObjectsBox = dataBox;
return true;
}
private MapIndex readMapObjectsForRendering(final int zoom, final RenderingRuleSearchRequest renderingReq,
ArrayList<BinaryMapDataObject> tempResult, ArrayList<BinaryMapDataObject> basemapResult,
TLongSet ids, int[] count, boolean[] ocean, boolean[] land, List<BinaryMapDataObject> coastLines,
List<BinaryMapDataObject> basemapCoastLines, int leftX, int rightX, int bottomY, int topY) {
BinaryMapIndexReader.SearchFilter searchFilter = new BinaryMapIndexReader.SearchFilter() {
@Override
public boolean accept(TIntArrayList types, BinaryMapIndexReader.MapIndex root) {
for (int j = 0; j < types.size(); j++) {
@ -318,11 +479,10 @@ public class MapRenderRepositories {
if (zoom > 16) {
searchFilter = null;
}
boolean ocean = false;
boolean land = false;
MapIndex mi = null;
searchRequest = BinaryMapIndexReader.buildSearchRequest(leftX, rightX, topY, bottomY, zoom, searchFilter);
for (BinaryMapIndexReader c : files.values()) {
boolean basemap = c.isBasemap();
searchRequest.clearSearchResults();
List<BinaryMapDataObject> res;
try {
@ -331,99 +491,52 @@ public class MapRenderRepositories {
res = new ArrayList<BinaryMapDataObject>();
log.debug("Search failed " + c.getRegionNames(), e); //$NON-NLS-1$
}
if(res.size() > 0) {
if(basemap) {
renderedState |= 1;
} else {
renderedState |= 2;
}
}
for (BinaryMapDataObject r : res) {
if (checkForDuplicateObjectIds) {
if (checkForDuplicateObjectIds && !basemap) {
if (ids.contains(r.getId()) && r.getId() > 0) {
// do not add object twice
continue;
}
ids.add(r.getId());
}
count++;
count[0]++;
if (r.containsType(r.getMapIndex().coastlineEncodingType)) {
if (c.isBasemap()) {
if (basemap) {
basemapCoastLines.add(r);
} else {
coastLines.add(r);
}
} else {
// do not mess coastline and other types
if (c.isBasemap()) {
if (basemap) {
basemapResult.add(r);
} else {
tempResult.add(r);
}
}
if (checkWhetherInterrupted()) {
return false;
return null;
}
}
if (searchRequest.isOcean()) {
mi = c.getMapIndexes().get(0);
ocean = true;
ocean[0] = true;
}
if (searchRequest.isLand()) {
mi = c.getMapIndexes().get(0);
land = true;
land[0] = true;
}
}
String coastlineTime = "";
boolean addBasemapCoastlines = true;
boolean emptyData = zoom > BASEMAP_ZOOM && tempResult.isEmpty() && coastLines.isEmpty();
boolean basemapMissing = zoom <= BASEMAP_ZOOM && basemapCoastLines.isEmpty() && mi == null;
boolean detailedLandData = zoom >= 14 && tempResult.size() > 0;
if (!coastLines.isEmpty()) {
long ms = System.currentTimeMillis();
boolean coastlinesWereAdded = processCoastlines(coastLines, leftX, rightX, bottomY, topY, zoom,
basemapCoastLines.isEmpty(), true, tempResult);
addBasemapCoastlines = (!coastlinesWereAdded && !detailedLandData) || zoom <= BASEMAP_ZOOM;
coastlineTime = "(coastline " + (System.currentTimeMillis() - ms) + " ms )";
} else {
addBasemapCoastlines = !detailedLandData;
}
if (addBasemapCoastlines) {
long ms = System.currentTimeMillis();
boolean coastlinesWereAdded = processCoastlines(basemapCoastLines, leftX, rightX, bottomY, topY, zoom,
true, true, tempResult);
addBasemapCoastlines = !coastlinesWereAdded;
coastlineTime = "(coastline " + (System.currentTimeMillis() - ms) + " ms )";
}
if (addBasemapCoastlines && mi != null) {
BinaryMapDataObject o = new BinaryMapDataObject(new int[]{leftX, topY, rightX, topY, rightX, bottomY, leftX, bottomY, leftX,
topY}, new int[]{ocean && !land ? mi.coastlineEncodingType : (mi.landEncodingType)}, null, -1);
o.setMapIndex(mi);
tempResult.add(o);
}
if (emptyData || basemapMissing) {
// message
MapIndex mapIndex;
if (!tempResult.isEmpty()) {
mapIndex = tempResult.get(0).getMapIndex();
} else {
mapIndex = new MapIndex();
mapIndex.initMapEncodingRule(0, 1, "natural", "coastline");
mapIndex.initMapEncodingRule(0, 2, "name", "");
}
}
if (zoom <= BASEMAP_ZOOM || emptyData) {
tempResult.addAll(basemapResult);
}
if (count > 0) {
log.info(String.format("BLat=%s, TLat=%s, LLong=%s, RLong=%s, zoom=%s", //$NON-NLS-1$
cBottomLatitude, cTopLatitude, cLeftLongitude, cRightLongitude, zoom));
log.info(String.format("Searching: %s ms %s (%s results found)", System.currentTimeMillis() - now, coastlineTime, count)); //$NON-NLS-1$
}
cObjects = tempResult;
cObjectsBox = dataBox;
return true;
return mi;
}
private void validateLatLonBox(QuadRect box) {
@ -442,60 +555,13 @@ public class MapRenderRepositories {
}
// only single thread to read !
public synchronized boolean checkIfMapIsEmpty(int leftX, int rightX, int topY, int bottomY, int zoom){
final boolean[] empty = new boolean[] {true};
SearchRequest<BinaryMapDataObject> searchRequest = BinaryMapIndexReader.buildSearchRequest(leftX, rightX, topY, bottomY, zoom,
null, new ResultMatcher<BinaryMapDataObject>() {
@Override
public boolean publish(BinaryMapDataObject object) {
empty[0] = false;
return false;
}
@Override
public boolean isCancelled() {
return !empty[0];
}
});
SearchRequest<RouteDataObject> searchRouteRequest = BinaryMapIndexReader.buildSearchRouteRequest(leftX, rightX, topY, bottomY,
new ResultMatcher<RouteDataObject>() {
@Override
public boolean publish(RouteDataObject object) {
empty[0] = false;
return false;
}
@Override
public boolean isCancelled() {
return !empty[0];
}
});
for (BinaryMapIndexReader c : files.values()) {
if (!c.isBasemap()) {
try {
c.searchMapIndex(searchRequest);
} catch (IOException e) {
// lots of FalsePositive cases
return false;
}
if (!empty[0]) {
return false;
}
for (RouteRegion r : c.getRoutingIndexes()) {
try {
List<RouteSubregion> regs = c.searchRouteIndexTree(searchRouteRequest, r.getSubregions());
if(!regs.isEmpty()) {
return false;
}
} catch (IOException e) {
// lots of FalsePositive cases
return false;
}
}
}
public boolean isLastMapRenderedEmpty(boolean checkBaseMap){
if(checkBaseMap) {
return prevBmp != null && previousRenderedState == 0;
} else {
return prevBmp != null && previousRenderedState == 1;
}
return empty[0];
}
public synchronized void loadMap(RotatedTileBox tileRect, List<IMapDownloaderCallback> notifyList) {
@ -539,10 +605,10 @@ public class MapRenderRepositories {
// prevent editing
requestedBox = new RotatedTileBox(tileRect);
// calculate data box
QuadRect dataBox = requestedBox.getLatLonBounds();
long now = System.currentTimeMillis();
if (cObjectsBox.left > dataBox.left || cObjectsBox.top > dataBox.top || cObjectsBox.right < dataBox.right
|| cObjectsBox.bottom < dataBox.bottom || (nativeLib != null) == (cNativeObjects == null)) {
// increase data box in order for rotate
@ -556,6 +622,7 @@ public class MapRenderRepositories {
dataBox.bottom -= hi;
}
validateLatLonBox(dataBox);
renderedState = 0;
boolean loaded;
if(nativeLib != null) {
cObjects = new LinkedList<BinaryMapDataObject>();
@ -618,6 +685,7 @@ public class MapRenderRepositories {
Bitmap reuse = prevBmp;
this.prevBmp = this.bmp;
this.prevBmpLocation = this.bmpLocation;
this.previousRenderedState = renderedState;
if (reuse != null && reuse.getWidth() == currentRenderingContext.width && reuse.getHeight() == currentRenderingContext.height) {
bmp = reuse;
bmp.eraseColor(currentRenderingContext.defaultColor);
@ -726,6 +794,7 @@ public class MapRenderRepositories {
cObjectsBox = new QuadRect();
requestedBox = prevBmpLocation = null;
previousRenderedState = 0;
// Do not clear main bitmap to not cause a screen refresh
// prevBmp = null;
// bmp = null;

View file

@ -165,7 +165,7 @@ public class DownloadedRegionsLayer extends OsmandMapLayer {
int right = MapUtils.get31TileNumberX(tileBox.getRightBottomLatLon().getLongitude());
int top = MapUtils.get31TileNumberY(tileBox.getLeftTopLatLon().getLatitude());
int bottom = MapUtils.get31TileNumberY(tileBox.getRightBottomLatLon().getLatitude());
final boolean empty = rm.getRenderer().checkIfMapIsEmpty(left, right, top, bottom, tileBox.getZoom());
final boolean empty = rm.getRenderer().isLastMapRenderedEmpty(false);
noMapsPresent = empty;
if (!empty && tileBox.getZoom() >= ZOOM_TO_SHOW_MAP_NAMES) {
return Collections.emptyList();