001: /* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
002: * This code is licensed under the GPL 2.0 license, availible at the root
003: * application directory.
004: */
005: package org.vfny.geoserver.wms.servlets;
006:
007: import com.vividsolutions.jts.geom.Envelope;
008:
009: import org.geoserver.ows.util.KvpUtils;
010: import org.geoserver.ows.util.RequestUtils;
011: import org.geoserver.platform.GeoServerExtensions;
012: import org.geotools.styling.Style;
013: import org.springframework.context.ApplicationContext;
014: import org.springframework.web.context.support.WebApplicationContextUtils;
015: import org.vfny.geoserver.Response;
016: import org.vfny.geoserver.ServiceException;
017: import org.vfny.geoserver.config.WMSConfig;
018: import org.vfny.geoserver.global.GeoServer;
019: import org.vfny.geoserver.global.MapLayerInfo;
020: import org.vfny.geoserver.global.WMS;
021: import org.vfny.geoserver.util.requests.readers.KvpRequestReader;
022: import org.vfny.geoserver.util.requests.readers.XmlRequestReader;
023: import org.vfny.geoserver.wms.requests.GetKMLReflectKvpReader;
024: import org.vfny.geoserver.wms.requests.GetMapRequest;
025: import org.vfny.geoserver.wms.responses.GetMapResponse;
026: import org.vfny.geoserver.wms.responses.map.kml.KMLMapProducerFactory;
027: import java.io.BufferedOutputStream;
028: import java.io.IOException;
029: import java.util.ArrayList;
030: import java.util.Enumeration;
031: import java.util.HashMap;
032: import java.util.List;
033: import java.util.Map;
034: import java.util.logging.Logger;
035: import javax.servlet.ServletConfig;
036: import javax.servlet.ServletException;
037: import javax.servlet.http.HttpServletRequest;
038: import javax.servlet.http.HttpServletResponse;
039:
040: /**
041: * This class takes in a simple WMS request, presumably from Google Earth, and
042: * produces a completed WMS request that outputs KML/KMZ. To map a request to
043: * this, simple pass a "layers=myLayer" parameter to "wms/kml_reflect":
044: * <b>http://localhost:8080/geoserver/wms/kml_reflect?layers=states<b>
045: * No extra information, such as styles or EPSG code need to be passed.
046: * A request to kml_reflect will return a network link for each layer
047: * passed in. Each network layer makes a full WMS request with these
048: * default values:
049: * - smart KMZ output (vector or raster output)
050: * - KMScore value of 30
051: * - Image size of 1024x1024
052: * - Full attribution on vector features
053: * - WMS version 1.0.0
054: * - Transparent
055: *
056: *
057: * @author Brent Owens
058: *
059: * @deprecated use {@link org.vfny.geoserver.wms.responses.map.kml.KMLReflector}.
060: *
061: */
062: public class KMLReflector extends WMService {
063: private static Logger LOGGER = org.geotools.util.logging.Logging
064: .getLogger("org.vfny.geoserver.wms.servlets");
065: final String KML_MIME_TYPE = "application/vnd.google-earth.kml+xml";
066: final String KMZ_MIME_TYPE = "application/vnd.google-earth.kmz+xml";
067:
068: // Values for the prepared WMS request. Later move these to web.xml server config
069: final int KMSCORE = 50;
070: final int REFRESH = 1;
071: final boolean KMATTR = true;
072: final boolean TRANSPARENT = true;
073: final int WIDTH = 1024;
074: final int HEIGHT = 1024;
075: final String VERSION = "1.0.0";
076: final String SRS = "EPSG:4326";
077: final String DEFAULT_BBOX = "-180,-90,180,90";
078:
079: public KMLReflector() {
080: super ("kml_reflect", null);
081: }
082:
083: public void init(ServletConfig config) throws ServletException {
084: super .init(config);
085: setWMS((WMS) config.getServletContext().getAttribute(
086: WMS.WEB_CONTAINER_KEY));
087: }
088:
089: protected Response getResponseHandler() {
090: ApplicationContext context = WebApplicationContextUtils
091: .getWebApplicationContext(getServletContext());
092:
093: //return new GetMapResponse(config);
094: return new GetMapResponse(getWMS(), context);
095: }
096:
097: protected KvpRequestReader getKvpReader(Map params) {
098: return new GetKMLReflectKvpReader(params, this );
099: }
100:
101: protected XmlRequestReader getXmlRequestReader() {
102: /**
103: * @todo Implement this org.vfny.geoserver.servlets.AbstractService
104: * abstract method
105: */
106: throw new java.lang.UnsupportedOperationException(
107: "Method getXmlRequestReader() not yet implemented.");
108: }
109:
110: /**
111: * This will create a <folder> with <networklinks>, one network link
112: * for each layer.
113: * The only mandatory parameter is "layers" with the layer names.
114: * Styles is optional, and so are all other parameters such as:
115: * KMScore
116: * KMAttr
117: * Version
118: * Width
119: * Height
120: * SRS
121: *
122: * The result is written to the buffered output stream.
123: */
124: public void doGet(HttpServletRequest request,
125: HttpServletResponse response) throws ServletException,
126: IOException {
127: //set to KML mime type, so GEarth opens automatically
128: response.setContentType(KMLMapProducerFactory.MIME_TYPE);
129:
130: // the output stream we will write to
131: BufferedOutputStream out = new BufferedOutputStream(response
132: .getOutputStream());
133:
134: Map requestParams = new HashMap();
135: String paramName;
136: String paramValue;
137:
138: // gather the parameters
139: for (Enumeration pnames = request.getParameterNames(); pnames
140: .hasMoreElements();) {
141: paramName = (String) pnames.nextElement();
142: paramValue = request.getParameter(paramName);
143: requestParams.put(paramName.toUpperCase(), paramValue);
144: }
145:
146: // Some settings for network links don't pass in a bounding box, so we will
147: // supply one in that case that covers the whole world (pray your data isn't big and
148: // the KMScore value isn't large)
149: if (!requestParams.containsKey("BBOX")) {
150: requestParams.put("BBOX", DEFAULT_BBOX);
151: }
152:
153: KvpRequestReader requestReader = getKvpReader(requestParams);
154:
155: GetMapRequest serviceRequest;
156:
157: try {
158: serviceRequest = (GetMapRequest) requestReader
159: .getRequest(request);
160: } catch (ServiceException e) {
161: e.printStackTrace();
162:
163: return;
164: }
165:
166: final MapLayerInfo[] layers = serviceRequest.getLayers();
167: LOGGER.info("KML NetworkLink sharing " + layers.length
168: + " layer(s) created.");
169:
170: Style[] styles = null;
171:
172: if ((serviceRequest.getStyles() != null)
173: && !serviceRequest.getStyles().isEmpty()) {
174: styles = (Style[]) serviceRequest.getStyles().toArray(
175: new Style[] {});
176: }
177:
178: // fill in our default values for the request if the user didn't pass the value in
179: if (!requestParams.containsKey("TRANSPARENT")) {
180: serviceRequest.setTransparent(TRANSPARENT);
181: }
182:
183: if (!requestParams.containsKey("KMATTR")) {
184: serviceRequest.setKMattr(KMATTR);
185: }
186:
187: if (!requestParams.containsKey("KMSCORE")) {
188: serviceRequest.setKMScore(KMSCORE);
189: }
190:
191: if (!requestParams.containsKey("WIDTH")) {
192: serviceRequest.setWidth(WIDTH);
193: }
194:
195: if (!requestParams.containsKey("HEIGHT")) {
196: serviceRequest.setHeight(HEIGHT);
197: }
198:
199: if (!requestParams.containsKey("VERSION")) {
200: serviceRequest.setVersion(VERSION);
201: }
202:
203: List filters = null;
204: String filterKey = null;
205: if (requestParams.containsKey("FILTER")) {
206: String filter = (String) requestParams.get("FILTER");
207: filters = KvpUtils.readFlat(filter,
208: KvpUtils.OUTER_DELIMETER);
209: filterKey = "filter";
210: } else if (requestParams.containsKey("CQL_FILTER")) {
211: String filter = (String) requestParams.get("CQL_FILTER");
212: filters = KvpUtils.readFlat(filter, KvpUtils.CQL_DELIMITER);
213: filterKey = "cql_filter";
214: } else if (requestParams.containsKey("FEATUREID")) {
215: //JD: featureid semantics slightly different then other types of
216: // filters
217: String filter = (String) requestParams.get("FEATUREID");
218: filters = new ArrayList();
219: for (int i = 0; i < layers.length; i++) {
220: filters.add(filter);
221: }
222: filterKey = "featureid";
223: }
224:
225: if (filters != null && filters.size() != layers.length) {
226: throw (IOException) new IOException()
227: .initCause(new ServiceException(layers.length
228: + " layers specified, but "
229: + filters.size() + " filters"));
230: }
231:
232: //set the content disposition
233: StringBuffer filename = new StringBuffer();
234: for (int i = 0; i < layers.length; i++) {
235: String name = layers[i].getName();
236:
237: //strip off prefix
238: int j = name.indexOf(':');
239: if (j > -1) {
240: name = name.substring(j + 1);
241: }
242:
243: filename.append(name + "_");
244: }
245: filename.setLength(filename.length() - 1);
246:
247: response.setHeader("Content-Disposition",
248: "attachment; filename=" + filename.toString() + ".kml");
249:
250: // we use the mandatory SRS value of 4326 (lat/lon)
251: serviceRequest.setFormat(KML_MIME_TYPE); // output mime type of KML
252:
253: StringBuffer sb = new StringBuffer();
254: sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
255: sb.append("<kml xmlns=\"http://earth.google.com/kml/2.0\">\n");
256:
257: sb.append("<Folder>\n");
258:
259: String proxifiedBaseUrl = RequestUtils.baseURL(request);
260: GeoServer gs = (GeoServer) GeoServerExtensions.extensions(
261: GeoServer.class).get(0);
262: proxifiedBaseUrl = RequestUtils.proxifiedBaseURL(
263: proxifiedBaseUrl, gs.getProxyBaseUrl());
264:
265: Envelope layerbbox = null;
266:
267: // make a network link for every layer
268: for (int i = 0; i < layers.length; i++) {
269: //if (layers[i].getType() == MapLayerInfo.TYPE_VECTOR) {
270: String style = "&styles="
271: + layers[i].getDefaultStyle().getName();
272:
273: if ((styles != null) && (styles.length >= (i + 1))) { // if the user specified styles
274: style = "&styles=" + styles[i].getName(); // use them, else we use the default style
275: }
276:
277: String filter = (String) (filters != null ? filters.get(i)
278: : null);
279: if (filter != null) {
280: filter = "&" + filterKey + "=" + filter;
281: } else {
282: filter = "";
283: }
284:
285: //Envelope le = (layers[i].getFeature() == null ? layers[i].getBoundingBox() : layers[i].getFeature().getLatLongBoundingBox());
286: Envelope le = layers[i].getLatLongBoundingBox();
287: if (layerbbox == null) {
288: layerbbox = new Envelope(le);
289: } else {
290: layerbbox.expandToInclude(le);
291: }
292:
293: if (serviceRequest.getSuperOverlay()) {
294: Envelope bbox = serviceRequest.getBbox();
295:
296: sb.append("<NetworkLink>\n");
297: sb.append("<name>" + layers[0].getName() + "</name>\n");
298: sb.append("<Region>");
299: sb.append("<LatLonAltBox>");
300: sb.append("<north>" + bbox.getMaxY() + "</north>");
301: sb.append("<south>" + bbox.getMinY() + "</south>");
302: sb.append("<east>" + bbox.getMaxX() + "</east>");
303: sb.append("<west>" + bbox.getMinX() + "</west>");
304: sb.append("</LatLonAltBox>");
305: sb.append("<Lod>");
306: sb.append("<minLodPixels>256</minLodPixels>");
307: sb.append("<maxLodPixels>-1</maxLodPixels>");
308: sb.append("</Lod>");
309: sb.append("</Region>");
310:
311: sb.append("<Link>\n");
312: sb
313: .append("<href><![CDATA["
314: + proxifiedBaseUrl
315: + "/wms?service=WMS&request=GetMap&format=application/vnd.google-earth.kml+XML"
316: + "&width="
317: + WIDTH
318: + "&height="
319: + HEIGHT
320: + "&srs="
321: + SRS
322: + "&layers="
323: + layers[i].getName()
324: + style
325: + "&bbox="
326: + (String) requestParams.get("BBOX")
327: + filter
328: + "&legend="
329: + String.valueOf(serviceRequest
330: .getLegend())
331: + "&superoverlay=true]]></href>\n");
332: sb
333: .append("<viewRefreshMode>onRegion</viewRefreshMode>\n");
334:
335: sb.append("</Link>\n");
336: sb.append("</NetworkLink>\n");
337: } else {
338:
339: sb.append("<NetworkLink>\n");
340: sb.append(getLookAt(le));
341: sb.append("<name>" + layers[i].getName() + "</name>\n");
342: sb.append("<open>1</open>\n");
343: sb.append("<visibility>1</visibility>\n");
344: sb.append("<Url>\n");
345: sb
346: .append("<href><![CDATA["
347: + proxifiedBaseUrl
348: + "/wms?service=WMS&request=GetMap&format=application/vnd.google-earth.kmz+XML"
349: + "&width="
350: + WIDTH
351: + "&height="
352: + HEIGHT
353: + "&srs="
354: + SRS
355: + "&layers="
356: + layers[i].getName()
357: + style
358: + filter // optional
359: + "&KMScore="
360: + serviceRequest.getKMScore()
361: + "&KMAttr="
362: + String.valueOf(serviceRequest
363: .getKMattr())
364: + "&legend="
365: + String.valueOf(serviceRequest
366: .getLegend()) + "]]></href>\n");
367: sb
368: .append("<viewRefreshMode>onStop</viewRefreshMode>\n");
369: sb.append("<viewRefreshTime>" + REFRESH
370: + "</viewRefreshTime>\n");
371: sb.append("</Url>\n");
372: sb.append("</NetworkLink>\n");
373: }
374: }
375:
376: sb.append(getLookAt(layerbbox));
377:
378: sb.append("</Folder>\n");
379:
380: sb.append("</kml>\n");
381:
382: byte[] kml_b = sb.toString().getBytes();
383: out.write(kml_b);
384: out.flush();
385: }
386:
387: private String getLookAt(Envelope e) {
388: double lon1 = e.getMinX();
389: double lat1 = e.getMinY();
390: double lon2 = e.getMaxX();
391: double lat2 = e.getMaxY();
392:
393: double R_EARTH = 6.371 * 1000000; // meters
394: double VIEWER_WIDTH = 22 * Math.PI / 180; // The field of view of the google maps camera, in radians
395: double[] p1 = getRect(lon1, lat1, R_EARTH);
396: double[] p2 = getRect(lon2, lat2, R_EARTH);
397: double[] midpoint = new double[] { (p1[0] + p2[0]) / 2,
398: (p1[1] + p2[1]) / 2, (p1[2] + p2[2]) / 2 };
399:
400: midpoint = getGeographic(midpoint[0], midpoint[1], midpoint[2]);
401:
402: // averaging the longitudes; using the rectangular coordinates makes the calculated center tend toward the corner that's closer to the equator.
403: midpoint[0] = ((lon1 + lon2) / 2);
404:
405: double distance = distance(p1, p2);
406:
407: double height = distance / (2 * Math.tan(VIEWER_WIDTH));
408:
409: LOGGER.fine("lat1: " + lat1 + "; lon1: " + lon1);
410: LOGGER.fine("lat2: " + lat2 + "; lon2: " + lon2);
411: LOGGER.fine("latmid: " + midpoint[1] + "; lonmid: "
412: + midpoint[0]);
413:
414: return "<LookAt id=\"geoserver\">"
415: + " <longitude>"
416: + ((lon1 + lon2) / 2)
417: + "</longitude> <!-- kml:angle180 -->"
418: + " <latitude>"
419: + midpoint[1]
420: + "</latitude> <!-- kml:angle90 -->"
421: + " <altitude>0</altitude> <!-- double --> "
422: + " <range>"
423: + distance
424: + "</range> <!-- double -->"
425: + " <tilt>0</tilt> <!-- float -->"
426: + " <heading>0</heading> <!-- float -->"
427: + " <altitudeMode>clampToGround</altitudeMode> "
428: + " <!--kml:altitudeModeEnum:clampToGround, relativeToGround, absolute -->"
429: + "</LookAt>";
430: }
431:
432: private double[] getRect(double lat, double lon, double radius) {
433: double theta = (90 - lat) * Math.PI / 180;
434: double phi = (90 - lon) * Math.PI / 180;
435:
436: double x = radius * Math.sin(phi) * Math.cos(theta);
437: double y = radius * Math.sin(phi) * Math.sin(theta);
438: double z = radius * Math.cos(phi);
439: return new double[] { x, y, z };
440: }
441:
442: private double[] getGeographic(double x, double y, double z) {
443: double theta, phi, radius;
444: radius = distance(new double[] { x, y, z }, new double[] { 0,
445: 0, 0 });
446: theta = Math.atan2(Math.sqrt(x * x + y * y), z);
447: phi = Math.atan2(y, x);
448:
449: double lat = 90 - (theta * 180 / Math.PI);
450: double lon = 90 - (phi * 180 / Math.PI);
451:
452: return new double[] { (lon > 180 ? lon - 360 : lon), lat,
453: radius };
454: }
455:
456: private double distance(double[] p1, double[] p2) {
457: double dx = p1[0] - p2[0];
458: double dy = p1[1] - p2[1];
459: double dz = p1[2] - p2[2];
460: return Math.sqrt(dx * dx + dy * dy + dz * dz);
461: }
462: }
|