普通视图

独立博客自省问卷

2024年10月15日 01:24

写在前面

想先说点其他的认识,最初 博客新解 - 印记 这篇博文就曾勾着我写篇类似的东西,各种原因没能成文,在此补上。

先说下自己的博客历程,[[domain-adventure|域名惊魂]] 这篇简要说过域名的来历,推算起来域名注册于 2019 年 2 月,彼时自己还是个“闲散”的学生,但其实自己搞静态博客的时间要更早一些,如本站 footer 中所写的那样,2016 年,大学二年级,记得当时自己还在组会的时候分享过建站的经验(现在回想,自己对此都是一知半解,同学们肯定更是听的一头雾水),那时候好像 github pages 网络条件也还可以,可惜的是,第一个站并没有留下什么文章,仓库中显示的留到现在的最早的一篇博文是 2018 年。

但如果不是狭义的“独立博客”的话,我的博客起始年份可以拉的更长,[[old-blog-archive|老博客归档]] 里还搜集了一些,忆往昔,颇有些羡慕当年的自己,当时还不是个实打实的 i 人,敢于在朋友面前“献丑”,没什么“偶像”包袱,想说什么说什么,2010 年,[[i-was-talking-in-my-sleep|我在说梦话]],我甚至不记得自己还是过歌迷群的管理。也很难想象 2011 年,16 岁的自己为什么能写出来 痛苦抹掉我的棱角,服服贴贴的做着不属于自己的自己。 , [[actually|事实上]] 这篇,可以回溯到 2012 年。它们首发平台为 QQ 空间(狗头),看了 QQ 空间的记录,这些文章(姑且称之为文章吧,现在看更有点微博和朋友圈的味道),可能阅读量只有 4~5 个人,但当年的自己感觉自己就是同学中的 KOL,回头望去当年的自己俨然一个少年英雄(自认为)。

收回来,19 年之后,建立这个站点的 slogan,i 味已经很重了:

在没人看到的地方写写画画。

期间自己做些技术笔记,偶尔抒抒胸臆,后面的一个转折点是加入了某中文博客群组,此事感谢 @Bruce,结交了一些朋友,这对我的博客思路和我对博客的看法有很大的影响。比如 @1900 帮我站找 bug,/docs/ 提到的受 @陈仓颉 影响建立了数字花园,眼红 @DemoChen 的书摘页面(施工拖延中)。自己的站虽然还是没上评论系统,但感觉已不再是一个孤岛。

“独立博客博主”这个群体很有意思,大家投入的时间各不相同,有人每天给自己的站点梳妆打扮,有人高强度出现在大家的评论区里,各有特点,又都很相似。给大家总结出共性还真有些难度,在这里,我想宏观做这么一个分类:横向博主纵向博主,就像研究、工作中的横向纵向类似。

  • 横向博主,就是像我一样的大多数人, 对世界和每个领域各有看法,一篇见闻,一篇随想,一篇技术笔记,在自己的一亩三分地上怡然自得。

  • 纵向博主,往往在一个垂直领域颇有造诣,可能是哲学、摄影、骑行,乃至技术,他们配享大 V 之位,只是或不想、或没能,读此等博主的文章每次都有薅到羊毛之感。

虽然每次看完“纵向博主”们的文章后,会有短暂的自我怀疑,怀疑自己写出内容的价值,多了些提笔的心理负担,但最终这些会落脚到逼自己多看几本书上,还是蛮正向的。

最使人舒服的是,在独立博客的圈子里,因为是严格意义上的去中心化,虽然各自站点访问量有大有小,关注度有高有低,但博主之间不会据此形成“阶级”,我不会因为自己网站每天只有几个人访问,而在网站每天有几千人的博主面前感到自卑,其他站点更不会因为影响力而影响到我的读者的数量,与博主们彼此之间就是同好的关系,这是一种很难得的公平,在其他任一平台上是无法达到的。

最近 Follow 让一个个独立的内容个体,(在我视角里)史无前例的被联系了起来,使独立博客未来有了新的可能,在 Follow 中,不管是订阅人数还是浏览记录都给了我极大的情绪价值,后面促使我多献献丑。

独立博客自省问卷

前面想说的说了个七七八八,言归正传。

1、你的博客更新频率是多少?

随缘更新,主要取决于工作强度如何,在季更和日更间横跳。

2、你的博客上次更新是什么时候?

本周写了三篇,但本年只写了六篇。

3、你的博客文章是原创的吗?

复制别人的文章完全对不起建立这个初衷。

4、你觉得自己的文章对他人有帮助吗?

以自我陶醉为主,即使是技术部分可能更多是表达我的一些思路,因为懒步骤往往不详细,想来对人有帮助也有限。

5、你上次换博客主题/程序是什么时候?

[[blog-diary-with-astro|博客折腾日记]] 有记录,上一次是从 Hugo 到 Astro,自己写的主题一直用到现在,但会有修修补补。

6、你上一次捣腾博客主题代码是什么时候?

就在刚才,但已完成差不多,本篇文章是从 obsidian 编辑并发出的,不打开 IDE 也就不想着更新样式了。

7、你会对博客主题进行二次开发?

感恩前端工业的蓬勃发展,使得自己这种半瓶水可以自己做个主题。

8、你多久打开自己博客自我陶醉一次?

同一,取决于工作强度,只有 0 或 无数次。

