From 4e4c10da630e107843de62b919b0bcf44130e2ee Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Thu, 1 Dec 2022 15:06:55 -0700 Subject: [PATCH 01/12] removed package-lock.json files --- components/dash-core-components/src/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/dash-core-components/src/index.js b/components/dash-core-components/src/index.js index a1635e029c..d2e70c7755 100644 --- a/components/dash-core-components/src/index.js +++ b/components/dash-core-components/src/index.js @@ -7,6 +7,7 @@ import DatePickerRange from './components/DatePickerRange.react'; import DatePickerSingle from './components/DatePickerSingle.react'; import Download from './components/Download.react'; import Dropdown from './components/Dropdown.react'; +import Geolocation from './components/Geolocation.react'; import Graph from './components/Graph.react'; import Input from './components/Input.react'; import Interval from './components/Interval.react'; @@ -37,6 +38,7 @@ export { DatePickerSingle, Download, Dropdown, + Geolocation, Graph, Input, Interval, From ff9d09555fc58eba41f245c59fba17ba74433f43 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Thu, 1 Dec 2022 15:14:35 -0700 Subject: [PATCH 02/12] added Geolocation and demo apps --- .../src/components/Geolocation.react.js | 193 ++++++++++++ .../geolocation/test_geolocation.py | 42 +++ usage_geolocation_demo.py | 292 ++++++++++++++++++ usage_geolocation_quickstart.py | 34 ++ 4 files changed, 561 insertions(+) create mode 100644 components/dash-core-components/src/components/Geolocation.react.js create mode 100644 components/dash-core-components/tests/integration/geolocation/test_geolocation.py create mode 100644 usage_geolocation_demo.py create mode 100644 usage_geolocation_quickstart.py diff --git a/components/dash-core-components/src/components/Geolocation.react.js b/components/dash-core-components/src/components/Geolocation.react.js new file mode 100644 index 0000000000..2729a26b36 --- /dev/null +++ b/components/dash-core-components/src/components/Geolocation.react.js @@ -0,0 +1,193 @@ +import {Component} from 'react'; +import PropTypes from 'prop-types'; + +/** + * The CurrentLocation component gets geolocation of the device from the web browser. See more info here: + * https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API + */ + +export default class Geolocation extends Component { + constructor(props) { + super(props); + this.success = this.success.bind(this); + this.error = this.error.bind(this); + this.updatePosition = this.updatePosition.bind(this); + } + updatePosition() { + if (!navigator.geolocation) { + this.error({ + code: 999, + message: 'Your browser does not support Geolocation', + }); + if (this.props.show_alert) { + alert(`ERROR(${this.error.code}): ${this.error.message}`); + } + } else { + this.props.setProps({ + update_now: false, + }); + + const positionOptions = { + enableHighAccuracy: this.props.high_accuracy, + maximumAge: this.props.maximum_age, + timeout: this.props.timeout, + }; + + navigator.geolocation.getCurrentPosition( + this.success, + this.error, + positionOptions + ); + } + } + + componentDidMount() { + this.updatePosition(); + } + + componentDidUpdate(prevProps) { + if ( + this.props.update_now || + prevProps.maximum_age !== this.props.maximum_age || + prevProps.timeout !== this.props.timeout || + prevProps.high_accuracy !== this.props.high_accuracy + ) { + this.updatePosition(); + } + } + + success(pos) { + const crd = pos.coords; + const position_obj = { + lat: crd.latitude ?? null, + lon: crd.longitude ?? null, + accuracy: crd.accuracy ?? null, + alt: crd.altitude ?? null, + alt_accuracy: crd.altitudeAccuracy ?? null, + speed: crd.speed ?? null, + heading: crd.heading ?? null, + }; + + this.props.setProps({ + local_date: new Date(pos.timestamp).toLocaleString(), + timestamp: pos.timestamp, + position: position_obj, + position_error: null, + }); + } + + error(err) { + if (this.props.show_alert) { + alert(`ERROR(${err.code}): ${err.message}`); + } + this.props.setProps({ + position: null, + position_error: { + code: err.code, + message: err.message, + }, + }); + } + + render() { + return null; + } +} + +Geolocation.defaultProps = { + update_now: false, + high_accuracy: false, + position_error: null, + maximum_age: 0, + timeout: Infinity, + show_alert: false, +}; + +Geolocation.propTypes = { + /** + * The ID used to identify this component in Dash callbacks. + */ + id: PropTypes.string, + + /** + * The local date and time when the device position was updated. + * Format: MM/DD/YYYY, hh:mm:ss p where p is AM or PM + */ + local_date: PropTypes.string, + + /** + * The Unix timestamp from when the position was updated + */ + timestamp: PropTypes.number, + + /** + * The position of the device. `lat`, `lon`, and `accuracy` will always be returned. The other data will be included + * when available, otherwise it will be NaN. + * + * `lat` is latitude in degrees. + * `lon` is longitude in degrees. + * `accuracy` is the accuracy of the lat/lon in meters. * + * + * `alt` is altitude above mean sea level in meters. + * `alt_accuracy` is the accuracy of the altitude in meters. + * `heading` is the compass heading in degrees. + * `speed` is the speed in meters per second. + * + */ + position: PropTypes.shape({ + lat: PropTypes.number, + lon: PropTypes.number, + accuracy: PropTypes.number, + alt: PropTypes.number, + alt_accuracy: PropTypes.number, + heading: PropTypes.number, + speed: PropTypes.number, + }), + + /** + * Position error + */ + position_error: PropTypes.shape({ + code: PropTypes.number, + message: PropTypes.string, + }), + + /** + * If true, error messages will be displayed as an alert + */ + show_alert: PropTypes.bool, + + /** + * Forces a one-time update of the position data. If set to True in a callback, the browser + * will update the position data and reset update_now back to False. This can, for example, be used to update the + * position with a button or an interval timer. + */ + update_now: PropTypes.bool, + + /** + * If true and if the device is able to provide a more accurate position, + * it will do so. Note that this can result in slower response times or increased power consumption (with a GPS + * chip on a mobile device for example). If false (the default value), the device can save resources by + * responding more quickly and/or using less power. + */ + high_accuracy: PropTypes.bool, + + /** + * The maximum age in milliseconds of a possible cached position that is acceptable to return. If set to 0, + * it means that the device cannot use a cached position and must attempt to retrieve the real current position. + * If set to Infinity the device must return a cached position regardless of its age. Default: 0. + */ + maximum_age: PropTypes.number, + + /** + * The maximum length of time (in milliseconds) the device is allowed to take in order to return a position. + * The default value is Infinity, meaning that data will not be return until the position is available. + */ + timeout: PropTypes.number, + + /** + * Dash-assigned callback that should be called to report property changes + * to Dash, to make them available for callbacks. + */ + setProps: PropTypes.func, +}; diff --git a/components/dash-core-components/tests/integration/geolocation/test_geolocation.py b/components/dash-core-components/tests/integration/geolocation/test_geolocation.py new file mode 100644 index 0000000000..88232024c1 --- /dev/null +++ b/components/dash-core-components/tests/integration/geolocation/test_geolocation.py @@ -0,0 +1,42 @@ +import dash +import dash_core_components as dcc +from dash.dependencies import Input, Output +import dash_html_components as html +import time + + +def test_geol001_position(dash_dcc): + dash_dcc.driver.execute_cdp_cmd( + "Emulation.setGeolocationOverride", + {"latitude": 45.527, "longitude": -73.5968, "accuracy": 100}, + ) + + app = dash.Dash(__name__) + app.layout = html.Div( + [ + dcc.Geolocation(id="geolocation"), + html.Div(id="text_position"), + ] + ) + + @app.callback( + Output("text_position", "children"), + Input("geolocation", "position"), + ) + def display_output(pos): + if pos: + return ( + f"lat {pos['lat']},lon {pos['lon']}, accuracy {pos['accuracy']} meters" + ) + else: + return "No position data available" + + dash_dcc.start_server(app) + + time.sleep(1) + + dash_dcc.wait_for_text_to_equal( + "#text_position", "lat 45.527,lon -73.5968, accuracy 100 meters" + ) + + assert dash_dcc.get_logs() == [] diff --git a/usage_geolocation_demo.py b/usage_geolocation_demo.py new file mode 100644 index 0000000000..e549f0f782 --- /dev/null +++ b/usage_geolocation_demo.py @@ -0,0 +1,292 @@ +import datetime as dt +from dash import Dash, dcc, html, Input, Output, dash_table, no_update +import plotly.graph_objects as go +from geopy.geocoders import Nominatim +import pandas as pd +import dash_bootstrap_components as dbc + + +app = Dash( + __name__, + prevent_initial_callbacks=True, + suppress_callback_exceptions=True, + external_stylesheets=[dbc.themes.SPACELAB], +) + +df = pd.DataFrame( + { + "lat": 0, + "lon": 0, + "alt": 0, + "accuracy": 0, + "altAccuracy": 0, + "heading": 0, + "speed": 0, + }, + index=[0], +) + +""" +============================================================================== +Markdown +""" +markdown_card = dbc.Card( + [ + dbc.CardHeader( + "Notes on Geolocation component and App settings", + className="font-weight-bold", + ), + dcc.Markdown( + """ + +- The __Update Now__ button sets the `update_now` prop to `True` in a callback. This does a one-time update of the +position data. + +- __Include Address__ This is a demo of how geopy can be used to to get an address based on the + position data from `dcc.Geolocation`. Sometimes it's slow. If the timeout is small, + it's better not to select this option. + +- __Enable High Accuracy__ This sets the `high_accuracy` prop. If selected, if the device is able to provide a +more accurate position, it will do so. Note that this can result in slower response times or increased power +consumption (with a GPS on a mobile device for example). If `False` (the default value), the device can save resources +by responding more quickly and/or using less power. Note: When selected, timeout should be set to a high number or a +max age should be provided as GPS sometimes takes longer to update. + +- __Show errors as alert__ This sets the `show_alert` prop. When `True` error messages will be displayed as an +`alert` in the browser. When `False` you can provide your own custom error handling in the app. + + +- __Max Age__ The `maximum_age` prop will provide a cached location after specified number of milliseconds if no new +position data is available before the timeout. If set to zero, it will not use a cached data. + +- __Timeout__ The `timeout` prop is the number of milliseconds allowed for the position to update without +generating an error message. + +- The __dates and times__ show when the position data was obtained. The date reflects the + current system time from the computer running the browser. The accuracy is dependant on this being set correctly + in the user's browser. + +- __Zoom and Center__ is not a component prop. In this demo app, the map uses uirevision to hold the user +settings for pan, zoom etc, so those won't change when the position data is updated. + +- __Follow me__ In this demo app, dcc.Interval is used to set the `update_now` prop to +True at the interval time specified. To stop following, set the interval time to zero. The app will + then disable `dcc.Interval` in a callback so the position will no longer be updated. + + """ + ), + ], + className="my-5", +) + + +""" +=============================================================================== +Map and address +""" + + +def get_address(lat, lon, show_address): + address = "" + if show_address: + geolocator = Nominatim(user_agent="my_location") + try: + location = geolocator.reverse(",".join([str(lat), str(lon)])) + address = location.address + except: # noqa: E722 + address = "address unavailable" + return address + + +def make_map(position, show_address, zoom=12): + lat = position["lat"] + lon = position["lon"] + fig = go.Figure( + go.Scattermapbox( + lat=[lat], + lon=[lon], + mode="markers", + marker=go.scattermapbox.Marker(size=14), + text=[get_address(lat, lon, show_address)], + ) + ) + fig.update_layout( + hovermode="closest", + mapbox=dict( + style="open-street-map", + center=go.layout.mapbox.Center(lat=lat, lon=lon), + zoom=zoom, + ), + uirevision=zoom, + ) + return dcc.Graph(figure=fig) + + +def make_position_card(pos, date, show_address): + return html.Div( + [ + html.H4(f"As of {date} your location:"), + html.P( + "within {} meters of lat,lon: ( {:.2f}, {:.2f})".format( + pos["accuracy"], pos["lat"], pos["lon"] + ) + ), + html.P(get_address(pos["lat"], pos["lon"], show_address)), + ] + ) + + +""" +=============================================================================== +Input components +""" + + +update_btn = dbc.Button( + "Update Now", + id="update_btn", + className="m-2", + color="secondary", +) +options_checklist = dbc.Checklist( + options=[ + {"label": "Include Address", "value": "address"}, + {"label": "Enable High Accuracy", "value": "high_accuracy"}, + {"label": "Show errors as alert", "value": "alert"}, + ], + className="mt-4", +) +max_age = dbc.Input( + placeholder="milliseconds", + type="number", + debounce=True, + value=0, + min=0, +) + +timeout_input = dbc.Input( + type="number", + debounce=True, + value=10000, + min=0, +) + +zoom_center = dbc.Input(type="number", value=8, min=0, max=22, step=1) + +follow_me_interval = dbc.Input(type="number", debounce=True, value=0, min=0, step=1000) + +geolocation = dcc.Geolocation() +interval = dcc.Interval(disabled=True) + +input_card = dbc.Card( + [ + update_btn, + options_checklist, + "Timeout (ms)", + timeout_input, + "Max Age (ms)", + max_age, + "Zoom and Center", + zoom_center, + "Follow me(update interval ms)", + follow_me_interval, + geolocation, + interval, + ], + body=True, +) + +output_card = dbc.Card(body=True) + +app.layout = dbc.Container( + [ + html.Div("dcc.Geolocation demo", className="bg-primary text-white p-2 h3 mb-2"), + dbc.Row([dbc.Col(input_card, width=4), dbc.Col(output_card, width=8)]), + dbc.Row(dbc.Col(markdown_card)), + ], + fluid=True, +) + + +@app.callback( + Output(geolocation, "high_accuracy"), + Output(geolocation, "maximum_age"), + Output(geolocation, "timeout"), + Output(geolocation, "show_alert"), + Input(options_checklist, "value"), + Input(max_age, "value"), + Input(timeout_input, "value"), +) +def update_options(checked, maxage, timeout): + if checked: + high_accuracy = True if "high_accuracy" in checked else False + alert = True if "alert" in checked else False + else: + high_accuracy, alert = False, False + return high_accuracy, maxage, timeout, alert + + +@app.callback( + Output(geolocation, "update_now"), + Input(update_btn, "n_clicks"), + Input(interval, "n_intervals"), + prevent_initial_call=True, +) +def update_now(*_): + return True + + +@app.callback( + Output(interval, "interval"), + Output(interval, "disabled"), + Input(follow_me_interval, "value"), +) +def update_interval(time): + disabled = True if time == 0 else False + return time, disabled + + +@app.callback( + Output(output_card, "children"), + Input(options_checklist, "value"), + Input(zoom_center, "value"), + Input(geolocation, "local_date"), + Input(geolocation, "timestamp"), + Input(geolocation, "position"), + Input(geolocation, "position_error"), + prevent_initial_call=True, +) +def display_output(checklist, zoom, date, timestamp, pos, err): + if err: + return "Error {} : {}".format(err["code"], err["message"]) + + # update message + show_address = True if checklist and "address" in checklist else False + position = ( + make_position_card(pos, date, show_address) + if pos + else "No position data available" + ) + + # update map + graph_map = make_map(pos, show_address, zoom) if pos else {} + + # update position data and error messages + dff = pd.DataFrame(pos, index=[0]) + position_table = dash_table.DataTable( + columns=[{"name": i, "id": i} for i in dff.columns], data=dff.to_dict("records") + ) + + # display iso dates + timestamp = timestamp if timestamp else 0 + datetime_local = dt.datetime.fromtimestamp(int(timestamp / 1000)) + datetime_utc = dt.datetime.utcfromtimestamp(int(timestamp / 1000)) + iso_date = "UTC datetime: {} Local datetime: {}".format( + datetime_utc, datetime_local + ) + + return [position, graph_map, position_table, iso_date] + + +if __name__ == "__main__": + app.run_server(debug=True) diff --git a/usage_geolocation_quickstart.py b/usage_geolocation_quickstart.py new file mode 100644 index 0000000000..1ba0201d25 --- /dev/null +++ b/usage_geolocation_quickstart.py @@ -0,0 +1,34 @@ +from dash import Dash, dcc, html, Input, Output + +app = Dash(__name__) + +app.layout = html.Div( + [ + html.Button("Update Position", id="update_btn"), + dcc.Geolocation(id="geolocation"), + html.Div(id="text_position"), + ] +) + + +@app.callback(Output("geolocation", "update_now"), Input("update_btn", "n_clicks")) +def update_now(click): + return True if click and click > 0 else False + + +@app.callback( + Output("text_position", "children"), + Input("geolocation", "local_date"), + Input("geolocation", "position"), +) +def display_output(date, pos): + if pos: + return html.P( + f"As of {date} your location was: lat {pos['lat']},lon {pos['lon']}, accuracy {pos['accuracy']} meters", + ) + else: + return "No position data available" + + +if __name__ == "__main__": + app.run_server(debug=True) From 5a50d0fd9217c78a7ca57d3b177bc7a3bf7c207a Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Thu, 1 Dec 2022 16:58:52 -0700 Subject: [PATCH 03/12] fixed test --- .../tests/integration/geolocation/test_geolocation.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/components/dash-core-components/tests/integration/geolocation/test_geolocation.py b/components/dash-core-components/tests/integration/geolocation/test_geolocation.py index 88232024c1..0cb69da332 100644 --- a/components/dash-core-components/tests/integration/geolocation/test_geolocation.py +++ b/components/dash-core-components/tests/integration/geolocation/test_geolocation.py @@ -1,8 +1,5 @@ -import dash -import dash_core_components as dcc -from dash.dependencies import Input, Output -import dash_html_components as html import time +from dash import Dash, dcc, html, Input, Output def test_geol001_position(dash_dcc): @@ -11,7 +8,7 @@ def test_geol001_position(dash_dcc): {"latitude": 45.527, "longitude": -73.5968, "accuracy": 100}, ) - app = dash.Dash(__name__) + app = Dash(__name__) app.layout = html.Div( [ dcc.Geolocation(id="geolocation"), From 01f1e408477898bcb19117e462263161b095cfa9 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Fri, 2 Dec 2022 09:07:01 -0700 Subject: [PATCH 04/12] updated test - check if geolocation available in CI --- .../tests/integration/geolocation/test_geolocation.py | 6 +++++- usage_geolocation_demo.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/components/dash-core-components/tests/integration/geolocation/test_geolocation.py b/components/dash-core-components/tests/integration/geolocation/test_geolocation.py index 0cb69da332..824fc757b2 100644 --- a/components/dash-core-components/tests/integration/geolocation/test_geolocation.py +++ b/components/dash-core-components/tests/integration/geolocation/test_geolocation.py @@ -19,8 +19,12 @@ def test_geol001_position(dash_dcc): @app.callback( Output("text_position", "children"), Input("geolocation", "position"), + Input("geolocation", "position_error") ) - def display_output(pos): + def display_output(pos, err): + if err: + print(f"Error {err['code']} : {err['message']}") + return f"Error {err['code']} : {err['message']}" if pos: return ( f"lat {pos['lat']},lon {pos['lon']}, accuracy {pos['accuracy']} meters" diff --git a/usage_geolocation_demo.py b/usage_geolocation_demo.py index e549f0f782..6a66ecd7ca 100644 --- a/usage_geolocation_demo.py +++ b/usage_geolocation_demo.py @@ -258,7 +258,7 @@ def update_interval(time): ) def display_output(checklist, zoom, date, timestamp, pos, err): if err: - return "Error {} : {}".format(err["code"], err["message"]) + return f"Error {err['code']} : {err['message']}" # update message show_address = True if checklist and "address" in checklist else False From 5cee1cb00e883b116f23f7e719691c5ae18c0987 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Fri, 2 Dec 2022 10:16:00 -0700 Subject: [PATCH 05/12] added geolocation permissions --- dash/testing/browser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 6b3dc941fc..6a52073e3e 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -484,6 +484,7 @@ def _get_chrome(self): "download.directory_upgrade": True, "safebrowsing.enabled": False, "safebrowsing.disable_download_protection": True, + "profile.default_content_setting_values.geolocation": 1 }, ) options.add_argument("--disable-dev-shm-usage") From b4514bbe70c173e1f3cf967c87e18c2ad1f8f784 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Fri, 2 Dec 2022 11:48:04 -0700 Subject: [PATCH 06/12] skip geolocation test in CI --- .../tests/integration/geolocation/test_geolocation.py | 9 +++++---- dash/testing/browser.py | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/dash-core-components/tests/integration/geolocation/test_geolocation.py b/components/dash-core-components/tests/integration/geolocation/test_geolocation.py index 824fc757b2..1f3fc4ca8a 100644 --- a/components/dash-core-components/tests/integration/geolocation/test_geolocation.py +++ b/components/dash-core-components/tests/integration/geolocation/test_geolocation.py @@ -1,7 +1,10 @@ import time +import os +import pytest from dash import Dash, dcc, html, Input, Output +@pytest.mark.skipif(os.getenv("CIRCLECI"), reason="geolocation is disabled on CI") def test_geol001_position(dash_dcc): dash_dcc.driver.execute_cdp_cmd( "Emulation.setGeolocationOverride", @@ -19,18 +22,16 @@ def test_geol001_position(dash_dcc): @app.callback( Output("text_position", "children"), Input("geolocation", "position"), - Input("geolocation", "position_error") + Input("geolocation", "position_error"), ) def display_output(pos, err): if err: - print(f"Error {err['code']} : {err['message']}") return f"Error {err['code']} : {err['message']}" if pos: return ( f"lat {pos['lat']},lon {pos['lon']}, accuracy {pos['accuracy']} meters" ) - else: - return "No position data available" + return "No position data available" dash_dcc.start_server(app) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 6a52073e3e..6b3dc941fc 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -484,7 +484,6 @@ def _get_chrome(self): "download.directory_upgrade": True, "safebrowsing.enabled": False, "safebrowsing.disable_download_protection": True, - "profile.default_content_setting_values.geolocation": 1 }, ) options.add_argument("--disable-dev-shm-usage") From b40d8e8d6f4b70f7ed67bf355bd746d62a122568 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Fri, 2 Dec 2022 12:47:20 -0700 Subject: [PATCH 07/12] skip geolocation test in CI --- .../tests/integration/geolocation/test_geolocation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/dash-core-components/tests/integration/geolocation/test_geolocation.py b/components/dash-core-components/tests/integration/geolocation/test_geolocation.py index 1f3fc4ca8a..d6a6afe592 100644 --- a/components/dash-core-components/tests/integration/geolocation/test_geolocation.py +++ b/components/dash-core-components/tests/integration/geolocation/test_geolocation.py @@ -4,7 +4,7 @@ from dash import Dash, dcc, html, Input, Output -@pytest.mark.skipif(os.getenv("CIRCLECI"), reason="geolocation is disabled on CI") +@pytest.mark.skipif(os.getenv("CIRCLECI" is not None), reason="geolocation is disabled on CI") def test_geol001_position(dash_dcc): dash_dcc.driver.execute_cdp_cmd( "Emulation.setGeolocationOverride", From 4dc8374f90acd4605a02ed3b8dd2aec71ee3597f Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Fri, 2 Dec 2022 13:08:35 -0700 Subject: [PATCH 08/12] fixed typo --- .../tests/integration/geolocation/test_geolocation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/dash-core-components/tests/integration/geolocation/test_geolocation.py b/components/dash-core-components/tests/integration/geolocation/test_geolocation.py index d6a6afe592..79d58c9fe5 100644 --- a/components/dash-core-components/tests/integration/geolocation/test_geolocation.py +++ b/components/dash-core-components/tests/integration/geolocation/test_geolocation.py @@ -4,7 +4,9 @@ from dash import Dash, dcc, html, Input, Output -@pytest.mark.skipif(os.getenv("CIRCLECI" is not None), reason="geolocation is disabled on CI") +@pytest.mark.skipif( + os.getenv("CIRCLECI") is not None, reason="geolocation is disabled on CI" +) def test_geol001_position(dash_dcc): dash_dcc.driver.execute_cdp_cmd( "Emulation.setGeolocationOverride", From af4caec97c769fb99d48b74ad173a14f5f933784 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Sat, 3 Dec 2022 12:58:46 -0700 Subject: [PATCH 09/12] update after review --- CHANGELOG.md | 1 + .../src/components/Geolocation.react.js | 9 +- usage_geolocation_demo.py | 292 ------------------ usage_geolocation_quickstart.py | 34 -- 4 files changed, 6 insertions(+), 330 deletions(-) delete mode 100644 usage_geolocation_demo.py delete mode 100644 usage_geolocation_quickstart.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 422026fc62..3d5a960330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Added +- [#2349](https://github.com/plotly/dash/pull/2349) Added new `dcc.Geolocation` component. - [#2261](https://github.com/plotly/dash/pull/2261) Added new `placeholder_text` property to `filterOptions` for DataTable which allows overriding the default filter field placeholder. ### Updated diff --git a/components/dash-core-components/src/components/Geolocation.react.js b/components/dash-core-components/src/components/Geolocation.react.js index 2729a26b36..c66dc405f2 100644 --- a/components/dash-core-components/src/components/Geolocation.react.js +++ b/components/dash-core-components/src/components/Geolocation.react.js @@ -14,6 +14,11 @@ export default class Geolocation extends Component { this.updatePosition = this.updatePosition.bind(this); } updatePosition() { + if (this.props.update_now) { + this.props.setProps({ + update_now: false, + }); + } if (!navigator.geolocation) { this.error({ code: 999, @@ -23,10 +28,6 @@ export default class Geolocation extends Component { alert(`ERROR(${this.error.code}): ${this.error.message}`); } } else { - this.props.setProps({ - update_now: false, - }); - const positionOptions = { enableHighAccuracy: this.props.high_accuracy, maximumAge: this.props.maximum_age, diff --git a/usage_geolocation_demo.py b/usage_geolocation_demo.py deleted file mode 100644 index 6a66ecd7ca..0000000000 --- a/usage_geolocation_demo.py +++ /dev/null @@ -1,292 +0,0 @@ -import datetime as dt -from dash import Dash, dcc, html, Input, Output, dash_table, no_update -import plotly.graph_objects as go -from geopy.geocoders import Nominatim -import pandas as pd -import dash_bootstrap_components as dbc - - -app = Dash( - __name__, - prevent_initial_callbacks=True, - suppress_callback_exceptions=True, - external_stylesheets=[dbc.themes.SPACELAB], -) - -df = pd.DataFrame( - { - "lat": 0, - "lon": 0, - "alt": 0, - "accuracy": 0, - "altAccuracy": 0, - "heading": 0, - "speed": 0, - }, - index=[0], -) - -""" -============================================================================== -Markdown -""" -markdown_card = dbc.Card( - [ - dbc.CardHeader( - "Notes on Geolocation component and App settings", - className="font-weight-bold", - ), - dcc.Markdown( - """ - -- The __Update Now__ button sets the `update_now` prop to `True` in a callback. This does a one-time update of the -position data. - -- __Include Address__ This is a demo of how geopy can be used to to get an address based on the - position data from `dcc.Geolocation`. Sometimes it's slow. If the timeout is small, - it's better not to select this option. - -- __Enable High Accuracy__ This sets the `high_accuracy` prop. If selected, if the device is able to provide a -more accurate position, it will do so. Note that this can result in slower response times or increased power -consumption (with a GPS on a mobile device for example). If `False` (the default value), the device can save resources -by responding more quickly and/or using less power. Note: When selected, timeout should be set to a high number or a -max age should be provided as GPS sometimes takes longer to update. - -- __Show errors as alert__ This sets the `show_alert` prop. When `True` error messages will be displayed as an -`alert` in the browser. When `False` you can provide your own custom error handling in the app. - - -- __Max Age__ The `maximum_age` prop will provide a cached location after specified number of milliseconds if no new -position data is available before the timeout. If set to zero, it will not use a cached data. - -- __Timeout__ The `timeout` prop is the number of milliseconds allowed for the position to update without -generating an error message. - -- The __dates and times__ show when the position data was obtained. The date reflects the - current system time from the computer running the browser. The accuracy is dependant on this being set correctly - in the user's browser. - -- __Zoom and Center__ is not a component prop. In this demo app, the map uses uirevision to hold the user -settings for pan, zoom etc, so those won't change when the position data is updated. - -- __Follow me__ In this demo app, dcc.Interval is used to set the `update_now` prop to -True at the interval time specified. To stop following, set the interval time to zero. The app will - then disable `dcc.Interval` in a callback so the position will no longer be updated. - - """ - ), - ], - className="my-5", -) - - -""" -=============================================================================== -Map and address -""" - - -def get_address(lat, lon, show_address): - address = "" - if show_address: - geolocator = Nominatim(user_agent="my_location") - try: - location = geolocator.reverse(",".join([str(lat), str(lon)])) - address = location.address - except: # noqa: E722 - address = "address unavailable" - return address - - -def make_map(position, show_address, zoom=12): - lat = position["lat"] - lon = position["lon"] - fig = go.Figure( - go.Scattermapbox( - lat=[lat], - lon=[lon], - mode="markers", - marker=go.scattermapbox.Marker(size=14), - text=[get_address(lat, lon, show_address)], - ) - ) - fig.update_layout( - hovermode="closest", - mapbox=dict( - style="open-street-map", - center=go.layout.mapbox.Center(lat=lat, lon=lon), - zoom=zoom, - ), - uirevision=zoom, - ) - return dcc.Graph(figure=fig) - - -def make_position_card(pos, date, show_address): - return html.Div( - [ - html.H4(f"As of {date} your location:"), - html.P( - "within {} meters of lat,lon: ( {:.2f}, {:.2f})".format( - pos["accuracy"], pos["lat"], pos["lon"] - ) - ), - html.P(get_address(pos["lat"], pos["lon"], show_address)), - ] - ) - - -""" -=============================================================================== -Input components -""" - - -update_btn = dbc.Button( - "Update Now", - id="update_btn", - className="m-2", - color="secondary", -) -options_checklist = dbc.Checklist( - options=[ - {"label": "Include Address", "value": "address"}, - {"label": "Enable High Accuracy", "value": "high_accuracy"}, - {"label": "Show errors as alert", "value": "alert"}, - ], - className="mt-4", -) -max_age = dbc.Input( - placeholder="milliseconds", - type="number", - debounce=True, - value=0, - min=0, -) - -timeout_input = dbc.Input( - type="number", - debounce=True, - value=10000, - min=0, -) - -zoom_center = dbc.Input(type="number", value=8, min=0, max=22, step=1) - -follow_me_interval = dbc.Input(type="number", debounce=True, value=0, min=0, step=1000) - -geolocation = dcc.Geolocation() -interval = dcc.Interval(disabled=True) - -input_card = dbc.Card( - [ - update_btn, - options_checklist, - "Timeout (ms)", - timeout_input, - "Max Age (ms)", - max_age, - "Zoom and Center", - zoom_center, - "Follow me(update interval ms)", - follow_me_interval, - geolocation, - interval, - ], - body=True, -) - -output_card = dbc.Card(body=True) - -app.layout = dbc.Container( - [ - html.Div("dcc.Geolocation demo", className="bg-primary text-white p-2 h3 mb-2"), - dbc.Row([dbc.Col(input_card, width=4), dbc.Col(output_card, width=8)]), - dbc.Row(dbc.Col(markdown_card)), - ], - fluid=True, -) - - -@app.callback( - Output(geolocation, "high_accuracy"), - Output(geolocation, "maximum_age"), - Output(geolocation, "timeout"), - Output(geolocation, "show_alert"), - Input(options_checklist, "value"), - Input(max_age, "value"), - Input(timeout_input, "value"), -) -def update_options(checked, maxage, timeout): - if checked: - high_accuracy = True if "high_accuracy" in checked else False - alert = True if "alert" in checked else False - else: - high_accuracy, alert = False, False - return high_accuracy, maxage, timeout, alert - - -@app.callback( - Output(geolocation, "update_now"), - Input(update_btn, "n_clicks"), - Input(interval, "n_intervals"), - prevent_initial_call=True, -) -def update_now(*_): - return True - - -@app.callback( - Output(interval, "interval"), - Output(interval, "disabled"), - Input(follow_me_interval, "value"), -) -def update_interval(time): - disabled = True if time == 0 else False - return time, disabled - - -@app.callback( - Output(output_card, "children"), - Input(options_checklist, "value"), - Input(zoom_center, "value"), - Input(geolocation, "local_date"), - Input(geolocation, "timestamp"), - Input(geolocation, "position"), - Input(geolocation, "position_error"), - prevent_initial_call=True, -) -def display_output(checklist, zoom, date, timestamp, pos, err): - if err: - return f"Error {err['code']} : {err['message']}" - - # update message - show_address = True if checklist and "address" in checklist else False - position = ( - make_position_card(pos, date, show_address) - if pos - else "No position data available" - ) - - # update map - graph_map = make_map(pos, show_address, zoom) if pos else {} - - # update position data and error messages - dff = pd.DataFrame(pos, index=[0]) - position_table = dash_table.DataTable( - columns=[{"name": i, "id": i} for i in dff.columns], data=dff.to_dict("records") - ) - - # display iso dates - timestamp = timestamp if timestamp else 0 - datetime_local = dt.datetime.fromtimestamp(int(timestamp / 1000)) - datetime_utc = dt.datetime.utcfromtimestamp(int(timestamp / 1000)) - iso_date = "UTC datetime: {} Local datetime: {}".format( - datetime_utc, datetime_local - ) - - return [position, graph_map, position_table, iso_date] - - -if __name__ == "__main__": - app.run_server(debug=True) diff --git a/usage_geolocation_quickstart.py b/usage_geolocation_quickstart.py deleted file mode 100644 index 1ba0201d25..0000000000 --- a/usage_geolocation_quickstart.py +++ /dev/null @@ -1,34 +0,0 @@ -from dash import Dash, dcc, html, Input, Output - -app = Dash(__name__) - -app.layout = html.Div( - [ - html.Button("Update Position", id="update_btn"), - dcc.Geolocation(id="geolocation"), - html.Div(id="text_position"), - ] -) - - -@app.callback(Output("geolocation", "update_now"), Input("update_btn", "n_clicks")) -def update_now(click): - return True if click and click > 0 else False - - -@app.callback( - Output("text_position", "children"), - Input("geolocation", "local_date"), - Input("geolocation", "position"), -) -def display_output(date, pos): - if pos: - return html.P( - f"As of {date} your location was: lat {pos['lat']},lon {pos['lon']}, accuracy {pos['accuracy']} meters", - ) - else: - return "No position data available" - - -if __name__ == "__main__": - app.run_server(debug=True) From 4a7b79d639ec75628caf40eb81575a537aacf45d Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Tue, 7 Feb 2023 13:08:14 -0700 Subject: [PATCH 10/12] update after review --- .../dash-core-components/src/components/Geolocation.react.js | 1 - 1 file changed, 1 deletion(-) diff --git a/components/dash-core-components/src/components/Geolocation.react.js b/components/dash-core-components/src/components/Geolocation.react.js index c66dc405f2..66357af6e6 100644 --- a/components/dash-core-components/src/components/Geolocation.react.js +++ b/components/dash-core-components/src/components/Geolocation.react.js @@ -11,7 +11,6 @@ export default class Geolocation extends Component { super(props); this.success = this.success.bind(this); this.error = this.error.bind(this); - this.updatePosition = this.updatePosition.bind(this); } updatePosition() { if (this.props.update_now) { From 0f780ae9aeed385e485a95051a74bf2324b77caa Mon Sep 17 00:00:00 2001 From: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com> Date: Thu, 23 Feb 2023 17:34:58 -0700 Subject: [PATCH 11/12] Update components/dash-core-components/src/components/Geolocation.react.js Co-authored-by: Alex Johnson --- .../dash-core-components/src/components/Geolocation.react.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/components/dash-core-components/src/components/Geolocation.react.js b/components/dash-core-components/src/components/Geolocation.react.js index 66357af6e6..2264a375cb 100644 --- a/components/dash-core-components/src/components/Geolocation.react.js +++ b/components/dash-core-components/src/components/Geolocation.react.js @@ -23,9 +23,6 @@ export default class Geolocation extends Component { code: 999, message: 'Your browser does not support Geolocation', }); - if (this.props.show_alert) { - alert(`ERROR(${this.error.code}): ${this.error.message}`); - } } else { const positionOptions = { enableHighAccuracy: this.props.high_accuracy, From 5300245e4a82a7b42694120cf4f7c42416ac3b75 Mon Sep 17 00:00:00 2001 From: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com> Date: Fri, 24 Feb 2023 07:59:18 -0700 Subject: [PATCH 12/12] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc9ab25a7e..651cf5918e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,7 +73,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Added -- [#2349](https://github.com/plotly/dash/pull/2349) Added new `dcc.Geolocation` component. +- [#2349](https://github.com/plotly/dash/pull/2349) Added new `dcc.Geolocation` component - [#2261](https://github.com/plotly/dash/pull/2261) Added new `placeholder_text` property to `filterOptions` for DataTable which allows overriding the default filter field placeholder. ### Updated