Browse Source

svg subpackage restructured, some bbox stuff mayhaps?

master
phryk 8 months ago
parent
commit
0c6bd39562

+ 8
- 0
fill.py View File

@@ -119,6 +119,10 @@ def map():
119 119
     m.name = 'grid'
120 120
     m.title = 'Grid'
121 121
     m.description = 'Markers every 30 degrees. Automatically created from fill.py.'
122
+    m.bbox_left = -180
123
+    m.bbox_right = 180
124
+    m.bbox_top = 75
125
+    m.bbox_bottom = -75
122 126
     m.save()
123 127
 
124 128
     for lat in range(-75, 76, 15):
@@ -140,6 +144,10 @@ def map():
140 144
     places.name = 'places'
141 145
     places.title = 'Some Places'
142 146
     places.description = 'Sample MapDataset from fill.py'
147
+    places.bbox_left = -180
148
+    places.bbox_right = 180
149
+    places.bbox_top = 75
150
+    places.bbox_bottom = -75
143 151
     places.save()
144 152
 
145 153
     dp = example.poobrains.svg.MapDatapoint()

+ 9
- 646
poobrains/svg/__init__.py View File

@@ -1,657 +1,20 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 
3
-import math
4 3
 import os
5
-import collections
6
-import json
7 4
 
8
-import geojson
5
+from poobrains import app
9 6
 
10
-from pyproj import Proj # map projection transformation shite
7
+from . import base
8
+from . import plot
9
+from . import geo
11 10
 
12
-from poobrains import Response, Markup, app, abort, flash, g, locked_cached_property
13
-import poobrains.helpers
14
-import poobrains.storage
15
-import poobrains.auth
16
-import poobrains.tagging
17
-import poobrains.commenting
11
+SVG = base.SVG
18 12
 
