package net.osmand.plus.activities; import java.io.IOException; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.ParserConfigurationException; import net.osmand.LogUtil; import net.osmand.OsmAndFormatter; import net.osmand.binary.BinaryMapIndexReader; import net.osmand.binary.BinaryRouteDataReader; import net.osmand.binary.BinaryRouteDataReader.RouteSegment; import net.osmand.binary.BinaryRouteDataReader.RouteSegmentResult; import net.osmand.binary.BinaryRouteDataReader.RoutingContext; import net.osmand.osm.LatLon; import net.osmand.osm.MapUtils; import net.osmand.plus.OsmandSettings; import net.osmand.plus.OsmandSettings.ApplicationMode; import net.osmand.plus.R; import net.osmand.plus.activities.RoutingHelper.RouteDirectionInfo; import net.osmand.plus.activities.RoutingHelper.TurnType; import net.osmand.plus.render.MapRenderRepositories; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import pl.edu.agh.jsonrpc.JSONRPCException; import pl.edu.agh.model.RoutingResult; import pl.edu.agh.model.SimpleLocationInfo; import pl.edu.agh.service.TrafficService; import pl.edu.agh.service.TrafficServiceStub; import android.content.Context; import android.location.Location; public class RouteProvider { private static final org.apache.commons.logging.Log log = LogUtil.getLog(RouteProvider.class); public enum RouteService { CLOUDMADE("CloudMade"), YOURS("YOURS"), OSMAND("OsmAnd"), AGH("AGH"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ private final String name; private RouteService(String name){ this.name = name; } public String getName() { return name; } } private static final Map<String, String> ERROR_MAPPING = new HashMap<String, String>() { { put(TrafficService.NO_START_ROUTE_ERROR, "Start point is far from allowed road."); put(TrafficService.NO_END_ROUTE_ERROR, "End point is far from allowed road."); put(TrafficService.CALCULATING_ROUTE_ERROR, "Error during route calculating."); } }; public RouteProvider(){ } public static class RouteCalculationResult { private final List<Location> locations; private List<RouteDirectionInfo> directions; private final String errorMessage; private int[] listDistance = null; public RouteCalculationResult(String errorMessage) { this(null, null, null, null, errorMessage); } public RouteCalculationResult(List<Location> list, List<RouteDirectionInfo> directions, Location start, LatLon end, String errorMessage) { this.directions = directions; this.errorMessage = errorMessage; this.locations = list; if (list != null) { prepareResult(start, end); } } public List<Location> getLocations() { return locations; } public List<RouteDirectionInfo> getDirections() { return directions; } public int[] getListDistance() { return listDistance; } private void prepareResult(Location start, LatLon end) { if (locations != null && !locations.isEmpty()) { // if there is no closest points to start - add it if (locations.get(0).distanceTo(start) > 200) { // add start point locations.add(0, start); if (directions != null) { for (RouteDirectionInfo i : directions) { i.routePointOffset++; } RouteDirectionInfo info = new RouteDirectionInfo(); info.turnType = TurnType.valueOf(TurnType.C); info.routePointOffset = 0; info.descriptionRoute = "" ;//getString(ctx, R.string.route_head); //$NON-NLS-1$ directions.add(0, info); } } // check points for duplicates (it is very bad for routing) - cloudmade could return it for (int i = 0; i < locations.size() - 1; ) { if(locations.get(i).distanceTo(locations.get(i+1)) == 0){ locations.remove(i); if (directions != null) { for (RouteDirectionInfo info : directions) { if(info.routePointOffset > i){ info.routePointOffset--; } } } } else { i++; } } // Remove unnecessary go straight from CloudMade // Remove also last direction because it will be added after if(directions != null && directions.size() > 1){ for (int i = 1; i < directions.size(); ) { RouteDirectionInfo r = directions.get(i); if(r.turnType.getValue().equals(TurnType.C)){ RouteDirectionInfo prev = directions.get(i-1); prev.expectedTime += r.expectedTime; directions.remove(i); } else { i++; } } } } listDistance = new int[locations.size()]; if (!locations.isEmpty()) { listDistance[locations.size() - 1] = 0; for (int i = locations.size() - 1; i > 0; i--) { listDistance[i - 1] = (int) locations.get(i - 1).distanceTo(locations.get(i)); listDistance[i - 1] += listDistance[i]; } } if (directions != null) { int sum = 0; for (int i = directions.size() - 1; i >= 0; i--) { directions.get(i).afterLeftTime = sum; sum += directions.get(i).expectedTime; directions.get(i).distance = listDistance[directions.get(i).routePointOffset]; if(i < directions.size() - 1){ directions.get(i).distance -=listDistance[directions.get(i + 1).routePointOffset]; } } } } public boolean isCalculated(){ return locations != null && !locations.isEmpty(); } public String getErrorMessage(){ return errorMessage; } } public RouteCalculationResult calculateRouteImpl(Location start, LatLon end, ApplicationMode mode, RouteService type, Context ctx, List<Location> gpxRoute, boolean fast){ long time = System.currentTimeMillis(); if (start != null && end != null) { if(log.isInfoEnabled()){ log.info("Start finding route from " + start + " to " + end +" using " + type.getName()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } try { RouteCalculationResult res; if(gpxRoute != null && !gpxRoute.isEmpty()){ // get the closest point to start and to end float minDist = Integer.MAX_VALUE; int startI = 0; int endI = gpxRoute.size(); if (start != null) { for (int i = 0; i < gpxRoute.size(); i++) { float d = gpxRoute.get(i).distanceTo(start); if (d < minDist) { startI = i; minDist = d; } } } else { start = gpxRoute.get(0); } Location l = new Location("temp"); //$NON-NLS-1$ l.setLatitude(end.getLatitude()); l.setLongitude(end.getLongitude()); minDist = Integer.MAX_VALUE; for (int i = startI; i < gpxRoute.size(); i++) { float d = gpxRoute.get(i).distanceTo(l); if (d < minDist) { endI = i + 1; minDist = d; } } res = new RouteCalculationResult(new ArrayList<Location>(gpxRoute.subList(startI, endI)), null, start, end, null); addMissingTurnsToRoute(res, start, end, mode, ctx); } else if (type == RouteService.YOURS) { res = findYOURSRoute(start, end, mode, fast); addMissingTurnsToRoute(res, start, end, mode, ctx); } else if (type == RouteService.OSMAND) { res = findVectorMapsRoute(start, end, mode, fast, (OsmandApplication)ctx.getApplicationContext()); addMissingTurnsToRoute(res, start, end, mode, ctx); } else if (type == RouteService.AGH) { res = findAGHRoute(start, end, ctx); addMissingTurnsToRoute(res, start, end, mode, ctx); } else { res = findCloudMadeRoute(start, end, mode, ctx, fast); // for test purpose addMissingTurnsToRoute(res, start, end, mode, ctx); } if(log.isInfoEnabled() && res.locations != null){ log.info("Finding route contained " + res.locations.size() + " points for " + (System.currentTimeMillis() - time) + " ms"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } return res; } catch (IOException e) { log.error("Failed to find route ", e); //$NON-NLS-1$ } catch (ParserConfigurationException e) { log.error("Failed to find route ", e); //$NON-NLS-1$ } catch (SAXException e) { log.error("Failed to find route ", e); //$NON-NLS-1$ } } return new RouteCalculationResult(null); } protected RouteCalculationResult findAGHRoute(Location start, LatLon end, Context ctx) { try { boolean useTrafficDataToRoute = OsmandSettings.usingTrafficDataToRoute(ctx); RoutingResult result = TrafficServiceStub.getInstance().calculateRoute( new SimpleLocationInfo(start.getLongitude(), start.getLatitude()), new SimpleLocationInfo(end.getLongitude(), end.getLatitude()), useTrafficDataToRoute); List<Location> locations = retrieveLocationList(result); return new RouteCalculationResult(locations, null, start, end, null); } catch (JSONRPCException e) { String exceptionMessage = e.getMessage(); String guiMessage = ERROR_MAPPING.containsKey(exceptionMessage) ? ERROR_MAPPING.get(exceptionMessage) : exceptionMessage; return new RouteCalculationResult(guiMessage); } } private List<Location> retrieveLocationList(RoutingResult result) { List<Location> locations = new ArrayList<Location>(); for (SimpleLocationInfo info : result.getLocations()) { Location loc = new Location(""); loc.setLongitude(info.getLongitude()); loc.setLatitude(info.getLatitude()); locations.add(loc); } return locations; } protected String getString(Context ctx, int resId){ if(ctx == null){ return ""; //$NON-NLS-1$ } return ctx.getString(resId); } protected void addMissingTurnsToRoute(RouteCalculationResult res, Location start, LatLon end, ApplicationMode mode, Context ctx){ if(!res.isCalculated()){ return; } // speed m/s float speed = 1.5f; int minDistanceForTurn = 5; if(mode == ApplicationMode.CAR){ speed = 15.3f; minDistanceForTurn = 35; } else if(mode == ApplicationMode.BICYCLE){ speed = 5.5f; minDistanceForTurn = 12; } List<RouteDirectionInfo> directions = new ArrayList<RouteDirectionInfo>(); int[] listDistance = res.getListDistance(); List<Location> locations = res.getLocations(); int previousLocation = 0; int prevBearingLocation = 0; RouteDirectionInfo previousInfo = new RouteDirectionInfo(); previousInfo.turnType = TurnType.valueOf(TurnType.C); previousInfo.routePointOffset = 0; previousInfo.descriptionRoute = getString(ctx, R.string.route_head); directions.add(previousInfo); int distForTurn = 0; float previousBearing = 0; int startTurnPoint = 0; for (int i = 1; i < locations.size() - 1; i++) { Location next = locations.get(i + 1); Location current = locations.get(i); float bearing = current.bearingTo(next); // try to get close to current location if possible while(prevBearingLocation < i - 1){ if(locations.get(prevBearingLocation + 1).distanceTo(current) > 70){ prevBearingLocation ++; } else { break; } } if(distForTurn == 0){ // measure only after turn previousBearing = locations.get(prevBearingLocation).bearingTo(current); startTurnPoint = i; } TurnType type = null; String description = null; float delta = previousBearing - bearing; while(delta < 0){ delta += 360; } while(delta > 360){ delta -= 360; } distForTurn += locations.get(i).distanceTo(locations.get(i + 1)); if (i < locations.size() - 1 && distForTurn < minDistanceForTurn) { // For very smooth turn we try to accumulate whole distance // simply skip that turn needed for situation // 1) if you are going to have U-turn - not 2 left turns // 2) if there is a small gap between roads (turn right and after 4m next turn left) - so the direction head continue; } if(delta > 50 && delta < 310){ if(delta < 70){ type = TurnType.valueOf(TurnType.TSLL); description = getString(ctx, R.string.route_tsll); } else if(delta < 110){ type = TurnType.valueOf(TurnType.TL); description = getString(ctx, R.string.route_tl); } else if(delta < 135){ type = TurnType.valueOf(TurnType.TSHL); description = getString(ctx, R.string.route_tshl); } else if(delta < 225){ type = TurnType.valueOf(TurnType.TU); description = getString(ctx, R.string.route_tu); } else if(delta < 250){ description = getString(ctx, R.string.route_tshr); type = TurnType.valueOf(TurnType.TSHR); } else if(delta < 290){ description = getString(ctx, R.string.route_tr); type = TurnType.valueOf(TurnType.TR); } else { description = getString(ctx, R.string.route_tslr); type = TurnType.valueOf(TurnType.TSLR); } // calculate for previousRoute previousInfo.distance = listDistance[previousLocation]- listDistance[i]; previousInfo.expectedTime = (int) (previousInfo.distance / speed); previousInfo.descriptionRoute += " " + OsmAndFormatter.getFormattedDistance(previousInfo.distance, ctx); //$NON-NLS-1$ previousInfo = new RouteDirectionInfo(); previousInfo.turnType = type; previousInfo.turnType.setTurnAngle(360 - delta); previousInfo.descriptionRoute = description; previousInfo.routePointOffset = startTurnPoint; directions.add(previousInfo); previousLocation = startTurnPoint; prevBearingLocation = i; // for bearing using current location } // clear dist for turn distForTurn = 0; } previousInfo.distance = listDistance[previousLocation]; previousInfo.expectedTime = (int) (previousInfo.distance / speed); previousInfo.descriptionRoute += " " + OsmAndFormatter.getFormattedDistance(previousInfo.distance, ctx); //$NON-NLS-1$ // add last direction go straight (to show arrow in screen after all turns) if(previousInfo.distance > 80){ RouteDirectionInfo info = new RouteDirectionInfo(); info.expectedTime = 0; info.distance = 0; info.descriptionRoute = ""; //$NON-NLS-1$ info.turnType = TurnType.valueOf(TurnType.C); info.routePointOffset = locations.size() - 1; directions.add(info); } if(res.directions == null || res.directions.isEmpty()){ res.directions = new ArrayList<RouteDirectionInfo>(directions); } else { int currentDirection= 0; // one more for (int i = 0; i <= res.directions.size() && currentDirection < directions.size(); i++) { while(currentDirection < directions.size()){ int distanceAfter = 0; if (i < res.directions.size()) { RouteDirectionInfo resInfo = res.directions.get(i); int r1 = directions.get(currentDirection).routePointOffset; int r2 = resInfo.routePointOffset; distanceAfter = listDistance[resInfo.routePointOffset]; float dist = locations.get(r1).distanceTo(locations.get(r2)); // take into account that move roundabout is special turn that could be very lengthy if (dist < 100) { // the same turn duplicate currentDirection++; continue; // while cycle } else if (directions.get(currentDirection).routePointOffset > resInfo.routePointOffset) { // check it at the next point break; } } // add turn because it was missed RouteDirectionInfo toAdd = directions.get(currentDirection); float calcSpeed = toAdd.expectedTime == 0 ? speed :((float) toAdd.distance / toAdd.expectedTime); if(i > 0){ // update previous RouteDirectionInfo previous = res.directions.get(i - 1); calcSpeed = previous.expectedTime == 0 ? calcSpeed :((float) previous.distance / previous.expectedTime); previous.distance = listDistance[previous.routePointOffset] - listDistance[toAdd.routePointOffset]; previous.expectedTime = (int) ((float) previous.distance / calcSpeed); } toAdd.distance = listDistance[toAdd.routePointOffset] - distanceAfter; toAdd.expectedTime = (int) ((float) toAdd.distance / calcSpeed); if(i < res.directions.size()){ res.directions.add(i, toAdd); } else { res.directions.add(toAdd); } i++; currentDirection++; } } } int sum = 0; for (int i = res.directions.size() - 1; i >= 0; i--) { res.directions.get(i).afterLeftTime = sum; sum += res.directions.get(i).expectedTime; } } protected RouteCalculationResult findYOURSRoute(Location start, LatLon end, ApplicationMode mode, boolean fast) throws MalformedURLException, IOException, ParserConfigurationException, FactoryConfigurationError, SAXException { List<Location> res = new ArrayList<Location>(); StringBuilder uri = new StringBuilder(); uri.append("http://www.yournavigation.org/api/1.0/gosmore.php?format=kml"); //$NON-NLS-1$ uri.append("&flat=").append(start.getLatitude()); //$NON-NLS-1$ uri.append("&flon=").append(start.getLongitude()); //$NON-NLS-1$ uri.append("&tlat=").append(end.getLatitude()); //$NON-NLS-1$ uri.append("&tlon=").append(end.getLongitude()); //$NON-NLS-1$ if(ApplicationMode.PEDESTRIAN == mode){ uri.append("&v=foot") ; //$NON-NLS-1$ } else if(ApplicationMode.BICYCLE == mode){ uri.append("&v=bicycle") ; //$NON-NLS-1$ } else { uri.append("&v=motorcar"); //$NON-NLS-1$ } uri.append("&fast=").append(fast ? "1" : "0").append("&layer=mapnik"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ URL url = new URL(uri.toString()); URLConnection connection = url.openConnection(); DocumentBuilder dom = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = dom.parse(new InputSource(new InputStreamReader(connection.getInputStream()))); NodeList list = doc.getElementsByTagName("coordinates"); //$NON-NLS-1$ for(int i=0; i<list.getLength(); i++){ Node item = list.item(i); String str = item.getFirstChild().getNodeValue(); if(str == null){ continue; } int st = 0; int next = 0; while((next = str.indexOf('\n', st)) != -1){ String coordinate = str.substring(st, next + 1); int s = coordinate.indexOf(','); if (s != -1) { try { double lon = Double.parseDouble(coordinate.substring(0, s)); double lat = Double.parseDouble(coordinate.substring(s + 1)); Location l = new Location("router"); //$NON-NLS-1$ l.setLatitude(lat); l.setLongitude(lon); res.add(l); } catch (NumberFormatException e) { } } st = next + 1; } } if(list.getLength() == 0){ if(doc.getChildNodes().getLength() == 1){ Node item = doc.getChildNodes().item(0); return new RouteCalculationResult(item.getNodeValue()); } } return new RouteCalculationResult(res, null, start, end, null); } protected RouteCalculationResult findVectorMapsRoute(Location start, LatLon end, ApplicationMode mode, boolean fast, OsmandApplication app) throws IOException { // TODO consider mode, fast/short mode MapRenderRepositories repositories = app.getResourceManager().getRenderer(); Collection<BinaryMapIndexReader> data = repositories.getVectorData(); BinaryRouteDataReader router = new BinaryRouteDataReader(data.toArray(new BinaryMapIndexReader[data.size()])); RoutingContext ctx = new BinaryRouteDataReader.RoutingContext(); RouteSegment st= router.findRouteSegment(start.getLatitude(), start.getLongitude(), ctx); if (st == null) { return new RouteCalculationResult("Start point is far from allowed road."); } RouteSegment en = router.findRouteSegment(end.getLatitude(), end.getLongitude(), ctx); if (en == null) { return new RouteCalculationResult("End point is far from allowed road."); } List<Location> res = new ArrayList<Location>(); try { List<RouteSegmentResult> result = router.searchRoute(ctx, st, en); for (RouteSegmentResult s : result) { boolean plus = s.startPointIndex < s.endPointIndex; int i = s.startPointIndex; while (true) { Location n = new Location(""); //$NON-NLS-1$ n.setLatitude(MapUtils.get31LatitudeY(s.object.getPoint31YTile(i))); n.setLongitude(MapUtils.get31LongitudeX(s.object.getPoint31XTile(i))); res.add(n); if (i == s.endPointIndex) { break; } if (plus) { i++; } else { i--; } } } return new RouteCalculationResult(res, null, start, end, null); } catch (OutOfMemoryError e) { return new RouteCalculationResult("No enough memory to calculate route."); } } protected RouteCalculationResult findCloudMadeRoute(Location start, LatLon end, ApplicationMode mode, Context ctx, boolean fast) throws MalformedURLException, IOException, ParserConfigurationException, FactoryConfigurationError, SAXException { List<Location> res = new ArrayList<Location>(); List<RouteDirectionInfo> directions = null; StringBuilder uri = new StringBuilder(); // possibly hide that API key because it is privacy of osmand uri.append("http://routes.cloudmade.com/A6421860EBB04234AB5EF2D049F2CD8F/api/0.3/"); //$NON-NLS-1$ uri.append(start.getLatitude()+"").append(","); //$NON-NLS-1$ //$NON-NLS-2$ uri.append(start.getLongitude()+"").append(","); //$NON-NLS-1$ //$NON-NLS-2$ uri.append(end.getLatitude()+"").append(","); //$NON-NLS-1$//$NON-NLS-2$ uri.append(end.getLongitude()+"").append("/"); //$NON-NLS-1$ //$NON-NLS-2$ if (ApplicationMode.PEDESTRIAN == mode) { uri.append("foot.gpx"); //$NON-NLS-1$ } else if (ApplicationMode.BICYCLE == mode) { uri.append("bicycle.gpx"); //$NON-NLS-1$ } else { if(fast){ uri.append("car.gpx"); //$NON-NLS-1$ } else { uri.append("car/shortest.gpx"); //$NON-NLS-1$ } } uri.append("?lang=").append(Locale.getDefault().getLanguage()); //$NON-NLS-1$ URL url = new URL(uri.toString()); URLConnection connection = url.openConnection(); DocumentBuilder dom = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = dom.parse(new InputSource(new InputStreamReader(connection.getInputStream()))); // TODO how to find that error occurred ? API gpx doesn't say nothing NodeList list = doc.getElementsByTagName("wpt"); //$NON-NLS-1$ for (int i = 0; i < list.getLength(); i++) { Element item = (Element) list.item(i); try { Location l = new Location("router"); //$NON-NLS-1$ l.setLatitude(Double.parseDouble(item.getAttribute("lat"))); //$NON-NLS-1$ l.setLongitude(Double.parseDouble(item.getAttribute("lon"))); //$NON-NLS-1$ res.add(l); } catch (NumberFormatException e) { } } list = doc.getElementsByTagName("rtept"); //$NON-NLS-1$ if(list.getLength() > 0){ directions = new ArrayList<RouteDirectionInfo>(); } RouteDirectionInfo previous = null; for (int i = 0; i < list.getLength(); i++) { Element item = (Element) list.item(i); try { RouteDirectionInfo dirInfo = new RouteDirectionInfo(); dirInfo.descriptionRoute = getContentFromNode(item, "desc"); //$NON-NLS-1$ String stime = getContentFromNode(item, "time"); //$NON-NLS-1$ if(stime != null){ dirInfo.expectedTime = Integer.parseInt(stime); } String stype = getContentFromNode(item, "turn"); //$NON-NLS-1$ if(stype != null){ dirInfo.turnType = TurnType.valueOf(stype.toUpperCase()); } else { dirInfo.turnType = TurnType.valueOf(TurnType.C); } String sturn = getContentFromNode(item, "turn-angle"); //$NON-NLS-1$ if(sturn != null){ dirInfo.turnType.setTurnAngle((float) Double.parseDouble(sturn)); } int offset = Integer.parseInt(getContentFromNode(item, "offset")); //$NON-NLS-1$ dirInfo.routePointOffset = offset; if(previous != null && previous.turnType != null && !TurnType.C.equals(previous.turnType.getValue())){ // calculate angle if(previous.routePointOffset > 0){ float paz = res.get(previous.routePointOffset - 1).bearingTo(res.get(previous.routePointOffset)); float caz; if(previous.turnType.isRoundAbout() && dirInfo.routePointOffset < res.size() - 1){ caz = res.get(dirInfo.routePointOffset).bearingTo(res.get(dirInfo.routePointOffset + 1)); } else { caz = res.get(dirInfo.routePointOffset - 1).bearingTo(res.get(dirInfo.routePointOffset)); } float angle = caz - paz; if(angle < 0){ angle += 360; } else if(angle > 360){ angle -= 360; } // that magic number helps to fix some errors for turn angle += 75; if(previous.turnType.getTurnAngle() < 0.5f){ previous.turnType.setTurnAngle(angle); } } } directions.add(dirInfo); previous = dirInfo; } catch (NumberFormatException e) { log.info("Exception", e); //$NON-NLS-1$ } catch (IllegalArgumentException e) { log.info("Exception", e); //$NON-NLS-1$ } } if(previous != null && previous.turnType != null && !TurnType.C.equals(previous.turnType.getValue())){ // calculate angle if(previous.routePointOffset > 0 && previous.routePointOffset < res.size() - 1){ float paz = res.get(previous.routePointOffset - 1).bearingTo(res.get(previous.routePointOffset)); float caz = res.get(previous.routePointOffset).bearingTo(res.get(res.size() -1)); float angle = caz - paz; if(angle < 0){ angle += 360; } if(previous.turnType.getTurnAngle() < 0.5f){ previous.turnType.setTurnAngle(angle); } } } return new RouteCalculationResult(res, directions, start, end, null); } private String getContentFromNode(Element item, String tagName){ NodeList list = item.getElementsByTagName(tagName); if(list.getLength() > 0){ return list.item(0).getFirstChild().getNodeValue(); } return null; } }