001package votorola.s.gwt.scene.geo; // Copyright 2011, 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.gwt.event.logical.shared.*;
005import com.google.web.bindery.event.shared.HandlerRegistration;
006import com.google.gwt.regexp.shared.*;
007import com.google.gwt.user.client.Window;
008import org.gwtopenmaps.openlayers.client.*;
009import org.gwtopenmaps.openlayers.client.event.*;
010import org.gwtopenmaps.openlayers.client.geometry.*;
011import votorola.a.web.gwt.*;
012import votorola.s.gwt.scene.*;
013import votorola.s.gwt.scene.feed.*;
014import votorola.g.hold.*;
015import votorola.g.lang.*;
016import votorola.g.web.gwt.*;
017
018
019/** A scoping model based on the viewport of an OpenLayers geomap.  The scoping state is
020  * exposed in the {@linkplain Scenes#sScopingSwitch() 's' switch}:
021  *
022  * <table class='definition' style='margin-left:1em'>
023  *     <tr>
024  *         <th class='key'>Switch</th>
025  *         <th>Controlled state</th>
026  *         <th>Default</th>
027  *         </tr>
028  *     <tr><td class='key'>s</td>
029  *
030  *         <td>The scoping state.  A colon-separated list of three numbers: 1) the zoom
031  *         level; 2) the x cooridinate in meters; and 3) the y coordinate in meters.</td>
032  *
033  *         <td>"2:0:0"</td>
034  *
035  *         </tr>
036  *     </table>
037  *
038  *     @see Scoping
039  */
040public class Geoscoping implements Scoping
041{
042
043
044    /** Constructs a Geoscoping.
045      *
046      *     @param _geomap the geomap whose viewport controls the scoping switch.
047      *     @param spool the spool for the release of associated holds.  When unwound it
048      *       releases the holds of the scoping model and thereby disables it.
049      */
050    public Geoscoping( Geomap _geomap, final Spool spool )
051    {
052        geomap = _geomap;
053        map = geomap.getMap();
054
055        spool.add( new Hold()
056        {
057            final HandlerRegistration hR = Scenes.i().sScopingSwitch().addHandler(
058              new ValueChangeHandler<String>()
059            {
060                public void onValueChange( final ValueChangeEvent<String> e )
061                {
062                    GWTX.i().bus().fireEventFromSource( new ScopeChangeEvent(), Geoscoping.this );
063                    if( reboundSuppress ) return;
064
065                    rebound( e.getValue(), /*isInit*/false );
066                }
067            });
068            public void release() { hR.removeHandler(); }
069        });
070     // Scheduler.get().scheduleDeferred( new Scheduler.ScheduledCommand()
071     // {
072     //     // initial panning fails if executed immediately or finally, so defer it
073     //     public void execute() // later, after GWT dispatch loop
074     //     {
075                rebound( Scenes.i().sScopingSwitch().get(), /*isInit*/true ); // init state
076
077     //     }
078     // });
079     ///// not necessary provided rebound() init calls map.setCenter() instead of panTo()
080        historyStacker = new HistoryStacker( spool );
081    }
082
083
084
085   // ------------------------------------------------------------------------------------
086
087
088    /** Tests whether the specified bite is within the current scope.
089      */
090    public boolean inScope( final BiteJS bite )
091    {
092        final JsArray<PersonJS> persons = bite.persons();
093        for( int p = 0, pN = persons.length(); p < pN; ++p )
094        {
095            final Residence res = persons.get(p).residence();
096            if( res == null ) continue;
097
098            final Bounds bounds = map.getExtent();
099            final Point point = new Point( res.lon(), res.lat() );
100            geomap.transformFromEPSG4326( point );
101            final double x = point.getX();
102            final double y = point.getY();
103            if( x >= bounds.getLowerLeftX() && x <= bounds.getUpperRightX()
104             && y >= bounds.getLowerLeftY() && y <= bounds.getUpperRightY() ) return true;
105        }
106
107        return false; // no person in scope
108    }
109
110
111
112   // - O b j e c t ----------------------------------------------------------------------
113
114
115    public @Override String toString()
116    {
117        final LonLat center = map.getCenter();
118        return "geoscope = zoom(" + map.getZoom() + ") longitude(" + center.lon() + ") latitude(" + center.lat() + ")";
119    }
120
121
122
123   // - S c o p i n g --------------------------------------------------------------------
124
125
126    public HandlerRegistration addHandler( final ScopeChangeHandler handler )
127    {
128        return GWTX.i().bus().addHandlerToSource( ScopeChangeEvent.TYPE, /*source*/Geoscoping.this,
129          handler );
130    }
131
132
133
134//// P r i v a t e ///////////////////////////////////////////////////////////////////////
135
136
137    private static final LonLat CENTER_DEFAULT;
138
139
140
141    private final Geomap geomap;
142
143
144
145    private final Map map;
146
147
148
149    private @Warning("init call") void rebound( final String s, final boolean isInit )
150    {
151        restackSuppress = true;
152        try
153        {
154            if( s == null )
155            {
156                reboundToDefault();
157                return;
158            }
159
160            final MatchResult m = S_PATTERN.exec( s );
161            if( m == null )
162            {
163                Window.alert( "Malformed scoping switch: s=" + s );
164                reboundToDefault();
165                return;
166            }
167
168            try
169            {
170                final int zoom = Integer.parseInt( m.getGroup( 1 ));
171                final float x = Float.parseFloat( m.getGroup( 2 ));
172                final float y = Float.parseFloat( m.getGroup( 3 ));
173                map.zoomTo( zoom );
174                if( isInit ) map.setCenter( new LonLat( x, y ));
175                else map.panTo( new LonLat( x, y ));
176            }
177            catch( NumberFormatException x )
178            {
179                Window.alert( "Unable to parse switch s=" + s + ": " + x.toString() );
180                reboundToDefault();
181            }
182        }
183        finally { restackSuppress = false; }
184    }
185
186
187
188    private boolean reboundSuppress;
189
190
191
192    private void reboundToDefault()
193    {
194        map.zoomTo( ZOOM_DEFAULT );
195        map.panTo( CENTER_DEFAULT );
196    }
197
198
199
200    private boolean restackSuppress;
201
202
203
204    private static final RegExp S_PATTERN = RegExp.compile(
205      "^([0-9]+):([-+]?[0-9]+):([-+]?[0-9]+)$" );
206
207
208
209    private static final int X_DEFAULT = 0;
210    private static final int Y_DEFAULT = 0;
211
212        static
213        {
214            CENTER_DEFAULT = new LonLat( X_DEFAULT, Y_DEFAULT );
215        }
216
217
218
219    private static final int ZOOM_DEFAULT = 2;
220
221
222
223   // ====================================================================================
224
225
226    private final HistoryStacker historyStacker;
227
228
229
230    /** A relayer of events from the map to the scoping switch.  We cannot easily force
231      * the zoom and pan controls of OpenLayers to work through the intermediation of the
232      * scoping switch, they are hardwired directly to the map.  So we employ this
233      * relayer as an intermediary to keep the scoping switch in synchrony.
234      */
235    private final class HistoryStacker implements Scheduler.ScheduledCommand
236    {
237
238        /** Constructs a HistoryStacker.
239          *
240          *     @param spool for release of internal holds.  When unwound, this instance
241          *         will release its internal holds and become disabled.
242          */
243        HistoryStacker( final Spool spool )
244        {
245         // Scheduler.get().scheduleFixedPeriod( new Scheduler.RepeatingCommand()
246         // {
247         //     // Give map time to settle, otherwise map.getZoom() sometimes returns null
248         //     // in GWT devmode, causing HostedModeException: "Something other than an
249         //     // int was returned from JSNI method"
250         //     public boolean execute()
251         //     {
252                    // A GWT-OpenLayers listener cannot register for multiple event types,
253                    // so standard practice is to instantiate each separately:
254                    final MapMoveEndListener l = new MapMoveEndListener()
255                    {
256                        public void onMapMoveEnd( MapMoveEndListener.MapMoveEndEvent _e )
257                        {
258                            if( restackSuppress ) return;
259
260                            restack();
261                        }
262                    };
263                    map.addMapMoveEndListener( l );
264                    spool.add( new Hold()
265                    {
266                        public void release() { map.removeListener( l ); }
267                    });
268         //      // restack(); // init state, in case it somehow moved during this delay
269         //         return /*to repeat*/false;
270         //     }
271         // }, 250/*ms*/ ); // no hurry, it will not immediately need restacking
272         ///// not necessary provided rebound() init calls map.setCenter() instead of panTo()
273        }
274
275
276        private void restack()
277        {
278            final LonLat center = map.getCenter();
279            final long x = Math.round( center.lon() ); // round to nearest meter, no harm
280            final long y = Math.round( center.lat() );
281            final int zoom = map.getZoom();
282            final String s;
283            if( x == X_DEFAULT && y == Y_DEFAULT && zoom == ZOOM_DEFAULT ) s = null;
284            else
285            {
286                final StringBuilder b = GWTX.stringBuilderClear();
287                b.append( zoom );
288                b.append( ':' );
289                b.append( x );
290                b.append( ':' );
291                b.append( y );
292                s = b.toString();
293            }
294
295            final Switch sSwitch = Scenes.i().sScopingSwitch();
296            if( ObjectX.nullEquals( s, sSwitch.get() )) return; // already set
297
298            reboundSuppress = true;
299            if( s == null ) sSwitch.clear();
300            else sSwitch.set( s );
301            Scheduler.get().scheduleFinally( HistoryStacker.this ); // continues in execute()
302              // Suppression lifted only after the token change event and the call to
303              // rebound().  That event is also "finally" scheduled, so note the
304              // "scheduling order assumption".
305        }
306
307
308       // - S c h e d u l e r . S c h e d u l e d - C o m m a n d ------------------------
309
310
311        public void execute() { reboundSuppress = false; } // continued from restack()
312
313
314    }
315
316
317
318}