9、你近期对自己博客域名什么感受?

总体满意,但因为手里还有 chenyuhang.com/chenyuhang.cn 何时舍弃此域名也未可知。

10、你每天都会看网站的流量统计吗?

偶尔到 cloudflare 中看看,此次更新把统计脚本也取消了,因为好像对自己没啥作用。

11、你通过博客的广告赚到钱了吗?

0 收入,但除了域名自己也没什么投入。

12、你去浏览别人的博客/网站主要为什么?

对我来说,就是一种信息获取、消遣,因为自己没有微博、抖音,B 站刷不出东西的时候,除了新闻,朋友们站点是不错的去处。

13、看到别人分享了一篇文章,你打开第一反应是什么?

技术文章会根据自己的需求,生活分享类的文章必看。

14、你觉得博客哪方面更重要?

内容,内容,内容!

当然界面不能太让人不适应。

15、近期通过写博客有哪些新收获?

想不出新的收获,因为自己最近更新了,内容管理的方式,[[idea-for-content-manage|一种内容管理的新想法]],更新内容变得更有动力了。

博文创建工具

2023年7月26日 16:16

简短来说

相比WordPress这种博客平台,使用Git-based/Markdown管理自己的内容,毫无疑问可控性更强,在各个平台迁移更灵活,但也增大了创建一篇博文的心智负担。虽然有hugo new这种命令行工具,但分类和标签还是需要手动编辑fontmatters。

之前也做过类似的尝试,用fish shell做了个简单的工具。

function blog
    cd /dest/to/blog
    set -Ux BLOG_SLUG $argv[2]

    set c $argv[1]
    argparse t/tag -- $argv
    or return

    set md (string split '"' (hugo new posts/$c/$argv[2].md) -f2)
    sed -i '' 5s/\"\"/\"$c\"/ $md
    if set -ql _flag_tag
        sed -i '' 6s/\"\"/\"$argv[3]\"/ $md
    end
    echo $md
    open $md

end

思路依然是通过slug创建文章,预先把现有的博文分类列出来,然后通过Tab完成补全,比在frontmatters里改,方便一些。

complete -c blog  -xa "comments  drafts  shares  stories  thoughs  thoughts  translations " -n '__fish_is_first_arg'

最近使用astro重构了博客,新增了i18n支持和短链等功能,需要副标题和Id等更多的抬头,这样就需要修改这个脚本,但是fish脚本维护起来有点费劲,所以决定用node脚本重构一下。

新的功能需求

  • 可交互的命令行工具
  • 通过在线翻译或者调用大模型的方式翻译标题
  • 自己生成短链、根据英文标题生成slug
  • 分类和标签通过现有的博文获取
  • 图片管理

实施路径

实现可交互的命令行工具

选择技术栈:

inquirer 用于交互命令,commander 用于解析命令行参数,初期考虑两个命令。

  1. create 创建博文
  2. upload 上传图片, 一个option --file -f 用于判断是上传文件还是直接上传剪贴板的文件。
  3. activate 切换当前编辑的文章(类似conda环境的管理)
  4. translate 翻译指定文章 //TODO

实现标题的自动翻译

经过实验,直接调用翻译效果很差,最后选择还是用openai-3.5-turbo(azure),根据输入的标题先判断一下是中英文,然后给出相反的prompt,根据输入的标题完成翻译。

{
    "messages": [
        {
            "role": "system",
            "content": "你是一个中英翻译助手,将下面这个博客文章的标题翻译成${dest}文。"
        },
        { "role": "user", "content": "${title}" }
    ]
}

这样,不管是中文还是英文标题,只需要输入一次就好,另一个交给AI。

实现短链的自动生成

考虑到自己的博文数量不会太大,只用三位区分大小写的字母来生成短链,这样可以保证短链的唯一性,而且不会太长。 使用short-uuid包,生成uuid,但只取前三位,然后判断该Id是否已经存在,如果存在,重新生成。

const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
const translator = uuid(chars)
let id = translator.new().slice(0, 3)
while (keyExits(id)) {
    id = translator.new().slice(0, 3)
}

短链最终使用Vercel的redirects来实现,这样只须更改根目录下的vercel.json,重新部署即可。

{
    "redirects": [{ "source": "/${id}", "destination": "/posts/${slug}" }]
}

图片管理

至于为什么不用现有的图床管理工具。

  1. 无法与现有的工作流完全适配
  2. 文件命名不符合自己的习惯

于是这里多做了这一步,在前面生成文章的时候,存一个全局的环境变量,这样图片管理的时候就在根据文章slug生成的文件夹下操作。

  • 已有的图片文件保存到一个指定的目录下,通过命令上传到对象存储。
  • 在剪贴板中的图片,首先通过pngpaste保存到本地,然后再上传到对象存储。

最后通过pbcopy将获取图片的链接,直接送到剪贴板里,这样就可以直接粘贴到博文中了,就像下面这样。

blog upload test-jpeg
URL: https://static.yuhang.ch/blog/blog-creation-tool/test-jpeg.jpeg

对于一个在剪切板中的截图来说,在这种模式下,自己只需要给他想一个slug就好了,工具帮助你把他传到对象储存的指定目录下,然后把链接放到剪贴板里。

结尾

这篇文章来自这个工具。

TMS逆向到WMTS

2023年3月17日 08:55

