Odoo 中实现(LBS)基于地理位置的服务

Odoo 在 OCA 有一个 geospatial 项目,这个 addon 存在很多年了,但一直没有进入 Odoo 官方的法眼,始终在 OCA 游荡,但是非常活跃,第一时间跟进 Odoo 的版本进化。

随着 Odoo 官方不顾后劲同学的死活每年更新一个大版本,OCA 的 addon 很多已经放弃了更新,消沉于在茫茫代码的海洋。这也是最近几年我们常常看到开源软件的一种新模式,通过商业公司的强力支持,不断抛弃那些附着在开源软件上的‘吸血者’。曾经 OCA 有成百上千大的 addon,现在大多都不灵了。比如 Google 的 Android 非常明显,不把追随者累吐血而亡是绝对不能罢休的。

但是 OCA/geospatial 没有沦落,它挺住了。

LBS 的简单需求

一个 简单的 LBS 服务需求,按照与用户距离排序找一些饭店给客户做选择,这样就需要拿到用户的地理位置,与数据库中的已经存在的饭店信息进行距离计算并且排序。

这个需求看上去如此简单,也如此常见,美团、点评之类的软件都‘轻松’实现了。但是实际上这个需求在一个普通数据库里面是无法实现的。

因为,客户的位置是变的,每个客户的请求都是不同的客户位置,也就是数据库的数据排序不是完全根据数据库的存储信息,而是要根据不同的客户的位置。

另外距离的计算,不是欧式距离那么简单,如果你曾经在‘石器时代’了解 GIS 系统,那么你多少了解 GIS 中通过坐标计算距离的方法不是算个欧式距离那么简单。参见
https://www.jianshu.com/p/9ed3b4dcd32a

实现方法

PostgreSQL 通过 PostGIS 扩展支持 GIS,Odoo 通过 geospatial 支持 PostGIS,所以要先安装 PostGIS,不同平台安装方法不一样。我尝试使用 Homebrew 在 Mac上安装失败,放弃了。直接在 Debian Linux 通过 apt 按装 PostGIS 没有问题。

在 Github 上 clone OCA/geospatial 的代码,geospatial 实际上包含了几个 addons,其中 base_geoengine 就够满足我们的需求了。

Odoo addon 会有一些 hook,可以在 addon 安装之前之后或者完成加载之后运行,base_geoengine 就利用了这个机制,为数据库动态加载了 PostGIS 的扩展。

Model 定义

# 饭店位置
geo_point = fields.GeoPoint("地址位置", srid=4326)

# 当前用户的距离
session_distance = fields.Float("距离", digits=(16,1), compute='_compute_session_distance')

其中的 srid 是所选取的空间参考系统ID。(A Spatial Reference System Identifier(SRID) is a unique value used to unambiguously identify projected, unprojected, and local spatial coordinate system definitions. These coordinate systems form the heart of all GIS applications.)

其中 GeoPoint 就是 geoengine base 提供的。

距离计算

    @api.depends()
    def _compute_session_distance(self):
        _longtitude = self._context.get("longtitude")
        _latitude = self._context.get("latitude")
        if not _longtitude or not _latitude:
            for model in self:
                model.session_distance = 0
            return
        
        cr = self.env.cr
        for model in self:
            if not model.geo_point:
                model.session_distance = 0
                continue
            sql = "select ST_X(geo_point) as lon, ST_Y(geo_point) as lat from %s where id=%s" % (self._table, model.id)
            cr.execute(sql)
            r = cr.fetchone()
            X = r[0]
            Y = r[1]
            sql = "SELECT ST_Distance(ST_GeomFromText('POINT(%s %s)', 4326)::geography, ST_GeomFromText('POINT(%s %s)', 4326)::geography) from %s" % (X, Y, _longtitude, _latitude, self._table)
            cr.execute(sql)
            model.session_distance = round(cr.fetchone()[0], 1)

session distance 这个 field 是通过计算而来(不是存储而来),每次客户请求这个数据的时候都根据当前的context 中客户的经纬度来计算。

按照距离排序

    @api.model
    def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):

        _longtitude = self._context.get("longtitude")
        _latitude = self._context.get("latitude")
        _logger.info("context ... %s" % self._context)

        # if order not default return default search
        if not _longtitude or not _latitude or order:
            return super(self.__class__, self)._search(args, offset, limit, order, count, access_rights_uid)
        
        if _longtitude and _latitude and not order:
            query = self._where_calc(args)
            order_by = "ORDER BY distance asc"
            from_clause, where_clause, where_clause_params = query.get_sql()
            where_str = where_clause and (" WHERE %s" % where_clause) or ''
            limit_str = limit and ' limit %d' % limit or ''
            offset_str = offset and ' offset %d' % offset or ''
            distance_str = "ST_distance(geo_point::geography, ST_GeomFromText('POINT(%s %s)', 4326)::geography) as distance" % (self._context.get("longtitude"), self._context.get("latitude"))
            query_str = 'SELECT %s, id FROM %s %s' % (distance_str, self._table, where_str + order_by + limit_str + offset_str)

            self.env.cr.execute(query_str, where_clause_params)
            res = self.env.cr.fetchall()

            # TDE note: with auto_join, we could have several lines about the same result
            # i.e. a lead with several unread messages; we uniquify the result using
            # a fast way to do it while preserving order (http://www.peterbe.com/plog/uniqifiers-benchmark)
            def _uniquify_list(seq):
                seen = set()
                return [x for x in seq if x not in seen and not seen.add(x)]

            return _uniquify_list([x[1] for x in res])

        return super(self.__class__, self)._search(args, offset, limit, order, count, access_rights_uid)

搜索排序 overload 了 _search 函数。 其中使用了 PostGIS 的函数计算距离,并且按照距离排序。

Model 初始数据

    <record id="location_thing_id_1" model="location.some_model">
    <field name="geo_longtitude">116.601144</field>
    <field name="geo_latitude">39.948574</field>
      ...
    <field name="geo_point">POINT(116.601144 39.948574)</field>
    </record>

如果在定义 geo point 的时候没有指定 srid,那么这个 POINT 值也无法确定意义。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容