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}