{"id":21235984,"url":"https://github.com/lddl/horizon","last_synced_at":"2026-03-14T09:03:51.943Z","repository":{"id":42989585,"uuid":"247066042","full_name":"LdDl/horizon","owner":"LdDl","description":"Map matching (snapping GPS points to road graph) and routing library in Go","archived":false,"fork":false,"pushed_at":"2025-04-22T14:06:28.000Z","size":31920,"stargazers_count":52,"open_issues_count":5,"forks_count":8,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-22T15:24:39.395Z","etag":null,"topics":["hacktoberfest","hidden-markov-model","hmm","hmm-viterbi-algorithm","horizon","map-matching","routing-engine"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/LdDl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-03-13T12:26:54.000Z","updated_at":"2025-02-22T21:17:08.000Z","dependencies_parsed_at":"2023-02-16T20:16:07.764Z","dependency_job_id":"ce8eb06b-138e-497f-b414-6683cdd3ae0d","html_url":"https://github.com/LdDl/horizon","commit_stats":null,"previous_names":[],"tags_count":24,"template":false,"template_full_name":null,"purl":"pkg:github/LdDl/horizon","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LdDl%2Fhorizon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LdDl%2Fhorizon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LdDl%2Fhorizon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LdDl%2Fhorizon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LdDl","download_url":"https://codeload.github.com/LdDl/horizon/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LdDl%2Fhorizon/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264619111,"owners_count":23638407,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hacktoberfest","hidden-markov-model","hmm","hmm-viterbi-algorithm","horizon","map-matching","routing-engine"],"created_at":"2024-11-21T00:05:29.512Z","updated_at":"2025-12-24T12:36:05.190Z","avatar_url":"https://github.com/LdDl.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Horizon v0.11.1 [![GoDoc](https://godoc.org/github.com/LdDl/horizon?status.svg)](https://godoc.org/github.com/LdDl/horizon) [![Build Status](https://travis-ci.com/LdDl/horizon.svg?branch=master)](https://travis-ci.com/LdDl/horizon) [![Sourcegraph](https://sourcegraph.com/github.com/LdDl/horizon/-/badge.svg)](https://sourcegraph.com/github.com/LdDl/horizon?badge) [![Go Report Card](https://goreportcard.com/badge/github.com/LdDl/horizon)](https://goreportcard.com/report/github.com/LdDl/horizon) [![GitHub tag](https://img.shields.io/github/tag/LdDl/horizon.svg)](https://github.com/LdDl/horizon/releases)\n\n# Work in progress\nHorizon is project aimed to do map matching (snap GPS data to map) and routing (find shortest path between two points)\n\n## Table of Contents\n- [About](#about)\n- [Installation](#installation)\n- [Usage](#usage)\n    - [Docker](#docker)\n- [Benchmark](#benchmark)\n- [Support](#support)\n- [ToDo](#todo)\n- [Theory](#theory)\n- [Dependencies](#dependencies)\n- [License](#license)\n\n## About\nHorizon is targeted to make map matching as [OSRM](https://github.com/Project-OSRM/osrm-backend) / [Graphopper](https://github.com/graphhopper/graphhopper) or [Valhala](https://github.com/valhalla/valhalla) have done, but in Go ecosystem.\n\nDemonstration:\n\u003cimg src=\"images/workflow.gif\" width=\"720\"\u003e\n\n## Installation\nVia _go get_:\n```shell\ngo get github.com/LdDl/horizon\ngo install github.com/LdDl/horizon/cmd/horizon@v0.11.1\n```\n\nVia downloading prebuilt binary and making updates in yours PATH environment varibale (both Linux and Windows):\n* Windows - https://github.com/LdDl/horizon/releases/download/v0.11.1/windows-horizon.zip\n* Linux - https://github.com/LdDl/horizon/releases/download/v0.11.1/linux-amd64-horizon.tar.gz\n\nCheck if **horizon** binary was installed properly:\n```shell\nhorizon -h\n```\n\n\u003cimg src=\"images/inst1.png\" width=\"720\"\u003e\n\n## Usage\n### notice: targeted for Linux users (no Windows/OSX instructions currenlty)\nInstruction has been made for Linux mainly. For Windows or OSX the way may vary.\n\n0. Installing Prerequisites\n\n\n    * Install [osm2ch tool](https://github.com/LdDl/osm2ch#osm2ch). It's needed for converting *.osm.pbf file to CSV for proper usage in [contraction hierarchies (ch) library](https://github.com/LdDl/ch#ch---contraction-hierarchies)\n        ```shell\n        go install github.com/LdDl/osm2ch/cmd/osm2ch@v1.5.1\n        # for disabling zlib:\n        export CGO_ENABLED=0 \u0026\u0026 go install github.com/LdDl/osm2ch/cmd/osm2ch@v1.5.1\n        ```\n    * Check if **osm2ch** binary was installed properly:\n        ```shell\n        osm2ch -h\n        ```\n        \u003cimg src=\"images/inst2.png\" width=\"720\"\u003e\n    \n    * Install [osmconvert tool](https://wiki.openstreetmap.org/wiki/Osmconvert). It's needed for removing excess data from road graph and compressing *.osm file. You can follow the [link](https://wiki.openstreetmap.org/wiki/Osmconvert#Binaries) for theirs instruction.\n    We advice to use this method (described in [Source](https://wiki.openstreetmap.org/wiki/Osmconvert#Source) paragraph):\n        ```shell\n        wget -O - http://m.m.i24.cc/osmconvert.c | sudo cc -x c - -lz -O3 -o osmconvert \u0026\u0026 sudo mv osmconvert /usr/local/bin/\n        ```\n    * Check if **osmconvert** binary was installed properly:\n        ```shell\n        osmconvert -h\n        ```\n        \u003cimg src=\"images/inst3.png\" width=\"720\"\u003e\n\n1. First of all (except previous step), you need to download road graph (OSM is most popular format, we guess). Notice: you must change bbox for your region (in this example we using central district of Moscow).\n    ```shell\n    wget 'https://overpass-api.de/api/map?bbox=37.5453,55.7237,37.7252,55.7837' -O map.osm\n    # or curl 'https://overpass-api.de/api/map?bbox=37.5453,55.7237,37.7252,55.7837' --output map.osm\n    ```\n    \u003cimg src=\"images/inst4.png\" width=\"720\"\u003e\n\n2. Compress *.osm file via [osmconvert](https://wiki.openstreetmap.org/wiki/Osmconvert).\n    ```shell\n    osmconvert map.osm --out-pbf -o=map.osm.pbf\n    # If you want skip authors/versions\n    # osmconvert map.osm --drop-author --drop-version --out-pbf -o=map.osm.pbf\n    ```\n    \u003cimg src=\"images/inst5.png\" width=\"720\"\u003e\n\n3. Convert *.osm.pbf to CSV via [osm2ch](https://github.com/LdDl/osm2ch#osm2ch).\n\n    Notice:\n    * osm2ch's default output geometry format is WKT and units is 'km' (kilometers). We are going to change units to meters. We are going to extract only edges adapted for cars also.\n    * Don't forget to prepare contraction hierarchies via flag 'contract=true'\n\n    ```shell\n    osm2ch --file map.osm.pbf --out map.csv --geomf wkt --units m --tags motorway,primary,primary_link,road,secondary,secondary_link,residential,tertiary,tertiary_link,unclassified,trunk,trunk_link --contract=true\n    ```\n    \u003cimg src=\"images/inst6.png\" width=\"720\"\u003e\n\n4. After step above there must be 3 files:\n    * map.csv - Information about edges and its geometries\n    * map_vertices.csv - Information about vertices and its geometries\n    * map_shortcuts.csv - Information about shortcuts which are obtained by contraction process\n\n5. Start **horizon** server. Provide bind address, port, filename for edges file, σ and β parameters, initial longitude/latitude (in example Moscow coordinates are provided) and zoom for web page of your needs. \n    ```shell\n    horizon -h 0.0.0.0 -p 32800 -f map.csv -sigma 50.0 -beta 30.0 -maplon 37.60011784074581 -maplat 55.74694688386492 -mapzoom 17.0\n    ```\n    \u003cimg src=\"images/inst7.png\" width=\"720\"\u003e\n\n    5.1. If you need to enable gRPC API use flags `grpc`, `gh`, `gp` and optionally `gr`, e.g.:\n\n    ```shell\n    horizon -h 0.0.0.0 -p 32800 -f map.csv -sigma 50.0 -beta 30.0 -maplon 37.60011784074581 -maplat 55.74694688386492 -mapzoom 17.0 -grpc=true -gh 0.0.0.0 -gp 32801 -gr=true\n    ```\n    \n    \u003cimg src=\"images/inst7-grpc.png\" width=\"720\"\u003e\n\n6. Check if server works fine via POST-request (we are using [cURL](https://curl.haxx.se)). Notice: order of provided GPS-points matters.\n    \n    * Map matching:\n        ```shell\n        curl 'http://localhost:32800/api/v0.1.0/mapmatch' \\\n            -X POST \\\n            -H 'Accept: application/json' \\\n            -H 'Content-Type: application/json' \\\n            --data-raw '{\"max_states\":5,\"gps\":[{\"tm\":\"2024-11-30T00:00:00\",\"lon_lat\":[37.601249363208915,55.745374309126895]},{\"tm\":\"2024-11-30T00:00:02\",\"lon_lat\":[37.600552781226014,55.7462238201015]},{\"tm\":\"2024-11-30T00:00:04\",\"lon_lat\":[37.59995939657391,55.747450858855984]},{\"tm\":\"2024-11-30T00:00:06\",\"lon_lat\":[37.60052698189332,55.7480171714195]},{\"tm\":\"2024-11-30T00:00:08\",\"lon_lat\":[37.600655978556816,55.748728680680564]},{\"tm\":\"2024-11-30T00:00:10\",\"lon_lat\":[37.600372185897115,55.74945469716283]},{\"tm\":\"2024-11-30T00:00:12\",\"lon_lat\":[37.600694677555865,55.75052191686339]},{\"tm\":\"2024-11-30T00:00:14\",\"lon_lat\":[37.600965570549214,55.751371315759044]},{\"tm\":\"2024-11-30T00:00:16\",\"lon_lat\":[37.600926871550165,55.752634490168425]},{\"tm\":\"2024-11-30T00:00:18\",\"lon_lat\":[37.60038508556347,55.75559625596534]}]}' ; echo\n        ```\n\n        _Note: You can specify `state_radius` field to limit the search area (value should be in meters, float), but this is at your own risk — it may cause matching to fail if no candidates are found within the radius._\n\n        \u003cimg src=\"images/inst8.png\" width=\"720\"\u003e\n\n        Or with gRPC enabled on server-side you call gRPC API via any gRPC client, e.g. [grpcurl](https://github.com/fullstorydev/grpcurl) tool (make sure you've enabled reflection for it):\n        ```shell\n        grpcurl -plaintext -emit-defaults -d '{\n        \"max_states\": 5,\n        \"gps\": [\n            {\"tm\": \"2024-11-30T00:00:00\", \"lon\": 37.601249363208915, \"lat\": 55.745374309126895},\n            {\"tm\": \"2024-11-30T00:00:02\", \"lon\": 37.600552781226014, \"lat\": 55.7462238201015},\n            {\"tm\": \"2024-11-30T00:00:04\", \"lon\": 37.59995939657391, \"lat\": 55.747450858855984},\n            {\"tm\": \"2024-11-30T00:00:06\", \"lon\": 37.60052698189332, \"lat\": 55.7480171714195},\n            {\"tm\": \"2024-11-30T00:00:08\", \"lon\": 37.600655978556816, \"lat\": 55.748728680680564},\n            {\"tm\": \"2024-11-30T00:00:10\", \"lon\": 37.600372185897115, \"lat\": 55.74945469716283},\n            {\"tm\": \"2024-11-30T00:00:12\", \"lon\": 37.600694677555865, \"lat\": 55.75052191686339},\n            {\"tm\": \"2024-11-30T00:00:14\", \"lon\": 37.600965570549214, \"lat\": 55.751371315759044},\n            {\"tm\": \"2024-11-30T00:00:16\", \"lon\": 37.600926871550165, \"lat\": 55.752634490168425},\n            {\"tm\": \"2024-11-30T00:00:18\", \"lon\": 37.60038508556347, \"lat\": 55.75559625596534}\n        ]\n        }' localhost:32801 horizon.Service/RunMapMatch | tr -d '\\n\\t ' ; echo\n        ```\n        \u003cimg src=\"images/inst8-grpc.png\" width=\"720\"\u003e\n\n    * For shortest path finding:\n        ```shell\n        curl 'http://localhost:32800/api/v0.1.0/shortest' \\\n            -X POST \\\n            -H 'accept: application/json' \\\n            -H  'Content-Type: application/json' \\\n            --data-raw '{\"gps\":[{\"lon_lat\":[37.601249363208915,55.745374309126895]},{\"lon_lat\":[37.600926871550165,55.752634490168425]}]}' ; echo\n        ```\n\n        _Note: You can optionally specify `state_radius` to limit the search area (in meters, float), but this is at your own risk — it may cause routing to fail if no candidates are found within the radius._\n\n        \u003cimg src=\"images/inst9.png\" width=\"720\"\u003e\n\n        Or with gRPC enabled on server-side you call gRPC API via any gRPC client, e.g. [grpcurl](https://github.com/fullstorydev/grpcurl) tool (make sure you've enabled reflection for it):\n        ```shell\n        grpcurl -plaintext -emit-defaults -d '{\n        \"gps\": [\n            {\"lon\": 37.601249363208915, \"lat\": 55.745374309126895},\n            {\"lon\": 37.600926871550165, \"lat\": 55.752634490168425}\n        ]\n        }' localhost:32801 horizon.Service/GetSP | tr -d '\\n\\t ' ; echo\n        ```\n        \u003cimg src=\"images/inst9-grpc.png\" width=\"720\"\u003e\n\n    * For isochrones estimation (_note: maxCost =\u003e it represents meters in current example_):\n        ```shell\n        curl 'http://localhost:32800/api/v0.1.0/isochrones' \\\n            -X POST \\\n            -H 'accept: application/json' \\\n            -H  'Content-Type: application/json' \\\n            --data-raw '{\"max_cost\":2100.0,\"lon_lat\":[37.601249363208915,55.745374309126895]}' ; echo\n        ```\n\n        _Note: You can optionally point field `nearest_radius` to limit the search area (in meters, float), but this is at your own risk — it may cause the request to fail if no vertex is found within the radius._\n\n        \u003cimg src=\"images/inst10.png\" width=\"720\"\u003e\n\n        Or with gRPC enabled on server-side you call gRPC API via any gRPC client, e.g. [grpcurl](https://github.com/fullstorydev/grpcurl) tool (make sure you've enabled reflection for it):\n        ```shell\n        grpcurl -plaintext -emit-defaults -d '{\n        \"max_cost\": 2100.0,\n        \"lon\": 37.601249363208915,\n        \"lat\": 55.745374309126895\n        }' localhost:32801 horizon.Service/GetIsochrones | tr -d '\\n\\t ' ; echo\n        ```\n        \u003cimg src=\"images/inst10-grpc.png\" width=\"720\"\u003e\n\n7. Open Front-end on link http://localhost:32800/\n\n    \u003cimg src=\"images/maplibre1.png\" width=\"720\"\u003e\n    \u003cimg src=\"images/maplibre2.png\" width=\"720\"\u003e\n\n    - Blue - observation point (measurement)\n    - Purple - represents matched edge\n    - Yellow - represents projection of the point onto the matched edge\n    - Green - represents picked either source or target vertex of the matched edge\n    - Red - represents cuts of excessed geometries (for first and last matched edges)\n    - Dark Blue - represents intermediate edges (i.e. there are some edges between two matched edges)\n    \n\n8. There is also [Swagger](https://en.wikipedia.org/wiki/Swagger_(software)) documentation for inialized REST API.\n\n    If you use http://localhost:32800/ then you can navigate to http://localhost:32800/api/v0.1.0/docs/index.html#overview for API documentation. It may look like (thanks [rapidoc](https://github.com/mrin9/RapiDoc#rapidoc)):\n    \n    \u003cimg src=\"images/swagger1.png\" width=\"720\"\u003e\n\n    If gRPC is enabled you can navigate to http://localhost:32800/api/v0.1.0/grpc/docs/index.html to see gRPC documentation:\n    \u003cimg src=\"images/grpc-doc-1.png\" width=\"720\"\u003e\n\n9. Each observation in either gRPC or HTTP API response has matcher code assigned. It helps to understand what was the result of matching for each observation. Here is a table of those:\n\n    | Code | Constant | Description |\n    |------|----------|-------------|\n    | 900 | CODE_OK | Successfully matched |\n    | 901 | CODE_NO_CANDIDATES | No candidates found for observation |\n    | 902 | CODE_ALONE_OBSERVATION | Interrupted segment (no route to previous/next observation), i.e. orphan observation |\n\n### Docker\nIf you don't want use binary or can't build it you can use public Docker image:\n```bash\ndocker pull dimahkiin/horizon:latest\n# Make sure you set up volume correctly. Is this example I've just used the current user directory\ndocker run -p 32800:32800 -p 32801:32801 \\\n  -v $(pwd):/app/data \\\n  dimahkiin/horizon \\\n  -h 0.0.0.0 -p 32800 -f /app/data/map.csv \\\n  -sigma 50.0 -beta 30.0 \\\n  -maplon 37.60011784074581 -maplat 55.7469688386492 -mapzoom 17.0 \\\n  -grpc=true -gh 0.0.0.0 -gp 32801 -gr=true\n```\n\n## Benchmark\nPlease follow [link](BENCHMARK.md)\n\n## Support\nIf you have troubles or questions please [open an issue](https://github.com/LdDl/ch/issues/new).\nFeel free to make PR's (we do not have contributing guidelines currently, but we will someday)\n\n## ToDo\nPlease see [ROADMAP.md](ROADMAP.md)\n\n## Theory\nThanks for approach described in this paper:\n**Newson, Paul, and John Krumm. \"Hidden Markov map matching through noise and sparseness.\" Proceedings of the 17th ACM SIGSPATIAL International Conference on Advances in Geographic Information Systems. ACM, 2009**\n\n[Hidden Markov model](https://en.wikipedia.org/wiki/Hidden_Markov_model) is used as backbone for preparing probabities for Viterbi algorithm. Notice that we do not use 'classical' [Normal distribution](https://en.wikipedia.org/wiki/Normal_distribution) for evaluating emission probabilty or [Exponential distribution](https://en.wikipedia.org/wiki/Exponential_distribution) for evaluatuin transition probabilties in HMM. Instead of it we use **Log-normal distribution** for emissions and **Log-exponential distribution** for transitions. Why is that? Because we do not want to get underflow (arithmetic) for small probabilities\n\n[Viterbi algorithm](https://en.wikipedia.org/wiki/Viterbi_algorithm) is used to evaluate the most suitable trace of GPS track.\n\n## Dependencies\n* Contraction hierarchies library with bidirectional Dijkstra's algorithm - [ch](https://github.com/LdDl/ch#ch---contraction-hierarchies). License is Apache-2.0\n* Viterbi's algorithm implementation - [viterbi](https://github.com/LdDl/viterbi#viterbi). License is Apache-2.0\n* S2 (spherical geometry) library - [s2](https://github.com/golang/geo#overview). License is Apache-2.0\n* Btree implementation - [btree](https://github.com/google/btree#btree-implementation-for-go). License is Apache-2.0\n* GeoJSON stuff - [go.geojson](https://github.com/paulmach/go.geojson#gogeojson). License is MIT\n* Fiber framework (used for server app) - [Fiber](https://github.com/gofiber/fiber). License is MIT\n* ~~MapboxGL for Front-end - [mapboxgl](https://github.com/mapbox/mapbox-gl-js). License is 3-Clause BSD license~~\nReplaced with Maplibre due Mapbox [changed license](https://github.com/mapbox/mapbox-gl-js/releases/tag/v2.0.0). License is modified 3-Clause BSD license, please see [ref. link](https://github.com/maplibre/maplibre-gl-js/blob/main/LICENSE.txt)\n* moments.js for Front-end - [moment.js](https://github.com/moment/moment/). License is MIT\n* rapidoc for [swagger](https://en.wikipedia.org/wiki/Swagger_(software)) visualization - [rapidoc](https://github.com/mrin9/RapiDoc/blob/master/LICENSE.txt). License is MIT\n\n## License\nYou can check it [here](https://github.com/LdDl/horizon/blob/master/LICENSE)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flddl%2Fhorizon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flddl%2Fhorizon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flddl%2Fhorizon/lists"}