     quadKey = @tile_xyz_to_quadkey(x, y, z)
     return @url.replace("{QUADKEY}", quadKey)
 
-
 class TileLayerView extends Glyph.View
 
+  trace: (msg) ->
+    console.warn "TileLayerView :: " + msg.toString()
+
+  _create_tile_provider: (provider_type, service_url, tile_size) ->
+    return new QUADKEYTileProvider(service_url, tile_size) if(provider_type.toLowerCase() == 'quadkeytileprovider')
+    return new TMSTileProvider(service_url, tile_size) if(provider_type.toLowerCase() == 'tmstileprovider')
+    return new WMTSTileProvider(service_url, tile_size)
+
   _index_data: () ->
+    @trace("._index_data()")
     @_xy_index()
 
+  get_extent: () ->
+    return [@x_range.get('start'), @y_range.get('start'), @x_range.get('end'), @y_range.get('end')]
+
+  _on_dblclick: (e) =>
+    extent = @get_extent()
+    [xmin, xmax, ymin, ymax] = extent
+    zoom_level = @tile_provider.get_level_by_extent(extent, @map_frame.get('height'), @map_frame.get('width'))
+
+    rect = @map_canvas.canvas.getBoundingClientRect()
+    x = e.clientX - rect.left
+    #TODO: y-dim is not perfect.  zooming doesn't seem completely correct
+    y = e.clientY - rect.top - (@map_frame.get('top') - @map_frame.get('height'))
+    y = @map_frame.get("height") - y
+
+    x_meters = @x_mapper.map_from_target(x)
+    y_meters = @y_mapper.map_from_target(y)
+
+    x_offset = Math.abs((@x_range.get('end') - @x_range.get('start')) / 2)
+    y_offset = Math.abs((@y_range.get('end') - @y_range.get('start')) / 2)
+
+    updated_extent =  [x_meters - x_offset, y_meters - y_offset, x_meters + x_offset, y_meters + y_offset]
+
+    if e.shiftKey
+      zoom_level = zoom_level - 1
+    else
+      zoom_level = zoom_level + 1
+
+    new_extent = @tile_provider.snap_to_zoom(updated_extent, @map_frame.get('height'), @map_frame.get('width'), zoom_level)
+    @x_range.set('start', new_extent[0])
+    @y_range.set('start', new_extent[1])
+    @x_range.set('end', new_extent[2])
+    @y_range.set('end', new_extent[3])
+
   _set_data: () ->
-    @need_load = (true for img in @url)
-    @loaded = (false for img in @url)
-
-    @_xy_index()
+    @trace("._set_data()")
+    @tile_provider = @_create_tile_provider(@provider_type[0], @url[0], @tile_size[0])
+    @pool = new ImagePool()
+    @initial_extent = [-20037508.34, -20037508.34, 20037508.34, 20037508.34]
+    @map_plot = @renderer.plot_view.model
+    @map_canvas = @renderer.plot_view.canvas_view.ctx
+    @map_frame = @renderer.plot_view.frame
+
+    @x_range = @map_plot.get('x_range')
+    @x_mapper = this.map_frame.get('x_mappers')['default']
+
+    @y_range = @map_plot.get('y_range')
+    @y_mapper = this.map_frame.get('y_mappers')['default']
+
+    @x_range.set('start', @initial_extent[0])
+    @y_range.set('start', @initial_extent[1])
+    @x_range.set('end', @initial_extent[2])
+    @y_range.set('end', @initial_extent[3])
+
+    @renderer.plot_view.$el.dblclick(@_on_dblclick)
 
   _map_data: () ->
-    @sw = @sdist(@renderer.xmapper, @x, @tile_size, 'edge', @mget('dilate'))
-    @sh = @sdist(@renderer.ymapper, @y, @tile_size, 'edge', @mget('dilate'))
+    @trace("._map_data()")
+    @sw = @sdist(@renderer.xmapper, @x, @mget('tile_size'), 'edge', @mget('dilate'))
+    @sh = @sdist(@renderer.ymapper, @y, @mget('tile_size'), 'edge', @mget('dilate'))
+
+  _on_tile_load: (e) =>
+    @tile_provider.tiles[e.target.cache_key] = {img: e.target, tile_coords: e.target.tile_coords, bounds: e.target.bounds, current: true}
+    @_render_tile(e.target.cache_key)
+
+  _on_tile_error: (e) =>
+    @trace("._on_tile_error()")
+    return ''
+
+  _is_valid_tile: (x, y, z) ->
+
+    #is tile currently on screen?
+    return true
+
+  _create_tile: (x, y, z, bounds) ->
+    tile = @pool.pop()
+    tile.onload = @_on_tile_load
+    tile.onerror = @_on_tile_error
+    tile.alt = ''
+    tile.src = @tile_provider.get_image_url(x, y, z)
+    tile.tile_coords = [x, y, z]
+    tile.bounds = bounds
+    tile.x_coord = bounds[0]
+    tile.y_coord = bounds[3]
+    tile.cache_key = @tile_provider.tile_xyz_to_key(x, y, z)
+    return tile
 
   _render: (ctx, indices, {url, image, need_load, sx, sy, sw, sh, angle}) ->
