Activities: display gpx on map
This commit is contained in:
		@@ -55,6 +55,61 @@ def get_activity(auth_user_id, activity_id):
 | 
			
		||||
    return jsonify(response_object), code
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@activities_blueprint.route(
 | 
			
		||||
    '/activities/<int:activity_id>/gpx',
 | 
			
		||||
    methods=['GET']
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_activity_gpx(auth_user_id, activity_id):
 | 
			
		||||
    """Get gpx file for an activity"""
 | 
			
		||||
    activity = Activity.query.filter_by(id=activity_id).first()
 | 
			
		||||
    gpx_content = ''
 | 
			
		||||
 | 
			
		||||
    message = ''
 | 
			
		||||
    code = 500
 | 
			
		||||
    response_object = {
 | 
			
		||||
        'status': 'error',
 | 
			
		||||
        'message': 'internal error',
 | 
			
		||||
        'data': {
 | 
			
		||||
            'gpx': gpx_content
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if activity:
 | 
			
		||||
        if not activity.gpx or activity.gpx == '':
 | 
			
		||||
            response_object = {
 | 
			
		||||
                'status': 'fail',
 | 
			
		||||
                'message': 'No gpx file for this activity (id: {})'.format(
 | 
			
		||||
                    activity_id
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            code = 400
 | 
			
		||||
            return jsonify(response_object), code
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            with open(activity.gpx, encoding='utf-8') as f:
 | 
			
		||||
                gpx_content = gpx_content + f.read()
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            appLog.error(e)
 | 
			
		||||
            return jsonify(response_object), code
 | 
			
		||||
 | 
			
		||||
        status = 'success'
 | 
			
		||||
        code = 200
 | 
			
		||||
    else:
 | 
			
		||||
        status = 'not found'
 | 
			
		||||
        message = 'Activity not found (id: {})'.format(activity_id)
 | 
			
		||||
        code = 404
 | 
			
		||||
 | 
			
		||||
    response_object = {
 | 
			
		||||
        'status': status,
 | 
			
		||||
        'message': message,
 | 
			
		||||
        'data': {
 | 
			
		||||
            'gpx': gpx_content
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return jsonify(response_object), code
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@activities_blueprint.route('/activities', methods=['POST'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def post_activity(auth_user_id):
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,10 @@ import mpwoApi from '../mwpoApi/activities'
 | 
			
		||||
import { history } from '../index'
 | 
			
		||||
import { setError } from './index'
 | 
			
		||||
 | 
			
		||||
export const setGpx = gpxContent => ({
 | 
			
		||||
  type: 'SET_GPX',
 | 
			
		||||
  gpxContent,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export function addActivity(form) {
 | 
			
		||||
  return function(dispatch) {
 | 
			
		||||
@@ -17,3 +21,18 @@ export function addActivity(form) {
 | 
			
		||||
    .catch(error => dispatch(setError(`activities: ${error}`)))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getActivityGpx(activityId) {
 | 
			
		||||
  return function(dispatch) {
 | 
			
		||||
    return mpwoApi
 | 
			
		||||
    .getActivityGpx(activityId)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'success') {
 | 
			
		||||
         dispatch(setGpx(ret.data.gpx))
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(setError(`activities: ${ret.message}`))
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => dispatch(setError(`activities: ${error}`)))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -73,7 +73,7 @@ class ActivityDisplay extends React.Component {
 | 
			
		||||
                  Map
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="card-body">
 | 
			
		||||
                  <ActivityMap />
 | 
			
		||||
                  <ActivityMap activity={activity} />
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,57 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Map, TileLayer } from 'react-leaflet'
 | 
			
		||||
import { GeoJSON, Map, TileLayer } from 'react-leaflet'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import { thunderforestApiKey } from '../../utils'
 | 
			
		||||
import { getActivityGpx } from '../../actions/activities'
 | 
			
		||||
import { getGeoJson, thunderforestApiKey } from '../../utils'
 | 
			
		||||
 | 
			
		||||
export default class ActivityMap extends React.Component {
 | 
			
		||||
class ActivityMap extends React.Component {
 | 
			
		||||
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
    lat: 51.505,
 | 
			
		||||
    lng: -0.09,
 | 
			
		||||
    zoom: 13,
 | 
			
		||||
      zoom: 13,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadActivityGpx(this.props.activity.id)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const position = [this.state.lat, this.state.lng]
 | 
			
		||||
    const { gpxContent } = this.props
 | 
			
		||||
    const { jsonData, bounds } = getGeoJson(gpxContent)
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Map center={position} zoom={this.state.zoom}>
 | 
			
		||||
        <TileLayer
 | 
			
		||||
          // eslint-disable-next-line max-len
 | 
			
		||||
          attribution='© <a href="http://www.thunderforest.com/">Thunderforest</a>, © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
 | 
			
		||||
          url="https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png"
 | 
			
		||||
          apikey={thunderforestApiKey}
 | 
			
		||||
        />
 | 
			
		||||
      </Map>
 | 
			
		||||
      <div>
 | 
			
		||||
        {jsonData && (
 | 
			
		||||
          <Map
 | 
			
		||||
            zoom={this.state.zoom}
 | 
			
		||||
            bounds={bounds}
 | 
			
		||||
            boundsOptions={{ padding: [50, 50] }}
 | 
			
		||||
          >
 | 
			
		||||
            <TileLayer
 | 
			
		||||
              // eslint-disable-next-line max-len
 | 
			
		||||
              attribution='© <a href="http://www.thunderforest.com/">Thunderforest</a>, © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
 | 
			
		||||
              // eslint-disable-next-line max-len
 | 
			
		||||
              url={`https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png?apikey=${thunderforestApiKey}`}
 | 
			
		||||
            />
 | 
			
		||||
            <GeoJSON data={jsonData} />
 | 
			
		||||
          </Map>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(
 | 
			
		||||
  state => ({
 | 
			
		||||
    gpxContent: state.gpx
 | 
			
		||||
  }),
 | 
			
		||||
  dispatch => ({
 | 
			
		||||
    loadActivityGpx: activityId => {
 | 
			
		||||
      dispatch(getActivityGpx(activityId))
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)(ActivityMap)
 | 
			
		||||
 
 | 
			
		||||
@@ -15,4 +15,16 @@ export default class MpwoApi {
 | 
			
		||||
      .catch(error => error)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getActivityGpx(activityId) {
 | 
			
		||||
    const request = new Request(`${apiUrl}activities/${activityId}/gpx`, {
 | 
			
		||||
      method: 'GET',
 | 
			
		||||
      headers: new Headers({
 | 
			
		||||
        Authorization: `Bearer ${window.localStorage.getItem('authToken')}`,
 | 
			
		||||
      }),
 | 
			
		||||
    })
 | 
			
		||||
    return fetch(request)
 | 
			
		||||
      .then(response => response.json())
 | 
			
		||||
      .catch(error => error)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -66,6 +66,15 @@ const formProfile = (state = initial.formProfile, action) => {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const gpx = (state = initial.gpx, action) => {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
    case 'SET_GPX':
 | 
			
		||||
      return action.gpxContent
 | 
			
		||||
    default:
 | 
			
		||||
      return state
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const message = (state = initial.message, action) => {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
    case 'AUTH_ERROR':
 | 
			
		||||
@@ -144,6 +153,7 @@ const reducers = combineReducers({
 | 
			
		||||
  activities,
 | 
			
		||||
  formData,
 | 
			
		||||
  formProfile,
 | 
			
		||||
  gpx,
 | 
			
		||||
  message,
 | 
			
		||||
  messages,
 | 
			
		||||
  router: routerReducer,
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,8 @@ export default {
 | 
			
		||||
  activities: {
 | 
			
		||||
    ...emptyData,
 | 
			
		||||
  },
 | 
			
		||||
  // check if storing gpx content is OK
 | 
			
		||||
  gpx: null,
 | 
			
		||||
  sports: {
 | 
			
		||||
    ...emptyData,
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,6 @@
 | 
			
		||||
import togeojson from '@mapbox/togeojson'
 | 
			
		||||
import bbox from 'geojson-bbox'
 | 
			
		||||
 | 
			
		||||
export const apiUrl = `${process.env.REACT_APP_API_URL}/api/`
 | 
			
		||||
export const thunderforestApiKey = `${
 | 
			
		||||
  process.env.REACT_APP_THUNDERFOREST_API_KEY
 | 
			
		||||
@@ -13,3 +16,18 @@ export function generateIds(arr) {
 | 
			
		||||
        return obj
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getGeoJson(gpxContent) {
 | 
			
		||||
  let jsonData, bboxCorners
 | 
			
		||||
  let bounds = [[0, 0], [0, 0]]
 | 
			
		||||
  if (gpxContent) {
 | 
			
		||||
    const gpx = new DOMParser().parseFromString(gpxContent, 'text/xml')
 | 
			
		||||
    jsonData = togeojson.gpx(gpx)
 | 
			
		||||
    bboxCorners = bbox(jsonData)
 | 
			
		||||
      bounds = [
 | 
			
		||||
      [bboxCorners[1], bboxCorners[0]],
 | 
			
		||||
      [bboxCorners[3], bboxCorners[2]]
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
  return { jsonData, bounds }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,10 @@
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@mapbox/togeojson": "^0.16.0",
 | 
			
		||||
    "chalk": "2.3.0",
 | 
			
		||||
    "date-fns": "^1.29.0",
 | 
			
		||||
    "geojson-bbox": "^0.0.0",
 | 
			
		||||
    "history": "^4.7.2",
 | 
			
		||||
    "leaflet": "^1.3.1",
 | 
			
		||||
    "react": "^16.3.1",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										39
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								yarn.lock
									
									
									
									
									
								
							@@ -55,6 +55,14 @@
 | 
			
		||||
    lodash "^4.2.0"
 | 
			
		||||
    to-fast-properties "^2.0.0"
 | 
			
		||||
 | 
			
		||||
"@mapbox/togeojson@^0.16.0":
 | 
			
		||||
  version "0.16.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@mapbox/togeojson/-/togeojson-0.16.0.tgz#5b283001078431821dc74e287acaf56a11ceb37c"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    concat-stream "~1.5.1"
 | 
			
		||||
    minimist "1.2.0"
 | 
			
		||||
    xmldom "~0.1.19"
 | 
			
		||||
 | 
			
		||||
"@types/chalk@^0.4.31":
 | 
			
		||||
  version "0.4.31"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-0.4.31.tgz#a31d74241a6b1edbb973cf36d97a2896834a51f9"
 | 
			
		||||
@@ -1692,6 +1700,14 @@ concat-stream@^1.6.0:
 | 
			
		||||
    readable-stream "^2.2.2"
 | 
			
		||||
    typedarray "^0.0.6"
 | 
			
		||||
 | 
			
		||||
concat-stream@~1.5.1:
 | 
			
		||||
  version "1.5.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    inherits "~2.0.1"
 | 
			
		||||
    readable-stream "~2.0.0"
 | 
			
		||||
    typedarray "~0.0.5"
 | 
			
		||||
 | 
			
		||||
configstore@^2.0.0:
 | 
			
		||||
  version "2.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/configstore/-/configstore-2.1.0.tgz#737a3a7036e9886102aa6099e47bb33ab1aba1a1"
 | 
			
		||||
@@ -3057,6 +3073,10 @@ gauge@~2.7.3:
 | 
			
		||||
    strip-ansi "^3.0.1"
 | 
			
		||||
    wide-align "^1.1.0"
 | 
			
		||||
 | 
			
		||||
geojson-bbox@^0.0.0:
 | 
			
		||||
  version "0.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/geojson-bbox/-/geojson-bbox-0.0.0.tgz#ea07fb62610ccc299879601a55ba884139bc1be8"
 | 
			
		||||
 | 
			
		||||
get-caller-file@^1.0.1:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
 | 
			
		||||
@@ -4619,7 +4639,7 @@ minimist@0.0.8:
 | 
			
		||||
  version "0.0.8"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
 | 
			
		||||
 | 
			
		||||
minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
 | 
			
		||||
minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
 | 
			
		||||
  version "1.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
 | 
			
		||||
 | 
			
		||||
@@ -5866,6 +5886,17 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable
 | 
			
		||||
    string_decoder "~1.0.3"
 | 
			
		||||
    util-deprecate "~1.0.1"
 | 
			
		||||
 | 
			
		||||
readable-stream@~2.0.0:
 | 
			
		||||
  version "2.0.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    core-util-is "~1.0.0"
 | 
			
		||||
    inherits "~2.0.1"
 | 
			
		||||
    isarray "~1.0.0"
 | 
			
		||||
    process-nextick-args "~1.0.6"
 | 
			
		||||
    string_decoder "~0.10.x"
 | 
			
		||||
    util-deprecate "~1.0.1"
 | 
			
		||||
 | 
			
		||||
readdirp@^2.0.0:
 | 
			
		||||
  version "2.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
 | 
			
		||||
@@ -7003,7 +7034,7 @@ type-is@~1.6.15:
 | 
			
		||||
    media-typer "0.3.0"
 | 
			
		||||
    mime-types "~2.1.15"
 | 
			
		||||
 | 
			
		||||
typedarray@^0.0.6:
 | 
			
		||||
typedarray@^0.0.6, typedarray@~0.0.5:
 | 
			
		||||
  version "0.0.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
 | 
			
		||||
 | 
			
		||||
@@ -7472,6 +7503,10 @@ xml-name-validator@^2.0.1:
 | 
			
		||||
  version "2.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
 | 
			
		||||
 | 
			
		||||
xmldom@~0.1.19:
 | 
			
		||||
  version "0.1.27"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
 | 
			
		||||
 | 
			
		||||
xtend@^4.0.0, xtend@^4.0.1:
 | 
			
		||||
  version "4.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user