Mercurial > repos > iuc > jbrowse2
diff jbrowse2.py @ 0:61add3f58f26 draft default tip
planemo upload for repository https://github.com/galaxyproject/tools-iuc/tree/master/tools/jbrowse2 commit 4fa86613193c985e0cb9a8fc795c56b8bc7b8532
| author | iuc |
|---|---|
| date | Thu, 02 Oct 2025 10:19:44 +0000 |
| parents | |
| children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jbrowse2.py Thu Oct 02 10:19:44 2025 +0000 @@ -0,0 +1,1750 @@ +#!/usr/bin/env python +import argparse +import csv +import datetime +import hashlib +import json +import logging +import os +import re +import shutil +import subprocess +import xml.etree.ElementTree as ET +from collections import defaultdict + +import requests + + +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger("jbrowse") +TODAY = datetime.datetime.now().strftime("%Y-%m-%d") +SELF_LOCATION = os.path.dirname(os.path.realpath(__file__)) +GALAXY_INFRASTRUCTURE_URL = None +mapped_chars = { + ">": "__gt__", + "<": "__lt__", + "'": "__sq__", + '"': "__dq__", + "[": "__ob__", + "]": "__cb__", + "{": "__oc__", + "}": "__cc__", + "@": "__at__", + "#": "__pd__", + "": "__cn__", +} + + +def etree_to_dict(t): + if t is None: + return {} + + d = {t.tag: {} if t.attrib else None} + children = list(t) + if children: + dd = defaultdict(list) + for dc in map(etree_to_dict, children): + for k, v in dc.items(): + dd[k].append(v) + d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}} + if t.attrib: + d[t.tag].update(("@" + k, v) for k, v in t.attrib.items()) + if t.text: + text = t.text.strip() + if children or t.attrib: + if text: + d[t.tag]["#text"] = text + else: + d[t.tag] = text + return d + + +INSTALLED_TO = os.path.dirname(os.path.realpath(__file__)) + + +def metadata_from_node(node): + metadata = {} + + if len(node.findall("dataset")) == 1: + + for key, value in node.findall("dataset")[0].attrib.items(): + metadata[f"dataset_{key}"] = value + + for key, value in node.findall("history")[0].attrib.items(): + metadata[f"history_{key}"] = value + + for key, value in node.findall("metadata")[0].attrib.items(): + metadata[f"metadata_{key}"] = value + + for key, value in node.findall("tool")[0].attrib.items(): + metadata[f"tool_{key}"] = value + + # Additional Mappings applied: + metadata[ + "dataset_edam_format" + ] = '<a target="_blank" href="http://edamontology.org/{0}">{1}</a>'.format( + metadata["dataset_edam_format"], metadata["dataset_file_ext"] + ) + metadata["history_user_email"] = '<a href="mailto:{0}">{0}</a>'.format( + metadata["history_user_email"] + ) + metadata[ + "history_display_name" + ] = '<a target="_blank" href="{galaxy}/history/view/{encoded_hist_id}">{hist_name}</a>'.format( + galaxy=GALAXY_INFRASTRUCTURE_URL, + encoded_hist_id=metadata["history_id"], + hist_name=metadata["history_display_name"], + ) + metadata[ + "tool_tool" + ] = '<a target="_blank" href="{galaxy}/datasets/{encoded_id}/show_params">{tool_id}</a>'.format( + galaxy=GALAXY_INFRASTRUCTURE_URL, + encoded_id=metadata["dataset_id"], + tool_id=metadata["tool_tool_id"], + # tool_version=metadata['tool_tool_version'], + ) + + # Load additional metadata from a TSV file if any given by user + bonus = node.findall("bonus") + if bonus and "src" in bonus[0].attrib and bonus[0].attrib["src"]: + with open(bonus[0].attrib["src"], "r") as bonus_tsv: + bonus_content = csv.reader(bonus_tsv, delimiter="\t", quotechar='"') + for row in bonus_content: + if len(row) == 2: + if row[0] in metadata: + log.warning(f"Overwriting existing metadata {row[0]} with value from bonus file {row[1]}") + metadata[row[0]] = row[1] + else: + log.warning(f"Skipping invalid bonus metadata line: {row}") + + return metadata + + +class JbrowseConnector(object): + def __init__(self, jbrowse, outdir, update): + self.jbrowse = jbrowse + self.outdir = outdir + self.update = update + + self.tracksToIndex = {} + + # This is the id of the current assembly + 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): + if cwd: + return self.outdir + else: + return subprocess.check_output(['pwd']).decode('utf-8').strip() + # return None + + def subprocess_check_call(self, command, output=None, cwd=True): + if output: + log.debug(f"cd {self.get_cwd(cwd)} && {' '.join(command)} > {output.name}") + subprocess.check_call(command, cwd=self.get_cwd(cwd), stdout=output) + else: + log.debug(f"cd {self.get_cwd(cwd)} && {' '.join(command)}") + subprocess.check_call(command, cwd=self.get_cwd(cwd)) + + def subprocess_popen(self, command, cwd=True): + log.debug(f"cd {self.get_cwd(cwd)} && {command}") + p = subprocess.Popen( + command, + cwd=self.get_cwd(cwd), + shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + output, err = p.communicate() + retcode = p.returncode + if retcode != 0: + log.error(f"cd {self.get_cwd(cwd)} && {command}") + log.error(output) + log.error(err) + raise RuntimeError(f"Command failed with exit code {retcode}") + + def subprocess_check_output(self, command, cwd=True): + log.debug(f"cd {self.get_cwd(cwd)} && {' '.join(command)}") + return subprocess.check_output(command, cwd=self.get_cwd(cwd)) + + def symlink_or_copy(self, src, dest): + # Use to support symlinking in jbrowse1, in jbrowse2 prefer to use remote uri + cmd = ["cp", src, dest] + + return self.subprocess_check_call(cmd) + + def _prepare_track_style(self, xml_conf): + style_data = { + "type": "LinearBasicDisplay", # No ideal default, but should be overwritten anyway + } + + if "display" in xml_conf["style"]: + style_data["type"] = xml_conf["style"]["display"] + + style_data["displayId"] = f"{xml_conf['label']}_{style_data['type']}" + + style_data.update(self._prepare_renderer_config(style_data["type"], xml_conf["style"])) + + 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"): + + # Doc: https://jbrowse.org/jb2/docs/config/svgfeaturerenderer/ + style_data["renderer"] = { + "type": "SvgFeatureRenderer", + "showLabels": xml_conf.get("show_labels", True), + "showDescriptions": xml_conf.get("show_descriptions", True), + "labels": { + "name": xml_conf.get("labels_name", "jexl:get(feature,'name') || get(feature,'id')"), + "description": xml_conf.get("descriptions_name", "jexl:get(feature,'note') || get(feature,'description')") + }, + "displayMode": xml_conf.get("display_mode", "normal"), + "maxHeight": xml_conf.get("max_height", 1200), + } + + 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"] + + # Doc: https://jbrowse.org/jb2/docs/config/snpcoveragerenderer/ + + return style_data + + def _prepare_format_details(self, xml_conf): + formatDetails = { + } + + if "feature" in xml_conf["formatdetails"]: + feat_jexl = xml_conf["formatdetails"]["feature"] + for key, value in mapped_chars.items(): + feat_jexl = feat_jexl.replace(value, key) + formatDetails["feature"] = feat_jexl + + if "subfeature" in xml_conf["formatdetails"]: + sfeat_jexl = xml_conf["formatdetails"]["subfeature"] + for key, value in mapped_chars.items(): + sfeat_jexl = sfeat_jexl.replace(value, key) + formatDetails["subfeatures"] = sfeat_jexl + + if "depth" in xml_conf["formatdetails"]: + formatDetails["depth"] = int(xml_conf["formatdetails"]["depth"]) + + return {"formatDetails": formatDetails} + + def _prepare_track_metadata(self, xml_conf): + metadata = { + } + + metadata = xml_conf["metadata"] + + return {"metadata": metadata} + + def check_existing(self, destination): + existing = os.path.join(destination, "config.json") + if os.path.exists(existing): + with open(existing, "r") as existing_conf: + conf = json.load(existing_conf) + if "assemblies" in conf: + for assembly in conf["assemblies"]: + if "name" in assembly: + + # Look for a default scaffold + default_seq = None + if 'defaultSession' in conf and 'views' in conf['defaultSession']: + for view in conf['defaultSession']['views']: + if 'init' in view and 'assembly' in view['init'] and 'loc' in view['init']: + if view['init']['assembly'] == assembly["name"]: + default_seq = view['init']['loc'].split(":")[0] + if "views" in view: + subviews = view["views"] + for subview in subviews: + if 'init' in subview and 'assembly' in subview['init'] and 'loc' in subview['init']: + if subview['init']['assembly'] == assembly["name"]: + default_seq = subview['init']['loc'].split(":")[0] + + self.assembly_ids[assembly["name"]] = default_seq + + def _load_old_genome_views(self): + + views = {} + + config_path = os.path.join(self.outdir, "config.json") + with open(config_path, "r") as config_file: + config_json = json.load(config_file) + + # Find default synteny views existing from a previous jbrowse dataset + if 'defaultSession' in config_json and 'views' in config_json['defaultSession']: + for view in config_json['defaultSession']['views']: + if view['type'] != "LinearSyntenyView": + if 'init' in view and 'assembly' in view['init']: + views[view['init']['assembly']] = view + + return views + + def _load_old_synteny_views(self): + + views = [] + + config_path = os.path.join(self.outdir, "config.json") + with open(config_path, "r") as config_file: + config_json = json.load(config_file) + + # Find default synteny views existing from a previous jbrowse dataset + if 'defaultSession' in config_json and 'views' in config_json['defaultSession']: + for view in config_json['defaultSession']['views']: + if view['type'] == "LinearSyntenyView": + views.append(view) + + return views + + def add_assembly(self, path, label, is_remote=False, cytobands=None, ref_name_aliases=None): + + if not is_remote: + # Find a non-existing filename for the new genome + # (to avoid colision when upgrading an existing instance) + rel_seq_path = os.path.join("data", label) + seq_path = os.path.join(self.outdir, rel_seq_path) + fn_try = 1 + while ( + os.path.exists(seq_path + ".fasta") + or os.path.exists(seq_path + ".fasta.gz") + or os.path.exists(seq_path + ".fasta.gz.fai") + or os.path.exists(seq_path + ".fasta.gz.gzi") + ): + rel_seq_path = os.path.join("data", f"{label}{fn_try}") + seq_path = os.path.join(self.outdir, rel_seq_path) + fn_try += 1 + + # Check if the assembly already exists from a previous run (--update mode) + if self.update: + + config_path = os.path.join(self.outdir, "config.json") + with open(config_path, "r") as config_file: + config_json = json.load(config_file) + + for asby in config_json['assemblies']: + if asby['name'] == label: + + # Find default views existing for this assembly + if 'defaultSession' in config_json and 'views' in config_json['defaultSession']: + for view in config_json['defaultSession']['views']: + if 'init' in view and 'assembly' in view['init']: + if view['init']['assembly'] == label: + + log.info("Found existing assembly from existing JBrowse2 instance, preserving it") + + self.default_views[view['init']['assembly']] = view + + return label + + # Copy ref alias file if any + if ref_name_aliases: + copied_ref_name_aliases = seq_path + ".aliases" + shutil.copy(ref_name_aliases, copied_ref_name_aliases) + copied_ref_name_aliases = rel_seq_path + ".aliases" + + # Copy cytobands file if any + if cytobands: + copied_cytobands = seq_path + ".cytobands" + shutil.copy(cytobands, copied_cytobands) + copied_cytobands = rel_seq_path + ".cytobands" + + # Find a non-existing label for the new genome + # (to avoid colision when upgrading an existing instance) + lab_try = 1 + uniq_label = label + while uniq_label in self.assembly_ids: + uniq_label = label + str(lab_try) + lab_try += 1 + + if is_remote: + + # Find a default scaffold to display + with requests.get(path + ".fai", stream=True) as response: + response.raise_for_status() + first_seq = next(response.iter_lines()) + first_seq = first_seq.decode("utf-8").split('\t')[0] + + self.assembly_ids[uniq_label] = first_seq + + # We assume we just need to suffix url with .fai and .gzi for indexes. + cmd_jb = [ + "jbrowse", + "add-assembly", + "--name", + uniq_label, + "--type", + "bgzipFasta", + "--out", + self.outdir, + "--skipCheck", + ] + + if ref_name_aliases: + cmd_jb.extend([ + "--refNameAliases", + copied_ref_name_aliases, + ]) + + cmd_jb.append(path) # Path is an url in remote mode + + self.subprocess_check_call(cmd_jb) + else: + # Find a default scaffold to display + with open(path, "r") as fa_handle: + fa_header = fa_handle.readline()[1:].strip().split(" ")[0] + + self.assembly_ids[uniq_label] = fa_header + + copied_genome = seq_path + ".fasta" + shutil.copy(path, copied_genome) + + # Compress with bgzip + cmd = ["bgzip", copied_genome] + self.subprocess_check_call(cmd) + + # FAI Index + cmd = ["samtools", "faidx", copied_genome + ".gz"] + self.subprocess_check_call(cmd) + + cmd_jb = [ + "jbrowse", + "add-assembly", + "--load", + "inPlace", + "--name", + uniq_label, + "--type", + "bgzipFasta", + "--out", + self.outdir, + "--skipCheck", + ] + + if ref_name_aliases: + cmd_jb.extend([ + "--refNameAliases", + copied_ref_name_aliases, + ]) + + cmd_jb.append(rel_seq_path + ".fasta.gz") + + self.subprocess_check_call(cmd_jb) + + if cytobands: + self.add_cytobands(uniq_label, copied_cytobands) + + return uniq_label + + def add_cytobands(self, assembly_name, cytobands_path): + + 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["cytobands"] = { + "adapter": { + "type": "CytobandAdapter", + "cytobandLocation": { + "uri": cytobands_path + } + } + } + + filled_assemblies = [] + for assembly in config_json["assemblies"]: + if assembly["name"] == assembly_name: + assembly.update(config_data) + filled_assemblies.append(assembly) + config_json["assemblies"] = filled_assemblies + + with open(config_path, "w") as config_file: + json.dump(config_json, config_file, indent=2) + + def text_index(self): + + for ass in self.tracksToIndex: + tracks = self.tracksToIndex[ass] + args = [ + "jbrowse", + "text-index", + "--target", + self.outdir, + "--assemblies", + ass, + ] + + tracks = ",".join(tracks) + if tracks: + args += ["--tracks", tracks] + + log.info(f"-----> Running text-index on assembly {ass} and tracks {tracks}") + + # Only run index if we want to index at least one + # If --tracks is not specified, it will index everything + self.subprocess_check_call(args) + + def add_gc_content(self, parent, trackData, **kwargs): + + adapter = {} + existing = os.path.join(self.outdir, "config.json") + if os.path.exists(existing): + with open(existing, "r") as existing_conf: + conf = json.load(existing_conf) + if "assemblies" in conf: + for assembly in conf["assemblies"]: + if assembly.get('name', "") == parent['uniq_id']: + adapter = assembly.get('sequence', {}).get('adapter', {}) + + json_track_data = { + "type": "GCContentTrack", + "trackId": trackData["label"], + "name": trackData["key"], + "adapter": adapter, + "category": [trackData["category"]], + "assemblyNames": [parent['uniq_id']], + } + + style_json = self._prepare_track_style(trackData) + + json_track_data.update(style_json) + + self.subprocess_check_call( + [ + "jbrowse", + "add-track-json", + "--target", + self.outdir, + json.dumps(json_track_data), + ] + ) + + def add_bigwig(self, parent, data, trackData, wiggleOpts, **kwargs): + + if trackData['remote']: + rel_dest = data + else: + rel_dest = os.path.join("data", trackData["label"] + ".bw") + dest = os.path.join(self.outdir, rel_dest) + self.symlink_or_copy(os.path.realpath(data), dest) + + 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'] + ) + + def add_bigwig_multi(self, parent, data_files, trackData, wiggleOpts, **kwargs): + + subadapters = [] + + sub_num = 0 + for data in data_files: + if trackData['remote']: + rel_dest = data[1] + else: + rel_dest = os.path.join("data", f"{trackData['label']}_sub{sub_num}.bw") + dest = os.path.join(self.outdir, rel_dest) + self.symlink_or_copy(os.path.realpath(data[1]), dest) + + subadapters.append({ + "type": "BigWigAdapter", + "name": data[0], + "bigWigLocation": { + "uri": rel_dest, + "locationType": "UriLocation" + } + }) + sub_num += 1 + + json_track_data = { + "type": "MultiQuantitativeTrack", + "trackId": trackData["label"], + "name": trackData["key"], + "adapter": { + "type": "MultiWiggleAdapter", + "subadapters": subadapters + }, + "category": [trackData["category"]], + "assemblyNames": [parent['uniq_id']], + } + + style_json = self._prepare_track_style(trackData) + + json_track_data.update(style_json) + + track_metadata = self._prepare_track_metadata(trackData) + + json_track_data.update(track_metadata) + + self.subprocess_check_call( + [ + "jbrowse", + "add-track-json", + "--target", + self.outdir, + json.dumps(json_track_data), + ] + ) + + # Anything ending in "am" (Bam or Cram) + def add_xam(self, parent, data, trackData, xamOpts, index=None, ext="bam", **kwargs): + index_ext = "bai" + if ext == "cram": + index_ext = "crai" + + 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}" + ) + + 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'] + ) + + def add_vcf(self, parent, data, trackData, vcfOpts={}, zipped=False, **kwargs): + if trackData['remote']: + rel_dest = data + else: + if zipped: + rel_dest = os.path.join("data", trackData["label"] + ".vcf.gz") + dest = os.path.join(self.outdir, rel_dest) + shutil.copy(os.path.realpath(data), dest) + else: + rel_dest = os.path.join("data", trackData["label"] + ".vcf") + dest = os.path.join(self.outdir, rel_dest) + shutil.copy(os.path.realpath(data), dest) + + cmd = ["bgzip", dest] + self.subprocess_check_call(cmd) + cmd = ["tabix", dest + ".gz"] + self.subprocess_check_call(cmd) + + rel_dest = os.path.join("data", trackData["label"] + ".vcf.gz") + + 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) + + self._add_track( + trackData["label"], + trackData["key"], + trackData["category"], + rel_dest, + parent, + config=style_json, + remote=trackData['remote'] + ) + + def add_gff(self, parent, data, format, trackData, gffOpts, **kwargs): + if trackData['remote']: + rel_dest = data + else: + 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 gffOpts.get('index', 'false') in ("yes", "true", "True"): + if parent['uniq_id'] not in self.tracksToIndex: + self.tracksToIndex[parent['uniq_id']] = [] + self.tracksToIndex[parent['uniq_id']].append(trackData["label"]) + + self._add_track( + trackData["label"], + trackData["key"], + trackData["category"], + rel_dest, + parent, + config=style_json, + remote=trackData['remote'] + ) + + def add_bed(self, parent, data, format, trackData, gffOpts, **kwargs): + if trackData['remote']: + rel_dest = data + else: + 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 gffOpts.get('index', 'false') in ("yes", "true", "True"): + if parent['uniq_id'] not in self.tracksToIndex: + self.tracksToIndex[parent['uniq_id']] = [] + self.tracksToIndex[parent['uniq_id']].append(trackData["label"]) + + self._add_track( + trackData["label"], + trackData["key"], + trackData["category"], + rel_dest, + parent, + config=style_json, + remote=trackData['remote'] + ) + + def add_paf(self, parent, data, trackData, pafOpts, **kwargs): + + if trackData['remote']: + rel_dest = data + + if rel_dest.endswith('pif') or rel_dest.endswith('pif.gz'): + adapter = "pif" + else: + adapter = "paf" + else: + rel_dest = os.path.join("data", trackData["label"] + ".pif.gz") + dest = os.path.join(self.outdir, rel_dest) + + cmd = ["jbrowse", "make-pif", "--out", dest, os.path.realpath(data)] + self.subprocess_check_call(cmd) + + adapter = "pif" + + if trackData["style"]["display"] == "LinearBasicDisplay": + # Normal style track + + json_track_data = { + "type": "SyntenyTrack", + "trackId": trackData["label"], + "name": trackData["key"], + "adapter": { + "type": "PairwiseIndexedPAFAdapter", + "pifGzLocation": { + "uri": rel_dest, + }, + "index": { + "location": { + "uri": rel_dest + ".tbi", + } + }, + }, + "category": [trackData["category"]], + "assemblyNames": [parent['uniq_id']], + } + else: + # Synteny viewer + + json_track_data = { + "type": "SyntenyTrack", + "trackId": trackData["label"], + "name": trackData["key"], + "adapter": { + "assemblyNames": [ + parent['uniq_id'], + "", # Placeholder until we know the next genome id + ], + }, + "category": [trackData["category"]], + "assemblyNames": [ + parent['uniq_id'], + "", # Placeholder until we know the next genome id + ] + } + + if adapter == "pif": + json_track_data["adapter"].update({ + "type": "PairwiseIndexedPAFAdapter", + "pifGzLocation": { + "uri": rel_dest, + }, + "index": { + "location": { + "uri": rel_dest + ".tbi", + } + }, + }) + else: + json_track_data["adapter"].update({ + "type": "PAFAdapter", + "pafLocation": { + "uri": rel_dest, + }, + }) + + style_json = self._prepare_track_style(trackData) + + json_track_data.update(style_json) + + track_metadata = self._prepare_track_metadata(trackData) + + json_track_data.update(track_metadata) + + if trackData["style"]["display"] == "LinearBasicDisplay": + self.subprocess_check_call( + [ + "jbrowse", + "add-track-json", + "--target", + self.outdir, + json.dumps(json_track_data), + ] + ) + else: + self.synteny_tracks.append(json_track_data) + + def add_hic(self, parent, data, trackData, hicOpts, **kwargs): + if trackData['remote']: + rel_dest = data + else: + rel_dest = os.path.join("data", trackData["label"] + ".hic") + dest = os.path.join(self.outdir, rel_dest) + self.symlink_or_copy(os.path.realpath(data), dest) + + 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'] + ) + + def add_maf(self, parent, data, trackData, mafOpts, **kwargs): + + # Add needed plugin + plugin_def = { + "name": "MafViewer", + "url": "https://unpkg.com/jbrowse-plugin-mafviewer/dist/jbrowse-plugin-mafviewer.umd.production.min.js" + } + self.plugins.append(plugin_def) + + rel_dest = os.path.join("data", trackData["label"] + ".maf") + dest = os.path.join(self.outdir, rel_dest) + + assembly_name = mafOpts.get("assembly_name", "") + if not assembly_name: + # Guess from assembly + assembly_name = parent['uniq_id'] + + self._convert_maf(data, dest, assembly_name) + + # Extract samples list + mafs = open(data, "r").readlines() + mafss = [x for x in mafs if (x.startswith("s\t") or x.startswith("s "))] + samp = [x.split()[1] for x in mafss if len(x.split()) > 0] + sampu = list(dict.fromkeys(samp)) + samples = [x.split(".")[0] for x in sampu] + samples.sort() + + json_track_data = { + "type": "MafTrack", + "trackId": trackData["label"], + "name": trackData["key"], + "adapter": { + "type": "MafTabixAdapter", + "samples": samples, + "bedGzLocation": { + "uri": rel_dest + ".gz", + }, + "index": { + "location": { + "uri": rel_dest + ".gz.tbi", + }, + }, + }, + "category": [trackData["category"]], + "assemblyNames": [parent['uniq_id']], + } + + style_json = self._prepare_track_style(trackData) + + json_track_data.update(style_json) + + track_metadata = self._prepare_track_metadata(trackData) + + json_track_data.update(track_metadata) + + self.subprocess_check_call( + [ + "jbrowse", + "add-track-json", + "--target", + self.outdir, + json.dumps(json_track_data), + ] + ) + + def add_sparql(self, parent, url, query, query_refnames, trackData): + json_track_data = { + "type": "FeatureTrack", + "trackId": trackData["label"], + "name": trackData["key"], + "adapter": { + "type": "SPARQLAdapter", + "endpoint": {"uri": url, "locationType": "UriLocation"}, + "queryTemplate": query, + }, + "category": [trackData["category"]], + "assemblyNames": [parent['uniq_id']], + } + + if query_refnames: + json_track_data["adapter"]["refNamesQueryTemplate"]: query_refnames + + # TODO handle metadata somehow for sparql too + + self.subprocess_check_call( + [ + "jbrowse", + "add-track-json", + "--target", + self.outdir, + json.dumps(json_track_data), + ] + ) + + def _add_track(self, track_id, label, category, path, assembly, config=None, trackType=None, load_action="inPlace", assemblies=None, remote=False): + """ + Adds a track to config.json using Jbrowse add-track cli + + By default, using `--load inPlace`: the file is supposed to be already placed at the `path` relative to + the outdir, `jbrowse add-track` will not touch it and trust us that the file is there and ready to use. + + With `load_action` parameter, you can ask `jbrowse add-track` to copy/move/symlink the file for you. + Not done by default because we often need more control on file copying/symlink for specific cases (indexes, symlinks of symlinks, ...) + """ + + cmd = [ + "jbrowse", + "add-track", + "--name", + label, + "--category", + category, + "--target", + self.outdir, + "--trackId", + track_id, + "--assemblyNames", + assemblies if assemblies else assembly['uniq_id'], + ] + + if not remote: + cmd.append("--load") + cmd.append(load_action) + + if config: + cmd.append("--config") + cmd.append(json.dumps(config)) + + if trackType: + cmd.append("--trackType") + cmd.append(trackType) + + cmd.append(path) + + self.subprocess_check_call(cmd) + + def _sort_gff(self, data, dest): + # Only index if not already done + if not os.path.exists(dest): + # Not using jbrowse sort-gff because it uses sort and has the problem exposed on https://github.com/tao-bioinfo/gff3sort + cmd = f"gff3sort.pl --precise '{data}' | grep -v \"^$\" > '{dest}'" + self.subprocess_popen(cmd, cwd=False) + + self.subprocess_check_call(["bgzip", "-f", dest], cwd=False) + self.subprocess_check_call(["tabix", "-f", "-p", "gff", dest + ".gz"], cwd=False) + + def _sort_bed(self, data, dest): + # Only index if not already done + if not os.path.exists(dest): + cmd = ["sort", "-k1,1", "-k2,2n", data] + with open(dest, "w") as handle: + self.subprocess_check_call(cmd, output=handle) + + self.subprocess_check_call(["bgzip", "-f", dest]) + self.subprocess_check_call(["tabix", "-f", "-p", "bed", dest + ".gz"]) + + def _convert_maf(self, data, dest, assembly_name): + # Only convert if not already done + if not os.path.exists(dest): + + dest_bed = dest + ".bed" + cmd = ["python", os.path.join(SELF_LOCATION, "maf2bed.py"), assembly_name, data, dest_bed] + self.subprocess_check_call(cmd, cwd=False) + + cmd = ["sort", "-k1,1", "-k2,2n", dest_bed] + with open(dest, "w") as handle: + self.subprocess_check_call(cmd, output=handle) + + self.subprocess_check_call(["bgzip", "-f", dest], cwd=False) + self.subprocess_check_call(["tabix", "-f", "-p", "bed", dest + ".gz"], cwd=False) + + def process_annotations(self, track, parent): + category = track["category"].replace("__pd__date__pd__", TODAY) + + track_labels = [] + + for i, ( + dataset_path, + dataset_ext, + track_human_label, + extra_metadata, + ) in enumerate(track["trackfiles"]): + # Unsanitize labels (element_identifiers are always sanitized by Galaxy) + for key, value in mapped_chars.items(): + track_human_label = track_human_label.replace(value, key) + + is_multi = type(dataset_path) is list + + log.info( + f"-----> Processing track {category} / {track_human_label} ({dataset_ext}, {len(dataset_path) if is_multi else 1} files)" + ) + + outputTrackConfig = { + "category": category, + } + + outputTrackConfig["key"] = track_human_label + # We add extra data to hash for the case of non-file tracks + if ( + "conf" in track + and "options" in track["conf"] + and "url" in track["conf"]["options"] + ): + non_file_info = track["conf"]["options"]["url"] + else: + non_file_info = "" + + # I chose to use track['category'] instead of 'category' here. This + # is intentional. This way re-running the tool on a different date + # will not generate different hashes and make comparison of outputs + # much simpler. + hashData = [ + str(dataset_path), + track_human_label, + track["category"], + non_file_info, + parent["uniq_id"], + ] + hashData = "|".join(hashData).encode("utf-8") + outputTrackConfig["label"] = hashlib.md5(hashData).hexdigest() + f"_{track['track_num']}_{i}" + outputTrackConfig["metadata"] = extra_metadata + + outputTrackConfig["style"] = track["style"] + + outputTrackConfig["formatdetails"] = track["formatdetails"] + + outputTrackConfig["remote"] = track["remote"] + + # Guess extension for remote data + if dataset_ext == "gff,gff3,bed": + if dataset_path.endswith(".bed") or dataset_path.endswith(".bed.gz"): + dataset_ext = "bed" + else: + dataset_ext = "gff" + elif dataset_ext == "vcf,vcf_bgzip": + if dataset_path.endswith(".vcf.gz"): + dataset_ext = "vcf_bgzip" + else: + dataset_ext = "vcf" + + if dataset_ext in ("gff", "gff3"): + self.add_gff( + parent, + dataset_path, + dataset_ext, + outputTrackConfig, + track["conf"]["options"]["gff"], + ) + elif dataset_ext == "bed": + self.add_bed( + parent, + dataset_path, + dataset_ext, + outputTrackConfig, + track["conf"]["options"]["gff"], + ) + elif dataset_ext == "bigwig": + if is_multi: + self.add_bigwig_multi( + parent, + dataset_path, outputTrackConfig, track["conf"]["options"]["wiggle"] + ) + else: + self.add_bigwig( + parent, + dataset_path, outputTrackConfig, track["conf"]["options"]["wiggle"] + ) + elif dataset_ext == "maf": + self.add_maf( + parent, + dataset_path, outputTrackConfig, track["conf"]["options"]["maf"] + ) + elif dataset_ext == "bam": + + if track["remote"]: + bam_index = dataset_path + '.bai' + else: + real_indexes = track["conf"]["options"]["pileup"]["bam_indices"][ + "bam_index" + ] + if not isinstance(real_indexes, list): + # <bam_indices> + # <bam_index>/path/to/a.bam.bai</bam_index> + # </bam_indices> + # + # The above will result in the 'bam_index' key containing a + # string. If there are two or more indices, the container + # becomes a list. Fun! + real_indexes = [real_indexes] + + bam_index = real_indexes[i] + + self.add_xam( + parent, + dataset_path, + outputTrackConfig, + track["conf"]["options"]["pileup"], + index=bam_index, + ext="bam", + ) + elif dataset_ext == "cram": + + if track["remote"]: + cram_index = dataset_path + '.crai' + else: + real_indexes = track["conf"]["options"]["cram"]["cram_indices"][ + "cram_index" + ] + if not isinstance(real_indexes, list): + # <bam_indices> + # <bam_index>/path/to/a.bam.bai</bam_index> + # </bam_indices> + # + # The above will result in the 'bam_index' key containing a + # string. If there are two or more indices, the container + # becomes a list. Fun! + real_indexes = [real_indexes] + + cram_index = real_indexes[i] + + self.add_xam( + parent, + dataset_path, + outputTrackConfig, + track["conf"]["options"]["cram"], + index=cram_index, + ext="cram", + ) + elif dataset_ext == "vcf": + self.add_vcf( + parent, + dataset_path, + outputTrackConfig + ) + elif dataset_ext == "vcf_bgzip": + self.add_vcf( + parent, + dataset_path, + outputTrackConfig, + zipped=True + ) + elif dataset_ext == "paf": # https://fr.wikipedia.org/wiki/Paf_le_chien + self.add_paf( + parent, + dataset_path, + outputTrackConfig, + track["conf"]["options"]["synteny"] + ) + elif dataset_ext in ("hic"): + self.add_hic( + parent, + dataset_path, + outputTrackConfig, + track["conf"]["options"]["hic"] + ) + elif dataset_ext == "sparql": + sparql_query = track["conf"]["options"]["sparql"]["query"] + for key, value in mapped_chars.items(): + sparql_query = sparql_query.replace(value, key) + sparql_query_refnames = track["conf"]["options"]["sparql"].get("query_refnames", "") + if sparql_query_refnames: + for key, value in mapped_chars.items(): + sparql_query_refnames = sparql_query_refnames.replace(value, key) + self.add_sparql( + parent, + track["conf"]["options"]["sparql"]["url"], + sparql_query, + sparql_query_refnames, + outputTrackConfig, + ) + elif dataset_ext == "gc": + self.add_gc_content( + parent, + outputTrackConfig, + ) + else: + log.error(f"Do not know how to handle {dataset_ext}") + + track_labels.append(outputTrackConfig["label"]) + + # Return non-human label for use in other fields + return track_labels + + def add_default_view_genome(self, genome, default_loc, tracks_on): + + refName = "" + start = end = None + if default_loc: + loc_match = re.search(r"^(\w+):(\d+)\.+(\d+)$", default_loc) + if loc_match: + refName = loc_match.group(1) + start = int(loc_match.group(2)) + end = int(loc_match.group(3)) + + if not refName and self.assembly_ids[genome['uniq_id']]: + refName = self.assembly_ids[genome['uniq_id']] + + if start and end: + loc_str = f"{refName}:{start}-{end}" + else: + loc_str = refName + + # Updating an existing jbrowse instance, merge with pre-existing view + view_specs = None + if self.update: + for existing in self.default_views.values(): + if len(existing) and existing["type"] == "LinearGenomeView": + if existing['init']['assembly'] == genome['uniq_id']: + view_specs = existing + if loc_str: + view_specs['init']['loc'] = loc_str + view_specs['init']['tracks'].extend(tracks_on) + + if view_specs is None: # Not updating, or updating from synteny + view_specs = { + "type": "LinearGenomeView", + "init": { + "assembly": genome['uniq_id'], + "loc": loc_str, + "tracks": tracks_on + } + } + + return view_specs + + def add_default_view_synteny(self, genome_views, synteny_tracks): + + # Add json for cached synteny tracks + # We cache them because we need to know the target genome uniq_id + for strack in synteny_tracks: + + # Target assembly is the next genome, find its uniq_id + query_assembly = strack["assemblyNames"][0] + ass_uniq_ids = list(self.assembly_ids.keys()) + query_index = ass_uniq_ids.index(query_assembly) + target_assembly = ass_uniq_ids[query_index + 1] + + strack["assemblyNames"][1] = target_assembly + strack["adapter"]["assemblyNames"][1] = target_assembly + + self.subprocess_check_call( + [ + "jbrowse", + "add-track-json", + "--target", + self.outdir, + json.dumps(strack), + ] + ) + + # Configure the synteny view + levels = [] + + for strack in synteny_tracks: + lev = { + "type": "LinearSyntenyViewHelper", + "tracks": [ + { + "type": "SyntenyTrack", + "configuration": strack["trackId"], + "displays": [ + { + "type": "LinearSyntenyDisplay", + "configuration": strack["trackId"] + "_LinearSyntenyDisplay" + } + ] + } + ], + "height": 100, + "level": len(levels) + } + levels.append(lev) + + view_specs = { + "type": "LinearSyntenyView", + "views": genome_views, + "levels": levels + } + + return view_specs + + def add_default_session(self, default_views): + """ + Add some default session settings: set some assemblies/tracks on/off + + This allows to select a default view: + - jb type (Linear, Circular, etc) + - default location on an assembly + - default tracks + - ... + + Now using this method: + https://github.com/GMOD/jbrowse-components/pull/4907 + + Different methods that were tested/discussed earlier: + - using a defaultSession item in config.json before PR 4970: this proved to be difficult: + forced to write a full session block, including hard-coded/hard-to-guess items, + no good way to let Jbrowse2 display a scaffold without knowing its size + - using JBrowse2 as an embedded React component in a tool-generated html file: + it works but it requires generating js code to actually do what we want = chosing default view, assembly, tracks, ... + - writing a session-spec inside the config.json file: this is not yet supported as of 2.10.2 (see PR 4148 below) + a session-spec is a kind of simplified defaultSession where you don't need to specify every aspect of the session + - passing a session-spec through URL params by embedding the JBrowse2 index.html inside an iframe + + Xrefs to understand the choices: + https://github.com/GMOD/jbrowse-components/issues/2708 + https://github.com/GMOD/jbrowse-components/discussions/3568 + https://github.com/GMOD/jbrowse-components/pull/4148 + """ + + if self.use_synteny_viewer: + session_name = "Synteny" + else: + session_name = ', '.join(x['init']['assembly'] for x in default_views) + + session_spec = { + "name": session_name, + "views": default_views + } + + config_path = os.path.join(self.outdir, "config.json") + with open(config_path, "r") as config_file: + config_json = json.load(config_file) + + 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 + """ + + 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["theme"] = { + "palette": { + "primary": {"main": data.get("primary_color", "#0D233F")}, + "secondary": {"main": data.get("secondary_color", "#721E63")}, + "tertiary": {"main": data.get("tertiary_color", "#135560")}, + "quaternary": {"main": data.get("quaternary_color", "#FFB11D")}, + }, + "typography": {"fontSize": int(data.get("font_size", 10))}, + } + + config_json["configuration"].update(config_data) + + with open(config_path, "w") as config_file: + json.dump(config_json, config_file, indent=2) + + def add_plugins(self, data): + """ + Add plugins to the config.json file + """ + + config_path = os.path.join(self.outdir, "config.json") + with open(config_path, "r") as config_file: + config_json = json.load(config_file) + + if "plugins" not in config_json: + config_json["plugins"] = [] + + config_json["plugins"].extend(data) + + with open(config_path, "w") as config_file: + json.dump(config_json, config_file, indent=2) + + 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 + """ + + copytree(jbrowse_dir, destination) + try: + shutil.rmtree(os.path.join(destination, "test_data")) + except OSError as e: + log.error(f"Error: {e.filename} - {e.strerror}.") + + if not os.path.exists(os.path.join(destination, "data")): + # It can already exist if upgrading an instance + os.makedirs(os.path.join(destination, "data")) + log.info(f"makedir {os.path.join(destination, 'data')}") + + os.symlink("./data/config.json", os.path.join(destination, "config.json")) + + +def copytree(src, dst, symlinks=False, ignore=None): + for item in os.listdir(src): + s = os.path.join(src, item) + d = os.path.join(dst, item) + if os.path.isdir(s): + shutil.copytree(s, d, symlinks, ignore) + else: + shutil.copy2(s, d) + + +def parse_style_conf(item): + if "type" in item.attrib and item.attrib["type"] in ["boolean", "integer"]: + if item.attrib["type"] == "boolean": + return item.text in ("yes", "true", "True") + elif item.attrib["type"] == "integer": + return int(item.text) + else: + return item.text + + +def validate_synteny(real_root): + + if len(real_root.findall('assembly/tracks/track[@format="synteny"]')) == 0: + # No synteny data, all good + return False + + assemblies = real_root.findall("assembly") + + if len(assemblies[-1].findall('tracks/track[@format="synteny"]')) > 0 and \ + assemblies[-1].find('tracks/track[@format="synteny"]/options/style/display').text == "LinearSyntenyDisplay": + raise RuntimeError("You should not set a synteny track on the last genome.") + + for assembly in assemblies[1:0]: + if len(assembly.findall('tracks/track[@format="synteny"]')) != 1 and \ + assembly.find('tracks/track[@format="synteny"]/options/style/display').text == "LinearSyntenyDisplay": + raise RuntimeError("To use the synteny viewer, you should add a synteny track to each assembly, except the last one.") + + return True + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="", 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") + args = parser.parse_args() + + tree = ET.parse(args.xml.name) + real_root = tree.getroot() + + # This should be done ASAP + # Sometimes this comes as `localhost` without a protocol + GALAXY_INFRASTRUCTURE_URL = real_root.find("metadata/galaxyUrl").text + if not GALAXY_INFRASTRUCTURE_URL.startswith("http"): + # so we'll prepend `http://` and hope for the best. Requests *should* + # be GET and not POST so it should redirect OK + GALAXY_INFRASTRUCTURE_URL = "http://" + GALAXY_INFRASTRUCTURE_URL + + jc = JbrowseConnector( + jbrowse=args.jbrowse, + outdir=args.outdir, + update=args.update, + ) + + # Synteny options are special, check them first + jc.use_synteny_viewer = validate_synteny(real_root) + + for assembly in real_root.findall("assembly"): + genome_el = assembly.find('genome') + + is_remote = genome_el.attrib.get("remote", "false") == "true" + + genome = { + "path": genome_el.attrib["path"] if is_remote else os.path.realpath(genome_el.attrib["path"]), + "meta": metadata_from_node(genome_el.find("metadata")), + "label": genome_el.attrib["label"], + } + + cytobands = None + cytobands_el = genome_el.find("cytobands") + if cytobands_el is not None and "path" in cytobands_el.attrib: + cytobands = cytobands_el.attrib["path"] + + ref_name_aliases = None + ref_name_aliases_el = genome_el.find("ref_name_aliases") + if ref_name_aliases_el is not None and "path" in ref_name_aliases_el.attrib: + ref_name_aliases = ref_name_aliases_el.attrib["path"] + + log.debug("Processing genome", genome) + genome["uniq_id"] = jc.add_assembly(genome["path"], genome["label"], is_remote, cytobands, ref_name_aliases) + + default_tracks_on = [] + + track_num = 0 + for track in assembly.findall("tracks/track"): + track_conf = {} + track_conf["trackfiles"] = [] + track_conf["track_num"] = track_num + + trackfiles = track.findall("files/trackFile") or [] + + is_multi = False + multi_paths = [] + multi_type = None + multi_metadata = {} + try: + multi_in_xml = track.find("options/multitrack") + if multi_in_xml is not None and parse_style_conf(multi_in_xml): + is_multi = True + multi_paths = [] + multi_type = trackfiles[0].attrib["ext"] + except KeyError: + pass + + is_remote = False + if trackfiles: + for x in trackfiles: + if is_multi: + is_remote = x.attrib.get("remote", "false") == "true" + multi_paths.append( + (x.attrib["label"], x.attrib["path"] if is_remote else os.path.realpath(x.attrib["path"])) + ) + multi_metadata.update(metadata_from_node(x.find("metadata"))) + else: + metadata = metadata_from_node(x.find("metadata")) + is_remote = x.attrib.get("remote", "false") == "true" + track_conf["trackfiles"].append( + ( + x.attrib["path"] if is_remote else os.path.realpath(x.attrib["path"]), + x.attrib["ext"], + x.attrib["label"], + metadata, + ) + ) + else: + # For tracks without files (sparql, gc) + track_conf["trackfiles"].append( + ( + "", # N/A, no path for sparql or gc + track.attrib["format"], + track.find("options/label").text, + {}, + ) + ) + + if is_multi: + etal_tracks_nb = len(multi_paths[1:]) + multi_label = f"{multi_paths[0][0]} + {etal_tracks_nb} other track{'s' if etal_tracks_nb > 1 else ''}" + + track_conf["trackfiles"].append( + ( + multi_paths, # Passing an array of paths to represent as one track + multi_type, # First file type + multi_label, # First file label + multi_metadata, # Mix of all metadata for multiple bigwig => only last file metadata coming from galaxy + custom oness + ) + ) + track_conf["category"] = track.attrib["cat"] + track_conf["format"] = track.attrib["format"] + track_conf["style"] = { + 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 []) + } + track_conf["formatdetails"] = { + item.tag: parse_style_conf(item) for item in (track.find("options/formatdetails") or []) + } + + track_conf["conf"] = etree_to_dict(track.find("options")) + + track_conf["remote"] = is_remote + + track_labels = jc.process_annotations(track_conf, genome) + + if track.attrib["visibility"] == "default_on": + for tlabel in track_labels: + default_tracks_on.append(tlabel) + + track_num += 1 + + default_loc = assembly.find("defaultLocation").text + + jc.default_views[genome['uniq_id']] = jc.add_default_view_genome(genome, default_loc, default_tracks_on) + + if jc.use_synteny_viewer: + synteny_view = jc.add_default_view_synteny(list(jc.default_views.values()), jc.synteny_tracks) + + views_for_session = jc._load_old_synteny_views() + + views_for_session.append(synteny_view) + else: + old_views = jc._load_old_genome_views() + + for old_view in old_views: + if old_view not in jc.default_views: + jc.default_views[old_view] = old_views[old_view] + + views_for_session = list(jc.default_views.values()) + + general_data = { + "analytics": real_root.find("metadata/general/analytics").text, + "primary_color": real_root.find("metadata/general/primary_color").text, + "secondary_color": real_root.find("metadata/general/secondary_color").text, + "tertiary_color": real_root.find("metadata/general/tertiary_color").text, + "quaternary_color": real_root.find("metadata/general/quaternary_color").text, + "font_size": real_root.find("metadata/general/font_size").text, + } + + jc.add_default_session(views_for_session) + jc.add_general_configuration(general_data) + jc.add_plugins(jc.plugins) + jc.text_index()
