Monthly Archives: April 2013

Perlin Noise and JavaScript

After looking around HTML5 demos, I got interested in the “Perlin Noise”, which provides a natural fluctuation of randomness. This small interest resulted in a week-long struggle… Finally I could make a working example, I’m putting here (and will be focusing on another topic for a while).

Regarding the perline noise generation in JavaScript, the following code was nice and handy.
https://gist.github.com/banksean/304522

Major struggle for me was that I couldn’t find good animation or graphical filter library in JavaScript, though I could find many good ActionScript ones for flash. For example, the following sites have nice one, but I couldn’t find an easy way to reproduce them in JavaScript (I can see some shape or image(sprite) anmation libraries like create.js, enchant.js, etc, though).

The following is a working example and some codes.

screenshot
perline_color
Working Example
http://jsdo.it/parroty/4FMA

Source Code – draw.js

var canvas;
var canvasWidth;
var canvasHeight;
var context;

var SPEED      = 5;
var MATRIX_NUM = 3;

var rectWidth  = 465;
var rectHeight = 465;
var mapSize    = 4;
var unitSize   = 2;

var matrixes;
var imageData;

var count = 0;

function init() {
  canvas = document.getElementById('c');
  document.addEventListener('resize', resize);
  resize();

  initPoints();
  imageData = context.createImageData(rectWidth, rectHeight);
  setInterval(loop, 1000 / 30);
}

function resize() {
  canvasWidth  = canvas.width = window.innerWidth;
  canvasHeight = canvas.height = window.innerHeight;
  context      = canvas.getContext('2d');
}

function initPoints() {
  matrixes = [];
  for(var i = 0; i < MATRIX_NUM; i++) {
    var matrix = new Matrix(rectWidth * mapSize, rectHeight * mapSize);
    var simplexNoise = new SimplexNoise;

    matrix.updatePoints(function(x, y) {
      return Math.floor(simplexNoise.noise(x / 150, y / 150)*128) + 128;
    });

    matrix.setRandomSpeed(SPEED)
    matrix.setUnit(unitSize, unitSize);

    matrixes.push(matrix);
  }
}

function loop() {
  for(var i = 0; i < MATRIX_NUM; i++) {
    matrixes[i].movePoints();
  }

  var data = imageData.data;
  for(var x = 0; x < rectWidth; x++) {
    for(var y = 0; y < rectHeight; y++) {
      var index = (x * rectHeight + y) * 4;

      data[index + 0] = matrixes[0].get(x, y);  // red
      data[index + 1] = matrixes[1].get(x, y);  // green
      data[index + 2] = matrixes[2].get(x, y);  // blue
      data[index + 3] = 100;                    // alpha
    }
  }
  context.putImageData(imageData, 0, 0);

  count += 1;
}

// Init
window.addEventListener ? window.addEventListener('load', init, false) : window.load = init;

Source Code – lib.js

var Point = function(x, y) {
  this.x = x;
  this.y = y;
}

var Matrix = function(width, height) {
  this.width  = width;
  this.height = height;

  this.count  = new Point(0, 0);
  this.speed  = new Point(0, 0);
  this.sign   = new Point(0, 0);
  this.unit   = new Point(1, 1);
  this.offset = new Point(Math.floor(width / 2), Math.floor(height / 2));

  this.points = [];
  for(var i = 0; i < width; i++) {
    this.points.push(new Array(height));
  };
  this.updatePoints(function(x, y){
    return 0;
  });
}

Matrix.prototype = {
  get: function(x, y) {
    var ix = this.getIndex(x + this.offset.x, this.width);
    var iy = this.getIndex(y + this.offset.y, this.height);

    return this.points[ix][iy];
  },

  set: function(x, y, value) {
    var ix = this.getIndex(x, this.width);
    var iy = this.getIndex(y, this.height);

    this.points[ix][iy] = value;
  },

  setSpeed: function(dx, dy) {
    this.speed.x = Math.abs(dx);
    this.speed.y = Math.abs(dy);

    this.sign.x = dx / Math.abs(dx);
    this.sign.y = dy / Math.abs(dy);
  },

  setRandomSpeed: function(max) {
    var rad = (360 * Math.random()) * (Math.PI / 180);
    var sx = Math.sin(rad);
    var sy = Math.cos(rad);

    this.setSpeed(sx, sy);
  },

  setUnit: function(x, y) {
    this.unit.x = x;
    this.unit.y = y;
  },

  getIndex: function(value, border) {
    var offset = Math.abs(value) % (2 * border);
    if(offset >= border) {
      return (2 * border) - offset - 1;
    }
    else {
      return offset;
    }
  },

  updatePoints: function(func) {
    var p = [];
    for(var i = 0; i < this.width; i++) {
      for(var j = 0; j < this.height; j++) {
        p[i * this.height + j] = func.call(null, i, j);
      }
    }

    for(var i = 0; i < this.width; i++) {
      for(var j = 0; j < this.height; j++) {
        this.set(i, j, p[i * this.height + j]);
      }
    }
  },

  eachPoint: function(func) {
    for(var i = 0; i < this.width; i++) {
      for(var j = 0; j < this.height; j++) {
        func.call(null, i, j, this.get(i, j));
      }
    }
  },

  movePoints: function() {
    this.count.x += this.speed.x;
    this.count.y += this.speed.y;

    if(this.count.x >= 1) {
       this.offset.x -= this.sign.x * this.unit.x;
       this.count.x--;
     }

    if(this.count.y >= 1) {
      this.offset.y -= this.sign.y * this.unit.y;
      this.count.y--;
    }
  },

  drawPoints: function() {
    for(var i = 0; i < this.width; i++) {
      var a = []
      for(var j = 0; j < this.height; j++) {
        a.push(("0" + this.get(i, j)).slice(-2));
      }
      console.log(a.join(","));
    }
    console.log("");
  },
}
Advertisements

Three.js – 3D Lineart

Just ported the previosly created Lineart into the Three.js version. It’s getting less time, but still the lack of API reference is a difficulty for me. Once it falls out of the tutorials, it requires diving into the deep Google searching, hmm.

It seems working, but not sure if it’s ok to have multiple geometries. Consolidating them provides better performance?

Screenshot
lineart_3d_paticle

Working Example
http://jsdo.it/parroty/6bZz

Source Code
It’s partial excerpt, which is related to Three.js. The full code can be referred in the above jsdo.it site.

var stats, camera, scene, renderer;
var lineart;
var geometries, materials;

var windowHalfX = window.innerWidth  / 2;
var windowHalfY = window.innerHeight / 2;

// parameters
var CAMERA_SPEED = 0.05;
var MOUSE_RANGE_MULTIPLY = 3;
var FRAME_RATE_WAIT = 1000 / 30;
var INITIAL_CAMERA_ZPOS = 1250;
var INITIAL_CAMERA_YPOS = 1250;
var BOX_SIZE = 1000;
var PARTICLE_SIZE = 10;
var PARTICLE_DURATION = 100;
var PARTICLES_COUNT = 10;
var PARTICLES_SPEED_MAX = 3;

var particles_meshes = [];
var particles_geos   = [];
var particles_life   = [];

var count = 0;
var mouseX = -300;
var mouseY = -300;

var canvas;
var context;
var texture;

init();
animate();

