├── globals.gd.uid ├── lib ├── gff.gd.uid ├── blast.gd.uid ├── fastaq.gd.uid ├── annot_feature.gd.uid ├── blast_hit.gd.uid ├── colour_themes.gd.uid ├── project_data.gd.uid ├── user_data.gd.uid ├── genomes_n_matches │ ├── match.gd.uid │ ├── contig.gd.uid │ ├── genome.gd.uid │ ├── genome_matches.gd.uid │ ├── genome_matches.gd │ └── contig.gd ├── gff.gd ├── blast_hit.gd ├── fastaq.gd ├── blast.gd ├── colour_themes.gd ├── user_data.gd ├── annot_feature.gd └── project_data.gd ├── main.gd.uid ├── docs ├── requirements.txt ├── pics │ ├── zl_docs_contig_opts.png │ ├── zl_docs_copy_region.png │ ├── zl_docs_match_filter.png │ ├── zl_docs_match_list.png │ ├── zl_docs_nav_buttons.png │ ├── zl_docs_search_panel.png │ ├── zl_docs_seq_search.png │ ├── zl_docs_zoom_buttons.png │ ├── zl_docs_basepair_view.png │ ├── zl_docs_use_test_data.png │ ├── zl_docs_view_test_data.png │ ├── zl_docs_genomes_overview.png │ ├── zl_docs_genome_revcomp_buttons.png │ ├── zl_docs_overview_screenshots.gif │ └── zl_docs_search_annot_ppe_example.png ├── saving_n_loading.rst ├── keyboard_shortcuts.rst ├── uninstalling.rst ├── Makefile ├── make.bat ├── index.rst ├── contig_editing.rst ├── conf.py ├── faq.rst ├── searching.rst ├── data_folder.rst ├── settings.rst ├── loading.rst ├── background.rst ├── installation.rst └── viewing.rst ├── scenes ├── game │ ├── game.gd.uid │ ├── TopCoordsText.gd.uid │ ├── FiltMinLengthLineEdit.gd.uid │ ├── FiltMinLengthSpinBox.gd.uid │ ├── FiltMinPcSpinBox.gd.uid │ ├── RightBottomScrollbar.gd.uid │ ├── RightTopScrollbar.gd.uid │ ├── annotation_line_edit.gd.uid │ ├── sequence_line_edit.gd.uid │ ├── FiltMaxLengthLineEdit.gd.uid │ ├── FiltMinIdentityLineEdit.gd.uid │ ├── mult_matches_item_list.gd.uid │ ├── contig_opts_v_box_container.gd.uid │ ├── mult_matches_v_box_container.gd.uid │ ├── utils_choice_h_box_container.gd.uid │ ├── FiltMinPcSpinBox.gd │ ├── FiltMinLengthSpinBox.gd │ ├── contig_opts_v_box_container.gd │ ├── RightTopScrollbar.gd │ ├── RightBottomScrollbar.gd │ ├── annotation_line_edit.gd │ ├── FiltMinIdentityLineEdit.gd │ ├── FiltMaxLengthLineEdit.gd │ ├── FiltMinLengthLineEdit.gd │ ├── mult_matches_v_box_container.gd │ ├── utils_choice_h_box_container.gd │ ├── sequence_line_edit.gd │ ├── game.gd │ ├── mult_matches_item_list.gd │ └── TopCoordsText.gd ├── init │ ├── init.gd.uid │ ├── StatusRichTextLabel.gd.uid │ ├── StatusRichTextLabel.gd │ ├── init.tscn │ └── init.gd ├── main_menu │ ├── main_menu.gd.uid │ ├── StatusRichTextLabel.gd.uid │ ├── main_menu.gd │ ├── StatusRichTextLabel.gd │ └── main_menu.tscn ├── new_project │ ├── GoButton.gd.uid │ ├── StatusLabel.gd.uid │ ├── new_project.gd.uid │ ├── InfoRichTextLabel.gd.uid │ ├── TopGenomeLineEdit.gd.uid │ ├── compare_line_edit.gd.uid │ ├── BottomGenomeLineEdit.gd.uid │ ├── GoButton.gd │ ├── StatusLabel.gd │ ├── TopGenomeLineEdit.gd │ ├── BottomGenomeLineEdit.gd │ ├── InfoRichTextLabel.gd │ ├── compare_line_edit.gd │ ├── new_project.gd │ └── new_project.tscn ├── settings │ ├── settings.gd.uid │ ├── OpenDataDirButton.gd.uid │ ├── ThemeOptionButton.gd.uid │ ├── blast_share_usage_button.gd.uid │ ├── min_gap_length_line_edit.gd.uid │ ├── mouse_wheel_invert_button.gd.uid │ ├── mouse_wheel_sens_line_edit.gd.uid │ ├── trackpad_lr_sens_line_edit.gd.uid │ ├── trackpad_zoom_invert_button.gd.uid │ ├── fasta_line_length_line_edit.gd.uid │ ├── max_matches_on_screen_line_edit.gd.uid │ ├── trackpad_pinch_sens_line_edit.gd.uid │ ├── trackpad_zoom_sens_line_edit.gd.uid │ ├── OpenDataDirButton.gd │ ├── blast_share_usage_button.gd │ ├── mouse_wheel_invert_button.gd │ ├── trackpad_zoom_invert_button.gd │ ├── mouse_wheel_sens_line_edit.gd │ ├── trackpad_lr_sens_line_edit.gd │ ├── trackpad_zoom_sens_line_edit.gd │ ├── trackpad_pinch_sens_line_edit.gd │ ├── min_gap_length_line_edit.gd │ ├── fasta_line_length_line_edit.gd │ ├── ThemeOptionButton.gd │ ├── max_matches_on_screen_line_edit.gd │ └── settings.gd ├── load_project │ ├── LoadDialog.gd.uid │ ├── load_project.gd.uid │ ├── load_project.gd │ ├── LoadDialog.gd │ └── load_project.tscn ├── save_project │ ├── SaveDialog.gd.uid │ ├── save_project.gd.uid │ ├── save_project.gd │ ├── SaveDialog.gd │ └── save_project.tscn └── genomes_n_matches │ ├── genomes_n_matches.gd.uid │ └── genomes_n_matches.tscn ├── .gitignore ├── ziplign_logo.png ├── ziplign_splash.png ├── fonts ├── ziplignicons.otf ├── dejavu-sans │ ├── DejaVuSans.ttf │ └── LICENSE └── Anonymous-Pro │ ├── Anonymous_Pro.ttf │ ├── Anonymous_Pro_B.ttf │ ├── Anonymous_Pro_I.ttf │ ├── Anonymous_Pro_BI.ttf │ └── SIL Open Font License.txt ├── .readthedocs.yaml ├── LICENSE ├── globals.gd ├── README.md ├── main.tscn ├── project.godot └── main.gd /globals.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dw10yjblmu2af 2 | -------------------------------------------------------------------------------- /lib/gff.gd.uid: -------------------------------------------------------------------------------- 1 | uid://f4y2hst7p7p1 2 | -------------------------------------------------------------------------------- /main.gd.uid: -------------------------------------------------------------------------------- 1 | uid://05c32ervgjnx 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme 2 | -------------------------------------------------------------------------------- /lib/blast.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dghf6pibj1hc 2 | -------------------------------------------------------------------------------- /lib/fastaq.gd.uid: -------------------------------------------------------------------------------- 1 | uid://1wu3478tqys0 2 | -------------------------------------------------------------------------------- /lib/annot_feature.gd.uid: -------------------------------------------------------------------------------- 1 | uid://3ityhp1sda5b 2 | -------------------------------------------------------------------------------- /lib/blast_hit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bhs5cstaj0ds4 2 | -------------------------------------------------------------------------------- /lib/colour_themes.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cr6ncbbv1woy1 2 | -------------------------------------------------------------------------------- /lib/project_data.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b4qhqgbme3ptu 2 | -------------------------------------------------------------------------------- /lib/user_data.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cvx07a2dj4uox 2 | -------------------------------------------------------------------------------- /scenes/game/game.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bw7f0d0ctl7xu 2 | -------------------------------------------------------------------------------- /scenes/init/init.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bnt1uogjfe8ji 2 | -------------------------------------------------------------------------------- /lib/genomes_n_matches/match.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dj68fg82mle8x 2 | -------------------------------------------------------------------------------- /scenes/game/TopCoordsText.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dqq6g8kkawo24 2 | -------------------------------------------------------------------------------- /scenes/main_menu/main_menu.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cpkc1mlqlnoxq 2 | -------------------------------------------------------------------------------- /scenes/new_project/GoButton.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cy4f02oyivk82 2 | -------------------------------------------------------------------------------- /scenes/settings/settings.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b3n8sedxlv0c8 2 | -------------------------------------------------------------------------------- /lib/genomes_n_matches/contig.gd.uid: -------------------------------------------------------------------------------- 1 | uid://crurgcwo2pw8q 2 | -------------------------------------------------------------------------------- /lib/genomes_n_matches/genome.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cv0kkfsu3engw 2 | -------------------------------------------------------------------------------- /scenes/game/FiltMinLengthLineEdit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://3io25ml8xff5 2 | -------------------------------------------------------------------------------- /scenes/game/FiltMinLengthSpinBox.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ekxtc4wa50ww 2 | -------------------------------------------------------------------------------- /scenes/game/FiltMinPcSpinBox.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d1hsroxjk65j1 2 | -------------------------------------------------------------------------------- /scenes/game/RightBottomScrollbar.gd.uid: -------------------------------------------------------------------------------- 1 | uid://n1xucv2eaaj6 2 | -------------------------------------------------------------------------------- /scenes/game/RightTopScrollbar.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bbukglkqrf5m 2 | -------------------------------------------------------------------------------- /scenes/game/annotation_line_edit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cixly4awfhh8d 2 | -------------------------------------------------------------------------------- /scenes/game/sequence_line_edit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cjdxvit8sgauy 2 | -------------------------------------------------------------------------------- /scenes/init/StatusRichTextLabel.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cvqxad6n0mnjv 2 | -------------------------------------------------------------------------------- /scenes/load_project/LoadDialog.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ou3mye2efndh 2 | -------------------------------------------------------------------------------- /scenes/load_project/load_project.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b8cd30wt2qkqt 2 | -------------------------------------------------------------------------------- /scenes/new_project/StatusLabel.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dms14vxnfbknb 2 | -------------------------------------------------------------------------------- /scenes/new_project/new_project.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bombolyqp5yek 2 | -------------------------------------------------------------------------------- /scenes/save_project/SaveDialog.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cjp4jd3i5v8we 2 | -------------------------------------------------------------------------------- /scenes/save_project/save_project.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dj10n5hbre7qe 2 | -------------------------------------------------------------------------------- /lib/genomes_n_matches/genome_matches.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d4j3i7tqs8gsb 2 | -------------------------------------------------------------------------------- /scenes/game/FiltMaxLengthLineEdit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bq1gn5budg555 2 | -------------------------------------------------------------------------------- /scenes/game/FiltMinIdentityLineEdit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bxo4cuw0rn0qg 2 | -------------------------------------------------------------------------------- /scenes/game/mult_matches_item_list.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cq5pgmjc4f8qu 2 | -------------------------------------------------------------------------------- /scenes/main_menu/StatusRichTextLabel.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cfdgwk63rjx6y 2 | -------------------------------------------------------------------------------- /scenes/new_project/InfoRichTextLabel.gd.uid: -------------------------------------------------------------------------------- 1 | uid://5g4uryhwtpkc 2 | -------------------------------------------------------------------------------- /scenes/new_project/TopGenomeLineEdit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://3ohtdnp6hbad 2 | -------------------------------------------------------------------------------- /scenes/new_project/compare_line_edit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ccfxk07eo2btw 2 | -------------------------------------------------------------------------------- /scenes/settings/OpenDataDirButton.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d0j8uhaclkmov 2 | -------------------------------------------------------------------------------- /scenes/settings/ThemeOptionButton.gd.uid: -------------------------------------------------------------------------------- 1 | uid://di88nwymay6ai 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /scenes/game/contig_opts_v_box_container.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b70l6rhsp3aup 2 | -------------------------------------------------------------------------------- /scenes/game/mult_matches_v_box_container.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b2cpohnvbuqxa 2 | -------------------------------------------------------------------------------- /scenes/game/utils_choice_h_box_container.gd.uid: -------------------------------------------------------------------------------- 1 | uid://djc5k5n4pbl85 2 | -------------------------------------------------------------------------------- /scenes/genomes_n_matches/genomes_n_matches.gd.uid: -------------------------------------------------------------------------------- 1 | uid://chnpgqub8eda3 2 | -------------------------------------------------------------------------------- /scenes/new_project/BottomGenomeLineEdit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://do5dej12rld3h 2 | -------------------------------------------------------------------------------- /scenes/settings/blast_share_usage_button.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c7tqksy5w5bex 2 | -------------------------------------------------------------------------------- /scenes/settings/min_gap_length_line_edit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cx6lfjrhabuq4 2 | -------------------------------------------------------------------------------- /scenes/settings/mouse_wheel_invert_button.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dy4nj22m1nqgp 2 | -------------------------------------------------------------------------------- /scenes/settings/mouse_wheel_sens_line_edit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dxycgp4w5tiuv 2 | -------------------------------------------------------------------------------- /scenes/settings/trackpad_lr_sens_line_edit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bnhgaxi2tbvm2 2 | -------------------------------------------------------------------------------- /scenes/settings/trackpad_zoom_invert_button.gd.uid: -------------------------------------------------------------------------------- 1 | uid://pe5316d2y286 2 | -------------------------------------------------------------------------------- /scenes/settings/fasta_line_length_line_edit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://da4wi64mcw7ib 2 | -------------------------------------------------------------------------------- /scenes/settings/max_matches_on_screen_line_edit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bf2dv3p7ibb6e 2 | -------------------------------------------------------------------------------- /scenes/settings/trackpad_pinch_sens_line_edit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://br7tigeoppqen 2 | -------------------------------------------------------------------------------- /scenes/settings/trackpad_zoom_sens_line_edit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://clty8alxoewy8 2 | -------------------------------------------------------------------------------- /ziplign_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/ziplign_logo.png -------------------------------------------------------------------------------- /ziplign_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/ziplign_splash.png -------------------------------------------------------------------------------- /scenes/game/FiltMinPcSpinBox.gd: -------------------------------------------------------------------------------- 1 | extends SpinBox 2 | 3 | 4 | func _ready(): 5 | value = 90.0 6 | -------------------------------------------------------------------------------- /fonts/ziplignicons.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/fonts/ziplignicons.otf -------------------------------------------------------------------------------- /scenes/game/FiltMinLengthSpinBox.gd: -------------------------------------------------------------------------------- 1 | extends SpinBox 2 | 3 | 4 | func _ready(): 5 | value = 100 6 | -------------------------------------------------------------------------------- /docs/pics/zl_docs_contig_opts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_contig_opts.png -------------------------------------------------------------------------------- /docs/pics/zl_docs_copy_region.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_copy_region.png -------------------------------------------------------------------------------- /docs/pics/zl_docs_match_filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_match_filter.png -------------------------------------------------------------------------------- /docs/pics/zl_docs_match_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_match_list.png -------------------------------------------------------------------------------- /docs/pics/zl_docs_nav_buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_nav_buttons.png -------------------------------------------------------------------------------- /docs/pics/zl_docs_search_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_search_panel.png -------------------------------------------------------------------------------- /docs/pics/zl_docs_seq_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_seq_search.png -------------------------------------------------------------------------------- /docs/pics/zl_docs_zoom_buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_zoom_buttons.png -------------------------------------------------------------------------------- /fonts/dejavu-sans/DejaVuSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/fonts/dejavu-sans/DejaVuSans.ttf -------------------------------------------------------------------------------- /docs/pics/zl_docs_basepair_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_basepair_view.png -------------------------------------------------------------------------------- /docs/pics/zl_docs_use_test_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_use_test_data.png -------------------------------------------------------------------------------- /docs/pics/zl_docs_view_test_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_view_test_data.png -------------------------------------------------------------------------------- /docs/pics/zl_docs_genomes_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_genomes_overview.png -------------------------------------------------------------------------------- /fonts/Anonymous-Pro/Anonymous_Pro.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/fonts/Anonymous-Pro/Anonymous_Pro.ttf -------------------------------------------------------------------------------- /fonts/Anonymous-Pro/Anonymous_Pro_B.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/fonts/Anonymous-Pro/Anonymous_Pro_B.ttf -------------------------------------------------------------------------------- /fonts/Anonymous-Pro/Anonymous_Pro_I.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/fonts/Anonymous-Pro/Anonymous_Pro_I.ttf -------------------------------------------------------------------------------- /fonts/Anonymous-Pro/Anonymous_Pro_BI.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/fonts/Anonymous-Pro/Anonymous_Pro_BI.ttf -------------------------------------------------------------------------------- /docs/pics/zl_docs_genome_revcomp_buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_genome_revcomp_buttons.png -------------------------------------------------------------------------------- /docs/pics/zl_docs_overview_screenshots.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_overview_screenshots.gif -------------------------------------------------------------------------------- /docs/pics/zl_docs_search_annot_ppe_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinghunt/ziplign/HEAD/docs/pics/zl_docs_search_annot_ppe_example.png -------------------------------------------------------------------------------- /scenes/init/StatusRichTextLabel.gd: -------------------------------------------------------------------------------- 1 | extends RichTextLabel 2 | 3 | 4 | func _on_init_add_to_text_label(t, add_newline=true): 5 | append_text(t) 6 | if add_newline: 7 | append_text("\n") 8 | -------------------------------------------------------------------------------- /scenes/settings/OpenDataDirButton.gd: -------------------------------------------------------------------------------- 1 | extends Button 2 | 3 | 4 | func _ready(): 5 | pass # Replace with function body. 6 | 7 | 8 | func _on_pressed(): 9 | OS.shell_show_in_file_manager(OS.get_user_data_dir()) 10 | -------------------------------------------------------------------------------- /scenes/game/contig_opts_v_box_container.gd: -------------------------------------------------------------------------------- 1 | extends VBoxContainer 2 | 3 | 4 | func _ready(): 5 | hide() 6 | 7 | 8 | func _on_genomes_n_matches_enable_contig_ops(enable): 9 | if enable: 10 | show() 11 | else: 12 | hide() 13 | -------------------------------------------------------------------------------- /scenes/game/RightTopScrollbar.gd: -------------------------------------------------------------------------------- 1 | extends HScrollBar 2 | 3 | 4 | func _ready(): 5 | page = 1 6 | pass # Replace with function body. 7 | 8 | 9 | func _on_genomes_n_matches_hscrollbar_set_top_value(x): 10 | set_value_no_signal(x) 11 | -------------------------------------------------------------------------------- /scenes/game/RightBottomScrollbar.gd: -------------------------------------------------------------------------------- 1 | extends HScrollBar 2 | 3 | 4 | func _ready(): 5 | page = 1 6 | pass # Replace with function body. 7 | 8 | 9 | func _on_genomes_n_matches_hscrollbar_set_bottom_value(x): 10 | set_value_no_signal(x) 11 | -------------------------------------------------------------------------------- /scenes/game/annotation_line_edit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | 4 | signal annotation_search 5 | 6 | func _ready(): 7 | text = "" 8 | 9 | func _on_text_submitted(new_text): 10 | text = new_text 11 | annotation_search.emit(text) 12 | release_focus() 13 | -------------------------------------------------------------------------------- /scenes/new_project/GoButton.gd: -------------------------------------------------------------------------------- 1 | extends Button 2 | 3 | func _ready(): 4 | set_disabled(true) 5 | 6 | 7 | func _on_new_project_enable_go_button(x): 8 | set_disabled(not x) 9 | 10 | 11 | func _on_use_test_data_button_pressed(): 12 | set_disabled(false) 13 | -------------------------------------------------------------------------------- /scenes/genomes_n_matches/genomes_n_matches.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://eap762pqaesx"] 2 | 3 | [ext_resource type="Script" uid="uid://chnpgqub8eda3" path="res://scenes/genomes_n_matches/genomes_n_matches.gd" id="1_jvu84"] 4 | 5 | [node name="GenomesNMatches" type="Node2D"] 6 | script = ExtResource("1_jvu84") 7 | -------------------------------------------------------------------------------- /scenes/new_project/StatusLabel.gd: -------------------------------------------------------------------------------- 1 | extends Label 2 | 3 | var default_text = "" 4 | 5 | 6 | func reset_text(): 7 | text = default_text 8 | 9 | # Called when the node enters the scene tree for the first time. 10 | func _ready(): 11 | reset_text() 12 | 13 | 14 | func _on_new_project_set_status_text(t): 15 | text = t 16 | -------------------------------------------------------------------------------- /scenes/save_project/save_project.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | signal open_save_dialog 4 | signal return_to_main_menu 5 | 6 | 7 | func _ready(): 8 | pass 9 | 10 | 11 | func _on_main_menu_save_project(): 12 | open_save_dialog.emit() 13 | 14 | 15 | func _on_save_dialog_finished_saving(): 16 | return_to_main_menu.emit() 17 | -------------------------------------------------------------------------------- /scenes/new_project/TopGenomeLineEdit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | 4 | func _on_new_project_update_top_genome_filename(new_text): 5 | text = new_text 6 | 7 | 8 | func _on_use_test_data_button_pressed(): 9 | text = Globals.userdata.example_data_file1 10 | text_submitted.emit(text) 11 | 12 | 13 | func _on_focus_exited(): 14 | text_submitted.emit(text) 15 | -------------------------------------------------------------------------------- /scenes/new_project/BottomGenomeLineEdit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | 4 | func _on_new_project_update_bottom_genome_filename(new_text): 5 | text = new_text 6 | 7 | 8 | func _on_use_test_data_button_pressed(): 9 | text = Globals.userdata.example_data_file2 10 | text_submitted.emit(text) 11 | 12 | 13 | func _on_focus_exited(): 14 | text_submitted.emit(text) 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | build: 8 | os: ubuntu-22.04 9 | tools: 10 | python: "3.12" 11 | 12 | sphinx: 13 | configuration: docs/conf.py 14 | 15 | python: 16 | install: 17 | - requirements: docs/requirements.txt 18 | -------------------------------------------------------------------------------- /scenes/settings/blast_share_usage_button.gd: -------------------------------------------------------------------------------- 1 | extends Button 2 | 3 | 4 | func _ready(): 5 | set_pressed(Globals.userdata.config.get_value("blast", "share_data", false)) 6 | update_text() 7 | 8 | func update_text(): 9 | if button_pressed: 10 | text = ":" 11 | else: 12 | text = ">" 13 | 14 | func _pressed(): 15 | Globals.userdata.config.set_value("blast", "share_data", button_pressed) 16 | update_text() 17 | -------------------------------------------------------------------------------- /scenes/settings/mouse_wheel_invert_button.gd: -------------------------------------------------------------------------------- 1 | extends Button 2 | 3 | 4 | func _ready(): 5 | set_pressed(Globals.userdata.config.get_value("mouse", "invert_wheel", false)) 6 | update_text() 7 | 8 | func update_text(): 9 | if button_pressed: 10 | text = ":" 11 | else: 12 | text = ">" 13 | 14 | func _pressed(): 15 | Globals.userdata.config.set_value("mouse", "invert_wheel", button_pressed) 16 | update_text() 17 | -------------------------------------------------------------------------------- /scenes/settings/trackpad_zoom_invert_button.gd: -------------------------------------------------------------------------------- 1 | extends Button 2 | 3 | 4 | func _ready(): 5 | set_pressed(Globals.userdata.config.get_value("trackpad", "invert_v", false)) 6 | update_text() 7 | 8 | func update_text(): 9 | if button_pressed: 10 | text = ":" 11 | else: 12 | text = ">" 13 | 14 | func _pressed(): 15 | Globals.userdata.config.set_value("trackpad", "invert_v", button_pressed) 16 | update_text() 17 | -------------------------------------------------------------------------------- /scenes/load_project/load_project.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | signal open_load_dialog 4 | signal return_to_game_reload 5 | signal return_to_main_menu 6 | 7 | 8 | func _ready(): 9 | pass 10 | 11 | 12 | func _on_main_menu_load_project(): 13 | open_load_dialog.emit() 14 | 15 | 16 | func _on_load_dialog_finished_loading(): 17 | return_to_game_reload.emit() 18 | 19 | 20 | func _on_load_dialog_cancelled_loading(): 21 | return_to_main_menu.emit() 22 | -------------------------------------------------------------------------------- /scenes/new_project/InfoRichTextLabel.gd: -------------------------------------------------------------------------------- 1 | extends RichTextLabel 2 | 3 | 4 | 5 | func _ready(): 6 | pass 7 | 8 | 9 | func _on_new_project_append_to_info_text(t, append_newline=true): 10 | append_text(t) 11 | if append_newline: 12 | append_text("\n") 13 | var scrollbar = $"..".get_v_scroll_bar() 14 | await scrollbar.changed 15 | $"..".scroll_vertical = scrollbar.max_value 16 | 17 | 18 | func _on_new_project_clear_info_text(): 19 | text = "" 20 | clear() 21 | -------------------------------------------------------------------------------- /scenes/load_project/LoadDialog.gd: -------------------------------------------------------------------------------- 1 | extends FileDialog 2 | 3 | 4 | signal finished_loading 5 | signal cancelled_loading 6 | 7 | 8 | func _ready(): 9 | hide() 10 | 11 | 12 | func _on_load_project_open_load_dialog(): 13 | set_current_dir(Globals.userdata.home_dir) 14 | show() 15 | 16 | 17 | func _on_file_selected(path): 18 | Globals.proj_data.init_from_dir(path) 19 | hide() 20 | finished_loading.emit() 21 | 22 | 23 | func _on_canceled(): 24 | hide() 25 | cancelled_loading.emit() 26 | -------------------------------------------------------------------------------- /scenes/new_project/compare_line_edit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | 4 | # Called when the node enters the scene tree for the first time. 5 | func _ready(): 6 | text = Globals.userdata.default_blast_options 7 | 8 | 9 | func _on_text_submitted(new_text): 10 | text = new_text 11 | Globals.userdata.blast_options = new_text 12 | 13 | 14 | func _on_focus_exited(): 15 | Globals.userdata.blast_options = text 16 | 17 | 18 | func _on_new_project_reset_compare_line_edit(): 19 | _on_text_submitted(Globals.userdata.default_blast_options) 20 | -------------------------------------------------------------------------------- /scenes/game/FiltMinIdentityLineEdit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | signal min_match_pc_id_changed 4 | 5 | var min_id = Globals.match_min_show_pc_id 6 | 7 | func _ready(): 8 | text = str(min_id) 9 | 10 | func _on_text_submitted(new_text): 11 | if new_text.is_valid_float(): 12 | min_id = float(new_text) 13 | if min_id < 0: 14 | min_id = 0 15 | elif min_id > 100: 16 | min_id = 100 17 | min_match_pc_id_changed.emit(min_id) 18 | text = str(min_id) 19 | release_focus() 20 | 21 | 22 | func _on_focus_exited(): 23 | _on_text_submitted(text) 24 | -------------------------------------------------------------------------------- /scenes/game/FiltMaxLengthLineEdit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | signal max_match_length_changed 4 | 5 | var max_l = Globals.match_max_show_length 6 | 7 | # Called when the node enters the scene tree for the first time. 8 | func _ready(): 9 | text = str(max_l) 10 | 11 | 12 | func _on_text_submitted(new_text): 13 | if new_text.is_valid_int(): 14 | max_l = int(new_text) 15 | if max_l < 0: 16 | max_l = 0 17 | max_match_length_changed.emit(max_l) 18 | text = str(max_l) 19 | release_focus() 20 | 21 | 22 | func _on_focus_exited(): 23 | _on_text_submitted(text) 24 | -------------------------------------------------------------------------------- /scenes/game/FiltMinLengthLineEdit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | signal min_match_length_changed 4 | 5 | var min_l = Globals.match_min_show_length 6 | 7 | # Called when the node enters the scene tree for the first time. 8 | func _ready(): 9 | text = str(min_l) 10 | 11 | 12 | func _on_text_submitted(new_text): 13 | if new_text.is_valid_int(): 14 | min_l = int(new_text) 15 | if min_l < 0: 16 | min_l = 0 17 | min_match_length_changed.emit(min_l) 18 | text = str(min_l) 19 | release_focus() 20 | 21 | 22 | func _on_focus_exited(): 23 | _on_text_submitted(text) 24 | -------------------------------------------------------------------------------- /scenes/settings/mouse_wheel_sens_line_edit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | 4 | var sens 5 | 6 | 7 | func _ready(): 8 | sens = Globals.userdata.config.get_value("mouse", "wheel_sens", 1.0) 9 | text = str(sens) 10 | 11 | func _on_text_submitted(new_text): 12 | if new_text.is_valid_float(): 13 | sens = float(new_text) 14 | if sens < 0: 15 | sens = 0 16 | new_text = "0" 17 | text = new_text 18 | Globals.userdata.config.set_value("mouse", "wheel_sens", sens) 19 | else: 20 | text = str(sens) 21 | 22 | 23 | func _on_focus_exited(): 24 | _on_text_submitted(text) 25 | -------------------------------------------------------------------------------- /scenes/settings/trackpad_lr_sens_line_edit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | 4 | var sens 5 | 6 | 7 | func _ready(): 8 | sens = Globals.userdata.config.get_value("trackpad", "h_sens", 1.0) 9 | text = str(sens) 10 | 11 | func _on_text_submitted(new_text): 12 | if new_text.is_valid_float(): 13 | sens = float(new_text) 14 | if sens < 0: 15 | sens = 0 16 | new_text = "0" 17 | text = new_text 18 | Globals.userdata.config.set_value("trackpad", "h_sens", sens) 19 | else: 20 | text = str(sens) 21 | 22 | 23 | func _on_focus_exited(): 24 | _on_text_submitted(text) 25 | -------------------------------------------------------------------------------- /scenes/settings/trackpad_zoom_sens_line_edit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | 4 | var sens 5 | 6 | 7 | func _ready(): 8 | sens = Globals.userdata.config.get_value("trackpad", "v_sens", 1.0) 9 | text = str(sens) 10 | 11 | func _on_text_submitted(new_text): 12 | if new_text.is_valid_float(): 13 | sens = float(new_text) 14 | if sens < 0: 15 | sens = 0 16 | new_text = "0" 17 | text = new_text 18 | Globals.userdata.config.set_value("trackpad", "v_sens", sens) 19 | else: 20 | text = str(sens) 21 | 22 | 23 | func _on_focus_exited(): 24 | _on_text_submitted(text) 25 | -------------------------------------------------------------------------------- /scenes/settings/trackpad_pinch_sens_line_edit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | 4 | var sens 5 | 6 | 7 | func _ready(): 8 | sens = Globals.userdata.config.get_value("trackpad", "p_sens", 1.0) 9 | text = str(sens) 10 | 11 | func _on_text_submitted(new_text): 12 | if new_text.is_valid_float(): 13 | sens = float(new_text) 14 | if sens < 0: 15 | sens = 0 16 | new_text = "0" 17 | text = new_text 18 | Globals.userdata.config.set_value("trackpad", "p_sens", sens) 19 | else: 20 | text = str(sens) 21 | 22 | 23 | func _on_focus_exited(): 24 | _on_text_submitted(text) 25 | -------------------------------------------------------------------------------- /docs/saving_n_loading.rst: -------------------------------------------------------------------------------- 1 | Saving and Loading 2 | ================== 3 | 4 | Once genomes have been imported and BLAST has been run, the entire project 5 | can be saved, via the "Save" button in the main menu. It saves the current 6 | state of the contigs, so if they have been re-ordered or reverse complemented 7 | then that is what is saved, not the original state of the genomes. 8 | 9 | Ziplign saves both genomes and the blast matches inside a single file that it 10 | can then load, via the "Load" button in the main menu. The file format is 11 | specific to Ziplign and will not work with other applications. 12 | 13 | -------------------------------------------------------------------------------- /docs/keyboard_shortcuts.rst: -------------------------------------------------------------------------------- 1 | Keyboard shortcuts 2 | ================== 3 | 4 | Some of the controls have keyboard shortcuts. They are listed below. 5 | 6 | * ``left``/``right``: moves both genomes to the left or the right 7 | * ``up``/``down``: moves the top/bottom genomes in opposite directions ("rotating 8 | the view") 9 | * ``f``: toggle fine control on or off 10 | * ``+``/``-``: zoom in/out 11 | * ``=``: set view back to the default 12 | * ``0``: zoom to base-pair view 13 | * (``shift`` and) ``1-9``: save/load temporary views 14 | * ``q`` or ``p``: when viewing genomes, return to main menu 15 | * ``r``: when in the main menu, return to viewing genomes 16 | -------------------------------------------------------------------------------- /scenes/settings/min_gap_length_line_edit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | var min_gap_length: int 4 | 5 | 6 | func _ready(): 7 | min_gap_length = Globals.userdata.config.get_value("other", "min_gap_length", 10) 8 | text = str(min_gap_length) 9 | 10 | 11 | func _on_text_submitted(new_text): 12 | if new_text.is_valid_int(): 13 | min_gap_length = int(new_text) 14 | if min_gap_length < 0: 15 | min_gap_length = 0 16 | new_text = "0" 17 | text = new_text 18 | Globals.userdata.config.set_value("other", "min_gap_length", min_gap_length) 19 | else: 20 | text = str(min_gap_length) 21 | 22 | 23 | func _on_focus_exited(): 24 | _on_text_submitted(text) 25 | -------------------------------------------------------------------------------- /docs/uninstalling.rst: -------------------------------------------------------------------------------- 1 | Uninstalling Ziplign 2 | ==================== 3 | 4 | Ziplign stores files in a data folder somewhere inside your home folder. 5 | Its location depends on your operating system. 6 | Open Ziplign, and then in settings use the 7 | "Open Ziplign data folder" to open the folder in your file browser. 8 | 9 | Close Ziplign, and then delete the Ziplign data folder. This has cleared all files 10 | that Ziplign made. If you were to restart Ziplign, then the folder will be recreated. 11 | 12 | Once the data folder is deleted, delete the Ziplign app itself, in other words 13 | the .exe file on Windows, .app on macOS, or the executable file on Linux. 14 | 15 | -------------------------------------------------------------------------------- /scenes/settings/fasta_line_length_line_edit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | var fasta_line_length: int 4 | 5 | 6 | func _ready(): 7 | fasta_line_length = Globals.userdata.config.get_value("other", "fasta_line_length", 60) 8 | text = str(fasta_line_length) 9 | 10 | 11 | func _on_text_submitted(new_text): 12 | if new_text.is_valid_int(): 13 | fasta_line_length = int(new_text) 14 | if fasta_line_length < 0: 15 | fasta_line_length = 0 16 | new_text = "0" 17 | text = new_text 18 | Globals.userdata.config.set_value("other", "fasta_line_length", fasta_line_length) 19 | else: 20 | text = str(fasta_line_length) 21 | 22 | 23 | func _on_focus_exited(): 24 | _on_text_submitted(text) 25 | -------------------------------------------------------------------------------- /scenes/settings/ThemeOptionButton.gd: -------------------------------------------------------------------------------- 1 | extends OptionButton 2 | 3 | signal theme_updated 4 | 5 | var names = Globals.theme.theme_names() 6 | 7 | func _ready(): 8 | for i in range(names.size()): 9 | add_item(names[i], i) 10 | if names[i] == Globals.theme.name: 11 | selected = i 12 | 13 | func set_selected_to_match_theme(): 14 | for i in range(names.size()): 15 | if names[i] == Globals.theme.name: 16 | selected = i 17 | break 18 | 19 | func _on_item_selected(index): 20 | if names[index] != Globals.theme.name: 21 | Globals.theme.set_theme(names[index]) 22 | Globals.reload_needed = true 23 | theme_updated.emit() 24 | Globals.userdata.config.set_value("colours", "theme", names[index]) 25 | -------------------------------------------------------------------------------- /scenes/settings/max_matches_on_screen_line_edit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | signal max_matches_updated 4 | var maxOnScreen: int 5 | 6 | 7 | func _ready(): 8 | maxOnScreen = Globals.userdata.config.get_value("other", "max_matches_on_screen", 500) 9 | text = str(maxOnScreen) 10 | 11 | func _on_text_submitted(new_text): 12 | if new_text.is_valid_int(): 13 | maxOnScreen = int(new_text) 14 | if maxOnScreen < 0: 15 | maxOnScreen = 0 16 | new_text = "0" 17 | text = new_text 18 | Globals.userdata.config.set_value("other", "max_matches_on_screen", maxOnScreen) 19 | max_matches_updated.emit() 20 | else: 21 | text = str(maxOnScreen) 22 | 23 | 24 | func _on_focus_exited(): 25 | _on_text_submitted(text) 26 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /scenes/game/mult_matches_v_box_container.gd: -------------------------------------------------------------------------------- 1 | extends VBoxContainer 2 | 3 | 4 | func _ready(): 5 | hide() 6 | 7 | 8 | 9 | func _on_genomes_n_matches_multimatch_list_found(match_ids): 10 | $MultiMatchesScrollContainer/MultMatchesItemList.set_matches(match_ids) 11 | show() 12 | 13 | func _on_genomes_n_matches_annotation_list_found(annot_data): 14 | $MultiMatchesScrollContainer/MultMatchesItemList.set_annotation(annot_data) 15 | show() 16 | 17 | func _on_genomes_n_matches_sequence_list_found(seq_data): 18 | $MultiMatchesScrollContainer/MultMatchesItemList.set_sequence(seq_data) 19 | show() 20 | 21 | func _on_up_button_button_up(): 22 | pass 23 | 24 | 25 | func _on_down_button_button_up(): 26 | pass 27 | 28 | 29 | func _on_close_button_button_up(): 30 | hide() 31 | -------------------------------------------------------------------------------- /scenes/game/utils_choice_h_box_container.gd: -------------------------------------------------------------------------------- 1 | extends HBoxContainer 2 | 3 | 4 | func _ready(): 5 | $FilterButton.set_pressed_no_signal(true) 6 | $SearchButton.set_pressed_no_signal(false) 7 | $"../SearchVBoxContainer".hide() 8 | $"../FilterVBoxContainer".show() 9 | 10 | 11 | func _on_filter_button_toggled(toggled_on): 12 | if not toggled_on: 13 | $FilterButton.set_pressed(true) 14 | else: 15 | $SearchButton.set_pressed_no_signal(false) 16 | $"../SearchVBoxContainer".hide() 17 | $"../FilterVBoxContainer".show() 18 | 19 | func _on_search_button_toggled(toggled_on): 20 | if not toggled_on: 21 | $SearchButton.set_pressed(true) 22 | else: 23 | $FilterButton.set_pressed_no_signal(false) 24 | $"../FilterVBoxContainer".hide() 25 | $"../SearchVBoxContainer".show() 26 | -------------------------------------------------------------------------------- /scenes/save_project/SaveDialog.gd: -------------------------------------------------------------------------------- 1 | extends FileDialog 2 | 3 | signal finished_saving 4 | 5 | 6 | func _ready(): 7 | hide() 8 | 9 | 10 | func _on_save_project_open_save_dialog(): 11 | #size.x = DisplayServer.window_get_size().x - 20 12 | #size.y = DisplayServer.window_get_size().y - 30 13 | #var window_size = get_viewport().get_visible_rect().size 14 | #print("in _on_save_project_open_save_dialog(). window_size:", window_size) 15 | #print("DisplayServer.window_get_size():", DisplayServer.window_get_size()) 16 | set_current_dir(Globals.userdata.home_dir) 17 | show() 18 | 19 | 20 | func _on_file_selected(path): 21 | Globals.proj_data.save_as_serialized_file(path) 22 | hide() 23 | finished_saving.emit() 24 | 25 | 26 | func _on_canceled(): 27 | hide() 28 | finished_saving.emit() 29 | -------------------------------------------------------------------------------- /scenes/game/sequence_line_edit.gd: -------------------------------------------------------------------------------- 1 | extends LineEdit 2 | 3 | signal sequence_search 4 | 5 | func _ready(): 6 | text = "" 7 | 8 | 9 | func _on_text_submitted(new_text): 10 | new_text = new_text.strip_edges() 11 | text = new_text 12 | sequence_search.emit(text) 13 | release_focus() 14 | 15 | 16 | func _gui_input(event): 17 | if Globals.paused: 18 | return 19 | 20 | if event.is_action_pressed("ui_paste"): 21 | var lines = DisplayServer.clipboard_get().strip_edges().split("\n") 22 | var start = 0 23 | if lines[0][0] == ">": 24 | start = 1 25 | 26 | var i = start 27 | while i < len(lines) and lines[i][0] != ">": 28 | lines[i] = lines[i].strip_edges() 29 | i += 1 30 | 31 | clear() 32 | await get_tree().create_timer(0.01).timeout 33 | set_text("".join(lines.slice(start, i))) 34 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Ziplign documentation 2 | ===================== 3 | 4 | Ziplign is a free application to visualise the comparison of two bacterial genomes. 5 | It is intended to be easy to use: you just need to drag and drop two sequence 6 | files to compare. 7 | Ziplign will run BLAST for you and show you the results. There is no need to use a 8 | terminal or type any commands. 9 | 10 | .. image:: pics/zl_docs_overview_screenshots.gif 11 | :width: 70% 12 | :alt: screenshots showing an overview of ziplign 13 | 14 | The source code is in the `Ziplign GitHub repository `_, 15 | under the permissive MIT license. Please report any issues on GitHub. 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | :caption: Contents: 20 | 21 | installation 22 | loading 23 | viewing 24 | searching 25 | contig_editing 26 | saving_n_loading 27 | keyboard_shortcuts 28 | settings 29 | data_folder 30 | uninstalling 31 | contributing 32 | background 33 | faq 34 | -------------------------------------------------------------------------------- /docs/contig_editing.rst: -------------------------------------------------------------------------------- 1 | Contig editing 2 | ============== 3 | 4 | Ziplign currently supports basic editing: contigs can be reverse complemented or 5 | moved. 6 | 7 | 8 | To reverse complement every contig in either genome, use the buttons in the left 9 | panel. 10 | 11 | .. image:: pics/zl_docs_genome_revcomp_buttons.png 12 | :width: 400 13 | :alt: screenshot showing genome reverse complement buttons 14 | 15 | 16 | Note: this could take a few seconds, because it has to process every 17 | blast alignment (to the base-pair level), reversing them all. 18 | 19 | 20 | Move or reverse complement a contig 21 | ----------------------------------- 22 | 23 | Select a contig by left-clicking on it. You will see a message at the top 24 | of the window similar to "Selected - contig: top genome / contig1". 25 | At the bottom of the left panel, the contig options will appear. 26 | There are buttons to move the contig, or reverse complement it. 27 | 28 | 29 | .. image:: pics/zl_docs_contig_opts.png 30 | :width: 400 31 | :alt: screenshot showing contig options buttons 32 | -------------------------------------------------------------------------------- /scenes/settings/settings.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | signal return_to_main_menu 4 | signal theme_updated 5 | signal redraw_matches 6 | 7 | func _ready(): 8 | hide() 9 | 10 | func _on_return_button_pressed(): 11 | Globals.userdata.save_config() 12 | hide() 13 | return_to_main_menu.emit() 14 | 15 | 16 | func _on_main_menu_open_settings(): 17 | $VBoxContainer/GridContainer/ThemeOptionButton.set_selected_to_match_theme() 18 | $VBoxContainer/GridContainer/MouseWheelSensLineEdit._ready() 19 | $VBoxContainer/GridContainer/MouseWheelInvertButton._ready() 20 | $VBoxContainer/GridContainer/TrackpadZoomSensLineEdit._ready() 21 | $VBoxContainer/GridContainer/TrackpadZoomInvertButton._ready() 22 | $VBoxContainer/GridContainer/TrackpadLRSensLineEdit._ready() 23 | $VBoxContainer/GridContainer/BlastShareUsageButton._ready() 24 | $VBoxContainer/GridContainer/MaxMatchesOnScreenLineEdit._ready() 25 | $VBoxContainer/GridContainer/FastaLineLengthLineEdit._ready() 26 | show() 27 | 28 | func _on_theme_option_button_theme_updated(): 29 | theme_updated.emit() 30 | 31 | func _on_max_matches_updated(): 32 | redraw_matches.emit() 33 | -------------------------------------------------------------------------------- /scenes/save_project/save_project.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://bsbxlfa2t7bm5"] 2 | 3 | [ext_resource type="Script" uid="uid://dj10n5hbre7qe" path="res://scenes/save_project/save_project.gd" id="1_8a3a3"] 4 | [ext_resource type="Script" uid="uid://cjp4jd3i5v8we" path="res://scenes/save_project/SaveDialog.gd" id="2_gsdyg"] 5 | 6 | [node name="SaveProject" type="Node"] 7 | script = ExtResource("1_8a3a3") 8 | 9 | [node name="SaveDialog" type="FileDialog" parent="."] 10 | mode = 2 11 | title = "Save project to file" 12 | size = Vector2i(1130, 630) 13 | visible = true 14 | exclusive = false 15 | unresizable = true 16 | borderless = true 17 | dialog_hide_on_ok = true 18 | access = 2 19 | script = ExtResource("2_gsdyg") 20 | 21 | [connection signal="open_save_dialog" from="." to="SaveDialog" method="_on_save_project_open_save_dialog"] 22 | [connection signal="canceled" from="SaveDialog" to="SaveDialog" method="_on_canceled"] 23 | [connection signal="file_selected" from="SaveDialog" to="SaveDialog" method="_on_file_selected"] 24 | [connection signal="finished_saving" from="SaveDialog" to="." method="_on_save_dialog_finished_saving"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Martin Hunt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scenes/init/init.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://dkqo7pcll5pjj"] 2 | 3 | [ext_resource type="Script" uid="uid://bnt1uogjfe8ji" path="res://scenes/init/init.gd" id="1_fgosu"] 4 | [ext_resource type="Script" uid="uid://cvqxad6n0mnjv" path="res://scenes/init/StatusRichTextLabel.gd" id="2_fgc48"] 5 | 6 | [node name="Init" type="Control"] 7 | top_level = true 8 | layout_mode = 3 9 | anchors_preset = 15 10 | anchor_right = 1.0 11 | anchor_bottom = 1.0 12 | grow_horizontal = 2 13 | grow_vertical = 2 14 | script = ExtResource("1_fgosu") 15 | 16 | [node name="StatusRichTextLabel" type="RichTextLabel" parent="."] 17 | layout_mode = 0 18 | offset_right = 1140.0 19 | offset_bottom = 644.0 20 | focus_mode = 2 21 | theme_override_font_sizes/bold_italics_font_size = 20 22 | theme_override_font_sizes/italics_font_size = 20 23 | theme_override_font_sizes/mono_font_size = 20 24 | theme_override_font_sizes/normal_font_size = 20 25 | theme_override_font_sizes/bold_font_size = 20 26 | selection_enabled = true 27 | script = ExtResource("2_fgc48") 28 | 29 | [connection signal="add_to_text_label" from="." to="StatusRichTextLabel" method="_on_init_add_to_text_label"] 30 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'Ziplign' 10 | copyright = '2024, Martin Hunt' 11 | author = 'Martin Hunt' 12 | release = '0.0.1' 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | 17 | extensions = [ 18 | "sphinx.ext.autosectionlabel", 19 | ] 20 | autosectionlabel_prefix_document = True 21 | 22 | templates_path = ['_templates'] 23 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 24 | 25 | 26 | 27 | # -- Options for HTML output ------------------------------------------------- 28 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 29 | 30 | html_theme = 'sphinx_rtd_theme' 31 | html_static_path = ['_static'] 32 | -------------------------------------------------------------------------------- /scenes/load_project/load_project.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://bdpfqi7xni7"] 2 | 3 | [ext_resource type="Script" uid="uid://b8cd30wt2qkqt" path="res://scenes/load_project/load_project.gd" id="1_63hee"] 4 | [ext_resource type="Script" uid="uid://ou3mye2efndh" path="res://scenes/load_project/LoadDialog.gd" id="3_6chvd"] 5 | 6 | [node name="LoadProject" type="Node"] 7 | script = ExtResource("1_63hee") 8 | 9 | [node name="LoadDialog" type="FileDialog" parent="."] 10 | title = "Open a File" 11 | size = Vector2i(1130, 630) 12 | visible = true 13 | unresizable = true 14 | borderless = true 15 | ok_button_text = "Open" 16 | dialog_hide_on_ok = true 17 | file_mode = 0 18 | access = 2 19 | script = ExtResource("3_6chvd") 20 | 21 | [connection signal="open_load_dialog" from="." to="LoadDialog" method="_on_load_project_open_load_dialog"] 22 | [connection signal="canceled" from="LoadDialog" to="LoadDialog" method="_on_canceled"] 23 | [connection signal="cancelled_loading" from="LoadDialog" to="." method="_on_load_dialog_cancelled_loading"] 24 | [connection signal="file_selected" from="LoadDialog" to="LoadDialog" method="_on_file_selected"] 25 | [connection signal="finished_loading" from="LoadDialog" to="." method="_on_load_dialog_finished_loading"] 26 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | Frequently Asked Questions 2 | ========================== 3 | 4 | 5 | Can I compare more than two genomes? 6 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 7 | 8 | Sorry, no. This would have been significantly harder to implement. ACT can do 9 | it. 10 | 11 | 12 | Why no Windows 10 support? 13 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 14 | 15 | Because: 16 | 17 | 1. I can't get BLAST to run. Everything else works. 18 | 2. I don't have a proper Windows 10 machine to test on (only a VM), so 19 | this is difficult to debug. 20 | 21 | If you know how to run BLAST natively in Windows 10, please get in contact. 22 | 23 | 24 | Why no Windows ARM support? 25 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 26 | 27 | Because there is no version of BLAST for Windows/ARM. 28 | Everything else works. 29 | 30 | 31 | Why do the tooltips have slightly blurry text? 32 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 33 | 34 | This seems to be a known issue with Godot rendering. 35 | Sorry but it probably won't get fixed in Ziplign unless it's changed in Godot. 36 | 37 | 38 | Why are the save/load windows slightly blurry? 39 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 40 | 41 | For the same reason as the tooltips. Please see the previous answer. 42 | One way around it is to make a custom save/load UI instead of the built-in 43 | ones that Ziplign currently uses, which is not worth the effort. 44 | 45 | 46 | Why not use ACT instead? 47 | ^^^^^^^^^^^^^^^^^^^^^^^^ 48 | 49 | Go ahead and use ACT. Or use Ziplign. Up to you. Choose your favourite. 50 | 51 | -------------------------------------------------------------------------------- /scenes/main_menu/main_menu.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | signal resume 4 | signal reload 5 | signal new_project 6 | signal load_project 7 | signal save_project 8 | signal open_settings 9 | signal rerun_status_check 10 | 11 | var quit_selected = false 12 | 13 | 14 | func _ready(): 15 | Globals.proj_data.connect("project_data_loaded", _on_project_data_loaded) 16 | hide() 17 | 18 | func _on_project_data_loaded(): 19 | $MainContainer/MainVBoxContainer/ResumeButton.disabled = false 20 | $MainContainer/MainVBoxContainer/SaveButton.disabled = false 21 | 22 | func _on_resume_button_pressed(): 23 | hide() 24 | if Globals.reload_needed: 25 | Globals.reload_needed = false 26 | reload.emit() 27 | else: 28 | resume.emit() 29 | 30 | 31 | func _on_new_button_pressed(): 32 | hide() 33 | new_project.emit() 34 | 35 | 36 | func _on_load_button_pressed(): 37 | hide() 38 | load_project.emit() 39 | 40 | 41 | func _on_save_button_pressed(): 42 | hide() 43 | save_project.emit() 44 | 45 | 46 | func _on_game_pause(): 47 | show() 48 | 49 | 50 | func _on_quit_button_pressed(): 51 | get_tree().quit() 52 | 53 | 54 | func _on_new_project_new_project_cancel(): 55 | show() 56 | 57 | 58 | #func _on_load_project_cancel_load_project(): 59 | # show() 60 | 61 | 62 | func _on_settings_return_to_main_menu(): 63 | show() 64 | 65 | 66 | func _on_settings_button_pressed(): 67 | hide() 68 | open_settings.emit() 69 | 70 | 71 | func _on_save_project_return_to_main_menu(): 72 | show() 73 | 74 | 75 | #func _on_load_project_return_to_game(): 76 | # hide() 77 | # resume.emit() 78 | 79 | 80 | func _on_load_project_return_to_main_menu(): 81 | show() 82 | 83 | 84 | func _on_load_project_return_to_game_reload(): 85 | hide() 86 | reload.emit() 87 | 88 | 89 | func _on_init_init_finished(): 90 | rerun_status_check.emit() 91 | show() 92 | -------------------------------------------------------------------------------- /scenes/main_menu/StatusRichTextLabel.gd: -------------------------------------------------------------------------------- 1 | extends RichTextLabel 2 | 3 | 4 | # Called when the node enters the scene tree for the first time. 5 | func _ready(): 6 | regenerate_text() 7 | 8 | 9 | func regenerate_text(): 10 | text = "" # otherwise text isn't actually cleared in the next line 11 | text = "Ziplign Version: " + ProjectSettings.get_setting("application/config/version") 12 | append_text("\nRunning on " + Globals.userdata.os + "/" + Globals.userdata.arch) 13 | append_text("\nzlhelper version: " + Globals.userdata.zlhelper_version) 14 | append_text("\nblastn version: " + Globals.userdata.blastn_version) 15 | 16 | if Globals.userdata.install_ok: 17 | append_text("\nInstall status: ok") 18 | if not Globals.userdata.install_ok: 19 | var zlhelper_str 20 | if Globals.userdata.zlhelper_exists: 21 | if Globals.userdata.zlhelper_version_ok: 22 | zlhelper_str = "found" 23 | else: 24 | zlhelper_str = "[color=red]bad version " + Globals.userdata.zlhelper_version + "[/color]" 25 | else: 26 | zlhelper_str = "[color=red]not found[/color]" 27 | 28 | var lookup = {true: "found", false: "[color=red]not found[/color]"} 29 | append_text("\n[color=red]Install status: bad![/color]") 30 | append_text("\nFolder/file checks:") 31 | append_text("\n Data folder - " + lookup[Globals.userdata.data_dir_exists]) 32 | append_text("\n zlhelper - " + zlhelper_str) 33 | append_text("\n makeblastdb - " + lookup[Globals.userdata.makeblastdb_exists]) 34 | append_text("\n blastn - " + lookup[Globals.userdata.blastn_exists]) 35 | append_text("\n blastn version - " + lookup[Globals.userdata.blastn_version != "unknown"]) 36 | append_text("\n Example data - " + lookup[Globals.userdata.example_data_exists]) 37 | 38 | 39 | func _on_main_menu_rerun_status_check(): 40 | Globals.userdata.check_all_paths() 41 | regenerate_text() 42 | -------------------------------------------------------------------------------- /scenes/game/game.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | signal pause 4 | signal new_project_go 5 | signal window_resized 6 | signal redraw_matches 7 | #signal reload 8 | 9 | 10 | func _ready(): 11 | hide() 12 | get_tree().get_root().size_changed.connect(resize) 13 | set_colours() 14 | resize() 15 | $"../BoxContainer/ColorRect".z_index = 100 16 | Globals.paused = true 17 | 18 | func set_colours(): 19 | $"../BoxContainer/ColorRect".color = Globals.theme.colours["genomes_bg"] 20 | 21 | func resize(): 22 | var window_size = get_viewport().get_visible_rect().size 23 | $MainHBoxContainer/BoxContainer/VBoxContainer2/RightTopScrollbar.size.x = window_size.x - Globals.controls_width 24 | $MainHBoxContainer/BoxContainer/VBoxContainer2/RightBottomScrollbar.size.x = window_size.x - Globals.controls_width 25 | $MainHBoxContainer/BoxContainer/VBoxContainer2.size.x = window_size.x - Globals.controls_width 26 | Globals.genomes_viewport_width = window_size.x - Globals.controls_width 27 | window_resized.emit() 28 | 29 | 30 | func _on_pause_button_pressed(): 31 | hide() 32 | $"../BoxContainer/ColorRect".z_index = 100 33 | get_viewport().canvas_transform.origin.y = Globals.y_offset_paused 34 | Globals.paused = true 35 | pause.emit() 36 | 37 | 38 | func resume(): 39 | $"../BoxContainer/ColorRect".z_index = 0 40 | get_viewport().canvas_transform.origin.y = -Globals.y_offset_not_paused 41 | Globals.paused = false 42 | show() 43 | 44 | func _on_main_menu_resume(): 45 | resume() 46 | 47 | 48 | func _on_new_project_new_project_go_button(): 49 | new_project_go.emit() 50 | resume() 51 | 52 | 53 | func _on_load_project_go_load_new_project(): 54 | new_project_go.emit() 55 | resume() 56 | 57 | 58 | func _on_main_menu_reload(): 59 | new_project_go.emit() 60 | set_colours() 61 | resume() 62 | 63 | 64 | func _on_settings_redraw_matches(): 65 | redraw_matches.emit() 66 | -------------------------------------------------------------------------------- /lib/gff.gd: -------------------------------------------------------------------------------- 1 | func load_gff_file(filename, genome): 2 | print("Loading GFF file:", filename) 3 | var file = FileAccess.open(filename, FileAccess.READ) 4 | var lines = file.get_as_text().rstrip("\n").split("\n") 5 | var to_dedup = {} 6 | var annotations = {} 7 | var ctg_name_lookup = {} 8 | for i in genome["order"]: 9 | ctg_name_lookup[genome["contigs"][i]["name"]] = i 10 | 11 | for line in lines: 12 | if line[0] == "#" or not "\t" in line: 13 | continue 14 | 15 | var fields = line.rstrip("\r").split("\t") 16 | # example line: 17 | # g1.c1 . gene 900 1400 . + . ID=gene1;foo=bar;name=name1 18 | var ctg_index = ctg_name_lookup[fields[0]] 19 | if ctg_index not in to_dedup: 20 | to_dedup[ctg_index] = {} 21 | annotations[ctg_index] = [] 22 | 23 | var tags = {} 24 | if len(fields) >= 9 and fields[8] != ".": 25 | var pairs = fields[8].split(";") 26 | for x in pairs: 27 | var keyval = x.split("=") 28 | tags[keyval[0]] = keyval[1] 29 | 30 | var to_add = [ 31 | int(fields[3]) - 1, # start position 32 | int(fields[4]) - 1, # end position 33 | fields[2], # type of feature 34 | fields[6] == "-", # is reverse 35 | tags, # dict of tags 36 | ] 37 | 38 | if "ID" in tags: 39 | to_dedup[ctg_index][tags["ID"]] = to_add 40 | else: 41 | annotations[ctg_index].append(to_add) 42 | 43 | for i in to_dedup: 44 | var to_delete = [] 45 | for name in to_dedup[i]: 46 | var record = to_dedup[i][name] 47 | if "Parent" in record[4] and record[4]["Parent"] in to_dedup[i]: 48 | var parent = to_dedup[i][record[4]["Parent"]] 49 | if record[0] == parent[0] and record[1] == parent[1]: 50 | to_delete.append(name) 51 | 52 | for x in to_delete: 53 | to_dedup[i].erase(x) 54 | 55 | if i not in annotations: 56 | annotations[i] = [] 57 | 58 | annotations[i].append_array(to_dedup[i].values()) 59 | 60 | for i in to_dedup: 61 | annotations[i].sort() # ie sorts by start position 62 | print("Loaded GFF file ok:", filename) 63 | return annotations 64 | -------------------------------------------------------------------------------- /docs/searching.rst: -------------------------------------------------------------------------------- 1 | Searching 2 | ========= 3 | 4 | The search panel can be opened by clicking the magnifying glass search 5 | icon on the left. 6 | 7 | 8 | .. image:: pics/zl_docs_search_panel.png 9 | :width: 100 10 | :alt: screenshot showing search panel 11 | 12 | 13 | 14 | Search for sequence 15 | ------------------- 16 | 17 | Search for a nucleotide sequence by putting it into the sequence search 18 | box and pressing enter. This finds exact sequence matches on either the forward 19 | or reverse strands of either genome, and is case-insensitive. 20 | The matches will be listed in the panel 21 | at the bottom. Click on a match to highlight it and jump so that it starts 22 | in the middle of the screen. Use the up/down buttons to scroll through the 23 | matches. 24 | 25 | Sequence can be pasted into the search box in FASTA format. Ziplign will deal 26 | with this by removing the header line. If multi-FASTA is pasted, only the 27 | first record will be used and all other ones ignored. This means you can 28 | highlight a region (see 29 | :ref:`Selecting/copying a region `), copy 30 | it and then paste into the search box. But pasting into another application 31 | will still paste the full FASTA format. 32 | 33 | 34 | .. image:: pics/zl_docs_seq_search.png 35 | :width: 400 36 | :alt: screenshot showing sequence search 37 | 38 | 39 | 40 | Search for annotation 41 | --------------------- 42 | 43 | Search for annotation features by typing a search term into the annotation 44 | search box and pressing enter. This will find all annotation features that 45 | contain the search string anywhere in their metadata. It is a case-insensitive 46 | search. For example, searching for "ppe" in the *M. tuberculosis* genome 47 | will return all of the PPE genes: PPE1, PPE2, ... etc. 48 | 49 | In the same way as sequence search, the results will be listed in the panel 50 | at the bottom. Selecting one of the results will highlight that feature 51 | and jump the view so that the feature starts in the middle of the screen. 52 | 53 | 54 | .. image:: pics/zl_docs_search_annot_ppe_example.png 55 | :width: 500 56 | :alt: screenshot showing search for ppe genes 57 | 58 | -------------------------------------------------------------------------------- /docs/data_folder.rst: -------------------------------------------------------------------------------- 1 | Ziplign data folder 2 | =================== 3 | 4 | This is an advanced topic, and only of use for debugging or to understand 5 | the details of what Ziplign is doing. 6 | 7 | Ziplign stores its data somewhere in your home folder. The location depends on the 8 | operating system, and is chosen by Godot (the language used to write Ziplign). 9 | 10 | They are (probably): 11 | 12 | * Windows: ``%APPDATA%\Ziplign`` 13 | * macOS: ``~/Library/Application Support/Ziplign`` 14 | * Linux: ``~/.local/share/Ziplign`` 15 | 16 | The entire folder can be deleted. When Ziplign starts, it will 17 | be recreated and any missing files replaced. But you will lose any changes 18 | made to your settings because they are saved to a file inside the folder. 19 | 20 | 21 | Contents of the data folder 22 | --------------------------- 23 | 24 | Config file 25 | ^^^^^^^^^^^ 26 | 27 | Settings (theme choice etc) are stored in the file ``config``. 28 | If it is deleted, Ziplign will recreate it, filled with the default options. 29 | 30 | 31 | Binary files 32 | ^^^^^^^^^^^^ 33 | 34 | Ziplign relies on extra programs, which are put in the ``bin/`` folder: 35 | 36 | 1. ``zlhelper``, made specifically for Ziplign. It handles all the extra computation 37 | outside of viewing the genomes: reading genome files, running blast and 38 | parsing the output etc. The source code is here: 39 | https://github.com/martinghunt/zlhelper. If it is not present when 40 | Ziplign starts up, it downloads the binary matching the current OS and 41 | architecture. 42 | 2. BLAST programs (``makeblastbd`` and ``blastn``). These are downloaded from 43 | the NCBI's ftp site when Ziplign starts, if they are not there already. 44 | On Windows, there will also be some library (dll) files in this 45 | folder. 46 | 47 | 48 | Test data 49 | ^^^^^^^^^ 50 | 51 | The test data is in the folder ``example_data``. This is made by ``zlhelper`` 52 | and is two small GFF3 files. 53 | 54 | 55 | Other files 56 | ^^^^^^^^^^^ 57 | 58 | Log files are automatically written inside the ``logs/`` folder. There is 59 | ``current_proj/``, which stores imported genome files and BLAST results. 60 | It is deleted and new files put in there each time new genomes are 61 | imported. The Godot engine stores cached files to do with graphics inside 62 | ``shader_cache/``. 63 | 64 | -------------------------------------------------------------------------------- /lib/blast_hit.gd: -------------------------------------------------------------------------------- 1 | class_name BlastHit 2 | 3 | 4 | var qry_id: int 5 | var ref_id: int 6 | var qstart: int 7 | var qend: int 8 | var rstart: int 9 | var rend: int 10 | var pcid: float 11 | var is_rev: bool 12 | var aln_data: PackedInt32Array 13 | 14 | 15 | func _init(qid, rid, pc, qs, qe, rs, re, rev, aln, aln_in_fives=true): 16 | qry_id = qid 17 | ref_id = rid 18 | qstart = qs 19 | qend = qe 20 | rstart = rs 21 | rend = re 22 | pcid = pc 23 | is_rev = rev 24 | if aln_in_fives: 25 | aln_data.resize(5 * len(aln_data)) 26 | for a in aln: 27 | for x in a: 28 | aln_data.append(x) 29 | else: 30 | aln_data = aln 31 | 32 | 33 | func qry_name(): 34 | return Globals.proj_data.genome_seqs[Globals.TOP]["contigs"][qry_id]["name"] 35 | 36 | 37 | func ref_name(): 38 | return Globals.proj_data.genome_seqs[Globals.BOTTOM]["contigs"][ref_id]["name"] 39 | 40 | 41 | func length(): 42 | return 1 + max(qend - qstart, rend - rstart) 43 | 44 | 45 | func flip(top_or_bottom, contig_index): 46 | if contig_index != null and \ 47 | ( 48 | (top_or_bottom == Globals.TOP and contig_index != qry_id) \ 49 | or (top_or_bottom == Globals.BOTTOM and contig_index != ref_id) 50 | ): 51 | return 52 | 53 | is_rev = not is_rev 54 | if top_or_bottom == Globals.TOP: 55 | var new_start = 1 + len(Globals.proj_data.genome_seqs[Globals.TOP]["contigs"][qry_id]["seq"]) - qend 56 | qend = 1 + len(Globals.proj_data.genome_seqs[Globals.TOP]["contigs"][qry_id]["seq"]) - qstart 57 | qstart = new_start 58 | 59 | for i in range(0, len(aln_data), 5): 60 | var var_type = aln_data[i+4] 61 | var new_qstart = qend - qstart - aln_data[i+1] 62 | var new_qend = qend - qstart - aln_data[i] 63 | var new_rstart = rend - rstart - aln_data[i+3] 64 | var new_rend = rend - rstart - aln_data[i+2] 65 | aln_data[i] = var_type 66 | aln_data[i+1] = new_rend 67 | aln_data[i+2] = new_rstart 68 | aln_data[i+3] = new_qend 69 | aln_data[i+4] = new_qstart 70 | 71 | aln_data.reverse() 72 | else: 73 | var new_start = 1 + len(Globals.proj_data.genome_seqs[Globals.BOTTOM]["contigs"][ref_id]["seq"]) - rend 74 | rend = 1 + len(Globals.proj_data.genome_seqs[Globals.BOTTOM]["contigs"][ref_id]["seq"]) - rstart 75 | rstart = new_start 76 | 77 | 78 | func to_array(): 79 | return [qry_id, ref_id, pcid, qstart, qend, rstart, rend, is_rev, aln_data] 80 | -------------------------------------------------------------------------------- /lib/fastaq.gd: -------------------------------------------------------------------------------- 1 | func to_fasta(infile, outprefix): 2 | var stderr = [] 3 | var mingap = Globals.userdata.config.get_value("other", "min_gap_length") 4 | var exit_code = OS.execute(Globals.userdata.zlhelper, ["import_seqfile", "-g", mingap, "-i", infile, "-o", outprefix], stderr, true) 5 | if exit_code != 0: 6 | print("Error importing sequence file: ", infile) 7 | print(stderr) 8 | return exit_code 9 | 10 | 11 | func download_genome(accession, outprefix): 12 | var stderr = [] 13 | var exit_code = OS.execute(Globals.userdata.zlhelper, ["download_genome", "-a", accession, "-o", outprefix], stderr, true) 14 | if exit_code != 0: 15 | print("Error downloading sequence file: ", accession) 16 | print(stderr) 17 | return exit_code 18 | 19 | 20 | func load_fasta_file(filename): 21 | print("Loading fasta file: ", filename) 22 | var file = FileAccess.open(filename, FileAccess.READ) 23 | var lines = file.get_as_text().rstrip("\n").split("\n") 24 | var contigs = {"order": [], "contigs": []} 25 | 26 | for line in lines: 27 | if line[0] == ">": 28 | var fields = line.trim_prefix(">").split(" ", false, 1) 29 | contigs["order"].append(len(contigs["order"])) 30 | var descr 31 | if len(fields) > 1: 32 | descr = fields[1] 33 | else: 34 | descr = "" 35 | contigs["contigs"].append({"name": fields[0], "descr": descr}) 36 | else: 37 | contigs["contigs"][-1]["seq"] = line 38 | print("Loaded fasta file ok: ", filename) 39 | return contigs 40 | 41 | 42 | func revcomp(seq_in): 43 | var seq_out = Array([], TYPE_STRING, "", null) 44 | seq_out.resize(len(seq_in)) 45 | for i in range(0, len(seq_in)): 46 | seq_out[i] = Globals.complement_dict.get(seq_in[-i-1], "N") 47 | return "".join(seq_out) 48 | 49 | 50 | func search_one_strand(search_term, refseq): 51 | var hits = [] 52 | var i = refseq.findn(search_term) 53 | while i != -1 and len(hits) < Globals.max_search_results: 54 | hits.append(i) 55 | if i != -1: 56 | i = refseq.findn(search_term, i+1) 57 | return hits 58 | 59 | 60 | func search_for_seq(search_term, refseq): 61 | var new_hits = search_one_strand(search_term, refseq) 62 | var hits = [] 63 | for x in new_hits: 64 | hits.append([x, false]) 65 | if len(hits) >= Globals.max_search_results: 66 | hits.sort() 67 | return hits 68 | 69 | search_term = revcomp(search_term) 70 | new_hits = search_one_strand(search_term, refseq) 71 | for x in new_hits: 72 | hits.append([x, true]) 73 | if len(hits) >= Globals.max_search_results: 74 | break 75 | 76 | hits.sort() 77 | return hits 78 | -------------------------------------------------------------------------------- /globals.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | const ProjData = preload("res://lib/project_data.gd") 4 | var proj_data = ProjData.new() 5 | const UsrData = preload("res://lib/user_data.gd") 6 | var userdata = UsrData.new() 7 | const ColThemes = preload("res://lib/colour_themes.gd") 8 | var theme = ColThemes.new() 9 | 10 | 11 | var bin_path = userdata.get_bin_path() 12 | var genomes_viewport_width = 1000 13 | var match_min_show_pc_id = 90.0 14 | var match_min_show_length = 100 15 | var match_max_show_length = 1000000 16 | var match_aln_step = 1 17 | var zoom_to_show_bp = 9.0 18 | var zoom_to_show_annot_2k = 0.0001 19 | var zoom_to_show_annot_1k = 0.0003 20 | var zoom_to_show_annot_500 = 0.002 21 | var zoom_to_show_annot_all = 0.006 22 | var controls_width = 140 23 | var reload_needed = false 24 | var paused = true 25 | var y_offset_paused = 0 26 | var y_offset_not_paused = 1000 27 | var matches_y_top = 100 28 | var matches_y_bottom = 400 29 | var matches_visible_extra = 500 30 | var match_aln_match_width = 1 31 | var match_aln_mismatch_width = 1 32 | var match_outline_width = 0.75 33 | var x_zoom = 1.0 34 | var top_x_left = 1.0 35 | var bottom_x_left = 1.0 36 | var max_search_results = 1000 37 | var expect_zlhelper_version = "v1.0.0" 38 | 39 | 40 | var complement_dict = { 41 | "A": "T", 42 | "C": "G", 43 | "G": "C", 44 | "T": "A", 45 | "N": "N" 46 | } 47 | 48 | const TOP = 0 49 | const BOTTOM = 1 50 | const top_or_bottom_str = {TOP: "top", BOTTOM: "bottom"} 51 | 52 | func load_fonts(): 53 | var f = { 54 | "dejavu": load("res://fonts/dejavu-sans/DejaVuSans.ttf"), 55 | "mono": load("res://fonts/Anonymous-Pro/Anonymous_Pro.ttf"), 56 | "mono_bold": load("res://fonts/Anonymous-Pro/Anonymous_Pro_B.ttf") 57 | } 58 | for x in f: 59 | pass 60 | f[x].subpixel_positioning = 0 61 | f[x].multichannel_signed_distance_field = true 62 | #fonts[x].antialiasing = 2 63 | #fonts[x].hinting = 0 64 | return f 65 | 66 | 67 | 68 | func get_char_sizes(font, font_size): 69 | var sizes = {} 70 | for c in ["A", "C", "G", "T", "N"]: 71 | sizes[c] = font.get_string_size(c, 0, -1, font_size)[0] 72 | return sizes 73 | 74 | var fonts = load_fonts() 75 | var font_acgt_size = 15 76 | var font_acgt_sizes = get_char_sizes(fonts["mono"], font_acgt_size) 77 | var font_annot_size = 12 78 | var font_annot_gap_size = 6 79 | 80 | 81 | func make_tooltip_style(): 82 | var style = StyleBoxFlat.new() 83 | style.bg_color = theme.colours["ui"]["panel_bg"] 84 | style.set_border_width_all(2) 85 | style.set_expand_margin_all(1) 86 | style.border_color = theme.colours["text"] 87 | return style 88 | 89 | var tooltip_style = make_tooltip_style() 90 | -------------------------------------------------------------------------------- /scenes/game/mult_matches_item_list.gd: -------------------------------------------------------------------------------- 1 | extends ItemList 2 | 3 | 4 | signal selected_a_match 5 | signal selected_an_annotation 6 | signal selected_a_sequence 7 | 8 | enum Display_type {MATCHES, ANNOTATION, SEQUENCE} 9 | var item_list = [] 10 | var displaying = Display_type.MATCHES 11 | 12 | 13 | func _ready(): 14 | pass 15 | 16 | 17 | func set_colors(): 18 | for i in range(item_count): 19 | if is_selected(i): 20 | set_item_custom_bg_color(i, Globals.theme.colours["ui"]["general_bg"]) 21 | else: 22 | set_item_custom_bg_color(i, Globals.theme.colours["ui"]["button_bg"]) 23 | 24 | 25 | func set_item_list(new_data, type_of_data): 26 | displaying = type_of_data 27 | deselect_all() 28 | clear() 29 | item_list = new_data 30 | for i in item_list: 31 | if displaying == Display_type.MATCHES: 32 | add_item(Globals.proj_data.get_match_text(i)) 33 | elif displaying == Display_type.ANNOTATION: 34 | add_item(Globals.top_or_bottom_str[i[0]] + ": " + i[3]) 35 | elif displaying == Display_type.SEQUENCE: 36 | var strand = "+" 37 | if i[3]: 38 | strand = "-" 39 | add_item(Globals.top_or_bottom_str[i[0]] + ": " + Globals.proj_data.genome_seqs[i[0]]["contigs"][i[1]]["name"] + " " + str(i[2]) + " " + strand) 40 | else: 41 | pass 42 | set_item_tooltip_enabled(item_count - 1, false) 43 | set_colors() 44 | 45 | 46 | func set_matches(match_ids): 47 | set_item_list(match_ids, Display_type.MATCHES) 48 | 49 | 50 | func set_annotation(annot_data): 51 | set_item_list(annot_data, Display_type.ANNOTATION) 52 | 53 | 54 | func set_sequence(seq_data): 55 | set_item_list(seq_data, Display_type.SEQUENCE) 56 | 57 | 58 | func _on_item_selected(index): 59 | set_colors() 60 | if displaying == Display_type.MATCHES: 61 | selected_a_match.emit(item_list[index]) 62 | elif displaying == Display_type.ANNOTATION: 63 | selected_an_annotation.emit(item_list[index]) 64 | elif displaying == Display_type.SEQUENCE: 65 | selected_a_sequence.emit(item_list[index]) 66 | else: 67 | pass 68 | 69 | 70 | func _on_up_button_button_up(): 71 | if is_selected(0): 72 | pass 73 | else: 74 | var selected = get_selected_items() 75 | if len(selected) == 0: 76 | selected = [1] 77 | 78 | select(selected[0] - 1) 79 | _on_item_selected(selected[0] - 1) 80 | set_colors() 81 | ensure_current_is_visible() 82 | 83 | 84 | func _on_down_button_button_up(): 85 | if is_selected(len(item_list) - 1): 86 | pass 87 | else: 88 | var selected = get_selected_items() 89 | if len(selected) == 0: 90 | selected = [len(item_list) - 2] 91 | select(selected[0] + 1) 92 | _on_item_selected(selected[0] + 1) 93 | set_colors() 94 | ensure_current_is_visible() 95 | -------------------------------------------------------------------------------- /lib/blast.gd: -------------------------------------------------------------------------------- 1 | const BlastHitClass = preload("blast_hit.gd") 2 | 3 | func run_blast(proj_dir, blast_program): 4 | var opts = ["blast", "-t", blast_program, "-b", Globals.userdata.bin, "-o", proj_dir] 5 | if Globals.userdata.config.get_value("blast", "share_data"): 6 | opts.append("--send_usage_report") 7 | 8 | if len(Globals.userdata.blast_options) > 0: 9 | opts.append("--") 10 | opts.append_array(Globals.userdata.blast_options.split(" ", false)) 11 | 12 | print("opts: ", Globals.userdata.zlhelper, opts) 13 | var stderr = [] 14 | var exit_code = OS.execute(Globals.userdata.zlhelper, opts, stderr, true) 15 | for x in stderr: 16 | print(x) 17 | if exit_code != 0: 18 | print("Error running blast") 19 | return [exit_code, stderr] 20 | 21 | 22 | func genome2name_lookup(gen): 23 | var lookup = {} 24 | for i in gen["order"]: 25 | lookup[gen["contigs"][i]["name"]] = i 26 | return lookup 27 | 28 | 29 | func load_tsv_file(filename, qry_genome, ref_genome): 30 | var file = FileAccess.open(filename, FileAccess.READ) 31 | var contents = file.get_as_text() 32 | contents = contents.rstrip("\n") 33 | var lines = contents.split("\n") 34 | var rows = [] 35 | var aln_json = JSON.new() 36 | var qry_lookup = genome2name_lookup(qry_genome) 37 | var ref_lookup = genome2name_lookup(ref_genome) 38 | 39 | for line in lines: 40 | # windows blast has an extra "\r" at the end of each line 41 | var fields = line.rstrip("\r").split("\t") 42 | var qcoords = [int(fields[3]), int(fields[4])] 43 | var refcoords = [int(fields[5]), int(fields[6])] 44 | if qcoords[0] > qcoords[1]: 45 | print("Error. query start > query end. Stopping. ", fields[0], "-", fields[1]) 46 | return 47 | var is_rev = (qcoords[0] < qcoords[1]) != (refcoords[0] < refcoords[1]) 48 | qcoords.sort() 49 | refcoords.sort() 50 | # Used to remove self-hits. But then sorted hits so shortest have 51 | # higher zindex, so that the biggest hit doesn't cover all the smaller 52 | # ones. Keep the self-hits for now. Can revisit if users raise issues 53 | #if fields[0] == fields[1] and qcoords == refcoords: 54 | # continue 55 | 56 | var error = aln_json.parse(fields[7]) 57 | if error != OK: 58 | print("Error parsing final field of blast line: ", fields) 59 | continue 60 | 61 | 62 | if int(qcoords[1]) - int(qcoords[0]) < 10: 63 | continue 64 | 65 | rows.append(BlastHitClass.new( 66 | qry_lookup[fields[0]], 67 | ref_lookup[fields[1]], 68 | float(fields[2]), 69 | qcoords[0], 70 | qcoords[1], 71 | refcoords[0], 72 | refcoords[1], 73 | is_rev, 74 | aln_json.data, 75 | )) 76 | 77 | rows.sort_custom(func(a, b): return a.length() > b.length()) 78 | return rows 79 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | Settings 2 | ======== 3 | 4 | The settings menu can be accessed using the "Settings" button in the main 5 | menu. 6 | 7 | Settings are automatically saved when you return to the main menu. 8 | Changes are persistent between closing and re-opening Ziplign. 9 | 10 | The options are: 11 | 12 | * Theme: the color theme can be changed from the default "Light" to various 13 | other themes. 14 | * Open Ziplign data folder: this will open your file manager at the 15 | built-in :doc:`folder where Ziplign stores user data `. 16 | This can be useful when debugging 17 | or when uninstalling Ziplign. In normal circumstances, you probably won't 18 | need to use this. 19 | * Mouse wheel sensitivity: increase or decrease the amount of zoom when moving 20 | the mouse wheel. The default is "1". The number chosen here 21 | is multiplied by an internal value for each mouse wheel movement. This means 22 | to move half as much, set the option to 0.5. To double the amount moved, set 23 | it to 2. If you want to turn it off completely, then set it to 0 (zero). Large 24 | values are not recommended, because one wheel movement will make Ziplign jump 25 | straight to maximum or minimum zoom. 26 | * Invert mouse wheel: use this to swap the effect of up/down on the mouse 27 | wheel, if you think zoom happens in the wrong direction 28 | * Trackpad zoom sensitivity: change the sensitivity of two-finger up/down 29 | movement to zoom using a trackpad. Similarly to the mouse wheel description, 30 | think of this as a multiplier, with the default being 1. 31 | * Invert trackpad zoom: use this to swap the effect of up/down on the 32 | trackpad, if you think zoom happens in the wrong direction 33 | * Trackpad left/right sensitivity: change the sensitivity of two-finger 34 | left/right movement. 35 | * Trackpad pinch sensitivity: change the sensitivity of pinch-zoom gesture 36 | on a trackpad. 37 | * Share BLAST usage with NCBI: by default, the command line BLAST program 38 | sends some data to the NCBI, as described in their 39 | `privacy statement `_. 40 | By default, Ziplign turns *off* this setting so that no data is shared. You can 41 | turn it back on if you choose. 42 | * Max BLAST matches on screen: the maximum number of BLAST matches to show. 43 | Displaying too many will significantly impact memory usage. 44 | The default of 500 should be enough - a lot more than this and the screen 45 | ends up filled in to the point where it is impossible to see anything useful. 46 | * FASTA line length: the length of each line when copying/pasting sequence. 47 | Must be a non-negative integer. Put zero to have no line breaks in sequences. 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ziplign 2 | 3 | Ziplign is a free application to visualise the comparison of two bacterial genomes. 4 | It is intended to be easy to use: you just need to drag and drop two sequence 5 | files to compare. Ziplign will run BLAST for you and show you the results. 6 | There is no need to use a terminal or type any commands. 7 | 8 | Documentation: https://ziplign.readthedocs.io/en/ 9 | 10 | demo screenshots 11 | 12 | ## Quick start 13 | 14 | 15 | ### Download Ziplign 16 | 17 | Download the [latest release](https://github.com/martinghunt/ziplign/releases/latest) 18 | for your operating system. It is available for Linux, Mac, and Windows 11. 19 | If you are using Linux, then make the downloaded file 20 | executable (eg `chmod 755`). 21 | 22 | Double-click the downloaded file to run Ziplign. 23 | The first time Ziplign is run, it will download some extra programs such 24 | as BLAST. This might take some time depending on internet speed. 25 | 26 | 27 | 28 | ### Test data 29 | 30 | Check if the install is OK using the test data. 31 | 32 | 1. Press the "New" button - this is the screen for loading new genomes. 33 | 2. Press the icon with a tick at the top right, 34 | which will fill in the boxes to use the 35 | built in test data. There is a screenshot in the 36 | [test data](https://ziplign.readthedocs.io/en/stable/installation.html#use-the-test-data) 37 | documentation. 38 | 3. Press the "Start" button at the bottom. This will process the 39 | test data and then switch to viewing the two genomes. 40 | 41 | You should now be able to move around and zoom using the buttons near the top 42 | left and/or the cursor keyboard keys. Zoom using the mouse wheel, 43 | or pinch gesture on a trackpad. 44 | 45 | 46 | ### Real data 47 | 48 | Get back to the main menu using the back arrow at the top left, or by pressing 49 | "q". 50 | 51 | 1. Press the "New" button again. 52 | 2. To compare two genomes that are in files on your computer, drag and drop 53 | one file into the "Top genome file" box, and 54 | a second genome into the "Bottom genome file" box. 55 | 3. Press the "Start" button at the bottom. Ziplign will run BLAST between the genomes and then 56 | switch to viewing them. 57 | 58 | Alternatively, genome accessions can be used instead of files on your 59 | computer, and Ziplign will download the genomes from the NCBI. 60 | As described in more detail in the [sequence files](https://ziplign.readthedocs.io/en/stable/loading.html#sequence-files) 61 | documentation, most common accession formats can be used, such as `GCA_*`, `NC_*` etc. 62 | For example, the S. flexneri and E. coli K12 genomes `GCF_000007405.1` and 63 | `GCF_000005845.2`. 64 | 65 | 66 | ### Further reading 67 | 68 | Please see the full documentation at: https://ziplign.readthedocs.io/en/. 69 | 70 | Highlights of Ziplign include: 71 | * [Search](https://ziplign.readthedocs.io/en/stable/searching.html) for sequence 72 | or annotation features 73 | * [Basic editing](https://ziplign.readthedocs.io/en/stable/contig_editing.html). 74 | Contigs can be reordered and reverse-complemented. 75 | * [Save/Load](https://ziplign.readthedocs.io/en/stable/saving_n_loading.html) 76 | a project in a single file that stores both genomes and BLAST results. 77 | 78 | -------------------------------------------------------------------------------- /scenes/game/TopCoordsText.gd: -------------------------------------------------------------------------------- 1 | extends RichTextLabel 2 | 3 | 4 | var annot_selected_top_or_bottom = "" 5 | var annot_selected_contig_name = "" 6 | var annot_selected_annot_id = -1 7 | var region_selected_data = [] 8 | var selected_top_or_bottom = -1 9 | var selected_contig_id = -1 10 | var selected_seq_start = -1 11 | var selected_seq_end = -1 12 | 13 | func _on_genomes_n_matches_match_selected(selected): 14 | text = "Match: " + Globals.proj_data.get_match_text(selected) 15 | 16 | 17 | func _on_genomes_n_matches_match_deselected(): 18 | text = "Nothing" 19 | 20 | 21 | func _on_genomes_n_matches_contig_selected(top_or_bottom, contig_id): 22 | selected_top_or_bottom = top_or_bottom 23 | selected_contig_id = contig_id 24 | text = "Contig: " + Globals.top_or_bottom_str[top_or_bottom] + \ 25 | " genome / " + Globals.proj_data.contig_name(top_or_bottom, contig_id) 26 | 27 | 28 | func _on_genomes_n_matches_contig_deselected(): 29 | text = "Nothing" 30 | 31 | 32 | func _on_genomes_n_matches_annot_selected(top_or_bottom, contig_name, annot_id, annot): 33 | annot_selected_top_or_bottom = top_or_bottom 34 | annot_selected_contig_name = contig_name 35 | annot_selected_annot_id = annot_id 36 | text = "Annotation: " + Globals.top_or_bottom_str[top_or_bottom] + " / " + str(contig_name) + ":" + str(annot) 37 | 38 | 39 | func _on_genomes_n_matches_annot_deselected(top_or_bottom, contig_name, annot_id): 40 | if top_or_bottom != annot_selected_top_or_bottom \ 41 | or contig_name != annot_selected_contig_name \ 42 | or annot_id != annot_selected_annot_id: 43 | return 44 | annot_selected_top_or_bottom = "" 45 | annot_selected_contig_name = "" 46 | annot_selected_annot_id = -1 47 | if not text.begins_with("Match"): 48 | text = "Nothing" 49 | 50 | 51 | func _on_genomes_n_matches_sequence_range_selected(top_or_bottom, contig_id, start, end, is_rev): 52 | var coords_str 53 | if is_rev: 54 | coords_str = str(end + 1) + "-" + str(start + 1) 55 | selected_seq_start = end 56 | selected_seq_end = start 57 | else: 58 | coords_str = str(start + 1) + "-" + str(end + 1) 59 | selected_seq_start = start 60 | selected_seq_end = end 61 | text = "Sequence: " + Globals.top_or_bottom_str[top_or_bottom] + " genome / " + \ 62 | Globals.proj_data.contig_name(top_or_bottom, contig_id) + \ 63 | " " + coords_str 64 | selected_top_or_bottom = top_or_bottom 65 | selected_contig_id = contig_id 66 | 67 | 68 | func _on_genomes_n_matches_drag_range_selected(top_or_bottom, range_start, range_end): 69 | region_selected_data = [top_or_bottom, range_start, range_end] 70 | 71 | text = "Region: " + Globals.top_or_bottom_str[top_or_bottom] + " " + \ 72 | Globals.proj_data.contig_name(top_or_bottom, range_start[0]) + \ 73 | ":" + str(range_start[1] + 1) + " to " + \ 74 | Globals.proj_data.contig_name(top_or_bottom, range_end[0]) + \ 75 | ":" + str(range_end[1] + 1) 76 | 77 | 78 | func _on_copy_to_clipboard_button_pressed(): 79 | if text.begins_with("Nothing"): 80 | return 81 | elif text.begins_with("Contig: "): 82 | DisplayServer.clipboard_set( 83 | Globals.userdata.os_newline.join( 84 | Globals.proj_data.contig_fasta_lines(selected_top_or_bottom, selected_contig_id) 85 | )) 86 | elif text.begins_with("Region: "): 87 | DisplayServer.clipboard_set( 88 | Globals.userdata.os_newline.join( 89 | Globals.proj_data.range_to_seq_lines(region_selected_data[0], region_selected_data[1], region_selected_data[2]) 90 | )) 91 | elif text.begins_with("Sequence: "): 92 | DisplayServer.clipboard_set( 93 | Globals.userdata.os_newline.join( 94 | Globals.proj_data.contig_fasta_subseq(selected_top_or_bottom, selected_contig_id, selected_seq_start, selected_seq_end) 95 | )) 96 | else: 97 | DisplayServer.clipboard_set(text.split(" ", false, 1)[1]) 98 | -------------------------------------------------------------------------------- /scenes/main_menu/main_menu.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://5etk1xbkj7g"] 2 | 3 | [ext_resource type="Script" uid="uid://cpkc1mlqlnoxq" path="res://scenes/main_menu/main_menu.gd" id="1_00gpy"] 4 | [ext_resource type="Script" uid="uid://cfdgwk63rjx6y" path="res://scenes/main_menu/StatusRichTextLabel.gd" id="2_4ot8r"] 5 | 6 | [sub_resource type="InputEventAction" id="InputEventAction_xx7b4"] 7 | action = &"resume" 8 | 9 | [sub_resource type="Shortcut" id="Shortcut_ow52b"] 10 | events = [SubResource("InputEventAction_xx7b4")] 11 | 12 | [node name="MainMenu" type="Control"] 13 | z_index = 1001 14 | layout_mode = 3 15 | anchors_preset = 15 16 | anchor_right = 1.0 17 | anchor_bottom = 1.0 18 | grow_horizontal = 2 19 | grow_vertical = 2 20 | script = ExtResource("1_00gpy") 21 | 22 | [node name="MainContainer" type="CenterContainer" parent="."] 23 | z_index = 1 24 | layout_mode = 0 25 | offset_right = 1148.0 26 | offset_bottom = 655.0 27 | 28 | [node name="MainVBoxContainer" type="VBoxContainer" parent="MainContainer"] 29 | layout_mode = 2 30 | 31 | [node name="ResumeButton" type="Button" parent="MainContainer/MainVBoxContainer"] 32 | layout_mode = 2 33 | tooltip_text = "Return to looking at genomes 34 | Keyboard shorcut: r" 35 | theme_override_font_sizes/font_size = 30 36 | disabled = true 37 | shortcut = SubResource("Shortcut_ow52b") 38 | shortcut_in_tooltip = false 39 | text = "Resume" 40 | 41 | [node name="NewButton" type="Button" parent="MainContainer/MainVBoxContainer"] 42 | layout_mode = 2 43 | theme_override_font_sizes/font_size = 30 44 | text = "New" 45 | 46 | [node name="SaveButton" type="Button" parent="MainContainer/MainVBoxContainer"] 47 | layout_mode = 2 48 | theme_override_font_sizes/font_size = 30 49 | disabled = true 50 | text = "Save" 51 | 52 | [node name="LoadButton" type="Button" parent="MainContainer/MainVBoxContainer"] 53 | layout_mode = 2 54 | theme_override_font_sizes/font_size = 30 55 | text = "Load" 56 | 57 | [node name="SettingsButton" type="Button" parent="MainContainer/MainVBoxContainer"] 58 | layout_mode = 2 59 | theme_override_font_sizes/font_size = 30 60 | text = "Settings" 61 | 62 | [node name="QuitButton" type="Button" parent="MainContainer/MainVBoxContainer"] 63 | layout_mode = 2 64 | theme_override_font_sizes/font_size = 30 65 | text = "Quit" 66 | 67 | [node name="StatusRichTextLabel" type="RichTextLabel" parent="."] 68 | clip_contents = false 69 | layout_mode = 2 70 | offset_right = 455.0 71 | offset_bottom = 28.0 72 | focus_mode = 2 73 | mouse_default_cursor_shape = 1 74 | theme_override_font_sizes/bold_italics_font_size = 20 75 | theme_override_font_sizes/italics_font_size = 20 76 | theme_override_font_sizes/mono_font_size = 20 77 | theme_override_font_sizes/normal_font_size = 20 78 | theme_override_font_sizes/bold_font_size = 20 79 | text = "Ziplign version: unknown" 80 | fit_content = true 81 | selection_enabled = true 82 | script = ExtResource("2_4ot8r") 83 | 84 | [connection signal="rerun_status_check" from="." to="StatusRichTextLabel" method="_on_main_menu_rerun_status_check"] 85 | [connection signal="pressed" from="MainContainer/MainVBoxContainer/ResumeButton" to="." method="_on_resume_button_pressed"] 86 | [connection signal="pressed" from="MainContainer/MainVBoxContainer/NewButton" to="." method="_on_new_button_pressed"] 87 | [connection signal="pressed" from="MainContainer/MainVBoxContainer/SaveButton" to="." method="_on_save_button_pressed"] 88 | [connection signal="pressed" from="MainContainer/MainVBoxContainer/LoadButton" to="." method="_on_load_button_pressed"] 89 | [connection signal="pressed" from="MainContainer/MainVBoxContainer/SettingsButton" to="." method="_on_settings_button_pressed"] 90 | [connection signal="pressed" from="MainContainer/MainVBoxContainer/QuitButton" to="." method="_on_quit_button_pressed"] 91 | -------------------------------------------------------------------------------- /docs/loading.rst: -------------------------------------------------------------------------------- 1 | Loading new genomes 2 | =================== 3 | 4 | Selecting "New" from the main menu takes you to the new project page, 5 | where you can import two genomes to compare. 6 | 7 | 8 | Sequence files 9 | -------------- 10 | 11 | You can add the top and bottom genome files with one of: 12 | 13 | * dragging and dropping from your file browser into the box 14 | * typing the full path to the filename in the box 15 | * typing an accession in the box. This can be a GenBank/RefSeq 16 | sequence accession, or an assembly accession starting 17 | with ``GCA_`` or ``GCF_``. 18 | 19 | Ziplign will first check if what you put in the box is a file on your computer. 20 | If it is not, then it checks to see if it "looks like" an accession. This 21 | means that it starts with ``GCA_``, ``GCF_``, ``AC_``, ``NC_``, ``NG_``, 22 | ``NT_``, ``NW_``, ``NZ_``, or it starts with two letters followed by at 23 | least six digits and then anything else afterwards - for example ``CP039850.1``. 24 | If it looks like an accession then it will try to download 25 | the sequence and annotation. Note that this is not sanity checked and providing 26 | an accession that does not exist will result in errors upon trying to 27 | download. It is intentionally permissive - allowing two letters plus six digits 28 | etc - so as to not rule out real accessions because it is not trivial to 29 | specify exactly what counts as a real accession. 30 | 31 | Ziplign automatically detects the format (and any compression) 32 | of each sequence file based on its contents, not the name of the file. 33 | 34 | Ziplign supports these file formats: 35 | 36 | * FASTA 37 | * FASTQ (loading sequencing reads is NOT recommended! The assumption is that 38 | you want to view contigs) 39 | * GFF3, as long as the sequence(s) are included in the file as well as the 40 | annotation. Ziplign will show the annotation features. 41 | * GenBank. Ziplign will use the sequence, but for now has partial support for the 42 | annotation. It will load the genes only, and no other features. 43 | * EMBL. Ziplign will use the sequence, but for now has partial support for the 44 | annotation. It will load the genes only, and no other features. 45 | 46 | Ziplign can read uncompressed files, and files compressed with gzip, bzip2 and 47 | xz. 48 | 49 | 50 | 51 | 52 | 53 | BLAST options 54 | ------------- 55 | 56 | Ziplign runs ``blastn`` to compare the genomes. You can add options to the 57 | ``blastn`` call. Please note that these options are NOT sanity checked and 58 | are simply added onto the end of the ``blastn`` call. 59 | 60 | Please do not use any options relating to file input or output, since it will 61 | cause Ziplign to crash. Specifically, Ziplign already uses the options ``-db``, 62 | ``-query``, ``-out``, and ``-outfmt``. Do not try to change them. 63 | 64 | The default in the BLAST options box is ``-evalue 0.1``, 65 | to remove obviously short/unlikely matches. This is different from the 66 | BLASTN defaults, which has no e-value cutoff. 67 | If you want no e-value cutoff, then delete the text from the box, so that it 68 | is empty and the default BLAST settings are used. 69 | It is beyond the scope of this help to go into the details of the extra 70 | BLASTN options. Run ``blastn -help`` in a terminal to see the full help. 71 | 72 | 73 | Start processing 74 | ---------------- 75 | 76 | Once the genome files are provided, the start button will change from 77 | disabled to enabled. Press it and Ziplign will then: 78 | 79 | * check the input genome files exist 80 | * import the genomes 81 | * make a BLAST index of the bottom genome 82 | * run BLAST using the top genome as the query against the bottom genome 83 | database 84 | * import the BLAST results 85 | * switch to the genome comparison view 86 | 87 | The progress is output in text at the bottom of the window. If anything 88 | goes wrong, error messages will appear there. 89 | -------------------------------------------------------------------------------- /lib/genomes_n_matches/genome_matches.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | class_name GenomeMatches 4 | 5 | const MatchClass = preload("match.gd") 6 | 7 | signal moved_to_selected_match 8 | signal match_selected 9 | signal match_deselected 10 | 11 | var matches = [] 12 | var hover_matches = [] 13 | var selected = -1 14 | var top = 100 15 | var bottom = 600 16 | var x_left_bottom = 0 17 | var x_left_top = 0 18 | var visible_matches = 0 19 | var previous_zoom = 1.0 20 | 21 | 22 | func _init(): 23 | previous_zoom = Globals.x_zoom 24 | 25 | 26 | func add_match(hit_id, start1, end1, start2, end2): 27 | matches.append(MatchClass.new(hit_id, start1, end1, start2, end2)) 28 | 29 | 30 | func number_of_matches(): 31 | return len(matches) 32 | 33 | 34 | func update_hide_and_show(): 35 | var total_visible = 0 36 | for m in matches: 37 | m.update_canvas_coords() 38 | if total_visible > Globals.userdata.config.get_value("other", "max_matches_on_screen"): 39 | m.make_invisible() 40 | else: 41 | m.update_visibility() 42 | if m.is_currently_visible: 43 | total_visible += 1 44 | 45 | 46 | func update_after_x_zoom_change(centre=null): 47 | if centre == null: 48 | centre = 0.5 * (Globals.genomes_viewport_width) 49 | x_left_bottom = centre - Globals.x_zoom * (centre - x_left_bottom) / previous_zoom 50 | x_left_top = centre - Globals.x_zoom * (centre - x_left_top) / previous_zoom 51 | update_hide_and_show() 52 | previous_zoom = Globals.x_zoom 53 | 54 | 55 | func set_top_x_left(x): 56 | x_left_top = x 57 | update_hide_and_show() 58 | 59 | 60 | func set_bottom_x_left(x): 61 | x_left_bottom = x 62 | update_hide_and_show() 63 | 64 | 65 | func set_x_lefts(new_x_left_top, new_x_left_bottom): 66 | x_left_top = new_x_left_top 67 | x_left_bottom = new_x_left_bottom 68 | update_hide_and_show() 69 | 70 | 71 | func move_to_selected(): 72 | if selected == -1: 73 | return -1 74 | 75 | moved_to_selected_match.emit(selected) 76 | return selected 77 | 78 | 79 | func get_matches_in_range(start, end, is_top): 80 | var found = [] 81 | for m in matches: 82 | if m.is_currently_visible and m.intersects_range(start, end, is_top): 83 | found.append(m.blast_id) 84 | return found 85 | 86 | 87 | func _ready(): 88 | for m in matches: 89 | add_child(m) 90 | m.connect("mouse_in", on_mouse_in_match) 91 | m.connect("mouse_out", on_mouse_out_match) 92 | 93 | 94 | func clear_all(): 95 | for m in matches: 96 | remove_child(m) 97 | m.free() 98 | matches.clear() 99 | 100 | 101 | func deselect(): 102 | if selected != -1: 103 | matches[selected].deselect() 104 | selected = -1 105 | match_deselected.emit() 106 | 107 | 108 | func set_selected_match(match_id): 109 | deselect() 110 | matches[match_id].select() 111 | selected = match_id 112 | match_selected.emit(match_id) 113 | 114 | 115 | func _unhandled_input(event): 116 | if Globals.paused: 117 | return 118 | 119 | if event is InputEventMouseButton: 120 | if event.button_index == MOUSE_BUTTON_LEFT: 121 | if event.pressed: 122 | if len(hover_matches) >= 1: 123 | var match_id = hover_matches[-1] 124 | if selected != -1 and selected == match_id and event.is_double_click(): 125 | moved_to_selected_match.emit(selected) 126 | return 127 | matches[selected].deselect() 128 | matches[match_id].select() 129 | match_selected.emit(match_id) 130 | selected = match_id 131 | elif len(hover_matches) == 0 and selected != -1: 132 | matches[selected].deselect() 133 | match_deselected.emit() 134 | selected = -1 135 | elif event.button_index == MOUSE_BUTTON_RIGHT: 136 | if event.pressed: 137 | if selected in hover_matches: 138 | matches[selected].deselect() 139 | match_deselected.emit() 140 | selected = -1 141 | 142 | 143 | func on_mouse_in_match(match_id): 144 | if match_id not in hover_matches: 145 | hover_matches.append(match_id) 146 | 147 | 148 | func on_mouse_out_match(match_id): 149 | hover_matches.erase(match_id) 150 | -------------------------------------------------------------------------------- /main.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=9 format=3 uid="uid://b261m6mf32s8e"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://5etk1xbkj7g" path="res://scenes/main_menu/main_menu.tscn" id="1_4bx2i"] 4 | [ext_resource type="Script" uid="uid://05c32ervgjnx" path="res://main.gd" id="1_y7efp"] 5 | [ext_resource type="PackedScene" uid="uid://o6vk13fe8nhl" path="res://scenes/new_project/new_project.tscn" id="2_xhjgo"] 6 | [ext_resource type="PackedScene" uid="uid://cajqs7kwfmv43" path="res://scenes/game/game.tscn" id="3_gtkd0"] 7 | [ext_resource type="PackedScene" uid="uid://bdpfqi7xni7" path="res://scenes/load_project/load_project.tscn" id="5_vxdry"] 8 | [ext_resource type="PackedScene" uid="uid://bshg2cnsbwrv2" path="res://scenes/settings/settings.tscn" id="6_c5h4v"] 9 | [ext_resource type="PackedScene" uid="uid://dkqo7pcll5pjj" path="res://scenes/init/init.tscn" id="7_gj10y"] 10 | [ext_resource type="PackedScene" uid="uid://bsbxlfa2t7bm5" path="res://scenes/save_project/save_project.tscn" id="7_hhjed"] 11 | 12 | [node name="Main" type="Node2D"] 13 | script = ExtResource("1_y7efp") 14 | 15 | [node name="BoxContainer" type="BoxContainer" parent="."] 16 | show_behind_parent = true 17 | z_as_relative = false 18 | anchors_preset = 15 19 | anchor_right = 1.0 20 | anchor_bottom = 1.0 21 | offset_right = 20000.0 22 | offset_bottom = 2000.0 23 | grow_horizontal = 2 24 | grow_vertical = 2 25 | size_flags_horizontal = 3 26 | size_flags_vertical = 3 27 | 28 | [node name="ColorRect" type="ColorRect" parent="BoxContainer"] 29 | custom_minimum_size = Vector2(2000, 1000) 30 | layout_mode = 2 31 | size_flags_horizontal = 3 32 | size_flags_vertical = 3 33 | mouse_filter = 1 34 | color = Color(0.756863, 1, 0.286275, 1) 35 | 36 | [node name="MainMenu" parent="." instance=ExtResource("1_4bx2i")] 37 | z_as_relative = false 38 | mouse_filter = 1 39 | 40 | [node name="Game" parent="." instance=ExtResource("3_gtkd0")] 41 | offset_top = 1000.0 42 | offset_right = 1183.0 43 | offset_bottom = 1656.0 44 | 45 | [node name="NewProject" parent="." instance=ExtResource("2_xhjgo")] 46 | z_index = 1000 47 | mouse_filter = 1 48 | 49 | [node name="LoadProject" parent="." instance=ExtResource("5_vxdry")] 50 | 51 | [node name="Settings" parent="." instance=ExtResource("6_c5h4v")] 52 | z_index = 1000 53 | mouse_filter = 1 54 | 55 | [node name="SaveProject" parent="." instance=ExtResource("7_hhjed")] 56 | 57 | [node name="Init" parent="." instance=ExtResource("7_gj10y")] 58 | z_index = 1000 59 | mouse_filter = 1 60 | 61 | [connection signal="start_init" from="." to="Init" method="_on_main_start_init"] 62 | [connection signal="load_project" from="MainMenu" to="LoadProject" method="_on_main_menu_load_project"] 63 | [connection signal="new_project" from="MainMenu" to="NewProject" method="_on_main_menu_new_project"] 64 | [connection signal="open_settings" from="MainMenu" to="Settings" method="_on_main_menu_open_settings"] 65 | [connection signal="reload" from="MainMenu" to="Game" method="_on_main_menu_reload"] 66 | [connection signal="resume" from="MainMenu" to="Game" method="_on_main_menu_resume"] 67 | [connection signal="save_project" from="MainMenu" to="SaveProject" method="_on_main_menu_save_project"] 68 | [connection signal="pause" from="Game" to="MainMenu" method="_on_game_pause"] 69 | [connection signal="new_project_cancel" from="NewProject" to="MainMenu" method="_on_new_project_new_project_cancel"] 70 | [connection signal="new_project_go_button" from="NewProject" to="Game" method="_on_new_project_new_project_go_button"] 71 | [connection signal="return_to_game_reload" from="LoadProject" to="MainMenu" method="_on_load_project_return_to_game_reload"] 72 | [connection signal="return_to_main_menu" from="LoadProject" to="MainMenu" method="_on_load_project_return_to_main_menu"] 73 | [connection signal="redraw_matches" from="Settings" to="Game" method="_on_settings_redraw_matches"] 74 | [connection signal="return_to_main_menu" from="Settings" to="MainMenu" method="_on_settings_return_to_main_menu"] 75 | [connection signal="theme_updated" from="Settings" to="." method="_on_settings_theme_updated"] 76 | [connection signal="return_to_main_menu" from="SaveProject" to="MainMenu" method="_on_save_project_return_to_main_menu"] 77 | [connection signal="init_finished" from="Init" to="MainMenu" method="_on_init_init_finished"] 78 | -------------------------------------------------------------------------------- /docs/background.rst: -------------------------------------------------------------------------------- 1 | A brief history of Ziplign 2 | ========================== 3 | 4 | If you are interested in why Ziplign was made in the first place, and some of the 5 | details of the implementation, then please read on. 6 | 7 | 8 | Motivation 9 | ---------- 10 | 11 | `Artemis `_ and 12 | `ACT `_ 13 | are excellent programs, both of which I use heavily. 14 | Sadly, they are no longer really actively maintained. 15 | There are lots of great genome viewers 16 | out there - obviously `IGV `_ 17 | being very popular. Implementing yet another genome 18 | viewer is probably not a good use of time. 19 | 20 | However, there is no tool for me that can do what ACT does. It is extremely 21 | comprehensive (because it essentially has the functionality of Artemis). 22 | But there are features that I always wish it had: 23 | 24 | * proper support for multi-FASTA files (you can hack to get around this) 25 | * run BLAST for you, instead of needing to provide your own comparison file 26 | * show base-level alignment between genomes (SNPs, indels). I have always 27 | wanted to see these when zoomed in to the sequence level. 28 | * easier contig reordering, and also be able to reverse complement individual 29 | contigs 30 | * out of the box support for Apple silicon. It does run on Apple silicon, but 31 | needs a bit of Java-related hacking to do so. This seems beyond the 32 | skills of less technical users 33 | * save and load a "project" - ie both genomes and the comparison file - instead 34 | of needing to keep 3 files around for each use of ACT 35 | 36 | I was playing around with the game engine `Godot `_, 37 | and tested how difficult it would be to get the basic functionality of ACT 38 | working. This was just showing rectangles (contigs), parallelograms/triangles 39 | (blast matches), and being able to slide the top and bottom genomes around and 40 | have the blast matches follow suit. 41 | It turned out to work quite well as a proof of concept. 42 | This then grew into Ziplign. 43 | 44 | The aim of Ziplign was always to be a minimal implementation of the essential 45 | things I want from ACT and nothing more: view two genomes, plus the 46 | features listed above. It's pronounced "zipline" and is a 47 | backronym: "Zoomable Interactive Paired-genome aLIGnment Navigator" (or 48 | have fun making up your own acronym). 49 | I rarely use ACT to view more than two genomes, 50 | so early on I decided (to save a lot of implementation pain) to only support 51 | two genomes. This is baked in and will not change. 52 | 53 | 54 | Under the hood 55 | -------------- 56 | 57 | I wanted Ziplign to be cross-platform, which was another reason to use Godot. 58 | It can export a project to Windows, Mac, and Linux (and x86 and ARM 59 | architecture). It can also make Android and i(pad)OS apps - which I thought 60 | about making, but I don't think it's possible to get them to run BLAST. 61 | It would have been nice to have an ipad app, but it is too locked down 62 | to make this practical. 63 | 64 | This was my first project using Godot (and indeed a GUI of any kind). 65 | Handling all the 2D graphics in Godot is relatively easy, what with it 66 | being a game engine. 67 | Displaying genomes and BLAST matches was probably the simplest part. 68 | The biggest challenge of making Ziplign was the GUI: 69 | adding menus, buttons, and then in particular being able to change colour 70 | themes etc. 71 | Most of this comes down 72 | to Ziplign not being a game, but a tool. Although Godot is set up to make such 73 | a program (its own IDE is made in Godot), 74 | it is primarily for making games. 75 | 76 | There is, unsurprisingly, 77 | no "bio-Godot". Seeing as Ziplign would need to run BLAST as a separate program, 78 | I decided to offload all the heavy lifting bioinformatics tasks to a separate 79 | program as well, called `zlhelper `_. 80 | This also needed to be cross-platform, 81 | and so is written in the Go programming language. It parses sequence files, 82 | handles reading compressed files, runs BLAST and parses its output. The plus 83 | side of using Go is that it can compile stand-alone binaries for Windows, 84 | macOS and Linux, and both ARM and x86 architecture. 85 | -------------------------------------------------------------------------------- /fonts/Anonymous-Pro/SIL Open Font License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Mark Simonson (http://www.ms-studio.com, mark@marksimonson.com), 2 | with Reserved Font Name Anonymous Pro. 3 | 4 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 5 | This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL 6 | 7 | ----------------------------------------------------------- 8 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 | ----------------------------------------------------------- 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. 13 | 14 | The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. 15 | 16 | DEFINITIONS 17 | "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. 18 | 19 | "Reserved Font Name" refers to any names specified as such after the copyright statement(s). 20 | 21 | "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). 22 | 23 | "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. 24 | 25 | "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. 26 | 27 | PERMISSION & CONDITIONS 28 | Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 29 | 30 | 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 31 | 32 | 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 33 | 34 | 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 35 | 36 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 37 | 38 | 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. 39 | 40 | TERMINATION 41 | This license becomes null and void if any of the above conditions are not met. 42 | 43 | DISCLAIMER 44 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installing Ziplign 2 | ================== 3 | 4 | Supported Operating Systems 5 | --------------------------- 6 | 7 | * Windows 11 (x86 only, no ARM support) 8 | * macOS (tested on sequoia 15.1) 9 | * Linux (tested on Ubuntu 20.04/x86, 22.04/x86, and 24.04/aarch64) 10 | 11 | 12 | Download Ziplign 13 | ---------------- 14 | 15 | Go to the `Ziplign release page `_ 16 | and download the correct version for your computer: 17 | 18 | * Windows 11 - ``ziplign.vX.Y.Z.windows.exe`` 19 | * macOS - ``ziplign.vX.Y.Z.mac.dmg`` 20 | * Linux x86_64/arm64 - ``ziplign.vX.Y.Z.linux.x86_64``/``ziplign.vX.Y.Z.linux.arm64`` 21 | 22 | The actual filenames have the release number in them instead of ``X.Y.Z`` 23 | in the above filenames. 24 | 25 | 26 | 27 | Windows 28 | ^^^^^^^ 29 | 30 | Double-click on the downloaded file and it should just work. 31 | You may find Windows Defender popping up, in which case you will need to 32 | tell it to allow Ziplign to run. 33 | 34 | 35 | macOS 36 | ^^^^^ 37 | 38 | Double-click the downloaded ``ziplign.mac.dmg`` file. It contains the app ``ziplign.app``. 39 | Drag or copy it into your Applications folder (or wherever you like depending on 40 | how you organise your files). 41 | 42 | macOS Gatekeeper will probably block it from running. If this happens, 43 | you have two options: 44 | 45 | 1. Go to "Privacy & Security" in the Settings app. Scroll to the bottom 46 | and Ziplign should be there for you to allow it 47 | 2. In a terminal, run this command: 48 | ``xattr -d com.apple.quarantine -r ziplign.app``. 49 | 50 | Then Ziplign should just work. We apologise for the inconvenience, but this 51 | process is standard for apps that have not been 52 | `"notarized" by Apple `_ (which 53 | means paying an annual fee). 54 | 55 | Linux 56 | ^^^^^ 57 | 58 | You may need to make the downloaded binary file executable 59 | (ie run ``chmod +x``). Then opening it in your file browser (or in 60 | a terminal if you want to see logging) should just work. 61 | 62 | 63 | First time running Ziplign 64 | -------------------------- 65 | 66 | The first time you run Ziplign, it will automatically download two extra programs: 67 | 68 | 1. ``zlhelper``. This is a separate command line program made for Ziplign, which 69 | handles bioinformatics tasks (the source code is here: 70 | https://github.com/martinghunt/zlhelper). Don't worry, you won't ever 71 | have to run it yourself - Ziplign uses it in the background. 72 | 2. NCBI-blast+. Specifically, Ziplign gets the programs ``makeblastdb`` and 73 | ``blastn``. These are part of a large download containing the full blast 74 | suite of programs, which is why the download is quite large. 75 | 76 | This might take some time, depending on your internet speed. 77 | You will see messages like: 78 | 79 | .. code-block:: text 80 | 81 | zlhelper not found: /Users/username/Library/Application Support/ziplign/bin/zlhelper 82 | Downloading: https://github.com/martinghunt/zlhelper/releases/download/v0.3.2/zlhelper_darwin_arm64 83 | 84 | and: 85 | 86 | .. code-block:: text 87 | 88 | Some blast programs not found, or version unknown. Downloading... 89 | Running: /Users/username/Library/Application Support/Ziplign/bin/zlhelper download_binaries --outdir /Users/username/Library/Application Support/ziplign/bin 90 | This may take some time, depending on internet bandwidth 91 | 92 | The filenames you see will vary depending on your OS 93 | (those examples are from macOS). 94 | 95 | Next time Ziplign runs, it will find those programs, not need to download them, 96 | and start up quickly. 97 | 98 | 99 | 100 | Use the test data 101 | ----------------- 102 | 103 | Ziplign has built-in test data. It is a good idea to try this out to check that 104 | the installation is working correctly. 105 | 106 | Press the "New" button. You should see the window below. 107 | 108 | .. image:: pics/zl_docs_use_test_data.png 109 | :width: 500 110 | :alt: screenshot showing how to use test data 111 | 112 | 1. Click the "test data" icon (the rightmost icon on the top row). 113 | This will fill in the top and bottom genome filenames with the names 114 | of the test files. 115 | 2. Click the "start" button (at the bottom) to begin processing. 116 | 117 | After pressing start, it will process the data. 118 | This means importing the genome files, 119 | running BLAST, and processing the output. If this all works successfully and 120 | Ziplign displays the genomes and BLAST matches, then everything is working 121 | as expected. It should look like this: 122 | 123 | .. image:: pics/zl_docs_view_test_data.png 124 | :width: 500 125 | :alt: screenshot showing the test data after loading 126 | 127 | 128 | 129 | Updating Ziplign 130 | ---------------- 131 | 132 | To update Ziplign, download a new release and then replace the existing Ziplign 133 | file with the new downloaded file. If Ziplign needs a newer version of ``zlhelper`` 134 | or the BLAST programs, then they will be automatically downloaded when the new 135 | version of Ziplign is started. 136 | -------------------------------------------------------------------------------- /lib/colour_themes.gd: -------------------------------------------------------------------------------- 1 | class_name ColourThemes 2 | 3 | 4 | var solarized = { 5 | "base03": "#002b36", 6 | "base02": "#073642", 7 | "base01": "#586e75", 8 | "base00": "#657b83", 9 | "base0": "#839496", 10 | "base1": "#93a1a1", 11 | "base2": "#eee8d5", 12 | "base3": "#fdf6e3", 13 | "yellow": "#b58900", # = RGB(181, 137, 0) 14 | "mid_yellow": Color(165.0/256.0, 126.0/256.0, 0), 15 | "dark_yellow": Color(157.0/256.0, 115.0/256.0, 0), 16 | "orange": "#cb4b16", 17 | "red": "#dc322f", 18 | "magenta": "#d33682", 19 | "violet": "#6c71c4", 20 | "blue": "#268bd2", 21 | "cyan": "#2aa198", 22 | "green": "#859900", 23 | } 24 | 25 | 26 | var colour_themes = { 27 | "Classic": { 28 | "ui": { 29 | "text": "black", 30 | "text_hover": "white", 31 | "button_bg": "silver", 32 | "button_highlight": "black", 33 | "button_pressed": "dimgray", 34 | "general_bg": "white", 35 | "panel_bg": "gainsboro" 36 | }, 37 | "blast_match": { 38 | "fwd": "red", 39 | "rev": "blue", 40 | "fwd_hover": "pink", 41 | "rev_hover": "light_blue", 42 | "selected": "yellow", 43 | "outline": "black", 44 | "bp_match": "darkred", 45 | "bp_match_end": "black", 46 | "bp_mismatch": "orange", 47 | }, 48 | "contig": { 49 | "edge": "black", 50 | "edge_hover": "gray", 51 | "edge_selected": "red", 52 | "fill": "orange", 53 | "fill_alt": "dark_orange", 54 | "fill_hover": "yellow", 55 | "fill_selected": "yellow", 56 | }, 57 | "text": "black", 58 | "genomes_bg": "white" 59 | }, 60 | "Solarized dark": { 61 | "ui": { 62 | "text": solarized["base3"], 63 | "text_hover": solarized["base03"], 64 | "button_bg": solarized["base01"], 65 | "button_highlight": solarized["base3"], 66 | "button_pressed": solarized["base0"], 67 | "general_bg": solarized["base03"], 68 | "panel_bg": solarized["base02"], 69 | }, 70 | "blast_match": { 71 | "fwd": solarized["red"], 72 | "rev": solarized["blue"], 73 | "fwd_hover": solarized["magenta"], 74 | "rev_hover": solarized["violet"], 75 | "selected": solarized["orange"], 76 | "outline": solarized["base1"], 77 | "bp_match": solarized["base02"], 78 | "bp_match_end": solarized["base02"], 79 | "bp_mismatch": solarized["base3"], 80 | }, 81 | "contig": { 82 | "edge": solarized["base3"], 83 | "edge_hover": solarized["base2"], 84 | "edge_selected": solarized["red"], 85 | "fill": solarized["yellow"], 86 | "fill_alt": solarized["dark_yellow"], 87 | "fill_hover": solarized["mid_yellow"], 88 | "fill_selected": solarized["mid_yellow"], 89 | }, 90 | "text": solarized["base3"], 91 | "genomes_bg": solarized["base03"], 92 | }, 93 | "Solarized light": { 94 | "ui": { 95 | "text": solarized["base03"], 96 | "text_hover": solarized["base3"], 97 | "button_bg": solarized["base1"], 98 | "button_highlight": solarized["base03"], 99 | "button_pressed": solarized["base02"], 100 | "general_bg": solarized["base3"], 101 | "panel_bg": solarized["base2"], 102 | }, 103 | "blast_match": { 104 | "fwd": solarized["red"], 105 | "rev": solarized["blue"], 106 | "fwd_hover": solarized["magenta"], 107 | "rev_hover": solarized["violet"], 108 | "selected": solarized["orange"], 109 | "outline": solarized["base1"], 110 | "bp_match": solarized["base02"], 111 | "bp_match_end": solarized["base02"], 112 | "bp_mismatch": solarized["base3"], 113 | }, 114 | "contig": { 115 | "edge": solarized["base03"], 116 | "edge_hover": solarized["base02"], 117 | "edge_selected": solarized["red"], 118 | "fill": solarized["yellow"], 119 | "fill_alt": solarized["dark_yellow"], 120 | "fill_hover": solarized["mid_yellow"], 121 | "fill_selected": solarized["mid_yellow"], 122 | }, 123 | "text": solarized["base03"], 124 | "genomes_bg": solarized["base3"], 125 | }, 126 | "Dark": { 127 | "ui": { 128 | "text": Color(0.95, 0.95, 0.95), 129 | "text_hover": "black", 130 | "button_bg": Color(0.2, 0.2, 0.2), 131 | "button_highlight": Color(0.95, 0.95, 0.95), 132 | "button_pressed": "gainsboro", 133 | "general_bg": "black", 134 | "panel_bg": Color(0.1, 0.1, 0.1), 135 | }, 136 | "blast_match": { 137 | "fwd": Color(0.3, 0.15, 0.15), 138 | "rev": Color(0.15, 0.15, 0.3), 139 | "fwd_hover": Color(0.5, 0.15, 0.15), 140 | "rev_hover": Color(0.15, 0.15, 0.5), 141 | "selected": Color(0.25, 0.25, 0.25), 142 | "outline": Color(0.95, 0.95, 0.95), 143 | "bp_match": Color(0.5, 0.5, 0.5), 144 | "bp_match_end": Color(0.5, 0.5, 0.5), 145 | "bp_mismatch": Color(0.95, 0.95, 0.95), 146 | }, 147 | "contig": { 148 | "edge": Color(0.8, 0.8, 0.8), 149 | "edge_hover": Color(0.7, 0.7, 0.7), 150 | "edge_selected": Color(0.3, 0.15, 0.15), 151 | "fill": Color(0.3, 0.3, 0.3), 152 | "fill_alt": Color(0.2, 0.2, 0.2), 153 | "fill_hover": Color(0.25, 0.25, 0.25), 154 | "fill_selected": Color(0.25, 0.25, 0.25), 155 | }, 156 | "text": Color(0.95, 0.95, 0.95), 157 | "genomes_bg": "black", 158 | }, 159 | "Light": { 160 | "ui": { 161 | "text": Color(0.1, 0.1, 0.1), 162 | "text_hover": "white", 163 | "button_bg": Color(0.8, 0.8, 0.8), 164 | "button_highlight": Color(0.1, 0.1, 0.1), 165 | "button_pressed": "gainsboro", 166 | "general_bg": "white", 167 | "panel_bg": Color(0.9, 0.9, 0.9), 168 | }, 169 | "blast_match": { 170 | "fwd": "firebrick", 171 | "rev": "darkslateblue", 172 | "fwd_hover": "indianred", 173 | "rev_hover": "slateblue", 174 | "selected": Color(0.8, 0.8, 0.8), 175 | "outline": Color(0.1, 0.1, 0.1), 176 | "bp_match": Color(0.1, 0.1, 0.1), 177 | "bp_match_end": Color(0.1, 0.1, 0.1), 178 | "bp_mismatch": "gold", 179 | }, 180 | "contig": { 181 | "edge": Color(0.1, 0.1, 0.1), 182 | "edge_hover": Color(0.2, 0.2, 0.2), 183 | "edge_selected": "firebrick", 184 | "fill": Color(0.8, 0.8, 0.8), 185 | "fill_alt": Color(0.65, 0.65, 0.65), 186 | "fill_hover": Color(0.71, 0.71, 0.71), 187 | "fill_selected": Color(0.71, 0.71, 0.71), 188 | }, 189 | "text": Color(0.1, 0.1, 0.1), 190 | "genomes_bg": "white", 191 | }, 192 | } 193 | 194 | 195 | var name = "Light" 196 | var colours = get_theme() 197 | 198 | func get_theme(): 199 | return colour_themes[name] 200 | 201 | 202 | func set_theme(new_name): 203 | name = new_name 204 | colours = get_theme() 205 | 206 | 207 | func theme_names(): 208 | return colour_themes.keys() 209 | -------------------------------------------------------------------------------- /scenes/new_project/new_project.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | 4 | signal update_top_genome_filename 5 | signal update_bottom_genome_filename 6 | signal new_project_go_button 7 | signal new_project_cancel 8 | signal append_to_info_text 9 | signal clear_info_text 10 | signal enable_go_button 11 | signal set_status_text 12 | signal reset_compare_line_edit 13 | 14 | var filename1 = "" 15 | var filename2 = "" 16 | var ACCESSION_PREFIXES = [ 17 | "GCA_", 18 | "GCF_", 19 | "AC_", 20 | "NC_", 21 | "NG_", 22 | "NT_", 23 | "NW_", 24 | "NZ_", 25 | ] 26 | 27 | func make_gb_regex(): 28 | var r = RegEx.new() 29 | r.compile("^\\w\\w[0-9]{6,}") 30 | return r 31 | var gb_regex = make_gb_regex() 32 | 33 | 34 | func _ready(): 35 | hide() 36 | enable_go_button.emit(true) 37 | get_viewport().files_dropped.connect(on_files_dropped) 38 | 39 | 40 | func clear_fields(): 41 | filename1 = "" 42 | filename2 = "" 43 | update_top_genome_filename.emit("") 44 | update_bottom_genome_filename.emit("") 45 | enable_go_button.emit(false) 46 | clear_info_text.emit() 47 | reset_compare_line_edit.emit() 48 | update_status_text() 49 | 50 | func set_filename1(filename): 51 | filename1 = filename 52 | update_top_genome_filename.emit(filename1) 53 | 54 | 55 | func set_filename2(filename): 56 | filename2 = filename 57 | update_bottom_genome_filename.emit(filename2) 58 | 59 | 60 | func update_go_button(): 61 | enable_go_button.emit(filename1 != "" and filename2 != "") 62 | 63 | 64 | func update_status_text(): 65 | if filename1 == "" and filename2 == "": 66 | set_status_text.emit("Need both genomes. Drag+drop, or type in the boxes") 67 | elif filename1 == "": 68 | set_status_text.emit("Top genome required. Drag+drop or type in the box") 69 | elif filename2 == "": 70 | set_status_text.emit("Bottom genome required. Drag+drop or type in the box") 71 | else: 72 | set_status_text.emit("Ready to go. Press the start button") 73 | 74 | 75 | func on_files_dropped(files): 76 | if len(files) == 1: 77 | var mouse_y = get_global_mouse_position().y 78 | var y_cutoff = $MainVBoxContainer/GridContainer/BottomGenomeLineEdit.global_position.y 79 | if mouse_y < y_cutoff: 80 | set_filename1(files[0]) 81 | else: 82 | set_filename2(files[0]) 83 | elif len(files) == 2: 84 | set_filename1(files[0]) 85 | set_filename2(files[1]) 86 | 87 | update_go_button() 88 | update_status_text() 89 | 90 | 91 | func looks_like_genome_accession(accession): 92 | for p in ACCESSION_PREFIXES: 93 | if accession.begins_with(p): 94 | return true 95 | return gb_regex.search(accession) != null 96 | 97 | 98 | func _on_go_button_pressed(): 99 | # see https://github.com/godotengine/godot/issues/73296 100 | # found adding all these timeouts made the text label update. 101 | # Without them it doesn't change 102 | await get_tree().process_frame 103 | var file_types = {"Top": "file", "Bottom": "file"} 104 | 105 | 106 | for l in [[filename1, "Top"], [filename2, "Bottom"]]: 107 | if FileAccess.file_exists(l[0]): 108 | append_to_info_text.emit(l[1] + " genome file found: " + l[0]) 109 | elif looks_like_genome_accession(l[0]): 110 | append_to_info_text.emit(l[1] + " genome looks like accession, not file. Will try to download") 111 | file_types[l[1]] = "accession" 112 | else: 113 | append_to_info_text.emit("[color=red]" + l[1] + " genome file not found: " + l[0] + "[/color]") 114 | set_status_text.emit(l[1] + " genome not found! Reset and try again") 115 | return 116 | 117 | append_to_info_text.emit("Initialising data folder:\n " + Globals.userdata.current_proj_dir) 118 | Globals.proj_data.create(Globals.userdata.current_proj_dir) 119 | await get_tree().create_timer(0.1).timeout 120 | 121 | append_to_info_text.emit("Start importing genomes (takes time if downloading)", false) 122 | await get_tree().create_timer(0.1).timeout 123 | var thread = Thread.new() 124 | thread.start(Globals.proj_data.import_genomes.bind(filename1, file_types["Top"], filename2, file_types["Bottom"])) 125 | var count = 0 126 | while thread.is_alive(): 127 | await get_tree().create_timer(min(count, 3)).timeout 128 | count += 1 129 | append_to_info_text.emit(".", false) 130 | var errors = thread.wait_to_finish() 131 | append_to_info_text.emit("\nImporting finished") 132 | var ok = (errors[0] == 0 and errors[1] == 0) 133 | append_to_info_text.emit(" top genome ok: " + str(errors[0] == 0)) 134 | append_to_info_text.emit(" bottom genome ok: " + str(errors[1] == 0)) 135 | if not ok: 136 | append_to_info_text.emit("[color=red]Errors loading genome(s). Cannot continue[/color]") 137 | set_status_text.emit("Errors loading genome(s). Reset and try again") 138 | return 139 | 140 | append_to_info_text.emit("Genomes imported. Running blast") 141 | await get_tree().create_timer(0.1).timeout 142 | var result = Globals.proj_data.run_blast() 143 | if result[0] != 0: 144 | append_to_info_text.emit("Blast output (has errors):\n" + "\n".join(result[1])) 145 | append_to_info_text.emit("[color=red]Error running blast[/color]") 146 | set_status_text.emit("Error running blast") 147 | return 148 | 149 | append_to_info_text.emit("Loading genomes") 150 | await get_tree().create_timer(0.1).timeout 151 | Globals.proj_data.load_genomes() 152 | 153 | append_to_info_text.emit("Blast finished. Loading results") 154 | await get_tree().create_timer(0.1).timeout 155 | Globals.proj_data.load_blast_hits() 156 | 157 | append_to_info_text.emit("Loading annotation") 158 | await get_tree().create_timer(0.1).timeout 159 | Globals.proj_data.load_annotation_files() 160 | Globals.proj_data.set_data_loaded() 161 | hide() 162 | clear_fields() 163 | new_project_go_button.emit() 164 | 165 | 166 | func _on_main_menu_new_project(): 167 | clear_fields() 168 | show() 169 | 170 | 171 | func _on_cancel_button_pressed(): 172 | hide() 173 | clear_fields() 174 | new_project_cancel.emit() 175 | 176 | 177 | func _on_top_genome_line_edit_text_submitted(new_text): 178 | set_filename1(new_text) 179 | update_go_button() 180 | update_status_text() 181 | 182 | 183 | func _on_bottom_genome_line_edit_text_submitted(new_text): 184 | set_filename2(new_text) 185 | update_go_button() 186 | update_status_text() 187 | 188 | 189 | func _on_reset_button_pressed(): 190 | clear_fields() 191 | 192 | 193 | func _on_open_file_manager_button_pressed(): 194 | OS.shell_show_in_file_manager(Globals.userdata.home_dir) 195 | -------------------------------------------------------------------------------- /lib/user_data.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | class_name UserData 4 | 5 | 6 | var default_config = { 7 | "colours": {"theme": "Light"}, 8 | "mouse": { 9 | "wheel_sens": 1, 10 | "invert_wheel": false, 11 | }, 12 | "trackpad": { 13 | "v_sens": 1, 14 | "invert_v": false, 15 | "h_sens": 1, 16 | "p_sens": 1, 17 | }, 18 | "blast": {"share_data": false}, 19 | "other": { 20 | "max_matches_on_screen": 500, 21 | "fasta_line_length": 60, 22 | "min_gap_length": 10, 23 | }, 24 | } 25 | 26 | 27 | func get_os(): 28 | var got_os = OS.get_name() 29 | match got_os: 30 | "Windows": 31 | return "windows" 32 | "macOS": 33 | return "mac" 34 | "Linux", "FreeBSD", "NetBSD", "OpenBSD", "BSD": 35 | return "linux" 36 | 37 | # TODO: handle this properly 38 | print("Unsupported OS: ", got_os) 39 | OS.alert("Error! Unsupported OS: " + got_os + ". Cannot continue.", "Error!") 40 | return null 41 | 42 | func get_os_newline(): 43 | if get_os() == "windows": 44 | return "\r\n" 45 | else: 46 | return "\n" 47 | 48 | var os_newline = get_os_newline() 49 | 50 | 51 | func get_bin_path(): 52 | return OS.get_user_data_dir().path_join("bin") 53 | 54 | 55 | func get_example_data_dir(): 56 | return OS.get_user_data_dir().path_join("example_data") 57 | 58 | 59 | func fix_windows_binary(b): 60 | if get_os() == "windows": 61 | return b + ".exe" 62 | else: 63 | return b 64 | 65 | 66 | var home_dir = OS.get_environment("USERPROFILE") if OS.has_feature("windows") else OS.get_environment("HOME") 67 | var bin = get_bin_path() 68 | var makeblastdb = fix_windows_binary(bin.path_join("makeblastdb")) 69 | var blastn = fix_windows_binary(bin.path_join("blastn")) 70 | var zlhelper = fix_windows_binary(bin.path_join("zlhelper")) 71 | var example_data_dir = get_example_data_dir() 72 | var data_dir = OS.get_user_data_dir() 73 | var example_data_file1 = example_data_dir.path_join("g1.gff") 74 | var example_data_file2 = example_data_dir.path_join("g2.gff") 75 | var data_dir_exists = false 76 | var bin_exists = false 77 | var blastn_exists = false 78 | var makeblastdb_exists = false 79 | var zlhelper_exists = false 80 | var zlhelper_version_ok = false 81 | var example_data_exists = false 82 | var current_proj_dir = OS.get_user_data_dir().path_join("current_proj") 83 | var install_ok = false 84 | var config_file = OS.get_user_data_dir().path_join("config") 85 | var config_file_exists = false 86 | var config = ConfigFile.new() 87 | var zlhelper_version = "unknown" 88 | var blastn_version = "unknown" 89 | var blast_options = "-evalue 0.1" 90 | var default_blast_options = "-evalue 0.1" 91 | 92 | func does_example_data_exist(): 93 | if DirAccess.dir_exists_absolute(example_data_dir): 94 | print("example data dir exists: ", example_data_dir) 95 | else: 96 | print("example data dir not found: ", example_data_dir) 97 | return false 98 | if FileAccess.file_exists(example_data_file1): 99 | print("example data genome file 1 exists: ", example_data_file1) 100 | else: 101 | return false 102 | if FileAccess.file_exists(example_data_file2): 103 | print("example data genome file 2 exists: ", example_data_file2) 104 | else: 105 | return false 106 | return true 107 | 108 | 109 | func get_architecture(): 110 | var got_arch = Engine.get_architecture_name() 111 | if "x86" in got_arch or "arm" in got_arch: 112 | return got_arch 113 | else: 114 | # TODO: handle this properly 115 | print("unsupported architecture: ", got_arch) 116 | OS.alert("Error! Unsupported architecture: " + got_arch + ". Cannot continue.", "Error!") 117 | return null 118 | 119 | var os = get_os() 120 | var arch = get_architecture() 121 | 122 | 123 | func run_os_execute_get_output(to_execute, options): 124 | print("Running: ", to_execute, " ", " ".join(options)) 125 | var output = [] 126 | var exit_code = OS.execute(to_execute, options, output, true) 127 | if exit_code != 0: 128 | print("Error running ", to_execute, ". output: ", output) 129 | return output[0].rstrip("\n").rstrip("\r").split("\n") 130 | 131 | 132 | func set_zlhelper_version(): 133 | zlhelper_version = "unknown" 134 | var output = run_os_execute_get_output(zlhelper, ["-v"]) 135 | if len(output) == 1: 136 | var fields = output[0].rstrip("\r").split(" ") 137 | if len(fields) == 3 and fields[0] == "zlhelper" and fields[1] == "version": 138 | zlhelper_version = fields[2] 139 | zlhelper_version_ok = zlhelper_version == Globals.expect_zlhelper_version 140 | if config.get_value("dev", "ignore_zlhelper_version", false): 141 | zlhelper_version_ok = true 142 | 143 | 144 | func set_blastn_version(): 145 | blastn_version = "unknown" 146 | var output = run_os_execute_get_output(blastn, ["-version"]) 147 | if len(output) > 0: 148 | var fields = output[0].rstrip("\r").split(" ") 149 | if len(fields) == 2 and fields[0] == "blastn:": 150 | blastn_version = fields[1] 151 | 152 | 153 | func check_all_paths(): 154 | data_dir_exists = DirAccess.dir_exists_absolute(data_dir) 155 | print("Checking bin directory:", bin) 156 | bin_exists = DirAccess.dir_exists_absolute(bin) 157 | print("bin dir exists:", bin_exists) 158 | blastn_exists = FileAccess.file_exists(blastn) 159 | print("blastn exists:", blastn_exists) 160 | makeblastdb_exists = FileAccess.file_exists(makeblastdb) 161 | print("makeblastdb exists:", makeblastdb_exists) 162 | set_blastn_version() 163 | print("blastn version:", blastn_version) 164 | zlhelper_exists = FileAccess.file_exists(zlhelper) 165 | print("zlhelper exists:", zlhelper_exists) 166 | example_data_exists = does_example_data_exist() 167 | print("example data found:", example_data_exists) 168 | config_file_exists = FileAccess.file_exists(config_file) 169 | print("config file found:", config_file_exists) 170 | install_ok = bin_exists and zlhelper_exists and zlhelper_version_ok \ 171 | and blastn_exists and makeblastdb_exists and makeblastdb_exists \ 172 | and example_data_exists and config_file_exists \ 173 | and blastn_version != "unknown" 174 | 175 | 176 | func make_default_config(): 177 | config.clear() 178 | for section in default_config: 179 | for key in default_config[section]: 180 | config.set_value(section, key, default_config[section][key]) 181 | config.save(config_file) 182 | 183 | 184 | func load_config(): 185 | config.load(config_file) 186 | var any_missing = false 187 | for section in default_config: 188 | for key in default_config[section]: 189 | if not config.has_section_key(section, key): 190 | config.set_value(section, key, default_config[section][key]) 191 | any_missing = true 192 | 193 | if any_missing: 194 | save_config() 195 | 196 | 197 | func save_config(): 198 | config.save(config_file) 199 | 200 | 201 | func _init(): 202 | pass 203 | -------------------------------------------------------------------------------- /lib/annot_feature.gd: -------------------------------------------------------------------------------- 1 | extends StaticBody2D 2 | 3 | class_name AnnotFeature 4 | 5 | signal mouse_in 6 | signal mouse_out 7 | 8 | var static_body_2d = StaticBody2D.new() 9 | var coll_poly = CollisionPolygon2D.new() 10 | var poly = Polygon2D.new() 11 | var outline = Line2D.new() 12 | var top = 50 13 | var bottom = 100 14 | var outline_width = 1.5 15 | var gff_data = [] 16 | var parent_ctg 17 | var name_label = Label.new() 18 | var hovering = false 19 | var selected = false 20 | var id = -1 21 | var default_z = 0 22 | var is_currently_visible = false 23 | 24 | 25 | func set_top_and_bottom(new_top, new_bottom): 26 | top = new_top 27 | bottom = new_bottom 28 | if "Parent" in gff_data[4] or "parent" in gff_data[4]: 29 | top += 2 30 | bottom -= 2 31 | set_polygon_coords() 32 | 33 | 34 | func is_rev(): 35 | return gff_data[3] 36 | 37 | 38 | func is_gap(): 39 | return gff_data[2] == "gap" 40 | 41 | 42 | func is_on_screen(): 43 | return -Globals.controls_width <= poly.polygon[1].x \ 44 | and poly.polygon[0].x <= Globals.controls_width + Globals.genomes_viewport_width 45 | 46 | 47 | func update_name_label(): 48 | if poly.polygon[1].x - poly.polygon[0].x > 15: 49 | name_label.position = Vector2(poly.polygon[0].x + 1, poly.polygon[0].y + 1) 50 | name_label.set_size(Vector2(poly.polygon[1].x - poly.polygon[0].x - 1, poly.polygon[2].y - poly.polygon[0].y - 1)) 51 | name_label.show() 52 | else: 53 | name_label.hide() 54 | 55 | 56 | func make_visible(): 57 | if is_currently_visible: 58 | return 59 | static_body_2d = StaticBody2D.new() 60 | add_child(static_body_2d) 61 | coll_poly = CollisionPolygon2D.new() 62 | static_body_2d.add_child(coll_poly) 63 | static_body_2d.set_pickable(true) 64 | static_body_2d.mouse_entered.connect(_on_mouse_entered) 65 | static_body_2d.mouse_exited.connect(_on_mouse_exited) 66 | outline.points = poly.polygon 67 | outline.add_point(outline.points[0]) 68 | coll_poly.polygon = poly.polygon 69 | is_currently_visible = true 70 | update_name_label() 71 | show() 72 | 73 | 74 | func make_invisible(): 75 | if not is_currently_visible: 76 | return 77 | 78 | coll_poly.free() 79 | static_body_2d.free() 80 | is_currently_visible = false 81 | hide() 82 | 83 | 84 | func set_visibility(zoom): 85 | if not is_on_screen(): 86 | make_invisible() 87 | elif zoom > Globals.zoom_to_show_annot_all \ 88 | or (zoom > Globals.zoom_to_show_annot_500 and (gff_data[1] - gff_data[0]) >= 500) \ 89 | or (zoom > Globals.zoom_to_show_annot_1k and (gff_data[1] - gff_data[0]) >= 1000) \ 90 | or (zoom > Globals.zoom_to_show_annot_2k and (gff_data[1] - gff_data[0]) >= 2000): 91 | make_visible() 92 | else: 93 | make_invisible() 94 | 95 | 96 | func set_default_z(z): 97 | default_z = z 98 | z_index = z 99 | 100 | 101 | func set_polygon_coords(): 102 | poly.polygon[0] = Vector2(-(0.5 * outline_width) + parent_ctg.x_start + (parent_ctg.x_end - parent_ctg.x_start) * 1.0 * (gff_data[0]-0.4) / parent_ctg.length_in_bp, top) 103 | poly.polygon[1] = Vector2((0.5 * outline_width) + parent_ctg.x_start + (parent_ctg.x_end - parent_ctg.x_start) * 1.0 * (gff_data[1]+0.4) / parent_ctg.length_in_bp, top) 104 | poly.polygon[2] = Vector2(poly.polygon[1].x, bottom) 105 | poly.polygon[3] = Vector2(poly.polygon[0].x, bottom) 106 | 107 | if not is_currently_visible: 108 | return 109 | 110 | outline.points = poly.polygon 111 | outline.add_point(outline.points[0]) 112 | coll_poly.polygon = poly.polygon 113 | update_name_label() 114 | 115 | 116 | func gff2tooltip(a: Array): 117 | return parent_ctg.name() + ": " + str(a[0] + 1) + "-" + str(a[1] + 1) + "\n" \ 118 | + "type=" + a[2] + "\n" \ 119 | + "tags=" + str(a[4]) 120 | 121 | 122 | func selected_str(): 123 | var tags = [] 124 | for k in gff_data[4]: 125 | tags.append(k + "=" + gff_data[4][k]) 126 | return str(gff_data[0] + 1) + "-" + str(gff_data[1] + 1) + \ 127 | " / type=" + gff_data[2] + \ 128 | " / " + ";".join(tags) 129 | 130 | 131 | func _init(new_id, gff_data_list, new_top, new_bottom, parent_contig): 132 | id = new_id 133 | gff_data = gff_data_list 134 | top = new_top 135 | bottom = new_bottom 136 | parent_ctg = parent_contig 137 | if "Parent" in gff_data[4] or "parent" in gff_data[4]: 138 | top += 2 139 | bottom -= 2 140 | outline_width = 1 141 | if is_gap(): 142 | outline_width = 0.5 143 | poly.polygon = [Vector2(0, 0), Vector2(0, 0), Vector2(0, 0), Vector2(0, 0)] 144 | outline.width = outline_width 145 | outline.default_color = Globals.theme.colours["ui"]["text"] 146 | poly.color = Globals.theme.colours["ui"]["panel_bg"] 147 | static_body_2d.set_pickable(true) 148 | static_body_2d.mouse_entered.connect(_on_mouse_entered) 149 | static_body_2d.mouse_exited.connect(_on_mouse_exited) 150 | name_label.add_theme_color_override("font_color", Globals.theme.colours["text"]) 151 | name_label.add_theme_font_override("font", Globals.fonts["dejavu"]) 152 | if is_gap(): 153 | name_label.add_theme_font_size_override("font_size", Globals.font_annot_gap_size) 154 | poly.color.a = 0.2 155 | outline.default_color.a = 0.5 156 | else: 157 | name_label.add_theme_font_size_override("font_size", Globals.font_annot_size) 158 | name_label.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER) 159 | name_label.clip_text = true 160 | z_index = 1 161 | 162 | for k in ["Name", "name", "ID"]: 163 | name_label.text = gff_data[4].get(k, "") 164 | if name_label.text != "": 165 | break 166 | if name_label.text == "": 167 | name_label.text = "UNKNOWN" 168 | if is_gap(): 169 | name_label.text = "" 170 | 171 | set_polygon_coords() 172 | add_child(static_body_2d) 173 | static_body_2d.add_child(coll_poly) 174 | add_child(poly) 175 | add_child(outline) 176 | add_child(name_label) 177 | hide() 178 | 179 | 180 | func start_x_coord(): 181 | return poly.polygon[0].x 182 | 183 | 184 | func select(): 185 | selected = true 186 | if hovering: 187 | outline.default_color = Globals.theme.colours["contig"]["edge_selected"] 188 | else: 189 | outline.default_color = Globals.theme.colours["ui"]["text"] 190 | 191 | poly.color = Globals.theme.colours["ui"]["text"] 192 | if is_gap(): 193 | poly.color.a = 0.2 194 | name_label.add_theme_color_override("font_color", Globals.theme.colours["ui"]["general_bg"]) 195 | z_index = 100 196 | 197 | 198 | func deselect(): 199 | selected = false 200 | if hovering: 201 | outline.default_color = Globals.theme.colours["contig"]["edge_selected"] 202 | else: 203 | outline.default_color = Globals.theme.colours["ui"]["text"] 204 | 205 | poly.color = Globals.theme.colours["ui"]["panel_bg"] 206 | if is_gap(): 207 | poly.color.a = 0.2 208 | name_label.add_theme_color_override("font_color", Globals.theme.colours["text"]) 209 | z_index = default_z 210 | 211 | 212 | func metadata_has_search_string(search_string): 213 | for k in gff_data[4]: 214 | if gff_data[4][k].findn(search_string) != -1: 215 | return true 216 | return false 217 | 218 | 219 | func _on_mouse_entered(): 220 | hovering = true 221 | outline.default_color = Globals.theme.colours["contig"]["edge_selected"] 222 | mouse_in.emit(id) 223 | 224 | 225 | func _on_mouse_exited(): 226 | hovering = false 227 | if not selected: 228 | outline.default_color = Globals.theme.colours["ui"]["text"] 229 | mouse_out.emit(id) 230 | -------------------------------------------------------------------------------- /docs/viewing.rst: -------------------------------------------------------------------------------- 1 | Viewing Genomes 2 | =============== 3 | 4 | 5 | Default view 6 | ------------ 7 | 8 | The initial view is zoomed to show an overview of both genomes and the 9 | BLAST matches between them. 10 | 11 | .. image:: pics/zl_docs_genomes_overview.png 12 | :width: 500 13 | :alt: screenshot showing initial view of genomes and matches 14 | 15 | 16 | Contigs 17 | ^^^^^^^ 18 | 19 | The contigs of each genome are shown in alternating colours, and annotation 20 | features (eg genes) are shown inside them. Features on the forwards strand 21 | (or with no strand annotated) are shown in the top half of the genome, and 22 | those on the reverse strand are in the bottom half of the genome. 23 | 24 | Names of contigs and contig coordinates are shown above the top genome, 25 | and below the bottom genome. 26 | 27 | 28 | BLAST matches 29 | ^^^^^^^^^^^^^ 30 | 31 | BLAST matches are show as parallelograms between the top and bottom genomes. 32 | Matches on the same strand in each genome are red, and those on opposite 33 | strands are blue (using the default colour scheme, otherwise the colours 34 | may be different). Reverse matches are "twisted" into two triangles because 35 | the match runs in opposing directions on each genome. 36 | The colour of each match is scaled with the percent 37 | identity, getting lighter the further away from an identity of 100%. 38 | 39 | Left-clicking on a match will highlight it, and show its details 40 | at the top of the window. It will say something like 41 | "Selected - Match: top_contig_name:901-1400 / bottom_contig_name:1-500 / pcid:99.5". 42 | In that example, the match is at position 901 to 1400 in the top contig, 43 | 1-500 in the bottom contig, with 99.5 percent identity. 44 | 45 | 46 | Navigation 47 | ---------- 48 | 49 | There are several ways to pan and zoom around the genomes. There are 50 | buttons in the left-hand panel, mouse/trackpad controls, and 51 | :doc:`keyboard shortcuts `. 52 | 53 | The scrollbars at the top and bottom are a simple way to slide the view 54 | of either genome to the left or right. 55 | 56 | 57 | 58 | Navigation panel 59 | ^^^^^^^^^^^^^^^^ 60 | 61 | .. image:: pics/zl_docs_nav_buttons.png 62 | :width: 400 63 | :alt: explanation of zoom controls 64 | 65 | These buttons let you shift the view of the top and/or bottom genome. 66 | They allow for all combinations of moving either genome to the left or right. 67 | By default one button press will move 50% of of the window width. 68 | Turning on the "fine control" button makes each movement significantly 69 | smaller. 70 | 71 | 72 | Zoom panel 73 | ^^^^^^^^^^ 74 | 75 | .. image:: pics/zl_docs_zoom_buttons.png 76 | :width: 400 77 | :alt: explanation of navigation buttons 78 | 79 | These buttons allow you to zoom in or out, reset the view, or zoom in 80 | enough to see the sequence of each genome. Zooming in and out is centered 81 | on the middle of the window. 82 | 83 | 84 | 85 | Jump with double-clicking 86 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 87 | 88 | Double-clicking at any point in a genome will shift its view so that the 89 | position where you double-clicked is in the middle of the screen. 90 | 91 | Double-clicking a BLAST match shifts the view so that the left of the match 92 | is at the centre of the screen. 93 | 94 | 95 | Mouse/trackpad gestures 96 | ^^^^^^^^^^^^^^^^^^^^^^^ 97 | 98 | You can zoom in and out using the mouse wheel, or using a pinch gesture 99 | on a trackpad. The zoom is centered on the position of the mouse pointer. 100 | On a Mac with magic mouse, sliding up/down will zoom. 101 | 102 | You can scroll left/right (ie moving both top and bottom genomes) using a 103 | swipe left/right gesture. This has only been tested on a Mac, which is 104 | sliding two fingers left or right on the trackpad, or a single finger 105 | sideways slide on a magic mouse. 106 | 107 | The sensitivity of these can be adjusted in the 108 | :doc:`settings `. 109 | 110 | 111 | 112 | Base-pair view 113 | -------------- 114 | 115 | Zooming in enough will show the contig sequences 116 | on the forward and reverse strands. It also shows more detailed BLAST 117 | alignments. Instead of just a solid block, it uses lines to show 118 | each matching position between the genomes, and highlights SNPs. Indels 119 | result in non-parallel lines. 120 | 121 | .. image:: pics/zl_docs_basepair_view.png 122 | :width: 500 123 | :alt: screenshot of zoomed in view showing base pairs 124 | 125 | This information is taken from the BLAST alignment output that you are 126 | probably familiar with, which has this format: 127 | 128 | .. code-block:: text 129 | 130 | AC--TGACGTACG 131 | || || ||*|| 132 | ACTGTG--GTCCG 133 | 134 | 135 | 136 | 137 | Filtering matches 138 | ----------------- 139 | 140 | Matches that are shown can be filtered by minimum length or percent identity, 141 | using the boxes in the left panel. If it is not already selected, press the 142 | "filter" icon to show the filter options. 143 | 144 | .. image:: pics/zl_docs_match_filter.png 145 | :width: 140 146 | :alt: screenshot of match filter controls 147 | 148 | By default, only matches of length 100bp-1Mbp and at least 90% identity are 149 | shown. The minimum values can be changed in the boxes. Note that showing all 150 | matches may slow Ziplign down if there is a large number of them on screen. 151 | 152 | If you are viewing a genome compared to itself, then you probably want to 153 | set the maximum match length to be less than the length of the genome, so that 154 | there is not one huge match filling in the whole window. 155 | 156 | 157 | Matches in a region 158 | ------------------- 159 | 160 | You can find all BLAST matches that lie in a particular region of 161 | either genome. On either genome, left-click and hold, then drag 162 | to highlight a region. When the button is released, all matches that overlap the 163 | highlighted region are listed in the bottom of the left hand panel. 164 | 165 | 166 | .. image:: pics/zl_docs_match_list.png 167 | :width: 520 168 | :alt: screenshots of making a list of matches 169 | 170 | Click on any of the matches listed on the left to jump to them in the main 171 | viewing window. Or there are up and down arrows to scroll through them, making 172 | it easy to jump to each match in turn. Pressing the "X" button 173 | will close the list. 174 | 175 | 176 | Selecting and copying a region 177 | ------------------------------ 178 | 179 | After dragging along any region of either genome with the left mouse 180 | button, it remains highlighted and the coordinates are in the top where 181 | it says "Selected = Region: ...". Dragging from left to right will select 182 | the forwards strand, and from right to left selects the reverse strand. 183 | 184 | The highlighted sequence can be copied to the clipboard using 185 | ctrl-c (Windows/linux) or cmd-c (macOS), or by pressing the "copy" button 186 | at the top. 187 | 188 | .. image:: pics/zl_docs_copy_region.png 189 | :width: 300 190 | :alt: screenshots of making a list of matches 191 | 192 | The sequence is copied in FASTA format and includes the sequence coordinates 193 | in the FASTA header. It will be reverse complemented if the reverse strand 194 | was highlighted. If the highlighted region spans more than one contig, then 195 | it will be in multi-FASTA format. 196 | 197 | 198 | Saving and loading positions 199 | ---------------------------- 200 | 201 | There are 9 slots for temporarily saving views. Pressing shift+1 saves 202 | the current position (zoom level and genome positions) to slot 1. 203 | Then pressing 1 will jump back to that saved position. This works 204 | for the keys 1-9, giving 9 save slots. 205 | 206 | Note that this is not remembered between sessions, and 207 | if you 208 | :doc:`move or reverse complement contigs ` 209 | (described later), the saved 210 | positions will not be aware of it. They will just jump to the position 211 | as defined by where the top/bottom scrollbars were and set the zoom level. 212 | 213 | 214 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Ziplign" 14 | config/version="1.1.0" 15 | run/main_scene="res://main.tscn" 16 | config/use_custom_user_dir=true 17 | config/features=PackedStringArray("4.4", "Forward Plus") 18 | run/max_fps=60 19 | run/low_processor_mode=true 20 | boot_splash/use_filter=false 21 | boot_splash/image="ziplign_splash.png" 22 | config/icon="ziplign_logo.png" 23 | boot_splash/minimum_display_time=1500 24 | 25 | [autoload] 26 | 27 | Globals="*res://globals.gd" 28 | 29 | [display] 30 | 31 | window/energy_saving/keep_screen_on=false 32 | window/stretch/mode="canvas_items" 33 | window/stretch/aspect="keep_height" 34 | 35 | [input] 36 | 37 | ui_left={ 38 | "deadzone": 0.5, 39 | "events": [] 40 | } 41 | ui_up={ 42 | "deadzone": 0.5, 43 | "events": [] 44 | } 45 | ui_down={ 46 | "deadzone": 0.5, 47 | "events": [] 48 | } 49 | genomes_tl_bl={ 50 | "deadzone": 0.5, 51 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 52 | ] 53 | } 54 | genomes_tr_br={ 55 | "deadzone": 0.5, 56 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 57 | ] 58 | } 59 | genomes_tl_br={ 60 | "deadzone": 0.5, 61 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 62 | ] 63 | } 64 | genomes_tr_bl={ 65 | "deadzone": 0.5, 66 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 67 | ] 68 | } 69 | zoom_minus={ 70 | "deadzone": 0.5, 71 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":45,"key_label":0,"unicode":45,"location":0,"echo":false,"script":null) 72 | ] 73 | } 74 | zoom_plus={ 75 | "deadzone": 0.5, 76 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":61,"key_label":0,"unicode":43,"location":0,"echo":false,"script":null) 77 | , null] 78 | } 79 | zoom_reset={ 80 | "deadzone": 0.5, 81 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":61,"key_label":0,"unicode":61,"location":0,"echo":false,"script":null) 82 | ] 83 | } 84 | zoom_bp={ 85 | "deadzone": 0.5, 86 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":48,"key_label":0,"unicode":48,"location":0,"echo":false,"script":null) 87 | ] 88 | } 89 | pause={ 90 | "deadzone": 0.5, 91 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":113,"location":0,"echo":false,"script":null) 92 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":80,"key_label":0,"unicode":112,"location":0,"echo":false,"script":null) 93 | ] 94 | } 95 | resume={ 96 | "deadzone": 0.5, 97 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":114,"location":0,"echo":false,"script":null) 98 | ] 99 | } 100 | view_1={ 101 | "deadzone": 0.5, 102 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":49,"key_label":0,"unicode":49,"location":0,"echo":false,"script":null) 103 | ] 104 | } 105 | view_2={ 106 | "deadzone": 0.5, 107 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":50,"key_label":0,"unicode":50,"location":0,"echo":false,"script":null) 108 | ] 109 | } 110 | view_3={ 111 | "deadzone": 0.5, 112 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":51,"key_label":0,"unicode":51,"location":0,"echo":false,"script":null) 113 | ] 114 | } 115 | view_4={ 116 | "deadzone": 0.5, 117 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":52,"key_label":0,"unicode":52,"location":0,"echo":false,"script":null) 118 | ] 119 | } 120 | view_5={ 121 | "deadzone": 0.5, 122 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":53,"key_label":0,"unicode":53,"location":0,"echo":false,"script":null) 123 | ] 124 | } 125 | view_6={ 126 | "deadzone": 0.5, 127 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":54,"key_label":0,"unicode":54,"location":0,"echo":false,"script":null) 128 | ] 129 | } 130 | view_7={ 131 | "deadzone": 0.5, 132 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":55,"key_label":0,"unicode":55,"location":0,"echo":false,"script":null) 133 | ] 134 | } 135 | view_8={ 136 | "deadzone": 0.5, 137 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":56,"key_label":0,"unicode":56,"location":0,"echo":false,"script":null) 138 | ] 139 | } 140 | view_9={ 141 | "deadzone": 0.5, 142 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":57,"key_label":0,"unicode":57,"location":0,"echo":false,"script":null) 143 | ] 144 | } 145 | fine_toggle={ 146 | "deadzone": 0.5, 147 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null) 148 | ] 149 | } 150 | 151 | [rendering] 152 | 153 | renderer/rendering_method="gl_compatibility" 154 | textures/vram_compression/import_s3tc_bptc=true 155 | textures/vram_compression/import_etc2_astc=true 156 | -------------------------------------------------------------------------------- /scenes/new_project/new_project.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=9 format=3 uid="uid://o6vk13fe8nhl"] 2 | 3 | [ext_resource type="Script" uid="uid://bombolyqp5yek" path="res://scenes/new_project/new_project.gd" id="1_cvvaa"] 4 | [ext_resource type="FontFile" uid="uid://qtretpwxqaht" path="res://fonts/ziplignicons.otf" id="2_3uycm"] 5 | [ext_resource type="Script" uid="uid://3ohtdnp6hbad" path="res://scenes/new_project/TopGenomeLineEdit.gd" id="2_ed3tn"] 6 | [ext_resource type="Script" uid="uid://dms14vxnfbknb" path="res://scenes/new_project/StatusLabel.gd" id="2_rom0h"] 7 | [ext_resource type="Script" uid="uid://do5dej12rld3h" path="res://scenes/new_project/BottomGenomeLineEdit.gd" id="3_4xijc"] 8 | [ext_resource type="Script" uid="uid://cy4f02oyivk82" path="res://scenes/new_project/GoButton.gd" id="4_n1phj"] 9 | [ext_resource type="Script" uid="uid://5g4uryhwtpkc" path="res://scenes/new_project/InfoRichTextLabel.gd" id="5_4kmm3"] 10 | [ext_resource type="Script" uid="uid://ccfxk07eo2btw" path="res://scenes/new_project/compare_line_edit.gd" id="5_ulwbr"] 11 | 12 | [node name="NewProject" type="Control"] 13 | layout_mode = 3 14 | anchors_preset = 15 15 | anchor_right = 1.0 16 | anchor_bottom = 1.0 17 | grow_horizontal = 2 18 | grow_vertical = 2 19 | script = ExtResource("1_cvvaa") 20 | 21 | [node name="MainVBoxContainer" type="VBoxContainer" parent="."] 22 | layout_mode = 0 23 | offset_right = 1110.0 24 | offset_bottom = 642.0 25 | 26 | [node name="HBoxContainer" type="HBoxContainer" parent="MainVBoxContainer"] 27 | layout_mode = 2 28 | 29 | [node name="CancelButton" type="Button" parent="MainVBoxContainer/HBoxContainer"] 30 | layout_mode = 2 31 | tooltip_text = "Back to main menu" 32 | theme_override_fonts/font = ExtResource("2_3uycm") 33 | theme_override_font_sizes/font_size = 40 34 | text = "3" 35 | 36 | [node name="ResetButton" type="Button" parent="MainVBoxContainer/HBoxContainer"] 37 | layout_mode = 2 38 | tooltip_text = "Reset all fields" 39 | theme_override_fonts/font = ExtResource("2_3uycm") 40 | theme_override_font_sizes/font_size = 40 41 | text = "6" 42 | 43 | [node name="OpenFileManagerButton" type="Button" parent="MainVBoxContainer/HBoxContainer"] 44 | layout_mode = 2 45 | tooltip_text = "Open file browser" 46 | theme_override_fonts/font = ExtResource("2_3uycm") 47 | theme_override_font_sizes/font_size = 40 48 | text = "4" 49 | 50 | [node name="UseTestDataButton" type="Button" parent="MainVBoxContainer/HBoxContainer"] 51 | layout_mode = 2 52 | tooltip_text = "Use built-in test data" 53 | theme_override_fonts/font = ExtResource("2_3uycm") 54 | theme_override_font_sizes/font_size = 40 55 | text = "7" 56 | 57 | [node name="HSeparator" type="HSeparator" parent="MainVBoxContainer"] 58 | layout_mode = 2 59 | 60 | [node name="StatusGenomeContainer" type="HBoxContainer" parent="MainVBoxContainer"] 61 | layout_mode = 2 62 | 63 | [node name="StatusLabel" type="Label" parent="MainVBoxContainer/StatusGenomeContainer"] 64 | layout_mode = 2 65 | theme_override_font_sizes/font_size = 30 66 | text = "default text" 67 | script = ExtResource("2_rom0h") 68 | 69 | [node name="Label" type="Label" parent="MainVBoxContainer/StatusGenomeContainer"] 70 | layout_mode = 2 71 | 72 | [node name="HSeparator6" type="HSeparator" parent="MainVBoxContainer"] 73 | layout_mode = 2 74 | 75 | [node name="GridContainer" type="GridContainer" parent="MainVBoxContainer"] 76 | layout_mode = 2 77 | columns = 2 78 | 79 | [node name="TopGenomeLabel" type="Label" parent="MainVBoxContainer/GridContainer"] 80 | layout_mode = 2 81 | theme_override_font_sizes/font_size = 30 82 | text = "Top genome file " 83 | 84 | [node name="TopGenomeLineEdit" type="LineEdit" parent="MainVBoxContainer/GridContainer"] 85 | layout_mode = 2 86 | size_flags_horizontal = 3 87 | theme_override_font_sizes/font_size = 30 88 | script = ExtResource("2_ed3tn") 89 | 90 | [node name="BottomGenomeLabel" type="Label" parent="MainVBoxContainer/GridContainer"] 91 | layout_mode = 2 92 | theme_override_font_sizes/font_size = 30 93 | text = "Bottom genome file " 94 | 95 | [node name="BottomGenomeLineEdit" type="LineEdit" parent="MainVBoxContainer/GridContainer"] 96 | layout_mode = 2 97 | theme_override_font_sizes/font_size = 30 98 | script = ExtResource("3_4xijc") 99 | 100 | [node name="CompareLabel" type="Label" parent="MainVBoxContainer/GridContainer"] 101 | layout_mode = 2 102 | theme_override_font_sizes/font_size = 30 103 | text = "Blast options " 104 | 105 | [node name="CompareLineEdit" type="LineEdit" parent="MainVBoxContainer/GridContainer"] 106 | layout_mode = 2 107 | theme_override_font_sizes/font_size = 30 108 | text = "Put blast options here" 109 | script = ExtResource("5_ulwbr") 110 | 111 | [node name="Label" type="Label" parent="MainVBoxContainer/GridContainer"] 112 | layout_mode = 2 113 | theme_override_font_sizes/font_size = 30 114 | text = "Start" 115 | 116 | [node name="GoButton" type="Button" parent="MainVBoxContainer/GridContainer"] 117 | layout_mode = 2 118 | size_flags_horizontal = 0 119 | theme_override_fonts/font = ExtResource("2_3uycm") 120 | theme_override_font_sizes/font_size = 30 121 | text = "5" 122 | script = ExtResource("4_n1phj") 123 | 124 | [node name="HSeparator2" type="HSeparator" parent="MainVBoxContainer"] 125 | layout_mode = 2 126 | 127 | [node name="ScrollContainer" type="ScrollContainer" parent="MainVBoxContainer"] 128 | layout_mode = 2 129 | size_flags_vertical = 3 130 | follow_focus = true 131 | horizontal_scroll_mode = 0 132 | 133 | [node name="InfoRichTextLabel" type="RichTextLabel" parent="MainVBoxContainer/ScrollContainer"] 134 | custom_minimum_size = Vector2(2.08165e-12, 100) 135 | layout_mode = 2 136 | size_flags_horizontal = 3 137 | size_flags_vertical = 3 138 | theme_override_font_sizes/normal_font_size = 22 139 | fit_content = true 140 | scroll_following = true 141 | script = ExtResource("5_4kmm3") 142 | 143 | [connection signal="append_to_info_text" from="." to="MainVBoxContainer/ScrollContainer/InfoRichTextLabel" method="_on_new_project_append_to_info_text"] 144 | [connection signal="clear_info_text" from="." to="MainVBoxContainer/ScrollContainer/InfoRichTextLabel" method="_on_new_project_clear_info_text"] 145 | [connection signal="enable_go_button" from="." to="MainVBoxContainer/GridContainer/GoButton" method="_on_new_project_enable_go_button"] 146 | [connection signal="reset_compare_line_edit" from="." to="MainVBoxContainer/GridContainer/CompareLineEdit" method="_on_new_project_reset_compare_line_edit"] 147 | [connection signal="set_status_text" from="." to="MainVBoxContainer/StatusGenomeContainer/StatusLabel" method="_on_new_project_set_status_text"] 148 | [connection signal="update_bottom_genome_filename" from="." to="MainVBoxContainer/GridContainer/BottomGenomeLineEdit" method="_on_new_project_update_bottom_genome_filename"] 149 | [connection signal="update_top_genome_filename" from="." to="MainVBoxContainer/GridContainer/TopGenomeLineEdit" method="_on_new_project_update_top_genome_filename"] 150 | [connection signal="pressed" from="MainVBoxContainer/HBoxContainer/CancelButton" to="." method="_on_cancel_button_pressed"] 151 | [connection signal="pressed" from="MainVBoxContainer/HBoxContainer/ResetButton" to="." method="_on_reset_button_pressed"] 152 | [connection signal="pressed" from="MainVBoxContainer/HBoxContainer/OpenFileManagerButton" to="." method="_on_open_file_manager_button_pressed"] 153 | [connection signal="pressed" from="MainVBoxContainer/HBoxContainer/UseTestDataButton" to="MainVBoxContainer/GridContainer/TopGenomeLineEdit" method="_on_use_test_data_button_pressed"] 154 | [connection signal="pressed" from="MainVBoxContainer/HBoxContainer/UseTestDataButton" to="MainVBoxContainer/GridContainer/BottomGenomeLineEdit" method="_on_use_test_data_button_pressed"] 155 | [connection signal="pressed" from="MainVBoxContainer/HBoxContainer/UseTestDataButton" to="MainVBoxContainer/GridContainer/GoButton" method="_on_use_test_data_button_pressed"] 156 | [connection signal="focus_exited" from="MainVBoxContainer/GridContainer/TopGenomeLineEdit" to="MainVBoxContainer/GridContainer/TopGenomeLineEdit" method="_on_focus_exited"] 157 | [connection signal="text_submitted" from="MainVBoxContainer/GridContainer/TopGenomeLineEdit" to="." method="_on_top_genome_line_edit_text_submitted"] 158 | [connection signal="focus_exited" from="MainVBoxContainer/GridContainer/BottomGenomeLineEdit" to="MainVBoxContainer/GridContainer/BottomGenomeLineEdit" method="_on_focus_exited"] 159 | [connection signal="text_submitted" from="MainVBoxContainer/GridContainer/BottomGenomeLineEdit" to="." method="_on_bottom_genome_line_edit_text_submitted"] 160 | [connection signal="focus_exited" from="MainVBoxContainer/GridContainer/CompareLineEdit" to="MainVBoxContainer/GridContainer/CompareLineEdit" method="_on_focus_exited"] 161 | [connection signal="text_submitted" from="MainVBoxContainer/GridContainer/CompareLineEdit" to="MainVBoxContainer/GridContainer/CompareLineEdit" method="_on_text_submitted"] 162 | [connection signal="pressed" from="MainVBoxContainer/GridContainer/GoButton" to="." method="_on_go_button_pressed"] 163 | -------------------------------------------------------------------------------- /scenes/init/init.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | signal add_to_text_label 4 | signal finished_downloading 5 | signal init_finished 6 | 7 | var http_request 8 | 9 | func _ready(): 10 | show() 11 | 12 | 13 | func download(url, outfile): 14 | add_to_text_label.emit("Downloading: " + url) 15 | http_request = HTTPRequest.new() 16 | add_child(http_request) 17 | http_request.request_completed.connect(self._http_request_completed) 18 | http_request.set_download_file(outfile) 19 | var result = http_request.request(url) 20 | 21 | if result == OK: 22 | add_to_text_label.emit("... downloaded ok") 23 | return true 24 | else: 25 | add_to_text_label.emit("... error downloading. Result: " + result) 26 | return false 27 | 28 | func _http_request_completed(result, _response_code, _headers, _body): 29 | if result != OK: 30 | print("Download Failed") 31 | remove_child(http_request) 32 | finished_downloading.emit() 33 | 34 | 35 | func download_zlhelper(): 36 | var url = "https://github.com/martinghunt/zlhelper/releases/download/" + \ 37 | Globals.expect_zlhelper_version + "/zlhelper_" 38 | if Globals.userdata.os == "mac": 39 | url += "darwin_" 40 | else: 41 | url += Globals.userdata.os + "_" 42 | 43 | if "arm" in Globals.userdata.arch: 44 | url += "arm64" 45 | else: 46 | url += "amd64" 47 | 48 | var ok = download(url, Globals.userdata.zlhelper) 49 | await finished_downloading 50 | return ok 51 | 52 | 53 | func chmod_make_executable(filename): 54 | var exit_code = OS.execute("chmod", ["+x", filename]) 55 | if exit_code == 0: 56 | add_to_text_label.emit("Made executable: " + filename) 57 | return true 58 | else: 59 | add_to_text_label.emit("Error making executable: " + filename) 60 | return false 61 | 62 | 63 | func size_of_blast_tarball(): 64 | var dir = DirAccess.open(Globals.userdata.bin) 65 | for filename in dir.get_files(): 66 | if filename.begins_with("ncbi-blast") and filename.ends_with(".tar.gz"): 67 | var f = FileAccess.open(Globals.userdata.bin.path_join(filename), FileAccess.READ) 68 | var size_mb = int(float(f.get_length()) / (1024 * 1024)) 69 | print(filename, ", ", size_mb, "M") 70 | return size_mb 71 | return 0 72 | 73 | 74 | func run_all(): 75 | add_to_text_label.emit("Ziplign Version: " + ProjectSettings.get_setting("application/config/version")) 76 | add_to_text_label.emit("Running on " + Globals.userdata.os + "/" + Globals.userdata.arch) 77 | Globals.userdata.check_all_paths() 78 | 79 | if not Globals.userdata.data_dir_exists: 80 | OS.alert("No user data folder found. Cannot continue. Expected to find: " + Globals.userdata.data_dir, "ERROR") 81 | return false 82 | 83 | if Globals.userdata.config_file_exists: 84 | add_to_text_label.emit("Config file found. Loading it. " + Globals.userdata.config_file) 85 | Globals.userdata.load_config() 86 | else: 87 | add_to_text_label.emit("Config file not found. Making default file. " + Globals.userdata.config_file) 88 | Globals.userdata.make_default_config() 89 | 90 | add_to_text_label.emit("Data folder found: " + Globals.userdata.data_dir) 91 | await get_tree().create_timer(0.1).timeout 92 | 93 | if Globals.userdata.bin_exists: 94 | add_to_text_label.emit("Binaries folder found: " + Globals.userdata.bin) 95 | else: 96 | add_to_text_label.emit("Binaries folder not found: " + Globals.userdata.bin) 97 | add_to_text_label.emit(" ... creating: " + Globals.userdata.bin) 98 | await get_tree().create_timer(0.1).timeout 99 | var error = DirAccess.make_dir_recursive_absolute(Globals.userdata.bin) 100 | if error != OK: 101 | add_to_text_label.emit(" ... ERROR creating: " + Globals.userdata.bin) 102 | OS.alert("Error making folder for binaries.\nCannot continue.\nTried to make:\n" + Globals.userdata.bin, "ERROR") 103 | return false 104 | add_to_text_label.emit(" ... created: " + Globals.userdata.bin) 105 | Globals.userdata.bin_exists = true 106 | 107 | var zlhelper_err_msg = "" 108 | 109 | if Globals.userdata.zlhelper_exists: 110 | add_to_text_label.emit("zlhelper found: " + Globals.userdata.zlhelper) 111 | Globals.userdata.set_zlhelper_version() 112 | if not Globals.userdata.zlhelper_version_ok: 113 | zlhelper_err_msg = "zlhelper version " + Globals.userdata.zlhelper_version + \ 114 | " is different from expected: " + \ 115 | Globals.expect_zlhelper_version + \ 116 | ". Going to download zlhelper" 117 | else: 118 | zlhelper_err_msg = "zlhelper not found: " + Globals.userdata.zlhelper 119 | 120 | if len(zlhelper_err_msg) > 0: 121 | await get_tree().create_timer(0.1).timeout 122 | add_to_text_label.emit(zlhelper_err_msg) 123 | var ok = await download_zlhelper() 124 | if not ok: 125 | OS.alert("Error downloading zlhelper.\nCannot continue", "ERROR") 126 | return false 127 | 128 | if Globals.userdata.os != "windows": 129 | print("making zlhelper executable") 130 | ok = chmod_make_executable(Globals.userdata.zlhelper) 131 | if not ok: 132 | OS.alert("Error making zlhelper executable.\nCannot continue", "ERROR") 133 | return false 134 | 135 | Globals.userdata.zlhelper_exists = true 136 | await get_tree().create_timer(0.1).timeout 137 | add_to_text_label.emit("zlhelper downloaded: " + Globals.userdata.zlhelper) 138 | 139 | Globals.userdata.set_zlhelper_version() 140 | add_to_text_label.emit("zlhelper version: " + Globals.userdata.zlhelper_version) 141 | 142 | var blast_ok = true 143 | if Globals.userdata.makeblastdb_exists: 144 | add_to_text_label.emit("makeblastdb found: " + Globals.userdata.makeblastdb) 145 | else: 146 | blast_ok = false 147 | add_to_text_label.emit("makeblastdb not found: " + Globals.userdata.makeblastdb) 148 | 149 | if Globals.userdata.blastn_exists: 150 | add_to_text_label.emit("blastn found: " + Globals.userdata.blastn) 151 | Globals.userdata.set_blastn_version() 152 | blast_ok = Globals.userdata.blastn_version != "unknown" 153 | add_to_text_label.emit("blastn version: " + Globals.userdata.blastn_version) 154 | else: 155 | blast_ok = false 156 | add_to_text_label.emit("blastn not found: " + Globals.userdata.blastn) 157 | 158 | await get_tree().create_timer(0.1).timeout 159 | 160 | 161 | if not blast_ok: 162 | add_to_text_label.emit("Some blast programs not found, or version unknown. Downloading...") 163 | var opts = ["download_binaries", "--outdir", Globals.userdata.bin] 164 | var stderr = [] 165 | await get_tree().create_timer(0.1).timeout 166 | add_to_text_label.emit("Running: " + Globals.userdata.zlhelper + " " + " ".join(opts)) 167 | add_to_text_label.emit("[b]This may take some time, depending on internet bandwidth[/b]") 168 | add_to_text_label.emit("Size of downloaded file (is up to around 200-250M total size): ") 169 | await get_tree().create_timer(0.1).timeout 170 | var thread = Thread.new() 171 | thread.start(OS.execute.bind(Globals.userdata.zlhelper, opts, stderr, true)) 172 | while thread.is_alive(): 173 | await get_tree().create_timer(3).timeout 174 | add_to_text_label.emit(" " + str(size_of_blast_tarball()) + "M", false) 175 | var exit_code = thread.wait_to_finish() 176 | add_to_text_label.emit("... blast downloaded") 177 | await get_tree().create_timer(0.1).timeout 178 | 179 | for x in stderr: 180 | print(x + "\n") 181 | if exit_code != 0: 182 | print("Error downloading blast") 183 | add_to_text_label.emit("Error downloading blast") 184 | OS.alert("Error downloading blast.\nCannot continue", "ERROR") 185 | return false 186 | 187 | add_to_text_label.emit(" ... finished downloading blast") 188 | 189 | if Globals.userdata.example_data_exists: 190 | add_to_text_label.emit("Example genome files found in " + Globals.userdata.example_data_dir) 191 | else: 192 | add_to_text_label.emit("Example genome files not found. Going to generate them") 193 | var opts = ["make_example_data", "--outdir", Globals.userdata.example_data_dir] 194 | var stderr = [] 195 | var exit_code = OS.execute(Globals.userdata.zlhelper, opts, stderr, true) 196 | for x in stderr: 197 | print(x + "\n") 198 | if exit_code != 0: 199 | print("Error making example genome files") 200 | add_to_text_label.emit("Error making example genome files") 201 | OS.alert("Error making example genome files.\nCannot continue", "ERROR") 202 | return false 203 | 204 | 205 | 206 | add_to_text_label.emit("Applying settings from config file") 207 | Globals.theme.set_theme(Globals.userdata.config.get_value("colours", "theme")) 208 | add_to_text_label.emit("Initialization finished") 209 | return true 210 | 211 | func _on_main_start_init(): 212 | var screen_size = DisplayServer.screen_get_size() 213 | var window_size = DisplayServer.window_get_size() 214 | var width_to_height = 1.0 * window_size.x / window_size.y 215 | var wanted_width = int(0.75 * screen_size.x) 216 | var wanted_height = 1 + int(wanted_width / width_to_height) 217 | print("Detected screen size: ", screen_size.x, "x", screen_size.y) 218 | print("Resize Ziplign window to: ", wanted_width, "x", wanted_height) 219 | DisplayServer.window_set_size(Vector2i(wanted_width, wanted_height)) 220 | DisplayServer.window_set_position(Vector2((screen_size.x - wanted_width) / 2, (screen_size.y - wanted_height) / 2)) 221 | show() 222 | await run_all() 223 | await get_tree().create_timer(1).timeout 224 | hide() 225 | $"..".reset_colours() 226 | init_finished.emit() 227 | -------------------------------------------------------------------------------- /fonts/dejavu-sans/LICENSE: -------------------------------------------------------------------------------- 1 | Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. 2 | Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) 3 | 4 | 5 | Bitstream Vera Fonts Copyright 6 | ------------------------------ 7 | 8 | Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is 9 | a trademark of Bitstream, Inc. 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of the fonts accompanying this license ("Fonts") and associated 13 | documentation files (the "Font Software"), to reproduce and distribute the 14 | Font Software, including without limitation the rights to use, copy, merge, 15 | publish, distribute, and/or sell copies of the Font Software, and to permit 16 | persons to whom the Font Software is furnished to do so, subject to the 17 | following conditions: 18 | 19 | The above copyright and trademark notices and this permission notice shall 20 | be included in all copies of one or more of the Font Software typefaces. 21 | 22 | The Font Software may be modified, altered, or added to, and in particular 23 | the designs of glyphs or characters in the Fonts may be modified and 24 | additional glyphs or characters may be added to the Fonts, only if the fonts 25 | are renamed to names not containing either the words "Bitstream" or the word 26 | "Vera". 27 | 28 | This License becomes null and void to the extent applicable to Fonts or Font 29 | Software that has been modified and is distributed under the "Bitstream 30 | Vera" names. 31 | 32 | The Font Software may be sold as part of a larger software package but no 33 | copy of one or more of the Font Software typefaces may be sold by itself. 34 | 35 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 36 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, 37 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, 38 | TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME 39 | FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING 40 | ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 41 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 42 | THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE 43 | FONT SOFTWARE. 44 | 45 | Except as contained in this notice, the names of Gnome, the Gnome 46 | Foundation, and Bitstream Inc., shall not be used in advertising or 47 | otherwise to promote the sale, use or other dealings in this Font Software 48 | without prior written authorization from the Gnome Foundation or Bitstream 49 | Inc., respectively. For further information, contact: fonts at gnome dot 50 | org. 51 | 52 | Arev Fonts Copyright 53 | ------------------------------ 54 | 55 | Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. 56 | 57 | Permission is hereby granted, free of charge, to any person obtaining 58 | a copy of the fonts accompanying this license ("Fonts") and 59 | associated documentation files (the "Font Software"), to reproduce 60 | and distribute the modifications to the Bitstream Vera Font Software, 61 | including without limitation the rights to use, copy, merge, publish, 62 | distribute, and/or sell copies of the Font Software, and to permit 63 | persons to whom the Font Software is furnished to do so, subject to 64 | the following conditions: 65 | 66 | The above copyright and trademark notices and this permission notice 67 | shall be included in all copies of one or more of the Font Software 68 | typefaces. 69 | 70 | The Font Software may be modified, altered, or added to, and in 71 | particular the designs of glyphs or characters in the Fonts may be 72 | modified and additional glyphs or characters may be added to the 73 | Fonts, only if the fonts are renamed to names not containing either 74 | the words "Tavmjong Bah" or the word "Arev". 75 | 76 | This License becomes null and void to the extent applicable to Fonts 77 | or Font Software that has been modified and is distributed under the 78 | "Tavmjong Bah Arev" names. 79 | 80 | The Font Software may be sold as part of a larger software package but 81 | no copy of one or more of the Font Software typefaces may be sold by 82 | itself. 83 | 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL 88 | TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | 94 | Except as contained in this notice, the name of Tavmjong Bah shall not 95 | be used in advertising or otherwise to promote the sale, use or other 96 | dealings in this Font Software without prior written authorization 97 | from Tavmjong Bah. For further information, contact: tavmjong @ free 98 | . fr. 99 | 100 | TeX Gyre DJV Math 101 | ----------------- 102 | Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. 103 | 104 | Math extensions done by B. Jackowski, P. Strzelczyk and P. Pianowski 105 | (on behalf of TeX users groups) are in public domain. 106 | 107 | Letters imported from Euler Fraktur from AMSfonts are (c) American 108 | Mathematical Society (see below). 109 | Bitstream Vera Fonts Copyright 110 | Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera 111 | is a trademark of Bitstream, Inc. 112 | 113 | Permission is hereby granted, free of charge, to any person obtaining a copy 114 | of the fonts accompanying this license (“Fonts”) and associated 115 | documentation 116 | files (the “Font Software”), to reproduce and distribute the Font Software, 117 | including without limitation the rights to use, copy, merge, publish, 118 | distribute, 119 | and/or sell copies of the Font Software, and to permit persons to whom 120 | the Font Software is furnished to do so, subject to the following 121 | conditions: 122 | 123 | The above copyright and trademark notices and this permission notice 124 | shall be 125 | included in all copies of one or more of the Font Software typefaces. 126 | 127 | The Font Software may be modified, altered, or added to, and in particular 128 | the designs of glyphs or characters in the Fonts may be modified and 129 | additional 130 | glyphs or characters may be added to the Fonts, only if the fonts are 131 | renamed 132 | to names not containing either the words “Bitstream” or the word “Vera”. 133 | 134 | This License becomes null and void to the extent applicable to Fonts or 135 | Font Software 136 | that has been modified and is distributed under the “Bitstream Vera” 137 | names. 138 | 139 | The Font Software may be sold as part of a larger software package but 140 | no copy 141 | of one or more of the Font Software typefaces may be sold by itself. 142 | 143 | THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS 144 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, 145 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, 146 | TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME 147 | FOUNDATION 148 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, 149 | SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN 150 | ACTION 151 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR 152 | INABILITY TO USE 153 | THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. 154 | Except as contained in this notice, the names of GNOME, the GNOME 155 | Foundation, 156 | and Bitstream Inc., shall not be used in advertising or otherwise to promote 157 | the sale, use or other dealings in this Font Software without prior written 158 | authorization from the GNOME Foundation or Bitstream Inc., respectively. 159 | For further information, contact: fonts at gnome dot org. 160 | 161 | AMSFonts (v. 2.2) copyright 162 | 163 | The PostScript Type 1 implementation of the AMSFonts produced by and 164 | previously distributed by Blue Sky Research and Y&Y, Inc. are now freely 165 | available for general use. This has been accomplished through the 166 | cooperation 167 | of a consortium of scientific publishers with Blue Sky Research and Y&Y. 168 | Members of this consortium include: 169 | 170 | Elsevier Science IBM Corporation Society for Industrial and Applied 171 | Mathematics (SIAM) Springer-Verlag American Mathematical Society (AMS) 172 | 173 | In order to assure the authenticity of these fonts, copyright will be 174 | held by 175 | the American Mathematical Society. This is not meant to restrict in any way 176 | the legitimate use of the fonts, such as (but not limited to) electronic 177 | distribution of documents containing these fonts, inclusion of these fonts 178 | into other public domain or commercial font collections or computer 179 | applications, use of the outline data to create derivative fonts and/or 180 | faces, etc. However, the AMS does require that the AMS copyright notice be 181 | removed from any derivative versions of the fonts which have been altered in 182 | any way. In addition, to ensure the fidelity of TeX documents using Computer 183 | Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces, 184 | has requested that any alterations which yield different font metrics be 185 | given a different name. 186 | 187 | $Id$ 188 | -------------------------------------------------------------------------------- /main.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | signal start_init 4 | 5 | func set_tooltip_theme(node): 6 | node.theme = Theme.new() 7 | node.theme.set_stylebox("panel", "TooltipPanel", Globals.tooltip_style) 8 | node.theme.set_color("font_color", "TooltipLabel", Globals.theme.colours["text"]) 9 | node.theme.set_font("font", "TooltipLabel", Globals.fonts["dejavu"]) 10 | 11 | 12 | func set_font_color(node): 13 | node.add_theme_color_override("font_color", Globals.theme.colours["ui"]["text"]) 14 | node.add_theme_color_override("font_hover_color", Globals.theme.colours["ui"]["text_hover"]) 15 | node.add_theme_color_override("font_focus_color", Globals.theme.colours["ui"]["text"]) 16 | 17 | 18 | func set_children_font(node): 19 | for x in node.get_children(): 20 | set_font_color(x) 21 | 22 | 23 | func set_caret_theme(node): 24 | node.add_theme_color_override("caret_color", Globals.theme.colours["ui"]["text"]) 25 | node.add_theme_constant_override("caret_width", 3) 26 | node.set_caret_blink_enabled(true) 27 | 28 | func set_filedialog_colors(dialog): 29 | dialog.get_theme_stylebox("panel").bg_color = Globals.theme.colours["ui"]["general_bg"] 30 | set_children_font(dialog.get_vbox()) 31 | set_font_color(dialog.get_cancel_button()) 32 | set_font_color(dialog.get_ok_button()) 33 | set_font_color(dialog.get_label()) 34 | set_font_color(dialog.get_line_edit()) 35 | dialog.add_theme_color_override("file_icon_color", Globals.theme.colours["ui"]["text"]) 36 | dialog.add_theme_color_override("folder_icon_color", Globals.theme.colours["ui"]["text"]) 37 | var children = dialog.get_vbox().get_children() 38 | var top_hbox_children = children[0].get_children() 39 | 40 | # the buttons that are just icons. Make them all text colour 41 | for i in [0, 1, 2, 6, 7]: 42 | top_hbox_children[i].modulate = Globals.theme.colours["ui"]["text"] 43 | 44 | for x in dialog.get_vbox().get_children(): 45 | set_children_font(x) 46 | 47 | var tree = children[2].get_children()[0] 48 | tree.add_theme_color_override("guide_color", Globals.theme.colours["ui"]["text"]) 49 | tree.get_theme_stylebox("panel").bg_color = Globals.theme.colours["ui"]["button_bg"] 50 | 51 | 52 | func reset_colours(): 53 | # Get an arbitrary button. Changing the styleboxes for it affects all buttons 54 | var b = $MainMenu/MainContainer/MainVBoxContainer/ResumeButton 55 | b.get_theme_stylebox("normal").bg_color = Globals.theme.colours["ui"]["button_bg"] 56 | b.get_theme_stylebox("hover").bg_color = Globals.theme.colours["ui"]["button_highlight"] 57 | 58 | set_filedialog_colors($LoadProject/LoadDialog) 59 | set_filedialog_colors($SaveProject/SaveDialog) 60 | $BoxContainer/ColorRect.color = Globals.theme.colours["ui"]["general_bg"] 61 | $Game/ColorRect.color = Globals.theme.colours["ui"]["general_bg"] 62 | $Game/ColorRect2.color = Globals.theme.colours["ui"]["panel_bg"] 63 | $Game/MainHBoxContainer/BoxContainer/ColorRect.color = Globals.theme.colours["ui"]["general_bg"] 64 | $Game/MainHBoxContainer/BoxContainer/VBoxContainer2/ColorRect.color = Globals.theme.colours["ui"]["general_bg"] 65 | 66 | var ui_text_to_set = [ 67 | $MainMenu/StatusRichTextLabel, 68 | $Init/StatusRichTextLabel, 69 | $Game/MainHBoxContainer/BoxContainer/VBoxContainer2/HBoxContainer/TopCoordsText, 70 | $NewProject/MainVBoxContainer/ScrollContainer/InfoRichTextLabel, 71 | $Game/ColorRect/ProcessingLabel, 72 | ] 73 | for x in ui_text_to_set: 74 | x.add_theme_color_override("default_color", Globals.theme.colours["ui"]["text"]) 75 | 76 | $Game/ColorRect/ProcessingLabel.add_theme_color_override("font_color", Globals.theme.colours["ui"]["text"]) 77 | 78 | var children_to_set = [ 79 | $MainMenu/MainContainer/MainVBoxContainer, 80 | $NewProject/MainVBoxContainer/HBoxContainer, 81 | $NewProject/MainVBoxContainer/GridContainer, 82 | $NewProject/MainVBoxContainer/StatusGenomeContainer, 83 | $Settings/VBoxContainer, 84 | $Settings/VBoxContainer/TopHBoxContainer, 85 | $Settings/VBoxContainer/GridContainer, 86 | $Game/MainHBoxContainer/BoxContainer/VBoxContainer2/HBoxContainer, 87 | $Game/MainHBoxContainer/VBoxContainer, 88 | $Game/MainHBoxContainer/VBoxContainer/TopGridContainer, 89 | $Game/MainHBoxContainer/VBoxContainer/NavigationGridContainer, 90 | $Game/MainHBoxContainer/VBoxContainer/ZoomGridContainer, 91 | $Game/MainHBoxContainer/VBoxContainer/UtilsChoiceHBoxContainer, 92 | $Game/MainHBoxContainer/VBoxContainer/SearchVBoxContainer, 93 | $Game/MainHBoxContainer/VBoxContainer/SearchVBoxContainer/HBoxContainer, 94 | $Game/MainHBoxContainer/VBoxContainer/FilterVBoxContainer, 95 | $Game/MainHBoxContainer/VBoxContainer/FilterVBoxContainer/HBoxContainer, 96 | $Game/MainHBoxContainer/VBoxContainer/FilterVBoxContainer/MinLengthBoxContainer, 97 | $Game/MainHBoxContainer/VBoxContainer/FilterVBoxContainer/MaxLengthBoxContainer, 98 | $Game/MainHBoxContainer/VBoxContainer/FilterVBoxContainer/MinPercentBoxContainer, 99 | $Game/MainHBoxContainer/VBoxContainer/RevcompGridContainer, 100 | $Game/MainHBoxContainer/VBoxContainer/MultMatchesVBoxContainer/HBoxContainer, 101 | $Game/MainHBoxContainer/VBoxContainer/ContigOptsVBoxContainer/HBoxContainer, 102 | $Game/MainHBoxContainer/VBoxContainer/ContigOptsVBoxContainer/HBoxContainer2, 103 | $Game/MainHBoxContainer/VBoxContainer/ContigOptsVBoxContainer/HBoxContainer3, 104 | ] 105 | for x in children_to_set: 106 | set_children_font(x) 107 | 108 | # Change stylebox for one scrollbar, so all others also change 109 | var sbar = $Game/MainHBoxContainer/BoxContainer/VBoxContainer2/RightTopScrollbar 110 | sbar.get_theme_stylebox("scroll").bg_color = Globals.theme.colours["ui"]["button_bg"] 111 | sbar.get_theme_stylebox("grabber").bg_color = Globals.theme.colours["ui"]["text"] 112 | sbar.get_theme_stylebox("grabber_highlight").bg_color = Globals.theme.colours["ui"]["button_highlight"] 113 | sbar.get_theme_stylebox("grabber_pressed").bg_color = Globals.theme.colours["ui"]["button_pressed"] 114 | 115 | var to_set_tooltip = [ 116 | $Game/MainHBoxContainer/BoxContainer/VBoxContainer2/RightTopScrollbar, 117 | $Game/MainHBoxContainer/BoxContainer/VBoxContainer2/RightBottomScrollbar 118 | ] 119 | to_set_tooltip.append_array($Game/MainHBoxContainer/VBoxContainer/TopGridContainer.get_children()) 120 | to_set_tooltip.append_array($Game/MainHBoxContainer/VBoxContainer/NavigationGridContainer.get_children()) 121 | to_set_tooltip.append_array($Game/MainHBoxContainer/VBoxContainer/ZoomGridContainer.get_children()) 122 | to_set_tooltip.append_array($Game/MainHBoxContainer/VBoxContainer/RevcompGridContainer.get_children()) 123 | to_set_tooltip.append_array($Game/MainHBoxContainer/VBoxContainer/UtilsChoiceHBoxContainer.get_children()) 124 | to_set_tooltip.append_array($Game/MainHBoxContainer/VBoxContainer/FilterVBoxContainer.get_children()) 125 | to_set_tooltip.append_array($Game/MainHBoxContainer/VBoxContainer/SearchVBoxContainer.get_children()) 126 | to_set_tooltip.append_array($MainMenu/MainContainer/MainVBoxContainer.get_children()) 127 | to_set_tooltip.append_array($Game/MainHBoxContainer/VBoxContainer/ContigOptsVBoxContainer.get_children()) 128 | to_set_tooltip.append_array($NewProject/MainVBoxContainer/HBoxContainer.get_children()) 129 | for x in to_set_tooltip: 130 | set_tooltip_theme(x) 131 | 132 | var optb = $Settings/VBoxContainer/GridContainer/ThemeOptionButton 133 | optb.get_theme_stylebox("normal").bg_color = Globals.theme.colours["ui"]["button_bg"] 134 | optb.get_theme_stylebox("hover").bg_color = Globals.theme.colours["ui"]["button_highlight"] 135 | set_font_color(optb) 136 | var popup = optb.get_popup() 137 | popup.get_theme_stylebox("panel").bg_color = Globals.theme.colours["ui"]["button_bg"] 138 | popup.get_theme_stylebox("hover").bg_color = Globals.theme.colours["ui"]["button_highlight"] 139 | set_font_color(popup) 140 | popup.add_theme_font_size_override("font_size", 30) 141 | 142 | 143 | var ledit = $Game/MainHBoxContainer/VBoxContainer/FilterVBoxContainer/MinLengthBoxContainer/FiltMinLengthLineEdit 144 | ledit.get_theme_stylebox("normal").bg_color = Globals.theme.colours["ui"]["button_bg"] 145 | Globals.tooltip_style.bg_color = Globals.theme.colours["ui"]["panel_bg"] 146 | Globals.tooltip_style.border_color = Globals.theme.colours["text"] 147 | 148 | var to_set_caret = [ 149 | $NewProject/MainVBoxContainer/GridContainer/TopGenomeLineEdit, 150 | $NewProject/MainVBoxContainer/GridContainer/BottomGenomeLineEdit, 151 | $NewProject/MainVBoxContainer/GridContainer/CompareLineEdit, 152 | $Game/MainHBoxContainer/VBoxContainer/FilterVBoxContainer/MinLengthBoxContainer/FiltMinLengthLineEdit, 153 | $Game/MainHBoxContainer/VBoxContainer/FilterVBoxContainer/MinPercentBoxContainer/FiltMinIdentityLineEdit, 154 | $Game/MainHBoxContainer/VBoxContainer/FilterVBoxContainer/MaxLengthBoxContainer/FiltMaxLengthLineEdit, 155 | $Game/MainHBoxContainer/VBoxContainer/SearchVBoxContainer/SequenceLineEdit, 156 | $Game/MainHBoxContainer/VBoxContainer/SearchVBoxContainer/AnnotationLineEdit, 157 | ] 158 | for node in to_set_caret: 159 | set_caret_theme(node) 160 | 161 | var to_set_text_boxes = [ 162 | $MainMenu/StatusRichTextLabel, 163 | $Game/MainHBoxContainer/BoxContainer/VBoxContainer2/HBoxContainer/TopCoordsText, 164 | ] 165 | for node in to_set_caret + to_set_text_boxes: 166 | node.add_theme_color_override("selection_color", Globals.theme.colours["ui"]["text"]) 167 | node.add_theme_color_override("font_selected_color", Globals.theme.colours["ui"]["panel_bg"]) 168 | 169 | for node in to_set_text_boxes: 170 | node.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) 171 | 172 | var l = $Game/MainHBoxContainer/VBoxContainer/MultMatchesVBoxContainer/MultiMatchesScrollContainer/MultMatchesItemList 173 | l.add_theme_color_override("font_color", Globals.theme.colours["ui"]["text"]) 174 | l.add_theme_color_override("font_selected_color", Globals.theme.colours["ui"]["text"]) 175 | l.set_colors() 176 | 177 | func _ready(): 178 | reset_colours() 179 | $NewProject/MainVBoxContainer/ScrollContainer/InfoRichTextLabel.add_theme_font_override("normal_font", Globals.fonts["mono"]) 180 | $MainMenu/StatusRichTextLabel.add_theme_font_override("normal_font", Globals.fonts["mono"]) 181 | $Init/StatusRichTextLabel.add_theme_font_override("normal_font", Globals.fonts["mono"]) 182 | $Init/StatusRichTextLabel.add_theme_font_override("bold_font", Globals.fonts["mono_bold"]) 183 | start_init.emit() 184 | 185 | func _on_settings_theme_updated(): 186 | reset_colours() 187 | 188 | -------------------------------------------------------------------------------- /lib/genomes_n_matches/contig.gd: -------------------------------------------------------------------------------- 1 | extends StaticBody2D 2 | 3 | class_name Contig 4 | 5 | signal mouse_in 6 | signal mouse_out 7 | signal annot_deselected 8 | signal annot_selected 9 | 10 | const AnnotFeatureClass = preload("res://lib/annot_feature.gd") 11 | var fastaq_lib = preload("res://lib/fastaq.gd").new() 12 | 13 | var static_body_2d = StaticBody2D.new() 14 | var coll_poly = CollisionPolygon2D.new() 15 | var poly = Polygon2D.new() 16 | var centerline = Line2D.new() 17 | var leftvline = Line2D.new() 18 | var rightvline = Line2D.new() 19 | var top_or_bottom 20 | var vline_width = 2 21 | var centerline_width = 1 22 | var centerline_width_zoomed = 1 23 | var top = 5 24 | var bottom = 30 25 | var gene_fwd_top = 9 26 | var gene_fwd_bottom = 17 27 | var gene_rev_top = 21 28 | var gene_rev_bottom = 28 29 | var gap_top = gene_fwd_bottom + 1 30 | var gap_bottom = gene_rev_top - 1 31 | var middle = 0.5 * (top + bottom) 32 | var centerline_y = middle 33 | var id 34 | var selected = false 35 | var hovering = false 36 | var selected_annot = -1 37 | var annot_hovering = [] 38 | var x_offset = 0 39 | var x_start 40 | var x_end 41 | var length_in_bp 42 | var fill_color 43 | var annot_polys = [] 44 | var gff_features = [] 45 | 46 | 47 | func set_gene_top_bottom(zoomed: bool): 48 | if zoomed: 49 | gene_fwd_top = top + 5 50 | gene_fwd_bottom = top + 0.6 * (middle - top) 51 | gene_rev_top = middle + 0.4 * (bottom - middle) 52 | gene_rev_bottom = bottom - 5 53 | #gap_top = gene_fwd_top 54 | #gap_bottom = gene_rev_bottom 55 | else: 56 | gene_fwd_top = top + 0.15 * (middle - top) 57 | gene_fwd_bottom = top + 0.85 * (middle - top) 58 | gene_rev_top = middle + 0.15 * (bottom - middle) 59 | gene_rev_bottom = middle + 0.85 * (bottom - middle) 60 | gap_top = gene_fwd_bottom + 1 61 | gap_bottom = gene_rev_top - 1 62 | 63 | 64 | func _init(new_id, new_top_or_bottom, new_x_start, new_x_end, new_top, new_bottom, bp_length, annotation): 65 | id = new_id 66 | top_or_bottom = new_top_or_bottom 67 | top = new_top 68 | bottom = new_bottom 69 | middle = 0.5 * (top + bottom) 70 | set_gene_top_bottom(false) 71 | centerline_y = middle 72 | static_body_2d.set_pickable(true) 73 | if id % 2 == 0: 74 | fill_color = Globals.theme.colours["contig"]["fill"] 75 | else: 76 | fill_color = Globals.theme.colours["contig"]["fill_alt"] 77 | poly.color = fill_color 78 | centerline.default_color = Globals.theme.colours["contig"]["edge"] 79 | centerline.add_point(Vector2(0, centerline_y)) 80 | centerline.add_point(Vector2(0, centerline_y)) 81 | centerline.width = centerline_width 82 | leftvline.default_color = Globals.theme.colours["contig"]["edge"] 83 | leftvline.add_point(Vector2(0, 0)) 84 | leftvline.add_point(Vector2(0, 0)) 85 | leftvline.width = vline_width 86 | rightvline.default_color = Globals.theme.colours["contig"]["edge"] 87 | rightvline.add_point(Vector2(0, 0)) 88 | rightvline.add_point(Vector2(0, 0)) 89 | rightvline.width = vline_width 90 | set_start_end(new_x_start, new_x_end) 91 | add_child(static_body_2d) 92 | static_body_2d.add_child(coll_poly) 93 | coll_poly.add_child(poly) 94 | coll_poly.add_child(centerline) 95 | coll_poly.add_child(leftvline) 96 | coll_poly.add_child(rightvline) 97 | length_in_bp = bp_length 98 | static_body_2d.mouse_entered.connect(_on_mouse_entered) 99 | static_body_2d.mouse_exited.connect(_on_mouse_exited) 100 | gff_features = annotation 101 | for feature in gff_features: 102 | if feature[0] == 0 and feature[1] == length_in_bp - 1 and (feature[1] - feature[0]) > 10000: 103 | continue 104 | var f_top = gene_fwd_top 105 | var f_bot = gene_fwd_bottom 106 | if feature[2] == "gap": 107 | f_top = gap_top 108 | f_bot = gap_bottom 109 | elif feature[3]: # is reverse 110 | f_top = gene_rev_top 111 | f_bot = gene_rev_bottom 112 | annot_polys.append(AnnotFeatureClass.new(len(annot_polys), feature, f_top, f_bot, self)) 113 | add_child(annot_polys[-1]) 114 | 115 | # Sort in coord order, but if start positions the same, put the longest 116 | # one first. Then set z indexes in the for loop afterwards, so that we 117 | # keep increasing the z index when a feature overlaps with the previous 118 | # rightmost feature. This should mean that no features get covered up. 119 | # There's probably some edge cases, but will work 99%(?) of the time 120 | annot_polys.sort_custom(func(a, b): return (a.gff_data[0] < b.gff_data[0]) or (a.gff_data[0] == b.gff_data[0] and a.gff_data[1] > b.gff_data[1])) 121 | var current_z = 1 122 | var last_end = 0 123 | var max_z = 500 # shouldn't get this many geatures overlapping! 124 | for i in range(0, len(annot_polys)): 125 | annot_polys[i].id = i 126 | annot_polys[i].connect("mouse_in", on_mouse_in_annot) 127 | annot_polys[i].connect("mouse_out", on_mouse_out_annot) 128 | if annot_polys[i].gff_data[0] < last_end: 129 | current_z = min(current_z + 1, max_z) 130 | else: 131 | current_z = 1 132 | annot_polys[i].set_default_z(current_z) 133 | last_end = max(last_end, annot_polys[i].gff_data[1]) 134 | 135 | 136 | func name(): 137 | return Globals.proj_data.genome_seqs[top_or_bottom]["contigs"][id]["name"] 138 | 139 | func shift(x_move): 140 | x_offset += x_move 141 | for i in range(3): 142 | poly.polygon[i][0] += x_move 143 | coll_poly.polygon[i][0] += x_move 144 | 145 | 146 | func set_polygons_coords(): 147 | poly.polygon = [ 148 | Vector2(x_start, top), 149 | Vector2(x_end, top), 150 | Vector2(x_end, bottom), 151 | Vector2(x_start, bottom), 152 | ] 153 | coll_poly.polygon = poly.polygon 154 | centerline.set_point_position(0, Vector2(x_start, centerline_y)) 155 | centerline.set_point_position(1, Vector2(x_end, centerline_y)) 156 | leftvline.set_point_position(0, Vector2(x_start, top)) 157 | leftvline.set_point_position(1, Vector2(x_start, bottom)) 158 | rightvline.set_point_position(0, Vector2(x_end, top)) 159 | rightvline.set_point_position(1, Vector2(x_end, bottom)) 160 | for x in annot_polys: 161 | x.set_polygon_coords() 162 | 163 | 164 | 165 | func set_zoomed_view(turn_on): 166 | if turn_on: 167 | centerline.width = centerline_width_zoomed 168 | if top_or_bottom == Globals.TOP: 169 | centerline_y = top 170 | else: 171 | centerline_y = bottom 172 | leftvline.hide() 173 | rightvline.hide() 174 | poly.hide() 175 | else: 176 | centerline.width = centerline_width 177 | centerline_y = middle 178 | leftvline.show() 179 | rightvline.show() 180 | poly.show() 181 | 182 | centerline.set_point_position(0, Vector2(x_start, centerline_y)) 183 | centerline.set_point_position(1, Vector2(x_end, centerline_y)) 184 | 185 | set_gene_top_bottom(turn_on) 186 | for x in annot_polys: 187 | if x.is_gap(): 188 | x.set_top_and_bottom(gap_top, gap_bottom) 189 | elif x.is_rev(): 190 | x.set_top_and_bottom(gene_rev_top, gene_rev_bottom) 191 | else: 192 | x.set_top_and_bottom(gene_fwd_top, gene_fwd_bottom) 193 | 194 | 195 | func find_first_annot_before_pos(pos): 196 | if len(annot_polys) == 0 \ 197 | or annot_polys[0].poly.polygon[0].x > pos: 198 | return -1 199 | if annot_polys[-1].poly.polygon[0].x < pos: 200 | return len(annot_polys) - 1 201 | var i = 0 202 | var j = len(annot_polys) - 1 203 | 204 | while i < j: 205 | var k = ceili(0.5 *(i + j)) 206 | if annot_polys[k].poly.polygon[0].x > pos: 207 | if k > 0 and annot_polys[k-1].poly.polygon[0].x <= pos: 208 | return k - 1 209 | j = k 210 | elif annot_polys[k].poly.polygon[0].x == pos: 211 | return k 212 | else: 213 | i = k 214 | 215 | return -1 216 | 217 | 218 | func set_start_end(new_start, new_end): 219 | x_start = new_start 220 | x_end = new_end 221 | set_polygons_coords() 222 | 223 | func select(): 224 | selected = true 225 | poly.color = Globals.theme.colours["contig"]["fill_selected"] 226 | centerline.default_color = Globals.theme.colours["contig"]["edge_selected"] 227 | leftvline.default_color = Globals.theme.colours["contig"]["edge_selected"] 228 | rightvline.default_color = Globals.theme.colours["contig"]["edge_selected"] 229 | 230 | func deselect(): 231 | selected = false 232 | if hovering: 233 | poly.color = Globals.theme.colours["contig"]["fill_hover"] 234 | centerline.default_color = Globals.theme.colours["contig"]["edge_hover"] 235 | leftvline.default_color = Globals.theme.colours["contig"]["edge_hover"] 236 | rightvline.default_color = Globals.theme.colours["contig"]["edge_hover"] 237 | else: 238 | poly.color = fill_color 239 | centerline.default_color = Globals.theme.colours["contig"]["edge"] 240 | leftvline.default_color = Globals.theme.colours["contig"]["edge"] 241 | rightvline.default_color = Globals.theme.colours["contig"]["edge"] 242 | 243 | 244 | func update_annot_visibility_in_range(start, end, x_zoom): 245 | if len(annot_polys) == 0 \ 246 | or start > end: 247 | return 248 | 249 | var i = find_first_annot_before_pos(end) 250 | while i >= 0 and annot_polys[i].poly.polygon[1].x > start - 500: 251 | annot_polys[i].set_visibility(x_zoom) 252 | i -= 1 253 | 254 | 255 | func set_annot_visibility(zoom): 256 | for x in annot_polys: 257 | x.set_visibility(zoom) 258 | 259 | 260 | func _on_mouse_entered(): 261 | hovering = true 262 | if selected: 263 | pass 264 | #poly.color = Globals.theme.colours["contig"]["fill_hover"] 265 | else: 266 | centerline.default_color = Globals.theme.colours["contig"]["edge_hover"] 267 | poly.color = Globals.theme.colours["contig"]["fill_hover"] 268 | 269 | mouse_in.emit(id) 270 | 271 | 272 | func _on_mouse_exited(): 273 | hovering = false 274 | if selected: 275 | poly.color = Globals.theme.colours["contig"]["fill_selected"] 276 | centerline.default_color = Globals.theme.colours["contig"]["edge_selected"] 277 | else: 278 | poly.color = fill_color 279 | centerline.default_color = Globals.theme.colours["contig"]["edge"] 280 | mouse_out.emit(id) 281 | 282 | 283 | func deselect_annot(): 284 | if selected_annot != -1: 285 | annot_polys[selected_annot].deselect() 286 | annot_deselected.emit(id, selected_annot) 287 | selected_annot = -1 288 | 289 | 290 | func select_annot(annot_id): 291 | annot_polys[annot_id].select() 292 | selected_annot = annot_id 293 | annot_selected.emit(id, annot_id) 294 | 295 | 296 | func on_mouse_in_annot(annot_id): 297 | annot_hovering.append(annot_id) 298 | 299 | 300 | func on_mouse_out_annot(annot_id): 301 | annot_hovering.erase(annot_id) 302 | 303 | 304 | func annotation_search(search_term): 305 | var matches = [] 306 | for annot in annot_polys: 307 | if annot.metadata_has_search_string(search_term): 308 | matches.append([annot.id, annot.name_label.text]) 309 | return matches 310 | 311 | 312 | func sequence_search(search_term): 313 | return fastaq_lib.search_for_seq(search_term, Globals.proj_data.genome_seqs[top_or_bottom]["contigs"][id]["seq"]) 314 | 315 | 316 | 317 | 318 | func _unhandled_input(event): 319 | if Globals.paused: 320 | return 321 | 322 | if event is InputEventMouseButton: 323 | if event.button_index == MOUSE_BUTTON_LEFT: 324 | if event.pressed: 325 | if len(annot_hovering) >= 1: 326 | var annot_id = annot_hovering[-1] 327 | deselect_annot() 328 | select_annot(annot_id) 329 | elif len(annot_hovering) == 0 and selected_annot != -1: 330 | deselect_annot() 331 | elif event.button_index == MOUSE_BUTTON_RIGHT: 332 | if event.pressed: 333 | if len(annot_hovering) > 0 and selected_annot == annot_hovering[-1]: 334 | deselect_annot() 335 | -------------------------------------------------------------------------------- /lib/project_data.gd: -------------------------------------------------------------------------------- 1 | class_name ProjectData 2 | 3 | signal project_data_loaded 4 | 5 | var fastaq_lib = preload("fastaq.gd").new() 6 | var blast_lib = preload("blast.gd").new() 7 | var gff_lib = preload("gff.gd").new() 8 | 9 | var root_dir = "" 10 | var genome_top_file_prefix = "" 11 | var genome_bottom_file_prefix = "" 12 | var genome_top_fa_file = "" 13 | var genome_bottom_fa_file = "" 14 | var genome_top_annot_file = "" 15 | var genome_bottom_annot_file = "" 16 | var blast_file = "" 17 | var blast_db = "" 18 | var genome_seqs = {Globals.TOP: {}, Globals.BOTTOM: {}} 19 | var blast_hits = [] 20 | var annotation = {Globals.TOP: {}, Globals.BOTTOM: {}} 21 | var data_loaded = false 22 | var blast_program = "blastn" 23 | 24 | 25 | func _init(): 26 | # stick in some dummy data the user will never see, just to stop scene 27 | # initialization crashing everything. We don't need blast matches, just 28 | # the genomes 29 | genome_seqs = { 30 | Globals.TOP: {"order": [0], "contigs": [{"name": "1", "descr": "foo", "seq": "ACGTA"}]}, 31 | Globals.BOTTOM: {"order": [0], "contigs": [{"name": "2", "descr": "bar", "seq": "ACGTAT"}]}, 32 | } 33 | data_loaded = false 34 | 35 | 36 | func create(dir_path): 37 | root_dir = dir_path 38 | set_paths() 39 | delete_all_files() 40 | DirAccess.make_dir_absolute(dir_path) 41 | 42 | 43 | func delete_all_files(): 44 | if not DirAccess.dir_exists_absolute(root_dir): 45 | return 46 | 47 | var files = DirAccess.get_files_at(root_dir) 48 | for f in files: 49 | print("delete: ", root_dir.path_join(f)) 50 | DirAccess.remove_absolute(root_dir.path_join(f)) 51 | 52 | 53 | func init_from_dir(dir_path): 54 | root_dir = dir_path 55 | if DirAccess.dir_exists_absolute(dir_path): 56 | set_paths() 57 | load_genomes() 58 | load_blast_hits() 59 | load_annotation_files() 60 | elif FileAccess.file_exists(dir_path): 61 | load_from_serialized_file(dir_path) 62 | set_data_loaded() 63 | 64 | 65 | func set_paths(): 66 | print("set paths. root_dir:", root_dir) 67 | genome_top_file_prefix = root_dir.path_join("g1") 68 | genome_bottom_file_prefix = root_dir.path_join("g2") 69 | genome_top_fa_file = genome_top_file_prefix + ".fa" 70 | genome_bottom_fa_file = genome_bottom_file_prefix + ".fa" 71 | genome_top_annot_file = genome_top_file_prefix + ".gff" 72 | genome_bottom_annot_file = genome_bottom_file_prefix + ".gff" 73 | blast_file = root_dir.path_join("blast") 74 | blast_db = root_dir.path_join("blast_db") 75 | 76 | 77 | func get_genome(filename, file_type, outprefix): 78 | if file_type == "file": 79 | return fastaq_lib.to_fasta(filename, outprefix) 80 | elif file_type == "accession": 81 | return fastaq_lib.download_genome(filename, outprefix) 82 | else: 83 | return -1 84 | 85 | 86 | func import_genomes(top_fasta, top_type, bottom_fasta, bottom_type): 87 | var err1 = get_genome(top_fasta, top_type, genome_top_file_prefix) 88 | var err2 = get_genome(bottom_fasta, bottom_type, genome_bottom_file_prefix) 89 | return [err1, err2] 90 | 91 | 92 | func load_genomes(): 93 | genome_seqs = {} # saves some ram if project already loaded 94 | genome_seqs[Globals.TOP] = fastaq_lib.load_fasta_file(genome_top_fa_file) 95 | genome_seqs[Globals.BOTTOM] = fastaq_lib.load_fasta_file(genome_bottom_fa_file) 96 | 97 | 98 | func load_blast_hits(): 99 | blast_hits.clear() # saves some ram if project already loaded 100 | blast_hits = blast_lib.load_tsv_file(blast_file, genome_seqs[Globals.TOP], genome_seqs[Globals.BOTTOM]) 101 | 102 | 103 | func load_annotation_files(): 104 | annotation = {Globals.TOP: {}, Globals.BOTTOM: {}} 105 | if FileAccess.file_exists(genome_top_annot_file): 106 | annotation[Globals.TOP] = gff_lib.load_gff_file(genome_top_annot_file, genome_seqs[Globals.TOP]) 107 | if FileAccess.file_exists(genome_bottom_annot_file): 108 | annotation[Globals.BOTTOM] = gff_lib.load_gff_file(genome_bottom_annot_file, genome_seqs[Globals.BOTTOM]) 109 | 110 | 111 | func run_blast(): 112 | return blast_lib.run_blast(root_dir, blast_program) 113 | 114 | 115 | func save_as_serialized_file(outfile): 116 | print("Save project to file: ", outfile) 117 | if not data_loaded: 118 | OS.alert("Cannot save data because nothing loaded", "ERROR!") 119 | else: 120 | var file = FileAccess.open(outfile, FileAccess.WRITE) 121 | var hits = [] 122 | for x in blast_hits: 123 | hits.append(x.to_array()) 124 | file.store_var(hits, true) 125 | file.store_var(genome_seqs, true) 126 | file.store_var(annotation, true) 127 | 128 | 129 | func load_from_serialized_file(infile): 130 | print("Loading project from file: ", infile) 131 | var file = FileAccess.open(infile, FileAccess.READ) 132 | blast_hits.clear() 133 | genome_seqs.clear() 134 | blast_hits.clear() 135 | blast_hits = file.get_var() 136 | for i in range(len(blast_hits)): 137 | blast_hits[i] = blast_lib.BlastHitClass.new( 138 | blast_hits[i][0], 139 | blast_hits[i][1], 140 | blast_hits[i][2], 141 | blast_hits[i][3], 142 | blast_hits[i][4], 143 | blast_hits[i][5], 144 | blast_hits[i][6], 145 | blast_hits[i][7], 146 | blast_hits[i][8], 147 | false 148 | ) 149 | genome_seqs = file.get_var() 150 | annotation = file.get_var() 151 | if annotation == null: 152 | annotation = {Globals.TOP: {}, Globals.BOTTOM: {}} 153 | set_data_loaded() 154 | 155 | 156 | func flip_blast_hit_range(top_or_bottom, start, end, contig_index=null): 157 | var i = start 158 | while i < end: 159 | blast_hits[i].flip(top_or_bottom, contig_index) 160 | i += 1 161 | 162 | 163 | func flip_all_blast_hits(top_or_bottom, contig_index=null): 164 | if len(blast_hits) < 100: 165 | for m in blast_hits: 166 | m.flip(top_or_bottom, contig_index) 167 | return 168 | 169 | var thread2 = Thread.new() 170 | var middle = int(0.5 * len(blast_hits)) 171 | thread2.start(flip_blast_hit_range.bind(top_or_bottom, 0, middle, contig_index)) 172 | flip_blast_hit_range(top_or_bottom, middle, len(blast_hits), contig_index) 173 | thread2.wait_to_finish() 174 | 175 | 176 | func reverse_complement_annotation_one_contig(top_or_bottom, contig_index): 177 | if contig_index not in annotation[top_or_bottom]: 178 | return 179 | var ctg_length = contig_length(top_or_bottom, contig_index) 180 | for a in annotation[top_or_bottom][contig_index]: 181 | a[3] = not a[3] # flip the strand 182 | var start = ctg_length - a[1] 183 | a[1] = ctg_length - a[0] - 1 184 | a[0] = start - 1 185 | 186 | 187 | func reverse_complement_annotation(top_or_bottom): 188 | for i in annotation[top_or_bottom]: 189 | reverse_complement_annotation_one_contig(top_or_bottom, i) 190 | 191 | 192 | func reverse_complement_genome(top_or_bottom): 193 | flip_all_blast_hits(top_or_bottom) 194 | reverse_complement_annotation(top_or_bottom) 195 | for ctg in genome_seqs[top_or_bottom]["contigs"]: 196 | ctg["seq"] = fastaq_lib.revcomp(ctg["seq"]) 197 | 198 | 199 | func reverse_complement_one_contig(top_or_bottom, contig_index): 200 | flip_all_blast_hits(top_or_bottom, contig_index) 201 | reverse_complement_annotation_one_contig(top_or_bottom, contig_index) 202 | genome_seqs[top_or_bottom]["contigs"][contig_index]["seq"] = fastaq_lib.revcomp(genome_seqs[top_or_bottom]["contigs"][contig_index]["seq"]) 203 | 204 | 205 | func move_contig(top_or_bottom, ctg_index_to_move, move_type): 206 | var to_move_order_i = genome_seqs[top_or_bottom]["order"].find(ctg_index_to_move) 207 | var other_order_i 208 | if move_type == "left": 209 | other_order_i = to_move_order_i - 1 210 | elif move_type == "start": 211 | other_order_i = 0 212 | elif move_type == "right": 213 | other_order_i = to_move_order_i + 1 214 | elif move_type == "end": 215 | other_order_i = len(genome_seqs[top_or_bottom]["order"]) - 1 216 | 217 | if to_move_order_i == other_order_i \ 218 | or other_order_i < 0 or other_order_i > len(genome_seqs[top_or_bottom]["order"]) - 1: 219 | return 220 | 221 | if abs(to_move_order_i - other_order_i) == 1: 222 | # no need to pop/insert, just swap the elements. More efficient 223 | var tmp_i = genome_seqs[top_or_bottom]["order"][to_move_order_i] 224 | genome_seqs[top_or_bottom]["order"][to_move_order_i] = genome_seqs[top_or_bottom]["order"][other_order_i] 225 | genome_seqs[top_or_bottom]["order"][other_order_i] = tmp_i 226 | else: 227 | var i = genome_seqs[top_or_bottom]["order"].pop_at(to_move_order_i) 228 | genome_seqs[top_or_bottom]["order"].insert(other_order_i, i) 229 | 230 | 231 | func set_data_loaded(): 232 | data_loaded = true 233 | project_data_loaded.emit() 234 | 235 | 236 | func has_annotation(): 237 | return len(annotation[Globals.TOP]) > 0 or len(annotation[Globals.BOTTOM]) > 0 238 | 239 | 240 | func get_match_text(i): 241 | var m = blast_hits[i] 242 | return m.qry_name() + ":" + str(m.qstart) + "-" + str(m.qend) + \ 243 | " / " + m.ref_name() + ":" + str(m.rstart) + "-" + str(m.rend) + \ 244 | " / pcid:" + str(m.pcid) 245 | 246 | 247 | func contig_name(top_or_bottom, contig_id): 248 | return genome_seqs[top_or_bottom]["contigs"][contig_id]["name"] 249 | 250 | 251 | func contig_length(top_or_bottom, contig_id): 252 | return len(genome_seqs[top_or_bottom]["contigs"][contig_id]["seq"]) 253 | 254 | 255 | func contig_fasta_subseq(top_or_bottom, contig_id, start, end, reverse=false): 256 | if reverse: 257 | var t = start 258 | start = end 259 | end = t 260 | var name 261 | var seq 262 | if start < end: 263 | name = ">" + contig_name(top_or_bottom, contig_id) + ":" + str(start + 1) + "-" + str(end + 1) 264 | seq = Globals.proj_data.genome_seqs[top_or_bottom]["contigs"][contig_id]["seq"].substr(start, 1 + end - start) 265 | else: 266 | name = ">" + contig_name(top_or_bottom, contig_id) + ":" + str(end + 1) + "-" + str(start + 1) + ":reverse_strand" 267 | seq = fastaq_lib.revcomp( 268 | Globals.proj_data.genome_seqs[top_or_bottom]["contigs"][contig_id]["seq"].substr(end, 1 + start - end) 269 | ) 270 | 271 | var line_len = Globals.userdata.config.get_value("other", "fasta_line_length", 0) 272 | if line_len == 0: 273 | return [name, seq] 274 | 275 | var seq_lines = [] 276 | var i = 0 277 | while i < len(seq): 278 | seq_lines.append(seq.substr(i, line_len)) 279 | i += line_len 280 | return [name] + seq_lines 281 | 282 | 283 | func range_to_seq_lines(top_or_bottom, range_start, range_end): 284 | var is_rev = range_start[0] > range_end[0] or (range_start[0] == range_end[0] and range_start[1] > range_end[1]) 285 | if range_start[0] == range_end[0]: 286 | var lines = contig_fasta_subseq(top_or_bottom, range_start[0], range_start[1], range_end[1]) 287 | return lines 288 | 289 | if is_rev: 290 | var t = range_start 291 | range_start = range_end 292 | range_end = t 293 | 294 | var out = [] 295 | for ctg_index in range(range_start[0], range_end[0] + 1): 296 | if ctg_index == range_start[0]: 297 | out.append_array(contig_fasta_subseq(top_or_bottom, ctg_index, 298 | range_start[1], contig_length(top_or_bottom, range_start[0])-1, 299 | is_rev)) 300 | elif ctg_index < range_end[0]: 301 | out.append_array(contig_fasta_subseq(top_or_bottom, ctg_index, 302 | 0, contig_length(top_or_bottom, ctg_index)-1, is_rev)) 303 | else: 304 | out.append_array(contig_fasta_subseq( 305 | top_or_bottom, ctg_index, 0, range_end[1], is_rev)) 306 | 307 | return out 308 | 309 | 310 | func contig_fasta_lines(top_or_bottom, contig_id): 311 | return [">" + contig_name(top_or_bottom, contig_id), 312 | genome_seqs[top_or_bottom]["contigs"][contig_id]["seq"], 313 | ] 314 | --------------------------------------------------------------------------------