Apple II Weather Display (part 2)

In part one of the Apple II weather display I quickly went over how data is fetched and phrased. Now its time to do something with it in part 2. In the order of functions I do the text parts first, and though its very similar to the process that the radar image goes through, its in monochrome and a bit simpler to explain. Before I go into how it works I should explain how I am dividing the Apple II’s screen.

The high resolution mode on an Apple II gives you 280×160 with 4 lines of text on the bottom, or 280×192 full screen. I will be using 280×192 full screen, because as useful as the text can be, it also would show the endless stream of gibberish while updating, and since the entire color video mode of the Apple II design is a NTSC hack, its not all that pretty on a standard TV while also displaying color graphics. I also divide the screen into “blocks” 35 pixels wide each, this came about due to my encoding system.

Without getting into high ASCII or special characters I decided to use a base 36 type numbering system, we only need 35 unique characters to represent any pixel on a line within a block. The value of 1 means the first pixel in a given block and so on to Z being pixel number 35 in a block, and up to 8 blocks for the entire screen. Block separators and other functions can be sent using lower case characters, making a nice n easy plain text system. For example, if the apple received bbb123Z it means skip to block 4 and place a pixel at 1,2,3, and 35 within that block (and if there is no pixels at all for that given line a n is sent).

Looking at text.lua …

-- Weather Underground to Apple //
-- 2011 Kevin Dady
--
-- Text to Graphics:
-- take text from web.data
-- make text images with image magick
-- phrase *.xpm files to apple //
-- send text images
-- end
text = {}
text.data = {}
text.data.input  = {}
text.data.packed = {{},{},{},{}}
text.data.apl2   = {{},{},{}}

text.createIMG = function()
if web.data[1] == nil then
web.data[1] = "No Advisories"
end
if web.data[3] == nil then
web.data[3] = web.data[7]
end
-- create the bottom text
cmd.imageMGK(" -background black -fill white -font req/VeraMoBd.ttf"..
" -dither none -map req/mono.xpm -size 279x53 -pointsize 11 -gravity West"..
" caption:'".. web.data[2].."\n"..web.data[6].."\n"..web.data[5].."\n"..web.data[3].. "'"..
" temp/textBTM.xpm")
-- create the text for advisories
cmd.imageMGK(" -background black -fill white -font req/VeraMoBd.ttf"..
" -dither none -map req/mono.xpm -size 139x64 -pointsize 11 -gravity Center"..
" caption:'".. web.data[1].."'"..
" temp/textALT.xpm")
-- create the text for temperature
cmd.imageMGK(" -background black -fill white -font req/VeraMoBd.ttf"..
" -dither none -map req/mono.xpm -size 139x74 -pointsize 36 -gravity Center"..
" caption:'".. web.data[4].."'"..
" temp/textTMP.xpm")
end

text.convertIMG = function()
local files  = {"textTMP.xpm","textALT.xpm","textBTM.xpm"}
local footer = {82,72,61}
local width  = {140,140,280}
-- read the files into a table one at a time
for img = 1, 3 do
local file = io.open("temp/".. files[img],"r")
table.insert(text.data.input, img, {})
for line in file:lines() do
table.insert(text.data.input[img], tostring(line))
end
file:close()
-- remove header
for y = 1, 7 do
table.remove(text.data.input[img], 1)
end
-- remove footer
table.remove(text.data.input[img], footer[img] - 7)
-- remove non pixel data
for y = 1, #text.data.input[img] do
text.data.input[img][y] = string.sub(text.data.input[img][y], 2, width[img])
end
end
end

text.sortIMG = function()
local newChar = ""
for img = 1, 3 do
-- need to convert the strings into tables
for y = 1, #text.data.input[img] do
table.insert(text.data.apl2[img], {})
-- for each column in the current row
for x = 1, #text.data.input[img][y] do
-- read the character at that Y,X point
newChar = string.sub(text.data.input[img][y], x,x)
if newChar == "." then
table.insert(text.data.apl2[img][y], x) -- pixel
end
end
end
end
end

text.packageIMG = function()
for img = 1, 2 do
for y = 1, #text.data.input[img] do
local one   = ""
local two   = ""
local three = ""
local four  = ""
for x = 1, #text.data.apl2[img][y] do
if text.data.apl2[img][y][x] <= 35 then
one = one .. string.sub(graphicsKey, text.data.apl2[img][y][x], text.data.apl2[img][y][x])
elseif text.data.apl2[img][y][x] <= 70 then
two = two .. string.sub(graphicsKey, text.data.apl2[img][y][x] - 35, text.data.apl2[img][y][x] - 35)
elseif text.data.apl2[img][y][x] <= 105 then
three = three .. string.sub(graphicsKey, text.data.apl2[img][y][x] - 70, text.data.apl2[img][y][x] - 70)
else
four = four .. string.sub(graphicsKey, text.data.apl2[img][y][x] - 105, text.data.apl2[img][y][x] - 105)
end
end
if one == "" and two == "" and three == "" and four == "" then
table.insert(text.data.packed[img], "n")
else table.insert(text.data.packed[img],one.."b"..two.."b"..three.."b"..four)
end
end
end
for y = 1, #text.data.input[3] do
local one   = ""
local two   = ""
local three = ""
local four  = ""
local five  = ""
local six   = ""
local seven = ""
local eight = ""
for x = 1, #text.data.apl2[3][y] do
if text.data.apl2[3][y][x] <= 35 then
one = one .. string.sub(graphicsKey, text.data.apl2[3][y][x], text.data.apl2[3][y][x])
elseif text.data.apl2[3][y][x] <= 70 then
two = two .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 35, text.data.apl2[3][y][x] - 35)
elseif text.data.apl2[3][y][x] <= 105 then
three = three .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 70, text.data.apl2[3][y][x] - 70)
elseif text.data.apl2[3][y][x] <= 140 then
four = four .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 105, text.data.apl2[3][y][x] - 105)
elseif text.data.apl2[3][y][x] <=175 then
five = five .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 140, text.data.apl2[3][y][x] -140)
elseif text.data.apl2[3][y][x] <= 210 then
six = six .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 175, text.data.apl2[3][y][x] - 175)
elseif text.data.apl2[3][y][x] <= 245 then
seven = seven .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 210, text.data.apl2[3][y][x] - 210)
else
eight = eight .. string.sub(graphicsKey, text.data.apl2[3][y][x] - 245, text.data.apl2[3][y][x] - 245)
end
end
if one == "" and two == "" and three == "" and four == "" and five == "" and six == "" and seven == "" and eight == "" then
table.insert(text.data.packed[3], "n")
else table.insert(text.data.packed[3],one.."b"..two.."b"..three.."b"..four.."b"..five.."b"..six.."b"..seven.."b"..eight)
end
end
end

text.sendIMG = function()
for img = 1, 3 do
for y = 1, #text.data.packed[img] do
cmd.sjinn(text.data.packed[img][y])
end
cmd.sleep(2)
end
end

I start off in text.createIMG() by checking a couple things, One if there is any advisories, and if not place a No Advisories tag, Second looking for “windchill”, there is not always a “windchill” and if not place “dew point” in its place so we don’t have any blank lines. once were good to go we send the script off to imagemagick to make 3 graphics that contain black and white text. one for the long text at the bottom, one for advisories, and one for temperature.

text.convertIMG() reads the XPM files generated by imagemagick and does some cleanup. It starts by chopping the header and footer off of the image file, and removes the line formatting from each line of the image.

text.sortIMG() takes the leftover string data and scans each character in each line, in this case a white pixel is represented by a “.” (period) and a black pixel (which we don’t care about) is represented by a ” ” (space). Each time a period is found its x position is added to the end of a table. By the end of the image we are left with a table that has a subtable for each line, and contains X values for each pixel in each line, for example:

data= {}

data[1] = {1,2,12,80}

data[2] = {140,143,144,150}

There are 3 images to process, and they are different sizes, though how tall they are does not really matter to my script, its the width we are concerned about. text.packageIMG() reads each image and divides them up into blocks, the temperature text and advisory text are both 140 pixels wide and consume 4,  35 pixel blocks, so each value in each line is read out of our tables above and have some basic math done on them. If a value is greater than 35 for example then its block 2, the value has 35 subtracted from it and that is our block value (36 – 35 = block 2 pixel 1) . The bottom text is the widest graphic, taking up the entire width of the screen, but its just the same thing just spread over 8 blocks. Once we have our blocks as encoded strings they are packed into a single string per line of the image with “b” separating each block.

radar.lua does pretty much the same thing, except instead of making graphics it dithers the radar image downloaded earlier.

-- Weather Underground to Apple //
-- 2011 Kevin Dady
--
-- Radar processing:
-- feed jpeg to image magick
-- phrase output.xpm to color tables
-- package for apple //
-- send radar

radar = {}

radar.data = {}
radar.data.input  = {}  -- raw file data table dump
radar.data.packed = {}  -- packed apple data
radar.data.apl2 = {{},{},{},{},{},{}} -- black, green, violet, orange, blue, white

radar.img = {}
radar.img.header = 11 -- xpm file header # of lines
radar.img.w = 141
radar.img.h = 141

radar.convertIMG = function()

