1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
|
## -*- tcl -*-
# # ## ### ##### ######## ############# ######################
## (c) 2022-2023 Andreas Kupries
# @@ Meta Begin
# Package map::point::store::memory 0.1
# Meta author {Andreas Kupries}
# Meta location https://core.tcl.tk/tklib
# Meta platform tcl
# Meta summary In-memory store for geo/point definitions
# Meta description In-memory store for geo/point definitions, with
# Meta description memoized calculation of extended attributes.
# Meta description Base data is taken from a backing store.
# Meta description Anything API-compatible to map::point::store::fs
# Meta subject {center, geo/point}
# Meta subject {diameter, geo/point}
# Meta subject {geo/point pixels, zoom}
# Meta subject {geo/point, center}
# Meta subject {geo/point, diameter}
# Meta subject {geo/point, memory store}
# Meta subject {geo/point, perimeter length}
# Meta subject {length, geo/point, perimeter}
# Meta subject {memory store, geo/point}
# Meta subject {perimeter length, geo/point}
# Meta subject {pixels, zoom, geo/point}
# Meta subject {store, geo/point, memory}
# Meta subject {zoom, geo/point pixels}
# Meta require {Tcl 8.6-}
# Meta require debug
# Meta require debug::caller
# Meta require {map::slippy 0.8}
# Meta require snit
# @@ Meta End
package provide map::point::store::memory 0.1
# # ## ### ##### ######## ############# ######################
## API
#
## <class> OBJ backend-store
#
## <obj> ids -> list (id...)
## <obj> get ID -> dict (name, geo, kind)
## <obj> visible GEOBOX ZOOM -> list (id...)
## <obj> pixels ID ZOOM -> list (point...)
#
# # ## ### ##### ######## ############# ######################
## Requirements
package require Tcl 8.6-
#
# ;# Tcllib
package require debug ;# - Narrative Tracing
package require debug::caller ;#
package require map::slippy 0.8 ;# - Map utilities
package require snit ;# - OO system
# # ## ### ##### ######## ############# ######################
## Ensemble setup.
namespace eval map { namespace export point ; namespace ensemble create }
namespace eval map::point { namespace export store ; namespace ensemble create }
namespace eval map::point::store { namespace export memory ; namespace ensemble create }
debug level tklib/map/point/store/memory
debug prefix tklib/map/point/store/memory {<[pid]> [debug caller] | }
# # ## ### ##### ######## ############# ######################
snit::type ::map::point::store::memory {
# ..................................................................
## System configuration
# . . .. ... ..... ........ ............. .....................
## State
#
# - Backing store, command prefix
# - Pixel store :: dict (id -> zoom -> point)
# - Attribute store :: dict (id -> attr)
# attr :: dict ("names" -> list (string...)
# "geo" -> geo
# "kind" -> string)
variable mystore {}
variable mypixels {}
variable myattr {}
# Visibility data based on zoom level (in the lower levels clusters begin to replace points).
# Note that cluster geo information is stored in myattr also.
variable myids {} ;# :: dict (zoom -> list (id...))
# . . .. ... ..... ........ ............. .....................
## Lifecycle
constructor {store maxzoom} {
debug.tklib/map/point/store/memory {}
set mystore $store
# This package computes the clustering on construction time, from the geo locations found in
# the backing store. A better system would be able to get the clustering directly from the
# store, without any investment of runtime. IOW the clustering would be pre-computed
# somewhere else, ahead of time. Further, the algorithm's core complexity likely is
# O(n**2). More complex data structures (rtree or similar) are needed for a better O.
# This package however is mainly for demonstration purposes, for use on/with data sets where
# the scaling issues of this approach do not appear yet.
$self LoadAndCluster $maxzoom
return
}
destructor {
debug.tklib/map/point/store/memory {}
return
}
# . . .. ... ..... ........ ............. .....................
## API
delegate method * to mystore except get ;# ids, visible
method get {id} {
debug.tklib/map/point/store/memory {}
return [dict get $myattr $id]
}
method pixels {id zoom} {
debug.tklib/map/point/store/memory {}
return [dict get $mypixels $id $zoom]
}
method visible {geobox zoom} {
debug.tklib/map/point/store/memory {}
# visible, taking zoom into account - i.e. deliver clusters as necessary.
set ids {}
foreach id [dict get $myids $zoom] {
set geo [dict get $myattr $id geo]
#puts $id//$geo
if {![map slippy geo box inside $geobox $geo]} continue
lappend ids $id
}
set ids [lsort -unique $ids]
#puts (($ids))
return $ids
}
# . . .. ... ..... ........ ............. .....................
## Helpers
method LoadAndCluster {maxzoom} {
debug.tklib/map/point/store/memory {}
# pins :: dict (zoom -> list (pin...))
# pin :: list (point id)
# point :: list (x y)
foreach id [DO ids] {
set attr [DO get $id]
set geo [dict get $attr geo]
set bbox [map slippy geo bbox $geo]
dict set attr bbox $bbox
dict set myattr $id $attr
# Compute points per zoom level, collect for clustering, and fill base layer of the
# pixel cache.
for {set z 0} {$z <= $maxzoom} {incr z} {
set point [map slippy geo 2point $z $geo]
dict lappend pins $z [list $point $id]
dict set mypixels $id $z $point
}
}
# Pins gives us the pixel data (including id), per zoom level. (Origin was per
# location). This is now clustered and then converted to the final pixel and visibility
# data.
dict for {z pins} $pins {
dict set myids $z [Cluster $z $pins]
}
return
}
proc Cluster {z pins} {
# pins :: list (pin...)
# pin :: list (point id)
# point :: list (x y)
upvar 1 counter counter mypixels mypixels myattr myattr
# At levels with suitable detail we forego any kind of clustering.
# We simply use the points as they are.
if {$z >= 18} {
return [lsort -unique [lmap pin $pins {
# pin :: list (point id)
lindex $pin end
}]]
}
set clusters {} ;# :: dict (point -> list(point...))
set map {} ;# :: dict (point -> id)
foreach pin [lsort -dict $pins] {
lassign $pin point id
dict set map $point $id
if {[FindCluster $clusters $point center]} {
# Extend found cluster
# - ATTENTION - This may move the cluster center.
set points [dict get $clusters $center]
dict unset clusters $center
lappend points $point
set center [map slippy point center-list $points]
dict set clusters $center $points
} else {
# Start a new cluster
dict set clusters $point [list $point]
}
}
# Convert the clusters into fake geo locations, pixel data, and the list of ids to consider
# at the level.
dict for {center points} $clusters {
if {[llength $points] < 2} {
# Cluster is actually single point, center is the point.
# Reuse the point itself.
set id [lindex [dict get $map $center] 0]
} else {
# Multiple points are an actual cluster.
set id c/[incr counter]
# Create the necessary attribute data for this fake.
dict set myattr $id names {}
dict set myattr $id geo [map slippy point 2geo $z $center]
dict set myattr $id kind cluster
dict set myattr $id count [llength $points] ;# Data for default cluster style
# And extend the pixel cache for it
dict set mypixels $id $z $center
}
lappend ids $id
}
return $ids
}
proc FindCluster {clusters point cv} {
upvar $cv center
set best Inf
dict for {centroid points} $clusters {
set d [map slippy point distance $centroid $point]
if {$d >= $best} continue
set best $d
set center $centroid
}
# Check if we can place the pointo into the nearest cluster, and return the result of that
# check. The chosen threshold is 1.5x the circle radius for default cluster style in
# `map::point::map-display`. This reduces the probability of neighbouring clusters visually
# overlapping (too much).
return [expr {$best <= 30}]
}
proc DO {args} {
debug.tklib/map/point/store/memory {}
upvar 1 mystore mystore
return [uplevel #0 [list {*}$mystore {*}$args]]
}
# . . .. ... ..... ........ ............. .....................
}
# # ## ### ##### ######## ############# ######################
return
|