Skip to content

rbcalc rubygem

New gem released – Ruby Bindings for the Bcalc library. More info here.

Categories: Code.

Tags: , , ,

Guitar Tabs for Geeks

I just added an old test app I made a while back on Github. It’s called Tabs (unsurprisingly) and it lets you input small guitar tabs using your keyboard in a -hopefully- intuitive manner. It was built to test a few javascript libraries I had bumped into, and also to experiment with Capybara + Selenium testing. Hence, it is not really meant for public use but I ended up using it quite a bit to share licks and song ideas with bandmates, so here it is!

Categories: Code.

Tags: , ,

Managing Sphinx Real-time indexing in Ruby

The Sphinx Search Server has support for real-time indexing for quite a while now. Real-time indexing means that you do not have to trigger an index update whenever you update searchable data in your application, which makes Sphinx a viable option for apps with big searchable datasets.

The following class implements a basic RT Index interface. This particular example uses Sequel as ORM, but you can easily modify it to use ActiveRecord or whatever else you prefer.

You can hook this class up your models’ after filters, thus ensuring that our search index is always up to date. No cronjobs, no nothing.
Here is an example:

class Artist < ActiveRecord::Base
  after_create :add_to_index
  after_update :update_index
  after_delete :remove_from_index
  
  private
  # make use you include the record pk and any searchable fields
  def add_to_index
    rt_index.insert(:id => id, :name => name)
  end
  
  def update_index
    rt_index.replace(:id => id, :name => name)
  end
  
  def remove_from_index
    rt_index.delete(id)
  end
  
  def rt_index
    RtIndex.new(:artists)
  end
end

And here is the complete class:

require 'sequel'
class RtIndex
  attr_reader :index_name
  
  def initialize index_name
    @index_name = index_name.to_sym
  end
  
  def insert hash
    DB["INSERT INTO #{index_name} (?) VALUES (?)",
      Sequel.lit(hash.keys.join(',')),
      Sequel.lit(hash.values.map {|v| DB.literal(v)}.join(','))
    ].insert
  end
  
  def replace hash
    DB["REPLACE INTO #{index_name} (?) VALUES (?)",
      Sequel.lit(hash.keys.join(',')),
      Sequel.lit(hash.values.map {|v| DB.literal(v)}.join(','))
    ].replace
  end
  
  def delete id
    DB["DELETE FROM #{index_name} WHERE id = ?",id].delete
  end
  
  class << self
    # you want to amend this to load sphinx configuration from wherever you keep it.
    def config
      { "sphinx" => { "host" => "localhost", "port" => 3309 } }
    end
  end
  
  # create a shared mysql connection to sphinx
  DB = Sequel.mysql(:host => config["sphinx"]["host"], :port => config["sphinx"]["rt_port"])
end

Creating reusable ORM-specific extensions is fairly trivial. A class method call such as has_rt_index :name, :description is easy to implement and left as an exercise.

Categories: Code.

Tags: ,

Redis Model

Here is a simple class I have been using more and more recently. It is a very simple Redis-backed Model implementation.
The class itself is minimal, only facilitating CRUD operations and automatic timestamp management.

The class assumes you have a Redis connection initialized in $redis.

require 'time'
require 'json'

# A basic Redis-backed Model implementation.
# Extend it by specifying your desired attr_accessors
# Enable timestamps by calling use_timestamps in your implementation
# Features auto-incremented ids and id namespacing
# Example:
#   class Zorro < RedisModel
#     attr_accessor :sword, :mask
#     use_timestamps
#   end
#   
#   z = Zorro.new(:sword => 'rapier', :mask => 'lame')
#   z.persist!
#   z
#   => #<Zorro @id="1", @sword="rapier", @mask="lame", @updated_at=2013-03-14 18:11:17 +0100, @created_at=2013-03-14 18:11:17 +0100>
#
class RedisModel
  # the only default attribute
  attr_accessor :id
  
  def initialize params = {}
    params.map do |k,v| 
      begin
        send(:"#{k}=",JSON.parse(v))
      rescue JSON::ParserError
        send(:"#{k}=",v)
      end
    end
    
    # typecast some standard attributes
    self.id = self.id.to_i if self.id
    if use_timestamps?
      self.created_at = Time.parse(self.created_at) if self.created_at.is_a?(String)
      self.updated_at = Time.parse(self.updated_at) if self.updated_at.is_a?(String)
    end
  end
  
  def persist!
    is_new = id.nil? or !self.class.exists?(id)
    
    if use_timestamps?
      self.updated_at = Time.now
      self.created_at = Time.now if is_new
    end
    
    self.id = generate_id if is_new
    
    $redis.hmset(namespaced_id, *self.to_array)
    return true
  end
  alias :save! :persist!
  
  def persist
    persist!
    self
  end
  alias :save :persist
  
  def destroy!
    $redis.del(namespaced_id)
  end
  
  def reload
    self.class.load(namespaced_id)
  end
  
  def to_hash
    instance_variables.inject({}) do |h,v|
      value = instance_variable_get(v)
      value = value.is_a?(String) ? value : value.to_json
      h.update v.to_s.gsub('@','') => value
    end
  end
  
  def use_timestamps?
    self.class.instance_variable_get(:@use_timestamps)
  end
  
  def to_array
    to_hash.to_a.flatten
  end
  
  def namespaced_id
    self.class.namespaced_id(self.id)
  end
  
  # useful for testing, but also a general convenience
  include Comparable
  def <=>(other)
    self.to_hash <=> other.to_hash
  end
  
  class << self
    @use_timestamps = false
    
    def namespaced_id(id)
      "#{self}::#{id}"
    end
    
    def all
      $redis.namespace(self)
    end
    
    def exists? id
      $redis.exists(namespaced_id(id))
    end
    
    def load id
      hash = $redis.hgetall(namespaced_id(id))
      hash == {} ? nil : new(hash)
    end
    alias :find :load
    
    def use_timestamps
      @use_timestamps = true
      self.send(:attr_accessor, :created_at)
      self.send(:attr_accessor, :updated_at)
    end
  end
  