19
-def normalize_coords(longitude, latitude):
20
-
21
-    #projection = Proj(init='epsg:4326') # WGS84 according to epsg.io, but results in equidistant latitudes
22
-    projection = Proj(init='epsg:3857') # WTS84, as used by OSM etc.
23
-    x_rad, y_rad = projection(longitude, latitude)
24
-
25
-    x = (x_rad / (math.pi * 2)) + 0.5
26
-    y = 1 - ((y_rad / math.pi) + 0.5)
27
-
28
-    return x, y
29
-
30
-
31
-def normalize_longitude(longitude):
32
-
33
-    # normalize longitude to linear scale (0-1)
34
-
35
-    normalization_factor = 20037508.3428
36
-
37
-    r_major=6378137.000
38
-    x = r_major*math.radians(longitude)
39
-    #return 50 + 50 * (x / normalization_factor)
40
-    
41
-    return 0.5 + 0.5 * (x / normalization_factor)
42
-
43
-
44
-def normalize_latitude(latitude):
45
-
46
-    #normalization_factor = 19994838.114 # this is the value this function would return for 85.0511° without normalization, which should™ make the map square
47
-    normalization_factor = 12890914.1373 # this is the value this function would return for 75° without normalization, which should™ make the map square
48
-    if latitude>89.5:latitude=89.5
49
-    if latitude<-89.5:latitude=-89.5
50
-    r_major=6378137.000
51
-    r_minor=6356752.3142518
52
-    temp=r_minor/r_major
53
-    eccent=math.sqrt(1-temp**2)
54
-    phi=math.radians(latitude)
55
-    sinphi=math.sin(phi)
56
-    con=eccent*sinphi
57
-    com=eccent/2
58
-    con=((1.0-con)/(1.0+con))**com
59
-    ts=math.tan((math.pi/2-phi)/2)/con
60
-    y=0-r_major*math.log(ts)
61
-
62
-    return 0.5 - 0.5 * (y / normalization_factor)
63
-
64
-
65
-class SVG(poobrains.auth.Protected):
66
-    
67
-    handle = None # needed so that app.expose registers a route with extra param, this is kinda hacky…
68
-    
69
-    class Meta:
70
-
71
-        modes = collections.OrderedDict([
72
-            ('teaser', 'read'),
73
-            ('full', 'read'),
74
-            ('raw', 'read'),
75
-            ('inline', 'read')
76
-        ])
77
-    
78
-    style = None
79
-
80
-    def __init__(self, handle=None, mode=None, **kwargs):
81
-
82
-        super(SVG, self).__init__(**kwargs)
83
-
84
-        self.handle = handle
85
-        self.style = Markup(app.scss_compiler.compile_string("@import 'svg';"))
86
-    
87
-    
88
-    def templates(self, mode=None):
89
-
90
-        r = super(SVG, self).templates(mode=mode)
91
-        return ["svg/%s" % template for template in r]
92
-
93
-    
94
-    def instance_url(self, mode='full', quiet=False, **url_params):
95
-    
96
-        url_params['handle'] = self.handle
97
-
98
-        return super(SVG, self).instance_url(mode=mode, quiet=quiet, **url_params) 
99
-
100
-
101
-    @poobrains.helpers.themed
102
-    def view(self, mode=None, handle=None):
103
-
104
-        if mode == 'raw':
105
-            
106
-            response = Response(self.render('raw'))
107
-            response.headers['Content-Type'] = u'image/svg+xml'
108
-            response.headers['Content-Disposition'] = u'filename="%s.svg"' % self.__class__.__name__
109
-            
110
-            # Disable "public" mode caching downstream (nginx, varnish) in order to hopefully not leak restricted content
111
-            response.cache_control.public = False
112
-            response.cache_control.private = True
113
-            response.cache_control.max_age = app.config['CACHE_LONG']
114
-
115
-            return response
116
-        
117
-        else:
118
-            return poobrains.helpers.ThemedPassthrough(super(SVG, self).view(mode=mode, handle=handle))
119
-
120
-
121
-class Dataset(poobrains.commenting.Commentable):
122
-
123
-
124
-    title = poobrains.storage.fields.CharField()
125
-    description = poobrains.md.MarkdownField(null=True)
126
-    label_x = poobrains.storage.fields.CharField(verbose_name="Label for the x-axis")
127
-    label_y = poobrains.storage.fields.CharField(verbose_name="Label for the y-axis")
128
-    #grid_step_x = poobrains.storage.fields.DoubleField(default=1.0)
129
-    #grid_step_y = poobrains.storage.fields.DoubleField(default=1.0)
130
-
131
-    @property
132
-    def ref_id(self):
133
-        return "dataset-%s" % self.name
134
-
135
-
136
-    @locked_cached_property
137
-    def authorized_datapoints(self):
138
-        return Datapoint.list('read', g.user).where(Datapoint.dataset == self)
139
-
140
-    
141
-    def datapoint_id(self, datapoint):
142
-        return "dataset-%s-%s" % (self.name, datapoint.x)
143
-
144
-    
145
-    def plot(self):
146
-
147
-        return Plot(datasets=[self]).render('inline')
148
-
149
-
150
-class DatapointFieldset(poobrains.form.Fieldset):
151
-
152
-    def __init__(self, datapoint, **kwargs):
153
-
154
-        super(DatapointFieldset, self).__init__(**kwargs)
155
-
156
-        self.datapoint = datapoint
157
-        self.x = poobrains.form.fields.Text(type=poobrains.form.types.FLOAT, value=self.datapoint.x, placeholder=Datapoint.x.verbose_name, help_text=Datapoint.x.help_text)
158
-        self.y = poobrains.form.fields.Text(type=poobrains.form.types.FLOAT, value=self.datapoint.y, placeholder=Datapoint.y.verbose_name, help_text=Datapoint.y.help_text)
159
-        self.error_lower = poobrains.form.fields.Text(type=poobrains.form.types.FLOAT, value=self.datapoint.error_lower, placeholder=Datapoint.error_lower.verbose_name, help_text=Datapoint.error_lower.help_text)
160
-        self.error_upper = poobrains.form.fields.Text(type=poobrains.form.types.FLOAT, value=self.datapoint.error_upper, placeholder=Datapoint.error_upper.verbose_name, help_text=Datapoint.error_upper.help_text)
161
-
162
-
163
-    def process(self, submit, dataset):
164
-
165
-        if self.datapoint in dataset.datapoints:
166
-            pass # florp
167
-        else:
168
-            self.datapoint.dataset = dataset
169
-            self.datapoint.x = self.fields['x'].value
170
-            self.datapoint.y = self.fields['y'].value
171
-            self.datapoint.error_lower = self.fields['error_lower'].value
172
-            self.datapoint.error_upper = self.fields['error_upper'].value
173
-
174
-            self.datapoint.save(force_insert=True)
175
-
176
-
177
-class Datapoint(poobrains.auth.Owned):
178
-
179
-    class Meta:
180
-        order_by = ['dataset', 'x']
181
-        primary_key = poobrains.storage.CompositeKey('dataset', 'x')
182
-        related_use_form = True
183
-
184
-    dataset = poobrains.storage.fields.ForeignKeyField(Dataset, related_name='datapoints')
185
-    x = poobrains.storage.fields.DoubleField()
186
-    y = poobrains.storage.fields.DoubleField()
187
-    error_lower = poobrains.storage.fields.FloatField(help_text="Lower margin of error", default=0.0)
188
-    error_upper = poobrains.storage.fields.FloatField(help_text="Upper margin of error", default=0.0)
189
-
190
-
191
-@app.expose('/svg/plot')
192
-class Plot(SVG):
193
-
194
-    padding = None
195
-    width = None
196
-    height = None
197
-    inner_width = None
198
-    inner_height = None
199
-    plot_width = None
200
-    plot_height = None
201
-    description_height = None
202
-
203
-    datasets = None
204
-    min_x = None
205
-    max_x = None
206
-    min_y = None
207
-    max_y = None
208
-    span_x = None
209
-    span_y = None
210
-        
211
-
212
-    class Meta:
213
-
214
-        modes = collections.OrderedDict([
215
-            ('teaser', 'read'),
216
-            ('full', 'read'),
217
-            ('raw', 'read'),
218
-            ('json', 'read'),
219
-            ('inline', 'read')
220
-        ])
221
-
222
-    def __init__(self, handle=None, mode=None, datasets=None, **kwargs):
223
-
224
-        super(Plot, self).__init__(handle=handle, mode=mode, **kwargs)
225
-
226
-        if handle is None and datasets is None:
227
-            abort(404, "No datasets selected")
228
-        
229
-        self.padding = app.config['SVG_PLOT_PADDING']
230
-        self.plot_width = app.config['SVG_PLOT_WIDTH']
231
-        self.plot_height = app.config['SVG_PLOT_HEIGHT']
232
-        self.description_height = app.config['SVG_PLOT_DESCRIPTION_HEIGHT']
233
-        self.width = self.plot_width + (2 * self.padding)
234
-        self.height = self.plot_height + self.description_height + (3 * self.padding)
235
-        self.inner_width = self.width - (2 * self.padding)
236
-        self.inner_height = self.height - (2 * self.padding)
237
-
238
-        if datasets:
239
-            self.datasets = datasets
240
-
241
-        else:
242
-
243
-            self.datasets = []
244
-            dataset_names = handle.split(',')
245
-
246
-            for name in dataset_names:
247
-
248
-                ds = Dataset.load(name)
249
-                if ds.permissions['read'].check(g.user):
250
-                    self.datasets.append(ds)
251
-
252
-        self.handle = ','.join([ds.name for ds in self.datasets]) # needed for proper URL generation
253
-
254
-        datapoint_count = 0
255
-        for datapoint in Datapoint.list('read', g.user).where(Datapoint.dataset << self.datasets):
256
-
257
-            datapoint_count += 1
258
-
259
-            y_lower = datapoint.y
260
-            if datapoint.error_lower:
261
-                y_lower -= datapoint.error_lower
262
-
263
-            y_upper = datapoint.y
264
-            if datapoint.error_upper:
265
-                y_upper += datapoint.error_upper
266
-
267
-
268
-            if self.min_x is None or datapoint.x < self.min_x:
269
-                self.min_x = datapoint.x
270
-
271
-            if self.max_x is None or datapoint.x > self.max_x:
272
-                self.max_x = datapoint.x
273
-               
274
-            if self.min_y is None or y_lower < self.min_y:
275
-                self.min_y = y_lower
276
-
277
-            if self.max_y is None or y_upper > self.max_y:
278
-                self.max_y = y_upper
279
-
280
-        if datapoint_count > 0:
281
-            self.span_x = self.max_x - self.min_x
282
-            self.span_y = self.max_y - self.min_y
283
-
284
-        else:
285
-            self.min_x = 0
286
-            self.max_x = 0
287
-            self.min_y = 0
288
-            self.max_y = 0
289
-            self.span_x = 0
290
-            self.span_y = 0
291
-
292
-
293
-    def render(self, mode=None):
294
-
295
-        if mode == 'json':
296
-
297
-            data = {}
298
-
299
-            for dataset in self.datasets:
300
-                data[dataset.name] = []
301
-
302
-                for datapoint in dataset.authorized_datapoints:
303
-                    data[dataset.name].append({
304
-                        'x': datapoint.x,
305
-                        'y': datapoint.y,
306
-                        'error_lower': datapoint.error_lower,
307
-                        'error_upper': datapoint.error_upper
308
-                    })
309
-
310
-            return Markup(json.dumps(data))
311
-
312
-        return super(Plot, self).render(mode=mode)
313
-
314
-
315
-    def normalize_x(self, value):
316
-
317
-        if self.span_x == 0.0:
318
-            return self.plot_width / 2.0
319
-
320
-        return (value - self.min_x) * (self.plot_width / self.span_x)
321
-
322
-
323
-    def normalize_y(self, value):
324
-
325
-        if self.span_y == 0.0:
326
-            return self.plot_height / 2.0
327
-
328
-        return self.plot_height - (value - self.min_y) * (self.plot_height / self.span_y)
329
-
330
-
331
-    @property
332
-    def label_x(self):
333
-
334
-        return u' / '.join([dataset.label_x for dataset in self.datasets])
335
-
336
-
337
-    @property
338
-    def label_y(self):
339
-
340
-        return u' / '.join([dataset.label_y for dataset in self.datasets])
341
-
342
-
343
-    @property
344
-    def grid_x(self):
345
-
346
-        if self.span_x == 0:
347
-            return [self.min_x]
348
-
349
-        grid_step = 10 ** (int(math.log10(self.span_x)) - 1)
350
-
351
-        offset = (self.min_x % grid_step) * grid_step # distance from start of plot to first line on the grid
352
-        start = self.min_x + offset
353
-
354
-        x = start
355
-        coords = [x]
356
-        while x <= self.max_x:
357
-            coords.append(x)
358
-            x += grid_step
359
-
360
-        return coords
361
-
362
-
363
-    @property
364
-    def grid_y(self):
365
-
366
-        if self.span_y == 0:
367
-            return [self.min_y]
368
-
369
-        grid_step = 10 ** (int(math.log10(self.span_y)) - 1)
370
-
371
-        offset = (self.min_y % grid_step) * grid_step # distance from start of plot to first line on the grid
372
-        start = self.min_y + offset
373
-
374
-        y = start
375
-        coords = [y]
376
-        while y <= self.max_y:
377
-            coords.append(y)
378
-            y += grid_step
379
-
380
-        return coords
381
-
382
-
383
-class MapDataset(poobrains.commenting.Commentable):
384
-
385
-    title = poobrains.storage.fields.CharField()
386
-    description = poobrains.md.MarkdownField(null=True)
387
-
388
-
389
-    @property
390
-    def authorized_datapoints(self):
391
-        return MapDatapoint.list('read', g.user).where(MapDatapoint.dataset == self)
392
-
393
-
394
-    def plot(self):
395
-
396
-        return Map(datasets=[self]).render('inline')
397
-
398
-
399
-class MapDatapointFieldset(poobrains.form.Fieldset):
400
-
401
-    def __init__(self, datapoint, **kwargs):
402
-
403
-        super(MapDatapointFieldset, self).__init__(**kwargs)
404
-
405
-        self.datapoint = datapoint
406
-        self.title = poobrains.form.fields.Text(type=poobrains.form.types.STRING, value=datapoint.title, placeholder=MapDatapoint.title.verbose_name, help_text=MapDatapoint.title.help_text)
407
-        self.latitude = poobrains.form.fields.Text(type=poobrains.form.types.FLOAT, value=self.datapoint.x, placeholder=MapDatapoint.latitude.verbose_name, help_text=MapDatapoint.latitude.help_text)
408
-        self.longitude = poobrains.form.fields.Text(type=poobrains.form.types.FLOAT, value=self.datapoint.y, placeholder=MapDatapoint.longitude.verbose_name, help_text=MapDatapoint.longitude.help_text)
409
-        self.description = poobrains.form.fields.TextArea(placeholder=MapDatapoint.description.verbose_name, help_text=MapDatapoint.description.help_text)
410
-
411
-
412
-    def process(self, submit, dataset):
413
-
414
-        if self.datapoint in dataset.datapoints:
415
-            pass # florp
416
-        else:
417
-            self.datapoint.dataset = dataset
418
-
419
-            self.datapoint.name = self.fields['name'].value
420
-            self.datapoint.latitude = self.fields['latitude'].value
421
-            self.datapoint.longitude = self.fields['longitude'].value
422
-            self.datapoint.description = self.fields['description'].value
423
-
424
-            self.datapoint.save(force_insert=True)
425
-
426
-
427
-class MapDatapoint(poobrains.auth.Owned):
428
-
429
-    class Meta:
430
-        related_use_form = True
431
-
432
-
433
-    width = None
434
-    height = None
435
-
436
-    dataset = poobrains.storage.fields.ForeignKeyField(MapDataset, related_name='datapoints')
437
-    title = poobrains.storage.fields.CharField()
438
-    description = poobrains.md.MarkdownField(null=True)
439
-    latitude = poobrains.storage.fields.DoubleField()
440
-    longitude = poobrains.storage.fields.DoubleField()
441
-
442
-
443
-    def __init__(self, *args, **kwargs):
444
-
445
-        super(MapDatapoint, self).__init__(*args, **kwargs)
446
-        self.width = app.config['SVG_MAP_WIDTH']
447
-        self.height = app.config['SVG_MAP_HEIGHT']
448
-        self.infobox_width = app.config['SVG_MAP_INFOBOX_WIDTH']
449
-        self.infobox_height = app.config['SVG_MAP_INFOBOX_HEIGHT']
450
-
451
-
452
-    @property
453
-    def ref_id(self):
454
-        return "dataset-%s-%s" % (self.dataset.name, self.id)
455
-
456
-    # mercator calculation shamelessly thieved from the osm wiki
457
-    # http://wiki.openstreetmap.org/wiki/Mercator
458
-    # NOTE: I *think* this is WGS84?
459
-    # NOTE: r_minor seems to have been WGS84 but missed a few decimal places
460
-
461
-    @property
462
-    def x(self):
463
-
464
-        if not self.longitude is None:
465
-            return normalize_longitude(self.longitude) * self.width
466
-
467
-    @property
468
-    def y(self):
469
-
470
-        if not self.latitude is None:
471
-
472
-            return normalize_latitude(self.latitude) * self.height
473
-
474
-
475
-    @property
476
-    def infobox_x(self):
477
-        max_x = self.width - self.infobox_width - 10
478
-        return self.x if self.x < max_x else max_x
479
-
480
-
481
-    @property
482
-    def infobox_y(self):
483
-        max_y = self.height - self.infobox_height - 10
484
-        return self.y if self.y < max_y else max_y
485
-
486
-
487
-@app.expose('/svg/map')
488
-class Map(SVG):
489
-    
490
-    width = None
491
-    height = None
492
-
493
-    datasets = None
494
-
495
-    def __init__(self, handle=None, mode=None, datasets=None, **kwargs):
496
-        
497
-        super(Map, self).__init__(handle=handle, mode=mode, **kwargs)
498
-        
499
-        if handle is None and datasets is None:
500
-            abort(404, "No datasets selected")
501
-
502
-        self.width = app.config['SVG_MAP_WIDTH']
503
-        #self.height = app.config['SVG_MAP_HEIGHT']
504
-
505
-
506
-        if datasets:
507
-            self.datasets = datasets
508
-
509
-        else:
510
-
511
-            self.datasets = []
512
-            dataset_names = handle.split(',')
513
-
514
-            for name in dataset_names:
515
-
516
-                ds = MapDataset.load(name)
517
-                if ds.permissions['read'].check(g.user):
518
-                    self.datasets.append(ds)
519
-
520
-        self.handle = ','.join([ds.name for ds in self.datasets]) # needed for proper URL generation
521
-
522
-        fd = open('/home/phryk/devel/data/countries.geo.json')
523
-        #fd = open('/home/phryk/devel/data/fnord.geojson')
524
-        self.geojson = geojson.loads(fd.read())
525
-        self.bbox = ((-25,65), (100,0)) # boundary box as ((lon_1, lat_1), (lon_2, lat_2))
526
-        #self.bbox = ((-180,75), (180,-75)) # boundary box as ((lon_1, lat_1), (lon_2, lat_2))
527
-   
528
-        self.normalized_bbox = [] 
529
-        for point in self.bbox:
530
-            self.normalized_bbox.append(normalize_coords(point[0], point[1]))
531
-
532
-        self.span_horizontal = abs(self.normalized_bbox[1][0] - self.normalized_bbox[0][0])
533
-        self.span_vertical = abs(self.normalized_bbox[1][1] - self.normalized_bbox[0][1])
534
-
535
-        #self.height = self.width * (self.span_horizontal / self.span_vertical)
536
-        self.height = self.width * (self.span_vertical / self.span_horizontal) / 2
537
-        #self.height = self.width * (self.span_horizontal / self.span_vertical) * 0.83
538
-
539
-
540
-    def grid(self):
541
-
542
-        r = '<g class="grid">'
543
-
544
-        for lat in range(-70, 80, 10):
545
-
546
-            _, y = normalize_coords(0, lat)
547
-            y -= self.normalized_bbox[0][1]
548
-            y /= self.span_vertical
549
-            y *= self.height
550
-
551
-            r += '<line class="lat-' + str(lat) + '" x1="0" y1="' + str(y) + '" x2="100%" y2="' + str(y) + '" stroke="rgba(255,255,255, 0.1)" />\n'
552
-
553
-        r += '</g>'
554
-
555
-        return Markup(r)
556
-
557
-
558
-    def render_geojson(self, data):
559
-
560
-        r = ''
561
-
562
-        if isinstance(data, geojson.feature.FeatureCollection):
563
-
564
-            if hasattr(data, 'id'):
565
-                r += f'<g id="{data.id}" class="featurecollection">\n'
566
-            else:
567
-                r += '<g class="featurecollection">\n'
568
-
569
-            for feature in data.features:
570
-                r += str(self.render_geojson(feature))
571
-
572
-            r += '</g>'
573
-        
574
-        elif isinstance(data, geojson.feature.Feature):
575
-
576
-            if hasattr(data, 'id'):
577
-                r += f'<g id="{data.id}" class="feature">\n'
578
-            else:
579
-                r += '<g class="feature">\n'
580
-
581
-            r += str(self.render_geojson(data.geometry))
582
-            r += '</g>'
583
-
584
-        elif isinstance(data, geojson.geometry.GeometryCollection):
585
-
586
-            if hasattr(data, 'id'):
587
-                r += f'<g id="{data.id}" class="geometrycollection">\n'
588
-            else:
589
-                r += '<g class="geometrycollection">\n'
590
-
591
-            for geometry in data.geometries:
592
-                r += str(self.render_geojson(geometry))
593
-
594
-            r += '</g>'
595
-
596
-        elif isinstance(data, geojson.geometry.Geometry):
597
-
598
-            if isinstance(data, geojson.geometry.Polygon):
599
-                
600
-                if hasattr(data, 'id'):
601
-                    r += '<path id="{data.id}" '
602
-                else:
603
-                    r += '<path '
604
-
605
-                r += 'd="'
606
-
607
-                for path in data.coordinates:
608
-
609
-                    x, y = normalize_coords(path[0][0], path[0][1])
610
-                    x -= self.normalized_bbox[0][0]
611
-                    x /= self.span_horizontal
612
-
613
-                    y -= self.normalized_bbox[0][1]
614
-                    y /= self.span_vertical
615
-
616
-                    x *= self.width
617
-                    y *= self.height
618
-
619
-                    r += 'M %f %f' % (x, y) # Move to first coordinate
620
-                    for point in path[1:-1]: # ignore last coordinate pair because as per RFC7946, first and last coordinate pair MUST be the same
621
-
622
-                        x, y = normalize_coords(point[0], point[1])
623
-
624
-                        x -= self.normalized_bbox[0][0]
625
-                        x /= self.span_horizontal
626
-
627
-                        y -= self.normalized_bbox[0][1]
628
-                        y /= self.span_vertical
629
-
630
-                        x *= self.width
631
-                        y *= self.height
632
-
633
-                        #x = normalize_longitude(point[0]) * self.width
634
-                        #y = normalize_latitude(point[1]) * self.height
635
-                        r += ' L ' + str(x) + ' ' + str(y)
636
-                    r+= ' z\n'
637
-                r += '" />\n'
638
-
639
-            elif isinstance(data, geojson.geometry.MultiPolygon):
640
-
641
-                r += '<g class="multipolygon">'
642
-
643
-                for coordinates in data.coordinates:
644
-                    fakepoly = geojson.geometry.Polygon(coordinates)
645
-                    r += str(self.render_geojson(fakepoly))
646
-
647
-                r += '</g>'
648
-
649
-
650
-            else:
651
-                r += "<!-- unhandled geometry of type %s -->" % type(data).__name__
652
-
653
-        return Markup(r)
13
+Dataset = plot.Dataset
14
+Datapoint = plot.Datapoint
654 15
 
