About

暑假已经被各种活预支得差不多了, 剩下的那么点时间打算全拿去玩. 出去旅游还是要做些小小的计划的吧, 于是看到了懒猫大佬做的一个地图 (EAF 旅游规划地图应用, GitHub: EAF Map).

开源技术方案参考:

  • 地图元素绘制: leaflet.js
  • 当前经纬地址: HTML5 Navigator GeoLocation
  • 地址名称搜索: geopy
  • 高速路径查询: http://router.project-osrm.org
  • 最优路径规划: python-tsp, numpy

from EAF 旅游规划地图应用 (Emacs China)

看了看方案说明, 感觉可以理解并且也不是非常庞大, 于是想着试试看做一下. 刚好试试练习一下最近接触的 CLOG 以及 CLOS.

注: 本文是边写程序边记录的一个类似于写码小吐槽的东西吧. 写了一大半之后发现完全没有任何的逻辑和章法, 总之就是超级乱. 如果最终可能的话, 我大概整理一下发布. (虽然按照我的尿性, 估计是不能了).

使用的技术的简单说明

Common Lisp

你说的对, 但是 Common Lisp 是一门非常强大和先进的函数式编程语言. 它拥有极其灵活和丰富的语法, 支持宏系统, 闭包, 高阶函数, 泛型函数等特性. Common Lisp 有着悠久的历史, 影响了许多现代编程语言的设计, 它拥有强大的标准库和活跃的社区. 虽然 Common Lisp 不如其他语言流行, 但它在人工智能, 金融等领域仍有应用. 总的来说, Common Lisp 是一门优雅, 高效, 富有表现力的编程语言, 值得每一位程序员去学习和探索.

Generated by Claude via Slack

一些注记
  • 按照 ANSI 标准, Common Lisp 是一门面向对象的编程语言 (来源, 可能是指 “These new goals included stricter standardization for portability, an object-oriented programming system, a condition system, iteration facilities, and a way to handle large character sets.” HyperSpec)

    并且忘了在哪里看到的, Common Lisp 并不是一门纯函数式的编程语言. 或者说, 它不只能够函数式编程, 所以只把它看作是函数式语言也是不太合理的.

  • 标准库之类的, 感觉大多数的库都非常的硬核, 比我年龄都大的库, 十多年前停止更新还能跑的库…
  • 人工智能应该并没有了, 金融, 应该是被 Clojure 吧… 不过我不是很了解. 并且 Lisp 应该并不是只有这样的专家系统的逻辑人工智能吧.

CLOG

CLOG The Common Lisp Omnificent GUI, 是一个依托网页技术的 Common Lisp 图形交互界面库. 不过我确实可以稍微理解一些那些想要用 JavaScript 来写各种后端程序的想法: 毕竟 UI 可以用 Electron 这样的巨大玩意来用网页折腾, “写网页为啥不用 JS”, 大概就是这样的想法吧…

Leaflet

一个 JS 的地图库, 可以往网页里面插入地图显示. 我目前的想法是这样的: 用 CLOG 给 Leaflet 写一个 Wrapper, 然后将其嵌入到之后的使用之中.

注: 在构建该部分代码的时候, 主要参考的是 clog-typeahead 项目.

声明库

在 Common Lisp 里面使用 ASDF 来进行库 (system) 的定义 在文件 clog-leaflet.asd 中:

;;; clog-leaflet.asd

