Full Code of RedisLabs/geo.lua for AI

master a5128f420d49 cached
6 files
38.0 KB
11.3k tokens
1 requests
Download .txt
Repository: RedisLabs/geo.lua
Branch: master
Commit: a5128f420d49
Files: 6
Total size: 38.0 KB

Directory structure:
gitextract_b7ntzx8_/

├── CHANGELOG.md
├── LICENSE
├── README.md
├── dataset1.json
├── dataset1.lua
└── geo.lua

================================================
FILE CONTENTS
================================================

================================================
FILE: CHANGELOG.md
================================================
[0.1.6] - 2016-
===
Adds spatial geometries, increases similarity to LOLCODE.

**IMPORTANT:** This release breaks the API.

 * New commands: `GEOMETRYADD`, `GEOMETRYGET`
 * `GEOPOLYGON` is now `GEOMETRYFILTER`. It:
  * Accepts a geometry as a filter (right now, only polygons)
  * Supports the `STORE` subcommand
 * Polygon search optimization

[0.1.5] - 2016-02-20 
===
Initial release.

[0.1.6]: https://github.com/RedisLabs/geo.lua/releases/tag/0.1.6
[0.1.5]: https://github.com/RedisLabs/geo.lua/releases/tag/0.1.5

================================================
FILE: LICENSE
================================================
geo.lua is provided under the 3-Clause BSD License: http://opensource.org/licenses/BSD-3-Clause

Copyright (c) 2016, Itamar Haber.
Copyright (c) 2016, Redis Labs, Inc.
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

================================================
FILE: README.md
================================================
geo.lua - helper library for Redis geospatial indices :earth_africa:
===

This is a Lua library containing miscellaneous geospatial helper routines for use with [Redis]. It requires the [Redis GEO API], available from v3.2.