cmd.imageMGK(" temp/radar.jpg -level 0%,70%,1 -dither none -map req/apple.xpm temp/output.xpm")
-- read the file into a table
local file = io.open("temp/output.xpm","r")
for line in file:lines() do
table.insert(radar.data.input, tostring(line))
end
file:close()

-- remove header
for i = 1, radar.img.header do -- hardcode
table.remove(radar.data.input, 1)
end

-- remove footer
table.remove(radar.data.input, radar.img.w)

-- remove non color data
for i = 1, #radar.data.input do
radar.data.input[i] = string.sub(radar.data.input[i], 2, radar.img.h)
end
-- only deal with odd rows, due to the even / odd, bit / line, funny way apple 2's display highres colors.
-- if we leave them all in the image there is a gret chance of 2 colors phasing into another,
-- since we are going to loose pixel resolution anyway, we can cut that down some by deleting every other line
-- giving 140x70 also making transfer size smaller.
local keep ={}
for i = 1, #radar.data.input do
if (i % 2) == 1 then table.insert(keep, radar.data.input[i]) end
end
radar.data.input = keep
end

-- " " = 0 Apple color black (1)
-- "X" = 1 Apple color green
-- "o" = 2 Apple color violet
-- "." = 5 Apple color orange
-- "O" = 6 Apple color blue
-- "+" = 7 Apple color white (2)

radar.sortIMG = function()
local newChar = ""
-- need to convert the strings into tables
for y = 1, #radar.data.input do
-- add a new "line" string to each color table
for color = 1, 6 do
table.insert(radar.data.apl2[color], {})
end
-- for each column in the current row
for x = 1, #radar.data.input[y] do
-- read the character at that Y,X point
newChar = string.sub(radar.data.input[y], x,x)
-- assign each character a individual table value
if newChar == " " then table.insert(radar.data.apl2[1][y], x) -- black
elseif newChar == "X" then table.insert(radar.data.apl2[4][y], x) -- GREEN
elseif newChar == "o" then table.insert(radar.data.apl2[3][y], x) -- violet
elseif newChar == "." then table.insert(radar.data.apl2[2][y], x) -- ORANGE
elseif newChar == "O" then table.insert(radar.data.apl2[5][y], x) -- blue
elseif newChar == "+" then table.insert(radar.data.apl2[6][y], x) -- white
end
end
end
end

radar.packageIMG = function()
for color = 1, 5 do    -- ignore white, white takes a long time to draw since it makes up most of the graphic
for y = 1, #radar.data.apl2[color] do
local one   = ""
local two   = ""
local three = ""
local four  = ""
for x = 1, #radar.data.apl2[color][y] do
if radar.data.apl2[color][y][x] <= 35 then
one = one .. string.sub(graphicsKey, radar.data.apl2[color][y][x], radar.data.apl2[color][y][x])
elseif radar.data.apl2[color][y][x] <= 70 then
two = two .. string.sub(graphicsKey, radar.data.apl2[color][y][x] - 35, radar.data.apl2[color][y][x] - 35)
elseif radar.data.apl2[color][y][x] <= 105 then
three = three .. string.sub(graphicsKey, radar.data.apl2[color][y][x] - 70, radar.data.apl2[color][y][x] - 70)
else
four = four .. string.sub(graphicsKey, radar.data.apl2[color][y][x] - 105, radar.data.apl2[color][y][x] - 105)
end
end

if one == "" and two == "" and three == "" and four == "" then
table.insert(radar.data.packed, "n")
else table.insert(radar.data.packed,one.."b"..two.."b"..three.."b"..four)
end
end
end
end

radar.send = function()
for line = 1, 350 do
cmd.sjinn(radar.data.packed[line])
end
end

Imagemagick is used to dither the radar image to 6 of the 8 “available” colors, the Apple II only has 6 unique colors in high resolution mode, and the other 2 are black 2 and white 2. This has to do with how the apple does color, I mentioned earlier that its a hack, which basically uses bit patterns and the phase of the color burst to generate different colors. The end effect is always interesting as you can never place a specific color on each and every location on each and every line.

In order to help cut down on artifacts between lines and to cut the data transfer in half I then only use the even lines of the image, which yea reduces my radar resolution from 140×140 to 140×70 but due to the above mentioned wonkyness of the Apple’s video system I would have lost most of that resolution anyway.

The output XPM file has its header, footer and line formatting removed and each color is split up into individual tables, from there the process is the same, reading each monochrome image, packing it into block defined lines and packed up for the Apple to consume. I ignore white and just draw that on the screen as it makes up most of the image, saving time, and the end data is 5 copies of the image line / block encoded, each acting as a mask for its unique color.

For the most part were done looking at the lua scripts, so join me for part 3 (the end) where we will explore the software the apple II uses.

Part 1 and Part 3 are also available.

One thought on “Apple II Weather Display (part 2)

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.