有一个 TMS 的瓦片数据源,需要“模拟”一个 WMTS 服务出来,需要怎么做?

这个情况,其实有现成的基础设施或者说轮子来解决,比如各个地图服务器等,.net生态也有 tile-map-service-net5这种开源工具,这个问题之所以是个问题在于两个限制条件。

  1. 所用客户端不支持加载XYZ/TMS格式的数据,只能加载 WMS 和 WMTS 格式的数据。
  2. 使用的数据是切好片的 TMS 结构的数据。
  3. 客户端不方便依赖外部地图服务器。

模仿资源链接

一些我们熟悉的互联网地图,用的都是 XYZ 或者 TMS的方式,例如OSM、Google Map 和Mapbox 等等,从之前的栅格瓦片到如今矢量瓦片更为常见,想要用TMS “模仿” WMTS 的请求格式,需要先了解他们直接有啥不一样。

XYZ(slippy map tilename)

  • 256*256 像素的图片
  • 每个 Zoom 层级是一个文件夹,每个Column 是个子文件夹,每个瓦片是一个用 Row 命名的图片文件
  • 格式类似/zoom/x/y.png
  • x 在 (180°W ~ 180°E),y 在(85.0511°N ~85.0551°S),Y轴从顶部向下。

可以从Openlayers TileDebug Example,看到一个简单的 XYZ 瓦片的示例。

TMS

TMS的 Wiki wikipedia没涉及什么细节、osgeo-specification 只描述了协议的一些应用细节。反倒是 geoserver docs 关于 TMS 的部分写的更务实一些。 TMS 是 WMTS 的前身,也是 OSGeo 制定的标准。

请求形如: http://host-name/tms/1.0.0/layer-name/0/0/0.png

为了支持多种文件格式和空间参考系统,也可以指定多个参数: http://host-name/tms/1.0.0/layer-name@griset-id@format-extension/z/x/y

TMS 标准的瓦片格网从左下角开始,Y轴从底部向上。有的地图服务器,例如geoserver,就支持一个额外的参数flipY=true 来翻转 Y 坐标,这样就可以兼容Y 轴从顶部向下的服务类型,比如 WMTS 和 XYZ。

tms-grid

WMTS

WMTS 相较上述两个直观的协议,内容更复杂,支持的场景也更多。2010 年由OGC第一次公布。起始在此之前,1997年 Allan Doyle的论文“Www mapping framework” 之后,OGC就开始谋划网络地图相关标准的制定了。在 WMTS 之前,最早的,也是应用最广泛的网络地图服务标准是 WMS。因为WMS每个请求是依据用户地图缩放级别和屏幕大小来组织地图响应,这些响应大小各异,在多核CPU还没那么普及的当年,这种按需实时生成地图的方式非常奢侈, 同时想要提升响应速度非常困难。于是有开发者开始尝试预先生成瓦片的方式,于是涌现出了许多方案,前面提到的 TMS 就是其中的一个,后面WMTS 应运而生,开始被广泛应用。 WMTS 支持键值对(kvp)和 Restful 的方式对请求参数编码。

KVP 形如:

<baseUrl>/layer=<full layer name>&style={style}&tilematrixset={TileMatrixSet}}&Service=WMTS&Request=GetTile&Version=1.0.0&Format=<imageFormat>&TileMatrix={TileMatrix}&TileCol={TileCol}&TileRow={TileRow}

Restful形如:

<baseUrl>/<full layer name>/{style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}?format=<imageFormat>

由于是栅格瓦片,这里只需要找到 XYZ 与 瓦片矩阵和瓦片行列号的对应关系就好了。

  • TileMatrix
  • TileRow
  • TileCol

这里的瓦片行列号是从左上角开始的,Y 轴从顶部向下。

wmts-grid

这样,就找到了 TMS 与 WMTS 的各参数对应关系,接下来就是如何把 TMS 转换成 WMTS 的请求了,如下:

  • TileRow = 2^zoom - 1 - y = (1 << zoom) - 1 - y
  • TileCol = x
  • TileMatrix = zoom

在不考虑其他空间参考的情况下,缩放层级对应瓦片矩阵,x对应瓦片列号,y取反(因为起始方向相反)。

模拟一个 WMTS Capabilities 描述文件

WMTS规范的要求,几乎可以说是细到头发丝,所以各个客户端,不管是Web端的 Openlayers ,还是桌面端的QGIS或Skyline等,都支持直接解析Capabilities 描述文件,然后根据描述文件的内容来选择图层、样式和空间参考,所以我们这里还要模拟一个 WMTS Capabilities 描述文件出来。

Capabilities 描述文件的构成

一个WMTS Capabilities描述文件的例子可以在opengis schema,天地图山东找到。

Capabilities 描述文件的内容非常多,这里只列出一些重要的部分(忽略标题,联系方式等):

OperationsMetadata:
    - GetCapabilities >> 获取 Capabilities 描述文件的方式
    - GetTile >> 获取瓦片的方式

Contents:
    - Layer
      - boundingBox >> 图层的经纬度范围
      - Style
      - TileMatrixSetLink >> 图层支持的空间参考
      - TileMatrixSet >> 空间参考
      - TileMatrixSetLimits >> 空间参考的缩放层级范围
      - TileMatrixLimits >> 每个缩放层级的瓦片行列号范围
    - Style
    - TileMatrixSet
      - TileMatrix

关键的部分就是 boundingBox、TileMatrixSetLimits、TileMatrixLimits ,只需要根据图层的空间参考和缩放层级来计算出来就好了。

boundingBox 的计算比较简单,就是图层的经纬度范围,这里就不展开了。

TileMatrixSetLimits 的计算比较简单,就是图层的空间参考的缩放层级范围。

TileMatrixLimits 的计算比较复杂,可以只在图层范围比较小的时候再弄,全球地图就没必要了,需要根据图层的空间参考和缩放层级来计算出来,下面是一段伪代码(4326 到 3857)。

FUNCTION GetTileRange(minLon, maxLon, minLat, maxLat, zoom, tile_size = 256)

minLonRad = minLon * PI / 180
maxLonRad = maxLon * PI / 180
minLatRad = minLat * PI / 180
maxLatRad = maxLat * PI / 180

tile_min_x = Floor((minLonRad + PI) / (2 * PI) * Pow(2, zoom))
tile_max_x = Floor((maxLonRad + PI) / (2 * PI) * Pow(2, zoom))
tile_min_y = Floor((PI - Log(Tan(minLatRad) + 1 / Cos(minLatRad))) / (2 * PI) * Pow(2, zoom))
tile_max_y = Floor((PI - Log(Tan(maxLatRad) + 1 / Cos(maxLatRad))) / (2 * PI) * Pow(2, zoom))

// adjust tile range based on tile size
tile_min_x = Floor((double)tile_min_x * tile_size / 256)
tile_max_x = Ceiling((double)tile_max_x * tile_size / 256)
tile_min_y = Floor((double)tile_min_y * tile_size / 256)
tile_max_y = Ceiling((double)tile_max_y * tile_size / 256)

RETURN  (tile_min_x, tile_max_x, tile_min_y, tile_max_y)

生成 WMTS Capabilities 描述文件

生成一个最小化的 WMTS Capabilities 描述文件,把上面的关键部分填充上,之后构造一个指向标准描述文件地址的的 Restful 风格的 URL。

后话

以上是一个简单的 TMS 转 WMTS 的思路,实际上还有很多细节需要考虑,比如空间参考的转换,缩放层级的转换,瓦片行列号的转换,瓦片的格式转换等等。 期间也踩了一些坑,感觉这部分更有意思。

第一部分,很快就参考 tile-map-service-net5 的思路,完成了 y >> tileRow的转换。代码在WebMercator.cs ,其实在StackOverflow上也有人问过这个问题,是有答案的,但我还是选择从软件里找答案,因为这样自己心里更踏实。

第二部分就很头大,首先模拟出了资源链接,构建了一个简单的 XML,但是在目标客户端上不能直接加载,很直接的想到了通过标准服务测试一下,然后哪来一个Capabilities 描述文件来修改。己想首先在比较熟悉的Openlayers上测试,然后再去修改Capabilities 描述文件。Openlayers的加载方式还是很灵活的,在没有Capabilities 描述文件的情况下,可以直接通过配置参数访问。

// fetch the WMTS Capabilities parse to the capabilities
const options = optionsFromCapabilities(capabilities, {
    layer: 'nurc:Pk50095',
    matrixSet: 'EPSG:900913',
    format: 'image/png',
    style: 'default'
})
const wmts_layer = new TileLayer({
    opacity: 1,
    source: new WMTS(options)
})

很遗憾,瓦片没有加载上,甚至networks里没有发送请求。于是又去另一个WMTS相关的例子哪里,自定义了一个 TileGrid,然后把瓦片的行列号转换成了3857的行列号,这时候可以加载了。

const projection = getProjection('EPSG:3857')
const projectionExtent = projection.getExtent()
const size = getWidth(projectionExtent) / 256
const resolutions = new Array(31)
const matrixIds = new Array(31)
for (let z = 0; z < 31; ++z) {
    // generate resolutions and matrixIds arrays for this WMTS
    resolutions[z] = size / Math.pow(2, z)
    matrixIds[z] = `EPSG:900913:${z}`
}
var wmtsTileGrid = new WMTSTileGrid({
    origin: getTopLeft(projectionExtent),
    resolutions: resolutions,
    matrixIds: matrixIds
})

在确认了是 TileGrid 的问题之后,首先将自己生成的TileGrid与Openlayers从Capabilities解析出来的TileGrid进行对比。发现自己生成的TileGrid有一些字段是空的,于是挨个测试,最后发现设置fullTileRanges_extent_两个内部参数为空时,影像可以加载。

去翻OL源码,发现fullTileRanges_extent_getFullTileRange中被用到。

也就是说,当fullTileRanges_extent_为空时,getFullTileRange会返回一个空的范围。

getFullTileRangewithinExtentAndZ中用到了,这里是用来判断当前可视区域是否有该图层的瓦片。 也就是说,当fullTileRanges_extent_为空时,获取不到 TileRangewithinExtentAndZ会一直返回true,这样就会一直加载瓦片了,也就是加载成功的原因。

相反,从Capabilities解析出来的fullTileRanges_extent_指向了错误的TileRange,导致withinExtentAndZ一直返回false,这样就不会加载瓦片了,也就是加载失败的原因。

终于找到了原因,但这里又被骗了。在wmts.js,构造函数上有一行注释:

class WMTS extends TileImage {
    /**
     * @param {Options} options WMTS options.
     */
    constructor(options) {
        // TODO: add support for TileMatrixLimits
    }
}

