001package arez.dom; 002 003import arez.Arez; 004import arez.ArezContext; 005import arez.ComputableValue; 006import arez.Task; 007import arez.annotations.Action; 008import arez.annotations.ArezComponent; 009import arez.annotations.ComponentNameRef; 010import arez.annotations.ComputableValueRef; 011import arez.annotations.ContextRef; 012import arez.annotations.DepType; 013import arez.annotations.Feature; 014import arez.annotations.Memoize; 015import arez.annotations.OnActivate; 016import arez.annotations.OnDeactivate; 017import java.util.Objects; 018import javax.annotation.Nonnull; 019import javax.annotation.Nullable; 020import jsinterop.annotations.JsFunction; 021import jsinterop.annotations.JsMethod; 022import jsinterop.annotations.JsPackage; 023import jsinterop.annotations.JsProperty; 024import jsinterop.annotations.JsType; 025 026/** 027 * A component that exposes the current geo position as an observable property. This component relies on the 028 * underlying <a href="https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API">Geolocation API</a> and 029 * it's usage is restricted in the same way as the underlying API (i.e. it is only available in secure contexts 030 * and it asks user permission before providing data.). 031 * 032 * <pre>{@code 033 * final GeoPosition geoPosition = GeoPosition.create(); 034 * Arez.context().observer( () -> consumePosition( geoPosition.getPosition() ) ); 035 * }</pre> 036 */ 037@ArezComponent( requireId = Feature.DISABLE, disposeNotifier = Feature.DISABLE ) 038public abstract class GeoPosition 039{ 040 @JsFunction 041 private interface PositionCallback 042 { 043 void onPosition( GeolocationPosition position ); 044 } 045 046 @JsFunction 047 private interface PositionErrorCallback 048 { 049 void onError( GeolocationPositionError error ); 050 } 051 052 @JsType( isNative = true, name = "GeolocationPosition", namespace = JsPackage.GLOBAL ) 053 private static class GeolocationPosition 054 { 055 @JsProperty( name = "coords" ) 056 native GeolocationCoordinates coords(); 057 } 058 059 @JsType( isNative = true, name = "GeolocationCoordinates", namespace = JsPackage.GLOBAL ) 060 static class GeolocationCoordinates 061 { 062 @JsProperty( name = "accuracy" ) 063 native double accuracy(); 064 065 @JsProperty( name = "altitude" ) 066 native Double altitude(); 067 068 @JsProperty( name = "heading" ) 069 native Double heading(); 070 071 @JsProperty( name = "latitude" ) 072 native double latitude(); 073 074 @JsProperty( name = "longitude" ) 075 native double longitude(); 076 077 @JsProperty( name = "speed" ) 078 native Double speed(); 079 } 080 081 @JsType( isNative = true, name = "GeolocationPositionError", namespace = JsPackage.GLOBAL ) 082 static class GeolocationPositionError 083 { 084 @JsProperty( name = "code" ) 085 native int code(); 086 087 @JsProperty( name = "message" ) 088 native String message(); 089 } 090 091 @JsType( isNative = true, name = "Geolocation", namespace = JsPackage.GLOBAL ) 092 private static class Geolocation 093 { 094 @JsMethod 095 native int watchPosition( PositionCallback successCallback, PositionErrorCallback errorCallback ); 096 097 @JsMethod 098 native void clearWatch( int watchId ); 099 } 100 101 @JsType( isNative = true, name = "Navigator", namespace = JsPackage.GLOBAL ) 102 private static class Navigator 103 { 104 @JsProperty( name = "geolocation" ) 105 native Geolocation geolocation(); 106 } 107 108 @SuppressWarnings( "unused" ) 109 public static final class Status 110 { 111 /** 112 * Position data is yet to start loading. 113 */ 114 public static final int INITIAL = -2; 115 /** 116 * Position data is loading. 117 */ 118 public static final int LOADING = -1; 119 /** 120 * No error acquiring position. 121 */ 122 public static final int POSITION_LOADED = 0; 123 /** 124 * The acquisition of the geolocation information failed because the page didn't have the permission to do it. 125 */ 126 public static final int PERMISSION_DENIED = 1; 127 /** 128 * The acquisition of the geolocation failed because at least one internal source of position returned an internal error. 129 */ 130 public static final int POSITION_UNAVAILABLE = 2; 131 /** 132 * The time allowed to acquire the geolocation, defined by PositionOptions.timeout information was reached before the information was obtained. 133 */ 134 public static final int TIMEOUT = 3; 135 136 private Status() 137 { 138 } 139 } 140 141 @Nullable 142 private Position _position; 143 private int _status; 144 @Nullable 145 private String _errorMessage; 146 private int _activateCount; 147 private int _watcherId; 148 149 /** 150 * Create the GeoPosition component. 151 * 152 * @return the newly created GeoPosition component. 153 */ 154 @Nonnull 155 public static GeoPosition create() 156 { 157 return new Arez_GeoPosition(); 158 } 159 160 GeoPosition() 161 { 162 _status = Status.INITIAL; 163 _activateCount = 0; 164 } 165 166 /** 167 * Return an immutable representation of the current position. 168 * This will be null unless {@link #getStatus()} has returned a {@link Status#POSITION_LOADED} value. 169 * 170 * @return the current position as reported by the geolocation API. 171 */ 172 @Memoize( depType = DepType.AREZ_OR_EXTERNAL ) 173 @Nullable 174 public Position getPosition() 175 { 176 return _position; 177 } 178 179 /** 180 * Return the status indicating whether the position is available. 181 * It will be one of the values provided by {@link Status}. 182 * 183 * @return the status of the position data. 184 */ 185 @Memoize( depType = DepType.AREZ_OR_EXTERNAL ) 186 public int getStatus() 187 { 188 return _status; 189 } 190 191 /** 192 * Return the error message reported by the geolocation API when position could not be loaded else null. 193 * 194 * @return the error message if any. 195 */ 196 @Memoize( depType = DepType.AREZ_OR_EXTERNAL ) 197 @Nullable 198 public String getErrorMessage() 199 { 200 return _errorMessage; 201 } 202 203 @ComputableValueRef 204 abstract ComputableValue<?> getPositionComputableValue(); 205 206 @ComputableValueRef 207 abstract ComputableValue<?> getStatusComputableValue(); 208 209 @ComputableValueRef 210 abstract ComputableValue<?> getErrorMessageComputableValue(); 211 212 @OnActivate 213 void onPositionActivate() 214 { 215 activate(); 216 } 217 218 @OnDeactivate 219 void onPositionDeactivate() 220 { 221 deactivate(); 222 } 223 224 @OnActivate 225 void onStatusActivate() 226 { 227 activate(); 228 } 229 230 @OnDeactivate 231 void onStatusDeactivate() 232 { 233 deactivate(); 234 } 235 236 @OnActivate 237 void onErrorMessageActivate() 238 { 239 activate(); 240 } 241 242 @OnDeactivate 243 void onErrorMessageDeactivate() 244 { 245 deactivate(); 246 } 247 248 private void activate() 249 { 250 if ( 0 == _activateCount ) 251 { 252 context().task( Arez.areNamesEnabled() ? componentName() + ".setLoadingStatus" : null, 253 () -> setStatus( Status.LOADING ), 254 Task.Flags.DISPOSE_ON_COMPLETE ); 255 _watcherId = navigator().geolocation().watchPosition( e -> onSuccess( e.coords() ), this::onFailure ); 256 } 257 _activateCount++; 258 } 259 260 private void deactivate() 261 { 262 _activateCount--; 263 if ( 0 == _activateCount ) 264 { 265 setStatus( Status.INITIAL ); 266 navigator().geolocation().clearWatch( _watcherId ); 267 _watcherId = 0; 268 } 269 } 270 271 @Action 272 void onFailure( @Nonnull final GeolocationPositionError e ) 273 { 274 setStatus( e.code() ); 275 final String errorMessage = e.message(); 276 if ( !Objects.equals( errorMessage, _errorMessage ) ) 277 { 278 _errorMessage = errorMessage; 279 getErrorMessageComputableValue().reportPossiblyChanged(); 280 } 281 if ( null != _position ) 282 { 283 _position = null; 284 getPositionComputableValue().reportPossiblyChanged(); 285 } 286 } 287 288 @Action 289 void setStatus( final int status ) 290 { 291 if ( status != _status ) 292 { 293 _status = status; 294 getStatusComputableValue().reportPossiblyChanged(); 295 } 296 } 297 298 @Action 299 void onSuccess( @Nonnull final GeolocationCoordinates coords ) 300 { 301 setStatus( Status.POSITION_LOADED ); 302 if ( null != _errorMessage ) 303 { 304 _errorMessage = null; 305 getErrorMessageComputableValue().reportPossiblyChanged(); 306 } 307 _position = 308 new Position( coords.accuracy(), coords.altitude(), coords.heading(), coords.latitude(), coords.longitude(), coords.longitude() ); 309 getPositionComputableValue().reportPossiblyChanged(); 310 } 311 312 @ComponentNameRef 313 abstract String componentName(); 314 315 @ContextRef 316 abstract ArezContext context(); 317 318 @JsProperty( name = "navigator", namespace = JsPackage.GLOBAL ) 319 private static native Navigator navigator(); 320}