(asdf:defsystem #:clog-leaflet
  :description "The CLOG wrapper for Leaflet."
  :author "凉凉 <https://github.com/li-yiyang>"
  :license "MIT"
  :version "0.0.0"
  :depends-on (#:clog)
  :components ((:file "clog-leaflet")))

于是在 clog-leaflet.lisp 文件中就可以定义一个对应的包 (系统为包的集合):

(defpackage #:clog-leaflet
  (:use #:cl #:clog)
  (:export init-leaflet))

(in-package :clog-leaflet)

显然, 想要使用 Leaflet, 那么还需要载入对应的库和文件 init-leaflet 吧:

;;; Implementation - Load Leaflet Lib

(defparameter *leaflet-css-path*
  "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
  "Path/URL to Leaflet CSS file.")

(defparameter *leaflet-js-path*
  "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
  "Path/URL to Leaflet Javascript file.")

(defparameter *leaflet-namespace*
  "window.LeafletNameSpace"
  "Name Space to store the Leaflet objects.")

(defun init-leaflet (body)
  "Load the Leaflet Javascripts and opens Leaflet Namespace. 
Only called once on first attach."
  (check-type body clog:clog-obj)
  (load-css    (html-document body)
               *leaflet-css-path* :load-only-once T)
  (load-script (html-document body)
               *leaflet-js-path*  :load-only-once T :wait-for-load NIL)
  (js-execute body (format NIL "~A = {};" *leaflet-namespace*)))
对 init-leaflet 的说明
  • load-cssload-script 即为 Preparing your page | Leaflet 中的说明, 但是这里有一个比较麻烦的地方就是, 在 CLOG 中的 load-script 是非阻塞的…

    可以猜猜看这会导致啥坑呢… 答案是: 如果在 load-script 之后, 立刻执行和载入的库相关的函数, 可能并不会如你所愿 – 因为还没加载呢…

    注: 但是没道理啊… 查文档可以知道, 对于 load-script 有一个 key: wait-for-load, 可以等待加载完毕再返回值… 不知道为什么之前会出现那样的 bug.

    哦, 一个难绷的地方出现了, 我竟然要将 :wait-for-load 设为 NIL 才能等待加载. 稍微有一些反直觉了属于是.

  • 以及在 load-cssload-script 中, 实际上还可以载入本地的文件, 一想到这个, 在一车小孩里面写代码的我就很生气… 为什么我一开始要用在线资源… 现在高铁一直在过山洞, 就因为卡在这个地方所以导致我直接没法继续了.

    所以开摆, 刚好下了点 (指约 20G 的) 番. 有理有据.

  • js-execute 的代码: 大概是这么实现的: js-execute 通过 clog-connection:execute 中的 websocket-driver:send 的方法来和前端发信, 然后前端通过 boot.js 来进行相应的一个操作吧.

    但是这样就可能会出现因为执行环境是局域环境而没法定义全局变量来用了. 大概原理是这样吧… 简单来说就是对于:

    (js-execute body "var x = 0;")
    ;; codes
    (js-execute body "console.log(x);")
    

    这样的代码并不能实现 (报错为 Can't find variable: x)

    我的一个比较投机取巧的解决方法就是在 window 里面放变量, 但是可能不一定好使就是了.

CLOG-Leaflet 的设计

一个菜鸟的瞎说
  • 在学习的时候, 感觉大部分的时候都是一种 “非常细微” 的 JRPG 的感觉: 比如把一个东西分类, 然后再细分变成了非常简单的一个东西. 于是学习就变成了听着 NPC 的鬼话先去定义了一个叫登龙剑的变量, 然后做了一个平 A 挥击的函数.

    虽然也不是说坏吧, 毕竟细节和各种东西都掌握得很好. 只是这么做了下去, 就感觉挺没意思的. 能在海拉鲁大地耍流氓, 谁救塞尔达啊

  • 但是看了一些大佬在设计程序的时候, 往往干的事情不是解决 JRPG 里面的问题, 而是构造 JRPG 的一个框架的感觉. 比如可以参考 EAF 的框架设计, 以及 lsp-bridge 的一个构造 (没错, 两个都是 懒猫 大佬的框架).

    估计以后可以往这些角度去思考, 拿到一个问题, 该怎么分解…

这里应当先思考该如何制作一个 CLOG-Leaflet 的一个框架, 先思考一下我有啥需求:

  • 我需要构造一个 clog-leaflet-map, 因为 Leaflet 相当于是在一个 div 元素上绑定一个地图元素, 所以我想要这样的对象应当有基本的 div 的操作, 并且还要提供 Leaflet 的接口.

    于是一个比较合理的想法就是, 令 clog-leaflet-mapclog-div 的一个子类.

  • 为了绘制一个地图, 需要指定的量:
  • 其他的需求为在图上做标记 Marker, 对于一个标记, 需要指定的量:
    • 可以指定一个 Popup 来说明点击弹出的量

那么如果就是这么简单的话, 只需要构建几个类即可:

Helper Functions

之前试过一些 ClojureScript 的代码, 感觉里面的一些注记符号和小函数还是很方便的, 比如如果想要把一些将 Common Lisp 中的值转换为 JS 的值的转换函数, 方便使用 js-execute:

;;; Helper Function

(defgeneric ->js (obj)
  (:documentation "Convert basic Common Lisp Datatype into JS data."))

(defmethod ->js (obj)
  "Default will be simply FORMAT function."
  (format NIL "~A" obj))

(defmethod ->js ((obj string))
  "Trun String into JS String."
  (format NIL "~S" obj))

(defmethod ->js ((obj list))
  "Trun List into JS Array."
  (format NIL "[~A]"
          (reduce (lambda (converted new)
                    (format NIL "~A, ~A" converted  (->js new)))
                  (rest obj) :initial-value (->js (first obj)))))

(defun plist->js (plst)
  "Convert plist (:p v) into JS code {p: v}."
  (declare (list plst))
  (labels ((iter (lst)
             (if (null lst)
                 ""
                 (format NIL "~A: ~A, ~A"
                         (string-downcase (string (first lst)))
                         (->js (second lst))
                         (iter (cddr lst))))))
    (format NIL "{~A}" (iter plst))))

CLOG-Leaflet-Map

CLOG-Leaflet-Map 类定义

定义一个继承自 clog-div 的类, 以及其对应的一堆功能:

;;; Implementation - clog-leaflet-map

(defclass clog-leaflet-map (clog-div)
  ((options :initarg :options
            :initform '(:latitute 0 :longtitute 0 :zoom 1)
            :reader options))
  (:documentation "Leaflet map Object. "))

(defgeneric create-leaflet-map (clog-obj &key options style class html-id)
  (:documentation "Create a new clog-leaflet-map as child of CLOG-OBJ."))

(defmethod create-leaflet-map ((obj clog:clog-obj)
                               &key (options '(:zoom 13
                                               :center (0 0)))
                                 (style NIL)
                                 (class NIL)
                                 (html-id NIL))
  (let ((new-div (create-div obj :html-id html-id
                                 :class class
                                 :style style)))
    (setf (width new-div) "400px"
          (height new-div) "400px")
    (attach-leaflet-map new-div options)
    (change-class new-div 'clog-leaflet-map)))
Leaflet Bindings with CLOG-Leaflet-Map 进行一个绑定
(defun attach-leaflet-map (obj options)
  "Attach Leaflet to OBJ, which should be clog-leaflet-map object."
  (let ((id (html-id obj)))
    (js-execute obj (format NIL "~A['MAP~A'] = L.map('~A', ~A);"
                            *leaflet-namespace* id id (plist->js options)))))

可以用一个非常简单的过程来测试一下:

(progn
  (initialize (lambda (body)
                (init-leaflet body)
                (create-leaflet-map body)))
  (open-browser))
其他的一些操作

一些辅助函数:

;;; Helper functions for Leaflet map
(defmethod ->js ((obj clog-leaflet-map))
  "Turn OBJ into JS variable name."
  (format NIL "~A['MAP~A']" *leaflet-namespace* (html-id obj)))

将 CLOG-Leaflet-Map 的示例转换为其对应的 JavaScript 的变量名来引用.

于是你应当可以看到如下的一个结果:

/_img/lisp/misc/clog-leaflet/clog-create-leaf-map-screenshot.png

于是接下来的任务就是往里面塞 Tile Layer, 塞 Marker 之类的东西了.

CLOG-Leaflet-Layer

我也不知道别的有啥 Tile Layer 的地址, 所以就用示例里面提供的默认的地址, 为了防止在代码里面出现又臭又长的地址字符串, 所以这里提前定义一下:

(defparameter *default-tile-layer-url*
  "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
  "Default Tile Layer URL.")

(defparameter *default-tile-layer-attribution*
  "&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
  "Default Tile Layer Attribution.")

接下来定义 Layer 类:

(defclass clog-leaflet-layer ()
  ((layer-name   :initarg :layer-name
                 :initform (format NIL "~A['LAYER~A']"
                                   *leaflet-namespace*
                                   (gensym))
                 :accessor layer-name)
   (url-template :initarg :url-template
                 :initform *default-tile-layer-url*
                 :reader url-template)
   (attribution  :initarg :attribution
                 :initform *default-tile-layer-attribution*
                 :reader attribution))
  (:documentation "Tile Layer for Leaflet Map."))

(defmethod ->js ((layer clog-leaflet-layer))
  "Turn LAYER into JS variable name."
  (layer-name layer))

(defgeneric create-leaflet-layer (clog-obj &key url-template attribution)
  (:documentation "Create a new Tile Layer and add to it."))

(defmethod create-leaflet-layer ((map clog-leaflet-map)
                                 &key (url-template *default-tile-layer-url*)
                                   (attribution *default-tile-layer-attribution*))
  "Attach a new Layer for the CLOG-Leaflet-Map object."
  (let* ((layer (make-instance 'clog-leaflet-layer
                               :url-template url-template
                               :attribution  attribution)))
    (js-execute map                     ; Create and store layer
                (format NIL "~A = L.tileLayer('~A', {attribution: '~A'})"
                        (->js layer)
                        (url-template layer)
                        (attribution layer)))
    (js-execute map                     ; Add Layer to Map
                (format NIL "~A.addTo(~A);"
                        (->js layer)
                        (->js map)))
    layer))

于是这个时候的测试代码就变成了:

(progn
  (initialize
   (lambda (body)
     (init-leaflet body)
     (let* ((map (create-leaflet-map body))
            (layer (create-leaflet-layer map)))
       (setf *map*   map
             *layer* layer)))))

(注: 其中用了两个外部变量来帮助我调试. )

你应该能够看到类似的结果:

/_img/lisp/misc/clog-leaflet/clog-add-tile-layer-screenshot.png

接下来慢慢添加一些标记类

理论上来说应该就可以开始写一些功能性的代码了, 不过现在无所谓. 更加具体的代码我觉得还是写一个小小仓库来包装一下吧.

MACROS 赛高!

在尝试写 Wrapper 的时候, 遇到了一个很无聊的事情, 就是我发现有很多无聊的代码都要重复构造, 这样就比较烦.

突然发现, 如果我写一个 MACROS 来自动帮我构代码的话, 那么岂不是非常的方便?

不过我是刚开始接触类似的做法, 并不是很会, 可能写出的结果并不是很漂亮.

需求

我发现我需要经常写如下的代码:

(js-execute map (format NIL "~A.setView(~A)"
                        (->js map) (->js center) ...))

并且这些代码基本都是一样的, 这就让我构造代码的时候感到很无聊, 毕竟这个时候就变成了我在抄写文档的感觉了. 并且抄得多, 功能还少, 这可太亏了.

于是我决定写一个 MACRO, 其应当可以快速地生成下面的代码.

实现

需要实现的核心功能是生成: <OBJ>.<JS-METHOD>(<PARAMETERS>) 这样的 JS 调用. 那么需要生成的模版应该如下: =”~A.<JS-METHOD>(~A, ~A, …)”= 这样模版.

在调用的形式上, 我希望能够越简单越好, 比如说:

(generate-js-wrapper
 clog-leaflet-map
 (set-zoom (zoom)
           :documentation "Set zoom.")
 (set-view (center zoom)
           :documentation "Set view."
           :js-name "setView"))

于是形式应该变成类似于 (defmacro generate-js-wrapper (class &rest definitions)), 然后对于 definitions 中的形式应为 (method paras . options).

那么问题应当提取为:

  • definitions 提取信息
  • 然后构造函数定义
(defmacro generate-js-wrapper (class &rest definitions)
  "The `DEFINITIONS' should be like:

   ((method-name (parameters) options))

for example, if you want to make a method for `clog-leflet-map'
called `zoom-in', then you just need to do:

  (generate-js-wrapper 'clog-leaflet-map 
                        (zoom-in (&optional (ratio 1)) 
                          :doc \"set zoom\"))

It will be converted into `~A.setZoom(~A)' like code.
The parameters can be just like normal function definition.

For options, they could be:
+ :doc for :documentation
+ :js for JavaScript method call

This macro is in early stage and it is limited."
  (labels
      ((generate (definition)
         (let* ((method  (first  definition))
                (paras   (second definition))
                (options (cddr   definition))
                (flat-para (loop for para in paras
                                 if (listp para)
                                   collect (first para)
                                 else if (and (not (eq para '&optional))
                                              (not (eq para '&key)))
                                        collect para))
                (js-name (getf options :js (->js method)))
                (docstr  (getf options :doc
                               (format NIL "<~A>.~A~A" class js-name
                                       (wrapper flat-para
                                                :left "("
                                                :right ")")))))
           `((defgeneric ,method ,(cons 'obj
                                   (mapcar (lambda (para)
                                             (if (listp para)
                                                 (first para)
                                                 para))
                                    paras))
               (:documentation ,docstr))
             (defmethod ,method ,(cons (list 'obj class) paras)
               (js-execute
                obj
                ,(append
                  `(format NIL
                           ,(format NIL "~A.~A~A" "~A" js-name
                                    (wrapper flat-para :left "(" :right ")"
                                                       :fn (lambda (-) "~A"))))
                  (cons `(->js obj)
                        (mapcar (lambda (para) `(->js ,para)) flat-para)))))))))
    (cons 'progn (apply #'append (mapcar #'generate definitions)))))

大概就是这样, 我现在觉得可以试试看把 &key 定义加到里面去. 这样估计会更加完善. 但是说不好, 还是暂时不加了.

The End definitely NOT

接下来的东西我觉得还是扔到 GitHub 上去建一个仓库来折腾一下比较好. 仓库地址在 clog-leaflet 这里. 估计会是一个非常简陋的一个仓库吧.

Nominatim

Nominatim (from the Latin, 'by name') is a tool to search OSM data by name and address and to generate synthetic addresses of OSM points (reverse geocoding).

官方文档 是这么介绍自己的. 这里我的需求就是两点:

  • search 能够根据名称搜索坐标
  • reverse 根据坐标反查

在这个时候, 用到的技术栈就是网络访问了. 实际上我感觉这时候做的事情更像是无脑调包, 毕竟我没有处理网络错误的一些报错, 也没有对得到的数据进行处理之类的事情…

实际上核心就是一个 GET 方法, 一个函数 fetch 调用, 一个 JSON 解析, 就, 完事了.

(defun fetch (method parameters &key (proxy *nominatim-proxy*))
  "The general fetch method for wrap API usage."
  (let* ((url (quri:render-uri
               (quri:make-uri
                :defaults *nominatim-host*
                :path method
                :query (merge-alist parameters *nominatim-fetch-parameters*))))
         (res (if proxy
                  (dex:get url :proxy proxy)
                  (dex:get url))))
    (yason:parse res)))

其中用到了:

在写完这个部分之后, 我的想法发生了一些微妙的改变, 之前一个小功能一个库的做法, 让我有一种接下来要干的事情很可能就会变成注水的担忧, 所以我决定将已经建好的库, 用作这个项目, 而之前写的这些 package, 都作为最终项目 (system) 的子模块.

前面写了的就也懒得改了.

GeoJSON

GeoJSON is a geospatial data interchange format based on JavaScript Object Notation (JSON). It defines several types of JSON objects and the manner in which they are combined to represent data about geographic features, their properties, and their spatial extents. GeoJSON uses a geographic coordinate reference system, World Geodetic System 1984, and units of decimal degrees.

from RFC 7946

想法是让 GeoJSON 的对象能够像是 Common Lisp 的对象一样被简单地处理, 这样的话就比较方便之后写程序去操控了.

Dowsing Rod

我决定将 这个小玩具 命名为 Dowsing Rod (翻译过来为寻龙尺). 但是我并不咋知道这种伪科学, 最早是在唐人街探案里面的唐仁用的那个东西看到的, 不是有这样的说法吗? 搞笑角色是无敌的, 所以用寻龙尺这种搞笑名字来命名, 不是很好玩吗?

科学和伪科学

为啥一个臭学物理的会想找这种伪科学来命名呢? 这有啥关系, 嘿嘿. 我突然想到我电磁学老师的微信名称: “永动机”.

果然就是知道不可能才更加浪漫吗?

开发过程中的小小想法

  • 模块化真爽, 以及模块化的调试的一个简单想法.

    (虽然肯定不会是啥新的东西就是了)

    • 把功能分割成多个小模块, 每个模块里面就暴露几个核心方法给其他的模块调用.
    • 开发的时候把 clog-leaflet 这样的 package 命名为 clog-leaflet-dev, 然后在 clog-leaflet-dev 里面就随便玩了. 在正式使用或者结束的时候, 就 (delete-package :clog-leaflet-dev) 把模块给删掉.
  • 写图形界面和用户模块真麻烦
    • 在写用户端函数的时候, 总是有一种万一用户传入了傻逼参数的话该怎么办, 或者是想要一个类似于超级完美的通用函数, 可以接受任意的参数输入…

      算了, 就随便乱写吧…

  • 代码的排版和文学编程…
    • 因为 Lisp 的语法缘故, 就必然会有层层的嵌套关系, 欸, 这个时候该怎么设置嵌套和缩进就很难受了. 太长的行看起来就很难受, 但是换行太多就没法在我的笔记本上看完全部的代码.

      (欸, 屏幕小还得不得不让软件分屏开看参考…)

    • 文学编程虽然很好, 但是目前我所做的也只是给代码进行分段和分块, 并标记不同的地方有什么功能, 和一些简单的说明, 实际上还是没法看出更加吸引人的地方.