普通视图

发现新文章,点击刷新页面。
昨天以前陈昱行博客

轨道上的中国

2025年1月4日 05:31

今年因为工作的原因密集解锁了很多新城市,粗略估计数量比我之前的二十多年到过的还要多。通过轨道在城市间穿梭,经常一天可以在三个城市产生痕迹,从南到北,从东到西,时不时会有种恍惚感,仿佛只是坐着地铁在某个城市中通勤一样。大多数的目的地,从高铁站进去,目的地城市的交通枢纽换乘地铁,之后从地铁口出来,出租车就可以在半个小时之内把我送到目的地,此谓——轨道上的中国也。其中更让我印象深刻的是深圳区域的一个目的地,虽是一个“镇”级的单位,居然也是通高铁的,这让一个老家县城都没有高铁站的人倍感羡慕,于是开始期盼自己的家乡何时有同样方便的交通。

链接远方

同样是工作的原因,得以有机会在一些胚胎期的“基建”面前近距离观察,高耸颀长的桥梁被一座座扎实健硕的桥墩连起来,蜿蜒连起面前与远方,远看可能会因为它的规模而感叹,但只有真的站在下面才能切实感受到它的力量和生命力,桩桩座座每一层都是佩戴黄色安全帽的劳动人民用汗水从无到有浇灌长大的,他们戴着被日照和雨淋褪色的安全帽,踩着明显不防穿刺的黑面白底的布鞋,操着五花八门的乡音,在桥墩上爬上爬下,培育着一座座桥,支撑着一个个家庭,但好像不管在主流媒介还是自媒体上他们都像隐身了一样。

桥之上

流离在施工现场和城市生活之间,又是另一种错乱。一路之不管乘务人员、做卫生的阿姨、五金店老板、网约车司机、施工现场戴着白红黄色并各司其职的工作人员,突然让“周末”这个从小清晰的概念模糊起来,这似乎只是一小部分人的“特权”,不过停到师傅们还有十几天可以放“寒假”了,倒不免有些羡慕。

灯光如太阳升起

背着书包,踩着轻轻拂过就会扬起黄土的地面,走出某块“世外之地”的时候,月亮照着我,我看着“太阳”,有了这么句感慨:从南到北,我好像看到基建强国的后面还写着四个大字:人民万岁。

Cloud Optimized GeoTIFF (COG) 概述|简介

2020年2月24日 11:36

概述

Cloud Optimized GeoTIFF (COG) 依赖两种辅助技术。

  • 第一种是GeoTiff的存储能力:用特殊方式存储像素,而不仅仅是将未处理的像素直接存储起来。

  • 第二种是HTTP Get 支持的范围请求,这种能力可以让client只请求文件中需要的那部分。

    前者的GeoTIFF存储方式,使后者的请求能方便的获取文件中需要被处理的那部分数据。

GeoTIFF的组织方式

COG使用的两种主要的数据组织技术是瓦片和概览图,数据的压缩也使得数据在线传输变得更高效。

瓦片切片在影像中创建了内置了切片,而不是简单的运用数据的条纹,使用数据的条纹的话,想要获取指定的数据需要读取整个数据,当切片可以被在指定区域快速被获取到成为可能之后,同样的需求只需要访问数据的特定部分就可以了。

概览图创建了同个影像的向下采样的多个版本。向下采样的意思是当从一个原始影像'缩小'时,有很多细节消失掉了(当前的1个像素在原始影像中可能存在100个甚至1000个像素),同时它的数据量也更小。通常一个GeoTIFF会有多个概览图来匹配不同缩放等级。这使得服务端的响应变得更快,因为渲染时只需要返回这个特定的像素值即可,无需再来找出用哪个像素值来表示这1000个像素,但是这也会使得整个文件的体积变大。

通过数据的压缩,会使软件能够快速获取影像,通常会有更好的用户体验,但是使HTTP GET的范围请求的工作更有效率依然是非常重要的。

HTTP Get 范围请求

HTTP的1.1版本引入了一个非常牛的功能:范围请求,在client请求服务端数据的GET请求时使用。如果服务端在response的header中有Accept-Ranges: bytes,这就说明数据中的bytes可以被客户端用任何想用的方式分块的请求。这通常也被称为"Byte Serving", 维基百科中有文章详细解释了其工作原理。client可以从服务端请求需要的bytes,在Web领域,这被广泛地应用,例如视频服务,这样,client就不需要下载下整个文件就可以来操作它了。

范围请求是一个可选的字段,所以服务端并非必须要实现它。但是大多数的云服务提供商(Amazon, Google, Microsoft, OpenStack etc)的对象存储工具提供了这个选项。所以大多数的存储在云上的数据已经能够提供范围请求的服务。

整合

介绍过这两个技术之后,两个部分之间如何一起工作就变得很明显了。GeoTIFF中的瓦片和概览图以确定的结构存储在云端的文件中,这样,范围请求就能请求到文件中相关的部分了。

概览图在client想要渲染一个整幅影像的快视图时起作用,整个过程不需要下载每一个像素,这样一来,请求变成请求体积更小、预先创建的概览图。GeoTIFF文件特定的结构在支持HTTP范围请求的服务端就能使client轻松的获取整个文件中需要的那部分。

切片在一些整幅影像的局部需要被处理或者可视化的时候发挥作用。这可以是概览图的一部分,也可以是全分辨率的。需要注意的是,瓦片组织所有的相关数据的区域在文件中的相同位置,所以范围请求可以在需要的时候获取它。

如果GeoTIFF没有被用概览图和切片 ‘cloud optimized’ 过,同样也能进行一些远程操作,但是它们需要下载整个数据或者需要下载的数据量超过实际需求的的数据。

优势

越来越多的地理信息数据被迁移到了云端☁️,而且其中大多数被存储在基于云服务的对象存储中,比如 S3 or Google Cloud Storage,传统的GIS文件格式能够方便的存储在云端,但是对于提供Web地图瓦片服务或者执行快速的数据处理时,这些格式就不再保持高效了,通常需要将数据全部下载到另一个地方,之后再转换为更优化的格式或者读入内存中。

Cloud Optimized GeoTIFF 通过一些 小技术使得使得数据流更高效,使得基于云服务的地理数据工作流成为可能。在线影像平台比如 Planet PlatformGBDX 使用这种方式提供影像服务从而使影像处理非常快速。使用COG技术的软件能通过获取需要的数据的那部分来优化执行时间。

许多新的地理信息软件比如GeoTrellis, Google Earth EngineIDAHO 同样在他们的软件架构中使用了COG的理念。每一个处理节点高速执行影像处理通过获取COG的部分的文件流。

对于现有的GeoTIFF标准的影响,不像引入一个新的文件格式。因为当前的软件不需要任何的修改也能够读取COG。它们不需要具备处理流文件的能力,只需要简单地将整个文件下载下来并且读取即可。

在云端提供Cloud Optimized GeoTIFF格式的文件能够帮助减少大量的文件拷贝。因为在线的软件能使用流文件而不需要保留其自己的副本,这就变得更加高效,也是当今一种通用的模式。此外,数据提供商无需提供多种格式的数据,因为老式软件和新式软件同样能读取这些数据。数据提供商只需要更新一个版本的数据,与此同时,无需多余的拷贝和下载,多种在线软件都能够同时使用它。

QUICK START

前言

这个教程说明开发者如何使用和生产Cloud Optimized GeoTIFF。

读取

最简单的使用方式是使用GDAL的VSI Curl 功能。可以阅读GDAL Wiki在How to read it with GDAL小节。当今大多数的地理信息软件都在使用GDAL作为依赖库,所以引入GDAL是读取COG功能的最快方式。

Planet 上,所有的数据都已经是COG格式,关于下载有一个小教程: download part of an image 。 大多数教程只讲了关于Planet API 的使用方法,但也说明了GDAL Warp怎样从大的COG文件中提取单个工作区域。

创建

同样在GDAL wiki关于COG的页面,How to generate it with GDAL

$ gdal_translate in.tif out.tif -co TILED=YES -co COPY_SRC_OVERVIEWS=YES -co COMPRESS=DEFLATE

或者使用rio-cogeo plugin:

$ rio cogeo create in.tif out.tif --cog-profile deflate

与多其他的地理信息软件应该也能够添加合适的略缩图和切片。

验证

使用rio-cogeo plugin:

$ rio cogeo validate test.tif

参考

https://www.cogeo.org/

独立博客自省问卷

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|一种内容管理的新想法]],更新内容变得更有动力了。

一种内容管理的新想法

2024年9月26日 01:24

一个季度没更新一篇博文,又在折腾这些有的没的,有些惭愧。

想法的来源是希望提笔写些内容的时候,不用疲于在各个仓库之间来回切换,博客、数字花园、还有一些更零碎的笔记。

如何在一个地方管理好这些内容,并且有一个舒服的书写环境?

在更换 obsidian 同步的时候,看到 obsidian 的 git 插件,随即产生一个想法:

如果我把 astro/content 中所有的内容用一个 vault 保存起来,并用该 git 插件提交内容,继而触发 vercel 的自动构建,自己不就有了一个桌面端的无与伦比的内容管理工具了吗?

在一路思考着并实践中,整个技术路线也越来越清晰了

  • 基于之前博客的项目,增加数字花园的内容,并集成之前解析 wikilink 的插件
  • 博客项目中的 content 目录,作为一个独立的 repo,使用 submodule 的方式集成到主项目中
  • 使用 github action,当 content 项目更新时,自动更新博客项目,并触发主项目构建

迁移数字花园

如何把数字花园从之前的 Starlight 绑定到博客的项目中来?

首先,我尝试使用最小的工作量把 starlight 集成进来。技术上确实没问题,只需要把路由配好,项目就跑起来了,倒是提升了一些跳转速度,但两个页面风格差异的问题直让我挠头,即使配上 GitHub - HiDeoo/starlight-theme-rapide: Starlight theme inspired by the Visual Studio Code Vitesse theme 这个主题切换的时候也很违和。尝试重写一部分组件,后来发现自己水平不够,有几个部分确实达不到自己想要的结果,只得另寻他法。

进一步,直接放弃了 starlight,用 astro 自己重写这部分内容,做的过程中自己发现如果只是把内容展示出来,给内容加一个大致的目录树的话,工作量其实不大,wikilink 的解析之前也做过,碰到的唯一一点障碍是 Routing too many redirects when using the config.json file generated by the Vercel adapter · Issue #418 · withastro/adapters · GitHub,不过自己不是强迫症的话这倒也不是问题。

content 更新触发构建

  1. 博客项目中 yuhang.ch/.github/workflows/update-content.yml at main · yuhangch/yuhang.ch · GitHub
  2. content 项目中 content/.github/workflows/deploy.yml at main · yuhangch/content · GitHub

这两个脚本都是 GPT 写的,踩到的坑都集中在 Github Token 的权限分配上。[[github-submodule-auto-update|自动更新submodule]] 这篇记了一部分。

最后

更新这部分内容的时候,顺便把 astro的一些新 feature应用了起来,比如 astro:envgetCollection ,因此项目从创建到完善,一直没什么敏感信息,所以两个仓库都设成公开可见了。

  1. GitHub - yuhangch/content

  2. GitHub - yuhangch/yuhang.ch: Yuhang's Site

至于这部分更新何时同步到GitHub - yuhangch/astro-theme-panda: A tiny blog theme for astro.,可能要等下一次空闲了。

未竟

2024年9月9日 14:56

发布日期上,这有个小心思,我把发布日期定在了九月九日,显然这不是这篇文章完成的日子。

说来悟空发布已经一月有余,这一个月时间里,发布之初,人在岭南出差,只得顶着延迟和30帧的动画远程耍棍,受了不少苦。游玩过程中,有乱棍打死BOSS后的得意,也有意料之外的卡关带来的社交焦虑;有看攻略依然迷路的无能狂怒,也有峰回路转初见壮美景色难言的欣喜。最后历尽艰辛完成全收集后又是一阵怅然若失,如果在其中某个阶段起笔,这篇文章成稿都会大有不同。

这一杯敬理想主义

我对此类极具审美,带些偏执,又违反商业规律的事物从来都是推崇又饱含敬佩的,前有老罗的锤科,后有卡总的游科。这些产品的成功与否于我没那么重要,我想看他们给这世界注入哪怕一丝的不同。成者王侯败者寇,锤科对于工业设计的偏执和游科对于场景美术的偏执如出一辙,其结果的差异值得玩味,后续有机会倒可以另开一文详细分析。

在看完、听完机核关于黑神话·悟空的节目之前,我对于这部文化和情绪上几乎完美的作品之所以能面世,简单的归于理想主义四字。在得到这些零碎的信息之后,掩盖在理想主义之下的一些细节也逐渐丰富了起来。

明确的目标,肯下苦功夫,一群志同道合的人。俗套的方法论,每一步都踩实却不容易。卡总,杨奇,李佳奇固然才华充沛,然而多得是才华解决不了的事情。在得知狼斥候都有60多种动作后,动补,绑定师这一条管线上的工作人员一起下的苦功夫就具象了起来。音乐总监李佳奇说:当你看到美术的同学把细节抠到那种程度,自己就完全不可能掏出一个糙的东西。

是要做一件“打破顽空”之事,团队才能这样有凝聚力吧,末了,突然我想到百年前也有那么一“支”团队。

wukong

未竟,那路在何方?

我是先从视频网站听到《未竟》这首歌的,彼时对于此作的剧情,舆论还在收敛前的发散阶段,在朋友的三言两语中自己对剧情还是有些担心的:当真是二流网文的水平?

在看过片尾动画和这首歌的歌词之后,突然心情就轻松了起来,因为之前的一切哪怕是游戏本身的游戏性都没那么重要了,每个人都能选择自己看到的所谓内核,我看到了我想看到的。

长生,长不了。

纵使天庭、灵山众神佛以何等美好的面貌示人,四洲香火鼎盛何等繁荣,普度众生的理论如何宣扬,都掩盖不了其吃“人”的本质,这“灵韵主义”终有覆灭之日。

一去不回便一去不回

片尾动画最后一幕,大圣与已成虚幻的战友最后碰杯,嘴角流下的酒像极了眼泪,抖罢披风,亮出英雄的背影,未竟前奏响起,想这次飞起千钧棒,不只为花果山,是为荡平这天地间的不平吧!

最后

全民瞩目,一个月2000W的销量,我的判断悟空注定要是个孤品了,但游戏科学证明了游戏制作这碗饭站着也能吃,大圣完成了他的使命,后续也定会有合适的后辈继承他的“意”,这不局限于电子游戏,还有其他的文化产品。

博客折腾日记

2024年3月27日 15:57

翻了下git记录,基于Astro的第一版博客大致完成与去年七月中旬,修修补补到23年末算是有了雏形。

hello world到几个核心栏目的完成,过程中自己新认识了很多朋友,交流中对博客和搭建博客这件事也有了很多新的想法。

为什么选择 Astro

其实告别Hugo之后,我的第一个选择是Nuxt,期间还发了个帖子说说用 nuxt 写了个博客的体验,而告别Hugo 的原因是,模板语言虽然保持了打包的高效,但魔改起来实在是太费心力了,而且那个时期自己也没找到好用的IDE插件,自己也动过从头写一个Hugo 主题的念头,每次都以失败告终。

帖子中,Nuxt在开发中给的正反馈开始让我欲罢不能,但也有几个问题:

  1. 一个纯净美好的HTML页面,在各种Vue组件的包装下,查看网页源码的时候显得特别杂乱。
  2. SPA应用(它没错但我不太喜欢)。

最后,被Astro页面的2023 Web Framework Performance Report唬住了。

简单使用后,不管是文档还是IDE的开发体验,都没啥大问题,算是正式入坑了,当时Astro还在快速迭代,也迎合了我折腾的欲望。

为什么喜欢 bearblog

很早之前,查资料找到这个博主Mike - Line Simplification,从这既偷到了页面简洁素雅的设计,同时也学习到了D3.js,收获颇丰。

之后,从Hacker News - Bear Blog – A privacy-first, fast blogging platform上看到bearblog,就一眼爱上了,之前Hugo的博客也是基于bearblog的主题。

简洁的页面设计和网页原生的按钮直戳我心,页面尽量只有内容,甚至连导航栏都藏起来,都是源自这儿。

为了简洁做的事

这段故事,自己也发了个帖子,突然感觉 tailwindcss 不香了

用 astro 做了一个静态网站,内容主要是文字为主。 当时用 tw 的时候是提高生产力为主,比如 light/dark 转换,prose 排版等等。 现在功能基本完成,想做一些优化的时候,发现某篇文章的 index.html: 总大小 ~ 78kb ,移除 tw 声明的变量和 class 定义之后 大小只有~ 24kb 。。。 尝试用 purgecss ,作用不是很明显(可能姿势不对)?

自己既想使用tailwindcss的便捷性,又不想为此付出这么大的代价(接近三倍的体积),有V友建议了Unocss

迁移起来没太大的问题,用法基本兼容tailwindcss,又回头看了下自己,首页单页面的体积只有11kb,此番折腾自己觉得还是值得的。

自己喜欢的多语言组织方式

在折腾多语言这一部分的时候,自己一直在动摇,一方面确实挺费劲的,另一方面,自己也忍不住的问自己,真的有英语为母语的朋友看我的博客吗?

最后,向往折腾的自己打败了“不自信”的自己,这个功能还是做出来了。

虽然目前Astro已经支持了多语言,但我还是在用第三方的插件astro-i18n-aut

原因是,官方的多语言,不管是组件还是内容,都需要组织在不同的目录下,而我不喜欢这种设计。

我的多语言的实现大概分两个部分,一是多语言的文本,用YAML存储,虽然这样需要多安一个包 @rollup/plugin-yaml,但我实在不喜欢JSON

layout:
    title: 陈昱行博客
    description: 陈昱行的个人博客,在没人看到的地方写写画画。少些技术,多些生活。这里自己的学习生活,偶尔分享一下自己对这个世界的看法。
nav:
    posts: 随笔
footer:
    home: 首页
    rss: 订阅
    timeline: 时间线

第二部分,是内容的部分,考虑到自己会有懒的时候,所以无法保证所有的内容都有两种语言,而我想不管在中文还是英文界面都显示所有的内容。

于是想到了这么一种设计,用扩展后缀的方式区分中英文内容,英文内容以.en.md或者.en.mdx结尾,如果有指定的内容则显示,否则fallback回中文的内容。

在frontmatter里强制增加英文标题,这样可以保证至少首页做到都是英文,看起来舒服一些。

闲话从伪动态到真动态

闲话这个栏目,起源于叽喳,后来迫于不太稳定,也想把数据掌握在自己手里,自己做了个类似的东西。live

这次借着博客重构的机会,把这部分内容也集成了进来。

开始之初,有两个方案:

  1. 像之前一样,使用leancloud存储数据。
  2. 将这部分内容也做成静态的方式,存在一个文本文件里。

最后调研了一圈,发现了cloudflare D1,好消息是Free Plan也可以用,于是也没再纠结。

DROP TABLE IF EXISTS moments;
CREATE TABLE IF NOT EXISTS moments (
  id integer PRIMARY KEY AUTOINCREMENT,
  body text NOT NULL,
  tags text WITH NULL,
  star integer NOT NULL default 0,
  created_at text NOT NULL
  deleted_at text WITH NULL
);
CREATE INDEX idx_moments_created_at ON moments (created_at);

使用cloudflare worker做了一个简单的接口,调取数据。

在客户端,通过SSR的方式拉取数据,并分页渲染,这样好处是不对外暴露接口,坏处是如果有人想刷某个接口,直接刷该网页我也没啥办法。

基于TMDB的影视评价

其实本意还是想通过,imdb或者豆瓣来做,一方面可以复用客户端,二是内容很全。

奈何两者的接口反爬都太严格了,完全没有角度,自己还尝试了通过headless的方式访问IMDB的rating导出csv的接口,以失败告终,值得选择了TMDB。

自己维护的好处是可以与自己的闲话互动,将自己的影视评论与具体的闲话条目联动。

DROP TABLE IF EXISTS reviews;
CREATE TABLE IF NOT EXISTS reviews (
	id integer PRIMARY KEY AUTOINCREMENT,
	imdb_id text NOT NULL,
	title text NOT NULL,
	title_en text NOT NULL,
	media_type text NOT NULL,
	imdb_rating real NOT NULL,
	rating real NOT NULL,
	release_date text NOT NULL,
	rated_date text NOT NULL,
	moments_id integer NOT NULL,
	created_at text NOT NULL,
	deleted_at text WITH NULL
);

短链

聊胜于无的功能,形式大于内容,大致思路如下:

  1. 在站点构建完成阶段,加入一段hook,为数据库中不存在的链接新建短链。
  2. 增加[id]路由,处理短链。

uuid

短链由一个固定前缀和三位随机数构成,对自己来说完全是够用的。

  • [b] 开头代表是博客的链接
  • [o] 开头代表是草稿本的链接
import shortUUID from "short-uuid";

const characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const short3 = shortUUID(characters);

export function getOpenUUID(exist: string[]) {
    // generate a v4 uuid, subtract the first 3 characters, and then convert to base62
    let uuid = 'o' + short3.generate().slice(0, 3);
    while (exist.includes(uuid)) {
        uuid = 'o' + short3.generate().slice(0, 3);
    }
    return uuid
}

短链持久化

这部分也犹豫了好久,直接用vercel kv,每月免费额度有限,最终还是结合了cloudflare D1:

DROP TABLE IF EXISTS shorts;
CREATE TABLE IF NOT EXISTS shorts (
  id text PRIMARY KEY NOT NULL,
  url text NOT NULL,
  created_at text NOT NULL,
  deleted_at text WITH NULL
);
CREATE INDEX idx_shorts_id ON shorts (id);

构建短链路由

完事具备,只需要做一个解析短链的路由即可大功告成。

import { createClient, kv as prodKV } from '@vercel/kv'

const notFound = new Response(null, {
    status: 404,
    statusText: 'Not found'
})
const getLink = async (id) => {
    const kv = DEV
        ? createClient({
            url: KV_REST_API_URL,
            token: KV_REST_API_TOKEN
        })
        : prodKV
    const cached = await kv.get(id)
    if (cached) {
        return cached
    }
    const apiURL = `api.blog.com`
    const headers = {
        Authorization: `Bearer ${BLOG_API_SECRET}`
    }
    const res = await fetch(apiURL, { headers })
    if (res.status === 404) {
        return null
    }
    const json = await res.json()
    const url = json.url
    await kv.set(id, url)
    return url
}
export const GET = async ({ params, redirect }) => {
    const { id } = params
    const type = id?.slice(0, 1)
    if (!type || !['o', 'b'].includes(type)) {
        return notFound
    }
    const link = await getLink(id)
    if (!link) {
        return notFound
    }
    return redirect(link, 308)
}

代码块

也是一波三折,最开始看上了code-hike,无奈因为#255, 一直没用上。

后来参考Highlight a line on code block with Astro,迁移了部分样式,其实也挺好看的,不过自己处理的样式有点复杂,感觉污染了CSS。

最后看到starlight - expressive code 的解决方案,解决了几乎所有的痛点,样式复制过来也没问题。

import expressiveCode from 'astro-expressive-code'
import {ExpressiveCodeTheme} from '@expressive-code/core'
import {readFileSync} from 'fs'
import {parse} from 'jsonc-parser'

const nightOwlDark = new ExpressiveCodeTheme(
    parse(readFileSync('./src/styles/expressive-code/night-owl-dark.jsonc', 'utf-8'))
)
const nightOwlLight = new ExpressiveCodeTheme(
    parse(readFileSync('./src/styles/expressive-code/night-owl-light.jsonc', 'utf-8'))
)

// 插件配置
···
expressiveCode({
    themes: [nightOwlDark, nightOwlLight],
    themeCssSelector: (theme) => {
        return '.' + theme.type
    }
})
···

component or directive?

最开始的时候,采用的是组件的形式增加提示,但问题是仅仅因为这个组件,就需要将md变为mdx,觉得成本有点高,后面还是改为使用remark-directive

:::note{.info} 提示:这是个提示 :::

使用的方式代码如下,使用方法参考remark-directive的例子即可。

:::note{.info}
提示:这是个提示
:::

除此之外,有篇博客有嵌入B站视频的需求,于是也用remark-directive来实现了。

export function RDBilibiliPlugin() {
    return (tree, file) => {
        visit(tree, function (node) {
            if (
                node.type === 'containerDirective' ||
                node.type === 'leafDirective'
            ) {
                if (node.name !== 'bilibili') return
                const data = node.data || (node.data = {})
                const attributes = node.attributes || {}
                const bvid = attributes.id
                if (!bvid) {
                    file.fail('Unexpected missing `id` on `youtube` directive', node)
                }
                data.hName = 'iframe'
                //<iframe src="//player.bilibili.com/player.html?bvid=BV1Zh411M7P7&autoplay=0" width="100%" allowfullscreen> </iframe>
                data.hProperties = {
                    src: `//player.bilibili.com/player.html?bvid=${bvid}&autoplay=0`,
                    width: '100%',
                    height: 400,
                    aspectRatio: '16 / 9',
                    // fit height
                    class: 'm-auto',
                    // height: 400,
                    frameBorder: 0,
                    allow: 'picture-in-picture',
                    allowFullScreen: true
                }
            }
        })
    }
}

为什么没有评论

看到一个博主,评论区是大大的 "请通过邮件联系" 的字,自己觉得很酷。

另一方面,是一个没想明白的问题,首先我不喜欢匿名评论,但如果采用认证的方式,不管是Github还是其他的第三方登录,还是仍然无法囊括所有的读者。

所以有事还是给我发邮件吧ღ( ´・ᴗ・` )比心

由博客框架到一项个人技能

从为了做博客接触Astro以来,从偏爱到现在选择它作为首选的静态构建工具,一切仿佛自然而然发生。

冬行广州

2024年2月1日 18:39

本想来广州可以体验不同于北方的的冬日,不料赶上一股冷空气,温度数值是高些,但和天津的零下天气比也没太大的不同。

南昌的雪

抵达广州前一日,途经南昌,在南昌站附近落脚,到酒店时已到午夜,到旁边菜馆尝了一道藜蒿腊肉,肉肥但没太腻,青菜很新鲜,其他就没太出彩的地方。 在这我第一次见到上茶水需要配合一个塑料盆的上法,我也是后面才反应过来这是为了涮杯子方便倒水用。房间窗户正对着一座立交桥,因仍有工作要处理,心烦意乱,工作桌旁椅子本是朝房间的里面摆放,我特意将椅子挪到桌子另一侧,以能看到窗外,于是有了这一张照片。

完全预料不到的惊喜最让人欣喜,次日一早,拉开窗帘,已是白茫茫一片,没想到自己就这样迎上了南昌 20 年来最大的一场雪。 雪鹅毛一样大,很是好看,早上在酒店门口好多和雪合照的人,但也带来了不小的的困扰,我为了来南方特意换上的带网洞的运动鞋面对现在的路况显得有些吃力,果不其然,鞋子就这样湿漉漉的一整天。

处理完工作上的事情,下午赶往广州,听司机师傅说马路上撒了盐,路上没什么积雪。抵达南昌西站已是午后,站内乌泱泱全是人,车次大屏基本都是晚点的信息。 好消息是我的车次只是晚点,看着焦躁的行人们,自己羞愧的有点庆幸,在火车上遇到一对母子和乘务员在争论,大抵意思是因为火车晚点,她们错过了换乘的列车。 她们问今晚住宿等一系列损失要谁负责,乘务员自然是负责不了,在解释了这是不可抗力之后仍然无效后,只得搬来列车长当救兵。

因为列车晚点,到广州时已经是深夜,阴风细雨,身上的羽绒服一点都没多余,南站附近没有连锁酒店,找到一个在平台上照片突出一下的一家宾馆,徒步前去,发现与预期不符。此时肚子已经在叫了,恰巧此地饭馆还不少,看到一家潮汕菜,点了田鸡粥和炒时蔬。 没想到此田鸡非我臆想的田园鸡,但肉很嫩,稍微有点口重,但也更突出了鲜味。

第二天早上,迫于头天晚上因为没伞,淋了雨,第二天在便利店割肉买了一把,此地离地铁站大致100米,也是这把伞服役的唯一一段路程。

有几次午餐也都印象深刻。最终吃到了我的“田园鸡”,是之前没见过的吃法,大致相当于津派涮肉的“手切鸡”,只是调料换成了更有南方特色的调味,配上麻酱应该也好吃,这种吃法优势是肉极致的嫩,唯一不足的是不太好嚼,后面在山东吃过一次重庆火锅有类似直接下鸡肉的吃法, 但那次的鸡肉太科技了,随软烂但已没了鸡肉味。另一次是吃到了用电饭煲煲的煲仔饭,可能正宗,但没有蔬菜点缀,一人一锅有点腻。其他几次的吃食就乏善可陈了,都是全国统一味道,可能是个人选择的缘故,不符合广州餐食很甜的固有印象。

需要处理的事情太多,在户外的时候,基本是在广州各种交通方式之间切换,而除了步行之外,基本又在地下,也只在住宿的地方看了眼珠江,也是之后才知道,我住的地方离鲁迅先生住的小白楼只有一路之隔。

此次广州之行,由于恰巧碰上少见的降温,导致本来那一丝来自温度差异的新鲜感也未能体验到,仔细想想,如果非要找出广州这座城市与之前到过的其他城市的一丝丝的不同,好像只是地铁中的三语播报了。

婚礼杂记

2023年10月28日 14:17

自己抗拒婚礼这件事,主要有几方面的考虑,一是从小对作为主角这件事就很抵触,二是张罗亲朋好友也非自己所长。自己虚荣心还是有的,取得点成绩也颇希望得点表扬, 但在某个场合作为主角接受表扬,或者某个场合突然就作为关注的焦点就会非常不自在,从小到大都是如此。我不像一些朋友害怕或反感血缘亲戚间的来往,但就结婚这件事, 向各式不太多日常交流的亲戚朋友发出邀请,着实是个不小的心理负担。

然而有些事情是没跳过这个选项的,婚礼便是其一,期间我跟对象也有过计划和挣扎,中间首先是做我妈的思想工作,可能我妈抱着尊重我俩的想法,开始也没太坚持,给了 我俩一种形势乐观的错觉,一直稀里糊涂到婚礼前几个月,两方父母见面正儿八经的商量婚礼这件事,我俩才明白,不要婚礼或者弄个极简单的仪式这件事的困难程度。

这个过程,我俩人也算是逐渐明白了新郎和新娘在婚礼这件事上,准确的定位,这在电视剧中和别人身上是不好体会的。我俩人一个是独子一个是家里老大,父母在 县城里混了半辈子,需要这个仪式作为一个里程碑,婚礼上我俩的朋友占少数,更多的是亲戚和父母的好友,所以我俩是这场仪式的前提,但不是绝对的主角。

婚礼的筹备是一项极复杂的工程,伴随着在地区一个个婚礼在大家意识里的相互融合,逐渐形成了一个无比繁杂的流程,所有的子流程最后都成了“公认”的不可或缺。 这决定的这件事本身就不是两个人或者一个小家庭能搞得定的,整个过程散在各地的哥哥姐姐像过年一样,风风火火忙的热火朝天,不过这次是为了我,这种感觉很温暖。 因此,在这种氛围中,也更加明确了结婚这件事不是两个人的事,是两个大家庭间沟通的开始。

在一大家人的支持下,婚礼之前,我两个人反倒显得有些轻松,除了试衣服,介绍两家人认识这些只有我俩能做的事情之外,也没太多其他的事情忙到首尾不顾,反倒是久违 的回家度假一样,过年都没这般轻快。说来蛮有意思,婚礼前的两天,可能是我俩自认识开始交流最少的两天,因为还是有许多事情需要沟通,以至于结婚的头天晚上, 自己躺在床上反倒有种孤独感。

婚礼当天,正如老师给我嘱咐过的一样,是有很多可在意可不在意的细节,我当然是不在意,只要婚礼能正常进行,最后就只会得到圆满这一评价,虽然过程中确实出了好多小插曲, 但也只是多了谈资。

2023回看周杰伦

2023年7月29日 10:36

2023年了,作为杰伦的铁杆歌迷已十年有余,自己从教室里那个天天晨读哼唱发如雪的小学生,变成办公室里塞着耳塞听歌的打工人。

他算是一个优质的偶像,「桀骜不驯」的、屌屌的气质,多少影响了我,虽然这没改变我是个内敛的人这回事。

狼牙月,伊人憔悴

应该是2006年前后,林俊杰春晚那首《一千年以后》后来成了我的流行歌启蒙,之后听表哥的磁带,又爱上了《冻结》,林俊杰应该是我「粉」的第一个流行歌手。

后来,某个暑假,市里的表哥带来了几张光盘,像是盗版刻录的,从里面听到了发如雪和东风破,再加上那一首夜曲,从此成了周杰伦的「唯粉」。

由于认识他「太晚」,所以之前的那几张专辑就成了我的宝藏,那个时期,我的MP3里(霸占的我爸的)只有周杰伦的歌,我把各个专辑整理好,轮番着听不够。好不夸张的说,那个时期我可以前奏一响秒知歌名,也可以叽里呱啦的背歌词。

烟花易冷,人事易分

这是我印象里第一张「追更」的专辑。准确的讲,这不是我在喜欢上周杰伦后的第一张专辑,而是在这个年纪,我拥有了一些互联网技能,简单的说就是会找资源了,所以印象深刻。

那个年代还没有诸如全民K歌这类软件,当时自己还用某个录音软件录歌玩,实话实说自己真没什么唱歌的天赋,高音上不去,低音下不来,现在回想,当时更多是享受技术上的成就感,记得当时在QQ空间上传音乐,还可以在QQ音乐中搜到,现在应该做不到了(苦笑)。

当时正值举办运动会,中午和同学去网吧,他们打游戏,我循环「烟花易冷」,不得不说,这个时代周杰伦依然能打,虽然站在今天的角度看,作品的风格已悄然发生变化,想「自导自演」、「我落泪情绪零碎」几首歌,只能说我有保留意见吧。

你的倒影是我回不去的风景

21世纪进入20年代,周杰伦和整个社会都发生很大的变化。

周杰伦结婚了,他给女儿写了歌,带着儿子拍MV,生活的重心不再是出新歌,发专辑,他不再是那个舞着双截棍,说着我不配的少年,毕竟我都已近而立之年。

然而这种疏离感并不是我俩都长大了这么简单。

吹捧周的中国风实在是赘余,"从娘子唱到双截棍,东风破到发如雪,一路走来,始终如一,多样的曲风,他只爱中国风。"

中国风是周和方的联手造的梦,精致的文本、天才的旋律、极致打磨的配乐,仿佛把人拉离地面,扔进历史的浪漫。

但周的其他一些「粗粝」又「真诚」的歌,对我的吸引力一点不比中国风弱,而此类型歌曲的数量分布以及完成情况,似乎也间接作用于这种疏离感。

为了验证这一不靠谱的感性认识,我简单搜了一下周自己作词的歌曲列表。周杰伦自己作词的歌曲的歌曲有哪些?[2019-10-04]

这是我看着歌名,记起来的歌词 (有几句还是找原歌词确认了一下)

坐着公车上学的我,看着窗外的牛啃草,是一种说不出的自由自在。 《梯田》

听这首歌的时候,我会回想起,家对面的麦田十几年之间变成幢幢高楼,夏天蝉鸣的树林。

我要一步一步往上爬。 《蜗牛》

虽然这首歌只有演唱会版本,但每每听,脑海里的画面是那个年轻人在那个年代昂扬进取的情感。

她要的是陪伴,而不是六百块。 《外婆》

外婆今年90多岁了,自己从大学离家,已近十年,每次回家能看外婆的时间更是少之又少,之前有过一篇记录。 她老了 。此文完成到现在,又是两年过去,这三年簌簌而过,我的生活似乎没有变化,但外婆的身体却老的飞快。 如今只能大部分时间卧床,由于一些常见于老年的病,如今也已经认不出我,想到会难过,能做得却不多。

那根本不是我的口气 想让观众看好戏。 《四面楚歌》

笑一个吧 功成名就不是目的。 《稻香》

这首歌的歌词,初看细看都看不出好,但给人的至于一点都不少。

为什么要听妈妈的话 长大后你就会开始懂了这段话。 《听妈妈的话》

自己这个年纪,早已理解了妈妈的严厉和当初觉得过分的管教,背的ABC,现在基本都作用于生活啦。

如果你对未来产生恐惧 眼前雾濛濛一片 那是因为你没把眼镜给擦干净。 《红模仿》

开头是嘲讽与自负,结尾是关心与建议。

这几首歌,比之如今所谓《罗刹海市》如何?

而另一些,则是对小朋友的失恋启蒙,是青年时代的周与少年时代的我的共情。

你已经远远离开 我也会慢慢走开。 《安静》

翻着我们的照片 想念若隐若现。 《借口》

看不见你的笑 我怎么睡得着。 《彩虹》

这些歌大多出自08年之前,后面这些感觉就截不出歌词了。

  • 2010《跨时代》超人不会飞 自导自演 好久不见
  • 2011《惊叹号》疗伤烧肉粽 mine mine 公主病
  • 2012《十二新作》哪里都是你 乌克丽丽 大笨钟 手语
  • 2014《哎呦不错哦》听爸爸的话 算什么男人 我要夏天
  • 2016《周杰伦的床边故事》英雄 爱情废柴 土耳其冰淇淋

生活是永久的 音乐是会留下来的

作为周,在青年时期天马行空、倾尽所能。留下了对爱情甜蜜和苦涩的音乐感知,并与世界共享,已是善莫大焉,我对此充满感激。

作为小天王的他,音乐影视事业有波折但一直站在山顶,优渥的生活,让他实在很难与这个社会的大多数在同个频率通过音乐交流。

早期的MV叙事里,他的角色是作为一个普通人,追求同样作为普通人的另一半;是一个年轻人冲破牢笼,向权利发起挑战。

民族情感上,是作为龙的传人,用中国功夫,在外国人的地界找回场子,是李小龙经历的一种精神延续,是一种民族叙事。

到了后面,从mine mine 这首歌第一次用英文作为歌名。

MV中的角色逐渐与国外环境完成融合,一方面,没了那种初来乍到的局促感,另一方面,更多的表达出自信,从一个初来乍到的毛头小子。 变成在市区开着豪车的焦点人物。

所以在这种情况下的难以共情,问题在我而不在他,这是阶级的问题。 还有一条,分处两岸,势必造成一些世界观方面的差异,当然这些在巨大的阶级差异前面就不足道了。

所以要求一个人(偶像)满足全方位的心里需要,既包括音乐的、文化的、意识形态的,本身就是不现实的,年轻时的自己还不懂,甚至在音乐方面都非常排他。

后来自己开始听唐朝、崔健,那些弥漫在世纪之交的气氛重新感染了我,自己有时候真在想,假如没有周杰伦,国摇现在会是怎样的处境?

人到中年的我,怕是等不到一个新的,20年代的周杰伦来为我传递冲破牢笼、抗争的情绪了,以至于只能从20年前的角落里扒拉一番。

前段时间偶然听到一首歌,是2000年尹吾的「个人」。

你和我各人拿各人的杯子

各人各喝各的茶

我们微笑 相互点头

很高雅 我们很讲卫生

你和我各人各说各人的事情

各人数各人的手指头

各人发表各人的意见

最后我们 各人走各人的路

这种对集体主义的情感(我理解的),是只有在这边土地上经历过的人才能感受的到并传递下去的。

周杰伦属于青春,但好像也留在了青春。

博文创建工具

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了解还是半瓶水,真是汗颜😅。

把玩 Damus 有感

2023年2月1日 19:30

技术上没什么看法,说实话对区块链了解≈0 ,主要说一下自己情感上的感受(矫情)。 好像时间拉回到新浪微博和各种牌子的微博刚起步的时代。 在信息流上看到有这么个 post:

I remember the early 1990s, when I watched the HTTP and HTML protocols evolving day to day on the lists for Mosaic and httpd. Nostr gives me that feeling again.

我没经历过 90 年代,但这种感觉跟我经历的 2010s 上半个十年也类似,大家都初来乍到:Hello world ,This is xxx 。此时此刻,形成了这么一种条件:很多人在一个频道发消息,所有人都想发消息,所有人都要看,所有人都会被看到。 中心还没来得及形成(现在已经有人在 global 频道分享推特创始人等名人的地址了,似乎”粉丝中心化“已经在路上了)。

我印象中微博大概是这么个变化:开始的时候也会有 global 的信息流,大家互粉互粉着信息流里多了很多朋友,形成一个类似于现在朋友圈这么一种状态;后来,一些内容好的用户脱颖而出,成为“小 v”,一些名人凭着光环,不产出优质内容也成为了”小 v“或者”大 v“;后面,随着推广变现逐渐被接受,开始有了专职博主,用内容作为谋生手段,有点像这些年 b 站 up 的状态。伴随着”粉丝的中心化“,普通人的声音越来越小,或许只能在各种 v 的评论区,发表些或极端、或精彩的言论才能有些点赞,体会那种”被看、被关注“的感觉。

就好像是社会一个倍速的推演,大多数总要变成 nobody 。这个推演随着各个平台兴衰频繁的轮回,像知乎、即刻、b 站,都是。开始的时候觉得像在开会(自己在圆桌上,指点江山),后来感觉像在开大会(领导在讲台上讲,讲的好坏对错自己只能听,想和邻座同志交换个想法,都因为音响声音太大不能如愿)。这样再想,v2 上,自己的想法发出去就会被人看到,真是让人无比舒适。

回到把玩 damus ,见证了这么一个”社会“开始运行的历史时刻,不过,对其后面的发展并不太看好,毕竟前面的 matrix ,也只在技术圈有所应用。之后怎么规避风险黑产,垃圾消息,公私钥认证方式能不能大面积被接受,贴图片地址换头像发图片的方式能被多少普通用户接受,都是需要解决的问题,这一切或许只差一个天才产品经理?或者直接快进到 20xx 年:

随着最后一个 nostr relay 关停,去中心化社交媒体 nostr 正式退出历史舞台。

比起干掉中心的服务器,怎么干掉社交媒体上聚光灯下的那几个言论中心,让每个人的声音都被尽可能多的人听到,每个人不再只是根据自己兴趣机械的加快分泌多巴胺,这好像不是个技术问题。不看好,但希望它茁壮成长🌳

发热门诊见闻

2022年8月15日 14:08

一日,午后空调强度过高,感了风寒。

凑巧,百亿补贴之PS5快递同日抵,拖无力之躯施于几上,联系某宝备份港服,一气呵成。二手盘美末购于某宝,初用光驱口,前后颠倒,反插得以读取进入游戏,末世逛约10分钟,剧情甚佳,然不适加剧,身乏腿麻,确染风寒,不敢怠慢,饮热水卧床。

少间,得体温计,塞腋下,38度,惧甚,使骑手购药,表单处强制低温,38超限,填假值,得药,谋次日医院诊病。

次日,单车至医院,服药后体温常,思对组织负责,毅然进发热门诊之偏路,须臾至。

见医生着N95口罩,旁保安神情肃穆,问:昨夜发热,要何流程?

医生铺一表单,记流调之各类信息,甚长,流调毕,见右侧立一木板,上书醒目红字,“核酸报告之后,需隔离静观足三时辰”,惊隔离时间之久,便问:可需足时方可诊病?医生不悦,答:“医院乃诊病之所”。其后,换N95口罩,沿标记得至门诊大厅。

尚早,门诊人不多,一医生立于门口,见我至,取一消毒纸巾,拭座椅数次,教我坐下,怕毒存于椅,询时间长短,可否不坐,医生曰否。遂坐,又口述填一表格,循指令配合体温、血压、心率之计量,不时毕。

基础参数得,次应取病毒关涉之物,门诊左出,一医生包裹更严,若待我者。医生宣:需鼻拭子、抗原。又问凝血可好,又询问几种药物情况,详情不记。答正常,后鼻拭子一长条棉签捅鼻,长而软,较咽拭子时间更长,又一普通棉签左右两鼻擦拭,谓之抗原。后使我戴橡胶手套,握试管返于前处。

再抵门诊,将试管返于医生,医生差我再坐,待血常规检查,不时来一医生,抽血一管,检测毕。

听于医生安排,挂号,缴费等结果,片刻,号至,医生诊,后开药,缴费,医生携药至,诊毕,医生引路,至所谓单独隔间。

隔间约二十有余,吾入十号房,与同行同扫码进一患者群,并医生三十余人。

不敢怠慢,快入房间,见房间较清洁,床着一白色一次性床单,又进卫生间,亦净,只垃圾袋黄色,其余皆白净。

隔离中,口渴,见群之消息,至旁房间自助扫码得两瓶水。群亦有医生关照病情,嘱多饮水,观察体温,常报。

正午,消息又至,可订饭食,统一15元,未时初,饭食至,口味颇佳。

入申时,报告出,得出隔间,签名后,得一隔离结束通知单,可做发热证明之用。

替WPS说句话

2022年8月12日 14:42

[!NOTE] 提示 可能需要先观看这期节目, 文尾有节目小窗。

我的观点

仅针对所谓WPS锁定本地文档的事件,在目前没有任何实锤的情况下,我的看法是:最多反映了技术能力的不足,应排除主观恶意行为,不应该上升到国产文字处理软件的层次,下面啰哩啰嗦,大致以下两个观点:

  1. 「国产文字处理软件」可以从技术能力、稳定性上进行批评,但不要打上主观审查内容、干扰用户创作的标签。
  2. 搞坏、弄丢用户文档不是「国产文字处理软件」独有的“功能”,用户要有文件备份的意识,不要过度信赖软件,毕竟软件是人写的。

首先,对云文件的审查管理是大家都能理解的行为,毕竟一个疏漏配合舆情可能公司都要没了。但对本地文件的审查,WPS方是没有义务的,锁定、删除纯粹给自己找麻烦。对于WPS提供的云文件、文件同步功能,考虑到用户体验,云文件与关联的本地文件之间势必会存在大量的交互,表现在用户端是感觉不到两个文件的差异,而在此背后是复杂的软件逻辑,作为国内的“头部”文字处理软件,按道理说在软件逻辑上不应该存在问题,但考虑到实际的复杂性,出现问题是完全有可能的,这就是我倾向于这是个技术能力的问题的原因。

虽然在结果上没什么区别,从用户的角度来看,文字处理软件缺少了可信性。但如果照督工这一棒子,「国产文字处理软件」因为伸长手恶意干扰内容创作直接被打死了。如果照技术力不足这点来定性(假设WPS真有锁定用户本地文件的情况发生),仔细分析,是由于与云文件的交互过程产生了这种情况,采取不使用云服务、使用政企离线版本是可以解决问题的。

另一方面来说,文件同步这个“服务”真的没那么好做,强如Microsoft,onedrive的同步也不是万无一失的,简单的搜索就能找到类似的问题,甚至比WPS“锁定”文件更严重。

至于督工提到的由于软件行为导致内容丢失、影响工作进度的后果是无法接受的这点,我认为需要考虑的是针对文档的重要性给予其应得的容灾性方面的保障,而不是寄希望于软件对文件的每次操作不会出错和硬盘永远不会出故障。

督工这一段输出我感觉更像是表达了对审查干扰创作的不满和对未来严肃咨询创作的担忧,WPS当了这个麻袋。作为一个在中国跟软件稍微有那么一点关系的工程师来说,虽然觉得WPS做的确实很拉,但还是希望它能好好活着。

督工回复

作为工程师,您也许是对的。 作为中国人,您也知道某些问题在软件之外。 作为用户,不需要考虑压力来自于谁,用脚和钱投投票就可以了。

节目

::bilibili[https://www.bilibili.com/video/BV1sT411L7yF]{#BV1sT411L7yF}

最伟大的作品周围

2022年7月16日 13:42

等15号的专辑是整个七月除了下班、周末为数不多的期待。

订了实体专辑,本来不打算买数字专辑,计划先用「盗版」顶一顶,入数字专辑自己大抵也是会导出来到其他播放器播放。15号晚上歌曲提前推出,自己还是多等不了这久,在九点二十八买了数字专辑。专辑界面几首新歌可播放刷出来的时候自己十分犹豫,不知道从听起,最后选择了按顺序来。

自己是个老歌迷,但做不来专业的乐评,等「作品」的这半个月混迹贴吧、粉丝群对专辑外的事却有些观察。

这半个月挺富有「故事性」的,首先是专辑会收录几首老歌的消息,引起来一些很大的争议。歌曲推出后,如约而至的没惊喜,水平下降的评价也从不让人失望。然后是关于专辑销量的与某肖的「榜一」之争,也颇具话题性。

近期沉迷只狼,为此下载了贴吧看一些梗图贴、萌新直播贴,看伦吧只是顺带,发现了一个有趣的现象,在只狼吧这个由「臭打游戏」的为主的吧里气氛出奇的好,对萌新问题耐心解答,积极帮忙分析问题,鲜有低素质发言。而在伦吧里,围绕嗓子、水准、诚意等关键词总能没啥营养的吵个数十楼,内容将对错好坏排列组合,乐此不疲。

我的想法,这种气氛的区别与讨论还是情绪输出有关,另一层面是两种所针对内容所要求的基本素质的不同。

第一个层面,🐺吧里,帖子主要是萌新提问、玩梗、经验分享,能出现的最大的争议可能就是对某个招式的处理哪种更优了,通常是有客观结果的,即使没有也能由习惯、个人喜好来结束讨论。而关于音乐作品「好与坏」就没办法「讨论」了,只能是我说好你说不行的简单的主观情绪输出了,由此出来的不友善的对话也就在所难免了。

所谓的第二个层面,其实与上述也相关,前面提到讨论音乐作品的「好与坏」没办法讨论,也不是绝对的,倘若是有深厚乐理基础的几个朋友,也是能对音乐作品风格、编曲、旋律编排进行深入讨论的,只是这种讨论不太会出现在贴吧里就是了。而针对特定游戏不同,通过几十个小时的时间,就可以是个「专家了」,而在游戏贴吧里,又极少有对多个游戏的横向比较,也减少了矛盾的出现。简单的说,讨论音乐质量本身有较高的门槛,缺少这些基础很容易就陷入「好坏陷阱」,游戏与音乐不同的一点是除了本身质量这种门槛较高的讨论外,玩家可以就游戏技巧、路线方案等更具有客观属性的内容进行讨论,形成一种感官上更好的交流氛围。

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.

酸评刘大鹅宇宙

2022年1月25日 16:20

进入刘大鹅宇宙的大概路径是这样的:在首页点进导演小策与凤凰传奇合作的短片,在弹幕里感受到了黑天🦢广场场长刘大鹅的人气,之后开始在导演小策的投稿里开始「刷剧」,大姨和大爷们淳朴的表演、接地气的「经典改编」使我实实在在沉迷了一整个居家隔离的下午。

过程中一直晃晃悠悠有些想法,尝试在此记录下来。

以下言论源自瞎琢磨与身边社会学

导演小策给「演员」们带来了什么

环荫柳村藏龙卧虎,或者说中国乡村就藏卧着很多曲艺龙虎,导演小策是那个把龙虎叫醒的人。有昆曲背景的曹姨,在最近一期的颁奖短剧中一开嗓就让我大受震撼。操着东北口音的年轻人三炮,一个生活中表演欲强,能说会道的「邻家大爷」,成功塑造了一个类刘能的形象。开火锅店的二狗,「舞痴」人设,演被暴打的黑社会竟带着一股该死的高级感。

导演小策和它新的演员们互相成就着,导演小策享受着「B站第一导演」的追捧,那么导演小策给他的「演员」们带来了什么?

线上很容易量化,在线上,他们是十足的「大明星」,甚至有了很多衍生梗。简单扫了一眼,「刘大鹅宇宙」的核心人物鹅姨和三炮都有了10w+的关注量,这是很多年轻人望尘莫及的数字。

那么线下呢?泛泛想来

  1. 多了项娱乐,家务农忙,麻将闲聊之外的又一种娱乐,可能在精神上的价值不弱于线上那些金灿灿的数字。
  2. 收益的分成,改善生活,收益这点虽然只能揣测,相信应该也不错。

再之后,「演员」们对自己作品作何感呢?客观来说,这些作品都是面向年轻人的,一些桥段和梗在我看来「演员」们理解是有「困难」的,那么在这种情况下,「演员」们是否能百分百在享受演出呢,或者只是我照着台词念,我理解不了观众为什么会笑,但观众喜欢我也就开心了?

长辈给晚辈熬的一锅粥🥣

所以有了这一段思考,就像因为我喜欢咸粥,姥姥每次给我煮咸粥,我也不曾想过姥姥自己是否喜欢,或者只是我多喝一碗,她就开心吧,我自己都觉得自己矫情,但还想继续往下想。

一个极端、不恰当的比喻,我联想到了耍猴🐒。。

猴子不清楚自己跳上跳下的笑点在哪,只是觉得周围很热闹,自己听老师傅的话,作出相应的动作,观众就会笑。「演员」们可能无法理解自己说的台词,但享受自己念的台词逗笑观众,这种情况下「演员」们是否百分百享受了演出真的重要吗?

这段想法其实是种引申,我看过花絮,导演小策会在片场细心的给「演员」们科普一些网络梗,只是我单方面的觉得一些网络梗长辈们理解起来比较困难,才有了这段的矫情🤦‍♂️,但不管长辈们感受如何,晚辈们真的很喜欢这一锅粥。

取巧又讨巧的剧本📒

作为十几分钟的短剧,导演小策完成的不错。

其中几个剧本形成了这样一个套路:选一个在年轻人圈子中耳熟能详的电影,取其故事的框架;将「原创人设」原封不动或微调之后放入新的故事中;在角色与原角色的碰撞中产生新的情节。例如开宗立派之于师父,教妇使用了多个刘华强的名场面。

从这个层面上来说,我认为,短片内容其实和B站中经典桥段的剪辑二创没什么区别,只是形式上作为拍摄的有导演有编剧有演员的短片,本质上观众消费的还是影视剧中的经典桥段,所谓「名场面」,观众很喜欢这种短平快,无需费脑筋来熟悉人物,只需要把新角色与原角色做简单的都应就能理解整个故事,此之谓讨巧。

而取巧也就好理解了,碍于时长和编剧的水平(不是否定编剧,意思是在短时间内用原创故事吸引观众太难),选取经典故事的框架、经典段落可以快速的让观众看明白整个故事,观众在在短片和经典故事完成匹配后,生出一点洋洋自得的感情(我的看片水平很高,看出两者间的联系),进而降低对短片实际故事性的要求。

原创故事是很困难的,这是编剧水平的限制,而在十几分钟内讲好这个故事又考验导演的能力,显然,这对导演小策的团队来说比较困难。所以有几个故事就只能在剧本上取个巧,借用经典故事、桥段,观众更易进入故事,也降低了对导演的要求。

我不想,也无法否定这几个作品的价值,我希望导演小策可以在众星捧月中正视自己的团队,明白在这几个作品中,团队在走一个捷径,至于这条捷径通向何方、能支持团队走多远这没人知道。

看过南周对导演小策的访谈,他提到离开朱一旦的原因是因为他发现自己可以轻易的根据套路写出一个剧本,他体会不到创作的感觉,因此,在我这,导演小策是有追求的,他不会在捷径上走太久。

我毫不怀疑之后他带着团队创作出更多更好的水平与此相当、甚至更好的有原创故事的短片。

阅读懒惰症

2022年1月19日 12:29

最近尝试阅读两个作品:电视剧结束后有段热情看了几页「雪中悍刀行」,近日翻起了「丰乳肥臀」,都很难投入进去。

发现自己患上了阅读懒惰症,症状是这样,我也是参照前段时间读完的「金拇指」、「生化保姆」才给自己确诊的,前面提到的两本书一个特点是在场景描写上花了大笔墨,「雪中悍刀行」中对人物描写,「丰乳肥臀」中对青稞丛、大种马的描写,给我深入故事造成困难。而顺畅读完的郑渊洁的两本童话则更偏重故事,场景都是生活场景不难想象,作者更多的笔墨花在人物的内心活动上,多了趣味,也不影响沉浸在故事当中。

想到这,似乎自己对阅读的需求更多的是故事,而非语言、文笔。细想来,这与看剧、刷B站影视区也并无差别,只想听故事,懒得去生嚼文字。

直到读到「听到了从花马鼻孔里喷出的喘息声,闻到了酸溜溜的马汗的味道」,发觉自己对生动的描写还是感冒的,而这些让我提不起兴趣的描写也是阅读中养分的重要组成,只是汲取稍难些。

阅读,与刷剧不同的一点,是可以自己重构作者的想象,当然前提是大脑要多消耗一些能量。

花,开山乡

2021年10月15日 09:34

放在往常,这样一部乡村振兴题材的剧,搭配如此的演员阵容我是断然不会看的,实话实讲,我这人看电视就图一演员,凑巧,前几日看了《功勋 · 能文能武李延年》,对演员王雷颇为喜欢,这才追起了这个剧。整部剧制作水平不俗,只是后期部分关于政策的台词有多处改动,对不上口型,影响了观感。

先整理一下剧情的大致脉络,在党组织涣散的某村,年富力壮的村主任架空了老支书,在县里知名企业的利益输送下,出卖村集体利益,同时其亲戚作为治保主任在村里各种耍无赖。前驻村工作队队长在任因病去世,队长生前对村主任的一些违规行为做了记录,而笔记本在后期才被老支书交了出来。从中央机关选派来的第一书记,军人出身,到村后首先将村党组织『重建』。继而通过对村民晓明利害,统一了两山论的思想,依法将被出卖的村集体利益索回。之后就是发展各产业的筚路蓝缕。至于对矿的非法开采,私以为完全作为支线,在此不述。

总结一下我认为这部剧有价值的地方:

第一书记,指从各级机关优秀年轻干部、后备干部,国有企业、事业单位的优秀人员和以往因年龄原因从领导岗位上调整下来、尚未退休的干部中选派到村(一般为软弱涣散村和贫困村)担任党组织负责人的党员。

1、表现了『第一书记』必要性

2、基层党组织的重要性

由于还只是部电视剧,为了增加戏剧性,把王雷饰演的男主一定程度上神化了。比如在各个阶段都能凭借私人关系和超强的能力化险为夷,突出体现在自己担任律师为塌方死去的村里的务工人员讨说法、招商引资通过战友一波灰色操作进入会场等等。

一方面来讲,基层困难的工作需要如此超强能力的干部来开展,但反过来想,如果这些困难的工作都要男主这样的『超强』能力才能开展,那不得不让人对基层的党建和扶贫工作担忧。

我深深的感谢像男主这样默默付出的『第一书记』们,他们在民族复兴、乡村振兴的路上替全体党员干部在负重前行。

期待更多的不把干部和群众都脸谱化的电视剧。

快递员、快递柜和我

2021年8月23日 14:09

写这几段是源于一个帖子。

原贴在:https://www.v2ex.com/t/797080

大家在讨论中,讲了政策、成本几个角度,从大的层面说,监管确实缺位,但客观上讲,快递配送这最后一公里形成强有力的规范过于困难。

我想表达的是,有人认为放快递柜是一种单方面的有利于快递服务提供商的,对这部分人我持保留意见。

就我个人而言,上学期间,快递一直是「强制」放在快递点的,在快递点提货需要排队、与工作人员「交互」甚至取件距离也会更远。自提点工作时间固定,而快递柜晚上11点回去也可以取,所以在上学时期,如果是快递柜的配送方式方式我反倒更高兴,所以在对快递柜的认知方面,由于是在学校期间和自提点对比形成的,我是对快递柜是有好感的,这是需要说明的地方。

工作租房开始,更多开始在家收快递,用顺丰不多,韵达是默认放在快递柜,京东是每件打电话,家里有人就上楼,家里没人就放快递柜,其他快递就直接自提点了,小区自提点还挺远的,不过骑车也不麻烦。

我评价最高的是京东的小哥,家里来的行李是发的京东,两大包被褥和衣服,那天正好是雨天,因为是工作日,白天我没在家,小哥打电话说先放自提点,我对快递员的心理预期是帮我送到家楼下是最后,万一人在单位,东西在自提点自己费些力气取也没什么问题,于是后半句也没认真听,说了谢谢就挂了电话。当我到自提点的时候,想自己抱回去,自提点小哥说这是暂存在这的,京东的小哥说晚上再给你送一趟。晚上收到快递,因为老小区没有电梯,收到快递时真的由衷的谢谢小哥。

所以从心疼快递小哥的体力劳动这一点上,有那么一个东西同时方便了平台和快递员我是支持的。

观20年前影视剧三部

2021年5月1日 15:48

最近看了三部00年代的影视剧,依次是《大江东去》、《绝对权力》和《高纬度战栗》,期间还看了一部《我不是潘金莲》。

前两部整体质量也很高,但个别地方有不足,《高纬度战栗》让我花了一天一夜刷完,30集的长度几乎没有水分,使人完全沉浸于那个高纬度省份。

看完这三部还未过瘾,又想找类似剧目看,但看了几部未找到满意的,遂择这个时间写点东西。近年的《人民的名义》受到好评,与这些剧目类型类似,但个人意见,其探讨的问题深度和人物的丰满程度与这几部过了二十年的剧还是差一些。

《大江东去》中重点讲了两个干部的转变,两位为城市建设、发展立过汗马功劳的市级干部,一个为了对自己这些成绩的“自我犒劳”,另一个为补偿一个等了自己十年的知己而收取了部分「新家庭启动资金」。

《绝对权力》中讨论了,当干部拥有「绝对权力」时,对工作的两面性,一方面在改革发展中,面对难以讨论出结果,得出绝对结论时,利于自己推行自己的执政理念和政策,另一方面,当拥有了绝对权力,即使干部严格约束自己,也无法避免权力被身边的人「偷借」。

《高纬度战栗》借了个刑侦片的壳子,探讨了如何对犯错误的「功臣」评价的问题。提了一个很不政治正确的观点,干部腐坏的过程中群众也负有责任。片中王志飞的刑警角色,让我想到了《破冰行动》中黄景瑜,这或许是此类角色该有的样子。

另一方面,一个很有意思的现象,在这三部剧中,都有「第三者」的形象出现 ,但在那个年代,这类形象没加太多价值判断,而且这些形象也没太使人反感。当然,只是个观察,从核心价值观的角度,把这类形象写的令人讨厌也无坏处。在《高纬度战栗》中,两个重点描写的家庭都对我的恐婚想法有治疗作用,婚姻双方那些为彼此考虑而形成的争吵是可以不那么使人厌恶的。

她老了

2021年2月5日 13:02

背影

昨天傍晚,妈妈蒸了豆沙包子,遣我送些去姥姥家。由于姥姥、姥爷都已是80岁的老人了,饭便都是妈妈和几个姨妈轮流来做,今日轮到了小姨。

进了家门,二老在看着电视,播的是大宅门。姥姥在数年前一次遭了煤气的害之后,耳力就慢慢开始退化,近些年记忆力亦差的厉害,我每次到家,能记起我的名字是多数,偶尔也有记不清的时候。看到我推门进来,姥姥招起了手,唤我在她旁边坐下,我跟姥爷说了来意,把包子在框里包好,在姥姥身边坐下了。

因妈妈在家也做了红豆汤,坐了片刻便起身,跟姥爷说了原因,便想回了。刚站直,趴在姥姥耳边说家里也做了饭,就回去吃了。姥姥想是听了个大概,明白了我要走的意思,说着为什么要走,这里有包子,留下吃吧,说着手有力的又拉我坐下,我笑着看看姥爷,是不好硬走,就又坐了回去。

姥姥嘴里开始重复着刚才的话,这里有饭,吃了再走吧。

又陪姥姥坐了会,看天色也黑了,又趴在姥姥耳边,说家里做了饭,我回家吃吧,要回去了。姥姥听这话,把我手拿过去,紧紧地攥了起来,又重复了几遍让我留下的话,我也又说了几遍家里做饭了的事,姥姥才开始松口。

“那你下午还过来吧?”,是姥姥又糊涂了,错把傍晚当成了正午,这是问我下午还来不来,跟姥姥解释现在不是正午这个事也不简单。因为小脑老化的问题,一天中记不清时刻是常有的事,近些年就更频繁了,有时午休后醒来以为是清晨,或中午打个盹以为到了晚上,都有。

这时在厨房招呼做饭的小姨出来了,见我扯着嗓子也解释不清,忙过来说,他来,他下午还过来。终于得以「脱身」,姥姥松开了手。

姥姥虽已糊涂的勤,一些事实,如当前是傍晚,我可能要过几天才能过来这些,本是不必要当真的。而我每次也是想和姥姥说明白,给她老人家解释清楚现在是傍晚,我回去吃晚饭,过几天会再过来。但这次我没再多说,跟小姨和姥爷打了招呼,出了门。

坐上电梯,刚刚姥姥紧握着我手的感觉,确使我突然想起已经过世多年的奶奶了。

奶奶过世已近十年,当时没什么借口,只是小,不想坐车回老家看看奶奶。每年只是逢节和爷爷奶奶的生日才会回,每次也多是吃过午饭便返。

奶奶更早之前因为得病,半个身子使不上劲的,说话也只能简单地发音,但每次都要紧紧的拉着我手,大声的说些什么。我听不懂,其实大家都听不懂,只能大人们猜着奶奶的意思,一句一句的跟我翻译。现在想来,也都是让我好好学习的意思。奶奶为数不多的能说的词是北京,奶奶想我考去北京,如今我25岁,现在看已无去北京的希望。

又突然有个想法,老人们理解儿女、理解孙辈们理解了一辈子,讲道理了一辈子,却只是姥姥这般糊涂之后才有表达自己真实想法的机会。「讲道理」的话,家里做了饭,是要回家吃的,姥爷理解了我,在劝着姥姥让我回。姥姥糊涂了,是不理解的,是「不讲道理」的,她想留下我吃饭,她也说让我留下吃饭。

她老了,我只是出门上了个大学而已。

她早就老了。

域名惊魂II:godaddy取消自动续费

2021年1月19日 14:08

前情回顾请参照【域名惊魂】

域名到期,02-04快到了,收到Godday家发来的邮件多了起来,因为我开了自动续费,所以没收到续费邮件,都是希望我更改支付方式以使自动续费生效。

您的付款方式缺失或即将到期。

切勿冒着失去域名和任何相关网站或电子邮件服务的风险。

如果您设置了备用付款方式,您的产品可能会受到保护。如果您没有备用付款方式,可以[立即添加备份付款方式]

时间来到01-09,已经拖无可拖,遂决定把保护费交上。

【域名惊魂】的铺垫,这次使用微信登陆很顺利的登上了账号。

界面如下:

续费界面

由此可知:可以点击的按钮有

  • 启用自动续费
  • 更新付款

点击[启用自动续费]无明显效果。

点击更新付款有:

更新

之后只能选择新的付款方式,执行到最后一步只能选择Visa信用卡支付。

当时就很懵逼,因为去年续费的时候是可以使用支付宝付款的。

继续翻找,在产品目录下,由于设置了了自动续费,无法直接续费。

取消自动续费需要到公众号->我的续费取消,取消之后就能直接续费了。

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>

关于“高级”、“实用”的一些思考

2020年12月24日 13:51

今天,从师兄那“继承”的系统又出了个问题,debug之余,又重新审视起了这个系统。

这个系统与高级不沾边,甚至某种程度上在今天的视角看来有点“LOW”:

  • PHP
  • Apache2.4
  • MySQL5.6
  • Openlayers 3

与之相对的是,它在校园后勤日常维护中发挥着极其稳定的作用,日常记录后勤人员的养护记录等。

这个项目,我后期参与了一些功能的添加,当时,自己有了一些自己开发应用的小基础,热衷于四处寻找新技术作为玩具,对PHP说是“嗤之以鼻”也不为过,所以,当时被安排任务之后,自己非常躁。

自己毛毛躁躁的,加上技术也是半吊子水平,项目推进过程中,任务的完成的情况基本上是:流程可以走通,但稍加测试可能就会有很多问题。

转眼自己研三上学期也快结束了,中间也参与了几个项目,项目的技术栈如自己所愿都更现代了,但实用程度与之相比都差了一截,这个系统在“日活”两三个用户的情况下,安安静静的躺在一台性能及其拉胯的虚拟主机中,一运转就是数年,作为一个2B的系统,和我们非计算机科班的“练手”项目来看,表现的甚至有点出色。

技术有“新”、“旧”,但没有那么简单的“好”、“坏”,师兄写的代码,从软件设计的角度来看,复用性极差,可读性也一般,但我在今天,没有PHP基础的情况下,能不费什么力的对老系统缝缝补补,某种程度上,维护性却是极好的,项目文件(*.php)躺在文件夹里安安静静的履行着他们的使命。

新技术层出不穷,优秀的程序员在提升开发效率、软件性能各个方面耕耘着,对自己来说,在有点盲目的追求新技术的同时,有点淡化了技术本身就是为了应用这点。使用不同的“新技术”陆陆续续做了一堆好玩但无用的“玩具”应用,却一直没有一个让自己感觉尽善尽美,生产环境可以无可挑剔运转的实用的应用。之后,应该会淡化对“新技术”的追求,更关注一些基础技能和通用理论的强化,争取做一个真的实用的“玩具”。

windows在WSL+CONDA环境自启动

2020年12月22日 16:32

自己做了个python应用,因为依赖相对复杂,所以使用WSL(-ubuntu)的方式部署。

在配置服务随Windows系统自动化启动的时候遇到问题。

因为需要切换环境,所以在使用以下语句,执行启动脚本时:

wsl -d ubuntu-18.04 -u chen "/etc/init.d/start-script"

最直接的想法,使用start-script脚本内容如下:

#! /bin/bash
conda activate env_name
cd /mnt/e/path/to/script
python test.py

但是,在使用conda activate命令切换环境,会因为conda环境没有初始化的问题,无法进入相应环境。

CommandNotFoundError: Your shell has not been properly configured to use 'conda activate'.
To initialize your shell, run

    $ conda init <SHELL_NAME>

Currently supported shells are:
  - bash
  - fish
  - tcsh
  - xonsh
  - zsh
  - powershell

See 'conda init --help' for more information and options.

IMPORTANT: You may need to close and restart your shell after running 'conda init'.

这时候使用下面语句切换环境即可:

source /home/user_name/miniconda3/bin/activate env_name
# 或者
source /home/user_name/anaconda3/bin/activate env_name

修改后的启动脚本为:

#! /bin/bash
source /home/user_name/miniconda3/bin/activate env_name
cd /mnt/e/path/to/script
python test.py

参考

calling-conda-source-activate-from-bash-script

终于等来了冷血狂宴

2020年12月16日 16:58

看海报,首先是没了范冰冰、杨幂这种咖位的演员,看到一半还想起来了李治廷的霓虹,有点遗憾。对我倒是没啥影响。刚开幕的时候感觉汪铎的漆拉脸有点圆,少了点味道,后面换了一些角度感觉就好看的多了。演的最好的居然是易烊千玺和王俊凯,挺贴合人物。

槽点还是挺多的。

整部片看下来,一个最大的槽点是:虽然大家全是法师,战斗依然却全靠肉搏,直到后面修川地藏和使徒出场,才有了点魂术大战的味道,很是过瘾。估计是法术大战太费想象力和钱了,也算是无奈之举。

另外,整个欧洲中世纪的风格也让我感觉不太舒服。来点“中西”

逻辑上的一些问题,情绪的变化有点突兀,减弱了代入感,一次是几个人被“通缉”,却能在天束幽花的府邸里会和,这在现实社会都让人感觉不可思议,更何况神仙打架的魂力世界,白银祭司连这点情报能力都没有,很难让人信服。另一次,众人在探讨麒零可不可以随银尘去的问题的时候,起初严肃的氛围是合理的的,毕竟去便是九死一生,但后面在得到莲泉和银尘的同意后,麒零的表现像是家长同意去游乐场一样,突然调皮起来,整个气氛变化的有点诡异。另外,在关卡设置中,也只有银尘和麒零的天赋能在最后的关卡中给银尘一点生机,若是不让麒零一起去,那银尘真就是不准备给自己一条生路了。

另一点是人物表情确实有点拉胯,影响了观感,但心里有这个思想准备,抗了下来。演员们不需要表情时,一个镜头怼到脸上,画质的提升,看到凡哥的胡茬,观感还是挺爽的。

还有一点是,为了照顾观众们,一些剧情通过回忆的方式做了“注释”,然而观感并不好,比如交代了只有当王爵死后,师徒才能变强,对麒零来说,头发会变成白色。之后才是麒零头发发生变化,这段回忆完全可以通过主剧情来展现,这一段对观众来说,真的不难理解。

值得肯定的点的是,一些对原著的微调是合理的。

比如在原著中,书中描写的使徒对王爵暧昧的感情,会让人理解起来有些不舒服,在这部电影里,突出的是守护这种情感,对我来说好了一大截。

少了文戏,多了打斗场面我觉得是个好路子,我也不太喜欢看他们演的文戏,哈哈哈。

情怀党真是太难了,整个观影过程就是靠着想看一眼吉尔伽美什挺过来的,最后看到吉尔伽美什的四象极限,真的要跳起来了。不过看这架势,郭导下一部筹资应该挺难的,给个机会来个下一部吧,还想看吉尔伽美什。

说到吉尔伽美什,之前看小说的时候就有这种感觉,全书的人的能力在不断的“通货膨胀”,如在小酒馆的时候,一些魂术师在平民面前几乎无敌,等王爵师徒出场,普通的魂术师变成麻瓜,最后一点一点膨胀,先是各个王爵几乎无敌,接着风源的人物登场,变得更厉害。仿佛只能这样才能抓住读者的阅读意愿一样,不知道小说或电影后面会怎么处理,吉尔伽美什毁天灭地之后,后面剧情怎么再推进?这里先打个问号。

rio-tiler在windows中安装使用

2020年12月9日 14:19

之前用rio-tiler,参考rio-viz做了个可以针对多个影像同时提供WTMS服务的小应用,前段时间需要在一台windows服务器上部署,遇到一些问题,记录下来。

rio-tiler依赖于rasterio,rasterio依赖于gdal,在win上安装都有点费劲。

直接使用pip安装rio-tiler会在安装gdal及rasterio时报错,卡住。

最开始的时候在Unofficial Windows Binaries for Python Extension Packages中找到了相对应版本的GDAL和rasterio,安装完成后依然有些问题,如rasterio引入时会报pyproj的CRS定义找不到的错误。

最开始使用conda的默认频道安装,在rio-tiler=2.0.0rc3版本中,依赖的rasterio≥1.1.7,而默认频道中没有该版本。使用conda-forge频道,发现版本比较全。

首先,我是用的miniconda3,体积更小,安装过程也没什么坑,新建环境,激活环境。

conda create -n rasterio python=3.7
conda activate rasterio
conda search rasterio -c conda-forge
Loading channels: done
# Name                       Version           Build  Channel
rasterio                      0.36.0  py27h43d01bd_1  pkgs/main
rasterio                      0.36.0  py35h49e1f75_1  pkgs/main
rasterio                      0.36.0  py36hb8ea33a_1  pkgs/main
rasterio                      1.0.13  py36h6bd7d87_0  pkgs/main
rasterio                      1.0.13  py37h6bd7d87_0  pkgs/main
rasterio                      1.0.18  py36h6bd7d87_0  pkgs/main
rasterio                      1.0.18  py37h6bd7d87_0  pkgs/main
rasterio                      1.0.21  py36h6bd7d87_0  pkgs/main
rasterio                      1.0.21  py37h6bd7d87_0  pkgs/main
rasterio                      1.0.23  py36hbf02ebe_0  conda-forge
rasterio                      1.0.24  py36h163c445_0  conda-forge
rasterio                      1.0.24  py37h163c445_0  conda-forge
rasterio                      1.0.25  py36h163c445_0  conda-forge
rasterio                      1.0.25  py37h163c445_0  conda-forge
rasterio                      1.0.25  py37he350917_1  conda-forge
rasterio                      1.0.26  py36h163c445_1  conda-forge
rasterio                      1.0.26  py37h163c445_1  conda-forge
rasterio                      1.0.27  py36h163c445_0  conda-forge
rasterio                      1.0.27  py37h163c445_0  conda-forge
rasterio                      1.0.28  py36h163c445_0  conda-forge
rasterio                      1.0.28  py36h163c445_1  conda-forge
rasterio                      1.0.28  py36h2617b1b_2  conda-forge
rasterio                      1.0.28  py37h163c445_0  conda-forge
rasterio                      1.0.28  py37h163c445_1  conda-forge
rasterio                      1.0.28  py37h2617b1b_2  conda-forge
rasterio                       1.1.0  py36h039b02d_0  pkgs/main
rasterio                       1.1.0  py36h2617b1b_0  conda-forge
rasterio                       1.1.0  py37h039b02d_0  pkgs/main
rasterio                       1.1.0  py37h2617b1b_0  conda-forge
rasterio                       1.1.1  py36h163c445_0  conda-forge
rasterio                       1.1.1  py36h2617b1b_0  conda-forge
rasterio                       1.1.1  py37h163c445_0  conda-forge
rasterio                       1.1.1  py37h2617b1b_0  conda-forge
rasterio                       1.1.1  py38h163c445_0  conda-forge
rasterio                       1.1.1  py38h2617b1b_0  conda-forge
rasterio                       1.1.2  py36h163c445_0  conda-forge
rasterio                       1.1.2  py37h163c445_0  conda-forge
rasterio                       1.1.2  py37h2617b1b_0  conda-forge
rasterio                       1.1.2  py38h163c445_0  conda-forge
rasterio                       1.1.3  py36h163c445_0  conda-forge
rasterio                       1.1.3  py36h2617b1b_0  conda-forge
rasterio                       1.1.3  py37h163c445_0  conda-forge
rasterio                       1.1.3  py37h2617b1b_0  conda-forge
rasterio                       1.1.3  py38h163c445_0  conda-forge
rasterio                       1.1.3  py38h2617b1b_0  conda-forge
rasterio                       1.1.4  py36h2409764_0  conda-forge
rasterio                       1.1.4  py36ha22ed69_0  conda-forge
rasterio                       1.1.4  py37h02db82b_0  conda-forge
rasterio                       1.1.4  py37h91b820b_0  conda-forge
rasterio                       1.1.4  py38h151dc71_0  conda-forge
rasterio                       1.1.4  py38hef609b1_0  conda-forge
rasterio                       1.1.5  py36h2409764_0  conda-forge
rasterio                       1.1.5  py36ha22ed69_0  conda-forge
rasterio                       1.1.5  py36ha22ed69_1  conda-forge
rasterio                       1.1.5  py37h02db82b_0  conda-forge
rasterio                       1.1.5  py37h02db82b_1  conda-forge
rasterio                       1.1.5  py37h91b820b_0  conda-forge
rasterio                       1.1.5  py38h151dc71_0  conda-forge
rasterio                       1.1.5  py38h151dc71_1  conda-forge
rasterio                       1.1.5  py38hef609b1_0  conda-forge
rasterio                       1.1.6  py36hc1acebe_0  conda-forge
rasterio                       1.1.6  py36hc1acebe_1  conda-forge
rasterio                       1.1.6  py37hce843d0_0  conda-forge
rasterio                       1.1.6  py37hce843d0_1  conda-forge
rasterio                       1.1.6  py38hf2e4ed7_0  conda-forge
rasterio                       1.1.6  py38hf2e4ed7_1  conda-forge
rasterio                       1.1.7  py36hc1acebe_0  conda-forge
rasterio                       1.1.7  py37hce843d0_0  conda-forge
rasterio                       1.1.7  py37hce843d0_1  conda-forge
rasterio                       1.1.7  py38hf2e4ed7_0  conda-forge
rasterio                       1.1.7  py38hf2e4ed7_1  conda-forge
rasterio                       1.1.7  py39h11aa1b2_1  conda-forge
rasterio                       1.1.8  py37hc4b0cd6_0  conda-forge
rasterio                       1.1.8  py38h5653988_0  conda-forge
rasterio                       1.1.8  py39hfec4536_0  conda-forge

之后直接指定版本安装

conda install rasterio=1.1.8

使用conda安装rasterio,顺便也解决了gdal的问题,而且安装过程中也没再遇到其他的问题。

到这一步,一般情况下可能会遇到rio-color装不上的问题,原因是有c++依赖,需要

error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools

这时候,按照提示,使用visual-cpp-build-tools将相关依赖装上即可(勾选以下两项)。

  • MSVC v142 – VS 2019 C++ x64/x86 生成工具
  • Windows 10 SDK (10.0.18362.0)

我在安装好依赖后,还遇到一个问题,大致是"rc.exe"找不到这个执行程序,经过搜索,发现在以下三个目录下都有“rc.exe”,我选择将x64的文件夹(系统为x64)加到了path环境变量里,问题解决。

C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\arm64\
C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64\
C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x86\

至此,不出意外的话,使用以下命令就能将rio-tiler装好了。

pip install rio-tiler --pre

无法读书的时代

2020年7月28日 12:56

经常性的沉迷B站是今年才发生的,之前看B站只关注一些主播的录播视频,翻动态刷完睡觉。现在经常会在推荐页流连许久,即使刷不出什么感兴趣的视频也会机械式的一遍遍的向下滑动。

视频自媒体作为新的风口无疑。之前在文字资讯主导之时,一些自媒体凭借不加求证换来的时效性和追求新鲜、猎奇心理的选题给了传统媒体一记重拳。换到如今,这些从业者实际工作和提供的内容质量本质没太大的变化,只是换了种形式。之前的方式是写手写稿,编辑审稿,排版之后就能和读者见面了,现如今还需要一个台前的主讲人和后期的剪辑。整个内容生产对团队要求更高、过程变得更长,也提供更许多工作岗位。

依托科技不断发展,视频的形式给予了内容表现更多的可能。现今的视频自媒体内容与之前文字内容的关系,一如影视剧与一些纸质小说的关系。在新的载体上,读者变为观众,观众获取内容更直观、具象。

这也让我想到了流行更早的音频节目,比如罗辑思维,其形式,现在想想其实是开了现在b站流行的知识分享节目的先河,回想当时晚上听的那些节目,如今脑补一样的内容配上罗胖老师坐在屏幕中间侃侃而谈,再配上b站的UI,毫无违和感。

之前自己是逼着自己看点书的,现在发现可以看书时间越来越少,微信读书只是偶尔打开看看书架,甚至南方周末的app都很少打开了,自己通过卸载微博躲过了一波信息绑架,如今看来还是未能幸免。

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

更方便的创建hugo文章

2020年6月9日 15:24

思路:

  • 使用hugo的new命令的 --editer参数
  • 新建指令默认输入--editor参数

以typora工具在macOS环境下为例,首先新建/usr/local/bin/typora,给予运行权限

#! /bin/bash
open -a typora $1

新建博文,快捷方式:

#! /bin/bash
blog_path=~/Codes/blog
cd $blog_path
hugo new --editor typora posts/$1.md

之后新建博客,只需:

blog content-title

此外,关于新建博文,自动添加tags和默认目录的问题:

修改$blog_path/archetypes/default.md即可

废弃

有这个想法很久了,开始的想法用golang写个小的cli工具,来更快的新建博文。

今天上手后感觉功能完全没必要,想到之前笔记上记得sed等工具,决定捡回来用一下。

痛点

日常使用,其实还好

$ j blog
$ hugo new posts/new-post.md
$ typora path/to/post.md

然后进入Typora

  1. 修改自动生成的文章Title改成中文。
  2. 从别的文章里复制TagsCategories
  3. 填写tagscategories

执行完以上步骤后才能开始写作,其过程也谈不上复杂,但总给人感觉还是有一部分重复劳动。

目标

我预期的目标

$ hugox new-post 新文章 随笔

然后弹出Typora,开始写作。

动手

逻辑很简单,使用脚本

  1. 进入blog目录。
  2. 新建markdown文档。
  3. 将自动生成的英文Title改为自定义题目。
  4. 新增Tags行。
  5. 根据是否有归档名参数,新增Categories空行或指定归档名。
  6. 使用TyporaCode打开指定文档。

传送门

Hugox

交叉学科之困

2020年3月28日 14:56

这两周工作有点多,加之对历史政治方面有了些兴趣翻看了两天《毛泽东选集》,论文相关的工作都没什么进展,博客也没更新。最近和师兄、同学聊了很多就业方面的事情,牵涉到了对我们专业的爱恨交织的感情,洗澡的时候又添了些新的想法,遂记录下来。

这里谈的交叉学科为「地理信息科学」,维基百科整个词条如下:

地理信息科学(英语:Geographic Information Science,简称GI Science)或地球空间信息学(Geoinformatics)是由全球定位系统(GPS)、地理信息系统(GIS)、遥感(RS)、计算机技术和数字传输网络等一系列现代技术高度集成,及在信息科学与地球系统科学交叉基础上形成的,以信息流为手段,研究地球系统内部物质流、能量流和人流运动规律的一门应用科学,由美籍英裔地理学家迈克尔·弗兰克·古德柴尔德于1992年提出。

地球信息科学的研究对象是地球系统,应用信息论、控制论和系统论来研究地球系统就形成了地球信息科学的方法论。

与地理信息系统相比,他更加侧重与将地理信息作为一门科学,而不仅仅是一门技术实现,主要研究:分布式计算、地理信息的认知、地理信息的互操作、比例尺、空间信息基础设施的未来、地理数据的不确定性和基于GIS的分析、GIS和社会、地理信息系统在环境中的空间分析、空间数据的获取和集成等等。地理信息科学在对地理信息技术研究的同时,还强调了支持地理信息技术发展的基础理论研究的重要性。

我与之的关系简单说就是调剂与被调剂的关系。在本科时候,对于GIS方向的研究,我粗浅的认知是与上文中地理信息系统相比之后的这一大段,自己对空间数据结构和空间分析相关理论的理解等等都抱有极大的求知欲。进入研究生学习后,因为自己调剂后是工程学位及专业的培养方向等等客观因素,自己在做的更多的是工程,也就是所谓的地理信息系统的应用方向。也做出来了一些应用工程,但总感觉自己的技能树变得怪怪的,对自己的核心竞争力产生了怀疑。

我理解的核心竞争力,是自己学科的理论知识体系,自己学习工作中积累的相关技术,可以满足于某个工作岗位的要求、对某个职位有竞争力,且非自己学科的人在短时期内无法具有这种竞争力。

整个地理信息科学知识体系还是挺庞大的,与其名号「地理信息科学」相比,我觉得我的知识路线说成是「测绘」、「信息科学」更为准确,虽然本科课程包括「自然地理」和「人文地理」等学科,但我期望的偏向技术的研究方向与之关联甚少。反倒是「地图学」以及计算机相关学科成了日常学习工作中的理论与技术基础。

对照「地理信息科学」这个学科,结合自己来看,自己的核心竞争力是什么呢?

就「地理」而言,自己的「自然地理」、「人文地理」等课程已经几乎是忘光了的状态,一些GIS软件的操作掌握的还可以,「地图学原理」掌握的也还行,但这些似乎都没达到核心竞争力的高度。对于唯一有作为核心竞争力潜力的「地图学基础」来说,地图投影,空间参考等知识,并不存在短时间内无法了解学习的壁垒,且在开源技术日趋发展的今日,学科外的人甚至无需了解这些也可以做出应用来。

就「信息技术」而言,研究生期间倒是根据工程需求掌握了一些所谓前端、后端的技术栈,但这些技术就可应用于生产的成熟性不说,单自己对自己评价就非常不乐观,因为工程要求时间紧,自己多以QUICK START开始,以可以运行为结束,很难有时间进行更深入的研究,或者说自己没有深入研究的理论基础。简单来说,自己这些「信息技术」与「空间信息」很大程度上是割裂开的。

在「地理信息科学」或者说「地理信息系统」的理论研究相对成熟的今天,培养了我的这个培养方案是否还有存在的必要?

从我的视角来看,我现在做的工作,即使我没有过本科学习,99%的工作依然可以不打折扣的完成,而我新学的技术全都是各个Open Source项目的docs,这让我很失落。这样可以做到的话,意味着没有我「地理信息科学」背景的人都能做到。

我能做到,可是我做得并不好,可以想像一个生命周期从Quick Start到可以运行的应用其质量如何。任何一个软件工程专业的人,甚至任何一个培训班出来的人做的都会比我好。这就是这个培养方案是否还有存在的必要的提问的来源。

这逼着我去跟科班出身的程序员去竞争,用我那羸弱的计算机科学知识体系的基础。或许不这样,自己要有一技之长,这是我想继续读博的想法来源。

想来,我们专业相比于地质、地理学更容易找工作的错觉来源,就是在这个互联网浪潮中,我们勉强的、被逼迫的掌握了一些羸弱的、残缺不全的在这个浪潮上可以使用的「搬砖技能」,仅此而已。

已经有几个师兄投身互联网公司之中了,用他们同样自学来的技术,赢得了上述中的竞争,做着跟我们专业不再相关的工作,可惜,我没有信心能像他们一样在竞争中胜出。

「地理信息科学」也许不在适合是个本科生的学习路线了,那些理论研究让博士们去做吧,如果只是想培养「地理信息系统」的应用人才,那大可不必,软件工程专业的同学已经完全可以胜任了。

图层控制几点问题 Openlayers[2]

2020年3月13日 17:53

openlayers图层控制其实挺易用,使用图层visibility的getter和setter就能实现。但如果考虑组件的绑定,树状控制和同个图层不同属性的控制,问题就麻烦了。

图层与控制器的绑定

首先,怎么实现图层和组件控制器的键值绑定就很棘手,图层数量不多的情况还可以手动的添加图层,一一的将图层和开关绑定。但图层多起来显得就没那么优雅了,维护起来也相当麻烦。

我的解决方法是这样,首先对图层没有固定的id的问题,在加载图层时,为其添加name属性:

let testLayer = new VectorLayer({
    visible: true,
    name: 'test-layer',
    source: testSource
})

有了唯一name之后,可以遍历地图中的图层,找到期望的图层,今而对其操作。

map.getLayers().forEach((layer) => {
    let layerName = layer.values_.name;
		if (layerName !== undefined && layerName === "test-layer" {
        console.log(layer)
	}
});

这样只需将自定的图层名称与控制器绑定,就可以使用控制器的状态来同步图层的显示状态了。

树状控制器

树状控制器不像单一的控制,有父节点和子节点两种,父节点控制整个图层组的显示状态,子节点控制其单个图层,但也受父节点的控制。

一开始的时候我的想法是在id中设置标志符,区分子节点或是父节点,后来一想其实是没必要的,因为对于独立的几个图层来说,只要控制好子节点的状态,组件会自动更新父节点的状态。

例如,在我使用的antd组件中,每次check事件都会产生一个已选项目的id集合,这个集合也就标志着当前地图应该显示的图层,这时候我们只要把集合中包含的,显示状态为隐藏的图层激活显示,将集合中未包含的,显示状态为激活显示的图层隐藏即可,结合之前图层的遍历方法。

function setVisible(keyset) {
//			keyset = ['layer1','layer2',...]
        map.getLayers().forEach(function (layer) {
            let layerName = layer.values_.name;
//			把集合中包含的,显示状态为隐藏的图层激活显示
            if (layerName !== undefined && keyset.includes(layerName)) {
                if (!layer.getVisible()) {
                    layer.setVisible(true);
                }
//			将集合中未包含的,显示状态为激活显示的图层隐藏
            } else if (layerName !== undefined) {
                if (layer.getVisible()) {
                    layer.setVisible(false);
                }
            }
        });
    }

同一图层不同属性要素的控制

对于矢量图层

采用的是更新style的方式,和在前文Openlayers[1]中提到的方法,给未设置显示的要素设置透明的symbol。

以字段 name 为例 ,names为需要显示要素的对应属性值的集合。

export function createStyleDisplay(names) {
    return function (feature) {
        const type = feature.get('name');
        const display = names.includes(type);
        if (display) {
            return new Style({
                stroke: new Stroke({
                    color: '#ffcc33',
                    width: 0
                }),
                image: new Icon({
                    //color: [113, 140, 0],
                    size: 0,
                    crossOrigin: 'anonymous',
                    //  对应类型的个性图标
                    src: `/label/${type}.png`
                })
            })
        } else {
  			//... 透明样式
        }
    }
}

对于栅格图层

我使用的是GeoServer作为服务端,对于栅格瓦片图层,可以采用更新请求参数的方法。

使用cql_filter ,GeoServer有官方说明[link]:对于图层控制的需要,可使用以下语法:

test_field IN ('type1','type2',...,'typeN')

对于一个当前显示的类型集合,应用图层改变示例如下:

export function doCQL(src, field_name, keys) {
    let ql = `${field_name} IN (`
    for (let k in keys) {
        ql += `'keys[k]', `
    }
    ql = ql.substr(0, ql.length - 2)
    ql += ')'
    let pms = src.getParams()
    pms.CQL_FILTER = ql
    src.updateParams(pms)
}

后话

在图层属性中添加name字段其实是无奈之举,没有getter,setter,只是强行从图层对象中获取。但没找到适应以下情况的方法:既可以将图层与唯一ID绑定,又可以通过ID直接获取图层,从而对其操作。

当前对图层操作中,需要当前地图的图层集合来选定某图层,并不是很高效。或许可以建一个hash表,key -> layer,有机会会在下个项目中尝试一下。希望大家如果有更好的解决方法的话,可以与在留言报分享😘。

记第一次PR

2020年3月3日 19:28

前天提交了自己第一个PR,被接收了,挺开心的。

这次PR形式大于内容,只是项目文档有点老旧,一个小地方需要修改,自己在部署过程中被稍微坑了一下。

在维护者几句的交流的过程中也有了一些想法。

自己的对自己的英文表达能力一直没啥自信,因为没什么经验,对交流过程中的遣词造句,俚语表达更是心虚,生怕不经意间表现的不太礼貌之类的。这次交流,自己生硬的很,感觉就像是个没有感情的翻译机器,但自己实在不好把握轻松幽默与礼貌的度,这些东西决定有机会还是要学一下。

严谨的态度。这次,只是改了文档中的一行,就没太考虑好,在自己的知识水平、技术能力桎梏下,以为没什么问题,实际上是一个理所当然的想法。被维护者问了个懵。没什么办法,这点还是要扎实学习技术。

对维护者很敬佩。维护者想来也是在业余时间和我这个新瓜蛋子交流,本来是个很小的改动,开发者自己来可能一分钟整个过程就处理完了,和我跨着时区硬生生六句,交流了三天。对我的一个没什么道理(技术水平)不够的处理,个人感觉还是挺明确的,是我理解不够。维护者措辞充满谦卑,没直接指出,而是问我的理由,是不是确实遇到了问题,最后说了感谢。

对于开源世界,自己一直非常向往。可是对于一个野生程序员来说,真的不简单啊,自己虽然研究生期间也是一直在做工程,可对编码、理论、计算机基础真的都还非常弱,希望自己的技术早日可以积累到可以轻松的给自己喜欢的项目贡献的那一天,这一小步,是一大步哦。

Terrocotta :一个轻量的瓦片服务

2020年3月1日 16:35

Introduction

最近一直想找一个轻量的影像瓦片的服务端,上周一直在看@Vincent Sarago 基于其自己一套工具 rio-tiler , lambda-proxy 的的瓦片服务的provider,前前后后看了衍生的几个项目,包括lambda-tilerlandsat-tilerrio-viz等等,经过简单的测试,感觉前两个工具均需要借助lambda才能发挥正常的性能,第三个应用框架用的tornado,考虑了并发问题,但是个单机应用,“移植”起来工程量挺大的,自己试了试放弃了。在单节点的情况下,请求阻塞问题非常严重,自己试着换了几个应用和服务端的组合,都没太大的改善。另外在单节点情况下,这种每个请求都要重新访问一次数据的方式并不经济。

简单的应用不行,在COG详情页看到了,Geotrellis项目,框架用scala实现的,在projects里发现了一个和需求很相近的实验项目,clone下来运行,并不能成功,好像是应用入口有变化,失败了,自己懒的上手改(不知道怎么改),就想着去quick start 里找个小例子,跑个tiles服务应该挺容易的(呸),scala在国内的新手使用体验是真的难,甚至比golang还难,构建工具sbt从maven中心仓库拉文件,乌龟似的启动速度,自己找了那寥寥无几的几篇更换国内源的方法,中间一度想吐🤮,最后换了华为云的源终于能接受了,sbt.build的诡异语法,硬着头皮坚持到io影像,最新版本的api根本跟docs大不一样了,自己照着api东改西改,又被魔鬼般的implict参数坑了十几分钟后:

$ rm -rf repos/geotrellis-test ~/.sbt
$ brew rmtree sbt

溜了溜了。

Terracotta

Github的feed真是个好东西,替我推荐了好多有用的玩意,Terracotta也是(太难打了,就叫他陶罐吧)。官方描述如下:

Terracotta is a pure Python tile server that runs as a WSGI app on a dedicated webserver or as a serverless app on AWS Lambda. It is built on a modern Python 3.6 stack, powered by awesome open-source software such as Flask, Zappa, and Rasterio.

提供传统部署和Lambda两种方式,轻量,pure python,都挺符合我的口味,“技术栈”也相对新。

陶罐与同样基于函数计算的lambda-tiler相比,不管是从结构来讲,或是理解起来,都是后者更简单。后者的整个流程非常直接,基于COG的portion请求特性和GDAL的VFS(Virtual File Systems),不管你的数据在哪,多大,只要告诉我它数据的本地地址或者HTTP地址,它就可以实时的拉取切片。在lambda的环境下,这种方式在性能上不会有太大问题。但对于在国内使用、部署有两个问题。

  • AWS在国内严重水土不服,给国内使用Lambda造成障碍,Aliyun等国内厂商也有函数计算的服务,但还不太成熟,移植proxy等成本也很高。
  • 一些open access 的数据比如Landsat 8Sentinel-2都托管在S3对象存储上,使用Lambda切片很大程度依赖在AWS各部件上快速访问,但如果在国内提供服务在访问速度上会受很大的影响。

当然,陶罐也是推荐部署在Lambda函数上的,的确,这种方式非常适合动态切片服务,但比Lambda-tiler,它加了一个易用、可靠的头文件的“缓存机制”。

在使用rio-tiler想实现一个可以快速部署在单机上、支持少用户,低请求的动态切片服务时,就曾经想在内存中对同源的数据的头文件缓存下来,因为每一张瓦片都要请求一次源数据获取头文件,在单机环境来看是很浪费的,当时自己的想法有建一个dict,根据数据源地址存储头文件或者建一个sqlite数据库来存储,试了建个dict的方式,但效果并不明显。

而陶罐在业务流程设计上就强制加入了这一点,这使得他在新增数据时会有一个预处理的过程,这比起直接处理有一个延后,但正所谓磨刀不误砍柴工,不得不说,这比起传统的预切片可要快出不少。

除此之外,对数据cog化,头文件注入等流程,陶罐都有很好的api支持。

Quick Start

试用非常简单,先切换到使用的环境,然后

$ pip install -U pip
$ pip install terracotta

查看一下版本

$ terracotta --version
$ terracotta, version 0.5.3.dev20+gd3e3da1

进入存放tif的目标文件夹,以cog格式对影像进行优化。

$ terracotta optimize-rasters *.tif -o optimized/

然后将希望serve的影像根据模式匹配存进sqlite数据库文件。

这里想吐槽一下这个功能,开始的时候我以为是一般的正则匹配,搞半天发现是{}的简单匹配,还不能不使用匹配,醉醉哒。

$ terracotta ingest optimized/LB8_{date}_{band}.tif -o test.sqlite

注入数据库完成后,启动服务

$ terracotta serve -d test.sqlite

服务默认在:5000启动,还提供了Web UI,需要另行启动,开另一个session:

$ terracotta connect localhost:5000

这样Web UI也就启动了。这样可以在提示的地址中访问到了。

Deployment

没看lambda的部署方式,因为大致和lambda-tiler方式差不多,因为国内aws访问半身不遂,移植到阿里云,腾讯云的serverless的成本又太高了,所以才放弃了这种方式。

传统的部署方式如下:

我是在centos的云主机上部署的,和docs里的大同小异。

首先新建环境,安装软件和依赖。

$ conda create --name gunicorn
$ source activate gunicorn
$ pip install cython
$ git clone https://github.com/DHI-GRAS/terracotta.git
$ cd /path/to/terracotta
$ pip install -e .
$ pip install gunicorn

准备数据,例子假设影像文件存放在/mnt/data/rasters/

$ terracotta optimize-rasters /mnt/data/rasters/*.tif -o /mnt/data/optimized-rasters
$ terracotta ingest /mnt/data/optimized-rasters/{name}.tif -o /mnt/data/terracotta.sqlite

新建服务,这里自己踩了两个坑,官方例子使用的是nginx反向代理到sock的方式,自己试了多个方法,没成功,也不想深入了解了。

server {
    listen 80;
    server_name VM_IP;

    location / {
        include proxy_params;
        proxy_pass http://unix:/mnt/data/terracotta.sock;
    }
}

另一个是,应用入口里的入口 版本更新过,service里的和上下文的不一样,修改之后如下

[Unit]
Description=Gunicorn instance to serve Terracotta
After=network.target

[Service]
User=root
WorkingDirectory=/mnt/data
Environment="PATH=/root/.conda/envs/gunicorn/bin"
Environment="TC_DRIVER_PATH=/mnt/data/terracotta.sqlite"
ExecStart=/root/.conda/envs/gunicorn/bin/gunicorn \
            --workers 3 --bind 0.0.0.0:5000  -m 007 terracotta.server.app:app

[Install]
WantedBy=multi-user.target

另外一个地方,使用"0.0.0.0",使外网可以访问。

官方解释如下:

  • Absolute path to Gunicorn executable
  • Number of workers to spawn (2 * cores + 1 is recommended)
  • Binding to a unix socket file terracotta.sock in the working directory
  • Dotted path to the WSGI entry point, which consists of the path to the python module containing the main Flask app and the app object: terracotta.server.app:app

服务里需要指定Gunicorn的执行路径,设置workers数量,绑定socket file,指定应用入口。

设置开机启动,启动服务。

$ sudo systemctl start terracotta
$ sudo systemctl enable terracotta
$ sudo systemctl restart terracotta

这样就能看到服务的表述了。

$ curl localhost:5000/swagger.json

当然,也可以用terracotta自带的client来看一下效果:

$ terracotta connect localhost:5000

Workflow

对与头文件存储方式的选择,sqlite自然是更方便,但mysql的灵活性和稳定性更高了,在线数据可以实现远程注入。

这里碰到点问题,driver的create方法新建失败,自己没看出问题在哪,就从driver里找出表定义,手动新建所需表。

from typing import Tuple

import terracotta as tc
import pymysql


# driver = tc.get_driver("mysql://root:password@ip-address:3306/tilesbox'")
key_names = ('type', 'date', 'band')
keys_desc = {'type': 'type', 'date': 'data\'s date', 'band': 'raster band'}

_MAX_PRIMARY_KEY_LENGTH = 767 // 4  # Max key length for MySQL is at least 767B
_METADATA_COLUMNS: Tuple[Tuple[str, ...], ...] = (
    ('bounds_north', 'REAL'),
    ('bounds_east', 'REAL'),
    ('bounds_south', 'REAL'),
    ('bounds_west', 'REAL'),
    ('convex_hull', 'LONGTEXT'),
    ('valid_percentage', 'REAL'),
    ('min', 'REAL'),
    ('max', 'REAL'),
    ('mean', 'REAL'),
    ('stdev', 'REAL'),
    ('percentiles', 'BLOB'),
    ('metadata', 'LONGTEXT')
)
_CHARSET: str = 'utf8mb4'
key_size = _MAX_PRIMARY_KEY_LENGTH // len(key_names)
key_type = f'VARCHAR({key_size})'

with pymysql.connect(host='ip-address', user='root',
                     password='password', port=3306,
                     binary_prefix=True, charset='utf8mb4', db='tilesbox') as cursor:
    cursor.execute(f'CREATE TABLE terracotta (version VARCHAR(255)) '
                   f'CHARACTER SET {_CHARSET}')

    cursor.execute('INSERT INTO terracotta VALUES (%s)', [str('0.5.2')])

    cursor.execute(f'CREATE TABLE key_names (key_name {key_type}, '
                   f'description VARCHAR(8000)) CHARACTER SET {_CHARSET}')
    key_rows = [(key, keys_desc[key]) for key in key_names]
    cursor.executemany('INSERT INTO key_names VALUES (%s, %s)', key_rows)

    key_string = ', '.join([f'{key} {key_type}' for key in key_names])
    cursor.execute(f'CREATE TABLE datasets ({key_string}, filepath VARCHAR(8000), '
                   f'PRIMARY KEY({", ".join(key_names)})) CHARACTER SET {_CHARSET}')

    column_string = ', '.join(f'{col} {col_type}' for col, col_type
                              in _METADATA_COLUMNS)
    cursor.execute(f'CREATE TABLE metadata ({key_string}, {column_string}, '
                   f'PRIMARY KEY ({", ".join(key_names)})) CHARACTER SET {_CHARSET}')

瓦罐的头文件存储共需要四个表。

Table Describe
terracotta 存储瓦罐版本信息
metadata 存储数据头文件
Key_names key类型及描述
Datasets 数据地址及(key)属性信息

服务启动修改如下:

[Unit]
Description=Gunicorn instance to serve Terracotta
After=network.target

[Service]
User=root
WorkingDirectory=/mnt/data
Environment="PATH=/root/.conda/envs/gunicorn/bin"
Environment="TC_DRIVER_PATH=root:password@ip-address:3306/tilesbox"
Environment="TC_DRIVER_PROVIDER=mysql"

ExecStart=/root/.conda/envs/gunicorn/bin/gunicorn \
            --workers 3 --bind 0.0.0.0:5000  -m 007 terracotta.server.app:app

[Install]
WantedBy=multi-user.target

对于注入本地文件,可参照如下方法:

import os
import terracotta as tc
from terracotta.scripts import optimize_rasters, click_types
import pathlib

driver = tc.get_driver("/path/to/data/google/tc.sqlite")
print(driver.get_datasets())

local = "/path/to/data/google/Origin.tiff"
outdir = "/path/to/data/google/cog"
filename = os.path.basename(os.path.splitext(local)[0])
seq = [[pathlib.Path(local)]]
path = pathlib.Path(outdir)
# 调用click方法
optimize_rasters.optimize_rasters.callback(raster_files=seq, output_folder=path, overwrite=True)

outfile = outdir + os.sep + filename + ".tif"

driver.insert(filepath=outfile, keys={'nomask': 'yes'})

print(driver.get_datasets())

运行如下

Optimizing rasters:   0%|          | [00:00<?, file=Origin.tiff]

Reading:   0%|          | 0/992
Reading:  12%|█▎        | 124/992
Reading:  21%|██▏       | 211/992
Reading:  29%|██▉       | 292/992
Reading:  37%|███▋      | 370/992
Reading:  46%|████▌     | 452/992
Reading:  54%|█████▍    | 534/992
Reading:  62%|██████▏   | 612/992
Reading:  70%|██████▉   | 693/992
Reading:  78%|███████▊  | 771/992
Reading:  87%|████████▋ | 867/992

Creating overviews:   0%|          | 0/1

Compressing:   0%|          | 0/1
Optimizing rasters: 100%|██████████| [00:06<00:00, file=Origin.tiff]
{('nomask',): '/path/to/data/google/nomask.tif', ('yes',): '/path/to/data/google/cog/Origin.tif'}

Process finished with exit code 0

稍加修改就可以传入input 文件名 和 output的文件夹名,就能实现影像优化、注入的工作流。

Reference

Cloud Optimized GeoTIFF (COG) 概述|简介

2020年2月24日 11:36

概述

Cloud Optimized GeoTIFF (COG) 依赖两种辅助技术。

  • 第一种是GeoTiff的存储能力:用特殊方式存储像素,而不仅仅是将未处理的像素直接存储起来。

  • 第二种是HTTP Get 支持的范围请求,这种能力可以让client只请求文件中需要的那部分。

    前者的GeoTIFF存储方式,使后者的请求能方便的获取文件中需要被处理的那部分数据。

GeoTIFF的组织方式

COG使用的两种主要的数据组织技术是瓦片和概览图,数据的压缩也使得数据在线传输变得更高效。

瓦片切片在影像中创建了内置了切片,而不是简单的运用数据的条纹,使用数据的条纹的话,想要获取指定的数据需要读取整个数据,当切片可以被在指定区域快速被获取到成为可能之后,同样的需求只需要访问数据的特定部分就可以了。

概览图创建了同个影像的向下采样的多个版本。向下采样的意思是当从一个原始影像'缩小'时,有很多细节消失掉了(当前的1个像素在原始影像中可能存在100个甚至1000个像素),同时它的数据量也更小。通常一个GeoTIFF会有多个概览图来匹配不同缩放等级。这使得服务端的响应变得更快,因为渲染时只需要返回这个特定的像素值即可,无需再来找出用哪个像素值来表示这1000个像素,但是这也会使得整个文件的体积变大。

通过数据的压缩,会使软件能够快速获取影像,通常会有更好的用户体验,但是使HTTP GET的范围请求的工作更有效率依然是非常重要的。

HTTP Get 范围请求

HTTP的1.1版本引入了一个非常牛的功能:范围请求,在client请求服务端数据的GET请求时使用。如果服务端在response的header中有Accept-Ranges: bytes,这就说明数据中的bytes可以被客户端用任何想用的方式分块的请求。这通常也被称为"Byte Serving", 维基百科中有文章详细解释了其工作原理。client可以从服务端请求需要的bytes,在Web领域,这被广泛地应用,例如视频服务,这样,client就不需要下载下整个文件就可以来操作它了。

范围请求是一个可选的字段,所以服务端并非必须要实现它。但是大多数的云服务提供商(Amazon, Google, Microsoft, OpenStack etc)的对象存储工具提供了这个选项。所以大多数的存储在云上的数据已经能够提供范围请求的服务。

整合

介绍过这两个技术之后,两个部分之间如何一起工作就变得很明显了。GeoTIFF中的瓦片和概览图以确定的结构存储在云端的文件中,这样,范围请求就能请求到文件中相关的部分了。

概览图在client想要渲染一个整幅影像的快视图时起作用,整个过程不需要下载每一个像素,这样一来,请求变成请求体积更小、预先创建的概览图。GeoTIFF文件特定的结构在支持HTTP范围请求的服务端就能使client轻松的获取整个文件中需要的那部分。

切片在一些整幅影像的局部需要被处理或者可视化的时候发挥作用。这可以是概览图的一部分,也可以是全分辨率的。需要注意的是,瓦片组织所有的相关数据的区域在文件中的相同位置,所以范围请求可以在需要的时候获取它。

如果GeoTIFF没有被用概览图和切片 ‘cloud optimized’ 过,同样也能进行一些远程操作,但是它们需要下载整个数据或者需要下载的数据量超过实际需求的的数据。

优势

越来越多的地理信息数据被迁移到了云端☁️,而且其中大多数被存储在基于云服务的对象存储中,比如 S3 or Google Cloud Storage,传统的GIS文件格式能够方便的存储在云端,但是对于提供Web地图瓦片服务或者执行快速的数据处理时,这些格式就不再保持高效了,通常需要将数据全部下载到另一个地方,之后再转换为更优化的格式或者读入内存中。

Cloud Optimized GeoTIFF 通过一些 小技术使得使得数据流更高效,使得基于云服务的地理数据工作流成为可能。在线影像平台比如 Planet PlatformGBDX 使用这种方式提供影像服务从而使影像处理非常快速。使用COG技术的软件能通过获取需要的数据的那部分来优化执行时间。

许多新的地理信息软件比如GeoTrellis, Google Earth EngineIDAHO 同样在他们的软件架构中使用了COG的理念。每一个处理节点高速执行影像处理通过获取COG的部分的文件流。

对于现有的GeoTIFF标准的影响,不像引入一个新的文件格式。因为当前的软件不需要任何的修改也能够读取COG。它们不需要具备处理流文件的能力,只需要简单地将整个文件下载下来并且读取即可。

在云端提供Cloud Optimized GeoTIFF格式的文件能够帮助减少大量的文件拷贝。因为在线的软件能使用流文件而不需要保留其自己的副本,这就变得更加高效,也是当今一种通用的模式。此外,数据提供商无需提供多种格式的数据,因为老式软件和新式软件同样能读取这些数据。数据提供商只需要更新一个版本的数据,与此同时,无需多余的拷贝和下载,多种在线软件都能够同时使用它。

QUICK START

前言

这个教程说明开发者如何使用和生产Cloud Optimized GeoTIFF。

读取

最简单的使用方式是使用GDAL的VSI Curl 功能。可以阅读GDAL Wiki在How to read it with GDAL小节。当今大多数的地理信息软件都在使用GDAL作为依赖库,所以引入GDAL是读取COG功能的最快方式。

Planet 上,所有的数据都已经是COG格式,关于下载有一个小教程: download part of an image 。 大多数教程只讲了关于Planet API 的使用方法,但也说明了GDAL Warp怎样从大的COG文件中提取单个工作区域。

创建

同样在GDAL wiki关于COG的页面,How to generate it with GDAL

$ gdal_translate in.tif out.tif -co TILED=YES -co COPY_SRC_OVERVIEWS=YES -co COMPRESS=DEFLATE

或者使用rio-cogeo plugin:

$ rio cogeo create in.tif out.tif --cog-profile deflate

与多其他的地理信息软件应该也能够添加合适的略缩图和切片。

验证

使用rio-cogeo plugin:

$ rio cogeo validate test.tif

参考

https://www.cogeo.org/

域名惊魂

2020年2月9日 10:49

这个故事告诉我们,没事不要一时性起就重置手机

对现在这个域名yuhang.ch,因为是名字全拼和的声母ch的最简组合。本科的时候就觊觎许久了,但当时受生活费限制(其实也就¥120/1yr),只能选择做活动的几块一年的域名玩。18年12月的时候又有突然想到这个域名,马上Godaddy查询,发现available,当时那个开心呀,没想太多就入了一年,心满意足。

悲剧发生在一次登陆之后,本着看到红点就要点下去的执念,点了开启二步认证,又本着总要试试新鲜事物的作死心态,选择了OAUTH的方式,用的是App Store的开源软件。手机重置的时候,因为对icloud的强烈信任,没点备份,其实当时对Godaddy二步认证最坏的情况有兜底的预想:大不了邮箱收个解除认证的邮件呗。

事实证明,我太乐观了T_T。19年12月我开始收到续费提醒,账号密码登录 -> 请输入二步认证码,心里duang的一下凉了一半,点击了解如何禁用两步认证,好嘛,流程有一整页。一步一步照着流程发邮件,心里长舒一口气,反正是自己的东西,身份证一通乱拍,这还不能证明这号码是我的吗?

三天后,事实证明我依然还是太乐观了,回复如下:

Thank you for the additional information, we are happy to continue. A certified English translation of your government-issued photo ID or alternate identification in English is required to move forward. We recommend utilizing your favorite search engine to find local companies in your area that are certified to do official translations of documents. A Certified Translation consists of the following three parts:

• The document in the original language text • The document in the translated text • A statement signed by the translator or translation company representative, with his or her signature notarized by a Notary Public, attesting that the translator or translation company representative believes the translated document to be an accurate and complete translation of the original document. Sometimes this statement bears the title "Certificate of Accuracy" or "Statement that Two Documents Have the Same Meaning." Some translators will attach a Curriculum Vitae to the notarized statement.

Once this is received, we will inform you if there is anything additional needed to process your request, or we will let you know that your request has been completed.

看到这其实心里是有点恼火的,这么大个企业,连个国内的工作人员都没有吗,我大天国身份证还要我给你们翻译一遍?气归气,抱着对这个域名的爱,老老实实查了查国内有资质的翻译公司,查了查价格,我的天,比我域名的年费还贵,心里duang的一下又凉了1/4。抱着死马当活马医,又原封不动的发了一遍申请,回复是三个步骤的加粗:

Thank you for your documentation unfortunately we cannot accept. A certified English translation of your government-issued photo ID or alternate identification in English is required to move forward. We recommend utilizing your favorite search engine to find local companies in your area that are certified to do official translations of documents. A Certified Translation consists of the following three parts:

• The document in the original language text • The document in the translated text • A statement signed by the translator or translation company representative, with his or her signature notarized by a Notary Public, attesting that the translator or translation company representative believes the translated document to be an accurate and complete translation of the original document. Sometimes this statement bears the title "Certificate of Accuracy" or "Statement that Two Documents Have the Same Meaning." Some translators will attach a Curriculum Vitae to the notarized statement.

Once this is received, we will inform you if there is anything additional needed to process your request, or we will let you know that your request has been completed.

绝望之余,转念一想,自己好歹学习聋哑英语这么多年,不会说也听不懂,但咱读写也是过关的咯,就准备自己给自己翻译,自己给自己开证明:

看着自己觉得帅的一批的签名,心想虽然希望不大,还是又发了一次申请,回复很真实,与上次一字不差。我的怒气槽已然满格,以礼相待不行,于是我拿出了键盘侠的身份牌,开喷:

Thanks for the quick reply from the first two letters. You have the same content twice for me. In the attachment, I provided a clear picture of the ID card, the original Chinese text of the ID card, the English translation of the ID card, and the the required statement is provided attached On the third page, What other needs are there?

I did find a local translation company in my favorite search engine. Their quotes far exceeded the value of my account, about forty dollars. Of course, I can wait for my domain name with a half-year deadline. I will apply again from other service providers, but I don't think this is the service that you, the best service provider in the world.

For my application, I feel that you can transfer my application to a staff who master Chinese. The text on the ID card is very easy to understand and it is easy to prove my identity.

If the application is still rejected, I think I prefer that after the domain name in this account expires, I will use the services of other service providers instead of continuing to submit the application.

Looking forward to your reply.

请无视我蹩脚的英语,大致意思如下:

1.虚假的谢谢

2.我身份证发了,也翻译了,还缺什么?

3.我找翻译公司了,翻译费比我域名还贵,我能怎么办嘛?

4.说我翻译的不行,你们找个内地员工帮我审审行不行嘛?

5.威胁之,你们店大欺客,我以后不来了啊!!

忐忑了几天,终审出炉了:

Unfortunately, we will not be able to assist you with your request as you are not the current account holder. Our department can only make this type of change after verifying the consent of the account holder on file with the required documentation. You will need to contact the person listed on record regarding any assistance with access, or advise them to submit their documentation directly for review.

Questions? Please contact Customer Support at link or call 480-505-8877 anytime, 24/7. We’d be happy to help.

反正就是证明不了我就是我了,一致申请不过的原因,过段时间之后我又想了想可能是注册的时候,我填的的是英文的全拼,所以我身份证的照片证明不了中文英文,但最终我都没能再登录上看看那个账号,这些也都成迷了。

这终审结果虽然使人绝望,但也有些新鲜内容,看到结尾真情款款的客服电话的邀请,我抓紧找了个没人的地方开始拨打。打之前还忐忑了一番,万一是个英文客服,我这口语水平怕是无法应付哦,但为了域名,冲冲冲!

滴滴滴滴几次指引,终于听到人工客服了,是普通话客服,我舒了一口气,开始输出,从讲道理到打苦情牌:

Q:我要禁用二步认证

A:登录的时候有指引,需要发邮件

Q:我发了,说不是我,有其他方法吗

A:...无

Q:我中文身份证还要翻译,是不是不太合理呀

A:...,嗯,但目前只有这一种途径哦

Q:...,谢谢

A:好的,祝您生活愉快

电话过后,就只剩下最后一个方法咯,等到到期,用另一个账户再申请一次,是否有人在这段时期抢注,就只能听天由命了。

于是,在域名到期之后,就开始了每日一次的检查,看看我的宝贝能注册了没。

首先是过期后的前几天,我的解析还是存在的,之后解析失效,搜索还是提示unavailable,就有些不淡定了,查相关资料:

第 1 天 我们会进行第一次计费尝试(共三次),以续订域名。如果计费失败,则域名过期,且域名会被停放。注册人可以免费续订此域名。
第 4 天 我们会进行第二次计费尝试。注册人仍然可以免费续订此域名。任何与此域名关联的网站或电子邮件服务可能会中断。
第 12 天 我们将进行第三次(最后一次)域名续费尝试。注册人仍可以对域名续费,且无需支付额外费用。
第 19 天 支付一年的续订费用外加 80 美元的赎回费用后,注册人可以续订域名。
第 25 天 我们会将域名放入过期域名拍卖行列。
第 31 天 域名可能再也无法赎回。
第 36 天 过期域名拍卖结束。如果在过期域名拍卖过程中没有出现抢注者或出价者,我们会将该域名列入清仓拍卖行列。
第 41 天 清仓拍卖结束。
第 42 天 我们会将域名退回注册局。在发布域名进行常规注册之前,注册局会保留域名。

这可把我急死了,不同的注册局情况还不一样,不确定因素增加,但也只能一天一次的上线查询。

鸡汤一点就是功夫不负有心人,大概也就是在一个半月的时间,睡眼惺忪一如往常的打开链接,available!!砰的从床上弹起来,打开电脑,也来不及找优惠码在哪填了,购物车,付款,支付宝扫码。时隔四十五天,终于重新获得了对yuhang.ch对所有权,撒花🎉。

对新的账号,我选择了电话短信的认证方式,真的挺香的。

矢量瓦片高亮选中 Openlayers[1]

2019年12月16日 18:29

自己做的小Demo中有这样一个小需求:通过数据库检索,获取指定属性的要素,然后高亮显示。

如果采用WFS常规方式加载,不存在问题,遍历layerfeature source即可,不考虑效率,逻辑是没有问题的,但有个需求是图层feature非常多(因为是全球范围海岸线生成的缓冲区),所以地图加载的过程中使用了矢量瓦片的形式,矢量瓦片类型的source没有getFeatures()方法,给需要高亮显示的要素进行遍历造成了麻烦。

图层的静态样式使用openlayers最新例子的方式设置:

//颜色表
const colorTable = {
  "No": "rgba(200, 54, 54, 0)",
  "type1": "#FF0000",
  "type2": "#E69800",
  ...
  "": "#00FFFFFF",
};
export function createRiskLevelStyle(feature) {

  const riskLevel = feature.get('props_X');
  let selected = !!selection[feature.getId()];
  return new Style({
    fill: new Fill({
      //color: colorTable[riskLevel],
      color: selected ? 'rgba(200,20,20,0.8)' : colorTable[riskLevel],
      //color:
    }),
    stroke: new Stroke({
      color: '#FFFFFF',
      width: 0.1,
    })
  })
}

其中selected用于鼠标点击的高亮显示,逻辑大概是点击后将以featureid作为键值存储,标识该要素被选中。

自然的在考虑这个需求的时候,我的首先想法是遍历featureCollection找到相应的要素对应的Id,存进selection全局变量中。但因为矢量瓦片的source没有getFeatures()方法所以这个想法就破产了。之后甚至想再新建一个普通的WFS层用来遍历数据,但数据量实在太大了,一次加载要50几M,这种方式也就彻底破产了。

之后,考虑到既然加载的时候样式可以用这种形式的styleFunc,在检索的时候,给图层赋新的Func会不会有效呢,性能又如何?于是对styleFun微调后如下:

export function createRiskLevelStyleSearch(names) {
    return function (feature) {
        const riskLevel = feature.get('props_X')
        let properties = feature.getProperties()
        let zh = properties['label']
        let en = properties['name']
        let searched = false
        if (zh === undefined || en === undefined) {
            searched = false
        } else {
            names.forEach((v) => {
                if (en === v.key) searched = true
            })
        }
        return new Style({
            fill: new Fill({
                //color: colorTable[riskLevel],
                color: searched ? 'rgba(200,20,20,0.8)' : colorTable[riskLevel]
                //color:
            }),
            stroke: new Stroke({
                color: '#FFFFFF',
                width: 0.1
            })
        })
    }
}

参数names是一个国家名的数组,itemkey对应需要检索的值。

这种方法在这个数据量下,效果还可以,如下图:

WPS 2.0 部分翻译

2019年11月16日 10:53

WPS

提供能力查询接口

任何WPS服务都应该自我包含,提供一个初始节点能被WPS客户端用来确认服务的能力
  • 初始节点(HTTP URI)。

  • 服务应提供一个系统的机制用来发现服务能力。

  • 发现机制应该是合乎逻辑,可以推断出来的。

抽象进程模型

抽象的进程模型对进程描述提供了许多角度和自由度

  • 一个进程需要提供一个独一无二的标识符来区分不同进程群中的进程
  • 进程的标识符应该是个字符串类型或一种更可取的URI
  • 一个进程应该有一个输入的任意数字(0或其他)
  • 进程的每一个输入应该有一个标识符来与其他输入做区分
  • 输入的标识码应该是一个字符串
  • 进程输入的参数应该是叠套的
  • 叠套的输入应该与其他子节点有不同的标识符
  • 一个进程应该有一个或多个输出
  • 进程的每个输出应该有独一无二的标识符来与进程中的其他输出做区分
  • 输出的标识符应该是个字符串
  • 每个进程的输出的基数是1
  • 进程的输出应该是嵌套的
  • 输出的标识符应该与其父节点不同
  • 所有的不作为父节点的输入和输出应该有定义好的数据格式
  • 如果输入输出需要编码表,应该是表2中列出的定义

任务控制

执行能力允许WPS的客户端实例化并执行任务,是一种重要的任务控制能力。另外。忽略和删除一个任务的能力在冲时间的执行过程中对于释放服务器资源是很有意义的。

  • 服务应给每个任务分配唯一的标识符
  • 服务应能返回异常,如果客户端尝试使用一个无效的任务标识符。
  • 服务应提供一个能力去执行进程,给有能力执行的进程建立一个新任务,这个能力能够使客户端执行能力定义中的进程。
  • 服务应该提供一种能力忽略一个已经提交的任务。这种能力能使客户端来指示他们不再对这个任务或者他们的结果感兴趣了,允许服务器尽可能的处理相关的计资源。

进程执行

服务在WPS服务上执行应该既能同步也能异步。同步执行适合任务能在短时间内完成的方法,异步执行更适合任务需要长时间才能完成的情况。

在同步情况下,一个WPS客户端提交一个执行请求到WPS服务端并且在任务执行结束并返回结果之前持续监听反馈。这需要一个客户端和服务器之间保持持续的连接。

在异步的情况下,客户端发送一个执行请求到WPS服务器并且立即收到一个包含状态信息的反馈。这个信息确认这个请求已经被服务器端收到并接受,任务已经在处理,将会在未来一段时间执行完毕。这个状态信息反馈还要包含进程的标识码,以供客户端在之后检查任务是否完成。另外,状态信息包含结果的地址,比如一个在任务结束后能定位处理结果的URL。

  • 服务应该允许客户端确定被执行的进程。
  • 服务应该返回异常当客户端尝试执行一个不可行的进程。
  • 一个成功的执行,服务器应该发送一个结果给客户端,这个结果会包含一个输出数据或者数据的索引。
  • 执行结果应该有失效时间,在失效时间之后这个输出数据将不再能获取。
  • 对于失败的操作,服务应该给客户端返回异常。
  • 服务应该允许客户端制定用于进程执行的输入数据。
  • 在客户端制定了一个无效的输入数据做进程操作的时候应该返回异常。
  • 服务应该允许客户端根据建议的进程可以提供的数据格式类型给输出结果定义一个期望的数据交换格式,
  • 当客户端制定了一个不被支持的数据交换格式时,服务应返回异常。
  • 服务应该提供是否具有同步、异步或都具有的执行能力。
  • 服务要对每个提供的进程允许的执行模式。
  • 如果两种执行模式都支持,应该让客户端定义想要想要使用的计算模式。
  • 如果用户没定义其想要的执行模式,服务应自动选择合适的执行模式。
  • 服务在用户定义不被支持的执行模式时应返回错误。

数据通过值和引用传输

客户端可能会发送和接收数据通过两种不同的方式:1)引用 2)值。简而言之,混合模式时可以的,典型的,小的原子数据比如整数,浮点数和短的字符串通过值提交,大的输入或输出通常用引用提供。

  • 服务可以通过值和引用来接收输入数据
  • 当提供的数据引用不可用时,服务应返回异常。
  • 服务应提供输出数据通过值和引用返回输出数据。
  • 支持的输出模型应该在每一个提供的进程中被指定。
  • 如果输出支持多种模式,应该使用户可选。如果用户指定不支持的传输模式,服务应返回异常。

任务监控

  • 服务应对异步任务提供支持机读的状态信息。
  • 通常,服务应用表三中的基础状态集来描述一个进程任务的当前状态。
  • 服务应报告一个正在运行的任务进程的百分比。
  • 服务应该提供一个进程结果可用的大致时间。
  • 服务应该建议一个下次进行状态查询时间。
  • 服务应报告一个该任务的过期时间,在此时间之后,任务的标识符将无效,现存的资源将被从服务器端移除。

Nba和其他一些事

2019年10月9日 20:59

这次莫雷事件,提升了我许久没有的时事好奇心。

最起初的想法是做一个吃瓜客,当时自己对香港的情况没有太多了解。只是在各种报道上看到香港似乎不太安定。

重新填这个坑已经是2020年了,之所以没继续写,一是开了个头,放下了一段时间,感情也不强烈了。二是不太想在博客中出现太过敏感的话题。

首先,基本立场是我爱祖国,反对各类外来势力干扰我国内政,反对各类自以为是的人借机发表不利于安定的言论。

其次,我对这场舆论的走向有一些自己的看法。

第一,他的言论肯定是不对的。

作为一个相当依赖中国市场的球队的管理层,在其局限的舆论环境下有这个想法,可以理解,但在社交媒体公开发表言论绝对是不对的。

第二,国内媒体对其表示谴责是合理的,正确的。

国内媒体在国际舆论场上有纠正其错误观点的义务,有向国际阐述、说明关于香港相关问题的我们的观点的义务和责任,

第三,在国内大量报道,引导公众舆论,也不全没道理。

此次事件,相比对香港相关情况的直接报道,对一部分人可能更具有吸引力。由次提醒更多的公众积极了解香港相关情况,是有其积极作用的。

第四,引申出的社会各界对其相关业务的“公共制裁”。

最直接的影响是火箭队的转播陆续在各个平台被取消。之后是各平台不再播报火箭队的相关新闻,NBA相关游戏也出了相关对策,对火箭队的球员、球衣进行特殊化处理。

这整个过程来说就让人感觉不再能像前面那几条那样坦然接受了,因为有些逻辑非常别扭。

首先,转播的问题,火箭的经理发表言论,则停止转播火箭队的比赛,乍看之下合情合理。但还有一点是,NBA的总裁萧华的发言其实并没有反对莫雷的言论,如果论影响力和错误程度是不输莫雷的,那NBA的其他比赛为什么不停播呢?因为事出莫雷,其他球队与莫雷的关系没那么直接吗?还是只是因为这中间是个更大的利益整体,无法停播呢?

其二,相关从业者们内心肯定是站在祖国一方的,对莫雷诸人嗤之以鼻。但他们是否必须要牺牲自己的经济利益才能表达内心呢?进一步说,我们是否可以宽容到给予这些人对个人行为的决定权?事实是好像不能。舆论对一切站出来大声的宣布自己与其(及自己经济利益)切割的行为大家赞赏。舆论会找到那些心疼自己经济利益的人,不会是所有人,但总会有一些人会结队对其与国家的感情表示怀疑。这些“公共制裁”,确实影响了莫雷诸人的收益,但同胞中的相关从业者的损失要大得多。

第三,这些“公共制裁”流于形式。论坛上,不能称呼其队名,但大家还是要讨论的,起个别号是不难的。社区驱动的资讯报道中,不能直呼队名,总有其他方法可以报道的。官方频道没有转播,总有其他途径可以收看到。游戏中的球员的衣服换了,卡片上的队名换了,为了吃饭,游戏还在运营,为了消遣,玩家还在点击各个卡片。

总结这整个过程,我由衷的希望,公共媒体在引导大家表达爱国情怀的时候,方式方法还要再揣摩揣摩。国家要爱,也要试着体谅一下自己的同胞。牺牲自己的利益来表达自己的态度,要视作一种普通的个人行为。不要做为榜样宣传来隐形的绑架他人。这样也就避免了上面的流于形式的公共制裁。

❌
❌