function init() {
  var container = document.createElement('div');
  document.body.appendChild(container);

  //camera and scene
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000);
  camera.position.z = INITIAL_CAMERA_ZPOS;
  camera.position.y = INITIAL_CAMERA_YPOS;
  scene = new THREE.Scene();

  //lineart object
  Lineart.initScreenSize(BOX_SIZE, BOX_SIZE, BOX_SIZE);
  lineart = new Lineart();

  //geometry for cube box
  cubeGeometry = new THREE.CubeGeometry(BOX_SIZE, BOX_SIZE, BOX_SIZE);
  material = new THREE.MeshBasicMaterial({
    color: 0xffffff, wireframe: true, transparent: true, opacity: 0.4, side: THREE.DoubleSide});
  mesh = new THREE.Mesh(cubeGeometry, material);
  scene.add(mesh);

  //geometry for lines
  geometries = [], materials = [];
  var polygons = lineart.polygons;
  for(var i = 0; i < polygons.length; i++) {
    var points = polygons[i].points;
    var pm = new THREE.LineBasicMaterial({color: 0x000000, opacity: 1.0, linewidth: 1});
    var pg = new THREE.Geometry();
    for(var j = 0; j < points.length + 1; j++) {
      pg.vertices.push(new THREE.Vector3(0, 0, 0));
    }
    scene.add(new THREE.Line(pg, pm));

    geometries.push(pg);
    materials.push(pm);
  }

  //init texture for particles
  canvas = document.createElement('canvas');
  canvas.width = 100;
  canvas.height = 100;

  context = canvas.getContext('2d');
  context.fillStyle = 'black';
  context.fillRect( 0, 0, 100, 100 );
  context.fillStyle = 'white';
  context.beginPath();
  context.arc(50, 50, 50, 0, 2 * Math.PI, false);
  context.stroke();
  context.closePath();
  context.fill();

  texture = new THREE.Texture(canvas);
  texture.needsUpdate = true;

  //renderer
  renderer = new THREE.WebGLRenderer({antialias: true});
  renderer.setSize(window.innerWidth, window.innerHeight);
  container.appendChild(renderer.domElement);

  //stats
  stats = new Stats();
  stats.domElement.style.position = 'absolute';
  stats.domElement.style.top = '0px';
  container.appendChild(stats.domElement);

  //events
  document.addEventListener('mousemove', onDocumentMouseMove, false);
  window.addEventListener('resize', onWindowResize, false);
}

function onWindowResize() {
  windowHalfX = window.innerWidth  / 2;
  windowHalfY = window.innerHeight / 2;

  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth, window.innerHeight);
}

function onDocumentMouseMove(event) {
  mouseX = event.clientX - windowHalfX;
  mouseY = event.clientY - windowHalfY;
}

function generateParticles(x, y, z) {
  var geo = new THREE.Geometry();
  geo.direction = [];
  geo.speed = [];

  for(var i = 0; i < PARTICLES_COUNT; i++) {
    geo.vertices.push(new THREE.Vector3(x - 0.5 * BOX_SIZE, y - 0.5 * BOX_SIZE, z - 0.5 * BOX_SIZE));
    geo.direction.push(new THREE.Vector3(Math.random()*2 - 1, Math.random()*2 - 1, Math.random()*2 - 1));
    geo.speed.push(Math.random()*PARTICLES_SPEED_MAX);

    var color = new THREE.Color(0xFFFFFF);
    color.setRGB(Math.random(), Math.random(), Math.random());
    geo.colors.push(color);
  }

  var material = new THREE.ParticleBasicMaterial({
    map: texture,
    size: PARTICLE_SIZE,
    blending: THREE.AdditiveBlending,
    depthTest: false,
    vertexColors: true,
    transparent: true
  });

  var ps = new THREE.ParticleSystem(geo, material);
  ps.sortParticles = true;
  scene.add(ps);

  particles_meshes.push(ps);
  particles_geos.push(geo);
  particles_life.push(0);
}

function updateParticles() {
  for(var i = 0; i < particles_geos.length; i++) {
    var geo = particles_geos[i];

    for(var j = 0; j < PARTICLES_COUNT; j++) {
      var position  = geo.vertices[j];
      var direction = geo.direction[j];
      var speed     = geo.speed[j];

      position.x += direction.x * speed;
      position.y += direction.y * speed;
      position.z += direction.z * speed;
    }
  }
}

function animate() {
  setTimeout(function() {
    requestAnimationFrame(animate);
  }, FRAME_RATE_WAIT);
  render();
  stats.update();
}