protected
  def generate_id
    $redis.incr("_#{self.class}_current_id")
  end
end

Categories: Code.

Tags: ,

Fancybox with fancier product zooming

Product Zooming using FancyboxFancybox is a popular open-source javascript lightboxing library, used in many contexts, including product presentation. I recently had a client request image zooming functionality to be included in their site’s shopping cart. The pages already used Fancybox to show the products, but Fancybox’s default behavior is to open new images and resize them to fit the containing element, whose size is in turn determined by the size of the viewport. The client required a detailed view instead, in a way that would allow the user to navigate around the image. After looking around the internets I did not find a library or addon that satisfied that criteria, but I did manage to extend Fancybox easily, thanks to the decently designed API it offers. Here is how I did it:

Step 1: Overriding default styles

We will redefine a few styles, overriding the default resizing behavior. To avoid screwing up things elsewhere in the application, we will prepend a custom selector in our special zooming lightbox. Thankfully, Fancybox gives us an easy way to do that as you can see in the next section.

/* we want the close icon inside the container */
.productviewer .fancybox-close {top:2px; right:2px;}
/* the transition smooths image navigation, or our eyes would water */
.productviewer .fancybox-image {
  width: auto;
  height: auto;
  cursor:move;
  transition: top 0.2s, left 0.2s;
  -webkit-transition: top 0.2s, left 0.2s;
}
.productviewer .fancybox-inner {overflow: hidden;}

Don’t forget that these styles should be loaded after the default stylesheets, or else the overrides will fail.

Step 2: Customize the javascript call

We will use the afterShow attribute to hook our custom logic to each lightboxed image.

$(document).ready(function() {
  if ($('.my-image-class'.length != 0)) {
    $(".my-image-class").fancybox({
      prevEffect: 'none',
      nextEffect: 'none',
      padding: 0, // padding and curvy borders are, like, so 2009
      margin: 0,
      minWidth: 480, // You may want to tweak this depending on your application
      autoScale: false,
      wrapCSS: 'productviewer', // allows us to use custom styles
      // Initialize extended product view
      afterShow : function() {
        // make image nagivation smaller, takes too much space
        $(".fancybox-next").css('width',40);
        $(".fancybox-prev").css('width',40);
        
        // determine original image size
        var img = $("img.fancybox-image");
        // We determine the actual image size by initializing a new image element
        // and retrieving its dimensions
        var t = new Image();
        t.src = (img.getAttribute ? img.getAttribute("src") : false) || img.src;
        var fullW = img.width();
        var fullH = img.height();
        
        // determine visible container height
        var containerW = $('.fancybox-inner').width();
        var containerH = $('.fancybox-inner').height();
        
        // Position the element in the centre of the container
        img.css({
          position: 'absolute',
          top: -((img.height() / 2)) + (containerH / 2),
          left: -((img.width() / 2)) + (containerW / 2)
        });
        
        
        // Hook up mousemove to scroll the image accordingly
        $(".fancybox-image").mousemove(function(e){
          var mouseX = e.pageX - img.offset().left; 
          var mouseY = e.pageY - img.offset().top;
          // determine the new position based on cursor position
          var posX = (Math.round((mouseX/fullW)*100)/100) *  (fullW-containerW);
          var posY = (Math.round((mouseY/fullH)*100)/100) * (fullHeight-containerH);
          // perform a scroll if we are within bounds.
          if (posX + containerW <= fullW)
            img.css({'left': '-' + posX + 'px'})
          if (posY + containerH <= fullHeight)
            img.css({'top': '-' + posY + 'px'})
        });
      }
    });
  };
});

You can view a working example of this script on this page.

Categories: Code.

Tags: , , ,

Chamber Music by Ari Benjamin Meyers

Chamber Music Build Snapshot

Early build stages: A bunch of XBees are happily chattering away while I am freaking out over interrupt-driven code going sour.

Today was the first on-site test for the system I am building for Ari’s piece ‘Chamber Music‘, opening on April 27th at the Berlinische Gallerie Museum For Modern Art.

Thankfully everything went smoothly, and I was¬†once again surprised by just how much more rewarding it feels to ¬†build something tangible that interacts with the world in a real way rather than writing code that runs solely in an abstract ‘cloud’.

The piece’s infrastructure includes an Arduino-based media player, sensors and radio transmitters. If you are in Berlin on the opening day, come by and say hi.

The piece will be on display until 28/04/2014.

 

Categories: Uncategorized.

Tags: , ,