Odoo 在 OCA 有一個 geospatial 項目,這個 addon 存在很多年了,但一直沒有進入 Odoo 官方的法眼蕴轨,始終在 OCA 游蕩,但是非澈Э裕活躍橙弱,第一時間跟進 Odoo 的版本進化。
隨著 Odoo 官方不顧后勁同學的死活每年更新一個大版本燥狰,OCA 的 addon 很多已經(jīng)放棄了更新棘脐,消沉于在茫茫代碼的海洋。這也是最近幾年我們常沉拢看到開源軟件的一種新模式蛀缝,通過商業(yè)公司的強力支持,不斷拋棄那些附著在開源軟件上的‘吸血者’净当。曾經(jīng) OCA 有成百上千大的 addon内斯,現(xiàn)在大多都不靈了蕴潦。比如 Google 的 Android 非常明顯像啼,不把追隨者累吐血而亡是絕對不能罷休的。
但是 OCA/geospatial 沒有淪落潭苞,它挺住了忽冻。
LBS 的簡單需求
一個 簡單的 LBS 服務需求,按照與用戶距離排序找一些飯店給客戶做選擇此疹,這樣就需要拿到用戶的地理位置僧诚,與數(shù)據(jù)庫中的已經(jīng)存在的飯店信息進行距離計算并且排序。
這個需求看上去如此簡單蝗碎,也如此常見湖笨,美團、點評之類的軟件都‘輕松’實現(xiàn)了蹦骑。但是實際上這個需求在一個普通數(shù)據(jù)庫里面是無法實現(xiàn)的慈省。
因為,客戶的位置是變的眠菇,每個客戶的請求都是不同的客戶位置边败,也就是數(shù)據(jù)庫的數(shù)據(jù)排序不是完全根據(jù)數(shù)據(jù)庫的存儲信息,而是要根據(jù)不同的客戶的位置捎废。
另外距離的計算笑窜,不是歐式距離那么簡單,如果你曾經(jīng)在‘石器時代’了解 GIS 系統(tǒng)登疗,那么你多少了解 GIS 中通過坐標計算距離的方法不是算個歐式距離那么簡單排截。參見
http://www.reibang.com/p/9ed3b4dcd32a
實現(xiàn)方法
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 就利用了這個機制床三,為數(shù)據(jù)庫動態(tài)加載了 PostGIS 的擴展。
Model 定義
# 飯店位置
geo_point = fields.GeoPoint("地址位置", srid=4326)
# 當前用戶的距離
session_distance = fields.Float("距離", digits=(16,1), compute='_compute_session_distance')
其中的 srid 是所選取的空間參考系統(tǒng)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 是通過計算而來(不是存儲而來),每次客戶請求這個數(shù)據(jù)的時候都根據(jù)當前的context 中客戶的經(jīng)緯度來計算差购。
按照距離排序
@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 函數(shù)四瘫。 其中使用了 PostGIS 的函數(shù)計算距離,并且按照距離排序欲逃。
Model 初始數(shù)據(jù)
<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 值也無法確定意義。