function render() {
  camera.position.x += ( mouseX * MOUSE_RANGE_MULTIPLY - camera.position.x) * CAMERA_SPEED;
  camera.position.y += (-mouseY * MOUSE_RANGE_MULTIPLY - camera.position.y) * CAMERA_SPEED;
  camera.lookAt(scene.position);

  lineart.move();
  var polygons = lineart.polygons;
  for(var i = 0; i < polygons.length; i++) {
    var polygon = polygons[i];
    var color   = polygon.color.to_i();
    var points  = polygon.points;

    for(var j = 0; j < points.length + 1; j++) {
      var v = geometries[i].vertices[j];
      var p = points[j % points.length];

      v.x = p.x - 0.5 * BOX_SIZE;
      v.y = p.y - 0.5 * BOX_SIZE;
      v.z = p.z - 0.5 * BOX_SIZE;
    }
    geometries[i].verticesNeedUpdate = true;
    materials[i].color.setHex(color);
  }

  while(PointQueue.size() > 0) {
    var p = PointQueue.pop();
    generateParticles(p[0], p[1], p[2]);
  }

  updateParticles();
  renderer.render(scene, camera);

  var dieCount = 0
  for(var i = 0; i < particles_life.length; i++) {
    particles_life[i] += 1;
    if(particles_life[i] > PARTICLE_DURATION) {
      dieCount += 1;
    }
  }

  for(var i = 0; i < dieCount; i++) {
    var mesh = particles_meshes.shift();
    var geos = particles_geos.shift();
    var life = particles_life.shift();
    scene.remove(mesh);
  }
}

Three.js – Drawing Waves

http://mrdoob.github.io/three.js/

As part of the JavaScript study, I made some trials for 3D rendering using Three.js library.

In the beginning, I had some difficulty to understand the API usage, maybe due to the lack of its documentation. But, by looking through the examples and tutorials, I could build up some basic 3D shape drawings on the HTML canvas.

I’m a little surprised by the power of WebGL. Though I once had seen several graphical WebGL demos, it seemed to require very complex coding/processing. Three.js API seems simple enough to use even for novice user like myself. It may require more efforts to create complex objects and consider performance tuning, but I didn’t have to struggle too much at least for the basic one.

The following is some trials I made, and working example on jsdo.it.

Screenshot
threejswave

Working Example
http://jsdo.it/parroty/2Rix

Source Code

var stats, camera, scene, renderer;
var geometry, points = [];

var windowHalfX = window.innerWidth  / 2;
var windowHalfY = window.innerHeight / 2;

// parameters
var SEPARATION     = 25;
var AMOUNTX        = 50;
var AMOUNTY        = 50;
var INITIAL_FACTOR = 1.0;
var WAVE_HEIGHT    = 200;
var WAVE_SPEED     = 0.2;
var ROTATION_SPEED = 0.1;
var DAMP_SPEED     = 0.005;
var CAMERA_SPEED   = 0.05;

var rotation = 0;
var factor   = INITIAL_FACTOR;
var mouseX   = -300;
var mouseY   = -300;
var initial_camera_zpos = 500;
var initial_camera_ypos = 500;

init();
animate();

function init() {
  var container = document.createElement('div');
  document.body.appendChild(container);

  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000);
  camera.position.z = initial_camera_zpos;
  camera.position.y = initial_camera_ypos;

  scene = new THREE.Scene();

  var width  = SEPARATION * AMOUNTX;
  var height = SEPARATION * AMOUNTY;

  geometry = new THREE.PlaneGeometry(width, height, AMOUNTX, AMOUNTY);
  var ground = new THREE.Mesh(geometry);

  calculateInitialPoints();
  updatePoints();

  ground.rotation.x = Math.PI / -2;
  scene.add(ground);

  renderer = new THREE.CanvasRenderer();
  renderer.setSize(window.innerWidth, window.innerHeight);
  container.appendChild(renderer.domElement);

  stats = new Stats();
  stats.domElement.style.position = 'absolute';
  stats.domElement.style.top = '0px';
  container.appendChild(stats.domElement);

  document.addEventListener('mousemove', onDocumentMouseMove, false);
  document.addEventListener('mousedown', onMouseDown, false);
  window.addEventListener('resize', onWindowResize, false);
}

function calculateInitialPoints() {
  for(var i = 0; i < geometry.vertices.length; i++) {
    var v = geometry.vertices[i];
    var x = (v.x / SEPARATION) * WAVE_SPEED;
    var y = (v.y / SEPARATION) * WAVE_SPEED;
    points[i] = WAVE_HEIGHT * (Math.cos(x*x + y*y) / Math.sqrt(x*x + y*y + 0.25));
  }
}

