Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 87 additions & 63 deletions tools/jbrowse2/jbrowse2.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,27 +131,23 @@ def metadata_from_node(node):


class JbrowseConnector(object):
def __init__(self, jbrowse, outdir, update):
def __init__(self, jbrowse, outdir, update, use_canvas_renderer=True, enable_workspaces=True, show_legends=True):
self.jbrowse = jbrowse
self.outdir = outdir
self.update = update

self.tracksToIndex = {}
self.use_canvas_renderer = use_canvas_renderer
self.enable_workspaces = enable_workspaces
self.show_legends = show_legends

# This is the id of the current assembly
self.tracksToIndex = {}
self.assembly_ids = {}

self.default_views = {}

self.plugins = []

self.use_synteny_viewer = False

self.synteny_tracks = []

self.clone_jbrowse(self.jbrowse, self.outdir)

# If upgrading, look at the existing data
self.check_existing(self.outdir)

def get_cwd(self, cwd):
Expand Down Expand Up @@ -199,7 +195,7 @@ def symlink_or_copy(self, src, dest):

def _prepare_track_style(self, xml_conf):
style_data = {
"type": "LinearBasicDisplay", # No ideal default, but should be overwritten anyway
"type": "LinearBasicDisplay", # No ideal default, but should be overwritten anyway
}

if "display" in xml_conf["style"]:
Expand All @@ -212,16 +208,13 @@ def _prepare_track_style(self, xml_conf):
return {"displays": [style_data]}

def _prepare_renderer_config(self, display_type, xml_conf):

style_data = {}

# if display_type in ("LinearBasicDisplay", "LinearVariantDisplay"):
# TODO LinearVariantDisplay does not understand these options when written in config.json
if display_type in ("LinearBasicDisplay"):
if display_type in ("LinearBasicDisplay",):

