Skip to content

Instantly share code, notes, and snippets.

@alex-hhh
Last active July 21, 2019 01:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alex-hhh/cdedfb550bb68411bd2331c8f4d2c421 to your computer and use it in GitHub Desktop.
Save alex-hhh/cdedfb550bb68411bd2331c8f4d2c421 to your computer and use it in GitHub Desktop.
#lang racket
(require json)
;; Example 0: Load and manipulate the Timezone GeoJSON file
;; You will also need to download timezone data from:
;;
;; https://github.com/evansiroky/timezone-boundary-builder/releases
;; Copyright (c) 2019 Alex Harsányi
;; Permission is hereby granted, free of charge, to any person obtaining a
;; copy of this software and associated documentation files (the "Software"),
;; to deal in the Software without restriction, including without limitation
;; the rights to use, copy, modify, merge, publish, distribute, sublicense,
;; and/or sell copies of the Software, and to permit persons to whom the
;; Software is furnished to do so, subject to the following conditions:
;; The above copyright notice and this permission notice shall be included in
;; all copies or substantial portions of the Software.
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
;; DEALINGS IN THE SOFTWARE.
(define timezone-data (time (call-with-input-file "./combined.json" read-json)))
;; Extract a random sample from a json OBJECT -- this can be used on the
;; timezone-data to obtain a smaller object which can be printed and inspected
;; while developing the functions which manipulate the actual timezone data...
(define (random-sample object [max-size 5])
(cond
((hash? object)
;; for hash tables, copy all the keys, but simplify the values
(for/hash ([(k v) (in-hash object)])
(values k (random-sample v max-size))))
((list? object)
;; Short lists are copied directly, but we only sample MAX-SIZE random
;; values from longer lists. In both cases the values themselves are
;; further simplified.
(let ([len (length object)])
(if (> len max-size)
(let ([samples (sort (build-list max-size (lambda (x) (random len))) <)])
(for/list ([(x position) (in-indexed (in-list object))]
#:when (member position samples))
(random-sample x max-size)))
(for/list ([x (in-list object)])
(random-sample x max-size)))))
(#t
;; Any other objects are not simplified.
object)))
;; Every GeoJSON node will have type and properties keys
(define (geojson-type node)
(hash-ref node 'type #f))
(define (geojson-properties node)
(hash-ref node 'properties (lambda () (hash))))
(define (geojson-features node)
(let ([type (geojson-type node)])
(if (equal? type "FeatureCollection")
(hash-ref node 'features null)
(error (format "geojson-features: bad node type: ~a" type)))))
(define (geojson-geometry node)
(let ([type (geojson-type node)])
(if (equal? type "Feature")
(hash-ref node 'geometry (lambda () (hash)))
(error (format "geojson-geometry: bad node type: ~a" type)))))
(define (geojson-coordinates node)
(let ([type (geojson-type node)])
(if (member type '("Point" "LineString" "Polygon" "MultiPoint" "MultiLineString" "MultiPolygon"))
(hash-ref node 'coordinates null)
(error (format "geojson-coordinates: bad node type: ~a" type)))))
;; Count the number of points in a geometry GeoJSON node
(define (geojson-geometry-point-count node)
(let ([type (geojson-type node)]
[coordinates (geojson-coordinates node)])
(cond ((equal? type "Point")
;; Coordinates is a single point
1)
((equal? type "LineString")
;; List of points
(length coordinates))
((equal? type "Polygon")
;; List of lists of points (main and holes. Count them all
(for/fold ([result 0])
([line (in-list coordinates)])
(+ result (length line))))
((equal? type "MultiPoint")
;; List of points
(length coordinates))
((equal? type "MultiLineString")
(for/fold ([result 0])
([line (in-list coordinates)])
(+ result (length line))))
((equal? type "MultiPolygon")
(for/fold ([result 0])
([polygon (in-list coordinates)])
(+ result
(for/fold ([result 0])
([line (in-list polygon)])
(+ result (length line))))))
(#t
(error (format "Unknown geometry type: ~a" type))))))
;; Count the number of points in a GeoJSON node and its sub-nodes.
(define (geojson-point-count node)
(let ((type (geojson-type node)))
(cond ((equal? type "FeatureCollection")
(for/fold ([result 0])
([feature (in-list (geojson-features node))])
(+ result (geojson-point-count feature))))
((equal? type "Feature")
(let ((geometry (geojson-geometry node)))
(geojson-geometry-point-count geometry))))))
;; return the timezone name stored in a GeoJSON feature
(define (tz-name node)
(let ((properties (geojson-properties node)))
(hash-ref properties 'tzid #f)))
;; Print a summary of the point count in the timezone data JSON
(define (analyze-tz-point-count node)
(define nzones (length (geojson-features node)))
(define total (geojson-point-count node))
(define tz-point-count
(for/list ([feature (in-list (geojson-features node))])
(list (tz-name feature) (geojson-point-count feature))))
(set! tz-point-count
(sort tz-point-count
(lambda (a b)
(match-define (list _ p1) a)
(match-define (list _ p2) b)
(> p1 p2))))
(printf "Total point count: ~a, total zone count ~a~%" total nzones)
(for ([tp (in-list tz-point-count)])
(match-define (list name pcount) tp)
(printf "~a ~a~%" name pcount)))
#lang racket
(require racket/draw)
(define dc (new bitmap-dc% [bitmap (make-object bitmap% 250 250)]))
(send* dc
(set-pen (send the-pen-list find-or-create-pen "black" 5.0 'solid))
(set-brush (send the-brush-list find-or-create-brush "red" 'solid))
(draw-rectangle 10 10 (- 250 20) (- 250 20)))
(define bm (send dc get-bitmap))
(send bm save-file "example1.png" 'png)
#lang racket
(require racket/draw)
(define dc (new bitmap-dc% [bitmap (make-object bitmap% 250 250)]))
(send* dc
(set-scale 250/2 250/2)
(set-origin 250/2 250/2)
(set-pen (send the-pen-list find-or-create-pen "black" 0.00001 'solid))
(set-brush (send the-brush-list find-or-create-brush "black" 'transparent))
(draw-rectangle -1 -1 2 2)
(set-brush (send the-brush-list find-or-create-brush "red" 'solid))
(draw-line -1 0 1 0)
(draw-line 0 -1 0 1))
(define bm (send dc get-bitmap))
(send bm save-file "example2.png" 'png)
#lang racket
(require racket/draw)
(define path (new dc-path%))
(send path move-to 1 0)
(for ([p (in-range 0 (* 2 pi) (/ (* 2 pi) 5))])
(define x (cos p))
(define y (sin p))
(send path line-to x y))
(send path close)
(define dc (new bitmap-dc% [bitmap (make-object bitmap% 250 250)]))
(send* dc
(set-smoothing 'smoothed)
(set-scale 250/2 250/2)
(set-origin 250/2 250/2)
(set-pen (send the-pen-list find-or-create-pen "black" 0.01 'solid))
(set-brush (send the-brush-list find-or-create-brush "red" 'solid))
(draw-path path))
(define bm (send dc get-bitmap))
(send bm save-file "example3.png" 'png)
#lang racket
;; Example 4: Render the timezone data using a small color map
;; You will also need to download timezone data from:
;;
;; https://github.com/evansiroky/timezone-boundary-builder/releases
;; Copyright (c) 2019 Alex Harsányi
;; Permission is hereby granted, free of charge, to any person obtaining a
;; copy of this software and associated documentation files (the "Software"),
;; to deal in the Software without restriction, including without limitation
;; the rights to use, copy, modify, merge, publish, distribute, sublicense,
;; and/or sell copies of the Software, and to permit persons to whom the
;; Software is furnished to do so, subject to the following conditions:
;; The above copyright notice and this permission notice shall be included in
;; all copies or substantial portions of the Software.
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
;; DEALINGS IN THE SOFTWARE.
(require json racket/draw math/base)
(define (lat-lon->map-point coordinates)
(match-define (list lon lat _ ...) coordinates)
(define-values (x y) (values (degrees->radians lon) (asinh (tan (degrees->radians lat)))))
(list (/ (+ 1 (/ x pi)) 2) (/ (- 1 (/ y pi)) 2)))
(define (draw-polygon dc polygons)
(define path
(for/fold ([path #f]) ([polygon (in-list polygons)] #:unless (null? polygon))
(define sub-path (new dc-path%))
(send/apply sub-path move-to (lat-lon->map-point (car polygon)))
(for ([point (in-list (cdr polygon))])
(send/apply sub-path line-to (lat-lon->map-point point)))
(send sub-path close)
(if path (begin (send path append sub-path) path) sub-path)))
(and path (send dc draw-path path 0 0)))
(define (draw-feature dc feature)
(let* ([geometry (hash-ref feature 'geometry (lambda () (hash)))]
[data (hash-ref geometry 'coordinates (lambda () null))])
(case (hash-ref geometry 'type #f)
(("Polygon") (draw-polygon dc data))
(("MultiPolygon") (for ([polygon (in-list data)]) (draw-polygon dc polygon)))
(else (printf "Skipping ~a geometry" (hash-ref geometry 'type #f))))))
(define color-map
(list (make-object color% 78 121 165)
(make-object color% 241 143 59)
(make-object color% 224 88 91)
(make-object color% 119 183 178)
(make-object color% 90 161 85)
(make-object color% 237 201 88)
(make-object color% 175 122 160)
(make-object color% 254 158 168)
(make-object color% 156 117 97)
(make-object color% 186 176 172)))
(define (make-timezone-bitmap tzdata width height)
(define dc (new bitmap-dc% [bitmap (make-object bitmap% width height)]))
(send* dc
(set-scale width height)
(set-smoothing 'smoothed)
(set-pen (send the-pen-list find-or-create-pen "black" (* 0.5 (/ 1 width)) 'solid)))
;; Iterate over each feature (timezone) and render it
(for ([feature (in-list (hash-ref tzdata 'features))]
[color (in-cycle (in-list color-map))])
(send dc set-brush (send the-brush-list find-or-create-brush color 'solid))
(draw-feature dc feature))
(send dc get-bitmap))
(define timezone-data (time (call-with-input-file "./combined.json" read-json)))
(define bm (make-timezone-bitmap timezone-data 800 700))
(send bm save-file "timezone-map.png" 'png 100 #:unscaled? #t)
#lang racket
(require json racket/draw math/base)
;; Example 5: Render the timezone data using a a unique color for each color
;; map
;; You will also need to download timezone data from:
;;
;; https://github.com/evansiroky/timezone-boundary-builder/releases
;; Copyright (c) 2019 Alex Harsányi
;; Permission is hereby granted, free of charge, to any person obtaining a
;; copy of this software and associated documentation files (the "Software"),
;; to deal in the Software without restriction, including without limitation
;; the rights to use, copy, modify, merge, publish, distribute, sublicense,
;; and/or sell copies of the Software, and to permit persons to whom the
;; Software is furnished to do so, subject to the following conditions:
;; The above copyright notice and this permission notice shall be included in
;; all copies or substantial portions of the Software.
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
;; DEALINGS IN THE SOFTWARE.
(define (lat-lon->map-point coordinates)
(match-define (list lon lat _ ...) coordinates)
(define-values (x y) (values (degrees->radians lon) (asinh (tan (degrees->radians lat)))))
(list (/ (+ 1 (/ x pi)) 2) (/ (- 1 (/ y pi)) 2)))
(define (draw-polygon dc polygons)
(define path
(for/fold ([path #f]) ([polygon (in-list polygons)] #:unless (null? polygon))
(define sub-path (new dc-path%))
(send/apply sub-path move-to (lat-lon->map-point (car polygon)))
(for ([point (in-list (cdr polygon))])
(send/apply sub-path line-to (lat-lon->map-point point)))
(send sub-path close)
(if path (begin (send path append sub-path) path) sub-path)))
(and path (send dc draw-path path 0 0)))
(define (draw-feature dc feature)
(let* ([geometry (hash-ref feature 'geometry (lambda () (hash)))]
[data (hash-ref geometry 'coordinates (lambda () null))])
(case (hash-ref geometry 'type #f)
(("Polygon") (draw-polygon dc data))
(("MultiPolygon") (for ([polygon (in-list data)]) (draw-polygon dc polygon)))
(else (printf "Skipping ~a geometry" (hash-ref geometry 'type #f))))))
(define (tz-name node)
(let ((properties (hash-ref node 'properties)))
(hash-ref properties 'tzid #f)))
(define (allocate-color id)
(let* ((b (exact-floor (/ id 100)))
(g (exact-floor (/ (- id (* b 100)) 10)))
(r (exact-floor (- id (* b 100) (* g 10)))))
(make-object color% (* r 28) (* g 28) (* b 28))))
(struct tzdata (name id color) #:transparent)
(define (allocate-tzdata node)
(define next-id 1) ; start at 1, 0 is reserved for "no tz data", the oceans
(for/list ([tznode (in-list (hash-ref node 'features))])
(define name (tz-name tznode))
(define id next-id)
(set! next-id (add1 next-id))
(define color (allocate-color id))
(tzdata name id color)))
(define (time-zone-color tz-data name)
(for/first ([t (in-list tz-data)] #:when (equal? (tzdata-name t) name))
(tzdata-color t)))
(define color-map
(list (make-object color% 78 121 165)
(make-object color% 241 143 59)
(make-object color% 224 88 91)
(make-object color% 119 183 178)
(make-object color% 90 161 85)
(make-object color% 237 201 88)
(make-object color% 175 122 160)
(make-object color% 254 158 168)
(make-object color% 156 117 97)
(make-object color% 186 176 172)))
(define (make-timezone-bitmap tzdata width height)
(define tz-data (allocate-tzdata timezone-data))
(define dc (new bitmap-dc% [bitmap (make-object bitmap% width height)]))
(send* dc
(set-scale width height)
(set-smoothing 'smoothed)
(set-pen (send the-pen-list find-or-create-pen "black" (* 0.5 (/ 1 width)) 'solid)))
;; Iterate over each feature (timezone) and render it
(for ([feature (in-list (hash-ref tzdata 'features))])
(define color (time-zone-color tz-data (tz-name feature)))
(send dc set-brush (send the-brush-list find-or-create-brush color 'solid))
(draw-feature dc feature))
(send dc get-bitmap))
(define timezone-data (call-with-input-file "./combined.json" read-json))
(define bm (make-timezone-bitmap timezone-data 800 500))
(send bm save-file "timezone-map-2.png" 'png 100 #:unscaled? #t)
#lang racket
(require json racket/draw math/base)
;; Example 6: Render a subset of the map
;; You will also need to download timezone data from:
;;
;; https://github.com/evansiroky/timezone-boundary-builder/releases
;; Copyright (c) 2019 Alex Harsányi
;; Permission is hereby granted, free of charge, to any person obtaining a
;; copy of this software and associated documentation files (the "Software"),
;; to deal in the Software without restriction, including without limitation
;; the rights to use, copy, modify, merge, publish, distribute, sublicense,
;; and/or sell copies of the Software, and to permit persons to whom the
;; Software is furnished to do so, subject to the following conditions:
;; The above copyright notice and this permission notice shall be included in
;; all copies or substantial portions of the Software.
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
;; DEALINGS IN THE SOFTWARE.
(define (lat-lon->map-point coordinates)
(match-define (list lon lat _ ...) coordinates)
(define-values (x y) (values (degrees->radians lon) (asinh (tan (degrees->radians lat)))))
(list (/ (+ 1 (/ x pi)) 2) (/ (- 1 (/ y pi)) 2)))
(define (draw-polygon dc polygons)
(define path
(for/fold ([path #f]) ([polygon (in-list polygons)] #:unless (null? polygon))
(define sub-path (new dc-path%))
(send/apply sub-path move-to (lat-lon->map-point (car polygon)))
(for ([point (in-list (cdr polygon))])
(send/apply sub-path line-to (lat-lon->map-point point)))
(send sub-path close)
(if path (begin (send path append sub-path) path) sub-path)))
(and path (send dc draw-path path 0 0)))
(define (draw-feature dc feature)
(let* ([geometry (hash-ref feature 'geometry (lambda () (hash)))]
[data (hash-ref geometry 'coordinates (lambda () null))])
(case (hash-ref geometry 'type #f)
(("Polygon") (draw-polygon dc data))
(("MultiPolygon") (for ([polygon (in-list data)]) (draw-polygon dc polygon)))
(else (printf "Skipping ~a geometry" (hash-ref geometry 'type #f))))))
(define (tz-name node)
(let ((properties (hash-ref node 'properties)))
(hash-ref properties 'tzid #f)))
(define color-map
(list (make-object color% 78 121 165)
(make-object color% 241 143 59)
(make-object color% 224 88 91)
(make-object color% 119 183 178)
(make-object color% 90 161 85)
(make-object color% 237 201 88)
(make-object color% 175 122 160)
(make-object color% 254 158 168)
(make-object color% 156 117 97)
(make-object color% 186 176 172)))
(define (setup-dc dc min-coord max-coord)
(define-values (width height) (send dc get-size))
(match-define (list xmin ymin) (lat-lon->map-point min-coord))
(match-define (list xmax ymax) (lat-lon->map-point max-coord))
(define-values (xscale yscale) (values (/ width (- xmax xmin)) (/ height (- ymax ymin))))
(define-values (xorigin yorigin) (values (- (* xmin xscale)) (- (* ymin yscale))))
(send* dc
(set-origin xorigin yorigin)
(set-scale xscale yscale)
(set-smoothing 'smoothed)
(set-pen (send the-pen-list find-or-create-pen "black" (* 0.5 (/ 1 xscale)) 'solid))))
(define (make-timezone-bitmap tzdata min-coord max-coord width height)
(define dc (new bitmap-dc% [bitmap (make-object bitmap% width height)]))
(setup-dc dc min-coord max-coord)
;; Iterate over each feature (timezone) and render it
(for ([feature (in-list (hash-ref tzdata 'features))]
[color (in-cycle color-map)])
(send dc set-brush (send the-brush-list find-or-create-brush color 'solid))
(draw-feature dc feature))
(send dc get-bitmap))
(define timezone-data (call-with-input-file "./combined.json" read-json))
(define bm (make-timezone-bitmap timezone-data
(list -13.008 61.481)
(list 33.02 30.894)
500 500))
(send bm save-file "timezone-map-3.png" 'png 100 #:unscaled? #t)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment