This repository was archived by the owner on Sep 16, 2025. It is now read-only.

Repository files navigation

Intro

Clusterbuster is a tile server that produces map tiles (in MVT format) from a table with a PostGIS geometry column. It uses this table to first filter the row (based on a query you provide) and the resulting points are then clustered dynamically. The combination of clusters and points is transformed into a vector tile. The MVT tiles are much smaller than the original data, moving all the heavy lifting from the front-end to the tile server. This allows the display of large data sets, that change regularly, on maps in resource constrained environments, such as mobile devices and embedded systems. Clusterbuster has a built-in and configurable in-memory LRU-cache for the resulting tiles, with which it can serve many concurrent users.

Getting started

Clusterbuster is designed to be used in a NodeJS server connected to a PostgreSQL database, with PostGIS extensions installed. You have to bring your own web framework, allowing clusterbuster to be easily integrated in any existing API that uses express.js, Koa, Hapi, etc..

const{ TileServer }=require('clusterbuster');TileServer({// types/TileServerConfig.ts
maxZoomLevel,attributes: ['status','speed'],filtersToWhere: filters=>{// You are responsible for protecting against SQL injection in this function. Because there are many ways to filter, it depends on the filter type on how to approach this.// For example a number can be safely used by passing it through parseFloat, strings are best treated by checking for a set of allowed valuesconstwhereStatements=[];// The below statement checks that filters.status is one of 'free' or 'busy' to prevent potential SQL injectionif(filters.status&&['busy','free'].includes(filters.status)){whereStatements.push(`status = '${filters.status}'`);}if(filters.speed&&['slow','fast'].includes(filters.speed)){whereStatements.push(`speed = '${filters.speed}'`);}returnwhereStatements;},}).then(asynctileServer=>{consttile=awaittileServer({// types/TileRequest.tsz: 1,x: 0,y: 1,table: 'public.my_points_table',geometry: 'my_geometry_column'extent: 4096,bufferSize: 256,});// send the tile in binary MVT format to the front-end});

See the express.js example for a fully functioning server that exposes the above tile server on a REST endpoint. You can see the TileServerConfig for the initial configuration options, to configure the cache, connection pool, filters, etc..

The TileRequest defines the per tile request options.

The above example assumes a postgres database with a public.my_points_table table matching:

CREATE EXTENSION postgis;
-- Table: public.my_points_tableCREATETABLEpublic.my_points_table
(
id TEXT,
my_geometry_column geometry(Point,4326),
speed TEXT,
status TEXT,
PRIMARY KEY(id)
)
TABLESPACE pg_default;
GRANT ALL ON TABLE public.my_points_table TO "tiler";

The internal postgress client can be configured with the following env vars:

PGUSER=tilerPGHOST=localhostPGPASSWORD=PGDATABASE=pointsPGPORT=5432

Filtering

The filtersToWhere function can be used to implement custom filtering logic. It should return an array of SQL snippets, which clusterbuster transforms into the WHERE clause using AND between each statement.

The resulting SQL query looks something like this:

SELECT ...
WHERE whereStatement1 AND whereStatement2

Caching

The default cache is a in-memory LRU cache local to the tile server. This cache is ideal for a single instance tile server. If you are going to use clusterbuster in a loadbalanced multi instance server environment, the cache can be a redis instance or cluster instead, allowing all tile server instances to share the same tile cache.

You need to install the peer dependency npm i ioredis and configure the tile server to use redis instead of LRU. See the TileCacheOptions for all the configuration options.

{ "cacheOptions": { "type": "redis" } }

Internals

The tile server creates clusters using the PostGIS ST_ClusterDBSCAN starting at the maximum zoomlevel and continues clustering iteratively for each zoom level until the zoom level of the tile request is reached. This 'cluster of clusters' clustering algorithm is inspired by the excellent supercluster library, which many people use to cluster on the front-end and even on the back-end (using something like supertiler).

PostGIS is used for all the work such as selecting points in the region of the tile and even for creating the binary tiles themselves, using ST_asMVT. The NodeJS server only creates the SQL statements which then create the tiles in the database. These tiles are then gzipped and stored in an in-memory LRU cache inside the NodeJS process.

The main performance bottleneck for clusterbuster is the PostgreSQL server as the clustering algorithm requires a lot of CPU cycles, especially at low (1-5) zoom levels. Because the lower zoom levels contain fewer total tiles to cover the whole map (1 tile for the whold world at zoom level 0),the LRU cache effectively shields the database from becoming overloaded when many users are using the map concurrently. The number of different filters reduces the effectiveness of the LRU cache, as each filter combination creates a unique set of cache keys for all tiles. Typically the 'default' filter settings will provide enough caching to keep the database from becoming overloaded by many concurrent users, but mileage may vary per use case.

Alternative tile servers (which inspired clusterbuster)

  • Martin - A Rust based tile server, which has the option to provide PL/pgSQL function instead of a table, allows for filtering of data before tiling.
  • Tilestrata (and especially tilestrata-postgis-mvt) - Tilestrata is a tile server framework with an elaborate plugin architecture. The tilestrata-postgis-mvt plugin uses the same ST_asMvt functions provided by PostGIS as ClusterBuster does.
  • Tegola - A Go based tile server

Alternative static tile generators

All of these tile servers and tile generators offer some subset of the functionality we required, but lacked atleast one, which is our motivation for making clusterbuster.

Tilerdynamic datafilteringbackend clustering
Clusterbuster
Martinx
Tilestrataxx
Tegolaxx
Tippecanoexx
Supertilerxx

When not to use clusterbuster

  • Your data is static (and no filtering is required) - use tippecanoe or supertiler to prerender all tiles and serve from a file hosting service such as S3 (which is much more economical and loads faster to the front-end)
  • Your data needs to be filtered, but not clustered: Using Martin you can implement filtering using PL/pgSQL
  • You don't need filtering or clustering, you just need tiles from a dynamic data set: You can use any of the dynamic tile servers in the table above.

Sponsors

Chargetrip logo

About

A Mapbox Vector Tile (MVT) map tiling server with built-in clustering and filtering

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
This repository was archived by the owner on Sep 16, 2025. It is now read-only.

Repository files navigation

Intro

Clusterbuster is a tile server that produces map tiles (in MVT format) from a table with a PostGIS geometry column. It uses this table to first filter the row (based on a query you provide) and the resulting points are then clustered dynamically. The combination of clusters and points is transformed into a vector tile. The MVT tiles are much smaller than the original data, moving all the heavy lifting from the front-end to the tile server. This allows the display of large data sets, that change regularly, on maps in resource constrained environments, such as mobile devices and embedded systems. Clusterbuster has a built-in and configurable in-memory LRU-cache for the resulting tiles, with which it can serve many concurrent users.

Getting started

Clusterbuster is designed to be used in a NodeJS server connected to a PostgreSQL database, with PostGIS extensions installed. You have to bring your own web framework, allowing clusterbuster to be easily integrated in any existing API that uses express.js, Koa, Hapi, etc..

const{ TileServer }=require('clusterbuster');TileServer({// types/TileServerConfig.ts
maxZoomLevel,attributes: ['status','speed'],filtersToWhere: filters=>{// You are responsible for protecting against SQL injection in this function. Because there are many ways to filter, it depends on the filter type on how to approach this.// For example a number can be safely used by passing it through parseFloat, strings are best treated by checking for a set of allowed valuesconstwhereStatements=[];// The below statement checks that filters.status is one of 'free' or 'busy' to prevent potential SQL injectionif(filters.status&&['busy','free'].includes(filters.status)){whereStatements.push(`status = '${filters.status}'`);}if(filters.speed&&['slow','fast'].includes(filters.speed)){whereStatements.push(`speed = '${filters.speed}'`);}returnwhereStatements;},}).then(asynctileServer=>{consttile=awaittileServer({// types/TileRequest.tsz: 1,x: 0,y: 1,table: 'public.my_points_table',geometry: 'my_geometry_column'extent: 4096,bufferSize: 256,});// send the tile in binary MVT format to the front-end});

See the express.js example for a fully functioning server that exposes the above tile server on a REST endpoint. You can see the TileServerConfig for the initial configuration options, to configure the cache, connection pool, filters, etc..

The TileRequest defines the per tile request options.

The above example assumes a postgres database with a public.my_points_table table matching:

CREATE EXTENSION postgis;
-- Table: public.my_points_tableCREATETABLEpublic.my_points_table
(
id TEXT,
my_geometry_column geometry(Point,4326),
speed TEXT,
status TEXT,
PRIMARY KEY(id)
)
TABLESPACE pg_default;
GRANT ALL ON TABLE public.my_points_table TO "tiler";

The internal postgress client can be configured with the following env vars:

PGUSER=tilerPGHOST=localhostPGPASSWORD=PGDATABASE=pointsPGPORT=5432

Filtering

The filtersToWhere function can be used to implement custom filtering logic. It should return an array of SQL snippets, which clusterbuster transforms into the WHERE clause using AND between each statement.

The resulting SQL query looks something like this:

SELECT ...
WHERE whereStatement1 AND whereStatement2

Caching

The default cache is a in-memory LRU cache local to the tile server. This cache is ideal for a single instance tile server. If you are going to use clusterbuster in a loadbalanced multi instance server environment, the cache can be a redis instance or cluster instead, allowing all tile server instances to share the same tile cache.

You need to install the peer dependency npm i ioredis and configure the tile server to use redis instead of LRU. See the TileCacheOptions for all the configuration options.

{ "cacheOptions": { "type": "redis" } }

Internals

The tile server creates clusters using the PostGIS ST_ClusterDBSCAN starting at the maximum zoomlevel and continues clustering iteratively for each zoom level until the zoom level of the tile request is reached. This 'cluster of clusters' clustering algorithm is inspired by the excellent supercluster library, which many people use to cluster on the front-end and even on the back-end (using something like supertiler).

PostGIS is used for all the work such as selecting points in the region of the tile and even for creating the binary tiles themselves, using ST_asMVT. The NodeJS server only creates the SQL statements which then create the tiles in the database. These tiles are then gzipped and stored in an in-memory LRU cache inside the NodeJS process.

The main performance bottleneck for clusterbuster is the PostgreSQL server as the clustering algorithm requires a lot of CPU cycles, especially at low (1-5) zoom levels. Because the lower zoom levels contain fewer total tiles to cover the whole map (1 tile for the whold world at zoom level 0),the LRU cache effectively shields the database from becoming overloaded when many users are using the map concurrently. The number of different filters reduces the effectiveness of the LRU cache, as each filter combination creates a unique set of cache keys for all tiles. Typically the 'default' filter settings will provide enough caching to keep the database from becoming overloaded by many concurrent users, but mileage may vary per use case.

Alternative tile servers (which inspired clusterbuster)

  • Martin - A Rust based tile server, which has the option to provide PL/pgSQL function instead of a table, allows for filtering of data before tiling.
  • Tilestrata (and especially tilestrata-postgis-mvt) - Tilestrata is a tile server framework with an elaborate plugin architecture. The tilestrata-postgis-mvt plugin uses the same ST_asMvt functions provided by PostGIS as ClusterBuster does.
  • Tegola - A Go based tile server

Alternative static tile generators

All of these tile servers and tile generators offer some subset of the functionality we required, but lacked atleast one, which is our motivation for making clusterbuster.

Tilerdynamic datafilteringbackend clustering
Clusterbuster
Martinx
Tilestrataxx
Tegolaxx
Tippecanoexx
Supertilerxx

When not to use clusterbuster

  • Your data is static (and no filtering is required) - use tippecanoe or supertiler to prerender all tiles and serve from a file hosting service such as S3 (which is much more economical and loads faster to the front-end)
  • Your data needs to be filtered, but not clustered: Using Martin you can implement filtering using PL/pgSQL
  • You don't need filtering or clustering, you just need tiles from a dynamic data set: You can use any of the dynamic tile servers in the table above.

Sponsors

Chargetrip logo

About

A Mapbox Vector Tile (MVT) map tiling server with built-in clustering and filtering

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Sep 16, 2025. It is now read-only.

Repository files navigation

Intro

Clusterbuster is a tile server that produces map tiles (in MVT format) from a table with a PostGIS geometry column. It uses this table to first filter the row (based on a query you provide) and the resulting points are then clustered dynamically. The combination of clusters and points is transformed into a vector tile. The MVT tiles are much smaller than the original data, moving all the heavy lifting from the front-end to the tile server. This allows the display of large data sets, that change regularly, on maps in resource constrained environments, such as mobile devices and embedded systems. Clusterbuster has a built-in and configurable in-memory LRU-cache for the resulting tiles, with which it can serve many concurrent users.

Getting started

Clusterbuster is designed to be used in a NodeJS server connected to a PostgreSQL database, with PostGIS extensions installed. You have to bring your own web framework, allowing clusterbuster to be easily integrated in any existing API that uses express.js, Koa, Hapi, etc..

const{ TileServer }=require('clusterbuster');TileServer({// types/TileServerConfig.ts
maxZoomLevel,attributes: ['status','speed'],filtersToWhere: filters=>{// You are responsible for protecting against SQL injection in this function. Because there are many ways to filter, it depends on the filter type on how to approach this.// For example a number can be safely used by passing it through parseFloat, strings are best treated by checking for a set of allowed valuesconstwhereStatements=[];// The below statement checks that filters.status is one of 'free' or 'busy' to prevent potential SQL injectionif(filters.status&&['busy','free'].includes(filters.status)){whereStatements.push(`status = '${filters.status}'`);}if(filters.speed&&['slow','fast'].includes(filters.speed)){whereStatements.push(`speed = '${filters.speed}'`);}returnwhereStatements;},}).then(asynctileServer=>{consttile=awaittileServer({// types/TileRequest.tsz: 1,x: 0,y: 1,table: 'public.my_points_table',geometry: 'my_geometry_column'extent: 4096,bufferSize: 256,});// send the tile in binary MVT format to the front-end});

See the express.js example for a fully functioning server that exposes the above tile server on a REST endpoint. You can see the TileServerConfig for the initial configuration options, to configure the cache, connection pool, filters, etc..

The TileRequest defines the per tile request options.

The above example assumes a postgres database with a public.my_points_table table matching:

CREATE EXTENSION postgis;
-- Table: public.my_points_tableCREATETABLEpublic.my_points_table
(
id TEXT,
my_geometry_column geometry(Point,4326),
speed TEXT,
status TEXT,
PRIMARY KEY(id)
)
TABLESPACE pg_default;
GRANT ALL ON TABLE public.my_points_table TO "tiler";

The internal postgress client can be configured with the following env vars:

PGUSER=tilerPGHOST=localhostPGPASSWORD=PGDATABASE=pointsPGPORT=5432

Filtering

The filtersToWhere function can be used to implement custom filtering logic. It should return an array of SQL snippets, which clusterbuster transforms into the WHERE clause using AND between each statement.

The resulting SQL query looks something like this:

SELECT ...
WHERE whereStatement1 AND whereStatement2

Caching

The default cache is a in-memory LRU cache local to the tile server. This cache is ideal for a single instance tile server. If you are going to use clusterbuster in a loadbalanced multi instance server environment, the cache can be a redis instance or cluster instead, allowing all tile server instances to share the same tile cache.

You need to install the peer dependency npm i ioredis and configure the tile server to use redis instead of LRU. See the TileCacheOptions for all the configuration options.

{ "cacheOptions": { "type": "redis" } }

Internals

The tile server creates clusters using the PostGIS ST_ClusterDBSCAN starting at the maximum zoomlevel and continues clustering iteratively for each zoom level until the zoom level of the tile request is reached. This 'cluster of clusters' clustering algorithm is inspired by the excellent supercluster library, which many people use to cluster on the front-end and even on the back-end (using something like supertiler).

PostGIS is used for all the work such as selecting points in the region of the tile and even for creating the binary tiles themselves, using ST_asMVT. The NodeJS server only creates the SQL statements which then create the tiles in the database. These tiles are then gzipped and stored in an in-memory LRU cache inside the NodeJS process.

The main performance bottleneck for clusterbuster is the PostgreSQL server as the clustering algorithm requires a lot of CPU cycles, especially at low (1-5) zoom levels. Because the lower zoom levels contain fewer total tiles to cover the whole map (1 tile for the whold world at zoom level 0),the LRU cache effectively shields the database from becoming overloaded when many users are using the map concurrently. The number of different filters reduces the effectiveness of the LRU cache, as each filter combination creates a unique set of cache keys for all tiles. Typically the 'default' filter settings will provide enough caching to keep the database from becoming overloaded by many concurrent users, but mileage may vary per use case.

Alternative tile servers (which inspired clusterbuster)

  • Martin - A Rust based tile server, which has the option to provide PL/pgSQL function instead of a table, allows for filtering of data before tiling.
  • Tilestrata (and especially tilestrata-postgis-mvt) - Tilestrata is a tile server framework with an elaborate plugin architecture. The tilestrata-postgis-mvt plugin uses the same ST_asMvt functions provided by PostGIS as ClusterBuster does.
  • Tegola - A Go based tile server

Alternative static tile generators

All of these tile servers and tile generators offer some subset of the functionality we required, but lacked atleast one, which is our motivation for making clusterbuster.

Tilerdynamic datafilteringbackend clustering
Clusterbuster
Martinx
Tilestrataxx
Tegolaxx
Tippecanoexx
Supertilerxx

When not to use clusterbuster

  • Your data is static (and no filtering is required) - use tippecanoe or supertiler to prerender all tiles and serve from a file hosting service such as S3 (which is much more economical and loads faster to the front-end)
  • Your data needs to be filtered, but not clustered: Using Martin you can implement filtering using PL/pgSQL
  • You don't need filtering or clustering, you just need tiles from a dynamic data set: You can use any of the dynamic tile servers in the table above.

Sponsors

Chargetrip logo

About

A Mapbox Vector Tile (MVT) map tiling server with built-in clustering and filtering

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Sep 16, 2025. It is now read-only.

Repository files navigation

Intro

Clusterbuster is a tile server that produces map tiles (in MVT format) from a table with a PostGIS geometry column. It uses this table to first filter the row (based on a query you provide) and the resulting points are then clustered dynamically. The combination of clusters and points is transformed into a vector tile. The MVT tiles are much smaller than the original data, moving all the heavy lifting from the front-end to the tile server. This allows the display of large data sets, that change regularly, on maps in resource constrained environments, such as mobile devices and embedded systems. Clusterbuster has a built-in and configurable in-memory LRU-cache for the resulting tiles, with which it can serve many concurrent users.

Getting started

Clusterbuster is designed to be used in a NodeJS server connected to a PostgreSQL database, with PostGIS extensions installed. You have to bring your own web framework, allowing clusterbuster to be easily integrated in any existing API that uses express.js, Koa, Hapi, etc..

const{ TileServer }=require('clusterbuster');TileServer({// types/TileServerConfig.ts
maxZoomLevel,attributes: ['status','speed'],filtersToWhere: filters=>{// You are responsible for protecting against SQL injection in this function. Because there are many ways to filter, it depends on the filter type on how to approach this.// For example a number can be safely used by passing it through parseFloat, strings are best treated by checking for a set of allowed valuesconstwhereStatements=[];// The below statement checks that filters.status is one of 'free' or 'busy' to prevent potential SQL injectionif(filters.status&&['busy','free'].includes(filters.status)){whereStatements.push(`status = '${filters.status}'`);}if(filters.speed&&['slow','fast'].includes(filters.speed)){whereStatements.push(`speed = '${filters.speed}'`);}returnwhereStatements;},}).then(asynctileServer=>{consttile=awaittileServer({// types/TileRequest.tsz: 1,x: 0,y: 1,table: 'public.my_points_table',geometry: 'my_geometry_column'extent: 4096,bufferSize: 256,});// send the tile in binary MVT format to the front-end});

See the express.js example for a fully functioning server that exposes the above tile server on a REST endpoint. You can see the TileServerConfig for the initial configuration options, to configure the cache, connection pool, filters, etc..

The TileRequest defines the per tile request options.

The above example assumes a postgres database with a public.my_points_table table matching:

CREATE EXTENSION postgis;
-- Table: public.my_points_tableCREATETABLEpublic.my_points_table
(
id TEXT,
my_geometry_column geometry(Point,4326),
speed TEXT,
status TEXT,
PRIMARY KEY(id)
)
TABLESPACE pg_default;
GRANT ALL ON TABLE public.my_points_table TO "tiler";

The internal postgress client can be configured with the following env vars:

PGUSER=tilerPGHOST=localhostPGPASSWORD=PGDATABASE=pointsPGPORT=5432

Filtering

The filtersToWhere function can be used to implement custom filtering logic. It should return an array of SQL snippets, which clusterbuster transforms into the WHERE clause using AND between each statement.

The resulting SQL query looks something like this:

SELECT ...
WHERE whereStatement1 AND whereStatement2

Caching

The default cache is a in-memory LRU cache local to the tile server. This cache is ideal for a single instance tile server. If you are going to use clusterbuster in a loadbalanced multi instance server environment, the cache can be a redis instance or cluster instead, allowing all tile server instances to share the same tile cache.

You need to install the peer dependency npm i ioredis and configure the tile server to use redis instead of LRU. See the TileCacheOptions for all the configuration options.

{ "cacheOptions": { "type": "redis" } }

Internals

The tile server creates clusters using the PostGIS ST_ClusterDBSCAN starting at the maximum zoomlevel and continues clustering iteratively for each zoom level until the zoom level of the tile request is reached. This 'cluster of clusters' clustering algorithm is inspired by the excellent supercluster library, which many people use to cluster on the front-end and even on the back-end (using something like supertiler).

PostGIS is used for all the work such as selecting points in the region of the tile and even for creating the binary tiles themselves, using ST_asMVT. The NodeJS server only creates the SQL statements which then create the tiles in the database. These tiles are then gzipped and stored in an in-memory LRU cache inside the NodeJS process.

The main performance bottleneck for clusterbuster is the PostgreSQL server as the clustering algorithm requires a lot of CPU cycles, especially at low (1-5) zoom levels. Because the lower zoom levels contain fewer total tiles to cover the whole map (1 tile for the whold world at zoom level 0),the LRU cache effectively shields the database from becoming overloaded when many users are using the map concurrently. The number of different filters reduces the effectiveness of the LRU cache, as each filter combination creates a unique set of cache keys for all tiles. Typically the 'default' filter settings will provide enough caching to keep the database from becoming overloaded by many concurrent users, but mileage may vary per use case.

Alternative tile servers (which inspired clusterbuster)

  • Martin - A Rust based tile server, which has the option to provide PL/pgSQL function instead of a table, allows for filtering of data before tiling.
  • Tilestrata (and especially tilestrata-postgis-mvt) - Tilestrata is a tile server framework with an elaborate plugin architecture. The tilestrata-postgis-mvt plugin uses the same ST_asMvt functions provided by PostGIS as ClusterBuster does.
  • Tegola - A Go based tile server

Alternative static tile generators

All of these tile servers and tile generators offer some subset of the functionality we required, but lacked atleast one, which is our motivation for making clusterbuster.

Tilerdynamic datafilteringbackend clustering
Clusterbuster
Martinx
Tilestrataxx
Tegolaxx
Tippecanoexx
Supertilerxx

When not to use clusterbuster

  • Your data is static (and no filtering is required) - use tippecanoe or supertiler to prerender all tiles and serve from a file hosting service such as S3 (which is much more economical and loads faster to the front-end)
  • Your data needs to be filtered, but not clustered: Using Martin you can implement filtering using PL/pgSQL
  • You don't need filtering or clustering, you just need tiles from a dynamic data set: You can use any of the dynamic tile servers in the table above.

Sponsors

Chargetrip logo

About

A Mapbox Vector Tile (MVT) map tiling server with built-in clustering and filtering

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
This repository was archived by the owner on Sep 16, 2025. It is now read-only.

Repository files navigation

Intro

Clusterbuster is a tile server that produces map tiles (in MVT format) from a table with a PostGIS geometry column. It uses this table to first filter the row (based on a query you provide) and the resulting points are then clustered dynamically. The combination of clusters and points is transformed into a vector tile. The MVT tiles are much smaller than the original data, moving all the heavy lifting from the front-end to the tile server. This allows the display of large data sets, that change regularly, on maps in resource constrained environments, such as mobile devices and embedded systems. Clusterbuster has a built-in and configurable in-memory LRU-cache for the resulting tiles, with which it can serve many concurrent users.

Getting started

Clusterbuster is designed to be used in a NodeJS server connected to a PostgreSQL database, with PostGIS extensions installed. You have to bring your own web framework, allowing clusterbuster to be easily integrated in any existing API that uses express.js, Koa, Hapi, etc..

const{ TileServer }=require('clusterbuster');TileServer({// types/TileServerConfig.ts
maxZoomLevel,attributes: ['status','speed'],filtersToWhere: filters=>{// You are responsible for protecting against SQL injection in this function. Because there are many ways to filter, it depends on the filter type on how to approach this.// For example a number can be safely used by passing it through parseFloat, strings are best treated by checking for a set of allowed valuesconstwhereStatements=[];// The below statement checks that filters.status is one of 'free' or 'busy' to prevent potential SQL injectionif(filters.status&&['busy','free'].includes(filters.status)){whereStatements.push(`status = '${filters.status}'`);}if(filters.speed&&['slow','fast'].includes(filters.speed)){whereStatements.push(`speed = '${filters.speed}'`);}returnwhereStatements;},}).then(asynctileServer=>{consttile=awaittileServer({// types/TileRequest.tsz: 1,x: 0,y: 1,table: 'public.my_points_table',geometry: 'my_geometry_column'extent: 4096,bufferSize: 256,});// send the tile in binary MVT format to the front-end});

See the express.js example for a fully functioning server that exposes the above tile server on a REST endpoint. You can see the TileServerConfig for the initial configuration options, to configure the cache, connection pool, filters, etc..

The TileRequest defines the per tile request options.

The above example assumes a postgres database with a public.my_points_table table matching:

CREATE EXTENSION postgis;
-- Table: public.my_points_tableCREATETABLEpublic.my_points_table
(
id TEXT,
my_geometry_column geometry(Point,4326),
speed TEXT,
status TEXT,
PRIMARY KEY(id)
)
TABLESPACE pg_default;
GRANT ALL ON TABLE public.my_points_table TO "tiler";

The internal postgress client can be configured with the following env vars:

PGUSER=tilerPGHOST=localhostPGPASSWORD=PGDATABASE=pointsPGPORT=5432

Filtering

The filtersToWhere function can be used to implement custom filtering logic. It should return an array of SQL snippets, which clusterbuster transforms into the WHERE clause using AND between each statement.

The resulting SQL query looks something like this:

SELECT ...
WHERE whereStatement1 AND whereStatement2

Caching

The default cache is a in-memory LRU cache local to the tile server. This cache is ideal for a single instance tile server. If you are going to use clusterbuster in a loadbalanced multi instance server environment, the cache can be a redis instance or cluster instead, allowing all tile server instances to share the same tile cache.

You need to install the peer dependency npm i ioredis and configure the tile server to use redis instead of LRU. See the TileCacheOptions for all the configuration options.

{ "cacheOptions": { "type": "redis" } }

Internals

The tile server creates clusters using the PostGIS ST_ClusterDBSCAN starting at the maximum zoomlevel and continues clustering iteratively for each zoom level until the zoom level of the tile request is reached. This 'cluster of clusters' clustering algorithm is inspired by the excellent supercluster library, which many people use to cluster on the front-end and even on the back-end (using something like supertiler).

PostGIS is used for all the work such as selecting points in the region of the tile and even for creating the binary tiles themselves, using ST_asMVT. The NodeJS server only creates the SQL statements which then create the tiles in the database. These tiles are then gzipped and stored in an in-memory LRU cache inside the NodeJS process.

The main performance bottleneck for clusterbuster is the PostgreSQL server as the clustering algorithm requires a lot of CPU cycles, especially at low (1-5) zoom levels. Because the lower zoom levels contain fewer total tiles to cover the whole map (1 tile for the whold world at zoom level 0),the LRU cache effectively shields the database from becoming overloaded when many users are using the map concurrently. The number of different filters reduces the effectiveness of the LRU cache, as each filter combination creates a unique set of cache keys for all tiles. Typically the 'default' filter settings will provide enough caching to keep the database from becoming overloaded by many concurrent users, but mileage may vary per use case.

Alternative tile servers (which inspired clusterbuster)

  • Martin - A Rust based tile server, which has the option to provide PL/pgSQL function instead of a table, allows for filtering of data before tiling.
  • Tilestrata (and especially tilestrata-postgis-mvt) - Tilestrata is a tile server framework with an elaborate plugin architecture. The tilestrata-postgis-mvt plugin uses the same ST_asMvt functions provided by PostGIS as ClusterBuster does.
  • Tegola - A Go based tile server

Alternative static tile generators

All of these tile servers and tile generators offer some subset of the functionality we required, but lacked atleast one, which is our motivation for making clusterbuster.

Tilerdynamic datafilteringbackend clustering
Clusterbuster
Martinx
Tilestrataxx
Tegolaxx
Tippecanoexx
Supertilerxx

When not to use clusterbuster

  • Your data is static (and no filtering is required) - use tippecanoe or supertiler to prerender all tiles and serve from a file hosting service such as S3 (which is much more economical and loads faster to the front-end)
  • Your data needs to be filtered, but not clustered: Using Martin you can implement filtering using PL/pgSQL
  • You don't need filtering or clustering, you just need tiles from a dynamic data set: You can use any of the dynamic tile servers in the table above.

Sponsors

Chargetrip logo

About

A Mapbox Vector Tile (MVT) map tiling server with built-in clustering and filtering

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Sep 16, 2025. It is now read-only.

Repository files navigation

Intro

Clusterbuster is a tile server that produces map tiles (in MVT format) from a table with a PostGIS geometry column. It uses this table to first filter the row (based on a query you provide) and the resulting points are then clustered dynamically. The combination of clusters and points is transformed into a vector tile. The MVT tiles are much smaller than the original data, moving all the heavy lifting from the front-end to the tile server. This allows the display of large data sets, that change regularly, on maps in resource constrained environments, such as mobile devices and embedded systems. Clusterbuster has a built-in and configurable in-memory LRU-cache for the resulting tiles, with which it can serve many concurrent users.

Getting started

Clusterbuster is designed to be used in a NodeJS server connected to a PostgreSQL database, with PostGIS extensions installed. You have to bring your own web framework, allowing clusterbuster to be easily integrated in any existing API that uses express.js, Koa, Hapi, etc..

const{ TileServer }=require('clusterbuster');TileServer({// types/TileServerConfig.ts
maxZoomLevel,attributes: ['status','speed'],filtersToWhere: filters=>{// You are responsible for protecting against SQL injection in this function. Because there are many ways to filter, it depends on the filter type on how to approach this.// For example a number can be safely used by passing it through parseFloat, strings are best treated by checking for a set of allowed valuesconstwhereStatements=[];// The below statement checks that filters.status is one of 'free' or 'busy' to prevent potential SQL injectionif(filters.status&&['busy','free'].includes(filters.status)){whereStatements.push(`status = '${filters.status}'`);}if(filters.speed&&['slow','fast'].includes(filters.speed)){whereStatements.push(`speed = '${filters.speed}'`);}returnwhereStatements;},}).then(asynctileServer=>{consttile=awaittileServer({// types/TileRequest.tsz: 1,x: 0,y: 1,table: 'public.my_points_table',geometry: 'my_geometry_column'extent: 4096,bufferSize: 256,});// send the tile in binary MVT format to the front-end});

See the express.js example for a fully functioning server that exposes the above tile server on a REST endpoint. You can see the TileServerConfig for the initial configuration options, to configure the cache, connection pool, filters, etc..

The TileRequest defines the per tile request options.

The above example assumes a postgres database with a public.my_points_table table matching:

CREATE EXTENSION postgis;
-- Table: public.my_points_tableCREATETABLEpublic.my_points_table
(
id TEXT,
my_geometry_column geometry(Point,4326),
speed TEXT,
status TEXT,
PRIMARY KEY(id)
)
TABLESPACE pg_default;
GRANT ALL ON TABLE public.my_points_table TO "tiler";

The internal postgress client can be configured with the following env vars:

PGUSER=tilerPGHOST=localhostPGPASSWORD=PGDATABASE=pointsPGPORT=5432

Filtering

The filtersToWhere function can be used to implement custom filtering logic. It should return an array of SQL snippets, which clusterbuster transforms into the WHERE clause using AND between each statement.

The resulting SQL query looks something like this:

SELECT ...
WHERE whereStatement1 AND whereStatement2

Caching

The default cache is a in-memory LRU cache local to the tile server. This cache is ideal for a single instance tile server. If you are going to use clusterbuster in a loadbalanced multi instance server environment, the cache can be a redis instance or cluster instead, allowing all tile server instances to share the same tile cache.

You need to install the peer dependency npm i ioredis and configure the tile server to use redis instead of LRU. See the TileCacheOptions for all the configuration options.

{ "cacheOptions": { "type": "redis" } }

Internals

The tile server creates clusters using the PostGIS ST_ClusterDBSCAN starting at the maximum zoomlevel and continues clustering iteratively for each zoom level until the zoom level of the tile request is reached. This 'cluster of clusters' clustering algorithm is inspired by the excellent supercluster library, which many people use to cluster on the front-end and even on the back-end (using something like supertiler).

PostGIS is used for all the work such as selecting points in the region of the tile and even for creating the binary tiles themselves, using ST_asMVT. The NodeJS server only creates the SQL statements which then create the tiles in the database. These tiles are then gzipped and stored in an in-memory LRU cache inside the NodeJS process.

The main performance bottleneck for clusterbuster is the PostgreSQL server as the clustering algorithm requires a lot of CPU cycles, especially at low (1-5) zoom levels. Because the lower zoom levels contain fewer total tiles to cover the whole map (1 tile for the whold world at zoom level 0),the LRU cache effectively shields the database from becoming overloaded when many users are using the map concurrently. The number of different filters reduces the effectiveness of the LRU cache, as each filter combination creates a unique set of cache keys for all tiles. Typically the 'default' filter settings will provide enough caching to keep the database from becoming overloaded by many concurrent users, but mileage may vary per use case.

Alternative tile servers (which inspired clusterbuster)

  • Martin - A Rust based tile server, which has the option to provide PL/pgSQL function instead of a table, allows for filtering of data before tiling.
  • Tilestrata (and especially tilestrata-postgis-mvt) - Tilestrata is a tile server framework with an elaborate plugin architecture. The tilestrata-postgis-mvt plugin uses the same ST_asMvt functions provided by PostGIS as ClusterBuster does.
  • Tegola - A Go based tile server

Alternative static tile generators

All of these tile servers and tile generators offer some subset of the functionality we required, but lacked atleast one, which is our motivation for making clusterbuster.

Tilerdynamic datafilteringbackend clustering
Clusterbuster
Martinx
Tilestrataxx
Tegolaxx
Tippecanoexx
Supertilerxx

When not to use clusterbuster

  • Your data is static (and no filtering is required) - use tippecanoe or supertiler to prerender all tiles and serve from a file hosting service such as S3 (which is much more economical and loads faster to the front-end)
  • Your data needs to be filtered, but not clustered: Using Martin you can implement filtering using PL/pgSQL
  • You don't need filtering or clustering, you just need tiles from a dynamic data set: You can use any of the dynamic tile servers in the table above.

Sponsors

Chargetrip logo

About

A Mapbox Vector Tile (MVT) map tiling server with built-in clustering and filtering

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Sep 16, 2025. It is now read-only.

Repository files navigation

Intro

Clusterbuster is a tile server that produces map tiles (in MVT format) from a table with a PostGIS geometry column. It uses this table to first filter the row (based on a query you provide) and the resulting points are then clustered dynamically. The combination of clusters and points is transformed into a vector tile. The MVT tiles are much smaller than the original data, moving all the heavy lifting from the front-end to the tile server. This allows the display of large data sets, that change regularly, on maps in resource constrained environments, such as mobile devices and embedded systems. Clusterbuster has a built-in and configurable in-memory LRU-cache for the resulting tiles, with which it can serve many concurrent users.

Getting started

Clusterbuster is designed to be used in a NodeJS server connected to a PostgreSQL database, with PostGIS extensions installed. You have to bring your own web framework, allowing clusterbuster to be easily integrated in any existing API that uses express.js, Koa, Hapi, etc..

const{ TileServer }=require('clusterbuster');TileServer({// types/TileServerConfig.ts
maxZoomLevel,attributes: ['status','speed'],filtersToWhere: filters=>{// You are responsible for protecting against SQL injection in this function. Because there are many ways to filter, it depends on the filter type on how to approach this.// For example a number can be safely used by passing it through parseFloat, strings are best treated by checking for a set of allowed valuesconstwhereStatements=[];// The below statement checks that filters.status is one of 'free' or 'busy' to prevent potential SQL injectionif(filters.status&&['busy','free'].includes(filters.status)){whereStatements.push(`status = '${filters.status}'`);}if(filters.speed&&['slow','fast'].includes(filters.speed)){whereStatements.push(`speed = '${filters.speed}'`);}returnwhereStatements;},}).then(asynctileServer=>{consttile=awaittileServer({// types/TileRequest.tsz: 1,x: 0,y: 1,table: 'public.my_points_table',geometry: 'my_geometry_column'extent: 4096,bufferSize: 256,});// send the tile in binary MVT format to the front-end});

See the express.js example for a fully functioning server that exposes the above tile server on a REST endpoint. You can see the TileServerConfig for the initial configuration options, to configure the cache, connection pool, filters, etc..

The TileRequest defines the per tile request options.

The above example assumes a postgres database with a public.my_points_table table matching:

CREATE EXTENSION postgis;
-- Table: public.my_points_tableCREATETABLEpublic.my_points_table
(
id TEXT,
my_geometry_column geometry(Point,4326),
speed TEXT,
status TEXT,
PRIMARY KEY(id)
)
TABLESPACE pg_default;
GRANT ALL ON TABLE public.my_points_table TO "tiler";

The internal postgress client can be configured with the following env vars:

PGUSER=tilerPGHOST=localhostPGPASSWORD=PGDATABASE=pointsPGPORT=5432

Filtering

The filtersToWhere function can be used to implement custom filtering logic. It should return an array of SQL snippets, which clusterbuster transforms into the WHERE clause using AND between each statement.

The resulting SQL query looks something like this:

SELECT ...
WHERE whereStatement1 AND whereStatement2

Caching

The default cache is a in-memory LRU cache local to the tile server. This cache is ideal for a single instance tile server. If you are going to use clusterbuster in a loadbalanced multi instance server environment, the cache can be a redis instance or cluster instead, allowing all tile server instances to share the same tile cache.

You need to install the peer dependency npm i ioredis and configure the tile server to use redis instead of LRU. See the TileCacheOptions for all the configuration options.

{ "cacheOptions": { "type": "redis" } }

Internals

The tile server creates clusters using the PostGIS ST_ClusterDBSCAN starting at the maximum zoomlevel and continues clustering iteratively for each zoom level until the zoom level of the tile request is reached. This 'cluster of clusters' clustering algorithm is inspired by the excellent supercluster library, which many people use to cluster on the front-end and even on the back-end (using something like supertiler).

PostGIS is used for all the work such as selecting points in the region of the tile and even for creating the binary tiles themselves, using ST_asMVT. The NodeJS server only creates the SQL statements which then create the tiles in the database. These tiles are then gzipped and stored in an in-memory LRU cache inside the NodeJS process.

The main performance bottleneck for clusterbuster is the PostgreSQL server as the clustering algorithm requires a lot of CPU cycles, especially at low (1-5) zoom levels. Because the lower zoom levels contain fewer total tiles to cover the whole map (1 tile for the whold world at zoom level 0),the LRU cache effectively shields the database from becoming overloaded when many users are using the map concurrently. The number of different filters reduces the effectiveness of the LRU cache, as each filter combination creates a unique set of cache keys for all tiles. Typically the 'default' filter settings will provide enough caching to keep the database from becoming overloaded by many concurrent users, but mileage may vary per use case.

Alternative tile servers (which inspired clusterbuster)

  • Martin - A Rust based tile server, which has the option to provide PL/pgSQL function instead of a table, allows for filtering of data before tiling.
  • Tilestrata (and especially tilestrata-postgis-mvt) - Tilestrata is a tile server framework with an elaborate plugin architecture. The tilestrata-postgis-mvt plugin uses the same ST_asMvt functions provided by PostGIS as ClusterBuster does.
  • Tegola - A Go based tile server

Alternative static tile generators

All of these tile servers and tile generators offer some subset of the functionality we required, but lacked atleast one, which is our motivation for making clusterbuster.

Tilerdynamic datafilteringbackend clustering
Clusterbuster
Martinx
Tilestrataxx
Tegolaxx
Tippecanoexx
Supertilerxx

When not to use clusterbuster

  • Your data is static (and no filtering is required) - use tippecanoe or supertiler to prerender all tiles and serve from a file hosting service such as S3 (which is much more economical and loads faster to the front-end)
  • Your data needs to be filtered, but not clustered: Using Martin you can implement filtering using PL/pgSQL
  • You don't need filtering or clustering, you just need tiles from a dynamic data set: You can use any of the dynamic tile servers in the table above.

Sponsors

Chargetrip logo

About

A Mapbox Vector Tile (MVT) map tiling server with built-in clustering and filtering

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
This repository was archived by the owner on Sep 16, 2025. It is now read-only.

Repository files navigation

Intro

Clusterbuster is a tile server that produces map tiles (in MVT format) from a table with a PostGIS geometry column. It uses this table to first filter the row (based on a query you provide) and the resulting points are then clustered dynamically. The combination of clusters and points is transformed into a vector tile. The MVT tiles are much smaller than the original data, moving all the heavy lifting from the front-end to the tile server. This allows the display of large data sets, that change regularly, on maps in resource constrained environments, such as mobile devices and embedded systems. Clusterbuster has a built-in and configurable in-memory LRU-cache for the resulting tiles, with which it can serve many concurrent users.

Getting started

Clusterbuster is designed to be used in a NodeJS server connected to a PostgreSQL database, with PostGIS extensions installed. You have to bring your own web framework, allowing clusterbuster to be easily integrated in any existing API that uses express.js, Koa, Hapi, etc..

const{ TileServer }=require('clusterbuster');TileServer({// types/TileServerConfig.ts
maxZoomLevel,attributes: ['status','speed'],filtersToWhere: filters=>{// You are responsible for protecting against SQL injection in this function. Because there are many ways to filter, it depends on the filter type on how to approach this.// For example a number can be safely used by passing it through parseFloat, strings are best treated by checking for a set of allowed valuesconstwhereStatements=[];// The below statement checks that filters.status is one of 'free' or 'busy' to prevent potential SQL injectionif(filters.status&&['busy','free'].includes(filters.status)){whereStatements.push(`status = '${filters.status}'`);}if(filters.speed&&['slow','fast'].includes(filters.speed)){whereStatements.push(`speed = '${filters.speed}'`);}returnwhereStatements;},}).then(asynctileServer=>{consttile=awaittileServer({// types/TileRequest.tsz: 1,x: 0,y: 1,table: 'public.my_points_table',geometry: 'my_geometry_column'extent: 4096,bufferSize: 256,});// send the tile in binary MVT format to the front-end});

See the express.js example for a fully functioning server that exposes the above tile server on a REST endpoint. You can see the TileServerConfig for the initial configuration options, to configure the cache, connection pool, filters, etc..

The TileRequest defines the per tile request options.

The above example assumes a postgres database with a public.my_points_table table matching:

CREATE EXTENSION postgis;
-- Table: public.my_points_tableCREATETABLEpublic.my_points_table
(
id TEXT,
my_geometry_column geometry(Point,4326),
speed TEXT,
status TEXT,
PRIMARY KEY(id)
)
TABLESPACE pg_default;
GRANT ALL ON TABLE public.my_points_table TO "tiler";

The internal postgress client can be configured with the following env vars:

PGUSER=tilerPGHOST=localhostPGPASSWORD=PGDATABASE=pointsPGPORT=5432

Filtering

The filtersToWhere function can be used to implement custom filtering logic. It should return an array of SQL snippets, which clusterbuster transforms into the WHERE clause using AND between each statement.

The resulting SQL query looks something like this:

SELECT ...
WHERE whereStatement1 AND whereStatement2

Caching

The default cache is a in-memory LRU cache local to the tile server. This cache is ideal for a single instance tile server. If you are going to use clusterbuster in a loadbalanced multi instance server environment, the cache can be a redis instance or cluster instead, allowing all tile server instances to share the same tile cache.

You need to install the peer dependency npm i ioredis and configure the tile server to use redis instead of LRU. See the TileCacheOptions for all the configuration options.

{ "cacheOptions": { "type": "redis" } }

Internals

The tile server creates clusters using the PostGIS ST_ClusterDBSCAN starting at the maximum zoomlevel and continues clustering iteratively for each zoom level until the zoom level of the tile request is reached. This 'cluster of clusters' clustering algorithm is inspired by the excellent supercluster library, which many people use to cluster on the front-end and even on the back-end (using something like supertiler).

PostGIS is used for all the work such as selecting points in the region of the tile and even for creating the binary tiles themselves, using ST_asMVT. The NodeJS server only creates the SQL statements which then create the tiles in the database. These tiles are then gzipped and stored in an in-memory LRU cache inside the NodeJS process.

The main performance bottleneck for clusterbuster is the PostgreSQL server as the clustering algorithm requires a lot of CPU cycles, especially at low (1-5) zoom levels. Because the lower zoom levels contain fewer total tiles to cover the whole map (1 tile for the whold world at zoom level 0),the LRU cache effectively shields the database from becoming overloaded when many users are using the map concurrently. The number of different filters reduces the effectiveness of the LRU cache, as each filter combination creates a unique set of cache keys for all tiles. Typically the 'default' filter settings will provide enough caching to keep the database from becoming overloaded by many concurrent users, but mileage may vary per use case.

Alternative tile servers (which inspired clusterbuster)

  • Martin - A Rust based tile server, which has the option to provide PL/pgSQL function instead of a table, allows for filtering of data before tiling.
  • Tilestrata (and especially tilestrata-postgis-mvt) - Tilestrata is a tile server framework with an elaborate plugin architecture. The tilestrata-postgis-mvt plugin uses the same ST_asMvt functions provided by PostGIS as ClusterBuster does.
  • Tegola - A Go based tile server

Alternative static tile generators

All of these tile servers and tile generators offer some subset of the functionality we required, but lacked atleast one, which is our motivation for making clusterbuster.

Tilerdynamic datafilteringbackend clustering
Clusterbuster
Martinx
Tilestrataxx
Tegolaxx
Tippecanoexx
Supertilerxx

When not to use clusterbuster

  • Your data is static (and no filtering is required) - use tippecanoe or supertiler to prerender all tiles and serve from a file hosting service such as S3 (which is much more economical and loads faster to the front-end)
  • Your data needs to be filtered, but not clustered: Using Martin you can implement filtering using PL/pgSQL
  • You don't need filtering or clustering, you just need tiles from a dynamic data set: You can use any of the dynamic tile servers in the table above.

Sponsors

Chargetrip logo

About

A Mapbox Vector Tile (MVT) map tiling server with built-in clustering and filtering

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages