SciCloj logo
This is part of the Scicloj Clojure Data Scrapbook.

Seattle Parks and Neighborhoods - DRAFT

Timothy Prately and Daniel Slutsky

(Probably, this notebook will be divided into a few book chapters.)

Choosing where to live depends on many factors such as job opportunities and cost of living. I like walking, so one factor that is important to me is access to parks. In this analysis we’ll rank neighborhoods by park area proportional to total area. This article demonstrates how to prepare the geospatial data, calculate the value we want, and how to explore the meaning behind the numbers.

(ns index
  (:require [geo
             [geohash :as geohash]
             [jts :as jts]
             [spatial :as spatial]
             [io :as geoio]
             [crs :as crs]]
            [tech.v3.datatype.functional :as fun]
            [tablecloth.api :as tc]
            [scicloj.kindly.v4.kind :as kind]
            [scicloj.kindly.v4.api :as kindly]
            [hiccup.core :as hiccup]
            [charred.api :as charred]
            [clojure2d.color :as color]
            [clojure.string :as str])
  (:import (org.locationtech.jts.index.strtree STRtree)
           (org.locationtech.jts.geom Geometry Point Polygon Coordinate)
           (org.locationtech.jts.geom.prep PreparedGeometry
                                           PreparedLineString
                                           PreparedPolygon
                                           PreparedGeometryFactory)
           (java.util TreeMap)))

Gathering geospatial data

Both the neighborhood geometry and park geometry can be downloaded from Seattle GeoData:

I’ve saved a snapshot in the data directory.

The data format is gzipped GeoJSON. Java has a built-in class for handling gzip streams, and we’ll use the factual/geojson library to parse the string representation.

(defn slurp-gzip
  "Read a gzipped file into a string"
  [path]
  (with-open [in (java.util.zip.GZIPInputStream. (clojure.java.io/input-stream path))]
    (slurp in)))

Now we can conveniently load the data files we downloaded previously.

(defonce neighborhoods-geojson
  (slurp-gzip "data/Seattle/Neighborhood_Map_Atlas_Neighborhoods.geojson.gz"))
(def neighborhoods-features
  (geoio/read-geojson neighborhoods-geojson))

Let’s check that we got some data.

(count neighborhoods-features)
94

This seems like a reasonable number of neighborhoods.

Each member of the dataset is called a Feature. Here is one:

