Last active
June 26, 2021 22:24
-
-
Save JeanRibes/fef879b6f71023e15e00b7ba5a5fa607 to your computer and use it in GitHub Desktop.
Conway's Game of Life as a QGIS layer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Conway's Game of Life implemented in QGIS vectorlayer | |
How to use: run in terminal: | |
qgis --code qgis_conway.py | |
""" | |
from random import randint, choice, random | |
from threading import Lock | |
import qgis | |
from PyQt5.QtCore import QTimer | |
from PyQt5.QtGui import QColor | |
from PyQt5.QtWidgets import QAction | |
from qgis.core import QgsPolygon, QgsProject, QgsFeature, QgsVectorLayer | |
from qgis.utils import iface | |
GLIDER = [ | |
(0, 0), | |
(0, 1), | |
(0, 2), | |
(1, 2), | |
(2, 1), | |
] | |
COMPANION_CUBE = [ | |
(0, 2), (0, 3), (0, 4), (0, 8), (0, 9), (0, 10), | |
(2, 0), (2, 5), (2, 7), (2, 12), | |
(3, 0), (3, 5), (3, 7), (3, 12), | |
(4, 0), (4, 5), (4, 7), (4, 12), | |
(5, 2), (5, 3), (5, 4), (5, 8), (5, 9), (5, 10), | |
(7, 2), (7, 3), (7, 4), (7, 8), (7, 9), (7, 10), | |
(6 + 2, 0), (6 + 2, 5), (6 + 2, 7), (6 + 2, 12), | |
(6 + 3, 0), (6 + 3, 5), (6 + 3, 7), (6 + 3, 12), | |
(6 + 4, 0), (6 + 4, 5), (6 + 4, 7), (6 + 4, 12), | |
(13, 2), (13, 3), (13, 4), (13, 8), (13, 9), (13, 10), | |
] | |
F = [ | |
(0, 1), (0, 2), | |
(1, 0), (1, 1), | |
(2, 1) | |
] | |
ACORN = [ | |
(0, 1), | |
(1, 3), | |
(2, 0), (2, 1), (2, 4), (2, 5), (2, 6) | |
] | |
SHAPES = [ACORN, GLIDER, F] | |
class Conway: | |
def __init__(self, size): | |
self.size = size | |
self.cells = [[False for _ in range(size)] for _ in range(size)] | |
def add_shape(self, xoff, yoff, shape): | |
for x, y in shape: | |
self((y + yoff) % self.size, (x + xoff) % self.size) | |
def shuffle(self, num): | |
for _ in range(num): | |
row = randint(0, self.size - 1) | |
col = randint(0, self.size - 1) | |
self.add_shape(row, col, choice(SHAPES)) | |
def step(self): | |
next_cells = [[False for _ in range(self.size)] for _ in range(self.size)] | |
changes = [] | |
for ir, row in enumerate(self.cells): | |
for ic, col in enumerate(row): | |
alive = col | |
new_state = self.action(ir, ic, alive) | |
next_cells[ir][ic] = new_state | |
if new_state != alive: | |
changes.append((ir, ic, new_state)) | |
self.cells = next_cells | |
if random() > 0.98: | |
self.shuffle(4) | |
return changes | |
def action(self, row, col, was_alive): | |
around = [] | |
for drow, dcol in [ | |
(-1, 1), | |
(0, 1), | |
(1, 1), | |
(-1, 0), # | |
# (0,0), | |
(1, 0), # | |
(-1, -1), | |
(0, -1), | |
(1, -1), | |
]: | |
try: | |
around.append(self.cells[row + drow][col + dcol]) | |
except IndexError: | |
pass | |
alive_around = around.count(True) | |
if alive_around == 3: | |
return True | |
elif alive_around < 2 or alive_around > 3: | |
return False | |
else: | |
return was_alive | |
def print(self): | |
print('-' * self.size * 3) | |
for row in self.cells: | |
for cell in row: | |
print('|x' if cell else '| ', end='') | |
print('|') | |
print('-' * self.size * 3) | |
def to_polycoords(self, x0, y0, w, h): | |
polygons = [] | |
for ir, row in enumerate(self.cells): | |
for ic, cell in enumerate(row): | |
if cell: | |
polygons.append( | |
self.cell_to_polycoord(ir, ic, x0, y0, w, h)) | |
return polygons | |
def cell_to_polycoord(self, ir, ic, x0, y0, w, h): | |
return ( | |
(x0 + ir * w, y0 + ic * h), | |
(x0 + (ir + 1) * w, y0 + ic * h), | |
(x0 + (ir + 1) * w, y0 + (ic + 1) * h), | |
(x0 + ir * w, y0 + (ic + 1) * h), | |
) | |
def cell_wkt(self, poly, *args): | |
coords = ', '.join(f"{x} {y}" for x, y in poly) | |
return f"POLYGON (({coords}))" | |
def to_wkt(self, *args): | |
for poly in self.to_polycoords(*args): | |
yield self.cell_wkt(poly, *args) | |
def __call__(self, row: int, col: int): | |
self.cells[row][col] = True | |
def checkerboard(self): | |
for i in range(self.size): | |
for j in range(self.size): | |
if (j + i) % 2: | |
q(i, j) | |
class QgsConway(Conway): | |
layer: QgsVectorLayer | |
def __init__(self, size, x0=-250000, y0=5200000, cellwidth=20000, cellheight=20000, layer_name='conway'): | |
self.x0 = x0 | |
self.y0 = y0 | |
self.cellwidth = cellwidth | |
self.cellheight = cellheight | |
super().__init__(size) | |
self.layer = QgsProject.instance().mapLayersByName(layer_name)[0] | |
self.lock = Lock() | |
self.feats = [[None for _ in range(size)] for _ in range(size)] | |
def cell_to_feat(self, ir, ic): | |
poly = QgsPolygon() | |
poly.fromWkt( | |
wkt=self.cell_wkt(self.cell_to_polycoord(ir, ic, self.x0, self.y0, self.cellwidth, self.cellheight))) | |
feat = QgsFeature() | |
feat.setGeometry(poly) | |
feat.setAttributes([ir, ic]) | |
# feat.setAttribute('row',ir) | |
# feat.setAttribute('col',ic) | |
self.feats[ir][ic] = feat.id() | |
return feat | |
def to_polygons(self): | |
l = [] | |
for ir, row in enumerate(self.cells): | |
for ic, cell in enumerate(row): | |
if cell: | |
l.append(self.cell_to_feat(ir, ic)) | |
return l | |
def show(self): | |
self.layer.dataProvider().truncate() | |
self.layer.startEditing() | |
self.layer.addFeatures(self.to_polygons()) | |
self.layer.commitChanges() | |
def one(self): | |
with self.lock: | |
changes = self.step() | |
self.layer.startEditing() | |
deads = [] | |
for ir, ic, alive in changes: | |
if alive: | |
self.layer.addFeature(self.cell_to_feat(ir, ic)) | |
else: | |
deads.append([ir, ic]) | |
# self.layer.commitChanges() | |
# self.layer.startEditing() | |
for feat in self.layer.getFeatures(): | |
attr = feat.attributes() | |
if attr in deads: | |
self.layer.deleteFeature(feat.id()) | |
self.feats[ir][ic] = None | |
self.layer.commitChanges() | |
def more(self): | |
with self.lock: | |
self.shuffle(1) | |
self.show() | |
if __name__ == '__main__': | |
iface.addVectorLayer('/usr/share/qgis/resources/data/world_map.gpkg|layername=countries', "world", 'ogr') | |
layer = QgsVectorLayer("Polygon?crs=epsg:3857&field=row:integer&field=col:integer", "conway", 'memory') | |
QgsProject.instance().addMapLayer(layer) | |
layer.renderer().symbol().setColor(QColor("black")) | |
layer.renderer().symbol().symbolLayer(0).setStrokeStyle(0) | |
global q | |
q = QgsConway(30, 0, 5750000, 20000, 20000) | |
q.checkerboard() | |
q.shuffle(3) | |
q.show() | |
act = QAction("step") | |
act.triggered.connect(q.one) | |
tb = iface.mainWindow().addToolBar("conway") | |
tb.addAction(act) | |
more = QAction("+") | |
more.triggered.connect(q.more) | |
tb.addAction(more) | |
cla = QAction("X") | |
timer = QTimer() | |
timer.timeout.connect(q.one) | |
timer.start(200) | |
q.timer = timer | |
stop = QAction("stop") | |
stop.triggered.connect(timer.stop) | |
tb.addAction(stop) | |
start = QAction("start") | |
start.triggered.connect(lambda: timer.start(200)) | |
tb.addAction(start) | |
def clean(): | |
act.deleteLater() | |
cla.deleteLater() | |
more.deleteLater() | |
stop.deleteLater() | |
start.deleteLater() | |
layer.deleteLater() | |
tb.deleteLater() | |
timer.stop() | |
timer.deleteLater() | |
q.clean = None | |
q.timer = None | |
q.tb = None | |
del qgis.utils.plugins['conway'] | |
cla.triggered.connect(clean) | |
tb.addAction(cla) | |
q.tb = tb | |
q.clean = clean | |
qgis.utils.plugins['conway'] = q |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment