Grid.frink

View or download Grid.frink in plain text format


/** This class makes a grid with coordinates around a graphics object.

    To use it, create a graphics object g with natural coordinates.  Completely
    draw into that graphics object.  Then, you can create another graphics
    object with grid lines using the following:

   grid = new Grid
   grid.auto[g]
   g.add[grid.getGrid[]]

   This creates automatic coordinate lines around the graphics object.  You
   can create a grid with more control using the functions provided below.  See
   the auto function to see some typical calls.

   You can also manually add grid lines corresponding to calendar dates.  See
   sample usage in manhattanhengemoon.frink .
*/


class Grid
{
   /** The graphics object this will use to render to. */
   var g2 = new graphics

   /** A flag indicating we've initialized a font. */
   var fontInitialized = false

   /** Static fields for Calendar fields. */
   class var YEAR         = staticJava["java.util.Calendar", "YEAR"]
   class var MONTH        = staticJava["java.util.Calendar", "MONTH"]
   class var DAY_OF_MONTH = staticJava["java.util.Calendar", "DAY_OF_MONTH"]
   class var HOUR_OF_DAY  = staticJava["java.util.Calendar", "HOUR_OF_DAY"]
   class var MINUTE       = staticJava["java.util.Calendar", "MINUTE"]
   class var SECOND       = staticJava["java.util.Calendar", "SECOND"]
   class var MILLISECOND  = staticJava["java.util.Calendar", "MILLISECOND"]
   
   
   /** Make an automatic grid. */
   auto[g is graphics, enclose=false] :=
   {
      autoHorizontalLines[g, enclose]
      autoVerticalLines[g, enclose]
      color[0,0,0]
      autoHorizontalLabels[g, enclose]
      autoVerticalLabels[g, enclose]
   }

   
   /** Make automatic horizontal lines */
   autoHorizontalLines[g is graphics, enclose=false, maxMajorTicks=10, maxMinorTicks=40] :=
   {
      [left, top, right, bottom] = getBoundingBox[g]
      if left == undef or top == undef
         return
      
      width = right-left
      height = bottom-top

      if (maxMinorTicks > 0) and (height != 0 height)
      {
         color[.5,.5,.5,.2]
         minorTick = calcAutoTickSize[height, maxMinorTicks]
         makeHorizontalLines[g, minorTick, enclose]
      }

      if (maxMajorTicks > 0) and (height != 0 height)
      {
         color[.5,.5,.5,.3]
         majorTick = calcAutoTickSize[height, maxMajorTicks]
         if (majorTick < 1)
            majorTick = 1
         
         makeHorizontalLines[g, majorTick, enclose]
      }
   }

   
   /** Make automatic horizontal labels */
   autoHorizontalLabels[g is graphics, enclose=false, unit=-1, maxMajorTicks=10, maxMinorTicks=40, formatFunc=undef] :=
   {
      [left, top, right, bottom] = getBoundingBox[g]
      if left == undef or top == undef
         return

      width = right-left
      height = bottom-top

      // TODO:  Set label colors
      if (maxMinorTicks > 0) and (height != 0 height)
      {
         minorTick = calcAutoTickSize[height, maxMinorTicks]
         makeHorizontalLabels[g, minorTick, unit, enclose, formatFunc]
      }
   }

   
   /** Make automatic vertical lines */
   autoVerticalLines[g is graphics, enclose=false, maxMajorTicks=10, maxMinorTicks=40] :=
   {
      [left, top, right, bottom] = getBoundingBox[g]
      if left == undef or top == undef
         return

      width = right-left
      height = bottom-top

      // TODO:  Set line colors
      if (maxMinorTicks > 0) and (width != 0 width)
      {
         minorTick = calcAutoTickSize[width, maxMinorTicks]
         makeVerticalLines[g, minorTick, enclose]
      }

      if (maxMajorTicks > 0) and (width != 0 width)
      {
         majorTick = calcAutoTickSize[width, maxMajorTicks]
         if (majorTick < 1)
            majorTick = 1
         makeVerticalLines[g, majorTick, enclose]
      }
   }

   /** Make automatic vertical labels */
   autoVerticalLabels[g is graphics, enclose=false, unit=1, maxMajorTicks=10, maxMinorTicks=40, formatFunc=undef] :=
   {
      [left, top, right, bottom] = getBoundingBox[g]
      if left == undef or top == undef
         return

      width = right-left
      height = bottom-top

      // TODO:  Set line colors
      if (maxMinorTicks > 0) and (width != 0 width)
      {
         minorTick = calcAutoTickSize[width, maxMinorTicks]
         makeVerticalLabels[g, minorTick, unit, enclose, formatFunc]
      }
   }

   
   /** Makes horizontal lines in the current drawing color.

       args:
         g:        An already-generated graphics object that we're going to
                   generate lines for.
         stepSize: the interval between lines.  This should have the same
                   dimensions as the vertical axis of the graphics object
         enclose:  A boolean flag.  If true, this makes at least one line
                   above and below the object, enclosing it.
   */

   makeHorizontalLines[g is graphics, stepSize, enclose = false] :=
   {
      [west, north, east, south] = getBoundingBox[g]
      if left == undef or top == undef
         return

      if enclose
      {
         southest = ceil[south, stepSize]
         northest = floor[north, stepSize]
      } else
      {
         southest = floor[south, stepSize]
         northest = ceil[north, stepSize]
      }
      
      for lat = northest to southest + 1/2 stepSize step stepSize
         g2.line[west, lat, east, lat]
   }

   
   /** Makes labels on the sides of the grid.

       args:
         g:        An already-generated graphics object that we're going to
                   generate labels for.
         stepSize: the interval between lines.  This should have the same
                   dimensions as the vertical axis of the graphics object
         enclose:  A boolean flag.  If true, this makes at least one label
                   above and below the object, enclosing it.
   */

   makeHorizontalLabels[g is graphics, stepSize, unit, enclose = false, formatFunc=undef] :=
   {
      if ! fontInitialized
         initializeFont[g]
      
      [west, north, east, south] = getBoundingBox[g]
      if left == undef or top == undef
         return

      if enclose
      {
         southest = ceil[south, stepSize]
         northest = floor[north, stepSize]
      } else
      {
         southest = floor[south, stepSize]
         northest = ceil[north, stepSize]
      }

      digits = 0
      if abs[stepSize] < abs[unit]
         digits = ceil[-log[abs[stepSize]]]
      for lat = northest to southest + 1/2 stepSize step stepSize
      {
         if (formatFunc == undef)
            text = format[lat, unit, digits]
         else
            text = formatFunc[long]
         
         g2.text[" $text", east, lat, "left", "center"]
         g2.text["$text ", west, lat, "right", "center"]
      }
   }

   
   /** Makes vertical lines in the current drawing color.

       args:
         g:        An already-generated graphics object that we're going to
                   generate labels for.
         stepSize: the interval between lines.  This should have the same
                   dimensions as the horizontal axis of the graphics object
         enclose:  A boolean flag.  If true, this makes at least one line
                   to the left and right the object, enclosing it.
   */

   makeVerticalLines[g is graphics, stepSize, enclose = false] :=
   {
//      println["In makeVerticalLines[$stepSize]"]
      
      [west, north, east, south] = getBoundingBox[g]
      if left == undef or top == undef
         return

//      println["West is $west,  east is $east"]

      if enclose
      {
         westest = floor[west, stepSize]
         eastest = ceil[east, stepSize]
      } else
      {
         westest = ceil[west, stepSize]
         eastest = floor[east, stepSize]
      }
      
      for long = westest to eastest + 1/2 stepSize step stepSize
         g2.line[long, north, long, south]
   }

   /** Makes vertical lines for calendar dates/times.  This assumes that
       the horizontal axis is a Julian Day.

       args:
         g:        An already-generated graphics object that we're going to
                   generate labels for.
   
         field:    A field corresponding to one of the fields: YEAR, MONTH,
                   DAY_OF_MONTH, HOUR_OF_DAY, MINUTE, SECOND, MILLISECOND
   
         enclose:  A boolean flag.  If true, this makes at least one line
                   to the left and right the object, enclosing it.
   
         tz:       A string indicating the (optional timezone)
   */

   makeVerticalCalendarLines[g is graphics, field, enclose=false, tz=undef] :=
   {
      [west, north, east, south] = getBoundingBox[g]
      if left == undef or top == undef
         return

      beginDate = JD[west]
      endDate = JD[east]

//      println["West is $west,  east is $east"]

      if enclose
      {
         earliest = beginPlusOffset[beginDate, field, 0]
         latest   = beginPlusOffset[endDate, field, 1]
      } else
      {
         earliest = beginPlusOffset[beginDate,field, 1]
         latest =   beginPlusOffset[endDate, field, 0]
      }

      date = earliest
      while date <= latest
      {
         g2.line[JD[date], north, JD[date], south]
         date = beginPlusOffset[date, field, 1]
      }
   }

   
   /** Makes vertical labels in the current drawing color.

       args:
         g:        An already-generated graphics object that we're going to
                   generate labels for.
         stepSize: the interval between lines.  This should have the same
                   dimensions as the horizontal axis of the graphics object
         enclose:  A boolean flag.  If true, this makes at least one line
                   to the left and right of the object, enclosing it.
   */

   makeVerticalLabels[g is graphics, stepSize, unit, enclose = false, formatFunc=undef] :=
   {
      if ! fontInitialized
         initializeFont[g]
      
      [west, north, east, south] = getBoundingBox[g]
      if west == undef or north == undef
         return

      if enclose
      {
         westest = floor[west, stepSize]
         eastest = ceil[east, stepSize]
      } else
      {
         westest = ceil[west, stepSize]
         eastest = floor[east, stepSize]
      }

      digits = 0
      if abs[stepSize] < abs[unit]
         digits = ceil[-log[abs[stepSize]]]

      // TODO:  Allow specification of rotation
      angle = -90 deg

      // If the 2 axes have different dimensions, there's no way to calculate
      // a useful rotated bounding box.
      if ! (west conforms north)
         angle = 0 deg
      
      quad = round[angle / (90 deg)] mod 4

      for long = westest to eastest + 1/2 stepSize step stepSize
      {
         if (formatFunc == undef)
            text = format[long, unit, digits]
         else
            text = formatFunc[long]

         // Bottom labels
         if quad == 0    // 0 degrees rotation
            g2.text[text, long, south, "center", "top"]
         if quad == 1    // 90 degrees ccw
            g2.text["$text ", long, south, "right", "center", angle]
         if quad == 2    // 180 degrees rotation
            g2.text[text, long, south, "center", "bottom", angle]
         if quad == 3    // 90 degrees cw
            g2.text[" $text", long, south, "left", "center", angle]
         
         // Top labels
         if quad == 0    // 0 degrees rotation
            g2.text[text, long, north, "center", "bottom"]
         if quad == 1    // 90 degrees ccw
            g2.text[" $text", long, north, "left", "center", angle]
         if quad == 2    // 180 degrees rotation
            g2.text[text, long, north, "center", "top", angle]
         if quad == 3    // 90 degrees cw
            g2.text["$text ", long, north, "right", "center", angle]
      }
   }

   
   /** Makes vertical labels for calendar dates/times.  This assumes that
       the horizontal axis is a Julian Day.

       args:
         g:        An already-generated graphics object that we're going to
                   generate labels for.
   
         field:    A field corresponding to one of the fields: YEAR, MONTH,
                   DAY_OF_MONTH, HOUR_OF_DAY, MINUTE, SECOND, MILLISECOND
   
         enclose:  A boolean flag.  If true, this makes at least one line
                   to the left and right the object, enclosing it.
   
         tz:       A string indicating the (optional timezone)
   */

   makeVerticalCalendarLabels[g is graphics, field, enclose=false, tz=undef] :=
   {
      [west, north, east, south] = getBoundingBox[g]
      if left == undef or top == undef
         return

      beginDate = JD[west]
      endDate = JD[east]

      if enclose
      {
         earliest = beginPlusOffset[beginDate, field, 0]
         latest   = beginPlusOffset[endDate, field, 1]
      } else
      {
         earliest = beginPlusOffset[beginDate,field, 1]
         latest =   beginPlusOffset[endDate, field, 0]
      }

      if field == YEAR
         fmt = ###yyyy###
      if field == MONTH
         fmt = ###yyyy-MM###
      if field == DAY_OF_MONTH
         fmt = ###yyyy-MM-dd###
      if field == HOUR_OF_DAY or field == MINUTE
         fmt = ###yyyy-MM-dd HH:mm###
      if field == SECOND
         fmt = ###yyyy-MM-dd HH:mm:ss###
      if field == MILLISECOND
         fmt = ###yyyy-MM-dd HH:mm:ss.SSS ###

      // TODO:  Allow specification of rotation
      angle = -90 deg

      // If the 2 axes have different dimensions, there's no way to calculate
      // a useful rotated bounding box.
      if ! (west conforms north)
         angle = 0 deg
      
      quad = round[angle / (90 deg)] mod 4
      
      date = earliest
      while date <= latest
      {
         jd = JD[date]
         
         if tz
            text = (date -> [fmt, tz])
         else
            text = (date -> fmt)
         
         // Bottom labels
         if quad == 0    // 0 degrees rotation
            g2.text[text, jd, south, "center", "top"]
         if quad == 1    // 90 degrees ccw
            g2.text["$text ", jd, south, "right", "center", angle]
         if quad == 2    // 180 degrees rotation
            g2.text[text, jd, south, "center", "bottom", angle]
         if quad == 3    // 90 degrees cw
            g2.text[" $text", jd, south, "left", "center", angle]
         
         // Top labels
         if quad == 0    // 0 degrees rotation
            g2.text[text, jd, north, "center", "bottom"]
         if quad == 1    // 90 degrees ccw
            g2.text[" $text", jd, north, "left", "center", angle]
         if quad == 2    // 180 degrees rotation
            g2.text[text, jd, north, "center", "top", angle]
         if quad == 3    // 90 degrees cw
            g2.text["$text ", jd, north, "right", "center", angle]

         date = beginPlusOffset[date, field, 1]
      }
   }

   /** Calculates the size of a tick given the span
       (which is either width or height) and the maximum number of ticks.
       This tries to make the ticks a multiple of 10, but if enough ticks fit,
       it will return a multiple of 2 or 5. */

   calcAutoTickSize[span, maxTicks] :=
   {
      tick = 10^ceil[log[span / maxTicks]]
      if (span / tick) * 5 < maxTicks
         tick = tick / 5
      else
         if (span / tick) * 2 < maxTicks
            tick = tick / 2

      return tick        
   }

   
   /** Sets the current drawing color to the specified color. */
   color[c is color] :=
   {
      g2.color[c]
   }

   
   /** Sets the current drawing color to the specified color. */
   color[r, g, b, a=1] :=
   {
      g2.color[r, g, b, a]
   }

   
   /** Sets the font name and height of the font.  This is analogous to
       graphics.font[fontName, height] */

   font[fontName, height] :=
   {
      g2.font[fontName, height]
      fontInitialized = true
   }

   
   /** If the font has not been initialized to something, its dimensions will
       be unknown and labels won't be sized correctly.  This allocates a
       graphics object and tries to guess a reasonable font height based on
       the size of the graphic. */

   initializeFont[g is graphics] :=
   {
      if ! fontInitialized
      {
         [west, north, east, south] = getBoundingBox[g]
         if west == undef or north == undef
            return

         height = (south - north) / 60
         font["Monospaced", height]
         fontInitialized = true
      }
   }

   /** Returns a graphics object for the grid that has been generated. */
   getGrid[] :=
   {
      return g2
   }

   /** Returns a graphics object for the grid that has been generated. */
   getGraphics[] :=
   {
      return g2
   }
}


View or download Grid.frink in plain text format


This is a program written in the programming language Frink.
For more information, view the Frink Documentation or see More Sample Frink Programs.

Alan Eliasen was born 18116 days, 3 hours, 54 minutes ago.