前几天,再次看到一些CI的Badge的时候,就想着要做一个自己的Badge: ![Badge][1] 接着,我就找了个图形工具简单地先设计了下面的一个Badge: ![Demo][2] 生成的格式是SVG,接着我就打开SVG看看里面发现了什么。 ```xml ``` 看了看代码很简单,我就想这可以用代码生成——我就可以生成出不同的样子了。 SVG与SVGWrite --- SVG就是一个XML > 可缩放矢量图形(Scalable Vector Graphics,SVG) ,是一种用来描述二维矢量图形的XML 标记语言。 要对这个XML进行修改也是一件很容易的事。只是,先找了PIL发现不支持,就找到了一个名为SVGWrite的工具。 > A Python library to create SVG drawings. 示例代码如下: ```python import svgwrite dwg = svgwrite.Drawing('test.svg', profile='tiny') dwg.add(dwg.line((0, 0), (10, 0), stroke=svgwrite.rgb(10, 10, 16, '%'))) dwg.add(dwg.text('Test', insert=(0, 0.2))) dwg.save() ``` 然后我就照猫画虎地写了一个: ```python import svgwrite dwg = svgwrite.Drawing('idea.svg', profile='full', size=(u'1006', u'150')) shapes = dwg.add(dwg.g(id='shapes', fill='none')) shapes.add(dwg.rect((0, 0), (640, 150), fill='#5E6772')) shapes.add(dwg.rect((640, 0), (366, 150), fill='#2196F3')) shapes.add(dwg.text('PHODAL', insert=(83, 119), fill='#FFFFFF',font_size=120, font_family='Helvetica')) shapes.add(dwg.text('idea', insert=(704, 122), fill='#FFFFFF', font_size=120, font_family='Helvetica')) dwg.save() ``` 发现和上面的样式几乎是一样的,就顺手做了剩下的几个。然后想了想,我这样做都一样,一点都不好看。 高级Badge --- 第一眼看到 ![Idea Prototype][4] 我就想着要不和这个一样好了,不就是画几条线的事么。 ```python def draw_for_bg_plus(): for x in range(y_text_split + rect_length, width, rect_length): shapes.add(dwg.line((x, 0), (x, height), stroke='#EEEEEE', stroke_opacity=0.3)) for y in range(rect_length, height, rect_length): shapes.add(dwg.line((y_text_split, y), (width, y), stroke='#EEEEEE', stroke_opacity=0.3)) for x in range(y_text_split + max_rect_length, width, max_rect_length): for y in range(0, height, max_rect_length): shapes.add(dwg.line((x, y - 4), (x, y + 4), stroke='#EEEEEE', stroke_width='2', stroke_opacity=0.4)) for y in range(0, height, max_rect_length): for x in range(y_text_split + max_rect_length, width, max_rect_length): shapes.add(dwg.line((x - 4, y), (x + 4, y), stroke='#EEEEEE', stroke_width='2', stroke_opacity=0.4)) draw_for_bg_plus() ``` 就有了下面的图,于是我又按照这种感觉来了好几下 ![Finally][3] 最后代码 --- GitHub: [https://github.com/phodal/brand](https://github.com/phodal/brand) [1]: /static/media/uploads/badge.png [2]: /static/media/uploads/demo.png [3]: /static/media/uploads/finally-brand.jpg [4]: /static/media/uploads/brand-idea-prototype.jpg 尽管没有特别的动力去构建一个全新的CMS,但是我还是愿意去撰文一篇来书写如何去做这样的事——编辑-发布-开发分离模式是如何工作的。微服务是我们对于复杂应用的一种趋势,编辑-发布-开发分离模式则是另外一种趋势。在上篇文章《[Repractise架构篇一: CMS的重构与演进](https://github.com/phodal/repractise/blob/gh-pages/chapters/refactor-cms.md)》中,我们说到编辑-发布-开发分离模式。 ##系统架构 如先前提到的,Carrot使用了下面的方案来搭建他们的静态内容的CMS。 ![Carrot][1] 在这个方案里内容是用Contentful来发布他们的内容。而在我司[ThoughtWorks](https://www.thoughtworks.com/)的官网里则采用了Github来管理这些内容。于是如果让我们写一个基于Github的CMS,那么架构变成了这样: ![Github 编辑-发布-开发][2] 或许你也用过Hexo / Jekyll / Octopress这样的静态博客,他们的原理都是类似的。我们有一个代码库用于生成静态页面,然后这些静态页面会被PUSH到Github Pages上。 从我们设计系统的角度来说,我们会在Github上有三个代码库: 1. Content。用于存放编辑器生成的JSON文件,这样我们就可以GET这些资源,并用Backbone / Angular / React 这些前端框架来搭建SPA。 2. Code。开发者在这里存放他们的代码,如主题、静态文件生成器、资源文件等等。 3. Builder。在这里它是运行于Travis CI上的一些脚本文件,用于Clone代码,并执行Code中的脚本。 以及一些额外的服务,当且仅当你有一些额外的功能需求的时候。 1. Extend Service。当我们需要搜索服务时,我们就需要这样的一些服务。如我正考虑使用Python的whoosh来完成这个功能,这时候我计划用Flask框架,但是只是计划中——因为没有合适的中间件。 2. Editor。相比于前面的那些知识这一步适合更重要,也就是为什么生成的格式是JSON而不是Markdown的原理。对于非程序员来说,要熟练掌握Markdown不是一件容易的事。于是,一个考虑中的方案就是使用 Electron + Node.js来生成API,最后通过GitHub API V3来实现上传。 So,这一个过程是如何进行的。 ###用户场景 整个过程的Pipeline如下所示: 1. 编辑使用他们的编辑器来编辑的内容并点击发布,然后这个内容就可以通过GitHub API上传到Content这个Repo里。 2. 这时候需要有一个WebHooks监测到了Content代码库的变化,便运行Builder这个代码库的Travis CI。 3. 这个Builder脚本首先,会设置一些基本的git配置。然后clone Content和Code的代码,接着运行构建命令,生成新的内容。 4. 然后Builder Commit内容,并PUSH内容。 这里还依赖于WebHook这个东西——还没想到一个合适的解决方案。下面,我们对里面的内容进行一些拆解,Content里面由于是JSON就不多解释了。 ##Builder: 构建工具 Github与Travis之间,可以做一个自动部署的工具。相信已经有很多人在Github上玩过这样的东西——先在Github上生成Token,然后用travis加密: ```bash travis encrypt-file ssh_key --add ``` 加密后的Key就会保存到``.travis.yml``文件里,然后就可以在Travis CI上push你的代码到Github上了。 接着,你需要创建个deploy脚本,并且在``after_success``执行它: ```yml after_success: - test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && bash deploy.sh ``` 在这个脚本里,你所需要做的就是clone content和code中的代码,并执行code中的生成脚本,生成新的内容后,提交代码。 ``` #!/bin/bash set -o errexit -o nounset rev=$(git rev-parse --short HEAD) cd stage/ git init git config user.name "Robot" git config user.email "robot@phodal.com" git remote add upstream "https://$GH_TOKEN@github.com/phodal-archive/echeveria-deploy.git" git fetch upstream git reset upstream/gh-pages git clone https://github.com/phodal-archive/echeveria-deploy code git clone https://github.com/phodal-archive/echeveria-content content pwd cp -a content/contents code/content cd code npm install npm install grunt-cli -g grunt mv dest/* ../ cd ../ rm -rf code rm -rf content touch . if [ ! -f CNAME ]; then echo "deploy.baimizhou.net" > CNAME fi git add -A . git commit -m "rebuild pages at ${rev}" git push -q upstream HEAD:gh-pages ``` 这就是这个builder做的事情——其中最主要的一个任务是``grunt``,它所做的就是: ```javascript grunt.registerTask('default', ['clean', 'assemble', 'copy']); ``` ##Code: 静态页面生成 Assemble是一个使用Node.js,Grunt.js,Gulp,Yeoman 等来实现的静态网页生成系统。这样的生成器有很多,Zurb Foundation, Zurb Ink, Less.js / lesscss.org, Topcoat, Web Experience Toolkit等组织都使用这个工具来生成。这个工具似乎上个Release在一年多以前,现在正在开始0.6。虽然,这并不重要,但是还是顺便一说。 我们所要做的就是在我们的``Gruntfile.js``中写相应的生成代码。 ```javascript assemble: { options: { flatten: true, partials: ['templates/includes/*.hbs'], layoutdir: 'templates/layouts', data: 'content/blogs.json', layout: 'default.hbs' }, site: { files: {'dest/': ['templates/*.hbs']} }, blogs: { options: { flatten: true, layoutdir: 'templates/layouts', data: 'content/*.json', partials: ['templates/includes/*.hbs'], pages: pages }, files: [ { dest: './dest/blog/', src: '!*' } ] } } ``` 配置中的site用于生成页面相关的内容,blogs则可以根据json文件的文件名生成对就的html文件存储到blog目录中。 生成后的目录结果如下图所示: ``` . ├── about.html ├── blog │ ├── blog-posts.html │ └── blogs.html ├── blog.html ├── css │ ├── images │ │ └── banner.jpg │ └── style.css ├── index.html └── js ├── jquery.min.js └── script.js 7 directories, 30 files ``` 这里的静态文件内容就是最后我们要发布的内容。 还需要做的一件事情就是: ```javascript grunt.registerTask('dev', ['default', 'connect:server', 'watch:site']); ``` 用于开发阶段这样的代码就够了,这个和你使用WebPack + React 似乎相差不了多少。 ##编辑-发布-开发分离 在这种情形中,编辑能否完成工作就不依赖于网站——脱稿又少了 个借口。这时候网站出错的概率太小了——你不需要一个缓存服务器、HTTP服务器,由于没有动态生成的内容,你也不需要守护进程。这些内容都是静态文件,你可以将他们放在任何可以提供静态文件托管的地方——CloudFront、S3等等。或者你再相信自己的服务器,Nginx可是全球第二好(第一还没出现)的静态文件服务器。 开发人员只在需要的时候去修改网站的一些内容。 So,你可能会担心如果这时候修改的东西有问题了怎么办。 1. 使用这种模式就意味着你需要有测试来覆盖这些构建工具、生成工具。 2. 相比于自己的代码,别人的CMS更可靠? 需要注意的是如果你上一次构建成功,你生成的文件都是正常的,那么你只需要回滚开发相关的代码即可。旧的代码仍然可以工作得很好。 其次,由于生成的是静态文件,查错的成本就比较低。 最后,重新放上之前的静态文件。 [1]: /static/media/uploads/carrot.png [2]: /static/media/uploads/travis-edit-publish-code.png > 动态网页是下一个要解决的难题。我们从数据库中读取数据,再用动态去渲染出一个静态页面,并且缓存服务器来缓存这个页面。既然我们都可以用Varnish、Squid这样的软件来缓存页面——表明它们可以是静态的,为什么不考虑直接使用静态网页呢? 为了实现之前说到的``编辑-发布-开发分离``的CMS,我还是花了两天的时间打造了一个面向普通用户的编辑器。效果截图如下所示: ![Echeveria Editor][1] 作为一个普通用户,这是一个很简单的软件。除了Electron + Node.js + React作了一个140M左右的软件,尽管打包完只有40M左右 ,但是还是会把用户吓跑的。不过作为一个快速构建的原型已经很不错了——构建速度很快、并且运行良好。 尽管这个界面看上去还是稍微复杂了一下,还在试着想办法将链接名和日期去掉——问题是为什么会有这两个东西? ##从Schema到数据库 我们在我们数据库中定义好了Schema——对一个数据库的结构描述。在《[编辑-发布-开发分离](https://www.phodal.com/blog/editing-publishing-coding-seperate/) 》一文中我们说到了echeveria-content的一个数据文件如下所示: ```javascript { "title": "白米粥", "author": "白米粥", "url": "baimizhou", "date": "2015-10-21", "description": "# Blog post \n > This is an example blog post \n Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ", "blogpost": "# Blog post \n > This is an example blog post \n Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \n Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." } ``` 比起之前的直接生成静态页面这里的数据就是更有意思地一步了,我们从数据库读取数据就是为了生成一个JSON文件。何不直接以JSON的形式存储文件呢? 我们都定义了这每篇文章的基本元素: 1. title 2. author 3. date 4. description 5. content 6. url 即使我们使用NoSQL我们也很难逃离这种模式。我们定义这些数据,为了在使用的时候更方便。存储这些数据只是这个过程中的一部分,下部分就是取出这些数据并对他们进行过滤,取出我们需要的数据。 Web的骨架就是这么简单,当然APP也是如此。难的地方在于存储怎样的数据,返回怎样的数据。不同的网站存储着不同的数据,如淘宝存储的是商品的信息,Google存储着各种网站的数据——人们需要不同的方式去存储这些数据,为了更好地存储衍生了更多的数据存储方案——于是有了GFS、Haystack等等。运营型网站想尽办法为最后一公里努力着,成长型的网站一直在想着怎样更好的返回数据,从更好的用户体验到机器学习。而数据则是这个过程中不变的东西。 尽管,我已经想了很多办法去尽可能减少元素——在最开始的版本里只有标题和内容。然而为了满足我们在数据库中定义的结构,不得不造出来这么多对于一般用户不友好的字段。如链接名是为了存储的文件名而存在的,即这个链接名在最后会变成文件名: ```javascript repo.write('master', 'contents/' + data.url + '.json', stringifyData, 'Robot: add article ' + data.title, options, function (err, data) { if(data.commit){ that.setState({message: "上传成功" + JSON.stringify(data)}); that.refs.snackbar.show(); that.setState({ sending: 0 }); } }); ``` 然后,上面的数据就会变成一个对象存储到“数据库”中。 今天 ,仍然有很多人用Word、Excel来存储数据。因为对于他们来说,这些软件更为直接,他们简单地操作一下就可以对数据进行排序、筛选。数据以怎样的形式存储并不重要,重要的是他们都以文件的形式存储着。 ##git作为NoSQL数据库 在控制台中运行一下 ``man git``你会得到下面的结果: ![Man Git][2] 这个答案看起来很有意思——不过这看上去似乎无关主题。 不同的数据库会以不同的形式存储到文件中去。blob是git中最为基本的存储单位,我们的每个content都是一个blob。redis可以以rdb文件的形式存储到文件系统中。完成一个CMS,我们并不需要那么多的查询功能。 > 这些上千年的组织机构,只想让人们知道他们想要说的东西。 我们使用NoSQL是因为: 1. 不使用关系模型 2. 在集群中运行良好 3. 开源 4. 无模式 5. 数据交换格式 我想其中只有两点对于我来说是比较重要的``集群``与``数据格式``。但是集群和数据格式都不是我们要考虑的问题。。。 我们也不存在数据格式的问题、开源的问题,什么问题都没有。。除了,我们之前说到的查询——但是这是可以解决的问题,我们甚至可以返回不同的历史版本的。在这一点上git做得很好,他不会像WordPress那样存储多个版本。 ###git + JSON文件 JSON文件 + Nginx就可以变成这样一个合理的API,甚至是运行方式。我们可以对其进行增、删、改、查,尽管就当前来说查需要一个额外的软件来执行,但是为了实现一个用得比较少的功能,而去花费大把的时间可能就是在浪费。 git的“API”提供了丰富的增、删、改功能——你需要commit就可以了。我们所要做的就是: 1. git commit 2. git push [1]: /static/media/uploads/eche-editor-screenshot.png [2]: /static/media/uploads/man-git.png 记录一下自己做的一个小东西,当然你也可以在github上找到它:[https://github.com/phodal/gmap-solr](https://github.com/phodal/gmap-solr) ##Solr > Solr是一个高性能,采用Java5开发,基于Lucene的全文搜索服务器。同时对其进行了扩展,提供了比Lucene更为丰富的查询语言,同时实现了可配置、可扩展并对查询性能进行了优化,并且提供了一个完善的功能管理界面,是一款非常优秀的全文搜索引擎。 简单地说: 它是一个搜索引擎 > 文档通过Http利用XML 加到一个搜索集合中。查询该集合也是通过http收到一个XML/JSON响应来实现。它的主要特性包括:高效、灵活的缓存功能,垂直搜索功能,高亮显示搜索结果,通过索引复制来提高可用性,提供一套强大Data Schema来定义字段,类型和设置文本分析,提供基于Web的管理界面等。 即schema.xml **Solr 安装** brew install solr ##Gmap Solr Polygon 搜索实战 思路: 用Flask搭建一个简单的servrices,接着在前台用google的api,对后台发出请求。 ###Solr Flask 由于,直接调用的是Solr的接口,所以我们的代码显得比较简单: class All(Resource): @staticmethod def get(): base_url = '' url = (base_url + 'select?q=' + request.query_string + '+&wt=json&indent=true') result = requests.get(url) return (result.json()['response']['docs']), 201, {'Access-Control-Allow-Origin': '*'} api.add_resource(All, '/geo/') 我们在前台需要做的便是,组装geo query。 ###Google map Polygon 在Google Map的API是支持Polygon搜索的,有对应的一个 google.maps.event.addListener(drawingManager, 'polygoncomplete', renderMarker); 函数来监听,完成``polygoncomplete``时执行的函数,当我们完成搜索时,便执行``renderMarker``,在里面做的便是: $.get('/geo/?' + query, function (results) { for (var i = 0; i < results.length; i++) { var location = results[i].geo[0].split(','); var myLatLng = new google.maps.LatLng(location[0], location[1]); var title = results[i].title; marker = new google.maps.Marker({ position: myLatLng, map: map, title: title }); contentString = '
简介: {{result.body}}
" + feature.get('distance') + "公里
" }); $(element).popover('show'); } else { $(element).popover('destroy'); } }); 当用户点击时,调用Bootstrap的Popover来显示信息。 ##其他: 服务端代码: [https://github.com/phodal/django-elasticsearch](https://github.com/phodal/django-elasticsearch) 客户端代码: [https://github.com/phodal/ionic-elasticsearch](https://github.com/phodal/ionic-elasticsearch) [1]: /static/media/uploads/elasticsearch_ionit_map.jpg 在设计 lan (Github: [https://github.com/phodal/lan](https://github.com/phodal/lan)) 物联网平台的时候,结合之前的一些经验,构建出一个实际应用中的物联网构架模型。 然后像[lan](https://github.com/phodal/lan)这样的应用,在里面刚属于服务层。 ##物联网层级结构 通常,我们很容易在网上看到如下图所示的三层结构: ![物联网三层结构][1] 从理论上划分这样的层级结构是没有问题的,也是有各种理论依据。然而理论和现实往往是严重脱轨的,如上图所示,图中将网络层单独分为了一层,而并没有独立出应用程序相关的功能。 从实践的角度上,我更愿意用如下的架构来构建我的物联网系统。 ![物联网层级结构][2] 其功能可以用下表来表示。 层级|作用|与下一层级的连接方式 ---|----------|------------------ 硬件层|获取、发送传感器数据,执行指令|串口、蓝牙、有线、SPI、WiFi、USB等等 协调层|协调硬件层与服务器的通信,并负责处理部分数据|网络连接及硬件层的连接方式 服务层|以视为服务器层|网络连接 应用程序层|为用户提供交互功能|网络连接 硬件层包含了数据众多的传感器、控制器、以及执行器,通常这部份会由硬件人员与硬件开发人员一起协作和开发。而协调层则是充当硬件与服务层通信的桥梁,这是在系统中需要**特别考虑**的部份,一个物联网系统的设计主要**取决于这个层级**。 ##物联网服务层 而服务层的核心是传统的Web应用程序的结构,只是协议层变成了一些适配器,我们需要支持不同的协议,这导致了我们在这个层需要有一个更好的结构,故而我们建议使用**六边形架构**。而在实际中,用户最后接触到的便是应用程序层,在这一层中需要有很好的用户体验设计及流畅度。 因而在设计[Lan](https://github.com/phodal/lan)物联网平台的时候,参考了之前的[物联网平台](https://github.com/phodal/diaonan)的设计,增加了用户授权以及模块化加载思想。 ![IoT Server Layer][3] 上图的模型可以让我们脱离具体的框架与实现,关注于业务上逻辑。 [1]: /static/media/uploads/iot-3-layer.jpg [2]: /static/media/uploads/iot-layer.jpg [3]: /static/media/uploads/iot-server.jpg > 尽管是在年末,并且也还没把书翻译完,也还没写完书的第一稿。但是,我还是觉得这是一个非常不错的话题——测试代码生成。 当我们在写一些UI测试的时候,我们总需要到浏览器去看一下一些DOM的变化。比如,我们点击了某个下拉菜单,会有另外一个联动的下拉菜单发生了变化。而如果这个事件更复杂的时候,有时我们可能就很难观察出来他们之间的变化。 ##Virtual DOM 尽管这里的例子是以Jasmine作为例子,但是我想对于React也会有同样的方法。 ###一个Jasmine jQuery测试 如下是一个简单的Jamine jQuery的测试示例: ```javascript describe("toHaveCss", function (){ beforeEach(function (){ setFixtures(sandbox()) }) it("should pass if the element has matching css", function (){ $("#sandbox").css("display", "none") $("#sandbox").css("margin-left", "10px") expect($("#sandbox")).toHaveCss({display: "none", "margin-left": "10px"}) }) }); ``` 在beforeEach的时候,我们设定了固定的DOM进去,按照用户的行为做一些相应的操作。接着依据这个DOM中的元素变化 ,来作一些断言。 那么,即使我们已经有一个固定的DOM,想要监听这个DOM的变化就是一件容易的事。在我们断言之前,我们就会有一个新的DOM。我们只需要Diff一下这两个DOM的变化,就可以生成这部分测试代码。 ###virtual-dom与HyperScript 在寻觅中发现了[virtual-dom](https://github.com/Matt-Esch/virtual-dom)这个库,一个可以支持创建元素、diff计算以及patch操作的库,并且它效率好像还不错。 virtual-dom可以说由下面几部分组成的: 1. createElement,用于创建virtual Node。 2. diff,顾名思义,diff算法。 3. h,用于创建虚拟树的DSL——HyperScript。HyperScript是一个JavaScript的HyperText。 4. patch,用于patch修改的内容。 举例来说,我们有下面一个生成Virtual DOM的函数: ```javascript function render(count) { return h('div', { style: { textAlign: 'center', lineHeight: (100 + count) + 'px', border: '1px solid red', width: (100 + count) + 'px', height: (100 + count) + 'px' } }, [String(count)]); } ``` render函数用于生成一个Virtual Node。在这里,我们可以将我们的变量传进去,如1。就会生成如下图所示的节点: ```javascript { "children": [ { "text": "1" } ], "count": 1, "descendantHooks": false, "hasThunks": false, "hasWidgets": false, "namespace": null, "properties": { "style": { "border": "1px solid red", "height": "101px", "lineHeight": "101px", "textAlign": "center", "width": "101px" } }, "tagName": "DIV" } ``` 其中包含中相对应的属性等等。而我们只要调用createElement就可以创建出这个DOM。 如果我们修改了这个节点的一些元素,或者我们render了一个count=2的值时,我们就可以diff两个DOM。如: ```javascript virtualDom.diff(render(2), render(1)) ``` 根据两个值的变化就会生成如下的一个对象: ```javascript { "0": { "patch": { "style": { "height": "101px", "lineHeight": "101px", "width": "101px" } }, "type": 4, "vNode": { ... } }, "1": { "patch": { "text": "1" }, "type": 1, "vNode": { "text": "2" } }, ... } ``` 第一个对象,即0中包含了一些属性的变化。而第二个则是文本的变化——从2变成了1。我们所要做的测试生成便是标记这些变化,并记录之。 ##标记DOM变化 由于virtual-dom依赖于虚拟节点vNode,我们需要将fixtures转换为hyperscript。这里我们就需要一个名为html2hyperscript的插件,来解析html。接着,我们就可以diff转换完后的DOM: ```javascript var leftNode = "", rightNode = ""; var fixtures = '{{description}}
{{/.}} ``{{#.}}``及``{{/.}}``可以用于JSON数组,即循环,也可以是判断是否存在。 最后的结果便是:看到项目上的移动框架,网上寻找了一下,发现原来这些一开始都有。于是,找了个示例开始构建一个移动平台的CMS——墨颀 CMS,方便项目深入理解的同时,也可以自己维护一个CMS系统。
把description去掉,再修改一个CSS,便是我们在首页看到的结果。 下一次我们将打开这些URL。 ###其他 ####如何查看是否支持JSON跨域请求 本次代码下载:[https://github.com/gmszone/moqi.mobi/archive/0.1.1.zip](https://github.com/gmszone/moqi.mobi/archive/0.1.1.zip) 一个简单的工具就是 curl I -s http://example.com 在这里我们查看 curl -I -s http://api.phodal.net/blog/page/1 应该要返回 Access-Control-Allow-Origin: * HTTP/1.1 200 OK Server: mokcy/0.17.0 Date: Thu, 24 Jul 2014 00:38:19 GMT Content-Type: application/json; charset=utf-8 Content-Length: 3943 Connection: keep-alive Vary: Accept-Encoding Access-Control-Allow-Origin: * Access-Control-Allow-Headers: X-Requested-With Cache-Control: max-age=600 在有了上部分的基础之后,我们就可以生成一个博客的内容——BlogPosts Detail。这样就完成了我们这个[移动CMS](http://cms.moqi.mobi)的几乎主要的功能了,有了上节想必对于我们来说要获取一个文章已经不是一件难的事情了。 ##获取每篇博客 于是我们照猫画虎地写了一个``BlogDetail.js`` define([ 'jquery', 'underscore', 'mustache', 'text!/blog_details.html' ],function($, _, Mustache, blogDetailsTemplate){ var BlogPostModel = Backbone.Model.extend({ name: 'Blog Posts', url: function(){ return this.instanceUrl; }, initialize: function(props){ this.instanceUrl = props; } }); var BlogDetailView = Backbone.View.extend ({ el: $("#content"), initialize: function () { }, getBlog: function(slug) { url = "http://api.phodal.net/blog/" + slug; var that = this; collection = new BlogPostModel; collection.initialize(url); collection.fetch({ success: function(collection, response){ that.render(response); } }); }, render: function(response){ this.$el.html(Mustache.to_html(blogDetailsTemplate, response)); } }); return BlogDetailView; }); 又写了一个``blog_details.html``,然后,然后{{description}}
{{/.}} 问题出现了,我们怎样才能进入最后的页面? ##添加博文的路由 在上一篇结束之后,每个博文都有对应的URL,即有对应的slug。而我们的博客的获取就是根据这个URL,获取的,换句话说,这些事情都是由API在做的。这里所要做的便是,获取博客的内容,再render。这其中又有一个问题是ajax执行的数据无法从外部取出,于是就有了上面的getBlog()调用render的方法。 ###Backbone路由参数 我们需要传进一个参数,以便告诉BlogDetail需要获取哪一篇博文。 routes: { 'index': 'homePage', 'blog/*slug': 'blog', '*actions': 'homePage' } ``*slug``便是这里的参数的内容,接着我们需要调用getBlog(slug)对其进行处理。 app_router.on('route:blog', function(blogSlug){ var blogDetailsView = new BlogDetail(); blogDetailsView.getBlog(blogSlug); }); 最后,我们的``router.js``的内容如下所示: define([ 'jquery', 'underscore', 'backbone', 'HomeView', 'BlogDetail' ], function($, _, Backbone, HomeView, BlogDetail) { var AppRouter = Backbone.Router.extend({ routes: { 'index': 'homePage', 'blog/*slug': 'blog', '*actions': 'homePage' } }); var initialize = function() { var app_router = new AppRouter; app_router.on('route:homePage', function() { var homeView = new HomeView(); homeView.render(); }); app_router.on('route:blog', function(blogSlug){ var blogDetailsView = new BlogDetail(); blogDetailsView.getBlog(blogSlug); }); Backbone.history.start(); }; return { initialize: initialize }; }); 接着我们便可以很愉快地打开每一篇博客查看里面的内容了。 当前[墨颀CMS](http://cms.moqi.mobi/)的一些基础功能设计已经接近尾声了,在完成博客的前两部分之后,我们需要对此进行一个简单的重构。为的是提取出其中的获取Blog内容的逻辑,于是经过一番努力之后,终于有了点小成果。 ##墨颀CMS 重构 我们想要的结果,便是可以直接初始化及渲染,即如下的结果: initialize: function(){ this.getBlog(); }, render: function(response){ var about = { about:aboutCMS, aboutcompany:urlConfig["aboutcompany"] }; response.push(about); this.$el.html(Mustache.to_html(blogPostsTemplate, response)); } 为的便是简化其中的逻辑,将与View无关的部分提取出来,最后的结果便是都放在初始化里,显然我们需要一个``render``,只是暂时放在``initialize``应该就够了。下面便是最后的结果: initialize: function(){ var params='#content'; var about = { about:aboutCMS, aboutcompany:configure["aboutcompany"] }; var blogView = new RenderBlog(params, '/1.json', blogPostsTemplate); blogView.renderBlog(about); } 我们只需要将id、url、template传进去,便可以返回结果,再用getBlog部分传进参数。再渲染结果,这样我们就可以提取出两个不同View里面的相同的部分。 ##构建函数 于是,我们就需要构建一个函数RenderBlog,只需要将id,url,template等传进去就可以了。 var RenderBlog = function (params, url, template) { this.params = params; this.url = url; this.template = template; }; 用Javascript的原型继承就可以实现这样的功能,虽然还不是很熟练,但是还是勉强用了上来。 RenderBlog.prototype.renderBlog = function(addInfo) { var template = this.template; var params = this.params; var url = this.url; var collection = new BlogPostModel; collection.initialize(url); collection.fetch({ success: function(collection, response){ if(addInfo !== undefined){ response.push(addInfo); } RenderBlog.prototype.render(params, template, response); } }); }; RenderBlog.prototype.render = function(params, template, response) { $(params).html(Mustache.to_html(template, response)); }; 大致便是将原来的函数中的功能抽取出来,再调用自己的方法。于是就这样可以继续进行下一步了,只是暂时没有一个明确的方向。 在和几个有兴趣做**移动CMS**的小伙伴讨论了一番之后,我们觉得当前比较重要的便是统一一下RESTful API。然而最近持续断网中,又遭遇了一次停电,暂停了对API的思考。在周末无聊的时光了看了《人间失格》,又看了会《一个人流浪,不必去远方》。开始思考所谓的技术以外的事情,或许这将是下一篇讨论的话题。 正在我对这个[移动CMS](http://cms.moqi.mobi/)的功能一筹莫展的时候,帮小伙伴在做一个图片滑动的时候,便想着将这个功能加进去,很顺利地找到了一个库。 ##移动CMS滑动 我们所需要的两个功能很简单 - 当用户向右滑动的时候,菜单应该展开 - 当用户向左滑动的时候,菜单应该关闭 在官网看到了一个简单的示例,然而并不是用于这个菜单,等到我完成之后我才知道:为什么不用于菜单? 找到了这样一个符合功能的库,虽然知道要写这个功能也不难。相比于自己写这个库,还不如用别人维护了一些时候的库来得简单、稳定。 > jQuery Plugin to obtain touch gestures from iPhone, iPod Touch and iPad, should also work with Android mobile phones (not tested yet!) 然而,它并不会其他一些设备上工作。 ###添加jQuery Touchwipe 添加到requirejs的配置中: require.config({ baseUrl: 'lib/', paths: { jquery: 'jquery-2.1.1.min', router: '../router', touchwipe: 'jquery.touchwipe.min' }, shim: { touchwipe: ["jquery"], underscore: { exports: '_' } } }); require(['../app'], function(App){ App.initialize(); }); (注:上面的代码中暂时去掉了一部分无关本文的,为了简单描述。) 接着,添加下面的代码添加到app.js的初始化方法中 $(window).touchwipe({ wipeLeft: function() { $.sidr('close'); }, wipeRight: function() { $.sidr('open'); }, preventDefaultEvents: false }); 就变成了我们需要的代码。。 define([ 'jquery', 'underscore', 'backbone', 'router', 'jquerySidr', 'touchwipe' ], function($, _, Backbone, Router){ var initialize = function(){ $(window).touchwipe({ wipeLeft: function() { $.sidr('close'); }, wipeRight: function() { $.sidr('open'); }, preventDefaultEvents: false }); $(document).ready(function() { $('#sidr').show(); $('#menu').sidr(); $("#sidr li a" ).bind('touchstart click', function() { if(null != Backbone.history.fragment){ _.each($("#sidr li"),function(li){ $(li).removeClass() }); $('a[href$="#/'+Backbone.history.fragment+'"]').parent().addClass("active"); $.sidr('close'); window.scrollTo(0,0); } }); }); Router.initialize(); }; return { initialize: initialize }; }); 便可以实现我们需要的 - 当用户向右滑动的时候,菜单应该展开 - 当用户向左滑动的时候,菜单应该关闭 #Oculus + Node.js + Three.js 打造VR世界 > Oculus Rift 是一款为电子游戏设计的头戴式显示器。这是一款虚拟现实设备。这款设备很可能改变未来人们游戏的方式。 周五Hackday Showcase的时候,突然有了点小灵感,便将闲置在公司的Oculus DK2借回家了——已经都是灰尘了~~。 在尝试一个晚上的开发环境搭建后,我放弃了开发原生应用的想法。一是没有属于自己的电脑(如果Raspberry Pi II不算的话)——没有Windows、没有GNU/Linux,二是公司配的电脑是Mac OS。对于嵌入式开发和游戏开发来说,Mac OS简直是手机中的Windows Phone——坑爹的LLVM、GCC(Mac OS )、OpenGL、OGLPlus、C++11。并且官方对Mac OS和Linux的SDK的支持已经落后了好几个世纪。 说到底,还是Web的开发环境到底还是比较容易搭建的。这个repo的最后效果图如下所示: ![最后效果图](docs/demo.jpg) 效果: 1. WASD控制前进、后退等等。 2. 旋转头部 = 真实的世界。 3. 附加效果: 看久了头晕。 现在,让我们开始构建吧。 ##Node Oculus Services 这里,我们所要做的事情便是将传感器返回来的四元数(Quaternions)与欧拉角(Euler angles)以API的形式返回到前端。 ###安装Node NMD Node.js上有一个Oculus的插件名为node-hmd,hmd即面向头戴式显示器。它就是Oculus SDK的Node接口,虽说年代已经有些久远了,但是似乎是可以用的——官方针对 Mac OS和Linux的SDK也已经很久没有更新了。 在GNU/Linux系统下,你需要安装下面的这些东西的 ``` freeglut3-dev mesa-common-dev libudev-dev libxext-dev libxinerama-dev libxrandr-dev libxxf86vm-dev ``` Mac OS如果安装失败,请使用Clang来,以及GCC的C标准库(PS: 就是 Clang + GCC的混合体,它们之间就是各种复杂的关系。。): ``` export CXXFLAGS=-stdlib=libstdc++ export CC=/usr/bin/clang export CXX=/usr/bin/clang++ ``` (PS: 我使用的是Mac OS El Captian + Xcode 7.0. 2)clang版本如下: ``` Apple LLVM version 7.0.2 (clang-700.1.81) Target: x86_64-apple-darwin15.0.0 Thread model: posix ``` 反正都是会报错的: ``` ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Service/Service_NetClient.o) was built for newer OSX version (10.7) than being linked (10.5) ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Tracking/Tracking_SensorStateReader.o) was built for newer OSX version (10.7) than being linked (10.5) ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Util/Util_ImageWindow.o) was built for newer OSX version (10.7) than being linked (10.5) ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Util/Util_Interface.o) was built for newer OSX version (10.7) than being linked (10.5) ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Util/Util_LatencyTest2Reader.o) was built for newer OSX version (10.7) than being linked (10.5) ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Util/Util_Render_Stereo.o) was built for newer OSX version (10.7) than being linked (10.5) node-hmd@0.2.1 node_modules/node-hmd ``` 不过,有最后一行就够了。 ###Node.js Oculus Hello,World 现在,我们就可以写一个Hello,World了,直接来官方的示例~~。 ```javascript var hmd = require('node-hmd'); var manager = hmd.createManager("oculusrift"); manager.getDeviceInfo(function(err, deviceInfo) { if(!err) { console.log(deviceInfo); } else { console.error("Unable to retrieve device information."); } }); manager.getDeviceOrientation(function(err, deviceOrientation) { if(!err) { console.log(deviceOrientation); } else { console.error("Unable to retrieve device orientation."); } }); ``` 运行之前,记得先连上你的Oculus。会有类似于下面的结果: ```javascript { CameraFrustumFarZInMeters: 2.5, CameraFrustumHFovInRadians: 1.29154372215271, CameraFrustumNearZInMeters: 0.4000000059604645, CameraFrustumVFovInRadians: 0.942477822303772, DefaultEyeFov: [ { RightTan: 1.0923680067062378, LeftTan: 1.0586576461791992, DownTan: 1.3292863368988037, UpTan: 1.3292863368988037 }, { RightTan: 1.0586576461791992, LeftTan: 1.0923680067062378, DownTan: 1.3292863368988037, UpTan: 1.3292863368988037 } ], DisplayDeviceName: '', DisplayId: 880804035, DistortionCaps: 66027, EyeRenderOrder: [ 1, 0 ], ... ``` 接着,我们就可以实时返回这些数据了。 ###Node Oculus WebSocket 在网上看到[http://laht.info/WebGL/DK2Demo.html](http://laht.info/WebGL/DK2Demo.html)这个虚拟现实的电影,并且发现了它有一个WebSocket,然而是Java写的,只能拿来当参考代码。 现在我们就可以写一个这样的Web Services,用的仍然是Express + Node.js + WS。 ```javascript var hmd = require("node-hmd"), express = require("express"), http = require("http").createServer(), WebSocketServer = require('ws').Server, path = require('path'); // Create HMD manager object console.info("Attempting to load node-hmd driver: oculusrift"); var manager = hmd.createManager("oculusrift"); if (typeof(manager) === "undefined") { console.error("Unable to load driver: oculusrift"); process.exit(1); } // Instantiate express server var app = express(); app.set('port', process.env.PORT || 3000); app.use(express.static(path.join(__dirname + '/', 'public'))); app.set('views', path.join(__dirname + '/public/', 'views')); app.set('view engine', 'jade'); app.get('/demo', function (req, res) { 'use strict'; res.render('demo', { title: 'Home' }); }); // Attach socket.io listener to the server var wss = new WebSocketServer({server: http}); var id = 1; wss.on('open', function open() { console.log('connected'); }); // On socket connection set up event emitters to automatically push the HMD orientation data wss.on("connection", function (ws) { function emitOrientation() { id = id + 1; var deviceQuat = manager.getDeviceQuatSync(); var devicePosition = manager.getDevicePositionSync(); var data = JSON.stringify({ id: id, quat: deviceQuat, position: devicePosition }); ws.send(data, function (error) { //it's a bug of websocket, see in https://github.com/websockets/ws/issues/337 }); } var orientation = setInterval(emitOrientation, 1000); ws.on("message", function (data) { clearInterval(orientation); orientation = setInterval(emitOrientation, data); }); ws.on("close", function () { setTimeout(null, 500); clearInterval(orientation); console.log("disconnect"); }); }); // Launch express server http.on('request', app); http.listen(3000, function () { console.log("Express server listening on port 3000"); }); ``` 总之,就是连上的时候不断地发现设备的数据: ```javascript var data = JSON.stringify({ id: id, quat: deviceQuat, position: devicePosition }); ws.send(data, function (error) { //it's a bug of websocket, see in https://github.com/websockets/ws/issues/337 }); ``` 上面有一行注释是我之前一直遇到的一个坑,总之需要callback就是了。 ##Three.js + Oculus Effect + DK2 Control 在最后我们需要如下的画面: ![Three.js Oculus Effect](docs/oculus-vr.jpg) 当然,如果你已经安装了Web VR这一类的东西,你就不需要这样的效果了。如标题所说,你已经知道要用Oculus Effect,它是一个Three.js的插件。 在之前的版本中,Three.js都提供了Oculus的Demo,当然只能用来看。并且交互的接口是HTTP,感觉很难玩~~。 ##Three.js DK2Controls 这时,我们就需要根据上面传过来的``四元数``(Quaternions)与欧拉角(Euler angles)来作相应的处理。 ```javascript { "position": { "x": 0.020077044144272804, "y": -0.0040545957162976265, "z": 0.16216422617435455 }, "quat": { "w": 0.10187230259180069, "x": -0.02359195239841938, "y": -0.99427556991577148, "z": -0.021934293210506439 } } ``` ###欧拉角与四元数 (ps: 如果没copy好,麻烦提出正确的说法,原谅我这个挂过高数的人。我只在高中的时候,看到这些资料。) > 欧拉角是一组用于描述刚体姿态的角度,欧拉提出,刚体在三维欧氏空间中的任意朝向可以由绕三个轴的转动复合生成。通常情况下,三个轴是相互正交的。 对应的三个角度又分别成为roll(横滚角),pitch(俯仰角)和yaw(偏航角),就是上面的postion里面的三个值。。 ``` roll = (rotation about Z); pitch = (rotation about (Roll • Y)); yaw = (rotation about (Pitch • Raw • Z));” ``` -- 引自《Oculus Rift In Action》 转换成代码。。 ``` this.headPos.set(sensorData.position.x * 10 - 0.4, sensorData.position.y * 10 + 1.75, sensorData.position.z * 10 + 10); ``` > 四元数是由爱尔兰数学家威廉·卢云·哈密顿在1843年发现的数学概念。 从明确地角度而言,四元数是复数的不可交换延伸。如把四元数的集合考虑成多维实数空间的话,四元数就代表着一个四维空间,相对于复数为二维空间。 反正就是用于``描述三维空间的旋转变换``。 结合下代码: ```javascript this.headPos.set(sensorData.position.x * 10 - 0.4, sensorData.position.y * 10 + 1.75, sensorData.position.z * 10 + 10); this.headQuat.set(sensorData.quat.x, sensorData.quat.y, sensorData.quat.z, sensorData.quat.w); this.camera.setRotationFromQuaternion(this.headQuat); this.controller.setRotationFromMatrix(this.camera.matrix); ``` 就是,我们需要设置camera和controller的旋转。 这使我有足够的理由相信Oculus就是一个手机 + 一个6轴运动处理组件的升级板——因为,我玩过MPU6050这样的传感器,如图。。。 ![Oculus 6050](docs/mpu6050.jpg) ###Three.js DK2Controls 虽然下面的代码不是我写的,但是还是简单地说一下。 ```javascript /* Copyright 2014 Lars Ivar Hatledal Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ THREE.DK2Controls = function (camera) { this.camera = camera; this.ws; this.sensorData; this.lastId = -1; this.controller = new THREE.Object3D(); this.headPos = new THREE.Vector3(); this.headQuat = new THREE.Quaternion(); var that = this; var ws = new WebSocket("ws://localhost:3000/"); ws.onopen = function () { console.log("### Connected ####"); }; ws.onmessage = function (evt) { var message = evt.data; try { that.sensorData = JSON.parse(message); } catch (err) { console.log(message); } }; ws.onclose = function () { console.log("### Closed ####"); }; this.update = function () { var sensorData = this.sensorData; if (sensorData) { var id = sensorData.id; if (id > this.lastId) { this.headPos.set(sensorData.position.x * 10 - 0.4, sensorData.position.y * 10 + 1.75, sensorData.position.z * 10 + 10); this.headQuat.set(sensorData.quat.x, sensorData.quat.y, sensorData.quat.z, sensorData.quat.w); this.camera.setRotationFromQuaternion(this.headQuat); this.controller.setRotationFromMatrix(this.camera.matrix); } this.lastId = id; } this.camera.position.addVectors(this.controller.position, this.headPos); if (this.camera.position.y < -10) { this.camera.position.y = -10; } if (ws) { if (ws.readyState === 1) { ws.send("get\n"); } } }; }; ``` 打开WebSocket的时候,不断地获取最新的传感器状态,然后update。谁在调用update方法?Three.js 我们需要在我们的初始化代码里初始化我们的control: ```javascript var oculusControl; function init() { ... oculusControl = new THREE.DK2Controls( camera ); ... } ``` 并且不断地调用update方法。 ```javascript function animate() { requestAnimationFrame( animate ); render(); stats.update(); } function render() { oculusControl.update( clock.getDelta() ); THREE.AnimationHandler.update( clock.getDelta() * 100 ); camera.useQuaternion = true; camera.matrixWorldNeedsUpdate = true; effect.render(scene, camera); } ``` 最后,添加相应的KeyHandler就好了~~。 ###Three.js KeyHandler KeyHandler对于习惯了Web开发的人来说就比较简单了: ```javascript this.onKeyDown = function (event) { switch (event.keyCode) { case 87: //W this.wasd.up = true; break; case 83: //S this.wasd.down = true; break; case 68: //D this.wasd.right = true; break; case 65: //A this.wasd.left = true; break; } }; this.onKeyUp = function (event) { switch (event.keyCode) { case 87: //W this.wasd.up = false; break; case 83: //S this.wasd.down = false; break; case 68: //D this.wasd.right = false; break; case 65: //A this.wasd.left = false; break; } }; ``` 然后就是万恶的if语句了: ```javascript if (this.wasd.up) { this.controller.translateZ(-this.translationSpeed * delta); } if (this.wasd.down) { this.controller.translateZ(this.translationSpeed * delta); } if (this.wasd.right) { this.controller.translateX(this.translationSpeed * delta); } if (this.wasd.left) { this.controller.translateX(-this.translationSpeed * delta); } this.camera.position.addVectors(this.controller.position, this.headPos); if (this.camera.position.y < -10) { this.camera.position.y = -10; } ``` 快接上你的HMD试试吧~~ ##结语 如我在[《RePractise前端篇: 前端演进史》](https://github.com/phodal/repractise/blob/gh-pages/chapters/frontend.md)一文中所说的,这似乎就是新的"前端"。 最后效果可参见 [Phodal|cart db][1] ##EXIF## > 可交换图像文件常被简称为EXIF(Exchangeable image file format),是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据。 EXIF信息以0xFFE1作为开头标记,后两个字节表示EXIF信息的长度。所以EXIF信息最大为64 kB,而内部采用TIFF格式。 ###ExifRead### 来自官方的简述 > **Python library to extract EXIF data from tiff and jpeg files.** ###ExifRead安装### pip install exifread ###ExifRead Exif.py### 官方写了一个exif.py的command可直接查看照片信息 EXIF.py images.jpg ##CartoDB## ###简介 ### Create dynamic maps, analyze and build location aware and geospatial applications with your data using the power using the power of PostGIS in the cloud. 简单的来说,就是我们可以创建包含位置信息的内容到上面去。 ![Phodal's Image][2] [1]: http://phodal.cartodb.com/viz/80484668-b165-11e3-be2e-0e73339ffa50/public_map [2]: /static/media/uploads/screen_shot_2014-03-22_at_10.05.39_am.jpg ##打造自己的照片地图## 主要步骤如下 - 需要遍历自己的全部图片文件, - 解析照片信息 - 生成地理信息文件 - 上传到cartodb ###python 遍历文件### 代码如下,来自于《python cookbook》 import os, fnmatch def all_files(root, patterns='*', single_level=False, yield_folders=False): patterns = patterns.split(';') for path, subdirs, files in os.walk(root): if yield_folders: files.extend(subdirs) files.sort() for name in files: for pattern in patterns: if fnmatch.fnmatch(name, pattern): yield os.path.join(path, name) break if single_level: break ###python 解析照片信息### 由于直接从照片中提取的信息是 [34, 12, 51513/1000] 也就是 N 34� 13' 12.718 几度几分几秒的形式,我们需要转换为 34.2143091667 具体的大致就是 def parse_gps(titude): first_number = titude.split(',')[0] second_number = titude.split(',')[1] third_number = titude.split(',')[2] third_number_parent = third_number.split('/')[0] third_number_child = third_number.split('/')[1] third_number_result = float(third_number_parent) / float(third_number_child) return float(first_number) + float(second_number)/60 + third_number_result/3600 也就是我们需要将second/60,还有minutes/3600。 ###python 提取照片信息生成文件### import json import exifread import os, fnmatch from exifread.tags import DEFAULT_STOP_TAG, FIELD_TYPES from exifread import process_file, __version__ def all_files(root, patterns='*', single_level=False, yield_folders=False): patterns = patterns.split(';') for path, subdirs, files in os.walk(root): if yield_folders: files.extend(subdirs) files.sort() for name in files: for pattern in patterns: if fnmatch.fnmatch(name, pattern): yield os.path.join(path, name) break if single_level: break def parse_gps(titude): first_number = titude.split(',')[0] second_number = titude.split(',')[1] third_number = titude.split(',')[2] third_number_parent = third_number.split('/')[0] third_number_child = third_number.split('/')[1] third_number_result = float(third_number_parent) / float(third_number_child) return float(first_number) + float(second_number)/60 + third_number_result/3600 jsonFile = open("gps.geojson", "w") jsonFile.writelines('{\n"type": "FeatureCollection","features": [\n') def write_data(paths): index = 1 for path in all_files('./' + paths, '*.jpg'): f = open(path[2:], 'rb') tags = exifread.process_file(f) # jsonFile.writelines('"type": "Feature","properties": {"cartodb_id":"'+str(index)+'"},"geometry": {"type": "Point","coordinates": [') latitude = tags['GPS GPSLatitude'].printable[1:-1] longitude = tags['GPS GPSLongitude'].printable[1:-1] print latitude print parse_gps(latitude) # print tags['GPS GPSLongitudeRef'] # print tags['GPS GPSLatitudeRef'] jsonFile.writelines('{"type": "Feature","properties": {"cartodb_id":"' + str(index) + '"') jsonFile.writelines(',"OS":"' + str(tags['Image Software']) + '","Model":"' + str(tags['Image Model']) + '","Picture":"'+str(path[7:])+'"') jsonFile.writelines('},"geometry": {"type": "Point","coordinates": [' + str(parse_gps(longitude)) + ',' + str( parse_gps(latitude)) + ']}},\n') index += 1 write_data('imgs') jsonFile.writelines(']}\n') jsonFile.close() 最终代码可见[python cartodb][3] [3]:https://github.com/gmszone/py_cartodb.git ###上传到cartodb### ###从零开始设计技能树: 使用Graphviz建立模型 在开始设计新的技能树——[Sherlock](https://github.com/phodal/sherlock)的同时,结合一下原有的技能树,说说如何去设计,新的技能树还很丑。 ![Sherlock][1] ##Graphviz > Graphviz (英文:Graph Visualization Software的缩写)是一个由AT&T实验室启动的开源工具包,用于绘制DOT语言脚本描述的图形。它也提供了供其它软件使用的库。Graphviz是一个自由软件,其授权为Eclipse Public License。其Mac版本曾经获得2004年的苹果设计奖。 一个简单的示例代码如下: graph example1 { Server1 -- Server2 Server2 -- Server3 Server3 -- Server1 } 执行编译后: dot -Tjpg lz.dot -o lz.jpg 就会生成下面的图片 ![lz][2] 接着我们便可以建立一个简单的模型来构建我们的技能树。 ##简单的技能树 先以JavaScript全栈作一个简单的示例,他们可能存在下面的依赖关系: - "JavaScript" -> "Web前端" - "HTML" -> "Web前端" - "CSS" -> "Web前端" - "Web前端" -> "Web开发" - "JavaScript" -> "Node.js" -> "Web服务端" - "SQL/NoSQL" -> "Web服务端" - "Web Server-Side" -> "Web开发" 即Web前端依赖于JavaScript、HTML、CSS,而Node.js依赖于JavaScript,当然我们也需要数据的支持,大部分的网站都是数据驱动型的开发。而构成完成的开发链的则是前端 + 服务端。 于是我们有了这张图: ![Tree][3] 而我们的代码是这样的: ```c digraph tree { nodesep=0.5; charset="UTF-8"; rankdir=LR; fixedsize=true; node [style="rounded,filled", width=0, height=0, shape=box, fillcolor="#E5E5E5", concentrate=true] "JavaScript" ->"Web前端" "HTML" -> "Web前端" "CSS" -> "Web前端" "Web前端" -> "Web开发" "JavaScript" -> "Node.js" -> "Web服务端" "SQL/NoSQL" -> "Web服务端" "Web服务端" -> "Web开发" } ``` 上面举出的是一个简单的例子,对应的我们可以做一些更有意思的东西,比如将dot放到Web上,详情见下一篇。 [1]: /static/media/uploads/sherlock.png [2]: /static/media/uploads/lz.jpg [3]: /static/media/uploads/tree.jpg #技能树之旅: 计算点数与从这开始 之前写了一篇[技能树之旅: 从模块分离到测试](http://www.phodal.com/blog/rebuild-skilltree-from-module-split-to-test/),现在来说说这其中发生了什么。 ##从这开始 在我们没有点击任何技能的时候,显示的是"从这开始",而当我们点下去时发生了什么? ![Start](http://www.phodal.com//static/media/uploads/start.jpg) 明显变化如下: - 样式变了 - URL变成了[http://skill.phodal.com/#_a2_1_Name](http://skill.phodal.com/#_a2_1_Name) - 点数 + 1 - 点亮了箭头 ###从Knockout开始 > Knockout是一个轻量级的UI类库,通过应用MVVM模式使JavaScript前端UI简单化。 据说有下面的一些特性。 - 声明式绑定 (Declarative Bindings):使用简明易读的语法很容易地将模型(model)数据关联到DOM元素上。 - UI界面自动刷新 (Automatic UI Refresh):当您的模型状态(model state)改变时,您的UI界面将自动更新。 - 依赖跟踪 (Dependency Tracking):为转变和联合数据,在你的模型数据之间隐式建立关系。 - 模板 (Templating):为您的模型数据快速编写复杂的可嵌套的UI。 在我们的html中的从这开始是这样一段HTML