这使我开始的时候误以为,fullTileRanges_extent_是根据经纬度范围(boundingBox)计算出来的,而不是根据TileMatrixLimits算的,于是乎又检查了一遍boundingBox,确认无误后,才开始着手修改TileMatrixLimits

开始的时候,以为 TileMatrixLimits 是每个层级的瓦片范围,而不是图层的范围,所以没注意到这个参数,这才走了弯路。

写在 2023 年,WMTS 已经不是一个新的协议了,OGC Tile API 已经成为正式标准了,自己对WMTS了解还是半瓶水,真是汗颜😅。

OL Search - 一个 Openlayers API 快速访问拓展

2022年6月16日 10:00

为什么

openlayers的API文档内容是极好的,然而使用起来却一言难尽。

一般的查api的方式有以下两种:

  • 搜索引擎 👉 openlayers + 关键字 👉 打开指定链接
  • 打开api doc页面 👉 搜索关键字 👉 通过搜索结果到达指定结果

OL Search [^1]

OL Search是一款浏览器拓展(目前只上架了Edge add-ons[^2]),可以通过浏览器地址栏快捷搜索openlayers api,步骤如下:

  1. <kbd>control</kbd>+<kbd>L</kbd> 或者 <kbd>cmd</kbd>+<kbd>L</kbd> 进入搜索栏。
  2. 输入ol关键字,<kbd>tab</kbd> 或者 <kbd>space</kbd> 进入 OL Search。
  3. 输入目标api (方法、成员变量、触发器等)的关键字,选择指定链接直达。

实现

主要分三步:

解析api文档

https://openlayers.org/en/latest/apidoc/navigation.tmpl.html

文档的导航栏部分镶嵌了一个HTML,来自上面的地址。 这里本来有两个思路。 一是通过修改openlayers自己的 api build的脚本生成一组与上述HTML内容一致的JSON格式的api文档信息。 但考虑到两点:

  • 后期维护问题,如果这么做,每个小版本更新需要重新更新插件。-插件体积变大。 另一种是直接解析上面的HTML的导航信息文件,这里遇到了问题,因为在浏览器的插件中,backgroud.js里无法访问DOMParser对象,这里走了弯路,最开始曲线救国,通过popup(点击拓展图标显示的小弹窗)加载数据。这种方式缺点很明显,用户安装完插件后无法直接使用,需要点击拓展图标等待索引文件初始化后才能使用。之后找到了一个纯javascript的DOM解析库,才解决了该问题。

模糊搜索

最开始的时候采用硬搜索,自己使用起来都不满意,因为打字偶尔的typo不可避免,因此模糊搜索应该是刚需。 这里参考了mdn-search 的做法,引入了fuse.js 。也做了一些多关键字的增强。 比如在搜索readFeatures这个方法的时候,各种格式例如EsriJSONKMLWKT等都有readFeatures方法,而默认搜索结果WKT在后面,假如我想找WKTreadFeatures的话就会影响体验。 通过fuse.jssearch.$or,实现了多关键字的复合搜索。 这样只需要输入readFeatures wkt 就可以将包含WKT的结果提到第一个候选。

干掉默认推荐

在监听地址栏omnibox内容变化事件的回调函数中,浏览器默认会在你给的推荐结果前面默认加一条默认推荐,其内容是你键入的内容,指向的地址是你拓展的地址加上该内容。默认行为即File not found。 这部分思路来自rust-search-extension ,首先根据用户的键入内容结合搜索结果,将默认推荐设置为原本的第二条结果(真正搜索结果的第一顺位),之后在用户回车后判断该选项是否是默认建议,如果是,则指向真正搜索结果的第一顺位的地址。

最后

希望该工具给重度使用openlayers api doc的各位同仁带来帮助。

[^1]: OL Search repo: https://github.com/yuhangch/ol-search [^2]: Edge Add-ons: https://microsoftedge.microsoft.com/addons/detail/ol-search/feooodhgjmplabaneabphdnbljlelgka

使用openlayers的vector source loader

2022年5月30日 13:58

The vector source's url option is the first choice for loading vector data, but it doesn't work when a special post-processing or loading strategy required.

https://openlayers.org/en/latest/apidoc/module-ol_source_Vector-VectorSource.html

Loading strategy

First, we should learn about loading strategy in openlayers. there are 3 standard loading strategy in openlayers.

  • all: loading all features in a request.
  • bbox: loading features according to current view's extent and resolution.
  • tile: loading features based on a tile grid, difference between all or bbox, it takes a TileGrid as parameter. Obviously, the bbox is the most suitable one for loader, because when we accept the all strategy, the url option seems ok.

Misunderstanding

Suppose such a feature, our data according to zoom level, when zoom changed, we have to request data again for current zoom level.

...
loader:function(extent,resolution,projection){
	console.log("loading data in resolution",resolution);
	getData(resoluton).then(response=>{
		let features = source.getFormat().readFeatures(response);
		source.clear();
		source.addFeatures(features);
	})
}
...

The demo code, we expect loader triggered when view's zoom changed, clear previous features and load new features at new zoom. but when scroll the wheel, It is not the case. The log message show the loader only triggered in the first few times, when we keep increasing the zoom level (resolution), loader is no longer triggered. But why? The extent is the main controller of the loader, when loader(extent...) called, the extent will add to the source's loaded extent (codein Vector.js), so ,if resolution changed but new extent is within loaded extent, loader will not be triggered. It's clear now, the extents in the first few time contain follows, so when we keep increasing zoom level, the vector source doesn't invoke its loader unless the extent exceed loaded extent.

使用Openlayers制作一个水平滚动地图

2022年5月12日 11:42

We usually use 2d map, actually, there are many avaliable web map library such like openlayers, leaflet, or mapbox-gl-js. I'll introduce a method to make a horizontal only map in openlayers. To control map horizontal only, we have to hook these interactions: pan, wheel scroll zoom. The default interaction openlayers use above can be found in follow link:

disable default interaction

The first step, we disable the default interaction of map.

const map = new Map({
  ...
  interactions: defaultInteractions({
    dragPan: false,
    mouseWheelZoom: false,
    doubleClickZoom: false
  })
  ...
}

After apply this option, the map can't be controlled anymore, this what we suppose.

hook interaction

drag pan

We first create a custom pan interaction extented from DragPan. The default interaction implement 3 method to handle Drag Event, Pointer Up, Pointer Down event. The Drag Event handler contains the coordinate compute. in other word, we need overide a new handleDragEvent .

class Drag extends DragPan {
  constructor() {
    super();
    this.handleDragEvent = function (mapBrowserEvent) {
      ...
          const delta = [
            this.lastCentroid[0] - centroid[0],
            // centroid[1] - this.lastCentroid[1],
            0
          ];
     ...
    }

The centroid second element storage the y coordinate, thus ,we comment the line about y delta and set zero to it.

const map = new Map({
...
interactions: defaultInteractions({
  dragPan: false,
  mouseWheelZoom: false,
  doubleClickZoom: false
}).extend([new Drag()]),
...
})

Add the custom drag interaction after defaultInteractions funtion, and our map now can be paned use mouse drag.

mouse wheel zoom

According the drag pan section, we can easily found the coordinate compute line of the MouseWheelZoom. They appearing at L187-L189, do a little tweak in handleEvent method:

const coordinate = mapBrowserEvent.coordinate
const horizontalCoordinate = [coordinate[0], 0]
this.lastAnchor_ = horizontalCoordinate

Same as dragPan, we add custom MouseWheelZoom interaction Zoom after default interactions.

const map = new Map({
...
interactions: defaultInteractions({
  dragPan: false,
  mouseWheelZoom: false,
  doubleClickZoom: false
}).extend([new Drag(),new Zoom]),
...
})

Now our map can zoom use mouse wheel, and it only work in horizontal direction.

helix 爽点与痛点

2022年4月25日 11:13

helix editor : https://helix-editor.com/

一个 rust 写的命令行 vim-like 的编辑器(上面👆有简单的演示视频就不截图了)。前几天在 ytb 上刷到的,尝试了几天有爽点也有痛点。

自称“后现代”,更像是调侃那些自称“现代”的编辑器。

所谓 vim-like ,键位继承自 Vim 和 Kakoune ,了解 Vim 可以直接上手,(熟悉的命令大部分也能用比如 :vs )但操作逻辑又有不同,是即爽又痛:

比如想 dd ,V 的时候会很难受 :在 helix 的按键是 x 选中行,而 d 可以替换 x 的功能。helix 中 w ,b 等会默认选择文本,因此 dw 要变成 wd 。

至于 Multiple selections ,之前没用过其他的就谈不了体验了。(类似 idea 里 option 下拉?,如果是的话那确实还挺好用的)

至于爽点:

对于 vscode 来说,直接命令行启动,不用 code . 等窗口弹出来。

对于 vim/nvim 来说,你不需要考虑 XXX-complete ,XXX-line ,fzf 还是 leaderf ,helix 提供了一揽子支持。

自带的 file-picker ,buffer-picker 的设计又很符合我的审美,不花里胡哨,简单够用。

lsp 、tree-sitter 支持良好,经常需要编辑的 json ,toml lsp 配置简单。试了试在 rust-analyzer 下写 rust ,居然还挺好用。(我还是选择 IDE🙃️

基本功能节制、够用、易用,但另一面是几乎没啥拓展性,在文档中没看到什么 extension/plugin 的字样。

对我来说,之前一般用 vscode 来编辑简单文本,helix 未来应该会是编辑简单文本的首选,但痛点也很痛,与 vim 键位的一些差别有时会精神分裂:

dd uu xd

于是又去 nvim 尝试配置 helix 样式的 file-picker ,buffer-picker (然后放弃了,编辑个文本又不是不能用

顺便问问大家有没有类似得编辑器?

  • Vim-support,not LIKE
  • Built-in language server support.
  • Syntax highlighting and code editing using Tree-sitter.
  • Built with XXX. No Electron. No VimScript. No JavaScript.
  • Runs in a terminal.

Javascript Shapefile/kml/geojson 转换

2021年1月16日 14:23

三个需求

  • geojson -> shapefile 并下载
  • geojson -> kml 并下载
  • Shapefile (zipped) -> geojson

geojson构建工具

这里选择常用的Javascript的几何计算类库[turfjs/turf]

使用cdn引入:

<script src="https://unpkg.com/@turf/turf/turf.min.js"></script>
<script>
    var bbox = turf.bbox(features)
</script>

或者:

npm install @turf/turf
import * as turf from '@turf/turf'

以折线为例:

let line_string = turf.lineString(
    [
        [-24, 63, 1],
        [-23, 60, 2],
        [-25, 65, 3],
        [-20, 69, 4]
    ],
    { name: 'line 1' }
)
let geojson_object = turf.featureCollection([line_string])

打印对象如下:

{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "properties": {
                "name": "line 1"
            },
            "geometry": {
                "type": "LineString",
                "coordinates": [
                    [-24, 63, 1],
                    [-23, 60, 2],
                    [-25, 65, 3],
                    [-20, 69, 4]
                ]
            }
        }
    ]
}

geojson 转 shapefile

使用[mapbox/shp-write]

使用npm安装:

npm install --save shp-write

或者直接引入,之后直接使用shpwrite变量:

<script src='https://unpkg.com/shp-write@latest/shpwrite.js'>

API很直观:

import shpwrite from 'shp-write'

// (optional) set names for feature types and zipped folder
var options = {
    folder: 'myshapes',
    types: {
        point: 'mypoints',
        polygon: 'mypolygons',
        line: 'mylines'
    }
}
// a GeoJSON bridge for features
shpwrite.download(geojson_object, options)

这里需注意一个问题,因为该包长时间没人维护,目前使用会出现以下问题:

Error: This method has been removed in JSZip 3.0, please check the upgrade guide.

参考[issue 48],将原shpwrite.js文件修改如下:

// ##### replace this:
var generateOptions = { compression: 'STORE' }

if (!process.browser) {
    generateOptions.type = 'nodebuffer'
}

return zip.generate(generateOptions)

// ##### with this:
var generateOptions = { compression: 'STORE', type: 'base64' }

if (!process.browser) {
    generateOptions.type = 'nodebuffer'
}

return zip.generateAsync(generateOptions)

// ##### and this:
module.exports = function (gj, options) {
    var content = zip(gj, options)
    location.href = 'data:application/zip;base64,' + content
}

// ##### with this:
module.exports = function (gj, options) {
    zip(gj, options).then(function (content) {
        location.href = 'data:application/zip;base64,' + content
    })
}

geojson转kml

使用[mapbox/tokml]包和[eligray/FileSaver]文件下载包

npm安装:

npm install --save tokml file-saver

使用cdn引入:

<script src='https://unpkg.com/tokml@0.4.0/tokml.js'>
<script src='https://unpkg.com/file-saver@2.0.0-rc.2/dist/FileSaver.js'>

使用如下:

var kml_doc = tokml(geojson_object, {
    documentName: 'doc name',
    documentDescription: 'doc description'
})
var file_name = 'polyline'
var kml_file = new File([kml_doc], `${file_name}.kml`, {
    type: 'text/xml;charset=utf-8'
})
// FileSaver.saveAs()
saveAs(kml_file)

Shapefile(zipped) 转 geojson

使用[calvinmetcalf/shapefile-js]包,以cdn引入为例

<script src="https://unpkg.com/shpjs@latest/dist/shp.js">
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>shapefile to geojson</title>
    </head>

    <input type="file" id="upload" />
    <script src="https://unpkg.com/shpjs@latest/dist/shp.js"></script>

    <body>
        <script>
            var Upload = document.getElementById('upload')
            Upload.onchange = function () {
                var fileList = Upload.files
                if (fileList.length < 1) {
                    return
                }
                var zip_file = fileList[0]
                zip_file.arrayBuffer().then((file) => {
                    shp(file).then((geojson) => {
                        console.log(geojson)
                    })
                })
            }
        </script>
    </body>
</html>

GeoServer 多波段影像使用同个样式

2020年7月18日 11:01

引子

需求是有一幅海洋要素的数据,数据有12个channel,12个channel对应12个月份的数据。图层发布后,可以使用样式选择相应出channel,显示某月的数据。简单粗暴的方式是复制12份style,为了利于以后的维护(多半要自己维护),遂想找一种方式类似“动态样式”的东西,可以从外部获取参数,使用同个style通过不同的参数选择不同的channel

这里被自己的自以为是小坑了一下:生产环境用的GeoServer版本比较低,2.11.x。自己看文档的时候看的最新的文档,测试不行后,又看了2.11的文档,文档里虽然有类似的用法,但在channal选择的时候不可用。

所以这个方式只适用于较新的版本。

图层发布

图层发布,多channel的影像理论都可以。

设置样式

一般的波段融合的channel select是这种形式[^1]

[^1]: GeoServer : RasterSymbolizer

<ChannelSelection>
  <RedChannel>
    <SourceChannelName>1</SourceChannelName>
  </RedChannel>
  <GreenChannel>
    <SourceChannelName>2</SourceChannelName>
  </GreenChannel>
  <BlueChannel>
    <SourceChannelName>3</SourceChannelName>
  </BlueChannel>
</ChannelSelection>

style中1,2,3 channel对应 (R,G,B)

对于选择单channel显示,使用Function获取“环境变量”,替换默认值

<RasterSymbolizer>
  <Opacity>1.0</se:Opacity>
  <ChannelSelection>
    <GrayChannel>
      <SourceChannelName>
            <Function name="env">
             <ogc:Literal>m</ogc:Literal>
             <ogc:Literal>1</ogc:Literal>
          </ogc:Function>
      </SourceChannelName>
    </GrayChannel>
  </ChannelSelection>
</RasterSymbolizer>

其中,channel name 中包裹了一个Function对象,它在env中的m的值为空时候提供1作为默认值,若m非空,则使用m的值作为 channel name。

wms请求中添加&env=m:2 即可选择编号为2的channel显示。

http://localhost:8083/geoserver/wms?service=WMS&version=1.1.0&request=GetMap&layers=geosolutions:usa&styles=&bbox=-130.85168,20.7052,-62.0054,54.1141&width=768&height=372&srs=EPSG:4326&format=application/openlayers&env=m:2

以下是一个完整的样式:

<?xml version="1.0" encoding="UTF-8"?>
<StyledLayerDescriptor xmlns="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/sld
http://schemas.opengis.net/sld/1.0.0/StyledLayerDescriptor.xsd" version="1.0.0">
  <NamedLayer>
    <Name>saltsld</Name>
    <UserStyle>
      <Title>A raster style</Title>
      <FeatureTypeStyle>
        <Rule>
          <RasterSymbolizer>
            <Opacity>1.0</Opacity>
            <ChannelSelection>
                <GrayChannel>
                        <SourceChannelName><ogc:Function name="env">
                    <ogc:Literal>m</ogc:Literal>
                    <ogc:Literal>1</ogc:Literal>
            </ogc:Function></SourceChannelName>
                </GrayChannel>
        </ChannelSelection>
            <ColorMap>
           <ColorMapEntry color="#0000ff" quantity="28.0"/>
           <ColorMapEntry color="#009933" quantity="30.0"/>
           <ColorMapEntry color="#ff9900" quantity="32.0" />
           <ColorMapEntry color="#ff0000" quantity="34.0"/>
 </ColorMap>
          </RasterSymbolizer>
        </Rule>
      </FeatureTypeStyle>
    </UserStyle>
  </NamedLayer>
</StyledLayerDescriptor>


9月

3月

后话

最终因为生产环境版本不容易更新,还是自己复制了12*2个样式。

Fish shell && Starship = 终端配置懒人包

2020年6月20日 21:11

一段废话

试图解决闪屏的问题,打算重新装遍系统。

苹果做电子消费品的态度真的是让人不服不行,照常理说,重装系统算挺硬核的操作了吧。

Mac产品经理:我觉得重装系统这种事吧,偶尔用户是有需要的,咱们设计个快捷键入口,让他们想装就装吧。

工程师:?????

然后工程师肝出了这么个重装系统[^1]的方法,重启电脑,之后:

  1. 从互联网安装最新版本的 macOS:按住 Option-Command-R 直到旋转地球出现,然后松开按键。

    此选项将安装与您电脑兼容的 macOS 最新版本。

  2. 从互联网重新安装您电脑原始版本的 macOS:按住 Shift-Option-Command-R 直到旋转地球出现,然后松开按键。

极致的简化逻辑,甚至于使得男同学接近女同学的方法又被无情 -1

除此之外,整个体验中,稳定的下载速度也是必不可少的。系统镜像通常要5、6GB的大小,不稳定的下载速度可能会让人烦躁(此处鞭尸巨硬)。

因为第一次装成了出厂自带的版本,我又重新装了一次,过程丝毫不痛苦,开机按个快捷键,然后去玩会新出的荒野乱斗,系统就装好等待配置了。两次的体验趋同,稳如老狗。

一直使用的zshohmyzsh,以及spaceship主题,装完系统准备还原工作环境的时候,打开终端,刚想把复制来的ohmyzsh的安装命令粘贴执行,按回车的小手被崇尚新鲜感的心理给制止了,于是到gayhub想找个不同的“配置傻瓜包”或者主题。

于是我找到了标题的 Fish Shellstarship

Fish shell

ohmyzsh把配置一个简单易用的zsh终端环境简化为:

  • 执行安装命令
  • 克隆插件仓库,在配置中选择自己要用的插件
  • 使用

Fish开发者:蛤?有点麻烦,不能开箱即用算哪门子方便

于是,使用fish的步骤是:

  1. 执行安装命令
  2. 使用

这里其实是把过程的步骤简化的夸张了点,实际中还有切换默认shell等等步骤,但整个过程对于我这种对高级功能没什么需求的人来说,这个过程已经被化简到极致了,我感觉自己以后是不会再碰zshohmyzsh了,除非fish跟我一个日常使用的环境工具有不可调节的冲突。

Fish shell懒人配置

fish默认支持语法高亮,自动补全。打开开关就可以使用vim-mode。几乎涵盖了我zsh中经常使用的插件。

一直没听说过,但fish的支持度比我预期的要高,安装autojump时,打印的信息中有关于在fish中如何配置的说明。conda环境也支持一行命令自动配置fish。

唯一的一个小痛点是自动补全不能映射之前使用的 ,

关于fish被称为更现代的shell的原因我还没了解,其高级特性我也一窍不通,但我真的挺喜欢它,在讨好懒人方面它给了我很好的第一印象。

starship:开箱即用又配置丰富

使用brew装好之后,在shell配置文件加一行执行命令就可以使用了。

如果你想配置各种语言工程prompticon,文档有完整的例子,甚至还有中文文档。

用户:我想改golang的prompt的emoji图标,这个老鼠太丑了

:来,把这一行加进去,换个你喜欢的emoji吧

效果图

[^1]: 重新安装 macOS

❌