-    for i in indices
-
-      if isNaN(sx[i]+sy[i]+angle[i])
-        continue
-
-      if need_load[i]
-        img = new Image()
-        img.onload = do (img, i) =>
-          return () =>
-            @loaded[i] = true
-            @renderer.request_render()
-
-        img.src = url[i]
-        need_load[i] = false
-      else if @loaded[i]
-        @_render_image(ctx, i, image[i], sx, sy, @tile_size, @tile_size, angle)
-
-  _final_sx_sy: (anchor, sx, sy, sw, sh) ->
-    switch anchor
-      when "top_left"      then [sx       , sy       ]
-      when "top_center"    then [sx - sw/2, sy       ]
-      when "top_right"     then [sx - sw  , sy       ]
-      when "right_center"  then [sx - sw  , sy - sh/2]
-      when "bottom_right"  then [sx - sw  , sy - sh  ]
-      when "bottom_center" then [sx - sw/2, sy - sh  ]
-      when "bottom_left"   then [sx       , sy - sh  ]
-      when "left_center"   then [sx       , sy - sh/2]
-      when "center"        then [sx - sw/2, sy - sh/2]
-
-  _render_image: (ctx, i, image, sx, sy, sw, sh, angle) ->
-
-    anchor = @mget('anchor') or "top_left"
-    [sx, sy] = @_final_sx_sy(anchor, sx[i], sy[i], 256, 256)
-
-    ctx.save()
-
-    ctx.globalAlpha = @mget("global_alpha")
-
-    if angle[i]
-      ctx.translate(sx, sy)
-      ctx.rotate(angle[i])
-      ctx.drawImage(image, 0, 0, 256, 256)
-      ctx.rotate(-angle[i])
-      ctx.translate(-sx, -sy)
-    else
-      ctx.drawImage(image, sx, sy, 256, 256)
-    ctx.restore()
+    @_update()
+
+  _render_tile: (tile_key) ->
+    tile_obj = @tile_provider.tiles[tile_key]
+    if tile_obj?
+      @map_canvas.save()
+      [sx, sy] = @renderer.plot_view.frame.map_to_screen([tile_obj.bounds[0]], [tile_obj.bounds[3]], @renderer.plot_view.canvas)
+      @map_canvas.rect(
+        @map_frame.get('left')+1, @map_frame.get('bottom')+2,
+        @map_frame.get('width')-2, @map_frame.get('height'),
+      )
+      @map_canvas.clip()
+      @map_canvas.drawImage(tile_obj.img, sx[0], sy[0], 256, 256)
+      @map_canvas.restore()
+
+  _update: () =>
+    if window.stop?
+      window.stop()
+    else if document.execCommand?
+      document.execCommand("Stop", false)
+
+    @trace("._render()")
+    extent = @get_extent()
+    zoom_level = @tile_provider.get_level_by_extent(extent, @map_frame.get('height'), @map_frame.get('width'))
+    new_extent = @tile_provider.snap_to_zoom(extent, @map_frame.get('height'), @map_frame.get('width'), zoom_level)
+
+    @x_range.set('start', new_extent[0])
+    @y_range.set('start', new_extent[1])
+    @x_range.set('end', new_extent[2])
+    @y_range.set('end', new_extent[3])
+    tiles = @tile_provider.get_tiles_by_extent(new_extent, zoom_level)
+
+    for t in tiles
+      [x, y, z, bounds] = t
+      if @_is_valid_tile(x, y, z)
+        key = @tile_provider.tile_xyz_to_key(x, y, z)
+        if key of @tile_provider.tiles
+          @_render_tile(key)
+        else
+          @_create_tile(x, y, z, bounds)
 
 class TileLayer extends Glyph.Model
   default_view: TileLayerView
   type: 'TileLayer'
-  tile_width: 256
-  tile_height: 256
   visuals: []
   distances: ['w', 'h']
   angles: ['angle']
-  fields: ['url:string']
+  fields: ['url:string', 'provider_type:string', 'tile_size']
 
   defaults: ->
     return _.extend {}, super(), {
       angle: 0
+      tile_size: 256
       global_alpha: 1.0
     }
 
   display_defaults: ->
     return _.extend {}, super(), {
+      level: 'glyph'
     }
 
 module.exports =

    return @url.replace("{X}", x).replace('{Y}', y).replace("{Z}", z)

class WMTSTileProvider extends MercatorTileProvider

  get_tile_meter_bounds: (x, y, z) ->
    [x, y, z] = @tms_to_wmts(x, y, z)
    return super(x, y, z)

  get_image_url: (x, y, z) ->
    [x, y, z] = @tms_to_wmts(x, y, z)

      else if value == '1'
        tileX |= mask

      else if value == '2 '
        tileY |= mask

      else if value == '3'
