Repository: ryanhausen/fitsmap (language: python)

Issue:
Store catalog column names in index.js rather than in JSON
Catalog data for each source is currently stored as a JSON object with key being the column name and the value being the source value. For large catalogs this will take up additional space. This could be optimized by storing the columns in index.js in the TIledMarkers object and storing the 


Discussion / hints:

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index b9a3ede..00abc79 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,15 +1,13 @@
 language: python
-services:
-  - docker
 python:
-  - "3.7"
+  - "3.7.9"
 install:
-  - pip install -r requirements.txt
-  - pip install .
-  - pip install pytest
-  - pip install pytest-cov
-  - pip install codecov
-  - pip install twine
+  - python -m pip install -r requirements.txt
+  - python -m pip install .
+  - python -m pip install pytest
+  - python -m pip install pytest-cov
+  - python -m pip install codecov
+  - python -m pip install twine
 script:
   - pytest --cov=fitsmap -vv
 after_success:
diff --git a/README.rst b/README.rst
index d1a1695..9aa081d 100644
--- a/README.rst
+++ b/README.rst
@@ -63,6 +63,7 @@ Requirements:
 - ``tqdm``
 - ``click``
 - ``mapbox_vector_tile``
+- ``cbor2``
 
 Use ``pip`` to install
 
diff --git a/fitsmap/cartographer.py b/fitsmap/cartographer.py
index a9774cf..40275d9 100644
--- a/fitsmap/cartographer.py
+++ b/fitsmap/cartographer.py
@@ -27,9 +27,9 @@
 
 import os
 import shutil
-from itertools import count, repeat, starmap
+from itertools import count, cycle, repeat, starmap
 from functools import partial, reduce
-from typing import Dict, List, Tuple
+from typing import Dict, Iterable, List, Tuple
 
 import numpy as np
 from astropy.wcs import WCS