16
+MapDataset = geo.MapDataset
17
+MapDatapoint = geo.MapDatapoint
655 18
 
656 19
 @app.before_first_request
657 20
 def register_svg_raw():

+ 60
- 0
poobrains/svg/base.py View File

@@ -0,0 +1,60 @@
1
+import collections
2
+
3
+from poobrains import Response, Markup, app
4
+import poobrains.helpers
5
+import poobrains.auth
6
+
7
+class SVG(poobrains.auth.Protected):
8
+    
9
+    handle = None # needed so that app.expose registers a route with extra param, this is kinda hacky…
10
+    
11
+    class Meta:
12
+
13
+        modes = collections.OrderedDict([
14
+            ('teaser', 'read'),
15
+            ('full', 'read'),
16
+            ('raw', 'read'),
17
+            ('inline', 'read')
18
+        ])
19
+    
20
+    style = None
21
+
22
+    def __init__(self, handle=None, mode=None, **kwargs):
23
+
24
+        super(SVG, self).__init__(**kwargs)
25
+
26
+        self.handle = handle
27
+        self.style = Markup(app.scss_compiler.compile_string("@import 'svg';"))
28
+    
29
+    
30
+    def templates(self, mode=None):
31
+
32
+        r = super(SVG, self).templates(mode=mode)
33
+        return ["svg/%s" % template for template in r]
34
+
35
+    
36
+    def instance_url(self, mode='full', quiet=False, **url_params):
37
+    
38
+        url_params['handle'] = self.handle
39
+
40
+        return super(SVG, self).instance_url(mode=mode, quiet=quiet, **url_params) 
41
+
42
+
43
+    @poobrains.helpers.themed
44
+    def view(self, mode=None, handle=None):
45
+
46
+        if mode == 'raw':
47
+            
48
+            response = Response(self.render('raw'))
49
+            response.headers['Content-Type'] = u'image/svg+xml'
50
+            response.headers['Content-Disposition'] = u'filename="%s.svg"' % self.__class__.__name__
51
+            
52
+            # Disable "public" mode caching downstream (nginx, varnish) in order to hopefully not leak restricted content
53
+            response.cache_control.public = False
54
+            response.cache_control.private = True
55
+            response.cache_control.max_age = app.config['CACHE_LONG']
56
+
57
+            return response
58
+        
59
+        else:
60
+            return poobrains.helpers.ThemedPassthrough(super(SVG, self).view(mode=mode, handle=handle))

+ 366
- 0
poobrains/svg/geo.py View File

@@ -0,0 +1,366 @@
1
+import math
2
+import geojson
3
+from pyproj import Proj # map projection transformation shite
4
+
5
+from poobrains import Markup, app, abort, g
6
+import poobrains.storage
7
+import poobrains.auth
8
+import poobrains.commenting
9
+
10
+from . import base
11
+
12
+
13
+def normalize_coords(longitude, latitude):
14
+
15
+    #projection = Proj(init='epsg:4326') # WGS84 according to epsg.io, but results in equidistant latitudes
16
+    projection = Proj(init='epsg:3857') # WTS84, as used by OSM etc.
17
+    x_rad, y_rad = projection(longitude, latitude)
18
+
19
+    x = (x_rad / (math.pi * 2)) + 0.5
20
+    y = 1 - ((y_rad / math.pi) + 0.5)
21
+
22
+    return x, y
23
+
24
+
25
+def normalize_longitude(longitude):
26
+
27
+    # normalize longitude to linear scale (0-1)
28
+
29
+    normalization_factor = 20037508.3428
30
+
31
+    r_major=6378137.000
32
+    x = r_major*math.radians(longitude)
33
+    #return 50 + 50 * (x / normalization_factor)
34
+    
35
+    return 0.5 + 0.5 * (x / normalization_factor)
36
+
37
+
38
+def normalize_latitude(latitude):
39
+
40
+    #normalization_factor = 19994838.114 # this is the value this function would return for 85.0511° without normalization, which should™ make the map square
41
+    normalization_factor = 12890914.1373 # this is the value this function would return for 75° without normalization, which should™ make the map square
42
+    if latitude>89.5:latitude=89.5
43
+    if latitude<-89.5:latitude=-89.5
44
+    r_major=6378137.000
45
+    r_minor=6356752.3142518
46
+    temp=r_minor/r_major
47
+    eccent=math.sqrt(1-temp**2)
48
+    phi=math.radians(latitude)
49
+    sinphi=math.sin(phi)
50
+    con=eccent*sinphi
51
+    com=eccent/2
52
+    con=((1.0-con)/(1.0+con))**com
53
+    ts=math.tan((math.pi/2-phi)/2)/con
54
+    y=0-r_major*math.log(ts)
55
+
56
+    return 0.5 - 0.5 * (y / normalization_factor)
57
+
58
+
59
+class MapDataset(poobrains.commenting.Commentable):
60
+
61
+    title = poobrains.storage.fields.CharField()
62
+    description = poobrains.md.MarkdownField(null=True)
63
+    bbox_left = poobrains.storage.fields.FloatField()
64
+    bbox_top = poobrains.storage.fields.FloatField()
65
+    bbox_right = poobrains.storage.fields.FloatField()
66
+    bbox_bottom = poobrains.storage.fields.FloatField()
67
+    background = poobrains.storage.fields.TextField(null=True) # background map as geojson
68
+
69
+
70
+    @property
71
+    def authorized_datapoints(self):
72
+        return MapDatapoint.list('read', g.user).where(MapDatapoint.dataset == self)
73
+
74
+
75
+    def plot(self):
76
+
77
+        return Map(datasets=[self]).render('inline')
78
+
79
+
80
+class MapDatapointFieldset(poobrains.form.Fieldset):
81
+
82
+    def __init__(self, datapoint, **kwargs):
83
+
84
+        super(MapDatapointFieldset, self).__init__(**kwargs)
85
+
86
+        self.datapoint = datapoint
87
+        self.title = poobrains.form.fields.Text(type=poobrains.form.types.STRING, value=datapoint.title, placeholder=MapDatapoint.title.verbose_name, help_text=MapDatapoint.title.help_text)
88
+        self.latitude = poobrains.form.fields.Text(type=poobrains.form.types.FLOAT, value=self.datapoint.x, placeholder=MapDatapoint.latitude.verbose_name, help_text=MapDatapoint.latitude.help_text)
89
+        self.longitude = poobrains.form.fields.Text(type=poobrains.form.types.FLOAT, value=self.datapoint.y, placeholder=MapDatapoint.longitude.verbose_name, help_text=MapDatapoint.longitude.help_text)
90
+        self.description = poobrains.form.fields.TextArea(placeholder=MapDatapoint.description.verbose_name, help_text=MapDatapoint.description.help_text)
91
+
92
+
93
+    def process(self, submit, dataset):
94
+
95
+        if self.datapoint in dataset.datapoints:
96
+            pass # florp
97
+        else:
98
+            self.datapoint.dataset = dataset
99
+
100
+            self.datapoint.name = self.fields['name'].value
101
+            self.datapoint.latitude = self.fields['latitude'].value
102
+            self.datapoint.longitude = self.fields['longitude'].value
103
+            self.datapoint.description = self.fields['description'].value
104
+
105
+            self.datapoint.save(force_insert=True)
106
+
107
+
108
+class MapDatapoint(poobrains.auth.Owned):
109
+
110
+    class Meta:
111
+        related_use_form = True
112
+
113
+
114
+    width = None
115
+    height = None
116
+
117
+    dataset = poobrains.storage.fields.ForeignKeyField(MapDataset, related_name='datapoints')
118
+    title = poobrains.storage.fields.CharField()
119
+    description = poobrains.md.MarkdownField(null=True)
120
+    latitude = poobrains.storage.fields.DoubleField()
121
+    longitude = poobrains.storage.fields.DoubleField()
122
+
123
+
124
+    def __init__(self, *args, **kwargs):
125
+
126
+        super(MapDatapoint, self).__init__(*args, **kwargs)
127
+        self.width = app.config['SVG_MAP_WIDTH']
128
+        self.height = app.config['SVG_MAP_HEIGHT']
129
+        self.infobox_width = app.config['SVG_MAP_INFOBOX_WIDTH']
130
+        self.infobox_height = app.config['SVG_MAP_INFOBOX_HEIGHT']
131
+
132
+
133
+    @property
134
+    def ref_id(self):
135
+        return "dataset-%s-%s" % (self.dataset.name, self.id)
136
+
137
+    # mercator calculation shamelessly thieved from the osm wiki
138
+    # http://wiki.openstreetmap.org/wiki/Mercator
139
+    # NOTE: I *think* this is WGS84?
140
+    # NOTE: r_minor seems to have been WGS84 but missed a few decimal places
141
+
142
+    @property
143
+    def x(self):
144
+
145
+        if not self.longitude is None:
146
+            return normalize_longitude(self.longitude) * self.width
147
+
148
+    @property
149
+    def y(self):
150
+
151
+        if not self.latitude is None:
152
+
153
+            return normalize_latitude(self.latitude) * self.height
154
+
155
+
156
+    @property
157
+    def infobox_x(self):
158
+        max_x = self.width - self.infobox_width - 10
159
+        return self.x if self.x < max_x else max_x
160
+
161
+
162
+    @property
163
+    def infobox_y(self):
164
+        max_y = self.height - self.infobox_height - 10
165
+        return self.y if self.y < max_y else max_y
166
+
167
+
168
+@app.expose('/svg/map')
169
+class Map(base.SVG):
170
+    
171
+    width = None
172
+    height = None
173
+
174
+    datasets = None
175
+
176
+    def __init__(self, handle=None, mode=None, datasets=None, **kwargs):
177
+        
178
+        super(Map, self).__init__(handle=handle, mode=mode, **kwargs)
179
+        
180
+        if handle is None and datasets is None:
181
+            abort(404, "No datasets selected")
182
+
183
+        self.width = app.config['SVG_MAP_WIDTH']
184
+        #self.height = app.config['SVG_MAP_HEIGHT']
185
+
186
+
187
+        if datasets:
188
+            self.datasets = datasets
189
+
190
+        else:
191
+
192
+            self.datasets = []
193
+            dataset_names = handle.split(',')
194
+
195
+            for name in dataset_names:
196
+
197
+                ds = MapDataset.load(name)
198
+                if ds.permissions['read'].check(g.user):
199
+                    self.datasets.append(ds)
200
+
201
+        self.handle = ','.join([ds.name for ds in self.datasets]) # needed for proper URL generation
202
+
203
+        #fd = open('/home/phryk/devel/data/countries.geo.json')
204
+        #fd = open('/home/phryk/devel/data/fnord.geojson')
205
+        #self.geojson = geojson.loads(fd.read())
206
+        #self.bbox = ((-25,65), (100,0)) # boundary box as ((lon_1, lat_1), (lon_2, lat_2))
207
+        #self.bbox = ((-25,60), (100,-1)) # boundary box as ((lon_1, lat_1), (lon_2, lat_2))
208
+        #self.bbox = ((-180,75), (180,-75)) # boundary box as ((lon_1, lat_1), (lon_2, lat_2))
209
+
210
+
211
+        self.bbox = [[self.datasets[0].bbox_left, self.datasets[0].bbox_top], [self.datasets[0].bbox_right, self.datasets[0].bbox_bottom]]
212
+
213
+        if len(self.datasets) > 1:
214
+
215
+            geojson_data = []
216
+
217
+            for dataset in self.datasets[1:]:
218
+
219
+                if dataset.background:
220
+                    geojson_data.append(geojson.loads(dataset.background))
221
+
222
+
223
+                if dataset.bbox_left < self.bbox[0][0]:
224
+                    self.bbox[0][0] = dataset.bbox_left
225
+
226
+                if dataset.bbox_top > self.bbox[0][1]:
227
+                    self.bbox[0][1] = dataset.bbox_top
228
+
229
+                if dataset.bbox_right > self.bbox[1][0]:
230
+                    self.bbox[1][0] = dataset.bbox_right
231
+
232
+                if dataset.bbox_bottom < self.bbox[1][1]:
233
+                    self.bbox[1][1] = dataset.bbox_bottom
234
+
235
+            self.geojson = geojson.FeatureCollection(geojson_data)
236
+
237
+        else:
238
+            self.geojson = geojson.loads(self.datasets[0].background)
239
+   
240
+        self.normalized_bbox = [] 
241
+        for point in self.bbox:
242
+            self.normalized_bbox.append(normalize_coords(point[0], point[1]))
243
+
244
+        self.span_horizontal = abs(self.normalized_bbox[1][0] - self.normalized_bbox[0][0])
245
+        self.span_vertical = abs(self.normalized_bbox[1][1] - self.normalized_bbox[0][1])
246
+
247
+        #self.height = self.width * (self.span_horizontal / self.span_vertical)
248
+        self.height = self.width * (self.span_vertical / self.span_horizontal) / 2
249
+        #self.height = self.width * (self.span_horizontal / self.span_vertical) * 0.83
250
+
251
+
252
+
253
+    def grid(self):
254
+
255
+        r = '<g class="grid">'
256
+
257
+        for lat in range(-70, 80, 10):
258
+
259
+            _, y = normalize_coords(0, lat)
260
+            y -= self.normalized_bbox[0][1]
261
+            y /= self.span_vertical
262
+            y *= self.height
263
+
264
+            r += '<line class="grid-y lat-' + str(lat) + '" x1="0" y1="' + str(y) + '" x2="100%" y2="' + str(y) + '" />\n'
265
+
266
+        r += '</g>'
267
+
268
+        return Markup(r)
269
+
270
+
271
+    def render_geojson(self, data):
272
+
273
+        r = ''
274
+
275
+        if isinstance(data, geojson.feature.FeatureCollection):
276
+
277
+            if hasattr(data, 'id'):
278
+                r += f'<g id="{data.id}" class="featurecollection">\n'
279
+            else:
280
+                r += '<g class="featurecollection">\n'
281
+
282
+            for feature in data.features:
283
+                r += str(self.render_geojson(feature))
284
+
285
+            r += '</g>'
286
+        
287
+        elif isinstance(data, geojson.feature.Feature):
288
+
289
+            if hasattr(data, 'id'):
290
+                r += f'<g id="{data.id}" class="feature">\n'
291
+            else:
292
+                r += '<g class="feature">\n'
293
+
294
+            r += str(self.render_geojson(data.geometry))
295
+            r += '</g>'
296
+
297
+        elif isinstance(data, geojson.geometry.GeometryCollection):
298
+
299
+            if hasattr(data, 'id'):
300
+                r += f'<g id="{data.id}" class="geometrycollection">\n'
301
+            else:
302
+                r += '<g class="geometrycollection">\n'
303
+
304
+            for geometry in data.geometries:
305
+                r += str(self.render_geojson(geometry))
306
+
307
+            r += '</g>'
308
+
309
+        elif isinstance(data, geojson.geometry.Geometry):
310
+
311
+            if isinstance(data, geojson.geometry.Polygon):
312
+                
313
+                if hasattr(data, 'id'):
314
+                    r += '<path id="{data.id}" '
315
+                else:
316
+                    r += '<path '
317
+
318
+                r += 'd="'
319
+
320
+                for path in data.coordinates:
321
+
322
+                    x, y = normalize_coords(path[0][0], path[0][1])
323
+                    x -= self.normalized_bbox[0][0]
324
+                    x /= self.span_horizontal
325
+
326
+                    y -= self.normalized_bbox[0][1]
327
+                    y /= self.span_vertical
328
+
329
+                    x *= self.width
330
+                    y *= self.height
331
+
332
+                    r += 'M %f %f' % (x, y) # Move to first coordinate
333
+                    for point in path[1:-1]: # ignore last coordinate pair because as per RFC7946, first and last coordinate pair MUST be the same
334
+
335
+                        x, y = normalize_coords(point[0], point[1])
336
+
337
+                        x -= self.normalized_bbox[0][0]
338
+                        x /= self.span_horizontal
339
+
340
+                        y -= self.normalized_bbox[0][1]
341
+                        y /= self.span_vertical
342
+
343
+                        x *= self.width
344
+                        y *= self.height
345
+
346
+                        #x = normalize_longitude(point[0]) * self.width
347
+                        #y = normalize_latitude(point[1]) * self.height
348
+                        r += ' L ' + str(x) + ' ' + str(y)
349
+                    r+= ' z\n'
350
+                r += '" />\n'
351
+
352
+            elif isinstance(data, geojson.geometry.MultiPolygon):
353
+
354
+                r += '<g class="multipolygon">'
355
+
356
+                for coordinates in data.coordinates:
357
+                    fakepoly = geojson.geometry.Polygon(coordinates)
358
+                    r += str(self.render_geojson(fakepoly))
359
+
360
+                r += '</g>'
361
+
362
+
363
+            else:
364
+                r += "<!-- unhandled geometry of type %s -->" % type(data).__name__
365
+
366
+        return Markup(r)

+ 272
- 0
poobrains/svg/plot.py View File

@@ -0,0 +1,272 @@
1
+import math
2
+import collections
3
+import json
4
+
5
+from poobrains import Markup, app, abort, g, locked_cached_property
6
+
7
+import poobrains.storage
8
+import poobrains.auth
9
+import poobrains.commenting
10
+
11
+from . import base
12
+
13
+class Dataset(poobrains.commenting.Commentable):
14
+
15
+
16
+    title = poobrains.storage.fields.CharField()
17
+    description = poobrains.md.MarkdownField(null=True)
18
+    label_x = poobrains.storage.fields.CharField(verbose_name="Label for the x-axis")
19
+    label_y = poobrains.storage.fields.CharField(verbose_name="Label for the y-axis")
20
+    #grid_step_x = poobrains.storage.fields.DoubleField(default=1.0)
21
+    #grid_step_y = poobrains.storage.fields.DoubleField(default=1.0)
22
+
23
+    @property
24
+    def ref_id(self):
25
+        return "dataset-%s" % self.name
26
+
27
+
28
+    @locked_cached_property
29
+    def authorized_datapoints(self):
30
+        return Datapoint.list('read', g.user).where(Datapoint.dataset == self)
31
+
32
+    
33
+    def datapoint_id(self, datapoint):
34
+        return "dataset-%s-%s" % (self.name, datapoint.x)
35
+
36
+    
37
+    def plot(self):
38
+
39
+        return Plot(datasets=[self]).render('inline')
40
+
41
+
42
+class DatapointFieldset(poobrains.form.Fieldset):
43
+
44
+    def __init__(self, datapoint, **kwargs):
45
+
46
+        super(DatapointFieldset, self).__init__(**kwargs)
47
+
48
+        self.datapoint = datapoint
49
+        self.x = poobrains.form.fields.Text(type=poobrains.form.types.FLOAT, value=self.datapoint.x, placeholder=Datapoint.x.verbose_name, help_text=Datapoint.x.help_text)
50
+        self.y = poobrains.form.fields.Text(type=poobrains.form.types.FLOAT, value=self.datapoint.y, placeholder=Datapoint.y.verbose_name, help_text=Datapoint.y.help_text)
51
+        self.error_lower = poobrains.form.fields.Text(type=poobrains.form.types.FLOAT, value=self.datapoint.error_lower, placeholder=Datapoint.error_lower.verbose_name, help_text=Datapoint.error_lower.help_text)
52
+        self.error_upper = poobrains.form.fields.Text(type=poobrains.form.types.FLOAT, value=self.datapoint.error_upper, placeholder=Datapoint.error_upper.verbose_name, help_text=Datapoint.error_upper.help_text)
53
+
54
+
55
+    def process(self, submit, dataset):
56
+
57
+        if self.datapoint in dataset.datapoints:
58
+            pass # florp
59
+        else:
60
+            self.datapoint.dataset = dataset
61
+            self.datapoint.x = self.fields['x'].value
62
+            self.datapoint.y = self.fields['y'].value
63
+            self.datapoint.error_lower = self.fields['error_lower'].value
64
+            self.datapoint.error_upper = self.fields['error_upper'].value
65
+
66
+            self.datapoint.save(force_insert=True)
67
+
68
+
69
+class Datapoint(poobrains.auth.Owned):
70
+
71
+    class Meta:
72
+        order_by = ['dataset', 'x']
73
+        primary_key = poobrains.storage.CompositeKey('dataset', 'x')
74
+        related_use_form = True
75
+
76
+    dataset = poobrains.storage.fields.ForeignKeyField(Dataset, related_name='datapoints')
77
+    x = poobrains.storage.fields.DoubleField()
78
+    y = poobrains.storage.fields.DoubleField()
79
+    error_lower = poobrains.storage.fields.FloatField(help_text="Lower margin of error", default=0.0)
80
+    error_upper = poobrains.storage.fields.FloatField(help_text="Upper margin of error", default=0.0)
81
+
82
+
83
+@app.expose('/svg/plot')
84
+class Plot(base.SVG):
85
+
86
+    padding = None
87
+    width = None
88
+    height = None
89
+    inner_width = None
90
+    inner_height = None
91
+    plot_width = None
92
+    plot_height = None
93
+    description_height = None
94
+
95
+    datasets = None
96
+    min_x = None
97
+    max_x = None
98
+    min_y = None
99
+    max_y = None
100
+    span_x = None
101
+    span_y = None
102
+        
103
+
104
+    class Meta:
105
+
106
+        modes = collections.OrderedDict([
107
+            ('teaser', 'read'),
108
+            ('full', 'read'),
109
+            ('raw', 'read'),
110
+            ('json', 'read'),
111
+            ('inline', 'read')
112
+        ])
113
+
114
+    def __init__(self, handle=None, mode=None, datasets=None, **kwargs):
115
+
116
+        super(Plot, self).__init__(handle=handle, mode=mode, **kwargs)
117
+
118
+        if handle is None and datasets is None:
119
+            abort(404, "No datasets selected")
120
+        
121
+        self.padding = app.config['SVG_PLOT_PADDING']
122
+        self.plot_width = app.config['SVG_PLOT_WIDTH']
123
+        self.plot_height = app.config['SVG_PLOT_HEIGHT']
124
+        self.description_height = app.config['SVG_PLOT_DESCRIPTION_HEIGHT']
125
+        self.width = self.plot_width + (2 * self.padding)
126
+        self.height = self.plot_height + self.description_height + (3 * self.padding)
127
+        self.inner_width = self.width - (2 * self.padding)
128
+        self.inner_height = self.height - (2 * self.padding)
129
+
130
+        if datasets:
131
+            self.datasets = datasets
132
+
133
+        else:
134
+
135
+            self.datasets = []
136
+            dataset_names = handle.split(',')
137
+
138
+            for name in dataset_names:
139
+
140
+                ds = Dataset.load(name)
141
+                if ds.permissions['read'].check(g.user):
142
+                    self.datasets.append(ds)
143
+
144
+        self.handle = ','.join([ds.name for ds in self.datasets]) # needed for proper URL generation
145
+
146
+        datapoint_count = 0
147
+        for datapoint in Datapoint.list('read', g.user).where(Datapoint.dataset << self.datasets):
148
+
149
+            datapoint_count += 1
150
+
151
+            y_lower = datapoint.y
152
+            if datapoint.error_lower:
153
+                y_lower -= datapoint.error_lower
154
+
155
+            y_upper = datapoint.y
156
+            if datapoint.error_upper:
157
+                y_upper += datapoint.error_upper
158
+
159
+
160
+            if self.min_x is None or datapoint.x < self.min_x:
161
+                self.min_x = datapoint.x
162
+
163
+            if self.max_x is None or datapoint.x > self.max_x:
164
+                self.max_x = datapoint.x
165
+               
166
+            if self.min_y is None or y_lower < self.min_y:
167
+                self.min_y = y_lower
168
+
169
+            if self.max_y is None or y_upper > self.max_y:
170
+                self.max_y = y_upper
171
+
172
+        if datapoint_count > 0:
173
+            self.span_x = self.max_x - self.min_x
174
+            self.span_y = self.max_y - self.min_y
175
+
176
+        else:
177
+            self.min_x = 0
178
+            self.max_x = 0
179
+            self.min_y = 0
180
+            self.max_y = 0
181
+            self.span_x = 0
182
+            self.span_y = 0
183
+
184
+
185
+    def render(self, mode=None):
186
+
187
+        if mode == 'json':
188
+
189
+            data = {}
190
+
191
+            for dataset in self.datasets:
192
+                data[dataset.name] = []
193
+
194
+                for datapoint in dataset.authorized_datapoints:
195
+                    data[dataset.name].append({
196
+                        'x': datapoint.x,
197
+                        'y': datapoint.y,
198
+                        'error_lower': datapoint.error_lower,
199
+                        'error_upper': datapoint.error_upper
200
+                    })
201
+
202
+            return Markup(json.dumps(data))
203
+
204
+        return super(Plot, self).render(mode=mode)
205
+
206
+
207
+    def normalize_x(self, value):
208
+
209
+        if self.span_x == 0.0:
210
+            return self.plot_width / 2.0
211
+
212
+        return (value - self.min_x) * (self.plot_width / self.span_x)
213
+
214
+
215
+    def normalize_y(self, value):
216
+
217
+        if self.span_y == 0.0:
218
+            return self.plot_height / 2.0
219
+
220
+        return self.plot_height - (value - self.min_y) * (self.plot_height / self.span_y)
221
+
222
+
223
+    @property
224
+    def label_x(self):
225
+
226
+        return u' / '.join([dataset.label_x for dataset in self.datasets])
227
+
228
+
229
+    @property
230
+    def label_y(self):
231
+
232
+        return u' / '.join([dataset.label_y for dataset in self.datasets])
233
+
234
+
235
+    @property
236
+    def grid_x(self):
237
+
238
+        if self.span_x == 0:
239
+            return [self.min_x]
240
+
241
+        grid_step = 10 ** (int(math.log10(self.span_x)) - 1)
242
+
243
+        offset = (self.min_x % grid_step) * grid_step # distance from start of plot to first line on the grid
244
+        start = self.min_x + offset
245
+
246
+        x = start
247
+        coords = [x]
248
+        while x <= self.max_x:
249
+            coords.append(x)
250
+            x += grid_step
251
+
252
+        return coords
253
+
254
+
255
+    @property
256
+    def grid_y(self):
257
+
258
+        if self.span_y == 0:
259
+            return [self.min_y]
260
+
261
+        grid_step = 10 ** (int(math.log10(self.span_y)) - 1)
262
+
263
+        offset = (self.min_y % grid_step) * grid_step # distance from start of plot to first line on the grid
264
+        start = self.min_y + offset
265
+
266
+        y = start
267
+        coords = [y]
268
+        while y <= self.max_y:
269
+            coords.append(y)
270
+            y += grid_step
271
+
272
+        return coords

+ 23
- 17
poobrains/themes/default/svg.scss View File

@@ -211,6 +211,23 @@ svg#checkbox {
211 211
 
212 212
 }
213 213
 
214
+.grid {
215
+
216
+    line {
217
+        stroke: opacify($color_font_light, -80%);
218
+        stroke-width: 1px;
219
+        stroke-linecap: butt; /* butts lol */
220
+
221
+        &.highlighted {
222
+            stroke: opacify($color_font_light, -50%);
223
+        }
224
+    }
225
+
226
+    .grid-label {
227
+        fill: $color_font_light;
228
+        font-size: 12px;
229
+    }
230
+}
214 231
 
215 232
 /* ## PLOTS ## */
216 233
 .plot {
@@ -220,23 +237,6 @@ svg#checkbox {
220 237
     .plot-inner {
221 238
         overflow: visible;
222 239
 
223
-        .grid {
224
-
225
-            line {
226
-                stroke: opacify($color_font_light, -80%);
227
-                stroke-width: 1px;
228
-                stroke-linecap: butt; /* butts lol */
229
-
230
-                &.highlighted {
231
-                    stroke: opacify($color_font_light, -50%);
232
-                }
233
-            }
234
-
235
-            .grid-label {
236
-                fill: $color_font_light;
237
-                font-size: 12px;
238
-            }
239
-        }
240 240
 
241 241
         g.datasets {
242 242
 
@@ -414,6 +414,12 @@ svg#checkbox {
414 414
 
415 415
 svg.map {
416 416
 
417
+    .geojson-render path {
418
+        stroke: $color_highlight;
419
+        stroke-width: 1px;
420
+        fill: $color_background_dark;
421
+    }
422
+
417 423
     .terrain {
418 424
 
419 425
         .ocean {

+ 0
- 2
poobrains/themes/default/svg/map-raw.jinja View File

@@ -28,7 +28,6 @@
28 28
 
29 29
     {{ content.grid() }}
30 30
 
31
-    {#
32 31
     <g class="datasets">
33 32
     {% for dataset in content.datasets %}
34 33
         <g class="dataset">
@@ -77,5 +76,4 @@
77 76
         </g>
78 77
         {% endfor %}
79 78
     </g>
80
-    #}
81 79
 </svg>

Loading…
Cancel
Save