Difference between revisions of "Walkyourplace Django"
(→Características nuevas) |
|||
(15 intermediate revisions by one user not shown) | |||
Line 1: | Line 1: | ||
+ | Update 08-09-2016 | ||
+ | |||
= State of the art = | = State of the art = | ||
Line 79: | Line 81: | ||
* test.py | * test.py | ||
* views.py | * views.py | ||
+ | En donde en models.py creamos clases que corresponderán a los modelos en la base de datos, en views.py creamos las funciones que guiarán en la aparición de las vistas (Que posteriormente llamaremos con urls.py), admin.py que es donde se registran los modelos de la bse de datos que se quieran mostrar en el panel de administración. Test.py para realizar pruebas (nunca lo he ocupado) e __init__.py para que sea reconocido por manage.py. | ||
+ | En wyp/views.py editamos lo siguiente: | ||
− | = | + | <source lang="python"> |
+ | from django.shortcuts import render_to_response | ||
+ | from django.template.context import RequestContext | ||
− | + | def inicio(request): | |
+ | return render_to_response('index.html',context_instance=RequestContext(request)) | ||
+ | </source> | ||
+ | |||
+ | y en wypdjango/urls.py editamos: | ||
+ | |||
+ | <source lang="python"> | ||
+ | from wyp.views import inicio | ||
+ | |||
+ | urlpatterns = patterns('', | ||
+ | ... | ||
+ | url(r'^admin/', include(admin.site.urls)), | ||
+ | url(r'^$', inicio), | ||
+ | ) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) | ||
+ | urlpatterns += staticfiles_urlpatterns() | ||
+ | </source> | ||
+ | |||
+ | Nos posicionamos en la raíz y ejecutamos los siguientes comandos: | ||
+ | |||
+ | <source lang="bash"> | ||
+ | python manage.py collectstatics #Rescata los archivos css y js del panel de administración y de los que tenga el template html | ||
+ | python manage.py makemigrations #Crea los archivos .py para editar la base de datos | ||
+ | python manage.py migrate #Edita la base de datos con los cambios juntados en makemigrations | ||
+ | python manage.py runserver #Corre un servidor local con la web | ||
+ | </source> | ||
+ | |||
+ | Con el último comando corriendo, nos dirigimos a un navegador web y entramos a http://localhost:8000/ y nos encontraremos con el index.html de walkyourplace. | ||
+ | |||
+ | Ya tenemos al menos la vista principal del proyecto, ahora debemos hacerlo funcionar. | ||
+ | |||
+ | Vemos que en la web inicial los request se solicitan a la url /call_wps.php en conjunto de los datos enviados vía GET, como ahora estamos trabajando en Django dicha url no está apuntando a nada ya que no se encuentra registrada en urls.py, por lo que el código php que junta esta información y realiza la petición de la información se debe construir esta vez en Python, para ello necesitamos los archivos que procesan la información (2) y pasarlos al proyecto. | ||
+ | |||
+ | == Bike Model == | ||
+ | |||
+ | Tomando de (2) los archivos de la carpeta Bike (AggregationService.py, CrimeService.py, ManagementService.py, POIService.py y config.py) y los copiamos dentro de la carpeta '''wyp'''. Tenemos que editarlos cada uno de ellos. | ||
+ | |||
+ | En AggregationService.py eliminamos las siguientes líneas: | ||
+ | <source lang="python"> | ||
+ | from bottle import route, run, request | ||
+ | import bottle | ||
+ | |||
+ | bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 | ||
+ | |||
+ | ... | ||
+ | |||
+ | @route('/aggregation', method='POST') | ||
+ | def service(): | ||
+ | poi = request.POST.get('poi', default=None) | ||
+ | crime = request.POST.get('crime', default=None) | ||
+ | walkshed = request.POST.get('bikeshed', default=None) | ||
+ | start_point = request.POST.get('start_point', default=None) | ||
+ | distance_decay_function = request.POST.get('distance_decay_function', default=None).lower() | ||
+ | walking_time_period = request.POST.get('biking_time_period', default=None) | ||
+ | if start_point and poi and crime and walkshed and distance_decay_function and walking_time_period is not None: | ||
+ | return aggregation(start_point, walkshed, crime, poi, distance_decay_function, walking_time_period) | ||
+ | |||
+ | |||
+ | run(host='0.0.0.0', port=7364, debug=True) | ||
+ | </source> | ||
+ | |||
+ | y modificamos la línea | ||
+ | |||
+ | <source lang="python"> | ||
+ | otp_url = "http://www.gisciencegroup.ucalgary.ca:8080/opentripplanner-api-webapp/ws/plan?arriveBy=false&time=6%3A58pm&ui_date=1%2F10%2F2013&mode=BICYCLE&optimize=QUICK&maxWalkDistance=5000&walkSpeed=1.38&date=2013-01-10&" | ||
+ | </source> | ||
+ | |||
+ | por | ||
+ | |||
+ | <source lang="python"> | ||
+ | otp_url = "http://146.155.17.18:23080/otp/routers/default/plan?arriveBy=false&time=6%3A58pm&ui_date=1%2F10%2F2013&mode=BICYCLE&optimize=QUICK&maxWalkDistance=5000&walkSpeed=1.38&date=2013-01-10&" | ||
+ | </source> | ||
+ | |||
+ | |||
+ | Para crime.py hay que hacer cambios drásticos ya que en Chile no contamos con la información geográfica de los crímenes de la ciudad, por lo que necesitamos que simplemente nos retorne 'NULL', aparte de eso debemos quitar todo lo relacionado con el paquete bottle. Por lo que eliminamos las líneas: | ||
+ | |||
+ | <source lang="python"> | ||
+ | from bottle import route, run, request | ||
+ | import bottle | ||
+ | bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 | ||
+ | |||
+ | @route('/crime') | ||
+ | def service(): | ||
+ | polygon = request.GET.get('bikeshed', default=None) | ||
+ | if polygon is not None: | ||
+ | return pointInPolygon(polygon) | ||
+ | |||
+ | run(host='0.0.0.0', port=7366, debug=True) | ||
+ | </source> | ||
+ | |||
+ | y agregamos después de la definición de la función: | ||
+ | |||
+ | <source lang="python"> | ||
+ | def pointInPolygon(polygon): | ||
+ | return '"NULL"' | ||
+ | </source> | ||
+ | |||
+ | En POIService.py también debemos eliminar lo relacionado con bottle, similar a los archivos anteriores, y debemos editar la línea: | ||
+ | |||
+ | <source lang="python"> | ||
+ | _overpass_url = 'http://overpass-api.de/api/interpreter' | ||
+ | </source> | ||
+ | |||
+ | por | ||
+ | |||
+ | <source lang="python"> | ||
+ | _overpass_url = 'http://146.155.17.18:24080/api/interpreter' | ||
+ | </source> | ||
+ | |||
+ | y en ManagementService.py primero le cambiaremos el nombre a ManagementServiceBike.py (mas adelante se verá porqué), luego eliminamos las siguientes líneas: | ||
+ | |||
+ | <source lang="python"> | ||
+ | from bottle import route, run, request | ||
+ | import bottle | ||
+ | bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 | ||
+ | |||
+ | ... | ||
+ | |||
+ | @route('/management') | ||
+ | def service(): | ||
+ | start_point = request.GET.get('start_point', default=None) | ||
+ | biking_time_period = request.GET.get('biking_time_period', default=None) | ||
+ | distance_decay_function = request.GET.get('distance_decay_function', default=None) | ||
+ | |||
+ | if start_point and biking_time_period is not None: | ||
+ | return management(start_point, biking_time_period, distance_decay_function) | ||
+ | run(host='0.0.0.0', port=7363, debug=True) | ||
+ | </source> | ||
+ | |||
+ | y rehacemos por completo la función management(), ya que está configurada para enviar las solicitudes a geoserver, esta vez la configuraremos para que funcione dentro de Django. | ||
+ | |||
+ | <source lang="python"> | ||
+ | def management(start_point, biking_time_period, distance_decay_function): | ||
+ | otp_isochrone_url = 'http://146.155.17.18:23080/otp/routers/default/plan?&' | ||
+ | otp_dataInputs = 'fromPlace=%s&date=2016/05/05&time=12:00:00&mode=BICYCLE&cutoffSec=%s' % (start_point,str(int(biking_time_period)*60)) | ||
+ | otp_url = otp_isochrone_url + otp_dataInputs | ||
+ | try: | ||
+ | bikeshed = urllib2.urlopen(otp_url).read() | ||
+ | except: | ||
+ | return '{"error":"OTP does not respond"}' | ||
+ | bikeshed = removeNonAscii(bikeshed) | ||
+ | bikeshed = bikeshed.replace('&','and') | ||
+ | bikeshed = bikeshed.replace(';',' ') | ||
+ | bikeshed = json.loads(bikeshed) | ||
+ | bikeshed = '{"type":"Polygon","coordinates":'+str(bikeshed['features'][0]['geometry']['coordinates'][0])+'}' | ||
+ | try: | ||
+ | poi_data = getPOIs(bikeshed) | ||
+ | except: | ||
+ | return '{"error":"Overpass does not respond"}' | ||
+ | crime_data = pointInPolygon(bikeshed) | ||
+ | aggregation_data = aggregation(start_point,bikeshed,crime_data,poi_data,distance_decay_function,biking_time_period) | ||
+ | result = '{"walkshed": %s, "poi": %s}' % (aggregation_data, poi_data) | ||
+ | return result | ||
+ | </source> | ||
+ | |||
+ | Finalmente necesitamos enlazar todo este código, por lo que creamos una nueva función en el archivo wyp/views.py con: | ||
+ | |||
+ | <source lang="python"> | ||
+ | from wyp.ManagementServiceBike import management as managementBike | ||
+ | def xml(request): | ||
+ | approach = request.GET.get('wps',default=None) | ||
+ | if approach == 'bike': | ||
+ | start_point = request.GET.get('start_point', default=None) | ||
+ | biking_time_period = request.GET.get('biking_time_period', default=None) | ||
+ | distance_decay_function = request.GET.get('distance_decay_function', default=None) | ||
+ | return HttpResponse(managementBike(start_point,biking_time_period,distance_decay_function)) | ||
+ | </source> | ||
+ | |||
+ | y lo agregamos en wypdjango/urls.py | ||
+ | |||
+ | <source lang="python"> | ||
+ | from wyp.views import inicio, xml | ||
+ | ... | ||
+ | urlpatterns = patterns('', | ||
+ | ... | ||
+ | url(r'^call_wps.php',xml), | ||
+ | ) | ||
+ | </source> | ||
+ | |||
+ | y finalmente ejecutamos el comando para correr el servidor y probamos que todo funciona correctamente. | ||
+ | <source lang="bash"> | ||
+ | python manage.py runserver | ||
+ | </source> | ||
+ | |||
+ | == Pedestrian & Transit Model == | ||
+ | |||
+ | Ahora podemos agregar la característica de caminata en el proyecto, para ello copiamos solo el archivo ManagementService.py que se encuentra en Pedestrian en (2) y le cambiamos el nombre a ManagementServicePedestrian.py y hacemos algo muy similar que el archivo de Bike, primero eliminamos todo lo relacionado con bottle: | ||
+ | |||
+ | <source lang="python"> | ||
+ | from bottle import route, run, request | ||
+ | import bottle | ||
+ | bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 | ||
+ | |||
+ | @route('/management') | ||
+ | def service(): | ||
+ | start_point = request.GET.get('start_point', default=None) | ||
+ | walking_time_period = request.GET.get('walking_time_period', default=None) | ||
+ | walking_speed = request.GET.get('walking_speed', default=None) | ||
+ | distance_decay_function = request.GET.get('distance_decay_function', default=None) | ||
+ | if start_point and walking_time_period and walking_speed and distance_decay_function is not None: | ||
+ | return management(start_point, walking_time_period, walking_speed, distance_decay_function) | ||
+ | run(host='0.0.0.0', port=8363, debug=True) | ||
+ | </source> | ||
+ | |||
+ | y reacemos la función management(), la diferencia con Bike es que en éste se utiliza isochroneOLD para obtener el espacio posible, por lo que nuestra nueva función queda: | ||
+ | |||
+ | <source lang="python"> | ||
+ | def management(start_point, walking_time_period, walking_speed, distance_decay_function): | ||
+ | otp_isochroneOLD_url = 'http://146.155.17.18:23080/otp/routers/default/isochroneOld?' | ||
+ | otp_dataInputs = 'fromPlace=%s&walkTime=%s&walkSpeed=%s&mode=WALK&toPlace=-33.5846161,-70.5410151&output=SHED' % ( | ||
+ | start_point, walking_time_period, walking_speed) | ||
+ | otp_url = otp_isochroneOLD_url + otp_dataInputs | ||
+ | try: | ||
+ | walkshed = urllib2.urlopen(otp_url).read() | ||
+ | except: | ||
+ | return '{"error":"OTP does not respond"}' | ||
+ | try: | ||
+ | poi_data = getPOIs(walkshed) | ||
+ | except: | ||
+ | return '{"error":"Overpass does not respond"}' | ||
+ | crime_data = pointInPolygon(walkshed) | ||
+ | aggregation_data = aggregation(start_point,walkshed,crime_data,poi_data,distance_decay_function,walking_time_period) | ||
+ | result = '{"walkshed": %s, "poi": %s}' % (aggregation_data, poi_data) | ||
+ | return result | ||
+ | </source> | ||
+ | |||
+ | y editamos el archivo wyp/views.py para que acepte los datos de Pedestrian: | ||
+ | |||
+ | <source lang="python"> | ||
+ | from ManagementServicePedestrian import management as managementPedestrian | ||
+ | |||
+ | def xml(request): | ||
+ | ... | ||
+ | elif approach == 'pedestrian': | ||
+ | start_point = request.GET.get('start_point', default=None) | ||
+ | walking_time_period = request.GET.get('walking_time_period', default=None) | ||
+ | walking_speed = request.GET.get('walking_speed', default=None) | ||
+ | distance_decay_function = request.GET.get('distance_decay_function', default=None) | ||
+ | return HttpResponse(managementPedestrian(start_point,walking_time_period,walking_speed,distance_decay_function)) | ||
+ | </source> | ||
+ | |||
+ | Y probamos como va funcionando con | ||
+ | |||
+ | <source lang="bash"> | ||
+ | python manage.py runserver | ||
+ | </source> | ||
+ | |||
+ | Para Transit hay que seguir pasos similares a Pedestrian, observar el código para ver como queda configurado. | ||
+ | |||
+ | == Resultados == | ||
+ | |||
+ | Finalmente el programa queda funcionando con muchos menos procesos y de forma más independiente, no requiere de una instalación de geoserver, tan solo Django Python, html, css y js. | ||
= Características nuevas = | = Características nuevas = | ||
− | + | Gracias a Django fue posible crear un panel de administración para modificar ciertos valores dentro del código, además de agregar características nuevas que se explicarán a continuación: | |
+ | |||
+ | == Demographic group == | ||
+ | |||
+ | La idea es evaluar los POIs para distintos grupos demográficos, quien hace ese trabajo es la función de AggregationService.py, pero para que la petición llegue desde la interfaz también debe de llegarle a ManagementService.py, por lo que realizaremos estas modificaciones: | ||
+ | |||
+ | Primero creamos el modelo de datos que contendrá a las opciones de grupos demográficos en la base de datos, para ello editamos el archivo wyp/models.py y agregamos lo siguiente: | ||
+ | |||
+ | <source lang="python"> | ||
+ | from django.db import models | ||
+ | |||
+ | class Demographic(models.Model): | ||
+ | value = models.CharField(max_length=255) #Nombre de la opcion demografica | ||
+ | bank = models.CharField(max_length=255) #Pesos | ||
+ | grocery = models.CharField(max_length=255) | ||
+ | restaurant = models.CharField(max_length=255) | ||
+ | shopping = models.CharField(max_length=255) | ||
+ | entertainment = models.CharField(max_length=255) | ||
+ | school = models.CharField(max_length=255) | ||
+ | library = models.CharField(max_length=255) | ||
+ | health = models.CharField(max_length=255) | ||
+ | sum = models.FloatField(blank=True,default=0) #Suma de los pesos anteriores | ||
+ | score_factor = models.FloatField(blank=True,default=0) #Valor de ponderacion | ||
+ | |||
+ | def __unicode__(self): | ||
+ | return self.value | ||
+ | |||
+ | #Generar el resultado de sum y score_factor automaticamente al guardar | ||
+ | def save(self, *args, **kwargs): | ||
+ | sumando = 0 | ||
+ | for i in ast.literal_eval(self.bank): | ||
+ | sumando += i | ||
+ | for i in ast.literal_eval(self.grocery): | ||
+ | sumando += i | ||
+ | for i in ast.literal_eval(self.restaurant): | ||
+ | sumando += i | ||
+ | for i in ast.literal_eval(self.shopping): | ||
+ | sumando += i | ||
+ | for i in ast.literal_eval(self.entertainment): | ||
+ | sumando += i | ||
+ | for i in ast.literal_eval(self.school): | ||
+ | sumando += i | ||
+ | for i in ast.literal_eval(self.library): | ||
+ | sumando += i | ||
+ | for i in ast.literal_eval(self.health): | ||
+ | sumando += i | ||
+ | self.sum = sumando | ||
+ | self.score_factor = 100.0/float(sumando) | ||
+ | super(Demographic,self).save(*args, **kwargs) | ||
+ | </source> | ||
+ | |||
+ | Agregamos este modelo al panel de administración editando el archivo '''wyp/admin.py''' | ||
+ | |||
+ | <source lang="python"> | ||
+ | from wyp.models import Demographic | ||
+ | |||
+ | admin.site.register(Demographic) | ||
+ | </source> | ||
+ | |||
+ | |||
+ | Modificamos el archivo wyp/views.py para mandar estos datos a la vista html | ||
+ | |||
+ | <source lang="python"> | ||
+ | def inicio(request): | ||
+ | demographic = Demographic.objects.all() | ||
+ | return render_to_response('index.html',{'demographic':demographic},context_instance=RequestContext(request)) | ||
+ | </source> | ||
+ | |||
+ | Luego modificamos el archivo html que se encuentra en template para que éstos aparezcan dinámicamente como una lista de opciones. template/index.html | ||
+ | |||
+ | <source lang="html4strict"> | ||
+ | <h3>Cycling Model</h3> | ||
+ | |||
+ | <div> | ||
+ | <form id="call_wps_bike" method="post" action="#"> | ||
+ | <fieldset> | ||
+ | <ul> | ||
+ | ... | ||
+ | |||
+ | <li><input type="checkbox" id="distance_decay_function_bike" | ||
+ | name="distance_decay_function"/>Distance | ||
+ | Decay Function | ||
+ | </li> | ||
+ | |||
+ | <li> | ||
+ | To: | ||
+ | <select id="demographic_bike"> | ||
+ | {% for demo in demographic%} | ||
+ | <option value="{{demo.value}}">{{demo.value}}</option> | ||
+ | {% endfor %} | ||
+ | </select> | ||
+ | </li> | ||
+ | |||
+ | <input type="button" class="button" onclick="call_wps('bike')" | ||
+ | value="Get Accessibility Score"/> | ||
+ | </ul> | ||
+ | </fieldset> | ||
+ | </form> | ||
+ | </div> | ||
+ | |||
+ | <h3>Walking Model</h3> | ||
+ | |||
+ | <div> | ||
+ | <form id="call_wps_pedestrian" method="post" action="#"> | ||
+ | <fieldset> | ||
+ | <ul> | ||
+ | <li> | ||
+ | <input type="hidden" id="start_point_pedestrian" name="start_point_pedestrian" | ||
+ | readonly/> | ||
+ | </li> | ||
+ | ... | ||
+ | |||
+ | <div id="walking_speed_pedestrian" | ||
+ | style="width: 100px; height: 6px; margin:15px 10px auto 15px; display: inline-block;"></div> | ||
+ | <span style="font-size: 10px;">Fast</span> | ||
+ | </div> | ||
+ | </li> | ||
+ | |||
+ | <li> | ||
+ | To: | ||
+ | <select id="demographic_pedestrian"> | ||
+ | {% for demo in demographic%} | ||
+ | <option value="{{demo.value}}">{{demo.value}}</option> | ||
+ | {% endfor %} | ||
+ | </select> | ||
+ | </li> | ||
+ | |||
+ | <li><input type="checkbox" id="distance_decay_function_pedestrian" | ||
+ | name="distance_decay_function"/>Distance | ||
+ | Decay Function | ||
+ | </li> | ||
+ | |||
+ | |||
+ | <input type="button" class="button" onclick="call_wps('pedestrian')" | ||
+ | value="Get Accessibility Score"/> | ||
+ | </ul> | ||
+ | </fieldset> | ||
+ | </form> | ||
+ | </div> | ||
+ | |||
+ | <h3>Transit and Walking Model</h3> | ||
+ | |||
+ | <div> | ||
+ | <form id="call_wps_transit" method="post" action="#"> | ||
+ | <fieldset> | ||
+ | <ul> | ||
+ | <li> | ||
+ | <input type="hidden" id="start_point_transit" name="start_point_transit" readonly/> | ||
+ | </li> | ||
+ | ... | ||
+ | |||
+ | <li> | ||
+ | To: | ||
+ | <select id="demographic_transit"> | ||
+ | {% for demo in demographic%} | ||
+ | <option value="{{demo.value}}">{{demo.value}}</option> | ||
+ | {% endfor %} | ||
+ | </select> | ||
+ | </li> | ||
+ | |||
+ | ... | ||
+ | |||
+ | <input type="button" class="button" onclick="call_wps('transit')" | ||
+ | value="Get Accessibility Score"/> | ||
+ | </ul> | ||
+ | </fieldset> | ||
+ | </form> | ||
+ | </div> | ||
+ | </source> | ||
+ | |||
+ | Para que estos valores sean reconocidos por el js que llama a la url /call_wps.php hay que modificar el archivo template/js/main.js para cada una de las opciones que se le agregó la opción demográfica | ||
+ | |||
+ | <source lang="javascript"> | ||
+ | function call_wps(approach){ | ||
+ | var start_point; | ||
+ | ... | ||
+ | var wps_call; | ||
+ | var demographic; | ||
+ | |||
+ | switch (approach) { | ||
+ | case "bike": | ||
+ | start_point = $('#start_point_bike').val(); | ||
+ | biking_time_period = $('#biking_time_period_amount').val(); | ||
+ | distance_decay_function = $('#distance_decay_function_bike').prop('checked'); | ||
+ | demographic = $('#demographic_bike option:selected').text(); | ||
+ | wps_call = "call_wps.php?wps=bike&start_point=" + start_point + "&biking_time_period=" + biking_time_period + "&distance_decay_function=" + distance_decay_function + "&demographic="+demographic; | ||
+ | break; | ||
+ | case "pedestrian": | ||
+ | start_point = $('#start_point_pedestrian').val(); | ||
+ | walking_time_period = $('#walking_time_period_amount_pedestrian').val(); | ||
+ | walking_speed_amount = $('#walking_speed_amount_pedestrian').val(); | ||
+ | demographic = $('#demographic_pedestrian option:selected').text(); | ||
+ | switch (walking_speed_amount) { | ||
+ | case '3': | ||
+ | walking_speed = .83; | ||
+ | break; | ||
+ | case '3.5': | ||
+ | walking_speed = .97; | ||
+ | break; | ||
+ | case '4': | ||
+ | walking_speed = 1.11; | ||
+ | break; | ||
+ | case '4.5': | ||
+ | walking_speed = 1.25; | ||
+ | break; | ||
+ | case '5': | ||
+ | walking_speed = 1.38; | ||
+ | break; | ||
+ | case '5.5': | ||
+ | walking_speed = 1.52; | ||
+ | break; | ||
+ | case '6': | ||
+ | walking_speed = 1.67; | ||
+ | break; | ||
+ | } | ||
+ | distance_decay_function = $('#distance_decay_function_pedestrian').prop('checked'); | ||
+ | wps_call = "call_wps.php?wps=pedestrian&start_point=" + start_point + "&walking_time_period=" + walking_time_period + "&walking_speed=" + walking_speed + "&distance_decay_function=" + distance_decay_function + "&demographic="+demographic; | ||
+ | break; | ||
+ | case "transit": | ||
+ | start_point = $('#start_point_transit').val(); | ||
+ | walking_time_period = $('#walking_time_period_amount_transit').val(); | ||
+ | walking_speed_amount = $('#walking_speed_amount_transit').val(); | ||
+ | demographic = $('#demographic_transit option:selected').text(); | ||
+ | switch (walking_speed_amount) { | ||
+ | case '3': | ||
+ | walking_speed = .83; | ||
+ | break; | ||
+ | case '3.5': | ||
+ | walking_speed = .97; | ||
+ | break; | ||
+ | case '4': | ||
+ | walking_speed = 1.11; | ||
+ | break; | ||
+ | case '4.5': | ||
+ | walking_speed = 1.25; | ||
+ | break; | ||
+ | case '5': | ||
+ | walking_speed = 1.38; | ||
+ | break; | ||
+ | case '5.5': | ||
+ | walking_speed = 1.52; | ||
+ | break; | ||
+ | case '6': | ||
+ | walking_speed = 1.67; | ||
+ | break; | ||
+ | } | ||
+ | distance_decay_function = $('#distance_decay_function_transit').prop('checked'); | ||
+ | walking_start_time = $('#walking_start_time').val() + ":00"; | ||
+ | wps_call = "call_wps.php?wps=transit&start_point=" + start_point + "&walking_start_time=" + walking_start_time + "&walking_time_period=" + walking_time_period + "&walking_speed=" + walking_speed + "&bus_waiting_time=0&bus_riding_time=0&distance_decay_function=" + distance_decay_function + "&demographic="+demographic; | ||
+ | break; | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | |||
+ | Con ésto, ahora podemos recibir el valor escogido para la opción demográfica vía GET, por lo que ahora nos vamos a editar el archivo '''wyp/views.py''' para manejarlo. | ||
+ | |||
+ | <source lang="python"> | ||
+ | def xml(request): | ||
+ | approach = request.GET.get('wps',default=None) | ||
+ | if approach == 'bike': | ||
+ | ... | ||
+ | demographic = request.GET.get('demographic',default=None) | ||
+ | return HttpResponse(managementBike(start_point,biking_time_period,distance_decay_function,demographic)) | ||
+ | elif approach == 'pedestrian': | ||
+ | ... | ||
+ | demographic = request.GET.get('demographic',default=None) | ||
+ | return HttpResponse(managementPedestrian(start_point,walking_time_period,walking_speed,distance_decay_function,demographic)) | ||
+ | elif approach == 'transit': | ||
+ | ... | ||
+ | demographic = request.GET.get('demographic', default=None) | ||
+ | return HttpResponse(managementTransit(start_point,walking_start_time,walking_time_period,walking_speed,bus_waiting_time,bus_riding_time, distance_decay_function,demographic)) | ||
+ | else: | ||
+ | return HttpResponse('') | ||
+ | </source> | ||
+ | |||
+ | Como vemos, ahora enviamos también la opción demográfica como nuevo parámetro de la función management* por lo que debemos modificar cada uno de ellos, de todas formas para todo es lo mismo por lo que solo se explicará el cambio realizado en Bike en el archivo '''wyp/ManagementServiceBike.py''' | ||
+ | |||
+ | <source lang="python"> | ||
+ | def management(start_point, biking_time_period, distance_decay_function,demographic): | ||
+ | ... | ||
+ | aggregation_data = aggregation(start_point,bikeshed,crime_data,poi_data,distance_decay_function,biking_time_period,demographic) | ||
+ | ... | ||
+ | </source> | ||
+ | |||
+ | y finalmente debemos editar el archivo en cuestión que nos retorna la puntuación, '''wyp/AggregationService.py'''. | ||
+ | |||
+ | <source lang="python"> | ||
+ | from models import Demographic | ||
+ | |||
+ | def aggregation(start_point, walkshed, crime_data, poi_data, distance_decay_function, walking_time_period,demographic): | ||
+ | if poi_data != '"NULL"': | ||
+ | |||
+ | demographic_value = Demographic.objects.get(value=demographic) | ||
+ | |||
+ | poi_weights = {"Bank": ast.literal_eval(demographic_value.bank), #bank | ||
+ | "Grocery": ast.literal_eval(demographic_value.grocery), #grocery | ||
+ | "Restaurant": ast.literal_eval(demographic_value.restaurant), #restaurant and coffee | ||
+ | "Shopping": ast.literal_eval(demographic_value.shopping), #shopping | ||
+ | "Entertainment": ast.literal_eval(demographic_value.entertainment), #entertainment | ||
+ | "School": ast.literal_eval(demographic_value.school), #school | ||
+ | "Library": ast.literal_eval(demographic_value.library), #library | ||
+ | "Health": ast.literal_eval(demographic_value.health) #health | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | |||
+ | Entonces, para probar que todo esto funciona debemos ejecutar estos comandos desde la raíz del proyecto | ||
+ | |||
+ | <source lang="bash"> | ||
+ | python manage.py collectstatics #por la midificacion de template/js/main.js | ||
+ | python manage.py makemigrations # por la creacion del modelo Demographic | ||
+ | python manage.py migrate | ||
+ | python manage.py runserver | ||
+ | </source> | ||
+ | |||
+ | == Alerta cuando uno de los servidores externos está caido == | ||
+ | |||
+ | Ha pasado que las VMs de OTP y de Overpass se caen por alguna razón o simplemente no parten correctamente lo que afecta directamente el funcionamiento de esta aplicación, para ello es importante saber cuál es la que está causando el problema, con ello necesitamos primero encontrar el error por lo que editaremos lo siguiente (nuevamente se mostrará con Bike, se realizó lo mismo con Pedestrian y Transit) | ||
+ | |||
+ | En el archivo '''wyp/ManagementServiceBike.py''' agregamos: | ||
+ | |||
+ | <source lang="python"> | ||
+ | try: | ||
+ | bikeshed = urllib2.urlopen(otp_url).read() | ||
+ | except: | ||
+ | return '{"error":"OTP does not respond"}' | ||
+ | ... | ||
+ | try: | ||
+ | poi_data = getPOIs(bikeshed) | ||
+ | except: | ||
+ | return '{"error":"Overpass does not respond"}' | ||
+ | </source> | ||
+ | |||
+ | Y para generar la alerta lo haremos gracias a la ayuda del archivo '''template/js/main.js''' | ||
+ | |||
+ | <source lang="javascript"> | ||
+ | }, | ||
+ | success: function (data, status) { | ||
+ | var wps_results = $.parseJSON(data); | ||
+ | |||
+ | var error_result = wps_results['error']; | ||
+ | if (error_result){ | ||
+ | $.unblockUI(); | ||
+ | alert(error_result); | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | == Cambio de servidores de OTP y de overpass por panel de administración == | ||
+ | |||
+ | Una de las cosas principales que tiene Django es que cuenta con un panel de administración para poder editar la información de la base de datos o controlar ciertas configuraciones. En este caso crearemos un panel que permita intercambiar el servidor de OTP o de Overpass a cual queramos, para ello la guardaremos en la base de datos editando el archivo '''wyp/models.py''' | ||
+ | |||
+ | <source lang="python"> | ||
+ | class Server(models.Model): | ||
+ | overpass = models.CharField(max_length=255,default='http://146.155.17.18:24080/api/interpreter') | ||
+ | otp_bike = models.CharField(max_length=255,default='http://146.155.17.18:23080/otp/routers/default/isochrone?&') | ||
+ | otp_walk = models.CharField(max_length=255,default='http://146.155.17.18:23080/otp/routers/default/isochroneOld?') | ||
+ | </source> | ||
+ | |||
+ | Lo agregamos al panel de administración agregando estas líneas en '''wyp/admin.py''' | ||
+ | |||
+ | <source lang="python"> | ||
+ | from wyp.models import Demographic, Server | ||
+ | admin.site.register(Server) | ||
+ | </source> | ||
+ | |||
+ | Ejecutamos las siguientes líneas de comando para agregar la información a la base de datos | ||
+ | |||
+ | <source lang="bash"> | ||
+ | python manage.py makemigrations | ||
+ | python manage.py migrate | ||
+ | python manage.py createsuperuser #crear un superusuario | ||
+ | python manage.py runserver | ||
+ | </source> | ||
+ | |||
+ | Entramos ahora a la dirección http://localhost:8000/admin/ e ingresamos con el superusuario, entramos a Servers y creamos un elemento, los campos vendrán listos por lo que solo hay que guardar tal cual como sale. | ||
+ | |||
+ | Luego editamos los archivos en donde se encuentra esta información | ||
+ | |||
+ | '''wyp/ManagementServiceBike.py''' | ||
+ | <source lang="python"> | ||
+ | otp_isochrone_url = Server.objects.all()[0].otp_bike | ||
+ | </source> | ||
+ | |||
+ | '''wyp/ManagementServicePedestrian.py''' | ||
+ | <source lang="python"> | ||
+ | otp_isochrone_url = Server.objects.all()[0].otp_walk | ||
+ | </source> | ||
+ | |||
+ | '''wyp/ManagementServiceTransit.py''' | ||
+ | <source lang="python"> | ||
+ | otp_isochrone_url = Server.objects.all()[0].otp_walk | ||
+ | </source> | ||
+ | |||
+ | '''wyp/POIService.py''' | ||
+ | <source lang="python"> | ||
+ | _overpass_url = Server.objects.all()[0].overpass | ||
+ | </source> | ||
+ | |||
+ | == Análisis de muchos puntos mediante la subida de un archivo CSV == | ||
+ | |||
+ | Este trabajo se realiza netamente desde el panel de administración por lo que no se necesita de intervenir la interfaz. Se necesita de un sistema que reciba el archiv csv, lo lea línea por línea y lo analice según corresponda, la solución fue implementar un proceso extra (fuera de la interfaz) que realice el trabajo con tal de dejarlo funcionar toda la noche, por ejemplo. | ||
+ | |||
+ | Como lo queremos ver en el panel de administración necesitamos crearle un modelo, para ello editamos nuevamente el archivo '''wyp/models.py''' agregando | ||
+ | |||
+ | <source lang="python"> | ||
+ | import ast, subprocess, shlex | ||
+ | |||
+ | TYPES_OF_ANALYSYS = ( | ||
+ | ('Bike','Cycling Model'), | ||
+ | ('Walk','Walking Model'), | ||
+ | ('Transit','Transit & Walking Model') | ||
+ | ) | ||
+ | |||
+ | class Multiple_point_analysis(models.Model): | ||
+ | csv_upload = models.FileField(upload_to='csv_uploads/') | ||
+ | type_of_analysis = models.CharField(max_length=10,choices=TYPES_OF_ANALYSYS) | ||
+ | demographic_group = models.ForeignKey(Demographic) | ||
+ | analysed_pointset = models.BooleanField(default=False,blank=True,editable=False) | ||
+ | results = models.FileField(upload_to='results/', blank=True) | ||
+ | |||
+ | def __unicode__(self): | ||
+ | return self.csv_upload.url.split('csv_uploads/')[-1] | ||
+ | |||
+ | def save(self, *args, **kwargs): | ||
+ | super(Multiple_point_analysis,self).save(*args, **kwargs) | ||
+ | if not self.analysed_pointset: | ||
+ | subprocess.Popen(shlex.split('python manage.py mass_analysis -i '+str(self.id))) | ||
+ | </source> | ||
+ | |||
+ | y lo mostramos en el panel editando el archivo '''wyp/admin.py''' | ||
+ | |||
+ | <source lang="python"> | ||
+ | from wyp.models import Demographic, Server, Multiple_point_analysis | ||
+ | admin.site.register(Multiple_point_analysis) | ||
+ | </source> | ||
+ | |||
+ | Además debemos agregar toda la siguiente rama de archivos: | ||
+ | * wyp/ | ||
+ | ** management/ | ||
+ | *** __init__.py | ||
+ | *** commands/ | ||
+ | **** __init__.py | ||
+ | **** _private.py | ||
+ | **** mass_analysis.py | ||
+ | |||
+ | En donde ambos __init__.py y _private.py son archivos vacíos, management y commands carpetas y mass_analysis.py nuestra función que analizará el archivo csv. Entonces construimos el archivo '''wyp/management/commands/mass_analysis.py''' agregando las siguientes líneas: | ||
+ | |||
+ | <source lang="python"> | ||
+ | from optparse import make_option | ||
+ | from django.core.management.base import BaseCommand, CommandError | ||
+ | from wypdjango import settings | ||
+ | from wyp.models import Multiple_point_analysis | ||
+ | from wyp.ManagementServiceBike import management as managementBike | ||
+ | from wyp.ManagementServicePedestrian import management as managementPedestrian | ||
+ | from wyp.ManagementServiceTransit import management as managementTransit | ||
+ | from django.core.files import File | ||
+ | import json | ||
+ | from time import time | ||
+ | |||
+ | class Command(BaseCommand): | ||
+ | help = "Performs mass analysis from a csv file.\nIt requires id model to analyze." | ||
+ | option_list = BaseCommand.option_list + ( | ||
+ | make_option( | ||
+ | '-i',action='store', | ||
+ | dest='i', | ||
+ | default='', | ||
+ | help='id of the csv file in the model'), | ||
+ | ) | ||
+ | |||
+ | def handle(self, *app_labels, **options): | ||
+ | csv_model = options['i'] | ||
+ | csv_model = Multiple_point_analysis.objects.get(id=int(csv_model)) | ||
+ | |||
+ | filename = csv_model.csv_upload.url.split('/media/')[-1] | ||
+ | |||
+ | url = settings.MEDIA_ROOT + filename | ||
+ | |||
+ | csv = open(url,'r') | ||
+ | lines = csv.read().split('\n') | ||
+ | csv.close() | ||
+ | |||
+ | temp = open('/tmp/analysis_from_wyp.csv','w') | ||
+ | temp.write('id;x;y;score;area;time;POI count;\n') | ||
+ | |||
+ | id_result = 1 | ||
+ | |||
+ | try: | ||
+ | |||
+ | if csv_model.type_of_analysis == 'Bike': | ||
+ | for line in lines: | ||
+ | aux = line.split(';') | ||
+ | if aux[0] != 'id' and len(aux)>2: | ||
+ | # aux = id;x;y;biking_time_period;distance_decay_function; | ||
+ | start_point = aux[1]+','+aux[2] | ||
+ | biking_time_period = aux[3] | ||
+ | distance_decay_function = aux[4] | ||
+ | start_time = time() | ||
+ | results = managementBike(start_point,biking_time_period,distance_decay_function,csv_model.demographic_group.value) | ||
+ | elapsed_time = time() - start_time | ||
+ | results = json.loads(results) | ||
+ | text = str(id_result)+';'+aux[1]+';'+aux[2]+';'+results['walkshed']['properties']['score']+';'+str(results['walkshed']['properties']['area'])+';'+str(elapsed_time)+';'+str(len(results['poi']['features']))+';\n' | ||
+ | temp.write(text) | ||
+ | id_result += 1 | ||
+ | |||
+ | if csv_model.type_of_analysis == 'Walk': | ||
+ | for line in lines: | ||
+ | aux = line.split(';') | ||
+ | if aux[0] != 'id' and len(aux)>2: | ||
+ | #aux = id;x;y;walking_time_period;walking_speed;distance_decay_function; | ||
+ | start_point = aux[1]+';'+aux[2] | ||
+ | walking_time_period = aux[3] | ||
+ | walking_speed = aux[4] | ||
+ | distance_decay_function = aux[5] | ||
+ | start_time = time() | ||
+ | results = managementPedestrian(start_point, walking_time_period, walking_speed, distance_decay_function, csv_model.demographic_group.value) | ||
+ | elapsed_time = time() - start_time | ||
+ | results = json.loads(results) | ||
+ | text = str(id_result) + ';' + aux[1] + ';' + aux[2] + ';'+results['walkshed']['properties']['score']+';'+str(results['walkshed']['properties']['area'])+';'+str(elapsed_time)+';'+str(len(results['poi']['features']))+';\n' | ||
+ | temp.write(text) | ||
+ | id_result += 1 | ||
+ | if csv_model.type_of_analysis == 'Transit': | ||
+ | for line in lines: | ||
+ | aux = line.split(';') | ||
+ | if aux[0] != 'id' and len(aux)>2: | ||
+ | # aux = id;x;y;start_time;walking_time_period;walking_speed;bus_waiting_time;bus_ride_time;distance_decay_function | ||
+ | start_point = aux[1] + ';' + aux[2] | ||
+ | start_time_transit = aux[3] | ||
+ | walking_time_period = aux[4] | ||
+ | walking_speed = aux[5] | ||
+ | #bus_waiting_time = aux[6] | ||
+ | bus_waiting_time = '0' | ||
+ | #bus_ride_time = aux[7] | ||
+ | bus_ride_time = '0' | ||
+ | distance_decay_function = aux[6] | ||
+ | start_time = time() | ||
+ | results = managementTransit(start_point,start_time_transit,walking_time_period,walking_speed,bus_waiting_time,bus_ride_time,distance_decay_function,csv_model.demographic_group.value) | ||
+ | elapsed_time = time() - start_time | ||
+ | results = json.loads(results) | ||
+ | text = str(id_result) + ';' + aux[1] + ';' + aux[2] + ';'+results['walkshed']['properties']['score']+';'+str(results['walkshed']['properties']['area'])+';'+str(elapsed_time)+';'+str(len(results['poi']['features']))+';\n' | ||
+ | temp.write(text) | ||
+ | id_result += 1 | ||
+ | |||
+ | except Exception as e: | ||
+ | pass | ||
+ | |||
+ | #Pendiente!!!!! | ||
+ | if csv_model.type_of_analysis == 'Transit': | ||
+ | management(start_point, start_time, walking_time_period, walking_speed, bus_waiting_time, bus_ride_time, distance_decay_function) | ||
+ | pass | ||
+ | |||
+ | temp.close() | ||
+ | |||
+ | csv_model.analysed_pointset = True | ||
+ | csv_model.results.save('results/' + csv_model.csv_upload.name.split('csv_uploads/')[-1] + '.csv', File(open('/tmp/analysis_from_wyp.csv'))) | ||
+ | csv_model.save() | ||
+ | </source> | ||
+ | |||
+ | Finalmente ejecuntamos estas líneas de comando para agregar el modelo a la base de datos: | ||
+ | |||
+ | <source lang="bash"> | ||
+ | python manage.py makemigrations | ||
+ | python manage.py migrate | ||
+ | python manage.py runserver | ||
+ | </source> | ||
+ | |||
+ | Entramos a http://localhost:8000/admin y nos encontraremos con la opción de Multiple_point_analysis en donde tenemos que hacer click en "add multiple_point_analysis". Llenamos los campos que se encuentran en negrita y apretamos en "SAVE". Esperamos a que finalice (~15 segundos por línea de CSV) y los resultados estarán en el campo "Results" como archivo csv para descargar. | ||
+ | |||
+ | === UPDATE Paralelización === | ||
+ | |||
+ | Al analizar la ciudad de Santiago con un archivo CSV, éste tardaba cerca de 23 horas en completar más de 52000 puntos, por lo que se decidió usar paralelización para utilizar todo el rendimiento de la VM. Para ello se utilizaron los librerías de Python '''threading''' y '''Queue'''. El código cambió en las siguientes líneas: | ||
+ | |||
+ | <source lang="Python"> | ||
+ | import threadint | ||
+ | import Queue | ||
+ | |||
+ | if csv_model.type_of_analysis == 'Bike': | ||
+ | args_list = list() | ||
+ | for line in lines: | ||
+ | aux = line.split(';') | ||
+ | if aux[0] != 'id' and len(aux)>2: | ||
+ | start_point = aux[1]+','+aux[2] | ||
+ | biking_time_period = aux[3] | ||
+ | distance_decay_function = aux[4] | ||
+ | start_time = time() | ||
+ | args_list.append([start_point,biking_time_period,distance_decay_function,csv_model.demographic_group.value,csv_model.escenario.id,start_time]) | ||
+ | if len(args_list)==40 or lines.index(line) == len(lines)-1: | ||
+ | process_list = list() | ||
+ | queue_list = list() | ||
+ | for arg in args_list: | ||
+ | q = Queue.Queue() | ||
+ | process = threading.Thread(target=text_from_bike, args=arg+[q]) | ||
+ | process.start() | ||
+ | process_list.append(process) | ||
+ | queue_list.append(q) | ||
+ | args_list = list() | ||
+ | for process in process_list: | ||
+ | process.join() | ||
+ | for queue in queue_list: | ||
+ | text = queue.get() | ||
+ | temp.write(text) | ||
+ | logging.info(text.strip('\n')) | ||
+ | id_result += 1 | ||
+ | </source> | ||
+ | |||
+ | El cambio en los otros modos es similar. El gran cambio de esta metodología es que crea grupos de 40 pequeños programas que solo se encargan de realizar la evaluación, éste recupera todos estos resultados y después ejecuta otros 40, con ésto el tiempo se reduce considerablemente, ya que habían puntos que tardaban solo 1 segundo en tener respuesta como otros más de 10. Por cada 40 puntos a analizar el nuevo tiempo corresponde a la duración del que tarde más. En otras palabras, si el grupo de 40 puntos son de score 0 y tardan menos de 1 segundo en ser analizados, el tiempo será menos de 1 segundo para todo el grupo en comparación a los respectivos 40 que sería analizar uno a uno. | ||
+ | |||
+ | == Sistema de Log para análisis de archivos CSV == | ||
+ | |||
+ | Como no se sabe si de verdad está funcionando o hay problemas de por medio al momento de analizar un archivo CSV, se creó un sistema de log que nos permita saber que es lo que está sucediendo. Para ello lo creamos como proceso al igual que el análisis del archivo. por lo que creamos el archivo '''wyp/management/commands/log.py''' y le agregamos: | ||
+ | |||
+ | <source lang="python"> | ||
+ | from optparse import make_option | ||
+ | from django.core.management.base import BaseCommand, CommandError | ||
+ | from wyp.models import Log_wyp | ||
+ | |||
+ | |||
+ | class Command(BaseCommand): | ||
+ | |||
+ | help = "Save logs to the system" | ||
+ | |||
+ | option_list = BaseCommand.option_list + ( | ||
+ | make_option( | ||
+ | '-l',action='store', | ||
+ | dest='l', | ||
+ | default='', | ||
+ | help='message log'), | ||
+ | ) | ||
+ | |||
+ | def handle(self, *app_labels, **options): | ||
+ | message = options['l'] | ||
+ | |||
+ | if message != '': | ||
+ | if len(message)>255: | ||
+ | Log_wyp(log=message[:255]).save() | ||
+ | else: | ||
+ | Log_wyp(log=message).save() | ||
+ | </source> | ||
+ | |||
+ | y agregamos el modelo en el archivo '''wyp/models.py''' | ||
+ | |||
+ | <source lang="python"> | ||
+ | class Log_wyp(models.Model): | ||
+ | log = models.CharField(max_length=255,default='') | ||
+ | created = models.DateTimeField(auto_now_add=True) | ||
+ | |||
+ | def __unicode__(self): | ||
+ | return str(self.created.strftime('%Y-%m-%d %H:%M:%S')) + ' ' +self.log | ||
+ | </source> | ||
+ | |||
+ | Lo mostramos en el panel de administración editando el archivo '''wyp/admin.py''' | ||
+ | |||
+ | <source lang="python"> | ||
+ | from wyp.models import Demographic, Server, Multiple_point_analysis, Log_wyp | ||
+ | admin.site.register(Log_wyp) | ||
+ | </source> | ||
+ | |||
+ | Ejecutamos los comandos para agregar el modelo a la base de datos | ||
+ | |||
+ | <source lang="bash"> | ||
+ | python manage.py makemigrations | ||
+ | python manage.py migrate | ||
+ | </source> | ||
+ | |||
+ | ahora, para poder agregar cualquier log lo podemos hacer ejecutando el comando: | ||
+ | <source lang="bash"> | ||
+ | python manage.py log -l "Mensaje a registrar" | ||
+ | </source> | ||
+ | |||
+ | Por lo que, por ejemplo, al integrarlo al archivo '''wyp/management/commands/mass_analysis.py''' se realizó lo siguiente: | ||
+ | |||
+ | <source lang="python"> | ||
+ | import ast, subprocess, shlex | ||
+ | |||
+ | subprocess.Popen(shlex.split('python manage.py log -l "' + filename + ' Starting analysis"')) | ||
+ | </source> | ||
+ | |||
+ | == Cambio de frecuencia de buses editando el archivo GTFS de Santiago == | ||
+ | |||
+ | Se trabaja actualmente en la construcción de una herramienta que permia cambiar las frecuencias de las líneas de buses y metro de la ciudad de Santiago con el objetivo de poder analizar los cambios de accesibilidad urbana derivadas de ésto. Con ello, en Django Python, se cargó en una base de datos SQLite (aparte de la que ya utiliza Django) el GTFS de los datos de la ciudad en cuestión gracias al módulo pygtfs. Para utilizar sus datos solo basta con llamarlos con sus funciones. | ||
+ | |||
+ | Con ello, las frecuencias de los recorridos se encuentran en el archivo frequencies.txt dentro del archivo comprimido GTFS.zip. Con el módulo pygtfs previamente configurado se pueden llamar dichos datos con los comandos | ||
+ | |||
+ | <source lang="python"> | ||
+ | sched = pygtfs.Schedule(MEDIA_ROOT + 'gtfs.sqlite') | ||
+ | frequencies = sched.frequencies | ||
+ | </source> | ||
+ | |||
+ | generando en '''frequencies''' una lista con todas las frecuencias y los datos adjuntos que podemos ver en el mismo archivo de texto pero como objeto python. | ||
+ | |||
+ | Luego se creó una vista en [http://146.155.17.19:17080/gtfs/update_frequencies] en donde encontraremos un espacio de selección de viaje identificado por su ID en la cual al ser seleccionada se desplegará su información, tal como su hora de inicio, hora de fin y su frecuencia de salida, este último en un campo editable. Cuando ya cambiamos este valor, podemos guardar este dato en una lista de futuros datos para ser actualizados, la cual la podemos ver en la zona inferior de este formulario. Ésta lista puede ser tan grande como SQLite aguante. | ||
+ | |||
+ | Para construir esta lista, se debió de crear una copia de los modelos que hay en pygtfs en la base de datos local de Django manteniendo solo los datos que se desean cambiar, al ser eliminados de la lista también son eliminados de la base de datos para evitar que éste se llene o cause algún tipo de inconveniente. | ||
+ | |||
+ | Ya cuando tengamos todos nuestros cambios guardados, apretamos en el botón de '''Actualizar GTFS'''. Éste proceso puede llegar a tardar entre 7 a 8 minutos en ser completado, por lo que el botón se bloqueará durante este tiempo. Una vez finalizado debemos dirigirnos al panel de administración para cambiar el router de evaluación y utilizar el que acabamos de construir. Para ello se entra a [http://146.155.17.19:17080/admin/wyp/server/1/change/] y se modifica la URL en cuestion para cambiar '''/routers/<texto-a-cambiar>/isochrone?''' por '''/routers/WYP/isochrone?'''. Una vez realizado este cambio podemos efectuar las consultas con nuestras nuevas frecuencias de buses. | ||
+ | |||
+ | Cabe destacar que este proceso es temporal mientras no se construya un front-end ideal, ésta herramienta se construyó de esta forma para probar su funcionalidad y no garantiza lo más óptimo. | ||
+ | |||
+ | == API REST para obtener datos del archivo de frecuencia de buses == | ||
+ | |||
+ | Para que el putno anterior de esta documentación funcione correctamente, fue necesario que se creara una API REST que nos permita obtener datos de las frecuencias mientras que por GET le enviemos la ID correspondiente al tramo a evaluar. En otras palabras, debemos ingresar una URL como ésta [http://146.155.17.19:17080/gtfs/frec_REST?f=101-I-S_V25-B14] y obtendremos un objeto Json con la información en cuestión. | ||
+ | |||
+ | == Sistema de análisis multiple de forma visual == | ||
+ | |||
+ | Se agregó la opción '''Mapping bbox''', el cual permite analizar el recuadro que vemos en el mapa con múltiples puntos. Al abrir esta opción nos encontramos con la selección del método de viaje que deseamos evaluar, la opción demográfica y la cantidad de puntos que deseamos procesar para obtener un mapeo de los resultados. Para este proceso se utilizó la parametrización, es decir, si se deciden evaluar con 3 puntos horizontales y 3 verticales, los 9 sectores a evaluar se realizan todos a la vez, por lo que mientras más puntos a evaluar, más carga para la VM. | ||
+ | |||
+ | Los resultados son mostrados en el mapa de forma de colores, el blanco representa score 0 y verde un score de 100. | ||
+ | |||
+ | = Funcionamiento actual = | ||
+ | |||
+ | El código se puede encontrar acá: http://146.155.17.14:18080/csfuente/wyp-django/ y se encuentra funcionando en http://146.155.17.19:17080/ |
Latest revision as of 12:42, 4 August 2017
Update 08-09-2016
Contents
- 1 State of the art
- 2 Migración a Django Python
- 3 Características nuevas
- 3.1 Demographic group
- 3.2 Alerta cuando uno de los servidores externos está caido
- 3.3 Cambio de servidores de OTP y de overpass por panel de administración
- 3.4 Análisis de muchos puntos mediante la subida de un archivo CSV
- 3.5 Sistema de Log para análisis de archivos CSV
- 3.6 Cambio de frecuencia de buses editando el archivo GTFS de Santiago
- 3.7 API REST para obtener datos del archivo de frecuencia de buses
- 3.8 Sistema de análisis multiple de forma visual
- 4 Funcionamiento actual
State of the art
EL código que se comenzó a editar se extrajo de los siguientes link:
- (1) https://github.com/mepa1363/wyp-client
- (2) https://github.com/mepa1363/wyp-server-ogc
- (3) https://github.com/mepa1363/wyp-wrapper-geoserver-centralized-transit
- (4) https://github.com/mepa1363/wyp-wrapper-geoserver-centralized-bike
- (5) https://github.com/mepa1363/wyp-wrapper-geoserver-centralized-pedestrian
- (6) https://github.com/mepa1363/wyp-wrapper-geoserver-centralized-walkscore
- (7) https://github.com/mepa1363/wyp-wrapper-geoserver-centralized-walkourplace
De los cuales (1) es la interfaz gráfica, (2) es el servidor el cual crea los puertos para la comunicación y los demás son los módulos de geoserver para manejar las peticiones.
Migración a Django Python
Lo primero es crear un nuevo proyecto en Django
django-admin startproject wypdjango
cd wypdjango
En donde nos encontramos con:
- manage.py
- wypdjango/
- __init__.py
- settings.py
- urls.py
- wsgi.py
Manage.py es quien nos ayudará a correr la web, tanto en etapa de desarrollo como en producción, éste tomará en cuenta todas las carpetas que contengan el archivo __init__.py (el cual no cuenta con contenido), por lo mismo éste archivo no debe ser editado. Settings.py es en donde se encuentra toda la configuración que le asignemos, entre ella la conexión a la base de datos y la posición de los templates html. Urls.py será en donde listemos las URLs que queramos utilizar, además cumple la función de enlazar dicha url con una función desarrollada en lenguaje Python. Entonces, sabiendo esto hay que crear dos nuevas carpetas llamadas static y template, la primera para contener todos los archivos css y js que necesitemos y la segunda para retener todos los html, luego editamos el archivo wypdjango/settings.py con lo siguiente:
import os BASE_DIR = os.path.dirname(os.path.dirname(__file__)) PROJECT_PATH = os.path.realpath(os.path.dirname(__file__)) ... INSTALLED_APPS = ( ... 'django.contrib.staticfiles', 'wyp', ) ... STATIC_URL = '/static/' TEMPLATE_DIRS = ( PROJECT_PATH + '/../template/', ) STATIC_ROOT = PROJECT_PATH + '/../static/' ADMIN_MEDIA_PREFIX = '/static/admin/' STATICFILES_DIRS = ( os.path.join(BASE_DIR,'template'), )
Luego se clona el repositorio git (1) dentro de la carpeta template
cd template git clone https://github.com/mepa1363/wyp-client .
Volvemos a la raíz y creamos una app dentro del proyecto para manejar la aparición del templace
cd ..
django-admin startapp wyp
Con esto se crea una carpeta llamada wyp con:
- __init__.py
- admin.py
- models.py
- test.py
- views.py
En donde en models.py creamos clases que corresponderán a los modelos en la base de datos, en views.py creamos las funciones que guiarán en la aparición de las vistas (Que posteriormente llamaremos con urls.py), admin.py que es donde se registran los modelos de la bse de datos que se quieran mostrar en el panel de administración. Test.py para realizar pruebas (nunca lo he ocupado) e __init__.py para que sea reconocido por manage.py.
En wyp/views.py editamos lo siguiente:
from django.shortcuts import render_to_response from django.template.context import RequestContext def inicio(request): return render_to_response('index.html',context_instance=RequestContext(request))
y en wypdjango/urls.py editamos:
from wyp.views import inicio urlpatterns = patterns('', ... url(r'^admin/', include(admin.site.urls)), url(r'^$', inicio), ) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += staticfiles_urlpatterns()
Nos posicionamos en la raíz y ejecutamos los siguientes comandos:
python manage.py collectstatics #Rescata los archivos css y js del panel de administración y de los que tenga el template html python manage.py makemigrations #Crea los archivos .py para editar la base de datos python manage.py migrate #Edita la base de datos con los cambios juntados en makemigrations python manage.py runserver #Corre un servidor local con la web
Con el último comando corriendo, nos dirigimos a un navegador web y entramos a http://localhost:8000/ y nos encontraremos con el index.html de walkyourplace.
Ya tenemos al menos la vista principal del proyecto, ahora debemos hacerlo funcionar.
Vemos que en la web inicial los request se solicitan a la url /call_wps.php en conjunto de los datos enviados vía GET, como ahora estamos trabajando en Django dicha url no está apuntando a nada ya que no se encuentra registrada en urls.py, por lo que el código php que junta esta información y realiza la petición de la información se debe construir esta vez en Python, para ello necesitamos los archivos que procesan la información (2) y pasarlos al proyecto.
Bike Model
Tomando de (2) los archivos de la carpeta Bike (AggregationService.py, CrimeService.py, ManagementService.py, POIService.py y config.py) y los copiamos dentro de la carpeta wyp. Tenemos que editarlos cada uno de ellos.
En AggregationService.py eliminamos las siguientes líneas:
from bottle import route, run, request import bottle bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 ... @route('/aggregation', method='POST') def service(): poi = request.POST.get('poi', default=None) crime = request.POST.get('crime', default=None) walkshed = request.POST.get('bikeshed', default=None) start_point = request.POST.get('start_point', default=None) distance_decay_function = request.POST.get('distance_decay_function', default=None).lower() walking_time_period = request.POST.get('biking_time_period', default=None) if start_point and poi and crime and walkshed and distance_decay_function and walking_time_period is not None: return aggregation(start_point, walkshed, crime, poi, distance_decay_function, walking_time_period) run(host='0.0.0.0', port=7364, debug=True)
y modificamos la línea
otp_url = "http://www.gisciencegroup.ucalgary.ca:8080/opentripplanner-api-webapp/ws/plan?arriveBy=false&time=6%3A58pm&ui_date=1%2F10%2F2013&mode=BICYCLE&optimize=QUICK&maxWalkDistance=5000&walkSpeed=1.38&date=2013-01-10&"
por
otp_url = "http://146.155.17.18:23080/otp/routers/default/plan?arriveBy=false&time=6%3A58pm&ui_date=1%2F10%2F2013&mode=BICYCLE&optimize=QUICK&maxWalkDistance=5000&walkSpeed=1.38&date=2013-01-10&"
Para crime.py hay que hacer cambios drásticos ya que en Chile no contamos con la información geográfica de los crímenes de la ciudad, por lo que necesitamos que simplemente nos retorne 'NULL', aparte de eso debemos quitar todo lo relacionado con el paquete bottle. Por lo que eliminamos las líneas:
from bottle import route, run, request import bottle bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 @route('/crime') def service(): polygon = request.GET.get('bikeshed', default=None) if polygon is not None: return pointInPolygon(polygon) run(host='0.0.0.0', port=7366, debug=True)
y agregamos después de la definición de la función:
def pointInPolygon(polygon): return '"NULL"'
En POIService.py también debemos eliminar lo relacionado con bottle, similar a los archivos anteriores, y debemos editar la línea:
_overpass_url = 'http://overpass-api.de/api/interpreter'
por
_overpass_url = 'http://146.155.17.18:24080/api/interpreter'
y en ManagementService.py primero le cambiaremos el nombre a ManagementServiceBike.py (mas adelante se verá porqué), luego eliminamos las siguientes líneas:
from bottle import route, run, request import bottle bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 ... @route('/management') def service(): start_point = request.GET.get('start_point', default=None) biking_time_period = request.GET.get('biking_time_period', default=None) distance_decay_function = request.GET.get('distance_decay_function', default=None) if start_point and biking_time_period is not None: return management(start_point, biking_time_period, distance_decay_function) run(host='0.0.0.0', port=7363, debug=True)
y rehacemos por completo la función management(), ya que está configurada para enviar las solicitudes a geoserver, esta vez la configuraremos para que funcione dentro de Django.
def management(start_point, biking_time_period, distance_decay_function): otp_isochrone_url = 'http://146.155.17.18:23080/otp/routers/default/plan?&' otp_dataInputs = 'fromPlace=%s&date=2016/05/05&time=12:00:00&mode=BICYCLE&cutoffSec=%s' % (start_point,str(int(biking_time_period)*60)) otp_url = otp_isochrone_url + otp_dataInputs try: bikeshed = urllib2.urlopen(otp_url).read() except: return '{"error":"OTP does not respond"}' bikeshed = removeNonAscii(bikeshed) bikeshed = bikeshed.replace('&','and') bikeshed = bikeshed.replace(';',' ') bikeshed = json.loads(bikeshed) bikeshed = '{"type":"Polygon","coordinates":'+str(bikeshed['features'][0]['geometry']['coordinates'][0])+'}' try: poi_data = getPOIs(bikeshed) except: return '{"error":"Overpass does not respond"}' crime_data = pointInPolygon(bikeshed) aggregation_data = aggregation(start_point,bikeshed,crime_data,poi_data,distance_decay_function,biking_time_period) result = '{"walkshed": %s, "poi": %s}' % (aggregation_data, poi_data) return result
Finalmente necesitamos enlazar todo este código, por lo que creamos una nueva función en el archivo wyp/views.py con:
from wyp.ManagementServiceBike import management as managementBike def xml(request): approach = request.GET.get('wps',default=None) if approach == 'bike': start_point = request.GET.get('start_point', default=None) biking_time_period = request.GET.get('biking_time_period', default=None) distance_decay_function = request.GET.get('distance_decay_function', default=None) return HttpResponse(managementBike(start_point,biking_time_period,distance_decay_function))
y lo agregamos en wypdjango/urls.py
from wyp.views import inicio, xml ... urlpatterns = patterns('', ... url(r'^call_wps.php',xml), )
y finalmente ejecutamos el comando para correr el servidor y probamos que todo funciona correctamente.
python manage.py runserver
Pedestrian & Transit Model
Ahora podemos agregar la característica de caminata en el proyecto, para ello copiamos solo el archivo ManagementService.py que se encuentra en Pedestrian en (2) y le cambiamos el nombre a ManagementServicePedestrian.py y hacemos algo muy similar que el archivo de Bike, primero eliminamos todo lo relacionado con bottle:
from bottle import route, run, request import bottle bottle.BaseRequest.MEMFILE_MAX = 1024 * 1024 @route('/management') def service(): start_point = request.GET.get('start_point', default=None) walking_time_period = request.GET.get('walking_time_period', default=None) walking_speed = request.GET.get('walking_speed', default=None) distance_decay_function = request.GET.get('distance_decay_function', default=None) if start_point and walking_time_period and walking_speed and distance_decay_function is not None: return management(start_point, walking_time_period, walking_speed, distance_decay_function) run(host='0.0.0.0', port=8363, debug=True)
y reacemos la función management(), la diferencia con Bike es que en éste se utiliza isochroneOLD para obtener el espacio posible, por lo que nuestra nueva función queda:
def management(start_point, walking_time_period, walking_speed, distance_decay_function): otp_isochroneOLD_url = 'http://146.155.17.18:23080/otp/routers/default/isochroneOld?' otp_dataInputs = 'fromPlace=%s&walkTime=%s&walkSpeed=%s&mode=WALK&toPlace=-33.5846161,-70.5410151&output=SHED' % ( start_point, walking_time_period, walking_speed) otp_url = otp_isochroneOLD_url + otp_dataInputs try: walkshed = urllib2.urlopen(otp_url).read() except: return '{"error":"OTP does not respond"}' try: poi_data = getPOIs(walkshed) except: return '{"error":"Overpass does not respond"}' crime_data = pointInPolygon(walkshed) aggregation_data = aggregation(start_point,walkshed,crime_data,poi_data,distance_decay_function,walking_time_period) result = '{"walkshed": %s, "poi": %s}' % (aggregation_data, poi_data) return result
y editamos el archivo wyp/views.py para que acepte los datos de Pedestrian:
from ManagementServicePedestrian import management as managementPedestrian def xml(request): ... elif approach == 'pedestrian': start_point = request.GET.get('start_point', default=None) walking_time_period = request.GET.get('walking_time_period', default=None) walking_speed = request.GET.get('walking_speed', default=None) distance_decay_function = request.GET.get('distance_decay_function', default=None) return HttpResponse(managementPedestrian(start_point,walking_time_period,walking_speed,distance_decay_function))
Y probamos como va funcionando con
python manage.py runserver
Para Transit hay que seguir pasos similares a Pedestrian, observar el código para ver como queda configurado.
Resultados
Finalmente el programa queda funcionando con muchos menos procesos y de forma más independiente, no requiere de una instalación de geoserver, tan solo Django Python, html, css y js.
Características nuevas
Gracias a Django fue posible crear un panel de administración para modificar ciertos valores dentro del código, además de agregar características nuevas que se explicarán a continuación:
Demographic group
La idea es evaluar los POIs para distintos grupos demográficos, quien hace ese trabajo es la función de AggregationService.py, pero para que la petición llegue desde la interfaz también debe de llegarle a ManagementService.py, por lo que realizaremos estas modificaciones:
Primero creamos el modelo de datos que contendrá a las opciones de grupos demográficos en la base de datos, para ello editamos el archivo wyp/models.py y agregamos lo siguiente:
from django.db import models class Demographic(models.Model): value = models.CharField(max_length=255) #Nombre de la opcion demografica bank = models.CharField(max_length=255) #Pesos grocery = models.CharField(max_length=255) restaurant = models.CharField(max_length=255) shopping = models.CharField(max_length=255) entertainment = models.CharField(max_length=255) school = models.CharField(max_length=255) library = models.CharField(max_length=255) health = models.CharField(max_length=255) sum = models.FloatField(blank=True,default=0) #Suma de los pesos anteriores score_factor = models.FloatField(blank=True,default=0) #Valor de ponderacion def __unicode__(self): return self.value #Generar el resultado de sum y score_factor automaticamente al guardar def save(self, *args, **kwargs): sumando = 0 for i in ast.literal_eval(self.bank): sumando += i for i in ast.literal_eval(self.grocery): sumando += i for i in ast.literal_eval(self.restaurant): sumando += i for i in ast.literal_eval(self.shopping): sumando += i for i in ast.literal_eval(self.entertainment): sumando += i for i in ast.literal_eval(self.school): sumando += i for i in ast.literal_eval(self.library): sumando += i for i in ast.literal_eval(self.health): sumando += i self.sum = sumando self.score_factor = 100.0/float(sumando) super(Demographic,self).save(*args, **kwargs)
Agregamos este modelo al panel de administración editando el archivo wyp/admin.py
from wyp.models import Demographic admin.site.register(Demographic)
Modificamos el archivo wyp/views.py para mandar estos datos a la vista html
def inicio(request): demographic = Demographic.objects.all() return render_to_response('index.html',{'demographic':demographic},context_instance=RequestContext(request))
Luego modificamos el archivo html que se encuentra en template para que éstos aparezcan dinámicamente como una lista de opciones. template/index.html
<h3>Cycling Model</h3> <div> <form id="call_wps_bike" method="post" action="#"> <fieldset> <ul> ... <li><input type="checkbox" id="distance_decay_function_bike" name="distance_decay_function"/>Distance Decay Function </li> <li> To: <select id="demographic_bike"> {% for demo in demographic%} <option value="{{demo.value}}">{{demo.value}}</option> {% endfor %} </select> </li> <input type="button" class="button" onclick="call_wps('bike')" value="Get Accessibility Score"/> </ul> </fieldset> </form> </div> <h3>Walking Model</h3> <div> <form id="call_wps_pedestrian" method="post" action="#"> <fieldset> <ul> <li> <input type="hidden" id="start_point_pedestrian" name="start_point_pedestrian" readonly/> </li> ... <div id="walking_speed_pedestrian" style="width: 100px; height: 6px; margin:15px 10px auto 15px; display: inline-block;"></div> <span style="font-size: 10px;">Fast</span> </div> </li> <li> To: <select id="demographic_pedestrian"> {% for demo in demographic%} <option value="{{demo.value}}">{{demo.value}}</option> {% endfor %} </select> </li> <li><input type="checkbox" id="distance_decay_function_pedestrian" name="distance_decay_function"/>Distance Decay Function </li> <input type="button" class="button" onclick="call_wps('pedestrian')" value="Get Accessibility Score"/> </ul> </fieldset> </form> </div> <h3>Transit and Walking Model</h3> <div> <form id="call_wps_transit" method="post" action="#"> <fieldset> <ul> <li> <input type="hidden" id="start_point_transit" name="start_point_transit" readonly/> </li> ... <li> To: <select id="demographic_transit"> {% for demo in demographic%} <option value="{{demo.value}}">{{demo.value}}</option> {% endfor %} </select> </li> ... <input type="button" class="button" onclick="call_wps('transit')" value="Get Accessibility Score"/> </ul> </fieldset> </form> </div>
Para que estos valores sean reconocidos por el js que llama a la url /call_wps.php hay que modificar el archivo template/js/main.js para cada una de las opciones que se le agregó la opción demográfica
function call_wps(approach){ var start_point; ... var wps_call; var demographic; switch (approach) { case "bike": start_point = $('#start_point_bike').val(); biking_time_period = $('#biking_time_period_amount').val(); distance_decay_function = $('#distance_decay_function_bike').prop('checked'); demographic = $('#demographic_bike option:selected').text(); wps_call = "call_wps.php?wps=bike&start_point=" + start_point + "&biking_time_period=" + biking_time_period + "&distance_decay_function=" + distance_decay_function + "&demographic="+demographic; break; case "pedestrian": start_point = $('#start_point_pedestrian').val(); walking_time_period = $('#walking_time_period_amount_pedestrian').val(); walking_speed_amount = $('#walking_speed_amount_pedestrian').val(); demographic = $('#demographic_pedestrian option:selected').text(); switch (walking_speed_amount) { case '3': walking_speed = .83; break; case '3.5': walking_speed = .97; break; case '4': walking_speed = 1.11; break; case '4.5': walking_speed = 1.25; break; case '5': walking_speed = 1.38; break; case '5.5': walking_speed = 1.52; break; case '6': walking_speed = 1.67; break; } distance_decay_function = $('#distance_decay_function_pedestrian').prop('checked'); wps_call = "call_wps.php?wps=pedestrian&start_point=" + start_point + "&walking_time_period=" + walking_time_period + "&walking_speed=" + walking_speed + "&distance_decay_function=" + distance_decay_function + "&demographic="+demographic; break; case "transit": start_point = $('#start_point_transit').val(); walking_time_period = $('#walking_time_period_amount_transit').val(); walking_speed_amount = $('#walking_speed_amount_transit').val(); demographic = $('#demographic_transit option:selected').text(); switch (walking_speed_amount) { case '3': walking_speed = .83; break; case '3.5': walking_speed = .97; break; case '4': walking_speed = 1.11; break; case '4.5': walking_speed = 1.25; break; case '5': walking_speed = 1.38; break; case '5.5': walking_speed = 1.52; break; case '6': walking_speed = 1.67; break; } distance_decay_function = $('#distance_decay_function_transit').prop('checked'); walking_start_time = $('#walking_start_time').val() + ":00"; wps_call = "call_wps.php?wps=transit&start_point=" + start_point + "&walking_start_time=" + walking_start_time + "&walking_time_period=" + walking_time_period + "&walking_speed=" + walking_speed + "&bus_waiting_time=0&bus_riding_time=0&distance_decay_function=" + distance_decay_function + "&demographic="+demographic; break; }
Con ésto, ahora podemos recibir el valor escogido para la opción demográfica vía GET, por lo que ahora nos vamos a editar el archivo wyp/views.py para manejarlo.
def xml(request): approach = request.GET.get('wps',default=None) if approach == 'bike': ... demographic = request.GET.get('demographic',default=None) return HttpResponse(managementBike(start_point,biking_time_period,distance_decay_function,demographic)) elif approach == 'pedestrian': ... demographic = request.GET.get('demographic',default=None) return HttpResponse(managementPedestrian(start_point,walking_time_period,walking_speed,distance_decay_function,demographic)) elif approach == 'transit': ... demographic = request.GET.get('demographic', default=None) return HttpResponse(managementTransit(start_point,walking_start_time,walking_time_period,walking_speed,bus_waiting_time,bus_riding_time, distance_decay_function,demographic)) else: return HttpResponse('')
Como vemos, ahora enviamos también la opción demográfica como nuevo parámetro de la función management* por lo que debemos modificar cada uno de ellos, de todas formas para todo es lo mismo por lo que solo se explicará el cambio realizado en Bike en el archivo wyp/ManagementServiceBike.py
def management(start_point, biking_time_period, distance_decay_function,demographic): ... aggregation_data = aggregation(start_point,bikeshed,crime_data,poi_data,distance_decay_function,biking_time_period,demographic) ...
y finalmente debemos editar el archivo en cuestión que nos retorna la puntuación, wyp/AggregationService.py.
from models import Demographic def aggregation(start_point, walkshed, crime_data, poi_data, distance_decay_function, walking_time_period,demographic): if poi_data != '"NULL"': demographic_value = Demographic.objects.get(value=demographic) poi_weights = {"Bank": ast.literal_eval(demographic_value.bank), #bank "Grocery": ast.literal_eval(demographic_value.grocery), #grocery "Restaurant": ast.literal_eval(demographic_value.restaurant), #restaurant and coffee "Shopping": ast.literal_eval(demographic_value.shopping), #shopping "Entertainment": ast.literal_eval(demographic_value.entertainment), #entertainment "School": ast.literal_eval(demographic_value.school), #school "Library": ast.literal_eval(demographic_value.library), #library "Health": ast.literal_eval(demographic_value.health) #health }
Entonces, para probar que todo esto funciona debemos ejecutar estos comandos desde la raíz del proyecto
python manage.py collectstatics #por la midificacion de template/js/main.js python manage.py makemigrations # por la creacion del modelo Demographic python manage.py migrate python manage.py runserver
Alerta cuando uno de los servidores externos está caido
Ha pasado que las VMs de OTP y de Overpass se caen por alguna razón o simplemente no parten correctamente lo que afecta directamente el funcionamiento de esta aplicación, para ello es importante saber cuál es la que está causando el problema, con ello necesitamos primero encontrar el error por lo que editaremos lo siguiente (nuevamente se mostrará con Bike, se realizó lo mismo con Pedestrian y Transit)
En el archivo wyp/ManagementServiceBike.py agregamos:
try: bikeshed = urllib2.urlopen(otp_url).read() except: return '{"error":"OTP does not respond"}' ... try: poi_data = getPOIs(bikeshed) except: return '{"error":"Overpass does not respond"}'
Y para generar la alerta lo haremos gracias a la ayuda del archivo template/js/main.js
}, success: function (data, status) { var wps_results = $.parseJSON(data); var error_result = wps_results['error']; if (error_result){ $.unblockUI(); alert(error_result); }
Cambio de servidores de OTP y de overpass por panel de administración
Una de las cosas principales que tiene Django es que cuenta con un panel de administración para poder editar la información de la base de datos o controlar ciertas configuraciones. En este caso crearemos un panel que permita intercambiar el servidor de OTP o de Overpass a cual queramos, para ello la guardaremos en la base de datos editando el archivo wyp/models.py
class Server(models.Model): overpass = models.CharField(max_length=255,default='http://146.155.17.18:24080/api/interpreter') otp_bike = models.CharField(max_length=255,default='http://146.155.17.18:23080/otp/routers/default/isochrone?&') otp_walk = models.CharField(max_length=255,default='http://146.155.17.18:23080/otp/routers/default/isochroneOld?')
Lo agregamos al panel de administración agregando estas líneas en wyp/admin.py
from wyp.models import Demographic, Server admin.site.register(Server)
Ejecutamos las siguientes líneas de comando para agregar la información a la base de datos
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser #crear un superusuario
python manage.py runserver
Entramos ahora a la dirección http://localhost:8000/admin/ e ingresamos con el superusuario, entramos a Servers y creamos un elemento, los campos vendrán listos por lo que solo hay que guardar tal cual como sale.
Luego editamos los archivos en donde se encuentra esta información
wyp/ManagementServiceBike.py
otp_isochrone_url = Server.objects.all()[0].otp_bike
wyp/ManagementServicePedestrian.py
otp_isochrone_url = Server.objects.all()[0].otp_walk
wyp/ManagementServiceTransit.py
otp_isochrone_url = Server.objects.all()[0].otp_walk
wyp/POIService.py
_overpass_url = Server.objects.all()[0].overpass
Análisis de muchos puntos mediante la subida de un archivo CSV
Este trabajo se realiza netamente desde el panel de administración por lo que no se necesita de intervenir la interfaz. Se necesita de un sistema que reciba el archiv csv, lo lea línea por línea y lo analice según corresponda, la solución fue implementar un proceso extra (fuera de la interfaz) que realice el trabajo con tal de dejarlo funcionar toda la noche, por ejemplo.
Como lo queremos ver en el panel de administración necesitamos crearle un modelo, para ello editamos nuevamente el archivo wyp/models.py agregando
import ast, subprocess, shlex TYPES_OF_ANALYSYS = ( ('Bike','Cycling Model'), ('Walk','Walking Model'), ('Transit','Transit & Walking Model') ) class Multiple_point_analysis(models.Model): csv_upload = models.FileField(upload_to='csv_uploads/') type_of_analysis = models.CharField(max_length=10,choices=TYPES_OF_ANALYSYS) demographic_group = models.ForeignKey(Demographic) analysed_pointset = models.BooleanField(default=False,blank=True,editable=False) results = models.FileField(upload_to='results/', blank=True) def __unicode__(self): return self.csv_upload.url.split('csv_uploads/')[-1] def save(self, *args, **kwargs): super(Multiple_point_analysis,self).save(*args, **kwargs) if not self.analysed_pointset: subprocess.Popen(shlex.split('python manage.py mass_analysis -i '+str(self.id)))
y lo mostramos en el panel editando el archivo wyp/admin.py
from wyp.models import Demographic, Server, Multiple_point_analysis admin.site.register(Multiple_point_analysis)
Además debemos agregar toda la siguiente rama de archivos:
- wyp/
- management/
- __init__.py
- commands/
- __init__.py
- _private.py
- mass_analysis.py
- management/
En donde ambos __init__.py y _private.py son archivos vacíos, management y commands carpetas y mass_analysis.py nuestra función que analizará el archivo csv. Entonces construimos el archivo wyp/management/commands/mass_analysis.py agregando las siguientes líneas:
from optparse import make_option from django.core.management.base import BaseCommand, CommandError from wypdjango import settings from wyp.models import Multiple_point_analysis from wyp.ManagementServiceBike import management as managementBike from wyp.ManagementServicePedestrian import management as managementPedestrian from wyp.ManagementServiceTransit import management as managementTransit from django.core.files import File import json from time import time class Command(BaseCommand): help = "Performs mass analysis from a csv file.\nIt requires id model to analyze." option_list = BaseCommand.option_list + ( make_option( '-i',action='store', dest='i', default='', help='id of the csv file in the model'), ) def handle(self, *app_labels, **options): csv_model = options['i'] csv_model = Multiple_point_analysis.objects.get(id=int(csv_model)) filename = csv_model.csv_upload.url.split('/media/')[-1] url = settings.MEDIA_ROOT + filename csv = open(url,'r') lines = csv.read().split('\n') csv.close() temp = open('/tmp/analysis_from_wyp.csv','w') temp.write('id;x;y;score;area;time;POI count;\n') id_result = 1 try: if csv_model.type_of_analysis == 'Bike': for line in lines: aux = line.split(';') if aux[0] != 'id' and len(aux)>2: # aux = id;x;y;biking_time_period;distance_decay_function; start_point = aux[1]+','+aux[2] biking_time_period = aux[3] distance_decay_function = aux[4] start_time = time() results = managementBike(start_point,biking_time_period,distance_decay_function,csv_model.demographic_group.value) elapsed_time = time() - start_time results = json.loads(results) text = str(id_result)+';'+aux[1]+';'+aux[2]+';'+results['walkshed']['properties']['score']+';'+str(results['walkshed']['properties']['area'])+';'+str(elapsed_time)+';'+str(len(results['poi']['features']))+';\n' temp.write(text) id_result += 1 if csv_model.type_of_analysis == 'Walk': for line in lines: aux = line.split(';') if aux[0] != 'id' and len(aux)>2: #aux = id;x;y;walking_time_period;walking_speed;distance_decay_function; start_point = aux[1]+';'+aux[2] walking_time_period = aux[3] walking_speed = aux[4] distance_decay_function = aux[5] start_time = time() results = managementPedestrian(start_point, walking_time_period, walking_speed, distance_decay_function, csv_model.demographic_group.value) elapsed_time = time() - start_time results = json.loads(results) text = str(id_result) + ';' + aux[1] + ';' + aux[2] + ';'+results['walkshed']['properties']['score']+';'+str(results['walkshed']['properties']['area'])+';'+str(elapsed_time)+';'+str(len(results['poi']['features']))+';\n' temp.write(text) id_result += 1 if csv_model.type_of_analysis == 'Transit': for line in lines: aux = line.split(';') if aux[0] != 'id' and len(aux)>2: # aux = id;x;y;start_time;walking_time_period;walking_speed;bus_waiting_time;bus_ride_time;distance_decay_function start_point = aux[1] + ';' + aux[2] start_time_transit = aux[3] walking_time_period = aux[4] walking_speed = aux[5] #bus_waiting_time = aux[6] bus_waiting_time = '0' #bus_ride_time = aux[7] bus_ride_time = '0' distance_decay_function = aux[6] start_time = time() results = managementTransit(start_point,start_time_transit,walking_time_period,walking_speed,bus_waiting_time,bus_ride_time,distance_decay_function,csv_model.demographic_group.value) elapsed_time = time() - start_time results = json.loads(results) text = str(id_result) + ';' + aux[1] + ';' + aux[2] + ';'+results['walkshed']['properties']['score']+';'+str(results['walkshed']['properties']['area'])+';'+str(elapsed_time)+';'+str(len(results['poi']['features']))+';\n' temp.write(text) id_result += 1 except Exception as e: pass #Pendiente!!!!! if csv_model.type_of_analysis == 'Transit': management(start_point, start_time, walking_time_period, walking_speed, bus_waiting_time, bus_ride_time, distance_decay_function) pass temp.close() csv_model.analysed_pointset = True csv_model.results.save('results/' + csv_model.csv_upload.name.split('csv_uploads/')[-1] + '.csv', File(open('/tmp/analysis_from_wyp.csv'))) csv_model.save()
Finalmente ejecuntamos estas líneas de comando para agregar el modelo a la base de datos:
python manage.py makemigrations python manage.py migrate python manage.py runserver
Entramos a http://localhost:8000/admin y nos encontraremos con la opción de Multiple_point_analysis en donde tenemos que hacer click en "add multiple_point_analysis". Llenamos los campos que se encuentran en negrita y apretamos en "SAVE". Esperamos a que finalice (~15 segundos por línea de CSV) y los resultados estarán en el campo "Results" como archivo csv para descargar.
UPDATE Paralelización
Al analizar la ciudad de Santiago con un archivo CSV, éste tardaba cerca de 23 horas en completar más de 52000 puntos, por lo que se decidió usar paralelización para utilizar todo el rendimiento de la VM. Para ello se utilizaron los librerías de Python threading y Queue. El código cambió en las siguientes líneas:
import threadint import Queue if csv_model.type_of_analysis == 'Bike': args_list = list() for line in lines: aux = line.split(';') if aux[0] != 'id' and len(aux)>2: start_point = aux[1]+','+aux[2] biking_time_period = aux[3] distance_decay_function = aux[4] start_time = time() args_list.append([start_point,biking_time_period,distance_decay_function,csv_model.demographic_group.value,csv_model.escenario.id,start_time]) if len(args_list)==40 or lines.index(line) == len(lines)-1: process_list = list() queue_list = list() for arg in args_list: q = Queue.Queue() process = threading.Thread(target=text_from_bike, args=arg+[q]) process.start() process_list.append(process) queue_list.append(q) args_list = list() for process in process_list: process.join() for queue in queue_list: text = queue.get() temp.write(text) logging.info(text.strip('\n')) id_result += 1
El cambio en los otros modos es similar. El gran cambio de esta metodología es que crea grupos de 40 pequeños programas que solo se encargan de realizar la evaluación, éste recupera todos estos resultados y después ejecuta otros 40, con ésto el tiempo se reduce considerablemente, ya que habían puntos que tardaban solo 1 segundo en tener respuesta como otros más de 10. Por cada 40 puntos a analizar el nuevo tiempo corresponde a la duración del que tarde más. En otras palabras, si el grupo de 40 puntos son de score 0 y tardan menos de 1 segundo en ser analizados, el tiempo será menos de 1 segundo para todo el grupo en comparación a los respectivos 40 que sería analizar uno a uno.
Sistema de Log para análisis de archivos CSV
Como no se sabe si de verdad está funcionando o hay problemas de por medio al momento de analizar un archivo CSV, se creó un sistema de log que nos permita saber que es lo que está sucediendo. Para ello lo creamos como proceso al igual que el análisis del archivo. por lo que creamos el archivo wyp/management/commands/log.py y le agregamos:
from optparse import make_option from django.core.management.base import BaseCommand, CommandError from wyp.models import Log_wyp class Command(BaseCommand): help = "Save logs to the system" option_list = BaseCommand.option_list + ( make_option( '-l',action='store', dest='l', default='', help='message log'), ) def handle(self, *app_labels, **options): message = options['l'] if message != '': if len(message)>255: Log_wyp(log=message[:255]).save() else: Log_wyp(log=message).save()
y agregamos el modelo en el archivo wyp/models.py
class Log_wyp(models.Model): log = models.CharField(max_length=255,default='') created = models.DateTimeField(auto_now_add=True) def __unicode__(self): return str(self.created.strftime('%Y-%m-%d %H:%M:%S')) + ' ' +self.log
Lo mostramos en el panel de administración editando el archivo wyp/admin.py
from wyp.models import Demographic, Server, Multiple_point_analysis, Log_wyp admin.site.register(Log_wyp)
Ejecutamos los comandos para agregar el modelo a la base de datos
python manage.py makemigrations python manage.py migrate
ahora, para poder agregar cualquier log lo podemos hacer ejecutando el comando:
python manage.py log -l "Mensaje a registrar"
Por lo que, por ejemplo, al integrarlo al archivo wyp/management/commands/mass_analysis.py se realizó lo siguiente:
import ast, subprocess, shlex subprocess.Popen(shlex.split('python manage.py log -l "' + filename + ' Starting analysis"'))
Cambio de frecuencia de buses editando el archivo GTFS de Santiago
Se trabaja actualmente en la construcción de una herramienta que permia cambiar las frecuencias de las líneas de buses y metro de la ciudad de Santiago con el objetivo de poder analizar los cambios de accesibilidad urbana derivadas de ésto. Con ello, en Django Python, se cargó en una base de datos SQLite (aparte de la que ya utiliza Django) el GTFS de los datos de la ciudad en cuestión gracias al módulo pygtfs. Para utilizar sus datos solo basta con llamarlos con sus funciones.
Con ello, las frecuencias de los recorridos se encuentran en el archivo frequencies.txt dentro del archivo comprimido GTFS.zip. Con el módulo pygtfs previamente configurado se pueden llamar dichos datos con los comandos
sched = pygtfs.Schedule(MEDIA_ROOT + 'gtfs.sqlite') frequencies = sched.frequencies
generando en frequencies una lista con todas las frecuencias y los datos adjuntos que podemos ver en el mismo archivo de texto pero como objeto python.
Luego se creó una vista en [1] en donde encontraremos un espacio de selección de viaje identificado por su ID en la cual al ser seleccionada se desplegará su información, tal como su hora de inicio, hora de fin y su frecuencia de salida, este último en un campo editable. Cuando ya cambiamos este valor, podemos guardar este dato en una lista de futuros datos para ser actualizados, la cual la podemos ver en la zona inferior de este formulario. Ésta lista puede ser tan grande como SQLite aguante.
Para construir esta lista, se debió de crear una copia de los modelos que hay en pygtfs en la base de datos local de Django manteniendo solo los datos que se desean cambiar, al ser eliminados de la lista también son eliminados de la base de datos para evitar que éste se llene o cause algún tipo de inconveniente.
Ya cuando tengamos todos nuestros cambios guardados, apretamos en el botón de Actualizar GTFS. Éste proceso puede llegar a tardar entre 7 a 8 minutos en ser completado, por lo que el botón se bloqueará durante este tiempo. Una vez finalizado debemos dirigirnos al panel de administración para cambiar el router de evaluación y utilizar el que acabamos de construir. Para ello se entra a [2] y se modifica la URL en cuestion para cambiar /routers/<texto-a-cambiar>/isochrone? por /routers/WYP/isochrone?. Una vez realizado este cambio podemos efectuar las consultas con nuestras nuevas frecuencias de buses.
Cabe destacar que este proceso es temporal mientras no se construya un front-end ideal, ésta herramienta se construyó de esta forma para probar su funcionalidad y no garantiza lo más óptimo.
API REST para obtener datos del archivo de frecuencia de buses
Para que el putno anterior de esta documentación funcione correctamente, fue necesario que se creara una API REST que nos permita obtener datos de las frecuencias mientras que por GET le enviemos la ID correspondiente al tramo a evaluar. En otras palabras, debemos ingresar una URL como ésta [3] y obtendremos un objeto Json con la información en cuestión.
Sistema de análisis multiple de forma visual
Se agregó la opción Mapping bbox, el cual permite analizar el recuadro que vemos en el mapa con múltiples puntos. Al abrir esta opción nos encontramos con la selección del método de viaje que deseamos evaluar, la opción demográfica y la cantidad de puntos que deseamos procesar para obtener un mapeo de los resultados. Para este proceso se utilizó la parametrización, es decir, si se deciden evaluar con 3 puntos horizontales y 3 verticales, los 9 sectores a evaluar se realizan todos a la vez, por lo que mientras más puntos a evaluar, más carga para la VM.
Los resultados son mostrados en el mapa de forma de colores, el blanco representa score 0 y verde un score de 100.
Funcionamiento actual
El código se puede encontrar acá: http://146.155.17.14:18080/csfuente/wyp-django/ y se encuentra funcionando en http://146.155.17.19:17080/