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}