# HG changeset patch # User fubar # Date 1705477852 0 # Node ID b04fd993b31e5ebbd980387e2585e90bcbdde330 # Parent 79f7265f90bdae7dac5519a60ed287e1bd6f0c69 planemo upload for repository https://github.com/galaxyproject/tools-iuc/tree/master/tools/jbrowse2 commit 53a108d8153c955044ae7eb8cb06bdcfd0036717 diff -r 79f7265f90bd -r b04fd993b31e 4f27993eab7751041e8c72047be3ea53_0.bw --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/4f27993eab7751041e8c72047be3ea53_0.bw Wed Jan 17 07:50:52 2024 +0000 @@ -0,0 +1,82 @@ + + + + + + + + + + + + + Galaxy + | ToolFactoryDev + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff -r 79f7265f90bd -r b04fd993b31e apache2_licence.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/apache2_licence.txt Wed Jan 17 07:50:52 2024 +0000 @@ -0,0 +1,178 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff -r 79f7265f90bd -r b04fd993b31e jb2_webserver.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jb2_webserver.py Wed Jan 17 07:50:52 2024 +0000 @@ -0,0 +1,184 @@ +#!/usr/bin/env python3# spec: simplest python web server with range support and multithreading that takes root path, +# port and bind address as command line arguments; by default uses the current dir as webroot, +# port 8000 and bind address of 0.0.0.0 +# borrowed from https://github.com/danvk/RangeHTTPServer +# and reborrowed from https://gist.github.com/glowinthedark/b99900abe935e4ab4857314d647a9068 +# +# The Apache 2.0 license copy in this repository is distributed with this code in accordance with that licence. +# https://www.apache.org/licenses/LICENSE-2.0.txt +# This part is not MIT licenced like the other components. + +# APPENDIX: How to apply the Apache License to your work. + +# To apply the Apache License to your work, attach the following +# boilerplate notice, with the fields enclosed by brackets "[]" +# replaced with your own identifying information. (Don't include +# the brackets!) The text should be enclosed in the appropriate +# comment syntax for the file format. We also recommend that a +# file or class name and description of purpose be included on the +# same "printed page" as the copyright notice for easier +# identification within third-party archives. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import argparse +import functools +import os +import re +import socketserver +import webbrowser +from http.server import SimpleHTTPRequestHandler + + +DEFAULT_PORT = 8080 + + +def copy_byte_range(infile, outfile, start=None, stop=None, bufsize=16 * 1024): + """Like shutil.copyfileobj, but only copy a range of the streams. + + Both start and stop are inclusive. + """ + if start is not None: + infile.seek(start) + while 1: + to_read = min(bufsize, stop + 1 - infile.tell() if stop else bufsize) + buf = infile.read(to_read) + if not buf: + break + outfile.write(buf) + + +BYTE_RANGE_RE = re.compile(r"bytes=(\d+)-(\d+)?$") + + +def parse_byte_range(byte_range): + """Returns the two numbers in 'bytes=123-456' or throws ValueError. + + The last number or both numbers may be None. + """ + if byte_range.strip() == "": + return None, None + + m = BYTE_RANGE_RE.match(byte_range) + if not m: + raise ValueError("Invalid byte range %s" % byte_range) + + first, last = [x and int(x) for x in m.groups()] + if last and last < first: + raise ValueError("Invalid byte range %s" % byte_range) + return first, last + + +class RangeRequestHandler(SimpleHTTPRequestHandler): + """Adds support for HTTP 'Range' requests to SimpleHTTPRequestHandler + + The approach is to: + - Override send_head to look for 'Range' and respond appropriately. + - Override copyfile to only transmit a range when requested. + """ + + def handle(self): + try: + SimpleHTTPRequestHandler.handle(self) + except Exception: + # ignored, thrown whenever the client aborts streaming (broken pipe) + pass + + def send_head(self): + if "Range" not in self.headers: + self.range = None + return SimpleHTTPRequestHandler.send_head(self) + try: + self.range = parse_byte_range(self.headers["Range"]) + except ValueError: + self.send_error(400, "Invalid byte range") + return None + first, last = self.range + + # Mirroring SimpleHTTPServer.py here + path = self.translate_path(self.path) + f = None + ctype = self.guess_type(path) + try: + f = open(path, "rb") + except IOError: + self.send_error(404, "File not found") + return None + + fs = os.fstat(f.fileno()) + file_len = fs[6] + if first >= file_len: + self.send_error(416, "Requested Range Not Satisfiable") + return None + + self.send_response(206) + self.send_header("Content-type", ctype) + + if last is None or last >= file_len: + last = file_len - 1 + response_length = last - first + 1 + + self.send_header("Content-Range", "bytes %s-%s/%s" % (first, last, file_len)) + self.send_header("Content-Length", str(response_length)) + self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + self.end_headers() + return f + + def end_headers(self): + self.send_header("Accept-Ranges", "bytes") + return SimpleHTTPRequestHandler.end_headers(self) + + def copyfile(self, source, outputfile): + if not self.range: + return SimpleHTTPRequestHandler.copyfile(self, source, outputfile) + + # SimpleHTTPRequestHandler uses shutil.copyfileobj, which doesn't let + # you stop the copying before the end of the file. + start, stop = self.range # set in send_head() + copy_byte_range(source, outputfile, start, stop) + + +class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + allow_reuse_address = True + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Simple Python Web Server with Range Support" + ) + parser.add_argument( + "--root", + default=os.getcwd(), + help="Root path to serve files from (default: current working directory)", + ) + parser.add_argument( + "--port", + type=int, + default=DEFAULT_PORT, + help=f"Port to listen on (default: {DEFAULT_PORT})", + ) + parser.add_argument( + "--bind", default="0.0.0.0", help="IP address to bind to (default: 0.0.0.0)" + ) + args = parser.parse_args() + + handler = functools.partial(RangeRequestHandler, directory=args.root) + + webbrowser.open(f"http://{args.bind}:{args.port}") + + with ThreadedTCPServer((args.bind, args.port), handler) as httpd: + print( + f"Serving HTTP on {args.bind} port {args.port} (http://{args.bind}:{args.port}/)" + ) + httpd.serve_forever() diff -r 79f7265f90bd -r b04fd993b31e jbrowse2.py --- a/jbrowse2.py Tue Jan 09 06:55:34 2024 +0000 +++ b/jbrowse2.py Wed Jan 17 07:50:52 2024 +0000 @@ -17,6 +17,10 @@ logging.basicConfig(level=logging.INFO) log = logging.getLogger("jbrowse") + +JB2VER = "v2.10.0" +# version pinned for cloning + TODAY = datetime.datetime.now().strftime("%Y-%m-%d") GALAXY_INFRASTRUCTURE_URL = None JB2REL = "v2.10.0" @@ -371,7 +375,6 @@ class JbrowseConnector(object): def __init__(self, outdir, genomes): - self.debug = False self.usejson = True self.giURL = GALAXY_INFRASTRUCTURE_URL self.outdir = outdir @@ -387,16 +390,14 @@ def subprocess_check_call(self, command, output=None): if output: - if self.debug: - log.debug("cd %s && %s > %s", self.outdir, " ".join(command), output) + log.debug("cd %s && %s > %s", self.outdir, " ".join(command), output) subprocess.check_call(command, cwd=self.outdir, stdout=output) else: log.debug("cd %s && %s", self.outdir, " ".join(command)) subprocess.check_call(command, cwd=self.outdir) def subprocess_popen(self, command): - if self.debug: - log.debug(command) + log.debug(command) p = subprocess.Popen( command, cwd=self.outdir, @@ -414,8 +415,7 @@ raise RuntimeError("Command failed with exit code %s" % (retcode)) def subprocess_check_output(self, command): - if self.debug: - log.debug(" ".join(command)) + log.debug(" ".join(command)) return subprocess.check_output(command, cwd=self.outdir) def symlink_or_copy(self, src, dest): @@ -465,9 +465,14 @@ self.config_json["assemblies"] = assemblies def make_assembly(self, fapath, gname): - faname = gname + ".fa.gz" + hashData = [ + fapath, + gname, + ] + hashData = "|".join(hashData).encode("utf-8") + ghash = hashlib.md5(hashData).hexdigest() + faname = ghash + ".fa.gz" fadest = os.path.join(self.outdir, faname) - # fadest = os.path.realpath(os.path.join(self.outdir, faname)) cmd = "bgzip -i -c %s -I %s.gzi > %s && samtools faidx %s" % ( fapath, fadest, @@ -556,7 +561,7 @@ # can be served - if public. # dsId = trackData["metadata"]["dataset_id"] # url = "%s/api/datasets/%s/display?to_ext=hic " % (self.giURL, dsId) - hname = trackData["name"] + hname = trackData["label"] dest = os.path.join(self.outdir, hname) cmd = ["cp", data, dest] # these can be very big. @@ -603,8 +608,6 @@ tId = trackData["label"] fname = "%s.bed" % tId dest = "%s/%s" % (self.outdir, fname) - # self.symlink_or_copy(data, dest) - # Process MAF to bed-like. Need build to munge chromosomes gname = self.genome_name cmd = [ "bash", @@ -722,11 +725,11 @@ trackDict["style"] = style_json self.tracksToAdd.append(trackDict) self.trackIdlist.append(tId) - os.unlink(gff3) def add_bigwig(self, data, trackData): - url = "%s.bw" % trackData["name"] + url = "%s.bigwig" % trackData["label"] + # slashes in names cause path trouble dest = os.path.join(self.outdir, url) cmd = ["cp", data, dest] self.subprocess_check_call(cmd) @@ -735,7 +738,7 @@ trackDict = { "type": "QuantitativeTrack", "trackId": tId, - "name": url, + "name": trackData["name"], "assemblyNames": [ self.genome_name, ], @@ -754,6 +757,7 @@ trackDict["style"] = style_json self.tracksToAdd.append(trackDict) self.trackIdlist.append(tId) + logging.debug("#### wig trackData=%s" % str(trackData)) def add_bam(self, data, trackData, bamOpts, bam_index=None, **kwargs): tId = trackData["label"] @@ -959,35 +963,24 @@ asstrack, ] - style_json = self._prepare_track_style(trackData) url = "%s.paf" % (trackData["label"]) dest = "%s/%s" % (self.outdir, url) self.symlink_or_copy(os.path.realpath(data), dest) - - if self.usejson: - trackDict = { - "type": "SyntenyTrack", - "trackId": tId, + trackDict = { + "type": "SyntenyTrack", + "trackId": tId, + "assemblyNames": [self.genome_name, pgname], + "name": tname, + "adapter": { + "type": "PAFAdapter", + "pafLocation": {"uri": url}, "assemblyNames": [self.genome_name, pgname], - "name": tname, - "adapter": { - "type": "PAFAdapter", - "pafLocation": {"uri": url}, - "assemblyNames": [self.genome_name, pgname], - }, - "config": style_json, - } - self.tracksToAdd.append(trackDict) - self.trackIdlist.append(tId) - else: - self._add_track( - trackData["label"], - trackData["key"], - trackData["category"], - dest, - assemblies=[self.genome_name, pgname], - config=style_json, - ) + }, + } + style_json = self._prepare_track_style(trackDict) + trackDict["style"] = style_json + self.tracksToAdd.append(trackDict) + self.trackIdlist.append(tId) def add_hicab(self, data, trackData, hicOpts, **kwargs): rel_dest = os.path.join("data", trackData["label"] + ".hic") @@ -995,14 +988,12 @@ self.symlink_or_copy(os.path.realpath(data), dest) - style_json = self._prepare_track_style(trackData) - self._add_track( trackData["label"], trackData["key"], trackData["category"], rel_dest, - config=style_json, + config={}, ) def add_sparql(self, url, query, query_refnames, trackData): @@ -1061,12 +1052,7 @@ } outputTrackConfig["key"] = track_human_label - if self.debug: - log.info( - "Processing category = %s, track_human_label = %s", - category, - track_human_label, - ) + # We add extra data to hash for the case of REST + SPARQL. if ( "conf" in track @@ -1076,7 +1062,7 @@ rest_url = track["conf"]["options"]["url"] else: rest_url = "" - + outputTrackConfig["trackset"] = track.get("trackset", {}) # 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 @@ -1165,26 +1151,22 @@ for track_conf in self.tracksToAdd: track_types[track_conf["trackId"]] = track_conf["type"] - - for on_track in data["visibility"]["default_on"]: - style_data = {"type": "LinearBasicDisplay", "height": 100} - if on_track in data["style"]: - if "display" in data["style"][on_track]: - style_data["type"] = data["style"][on_track]["display"] - del data["style"][on_track]["display"] - style_data.update(data["style"][on_track]) - if on_track in data["style_labels"]: - # TODO fix this: it should probably go in a renderer block (SvgFeatureRenderer) but still does not work - # TODO move this to per track displays? - style_data["labels"] = data["style_labels"][on_track] - - tracks_data.append( - { - "type": track_types[on_track], - "configuration": on_track, - "displays": [style_data], - } - ) + tId = track_conf["trackId"] + if tId in data["visibility"]["default_on"]: + style_data = {"type": "LinearBasicDisplay"} + if "displays" in track_conf: + style_data["type"] = track_conf["displays"][0]["type"] + if track_conf.get("style_labels", None): + # TODO fix this: it should probably go in a renderer block (SvgFeatureRenderer) but still does not work + # TODO move this to per track displays? + style_data["labels"] = track_conf["style_labels"] + tracks_data.append( + { + "type": track_types[tId], + "configuration": tId, + "displays": [style_data], + } + ) # The view for the assembly we're adding view_json = {"type": "LinearGenomeView", "tracks": tracks_data} @@ -1199,7 +1181,7 @@ elif self.genome_name is not None: refName = self.genome_name start = 0 - end = 100000 # Booh, hard coded! waiting for https://github.com/GMOD/jbrowse-components/issues/2708 + end = 10000 # Booh, hard coded! waiting for https://github.com/GMOD/jbrowse-components/issues/2708 if refName is not None: # TODO displayedRegions is not just zooming to the region, it hides the rest of the chromosome @@ -1265,8 +1247,11 @@ def clone_jbrowse(self): """Clone a JBrowse directory into a destination directory.""" + # dest = os.path.realpath(self.outdir) dest = self.outdir - cmd = ["jbrowse", "create", "-t", JB2REL, "-f", dest] + cmd = ["rm", "-rf", dest + "/*"] + self.subprocess_check_call(cmd) + cmd = ["jbrowse", "create", dest, "-t", JB2VER, "-f"] self.subprocess_check_call(cmd) for fn in [ "asset-manifest.json", @@ -1278,7 +1263,7 @@ ]: cmd = ["rm", "-rf", os.path.join(self.outdir, fn)] self.subprocess_check_call(cmd) - cmd = ["cp", os.path.join(INSTALLED_TO, "webserver.py"), self.outdir] + cmd = ["cp", os.path.join(INSTALLED_TO, "jb2_webserver.py"), self.outdir] self.subprocess_check_call(cmd) @@ -1386,21 +1371,17 @@ {}, # No metadata for multiple bigwig ) ) - 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") - } - - track_conf["style"] = { - item.tag: parse_style_conf(item) for item in track.find("options/style") - } - - track_conf["style_labels"] = { - item.tag: parse_style_conf(item) - for item in track.find("options/style_labels") - } + if track.find("options/style"): + track_conf["style"] = { + item.tag: parse_style_conf(item) for item in track.find("options/style") + } + if track.find("options/style_labels"): + track_conf["style_labels"] = { + item.tag: parse_style_conf(item) + for item in track.find("options/style_labels") + } track_conf["conf"] = etree_to_dict(track.find("options")) keys = jc.process_annotations(track_conf) @@ -1410,10 +1391,14 @@ default_session_data["visibility"][ track.attrib.get("visibility", "default_off") ].append(key) - default_session_data["style"][key] = track_conf[ - "style" - ] # TODO do we need this anymore? - default_session_data["style_labels"][key] = track_conf["style_labels"] + if track_conf.get("style", None): + default_session_data["style"][key] = track_conf[ + "style" + ] # TODO do we need this anymore? + if track_conf.get("style_lables", None): + default_session_data["style_labels"][key] = track_conf.get( + "style_labels", None + ) default_session_data["defaultLocation"] = root.find( "metadata/general/defaultLocation" @@ -1444,7 +1429,6 @@ jc.config_json["tracks"] = jc.tracksToAdd if jc.usejson: jc.write_config() - # jc.add_default_view() jc.add_default_session(default_session_data) # jc.text_index() not sure what broke here. diff -r 79f7265f90bd -r b04fd993b31e jbrowse2.xml --- a/jbrowse2.xml Tue Jan 09 06:55:34 2024 +0000 +++ b/jbrowse2.xml Wed Jan 17 07:50:52 2024 +0000 @@ -130,145 +130,9 @@ #end if - - - ## TODO other label options: https://github.com/GMOD/jbrowse-components/blob/main/plugins/svg/src/SvgFeatureRenderer/configSchema.ts - #if 'label' in $track.data_format.jbstyle.track_style - ${track.data_format.jbstyle.track_style.label} - #end if - #if 'description' in $track.data_format.jbstyle.track_style - ${track.data_format.jbstyle.track_style.description} - #end if - - #if str($track.data_format.data_format_select) == "gene_calls" or str($track.data_format.data_format_select) == "blast" or str($track.data_format.data_format_select) == "sparql": - - #if str($track.data_format.jbcolor_scale.color_score.color_score_select) == "none": - ignore - - #if str($track.data_format.jbcolor_scale.color_score.color.color_select) == "automatic": - __auto__ - #else - ${track.data_format.jbcolor_scale.color_score.color.style_color} - #end if - - #else - score - ${track.data_format.jbcolor_scale.color_score.score_scaling} - - ${track.data_format.jbcolor_scale.color_score.score_scales.scale_select} + - #if str($track.data_format.jbcolor_scale.color_score.score_scales.scale_select) == "manual": - ${track.data_format.jbcolor_scale.color_score.score_scales.minimum} - ${track.data_format.jbcolor_scale.color_score.score_scales.maximum} - #end if - - - ${track.data_format.jbcolor_scale.color_score.color_scheme.score_scheme} - ## auto_color - #if str($track.data_format.jbcolor_scale.color_score.color_scheme.score_scheme) == "opacity": - #if str($track.data_format.jbcolor_scale.color_score.color_scheme.color.color_select) == "automatic": - __auto__ - #else - ${track.data_format.jbcolor_scale.color_score.color_scheme.color.style_color} - #end if - #end if - - #end if - - - #for $menu_item in $track.data_format.jbmenu.track_menu: - - ${menu_item.menu_action} - #if str($menu_item.menu_label) != "": - - #end if - #if str($menu_item.menu_title) != "": - ${menu_item.menu_title} - #end if - #if str($menu_item.menu_url) != "": - ${menu_item.menu_url.replace("&", "&").replace("\"", """)} - #end if - #if str($menu_item.menu_icon) != "": - ${menu_item.menu_icon} - #end if - - #end for - - #end if - - #if str($track.data_format.data_format_select) == "wiggle": - - ${track.data_format.xyplot} - ${track.data_format.var_band} - #if str($track.data_format.scaling.scale_select) == "auto_local": - local - #else if str($track.data_format.scaling.scale_select) == "auto_global": - global - #else: - ${track.data_format.scaling.minimum} - ${track.data_format.scaling.maximum} - #end if - ${track.data_format.scale_select2} - - ## Wiggle tracks need special color config - #if str($track.data_format.jbcolor.color.color_select) != "automatic": - ${track.data_format.jbcolor.color.style_pos_color} - ${track.data_format.jbcolor.color.style_neg_color} - #else: - __auto__ - __auto__ - #end if - - ## Bicolor pivot config - #if str($track.data_format.jbcolor.bicolor_pivot.bicolor_pivot_select) == "zero": - zero - #else if str($track.data_format.jbcolor.bicolor_pivot.bicolor_pivot_select) == "mean": - mean - #else: - ${track.data_format.jbcolor.bicolor_pivot.pivot_point} - #end if - - #else if str($track.data_format.data_format_select) == "pileup": + #if str($track.data_format.data_format_select) == "pileup": #for $dataset in $track.data_format.annotation: @@ -291,14 +155,12 @@ #end if ${track.data_format.is_protein} ${track.data_format.min_gap} - ${track.data_format.index} #else if str($track.data_format.data_format_select) == "gene_calls": #if $track.data_format.match_part.match_part_select == "true": ${track.data_format.match_part.name} #end if - ${track.data_format.index} #else if str($track.data_format.data_format_select) == "synteny": @@ -360,17 +222,17 @@ value="Default" help="Organise your tracks into Categories for a nicer end-user experience. You can use #date# and it will be replaced with the current date in 'yyyy-mm-dd' format, which is very useful for repeatedly updating a JBrowse instance when member databases / underlying tool versions are updated." optional="False"/> - + - - + + - - + + - - + + @@ -393,18 +255,6 @@ name="is_protein" truevalue="true" falsevalue="false" /> - - - - - - @@ -429,68 +279,20 @@ - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - @@ -498,7 +300,7 @@ format="fasta" name="synteny_genome" type="data" /> - + @@ -593,7 +395,6 @@ - @@ -908,7 +709,6 @@ - @@ -928,13 +728,10 @@ - - - + - @@ -955,16 +752,22 @@ Use and local viewing ===================== + A JBrowse2 history item can be opened by viewing it (the "eye" icon). -They can also be downloaded as archives ("floppy disk" icon) to share and for local viewing. -One extra step is required before they can be viewed. A local python web server must be started using a script included in each archive. -Unzip the archive (*unzip [filename].zip*) and change to the first level directory. It contains a file named "webserver.py" + +The same browser data and setup can also be downloaded as a compressed zip archive by clicking the download ("floppy disk") icon in the history. +This can be shared and viewed without Galaxy. -Assuming you have python3 installed, running: +A replacement application to serve the browser is required without Galaxy. A local python web server can be started using a script included in each archive, +assuming that Python3 is already working on your desktop - if not you will have to install it first. Unzip the archive (*unzip [filename].zip*) and change +directory to the first level in that zip archive. It contains a file named *jb2_webserver.py* -*python3 webserver.py* +With python3 installed, -will serve the unarchived JBrowse2 configuration, so it can be browsed by pointing a web browser to localhost:8080 +*python3 jb2_webserver.py* + +will serve the unarchived JBrowse2 configuration from the same directory as the python script automatically. If a new browser window does not open, +but the script appears to be running, try pointing your web browser to the default of *localhost:8080* Overview -------- @@ -973,13 +776,9 @@ JavaScript and HTML5. The JBrowse-in-Galaxy (JiG) tool was written to help build complex -JBrowse installations straight from Galaxy, taking advantage of the -latest Galaxy features such as dataset collections, sections, and colour -pickers. It allows you to build up a JBrowse instance without worrying +JBrowse installations straight from Galaxy. It allows you to build up a JBrowse instance without worrying about how to run the command line tools to format your data, and which -options need to be supplied and where. Additionally it comes with many -javascript functions to handle colouring of features which would be -nearly impossible to write without the assistance of this tool. +options need to be supplied and where. The JBrowse-in-Galaxy tool is maintained by `the Galaxy IUC `__, who you can help you @@ -988,8 +787,8 @@ Options ------- -The first option you encounter is the **Fasta Sequence(s)**. This option -now accepts multiple fasta files, allowing you to build JBrowse +The first option you encounter is the **Reference sequence(s)** to use. This option +now accepts multiple fasta files, allowing you to build JBrowse2 instances that contain data for multiple genomes or chrosomomes (generally known as "landmark features" in gff3 terminology.) @@ -1046,11 +845,6 @@ they should adjust feature locations by 3x. -VCFs/SNPs -~~~~~~~~~ - -These tracks do not support any special configuration. - @ATTRIBUTION@ ]]> diff -r 79f7265f90bd -r b04fd993b31e plants.sh --- a/plants.sh Tue Jan 09 06:55:34 2024 +0000 +++ b/plants.sh Wed Jan 17 07:50:52 2024 +0000 @@ -1,1 +1,1 @@ -planemo shed_update --shed_target toolshed --owner fubar --name jbrowse2 --shed_key 8d01f2f35d48a0405f72d6d37aedde60 jbrowse2 +planemo shed_update --shed_target toolshed --owner fubar --name jbrowse2 --shed_key 8d01f2f35d48a0405f72d6d37aedde60 ./ diff -r 79f7265f90bd -r b04fd993b31e readme.rst --- a/readme.rst Tue Jan 09 06:55:34 2024 +0000 +++ b/readme.rst Wed Jan 17 07:50:52 2024 +0000 @@ -8,92 +8,40 @@ workflow summary. E.g. annotate a genome, then visualise all of the associated datasets as an interactive HTML page. This tool MUST be whitelisted (or ``sanitize_all_html=False`` in galaxy.ini) to function correctly. -gunicorn does not support byte range requests, so this tool must be served by nginx -or other web server, correctly configured to support range requests. +The built-in Galaxy gunicorn server does not support byte range requests, so this tool must be proxied by nginx +or another web server, correctly configured to support range requests. A tiny web server is bundled +with each JBrowse2 archive - see below. Installation ============ -It is recommended to install this wrapper via the Galaxy Tool Shed. +This wrapper is normally installed by a server administrator from the Galaxy Tool Shed IUC JBrowse2 repository. -Running Locally -=============== +Local display +============= -The Galaxy tool interface writes out a xml file which is then used to generate -the visualizations. An example used during development/testing can be seen in -`test-data/*/test.xml`. The format is in no way rigorously defined and is -likely to change at any time! Beware. ;) +Each JBrowse2 history item can be downloaded ("floppy disk" icon) to your local disk. There, it can be unzipped into a new directory. +That directory includes a python script, *jb2_webserver.py* that will run a local web server able to serve byte range requests, +giving the same view as seen when viewed from the Galaxy history. + +From the newly unzipped directory where that file can be found, and with Python3 installed and working, + +`python3 jb2_webserver.py` + +will open the preconfigured browser using the default web browser application. History ======= - 2.10.0+galaxy2 - - UPDATED to JBrowse 2.10.0 - - REMOVED most colour and track control from XML and script. + - UPDATED existing JBrowse1.16.11 code to JBrowse 2.10.0 - seems to work well with defaults. - need to document and implement track settings by running the browser locally. - works well enough to be useful in workflows such as TreeValGal. - JB2 seems to set defaults wisely. - not yet ideal for users who need fine grained track control. -- 1.16.11+galaxy0 - - - UPDATED to JBrowse 1.16.11 - -- 1.16.10+galaxy0 - - - UPDATED to JBrowse 1.16.10 - - ADDED GALAXY_JBROWSE_SYMLINKS environment variable: if set, the tool will make symlinks to bam/bigwig files instead of copying them - -- 1.16.9+galaxy0 - - - UPDATED to JBrowse 1.16.9 - -- 1.16.8+galaxy0 - - - UPDATED to JBrowse 1.16.8 - -- 1.16.5+galaxy0 - - - UPDATED to JBrowse 1.16.5 - -- 1.16.4+galaxy0 - - - UPDATED to JBrowse 1.16.4 - - ADDED filter too big metadata - - CHANGED default value for topLevelFeatures (gene subfeatures are now inferred) and style.className (feature style was fixed) - -- 1.16.2+galaxy0 - - - UPDATED to JBrowse 1.16.2 - - ADDED support for NeatHTMLFeatures and NeatCanvasFeatures track types - -- 1.16.1+galaxy0 - - - UPDATED to JBrowse 1.16.1 - - ADDED support for MultiBigWig plugin - - ADDED support for tabix indexing of fasta and gff - - ADDED support for REST and SPARQL endpoints - - ADDED option to change chunk size for BAM tracks - - FIXED loading of VCF files. They were gzipped and the URLs were incorrect - - FIXED metadata on tracks types other than GFF+HTML - - FIXED infrastructure URL parsing (and embedding in links) for some tracks - - REMOVED support for selecting multiple genomes as input due to tracking of track metadata - - REMOVED support for themes as JBrowse no longer allow runtime loading of plugins - -- 0.7 Support for plugins (currently GC Content, Bookmarks, ComboTrackSelector), - track metadata -- 0.5.2 Support for CanvasFeatures options. -- 0.5.1 Support for contextual menus. Conda tests. -- 0.5 Update existing instances on disk. Index names. Support HTML tracks - instead of Canvas. Support default tracks. General JBrowse optinos -- 0.4 Support for dataset collections and customisation of tracks including - labelling, colours, styling. Added support for genetic code selection. - Fixed package installation recipe issues. -- 0.3 Added support for BigWig, etc. -- 0.2 Added support for BAM, Blast, VCF. -- 0.1 Initial public release. Wrapper License (MIT/BSD Style) =============================== diff -r 79f7265f90bd -r b04fd993b31e webserver.py --- a/webserver.py Tue Jan 09 06:55:34 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 - -# spec: simplest python web server with range support and multithreading that takes root path, -# port and bind address as command line arguments; by default uses the current dir as webroot, -# port 8000 and bind address of 0.0.0.0 -# borrowed from https://github.com/danvk/RangeHTTPServer -# and reborrowed from https://gist.github.com/glowinthedark/b99900abe935e4ab4857314d647a9068 - - -import argparse -import functools -import os -import re -import socketserver -import webbrowser -from http.server import SimpleHTTPRequestHandler - - -DEFAULT_PORT = 8080 - - -def copy_byte_range(infile, outfile, start=None, stop=None, bufsize=16 * 1024): - """Like shutil.copyfileobj, but only copy a range of the streams. - - Both start and stop are inclusive. - """ - if start is not None: - infile.seek(start) - while 1: - to_read = min(bufsize, stop + 1 - infile.tell() if stop else bufsize) - buf = infile.read(to_read) - if not buf: - break - outfile.write(buf) - - -BYTE_RANGE_RE = re.compile(r"bytes=(\d+)-(\d+)?$") - - -def parse_byte_range(byte_range): - """Returns the two numbers in 'bytes=123-456' or throws ValueError. - - The last number or both numbers may be None. - """ - if byte_range.strip() == "": - return None, None - - m = BYTE_RANGE_RE.match(byte_range) - if not m: - raise ValueError("Invalid byte range %s" % byte_range) - - first, last = [x and int(x) for x in m.groups()] - if last and last < first: - raise ValueError("Invalid byte range %s" % byte_range) - return first, last - - -class RangeRequestHandler(SimpleHTTPRequestHandler): - """Adds support for HTTP 'Range' requests to SimpleHTTPRequestHandler - - The approach is to: - - Override send_head to look for 'Range' and respond appropriately. - - Override copyfile to only transmit a range when requested. - """ - - def handle(self): - try: - SimpleHTTPRequestHandler.handle(self) - except Exception: - # ignored, thrown whenever the client aborts streaming (broken pipe) - pass - - def send_head(self): - if "Range" not in self.headers: - self.range = None - return SimpleHTTPRequestHandler.send_head(self) - try: - self.range = parse_byte_range(self.headers["Range"]) - except ValueError: - self.send_error(400, "Invalid byte range") - return None - first, last = self.range - - # Mirroring SimpleHTTPServer.py here - path = self.translate_path(self.path) - f = None - ctype = self.guess_type(path) - try: - f = open(path, "rb") - except IOError: - self.send_error(404, "File not found") - return None - - fs = os.fstat(f.fileno()) - file_len = fs[6] - if first >= file_len: - self.send_error(416, "Requested Range Not Satisfiable") - return None - - self.send_response(206) - self.send_header("Content-type", ctype) - - if last is None or last >= file_len: - last = file_len - 1 - response_length = last - first + 1 - - self.send_header("Content-Range", "bytes %s-%s/%s" % (first, last, file_len)) - self.send_header("Content-Length", str(response_length)) - self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) - self.end_headers() - return f - - def end_headers(self): - self.send_header("Accept-Ranges", "bytes") - return SimpleHTTPRequestHandler.end_headers(self) - - def copyfile(self, source, outputfile): - if not self.range: - return SimpleHTTPRequestHandler.copyfile(self, source, outputfile) - - # SimpleHTTPRequestHandler uses shutil.copyfileobj, which doesn't let - # you stop the copying before the end of the file. - start, stop = self.range # set in send_head() - copy_byte_range(source, outputfile, start, stop) - - -class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): - allow_reuse_address = True - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Simple Python Web Server with Range Support" - ) - parser.add_argument( - "--root", - default=os.getcwd(), - help="Root path to serve files from (default: current working directory)", - ) - parser.add_argument( - "--port", - type=int, - default=DEFAULT_PORT, - help=f"Port to listen on (default: {DEFAULT_PORT})", - ) - parser.add_argument( - "--bind", default="0.0.0.0", help="IP address to bind to (default: 0.0.0.0)" - ) - args = parser.parse_args() - - handler = functools.partial(RangeRequestHandler, directory=args.root) - - webbrowser.open(f"http://{args.bind}:{args.port}") - - with ThreadedTCPServer((args.bind, args.port), handler) as httpd: - print( - f"Serving HTTP on {args.bind} port {args.port} (http://{args.bind}:{args.port}/)" - ) - httpd.serve_forever()