function updatePoints() {
  for(var i = 0; i < geometry.vertices.length; i++) {
    var v = geometry.vertices[i];
    v.z = points[i] * Math.cos(rotation) * factor;
  }
}

function calculateNextParameters() {
  rotation += ROTATION_SPEED;
  if(factor > 0) {
    factor -= DAMP_SPEED;
  }
}

function onWindowResize() {
  windowHalfX = window.innerWidth  / 2;
  windowHalfY = window.innerHeight / 2;

  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth, window.innerHeight);
}

function onDocumentMouseMove(event) {
  mouseX = event.clientX - windowHalfX;
  mouseY = event.clientY - windowHalfY;
}

function onMouseDown(event) {
  factor = INITIAL_FACTOR;
}

function animate() {
  requestAnimationFrame(animate);
  render();
  stats.update();
}

function render() {
  camera.position.x += ( mouseX - camera.position.x) * CAMERA_SPEED;
  camera.position.y += (-mouseY - camera.position.y) * CAMERA_SPEED;
  camera.lookAt(scene.position);

  updatePoints();
  renderer.render(scene, camera);
  calculateNextParameters();
}

PeepCode – Refactoring (Play by Play)

https://peepcode.com/products/play-by-play-benorenstein

Just completed the recent episode of peepcode – play-by-play. Nice one again. Many of the topics covered might be what I previously saw somewhere. But, revisiting them with actual examples along with experts thoughts, is very helpful to get better understanding of them.

For example, spending minimum time in “red” testing status is often preached as good practice. In the screencast, test execution is actually repeated in every small code changes. Also, sometimes this step discovers the simple mistakes which itself can be easily fixed at the moment. This kind of actual process provides more confidence in that practice and it leads to the motivation for having more test coverage with fast executing tests.

It also covers some of the concept of programming techniques like the followings.

The basic idea is that at the core of your system use value objects, immutable data, and simple functions that return data then wrap an imperative shell around it.

You should be particularly shy with your data. The strength of OO comes from when you use an object to package up data with its operations. You want to expose as little of that data directly as possible.

I also watched a destroyallsoftware episode – functional-core-imperative-shell. It was a nice one too. It would be a very good advantage to learn from examples rather than just a theory.

Lastly, I didn’t know the following quote mentioned in the screencast (it’s slightly differently mentioned, but it may be this?). It’s a nice one too, again.

As Kent Beck says: “I’m not an excellent programmer, I’m just a good one with excellent habits.”

Ruboto – Flickr Photo Search

A little more trial on Ruboto. Initially, I just wanted to create a sample app to use ListView which displays some information acquired from certain online service (ex. twitter). However, many of the online APIs use SSL and/or OAuth kind of encryptions/authentications, and it doesn’t work well with Ruboto. After struggles, I could finally make it work with simple flickr API call. It provides nice and simple API, and I didn’t have to use standard ruby-gem (which I couldn’t use with Ruboto). Anyway, I could learn some Android’s GUI layout defined using XML, and some API calls like AsyncTask. It was a good exercise.

Ruboto is an interesting framework with powerful JRuby features. Though it has limitation in performance, many of the Android API works fine by just importing Java library through “java_import” keyword of JRuby. It’s a little tough to track the trace of “adb logcat” results, as it involves mixtures of Java/Ruby stack traces. But many of the cases, it seems to provide enough information for troubleshooting.

