Posted 07 December 2024
I recently came back to Condor virtual soaring after several years away, and also started using XCSoar on a tablet as an auxiliary navigation tool. One challenge in doing this is getting the scenery (.XCM) files associated with the various Condor sceneries. Some years ago ‘Folken’ (folken@kabelsalat.ch) created the https://mapgen.xcsoar.org/ web app to facilitate this process. The website accepts a map name, email address, and map bounds information and produces the corresponding .XCM file, which can then be dropped into XCSoar for navigation support – neat! Map bounds can be defined three ways – as manually entered max/min lat/lon values, as a rectangle on a dynamic world map, or as a waypoint file (.CUP or .DAT).
Unfortunately, as I and several others have found, the web app doesn’t actually support waypoint file bounds; it produces a ‘unsupported waypoint file’ error whenever a waypoint file is submitted. The developer has been unwilling/unable to work the problem due to other demands on his time, so I decided to take a shot at finding/fixing the problem.
First attempt: ‘backend code’ assessment: https://github.com/paynterf/XCSoarMapGenDebug
Last February (Feb 2024) I looked through the github repository, and because I am totally clueless regarding modern (or any age, for that matter) web app development, I decided to concentrate on the ‘backend code’ to either find the problem(s) or exclude this code as the culprit. To do this I created the above repo, and eventually worked my way through most of the backend code, without finding any issues – oh well.
Current attempt: Build and run the web app (ugh!):
After ignoring this problem again for almost a full year, I decided to take another shot at this. Folken’s website has a Readme that details the process of setting up a web server on a Debian linux box, and since I happen to have an old laptop with Debian installed, I decided to give this a whirl. The Readme describes how to use an ‘Ansible Playbook‘ to build and provision an XCMapGen web app. I tried this several times, and to say I got ‘a bit disoriented’ would be the understatement of the century. After having to reload Debian on my laptop several times, I reached out to Folken for help. Amazingly, he actually answered and was (and is) quite helpful. He told me he had gotten away from Ansible and was now using Docker for website build and provisioning. In addition, he gave me detailed steps on how to use Docker to bring up an XCSoarMapGen website on my Debian laptop. Here’s his email back to me:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
Yes, so basically follow the docker install https://docs.docker.com/engine/install/debian/ Install docker-compose: apt-get install docker-compose git clone https://github.com/XCSoar/mapgen.git cd mapgen The following will build the containers and start them docker-compose up browser to: http://localhost:9090/ should give you the mapgen frontend page ctrl-c to terminate the docker process then you can make a change in the git repo and do docker-compose up --build this will rebuild the images and start them with your change. Regards, - Philipp |
Of course I had never heard of Docker (or Flask, or Cherrypy, or Ansible or…..), so I was in for some serious research work. Eventually I got the XCSoarMapGen website running at ‘localhost’ on my Debian linux laptop, and for me that was quite an achievement – from web ignoramus to web genius in 234 easy steps! 😎
After some more help from Folken, I got to the point where I could watch the activity between the web page and the backend code, but eventually I decided that this wasn’t getting me anywhere – I really needed to be able to run the backend code in debug mode, but of course I had no idea how to do that. After lots more inet searches for ‘Debugging web applications’ and similar, I found (and worked through) a number of tutorials, like Marcel Demper’s “Debugging Python in Docker using VSCode” and Debugging flask application within a docker container using VSCode, but I was never able to actually get my localhost XCSoarMapGen app to run under debug control – rats!
However, what I did get from the tutorials was the fact that the tutorials both referred to Flask (a software tool I’d never heard of before), while Folken’s XCSoarMapGen app uses CherryPy (another software tool I’d never heard of before). So this sent me off on another wild-goose chase through the internet to learn about debugging with Cherrypy.
After another hundred years or so of web searches and non-working tutorials, I never figured out how to actively debug a Cherrypy app, but I did figure out how to insert print (actually cherrypy.log() statements into the code and see the results by looking at the ‘error.log’ file produced by (I think) Cherrypy. Here’s the ‘parse_seeyou_waypoints()’ function in the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
def parse_seeyou_waypoints(lines, bounds=None): waypoint_list = WaypointList() cherrypy.log('in parse_seeyou_waypoints function') first = True # wpnum = 0 # for line in lines: # wpnum = wpnum + 1 # cherrypy.log('wpnum = %s, line = %s' % (wpnum, line)) wpnum = 0 for line in lines: wpnum = wpnum + 1 cherrypy.log('in for loop: wpnum = %s line = %s' %(wpnum, line)) if first: first = False continue line = line.strip() cherrypy.log('in for loop after line = line.strip(): wpnum = %s line = %s' %(wpnum, line)) if line == "name,code,country,lat,lon,elev,style,rwdir,rwlen,freq,desc": continue if line == "" or line.startswith("*"): continue if line == "-----Related Tasks-----": break fields = [] line = __CSVLine(line) cherrypy.log('in for loop after line = __CSVLine(line): wpnum = %s' %wpnum) while line.has_next(): fields.append(next(line)) if len(fields) < 6: continue lat = __parse_coordinate(fields[3]) if bounds and (lat > bounds.top or lat < bounds.bottom): continue lon = __parse_coordinate(fields[4]) if bounds and (lon > bounds.right or lon < bounds.left): continue wp = Waypoint() wp.lat = lat wp.lon = lon wp.altitude = __parse_altitude(fields[5]) wp.name = fields[0].strip() wp.country_code = fields[2].strip() #cherrypy.log('waypoint %s: name = %s' %(wpnum, wp.name)) if len(fields) > 6 and len(fields[6]) > 0: wp.cup_type = int(fields[6]) if len(fields) > 7 and len(fields[7]) > 0: wp.runway_dir = int(fields[7]) if len(fields) > 8 and len(fields[8]) > 0: wp.runway_len = __parse_length(fields[8]) if len(fields) > 9 and len(fields[9]) > 0: wp.freq = float(fields[9]) if len(fields) > 10 and len(fields[10]) > 0: wp.comment = fields[10].strip() cherrypy.log('waypoint %s: name = %s' %(wpnum, wp.name)) waypoint_list.append(wp) return waypoint_list |
And here’s some output from ./error.log:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
.173E,978.0m,1,,,,\r\n' frank@M6700:~/mapgen$ sudo docker exec -it mapgen_mapgen-frontend_1 bash root@23c482b85ae1:/# tail -f ./error.log [07/Dec/2024:01:13:19] ENGINE Listening for SIGTERM. [07/Dec/2024:01:13:19] ENGINE Listening for SIGHUP. [07/Dec/2024:01:13:19] ENGINE Listening for SIGUSR1. [07/Dec/2024:01:13:19] ENGINE Bus STARTING [07/Dec/2024:01:13:19] ENGINE Started monitor thread '_TimeoutMonitor'. [07/Dec/2024:01:13:19] ENGINE Started monitor thread 'Autoreloader'. [07/Dec/2024:01:13:19] ENGINE Serving on http://127.0.0.1:8080 [07/Dec/2024:01:13:19] ENGINE Bus STARTED [07/Dec/2024:01:13:43] In the 'index.html' routine [07/Dec/2024:01:13:43] params: name = sfasdfsdf, mail = paynterf@gmail.com, detail = 3 [07/Dec/2024:01:13:43] waypoint_file = slovenia3.cup, selection = waypoint [07/Dec/2024:01:13:43] in TRY block filename = slovenia3.cup [07/Dec/2024:01:13:43] in parse_waypoint_file: filename = Slovenia3.cup [07/Dec/2024:01:13:43] in parse_waypoint_file filename.lower().endswith(".cup"): filename = Slovenia3.cup [07/Dec/2024:01:13:43] in parse_seeyou_waypoints function [07/Dec/2024:01:13:43] in for loop: wpnum = 1 line = b'name,code,country,lat,lon,elev,style,rwdir,rwlen,freq,desc\r\n' [07/Dec/2024:01:13:43] in for loop: wpnum = 2 line = b'"Abfaltersb Chp",Abfalter,,4645.518N,01232.392E,952.0m,1,,,,\r\n' [07/Dec/2024:01:13:43] in for loop after line = line.strip(): wpnum = 2 line = b'"Abfaltersb Chp",Abfalter,,4645.518N,01232.392E,952.0m,1,,,,' |
So, the run/debug/edit/run cycle is:
- Make changes to the source code, insert/edit cherrypy.log() statements
- Rebuild the affected files and restart the XCSoarMapGen website at localhost:9090 on my lilnux box with ‘sudo docker-compose up –build’
- Reconnect to the error log output with ‘sudo docker exec -it mapgen_mapgen-frontend_1 bash’ followed by (at the #prompt) ‘tail -f ./error.log’
Here’s the startup command and resulting output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
frank@M6700:~/mapgen$ sudo docker-compose up --build [sudo] password for frank: Building mapgen-frontend Sending build context to Docker daemon 1.134MB Step 1/15 : FROM debian:bullseye-slim ---> 5d941b17065a Step 2/15 : ENV MAPFRNT_PKG="ca-certificates python3-cherrypy3 python3-genshi" ---> Using cache ---> f0efa56a07e9 Step 3/15 : ENV NGINX_PKG="nginx" ---> Using cache ---> 64b225b1a575 Step 4/15 : ENV SUPERVISOR_PKG="supervisor" ---> Using cache ---> 1dc445339b60 Step 5/15 : ENV DEBIAN_FRONTEND=noninteractive ---> Using cache ---> 28a98a529272 Step 6/15 : RUN apt-get update && apt-get install -y --no-install-recommends $MAPFRNT_PKG $NGINX_PKG $SUPERVISOR_PKG && apt-get clean && rm -rf /var/lib/apt/lists/* ---> Using cache ---> 6016078d5233 Step 7/15 : COPY lib/ /opt/mapgen/lib/ ---> Using cache ---> 0fcbf832183b Step 8/15 : COPY bin/ /opt/mapgen/bin/ ---> Using cache ---> 52d296f20270 Step 9/15 : VOLUME /opt/mapgen/jobs ---> Using cache ---> d9248d8ecf57 Step 10/15 : COPY ./container/frontend/files/default /etc/nginx/sites-available/default ---> Using cache ---> cfe9c7dd6c25 Step 11/15 : RUN ln -fs /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default ---> Using cache ---> 29396baacf78 Step 12/15 : EXPOSE 9090 ---> Using cache ---> ff016e98ce21 Step 13/15 : RUN mkdir -p /var/log/supervisor ---> Using cache ---> 76415856e420 Step 14/15 : COPY ./container/frontend/files/supervisord.conf /etc/supervisor/conf.d/supervisord.conf ---> Using cache ---> a3cee45adfdc Step 15/15 : CMD ["/usr/bin/supervisord"] ---> Using cache ---> aa2fae6fd52f Successfully built aa2fae6fd52f Successfully tagged ghcr.io/xcsoar/mapgen-frontend:latest Building mapgen-worker Sending build context to Docker daemon 1.134MB Step 1/9 : FROM debian:bullseye-slim ---> 5d941b17065a Step 2/9 : ENV MAPWRK_PKGS="ca-certificates python3-jsonschema python3-requests p7zip gdal-bin mapserver-bin wget" ---> Using cache ---> a59de0e1adcf Step 3/9 : ENV DEBIAN_FRONTEND=noninteractive ---> Using cache ---> 314014120da4 Step 4/9 : RUN apt-get update && apt install -y --no-install-recommends $MAPWRK_PKGS && apt-get clean && rm -rf /var/lib/apt/lists/* ---> Using cache ---> 48970fe8cdf3 Step 5/9 : COPY lib/ /opt/mapgen/lib/ ---> Using cache ---> 3d67bf668274 Step 6/9 : COPY bin/ /opt/mapgen/bin/ ---> Using cache ---> cb63f1c54e09 Step 7/9 : VOLUME /opt/mapgen/data ---> Using cache ---> bbb5238f31b6 Step 8/9 : VOLUME /opt/mapgen/jobs ---> Using cache ---> 5a18ccc8f8c5 Step 9/9 : ENTRYPOINT ["/opt/mapgen/bin/mapgen-worker"] ---> Using cache ---> cb15ed27d785 Successfully built cb15ed27d785 Successfully tagged ghcr.io/xcsoar/mapgen-worker:latest Starting mapgen_mapgen-frontend_1 ... done Starting mapgen_mapgen-worker_1 ... done Attaching to mapgen_mapgen-worker_1, mapgen_mapgen-frontend_1 mapgen-worker_1 | Traceback (most recent call last): mapgen-worker_1 | File "/opt/mapgen/bin/mapgen-worker", line 9, in <module> mapgen-worker_1 | from xcsoar.mapgen.server.worker import Worker mapgen-worker_1 | File "/opt/mapgen/lib/xcsoar/mapgen/server/worker.py", line 9, in <module> mapgen-worker_1 | from xcsoar.mapgen.generator import Generator mapgen-worker_1 | File "/opt/mapgen/lib/xcsoar/mapgen/generator.py", line 6, in <module> mapgen-worker_1 | from xcsoar.mapgen.waypoints import welt2000cup mapgen-worker_1 | File "/opt/mapgen/lib/xcsoar/mapgen/waypoints/welt2000cup.py", line 5, in <module> mapgen-worker_1 | from xcsoar.mapgen.waypoints.seeyou_reader import parse_seeyou_waypoints mapgen-worker_1 | File "/opt/mapgen/lib/xcsoar/mapgen/waypoints/seeyou_reader.py", line 4, in <module> mapgen-worker_1 | import cherrypy mapgen-worker_1 | ModuleNotFoundError: No module named 'cherrypy' mapgen-frontend_1 | /usr/lib/python3/dist-packages/supervisor/options.py:474: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a "-c" argument specifying an absolute path to a configuration file for improved security. mapgen-frontend_1 | self.warnings.warn( mapgen-frontend_1 | 2024-12-07 19:56:07,358 CRIT Supervisor is running as root. Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message. mapgen-frontend_1 | 2024-12-07 19:56:07,358 INFO Included extra file "/etc/supervisor/conf.d/supervisord.conf" during parsing mapgen-frontend_1 | 2024-12-07 19:56:07,362 INFO RPC interface 'supervisor' initialized mapgen-frontend_1 | 2024-12-07 19:56:07,362 CRIT Server 'unix_http_server' running without any HTTP authentication checking mapgen-frontend_1 | 2024-12-07 19:56:07,362 INFO supervisord started with pid 1 mapgen_mapgen-worker_1 exited with code 1 mapgen-frontend_1 | 2024-12-07 19:56:08,364 INFO spawned: 'mapgen-frontend' with pid 8 mapgen-frontend_1 | 2024-12-07 19:56:08,366 INFO spawned: 'nginx_00' with pid 9 mapgen-frontend_1 | 2024-12-07 19:56:08,393 INFO success: nginx_00 entered RUNNING state, process has stayed up for > than 0 seconds (startsecs) mapgen-frontend_1 | 2024-12-07 19:56:09,711 INFO success: mapgen-frontend entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) |
And here’s the commands (in a separate terminal) to start the logging output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
frank@M6700:~/mapgen$ sudo docker exec -it mapgen_mapgen-frontend_1 bash [sudo] password for frank: Error response from daemon: Container 23c482b85ae1ca05524364fd47c03da46d5dbb6f35fe2e3ac8eb0be5e2a4324f is not running frank@M6700:~/mapgen$ sudo docker exec -it mapgen_mapgen-frontend_1 bash root@23c482b85ae1:/# tail -f ./error.log [07/Dec/2024:15:22:57] ENGINE Bus EXITED [07/Dec/2024:15:22:57] ENGINE Waiting for child threads to terminate... [07/Dec/2024:19:56:08] ENGINE Listening for SIGTERM. [07/Dec/2024:19:56:08] ENGINE Listening for SIGHUP. [07/Dec/2024:19:56:08] ENGINE Listening for SIGUSR1. [07/Dec/2024:19:56:08] ENGINE Bus STARTING [07/Dec/2024:19:56:08] ENGINE Started monitor thread '_TimeoutMonitor'. [07/Dec/2024:19:56:08] ENGINE Started monitor thread 'Autoreloader'. [07/Dec/2024:19:56:08] ENGINE Serving on http://127.0.0.1:8080 [07/Dec/2024:19:56:08] ENGINE Bus STARTED |
So now I can run the web app at localhost:9090 on my linux box, and watch the action via cherrypy.log() statements – cool!
After a while I had narrowed down my search to the ‘parse_seeyou_waypoints(lines, bounds=None)’ function in the ‘seeyou_reader.py’ file (repeated here for convenience).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
def parse_seeyou_waypoints(lines, bounds=None): waypoint_list = WaypointList() cherrypy.log('in parse_seeyou_waypoints function') first = True # wpnum = 0 # for line in lines: # wpnum = wpnum + 1 # cherrypy.log('wpnum = %s, line = %s' % (wpnum, line)) wpnum = 0 for line in lines: wpnum = wpnum + 1 cherrypy.log('in for loop: wpnum = %s line = %s' %(wpnum, line)) if first: first = False continue line = line.strip() cherrypy.log('in for loop after line = line.strip(): wpnum = %s line = %s' %(wpnum, line)) if line == "name,code,country,lat,lon,elev,style,rwdir,rwlen,freq,desc": continue if line == "" or line.startswith("*"): continue if line == "-----Related Tasks-----": break fields = [] line = __CSVLine(line) cherrypy.log('in for loop after line = __CSVLine(line): wpnum = %s' %wpnum) while line.has_next(): fields.append(next(line)) if len(fields) < 6: continue lat = __parse_coordinate(fields[3]) if bounds and (lat > bounds.top or lat < bounds.bottom): continue lon = __parse_coordinate(fields[4]) if bounds and (lon > bounds.right or lon < bounds.left): continue wp = Waypoint() wp.lat = lat wp.lon = lon wp.altitude = __parse_altitude(fields[5]) wp.name = fields[0].strip() wp.country_code = fields[2].strip() #cherrypy.log('waypoint %s: name = %s' %(wpnum, wp.name)) if len(fields) > 6 and len(fields[6]) > 0: wp.cup_type = int(fields[6]) if len(fields) > 7 and len(fields[7]) > 0: wp.runway_dir = int(fields[7]) if len(fields) > 8 and len(fields[8]) > 0: wp.runway_len = __parse_length(fields[8]) if len(fields) > 9 and len(fields[9]) > 0: wp.freq = float(fields[9]) if len(fields) > 10 and len(fields[10]) > 0: wp.comment = fields[10].strip() cherrypy.log('waypoint %s: name = %s' %(wpnum, wp.name)) waypoint_list.append(wp) return waypoint_list |
Here’s the email I sent off to Folken:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
I have successfully instrumented the code with cherrypy.log() statements to the point where I *think* I see the problem with handling .CUP waypoint files. I have attached my instrumented version of 'seeyou_reader.py' so you can see the cherrypy.log() statements, along with the error.log showing the results from running the app. From what I can tell right now, it appears that the 'parse_seeyou_waypoints(lines, bounds=None):' function is called OK, but the 'for line in lines:' for loop terminates without processing any lines (don't know why at the moment). I added a small 'for line in lines' for loop above the original one, and had it just list the lines in the file. It did so correctly, printing out all 825 waypoint strings. Here is a small snippet from the end of error.log (full file attached) root@b1113e2e7ccb:/# tail -f ./error.log [06/Dec/2024:01:24:50] wpnum = 816, line = b'"LXNAV HQ",LXNAV HQ,,4614.085N,01516.631E,238.0m,1,,,,\r\n' [06/Dec/2024:01:24:50] wpnum = 817, line = b'"SLJEME TOWER",SLJEME T,,4553.967N,01556.891E,1026.0m,1,,,,\r\n' [06/Dec/2024:01:24:50] wpnum = 818, line = b'"Laufkraftwerk Fe",Laufkraf,,4632.797N,01417.903E,432.0m,1,,,,\r\n' [06/Dec/2024:01:24:50] wpnum = 819, line = b'"Sostanj Power Pl",Sostanj ,,4622.372N,01502.904E,370.0m,1,,,,\r\n' [06/Dec/2024:01:24:50] wpnum = 820, line = b'"Tauernmoossee",Tauernmo,,4709.612N,01238.527E,2008.0m,1,,,,\r\n' [06/Dec/2024:01:24:50] wpnum = 821, line = b'"Weissee",Weissee,,4708.138N,01237.373E,2232.0m,1,,,,\r\n' [06/Dec/2024:01:24:50] wpnum = 822, line = b'"Margaritze staus",Margarit,,4704.040N,01245.870E,1960.0m,1,,,,\r\n' [06/Dec/2024:01:24:50] wpnum = 823, line = b'"Hochwurtenspeich",Hochwurt,,4701.033N,01300.433E,2402.0m,1,,,,\r\n' [06/Dec/2024:01:24:50] wpnum = 824, line = b'"Koelnbrein Dam",Koelnbre,,4704.775N,01320.475E,1788.0m,1,,,,\r\n' [06/Dec/2024:01:24:50] wpnum = 825, line = b'"Diga della Maina",Diga del,,4626.879N,01244.173E,978.0m,1,,,,\r\n' Here's the output from error.log before I added the 'for' loop to write out all .CUP file lines: [06/Dec/2024:00:58:46] ENGINE Bus STARTED [06/Dec/2024:00:59:14] In the 'index.html' routine [06/Dec/2024:00:59:14] params: name = sfasdfsdf, mail = paynterf@gmail.com, detail = 3 [06/Dec/2024:00:59:14] waypoint_file = slovenia3.cup, selection = waypoint [06/Dec/2024:00:59:14] in TRY block filename = slovenia3.cup [06/Dec/2024:00:59:14] in parse_waypoint_file: filename = Slovenia3.cup [06/Dec/2024:00:59:14] in parse_waypoint_file filename.lower().endswith(".cup"): filename = Slovenia3.cup [06/Dec/2024:00:59:14] in parse_seeyou_waypoints function [06/Dec/2024:00:59:14] in for loop: wpnum = 1 [06/Dec/2024:00:59:14] in for loop: wpnum = 2 [06/Dec/2024:00:59:14] in for loop after line = line.strip(): wpnum = 2 [06/Dec/2024:00:59:40] In the 'index.html' routine [06/Dec/2024:00:59:40] params: name = sfasdfsdf, mail = paynterf@gmail.com, detail = 3 [06/Dec/2024:00:59:40] waypoint_file = slovenia3.cup, selection = waypoint [06/Dec/2024:00:59:40] in TRY block filename = slovenia3.cup [06/Dec/2024:00:59:40] in parse_waypoint_file: filename = Slovenia3.cup [06/Dec/2024:00:59:40] in parse_waypoint_file filename.lower().endswith(".cup"): filename = Slovenia3.cup [06/Dec/2024:00:59:40] in parse_seeyou_waypoints function [06/Dec/2024:00:59:40] in for loop: wpnum = 1 [06/Dec/2024:00:59:40] in for loop: wpnum = 2 [06/Dec/2024:00:59:40] in for loop after line = line.strip(): wpnum = 2 ^C As you can see, the original 'for' loop only runs twice - the first one (I think) just skips over the header line, as it should. However, I don't think it should terminate with only two iterations as it is doing now. I'll play with this a bit more and see if I can figure out exactly what is (or isn't) going on, but I thought you'd like to know that there is definitely something amiss in ' parse_seeyou_waypoints(lines, bounds=None): Regards, Frank |
After sleeping on this for a while, I realized there were two other mysteries associated with this file.
- I noticed the waypoint printout from the temporary for loop at the start of the function showed the letter ‘b’ prepended on each waypoint line, and that isn’t what’s actually in the waypoint file. I have no idea how that letter got added, or even if it is real (could be some artifact of the way linux handles string output) – it’s a mystery.
- I realized there was a disconnect between the syntax of the call to ‘parse_seeyou_waypoints()’ in ‘parser.py’ and the actual definition of the function in ‘seeyou_reader.py’. In parser.py the call is ‘return parse_seeyou_waypoints(file)’, but in seeyou_reader.py the definition is ‘def parse_seeyou_waypoints(lines, bounds=None):’ So parser.py is sending a file object, but seeyou_reader.py is expecting a ‘lines’ object (presumably the list of lines read in from the waypoint (.CUP) file).
Just to make sure I wasn’t blowing smoke, I did a ‘git grep parse_seeyou_waypoints’ in the repo directory and got the following:
1 2 3 4 5 6 7 |
frank@M6700:~/mapgen$ git grep parse_seeyou_waypoints lib/xcsoar/mapgen/waypoints/parser.py:from xcsoar.mapgen.waypoints.seeyou_reader import parse_seeyou_waypoints lib/xcsoar/mapgen/waypoints/parser.py: return parse_seeyou_waypoints(file) lib/xcsoar/mapgen/waypoints/seeyou_reader.py:def parse_seeyou_waypoints(lines, bounds=None): lib/xcsoar/mapgen/waypoints/seeyou_reader.py: cherrypy.log('in parse_seeyou_waypoints function') lib/xcsoar/mapgen/waypoints/welt2000cup.py:from xcsoar.mapgen.waypoints.seeyou_reader import parse_seeyou_waypoints lib/xcsoar/mapgen/waypoints/welt2000cup.py: return parse_seeyou_waypoints(f, bounds) |
This shows there is exactly one call to parse_seeyou_waypoint(), so it’s clear that what is being sent is not what is expected. I’m not sure that this problem is the reason that waypoint files aren’t being processed, but is sure is a good bet.
So, what to do? It looks like both the parse_winpilot_waypoints() and parse_seeyou_waypoints functions are called with a ‘file’ parameter, and expect a ‘lines’ parameter, so it’s probably best to do the file->lines conversion in parse_waypoint_file(). Just as a sidenote, I noticed that ‘parse_winpilot_waypoints()’ doesn’t include a ‘bounds = None’ default parameter – wonder why?
08 December 2024 Update:
Well, I ran around in circles for quite a while, trying to get my head around the issue of ‘mixed signals’ – where the call from parser.py is: shows
1 |
parse_seeyou_waypoints(file) |
but the definition of parse_seeyou_waypoints() in seeyou_reader.py is
1 |
def parse_seeyou_waypoints(lines, bounds = none) |
To add to the mystery, it is apparently OK to treat the ‘lines’ argument as a list of strings, so:
An error has occurred. Please try again later. |
and this loop correctly prints out all the lines in the selected .CUP file (albeit with a leading ‘b’ that I can’t yet explain). So clearly there is some Python magic going on where a ‘file’ object gets turned into a ‘lines’ object on-the-fly!
09 December 2024 Update:
To try and clear up the ambiguity between ‘file’ and ‘lines’, I placed the following code into the ‘parse_waypoint_file(filename, file = none)’ function definition in parser.py
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def parse_waypoint_file(filename, file=None): cherrypy.log('in parse_waypoint_file: filename = %s' % filename) if not file: cherrypy.log('in parse_waypoint_file: if not file block with filename = %s' % filename) else: lines = file.readlines() cherrypy.log('in parse_waypoint_file: %s lines read from %s' %(len(lines), filename)) wpnum = 0 for line in lines: wpnum+=1 cherrypy.log('line%s: %s' % (wpnum, line)) |
When I refreshed the web app, I immediately got the printout of all the lines in the file, even though the ‘Waypoint File:’ box showed ‘no file selected’. I suspect this is because the file information from the last selection is cached.
I closed and re-opened the web site (‘x’ed out the tab and then retyped ‘localhost:9090’ in a new tab), and then re-started the web app. This time the only readout was ‘At the top of the index.html function, with params = {}’.
Then I entered a map name and my email address clicked on the ‘Browse’ button and selected the ‘Slovenia.cup’ file. This did not trigger the code in parser.py.
Then I clicked on the ‘Generate’ button and this triggered the ‘lines’ display. Then I entered ‘F5’ to regenerate the page, and the line list printout was triggered again. So, I think it’s clear that the site chaches entry data.
OK, I think I might have gotten somewhere; so the line I added in parser.py/parse_waypoint_file(filename, file=None):
1 |
lines = file.readlines() |
Works as expected, and loads the ‘lines’ object with a list of lines in the file. In addition, I could now change the call to ‘parse_seeyou_waypoints’ as follows:
1 2 |
#return parse_seeyou_waypoints(file) return parse_seeyou_waypoints(lines)#241207 gfp bugfix: |
Thereby (at least for me) getting rid of the headache I experienced every time I looked at the disconnect between the way parse_seeyou_waypoints was called from parse_waypoint_file and the way it is defined in seeyou_reader.py. When I run this configuration, I get the full waypoint list printout in both parse_waypoint_file() and parse_seeyou_waypoints() – yay!
However, we are still left with the original problem, which is that .CUP (and probably .DAT) files aren’t getting processed properly. I am now starting to believe that the ‘b’ character prepended on all the lines read in from the waypoint file is actually there. If that were in fact the case, that might well explain why the processing loop quits after line 1 (or maybe after line 2 – not entirely sure). When viewed in a normal text viewer like Notepad++ in windows, or in https://filehelper.com/view, I see:
1 2 3 4 5 6 7 8 9 10 11 |
name,code,country,lat,lon,elev,style,rwdir,rwlen,freq,desc "Abfaltersb Chp",Abfalter,,4645.518N,01232.392E,952.0m,1,,,, "Admont Chp",Admont C,,4734.994N,01427.156E,628.0m,1,,,, "Admont Stift Kir",Admont S,,4734.517N,01427.700E,646.0m,1,,,, "Admonterhaus",Admonter,,4737.917N,01429.483E,1720.0m,1,,,, "Aflenz Kirche",Aflenz K,,4732.717N,01514.467E,772.0m,1,,,, |
but when I use ‘cherrypy.log(‘line%s: %s’ % (wpnum, line)) in a loop to display the lines in a linux terminal window, I get:
1 2 3 4 5 6 7 8 |
[09/Dec/2024:23:37:08] line1: b'name,code,country,lat,lon,elev,style,rwdir,rwlen,freq,desc\r\n' [09/Dec/2024:23:37:08] line2: b'"Abfaltersb Chp",Abfalter,,4645.518N,01232.392E,952.0m,1,,,,\r\n' [09/Dec/2024:23:37:08] line3: b'"Admont Chp",Admont C,,4734.994N,01427.156E,628.0m,1,,,,\r\n' [09/Dec/2024:23:37:08] line4: b'"Admont Stift Kir",Admont S,,4734.517N,01427.700E,646.0m,1,,,,\r\n' [09/Dec/2024:23:37:08] line5: b'"Admonterhaus",Admonter,,4737.917N,01429.483E,1720.0m,1,,,,\r\n' [09/Dec/2024:23:37:08] line6: b'"Aflenz Kirche",Aflenz K,,4732.717N,01514.467E,772.0m,1,,,,\r\n' [09/Dec/2024:23:37:08] line7: b'"Afritz Church",Afritz C,,4643.650N,01347.983E,710.0m,1,,,,\r\n' [09/Dec/2024:23:37:08] line8: b'"Aich Assac Chp",Aich Ass,,4725.480N,01351.460E,736.0m,1,,,,\r\n' |
10 December 2024 Update:
So, the problem with the leading ‘b’ turned out to be an issue with the binary-to-ascii decoder used in cherrypy.log() statements. Because no decoder was specified, I got no decoding – hence the leading ‘b’. Once I bought a clue from yet another post to StackExchange, I prefixed any log statements with something like
1 |
line = byteline.decode('ISO-8859-2') #gfp 241210: added 'ISO-8859-2' decoding for correct cherrypy logging display |
The ‘ISO-8859-2’ decoder was used because some waypoints use eastern European accent marks.
At the end of the day today, I had worked my way through a number of other minor and not-so-minor problems (most caused by stupidity on my part), and arrived at the point where the code is properly processing the Slovenia3.cup file, at least as far as extracting field elements from the lines, as shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
[10/Dec/2024:23:15:01] At the top of the index.html function, with params = {'name': 'Slovenia', 'mail': 'paynterf@gmail.com', 'level_of_detail': '3', 'selection': 'waypoint', 'waypoint_file': <cherrypy._cpreqbody.Part object at 0x7f8250240d90>, 'left': '', 'right': '', 'bottom': '', 'top': ''} [10/Dec/2024:23:15:01] params: name = Slovenia, mail = paynterf@gmail.com, detail = 3 [10/Dec/2024:23:15:01] waypoint_file = <_io.BufferedRandom name=7>, waypoint_filename = Slovenia3.cup, selection = waypoint [10/Dec/2024:23:15:01] waypoint_file = <_io.BufferedRandom name=7> [10/Dec/2024:23:15:01] in TRY block filename = slovenia3.cup [10/Dec/2024:23:15:01] in parse_waypoint_file: filename = Slovenia3.cup [10/Dec/2024:23:15:01] in parse_waypoint_file: 825 lines read from Slovenia3.cup [10/Dec/2024:23:15:01] in parse_waypoint_file filename.lower().endswith(".cup"): filename = Slovenia3.cup [10/Dec/2024:23:15:01] in parse_seeyou_waypoints function: [10/Dec/2024:23:15:01] for loop row 1: name,code,country,lat,lon,elev,style,rwdir,rwlen,freq,desc [10/Dec/2024:23:15:01] header line found at row 1: name,code,country,lat,lon,elev,style,rwdir,rwlen,freq,desc [10/Dec/2024:23:15:01] for loop row 2: "Abfaltersb Chp",Abfalter,,4645.518N,01232.392E,952.0m,1,,,, [10/Dec/2024:23:15:01] extracted fields for line = 2 [10/Dec/2024:23:15:01] field[0] = Abfaltersb Chp [10/Dec/2024:23:15:01] field[1] = Abfalter [10/Dec/2024:23:15:01] field[2] = [10/Dec/2024:23:15:01] field[3] = 4645.518N [10/Dec/2024:23:15:01] field[4] = 01232.392E [10/Dec/2024:23:15:01] field[5] = 952.0m [10/Dec/2024:23:15:01] field[6] = 1 [10/Dec/2024:23:15:01] field[7] = [10/Dec/2024:23:15:01] field[8] = [10/Dec/2024:23:15:01] field[9] = [10/Dec/2024:23:15:01] for loop row 3: "Admont Chp",Admont C,,4734.994N,01427.156E,628.0m,1,,,, [10/Dec/2024:23:15:01] extracted fields for line = 3 [10/Dec/2024:23:15:01] field[0] = Admont Chp [10/Dec/2024:23:15:01] field[1] = Admont C [10/Dec/2024:23:15:01] field[2] = [10/Dec/2024:23:15:01] field[3] = 4734.994N [10/Dec/2024:23:15:01] field[4] = 01427.156E [10/Dec/2024:23:15:01] field[5] = 628.0m [10/Dec/2024:23:15:01] field[6] = 1 [10/Dec/2024:23:15:01] field[7] = [10/Dec/2024:23:15:01] field[8] = [10/Dec/2024:23:15:01] field[9] = [10/Dec/2024:23:15:01] for loop row 4: "Admont Stift Kir",Admont S,,4734.517N,01427.700E,646.0m,1,,,, [10/Dec/2024:23:15:01] extracted fields for line = 4 [10/Dec/2024:23:15:01] field[0] = Admont Stift Kir [10/Dec/2024:23:15:01] field[1] = Admont S [10/Dec/2024:23:15:01] field[2] = [10/Dec/2024:23:15:01] field[3] = 4734.517N [10/Dec/2024:23:15:01] field[4] = 01427.700E [10/Dec/2024:23:15:01] field[5] = 646.0m [10/Dec/2024:23:15:01] field[6] = 1 [10/Dec/2024:23:15:01] field[7] = [10/Dec/2024:23:15:01] field[8] = [10/Dec/2024:23:15:01] field[9] = . . . [10/Dec/2024:23:15:01] for loop row 26: "Amzs KoââĂ",Amzs KoĂ,,4539.139N,01450.914E,470.0m,1,,,, [10/Dec/2024:23:15:01] extracted fields for line = 26 [10/Dec/2024:23:15:01] field[0] = Amzs KoââĂ [10/Dec/2024:23:15:01] field[1] = Amzs KoĂ [10/Dec/2024:23:15:01] field[2] = [10/Dec/2024:23:15:01] field[3] = 4539.139N [10/Dec/2024:23:15:01] field[4] = 01450.914E [10/Dec/2024:23:15:01] field[5] = 470.0m [10/Dec/2024:23:15:01] field[6] = 1 [10/Dec/2024:23:15:01] field[7] = [10/Dec/2024:23:15:01] field[8] = [10/Dec/2024:23:15:01] field[9] = . . . [10/Dec/2024:23:15:01] for loop row 825: "Diga della Maina",Diga del,,4626.879N,01244.173E,978.0m,1,,,, [10/Dec/2024:23:15:01] extracted fields for line = 825 [10/Dec/2024:23:15:01] field[0] = Diga della Maina [10/Dec/2024:23:15:01] field[1] = Diga del [10/Dec/2024:23:15:01] field[2] = [10/Dec/2024:23:15:01] field[3] = 4626.879N [10/Dec/2024:23:15:01] field[4] = 01244.173E [10/Dec/2024:23:15:01] field[5] = 978.0m [10/Dec/2024:23:15:01] field[6] = 1 [10/Dec/2024:23:15:01] field[7] = [10/Dec/2024:23:15:01] field[8] = [10/Dec/2024:23:15:01] field[9] = |
As shown in the printout for row 26, the eastern European accent marks are being handled properly. The ‘parse_seeyou_waypoints()’ function responsible for this is shown below: Note that not all of this function is enabled yet – that’s next!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
def parse_seeyou_waypoints(lines, bounds=None): waypoint_list = WaypointList() cherrypy.log('in parse_seeyou_waypoints function:') #gfp 241210: modified to wait for header line before processing #gfp 241210: added 'ISO-8859-2' decoding for correct cherrypy logging display #gfp 241208 added to print out all lines in selected .CUP file # wpnum = 0 # for byteline in lines: # wpnum = wpnum + 1 # line = byteline.decode('ISO-8859-2') header = 'name,code,country,lat,lon,elev,style,rwdir,rwlen,freq,desc' wpnum = 0 for byteline in lines: wpnum = wpnum + 1 line = byteline.decode('ISO-8859-2') #gfp 241210: added 'ISO-8859-2' decoding for correct cherrypy logging display line = line.strip() # cherrypy.log('in for loop: wpnum = %s line = %s' %(wpnum, line)) cherrypy.log(f'for loop row {wpnum}: {line}') #check for blank lines or comments if line == "" or line.startswith("*"): continue if header in line: cherrypy.log(f'header line found at row {wpnum}: {line}') continue #skip to next line (first waypoint line) if line == "-----Related Tasks-----": cherrypy.log('In -----Related Tasks----: line = %s' % line) break # cherrypy.log('in for loop before line = __CSVLine(line): wpnum = %s' %wpnum) fields = [] # line = __CSVLine(line) CSVline = __CSVLine(line) cherrypy.log(f'row {wpnum}: line = __CSVLine(line) ->> {line}') # while line.has_next(): # fields.append(next(line)) while CSVline.has_next(): fields.append(next(CSVline)) #display fields for this line cherrypy.log('extracted fields for line = %s' %wpnum) idx = 0 for field in fields: cherrypy.log(f' field[{idx}] = {field}') idx += 1 # if len(fields) < 6: # continue # lat = __parse_coordinate(fields[3]) # if bounds and (lat > bounds.top or lat < bounds.bottom): # continue # lon = __parse_coordinate(fields[4]) # if bounds and (lon > bounds.right or lon < bounds.left): # continue # wp = Waypoint() # wp.lat = lat # wp.lon = lon # wp.altitude = __parse_altitude(fields[5]) # wp.name = fields[0].strip() # wp.country_code = fields[2].strip() # #cherrypy.log('waypoint %s: name = %s' %(wpnum, wp.name)) # if len(fields) > 6 and len(fields[6]) > 0: # wp.cup_type = int(fields[6]) # if len(fields) > 7 and len(fields[7]) > 0: # wp.runway_dir = int(fields[7]) # if len(fields) > 8 and len(fields[8]) > 0: # wp.runway_len = __parse_length(fields[8]) # if len(fields) > 9 and len(fields[9]) > 0: # wp.freq = float(fields[9]) # if len(fields) > 10 and len(fields[10]) > 0: # wp.comment = fields[10].strip() # cherrypy.log('waypoint %s: name = %s' %(wpnum, wp.name)) # waypoint_list.append(wp) # return waypoint_list |
11 December 2024 Update:
I uncommented the rest of the parse_seeyou_waypoints(lines, bounds=None) function, and except for an oddity regarding the default parameter ‘bounds’, it all seemed to function OK. When parse_waypoint_file() calls parse_seeyou_waypoints() it doesn’t use the ‘bounds’ argument, so it is set to ‘None’ at the start of that function. And, there is nothing in the function to initialize ‘bounds’ to anything, so I’m not sure why it was included in the first place. The ‘bounds’ object is referenced twice, as follows:
1 2 3 4 5 6 7 |
lat = __parse_coordinate(fields[3]) if bounds and (lat > bounds.top or lat < bounds.bottom): continue lon = __parse_coordinate(fields[4]) if bounds and (lon > bounds.right or lon < bounds.left): continue |
but since ‘bounds’ is never initialized and is always ‘None’, these two ‘if’ statements always fail. This is definitely fishy – maybe the original coder had something in mind that never got included?
As I understand things so far, the purpose of ‘parse_seeyou_waypoints(()’ is to create a list of waypoint objects and return it to the calling function with
1 |
return waypoint_list |
Since the calling code in ‘parse_waypoint_file()’ is:
1 |
return parse_seeyou_waypoints(lines)#241207 gfp bugfix: |
I think this means that whatever calls ‘parse_waypoint_file()’ receives the now-filled waypoint list. This appears to be the calling code in ‘server.py’:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
try: filename = waypoint_file.filename.lower() cherrypy.log('in TRY block filename = %s' % filename) if not filename.endswith(".dat") and ( filename.endswith(".dat") or not filename.endswith(".cup") ): raise RuntimeError( "Waypoint file {} has an unsupported format.".format( waypoint_file.filename ) ) desc.bounds = parse_waypoint_file( waypoint_file.filename, waypoint_file.file ).get_bounds() desc.waypoint_file = ( "waypoints.cup" if filename.endswith(".cup") else "waypoints.dat" ) except: return view.render( error="Unsupported waypoint file " + waypoint_file.filename ) | HTMLFormFiller(data=params) |
So server.py calls parse_waypoint_file with the filename and the file object (pointer?). parse_waypoint_file extracts a list of lines from the file, and then passes that list to (for the .CUP file case) to parse_seeyou_waypoints(), and gets a waypoint list back, which is then calls the ‘.get_bounds()’ method in the waypoint list class. The ‘get_bounds() method is declared in the ‘WaypointList’ class as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
class WaypointList: def __init__(self): self.__list = [] def __len__(self): return len(self.__list) def __getitem__(self, i): if i < 0 or i > len(self): return None return self.__list[i] def __iter__(self): return iter(self.__list) def append(self, wp): if not isinstance(wp, Waypoint): raise TypeError("Waypoint expected") self.__list.append(wp) def extend(self, wp_list): if not isinstance(wp_list, WaypointList): raise TypeError("WaypointList expected") self.__list.extend(wp_list) def get_bounds(self, offset_distance=15.0): rc = GeoRect(180, -180, -90, 90) for wp in self.__list: cherrypy.log(f'In list.py: {wp.name}, lat: {wp.lat:.3f}, lon: {wp.lon:.3f}') rc.left = min(rc.left, wp.lon) rc.right = max(rc.right, wp.lon) rc.top = max(rc.top, wp.lat) rc.bottom = min(rc.bottom, wp.lat) rc.expand(offset_distance) cherrypy.log(f'In list.py - final rc: left {rc.left:3f}, right: {rc.right:.3f}, top: {rc.top:.3f}, bot {rc.bottom:.3f}') return rc |
When instrumented as shown above to print out the final min/max lat/lons, I got the following:
1 |
[12/Dec/2024:02:31:37] In list.py - final rc: left 12.247120, right: 17.041, top: 47.921, bot 45.351 |
Comparing the above figures with the actual Slovenia map in Condor2, they cover the measured map extents quite nicely – yay!
12 December 2024 Update:
While looking through the code in server.py, I ran across the following ‘if’ statement:
1 2 3 4 5 6 7 8 |
if not filename.endswith(".dat") and ( filename.endswith(".dat") or not filename.endswith(".cup") ): raise RuntimeError( "Waypoint file {} has an unsupported format.".format( waypoint_file.filename ) ) |
The intent of the above boolean expression is to warn the user that the extension of the selected file is neither ‘.DAT’ nor ‘.CUP’. So, if the extension is .DAT then the expression is false immediately, and therefore the warning is not emitted. If the extension isn’t .DAT, then the second half of the expression is evaluated, and it is true only if the file extension is NOT .CUP. So, it works but it sure is confusing. A much better way of writing this would be
1 2 3 4 5 6 7 |
if not filename.endswith(".dat") and not filename.endswith(".cup") ): raise RuntimeError( "Waypoint file {} has an unsupported format.".format( waypoint_file.filename ) ) |
1 |
if not filename.endswith(".dat") and not filename.endswith(".cup"): |
At the end of the day I had things running pretty well, and I added a line in server.py to print out the min/max lat/lon values calculated from parsing the ‘slovenia3.cup’ file, resulting in the following output:
1 |
in server.py: slovenia3.cup bounds: left = 12.247, right: 17.041, top: 47.921, bot 45.351 |
18 December 2024 Update:
At this point, I have the website working to the point where it can successfully parse either a .CUP file or a .DAT file and print the bounds in the ‘error’ field on the web page. In order to get to this place, I actually wrote a small python script to convert back and forth between .CUP and .DAT formats, so that I could test MapGen using the same exact waypoints in both formats. The calculated bounds, of course, should also be identical.
Here’s the bounds output from Slovenia3.dat:
1 |
slovenia3.dat bounds: left = 12.247, right: 17.041, top: 47.921, bot 45.351 |