├── .gitignore ├── README.md ├── ascii_qgis.config ├── ascii_qgis.py └── parfait ├── QGIS.py ├── __init__.py ├── layer_wrappers.py ├── printing.py ├── projects.py ├── qt.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | *.jpg 4 | *.png 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #What the heck is this? 2 | A ASCII map thingo for QGIS projects 3 | 4 | ![](https://raw.githubusercontent.com/NathanW2/ascii_qgis/gh-pages/images/newrender.png) 5 | ![](https://raw.githubusercontent.com/NathanW2/ascii_qgis/gh-pages/images/newrender2.png) 6 | 7 | # How do I run it 8 | - Edit the paths in the .config file 9 | - Run ascii_qgis.py 10 | - try the open-project command 11 | 12 | # Why did you make this? 13 | Because........ I can 14 | 15 | # What commands can I use? 16 | Command-list to see 17 | 18 | # Does this really have any use? 19 | Maybe...maybe not 20 | 21 | # Really? 22 | Yes indeed because ASCII! 23 | 24 | ## Note 25 | 26 | Only tested on my machines and there might be bugs that you will run into. 27 | -------------------------------------------------------------------------------- /ascii_qgis.config: -------------------------------------------------------------------------------- 1 | {"showhelp": false, "paths": ["/media/nathan/Data/gis_data/QGIS Projects", "F:\\gis_data\\QGIS Projects"]} -------------------------------------------------------------------------------- /ascii_qgis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import time 3 | import curses 4 | import curses.panel 5 | import os 6 | import sys 7 | import json 8 | from collections import namedtuple 9 | from curses.textpad import Textbox, rectangle 10 | from qgis.core import QgsMapLayerRegistry, QgsProject, QgsMapRendererParallelJob, QgsLayerTreeGroup, QgsLayerTreeLayer, QgsRectangle, QgsPoint, QgsMapSettings, \ 11 | QgsMapLayer, QGis 12 | from qgis.gui import QgsMapCanvas, QgsLayerTreeMapCanvasBridge 13 | from PyQt4.QtCore import QSize, Qt 14 | from PyQt4.QtGui import QColor, QImage 15 | from parfait import QGIS, projects 16 | 17 | import logging 18 | logging.basicConfig(filename='render.log', level=logging.DEBUG) 19 | 20 | # Bunch of good old globals......for now 21 | scr = None 22 | project = None 23 | pad = None 24 | legendwindow = None 25 | color_mode_enabled = True 26 | ascii_mode_enabled = False 27 | aboutwindow = None 28 | modeline = None 29 | mapwindow = None 30 | canvas = None 31 | 32 | layercolormapping = {} 33 | colors = {} 34 | 35 | config = {} 36 | commands = {} 37 | 38 | TOPBORDER = 5 39 | BOTTOMBORDER = 2 40 | 41 | if hasattr(curses, "CTL_UP"): 42 | UP = curses.CTL_UP 43 | DOWN = curses.CTL_DOWN 44 | LEFT = curses.CTL_LEFT 45 | RIGHT = curses.CTL_RIGHT 46 | PAGEUP = curses.CTL_PGUP 47 | PAGEDOWN = curses.CTL_PGDN 48 | else: 49 | UP = 566 50 | DOWN = 525 51 | LEFT = 545 52 | RIGHT = 560 53 | PAGEUP = 555 54 | PAGEDOWN = 550 55 | 56 | codes = [ 57 | '@', # Point 58 | '.', # Line 59 | '#', # Polygon 60 | ' ', # Unsupported 61 | ' ' # Unknown 62 | ] 63 | 64 | def command(names=None, *args, **kwargs): 65 | def escape_name(funcname): 66 | """ 67 | Escape the name of the given function. 68 | """ 69 | funcname = funcname.replace("_", "-") 70 | funcname = funcname.replace(" ", "-") 71 | return funcname.lower() 72 | 73 | if not names: 74 | names = [] 75 | def _command(func): 76 | name = escape_name(func.__name__) 77 | commands[name] = func 78 | for name in names: 79 | commands[name] = func 80 | return func 81 | return _command 82 | 83 | 84 | class QAndA: 85 | QUESTION = 3 86 | QUESTIOnERROR = 4 87 | 88 | def __init__(self, question, type=QUESTION, completions=None): 89 | self.question = question 90 | self.type = type 91 | if not completions: 92 | completions = [] 93 | self.completions = completions 94 | 95 | 96 | @command(names=['command-list']) 97 | def show_commands(): 98 | cmds = "\n".join(commands) 99 | aboutwindow.display(title="Commands", content=cmds) 100 | aboutwindow.hide() 101 | redraw_main_stuff() 102 | 103 | 104 | @command(names=["help", "?"]) 105 | def show_help(): 106 | abouttxt = """ 107 | YAY ASCII! 108 | 109 | Type commands into the bottom 110 | to take action. 111 | 112 | Try something like open-project 113 | which can take a name 114 | of a project or a path. 115 | (Config the paths in ascii_qgis.config 116 | 117 | Once a project is loaded you can 118 | use these to move the map around 119 | 120 | CTRL + UP - Pan Up 121 | CTRL + DOWN - Pan Down 122 | CTRL + LEFT - Pan Left 123 | CTRL + RIGHT - Pan Right 124 | 125 | CTRL + PAGE UP - Zoom In 126 | CTRL + PAGE DOWN - Zoom Out 127 | 128 | Details: 129 | 130 | Running QGIS Version: {} 131 | 132 | """.format(QGis.QGIS_VERSION) 133 | aboutwindow.display(title="Help", content=abouttxt) 134 | aboutwindow.hide() 135 | redraw_main_stuff() 136 | 137 | 138 | @command(names=['about', 'faq', 'wat!?']) 139 | def show_about(): 140 | abouttxt = """ 141 | > What the heck is this? 142 | A ASCII map thingo for QGIS projects 143 | 144 | > Why did you make this? 145 | Because........ I can 146 | 147 | > What commands can I use? 148 | Command-list to see 149 | 150 | > Does this really have any use? 151 | Maybe...maybe not 152 | 153 | > Really? 154 | Yes indeed because ASCII! 155 | """ 156 | aboutwindow.display(title="FAQ - ESC to close", content=abouttxt) 157 | aboutwindow.hide() 158 | redraw_main_stuff() 159 | 160 | 161 | def redraw_main_stuff(): 162 | """ 163 | Redraw the map, legend, and clear the edit bar 164 | :return: 165 | """ 166 | mapwindow.render_map() 167 | legendwindow.render_legend() 168 | pad.clear() 169 | 170 | 171 | @command(names=['exit', 'quit']) 172 | def _exit(): 173 | curses.endwin() 174 | sys.exit() 175 | 176 | 177 | def _resolve_project_path(name): 178 | for path in config['paths']: 179 | if not name.endswith(".qgs"): 180 | name = name + ".qgs" 181 | 182 | fullpath = os.path.join(path, name) 183 | if os.path.exists(fullpath): 184 | return fullpath 185 | return None 186 | 187 | 188 | def _open_project(fullpath): 189 | global project 190 | project = projects.open_project(fullpath) 191 | return project 192 | 193 | 194 | @command(names=['load-project']) 195 | def open_project(): 196 | projectq = QAndA(question="Which project to open?", type=QAndA.QUESTION) 197 | project = yield projectq 198 | fullpath = _resolve_project_path(project) 199 | while not _resolve_project_path(project): 200 | projectq.type = QAndA.QUESTIOnERROR 201 | project = yield projectq 202 | fullpath = _resolve_project_path(project) 203 | 204 | answerq = QAndA(question="Really load ({}) | Y/N ".format(fullpath), type=QAndA.QUESTION) 205 | answer = yield answerq 206 | while not answer or answer[0].upper() not in ['Y', 'N']: 207 | answer = yield answerq 208 | 209 | if answer[0].upper() == "Y": 210 | _open_project(fullpath) 211 | assign_layer_colors() 212 | legendwindow.render_legend() 213 | mapwindow.render_map() 214 | 215 | @command() 216 | def toggle_ascii_mode(): 217 | global ascii_mode_enabled 218 | ascii_mode_enabled = not ascii_mode_enabled 219 | mapwindow.render_map() 220 | legendwindow.render_legend() 221 | 222 | @command() 223 | def toggle_color_mode(): 224 | global color_mode_enabled 225 | color_mode_enabled = not color_mode_enabled 226 | global ascii_mode_enabled 227 | ascii_mode_enabled = not color_mode_enabled 228 | mapwindow.render_map() 229 | legendwindow.render_legend() 230 | 231 | @command() 232 | def zoom_out(): 233 | factor = yield QAndA("By how much?", type=QAndA.QUESTION) 234 | mapwindow.zoom_out(float(factor)) 235 | 236 | @command() 237 | def zoom_in(): 238 | factor = yield QAndA("By how much?", type=QAndA.QUESTION) 239 | mapwindow.zoom_in(float(factor)) 240 | 241 | 242 | def assign_layer_colors(): 243 | """ 244 | Assign all the colors for each layer up front so we can use 245 | it though the application. 246 | """ 247 | import itertools 248 | colors = itertools.cycle(range(11, curses.COLORS - 10)) 249 | layercolormapping.clear() 250 | root = QgsProject.instance().layerTreeRoot() 251 | layers = [node.layer() for node in root.findLayers()] 252 | for layer in reversed(layers): 253 | if not layer.type() == QgsMapLayer.VectorLayer: 254 | continue 255 | layercolormapping[layer.id()] = colors.next() 256 | 257 | 258 | def timeme(func): 259 | def wrap(*args, **kwargs): 260 | time1 = time.time() 261 | ret = func(*args, **kwargs) 262 | time2 = time.time() 263 | logging.info('%s function took %0.3f ms' % (func.func_name, (time2-time1)*1000.0)) 264 | return ret 265 | return wrap 266 | 267 | 268 | class AboutWindow(): 269 | def __init__(self): 270 | y, x = scr.getmaxyx() 271 | self.infowin = curses.newwin(y / 2, x / 2, y / 4, x / 4) 272 | self.infopanel = curses.panel.new_panel(self.infowin) 273 | self.infowin.keypad(1) 274 | 275 | def display(self, title, content): 276 | curses.curs_set(0) 277 | self.infowin.clear() 278 | y, x = self.infowin.getmaxyx() 279 | self.infowin.bkgd(" ", curses.color_pair(6)) 280 | self.infowin.box() 281 | self.infowin.addstr(0, 0, title + " - 'q' to close", curses.A_UNDERLINE | curses.A_BOLD) 282 | for count, line in enumerate(content.split('\n'), start=1): 283 | try: 284 | self.infowin.addstr(count, 1, line) 285 | except: 286 | pass 287 | 288 | self.infopanel.show() 289 | curses.panel.update_panels() 290 | curses.doupdate() 291 | while self.infowin.getch() != ord('q'): 292 | pass 293 | curses.curs_set(1) 294 | 295 | def hide(self): 296 | self.infopanel.hide() 297 | curses.panel.update_panels() 298 | curses.doupdate() 299 | 300 | 301 | class Legend(): 302 | def __init__(self): 303 | y, x = scr.getmaxyx() 304 | self.win = curses.newwin(y - TOPBORDER, 30, BOTTOMBORDER, 0) 305 | self.win.keypad(1) 306 | self.items = [] 307 | self.title = "Layers (F5)" 308 | 309 | def render_legend(self): 310 | def render_item(node, row, col): 311 | nodestr = str(node) 312 | 313 | color = 0 314 | char = ' ' 315 | islayer = False 316 | if isinstance(node, QgsLayerTreeLayer): 317 | nodestr = "(L) " + node.layerName() 318 | if ascii_mode_enabled: 319 | char = codes[node.layer().geometryType()] 320 | if color_mode_enabled: 321 | color = layercolormapping.get(node.layerId(), 0) 322 | islayer = True 323 | if isinstance(node, QgsLayerTreeGroup): 324 | nodestr = "(G) " + node.name() 325 | 326 | 327 | state = "[ ]" 328 | if node.isVisible(): 329 | state = "[x]" 330 | 331 | expanded = ' ' 332 | if not islayer: 333 | if node.isExpanded(): 334 | expanded = '-' 335 | else: 336 | expanded = '+' 337 | 338 | # This could be made generic for reuse in other places 339 | parts = [ 340 | (expanded, 0), 341 | (state, 0), 342 | (char * 2, color), 343 | (nodestr, 0), 344 | ] 345 | 346 | currentx = col 347 | y, maxsize = self.win.getmaxyx() 348 | for part, color in parts: 349 | tempx = currentx + len(part) 350 | oversize = tempx > maxsize - 1 351 | if oversize: 352 | diff = tempx - (maxsize - 1) 353 | part = part[:-diff] 354 | self.win.addstr(row, currentx, part, curses.color_pair(color)) 355 | currentx += len(part) 356 | if oversize: 357 | break 358 | self.items.append((nodestr, row, col + len(expanded) + 1, node)) 359 | 360 | def render_nodes(node): 361 | self.items = [] 362 | depth = [1, 1] 363 | 364 | def wrapped(box): 365 | render_item(box, depth[0], depth[1]) 366 | 367 | if box.isExpanded(): 368 | depth[1] += 1 369 | for child in box.children(): 370 | depth[0] += 1 371 | wrapped(child) 372 | depth[1] -= 1 373 | 374 | for child in node.children(): 375 | wrapped(child) 376 | depth[0] += 1 377 | 378 | size = 30 379 | self.win.clear() 380 | self.win.box() 381 | self.win.addstr(0, 2, self.title, curses.A_BOLD) 382 | root = QgsProject.instance().layerTreeRoot() 383 | render_nodes(root) 384 | self.win.refresh() 385 | 386 | def focus(self): 387 | def move_item(index): 388 | try: 389 | item = self.items[index] 390 | except IndexError: 391 | return 392 | itemrow = item[1] 393 | logging.info("Selected legend item {} at row {}".format(item[0], itemrow)) 394 | self.win.move(itemrow, item[2]) 395 | 396 | modeline.update_activeWindow("Legend") 397 | index = 0 398 | move_item(index) 399 | self.win.nodelay(1) 400 | curses.curs_set(1) 401 | while True: 402 | char = self.win.getch() 403 | if char == -1: 404 | continue 405 | 406 | logging.info(char) 407 | 408 | try_handle_global_event(char) 409 | 410 | if char == curses.KEY_DOWN: 411 | logging.info("Down we go") 412 | index += 1 413 | maxindex = len(self.items) 414 | if index > maxindex: 415 | index = maxindex 416 | move_item(index) 417 | if char == curses.KEY_UP: 418 | logging.info("Up we go") 419 | index -= 1 420 | if index < 0: 421 | index = 0 422 | move_item(index) 423 | if char == 32: 424 | item = self.items[index] 425 | if item[3].isVisible(): 426 | item[3].setVisible(Qt.Unchecked) 427 | else: 428 | item[3].setVisible(Qt.Checked) 429 | mapwindow.render_map() 430 | self.render_legend() 431 | move_item(index) 432 | if char in (curses.KEY_LEFT, curses.KEY_RIGHT): 433 | item = self.items[index] 434 | close = char == curses.KEY_RIGHT 435 | item[3].setExpanded(close) 436 | self.render_legend() 437 | move_item(index) 438 | 439 | 440 | #NOTE: Unused at the moment. Translates color into a pixel code 441 | # def get_pixel_value(pixels, x, y): 442 | # if ascii_mode_enabled: 443 | # color = "MNHQ$OC?7>!:-;. " 444 | # else: 445 | # color = "" * 16 446 | # rgba = QColor(pixels.pixel(x, y)) 447 | # rgb = rgba.red(), rgba.green(), rgba.blue() 448 | # index = int(sum(rgb) / 3.0 / 256.0 * 16) 449 | # pair = curses.color_pair(index + 10) 450 | # if ascii_mode_enabled: 451 | # pair = 1 452 | # 453 | # try: 454 | # return color[index], pair 455 | # except IndexError: 456 | # return " ", pair 457 | 458 | @timeme 459 | def stack(layers, fill=(' ', 0)): 460 | """ 461 | Stack a bunch of arrays and return a single array. 462 | :param layers: 463 | :param fill: 464 | :return: 465 | """ 466 | output_array = [] 467 | for row_stack in zip(*layers): 468 | o_row = [] 469 | for pixel_stack in zip(*row_stack): 470 | opaque_pixels = [_p for _p in pixel_stack if _p[0] != ' '] 471 | if len(opaque_pixels) is 0: 472 | o_row.append(fill) 473 | else: 474 | o_row.append(opaque_pixels[-1]) 475 | output_array.append(o_row) 476 | return output_array 477 | 478 | @timeme 479 | def generate_layers_ascii(setttings, width, height): 480 | root = QgsProject.instance().layerTreeRoot() 481 | layers = [node.layer() for node in root.findLayers() 482 | if node.layer().type() == QgsMapLayer.VectorLayer and node.isVisible()] 483 | 484 | layersdata = [] 485 | for layer in reversed(layers): 486 | colorpair = layercolormapping[layer.id()] 487 | char = codes[layer.geometryType()] 488 | image = render_layer(setttings, layer, width, height) 489 | layerdata = [] 490 | for row in range(1, height - 1): 491 | rowdata = [] 492 | for col in range(1, width - 1): 493 | color = QColor(image.pixel(col, row)) 494 | # All non white is considered a feature. 495 | # Should pull background colour from project file 496 | if not color == QColor(255, 255, 255): 497 | rowdata.append((char, colorpair)) 498 | rowdata.append((char, colorpair)) 499 | else: 500 | rowdata.append((' ', 8)) 501 | rowdata.append((' ', 8)) 502 | layerdata.append(rowdata) 503 | layersdata.append(layerdata) 504 | return stack(layersdata) 505 | 506 | 507 | @timeme 508 | def render_layer(settings, layer, width, height): 509 | settings.setLayers([layer.id()]) 510 | settings.setFlags(settings.flags() ^ QgsMapSettings.Antialiasing) 511 | settings.setOutputSize(QSize(width, height)) 512 | job = QgsMapRendererParallelJob(settings) 513 | job.start() 514 | job.waitForFinished() 515 | image = job.renderedImage() 516 | # image.save(r"/media/nathan/Data/dev/qgis-term/{}.jpg".format(layer.name())) 517 | return image 518 | 519 | 520 | class Map(): 521 | """ 522 | Map window 523 | """ 524 | def __init__(self): 525 | y, x = scr.getmaxyx() 526 | self.mapwin = curses.newwin(y - TOPBORDER, x - 30, BOTTOMBORDER, 30) 527 | self.mapwin.keypad(1) 528 | self.settings = None 529 | self.title = "Map (F6)" 530 | 531 | def render_map(self): 532 | y, x = scr.getmaxyx() 533 | x -= 30 534 | 535 | self.mapwin.clear() 536 | self.mapwin.box() 537 | self.mapwin.addstr(0, 2, self.title, curses.A_BOLD) 538 | 539 | height, width = self.mapwin.getmaxyx() 540 | # Only render the image if we have a open project 541 | if not self.settings and project: 542 | self.settings = project.map_settings 543 | 544 | if project: 545 | settings = self.settings 546 | data = generate_layers_ascii(self.settings, width, height) 547 | for row, rowdata in enumerate(data, start=1): 548 | if row >= height: 549 | break 550 | 551 | for col, celldata in enumerate(rowdata, start=1): 552 | if col >= width - 1: 553 | break 554 | 555 | value, color = celldata[0], celldata[1] 556 | if value == ' ': 557 | color = 8 558 | if not ascii_mode_enabled: 559 | value = ' ' 560 | 561 | if not color_mode_enabled: 562 | color = 0 563 | 564 | self.mapwin.addstr(row, col, value, curses.color_pair(color)) 565 | 566 | self.mapwin.refresh() 567 | 568 | def focus(self): 569 | modeline.update_activeWindow("Map") 570 | curses.curs_set(0) 571 | while True: 572 | event = self.mapwin.getch() 573 | # if event == -1: 574 | # continue 575 | logging.info(event) 576 | try_handle_global_event(event) 577 | 578 | if event == curses.KEY_UP: 579 | self.pan("up") 580 | if event == curses.KEY_DOWN: 581 | self.pan("down") 582 | if event == curses.KEY_LEFT: 583 | self.pan("left") 584 | if event == curses.KEY_RIGHT: 585 | self.pan("right") 586 | if event == curses.KEY_NPAGE: 587 | self.zoom_out(5) 588 | if event == curses.KEY_PPAGE: 589 | self.zoom_in(5) 590 | 591 | def zoom_out(self, factor): 592 | if not self.settings: 593 | return 594 | extent = self.settings.extent() 595 | extent.scale(float(factor), None) 596 | self.settings.setExtent(extent) 597 | self.render_map() 598 | 599 | def zoom_in(self, factor): 600 | if not self.settings: 601 | return 602 | extent = self.settings.extent() 603 | extent.scale(1 / float(factor), None) 604 | self.settings.setExtent(extent) 605 | self.render_map() 606 | 607 | def pan(self, direction): 608 | if not self.settings: 609 | return 610 | 611 | def setCenter(point): 612 | x, y = point.x(), point.y() 613 | rect = QgsRectangle(x - extent.width() / 2.0, y - extent.height() / 2.0, 614 | x + extent.width() / 2.0, y + extent.height() / 2.0) 615 | self.settings.setExtent(rect) 616 | self.render_map() 617 | 618 | extent = self.settings.visibleExtent() 619 | dx = abs(extent.width() / 4) 620 | dy = abs(extent.height() / 4) 621 | extent = self.settings.extent() 622 | center = extent.center() 623 | 624 | if direction == "up": 625 | newpoint = QgsPoint(center.x() + 0, center.y() + dy) 626 | setCenter(newpoint) 627 | if direction == "down": 628 | newpoint = QgsPoint(center.x() - 0, center.y() - dy) 629 | setCenter(newpoint) 630 | if direction == "left": 631 | newpoint = QgsPoint(center.x() - dx, center.y() - 0) 632 | setCenter(newpoint) 633 | if direction == "right": 634 | newpoint = QgsPoint(center.x() + dx, center.y() + 0) 635 | setCenter(newpoint) 636 | 637 | class ModeLine(): 638 | def __init__(self): 639 | y, x = scr.getmaxyx() 640 | self.modeline = curses.newwin(1, x, y - 1, 0) 641 | self.modeline.bkgd(curses.color_pair(6)) 642 | self.modeline.refresh() 643 | 644 | def update_activeWindow(self, name): 645 | self.modeline.erase() 646 | self.modeline.addstr(0, 0, "Window: {}".format(name)) 647 | self.modeline.refresh() 648 | 649 | 650 | class EditPad(): 651 | def __init__(self): 652 | y, x = scr.getmaxyx() 653 | self.edit = curses.newwin(1, x, y - 2, 0) 654 | self.status = curses.newwin(1, x, y - 3, 0) 655 | self.pad = Textbox(self.edit, insert_mode=True) 656 | self.lastcmd = [] 657 | 658 | def update_cmd_status(self, message, color=None): 659 | if not color: 660 | color = curses.color_pair(1) 661 | self.status.clear() 662 | try: 663 | self.status.addstr(0, 0, message, color) 664 | self.status.refresh() 665 | except: 666 | pass 667 | 668 | def focus(self): 669 | modeline.update_activeWindow("Command Entry") 670 | self.edit.erase() 671 | entercommandstr = "Enter command. TAB for auto complete. (command-list for command help or ? for general help)" 672 | pad.update_cmd_status(entercommandstr) 673 | 674 | curses.curs_set(1) 675 | while True: 676 | message = self.pad.edit(validate=self.handle_key_event).strip() 677 | try: 678 | cmd = commands[message] 679 | except KeyError: 680 | self.update_cmd_status("Unknown command: {}".format(message), colors['red']) 681 | continue 682 | 683 | if message not in self.lastcmd: 684 | self.lastcmd.append(message) 685 | 686 | func = cmd() 687 | if not func: 688 | self.update_cmd_status(entercommandstr) 689 | self.edit.clear() 690 | self.edit.refresh() 691 | continue 692 | 693 | try: 694 | qanda = func.send(None) 695 | while True: 696 | self.edit.clear() 697 | self.update_cmd_status(qanda.question, color=curses.color_pair(qanda.type)) 698 | message = self.pad.edit(validate=self.handle_key_event).strip() 699 | qanda = func.send(message) 700 | except StopIteration: 701 | pass 702 | 703 | self.update_cmd_status(entercommandstr) 704 | self.edit.erase() 705 | 706 | def clear(self): 707 | self.edit.erase() 708 | 709 | def handle_key_event(self, event): 710 | """ 711 | Handle edit pad key events 712 | :param event: 713 | :return: 714 | """ 715 | logging.info("Key Event:{}".format(event)) 716 | if event == curses.KEY_UP: 717 | try: 718 | cmd = self.lastcmd[0] 719 | except IndexError: 720 | return event 721 | 722 | self.edit.clear() 723 | self.edit.addstr(0, 0, cmd) 724 | self.edit.refresh() 725 | 726 | try_handle_global_event(event) 727 | 728 | if event == 9: 729 | logging.info("Calling auto complete on TAB key") 730 | data = self.pad.gather().strip() 731 | cmds = {key[:len(data)]: key for key in commands.keys()} 732 | logging.info("Options are") 733 | for cmd, fullname in cmds.iteritems(): 734 | if cmd == data: 735 | logging.info("Grabbed the first match which was {}".format(fullname)) 736 | self.edit.clear() 737 | self.edit.addstr(0, 0, fullname) 738 | self.edit.refresh() 739 | break 740 | return event 741 | 742 | 743 | def try_handle_global_event(event): 744 | if event == curses.KEY_F5: 745 | legendwindow.focus() 746 | if event == curses.KEY_F6: 747 | mapwindow.focus() 748 | if event == curses.KEY_F7: 749 | pad.focus() 750 | 751 | 752 | def init_colors(): 753 | """ 754 | Init the colors for the screen 755 | """ 756 | curses.use_default_colors() 757 | 758 | # Colors we use for messages, etc 759 | curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) 760 | curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) 761 | curses.init_pair(3, curses.COLOR_CYAN, curses.COLOR_BLACK) 762 | curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK) 763 | curses.init_pair(5, curses.COLOR_GREEN, curses.COLOR_BLACK) 764 | curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_WHITE) 765 | curses.init_pair(7, curses.COLOR_RED, curses.COLOR_BLACK) 766 | curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_WHITE) 767 | colors['white'] = curses.color_pair(1) 768 | colors['green'] = curses.color_pair(2) 769 | colors['cyan'] = curses.color_pair(3) 770 | colors['yellow'] = curses.color_pair(4) 771 | colors['green-black'] = curses.color_pair(5) 772 | colors['black-white'] = curses.color_pair(6) 773 | colors['red'] = curses.color_pair(7) 774 | colors['white-white'] = curses.color_pair(8) 775 | 776 | # Allocate colour ranges here for the ma display. 777 | maprange = 10 778 | for i in range(curses.COLORS - maprange): 779 | curses.init_pair(i + maprange, 0, i) 780 | 781 | 782 | def main(screen): 783 | """ 784 | Main entry point 785 | :param screen: 786 | :return: 787 | """ 788 | logging.info("Supports color: {}".format(curses.can_change_color())) 789 | logging.info("Colors: {}".format(curses.COLORS)) 790 | logging.info("Color Pairs: {}".format(curses.COLOR_PAIRS)) 791 | logging.info("Loading config") 792 | with open("ascii_qgis.config") as f: 793 | global config 794 | config = json.load(f) 795 | 796 | 797 | init_colors() 798 | 799 | screen.refresh() 800 | 801 | global scr, pad, aboutwindow, legendwindow, mapwindow, modeline 802 | scr = screen 803 | pad = EditPad() 804 | modeline = ModeLine() 805 | mapwindow = Map() 806 | legendwindow = Legend() 807 | aboutwindow = AboutWindow() 808 | 809 | legendwindow.render_legend() 810 | mapwindow.render_map() 811 | 812 | screen.addstr(0, 0, "ASCII") 813 | screen.addstr(0, 5, " QGIS Enterprise", curses.color_pair(4)) 814 | screen.refresh() 815 | 816 | if config.get('showhelp', True): 817 | show_help() 818 | 819 | pad.focus() 820 | 821 | 822 | app = QGIS.init(guienabled=False) 823 | 824 | if __name__ == "__main__": 825 | logging.info("Staring QGIS ASCII :)") 826 | logging.info("ASCII QGIS because we can") 827 | curses.wrapper(main) 828 | -------------------------------------------------------------------------------- /parfait/QGIS.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from qgis.core import QgsApplication 3 | 4 | 5 | def init(args=None, guienabled=True, configpath=None, sysexit=True): 6 | """ 7 | Create a new QGIS Qt application. 8 | 9 | You should use this before creating any Qt widgets or QGIS objects for 10 | your custom QGIS based application. 11 | 12 | usage: 13 | from wrappers import QGIS 14 | 15 | QGIS.init() 16 | 17 | args - args passed to the underlying QApplication. 18 | guienabled - True by default will create a QApplication with a GUI. Pass 19 | False if you wish to create no GUI based app, e.g a server app. 20 | configpath - Custom config path QGIS will use to load settings. 21 | sysexit - Call sys.exit on app exit. True by default. 22 | """ 23 | if not args: 24 | args = [] 25 | if not configpath: 26 | configpath = '' 27 | app = QgsApplication(args, guienabled, configpath) 28 | QgsApplication.initQgis() 29 | return app 30 | -------------------------------------------------------------------------------- /parfait/__init__.py: -------------------------------------------------------------------------------- 1 | from printing import render_template 2 | from layer_wrappers import map_layers, load_vector, add_layer 3 | from projects import open_project 4 | from qgis.core.contextmanagers import qgisapp 5 | import QGIS 6 | 7 | -------------------------------------------------------------------------------- /parfait/layer_wrappers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from qgis.core import QgsMapLayerRegistry, QgsVectorLayer 4 | 5 | _layerreg = QgsMapLayerRegistry.instance() 6 | 7 | 8 | def map_layers(name=None, type=None): 9 | """ 10 | Return all the loaded layers. Filters by name (optional) first and then type (optional) 11 | :param name: (optional) name of layer to return.. 12 | :param type: (optional) The QgsMapLayer type of layer to return. 13 | :return: List of loaded layers. If name given will return all layers with matching name. 14 | """ 15 | layers = _layerreg.mapLayers().values() 16 | _layers = [] 17 | if name or type: 18 | if name: 19 | _layers = [layer for layer in layers if re.match(name, layer.name())] 20 | if type: 21 | _layers += [layer for layer in layers if layer.type() == type] 22 | return _layers 23 | else: 24 | return layers 25 | 26 | 27 | def add_layer(layer, load_in_legend=True): 28 | """ 29 | Add a open layer to the QGIS session and layer registry. 30 | :param layer: The layer object to add the QGIS layer registry and session. 31 | :param load_in_legend: True if this layer should be added to the legend. 32 | :return: The added layer 33 | """ 34 | if not hasattr(layer, "__iter__"): 35 | layer = [layer] 36 | QgsMapLayerRegistry.instance().addMapLayers(layer, load_in_legend) 37 | return layer 38 | 39 | 40 | def load_vector(path, name=None, provider="ogr"): 41 | """ 42 | Load a vector layer and return the QgsVectorLayer instance. 43 | :param path: Path to the vector layer. 44 | :param name: The name of the new layer. 45 | :param provider: The provider to open this layer with defaults to ogr. 46 | :return: A QgsVectorLayer instance for the layer. 47 | """ 48 | if not name: 49 | name = os.path.basename(path) 50 | layer = QgsVectorLayer(path, name, provider) 51 | return layer 52 | 53 | -------------------------------------------------------------------------------- /parfait/printing.py: -------------------------------------------------------------------------------- 1 | from PyQt4.QtXml import QDomDocument 2 | from qgis.core import QgsComposition 3 | 4 | 5 | class Composer(): 6 | pass 7 | 8 | class ComposerTemplate(): 9 | """ 10 | Contains methods for working with loading/saving QGIS composer template files 11 | """ 12 | def __init__(self, composition): 13 | self.composition = composition 14 | 15 | @classmethod 16 | def from_file(cls, template_path, mapsettings, data=None): 17 | """ 18 | Create a template object from the given template path, map setttings, and data. 19 | :param template_path: The template path. 20 | :param mapsettings: QgsMapSettings used to render the map. 21 | :param data: A dict of data to use for labels. 22 | :return: A ComposerTemplate obect which wraps the created template. 23 | """ 24 | if not data: 25 | data = {} 26 | 27 | with open(template_path) as f: 28 | template_content = f.read() 29 | 30 | document = QDomDocument() 31 | document.setContent(template_content) 32 | composition = QgsComposition(mapsettings) 33 | composition.loadFromTemplate(document, data) 34 | return cls(composition) 35 | 36 | def __getitem__(self, item): 37 | """ 38 | Return the composer item with the given name. 39 | 40 | The same as doing getComposerItemById(item) 41 | """ 42 | return self.composition.getComposerItemById(item) 43 | 44 | def export(self, outpath): 45 | self.composition.refreshItems() 46 | self.composition.exportAsPDF(outpath) 47 | 48 | 49 | def render_template(template_path, settings, canvas, outpath, data=None): 50 | """ 51 | Render the template at the given path using the settings and the export to the output path. 52 | 53 | Template assumes there is a map and legend objet in the template. 54 | :param template_path: The path to the template. 55 | :param settings: The QgsMapSettings used to render the template 56 | :param canvas: QgsMapCanvas item used to render the template. (This is gross but needed for now) 57 | :param outpath: The output path. Current only exports to PDF. 58 | :param data: A dict of data to use for labels. 59 | :return: 60 | """ 61 | template = ComposerTemplate.from_file(template_path, canvas.mapSettings(), data) 62 | 63 | map_item = template['map'] 64 | map_item.setMapCanvas(canvas) 65 | map_item.zoomToExtent(settings.extent()) 66 | 67 | legend_item = template['legend'] 68 | legend_item.updateLegend() 69 | template.export(outpath) 70 | -------------------------------------------------------------------------------- /parfait/projects.py: -------------------------------------------------------------------------------- 1 | import os 2 | from parfait.layer_wrappers import map_layers 3 | from qgis.core.contextmanagers import qgisapp 4 | from qgis.core import QgsProject, QgsMapLayerRegistry, QgsMapSettings, QgsComposition 5 | from qgis.gui import QgsMapCanvas, QgsLayerTreeMapCanvasBridge 6 | from PyQt4.QtCore import QFileInfo, QDir 7 | from PyQt4.QtXml import QDomDocument 8 | 9 | 10 | def composers(projectfile, mapsettings): 11 | with open(projectfile) as f: 12 | xml = f.read() 13 | 14 | doc = QDomDocument() 15 | doc.setContent(xml) 16 | nodes = doc.elementsByTagName("Composer") 17 | for nodeid in range(nodes.count()): 18 | node = nodes.at(0).toElement() 19 | name = node.attribute("title") 20 | compositionnodes = doc.elementsByTagName("Composition") 21 | if compositionnodes.count() == 0: 22 | continue 23 | 24 | compositionelm = compositionnodes.at(0).toElement() 25 | comp = QgsComposition(mapsettings) 26 | comp.readXML(compositionelm, doc) 27 | atlaselm = node.firstChildElement("Atlas") 28 | comp.atlasComposition().readXML(atlaselm, doc) 29 | comp.addItemsFromXML(node, doc) 30 | comp.refreshZList() 31 | yield name, comp 32 | 33 | 34 | 35 | class Project(object): 36 | """ 37 | A wrapper for handling project based logic. 38 | note: This class really just talks to the QgsProject.instance() object. QGIS can still 39 | only open and load a single project at a time. QgsProject is still a bad singleton object. 40 | """ 41 | def __init__(self, bridge=None): 42 | self.bridge = bridge 43 | 44 | def __enter__(self): 45 | return self 46 | 47 | @classmethod 48 | def from_file(cls, filename, canvas, relative_base=None): 49 | """ 50 | Load a project file from a path. 51 | :param filename: The path to the project file. 52 | :param canvas: (optional) Passing a canvas will auto add layers to the canvas when the load is 53 | loaded. 54 | :param relative_base_path: (optional) Relative base path for the project file to load layers from 55 | :return: A Project object which wraps QgsProject.instance() 56 | """ 57 | QgsProject.instance().clear() 58 | bridge = None 59 | if canvas: 60 | bridge = QgsLayerTreeMapCanvasBridge(QgsProject.instance().layerTreeRoot(), canvas) 61 | if relative_base is None: 62 | relative_base = os.path.dirname(filename) 63 | QDir.setCurrent(relative_base) 64 | QgsProject.instance().read(QFileInfo(filename)) 65 | if bridge: 66 | bridge.setCanvasLayers() 67 | return cls(bridge) 68 | 69 | def __exit__(self, exc_type, exc_val, exc_tb): 70 | self.close() 71 | 72 | def close(self): 73 | """ 74 | Close the current project. 75 | """ 76 | QgsProject.instance().clear() 77 | QgsMapLayerRegistry.instance().removeAllMapLayers() 78 | if self.bridge: 79 | self.bridge.clear() 80 | 81 | @property 82 | def map_settings(self): 83 | """ 84 | Return the settings that have been set for the map canvas. 85 | @return: A QgsMapSettings instance with the settings read from the project. 86 | """ 87 | xml = open(QgsProject.instance().fileName()).read() 88 | doc = QDomDocument() 89 | doc.setContent(xml) 90 | canvasnodes = doc.elementsByTagName("mapcanvas") 91 | node = canvasnodes.at(0).toElement() 92 | settings = QgsMapSettings() 93 | settings.readXML(node) 94 | return settings 95 | 96 | def composers(self): 97 | for composer in composers(QgsProject.instance().fileName(), self.map_settings): 98 | yield composer 99 | 100 | 101 | def open_project(projectfile, canvas=None, relative_base_path=None): 102 | """ 103 | Open a QGIS project file 104 | :param projectfile: The path to the project file to load. 105 | :param canvas: (optional) Canvas object. 106 | :param relative_base_path: (optional) Relative base path for the project file to load layers from 107 | :return: A Project object wrapper with handy functions for doing project related stuff. 108 | """ 109 | return Project.from_file(projectfile, canvas, relative_base_path) 110 | 111 | -------------------------------------------------------------------------------- /parfait/qt.py: -------------------------------------------------------------------------------- 1 | import os 2 | import inspect 3 | 4 | from PyQt4 import uic 5 | 6 | def load_ui(name): 7 | if os.path.exists(name): 8 | uifile = name 9 | else: 10 | frame = inspect.stack()[1] 11 | filename = inspect.getfile(frame[0]) 12 | uifile = os.path.join(os.path.dirname(filename), name) 13 | if not os.path.exists(uifile): 14 | uifile = os.path.join(os.path.dirname(filename), "ui", name) 15 | 16 | widget, base = uic.loadUiType(uifile) 17 | return widget, base 18 | -------------------------------------------------------------------------------- /parfait/utils.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanW2/ascii_qgis/9e2fe7e826cf9c97247702f8f770a405fe9b2b07/parfait/utils.py --------------------------------------------------------------------------------