Repository: chrovis/cljam
Pull request #145 (2018-08-29 03:05:06)

Add basic reader/writer support for WIG.
This PR adds the feature to support [Wiggle Track Format (WIG)](https://genome.ucsc.edu/goldenpath/help/wiggle.html).


Patch:
diff --git a/src/cljam/io/util.clj b/src/cljam/io/util.clj
index 40d6a7e..144a11d 100644
--- a/src/cljam/io/util.clj
+++ b/src/cljam/io/util.clj
@@ -15,6 +15,7 @@
             cljam.io.twobit.writer
             cljam.io.fastq
             cljam.io.bed
+            cljam.io.wig
             [cljam.util :as util]))
 
 (defn alignment-reader?
@@ -127,6 +128,16 @@
   [wtr]
   (instance? cljam.io.bed.BEDWriter wtr))
 
+(defn wig-reader?
+  "Checks if given object is an instance of WIGReader."
+  [rdr]
+  (instance? cljam.io.wig.WIGReader rdr))
+
+(defn wig-writer?
+  "Checks if given object is an instance of WIGWriter."
+  [wtr]
+  (instance? cljam.io.wig.WIGWriter wtr))
+
 (defn file-type
   "Detects a file format from a path of f, returning a keyword representing the
   format. Throws an exception if an unsupported file is supplied."
@@ -142,4 +153,5 @@
     #"(?i)\.vcf" :vcf
     #"(?i)\.bcf$" :bcf
     #"(?i)\.bed" :bed
+    #"(?i)\.wig" :wig
     (throw (IllegalArgumentException. "Invalid file type"))))
