001package votorola.s.gwt.scene.geo; // Copyright 2011-2012, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. 002 003import com.google.gwt.core.client.*; 004import com.google.web.bindery.event.shared.HandlerRegistration; 005import com.google.gwt.user.client.Cookies; 006import com.google.gwt.view.client.SelectionChangeEvent; 007import java.util.*; 008import org.gwtopenmaps.openlayers.client.*; 009import org.gwtopenmaps.openlayers.client.control.*; 010import org.gwtopenmaps.openlayers.client.event.*; 011import org.gwtopenmaps.openlayers.client.feature.*; 012import org.gwtopenmaps.openlayers.client.geometry.*; 013import org.gwtopenmaps.openlayers.client.layer.*; 014import org.gwtopenmaps.openlayers.client.layer.Vector; // over java.util.Vector 015import org.gwtopenmaps.openlayers.client.util.JSObject; 016import votorola.a.count.*; 017import votorola.a.web.gwt.*; 018import votorola.s.gwt.scene.*; 019import votorola.s.gwt.scene.feed.*; 020import votorola.s.gwt.stage.*; 021import votorola.g.hold.*; 022import votorola.g.lang.*; 023 024 025/** A GWT widget wrapping an OpenLayers geographic map and its views. Apparently 026 * OpenLayers does not allow for separate views of the same map to be placed here and 027 * there; all must be stacked (layered) together under a single DOM object. This is the 028 * widget that wraps that DOM object. It uses {@linkplain Geoscoping geoscoping}, 029 * q.v. for the format of the 's' scoping switch. 030 * 031 * @see <a href='http://reluk.ca/y/vw/xf/#c=DG'>Live example of a Geomap 032 * (right)</a> 033 */ 034public final class Geomap extends MapWidget implements Scene 035{ 036 037 038 /** Does nothing itself but the call forces static initialization of this class. 039 */ 040 public static void forceInitClass() {} 041 042 043 044 /** Constructs a Geomap. 045 * 046 * @param _spool the spool for the release of associated holds. When unwound it 047 * releases the holds of the geomap and thereby disables it. 048 */ 049 public Geomap( Spool _spool ) 050 { 051 super( /*width*/"100%", /*height*/"100%", newMapOptions() ); // options must be passed into constructor, or nothing is displayed (GWT-OpenLayers 0.5) 052 this.spool = _spool; 053 054 final org.gwtopenmaps.openlayers.client.Map map = getMap(); 055 spool.add( new Hold() 056 { 057 public void release() 058 { 059 try{ map.destroy(); } 060 catch( final JavaScriptException x ) 061 { 062 if( !GWT.isProdMode() && "NOT_FOUND_ERR".equals( x.getName() ) ) 063 { 064 System.out.println( 065 "suppressing known error (devmode under Chrome) during map.destroy(): " 066 + x ); 067 return; 068 } 069 throw x; 070 } 071 } 072 }); 073 074 // map.addControl( new MousePosition() ); 075 076 // Layers 077 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 078 { 079 final VectorOptions options = new VectorOptions(); 080 options.setStyle( newPersonStyle() ); 081 peopleVector = new Vector( "People", options ); 082 } 083 map.addLayer( peopleVector ); 084 { 085 init_baseLayer( map, OSM.CycleMap( "Cycle Map OSM" )); 086 final Layer defaultLayer; 087 { 088 final GoogleOptionsX go = new GoogleOptionsX(); 089 go.setType( googleMapTypeId_TERRAIN() ); 090 init_baseLayer( map, new Google( "Google Physical", go )); 091 init_baseLayer( map, new Google( "Google Roads" )); 092 go.setType( googleMapTypeId_HYBRID() ); 093 init_baseLayer( map, new Google( "Google Hybrid", go )); 094 go.setType( googleMapTypeId_SATELLITE() ); 095 init_baseLayer( map, defaultLayer = new Google( "Google Satellite", go )); 096 } 097 init_baseLayer( map, OSM.Mapnik( "Mapnik OSM" )); 098 // init_baseLayer( map, OSM.Osmarender( "Osmarender OSM" )); 099 ///// JavaScriptException: "$wnd.OpenLayers.Layer.OSM.Osmarender is not a constructor", 2012.3 100 // { 101 // final WMSParams p = new WMSParams(); 102 // p.setLayers( "basic" ); 103 // init_baseLayer( map, new WMS( "VMAP OSGeo WMS", 104 // "http://vmap0.tiles.osgeo.org/wms/vmap0", p )); 105 // } 106 ///// WMS doesn't use the spherical mercator projection, at least not by default 107 108 Layer initialLayer = null; 109 { 110 new BaseLayerPersister(); 111 final String cookie = Cookies.getCookie( BASE_LAYER_COOKIE ); 112 if( cookie != null ) initialLayer = map.getLayerByName( cookie ); 113 if( initialLayer == null ) 114 { 115 initialLayer = map.getLayerByName( getDefaultBaseLayerName() ); 116 } 117 if( initialLayer == null ) initialLayer = defaultLayer; 118 } 119 120 map.setBaseLayer( initialLayer ); 121 } 122 map.addControl( new LayerSwitcher() ); 123 124 // - - - 125 new BiteLighter(); 126 scoping = new Geoscoping( Geomap.this, spool ); 127 // Zooms and centers. Must at least zoom or map invisible. Must zoom late, or 128 // it has no effect. 129 } 130 131 132 133 private static void init_baseLayer( final org.gwtopenmaps.openlayers.client.Map map, 134 final Layer layer ) 135 { 136 // layer.setIsBaseLayer( true ); 137 /// that's usually the default 138 map.addLayer( layer ); 139 } 140 141 142 143 // ------------------------------------------------------------------------------------ 144 145 146 /** The default base layer for the geomap. The layer is specified by its name in the 147 * layer switcher. The state of the switcher is persisted through a cookie, so this 148 * value is only a default. If no value is set, then "Google Satellite" is used. 149 * 150 * @see #setDefaultBaseLayerName(String) 151 */ 152 public static @GWTConfigCallback String getDefaultBaseLayerName() { return defaultBaseLayerName; } 153 154 155 private static String defaultBaseLayerName = "Google Satellite"; // because it looks best 156 157 158 private static native void exposeDefaultBaseLayerName() 159 /*-{ 160 $wnd.s_gwt_scene_geo_Geomap_getDefaultBaseLayerName = $entry( 161 @votorola.s.gwt.scene.geo.Geomap::getDefaultBaseLayerName() ); 162 $wnd.s_gwt_scene_geo_Geomap_setDefaultBaseLayerName = $entry( 163 @votorola.s.gwt.scene.geo.Geomap::setDefaultBaseLayerName(Ljava/lang/String;) ); 164 }-*/; 165 166 167 static 168 { 169 assert SceneIn.isForcedInit(): "forced init " + Geomap.class.getName(); 170 exposeDefaultBaseLayerName(); 171 } 172 173 174 /** Sets the default base layer for the geomap. Call this method from the global 175 * configuration function {@linkplain SceneIn voGWTConfig.s_gwt_scene} in this 176 * manner:<pre 177 * 178 *> s_gwt_scene_geo_Geomap_setDefaultBaseLayerName( 'Osmarender OSM' ); 179 * // otherwise 'Google Satellite'</pre> 180 * 181 * @throws NullPointerException if s is null. 182 * @see #getDefaultBaseLayerName() 183 */ 184 public static @GWTConfigCallback void setDefaultBaseLayerName( final String s ) 185 { 186 if( s == null ) throw new NullPointerException(); // fail fast 187 188 defaultBaseLayerName = s; 189 } 190 191 192 193 /** Transforms from latitude/longitude coordinates (EPSG:4326) to the projection of 194 * this geomap, which is spherical mercator (EPSG:900913). 195 */ 196 void transformFromEPSG4326( final Point point ) // because GWT-OpenLayers lacks Point.transform() 197 { 198 // Cf. forwardMercator() in OpenLayers.Layer.SphericalMercator and 199 // org.gwtopenmaps.openlayers.client.layer.Google, which works with LonLat. 200 201 transform( point.getJSObject(), EPSG4326.getJSObject(), getMap().getJSObject() ); 202 } 203 204 205 206 // - M a p - ( votorola.s.gwt.scene ) ------------------------------------------------- 207 208 209 public boolean inScope( BiteJS bite ) { return scoping.inScope( bite ); } 210 211 212 213//// P r i v a t e /////////////////////////////////////////////////////////////////////// 214 215 216 private static final Projection EPSG4326 = new Projection( "EPSG:4326" ); // WGS84 with latitude/longitude coordinates 217 218 219 220 private static native String googleMapTypeId_HYBRID() 221 /*-{ 222 return $wnd.google.maps.MapTypeId.HYBRID; 223 }-*/; 224 225 private static native String googleMapTypeId_ROADMAP() 226 /*-{ 227 return $wnd.google.maps.MapTypeId.ROADMAP; 228 }-*/; 229 230 private static native String googleMapTypeId_SATELLITE() 231 /*-{ 232 return $wnd.google.maps.MapTypeId.SATELLITE; 233 }-*/; 234 235 private static native String googleMapTypeId_TERRAIN() 236 /*-{ 237 return $wnd.google.maps.MapTypeId.TERRAIN; 238 }-*/; 239 240 241 242 private static MapOptions newMapOptions() 243 { 244 final MapOptions o = new MapOptions(); 245 246 o.setProjection( "EPSG:900913" ); // incoming map data (spherical mercator) 247 // Expected as a string not a Projection, for some reason. You get the 248 // Projection as map.getProjectionObject() (not yet coded in GWT), but only 249 // after adding a base layer. 250 o.setUnits( "m" ); // spherical mercator uses meters 251 o.setDisplayProjection( EPSG4326 ); // outgoing meta-information for users 252 // o.setNumZoomLevels( 18 ); 253 254 // o.setMaxResolution( 156543.0339f ); 255 // o.setMaxExtent( new Bounds( -20037508.34, -20037508.34, 20037508.34, 20037508.344 )); 256 /// only needed if "using a standalone WMS or TMS layer" 257 // http://docs.openlayers.org/library/spherical_mercator.html 258 259 return o; 260 } 261 262 263 264 private static Style newPersonStyle() 265 { 266 final Style s = new Style(); // apparently cannot set CSS class, so must use this API 267 // s.setFillColor( "#CC4444" ); 268 // s.setFillOpacity( 0.2f ); 269 s.setFillOpacity( 0 ); 270 s.setPointRadius( 12/*pixels*/ ); 271 s.setStrokeColor( "#FF0000" ); 272 s.setStrokeOpacity( 0.7f ); 273 s.setStrokeWidth( 2/*pixels*/ ); 274 return s; 275 } 276 277 278 279 private final Vector peopleVector; 280 281 282 283 private final Geoscoping scoping; 284 285 286 287 private final Spool spool; 288 289 290 291 private static native void transform( JSObject object, JSObject source, JSObject map ) 292 /*-{ 293 object.transform( source, map.getProjectionObject() ); 294 }-*/; 295 296 297 298 // ==================================================================================== 299 300 301 private static final String BASE_LAYER_COOKIE = "voGeomap.initialBaseLayer"; 302 303 304 305 private final class BaseLayerPersister 306 { 307 308 /** Constructs a BaseLayerPersister. 309 */ 310 BaseLayerPersister() 311 { 312 // Registration key in GWT-OpenLayers is listener itself, so we must implement 313 // a separate listener for each event type. 314 final MapBaseLayerChangedListener l = new MapBaseLayerChangedListener() 315 { 316 public void onBaseLayerChanged( 317 final MapBaseLayerChangedListener.MapBaseLayerChangedEvent e ) 318 { 319 final Layer layer = e.getLayer(); 320 if( layer == null ) return; 321 322 final Date expiryDate = new Date( System.currentTimeMillis() + 1000L/*ms per s*/ 323 * 3600/*s per hour*/ * 24/*hours per day*/ * 30/*days per month*/ 324 * 6/*months*/ ); 325 assert Cookies.getUriEncode(): "cookies automatically URI encoded"; 326 Cookies.setCookie( BASE_LAYER_COOKIE, layer.getName(), expiryDate ); 327 } 328 }; 329 getMap().addMapBaseLayerChangedListener( l ); 330 spool.add( new Hold() 331 { 332 public void release() { getMap().removeListener( l ); } 333 }); 334 } 335 336 337 } 338 339 340 341 // ==================================================================================== 342 343 344 private final class BiteLighter implements SelectionChangeEvent.Handler, TheatreInitializer 345 { 346 347 BiteLighter() { Stage.i().addInitializer( BiteLighter.this ); } // auto-removed 348 349 350 private void init() 351 { 352 if( spool.isUnwinding() ) return; 353 354 spool.add( new Hold() 355 { 356 final HandlerRegistration hR = 357 Scenes.i().biteSelection().addSelectionChangeHandler( BiteLighter.this ); 358 public void release() { hR.removeHandler(); } 359 }); 360 relight(); // init state 361 } 362 363 364 // -------------------------------------------------------------------------------- 365 366 367 private void clear( final Stage stage ) 368 { 369 Stage.setActorName( null ); 370 stage.setPollName( null ); 371 } 372 373 374 private final Style firstPersonStyle = newPersonStyle(); 375 376 { 377 firstPersonStyle.setStrokeOpacity( 1 ); // emphasized 378 firstPersonStyle.setStrokeWidth( 3/*pixels*/ ); 379 } 380 381 382 383 private void relight() 384 { 385 final BiteJS bite = Scenes.i().biteSelection().getSelectedObject(); 386 // if( SAME GEOLOCS AS LAST TIME )) return; // OPT if it's ever needed 387 peopleVector.destroyFeatures(); 388 final Stage stage = Stage.i(); 389 if( bite == null ) 390 { 391 clear( stage ); 392 return; 393 } 394 395 final Poll poll = bite.poll(); 396 if( poll != null ) stage.setPollName( poll.name() ); 397 final JsArray<PersonJS> persons = bite.persons(); 398 for( int p = 0, pN = persons.length(); p < pN; ++p ) 399 { 400 final Person person = persons.get( p ); 401 if( p == 0 ) Stage.setActorName( person.username() ); // put first person on stage 402 final Residence res = person.residence(); 403 if( res == null ) continue; 404 405 final Point point = new Point( res.lon(), res.lat() ); 406 transformFromEPSG4326( point ); 407 final VectorFeature feature = new VectorFeature( point ); 408 if( p == 0 ) feature.setStyle( firstPersonStyle ); 409 peopleVector.addFeature( feature ); 410 } 411 } 412 413 414 // - S e l e c t i o n - C h a n g e - E v e n t . H a n d l e r ------------------ 415 416 417 public void onSelectionChange( SelectionChangeEvent _e ) { relight(); } 418 419 420 // - T h e a t r e - I n i t i a l i z e r ---------------------------------------- 421 422 423 public void initFrom( Stage _s, boolean _isReferencePending ) { init(); } 424 // Does the reverse (init to) for the same reason documented for 425 // s.gwt.scene.BiteStager.initFrom. 426 427 428 public void initFromComplete( Stage _stage, boolean _isReferencePending ) {} 429 430 431 public void initTo( Stage _s ) { init(); } 432 433 434 public void initTo( Stage _s, TheatrePage _referrer ) { init(); } 435 436 437 public void initToComplete( Stage _s, boolean _isReferencePending ) {} 438 439 440 public void initUltimately( Stage _s, TheatrePage _referrer ) {} 441 442 } 443 444 445 446 // ==================================================================================== 447 448 449 private static final class GoogleOptionsX extends GoogleOptions 450 { 451 452 public void setType( final String type ) { getJSObject().setProperty( "type", type ); } 453 454 } 455 456 457}