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}