diff --git a/src/cljam/io/wig.clj b/src/cljam/io/wig.clj
new file mode 100644
index 0000000..78eb302
--- /dev/null
+++ b/src/cljam/io/wig.clj
@@ -0,0 +1,226 @@
+(ns cljam.io.wig
+  "Functions to read and write the WIG (Wiggle) format. See
+  https://genome.ucsc.edu/goldenpath/help/wiggle.html for the detail WIG
+  specifications."
+  (:require [clojure.java.io :as cio]
+            [clojure.string :as cstr]
+            [proton.core :refer [as-long as-double]]
+            [cljam.io.protocols :as protocols]
+            [cljam.util :as util]
+            [cljam.util.chromosome :as chr])
+  (:import [java.net URL]
+           [java.io BufferedReader BufferedWriter Closeable]))
+
+(declare read-fields write-fields)
+
+(defrecord WIGReader [^BufferedReader reader ^URL url]
+  Closeable
+  (close [this]
+    (.close ^Closeable (.reader this)))
+  protocols/IReader
+  (reader-url [this] (.url this))
+  (read [this] (read-fields this))
+  (read [this option] (read-fields this))
+  (indexed? [_] false))
+
+(defrecord WIGWriter [^BufferedWriter writer ^URL url]
+  Closeable
+  (close [this]
+    (.close ^Closeable (.writer this)))
+  protocols/IWriter
+  (writer-url [this] (.url this)))
+
+(defn ^WIGReader reader
+  "Returns an open cljam.io.wig.WIGReader of f. Should be used inside with-open
+  to ensure the reader is properly closed."
+  [f]
+  (WIGReader. (cio/reader (util/compressor-input-stream f)) (util/as-url f)))
+
+(defn ^WIGWriter writer
+  "Returns an open cljam.io.wig.WIGWriter of f. Should be used inside with-open
+  to ensure the writer is properly closed."
+  [f]
+  (WIGWriter. (cio/writer (util/compressor-output-stream f)) (util/as-url f)))
+
+(def ^:const wig-fields [:track :chr :start :end :value])
+
+(defn- header-or-comment?
+  "Returns true if given string is a blank, header, or comment line."
+  [^String s]
+  (or (empty? s)
+      (.startsWith s "browser")
+      (.startsWith s "#")))
+
+(defn- normalize
+  "Normalize WIG lines."
+  [m]
+  (update m :chr chr/normalize-chromosome-key))
+
+(defn- fields->map
+  "Convert vector [\"key1=value1\" \"key2=value2\" ...] to map
+  {:key1 \"value1\" :key2 \"value2\" ...}."
+  [fields]
+  (into {}
+        (map (fn [field]
+               (let [[k v] (cstr/split field #"=")]
+                 [(keyword k) v])))
+        fields))
+
+(defn- str->wiggle-track-data
+  "Convert string to Wiggle track data values which can be integer or real,
+  positive or negative values."
+  [^String s]
+  (if-let [l (as-long s)]
+    l
+    (as-double s)))
+
+(defn- deserialize-wigs
+  "Parse WIG lines and return a lazy sequence of flat map."
+  [lines]
+  (letfn [(deserialize [lines pre-start track]
+            (lazy-seq
+             (when-first [line lines]
+               (let [fields (-> line cstr/trim (cstr/split #"\s+"))]
+                 (case (first fields)
+                   ; track definition line
+                   "track"
+                   (let [type (->> fields rest fields->map :type)]
+                     (if (= type "wiggle_0")
+                       (deserialize (rest lines) pre-start {:line line})
+                       (throw "The track type with version must be `wiggle_0`")))
+
+                   ; declaration line of variableStep
+                   "variableStep"
+                   (let [{:keys [chrom span]
+                          :or {span 1}} (->> fields rest fields->map)
+                         span (as-long span)
+                         track (assoc track :format :variable-step
+                                            :chr chrom
+                                            :span span
+                                            :step nil)]
+                     (deserialize (rest lines) nil track))
+
+                   ; declaration line of fixedStep
+                   "fixedStep"
+                   (let [{:keys [chrom start span step]
+                          :or {span 1, step 1}} (->> fields rest fields->map)
+                         step (as-long step)
+                         pre-start (- (as-long start) step)
+                         span (as-long span)
+                         track (assoc track :format :fixed-step
+                                            :chr chrom
+                                            :span span
+                                            :step step)]
+                     (deserialize (rest lines) pre-start track))
+
+                   ; data line
+                   (let [m (case (:format track)
+                             :variable-step
+                             (let [{:keys [chr span]} track
+                                   [start value] fields
+                                   start (as-long start)
+                                   end (dec (+ start span))
+                                   value (str->wiggle-track-data value)]
+                               {:track track
+                                :chr chr
+                                :start start
+                                :end end
+                                :value value})
+
+                             :fixed-step
+                             (let [{:keys [chr span step]} track
+                                   start (+ pre-start step)
+                                   end (dec (+ start span))
+                                   value (-> fields first str->wiggle-track-data)]
+                               {:track track
+                                :chr chr
+                                :start start
+                                :end end
+                                :value value})
+
+                             (throw
+                              (IllegalArgumentException.
+                               "Invalid wiggle format")))]
+                     (cons m (deserialize (rest lines)
+                                          (:start m)
+                                          track))))))))]
+    (deserialize lines nil {:line nil
+                            :format nil
+                            :chr nil
+                            :step nil
+                            :span nil})))
+
+(defn- serialize-wigs
+  "Serialize a sequence of WIG fields into a lazy sequence of string."
+  [wigs]
+  {:pre [;; These fields are required.
+         (map (fn [wig] (every? some? ((apply juxt wig-fields) wig))) wigs)
+         ;; There are two options for formatting wiggle data.
+         (map (fn [wig]
+                (let [format (-> wig :track :format)]
+                  (or (= format :fixed-step)
+                      (= format :variable-step))))
+              wigs)]}
+  (letfn [(serialize [wigs]
+            (->> wigs
+                 (sequence
+                  (comp
+                   (partition-by (juxt (comp :format :track)
+                                       #(- (:end %) (:start %))))
+                   (map
+                    (fn [[{{:keys [line format span step]} :track
+                           chr :chr
+                           start :start
+                           end :end} :as xs]]
+                      (case format
+                        :variable-step
+                        (let [declaration-line (->> (cond-> ["variableStep"
+                                                             (str "chrom=" chr)]
+                                                      (not= span 1)
+                                                      (conj (str "span=" span)))
+                                                    (cstr/join \space))
+                              data-lines (->> xs
+                                              (map (fn [{:keys [start value]}]
+                                                     (cstr/join \space
+                                                                [start value])))
+                                              (cstr/join \newline))]
+                          (cstr/join \newline
+                                     (cond->> [declaration-line data-lines]
+                                       line (cons line))))
+
+                        :fixed-step
+                        (let [declaration-line (->> (cond-> ["fixedStep"
+                                                             (str "chrom=" chr)
+                                                             (str "start=" start)]
+                                                      (not= step 1)
+                                                      (conj (str "step=" step))
+
+                                                      (not= span 1)
+                                                      (conj (str "span=" span)))
+                                                    (cstr/join \space))
+                              data-lines (->> xs
+                                              (map (fn [{:keys [value]}] value))
+                                              (cstr/join \newline))]
+                          (cstr/join \newline
+                                     (cond->> [declaration-line data-lines]
+                                       line (cons line)))))))))))]
+    (serialize wigs)))
+
+(defn read-fields
+  "Read sequence of WIG fields from reader"
+  [^WIGReader rdr]
+  (->> rdr
+       .reader
+       line-seq
+       (remove header-or-comment?)
+       deserialize-wigs
+       (map normalize)))
+
+(defn write-fields
+  "Write sequence of WIG fields to writer."
+  [^WIGWriter wtr xs]
+  (let [w ^BufferedWriter (.writer wtr)]
+    (->> xs
+         serialize-wigs
+         (interpose \newline)
+         (run! #(.write w (str %))))))


Test patch:
diff --git a/test-resources/wig/test1.wig b/test-resources/wig/test1.wig
new file mode 100644
index 0000000..2ebf73a
--- /dev/null
+++ b/test-resources/wig/test1.wig
@@ -0,0 +1,34 @@
+browser position chr19:49304200-49310700
+browser hide all
+#	150 base wide bar graph at arbitrarily spaced positions,
+#	threshold line drawn at y=11.76
+#	autoScale off viewing range set to [0:25]
+#	priority = 10 positions this as the first graph
+#	Note, one-relative coordinate system in use for this format
+track type=wiggle_0 name="variableStep" description="variableStep format" visibility=full autoScale=off viewLimits=0.0:25.0 color=50,150,255 yLineMark=11.76 yLineOnOff=on priority=10
+variableStep chrom=chr19 span=150
+49304701 10.0
+49304901 12.5
+49305401 15.0
+49305601 17.5
+49305901 20.0
+49306081 17.5
+49306301 15.0
+49306691 12.5
+49307871 10.0
+#	200 base wide points graph at every 300 bases, 50 pixel high graph
+#	autoScale off and viewing range set to [0:1000]
+#	priority = 20 positions this as the second graph
+#	Note, one-relative coordinate system in use for this format
+track type=wiggle_0 name="fixedStep" description="fixedStep format" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20
+fixedStep chrom=chr19 start=49307401 step=300 span=200
+1000
+ 900
+ 800
+ 700
+ 600
+ 500
+ 400
+ 300
+ 200
+ 100
diff --git a/test-resources/wig/test2.wig b/test-resources/wig/test2.wig
new file mode 100644
index 0000000..deed0f2
--- /dev/null
+++ b/test-resources/wig/test2.wig
@@ -0,0 +1,19 @@
+browser position chr19:49304200-49310700
+browser hide all
+#	150 base wide bar graph at arbitrarily spaced positions,
+#	threshold line drawn at y=11.76
+#	autoScale off viewing range set to [0:25]
+#	priority = 10 positions this as the first graph
+#	Note, one-relative coordinate system in use for this format
+track type=wiggle_0 name="variableStep" description="variableStep format" visibility=full autoScale=off viewLimits=0.0:25.0 color=50,150,255 yLineMark=11.76 yLineOnOff=on priority=10
+variableStep chrom=chr19
+49304701 10.0
+49304901 12.5
+#	200 base wide points graph at every 300 bases, 50 pixel high graph
+#	autoScale off and viewing range set to [0:1000]
+#	priority = 20 positions this as the second graph
+#	Note, one-relative coordinate system in use for this format
+track type=wiggle_0 name="fixedStep" description="fixedStep format" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20
+fixedStep chrom=chr19 start=49307401
+1000
+900
diff --git a/test/cljam/io/util_test.clj b/test/cljam/io/util_test.clj
index dc17f6f..4e1d9b6 100644
--- a/test/cljam/io/util_test.clj
+++ b/test/cljam/io/util_test.clj
@@ -8,7 +8,8 @@
             [cljam.io.sam :as sam]
             [cljam.io.sequence :as cseq]
             [cljam.io.util :as io-util]
-            [cljam.io.vcf :as vcf]))
+            [cljam.io.vcf :as vcf]
+            [cljam.io.wig :as wig]))
 
 (deftest about-file-type-detection
   (are [?path ?expected] (and (= (io-util/file-type ?path) ?expected)
@@ -54,7 +55,9 @@
     "foo.bed" :bed
     "foo.bed.gz" :bed
     "foo.BED" :bed
-    "foo.BED.GZ" :bed)
+    "foo.BED.GZ" :bed
+    "foo.wig" :wig
+    "foo.WIG" :wig)
   (are [?dir]
       (are [?path] (thrown? Exception (io-util/file-type (str ?dir ?path)))
         "foo.bam.gz"
@@ -81,7 +84,8 @@
         io-util/fasta-reader? false
         io-util/twobit-reader? false
         io-util/fastq-reader? false
-        io-util/bed-reader? false)))
+        io-util/bed-reader? false
+        io-util/wig-reader? false)))
   (testing "bam reader"
     (with-open [r (sam/reader small-bam-file)]
       (are [?pred ?expected] (= (?pred r) ?expected)
@@ -95,7 +99,8 @@
         io-util/fasta-reader? false
         io-util/twobit-reader? false
         io-util/fastq-reader? false
-        io-util/bed-reader? false))
+        io-util/bed-reader? false
+        io-util/wig-reader? false))
     (with-open [r (sam/reader small-bam-file)
                 cloned (sam/reader r)]
       (is (true? (io-util/bam-reader? cloned))))
@@ -114,7 +119,8 @@
         io-util/fasta-reader? false
         io-util/twobit-reader? false
         io-util/fastq-reader? false
-        io-util/bed-reader? false)))
+        io-util/bed-reader? false
+        io-util/wig-reader? false)))
   (testing "bcf reader"
     (with-open [r (vcf/reader test-bcf-v4_3-file)]
       (are [?pred ?expected] (= (?pred r) ?expected)
@@ -128,7 +134,8 @@
         io-util/fasta-reader? false
         io-util/twobit-reader? false
         io-util/fastq-reader? false
-        io-util/bed-reader? false)))
+        io-util/bed-reader? false
+        io-util/wig-reader? false)))
   (testing "fasta reader"
     (with-open [r (cseq/reader test-fa-file)]
       (are [?pred ?expected] (= (?pred r) ?expected)
@@ -142,7 +149,8 @@
         io-util/fasta-reader? true
         io-util/twobit-reader? false
         io-util/fastq-reader? false
-        io-util/bed-reader? false)))
+        io-util/bed-reader? false
+        io-util/wig-reader? false)))
   (testing "twobit reader"
     (with-open [r (cseq/reader test-twobit-file)]
       (are [?pred ?expected] (= (?pred r) ?expected)
@@ -156,7 +164,8 @@
         io-util/fasta-reader? false
         io-util/twobit-reader? true
         io-util/fastq-reader? false
-        io-util/bed-reader? false)))
+        io-util/bed-reader? false
+        io-util/wig-reader? false)))
   (testing "fastq reader"
     (with-open [r (fastq/reader test-fq-file)]
       (are [?pred ?expected] (= (?pred r) ?expected)
@@ -170,7 +179,8 @@
         io-util/sequence-reader? false
         io-util/twobit-reader? false
         io-util/fastq-reader? true
-        io-util/bed-reader? false)))
+        io-util/bed-reader? false
+        io-util/wig-reader? false)))
   (testing "bed reader"
     (with-open [r (bed/reader test-bed-file1)]
       (are [?pred ?expected] (= (?pred r) ?expected)
@@ -184,7 +194,23 @@
         io-util/fasta-reader? false
         io-util/twobit-reader? false
         io-util/fastq-reader? false
-        io-util/bed-reader? true))))
+        io-util/bed-reader? true
+        io-util/wig-reader? false)))
+  (testing "wig reader"
+    (with-open [r (wig/reader test-wig-file1)]
+      (are [?pred ?expected] (= (?pred r) ?expected)
+        io-util/alignment-reader? false
+        io-util/sam-reader? false
+        io-util/bam-reader? false
+        io-util/variant-reader? false
+        io-util/vcf-reader? false
+        io-util/bcf-reader? false
+        io-util/sequence-reader? false
+        io-util/fasta-reader? false
+        io-util/twobit-reader? false
+        io-util/fastq-reader? false
+        io-util/bed-reader? false
+        io-util/wig-reader? true))))
 
 (deftest writer-predicates-test
   (testing "sam writer"
@@ -200,7 +226,8 @@
         io-util/vcf-writer? false
         io-util/bcf-writer? false
         io-util/fastq-writer? false
-        io-util/bed-writer? false)))
+        io-util/bed-writer? false
+        io-util/wig-writer? false)))
   (testing "bam writer"
     (with-open [r (sam/writer (.getAbsolutePath (cio/file util/temp-dir "temp.bam")))]
       (are [?pred ?expected] (= (?pred r) ?expected)
@@ -214,7 +241,8 @@
         io-util/vcf-writer? false
         io-util/bcf-writer? false
         io-util/fastq-writer? false
-        io-util/bed-writer? false)))
+        io-util/bed-writer? false
+        io-util/wig-writer? false)))
   (testing "fasta writer"
     (with-open [r (cseq/writer (.getAbsolutePath (cio/file util/temp-dir "temp.fa")))]
       (are [?pred ?expected] (= (?pred r) ?expected)
@@ -228,7 +256,8 @@
         io-util/vcf-writer? false
         io-util/bcf-writer? false
         io-util/fastq-writer? false
-        io-util/bed-writer? false)))
+        io-util/bed-writer? false
+        io-util/wig-writer? false)))
   (testing "twobit writer"
     (with-open [r (cseq/writer (.getAbsolutePath (cio/file util/temp-dir "temp.2bit")))]
       (are [?pred ?expected] (= (?pred r) ?expected)
@@ -242,7 +271,8 @@
         io-util/vcf-writer? false
         io-util/bcf-writer? false
         io-util/fastq-writer? false
-        io-util/bed-writer? false)))
+        io-util/bed-writer? false
+        io-util/wig-writer? false)))
   (testing "vcf writer"
     (with-open [r (vcf/writer (.getAbsolutePath (cio/file util/temp-dir "temp.vcf")) {} [])]
       (are [?pred ?expected] (= (?pred r) ?expected)
@@ -256,7 +286,8 @@
         io-util/vcf-writer? true
         io-util/bcf-writer? false
         io-util/fastq-writer? false
-        io-util/bed-writer? false)))
+        io-util/bed-writer? false
+        io-util/wig-writer? false)))
   (testing "bcf writer"
     (with-open [r (vcf/writer (.getAbsolutePath (cio/file util/temp-dir "temp.bcf")) {} [])]
       (are [?pred ?expected] (= (?pred r) ?expected)
@@ -270,7 +301,8 @@
         io-util/vcf-writer? false
         io-util/bcf-writer? true
         io-util/fastq-writer? false
-        io-util/bed-writer? false)))
+        io-util/bed-writer? false
+        io-util/wig-writer? false)))
   (testing "fastq writer"
     (with-open [r (fastq/writer (.getAbsolutePath (cio/file util/temp-dir "temp.fq")))]
       (are [?pred ?expected] (= (?pred r) ?expected)
@@ -284,7 +316,8 @@
         io-util/vcf-writer? false
         io-util/bcf-writer? false
         io-util/fastq-writer? true
-        io-util/bed-writer? false)))
+        io-util/bed-writer? false
+        io-util/wig-writer? false)))
   (testing "bed writer"
     (with-open [r (bed/writer (.getAbsolutePath (cio/file util/temp-dir "temp.bed")))]
       (are [?pred ?expected] (= (?pred r) ?expected)
@@ -298,4 +331,20 @@
         io-util/vcf-writer? false
         io-util/bcf-writer? false
         io-util/fastq-writer? false
-        io-util/bed-writer? true))))
+        io-util/bed-writer? true
+        io-util/wig-writer? false)))
+  (testing "wig writer"
+    (with-open [r (wig/writer (.getAbsolutePath (cio/file util/temp-dir "temp.wig")))]
+      (are [?pred ?expected] (= (?pred r) ?expected)
+        io-util/alignment-writer? false
+        io-util/sam-writer? false
+        io-util/bam-writer? false
+        io-util/sequence-writer? false
+        io-util/fasta-writer? false
+        io-util/twobit-writer? false
+        io-util/variant-writer?  false
+        io-util/vcf-writer? false
+        io-util/bcf-writer? false
+        io-util/fastq-writer? false
+        io-util/bed-writer? false
+        io-util/wig-writer? true))))
diff --git a/test/cljam/io/wig_test.clj b/test/cljam/io/wig_test.clj
new file mode 100644
index 0000000..2ac6065
--- /dev/null
+++ b/test/cljam/io/wig_test.clj
@@ -0,0 +1,242 @@
+(ns cljam.io.wig-test
+  (:require [clojure.test :refer :all]
+            [clojure.java.io :as cio]
+            [clojure.string :as cstr]
+            [cljam.test-common :refer :all]
+            [cljam.io.wig :as wig]
+            [cljam.util :as util])
+  (:import [java.io BufferedReader InputStreamReader ByteArrayInputStream
+            ByteArrayOutputStream OutputStreamWriter BufferedWriter]
+           [cljam.io.wig WIGReader WIGWriter]))
+
+(def ^:private test-wig-fields1
+  [{:track {:line "track type=wiggle_0 name=\"variableStep\" description=\"variableStep format\" visibility=full autoScale=off viewLimits=0.0:25.0 color=50,150,255 yLineMark=11.76 yLineOnOff=on priority=10"
+            :format :variable-step :chr "chr19" :step nil :span 150}
+    :chr "chr19" :start 49304701 :end 49304850 :value 10.0}
+   {:track {:line "track type=wiggle_0 name=\"variableStep\" description=\"variableStep format\" visibility=full autoScale=off viewLimits=0.0:25.0 color=50,150,255 yLineMark=11.76 yLineOnOff=on priority=10"
+            :format :variable-step :chr "chr19" :step nil :span 150}
+    :chr "chr19" :start 49304901 :end 49305050 :value 12.5}
+   {:track {:line "track type=wiggle_0 name=\"variableStep\" description=\"variableStep format\" visibility=full autoScale=off viewLimits=0.0:25.0 color=50,150,255 yLineMark=11.76 yLineOnOff=on priority=10"
+            :format :variable-step :chr "chr19" :step nil :span 150}
+    :chr "chr19" :start 49305401 :end 49305550 :value 15.0}
+   {:track {:line "track type=wiggle_0 name=\"variableStep\" description=\"variableStep format\" visibility=full autoScale=off viewLimits=0.0:25.0 color=50,150,255 yLineMark=11.76 yLineOnOff=on priority=10"
+            :format :variable-step :chr "chr19" :step nil :span 150}
+    :chr "chr19" :start 49305601 :end 49305750 :value 17.5}
+   {:track {:line "track type=wiggle_0 name=\"variableStep\" description=\"variableStep format\" visibility=full autoScale=off viewLimits=0.0:25.0 color=50,150,255 yLineMark=11.76 yLineOnOff=on priority=10"
+            :format :variable-step :chr "chr19" :step nil :span 150}
+    :chr "chr19" :start 49305901 :end 49306050 :value 20.0}
+   {:track {:line "track type=wiggle_0 name=\"variableStep\" description=\"variableStep format\" visibility=full autoScale=off viewLimits=0.0:25.0 color=50,150,255 yLineMark=11.76 yLineOnOff=on priority=10"
+            :format :variable-step :chr "chr19" :step nil :span 150}
+    :chr "chr19" :start 49306081 :end 49306230 :value 17.5}
+   {:track {:line "track type=wiggle_0 name=\"variableStep\" description=\"variableStep format\" visibility=full autoScale=off viewLimits=0.0:25.0 color=50,150,255 yLineMark=11.76 yLineOnOff=on priority=10"
+            :format :variable-step :chr "chr19" :step nil :span 150}
+    :chr "chr19" :start 49306301 :end 49306450 :value 15.0}
+   {:track {:line "track type=wiggle_0 name=\"variableStep\" description=\"variableStep format\" visibility=full autoScale=off viewLimits=0.0:25.0 color=50,150,255 yLineMark=11.76 yLineOnOff=on priority=10"
+            :format :variable-step :chr "chr19" :step nil :span 150}
+    :chr "chr19" :start 49306691 :end 49306840 :value 12.5}
+   {:track {:line "track type=wiggle_0 name=\"variableStep\" description=\"variableStep format\" visibility=full autoScale=off viewLimits=0.0:25.0 color=50,150,255 yLineMark=11.76 yLineOnOff=on priority=10"
+            :format :variable-step :chr "chr19" :step nil :span 150}
+    :chr "chr19" :start 49307871 :end 49308020 :value 10.0}
+   {:track {:line "track type=wiggle_0 name=\"fixedStep\" description=\"fixedStep format\" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20"
+            :format :fixed-step :chr "chr19" :step 300 :span 200}
+    :chr "chr19" :start 49307401 :end 49307600 :value 1000}
+   {:track {:line "track type=wiggle_0 name=\"fixedStep\" description=\"fixedStep format\" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20"
+            :format :fixed-step :chr "chr19" :step 300 :span 200}
+    :chr "chr19" :start 49307701 :end 49307900 :value 900}
+   {:track {:line "track type=wiggle_0 name=\"fixedStep\" description=\"fixedStep format\" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20"
+            :format :fixed-step :chr "chr19" :step 300 :span 200}
+    :chr "chr19" :start 49308001 :end 49308200 :value 800}
+   {:track {:line "track type=wiggle_0 name=\"fixedStep\" description=\"fixedStep format\" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20"
+            :format :fixed-step :chr "chr19" :step 300 :span 200}
+    :chr "chr19" :start 49308301 :end 49308500 :value 700}
+   {:track {:line "track type=wiggle_0 name=\"fixedStep\" description=\"fixedStep format\" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20"
+            :format :fixed-step :chr "chr19" :step 300 :span 200}
+    :chr "chr19" :start 49308601 :end 49308800 :value 600}
+   {:track {:line "track type=wiggle_0 name=\"fixedStep\" description=\"fixedStep format\" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20"
+            :format :fixed-step :chr "chr19" :step 300 :span 200}
+    :chr "chr19" :start 49308901 :end 49309100 :value 500}
+   {:track {:line "track type=wiggle_0 name=\"fixedStep\" description=\"fixedStep format\" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20"
+            :format :fixed-step :chr "chr19" :step 300 :span 200}
+    :chr "chr19" :start 49309201 :end 49309400 :value 400}
+   {:track {:line "track type=wiggle_0 name=\"fixedStep\" description=\"fixedStep format\" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20"
+            :format :fixed-step :chr "chr19" :step 300 :span 200}
+    :chr "chr19" :start 49309501 :end 49309700 :value 300}
+   {:track {:line "track type=wiggle_0 name=\"fixedStep\" description=\"fixedStep format\" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20"
+            :format :fixed-step :chr "chr19" :step 300 :span 200}
+    :chr "chr19" :start 49309801 :end 49310000 :value 200}
+   {:track {:line "track type=wiggle_0 name=\"fixedStep\" description=\"fixedStep format\" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20"
+            :format :fixed-step :chr "chr19" :step 300 :span 200}
+    :chr "chr19" :start 49310101 :end 49310300 :value 100}])
+
+(def ^:private ^String temp-wig-file (str temp-dir "/test.wig"))
+
+(defn- str->wig
+  [^String s]
+  (with-open [bais (ByteArrayInputStream. (.getBytes s))
+              isr (InputStreamReader. bais)
+              br (wig/WIGReader. (BufferedReader. isr) nil)]
+    (doall (wig/read-fields br))))
+
+(defn- wig->str
+  [xs]
+  (with-open [bao (ByteArrayOutputStream.)
+              osw (OutputStreamWriter. bao)
+              bw (wig/WIGWriter. (BufferedWriter. osw) nil)]
+    (wig/write-fields bw xs)
+    (.flush ^BufferedWriter (.writer bw))
+    (.toString bao)))
+
+(deftest reader
+  (testing "make reader instance"
+    (with-open [rdr (wig/reader test-wig-file1)]
+      (is (instance? cljam.io.wig.WIGReader rdr))))
+
+  (testing "throw Exception"
+    (is (thrown? Exception (wig/reader "test-resources/wig/not-found.wig"))))
+
+  (testing "read wig"
+    (is (= (str->wig "variableStep chrom=chr1 span=10\n1 1\n101 2")
+           [{:track {:line nil :format :variable-step :chr "chr1" :step nil :span 10}
+             :chr "chr1" :start 1 :end 10 :value 1}
+            {:track {:line nil :format :variable-step :chr "chr1" :step nil :span 10}
+             :chr "chr1" :start 101 :end 110 :value 2}]))
+    (is (= (str->wig "fixedStep chrom=chr1 start=1 step=20 span=10\n1\n2")
+           [{:track {:line nil :format :fixed-step :chr "chr1" :step 20 :span 10}
+             :chr "chr1" :start 1 :end 10 :value 1}
+            {:track {:line nil :format :fixed-step :chr "chr1" :step 20 :span 10}
+             :chr "chr1" :start 21 :end 30 :value 2}])))
+
+  (testing "read wig with default span value for variableStep"
+    (is (= (str->wig "variableStep chrom=chr1\n1 1\n101 2")
+           [{:track {:line nil :format :variable-step :chr "chr1" :step nil :span 1}
+             :chr "chr1" :start 1 :end 1 :value 1}
+            {:track {:line nil :format :variable-step :chr "chr1" :step nil :span 1}
+             :chr "chr1" :start 101 :end 101 :value 2}])))
+
+  (testing "read wig with default span and step value for fixedStep"
+    (is (= (str->wig "fixedStep chrom=chr1 start=1\n1\n2")
+           [{:track {:line nil :format :fixed-step :chr "chr1" :step 1 :span 1}
+             :chr "chr1" :start 1 :end 1 :value 1}
+            {:track {:line nil :format :fixed-step :chr "chr1" :step 1 :span 1}
+             :chr "chr1" :start 2 :end 2 :value 2}])))
+
+  (testing "read a single track with a single variableStep"
+    (is (= (str->wig (cstr/join \newline ["track type=wiggle_0"
+                                          "variableStep chrom=chr1 span=10"
+                                          "1 1"]))
+           [{:track {:line "track type=wiggle_0" :format :variable-step
+                     :chr "chr1" :step nil :span 10}
+             :chr "chr1" :start 1 :end 10 :value 1}])))
+
+  (testing "read a single track with a single fixedStep"
+    (is (= (str->wig (cstr/join \newline ["track type=wiggle_0"
+                                          "fixedStep chrom=chr1 start=1 span=10"
+                                          "1"]))
+           [{:track {:line "track type=wiggle_0" :format :fixed-step
+                     :chr "chr1" :step 1 :span 10}
+             :chr "chr1" :start 1 :end 10 :value 1}])))
+
+  (testing "read wig with continuous same formats"
+    (let [s (cstr/join \newline ["variableStep chrom=chr19 span=5"
+                                 "1 1"
+                                 "11 10"
+                                 "variableStep chrom=chr19 span=10"
+                                 "101 1"
+                                 "111 10"])]
+      (is (= (str->wig s)
+             [{:track {:line nil :format :variable-step :chr "chr19" :step nil :span 5}
+               :chr "chr19" :start 1 :end 5 :value 1}
+              {:track {:line nil :format :variable-step :chr "chr19" :step nil :span 5}
+               :chr "chr19" :start 11 :end 15 :value 10}
+              {:track {:line nil :format :variable-step :chr "chr19" :step nil :span 10}
+               :chr "chr19" :start 101 :end 110 :value 1}
+              {:track {:line nil :format :variable-step :chr "chr19" :step nil :span 10}
+               :chr "chr19" :start 111 :end 120 :value 10}])))
+    (let [s (cstr/join \newline ["fixedStep chrom=chr19 start=1 step=10 span=5"
+                                 "1"
+                                 "10"
+                                 "fixedStep chrom=chr19 start=101 step=10 span=10"
+                                 "1"
+                                 "10"])]
+      (is (= (str->wig s)
+             [{:track {:line nil :format :fixed-step :chr "chr19" :step 10 :span 5}
+               :chr "chr19" :start 1 :end 5 :value 1}
+              {:track {:line nil :format :fixed-step :chr "chr19" :step 10 :span 5}
+               :chr "chr19" :start 11 :end 15 :value 10}
+              {:track {:line nil :format :fixed-step :chr "chr19" :step 10 :span 10}
+               :chr "chr19" :start 101 :end 110 :value 1}
+              {:track {:line nil :format :fixed-step :chr "chr19" :step 10 :span 10}
+               :chr "chr19" :start 111 :end 120 :value 10}]))))
+
+  (testing "read wig file"
+    (with-open [r (wig/reader test-wig-file1)]
+      (is (= (wig/read-fields r)
+             test-wig-fields1)))
+    (with-open [r (wig/reader test-wig-file2)]
+      (is (= (wig/read-fields r)
+             [{:track {:line "track type=wiggle_0 name=\"variableStep\" description=\"variableStep format\" visibility=full autoScale=off viewLimits=0.0:25.0 color=50,150,255 yLineMark=11.76 yLineOnOff=on priority=10"
+                       :format :variable-step :chr "chr19" :step nil :span 1}
+               :chr "chr19" :start 49304701 :end 49304701 :value 10.0}
+              {:track {:line "track type=wiggle_0 name=\"variableStep\" description=\"variableStep format\" visibility=full autoScale=off viewLimits=0.0:25.0 color=50,150,255 yLineMark=11.76 yLineOnOff=on priority=10"
+                       :format :variable-step :chr "chr19" :step nil :span 1}
+               :chr "chr19" :start 49304901 :end 49304901 :value 12.5}
+              {:track {:line "track type=wiggle_0 name=\"fixedStep\" description=\"fixedStep format\" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20"
+                       :format :fixed-step :chr "chr19" :step 1 :span 1}
+               :chr "chr19" :start 49307401 :end 49307401 :value 1000}
+              {:track {:line "track type=wiggle_0 name=\"fixedStep\" description=\"fixedStep format\" visibility=full autoScale=off viewLimits=0:1000 color=0,200,100 maxHeightPixels=100:50:20 graphType=points priority=20"
+                       :format :fixed-step :chr "chr19" :step 1 :span 1}
+               :chr "chr19" :start 49307402 :end 49307402 :value 900}])))))
+
+(deftest writer
+  (testing "make writer instance"
+    (with-open [wtr (wig/writer (.getAbsolutePath (cio/file util/temp-dir "temp.wig")))]
+      (is (instance? cljam.io.wig.WIGWriter wtr))))
+
+  (testing "write wig"
+    (is (= (-> "variableStep chrom=chr1 span=10\n1 1\n101 2" str->wig wig->str)
+           "variableStep chrom=chr1 span=10\n1 1\n101 2"))
+    (is (= (-> "fixedStep chrom=chr1 start=1 step=20 span=10\n1\n2" str->wig wig->str)
+           "fixedStep chrom=chr1 start=1 step=20 span=10\n1\n2")))
+
+  (testing "write wig with default span value for variableStep"
+    (is (= (-> "variableStep chrom=chr1\n1 1\n101 2" str->wig wig->str)
+           "variableStep chrom=chr1\n1 1\n101 2")))
+
+  (testing "write wig with default span and step value for fixedStep"
+    (is (= (-> "fixedStep chrom=chr1 start=1\n1\n2" str->wig wig->str)
+           "fixedStep chrom=chr1 start=1\n1\n2")))
+
+  (testing "write wig with continuous same formats"
+    (let [s (cstr/join \newline ["variableStep chrom=chr19 span=5"
+                                 "1 1"
+                                 "11 10"
+                                 "variableStep chrom=chr19 span=10"
+                                 "101 1"
+                                 "111 10"])]
+      (is (= s (-> s str->wig wig->str))))
+    (let [s (cstr/join \newline ["fixedStep chrom=chr19 start=1 step=10 span=5"
+                                 "1"
+                                 "10"
+                                 "fixedStep chrom=chr19 start=101 step=10 span=10"
+                                 "1"
+                                 "10"])]
+      (is (= s (-> s str->wig wig->str))))))
+
+(deftest source-type-test
+  (testing "reader"
+    (with-open [server (http-server)]
+      (are [x] (with-open [rdr (wig/reader x)]
+                 (= (wig/read-fields rdr) test-wig-fields1))
+        test-wig-file1
+        (cio/file test-wig-file1)
+        (cio/as-url (cio/file test-wig-file1))
+        (cio/as-url (str (:uri server) "/wig/test1.wig")))))
+
+  (testing "writer"
+    (are [x] (with-before-after {:before (prepare-cache!)
+                                 :after (clean-cache!)}
+               (with-open [wtr (wig/writer x)]
+                 (not-throw? (wig/write-fields wtr test-wig-fields1))))
+      temp-wig-file
+      (cio/file temp-wig-file)
+      (cio/as-url (cio/file temp-wig-file)))))
diff --git a/test/cljam/test_common.clj b/test/cljam/test_common.clj
index a1d251c..8994b63 100644
--- a/test/cljam/test_common.clj
+++ b/test/cljam/test_common.clj
@@ -196,6 +196,11 @@
 
 (def test-gff3-file "test-resources/gff3/example.gff3")
 
+;; ### WIG files
+
+(def test-wig-file1 "test-resources/wig/test1.wig")
+(def test-wig-file2 "test-resources/wig/test2.wig")
+
 (def to-sam-alignment
   (comp
    protocols/map->SAMAlignment