[
  {
    "path": "CHANGELOG.md",
    "content": "[0.1.6] - 2016-\n===\nAdds spatial geometries, increases similarity to LOLCODE.\n\n**IMPORTANT:** This release breaks the API.\n\n * New commands: `GEOMETRYADD`, `GEOMETRYGET`\n * `GEOPOLYGON` is now `GEOMETRYFILTER`. It:\n  * Accepts a geometry as a filter (right now, only polygons)\n  * Supports the `STORE` subcommand\n * Polygon search optimization\n\n[0.1.5] - 2016-02-20 \n===\nInitial release.\n\n[0.1.6]: https://github.com/RedisLabs/geo.lua/releases/tag/0.1.6\n[0.1.5]: https://github.com/RedisLabs/geo.lua/releases/tag/0.1.5"
  },
  {
    "path": "LICENSE",
    "content": "geo.lua is provided under the 3-Clause BSD License: http://opensource.org/licenses/BSD-3-Clause\n\nCopyright (c) 2016, Itamar Haber.\nCopyright (c) 2016, Redis Labs, Inc.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n\n2. 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.\n\n3. 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.\n\nTHIS 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."
  },
  {
    "path": "README.md",
    "content": "geo.lua - helper library for Redis geospatial indices :earth_africa:\n===\n\nThis is a Lua library containing miscellaneous geospatial helper routines for use with [Redis]. It requires the [Redis GEO API], available from v3.2.\n\nIn broad strokes, the library provides:\n* [Polygon searches](#GEOMETRYFILTER) on geoset members\n* Navigational information such as [bearing](#GEOBEARING) and [path length](#GEOPATHLEN)\n* GeoJSON [decoding](#GEOJSONADD) and [encoding](#GEOJSONENCODE)\n* A playground for testing experimental geospatial APIs\n\nThe library is strictly :straight_ruler: metric, sorry.\n![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)\n\nUsing geo.lua\n---\nThe library is an ordinary Redis Lua script - use [`EVAL`] or [`EVALSHA`] to call it. The following example demonstrates usage from the prompt:\n\n````Bash\n$ redis-cli SCRIPT LOAD \"$(cat geo.lua)\"\n\"fd07...\"\n$ redis-cli EVALSHA fd07... 0 help\n 1) \"geo.lua (0.1.5): A helper library for Redis geospatial indices\"\n ...\n````\n\nLibrary API\n---\n\n### GEO API completeness\n\n<a name=\"GEOBEARING\"></a>\n#### GEOBEARING KEYS[1] geoset ARGV[2] member1 3] member2\nReturn the initial and final bearing between two members.\n> Time complexity: O(1).\n\nFor information about the calculation refer to  http://mathforum.org/library/drmath/view/55417.html.\n\n**Return:** Array reply, specifically the initial and final bearings.\n\n<a name=\"GEOPATHLEN\"></a>\n#### GEOPATHLEN KEYS[1] geoset ARGV[2] member [...]\nThe length of a path between members.\n> Time complexity: O(N) where N is the number of members.\n\n**Note:** This is basically a variadic form for [`GEODIST`].\n\n**Return:** String reply, specifically the length in meters.\n\n<a name=\"GEODEL\"></a>\n#### GEODEL KEYS[1] geoset ARGV[2] member [...]\nDelete members from a geoset.\n> 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.\n\nThis 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.\n\n**Return:** Integer reply, specifically the number of members actually deleted.\n\n### 2D geometries and polygon search\nRedis' 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:\n\n| Geometry             | Storage | `GEOMETRYADD` | `GEOMETRYGET` | `GEOMETRYFILTER` | `GEOJSONADD`\n| :---                 | :---    | :---:         | :---:         | :---:            | :---:\n| `Point`              | geoset  | N/A           | N/A           | No               | Yes\n| `Polygon`            | geomash | Yes           | Yes           | Yes              | Yes\n| `MultiPolygon`       | geomash | TODO          | TODO          | TODO             | RODO\n| `MultiPoint`         | TODO    | TODO          | TODO          | No               | TODO\n| `LineString`         | TODO    | TODO          | TODO          | TODO             | TODO\n| `MultiLineString`    | TODO    | TODO          | TODO          | TODO             | TODO\n| `GeometryCollection` | TODO    | TODO          | TODO          | TODO             | TODO\n\n<a name=\"GEOMETRYADD\"></a>\n#### GEOMETRYADD KEYS[1] geomash ARGV[2] geometry-type 3] id 4..] <geometry data>\nUpsert a single geometry to a geomash.\n> Time complexity:\n\n`ARGV[4]` and above describe the geometry. Depending on the geometry's type:\n  * `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).\n\n**Note:** there is no `GEOMETYREM`, use [`HDEL`](http://redis.io/commands/hdel) instead.\n\n**Return:** Integer reply, specifically 0 if updated and 1 if added.\n\n<a name=\"GEOMETRYGET\"></a>\n#### GEOMETRYGET KEYS[1] geomash ARGV[a] [WITHPERIMETER|WITHBOX|WITHCIRCLE] [...] ARGV[2] id  [...]\nReturns geometries' coordinates.\n> Time complexity:\n\nThe reply can be enriched with meta data about the geometry. The following sub-commands are supported, depending on the type of geometry.\n  * `WITHPERIMETER`: For `Polygon`, The total length of edges\n  * `WITHBOX`: The bounding box of the geometry\n  * `WITHCIRCLE`: The bounding circle of the geometry\n\n**Return:** Array reply, specifically:`\n  * The geometry's type\n  * The geometry's coordinates\n  * When called with `WITHPERIMETER`, the total length of edges\n  * When called with `WITHBOX`, the minimum and maximum coordinates of the bounding box, as well as it's bounding circle's radius\n  * When called with `WITHCIRCLE`, the center coordinates and radius of bounding circle\n\n<a name=\"GEOMETRYFILTER\"></a>\n#### GEOMETRYFILTER KEYS[1] geoset 2] geomash a] [target] ARGV[a] [STORE|WITHCOORD] [...] ARGV[2] id\nSearch for geoset members inside a geometry.\n> Time complexity:\n\nThis 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)).\n\nThe following sub-commands are supported:\n * `STORE`: Stores the search results in the target geoset\n * `WITHCOORD`: Returns the members' coordinates as well\n\n**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.\n\n### GeoJSON\nA 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.\n\n<a name=\"GEOJSONADD\"></a>\n#### GEOJSONADD KEYS[1] geoset 2] geomash ARGV[1] GeoJSON\n> Time complexity: O(log(N)) for each feature in the GeoJSON object, where N is the number of members in the geoset.\n\nUpsert 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`.\n\n**Return:** Integer reply, specifically the number of features upserted.\n\n<a name=\"GEOJSONENCODE\"></a>\n#### GEOJSONENCODE KEYS[1] geoset ARGV[2] <GEO command> 3] [arg] [...]\n> Time complexity: depends on the GEO command and its arguments.\n\nEncodes the reply of GEO commands as a GeoJSON object. Valid GEO commands are:\n* [`GEOHASH`]\n* [`GEOPOS`]\n* [`GEORADIUS`] and [`GEORADIUSBYMEMBER`]\n* [`GEOMETRYFILTER`]\n\n**Return:** String, specifically the reply of the GEO command encoded as a GeoJSON object.\n\n### Location updates\nImplements a real-time location tracking mechanism. Inspired by [Matt Stancliff @mattsta](https://matt.sh/redis-geo).\n\n<a name=\"GEOTRACK\"></a>\n#### GEOTRACK KEYS[1] geoset ARGV[2] longitude 3] latitude 4] member [...]\n> 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).\n\n[`GEOADD`]s a member and [`PUBLISH`]s on channel `__geo:<geoset>:<member>` a message with the format of `<longitude>:<latitude>`.\n\nClients 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>:*`).\n\n**Return:** Integer reply, specifically the number of members upserted.\n\n### xyzsets\nRedis' 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.\n\n<a name=\"GEOZADD\"></a>\n#### GEOZADD KEYS[1] geoset 2] azset ARGV[2] logitude 3] latitude 4] altitude 5] member [...]\n> Time complexity: O(log(N)) for each item added, where N is the number of elements in the geoset.\n\nUpsert members. Altitude is given as meters above (or below) sea level.\n\n**Return:** Integer reply, specifically the number of members upserted.\n\n<a name=\"GEOZREM\"></a>\n#### GEOZREM KEYS[1] geoset 2] azset ARGV[2] member [...]\n> 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.\n\nRemove members.\n\n**Return:** Integer reply, specifically the number of members actually deleted.\n\n<a name=\"GEOZPOS\"></a>  \n#### GEOZPOS KEYS[1] geoset 2] azset ARGV[2] member [...]\n> Time complexity: O(log(N)) for each member requested, where N is the number of elements in the geoset.\n\nThe position of members in 3D.\n\n**Return:** Array reply, specifically the members and their positions.\n\n#### TODO: GEOZDIST\nReturns the distance between members.\n\n#### TODO: GEOZCYLINDER\nPerform cylinder-bound search.\n\n#### TODO: GEOZCYLINDERBYMEMBER\nPerform cylinder-bound search by member (a 20 characters command!).\n\n#### TODO: GEOZSPHERE\nUseful for directing air traffic and impact research (bombs, comets).\n\n#### TODO: GEOZCONE\nGood for comparing :alien: sightings vis a vis :cow: abduction data.\n\n### Motility\nStoring 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).\n\n#### TODO: GEOMADD KEYS[1] geoset 2] vector hash ARGV[2] longitude 3] latitude 4] bearing 5] velocity 6] member [...]\n\nUpsert members. Bearing given in degrees, velocity in meters/second.\n\n#### TODO: GEOMREM KEYS[1] geoset 2] vector hash ARGV[2] member [...]\nRemove members.\n\n#### TODO: GEOMPOSWHEN KEYS[1] geoset 2] vector hash ARGV[2] seconds 3] member [...]\nProject members position in future or past.\n\n#### TODO: GEOMMEETWHENWHERE KEYS[1] geoset 2] vector hash ARGV[2] member1 3] member2\nHelp solving basic math exercises.\n\nLicense\n---\n3-Clause BSD.\n\nContributing\n---\n\nYou're encouraged to contribute to the open source geo.lua project. There are two ways you can do so.\n\n### Issues\n\nIf you encounter an issue while using the geo.lua library, please report it at the project's issues tracker. Feature suggestions are also welcome.\n\n### Pull request\n\nCode contributions to the geo.lua project can be made using pull requests. To submit a pull request:\n\n1. Fork this project.\n2. Make and commit your changes.\n3. Submit your changes as a pull request.\n\n[Redis]: http://redis.io\n[Redis GEO API]: (http://redis.io/commands#geo)\n[`GEOADD`]: (http://redis.io/commands/geoadd)\n[`GEODIST`]: (http://redis.io/commands/geodist)\n[`GEOHASH`]: (http://redis.io/commands/geohash)\n[`GEOPOS`]: (http://redis.io/commands/geopos)\n[`GEORADIUS`]: (http://redis.io/commands/georadius)\n[`GEORADIUSBYMEMBER`]: (http://redis.io/commands/georadiusbymember)\n[`EVAL`]: (http://redis.io/commands/eval)\n[`EVALSHA`]: (http://redis.io/commands/evalsha)\n[`PUBLISH`]: (http://redis.io/commands/publish)\n[`SUBSCRIBE`]: (http://redis.io/commands/subscribe)\n[`PSUBSCRIBE`]: (http://redis.io/commands/psubscribe)\n[`ZREM`]: (http://redis.io/commands/zrem)\n"
  },
  {
    "path": "dataset1.json",
    "content": "{ \\\"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\\\"} } ] }"
  },
  {
    "path": "dataset1.lua",
    "content": "local data = {\n  '15.0074779', '37.4908267', 'RL@Catania',\n  '34.84076', '32.10942', 'RL@TLV',\n  '-122.0678325', '37.3775256', 'RL@MV',\n  '34.8380433', '32.1098095', 'Hudson'\n}\n\nlocal alt = {\n  '97', 'RL@Catania',\n  '18', 'RL@TLV',\n  '38', 'RL@MV',\n  '18', 'Hudson'\n}  \n\nredis.call('DEL', unpack(KEYS))\nredis.call('GEOADD', KEYS[1], unpack(data))\nredis.call('ZADD', KEYS[2], unpack(data))"
  },
  {
    "path": "geo.lua",
    "content": "local _NAME = 'geo.lua'\nlocal _VERSION = '0.1.6'\nlocal _DESCRIPTION = 'A helper library for Redis geospatial indices'\nlocal _COPYRIGHT = '2016 Itamar Haber, Redis Labs'\nlocal _USAGE = [[\n\nCall with ARGV[1] being one of the following commands:\n  \n  - GEO API completeness -\n  GEODEL        - delete a member\n  GEOBEARING    - initial and final bearing between members\n  GEOPATHLEN    - length of a path between members\n  \n  - 2D geometries and polygon search -\n  GEOMETRYADD   - upsert a geometry to a polyhash\n  GEOMETRYGET   - returns coordinates and other stuff for polygons\n  GEOMETRYFILER - search geoset inside a geometry\n    \n  - GeoJSON -\n  GEOJSONADD    - upsert members to geoset from GeoJSON object\n  GEOJSONENCODE - return GeoJSON object for these GEO commands:\n                  GEOHASH, GEOPOS, GEORADIUS[BYMEMBER] and\n                  GEOMETRYFILTER\n\n  - xyzsets (longitude, latitude & altitude) -\n  GEOZADD       - upsert member with altitude\n  GEOZREM       - remove member\n  GEOZPOS       - the 3d position of members\n  \n  - location updates (credit @mattsta) -\n  GEOTRACK      - positional updates notifications\n\n  help          - this text, for more information see the README file\n\n]]\n\nlocal Geo = {}      -- GEO library\n\n-- private\n\n-- Geospatial 2D Geometry\nlocal Geometry = {}\nGeometry.__index = Geometry\n\nsetmetatable(Geometry, {\n              __call = function(cls, ...)\n                return cls.new(...)\n                end })\n\nGeometry._TYPES = { 'Point',           -- TODO\n                    'MultiPoint',      -- TODO\n                    'LineString',      -- TODO \n                    'MultiLineString', -- TODO\n                    'Polygon',\n                    'MultiPolygon' }   -- TODO\nGeometry._TENUM = {}\nfor i = 1, #Geometry._TYPES do\n  Geometry._TENUM[Geometry._TYPES[i]:upper()] = i\n  Geometry['_T' .. Geometry._TYPES[i]:upper()] = i\nend\n\n--- Calculates distance between two coordinates.\n-- Just like calling GEODIST, but slower and of slightly different\n-- accuracy.\n-- @param lon1 The longitude of the 1st coordinate\n-- @param lat1 The latitude of the 1st coordinate\n-- @param lon2 The longitude of the 2nd coordinate\n-- @param lat2 The latitude of the 2nd coordinate\n-- @return distance The distance in meters\nGeometry._distance = function (lon1, lat1, lon2, lat2)\n  local R = 6372797.560856 -- Earth's, in meters\n  local lon1r, lat1r, lon2r, lat2r = \n    math.rad(lon1), math.rad(lat1), math.rad(lon2), math.rad(lat2)\n  local u = math.sin((lat2r - lat1r) / 2)\n  local v = math.sin((lon2r - lon1r) / 2)\n  return 2.0 * R * math.asin(math.sqrt(u * u + math.cos(lat1r) * math.cos(lat2r) * v * v))\nend\n\n--- Geometry constructor.\n-- Creates a geometry object.\n-- @param geomtype The geometry's type, optional\n-- @param ... The geometry's geometries\n-- @return self New Geometry object\nGeometry.new = function(geomtype, ...)\n  local self = setmetatable({}, Geometry)\n  self.geomtype = geomtype\n  self.coordn, self.coordx, self.coordy = {}, {}, {}\n  self.meta = {}\n  \n  if #arg > 0 then\n    if self.geomtype == Geometry._TPOLYGON then \n      -- A polygon's coordinates are given by one or more LineRings\n      -- A LineRing is a LineString where the first and last vertices are identical\n      -- The first and mandatory ring is the polygon's outer ring\n      -- Any subsequent rings are considered holes, islands, and repeating...\n            \n      -- Polygon vertices are stored as two coordinate lists\n      -- When there's more than one ring, O means the (0,0) and the two lists:\n      --   begins with O\n      --   rings are delimeted with O\n      --   end with O\n      self.coordx[#self.coordx+1], self.coordy[#self.coordy+1] = 0, 0\n      for _, ring in ipairs(arg[1]) do\n        local n = #ring / 2\n        self.coordn[#self.coordn+1] = n\n        for i = 1, n do  \n          self.coordx[#self.coordx+1], self.coordy[#self.coordy+1] = ring[2*i-1], ring[2*i]\n        end\n        self.coordx[#self.coordx+1], self.coordy[#self.coordy+1] = 0, 0\n      end\n      \n      -- the outer ring\n      local op = 0                    -- outer perimeter\n      local ov = self.coordn[1]       -- number of vertices\n      local bbx1 = self.coordx[2]     -- bounding box\n      local bby1 = self.coordy[2]\n      local bbx2 = self.coordx[2]  \n      local bby2 = self.coordy[2]\n      local bbr = 0\n      local bcx, bcy, bcr = 0, 0, 0   -- bounding circle\n      -- traverse outer vertices\n      for i = 2, 1 + ov do\n        local ix, iy = self.coordx[i], self.coordy[i]\n        local jx, jy = self.coordx[i+1], self.coordy[i+1]\n        -- grow the outer perimeter\n        op = op + Geometry._distance(ix, iy, jx, jy)\n        -- add up for the averages\n        bcx = bcx + ix\n        bcy = bcy + iy\n        -- adjust bounding box if needed\n        if bbx1 > ix then\n          bbx1 = ix\n        elseif bbx2 < ix then\n          bbx2 = ix\n        end\n        if bby1 > iy then\n          bby1 = iy\n        elseif bby2 < iy then\n          bby2 = iy\n        end            \n      end\n      bcx = bcx / (ov - 1)\n      bcy = bcy / (ov - 1)\n      bbr = Geometry._distance(bbx1, bby1, bbx2, bby2) / 2\n  \n      -- farthest point from the centroid is the radius\n      for i = 2, 1 + ov do\n        local d = Geometry._distance(bcx, bcy, self.coordx[i], self.coordy[i])\n        if bcr < d then\n          bcr = d\n        end\n      end\n  \n      -- lets not be too accurate\n      bcr = math.ceil(bcr)\n      bbr = math.ceil(bbr)\n \n      self.meta = { op, ov, bcx, bcy, bcr, bbx1, bby1, bbx2, bby2, bbr }\n    else\n      error('Unknown argument(s) to for type')\n    end\n  end\n  \n  return self\nend\n\n--- Checks if a point is inside a polygon.\n-- https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html\n-- @param pt Point\n-- @param po Polygon\n-- @return boolean True if PIP\nfunction Geometry:PNPOLY(x, y)\n  local c = false\n  -- don't think outside the box\n  if self.meta[6]<=x and self.meta[8]>=x and self.meta[7]<=y and self.meta[9]>=y then\n    \n    local nvert = #self.coordy\n    local vertx = self.coordx\n    local verty = self.coordy\n    local testx = x\n    local testy = y\n    local j = nvert\n    \n    for i = 1, nvert do\n      if  ((verty[i]>testy) ~= (verty[j]>testy)) and\n          testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i] then\n        c = not c\n      end\n      j = i\n    end\n  end\n\n  return c\nend\n\n--- Returns the bounding circle for a geometry.\n-- @return Table three elements: x, y and radius\nfunction Geometry:getBoundingCircle()\n  -- TODO: error on `Point`\n  return { self.meta[3], self.meta[4], self.meta[5] }\nend\n\n--- Returns the bounding box for a geometry.\n-- return Table five elements: min x and y, max x and y, radius\nfunction Geometry:getBoundingBox()\n  -- TODO: error on `Point`\n  return { self.meta[6], self.meta[7], self.meta[8], self.meta[9], self.meta[10] }\nend\n\n--- Returns the geometry's type name.\n-- @return name The geometry's name\nfunction Geometry:typeAsString()\n  return Geometry._TYPES[self.geomtype]\nend\n\n--- Returns a table of geometry's coordinates.\n-- Every geometry is a table, and in it every element is a vertex table.\n-- @return coord The coordinates as a table for a RESP reply\nfunction Geometry:coordAsRESP()\n  local reply = {}\n\n  local verti = 2\n  for geomi, geomn in ipairs(self.coordn) do\n    local r = {}\n    local vertn = verti + geomn - 1\n    for i = verti, vertn do\n      r[#r+1] = { tostring(self.coordx[i]), tostring(self.coordy[i]) }\n    end\n    verti = vertn + 2\n    reply[#reply+1] = r\n  end\n  \n  return reply\nend\n\n--- Returns a table of the geometry's meta information.\n-- @return meta The meta information\nfunction Geometry:metaAsRESP(subcmds)\n  local reply = {}\n  \n  if not subcmds or not subcmds['ANY'] then\n    return\n  end\n  if self.geomtype == Geometry._TPOLYGON then  \n    if subcmds['WITHPERIMETER'] then\n      reply[#reply+1] = tostring(self.meta[1])\n    end\n    if subcmds['WITHBOX'] then\n      local bbox = self:getBoundingBox()\n      reply[#reply+1] = { { tostring(bbox[1]), tostring(bbox[2]) },\n                          { tostring(bbox[3]), tostring(bbox[4]) },\n                          bbox[5]}\n    end\n    if subcmds['WITHCIRCLE'] then\n      local bcircle = self:getBoundingCircle()\n      reply[#reply+1] = { { tostring(bcircle[1]), tostring(bcircle[2]) },\n                          bcircle[3] }\n    end\n  end           \n  \n  if #reply > 0 then\n    return reply\n  else\n    return\n  end\nend\n\n--- Returns the msgpack-serialized geometry object.\n-- @return string The serialized object\nfunction Geometry:dump()\n  local payload = {}\n  payload[#payload+1] = self.geomtype\n  payload[#payload+1] = self.coordn\n  payload[#payload+1] = self.coordx\n  payload[#payload+1] = self.coordy\n  payload[#payload+1] = self.meta\n  return cmsgpack.pack(payload)\nend\n\n--- Loads the geometry from its serialized form.\n-- @param msgpack The serialized geometry\n-- @return null\nfunction Geometry:load(msgpack)\n  local payload = cmsgpack.unpack(msgpack)\n  self.geomtype = table.remove(payload, 1)\n  self.coordn = table.remove(payload, 1)\n  self.coordx = table.remove(payload, 1)\n  self.coordy = table.remove(payload, 1)\n  self.meta = table.remove(payload, 1)\nend\n\n-- Data structure type\nGeo._TYPE_GEO = 1     -- regular geoset\nGeo._TYPE_XYZ = 2     -- xyzset\nGeo._TYPE_GEOM = 3    -- geomash\n\n--- Keys validation.\n-- Extract and validate types of keys for command\n-- @param geotype The type of command\n-- @return geoset|polyhash Key name\n-- @return [azset]  Key name\nGeo._getcommandkeys = function (geotype)\n  \n  local function asserttype(k, t)\n    local r = redis.call('TYPE', k)\n    assert(r['ok'] == t or r['ok'] == 'none', 'WRONGTYPE Operation against a key holding the wrong kind of value')\n  end\n  \n  if geotype == Geo._TYPE_GEO then\n    local geokey = assert(table.remove(KEYS, 1), 'No geoset key name provided')\n    asserttype(geokey, 'zset')\n    return geokey\n  elseif geotype == Geo._TYPE_XYZ then\n    local geokey = assert(table.remove(KEYS, 1), 'No geoset key name provided')\n    asserttype(geokey, 'zset')\n    local zsetkey = assert(table.remove(KEYS, 1), 'No altitude sorted set key name provided')\n    asserttype(zsetkey, 'zset')\n    return geokey, zsetkey\n  elseif geotype == Geo._TYPE_GEOM then\n    local geomkey = assert(table.remove(KEYS, 1), 'No geomash key name provided')\n    asserttype(geomkey, 'hash')\n    return geomkey\n  end\nend\n\n-- public API\n\n--- Delete a member.\n-- @return deleted Number of deleted members\nGeo.GEODEL = function()\n  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)\n  return redis.call('ZREM', geokey, unpack(ARGV))\nend\n\n--- Returns bearing between two members.\n-- @return bearings Table containing the initial and final bearings\nGeo.GEOBEARING = function()\n  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)\n  \n  assert(#ARGV == 2, 'Two members must be provided')\n  local member1, member2 = table.remove(ARGV, 1), table.remove(ARGV, 1)\n  \n  local coords = redis.call('GEOPOS', geokey, member1, member2)\n  assert(coords[1] and coords[2], 'Two existing members must be provided')\n  \n  local function initialbearing(lon1, lat1, lon2, lat2)\n    local p1, p2 = math.rad(lat1), math.rad(lat2)\n    local dl = math.rad(lon2 - lon1)\n    local y = math.sin(dl)*math.cos(p2)\n    local x = math.cos(p1)*math.sin(p2) - math.sin(p1)*math.cos(p2)*math.cos(dl)\n    local b = (math.deg(math.atan2(y, x)) + 360) % 360\n    return b\n  end\n  \n  local ib = initialbearing(coords[1][1], coords[1][2], coords[2][1], coords[2][2])\n  local fb = (initialbearing(coords[2][1], coords[2][2], coords[1][1], coords[1][2]) + 180) % 360\n\n  return { tostring(ib), tostring(fb) }\nend\n\n--- Calculates the length of a path given by members.\n-- @return length Path's length in meters\nGeo.GEOPATHLEN = function()\n  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)\n  assert(#ARGV > 1, 'Need at least two members to make a path')\n  \n  local total = 0\n  local prev = table.remove(ARGV, 1)\n  while #ARGV > 0 do\n    local curr = table.remove(ARGV, 1)\n    local dist = redis.call('GEODIST', geokey, prev, curr, 'm')\n    if dist then\n      total = total + dist\n      prev = curr\n    else\n      return\n    end\n  end\n  \n  return total\nend\n\n--- Upserts a single geometry into a geomash.\n-- TODO: support more geometries besides an unholey `Polygon`\n-- TODO: support upsert of multiple geometries\n-- @return upserted Number 0 if updated, 1 if added\nGeo.GEOMETRYADD = function()\n  local geomkey = Geo._getcommandkeys(Geo._TYPE_GEOM)\n  local geomtype = assert(table.remove(ARGV, 1), 'Expecting a geometry type')\n  geomtype = geomtype:upper()\n  geomtype = assert(Geometry._TENUM[geomtype], 'Invalid geometry type')\n  local id = assert(table.remove(ARGV, 1), 'Expecting a geometry id')\n  \n  assert(geomtype == Geometry._TPOLYGON, 'Sorry, atm only `POLYGON` geometry type is supported')\n  assert(#ARGV > 7, 'Expecting at least 4 coordinates')\n  assert(#ARGV % 2 == 0, 'Expecting an even number of arguments as coordinates')  \n  assert(ARGV[1] == ARGV[#ARGV-1] and ARGV[2] == ARGV[#ARGV], 'The first and last vertices must be the identical')\n  \n  local coord = {}\n  for i, v in ipairs(ARGV) do\n    coord[#coord+1] = assert(tonumber(v), 'Expecting numbers as coordinates')\n  end\n  \n  local geom = Geometry.new(Geometry._TPOLYGON, { coord })\n  return redis.call('HSET', geomkey, id, geom:dump())\nend\n\n--- Returns geometries from a geomash.\n-- @return vertices Table of vertices\nGeo.GEOMETRYGET = function()\n  local geomkey = Geo._getcommandkeys(Geo._TYPE_GEOM)\n  local subcmds = { WITHPERIMETER = false,\n                    WITHBOX = false,\n                    WITHCIRCLE = false }\n  assert(#ARGV > 0, 'Expecting at least one argument')\n  for i = 1, math.min(3, #ARGV) do\n    local s = ARGV[1]:upper()\n    -- there are edge cases this will not cover but good enough\n    if subcmds[s] ~= nil and not subcmds[s] then\n      subcmds['ANY'] = true\n      subcmds[s] = true\n      table.remove(ARGV, 1)\n    end\n  end  \n  \n  assert(#ARGV > 0, 'Expecting at least one geometry id')\n  \n  local r = redis.call('HMGET', geomkey, unpack(ARGV))\n  -- cast each geometry in the reply to RESP \n  for ri, rv in ipairs(r) do\n    if rv then -- i.e. not (nil), meaning v is a geometry\n      local geom = Geometry.new()\n      geom:load(rv)\n      local rep = {} \n      rep[#rep+1] = geom:typeAsString()\n      rep[#rep+1] = geom:coordAsRESP()\n      local meta = geom:metaAsRESP(subcmds)\n      if meta then rep[#rep+1]= meta end\n      r[ri] = rep\n    end\n  end\n  \n  return r\nend\n\n--- Performs a search for members inside a geometry.\n-- @return members Table with the members\nGeo.GEOMETRYFILTER = function()\n  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)\n  local geomkey = Geo._getcommandkeys(Geo._TYPE_GEOM)\n  \n  assert(#ARGV > 0, 'Expecting at least one argument')\n  local subcmds = { WITHCOORD = false,\n                    STORE = false }\n  for i = 1, math.min(2, #ARGV) do\n    local s = ARGV[1]:upper()\n    -- there are edge cases this will not cover but good enough\n    if subcmds[s] ~= nil and not subcmds[s] then\n      subcmds[s] = true\n      table.remove(ARGV, 1)\n    end\n  end\n\n  local targetkey\n  if subcmds['STORE'] then\n    -- TODO: this currently triggers an ambiguous error \n    targetkey = Geo._getcommandkeys(Geo._TYPE_GEO)\n  end\n  \n  assert(#ARGV == 1, 'Expecting a single geometry id')\n  local r = assert(redis.call('HGET', geomkey, ARGV[1]), 'Geometry id not found: ' .. ARGV[1])\n  local geom = Geometry:new()\n  geom:load(r)\n  assert(geom.geomtype == Geometry._TPOLYGON, 'Unsupported (TODO) filter geometry: ' .. geom:typeAsString())\n  \n  local bbox = geom:getBoundingBox()\n  local members = redis.call('GEORADIUS', geokey, (bbox[1] + bbox[3]) / 2, (bbox[2] + bbox[4]) / 2, bbox[5], 'm', 'WITHCOORD')\n  local reply, geoadd = {}\n  for i, v in ipairs(members) do\n    if geom:PNPOLY(tonumber(v[2][1]), tonumber(v[2][2])) then\n      if subcmds['WITHCOORD'] then\n        reply[#reply+1] = v\n      else\n        reply[#reply+1] = v[1]\n      end\n      if subcmds['STORE'] then\n        geoadd[#geoadd+1] = v[2][1]\n        geoadd[#geoadd+1] = v[2][2]\n        geoadd[#geoadd+1] = v[1]\n      end  \n    end\n  end\n  \n  if subcmds['STORE'] then\n    return redis.call('GEOADD', targetkey, unpack(geoadd))\n  else\n    return reply\n  end\nend\n\n--- Adds members in a GeoJSON object to a geoset.\n-- @return added The number of members added\nGeo.GEOJSONADD = function()\n  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)\n  assert(#ARGV == 1, 'Expecting a single argument')\n  local geojson = assert(cjson.decode(table.remove(ARGV, 1)), 'Expecting a valid JSON object')\n  assert(geojson['type'], 'Expecting a valid GeoJSON object but got no type')\n  assert(geojson['type'] == 'FeatureCollection', 'Expecting a FeatureCollection, got ' .. geojson['type'])\n  assert(type(geojson['features']) == 'table', 'No features found in FeatureCollection')\n  \n  local geoadd = {}\n  for i, v in ipairs(geojson['features']) do\n    assert(v['type'], 'Expecting a valid GeoJSON object but got no type for feature')\n    assert(v['type'] == 'Feature', 'Expecting Feature as type, got ' .. v['type'])\n    assert(v['geometry'], 'No feature geometry')\n    assert(v['geometry']['type'] == 'Point', 'Feature geometry must be a Point')\n    local coord = assert(v['geometry']['coordinates'], 'No feature geometry coordinates provided')\n    assert(type(coord) == 'table' and #coord > 1, 'Feature geometry coordinates must consist at least 2 values: longitude and latitude')\n    geoadd[#geoadd+1] = coord[1]\n    geoadd[#geoadd+1] = coord[2]\n    local id = assert(v['properties']['id'], 'No id provided for member')\n    table.insert(geoadd, id)\n  end\n  \n  return redis.call('GEOADD', geokey, unpack(geoadd))\nend\n\n--- Adds polygons in a GeoJSON object to a polyhash.\n-- Note: only the LineRing of the polygon.\n-- @return added The number of members added\nGeo.GEOJSONPOLYADD = function()\n  local polykey = Geo._getcommandkeys(Geo._TYPE_GEOM)\n  assert(#ARGV == 1, 'Expecting a single argument')\n  local geojson = assert(cjson.decode(table.remove(ARGV, 1)), 'Expecting a valid JSON object')\n  assert(geojson['type'], 'Expecting a valid GeoJSON object but got no type')\n  assert(geojson['type'] == 'FeatureCollection', 'Expecting a FeatureCollection, got ' .. geojson['type'])\n  assert(type(geojson['features']) == 'table', 'No features found in FeatureCollection')\n  \n  local polyadd = {}\n  for i, v in ipairs(geojson['features']) do\n    assert(v['type'], 'Expecting a valid GeoJSON object but got no type for feature')\n    assert(v['type'] == 'Feature', 'Expecting Feature as type, got ' .. v['type'])\n    local id = assert(v['properties']['id'], 'No id provided for member')    \n    assert(v['geometry'], 'No feature geometry')\n    assert(v['geometry']['type'] == 'Polygon', 'Feature geometry must be a Polygon, got ' .. v['geometry']['type'])\n    local coord = assert(v['geometry']['coordinates'][1], 'No feature geometry coordinates provided')\n    assert(type(coord) == 'table' and #coord > 2, 'Feature geometry coordinates must have at least 3 coordinates ' .. id)\n    local enc = Geo._polygonencode(coord)\n    polyadd[#polyadd+1] = { id, enc }\n  end\n  \n  return redis.call('HMSET', polykey, unpack(polyadd))\nend\n\n--- Encodes the output of GEO commands as GeoJSON object.\n-- @return json The serialized GeoJSON object\nGeo.GEOJSONENCODE = function()\n  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)\n  assert(#ARGV > 1, 'Expecting at least two arguments')\n  local geocmd = table.remove(ARGV, 1):upper()\n  local subcmds = {}\n  for _, v in ipairs(ARGV) do\n    local s = v:upper()\n    if s == 'WITHCOORD' or s == 'WITHHASH' or s == 'WITHDIST' then\n      subcmds[s] = true\n    end\n  end\n  local geojson = {type = 'FeatureCollection', features = {}}\n  \n  local r = {}\n  if geocmd == 'GEOPOS' or geocmd == 'GEOHASH' then\n    r = redis.call('GEOPOS', geokey, unpack(ARGV))\n    for i, v in pairs(r) do\n      if v then\n        r[i] = { ARGV[i], v }\n      else -- (nil)\n        r[i] = nil\n      end\n    end\n    if geocmd == 'GEOHASH' then\n      local h = redis.call(geocmd, geokey, unpack(ARGV))\n      for i, v in pairs(h) do\n        if v then\n          r[i] = { r[i][1], v, r[i][2] }\n        end\n      end      \n    end\n  elseif geocmd == 'GEORADIUS' or geocmd == 'GEORADIUSBYMEMBER' then\n    assert(subcmds['WITHCOORD'], geocmd .. ' must be called with WITHCOORD')\n    r = redis.call(geocmd, geokey, unpack(ARGV))\n  elseif geocmd == 'GEOMETRYFILTER' then\n    assert(subcmds['WITHCOORD'], geocmd .. ' must be called with WITHCOORD')\n    -- return the geokey\n    KEYS[#KEYS+1] = geokey\n    -- return the geomash\n    KEYS[#KEYS+1] = table.remove(ARGV, 1)\n    r = Geo.GEOMETRYFILTER()\n  else\n    error('Unsupported command for GeoJSON encoding: ' .. geocmd)\n  end\n\n  for _, feature in pairs(r) do\n    local jf =  { type = 'Feature',\n                  geometry = {\n                    type = 'Point',\n                    coordinates = {}\n                  },\n                  properties = {}\n                }\n    jf['properties']['id'] = table.remove(feature, 1)\n    if geocmd == 'GEOHASH' then\n      jf['properties']['geohash'] = table.remove(feature, 1)\n    end\n    if subcmds['WITHDIST'] then\n      jf['properties']['distance'] = table.remove(feature, 1)\n    end\n    if subcmds['WITHHASH'] then\n      jf['properties']['rawhash'] = table.remove(feature, 1)\n    end\n    local coords = table.remove(feature, 1)\n    jf['geometry']['coordinates'] = {tonumber(coords[1]), tonumber(coords[2])}\n    table.insert(geojson['features'], jf)\n  end\n  \n  return cjson.encode(geojson)\nend\n\n--- Adds members to an xyzset.\n-- @return added The number of members added\nGeo.GEOZADD = function()\n  local geokey, zsetkey = Geo._getcommandkeys(Geo._TYPE_XYZ)\n  -- ARGV's should be made of tuples (longitude, latitude, altitude, member)\n  assert(#ARGV > 0 and #ARGV % 4 == 0, 'Expecting a positive multiple of four arguements')\n  \n  local geoadd, zadd = {}, {}\n  while #ARGV > 0 do\n    geoadd[#geoadd+1] = table.remove(ARGV, 1)\n    geoadd[#geoadd+1] = table.remove(ARGV, 1)\n    zadd[#zadd+1] = table.remove(ARGV, 1)\n    zadd[#zadd+1] = table.remove(ARGV, 1)\n    geoadd[#geoadd+1] = zadd[#zadd]\n  end\n  \n  redis.call('ZADD', zsetkey, unpack(zadd))\n  return redis.call('GEOADD', geokey, unpack(geoadd))\nend\n\n--- Delete a member.\n-- @return deleted Number of deleted members\nGeo.GEOZREM = function()\n  local geokey, zsetkey = Geo._getcommandkeys(Geo._TYPE_XYZ)\n  assert(#ARGV > 0, 'No members to remove provided')\n  \n  redis.call('ZREM', zsetkey, unpack(ARGV))\n  return redis.call('ZREM', geokey, unpack(ARGV))\nend\n\n--- The positions of members.\n-- @return members Table with members' positions\nGeo.GEOZPOS = function()\n  local geokey, zsetkey = Geo._getcommandkeys(Geo._TYPE_XYZ)\n  assert(#ARGV > 0, 'No members to remove provided')\n  \n  local r = redis.call('GEOPOS', geokey, unpack(ARGV))\n  for i, v in ipairs(r) do\n    if v then -- not (nil)\n      v[#v+1] = redis.call('ZSCORE', zsetkey, ARGV[i])\n    end\n  end\n  \n  return r\nend\n\n--- Upserts a member's position and publishes a notification.\n-- @return upserted Number of upserted members\nGeo.GEOTRACK = function()\n  local geokey = Geo._getcommandkeys(Geo._TYPE_GEO)\n  \n    -- ARGV's should be made of tuples (longitude, latitude, member)\n  assert(#ARGV > 0 and #ARGV % 3 == 0, 'Expecting a positive multiple of three arguements')\n  \n  local reply = 0\n  while #ARGV > 0 do\n    local lon, lat, member = table.remove(ARGV, 1), table.remove(ARGV, 1),table.remove(ARGV, 1)\n    reply = reply + redis.call('GEOADD', geokey, lon, lat, member)\n    redis.call('PUBLISH', '__geo:' .. geokey .. ':' .. member, lon .. ':' .. lat)\n    end\n  \n  return reply\nend\n\n--- Provides comfort.\n-- @return help The help\nGeo.HELP = function()\n  local reply = { }\n  table.insert(reply, _NAME .. \" (\" .. _VERSION .. \"): \" .. _DESCRIPTION)\n  local curr = 1\n  local from, to = string.find(_USAGE, '\\n', curr)\n  while from do\n    table.insert(reply, string.sub(_USAGE, curr, from-1))\n    curr = to + 1\n    from, to = string.find(_USAGE, '\\n', curr)\n  end\n  table.insert(reply, string.sub(_USAGE, curr))\n  return reply\nend\n\n-- \"main\"\nassert(redis.call('COMMAND', 'INFO', 'GEOADD'), 'Redis GEO API is missing (are you using v3.2 or above?)')\n\nlocal command_name = assert(table.remove(ARGV, 1), 'No command provided - try `help`')\ncommand_name = command_name:upper()\n\nlocal command = assert(Geo[command_name], 'Unknown command ' .. command_name)\nreturn command()\n"
  }
]