package 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.
import com.google.gwt.core.client.*;
import com.google.gwt.event.logical.shared.*;
import com.google.web.bindery.event.shared.HandlerRegistration;
import com.google.gwt.regexp.shared.*;
import com.google.gwt.user.client.Window;
import org.gwtopenmaps.openlayers.client.*;
import org.gwtopenmaps.openlayers.client.event.*;
import org.gwtopenmaps.openlayers.client.geometry.*;
import votorola.a.web.gwt.*;
import votorola.s.gwt.scene.*;
import votorola.s.gwt.scene.feed.*;
import votorola.g.hold.*;
import votorola.g.lang.*;
import votorola.g.web.gwt.*;
/** A scoping model based on the viewport of an OpenLayers geomap. The scoping state is
* exposed in the {@linkplain Scenes#sScopingSwitch() 's' switch}:
*
*
*
* Switch |
* Controlled state |
* Default |
*
* s |
*
* The scoping state. A colon-separated list of three numbers: 1) the zoom
* level; 2) the x cooridinate in meters; and 3) the y coordinate in meters. |
*
* "2:0:0" |
*
*
*
*
* @see Scoping
*/
public class Geoscoping implements Scoping
{
/** Constructs a Geoscoping.
*
* @param _geomap the geomap whose viewport controls the scoping switch.
* @param spool the spool for the release of associated holds. When unwound it
* releases the holds of the scoping model and thereby disables it.
*/
public Geoscoping( Geomap _geomap, final Spool spool )
{
geomap = _geomap;
map = geomap.getMap();
spool.add( new Hold()
{
final HandlerRegistration hR = Scenes.i().sScopingSwitch().addHandler(
new ValueChangeHandler()
{
public void onValueChange( final ValueChangeEvent e )
{
GWTX.i().bus().fireEventFromSource( new ScopeChangeEvent(), Geoscoping.this );
if( reboundSuppress ) return;
rebound( e.getValue(), /*isInit*/false );
}
});
public void release() { hR.removeHandler(); }
});
// Scheduler.get().scheduleDeferred( new Scheduler.ScheduledCommand()
// {
// // initial panning fails if executed immediately or finally, so defer it
// public void execute() // later, after GWT dispatch loop
// {
rebound( Scenes.i().sScopingSwitch().get(), /*isInit*/true ); // init state
// }
// });
///// not necessary provided rebound() init calls map.setCenter() instead of panTo()
historyStacker = new HistoryStacker( spool );
}
// ------------------------------------------------------------------------------------
/** Tests whether the specified bite is within the current scope.
*/
public boolean inScope( final BiteJS bite )
{
final JsArray persons = bite.persons();
for( int p = 0, pN = persons.length(); p < pN; ++p )
{
final Residence res = persons.get(p).residence();
if( res == null ) continue;
final Bounds bounds = map.getExtent();
final Point point = new Point( res.lon(), res.lat() );
geomap.transformFromEPSG4326( point );
final double x = point.getX();
final double y = point.getY();
if( x >= bounds.getLowerLeftX() && x <= bounds.getUpperRightX()
&& y >= bounds.getLowerLeftY() && y <= bounds.getUpperRightY() ) return true;
}
return false; // no person in scope
}
// - O b j e c t ----------------------------------------------------------------------
public @Override String toString()
{
final LonLat center = map.getCenter();
return "geoscope = zoom(" + map.getZoom() + ") longitude(" + center.lon() + ") latitude(" + center.lat() + ")";
}
// - S c o p i n g --------------------------------------------------------------------
public HandlerRegistration addHandler( final ScopeChangeHandler handler )
{
return GWTX.i().bus().addHandlerToSource( ScopeChangeEvent.TYPE, /*source*/Geoscoping.this,
handler );
}
//// P r i v a t e ///////////////////////////////////////////////////////////////////////
private static final LonLat CENTER_DEFAULT;
private final Geomap geomap;
private final Map map;
private @Warning("init call") void rebound( final String s, final boolean isInit )
{
restackSuppress = true;
try
{
if( s == null )
{
reboundToDefault();
return;
}
final MatchResult m = S_PATTERN.exec( s );
if( m == null )
{
Window.alert( "Malformed scoping switch: s=" + s );
reboundToDefault();
return;
}
try
{
final int zoom = Integer.parseInt( m.getGroup( 1 ));
final float x = Float.parseFloat( m.getGroup( 2 ));
final float y = Float.parseFloat( m.getGroup( 3 ));
map.zoomTo( zoom );
if( isInit ) map.setCenter( new LonLat( x, y ));
else map.panTo( new LonLat( x, y ));
}
catch( NumberFormatException x )
{
Window.alert( "Unable to parse switch s=" + s + ": " + x.toString() );
reboundToDefault();
}
}
finally { restackSuppress = false; }
}
private boolean reboundSuppress;
private void reboundToDefault()
{
map.zoomTo( ZOOM_DEFAULT );
map.panTo( CENTER_DEFAULT );
}
private boolean restackSuppress;
private static final RegExp S_PATTERN = RegExp.compile(
"^([0-9]+):([-+]?[0-9]+):([-+]?[0-9]+)$" );
private static final int X_DEFAULT = 0;
private static final int Y_DEFAULT = 0;
static
{
CENTER_DEFAULT = new LonLat( X_DEFAULT, Y_DEFAULT );
}
private static final int ZOOM_DEFAULT = 2;
// ====================================================================================
private final HistoryStacker historyStacker;
/** A relayer of events from the map to the scoping switch. We cannot easily force
* the zoom and pan controls of OpenLayers to work through the intermediation of the
* scoping switch, they are hardwired directly to the map. So we employ this
* relayer as an intermediary to keep the scoping switch in synchrony.
*/
private final class HistoryStacker implements Scheduler.ScheduledCommand
{
/** Constructs a HistoryStacker.
*
* @param spool for release of internal holds. When unwound, this instance
* will release its internal holds and become disabled.
*/
HistoryStacker( final Spool spool )
{
// Scheduler.get().scheduleFixedPeriod( new Scheduler.RepeatingCommand()
// {
// // Give map time to settle, otherwise map.getZoom() sometimes returns null
// // in GWT devmode, causing HostedModeException: "Something other than an
// // int was returned from JSNI method"
// public boolean execute()
// {
// A GWT-OpenLayers listener cannot register for multiple event types,
// so standard practice is to instantiate each separately:
final MapMoveEndListener l = new MapMoveEndListener()
{
public void onMapMoveEnd( MapMoveEndListener.MapMoveEndEvent _e )
{
if( restackSuppress ) return;
restack();
}
};
map.addMapMoveEndListener( l );
spool.add( new Hold()
{
public void release() { map.removeListener( l ); }
});
// // restack(); // init state, in case it somehow moved during this delay
// return /*to repeat*/false;
// }
// }, 250/*ms*/ ); // no hurry, it will not immediately need restacking
///// not necessary provided rebound() init calls map.setCenter() instead of panTo()
}
private void restack()
{
final LonLat center = map.getCenter();
final long x = Math.round( center.lon() ); // round to nearest meter, no harm
final long y = Math.round( center.lat() );
final int zoom = map.getZoom();
final String s;
if( x == X_DEFAULT && y == Y_DEFAULT && zoom == ZOOM_DEFAULT ) s = null;
else
{
final StringBuilder b = GWTX.stringBuilderClear();
b.append( zoom );
b.append( ':' );
b.append( x );
b.append( ':' );
b.append( y );
s = b.toString();
}
final Switch sSwitch = Scenes.i().sScopingSwitch();
if( ObjectX.nullEquals( s, sSwitch.get() )) return; // already set
reboundSuppress = true;
if( s == null ) sSwitch.clear();
else sSwitch.set( s );
Scheduler.get().scheduleFinally( HistoryStacker.this ); // continues in execute()
// Suppression lifted only after the token change event and the call to
// rebound(). That event is also "finally" scheduled, so note the
// "scheduling order assumption".
}
// - 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 ------------------------
public void execute() { reboundSuppress = false; } // continued from restack()
}
}