# Doc: https://jbrowse.org/jb2/docs/config/svgfeaturerenderer/
# Doc: https://jbrowse.org/jb2/docs/config/arcrenderer/
style_data["renderer"] = {
"type": "SvgFeatureRenderer",
"type": "CanvasFeatureRenderer",
"showLabels": xml_conf.get("show_labels", True),
"showDescriptions": xml_conf.get("show_descriptions", True),
"labels": {
Expand All @@ -233,36 +226,24 @@ def _prepare_renderer_config(self, display_type, xml_conf):
}

elif display_type == "LinearArcDisplay":

# Doc: https://jbrowse.org/jb2/docs/config/arcrenderer/
style_data["renderer"] = {
"type": "ArcRenderer",
"label": xml_conf.get("labels_name", "jexl:get(feature,'score')"),
"displayMode": xml_conf.get("display_mode", "arcs"),
}

elif display_type == "LinearWiggleDisplay":

wig_renderer = xml_conf.get("renderer", "xyplot")
style_data["defaultRendering"] = wig_renderer

elif display_type == "MultiLinearWiggleDisplay":

wig_renderer = xml_conf.get("renderer", "multirowxy")
style_data["defaultRendering"] = wig_renderer

elif display_type == "LinearSNPCoverageDisplay":

# Does not work
# style_data["renderer"] = {
# "type": "SNPCoverageRenderer",
# "displayCrossHatches": xml_conf.get("display_cross_hatches", True),
# }

style_data["scaleType"] = xml_conf.get("scale_type", "linear")
if "min_score" in xml_conf:
style_data["minScore"] = xml_conf["min_score"]

if "max_score" in xml_conf:
style_data["maxScore"] = xml_conf["max_score"]

Expand Down Expand Up @@ -355,6 +336,8 @@ def _load_old_synteny_views(self):

def add_assembly(self, path, label, is_remote=False, cytobands=None, ref_name_aliases=None):

label = re.sub(r"[/\\]", " ", label) # sanitize path-unsafe characters

if not is_remote:
# Find a non-existing filename for the new genome
# (to avoid colision when upgrading an existing instance)
Expand Down Expand Up @@ -663,31 +646,39 @@ def add_xam(self, parent, data, trackData, xamOpts, index=None, ext="bam", **kwa

if trackData['remote']:
rel_dest = data
# Index will be set automatically as xam url + xai .suffix by add-track cmd
else:
rel_dest = os.path.join("data", trackData["label"] + f".{ext}")
dest = os.path.join(self.outdir, rel_dest)
self.symlink_or_copy(os.path.realpath(data), dest)

if index is not None and os.path.exists(os.path.realpath(index)):
# xai most probably made by galaxy and stored in galaxy dirs, need to copy it to dest
self.subprocess_check_call(
["cp", os.path.realpath(index), dest + f".{index_ext}"]
)
else:
# Can happen in exotic condition
# e.g. if bam imported as symlink with datatype=unsorted.bam, then datatype changed to bam
# => no index generated by galaxy, but there might be one next to the symlink target
# this trick allows to skip the bam sorting made by galaxy if already done outside
if os.path.exists(os.path.realpath(data) + f".{index_ext}"):
self.symlink_or_copy(
os.path.realpath(data) + f".{index_ext}", dest + f".{index_ext}"
)
else:
log.warn(
f"Could not find a bam index (.{index_ext} file) for {data}"
f"Could not find a {ext} index (.{index_ext} file) for {data}"
)

style_json = self._prepare_track_style(trackData)
track_metadata = self._prepare_track_metadata(trackData)
style_json.update(track_metadata)

self._add_track(
trackData["label"],
trackData["key"],
trackData["category"],
rel_dest,
parent,
config=style_json,
remote=trackData['remote']
)

style_json = self._prepare_track_style(trackData)

track_metadata = self._prepare_track_metadata(trackData)
Expand Down Expand Up @@ -751,19 +742,19 @@ def add_gff(self, parent, data, format, trackData, gffOpts, **kwargs):
rel_dest = os.path.join("data", trackData["label"] + ".gff")
dest = os.path.join(self.outdir, rel_dest)
rel_dest = rel_dest + ".gz"

self._sort_gff(data, dest)

style_json = self._prepare_track_style(trackData)

formatdetails = self._prepare_format_details(trackData)

style_json.update(formatdetails)

track_metadata = self._prepare_track_metadata(trackData)

style_json.update(track_metadata)

if "displays" in style_json:
for display in style_json["displays"]:
if "renderer" in display and display["renderer"]["type"] == "SvgFeatureRenderer":
display["renderer"]["type"] = "CanvasFeatureRenderer"

if gffOpts.get('index', 'false') in ("yes", "true", "True"):
if parent['uniq_id'] not in self.tracksToIndex:
self.tracksToIndex[parent['uniq_id']] = []
Expand Down Expand Up @@ -806,15 +797,16 @@ def add_gtf(self, parent, data, format, trackData, gffOpts, **kwargs):
}

style_json = self._prepare_track_style(trackData)

formatdetails = self._prepare_format_details(trackData)

style_json.update(formatdetails)

track_metadata = self._prepare_track_metadata(trackData)

style_json.update(track_metadata)

if "displays" in style_json:
for display in style_json["displays"]:
if "renderer" in display and display["renderer"]["type"] == "SvgFeatureRenderer":
display["renderer"]["type"] = "CanvasFeatureRenderer"

json_track_data.update(style_json)

self.subprocess_check_call(
Expand All @@ -834,19 +826,19 @@ def add_bed(self, parent, data, format, trackData, gffOpts, **kwargs):
rel_dest = os.path.join("data", trackData["label"] + ".bed")
dest = os.path.join(self.outdir, rel_dest)
rel_dest = rel_dest + ".gz"

self._sort_bed(data, dest)

style_json = self._prepare_track_style(trackData)

formatdetails = self._prepare_format_details(trackData)

style_json.update(formatdetails)

track_metadata = self._prepare_track_metadata(trackData)

style_json.update(track_metadata)

if "displays" in style_json:
for display in style_json["displays"]:
if "renderer" in display and display["renderer"]["type"] == "SvgFeatureRenderer":
display["renderer"]["type"] = "CanvasFeatureRenderer"

if gffOpts.get('index', 'false') in ("yes", "true", "True"):
if parent['uniq_id'] not in self.tracksToIndex:
self.tracksToIndex[parent['uniq_id']] = []
Expand Down Expand Up @@ -1339,7 +1331,7 @@ def process_annotations(self, track, parent):
parent,
dataset_path,
outputTrackConfig,
track["conf"]["options"]["synteny"]
track["conf"].get("options", {}).get("synteny", {})
)
elif dataset_ext in ("hic"):
self.add_hic(
Expand Down Expand Up @@ -1514,24 +1506,27 @@ def add_default_session(self, default_views):
with open(config_path, "r") as config_file:
config_json = json.load(config_file)

if "defaultSession" not in config_json:
config_json["defaultSession"] = {}
config_json["defaultSession"].update(session_spec)

with open(config_path, "w") as config_file:
json.dump(config_json, config_file, indent=2)

def add_general_configuration(self, data):
"""
Add some general configuration to the config.json file
Add general configuration to the config.json file, including v4.x options.
"""

config_path = os.path.join(self.outdir, "config.json")
with open(config_path, "r") as config_file:
config_json = json.load(config_file)

config_data = {}

config_data["disableAnalytics"] = data.get("analytics", "false") == "true"

config_data["workspaces"] = self.enable_workspaces
config_data["showLegends"] = self.show_legends

config_data["theme"] = {
"palette": {
"primary": {"main": data.get("primary_color", "#0D233F")},
Expand All @@ -1541,7 +1536,12 @@ def add_general_configuration(self, data):
},
"typography": {"fontSize": int(data.get("font_size", 10))},
}
config_data["useCanvasRenderer"] = data.get("use_canvas_renderer", "true") == "true"
config_data["enableWorkspaces"] = data.get("enable_workspaces", "true") == "true"
config_data["showLegends"] = data.get("show_legends", "true") == "true"

if "configuration" not in config_json:
config_json["configuration"] = {}
config_json["configuration"].update(config_data)

with open(config_path, "w") as config_file:
Expand All @@ -1566,11 +1566,9 @@ def add_plugins(self, data):

def clone_jbrowse(self, jbrowse_dir, destination):
"""
Clone a JBrowse directory into a destination directory.

Not using `jbrowse create` command to allow running on internet-less compute + to make sure code is frozen
Clone a JBrowse directory into a destination directory.
For v4.x, ensure all static files (including WebGPU/WebGL assets) are copied.
"""

copytree(jbrowse_dir, destination)
try:
shutil.rmtree(os.path.join(destination, "test_data"))
Expand Down Expand Up @@ -1626,12 +1624,19 @@ def validate_synteny(real_root):


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="", epilog="")
parser = argparse.ArgumentParser(description="JBrowse2 Galaxy wrapper", epilog="")
parser.add_argument("xml", type=argparse.FileType("r"), help="Track Configuration")

parser.add_argument('--jbrowse', help='Folder containing a jbrowse release')
parser.add_argument("--update", help="Update an existing JBrowse2 instance", action="store_true")
parser.add_argument("--outdir", help="Output directory", default="out")
parser.add_argument("--use-canvas-renderer", action="store_true", default=True,
help="Use CanvasFeatureRenderer for feature tracks (recommended for v4.x).")
parser.add_argument("--enable-workspaces", action="store_true", default=True,
help="Enable tiled window management (Workspaces) in v4.x.")
parser.add_argument("--show-legends", action="store_true", default=True,
help="Show color legends in v4.x.")

args = parser.parse_args()

tree = ET.parse(args.xml.name)
Expand All @@ -1645,10 +1650,33 @@ def validate_synteny(real_root):
# be GET and not POST so it should redirect OK
GALAXY_INFRASTRUCTURE_URL = "http://" + GALAXY_INFRASTRUCTURE_URL

use_canvas_renderer = real_root.find("metadata/general/useCanvasRenderer")
enable_workspaces = real_root.find("metadata/general/enableWorkspaces")
show_legends = real_root.find("metadata/general/showLegends")

use_canvas_renderer = (
use_canvas_renderer.text.lower() == "true"
if use_canvas_renderer is not None and use_canvas_renderer.text is not None
else args.use_canvas_renderer
)
enable_workspaces = (
enable_workspaces.text.lower() == "true"
if enable_workspaces is not None and enable_workspaces.text is not None
else args.enable_workspaces
)
show_legends = (
show_legends.text.lower() == "true"
if show_legends is not None and show_legends.text is not None
else args.show_legends
)

jc = JbrowseConnector(
jbrowse=args.jbrowse,
outdir=args.outdir,
update=args.update,
use_canvas_renderer=use_canvas_renderer if isinstance(use_canvas_renderer, bool) else (use_canvas_renderer.lower() == "true" if isinstance(use_canvas_renderer, str) else args.use_canvas_renderer),
enable_workspaces=enable_workspaces if isinstance(enable_workspaces, bool) else (enable_workspaces.lower() == "true" if isinstance(enable_workspaces, str) else args.enable_workspaces),
show_legends=show_legends if isinstance(show_legends, bool) else (show_legends.lower() == "true" if isinstance(show_legends, str) else args.show_legends),
)

# Synteny options are special, check them first
Expand Down Expand Up @@ -1750,10 +1778,6 @@ def validate_synteny(real_root):
item.tag: parse_style_conf(item) for item in (track.find("options/style") or [])
}

track_conf["style"] = {
item.tag: parse_style_conf(item) for item in (track.find("options/style") or [])
}

track_conf["style_labels"] = {
item.tag: parse_style_conf(item)
for item in (track.find("options/style_labels") or [])
Expand Down Expand Up @@ -1806,4 +1830,4 @@ def validate_synteny(real_root):
jc.add_default_session(views_for_session)
jc.add_general_configuration(general_data)
jc.add_plugins(jc.plugins)
jc.text_index()
jc.text_index()
Loading
Loading