(-> neighborhoods-features
    first
    kind/pprint)
{:properties
 {:OBJECTID 27,
  :L_HOOD "Ballard",
  :S_HOOD "Loyal Heights",
  :S_HOOD_ALT_NAMES nil,
  :Shape__Area 2.13206555455933E7,
  :Shape__Length 18831.00959637},
 :geometry
 #object[org.locationtech.jts.geom.Polygon 0x734a2288 "POLYGON ((-122.376336564723 47.6759176989664, -122.376707907517 47.6759982290459, -122.377903057631 47.6759978849021, -122.379988625303 47.6759893429385, -122.38182788443 47.675992079457, -122.383602752477 47.675979810424, -122.385435721425 47.6759587956618, -122.387087434206 47.6759521343769, -122.387702391216 47.6759476875929, -122.388955759168 47.6759424488678, -122.390449346263 47.6759259868868, -122.391773109273 47.6759158131504, -122.392827172066 47.6759153143353, -122.392956550545 47.6759141859541, -122.392956303464 47.6763287099834, -122.392959618409 47.676929558447, -122.392961263359 47.6775085790216, -122.392971502986 47.6781034200237, -122.392975009807 47.6792517755972, -122.392983771129 47.6798142464259, -122.39300195772 47.6811303942788, -122.393014530791 47.6824285808909, -122.393016158048 47.6837341155494, -122.393022827056 47.685021586284, -122.393030577589 47.6863450742964, -122.393032746621 47.687668638629, -122.393046187318 47.6889956475812, -122.393010665051 47.6902980913372, -122.393002415356 47.6905813205566, -122.392432373275 47.6905826938111, -122.390347910813 47.6905631204492, -122.388192979124 47.6905805029145, -122.385832831602 47.6905766041982, -122.383275316007 47.690593164597, -122.383201215024 47.6905936585447, -122.381453020656 47.6905978829044, -122.379876313408 47.6905988541749, -122.379103841332 47.6906041929746, -122.377117910432 47.6906029639028, -122.376810344766 47.6906051591463, -122.376810264834 47.6905341242701, -122.376810989127 47.6900117143006, -122.376803067488 47.688976146724, -122.376794751014 47.6879443550321, -122.376800402808 47.6870068457338, -122.376784403749 47.6860923366759, -122.376785260609 47.6851813698577, -122.376780535885 47.6842704786057, -122.376787310842 47.6833707854178, -122.376777423242 47.6822860591882, -122.376767820487 47.6811938310584, -122.376770442503 47.680154307228, -122.376770065639 47.6792017979916, -122.376769882216 47.6780678404081, -122.376759750881 47.6775387646826, -122.376761985459 47.676862137338, -122.376772171763 47.6764361697665, -122.376772451106 47.6764114022001, -122.376770700102 47.6763866189683, -122.37676692258 47.6763619488292, -122.376761120573 47.6763374777451, -122.376753802121 47.6763131984825, -122.376744969247 47.6762891966531, -122.376733608705 47.6762655291531, -122.376721246367 47.6762423037704, -122.376706358898 47.6762194979699, -122.376690473449 47.6761972626929, -122.376672575492 47.6761756116819, -122.376653693551 47.6761550023704, -122.376620811506 47.6761243856628, -122.37659791092 47.6761051588887, -122.376573006221 47.6760868163511, -122.376547112434 47.6760693435982, -122.376519722058 47.6760527482053, -122.376336564723 47.6759176989664))"]}

Each feature, in our case, represents a geographic region with a geometry and some properties.

And similarly for the parks:

(defonce parks-geojson
  (slurp-gzip "data/Seattle/Park_Boundary_(details).geojson.gz"))
(def parks-features
  (geoio/read-geojson parks-geojson))
(count parks-features)
2809

There are more parks than neighborhoods, which sounds right.

(delay
  (-> parks-features
      first
      kind/pprint))
{:properties
 {:SE_ANNO_CAD_DATA "",
  :NAMEFLAG 9,
  :ADDRESS " ",
  :MAINT "DPR",
  :LEASE "N",
  :PMA_NAME "East Duwamish GS: S Chicago St",
  :SUBPARCEL 9851,
  :PMA 442,
  :REVIEW_DATE "2004-04-08T00:00:00Z",
  :GlobalID "{D4B7025E-1135-4232-88AB-CBF72932E6AE}",
  :OBJECTID 207882,
  :AMWOID "PROPERTY-EDUWSC",
  :SDQL "QL-D1",
  :USE_ nil,
  :PIN "4006000485",
  :GIS_EDT_DT "2024-01-19T14:07:23Z",
  :SHAPE_Area 1.4691943312522366E-6,
  :SHAPE_Length 0.005215887160444538,
  :GIS_CRT_DT "2024-01-19T14:07:23Z",
  :NAME "EAST DUWAMISH GREENBELT",
  :OWNER "DPR",
  :ACQ_DATE "1997-10-21T00:00:00Z"},
 :geometry
 #object[org.locationtech.jts.geom.MultiPolygon 0x1788033d "MULTIPOLYGON (((-122.28235899499998 47.525165833000074, -122.28168484899999 47.525164389000054, -122.28168307199996 47.52516438600003, -122.28145050399996 47.52516388600003, -122.281450582 47.525147437000044, -122.28092621899998 47.525146310000025, -122.28092721899998 47.52433074500004, -122.28129411599997 47.52433142700005, -122.28247185299995 47.52433360800006, -122.28270306299999 47.52433403400005, -122.28270162899997 47.52516656600005, -122.28235899499998 47.525165833000074)))"]}

And the parks are defined as geographic regions.

Drawing a map

Seattle coordinates

(def Seattle-center
  [47.608013 -122.335167])

The map we will create is A choropleth, though for now, we will use a fixed color, which is not so informative.

We will enrich every feature (e.g., neighborhood) with data relevant for its visual representation.

(defn enrich-feature [{:as   feature :keys [geometry]}
                      {:keys [tooltip-keys
                              style]}]
  (-> feature
      (update :properties
              (fn [properties]
                (-> properties
                    (assoc :tooltip (-> properties
                                        (select-keys tooltip-keys)
                                        (->> (map (fn [[k v]]
                                                    [:p [:b k] ":  " v]))
                                             (into [:div]))
                                        hiccup/html)
                           :style style))))))
(def neighborhoods-enriched-features
  (-> neighborhoods-geojson
      (charred/read-json {:key-fn keyword})
      :features
      (->> (mapv (fn [feature]
                   (-> feature
                       (enrich-feature
                        {:tooltip-keys [:L_HOOD :S_HOOD]
                         :style {:opacity     0.3
                                 :fillOpacity 0.1
                                 :color      "purple"
                                 :fillColor  "purple"}})))))))

Here is how we may generate a Choroplet map in Leaflet:

(defn choropleth-map [details]
  (delay
    (kind/reagent
     ['(fn [{:keys [provider
                    center
                    enriched-features]}]
         [:div
          {:style {:height "900px"}
           :ref   (fn [el]
                    (let [m (-> js/L
                                (.map el)
                                (.setView (clj->js center)
                                          11))]
                      (-> js/L
                          .-tileLayer
                          (.provider provider)
                          (.addTo m))
                      (-> js/L
                          (.geoJson (clj->js enriched-features)
                                    (clj->js {:style (fn [feature]
                                                       (-> feature
                                                           .-properties
                                                           .-style))}))
                          (.bindTooltip (fn [layer]
                                          (-> layer
                                              .-feature
                                              .-properties
                                              .-tooltip)))
                          (.addTo m))))}])
      details]
     {:reagent/deps [:leaflet]})))

We pick a tile layer provider from leaflet-providers.

(defn Seattle-choropleth-map [enriched-features]
  (choropleth-map
   {:provider "OpenStreetMap.Mapnik"
    :center     Seattle-center
    :enriched-features enriched-features}))

For our basic neighborhoods map:

(delay
  (Seattle-choropleth-map
   neighborhoods-enriched-features))