@@ -59,7 +59,7 @@ def chart(
     layer_zooms = lambda l: list(map(int, os.listdir(os.path.join(out_dir, l))))
     zooms = reduce(lambda x, y: x + y, list(map(layer_zooms, img_layer_names)))
     zooms = [0] if len(zooms) == 0 else zooms
-    convert_layer_name_func = partial(layer_name_to_dict, min(zooms), max(zooms))
+    convert_layer_name_func = partial(layer_name_to_dict, out_dir, min(zooms), max(zooms))
     img_layer_dicts = list(
         starmap(convert_layer_name_func, zip(img_layer_names, repeat(None)))
     )
@@ -89,7 +89,7 @@ def chart(
     # HTML file contents =======================================================
 
 
-def layer_name_to_dict(min_zoom: int, max_zoom: int, name: str, color: str,) -> dict:
+def layer_name_to_dict(out_dir:str, min_zoom: int, max_zoom: int, name: str, color: str,) -> dict:
     """Convert layer name to dict for conversion."""
 
     layer_dict = dict(
@@ -103,6 +103,13 @@ def layer_name_to_dict(min_zoom: int, max_zoom: int, name: str, color: str,) ->
         layer_dict["color"] = color
         layer_dict["directory"] = layer_dict["directory"].replace("png", "pbf")
 
+        cat_col_path = os.path.join(out_dir, f"{name}.columns")
+        with open(cat_col_path, "r") as f:
+            columns = f.readline().strip().split(",")
+            layer_dict["columns"] = [f'"{c}"' for c in columns]
+        os.remove(cat_col_path)
+
+
     return layer_dict
 
 
@@ -134,6 +141,7 @@ def cat_layer_dict_to_str(layer: dict, rows_per_column: int) -> str:
         'tileURL:"' + layer["directory"] + '", ',
         'color: "' + layer["color"] + '", ',
         f"rowsPerColumn: {rpc_str}, ",
+        f'catalogColumns: [{",".join(layer["columns"])}],',
         "minZoom: " + str(layer["min_zoom"]) + ", ",
         "maxZoom: " + str(layer["max_zoom"]) + ", ",
         "maxNativeZoom: " + str(layer["max_native_zoom"]) + " ",
@@ -143,8 +151,8 @@ def cat_layer_dict_to_str(layer: dict, rows_per_column: int) -> str:
     return "".join(layer_str)
 
 
-def get_colors() -> List[str]:
-    return [
+def get_colors() -> Iterable[str]:
+    return cycle([
         "#4C72B0",
         "#DD8452",
         "#55A868",
@@ -155,7 +163,7 @@ def get_colors() -> List[str]:
         "#8C8C8C",
         "#CCB974",
         "#64B5CD",
-    ]
+    ])
 
 
 def leaflet_crs_js(tile_layers: List[dict]) -> str:
@@ -187,8 +195,20 @@ def leaflet_map_js(tile_layers: List[dict]):
     return js
 
 
+def loading_screen_js(tile_layers: List[dict]):
+    js = "\n".join(
+        [
+            f'{tile_layers[0]["name"]}.on("load", () => {{',
+            '    document.getElementById("loading-screen").style.visibility = "hidden";',
+            '    document.getElementById("map").style.visibility = "visible";',
+            "});"
+        ]
+    )
+
+    return js
+
 def move_support_images(out_dir: str) -> List[str]:
-    img_extensions = [".png", ".jpg", ".ico"]
+    img_extensions = [".png", ".jpg", ".ico", ".svg"]
 
     support_dir = os.path.join(os.path.dirname(__file__), "support")
     out_img_dir = os.path.join(out_dir, "imgs")
@@ -217,14 +237,14 @@ def move_support_images(out_dir: str) -> List[str]:
 def build_conditional_css(out_dir: str) -> str:
 
     search_css = "https://unpkg.com/leaflet-search@2.9.8/dist/leaflet-search.src.css"
-    css_string = "    <link rel='stylesheet' href='{}'/>"
+    css_string = "    <link rel='preload' href='{}'  as='style' onload='this.rel=\"stylesheet\"'/>"
 
     support_dir = os.path.join(os.path.dirname(__file__), "support")
     out_css_dir = os.path.join(out_dir, "css")
 
     local_css_files = list(
         filter(
-            lambda f: os.path.splitext(f)[1] == ".css", sorted(os.listdir(support_dir))
+            lambda f: f.endswith(".min.css"), sorted(os.listdir(support_dir))
         )
     )
 
@@ -252,7 +272,7 @@ def build_conditional_js(out_dir: str) -> str:
 
     local_js_files = list(
         sorted(
-            filter(lambda f: os.path.splitext(f)[1] == ".js", os.listdir(support_dir))
+            filter(lambda f: f.endswith(".min.js"), os.listdir(support_dir))
         )
     )
 
@@ -268,15 +288,29 @@ def build_conditional_js(out_dir: str) -> str:
         )
     )
 
-    remote_js = [
-        '    <script src="https://unpkg.com/pbf@3.0.5/dist/pbf.js", crossorigin=""></script>',
-        '    <script src="https://cdn.jsdelivr.net/npm/leaflet-search" crossorigin=""></script>',
+    # some files have to be loaded before index.js, so that index.js can
+    # sucessfully run. Other files get placed after index.js so that the map
+    # can load first and then those will load in the background
+    pre_index_files = [
+        "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.js",
+        "https://cdnjs.cloudflare.com/ajax/libs/leaflet-search/3.0.2/leaflet-search.src.min.js",
+        "js/customSearch.min.js",
+        "js/tiledMarkers.min.js",
+        "js/urlCoords.js",
+        "js/index.js",
+    ]
+
+    post_index_files = [
+        "https://unpkg.com/cbor-web@8.1.0/dist/cbor.js",
+        "https://unpkg.com/pbf@3.0.5/dist/pbf.js",
+        "js/l.ellipse.min.js",
+        "js/vector-tile.min.js",
     ]
 
-    js_string = "    <script src='js/{}'></script>"
-    local_js = list(map(lambda s: js_string.format(s), local_js_files))
+    js_string = "    <script defer src='{}'></script>"
+    js_tags = list(map(lambda s: js_string.format(s), pre_index_files + post_index_files))
 
-    return "\n".join(remote_js + local_js)
+    return "\n".join(js_tags)
 
 
 def leaflet_layer_control_declaration(
@@ -388,6 +422,8 @@ def build_index_js(
             else "",
             "",
             "// Map event setup =============================================================",
+            loading_screen_js(image_layer_dicts),
+            ""
             'map.on("moveend", updateLocationBar);',
             'map.on("zoomend", updateLocationBar);',
             "",
@@ -405,31 +441,31 @@ def build_index_js(
 def build_html(title: str, extra_js: str, extra_css: str) -> str:
     html = [
         "<!DOCTYPE html>",
-        "<html>",
+        '<html lang="en">',
         "<head>",
         "    <title>{}</title>".format(title),
         '    <meta charset="utf-8" />',
         '    <meta name="viewport" content="width=device-width, initial-scale=1.0">',
         '    <link rel="shortcut icon" type="image/x-icon" href="imgs/favicon.ico" />',
-        '    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css" integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA==" crossorigin=""/>',
+        '    <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.css" integrity="sha512-1xoFisiGdy9nvho8EgXuXvnpR5GAMSjFwp40gSRE3NwdUdIMIKuPa7bqoUhLD0O/5tPNhteAsE5XyyMi5reQVA==" crossorigin="anonymous" referrerpolicy="no-referrer" as="style" onload="this.rel=\'stylesheet\'"/>',
         extra_css,
-        "    <script src='https://unpkg.com/leaflet@1.3.4/dist/leaflet.js' integrity='sha512-nMMmRyTVoLYqjP9hrbed9S+FzjZHW5gY1TWCHA5ckwXZBadntCNs8kEqAWdrb9O7rxbCaA4lKTIWjDXZxflOcA==' crossorigin=''></script>",
+        '    <script defer src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.js" integrity="sha512-SeiQaaDh73yrb56sTW/RgVdi/mMqNeM2oBwubFHagc5BkixSpP1fvqF47mKzPGWYSSy4RwbBunrJBQ4Co8fRWA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>',
         extra_js,
         "    <style>",
-        "        html, body {",
-        "            height: 100%;",
-        "            margin: 0;",
-        "        }",
-        "        #map {",
-        "            width: 100%;",
-        "            height: 100%;",
-        "        }",
+        "        /* Map */",
+        r"        html,body{height:100%;padding:0;margin:0;font-family:Helvetica,Arial,sans-serif}#map{width:100%;height:100%;visibility:hidden}",
+        "        /* Loading Page */",
+        r"        .overlay{background:#fff;height:100vh;width:100%;position:absolute}.brand{position:absolute;top:100px;left:50%;transform:translateX(-50%)}.brand img{width:100%;height:auto}.loadingtext{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-weight:700;font-size:xx-large}.loading{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);-moz-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);-o-transform:translate(-50%,-50%);transform:translate(-50%,-50%);border-bottom:16px solid #0044aaff;border-top:16px solid #0044aaff;border-left:16px solid #80b3ffff;border-right:16px solid #80b3ffff;width:250px;height:250px;-webkit-border-radius:50%;-moz-border-radius:50%;border-radius:50%;-webkit-animation:rotate 1s ease-in-out infinite;-o-animation:rotate 1s ease-in-out infinite;animation:rotate 1s ease-in-out infinite}",
+        r"        @keyframes rotate{0%{-webkit-transform:translate(-50%,-50%) rotate(0deg);-moz-transform:translate(-50%,-50%) rotate(0deg);-ms-transform:translate(-50%,-50%) rotate(0deg);-o-transform:translate(-50%,-50%) rotate(0deg);transform:translate(-50%,-50%) rotate(0deg)}100%{-webkit-transform:translate(-50%,-50%) rotate(360deg);-moz-transform:translate(-50%,-50%) rotate(360deg);-ms-transform:translate(-50%,-50%) rotate(360deg);-o-transform:translate(-50%,-50%) rotate(360deg);transform:translate(-50%,-50%) rotate(360deg)}}",
         "    </style>",
         "</head>",
         "<body>",
+        '    <div id="loading-screen" class="overlay">',
+        '        <div class="brand"><img src="imgs/loading-logo.svg" /></div>',
+        '        <div class="loading"></div>',
+        '        <div class="loadingtext">Loading...</div>',
+        '    </div>',
         '    <div id="map"></div>',
-        '    <script src="js/urlCoords.js"></script>',
-        '    <script src="js/index.js"></script>',
         "</body>",
         f"<!--Made with fitsmap v{utils.get_version()}-->",
         "</html>\n",
diff --git a/fitsmap/convert.py b/fitsmap/convert.py
index e2578d5..427bce3 100644
--- a/fitsmap/convert.py
+++ b/fitsmap/convert.py
@@ -33,6 +33,7 @@
 from queue import Empty, Queue
 from typing import Any, Callable, Dict, Iterable, List, Tuple
 
+import cbor2
 import mapbox_vector_tile as mvt
 import matplotlib as mpl
 import matplotlib.pyplot as plt
@@ -620,15 +621,21 @@ def line_to_json(
     # y = img_y
     # x = img_x
 
-    src_desc = {k: v for k, v in zip(columns, src_vals)}
+    # src_desc = {k: v for k, v in zip(columns, src_vals)}
 
-    src_desc["fm_y"] = y
-    src_desc["fm_x"] = x
-    src_desc["fm_cat"] = catalog_assets_path.split(os.sep)[-1]
+    # src_desc["fm_y"] = y
+    # src_desc["fm_x"] = x
+    # src_desc["fm_cat"] = catalog_assets_path.split(os.sep)[-1]
 
-    src_json = os.path.join(catalog_assets_path, f"{src_id}.json")
-    with open(src_json, "w") as f:
-        json.dump(src_desc, f, separators=(",", ":"))  # no whitespace
+    src_vals += [y, x, catalog_assets_path.split(os.sep)[-1]]
+
+    # src_json = os.path.join(catalog_assets_path, f"{src_id}.json")
+    # with open(src_json, "w") as f:
+    #     json.dump(dict(id=src_id,v=src_vals), f, separators=(",", ":"))  # no whitespace
+
+    src_json = os.path.join(catalog_assets_path, f"{src_id}.cbor")
+    with open(src_json, "wb") as f:
+        cbor2.dump(dict(id=src_id,v=src_vals), f)
 
     return dict(
         geometry=dict(coordinates=[x, y],),
@@ -788,6 +795,9 @@ def tile_markers(
 
     wcs = WCS(wcs_file) if wcs_file else None
 
+    with open(os.path.join(out_dir, f"{catalog_layer_name}.columns"), "w") as f:
+        f.write(",".join(columns))
+
     catalog_assets_parent_path = os.path.join(out_dir, "catalog_assets")
     if "catalog_assets" not in os.listdir(out_dir):
         os.mkdir(catalog_assets_parent_path)
diff --git a/fitsmap/support/MarkerCluster.Default.min.css b/fitsmap/support/MarkerCluster.Default.min.css
new file mode 100644
index 0000000..bbe48d9
--- /dev/null
+++ b/fitsmap/support/MarkerCluster.Default.min.css
@@ -0,0 +1,2 @@
+.marker-cluster-small{background-color:rgba(181,226,140,.6)}.marker-cluster-small div{background-color:rgba(110,204,57,.6)}.marker-cluster-medium{background-color:rgba(241,211,87,.6)}.marker-cluster-medium div{background-color:rgba(240,194,12,.6)}.marker-cluster-large{background-color:rgba(253,156,115,.6)}.marker-cluster-large div{background-color:rgba(241,128,23,.6)}.marker-cluster-x-large{background-color:rgba(240,76,76,.6)}.marker-cluster-x-large div{background-color:rgba(246,23,23,.6)}.leaflet-oldie .marker-cluster-small{background-color:#b5e28c}.leaflet-oldie .marker-cluster-small div{background-color:#6ecc39}.leaflet-oldie .marker-cluster-medium{background-color:#f1d357}.leaflet-oldie .marker-cluster-medium div{background-color:#f0c20c}.leaflet-oldie .marker-cluster-large{background-color:#fd9c73}.leaflet-oldie .marker-cluster-large div{background-color:#f18017}.leaflet-oldie .marker-cluster-x-large{background-color:#f04c4c}.leaflet-oldie .marker-cluster-x-large div{background-color:#f61717}
+.marker-cluster{background-clip:padding-box;border-radius:20px}.marker-cluster div{width:30px;height:30px;margin-left:5px;margin-top:5px;text-align:center;border-radius:15px;font:12px "Helvetica Neue",Arial,Helvetica,sans-serif}.marker-cluster span{line-height:30px}
\ No newline at end of file
diff --git a/fitsmap/support/MarkerCluster.min.css b/fitsmap/support/MarkerCluster.min.css
new file mode 100644
index 0000000..a4472c2
--- /dev/null
+++ b/fitsmap/support/MarkerCluster.min.css
@@ -0,0 +1,1 @@
+.leaflet-cluster-anim .leaflet-marker-icon,.leaflet-cluster-anim .leaflet-marker-shadow{-webkit-transition:-webkit-transform .3s ease-out,opacity .3s ease-in;-moz-transition:-moz-transform .3s ease-out,opacity .3s ease-in;-o-transition:-o-transform .3s ease-out,opacity .3s ease-in;transition:transform .3s ease-out,opacity .3s ease-in}.leaflet-cluster-spider-leg{-webkit-transition:-webkit-stroke-dashoffset .3s ease-out,-webkit-stroke-opacity .3s ease-in;-moz-transition:-moz-stroke-dashoffset .3s ease-out,-moz-stroke-opacity .3s ease-in;-o-transition:-o-stroke-dashoffset .3s ease-out,-o-stroke-opacity .3s ease-in;transition:stroke-dashoffset .3s ease-out,stroke-opacity .3s ease-in}
\ No newline at end of file
diff --git a/fitsmap/support/MarkerPopup.min.css b/fitsmap/support/MarkerPopup.min.css
new file mode 100644
index 0000000..6792a26
--- /dev/null
+++ b/fitsmap/support/MarkerPopup.min.css
@@ -0,0 +1,1 @@
+.catalog-table{width:max-content}
\ No newline at end of file
diff --git a/fitsmap/support/TileNearestNeighbor.min.css b/fitsmap/support/TileNearestNeighbor.min.css
new file mode 100644
index 0000000..52a7197
--- /dev/null
+++ b/fitsmap/support/TileNearestNeighbor.min.css
@@ -0,0 +1,1 @@
+.leaflet-container .leaflet-overlay-pane svg,.leaflet-container .leaflet-marker-pane img,.leaflet-container .leaflet-shadow-pane img,.leaflet-container .leaflet-tile-pane img,.leaflet-container img.leaflet-image-layer{max-width:none !important;image-rendering:optimizeSpeed;image-rendering:-moz-crisp-edges;image-rendering:-o-crisp-edges;image-rendering:-webkit-optimize-contrast;image-rendering:optimize-contrast;image-rendering:crisp-edges;image-rendering:pixelated;-ms-interpolation-mode:nearest-neighbor}
\ No newline at end of file
diff --git a/fitsmap/support/customSearch.js.txt b/fitsmap/support/customSearch.js
similarity index 60%
rename from fitsmap/support/customSearch.js.txt
rename to fitsmap/support/customSearch.js
index c7e30ca..aabd3e1 100644
--- a/fitsmap/support/customSearch.js.txt
+++ b/fitsmap/support/customSearch.js
@@ -4,26 +4,39 @@ function buildCustomSearch(catalogPaths, zoom){
 
     function searchByFnames(text, callbackResponse) {
 
-        const searchCandidates = catalogPaths.map(c => c + text + ".json");
-
+        const searchCandidates = catalogPaths.map(c => c + text + ".cbor");
+        console.log(searchCandidates);
         // console.log("candidates", searchCandidates);
         Promise.all(
             searchCandidates.map(f => {
                 return fetch(f).then(response => {
                     if (response.ok){
                         //console.log(response);
-                        return response.json();
+                        return response.arrayBuffer();
                     } else {
                         //console.log(response);
                         return Promise.resolve(null);
                     }
+                }).then(buffer => {
+                    //console.log(buffer);
+                    return buffer ? cbor.decodeAll(buffer) : Promise.resolve(null)
+                }).then(json => {
+                    //console.log(json[0]);
+                    return (json && json.length) ? json[0] : Promise.resolve(null);
                 })
+
             })
         ).then(values => {
-            // console.log("values", values);
-            const searchResults = values.filter(v => v).map(v => {
-                // console.log(v);
-                return {title:v.id + ":" + v.fm_cat, loc:[v.fm_y,v.fm_x]}
+            //console.log("values", values);
+            const searchResults = values.filter(val => val).map(val => {
+                //console.log("val", val, val.v);
+                const yIdx = val.v.length - 3;
+                const xIdx = val.v.length - 2;
+                const catIdx = val.v.length - 1;
+                return {
+                    title:val.id + ":" + val.v[catIdx],
+                    loc:[val.v[yIdx],val.v[xIdx]]
+                }
             });
             // console.log(searchResults);
             callbackResponse(searchResults);
diff --git a/fitsmap/support/customSearch.min.js b/fitsmap/support/customSearch.min.js
index 4613677..d4d684e 100644
--- a/fitsmap/support/customSearch.min.js
+++ b/fitsmap/support/customSearch.min.js
@@ -1,1 +1,1 @@
-"use strict";function buildCustomSearch(o,t){return new L.Control.Search({sourceData:function(t,e){const n=o.map((o=>o+t+".json"));return Promise.all(n.map((o=>fetch(o).then((o=>o.ok?o.json():Promise.resolve(null)))))).then((o=>{const t=o.filter((o=>o)).map((o=>({title:o.id+":"+o.fm_cat,loc:[o.fm_y,o.fm_x]})));e(t)})),{abort:function(){console.log("aborted request:"+t)}}},textPlaceholder:"Enter Catalog ID",textErr:"Catalog ID not found",markerLocation:!0,hideMarkerOnCollapse:!0,moveToLocation:function(o,e,n){n.setView(o,t)}})}
\ No newline at end of file
+"use strict";function buildCustomSearch(e,o){return new L.Control.Search({sourceData:function(o,t){const n=e.map((e=>e+o+".cbor"));return console.log(n),Promise.all(n.map((e=>fetch(e).then((e=>e.ok?e.arrayBuffer():Promise.resolve(null))).then((e=>e?cbor.decodeAll(e):Promise.resolve(null))).then((e=>e&&e.length?e[0]:Promise.resolve(null)))))).then((e=>{const o=e.filter((e=>e)).map((e=>{const o=e.v.length-3,t=e.v.length-2,n=e.v.length-1;return{title:e.id+":"+e.v[n],loc:[e.v[o],e.v[t]]}}));t(o)})),{abort:function(){console.log("aborted request:"+o)}}},textPlaceholder:"Enter Catalog ID",textErr:"Catalog ID not found",markerLocation:!0,hideMarkerOnCollapse:!0,moveToLocation:function(e,t,n){n.setView(e,o)}})}
\ No newline at end of file
diff --git a/fitsmap/support/favicon.ico b/fitsmap/support/favicon.ico
index 9f19888..7fa0d52 100644
Binary files a/fitsmap/support/favicon.ico and b/fitsmap/support/favicon.ico differ
diff --git a/fitsmap/support/l.ellipse.js.txt b/fitsmap/support/l.ellipse.js
similarity index 100%
rename from fitsmap/support/l.ellipse.js.txt
rename to fitsmap/support/l.ellipse.js
diff --git a/fitsmap/support/loading-logo.svg b/fitsmap/support/loading-logo.svg
new file mode 100644
index 0000000..d38e26a
--- /dev/null
+++ b/fitsmap/support/loading-logo.svg
@@ -0,0 +1,208 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="1200"
+   height="675"
+   viewBox="0 0 317.49999 178.59376"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)"
+   sodipodi:docname="logo.svg"
+   inkscape:export-filename="/home/ryanhausen/repos/fitsmap/docs/logo.svg.png"
+   inkscape:export-xdpi="96"
+   inkscape:export-ydpi="96">
+  <defs
+     id="defs2">
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect824"
+       is_visible="true"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect4652"
+       is_visible="true"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect4648"
+       is_visible="true"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect4652-2"
+       is_visible="true"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect824-6"
+       is_visible="true"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect824-2"
+       is_visible="true"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect824-2-2"
+       is_visible="true"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="1"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="600"
+     inkscape:cy="286.84096"
+     inkscape:document-units="mm"
+     inkscape:current-layer="g975"
+     showgrid="false"
+     units="px"
+     inkscape:pagecheckerboard="false"
+     inkscape:window-width="1853"
+     inkscape:window-height="1016"
+     inkscape:window-x="67"
+     inkscape:window-y="27"
+     inkscape:window-maximized="0"
+     showguides="true" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-118.40623)">
+    <g
+       id="g4529"
+       transform="translate(1.4314917,-4.7451173)">
+      <g
+         id="g928"
+         transform="matrix(0.87928042,0,0,0.98999915,15.078303,6.554227)">
+        <g
+           id="g921"
+           transform="matrix(1,0,0,0.79790939,-6.9358631,87.558302)" />
+      </g>
+      <g
+         id="g996"
+         transform="translate(-1.3245165,-6.4891338)">
+        <g
+           style="fill:#0044aa;stroke:#0044aa;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none"
+           id="g975">
+          <g
+             id="g881"
+             transform="translate(-10.282039,0.60506709)">
+            <text
+               xml:space="preserve"
+               style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:389.00001049%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+               font-family="Helvetica,Arial,sans-serif"
+               x="78.428459"
+               y="229.36147"
+               id="text4522"
+               transform="translate(1.3245165,6.4891341)"><tspan
+                 sodipodi:role="line"
+                 id="tspan4520"
+                 x="78.428459"
+                 y="229.36147"
+                 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:63.5px;stroke-width:0.26458332px"><tspan
+                   id="tspan979"
+                   style="fill:#0044aa;fill-opacity:1">Fits</tspan><tspan
+                   id="tspan985"
+                   style="fill:#80b3ff;fill-opacity:1">Map</tspan></tspan></text>
+            <rect
+               y="191.41411"
+               x="22.446129"
+               height="44.804165"
+               width="44.804165"
+               id="rect4518"
+               style="opacity:1;fill:none;fill-opacity:1;stroke:#80b3ff;stroke-width:5.1960001;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+               inkscape:export-xdpi="342.8996"
+               inkscape:export-ydpi="342.8996" />
+            <path
+               inkscape:original-d="m 44.576025,211.71523 c 2.33e-4,5.67614 2.33e-4,11.35245 0,17.029"
+               inkscape:path-effect="#path-effect824-2"
+               inkscape:connector-curvature="0"
+               id="path822-9"
+               d="m 44.576025,211.71523 c 0,5.67614 0,11.35245 0,17.029"
+               style="fill:#0044aa;fill-rule:evenodd;stroke:#0044aa;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+            <path
+               style="fill:#0044aa;fill-rule:evenodd;stroke:#0044aa;stroke-width:2.04532743;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+               d="m 44.42805,229.7316 h -0.829361 v 1.02121 z"
+               id="path901-1"
+               inkscape:connector-curvature="0" />
+            <path
+               inkscape:original-d="m 59.092974,213.70859 c -2.500196,-2.4e-4 -5.000417,2.4e-4 -7.500694,2.4e-4 -2.699674,1e-5 -5.399395,-4.8e-4 -8.099183,-2.4e-4"
+               inkscape:path-effect="#path-effect824-2-2"
+               inkscape:connector-curvature="0"
+               id="path822-9-3"
+               d="m 59.092974,213.70859 c -2.500196,8e-5 -5.000417,1.6e-4 -7.600392,1.6e-4 -2.599976,0 -5.299697,-8e-5 -7.999485,-1.6e-4"
+               style="fill:#0044aa;fill-rule:evenodd;stroke:#0044aa;stroke-width:3.99984002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+               sodipodi:nodetypes="ccc" />
+            <path
+               style="fill:#0044aa;fill-rule:evenodd;stroke:#0044aa;stroke-width:2.13300371;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+               d="m 60.094527,213.64584 v -0.87011 h 1.227359 z"
+               id="path901-6"
+               inkscape:connector-curvature="0" />
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/fitsmap/support/tiledMarkers.js.txt b/fitsmap/support/tiledMarkers.js
similarity index 87%
rename from fitsmap/support/tiledMarkers.js.txt
rename to fitsmap/support/tiledMarkers.js
index 2556d55..e374840 100644
--- a/fitsmap/support/tiledMarkers.js.txt
+++ b/fitsmap/support/tiledMarkers.js
@@ -6,6 +6,7 @@ L.GridLayer.TiledMarkers = L.GridLayer.extend({
         tileURL: "",
         color: "#4C72B0",
         rowsPerCol: Infinity,
+        catalogColumns: [],
     },
 
     initialize: function(options) {
@@ -15,9 +16,11 @@ L.GridLayer.TiledMarkers = L.GridLayer.extend({
 
     convertJSONtoHTMLTable: function(json) {
         // these are extra keys used for the search function
-        const dontDisplay = ["fm_y", "fm_x", "fm_cat"]
-        const nItems = Object.keys(json).length - dontDisplay.length;
+        // const dontDisplay = ["fm_y", "fm_x", "fm_cat"]
+        // const nItems = Object.keys(json).length - dontDisplay.length;
+        const nItems = this.options.catalogColumns.length;
         const rowsPerCol = this.options.rowsPerColumn;
+        const values = json["v"];
 
         let nCols = Math.floor(nItems / (Number.isFinite(rowsPerCol) ? rowsPerCol : nItems));
         // console.log(nCols);
@@ -28,24 +31,23 @@ L.GridLayer.TiledMarkers = L.GridLayer.extend({
         let html = "<span>Catalog Information</span>" +
                     "<table class='catalog-table'>";
 
-        const display_keys = Object.keys(json).filter(k => {return !dontDisplay.includes(k)});
+        // const display_keys = catalogColumns.filter(k => {return !dontDisplay.includes(k)});
         // console.log(display_keys);
 
         let colCounter = 0;
-        display_keys.forEach(key => {
-            // console.log(key);
-            if (colCounter==0){
-                html += "<tr>"
-            }
+        html += "<tr>";
+        for (let i = 0; i < nItems; i++){
+            let key = this.options.catalogColumns[i];
+            let value = values[i];
 
-            html += `<td><b>${key}:<b></td><td>${json[key]}</td>`;
+            html += `<td><b>${key}:<b></td><td>${value}</td>`;
 
             colCounter += 1;
             if (colCounter == nCols){
                 colCounter = 0;
                 html += "</tr>";
             }
-        });
+        }
 
         html += "</table>";
 
@@ -61,15 +63,17 @@ L.GridLayer.TiledMarkers = L.GridLayer.extend({
                 console.log(response);
                 throw new Error("Failed to fetch JSON", response);
             }
-            return response.json();
+            return response.arrayBuffer();
+        }).then(buffer =>{
+            return cbor.decodeAll(buffer);
         }).then(json => {
-            popup.setContent(_this.convertJSONtoHTMLTable(json)).update();
+            popup.setContent(_this.convertJSONtoHTMLTable(json[0])).update();
         })
         .catch((error) => {
             console.log("ERROR in Popup Rendering", error);
         });
 
-        console.log(marker);
+        // console.log(marker);
 
         // https://stackoverflow.com/a/51749619/2691018
         document.querySelector(".leaflet-popup-pane").addEventListener("load", function(event) {
@@ -96,12 +100,12 @@ L.GridLayer.TiledMarkers = L.GridLayer.extend({
             if (src.a==-1){
                 return L.circleMarker(latlng, {
                     color: this.options.color,
-                    assetPath: `catalog_assets/${src.cat_path}/${src.catalog_id}.json`
+                    assetPath: `catalog_assets/${src.cat_path}/${src.catalog_id}.cbor`
                 }).bindPopup(p);
             } else {
                 return L.ellipse(latlng, [src.a, src.b], (src.theta * (180/Math.PI) * -1), {
                     color: this.options.color,
-                    assetPath: `catalog_assets/${src.cat_path}/${src.catalog_id}.json`
+                    assetPath: `catalog_assets/${src.cat_path}/${src.catalog_id}.cbor`
                 }).bindPopup(p);
             }
         }
diff --git a/fitsmap/support/tiledMarkers.min.js b/fitsmap/support/tiledMarkers.min.js
index 3299570..91a6a7c 100644
--- a/fitsmap/support/tiledMarkers.min.js
+++ b/fitsmap/support/tiledMarkers.min.js
@@ -1,1 +1,1 @@
-"use strict";L.GridLayer.TiledMarkers=L.GridLayer.extend({options:{tileURL:"",color:"#4C72B0",rowsPerCol:1/0},initialize:function(t){L.setOptions(this,t),this.tilePointCache={}},convertJSONtoHTMLTable:function(t){const e=["fm_y","fm_x","fm_cat"],o=Object.keys(t).length-e.length,n=this.options.rowsPerColumn;let a=Math.floor(o/(Number.isFinite(n)?n:o));Number.isFinite(n)&&o%n>0&&(a+=1);let s="<span>Catalog Information</span><table class='catalog-table'>";const i=Object.keys(t).filter((t=>!e.includes(t)));let r=0;return i.forEach((e=>{0==r&&(s+="<tr>"),s+=`<td><b>${e}:<b></td><td>${t[e]}</td>`,r+=1,r==a&&(r=0,s+="</tr>")})),s+="</table>",s},renderPopupContents:function(t,e){const o=e.getPopup();return fetch(e.options.assetPath).then((t=>{if(!t.ok)throw console.log(t),new Error("Failed to fetch JSON",t);return t.json()})).then((e=>{o.setContent(t.convertJSONtoHTMLTable(e)).update()})).catch((t=>{console.log("ERROR in Popup Rendering",t)})),console.log(e),document.querySelector(".leaflet-popup-pane").addEventListener("load",(function(t){const e=t.target.tagName,o=map._popup;"IMG"===e&&o&&!o._updated&&(o._updated=!0,o.update())}),!0),"Loading..."},createClusterIcon:function(t){const e=L.latLng(t.global_y,t.global_x);if(!t.cluster){const o=L.popup({maxWidth:"auto"}).setLatLng(e).setContent((t=>this.renderPopupContents(this,t)));return-1==t.a?L.circleMarker(e,{color:this.options.color,assetPath:`catalog_assets/${t.cat_path}/${t.catalog_id}.json`}).bindPopup(o):L.ellipse(e,[t.a,t.b],t.theta*(180/Math.PI)*-1,{color:this.options.color,assetPath:`catalog_assets/${t.cat_path}/${t.catalog_id}.json`}).bindPopup(o)}const o=t.point_count,n=o<100?"small":o<1e3?"medium":o<1e6?"large":"x-large",a=L.divIcon({html:`<div><span>${t.point_count_abbreviated}</span></div>`,className:`marker-cluster marker-cluster-${n}`,iconSize:L.point(40,40)});return L.marker(e,{icon:a}).bindPopup(`${t.global_y}, ${t.global_x}`)},parseTileResponse:function(t,e){200==e.status&&e.arrayBuffer().then((e=>{this.tilePointCache[t]||(this.tilePointCache[t]=[]);const o=new Pbf(e),n=new VectorTile(o).layers.Points;for(let e=0;e<n.length;e++){let o=n.feature(e);this.tilePointCache[t].push(this.createClusterIcon(o.properties).addTo(map))}})).catch((t=>console.log(t)))},createTile:function(t,e){const o=2**t.z-t.y-1,n=(t.z,t.x,this.options.tileURL.replace("{z}",`${t.z}`).replace("{y}",`${o}`).replace("{x}",`${t.x}`)),a=`${t.z},${t.y},${t.x}`;return fetch(n).then((t=>this.parseTileResponse(a,t))).catch((t=>{console.log(t)})),L.DomUtil.create("canvas","leaflet-tile")},clearItems:function(t){const e=`${t.coords.z},${t.coords.y},${t.coords.x}`;if(this.tilePointCache[e])for(;this.tilePointCache[e].length;){let t=this.tilePointCache[e].pop().remove();t=null}}}),L.gridLayer.tiledMarkers=function(t){const e=new L.GridLayer.TiledMarkers(t);return e.on("tileunload",e.clearItems),e};
\ No newline at end of file
+"use strict";L.GridLayer.TiledMarkers=L.GridLayer.extend({options:{tileURL:"",color:"#4C72B0",rowsPerCol:1/0,catalogColumns:[]},initialize:function(t){L.setOptions(this,t),this.tilePointCache={}},convertJSONtoHTMLTable:function(t){const e=this.options.catalogColumns.length,o=this.options.rowsPerColumn,n=t.v;let a=Math.floor(e/(Number.isFinite(o)?o:e));Number.isFinite(o)&&e%o>0&&(a+=1);let r="<span>Catalog Information</span><table class='catalog-table'>",i=0;r+="<tr>";for(let t=0;t<e;t++){r+=`<td><b>${this.options.catalogColumns[t]}:<b></td><td>${n[t]}</td>`,i+=1,i==a&&(i=0,r+="</tr>")}return r+="</table>",r},renderPopupContents:function(t,e){const o=e.getPopup();return fetch(e.options.assetPath).then((t=>{if(!t.ok)throw console.log(t),new Error("Failed to fetch JSON",t);return t.arrayBuffer()})).then((t=>cbor.decodeAll(t))).then((e=>{o.setContent(t.convertJSONtoHTMLTable(e[0])).update()})).catch((t=>{console.log("ERROR in Popup Rendering",t)})),document.querySelector(".leaflet-popup-pane").addEventListener("load",(function(t){const e=t.target.tagName,o=map._popup;"IMG"===e&&o&&!o._updated&&(o._updated=!0,o.update())}),!0),"Loading..."},createClusterIcon:function(t){const e=L.latLng(t.global_y,t.global_x);if(!t.cluster){const o=L.popup({maxWidth:"auto"}).setLatLng(e).setContent((t=>this.renderPopupContents(this,t)));return-1==t.a?L.circleMarker(e,{color:this.options.color,assetPath:`catalog_assets/${t.cat_path}/${t.catalog_id}.cbor`}).bindPopup(o):L.ellipse(e,[t.a,t.b],t.theta*(180/Math.PI)*-1,{color:this.options.color,assetPath:`catalog_assets/${t.cat_path}/${t.catalog_id}.cbor`}).bindPopup(o)}const o=t.point_count,n=o<100?"small":o<1e3?"medium":o<1e6?"large":"x-large",a=L.divIcon({html:`<div><span>${t.point_count_abbreviated}</span></div>`,className:`marker-cluster marker-cluster-${n}`,iconSize:L.point(40,40)});return L.marker(e,{icon:a}).bindPopup(`${t.global_y}, ${t.global_x}`)},parseTileResponse:function(t,e){200==e.status&&e.arrayBuffer().then((e=>{this.tilePointCache[t]||(this.tilePointCache[t]=[]);const o=new Pbf(e),n=new VectorTile(o).layers.Points;for(let e=0;e<n.length;e++){let o=n.feature(e);this.tilePointCache[t].push(this.createClusterIcon(o.properties).addTo(map))}})).catch((t=>console.log(t)))},createTile:function(t,e){const o=2**t.z-t.y-1,n=(t.z,t.x,this.options.tileURL.replace("{z}",`${t.z}`).replace("{y}",`${o}`).replace("{x}",`${t.x}`)),a=`${t.z},${t.y},${t.x}`;return fetch(n).then((t=>this.parseTileResponse(a,t))).catch((t=>{console.log(t)})),L.DomUtil.create("canvas","leaflet-tile")},clearItems:function(t){const e=`${t.coords.z},${t.coords.y},${t.coords.x}`;if(this.tilePointCache[e])for(;this.tilePointCache[e].length;){let t=this.tilePointCache[e].pop().remove();t=null}}}),L.gridLayer.tiledMarkers=function(t){const e=new L.GridLayer.TiledMarkers(t);return e.on("tileunload",e.clearItems),e};
\ No newline at end of file
diff --git a/fitsmap/support/vector-tile.js.txt b/fitsmap/support/vector-tile.js
similarity index 100%
rename from fitsmap/support/vector-tile.js.txt
rename to fitsmap/support/vector-tile.js
diff --git a/requirements.txt b/requirements.txt
index 531d72f..cd389a1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,6 +7,7 @@ attrs==19.3.0
 Babel==2.9.1
 black==19.10b0
 bleach==3.3.0
+cbor2==5.4.2.post1
 certifi==2020.6.20
 cffi==1.14.5
 chardet==4.0.0
@@ -50,7 +51,7 @@ olefile==0.46
 packaging==20.0
 partd==1.2.0
 pathspec==0.6.0
-Pillow==8.3.2
+Pillow==9.1.0
 pkginfo==1.7.0
 pluggy==0.13.0
 protobuf==3.17.3
@@ -64,7 +65,7 @@ pylint==2.4.4
 pymongo==3.11.4
 pyparsing==2.4.5
 PyQt5==5.12.3
-PyQt5-sip==4.19.18
+PyQt5_sip==4.19.18
 PyQtWebEngine==5.12.1
 pytest==5.2.3
 pytest-arraydiff==0.3
diff --git a/setup.py b/setup.py
index c5bda37..da4ec81 100644
--- a/setup.py
+++ b/setup.py
@@ -36,6 +36,7 @@ def read(fname):
 
 REQUIRES = [
     "astropy",
+    "cbor2",
     "imageio",
     "numpy",
     "matplotlib",


Test patch:
diff --git a/fitsmap/tests/data/expected_test_tiling_image_mpl.tar.xz b/fitsmap/tests/data/expected_test_tiling_image_mpl.tar.xz
index c3f7546..f93464b 100644
Binary files a/fitsmap/tests/data/expected_test_tiling_image_mpl.tar.xz and b/fitsmap/tests/data/expected_test_tiling_image_mpl.tar.xz differ
diff --git a/fitsmap/tests/data/expected_test_tiling_image_pil.tar.xz b/fitsmap/tests/data/expected_test_tiling_image_pil.tar.xz
index b00e635..cd72c03 100644
Binary files a/fitsmap/tests/data/expected_test_tiling_image_pil.tar.xz and b/fitsmap/tests/data/expected_test_tiling_image_pil.tar.xz differ
diff --git a/fitsmap/tests/data/expected_test_web.tar.xz b/fitsmap/tests/data/expected_test_web.tar.xz
index 5ab2e67..1b9162c 100644
Binary files a/fitsmap/tests/data/expected_test_web.tar.xz and b/fitsmap/tests/data/expected_test_web.tar.xz differ
diff --git a/fitsmap/tests/data/test_index.html b/fitsmap/tests/data/test_index.html
index e14fad3..cfd493f 100644
--- a/fitsmap/tests/data/test_index.html
+++ b/fitsmap/tests/data/test_index.html
@@ -1,38 +1,42 @@
 <!DOCTYPE html>
-<html>
+<html lang="en">
 <head>
     <title>test</title>
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <link rel="shortcut icon" type="image/x-icon" href="imgs/favicon.ico" />
-    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css" integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA==" crossorigin=""/>
-    <link rel='stylesheet' href='https://unpkg.com/leaflet-search@2.9.8/dist/leaflet-search.src.css'/>
-    <link rel='stylesheet' href='css/MarkerCluster.Default.css'/>
-    <link rel='stylesheet' href='css/MarkerCluster.css'/>
-    <link rel='stylesheet' href='css/MarkerPopup.css'/>
-    <link rel='stylesheet' href='css/TileNearestNeighbor.css'/>
-    <script src='https://unpkg.com/leaflet@1.3.4/dist/leaflet.js' integrity='sha512-nMMmRyTVoLYqjP9hrbed9S+FzjZHW5gY1TWCHA5ckwXZBadntCNs8kEqAWdrb9O7rxbCaA4lKTIWjDXZxflOcA==' crossorigin=''></script>
-    <script src="https://unpkg.com/pbf@3.0.5/dist/pbf.js", crossorigin=""></script>
-    <script src="https://cdn.jsdelivr.net/npm/leaflet-search" crossorigin=""></script>
-    <script src='js/customSearch.min.js'></script>
-    <script src='js/l.ellipse.min.js'></script>
-    <script src='js/tiledMarkers.min.js'></script>
-    <script src='js/vector-tile.min.js'></script>
+    <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.css" integrity="sha512-1xoFisiGdy9nvho8EgXuXvnpR5GAMSjFwp40gSRE3NwdUdIMIKuPa7bqoUhLD0O/5tPNhteAsE5XyyMi5reQVA==" crossorigin="anonymous" referrerpolicy="no-referrer" as="style" onload="this.rel='stylesheet'"/>
+    <link rel='preload' href='https://unpkg.com/leaflet-search@2.9.8/dist/leaflet-search.src.css'  as='style' onload='this.rel="stylesheet"'/>
+    <link rel='preload' href='css/MarkerCluster.Default.min.css'  as='style' onload='this.rel="stylesheet"'/>
+    <link rel='preload' href='css/MarkerCluster.min.css'  as='style' onload='this.rel="stylesheet"'/>
+    <link rel='preload' href='css/MarkerPopup.min.css'  as='style' onload='this.rel="stylesheet"'/>
+    <link rel='preload' href='css/TileNearestNeighbor.min.css'  as='style' onload='this.rel="stylesheet"'/>
+    <script defer src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.js" integrity="sha512-SeiQaaDh73yrb56sTW/RgVdi/mMqNeM2oBwubFHagc5BkixSpP1fvqF47mKzPGWYSSy4RwbBunrJBQ4Co8fRWA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+    <script defer src='https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.js'></script>
+    <script defer src='https://cdnjs.cloudflare.com/ajax/libs/leaflet-search/3.0.2/leaflet-search.src.min.js'></script>
+    <script defer src='js/customSearch.min.js'></script>
+    <script defer src='js/tiledMarkers.min.js'></script>
+    <script defer src='js/urlCoords.js'></script>
+    <script defer src='js/index.js'></script>
+    <script defer src='https://unpkg.com/cbor-web@8.1.0/dist/cbor.js'></script>
+    <script defer src='https://unpkg.com/pbf@3.0.5/dist/pbf.js'></script>
+    <script defer src='js/l.ellipse.min.js'></script>
+    <script defer src='js/vector-tile.min.js'></script>
     <style>
-        html, body {
-            height: 100%;
-            margin: 0;
-        }
-        #map {
-            width: 100%;
-            height: 100%;
-        }
+        /* Map */
+        html,body{height:100%;padding:0;margin:0;font-family:Helvetica,Arial,sans-serif}#map{width:100%;height:100%;visibility:hidden}
+        /* Loading Page */
+        .overlay{background:#fff;height:100vh;width:100%;position:absolute}.brand{position:absolute;top:100px;left:50%;transform:translateX(-50%)}.brand img{width:100%;height:auto}.loadingtext{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-weight:700;font-size:xx-large}.loading{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);-moz-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);-o-transform:translate(-50%,-50%);transform:translate(-50%,-50%);border-bottom:16px solid #0044aaff;border-top:16px solid #0044aaff;border-left:16px solid #80b3ffff;border-right:16px solid #80b3ffff;width:250px;height:250px;-webkit-border-radius:50%;-moz-border-radius:50%;border-radius:50%;-webkit-animation:rotate 1s ease-in-out infinite;-o-animation:rotate 1s ease-in-out infinite;animation:rotate 1s ease-in-out infinite}
+        @keyframes rotate{0%{-webkit-transform:translate(-50%,-50%) rotate(0deg);-moz-transform:translate(-50%,-50%) rotate(0deg);-ms-transform:translate(-50%,-50%) rotate(0deg);-o-transform:translate(-50%,-50%) rotate(0deg);transform:translate(-50%,-50%) rotate(0deg)}100%{-webkit-transform:translate(-50%,-50%) rotate(360deg);-moz-transform:translate(-50%,-50%) rotate(360deg);-ms-transform:translate(-50%,-50%) rotate(360deg);-o-transform:translate(-50%,-50%) rotate(360deg);transform:translate(-50%,-50%) rotate(360deg)}}
     </style>
 </head>
 <body>
+    <div id="loading-screen" class="overlay">
+        <div class="brand"><img src="imgs/loading-logo.svg" /></div>
+        <div class="loading"></div>
+        <div class="loadingtext">Loading...</div>
+    </div>
     <div id="map"></div>
-    <script src="js/urlCoords.js"></script>
-    <script src="js/index.js"></script>
 </body>
-<!--Made with fitsmap v0.8.0-->
+<!--Made with fitsmap v0.8.5-->
 </html>
diff --git a/fitsmap/tests/data/test_index_wcs.html b/fitsmap/tests/data/test_index_wcs.html
index e14fad3..cfd493f 100644
--- a/fitsmap/tests/data/test_index_wcs.html
+++ b/fitsmap/tests/data/test_index_wcs.html
@@ -1,38 +1,42 @@
 <!DOCTYPE html>
-<html>
+<html lang="en">
 <head>
     <title>test</title>
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <link rel="shortcut icon" type="image/x-icon" href="imgs/favicon.ico" />
-    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css" integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA==" crossorigin=""/>
-    <link rel='stylesheet' href='https://unpkg.com/leaflet-search@2.9.8/dist/leaflet-search.src.css'/>
-    <link rel='stylesheet' href='css/MarkerCluster.Default.css'/>
-    <link rel='stylesheet' href='css/MarkerCluster.css'/>
-    <link rel='stylesheet' href='css/MarkerPopup.css'/>
-    <link rel='stylesheet' href='css/TileNearestNeighbor.css'/>
-    <script src='https://unpkg.com/leaflet@1.3.4/dist/leaflet.js' integrity='sha512-nMMmRyTVoLYqjP9hrbed9S+FzjZHW5gY1TWCHA5ckwXZBadntCNs8kEqAWdrb9O7rxbCaA4lKTIWjDXZxflOcA==' crossorigin=''></script>
-    <script src="https://unpkg.com/pbf@3.0.5/dist/pbf.js", crossorigin=""></script>
-    <script src="https://cdn.jsdelivr.net/npm/leaflet-search" crossorigin=""></script>
-    <script src='js/customSearch.min.js'></script>
-    <script src='js/l.ellipse.min.js'></script>
-    <script src='js/tiledMarkers.min.js'></script>
-    <script src='js/vector-tile.min.js'></script>
+    <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.css" integrity="sha512-1xoFisiGdy9nvho8EgXuXvnpR5GAMSjFwp40gSRE3NwdUdIMIKuPa7bqoUhLD0O/5tPNhteAsE5XyyMi5reQVA==" crossorigin="anonymous" referrerpolicy="no-referrer" as="style" onload="this.rel='stylesheet'"/>
+    <link rel='preload' href='https://unpkg.com/leaflet-search@2.9.8/dist/leaflet-search.src.css'  as='style' onload='this.rel="stylesheet"'/>
+    <link rel='preload' href='css/MarkerCluster.Default.min.css'  as='style' onload='this.rel="stylesheet"'/>
+    <link rel='preload' href='css/MarkerCluster.min.css'  as='style' onload='this.rel="stylesheet"'/>
+    <link rel='preload' href='css/MarkerPopup.min.css'  as='style' onload='this.rel="stylesheet"'/>
+    <link rel='preload' href='css/TileNearestNeighbor.min.css'  as='style' onload='this.rel="stylesheet"'/>
+    <script defer src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.js" integrity="sha512-SeiQaaDh73yrb56sTW/RgVdi/mMqNeM2oBwubFHagc5BkixSpP1fvqF47mKzPGWYSSy4RwbBunrJBQ4Co8fRWA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+    <script defer src='https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.js'></script>
+    <script defer src='https://cdnjs.cloudflare.com/ajax/libs/leaflet-search/3.0.2/leaflet-search.src.min.js'></script>
+    <script defer src='js/customSearch.min.js'></script>
+    <script defer src='js/tiledMarkers.min.js'></script>
+    <script defer src='js/urlCoords.js'></script>
+    <script defer src='js/index.js'></script>
+    <script defer src='https://unpkg.com/cbor-web@8.1.0/dist/cbor.js'></script>
+    <script defer src='https://unpkg.com/pbf@3.0.5/dist/pbf.js'></script>
+    <script defer src='js/l.ellipse.min.js'></script>
+    <script defer src='js/vector-tile.min.js'></script>
     <style>
-        html, body {
-            height: 100%;
-            margin: 0;
-        }
-        #map {
-            width: 100%;
-            height: 100%;
-        }
+        /* Map */
+        html,body{height:100%;padding:0;margin:0;font-family:Helvetica,Arial,sans-serif}#map{width:100%;height:100%;visibility:hidden}
+        /* Loading Page */
+        .overlay{background:#fff;height:100vh;width:100%;position:absolute}.brand{position:absolute;top:100px;left:50%;transform:translateX(-50%)}.brand img{width:100%;height:auto}.loadingtext{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-weight:700;font-size:xx-large}.loading{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);-moz-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);-o-transform:translate(-50%,-50%);transform:translate(-50%,-50%);border-bottom:16px solid #0044aaff;border-top:16px solid #0044aaff;border-left:16px solid #80b3ffff;border-right:16px solid #80b3ffff;width:250px;height:250px;-webkit-border-radius:50%;-moz-border-radius:50%;border-radius:50%;-webkit-animation:rotate 1s ease-in-out infinite;-o-animation:rotate 1s ease-in-out infinite;animation:rotate 1s ease-in-out infinite}
+        @keyframes rotate{0%{-webkit-transform:translate(-50%,-50%) rotate(0deg);-moz-transform:translate(-50%,-50%) rotate(0deg);-ms-transform:translate(-50%,-50%) rotate(0deg);-o-transform:translate(-50%,-50%) rotate(0deg);transform:translate(-50%,-50%) rotate(0deg)}100%{-webkit-transform:translate(-50%,-50%) rotate(360deg);-moz-transform:translate(-50%,-50%) rotate(360deg);-ms-transform:translate(-50%,-50%) rotate(360deg);-o-transform:translate(-50%,-50%) rotate(360deg);transform:translate(-50%,-50%) rotate(360deg)}}
     </style>
 </head>
 <body>
+    <div id="loading-screen" class="overlay">
+        <div class="brand"><img src="imgs/loading-logo.svg" /></div>
+        <div class="loading"></div>
+        <div class="loadingtext">Loading...</div>
+    </div>
     <div id="map"></div>
-    <script src="js/urlCoords.js"></script>
-    <script src="js/index.js"></script>
 </body>
-<!--Made with fitsmap v0.8.0-->
+<!--Made with fitsmap v0.8.5-->
 </html>
diff --git a/fitsmap/tests/helpers.py b/fitsmap/tests/helpers.py
index ce9042c..bb34318 100644
--- a/fitsmap/tests/helpers.py
+++ b/fitsmap/tests/helpers.py
@@ -144,7 +144,7 @@ def recursive_diff(dir_compare):
             sub_match = True
 
         if len(dir_compare.diff_files):
-            print(dir_compare.diff_files)
+            print(dir_compare.left, dir_compare.right, dir_compare.diff_files)
 
         return sub_match and len(dir_compare.diff_files) == 0
 
diff --git a/fitsmap/tests/test_cartographer.py b/fitsmap/tests/test_cartographer.py
index 46a9ddd..672d36c 100644
--- a/fitsmap/tests/test_cartographer.py
+++ b/fitsmap/tests/test_cartographer.py
@@ -33,12 +33,13 @@
 @pytest.mark.cartographer
 def test_layer_name_to_dict_image():
     """test cartographer.layer_name_to_dict"""
+    out_dir = "."
     min_zoom = 0
     max_zoom = 2
     name = "test"
     color = ""  # "#4C72B0"
 
-    actual_dict = c.layer_name_to_dict(min_zoom, max_zoom, name, color)
+    actual_dict = c.layer_name_to_dict(out_dir, min_zoom, max_zoom, name, color)
 
     expected_dict = dict(
         directory=name + "/{z}/{y}/{x}.png",
@@ -55,12 +56,18 @@ def test_layer_name_to_dict_image():
 @pytest.mark.cartographer
 def test_layer_name_to_dict_catalog():
     """test cartographer.layer_name_to_dict"""
+    helpers.setup()
+    out_dir = helpers.DATA_DIR
     min_zoom = 0
     max_zoom = 2
     name = "test"
     color = "#4C72B0"
+    columns="a,b,c"
+
+    with open(os.path.join(out_dir,f"{name}.columns"), "w") as f:
+        f.write(columns)
 
-    actual_dict = c.layer_name_to_dict(min_zoom, max_zoom, name, color)
+    actual_dict = c.layer_name_to_dict(out_dir, min_zoom, max_zoom, name, color)
 
     expected_dict = dict(
         directory=name + "/{z}/{y}/{x}.pbf",
@@ -69,8 +76,11 @@ def test_layer_name_to_dict_catalog():
         max_zoom=max_zoom + 5,
         max_native_zoom=max_zoom,
         color=color,
+        columns=[f'"{c}"' for c in columns.split(",")]
     )
 
+    helpers.tear_down()
+
     assert expected_dict == actual_dict
 
 
@@ -119,6 +129,7 @@ def test_cat_layer_dict_to_str():
     min_zoom = 0
     max_zoom = 2
     name = "test"
+    columns="a,b,c"
 
     layer_dict = dict(
         directory=name + "/{z}/{y}/{x}.png",
@@ -127,6 +138,7 @@ def test_cat_layer_dict_to_str():
         max_zoom=max_zoom + 5,
         max_native_zoom=max_zoom,
         color="red",
+        columns=[f'"{c}"' for c in columns.split(",")]
     )
 
     actual_str = c.cat_layer_dict_to_str(layer_dict, float("inf"))
@@ -139,6 +151,7 @@ def test_cat_layer_dict_to_str():
             'tileURL:"' + layer_dict["directory"] + '", ',
             'color: "' + layer_dict["color"] + '", ',
             f"rowsPerColumn: Infinity, ",
+            f'catalogColumns: [{",".join(layer_dict["columns"])}],',
             "minZoom: " + str(layer_dict["min_zoom"]) + ", ",
             "maxZoom: " + str(layer_dict["max_zoom"]) + ", ",
             "maxNativeZoom: " + str(layer_dict["max_native_zoom"]) + " ",
@@ -146,6 +159,8 @@ def test_cat_layer_dict_to_str():
         ]
     )
 
+    helpers.tear_down()
+
     assert expected_str == actual_str
 
 
@@ -205,7 +220,9 @@ def test_get_colors():
         "#64B5CD",
     ]
 
-    assert expected == c.get_colors()
+    color_iter = c.get_colors()
+
+    assert expected == [next(color_iter) for _ in range(len(expected))]
 
 
 @pytest.mark.unit
@@ -279,15 +296,14 @@ def test_build_conditional_css():
 
     actual_css = c.build_conditional_css(helpers.TEST_PATH)
 
-    expected_css = "\n".join(
-        [
-            "    <link rel='stylesheet' href='https://unpkg.com/leaflet-search@2.9.8/dist/leaflet-search.src.css'/>",
-            "    <link rel='stylesheet' href='css/MarkerCluster.Default.css'/>",
-            "    <link rel='stylesheet' href='css/MarkerCluster.css'/>",
-            "    <link rel='stylesheet' href='css/MarkerPopup.css'/>",
-            "    <link rel='stylesheet' href='css/TileNearestNeighbor.css'/>",
-        ]
-    )
+    expected_css = "\n".join([
+        "    <link rel='preload' href='https://unpkg.com/leaflet-search@2.9.8/dist/leaflet-search.src.css'  as='style' onload='this.rel=\"stylesheet\"'/>",
+        "    <link rel='preload' href='css/MarkerCluster.Default.min.css'  as='style' onload='this.rel=\"stylesheet\"'/>",
+        "    <link rel='preload' href='css/MarkerCluster.min.css'  as='style' onload='this.rel=\"stylesheet\"'/>",
+        "    <link rel='preload' href='css/MarkerPopup.min.css'  as='style' onload='this.rel=\"stylesheet\"'/>",
+        "    <link rel='preload' href='css/TileNearestNeighbor.min.css'  as='style' onload='this.rel=\"stylesheet\"'/>",
+    ])
+
 
     helpers.tear_down()
 
@@ -303,16 +319,18 @@ def test_build_conditional_js():
 
     acutal_js = c.build_conditional_js(helpers.TEST_PATH)
 
-    expected_js = "\n".join(
-        [
-            '    <script src="https://unpkg.com/pbf@3.0.5/dist/pbf.js", crossorigin=""></script>',
-            '    <script src="https://cdn.jsdelivr.net/npm/leaflet-search" crossorigin=""></script>',
-            "    <script src='js/customSearch.min.js'></script>",
-            "    <script src='js/l.ellipse.min.js'></script>",
-            "    <script src='js/tiledMarkers.min.js'></script>",
-            "    <script src='js/vector-tile.min.js'></script>",
-        ]
-    )
+    expected_js = "\n".join([
+        "    <script defer src='https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.js'></script>",
+        "    <script defer src='https://cdnjs.cloudflare.com/ajax/libs/leaflet-search/3.0.2/leaflet-search.src.min.js'></script>",
+        "    <script defer src='js/customSearch.min.js'></script>",
+        "    <script defer src='js/tiledMarkers.min.js'></script>",
+        "    <script defer src='js/urlCoords.js'></script>",
+        "    <script defer src='js/index.js'></script>",
+        "    <script defer src='https://unpkg.com/cbor-web@8.1.0/dist/cbor.js'></script>",
+        "    <script defer src='https://unpkg.com/pbf@3.0.5/dist/pbf.js'></script>",
+        "    <script defer src='js/l.ellipse.min.js'></script>",
+        "    <script defer src='js/vector-tile.min.js'></script>",
+    ])
 
     helpers.tear_down()
 
@@ -328,7 +346,7 @@ def test_move_support_images():
 
     actual_moved_images = c.move_support_images(helpers.TEST_PATH)
 
-    expected_moved_images = ["favicon.ico"]
+    expected_moved_images = ["favicon.ico", "loading-logo.svg"]
 
     helpers.tear_down()
 
@@ -349,31 +367,31 @@ def test_build_html():
     expected_html = "\n".join(
         [
             "<!DOCTYPE html>",
-            "<html>",
+            '<html lang="en">',
             "<head>",
             "    <title>{}</title>".format(title),
             '    <meta charset="utf-8" />',
             '    <meta name="viewport" content="width=device-width, initial-scale=1.0">',
             '    <link rel="shortcut icon" type="image/x-icon" href="imgs/favicon.ico" />',
-            '    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css" integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA==" crossorigin=""/>',
+            '    <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.css" integrity="sha512-1xoFisiGdy9nvho8EgXuXvnpR5GAMSjFwp40gSRE3NwdUdIMIKuPa7bqoUhLD0O/5tPNhteAsE5XyyMi5reQVA==" crossorigin="anonymous" referrerpolicy="no-referrer" as="style" onload="this.rel=\'stylesheet\'"/>',
             extra_css,
-            "    <script src='https://unpkg.com/leaflet@1.3.4/dist/leaflet.js' integrity='sha512-nMMmRyTVoLYqjP9hrbed9S+FzjZHW5gY1TWCHA5ckwXZBadntCNs8kEqAWdrb9O7rxbCaA4lKTIWjDXZxflOcA==' crossorigin=''></script>",
+            '    <script defer src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.js" integrity="sha512-SeiQaaDh73yrb56sTW/RgVdi/mMqNeM2oBwubFHagc5BkixSpP1fvqF47mKzPGWYSSy4RwbBunrJBQ4Co8fRWA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>',
             extra_js,
             "    <style>",
-            "        html, body {",
-            "            height: 100%;",
-            "            margin: 0;",
-            "        }",
-            "        #map {",
-            "            width: 100%;",
-            "            height: 100%;",
-            "        }",
+            "        /* Map */",
+            r"        html,body{height:100%;padding:0;margin:0;font-family:Helvetica,Arial,sans-serif}#map{width:100%;height:100%;visibility:hidden}",
+            "        /* Loading Page */",
+            r"        .overlay{background:#fff;height:100vh;width:100%;position:absolute}.brand{position:absolute;top:100px;left:50%;transform:translateX(-50%)}.brand img{width:100%;height:auto}.loadingtext{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-weight:700;font-size:xx-large}.loading{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);-moz-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);-o-transform:translate(-50%,-50%);transform:translate(-50%,-50%);border-bottom:16px solid #0044aaff;border-top:16px solid #0044aaff;border-left:16px solid #80b3ffff;border-right:16px solid #80b3ffff;width:250px;height:250px;-webkit-border-radius:50%;-moz-border-radius:50%;border-radius:50%;-webkit-animation:rotate 1s ease-in-out infinite;-o-animation:rotate 1s ease-in-out infinite;animation:rotate 1s ease-in-out infinite}",
+            r"        @keyframes rotate{0%{-webkit-transform:translate(-50%,-50%) rotate(0deg);-moz-transform:translate(-50%,-50%) rotate(0deg);-ms-transform:translate(-50%,-50%) rotate(0deg);-o-transform:translate(-50%,-50%) rotate(0deg);transform:translate(-50%,-50%) rotate(0deg)}100%{-webkit-transform:translate(-50%,-50%) rotate(360deg);-moz-transform:translate(-50%,-50%) rotate(360deg);-ms-transform:translate(-50%,-50%) rotate(360deg);-o-transform:translate(-50%,-50%) rotate(360deg);transform:translate(-50%,-50%) rotate(360deg)}}",
             "    </style>",
             "</head>",
             "<body>",
+        '    <div id="loading-screen" class="overlay">',
+        '        <div class="brand"><img src="imgs/loading-logo.svg" /></div>',
+        '        <div class="loading"></div>',
+        '        <div class="loadingtext">Loading...</div>',
+        '    </div>',
             '    <div id="map"></div>',
-            '    <script src="js/urlCoords.js"></script>',
-            '    <script src="js/index.js"></script>',
             "</body>",
             f"<!--Made with fitsmap v{helpers.get_version()}-->",
             "</html>\n",
@@ -395,6 +413,11 @@ def test_chart_no_wcs():
     map_layer_names = "test_layer"
     marker_file_names = "test_marker"
     wcs = None
+    columns="a,b,c"
+
+    with open(os.path.join(out_dir, f"{marker_file_names}.columns"), "w") as f:
+        f.write(columns)
+
 
     list(
         map(
@@ -446,6 +469,10 @@ def test_chart_with_wcs():
     map_layer_names = "test_layer"
     marker_file_names = "test_marker"
     wcs = WCS(os.path.join(out_dir, "test_image.fits"))
+    columns="a,b,c"
+
+    with open(os.path.join(out_dir, f"{marker_file_names}.columns"), "w") as f:
+        f.write(columns)
 
     list(
         map(
diff --git a/fitsmap/tests/test_convert.py b/fitsmap/tests/test_convert.py
index b345d2c..82c4974 100644
--- a/fitsmap/tests/test_convert.py
+++ b/fitsmap/tests/test_convert.py
@@ -679,7 +679,7 @@ def test_tile_img_mpl_parallel():
     assert dirs_match
 
 
-@pytest.mark.integration
+# @pytest.mark.integration
 @pytest.mark.convert
 @pytest.mark.filterwarnings("ignore:.*:astropy.io.fits.verify.VerifyWarning")
 def test_files_to_map():