The following is some excerpt from my trial. Entire code is in my GitHub repo (https://github.com/parroty/ruboto_flickr).

flickr1

flickr2

ruboto_flickr_activity.rb

require 'ruboto/widget'
require "ruboto/util/stack"
with_large_stack {
  require 'flickr_reader'
}

java_import "android.widget.ArrayAdapter"
java_import "android.widget.ListView"
java_import "android.graphics.drawable.Drawable"
java_import "android.os.AsyncTask"
java_import "android.app.ProgressDialog"
java_import "android.content.Context"
java_import "android.content.DialogInterface"
java_import "android.util.Log"
java_import "java.net.URL"

ruboto_import_widgets :Button, :LinearLayout, :TextView

IMAGE_PER_PAGE = 10

class RubotoFlickrActivity
  def on_create(bundle)
    super
    setTitle 'Flickr Searcher'
    self.setContentView(Ruboto::R::layout::activity_main)

    view = findViewById(Ruboto::R::id::list_view)
    view.setAdapter(IconicAdapter.new(self, []))
    view.setScrollingCacheEnabled(false)

    btn = findViewById(Ruboto::R::id::search_button)
    btn.setOnClickListener(MyOnClickListner.new(self))
  end

  def update_list_view(search_word)
    ImageCache.clear
    view = findViewById(Ruboto::R::id::list_view)
    task = SearchTask.new(self, view, search_word)
    task.execute
  end
end

class MyOnClickListner
  def initialize(activity)
    @activity = activity
  end

  def onClick(view)
    text_view = @activity.findViewById(Ruboto::R::id::search_text)
    @activity.update_list_view("#{text_view.text}")
  end
end

class IconicAdapter < ArrayAdapter
  def initialize(activity, items)
    @activity = activity
    @items    = items

    super(@activity, Ruboto::R::layout::row, Ruboto::R::id::title, @items.map(&:headline))
  end

  def getView(position, convert_view, parent)
    row  = super
    item = @items[position]

    view = row.findViewById(Ruboto::R::id::icon)
    task = ImageLoadTask.new(@activity, self, item, view)
    task.execute

    content_view = row.findViewById(Ruboto::R::id::content)
    content = "#{item.info.description}\n\n#{item.info.url}"
    content_view.setText(content)

    row
  end

  def fetch_image(address)
    input_stream = URL.new(address).getContent
    drawable = Drawable.createFromStream(input_stream, "")
    input_stream.close

    drawable
  end
end

class SearchTask < AsyncTask
  PROGRESS_MAX = 100

  def initialize(context, view, search_word)
    super()
    @context = context
    @view = view
    @search_word = search_word
  end

  def onPreExecute
    @dialog = ProgressDialog.new(@context)
    @dialog.setTitle("Please wait for images to load")
    @dialog.setMessage("Searching flickr ...")
    @dialog.setProgressStyle(ProgressDialog::STYLE_HORIZONTAL)
    @dialog.setCancelable(false)
    @dialog.setMax(PROGRESS_MAX)
    @dialog.setProgress(0)
    @dialog.show
  end

  def onProgressUpdate(values)
    @dialog.setProgress(values.first)
  end

  def doInBackground(param)
    with_large_stack {
      reader = FlickrReader.new
      reader.search(:tag => @search_word, :per_page => IMAGE_PER_PAGE) do |index|
        publishProgress((index + 1) * (PROGRESS_MAX / IMAGE_PER_PAGE))
      end
    }
  end

  def onPostExecute(items)
    @dialog.dismiss
    @view.setAdapter(IconicAdapter.new(@context, items))
  end
end

class ImageLoadTask < AsyncTask
  def initialize(activity, adapter, item, view)
    super()
    @activity = activity
    @adapter  = adapter
    @item     = item
    @view     = view
  end

  def doInBackground(param)
    url = @item.small_image_url
    ImageCache.get(url) || ImageCache.put(url, @adapter.fetch_image(url))
  end

  def onPostExecute(param)
    @view.setImageDrawable(param)
  end
end

class ImageCache
  @@image_hash = {}

  def self.put(key, image)
    @@image_hash[key] = image
  end

  def self.get(key)
    @@image_hash[key]
  end

  def self.clear
    @@image_hash = {}
  end
end

flickr_reader.rb

require 'open-uri'
require 'rexml/document'
require 'cgi'

class FlickrReader
  def search(options)
    photos_xml = API.search(options[:tag], options[:per_page])

    index = 0
    photos = Parser.parse_photos_search(photos_xml)
    photos.each do |photo|
      info_xml = API.get_info(photo.id)
      photo.info = Parser.parse_photos_get_info(info_xml)
      yield index if block_given?
      index += 1
    end
    photos
  end
end

class API
  FLICKR_API_URL = "http://www.flickr.com/services/rest/?api_key=%s&method=%s&%s"
  ATTRIBUTION_LICENSE = '4'

  def self.search(tag, per_page = 10)
    exec('flickr.photos.search', 'tags' => tag, 'license' => ATTRIBUTION_LICENSE, 'per_page' => per_page.to_s)
  end

  def self.get_info(photo_id)
    exec('flickr.photos.getInfo', 'photo_id' => photo_id.to_s)
  end

private
  def self.exec(method_name, arg_map = {}.freeze)
    args = arg_map.collect do |k,v|
      CGI.escape(k) << '=' << CGI.escape(v)
    end.join('&')

    if ENV['FLICKR_API_KEY']
      api_key = ENV['FLICKR_API_KEY']
    else
      require 'flickr_api_key'
      api_key = FLICKR_API_KEY
    end

    url = FLICKR_API_URL % [api_key, method_name, args]
    REXML::Document.new(open(url).read)
  end
end

class Parser
  def self.parse_photos_search(xml)
    list = []
    REXML::XPath.each(xml, '//photo') do |elem|
      photo = Photo.new
      photo.server = elem.attribute('server').to_s
      photo.id     = elem.attribute('id').to_s
      photo.secret = elem.attribute('secret').to_s
      photo.title  = elem.attribute('title').to_s

      list << photo
    end
    list
  end

  def self.parse_photos_get_info(xml)
    info = PhotoInfo.new
    info.description = REXML::XPath.first(xml, '//description').text || ""
    info.url         = REXML::XPath.first(xml, '//url').text || ""
    info.owner       = REXML::XPath.first(xml, '//owner').attribute('username').to_s
    info
  end
end

class Photo
  attr_accessor :server, :id, :secret, :title, :info

  def small_image_url
    "http://static.flickr.com/#{@server}/#{@id}_#{@secret}_m.jpg"
  end

  def info=(info)
    @info = info
  end

  def headline
    if @info && @info.owner
      "#{title} by #{@info.owner}"
    else
      title
    end
  end

  def to_s
    headline
  end
end

class PhotoInfo
  attr_accessor :description, :url, :owner
end

Reading Seven Databases in Seven Weeks

http://www.amazon.com/Seven-Databases-Weeks-Modern-Movement/dp/1934356921

Just finished reading “Seven Databases in Seven Weeks: A Guide to Modern Databases and the NoSQL Movement”. Though I haven’t worked much on databases previosuly, recent transitions in database world (NoSQL, etc.) sounds interesting (or a little scarey). That made me come up with a mind to study some. This book well covers different types of databases and explans the advantages/disadvantages of each type.

Some notes are,

  • Relational Database (e.g. PostgreSQL)
    • When the layout of the data is known in advance, it works very well. However, if the data structure is highly variable or deeply hierarchical, it doesn’t fit.
  • Key Value (e.g. Riak, Redis)
    • It’s horizontally scalable and fast, due to it’s simplicity in data structure. However, it often lacks indexing and scanning and can result in poor performance, if the required operations are more than CRUD (create, read, update, delete) oeprations.
  • Document (e.g. MongoDB, CouchDB)
    • it is suited to problems involving highly variable domains. Also, it maps well with object-oriented models. However, it lacks some capabilities that highly normalized rational databases provides, in return for its flexibility.
  • Columnar (e.g. HBase)
    • Its focus is horizontal scalability and works well for the “Big-Data” problem, as Facebook did for their messaging infrastructure. However, as similar as relational database, schema should be well designed in advance. Otherwise, it may not be the good fit.

Also, I found interesting that RDBMS like PostgreSQL has many nice features like the following.

http://www.postgresql.org/docs/devel/static/textsearch-controls.html
http://www.postgresql.org/docs/9.2/static/pgtrgm.html
http://www.postgresql.org/docs/9.2/static/queries-with.html

It’s not related to this book, but I also had a chance to watch Postgres The Bits You Haven’t Found, Heroku’s Peter van Hardenberg at Waza 2013, and it was a nice one too.