In broad strokes, the library provides:
* [Polygon searches](#GEOMETRYFILTER) on geoset members
* Navigational information such as [bearing](#GEOBEARING) and [path length](#GEOPATHLEN)
* GeoJSON [decoding](#GEOJSONADD) and [encoding](#GEOJSONENCODE)
* A playground for testing experimental geospatial APIs

The library is strictly :straight_ruler: metric, sorry.
![Metric system adoption worldwide](https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/Metric_system_adoption_map.svg/2000px-Metric_system_adoption_map.svg.png)

Using geo.lua
---
The library is an ordinary Redis Lua script - use [`EVAL`] or [`EVALSHA`] to call it. The following example demonstrates usage from the prompt:

````Bash
$ redis-cli SCRIPT LOAD "$(cat geo.lua)"
"fd07..."
$ redis-cli EVALSHA fd07... 0 help
 1) "geo.lua (0.1.5): A helper library for Redis geospatial indices"
 ...
````

Library API
---

### GEO API completeness

<a name="GEOBEARING"></a>
#### GEOBEARING KEYS[1] geoset ARGV[2] member1 3] member2
Return the initial and final bearing between two members.
> Time complexity: O(1).

For information about the calculation refer to  http://mathforum.org/library/drmath/view/55417.html.

**Return:** Array reply, specifically the initial and final bearings.

<a name="GEOPATHLEN"></a>
#### GEOPATHLEN KEYS[1] geoset ARGV[2] member [...]
The length of a path between members.
> Time complexity: O(N) where N is the number of members.

**Note:** This is basically a variadic form for [`GEODIST`].

**Return:** String reply, specifically the length in meters.

<a name="GEODEL"></a>
#### GEODEL KEYS[1] geoset ARGV[2] member [...]
Delete members from a geoset.
> Time complexity: O(M*log(N)) with N being the number of elements in the geoset and M the number of elements to be removed.

This command is an alias for [`ZREM`] to disprove the [GEO incompleteness theorem](https://twitter.com/monadic/status/690889597866393600). Technically it should be called `GEOREM` however.

**Return:** Integer reply, specifically the number of members actually deleted.

### 2D geometries and polygon search
Redis' geosets allow [storing](http://redis.io/commands/geoadd) (and [querying](http://redis.io/commands/georadius)) of `Point` (2D, x/y, longitude & latitude) geometries. Other geometries can be stored serialized (using [msgpack](http://msgpack.org/index.html)) in a Redis Hash data structure - the geomash. The geoset and the geomash are used for storing geometries in the following manner:

| Geometry             | Storage | `GEOMETRYADD` | `GEOMETRYGET` | `GEOMETRYFILTER` | `GEOJSONADD`
| :---                 | :---    | :---:         | :---:         | :---:            | :---:
| `Point`              | geoset  | N/A           | N/A           | No               | Yes
| `Polygon`            | geomash | Yes           | Yes           | Yes              | Yes
| `MultiPolygon`       | geomash | TODO          | TODO          | TODO             | RODO
| `MultiPoint`         | TODO    | TODO          | TODO          | No               | TODO
| `LineString`         | TODO    | TODO          | TODO          | TODO             | TODO
| `MultiLineString`    | TODO    | TODO          | TODO          | TODO             | TODO
| `GeometryCollection` | TODO    | TODO          | TODO          | TODO             | TODO

<a name="GEOMETRYADD"></a>
#### GEOMETRYADD KEYS[1] geomash ARGV[2] geometry-type 3] id 4..] <geometry data>
Upsert a single geometry to a geomash.
> Time complexity:

`ARGV[4]` and above describe the geometry. Depending on the geometry's type:
  * `POLYGON` - each pair of ARGVs (e.g., 4 and 5, 6 and 7, ...) represent the coordinates of a vertex (longitude and latitude). A minimum of 4 vertices are required to define a polygon, with the first and last vertices being equal (a LineRing).

**Note:** there is no `GEOMETYREM`, use [`HDEL`](http://redis.io/commands/hdel) instead.

**Return:** Integer reply, specifically 0 if updated and 1 if added.

<a name="GEOMETRYGET"></a>
#### GEOMETRYGET KEYS[1] geomash ARGV[a] [WITHPERIMETER|WITHBOX|WITHCIRCLE] [...] ARGV[2] id  [...]
Returns geometries' coordinates.
> Time complexity:

The reply can be enriched with meta data about the geometry. The following sub-commands are supported, depending on the type of geometry.
  * `WITHPERIMETER`: For `Polygon`, The total length of edges
  * `WITHBOX`: The bounding box of the geometry
  * `WITHCIRCLE`: The bounding circle of the geometry

**Return:** Array reply, specifically:`
  * The geometry's type
  * The geometry's coordinates
  * When called with `WITHPERIMETER`, the total length of edges
  * When called with `WITHBOX`, the minimum and maximum coordinates of the bounding box, as well as it's bounding circle's radius
  * When called with `WITHCIRCLE`, the center coordinates and radius of bounding circle

<a name="GEOMETRYFILTER"></a>
#### GEOMETRYFILTER KEYS[1] geoset 2] geomash a] [target] ARGV[a] [STORE|WITHCOORD] [...] ARGV[2] id
Search for geoset members inside a geometry.
> Time complexity:

This command performs a [`GEORADIUS`] search that contains the geometry's bounding box. The results are then filtered using a Point-In-Polygon (PIP) algorithm ([source](https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html#The C Code)).

The following sub-commands are supported:
 * `STORE`: Stores the search results in the target geoset
 * `WITHCOORD`: Returns the members' coordinates as well

**Return:** Array reply, specifically the members and their coordinates (if called with `WITHCOORD`). When the `STORE` directive is used, the reply is an Integer that indicates the number of members that were upserted to the target geoset.

### GeoJSON
A minimal implementation of the [spec's v1.0](http://geojson.org/geojson-spec.html) for decoding and encoding geoset and geomash members from/to `FeatureCollection`s and `Feature`s.

<a name="GEOJSONADD"></a>
#### GEOJSONADD KEYS[1] geoset 2] geomash ARGV[1] GeoJSON
> Time complexity: O(log(N)) for each feature in the GeoJSON object, where N is the number of members in the geoset.

Upsert points to the geoset, other geometries to the geomash. A valid input GeoJSON object must be `FeatureCollection`. Each `Feature`'s type must be `Point` or a `Polygon`, and the feature's properties must include a member named `id`.

**Return:** Integer reply, specifically the number of features upserted.

<a name="GEOJSONENCODE"></a>
#### GEOJSONENCODE KEYS[1] geoset ARGV[2] <GEO command> 3] [arg] [...]
> Time complexity: depends on the GEO command and its arguments.

Encodes the reply of GEO commands as a GeoJSON object. Valid GEO commands are:
* [`GEOHASH`]
* [`GEOPOS`]
* [`GEORADIUS`] and [`GEORADIUSBYMEMBER`]
* [`GEOMETRYFILTER`]

**Return:** String, specifically the reply of the GEO command encoded as a GeoJSON object.

### Location updates
Implements a real-time location tracking mechanism. Inspired by [Matt Stancliff @mattsta](https://matt.sh/redis-geo).

<a name="GEOTRACK"></a>
#### GEOTRACK KEYS[1] geoset ARGV[2] longitude 3] latitude 4] member [...]
> Time complexity: O(log(N)+M+P) for each item added, where N is the number of elements in the geoset, M is the number of clients subscribed to the receiving channel and P is the total number of subscribed patterns (by any client).

[`GEOADD`]s a member and [`PUBLISH`]s on channel `__geo:<geoset>:<member>` a message with the format of `<longitude>:<latitude>`.

Clients can track updates made to a specific member by subscribing to that member's channel (i.e. [`SUBSCRIBE`]`__geo:<geoset>:<member>`) or to all members updates (i.e. [`PSUBSCRIBE`]`__geo:<geoset>:*`).

**Return:** Integer reply, specifically the number of members upserted.

### xyzsets
Redis' geospatial indices only encode the longitude and latitude of members with no regard to their altitude. An xyzset uses two sorted sets, one as geoset and the other for storing altitudes.

<a name="GEOZADD"></a>
#### GEOZADD KEYS[1] geoset 2] azset ARGV[2] logitude 3] latitude 4] altitude 5] member [...]
> Time complexity: O(log(N)) for each item added, where N is the number of elements in the geoset.

Upsert members. Altitude is given as meters above (or below) sea level.

**Return:** Integer reply, specifically the number of members upserted.

<a name="GEOZREM"></a>
#### GEOZREM KEYS[1] geoset 2] azset ARGV[2] member [...]
> Time complexity: O(M*log(N)) with N being the number of elements in the geoset and M the number of elements to be removed.

Remove members.

**Return:** Integer reply, specifically the number of members actually deleted.

<a name="GEOZPOS"></a>  
#### GEOZPOS KEYS[1] geoset 2] azset ARGV[2] member [...]
> Time complexity: O(log(N)) for each member requested, where N is the number of elements in the geoset.

The position of members in 3D.

**Return:** Array reply, specifically the members and their positions.

#### TODO: GEOZDIST
Returns the distance between members.

#### TODO: GEOZCYLINDER
Perform cylinder-bound search.

#### TODO: GEOZCYLINDERBYMEMBER
Perform cylinder-bound search by member (a 20 characters command!).

#### TODO: GEOZSPHERE
Useful for directing air traffic and impact research (bombs, comets).

#### TODO: GEOZCONE
Good for comparing :alien: sightings vis a vis :cow: abduction data.

### Motility
Storing each member's vector in an additional hash data structure, where the field is the member's name and the value is the serialized vector (bearing & velocity).

#### TODO: GEOMADD KEYS[1] geoset 2] vector hash ARGV[2] longitude 3] latitude 4] bearing 5] velocity 6] member [...]

Upsert members. Bearing given in degrees, velocity in meters/second.

#### TODO: GEOMREM KEYS[1] geoset 2] vector hash ARGV[2] member [...]
Remove members.

#### TODO: GEOMPOSWHEN KEYS[1] geoset 2] vector hash ARGV[2] seconds 3] member [...]
Project members position in future or past.

#### TODO: GEOMMEETWHENWHERE KEYS[1] geoset 2] vector hash ARGV[2] member1 3] member2
Help solving basic math exercises.

License
---
3-Clause BSD.

Contributing
---

You're encouraged to contribute to the open source geo.lua project. There are two ways you can do so.

### Issues

If you encounter an issue while using the geo.lua library, please report it at the project's issues tracker. Feature suggestions are also welcome.

### Pull request

Code contributions to the geo.lua project can be made using pull requests. To submit a pull request:

1. Fork this project.
2. Make and commit your changes.
3. Submit your changes as a pull request.

[Redis]: http://redis.io
[Redis GEO API]: (http://redis.io/commands#geo)
[`GEOADD`]: (http://redis.io/commands/geoadd)
[`GEODIST`]: (http://redis.io/commands/geodist)
[`GEOHASH`]: (http://redis.io/commands/geohash)
[`GEOPOS`]: (http://redis.io/commands/geopos)
[`GEORADIUS`]: (http://redis.io/commands/georadius)
[`GEORADIUSBYMEMBER`]: (http://redis.io/commands/georadiusbymember)
[`EVAL`]: (http://redis.io/commands/eval)
[`EVALSHA`]: (http://redis.io/commands/evalsha)
[`PUBLISH`]: (http://redis.io/commands/publish)
[`SUBSCRIBE`]: (http://redis.io/commands/subscribe)
[`PSUBSCRIBE`]: (http://redis.io/commands/psubscribe)
[`ZREM`]: (http://redis.io/commands/zrem)


================================================
FILE: dataset1.json
================================================
{ \"type\": \"FeatureCollection\", \"features\": [ { \"type\": \"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [15.0074779, 37.4908267]}, \"properties\": {\"id\": \"RL@Catania\"} }, { \"type\": \"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [34.84076, 32.10942]}, \"properties\": {\"id\": \"RL@TLV\"} }, { \"type\": \"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [-122.0678325, 37.3775256]}, \"properties\": {\"id\": \"RL@MV\"} }, { \"type\": \"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [34.8380433, 32.1098095]}, \"properties\": {\"id\": \"Hudson\"} } ] }

================================================
FILE: dataset1.lua
================================================
local data = {
  '15.0074779', '37.4908267', 'RL@Catania',
  '34.84076', '32.10942', 'RL@TLV',
  '-122.0678325', '37.3775256', 'RL@MV',
  '34.8380433', '32.1098095', 'Hudson'
}

local alt = {
  '97', 'RL@Catania',
  '18', 'RL@TLV',
  '38', 'RL@MV',
  '18', 'Hudson'
}  

redis.call('DEL', unpack(KEYS))
redis.call('GEOADD', KEYS[1], unpack(data))
redis.call('ZADD', KEYS[2], unpack(data))

================================================
FILE: geo.lua
================================================
local _NAME = 'geo.lua'
local _VERSION = '0.1.6'
local _DESCRIPTION = 'A helper library for Redis geospatial indices'
local _COPYRIGHT = '2016 Itamar Haber, Redis Labs'
local _USAGE = [[

Call with ARGV[1] being one of the following commands:
  
  - GEO API completeness -
  GEODEL        - delete a member
  GEOBEARING    - initial and final bearing between members
  GEOPATHLEN    - length of a path between members
  
  - 2D geometries and polygon search -
  GEOMETRYADD   - upsert a geometry to a polyhash
  GEOMETRYGET   - returns coordinates and other stuff for polygons
  GEOMETRYFILER - search geoset inside a geometry
    
  - GeoJSON -
  GEOJSONADD    - upsert members to geoset from GeoJSON object
  GEOJSONENCODE - return GeoJSON object for these GEO commands:
                  GEOHASH, GEOPOS, GEORADIUS[BYMEMBER] and
                  GEOMETRYFILTER

  - xyzsets (longitude, latitude & altitude) -
  GEOZADD       - upsert member with altitude
  GEOZREM       - remove member
  GEOZPOS       - the 3d position of members
  
  - location updates (credit @mattsta) -
  GEOTRACK      - positional updates notifications

  help          - this text, for more information see the README file

]]

local Geo = {}      -- GEO library

-- private

-- Geospatial 2D Geometry
local Geometry = {}
Geometry.__index = Geometry

setmetatable(Geometry, {
              __call = function(cls, ...)
                return cls.new(...)
                end })

Geometry._TYPES = { 'Point',           -- TODO
                    'MultiPoint',      -- TODO
                    'LineString',      -- TODO 
                    'MultiLineString', -- TODO
                    'Polygon',
                    'MultiPolygon' }   -- TODO
Geometry._TENUM = {}
for i = 1, #Geometry._TYPES do
  Geometry._TENUM[Geometry._TYPES[i]:upper()] = i
  Geometry['_T' .. Geometry._TYPES[i]:upper()] = i
end

--- Calculates distance between two coordinates.
-- Just like calling GEODIST, but slower and of slightly different
-- accuracy.
-- @param lon1 The longitude of the 1st coordinate
-- @param lat1 The latitude of the 1st coordinate
-- @param lon2 The longitude of the 2nd coordinate
-- @param lat2 The latitude of the 2nd coordinate
-- @return distance The distance in meters
Geometry._distance = function (lon1, lat1, lon2, lat2)
  local R = 6372797.560856 -- Earth's, in meters
  local lon1r, lat1r, lon2r, lat2r = 
    math.rad(lon1), math.rad(lat1), math.rad(lon2), math.rad(lat2)
  local u = math.sin((lat2r - lat1r) / 2)
  local v = math.sin((lon2r - lon1r) / 2)
  return 2.0 * R * math.asin(math.sqrt(u * u + math.cos(lat1r) * math.cos(lat2r) * v * v))
end

--- Geometry constructor.
-- Creates a geometry object.
-- @param geomtype The geometry's type, optional
-- @param ... The geometry's geometries
-- @return self New Geometry object
Geometry.new = function(geomtype, ...)
  local self = setmetatable({}, Geometry)
  self.geomtype = geomtype
  self.coordn, self.coordx, self.coordy = {}, {}, {}
  self.meta = {}
  
  if #arg > 0 then
    if self.geomtype == Geometry._TPOLYGON then 
      -- A polygon's coordinates are given by one or more LineRings
      -- A LineRing is a LineString where the first and last vertices are identical
      -- The first and mandatory ring is the polygon's outer ring
      -- Any subsequent rings are considered holes, islands, and repeating...
            
      -- Polygon vertices are stored as two coordinate lists
      -- When there's more than one ring, O means the (0,0) and the two lists:
      --   begins with O
      --   rings are delimeted with O
      --   end with O
      self.coordx[#self.coordx+1], self.coordy[#self.coordy+1] = 0, 0
      for _, ring in ipairs(arg[1]) do
        local n = #ring / 2
        self.coordn[#self.coordn+1] = n
        for i = 1, n do  
          self.coordx[#self.coordx+1], self.coordy[#self.coordy+1] = ring[2*i-1], ring[2*i]
        end
        self.coordx[#self.coordx+1], self.coordy[#self.coordy+1] = 0, 0
      end
      
      -- the outer ring
      local op = 0                    -- outer perimeter
      local ov = self.coordn[1]       -- number of vertices
      local bbx1 = self.coordx[2]     -- bounding box
      local bby1 = self.coordy[2]
      local bbx2 = self.coordx[2]  
      local bby2 = self.coordy[2]
      local bbr = 0
      local bcx, bcy, bcr = 0, 0, 0   -- bounding circle
      -- traverse outer vertices
      for i = 2, 1 + ov do
        local ix, iy = self.coordx[i], self.coordy[i]
        local jx, jy = self.coordx[i+1], self.coordy[i+1]
        -- grow the outer perimeter
        op = op + Geometry._distance(ix, iy, jx, jy)
        -- add up for the averages
        bcx = bcx + ix
        bcy = bcy + iy
        -- adjust bounding box if needed
        if bbx1 > ix then
          bbx1 = ix
        elseif bbx2 < ix then
          bbx2 = ix
        end
        if bby1 > iy then
          bby1 = iy
        elseif bby2 < iy then
          bby2 = iy
        end            
      end
      bcx = bcx / (ov - 1)
      bcy = bcy / (ov - 1)
      bbr = Geometry._distance(bbx1, bby1, bbx2, bby2) / 2
  
      -- farthest point from the centroid is the radius
      for i = 2, 1 + ov do
        local d = Geometry._distance(bcx, bcy, self.coordx[i], self.coordy[i])
        if bcr < d then
          bcr = d
        end
      end
  
      -- lets not be too accurate
      bcr = math.ceil(bcr)
      bbr = math.ceil(bbr)
 
      self.meta = { op, ov, bcx, bcy, bcr, bbx1, bby1, bbx2, bby2, bbr }
    else
      error('Unknown argument(s) to for type')
    end
  end
  
  return self
end

--- Checks if a point is inside a polygon.
-- https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html
-- @param pt Point
-- @param po Polygon
-- @return boolean True if PIP
function Geometry:PNPOLY(x, y)
  local c = false
  -- don't think outside the box
  if self.meta[6]<=x and self.meta[8]>=x and self.meta[7]<=y and self.meta[9]>=y then
    
    local nvert = #self.coordy
    local vertx = self.coordx
    local verty = self.coordy
    local testx = x
    local testy = y
    local j = nvert
    
    for i = 1, nvert do
      if  ((verty[i]>testy) ~= (verty[j]>testy)) and
          testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i] then
        c = not c
      end
      j = i
    end
  end

  return c
end

--- Returns the bounding circle for a geometry.
-- @return Table three elements: x, y and radius
function Geometry:getBoundingCircle()
  -- TODO: error on `Point`
  return { self.meta[3], self.meta[4], self.meta[5] }
end

--- Returns the bounding box for a geometry.
-- return Table five elements: min x and y, max x and y, radius
function Geometry:getBoundingBox()
  -- TODO: error on `Point`
  return { self.meta[6], self.meta[7], self.meta[8], self.meta[9], self.meta[10] }
end

--- Returns the geometry's type name.
-- @return name The geometry's name
function Geometry:typeAsString()
  return Geometry._TYPES[self.geomtype]
end

--- Returns a table of geometry's coordinates.
-- Every geometry is a table, and in it every element is a vertex table.
-- @return coord The coordinates as a table for a RESP reply
function Geometry:coordAsRESP()
  local reply = {}

  local verti = 2
  for geomi, geomn in ipairs(self.coordn) do
    local r = {}
    local vertn = verti + geomn - 1
    for i = verti, vertn do
      r[#r+1] = { tostring(self.coordx[i]), tostring(self.coordy[i]) }
    end
    verti = vertn + 2
    reply[#reply+1] = r
  end
  
  return reply
end

--- Returns a table of the geometry's meta information.
-- @return meta The meta information
function Geometry:metaAsRESP(subcmds)
  local reply = {}
  
  if not subcmds or not subcmds['ANY'] then
    return
  end
  if self.geomtype == Geometry._TPOLYGON then  
    if subcmds['WITHPERIMETER'] then
      reply[#reply+1] = tostring(self.meta[1])
    end
    if subcmds['WITHBOX'] then
      local bbox = self:getBoundingBox()
      reply[#reply+1] = { { tostring(bbox[1]), tostring(bbox[2]) },
                          { tostring(bbox[3]), tostring(bbox[4]) },
                          bbox[5]}
    end
    if subcmds['WITHCIRCLE'] then
      local bcircle = self:getBoundingCircle()
      reply[#reply+1] = { { tostring(bcircle[1]), tostring(bcircle[2]) },
                          bcircle[3] }
    end
  end           
  
  if #reply > 0 then
    return reply
  else
    return
  end
end

--- Returns the msgpack-serialized geometry object.
-- @return string The serialized object
function Geometry:dump()
  local payload = {}
  payload[#payload+1] = self.geomtype
  payload[#payload+1] = self.coordn
  payload[#payload+1] = self.coordx
  payload[#payload+1] = self.coordy
  payload[#payload+1] = self.meta
  return cmsgpack.pack(payload)
end

--- Loads the geometry from its serialized form.
-- @param msgpack The serialized geometry
-- @return null
function Geometry:load(msgpack)
  local payload = cmsgpack.unpack(msgpack)
  self.geomtype = table.remove(payload, 1)
  self.coordn = table.remove(payload, 1)
  self.coordx = table.remove(payload, 1)
  self.coordy = table.remove(payload, 1)
  self.meta = table.remove(payload, 1)
end

-- Data structure type
Geo._TYPE_GEO = 1     -- regular geoset
Geo._TYPE_XYZ = 2     -- xyzset
Geo._TYPE_GEOM = 3    -- geomash

--- Keys validation.
-- Extract and validate types of keys for command
-- @param geotype The type of command
-- @return geoset|polyhash Key name
-- @return [azset]  Key name
Geo._getcommandkeys = function (geotype)
  
  local function asserttype(k, t)
    local r = redis.call('TYPE', k)
    assert(r['ok'] == t or r['ok'] == 'none', 'WRONGTYPE Operation against a key holding the wrong kind of value')
  end
  
  if geotype == Geo._TYPE_GEO then
    local geokey = assert(table.remove(KEYS, 1), 'No geoset key name provided')
    asserttype(geokey, 'zset')
    return geokey
  elseif geotype == Geo._TYPE_XYZ then
    local geokey = assert(table.remove(KEYS, 1), 'No geoset key name provided')
    asserttype(geokey, 'zset')
    local zsetkey = assert(table.remove(KEYS, 1), 'No altitude sorted set key name provided')
    asserttype(zsetkey, 'zset')
    return geokey, zsetkey
  elseif geotype == Geo._TYPE_GEOM then
    local geomkey = assert(table.remove(KEYS, 1), 'No geomash key name provided')
    asserttype(geomkey, 'hash')
    return geomkey
  end
end

-- public API

--- Delete a member.
-- @return deleted Number of deleted members
Geo.GEODEL = function()
  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)
  return redis.call('ZREM', geokey, unpack(ARGV))
end

--- Returns bearing between two members.
-- @return bearings Table containing the initial and final bearings
Geo.GEOBEARING = function()
  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)
  
  assert(#ARGV == 2, 'Two members must be provided')
  local member1, member2 = table.remove(ARGV, 1), table.remove(ARGV, 1)
  
  local coords = redis.call('GEOPOS', geokey, member1, member2)
  assert(coords[1] and coords[2], 'Two existing members must be provided')
  
  local function initialbearing(lon1, lat1, lon2, lat2)
    local p1, p2 = math.rad(lat1), math.rad(lat2)
    local dl = math.rad(lon2 - lon1)
    local y = math.sin(dl)*math.cos(p2)
    local x = math.cos(p1)*math.sin(p2) - math.sin(p1)*math.cos(p2)*math.cos(dl)
    local b = (math.deg(math.atan2(y, x)) + 360) % 360
    return b
  end
  
  local ib = initialbearing(coords[1][1], coords[1][2], coords[2][1], coords[2][2])
  local fb = (initialbearing(coords[2][1], coords[2][2], coords[1][1], coords[1][2]) + 180) % 360

  return { tostring(ib), tostring(fb) }
end

--- Calculates the length of a path given by members.
-- @return length Path's length in meters
Geo.GEOPATHLEN = function()
  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)
  assert(#ARGV > 1, 'Need at least two members to make a path')
  
  local total = 0
  local prev = table.remove(ARGV, 1)
  while #ARGV > 0 do
    local curr = table.remove(ARGV, 1)
    local dist = redis.call('GEODIST', geokey, prev, curr, 'm')
    if dist then
      total = total + dist
      prev = curr
    else
      return
    end
  end
  
  return total
end

--- Upserts a single geometry into a geomash.
-- TODO: support more geometries besides an unholey `Polygon`
-- TODO: support upsert of multiple geometries
-- @return upserted Number 0 if updated, 1 if added
Geo.GEOMETRYADD = function()
  local geomkey = Geo._getcommandkeys(Geo._TYPE_GEOM)
  local geomtype = assert(table.remove(ARGV, 1), 'Expecting a geometry type')
  geomtype = geomtype:upper()
  geomtype = assert(Geometry._TENUM[geomtype], 'Invalid geometry type')
  local id = assert(table.remove(ARGV, 1), 'Expecting a geometry id')
  
  assert(geomtype == Geometry._TPOLYGON, 'Sorry, atm only `POLYGON` geometry type is supported')
  assert(#ARGV > 7, 'Expecting at least 4 coordinates')
  assert(#ARGV % 2 == 0, 'Expecting an even number of arguments as coordinates')  
  assert(ARGV[1] == ARGV[#ARGV-1] and ARGV[2] == ARGV[#ARGV], 'The first and last vertices must be the identical')
  
  local coord = {}
  for i, v in ipairs(ARGV) do
    coord[#coord+1] = assert(tonumber(v), 'Expecting numbers as coordinates')
  end
  
  local geom = Geometry.new(Geometry._TPOLYGON, { coord })
  return redis.call('HSET', geomkey, id, geom:dump())
end

--- Returns geometries from a geomash.
-- @return vertices Table of vertices
Geo.GEOMETRYGET = function()
  local geomkey = Geo._getcommandkeys(Geo._TYPE_GEOM)
  local subcmds = { WITHPERIMETER = false,
                    WITHBOX = false,
                    WITHCIRCLE = false }
  assert(#ARGV > 0, 'Expecting at least one argument')
  for i = 1, math.min(3, #ARGV) do
    local s = ARGV[1]:upper()
    -- there are edge cases this will not cover but good enough
    if subcmds[s] ~= nil and not subcmds[s] then
      subcmds['ANY'] = true
      subcmds[s] = true
      table.remove(ARGV, 1)
    end
  end  
  
  assert(#ARGV > 0, 'Expecting at least one geometry id')
  
  local r = redis.call('HMGET', geomkey, unpack(ARGV))
  -- cast each geometry in the reply to RESP 
  for ri, rv in ipairs(r) do
    if rv then -- i.e. not (nil), meaning v is a geometry
      local geom = Geometry.new()
      geom:load(rv)
      local rep = {} 
      rep[#rep+1] = geom:typeAsString()
      rep[#rep+1] = geom:coordAsRESP()
      local meta = geom:metaAsRESP(subcmds)
      if meta then rep[#rep+1]= meta end
      r[ri] = rep
    end
  end
  
  return r
end

--- Performs a search for members inside a geometry.
-- @return members Table with the members
Geo.GEOMETRYFILTER = function()
  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)
  local geomkey = Geo._getcommandkeys(Geo._TYPE_GEOM)
  
  assert(#ARGV > 0, 'Expecting at least one argument')
  local subcmds = { WITHCOORD = false,
                    STORE = false }
  for i = 1, math.min(2, #ARGV) do
    local s = ARGV[1]:upper()
    -- there are edge cases this will not cover but good enough
    if subcmds[s] ~= nil and not subcmds[s] then
      subcmds[s] = true
      table.remove(ARGV, 1)
    end
  end

  local targetkey
  if subcmds['STORE'] then
    -- TODO: this currently triggers an ambiguous error 
    targetkey = Geo._getcommandkeys(Geo._TYPE_GEO)
  end
  
  assert(#ARGV == 1, 'Expecting a single geometry id')
  local r = assert(redis.call('HGET', geomkey, ARGV[1]), 'Geometry id not found: ' .. ARGV[1])
  local geom = Geometry:new()
  geom:load(r)
  assert(geom.geomtype == Geometry._TPOLYGON, 'Unsupported (TODO) filter geometry: ' .. geom:typeAsString())
  
  local bbox = geom:getBoundingBox()
  local members = redis.call('GEORADIUS', geokey, (bbox[1] + bbox[3]) / 2, (bbox[2] + bbox[4]) / 2, bbox[5], 'm', 'WITHCOORD')
  local reply, geoadd = {}
  for i, v in ipairs(members) do
    if geom:PNPOLY(tonumber(v[2][1]), tonumber(v[2][2])) then
      if subcmds['WITHCOORD'] then
        reply[#reply+1] = v
      else
        reply[#reply+1] = v[1]
      end
      if subcmds['STORE'] then
        geoadd[#geoadd+1] = v[2][1]
        geoadd[#geoadd+1] = v[2][2]
        geoadd[#geoadd+1] = v[1]
      end  
    end
  end
  
  if subcmds['STORE'] then
    return redis.call('GEOADD', targetkey, unpack(geoadd))
  else
    return reply
  end
end

--- Adds members in a GeoJSON object to a geoset.
-- @return added The number of members added
Geo.GEOJSONADD = function()
  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)
  assert(#ARGV == 1, 'Expecting a single argument')
  local geojson = assert(cjson.decode(table.remove(ARGV, 1)), 'Expecting a valid JSON object')
  assert(geojson['type'], 'Expecting a valid GeoJSON object but got no type')
  assert(geojson['type'] == 'FeatureCollection', 'Expecting a FeatureCollection, got ' .. geojson['type'])
  assert(type(geojson['features']) == 'table', 'No features found in FeatureCollection')
  
  local geoadd = {}
  for i, v in ipairs(geojson['features']) do
    assert(v['type'], 'Expecting a valid GeoJSON object but got no type for feature')
    assert(v['type'] == 'Feature', 'Expecting Feature as type, got ' .. v['type'])
    assert(v['geometry'], 'No feature geometry')
    assert(v['geometry']['type'] == 'Point', 'Feature geometry must be a Point')
    local coord = assert(v['geometry']['coordinates'], 'No feature geometry coordinates provided')
    assert(type(coord) == 'table' and #coord > 1, 'Feature geometry coordinates must consist at least 2 values: longitude and latitude')
    geoadd[#geoadd+1] = coord[1]
    geoadd[#geoadd+1] = coord[2]
    local id = assert(v['properties']['id'], 'No id provided for member')
    table.insert(geoadd, id)
  end
  
  return redis.call('GEOADD', geokey, unpack(geoadd))
end

--- Adds polygons in a GeoJSON object to a polyhash.
-- Note: only the LineRing of the polygon.
-- @return added The number of members added
Geo.GEOJSONPOLYADD = function()
  local polykey = Geo._getcommandkeys(Geo._TYPE_GEOM)
  assert(#ARGV == 1, 'Expecting a single argument')
  local geojson = assert(cjson.decode(table.remove(ARGV, 1)), 'Expecting a valid JSON object')
  assert(geojson['type'], 'Expecting a valid GeoJSON object but got no type')
  assert(geojson['type'] == 'FeatureCollection', 'Expecting a FeatureCollection, got ' .. geojson['type'])
  assert(type(geojson['features']) == 'table', 'No features found in FeatureCollection')
  
  local polyadd = {}
  for i, v in ipairs(geojson['features']) do
    assert(v['type'], 'Expecting a valid GeoJSON object but got no type for feature')
    assert(v['type'] == 'Feature', 'Expecting Feature as type, got ' .. v['type'])
    local id = assert(v['properties']['id'], 'No id provided for member')    
    assert(v['geometry'], 'No feature geometry')
    assert(v['geometry']['type'] == 'Polygon', 'Feature geometry must be a Polygon, got ' .. v['geometry']['type'])
    local coord = assert(v['geometry']['coordinates'][1], 'No feature geometry coordinates provided')
    assert(type(coord) == 'table' and #coord > 2, 'Feature geometry coordinates must have at least 3 coordinates ' .. id)
    local enc = Geo._polygonencode(coord)
    polyadd[#polyadd+1] = { id, enc }
  end
  
  return redis.call('HMSET', polykey, unpack(polyadd))
end

--- Encodes the output of GEO commands as GeoJSON object.
-- @return json The serialized GeoJSON object
Geo.GEOJSONENCODE = function()
  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)
  assert(#ARGV > 1, 'Expecting at least two arguments')
  local geocmd = table.remove(ARGV, 1):upper()
  local subcmds = {}
  for _, v in ipairs(ARGV) do
    local s = v:upper()
    if s == 'WITHCOORD' or s == 'WITHHASH' or s == 'WITHDIST' then
      subcmds[s] = true
    end
  end
  local geojson = {type = 'FeatureCollection', features = {}}
  
  local r = {}
  if geocmd == 'GEOPOS' or geocmd == 'GEOHASH' then
    r = redis.call('GEOPOS', geokey, unpack(ARGV))
    for i, v in pairs(r) do
      if v then
        r[i] = { ARGV[i], v }
      else -- (nil)
        r[i] = nil
      end
    end
    if geocmd == 'GEOHASH' then
      local h = redis.call(geocmd, geokey, unpack(ARGV))
      for i, v in pairs(h) do
        if v then
          r[i] = { r[i][1], v, r[i][2] }
        end
      end      
    end
  elseif geocmd == 'GEORADIUS' or geocmd == 'GEORADIUSBYMEMBER' then
    assert(subcmds['WITHCOORD'], geocmd .. ' must be called with WITHCOORD')
    r = redis.call(geocmd, geokey, unpack(ARGV))
  elseif geocmd == 'GEOMETRYFILTER' then
    assert(subcmds['WITHCOORD'], geocmd .. ' must be called with WITHCOORD')
    -- return the geokey
    KEYS[#KEYS+1] = geokey
    -- return the geomash
    KEYS[#KEYS+1] = table.remove(ARGV, 1)
    r = Geo.GEOMETRYFILTER()
  else
    error('Unsupported command for GeoJSON encoding: ' .. geocmd)
  end

  for _, feature in pairs(r) do
    local jf =  { type = 'Feature',
                  geometry = {
                    type = 'Point',
                    coordinates = {}
                  },
                  properties = {}
                }
    jf['properties']['id'] = table.remove(feature, 1)
    if geocmd == 'GEOHASH' then
      jf['properties']['geohash'] = table.remove(feature, 1)
    end
    if subcmds['WITHDIST'] then
      jf['properties']['distance'] = table.remove(feature, 1)
    end
    if subcmds['WITHHASH'] then
      jf['properties']['rawhash'] = table.remove(feature, 1)
    end
    local coords = table.remove(feature, 1)
    jf['geometry']['coordinates'] = {tonumber(coords[1]), tonumber(coords[2])}
    table.insert(geojson['features'], jf)
  end
  
  return cjson.encode(geojson)
end

--- Adds members to an xyzset.
-- @return added The number of members added
Geo.GEOZADD = function()
  local geokey, zsetkey = Geo._getcommandkeys(Geo._TYPE_XYZ)
  -- ARGV's should be made of tuples (longitude, latitude, altitude, member)
  assert(#ARGV > 0 and #ARGV % 4 == 0, 'Expecting a positive multiple of four arguements')
  
  local geoadd, zadd = {}, {}
  while #ARGV > 0 do
    geoadd[#geoadd+1] = table.remove(ARGV, 1)
    geoadd[#geoadd+1] = table.remove(ARGV, 1)
    zadd[#zadd+1] = table.remove(ARGV, 1)
    zadd[#zadd+1] = table.remove(ARGV, 1)
    geoadd[#geoadd+1] = zadd[#zadd]
  end
  
  redis.call('ZADD', zsetkey, unpack(zadd))
  return redis.call('GEOADD', geokey, unpack(geoadd))
end

--- Delete a member.
-- @return deleted Number of deleted members
Geo.GEOZREM = function()
  local geokey, zsetkey = Geo._getcommandkeys(Geo._TYPE_XYZ)
  assert(#ARGV > 0, 'No members to remove provided')
  
  redis.call('ZREM', zsetkey, unpack(ARGV))
  return redis.call('ZREM', geokey, unpack(ARGV))
end

--- The positions of members.
-- @return members Table with members' positions
Geo.GEOZPOS = function()
  local geokey, zsetkey = Geo._getcommandkeys(Geo._TYPE_XYZ)
  assert(#ARGV > 0, 'No members to remove provided')
  
  local r = redis.call('GEOPOS', geokey, unpack(ARGV))
  for i, v in ipairs(r) do
    if v then -- not (nil)
      v[#v+1] = redis.call('ZSCORE', zsetkey, ARGV[i])
    end
  end
  
  return r
end

--- Upserts a member's position and publishes a notification.
-- @return upserted Number of upserted members
Geo.GEOTRACK = function()
  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)
  
    -- ARGV's should be made of tuples (longitude, latitude, member)
  assert(#ARGV > 0 and #ARGV % 3 == 0, 'Expecting a positive multiple of three arguements')
  
  local reply = 0
  while #ARGV > 0 do
    local lon, lat, member = table.remove(ARGV, 1), table.remove(ARGV, 1),table.remove(ARGV, 1)
    reply = reply + redis.call('GEOADD', geokey, lon, lat, member)
    redis.call('PUBLISH', '__geo:' .. geokey .. ':' .. member, lon .. ':' .. lat)
    end
  
  return reply
end

--- Provides comfort.
-- @return help The help
Geo.HELP = function()
  local reply = { }
  table.insert(reply, _NAME .. " (" .. _VERSION .. "): " .. _DESCRIPTION)
  local curr = 1
  local from, to = string.find(_USAGE, '\n', curr)
  while from do
    table.insert(reply, string.sub(_USAGE, curr, from-1))
    curr = to + 1
    from, to = string.find(_USAGE, '\n', curr)
  end
  table.insert(reply, string.sub(_USAGE, curr))
  return reply
end

-- "main"
assert(redis.call('COMMAND', 'INFO', 'GEOADD'), 'Redis GEO API is missing (are you using v3.2 or above?)')

local command_name = assert(table.remove(ARGV, 1), 'No command provided - try `help`')
command_name = command_name:upper()

local command = assert(Geo[command_name], 'Unknown command ' .. command_name)
return command()
Download .txt
gitextract_b7ntzx8_/

├── CHANGELOG.md
├── LICENSE
├── README.md
├── dataset1.json
├── dataset1.lua
└── geo.lua
Condensed preview — 6 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (40K chars).
[
  {
    "path": "CHANGELOG.md",
    "chars": 518,
    "preview": "[0.1.6] - 2016-\n===\nAdds spatial geometries, increases similarity to LOLCODE.\n\n**IMPORTANT:** This release breaks the AP"
  },
  {
    "path": "LICENSE",
    "chars": 1617,
    "preview": "geo.lua is provided under the 3-Clause BSD License: http://opensource.org/licenses/BSD-3-Clause\n\nCopyright (c) 2016, Ita"
  },
  {
    "path": "README.md",
    "chars": 11374,
    "preview": "geo.lua - helper library for Redis geospatial indices :earth_africa:\n===\n\nThis is a Lua library containing miscellaneous"
  },
  {
    "path": "dataset1.json",
    "chars": 634,
    "preview": "{ \\\"type\\\": \\\"FeatureCollection\\\", \\\"features\\\": [ { \\\"type\\\": \\\"Feature\\\", \\\"geometry\\\": {\\\"type\\\": \\\"Point\\\", \\\"coordi"
  },
  {
    "path": "dataset1.lua",
    "chars": 388,
    "preview": "local data = {\n  '15.0074779', '37.4908267', 'RL@Catania',\n  '34.84076', '32.10942', 'RL@TLV',\n  '-122.0678325', '37.377"
  },
  {
    "path": "geo.lua",
    "chars": 24383,
    "preview": "local _NAME = 'geo.lua'\nlocal _VERSION = '0.1.6'\nlocal _DESCRIPTION = 'A helper library for Redis geospatial indices'\nlo"
  }
]

About this extraction

This page contains the full source code of the RedisLabs/geo.lua GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 6 files (38.0 KB), approximately 11.3k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!