001package arez.dom; 002 003import akasha.GeolocationCoordinates; 004import akasha.GeolocationPositionError; 005import akasha.WindowGlobal; 006import arez.Arez; 007import arez.ArezContext; 008import arez.ComputableValue; 009import arez.Task; 010import arez.annotations.Action; 011import arez.annotations.ArezComponent; 012import arez.annotations.ComponentNameRef; 013import arez.annotations.ComputableValueRef; 014import arez.annotations.ContextRef; 015import arez.annotations.DepType; 016import arez.annotations.Feature; 017import arez.annotations.Memoize; 018import arez.annotations.OnActivate; 019import arez.annotations.OnDeactivate; 020import java.util.Objects; 021import javax.annotation.Nonnull; 022import javax.annotation.Nullable; 023 024/** 025 * A component that exposes the current geo position as an observable property. This component relies on the 026 * underlying <a href="https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API">Geolocation API</a> and 027 * it's usage is restricted in the same way as the underlying API (i.e. it is only available in secure contexts 028 * and it asks user permission before providing data.). 029 * 030 * <pre>{@code 031 * EventDrivenValue<Window, Integer> innerWidth = EventDrivenValue.create( window, "resize", () -> window.innerWidth ) 032 * }</pre> 033 * 034 * <p>It is important that the code not add a listener to the underlying event source until there is an 035 * observer accessing the <code>"value"</code> observable defined by the EventDrivenValue class. The first 036 * observer that observes the observable will result in an event listener being added to the event source 037 * and this listener will not be removed until there is no observers left observing the value. This means 038 * that a component that is not being used has very little overhead.</p> 039 */ 040@ArezComponent( requireId = Feature.DISABLE, disposeNotifier = Feature.DISABLE ) 041public abstract class GeoPosition 042{ 043 @SuppressWarnings( "unused" ) 044 public static final class Status 045 { 046 /** 047 * Position data is yet to start loading. 048 */ 049 public static final int INITIAL = -2; 050 /** 051 * Position data is loading. 052 */ 053 public static final int LOADING = -1; 054 /** 055 * No error acquiring position. 056 */ 057 public static final int POSITION_LOADED = 0; 058 /** 059 * The acquisition of the geolocation information failed because the page didn't have the permission to do it. 060 */ 061 public static final int PERMISSION_DENIED = GeolocationPositionError.PERMISSION_DENIED; 062 /** 063 * The acquisition of the geolocation failed because at least one internal source of position returned an internal error. 064 */ 065 public static final int POSITION_UNAVAILABLE = GeolocationPositionError.POSITION_UNAVAILABLE; 066 /** 067 * The time allowed to acquire the geolocation, defined by PositionOptions.timeout information was reached before the information was obtained. 068 */ 069 public static final int TIMEOUT = GeolocationPositionError.TIMEOUT; 070 071 private Status() 072 { 073 } 074 } 075 076 @Nullable 077 private Position _position; 078 private int _status; 079 @Nullable 080 private String _errorMessage; 081 private int _activateCount; 082 private int _watcherId; 083 084 /** 085 * Create the GeoPosition component. 086 * 087 * @return the newly created GeoPosition component. 088 */ 089 @Nonnull 090 public static GeoPosition create() 091 { 092 return new Arez_GeoPosition(); 093 } 094 095 GeoPosition() 096 { 097 _status = Status.INITIAL; 098 _activateCount = 0; 099 } 100 101 /** 102 * Return an immutable representation of the current position. 103 * This will be null unless {@link #getStatus()} has returned a {@link Status#POSITION_LOADED} value. 104 * 105 * @return the current position as reported by the geolocation API. 106 */ 107 @Memoize( depType = DepType.AREZ_OR_EXTERNAL ) 108 @Nullable 109 public Position getPosition() 110 { 111 return _position; 112 } 113 114 /** 115 * Return the status indicating whether the position is available. 116 * It will be one of the values provided by {@link Status}. 117 * 118 * @return the status of the position data. 119 */ 120 @Memoize( depType = DepType.AREZ_OR_EXTERNAL ) 121 public int getStatus() 122 { 123 return _status; 124 } 125 126 /** 127 * Return the error message reported by the geolocation API when position could not be loaded else null. 128 * 129 * @return the error message if any. 130 */ 131 @Memoize( depType = DepType.AREZ_OR_EXTERNAL ) 132 @Nullable 133 public String getErrorMessage() 134 { 135 return _errorMessage; 136 } 137 138 @ComputableValueRef 139 abstract ComputableValue<?> getPositionComputableValue(); 140 141 @ComputableValueRef 142 abstract ComputableValue<?> getStatusComputableValue(); 143 144 @ComputableValueRef 145 abstract ComputableValue<?> getErrorMessageComputableValue(); 146 147 @OnActivate 148 void onPositionActivate() 149 { 150 activate(); 151 } 152 153 @OnDeactivate 154 void onPositionDeactivate() 155 { 156 deactivate(); 157 } 158 159 @OnActivate 160 void onStatusActivate() 161 { 162 activate(); 163 } 164 165 @OnDeactivate 166 void onStatusDeactivate() 167 { 168 deactivate(); 169 } 170 171 @OnActivate 172 void onErrorMessageActivate() 173 { 174 activate(); 175 } 176 177 @OnDeactivate 178 void onErrorMessageDeactivate() 179 { 180 deactivate(); 181 } 182 183 private void activate() 184 { 185 if ( 0 == _activateCount ) 186 { 187 context().task( Arez.areNamesEnabled() ? componentName() + ".setLoadingStatus" : null, 188 () -> setStatus( Status.LOADING ), 189 Task.Flags.DISPOSE_ON_COMPLETE ); 190 _watcherId = WindowGlobal.navigator().geolocation().watchPosition( e -> onSuccess( e.coords() ), this::onFailure ); 191 } 192 _activateCount++; 193 } 194 195 private void deactivate() 196 { 197 _activateCount--; 198 if ( 0 == _activateCount ) 199 { 200 setStatus( Status.INITIAL ); 201 WindowGlobal.navigator().geolocation().clearWatch( _watcherId ); 202 _watcherId = 0; 203 } 204 } 205 206 @Action 207 void onFailure( @Nonnull final GeolocationPositionError e ) 208 { 209 setStatus( e.code() ); 210 final String errorMessage = e.message(); 211 if ( !Objects.equals( errorMessage, _errorMessage ) ) 212 { 213 _errorMessage = errorMessage; 214 getErrorMessageComputableValue().reportPossiblyChanged(); 215 } 216 if ( null != _position ) 217 { 218 _position = null; 219 getPositionComputableValue().reportPossiblyChanged(); 220 } 221 } 222 223 @Action 224 void setStatus( final int status ) 225 { 226 if ( status != _status ) 227 { 228 _status = status; 229 getStatusComputableValue().reportPossiblyChanged(); 230 } 231 } 232 233 @Action 234 void onSuccess( @Nonnull final GeolocationCoordinates coords ) 235 { 236 setStatus( Status.POSITION_LOADED ); 237 if ( null != _errorMessage ) 238 { 239 _errorMessage = null; 240 getErrorMessageComputableValue().reportPossiblyChanged(); 241 } 242 _position = 243 new Position( coords.accuracy(), coords.altitude(), coords.heading(), coords.latitude(), coords.longitude(), coords.longitude() ); 244 getPositionComputableValue().reportPossiblyChanged(); 245 } 246 247 @ComponentNameRef 248 abstract String componentName(); 249 250 @ContextRef 251 abstract ArezContext context(); 252}