| 1 |
;; poi.lisp |
|---|
| 2 |
|
|---|
| 3 |
;; Klassen und Funktione fÃŒr die "Points of Information", die fÃŒr die |
|---|
| 4 |
;; Quadratmeter-Datenbank gespeichert werden. |
|---|
| 5 |
|
|---|
| 6 |
(in-package :bos.m2) |
|---|
| 7 |
|
|---|
| 8 |
;;; POI-Anwendungsklassen und Konstruktoren |
|---|
| 9 |
|
|---|
| 10 |
;;; textual-attributes-mixin |
|---|
| 11 |
(defpersistent-class textual-attributes-mixin () |
|---|
| 12 |
((title :initform (make-string-hash-table) |
|---|
| 13 |
:documentation "angezeigter name") |
|---|
| 14 |
(subtitle :initform (make-string-hash-table) |
|---|
| 15 |
:documentation "unterschrift") |
|---|
| 16 |
(description :initform (make-string-hash-table) |
|---|
| 17 |
:documentation "beschreibungstext"))) |
|---|
| 18 |
|
|---|
| 19 |
(defmethod initialize-instance :after ((obj textual-attributes-mixin) |
|---|
| 20 |
&key language title subtitle description) |
|---|
| 21 |
(update-textual-attributes obj language |
|---|
| 22 |
:title title |
|---|
| 23 |
:subtitle subtitle |
|---|
| 24 |
:description description)) |
|---|
| 25 |
|
|---|
| 26 |
(deftransaction update-textual-attributes (obj language &key title subtitle description) |
|---|
| 27 |
(when title |
|---|
| 28 |
(setf (slot-string obj 'title language) title)) |
|---|
| 29 |
(when subtitle |
|---|
| 30 |
(setf (slot-string obj 'subtitle language) subtitle)) |
|---|
| 31 |
(when description |
|---|
| 32 |
(setf (slot-string obj 'description language) description)) |
|---|
| 33 |
obj) |
|---|
| 34 |
|
|---|
| 35 |
(deftransaction very-shallow-copy-textual-attributes (from to) |
|---|
| 36 |
"Useful for making the TEXTUAL-ATTRIBUTES of FROM available to TO, |
|---|
| 37 |
before FROM is deleted. Please note that copying is so shallow that |
|---|
| 38 |
FROM and TO must not both continue to exist." |
|---|
| 39 |
(setf (slot-value to 'title) (slot-value from 'title) |
|---|
| 40 |
(slot-value to 'subtitle) (slot-value from 'subtitle) |
|---|
| 41 |
(slot-value to 'description) (slot-value from 'description)) |
|---|
| 42 |
to) |
|---|
| 43 |
|
|---|
| 44 |
;;; poi-medium |
|---|
| 45 |
(defpersistent-class poi-medium (textual-attributes-mixin) |
|---|
| 46 |
((poi :reader poi-medium-poi :initarg :poi))) |
|---|
| 47 |
|
|---|
| 48 |
(deftransaction make-poi-medium (class-name &rest rest &key language title subtitle description poi initargs) |
|---|
| 49 |
(declare (ignore poi initargs)) |
|---|
| 50 |
(assert (if (or title subtitle description) language t) nil |
|---|
| 51 |
"language needs to be specified, if any of title, subtitle |
|---|
| 52 |
or description is given") |
|---|
| 53 |
(apply #'make-instance class-name (remove-keys '(:initargs) rest))) |
|---|
| 54 |
|
|---|
| 55 |
(defmethod initialize-instance :after ((poi-medium poi-medium) &key poi) |
|---|
| 56 |
(when poi |
|---|
| 57 |
(push poi-medium (poi-media poi)))) |
|---|
| 58 |
|
|---|
| 59 |
(defmethod print-object ((object poi-medium) stream) |
|---|
| 60 |
(print-unreadable-object (object stream :type t :identity nil) |
|---|
| 61 |
(format stream "~D" (store-object-id object)))) |
|---|
| 62 |
|
|---|
| 63 |
(defgeneric poi-medium-creation-time (medium) |
|---|
| 64 |
(:method ((medium blob)) |
|---|
| 65 |
(blob-timestamp medium))) |
|---|
| 66 |
|
|---|
| 67 |
(defmethod destroy-object :before ((poi-medium poi-medium)) |
|---|
| 68 |
(with-slots (poi) poi-medium |
|---|
| 69 |
(when poi |
|---|
| 70 |
(setf (poi-media poi) (remove poi-medium (poi-media poi)))))) |
|---|
| 71 |
|
|---|
| 72 |
;;; poi-image |
|---|
| 73 |
(defpersistent-class poi-image (store-image poi-medium) |
|---|
| 74 |
()) |
|---|
| 75 |
|
|---|
| 76 |
;;; poi-airal |
|---|
| 77 |
(defpersistent-class poi-airal (store-image poi-medium) |
|---|
| 78 |
()) |
|---|
| 79 |
|
|---|
| 80 |
;;; poi-panorama |
|---|
| 81 |
(defpersistent-class poi-panorama (store-image poi-medium) |
|---|
| 82 |
()) |
|---|
| 83 |
|
|---|
| 84 |
;;; poi-movie |
|---|
| 85 |
(defpersistent-class poi-movie (poi-medium) |
|---|
| 86 |
((url :accessor poi-movie-url :initarg :url :initform nil) |
|---|
| 87 |
(created :initform (error "need :created initarg when creating poi-medium") |
|---|
| 88 |
:initarg :created |
|---|
| 89 |
:reader poi-medium-creation-time))) |
|---|
| 90 |
|
|---|
| 91 |
;;; poi |
|---|
| 92 |
(defpersistent-class poi (textual-attributes-mixin) |
|---|
| 93 |
((name |
|---|
| 94 |
:reader poi-name :initarg :name |
|---|
| 95 |
:index-type string-unique-index |
|---|
| 96 |
:index-reader find-poi :index-values all-pois |
|---|
| 97 |
:documentation "symbolischer name") |
|---|
| 98 |
(published-web |
|---|
| 99 |
:accessor poi-published-web :initarg :published-web :initform nil |
|---|
| 100 |
:documentation "wenn dieses flag nil ist, wird der poi auf der Website nicht angezeigt") |
|---|
| 101 |
(published-earth |
|---|
| 102 |
:accessor poi-published-earth :initarg :published-earth :initform nil |
|---|
| 103 |
:documentation "wenn dieses flag nil ist, wird der poi in Google Earth nicht angezeigt") |
|---|
| 104 |
(area |
|---|
| 105 |
:accessor poi-area :initarg :area :initform nil |
|---|
| 106 |
:documentation "polygon mit den poi-koordinaten") |
|---|
| 107 |
(icon |
|---|
| 108 |
:accessor poi-icon :initarg :icon :initform "palme" |
|---|
| 109 |
:documentation "name des icons") |
|---|
| 110 |
(media |
|---|
| 111 |
:accessor poi-media :initarg :media :initform nil |
|---|
| 112 |
:documentation "liste aller poi-medien, wie poi-image, poi-airal ...") |
|---|
| 113 |
(lod-min |
|---|
| 114 |
:accessor poi-lod-min :initarg :poi-lod-min :initform 600 |
|---|
| 115 |
:documentation "the lod minimum used in Google Earth"))) |
|---|
| 116 |
|
|---|
| 117 |
|
|---|
| 118 |
(defmethod convert-slot-value-while-restoring ((object poi) (slot-name (eql 'published)) |
|---|
| 119 |
published) |
|---|
| 120 |
(setf (slot-value object 'published-web) published)) |
|---|
| 121 |
|
|---|
| 122 |
(deftransaction make-poi (name &rest rest &key area language title subtitle description) |
|---|
| 123 |
(declare (ignore area)) |
|---|
| 124 |
(assert (if (or title subtitle description) language t) nil |
|---|
| 125 |
"language needs to be specified, if any of title, subtitle |
|---|
| 126 |
or description is given") |
|---|
| 127 |
(apply #'make-instance 'poi :name name rest)) |
|---|
| 128 |
|
|---|
| 129 |
(defmethod destroy-object :before ((poi poi)) |
|---|
| 130 |
(mapc #'delete-object (poi-media poi))) |
|---|
| 131 |
|
|---|
| 132 |
(deftransaction update-poi (poi &key published-web published-earth icon area lod-min) |
|---|
| 133 |
(check-type published-web boolean) |
|---|
| 134 |
(check-type published-earth boolean) |
|---|
| 135 |
(check-type area list) |
|---|
| 136 |
(setf (poi-published-web poi) published-web |
|---|
| 137 |
(poi-published-earth poi) published-earth) |
|---|
| 138 |
(when icon |
|---|
| 139 |
(setf (poi-icon poi) icon)) |
|---|
| 140 |
(when area |
|---|
| 141 |
(setf (poi-area poi) area)) |
|---|
| 142 |
(when lod-min |
|---|
| 143 |
(setf (poi-lod-min poi) (abs lod-min))) |
|---|
| 144 |
poi) |
|---|
| 145 |
|
|---|
| 146 |
(defmethod poi-complete ((poi poi) language) |
|---|
| 147 |
(and (every #'(lambda (slot-name) (slot-string poi slot-name language nil)) '(title subtitle description)) |
|---|
| 148 |
(poi-area poi) |
|---|
| 149 |
(<= 6 (count-if (lambda (medium) (typep medium 'poi-image)) (poi-media poi))) |
|---|
| 150 |
t)) |
|---|
| 151 |
|
|---|
| 152 |
(defmethod poi-center-x ((poi poi)) |
|---|
| 153 |
(first (poi-area poi))) |
|---|
| 154 |
|
|---|
| 155 |
(defmethod poi-center-y ((poi poi)) |
|---|
| 156 |
(second (poi-area poi))) |
|---|
| 157 |
|
|---|
| 158 |
(defun poi-center-lon-lat (poi) |
|---|
| 159 |
(geo-utm:utm-x-y-to-lon-lat (+ +nw-utm-x+ (poi-center-x poi)) (- +nw-utm-y+ (poi-center-y poi)) +utm-zone+ t)) |
|---|
| 160 |
|
|---|
| 161 |
(defmethod (setf poi-media) :after (value (poi poi)) |
|---|
| 162 |
(setf (slot-value poi 'media) (sort (slot-value poi 'media) #'> :key #'poi-medium-creation-time))) |
|---|
| 163 |
|
|---|
| 164 |
;;; POI media are stored in one list - for convenience we provide |
|---|
| 165 |
;;; accessors by type. POI-IMAGES e.g. returns a list of all |
|---|
| 166 |
;;; POI-IMAGES in the same order as they appear in the media list. The |
|---|
| 167 |
;;; second value is a list of corresponding positions in that list. |
|---|
| 168 |
(macrolet ((define-poi-medium-reader (name) |
|---|
| 169 |
(let ((type (find-symbol (subseq (symbol-name name) 0 (1- (length (symbol-name name))))))) |
|---|
| 170 |
(assert type) |
|---|
| 171 |
`(defun ,name (poi) |
|---|
| 172 |
;; this surely could be optimized |
|---|
| 173 |
(let ((media-of-type (remove-if-not (lambda (medium) (typep medium ',type)) (poi-media poi)))) |
|---|
| 174 |
(values media-of-type |
|---|
| 175 |
(mapcar (lambda (medium) (position medium (poi-media poi))) media-of-type))))))) |
|---|
| 176 |
(define-poi-medium-reader poi-images) |
|---|
| 177 |
(define-poi-medium-reader poi-airals) |
|---|
| 178 |
(define-poi-medium-reader poi-panoramas) |
|---|
| 179 |
(define-poi-medium-reader poi-movies)) |
|---|
| 180 |
|
|---|
| 181 |
(defun poi-sat-images (poi) |
|---|
| 182 |
"We use the 6 last (oldest) images of POI as images for the |
|---|
| 183 |
satellite application." |
|---|
| 184 |
(multiple-value-bind (images positions) |
|---|
| 185 |
(poi-images poi) |
|---|
| 186 |
(let* ((length (length images)) |
|---|
| 187 |
(start (max 0 (- length 6)))) |
|---|
| 188 |
(values (subseq images start length) |
|---|
| 189 |
(subseq positions start length))))) |
|---|
| 190 |
|
|---|
| 191 |
;;; Provides for the shifting of images in the edit-poi handler. |
|---|
| 192 |
;;; Exchanges (nth index (poi-sat-images poi)) with |
|---|
| 193 |
;;; (nth (1+ index) (poi-sat-images poi)). |
|---|
| 194 |
(deftransaction poi-sat-images-exchange-neighbours (poi index) |
|---|
| 195 |
(check-type index (mod 6)) |
|---|
| 196 |
(multiple-value-bind (images positions) |
|---|
| 197 |
(poi-images poi) |
|---|
| 198 |
(declare (ignore images)) |
|---|
| 199 |
(let ((media-index-a (nth index positions)) |
|---|
| 200 |
(media-index-b (nth (mod (1+ index) 6) positions))) |
|---|
| 201 |
(rotatef (nth media-index-a (poi-media poi)) |
|---|
| 202 |
(nth media-index-b (poi-media poi)))))) |
|---|
| 203 |
|
|---|
| 204 |
(defun make-poi-javascript (language) |
|---|
| 205 |
"Erzeugt das POI-Javascript fÃŒr das Infosystem" |
|---|
| 206 |
(with-output-to-string (*standard-output*) |
|---|
| 207 |
(format t "var anzahlSponsoren = ~D;~%" (number-of-paying-sponsors)) |
|---|
| 208 |
(format t "var anzahlVerkauft = ~D;~%" (number-of-sold-sqm)) |
|---|
| 209 |
(format t "var pois = new Array;~%") |
|---|
| 210 |
(dolist (poi (sort (remove-if #'(lambda (poi) (or (not (poi-complete poi language)) |
|---|
| 211 |
(not (poi-published-web poi)))) |
|---|
| 212 |
(store-objects-with-class 'poi)) |
|---|
| 213 |
#'(lambda (poi-1 poi-2) (string-lessp (slot-string poi-1 'title language) (slot-string poi-2 'title language))))) |
|---|
| 214 |
(format t " |
|---|
| 215 |
var poi = { id: ~S, |
|---|
| 216 |
symbol: ~S, |
|---|
| 217 |
icon: ~S, |
|---|
| 218 |
name: ~S, |
|---|
| 219 |
untertitel: ~S, |
|---|
| 220 |
text: ~S, |
|---|
| 221 |
x: ~D, |
|---|
| 222 |
y: ~D, |
|---|
| 223 |
thumbnail: ~D, |
|---|
| 224 |
published_earth: ~:[false~;true~] |
|---|
| 225 |
}; |
|---|
| 226 |
" |
|---|
| 227 |
(store-object-id poi) |
|---|
| 228 |
(poi-name poi) |
|---|
| 229 |
(poi-icon poi) |
|---|
| 230 |
(slot-string poi 'title language) |
|---|
| 231 |
(slot-string poi 'subtitle language) |
|---|
| 232 |
(escape-nl (slot-string poi 'description language)) |
|---|
| 233 |
(poi-center-x poi) |
|---|
| 234 |
(poi-center-y poi) |
|---|
| 235 |
(length (poi-sat-images poi)) |
|---|
| 236 |
(poi-published-earth poi)) |
|---|
| 237 |
(format t "poi.thumbnail = ~D;~%" (length (poi-sat-images poi))) |
|---|
| 238 |
(when (poi-airals poi) |
|---|
| 239 |
|
|---|
| 240 |
(format t "poi.luftbild = ~D;~%" (store-object-id (first (poi-airals poi))))) |
|---|
| 241 |
(when (poi-panoramas poi) |
|---|
| 242 |
(format t "poi.panoramas = [ ~{~D~^, ~} ];~%" (mapcar #'store-object-id (poi-panoramas poi)))) |
|---|
| 243 |
(when (poi-movies poi) |
|---|
| 244 |
(format t "poi.movies = [ ~{~S~^, ~} ];~%" |
|---|
| 245 |
(mapcar #'(lambda (movie) |
|---|
| 246 |
(assert (stringp (poi-movie-url movie)) nil |
|---|
| 247 |
"POI-MOVIE-URL of ~S is ~S, but should be a string" |
|---|
| 248 |
movie (poi-movie-url movie)) |
|---|
| 249 |
(poi-movie-url movie)) |
|---|
| 250 |
(poi-movies poi)))) |
|---|
| 251 |
(loop for slot-name in '(title subtitle description) |
|---|
| 252 |
for javascript-name in '("imageueberschrift" "imageuntertitel" "imagetext") |
|---|
| 253 |
for slot-values = (mapcar (lambda (image) |
|---|
| 254 |
(escape-nl (slot-string image slot-name language))) |
|---|
| 255 |
(poi-sat-images poi)) |
|---|
| 256 |
when slot-values |
|---|
| 257 |
do (format t "poi.~A = [ ~{~S~^, ~} ];~%" javascript-name slot-values)) |
|---|
| 258 |
(format t "pois.push(poi);~%")) |
|---|
| 259 |
(dolist (allocation-area (remove-if (complement #'allocation-area-active-p) (class-instances 'allocation-area))) |
|---|
| 260 |
(destructuring-bind (x y) (allocation-area-center allocation-area) |
|---|
| 261 |
(format t "poi = [];~%") |
|---|
| 262 |
(format t "poi['icon'] = ~S;~%" "sale") |
|---|
| 263 |
(format t "poi['name'] = ~S;~%" "Zu Verkaufen") |
|---|
| 264 |
(format t "poi['x'] = ~D;~%" x) |
|---|
| 265 |
(format t "poi['y'] = ~D;~%" y) |
|---|
| 266 |
(format t "poi['thumbnail'] = 0;~%") |
|---|
| 267 |
(format t "pois.push(poi);~%"))))) |
|---|
| 268 |
|
|---|
| 269 |
;;; poi schema evolution aids |
|---|
| 270 |
|
|---|
| 271 |
(define-modify-macro appendf (&rest args) append) |
|---|
| 272 |
|
|---|
| 273 |
(defmethod convert-slot-value-while-restoring ((poi poi) (slot-name (eql 'airals)) value) |
|---|
| 274 |
(unless (slot-boundp poi 'media) (setf (slot-value poi 'media) nil)) |
|---|
| 275 |
(appendf (slot-value poi 'media) (mapcar (lambda (obj) (change-class obj 'poi-airal :poi poi)) value))) |
|---|
| 276 |
|
|---|
| 277 |
(defmethod convert-slot-value-while-restoring ((poi poi) (slot-name (eql 'images)) value) |
|---|
| 278 |
(unless (slot-boundp poi 'media) (setf (slot-value poi 'media) nil)) |
|---|
| 279 |
(appendf (slot-value poi 'media) (mapcar (lambda (obj) (change-class obj 'poi-image :poi poi)) value))) |
|---|
| 280 |
|
|---|
| 281 |
(defmethod convert-slot-value-while-restoring ((poi poi) (slot-name (eql 'movies)) value) |
|---|
| 282 |
(unless (slot-boundp poi 'media) (setf (slot-value poi 'media) nil)) |
|---|
| 283 |
(appendf (slot-value poi 'media) (mapcar (lambda (url) `(poi-movie :url ,url :poi ,poi)) value))) |
|---|
| 284 |
|
|---|
| 285 |
(defmethod convert-slot-value-while-restoring ((poi poi) (slot-name (eql 'panoramas)) value) |
|---|
| 286 |
(unless (slot-boundp poi 'media) (setf (slot-value poi 'media) nil)) |
|---|
| 287 |
(appendf (slot-value poi 'media) (mapcar (lambda (obj) (change-class obj 'poi-panorama :poi poi)) value))) |
|---|
| 288 |
|
|---|
| 289 |
(defun pois-sanity-check () |
|---|
| 290 |
(labels ((poi-sanity-check (poi) |
|---|
| 291 |
(dolist (medium (poi-media poi)) |
|---|
| 292 |
(unless (eq poi (poi-medium-poi medium)) |
|---|
| 293 |
(warn "~s does not point to ~s" medium poi))) |
|---|
| 294 |
(dolist (movie (poi-movies poi)) |
|---|
| 295 |
(unless (stringp (poi-movie-url movie)) |
|---|
| 296 |
(warn "~s has a url of ~s" movie (poi-movie-url movie)))))) |
|---|
| 297 |
(mapc #'poi-sanity-check (class-instances 'poi)) |
|---|
| 298 |
(values))) |
|---|
| 299 |
|
|---|
| 300 |
(defvar *language* "en" |
|---|
| 301 |
"Current language for JSON encoding") |
|---|
| 302 |
|
|---|
| 303 |
(defmethod json:encode ((object symbol) &optional stream) |
|---|
| 304 |
(json:encode (string-downcase (symbol-name object)) stream)) |
|---|
| 305 |
|
|---|
| 306 |
(defmethod json:encode-slots progn ((object store-object)) |
|---|
| 307 |
(json:encode-object-element "id" (store-object-id object))) |
|---|
| 308 |
|
|---|
| 309 |
(defmethod json:encode-slots progn ((poi poi)) |
|---|
| 310 |
(json:encode-object-elements |
|---|
| 311 |
"name" (poi-name poi) |
|---|
| 312 |
"icon" (poi-icon poi) |
|---|
| 313 |
"x" (poi-center-x poi) |
|---|
| 314 |
"y" (poi-center-y poi)) |
|---|
| 315 |
(json:with-object-element ("media") |
|---|
| 316 |
(json:with-array () |
|---|
| 317 |
(dolist (medium (poi-media poi)) |
|---|
| 318 |
(json:encode-object medium))))) |
|---|
| 319 |
|
|---|
| 320 |
(defmethod json:encode-slots progn ((blob blob)) |
|---|
| 321 |
(json:encode-object-elements |
|---|
| 322 |
"type" (blob-type blob) |
|---|
| 323 |
"timestamp" (format-date-time (blob-timestamp blob) :mail-style t))) |
|---|
| 324 |
|
|---|
| 325 |
(defmethod json:encode-slots progn ((image store-image)) |
|---|
| 326 |
(json:encode-object-elements |
|---|
| 327 |
"name" (store-image-name image) |
|---|
| 328 |
"width" (store-image-width image) |
|---|
| 329 |
"height" (store-image-height image))) |
|---|
| 330 |
|
|---|
| 331 |
(defmethod json:encode-slots progn ((object bos.m2::textual-attributes-mixin)) |
|---|
| 332 |
(dolist (field '(title subtitle description)) |
|---|
| 333 |
(let ((string (slot-string object field *language*))) |
|---|
| 334 |
(unless (equal "" string) |
|---|
| 335 |
(json:encode-object-element field string))))) |
|---|
| 336 |
|
|---|
| 337 |
(defmethod json:encode-slots progn ((medium poi-medium)) |
|---|
| 338 |
(json:encode-object-element |
|---|
| 339 |
"mediumType" |
|---|
| 340 |
(cl-ppcre:regex-replace "^poi-" (string-downcase (class-name (class-of medium))) ""))) |
|---|
| 341 |
|
|---|
| 342 |
(defmethod json:encode-slots progn ((movie poi-movie)) |
|---|
| 343 |
(json:encode-object-elements |
|---|
| 344 |
"url" (poi-movie-url movie) |
|---|
| 345 |
"timestamp" (format-date-time (poi-medium-creation-time movie) :mail-style t))) |
|---|
| 346 |
|
|---|
| 347 |
(defun poi-as-json (poi language) |
|---|
| 348 |
(let ((*language* language)) |
|---|
| 349 |
(json:encode-object poi))) |
|---|
| 350 |
|
|---|
| 351 |
(defun pois-as-json (language) |
|---|
| 352 |
(json:with-array () |
|---|
| 353 |
(dolist (poi (class-instances 'poi)) |
|---|
| 354 |
(when (poi-complete poi language) |
|---|
| 355 |
(poi-as-json poi language))))) |
|---|