This module clips out a small region from a set of equirectangular world maps. It can cross boundaries between maps, pasting together pieces of up to four maps per figure. It adds markings for longitude and latitude, feature markings and annotations. You can click on marked features to go to the target articles.

Usage

{{#invoke:MapClip|map|parameters}}

Parameters

Note that all parameters can be provided either as a single number (fractional degrees) or three numbers (fractional degrees, minutes, seconds).

Note that marks for degrees, minutes, seconds are ignored - the numbers are simply assumed to be deg, min, sec in that order, with any other numbers ignored.

Each of these values can be fractional. "W", "S", or "-" causes the latitude/longitude to be counted as negative (the module doesn't actually check to see it is in the right direction).

Giving a south value greater than a north value causes an error; east and west values can be swapped to show the other half of the world.

Mandatory parameters

  • regionwestedge,regioneastedge,regionsouthedge,regionnorthedge - required parameters set the edges (in degrees) of the region to be mapped

Optional parameters

  • mapfile - specifies a custom set of map files to use. Map files are in [[ ]] listed from left to right. If multiple rows are present they should be separated by "|" (I think this can be passed in with {{!}}).
  • grid - this important parameter specifies the color for a grid of latitude and longitude meridians used to annotate the map. Omitting it omits the grid. The spacing of the grid is presently only automatic.
  • mapwestedge,mapeastedge',mapsouthedge,mapnorthedge - optional parameters set the edges in degrees of the entire set of map image files. Default to -180,180,-90,90. If mapfile is not specified the default is provided for these and overrides this parameter value (you don't need it).
  • mapwidthpx,mapheightpx - tell the program how many pixel an individual map image file has. I've actually had it work with the wrong figure.
  • float- right or left to allow the image to "float" in the html sense (like a normal image)
  • nowiki - returns the html text (a mass of divs and a file link) for display of the map, rather than the map itself. This is not the same as simply enclosing the #invoke in nowiki tags.

Per-feature parameters

Each of these uses an independent number N. If a feature is present it must have a featureNlat and a featureNlong; the others are optional per feature. Numbers should start from 1 and not skip any for best results.

  • featureNlat - the latitude of the feature (can be one or three numbers like the others)
  • featureNlong - longitude
  • featureNname - the main purpose of the name of the feature presently is to be a Wikilink; apart from linking to an article this shouldn't be provided. There might be a way to distinguish mouseover from the link in the future...
  • featureNimage - overrides the default File:Full Star Yellow.svg with a filename you specify. Omit File: from the parameter.
  • featureNsize - overrides the default 15px size for the image.
  • featureNtext - suppresses placement of any image - instead this arbitrary text is used. This can be a link or not, and is displayed with upper left corner at the latitude and longitude you've provided. The purpose is to allow annotation of general features or groups of features. It can also be used to add a caption to a star on the map, by displacing it a little to the right (larger longitude) or in some other direction.

Example

{{#invoke:MapClip|map|regionwestedge=-82|regioneastedge=-80|regionnorthedge=26|regionsouthedge=24|feature1=Key West|feature1lat=24°33′33″N|feature1long=81°47′03″W|feature2=Old Rhodes Key|feature2lat=25.365957°N|feature2long=80.241866°W|feature3=Old Totten Key|feature3lat=25.3796°N|feature3long=80.2484°W|feature4=Reid Key|feature4lat=25.392779°N|feature4long=80.240149°W|feature5=Duck Key|feature5lat=24°46′32″N|feature5long=80°54′39″W|feature6=Pigeon Key|feature6lat=24.703991°N|feature6long=81.155308°W|feature7=Summerland Key|feature7lat=24.657°N|feature7long=81.441°W|feature8text=[[Lower Keys]]|feature8lat=24.4|feature8long=-81.8|feature9text=[[Middle Keys]]|feature9lat=24.55|feature9long=-81.13|grid=grey}}

produces

Topographic30deg N0W60.png
82W
81W
80W
26N
25N
24N
Key West
Old Rhodes Key
Old Totten Key
Reid Key
Duck Key
Pigeon Key
Summerland Key

 --- The purpose of this module is to clip out a segment from a set of files that makes up a map
 --- various annotations and scale bars should be added.
 --- The spritedraw function is being considered as a possible direct copy (future "require")
 --- from Module:Sprite - however, both modules are too inchoate at this time to do that confidently,
 --- and some modification may be needed.


local p={}

function processdegrees(degreestring)
    local neg=mw.ustring.match(degreestring,"^%s*%-") or mw.ustring.match(degreestring,"S") or mw.ustring.match(degreestring,"W")
    if neg then neg=-1 else neg=1 end
    local onenumber=mw.ustring.match(degreestring,"^[^%d%.]*([%d%.]+)[^%d%.]*$")
    if onenumber then
        return (neg*tonumber(onenumber))
    else local deg=mw.ustring.match(degreestring,"^[^%d%.]*([%d%.]+)")
        if not(deg) then return nil end
        local min=mw.ustring.match(degreestring,"^[^%d%.]*[%d%.]+[^%d%.]*([%d%.]+)")
        local sec=mw.ustring.match(degreestring,"^[^%d%.]*[%d%.]+[^%d%.]*[%d%.]+[^%d%.]*([%d%.]+)")
        return neg*(tonumber(deg)+tonumber(min or 0)/60+tonumber(sec or 0)/3600)
    end
end
    
    
function spritedraw(left,right,top,bottom,image,imagewidth,scale,float)
    top=math.floor(top*scale)
    bottom=math.ceil(bottom*scale)
    left=math.floor(left*scale)
    right=math.ceil(right*scale)
    local scalestring=""
    if scale~=1 then scalestring=math.floor(imagewidth*scale)..'px|' end
    output='<div style="position:absolute;overflow:visible;'..float..'top:'..(15-top)..'px;left:'..(40-left)..'px;clip:rect('..top..'px,'..right..'px,'..bottom..'px,'..left..'px);">[[File:'..image..'|'..scalestring..']]</div>'
    return output
end

function p.map(frame)
    --- variables "map" refer to the original image file
    --- variables "region" refer to the clipped area to be displayed
   local debuglog=""
   local args=frame.args
   local parent=frame.getParent(frame)
   local pargs=parent.args
    --- pixel values (setting regionwidth forces scaling.
    --- Regionheight may not be implemented because there's no way to 1-way scale I know of
   local mapwidthpx=args.mapwidthpx or pargs.mapwidthpx
   local mapheightpx=args.mapheightpx or pargs.mapheightpx
   local directions={'north','south','east','west'}
   local north,south,east,west=1,2,3,4
   local worldedge={90,-90,180,-180}
   local mapedgestring,mapedge,regionedgestring,regionedge={},{},{},{}
   for d =1,4 do
      mapedgestring[d]=args['map'..directions[d]..'edge'] or args['map'..directions[d]..'edge'] or ""
      mapedge[d]=processdegrees(mapedgestring[d]) or worldedge[d]
      regionedgestring[d]=args['region'..directions[d]..'edge'] or args['region'..directions[d]..'edge'] or ""
      regionedge[d]=processdegrees(regionedgestring[d]) or worldedge[d]
   end
   local mapwidthdeg=mapedge[east]-mapedge[west]
   if mapwidthdeg<=0 then mapwidthdeg=mapwidthdeg+360 end
   local regionwidthdeg=regionedge[east]-regionedge[west]
   if regionwidthdeg<=0 then regionwidthdeg=regionwidthdeg+360 end
   local mapfile=args.mapfile or pargs.mapfile or ""
   local mapfiles={}
   local row=0
   mapfile=mapfile.."|" -- last row will be processed like the others
   while mw.ustring.match(mapfile,"|") do
       row=row+1
       local rowtext=mw.ustring.match(mapfile,"^([^|]*)|")
       mapfiles[row]={}
       prowl=mw.ustring.gmatch(rowtext,"%[%[([^%[%]])*%]%]")
       repeat
           local f=prowl()
           if not f then break;end
           table.insert(mapfiles[row],f)
       until false
       mapfile=mw.ustring.gsub(mapfile,"^[^|]*|","")
   end
   if not mapfiles[1][1] then
       mapedge={90,-90,180,-180} -- ad hoc calibration was done here, but turned out to be a bug!
       if regionwidthdeg<=60 then
           mapwidthpx=1800
           mapheightpx=1800
           mapfiles=
{{'Topographic30deg_N60W150.png',
'Topographic30deg_N60W120.png',
'Topographic30deg_N60W90.png',
'Topographic30deg_N60W60.png',
'Topographic30deg_N60W30.png',
'Topographic30deg_N60W0.png',
'Topographic30deg_N60E0.png',
'Topographic30deg_N60E30.png',
'Topographic30deg_N60E60.png',
'Topographic30deg_N60E90.png',
'Topographic30deg_N60E120.png',
'Topographic30deg_N60E150.png'},
{'Topographic30deg_N30W150.png',
'Topographic30deg_N30W120.png',
'Topographic30deg_N30W90.png',
'Topographic30deg_N30W60.png',
'Topographic30deg_N30W30.png',
'Topographic30deg_N30W0.png',
'Topographic30deg_N30E0.png',
'Topographic30deg_N30E30.png',
'Topographic30deg_N30E60.png',
'Topographic30deg_N30E90.png',
'Topographic30deg_N30E120.png',
'Topographic30deg_N30E150.png'},
{'Topographic30deg_N0W150.png',
'Topographic30deg_N0W120.png',
'Topographic30deg_N0W90.png',
'Topographic30deg_N0W60.png',
'Topographic30deg_N0W30.png',
'Topographic30deg_N0W0.png',
'Topographic30deg_N0E0.png',
'Topographic30deg_N0E30.png',
'Topographic30deg_N0E60.png',
'Topographic30deg_N0E90.png',
'Topographic30deg_N0E120.png',
'Topographic30deg_N0E150.png'},
{'Topographic30deg_S0W150.png',
'Topographic30deg_S0W120.png',
'Topographic30deg_S0W90.png',
'Topographic30deg_S0W60.png',
'Topographic30deg_S0W30.png',
'Topographic30deg_S0W0.png',
'Topographic30deg_S0E0.png',
'Topographic30deg_S0E30.png',
'Topographic30deg_S0E60.png',
'Topographic30deg_S0E90.png',
'Topographic30deg_S0E120.png',
'Topographic30deg_S0E150.png'},
{'Topographic30deg_S30W150.png',
'Topographic30deg_S30W120.png',
'Topographic30deg_S30W90.png',
'Topographic30deg_S30W60.png',
'Topographic30deg_S30W30.png',
'Topographic30deg_S30W0.png',
'Topographic30deg_S30E0.png',
'Topographic30deg_S30E30.png',
'Topographic30deg_S30E60.png',
'Topographic30deg_S30E90.png',
'Topographic30deg_S30E120.png',
'Topographic30deg_S30E150.png'},
{'Topographic30deg_S60W150.png',
'Topographic30deg_S60W120.png',
'Topographic30deg_S60W90.png',
'Topographic30deg_S60W60.png',
'Topographic30deg_S60W30.png',
'Topographic30deg_S60W0.png',
'Topographic30deg_S60E0.png',
'Topographic30deg_S60E30.png',
'Topographic30deg_S60E60.png',
'Topographic30deg_S60E90.png',
'Topographic30deg_S60E120.png',
'Topographic30deg_S60E150.png'}}
       else
           mapwidthpx=1991
           mapheightpx=1990
           mapfiles={{'WorldMap_180-0-270-90.png','WorldMap_270-0-360-90.png','WorldMap_0-0-90-90.png','WorldMap_90-0-180-90.png'},{'WorldMap_-180,-90,-90,0.png','WorldMap_-90,-90,-0,0.png','WorldMap_0,-90,90,0.png','WorldMap_-270,-90,-180,0.png'}}
       end
   end
   if not (mapwidthpx and mapheightpx) then return "Module:MapClip error: mapwidthpx and mapheightpx must be supplied if a map image file is specified" end
   mapwidthpx=tonumber(mapwidthpx);mapheightpx=tonumber(mapheightpx)
   local totalmapwidthpx=mapwidthpx*#mapfiles[1]
   local totalmapheightpx=mapheightpx*#mapfiles
   local mapheightdeg=mapedge[north]-mapedge[south]
   if mapheightdeg<=0 then return "[[Module:MapClip]] error: mapnorthedge is south of mapsouthedge" end
   if ((regionedge[north]-regionedge[south])<0) then return "[[Module:MapClip]] error: regionnorthedge is south of regionsouthedge" end

   local widthratio=totalmapwidthpx/mapwidthdeg
   local heightratio=totalmapheightpx/mapheightdeg
   local left=(regionedge[west]-mapedge[west])*widthratio
   local xfile=math.floor(left/mapwidthpx)
   left=left-xfile*mapwidthpx
   local right=(regionedge[east]-mapedge[west])*widthratio-xfile*mapwidthpx
   local top=(mapedge[north]-regionedge[north])*heightratio
   local yfile=math.floor(top/mapheightpx)
   top=top-yfile*mapheightpx
   local bottom=(mapedge[north]-regionedge[south])*heightratio-yfile*mapheightpx
   local imagewidth=mapwidthpx
   local displaywidth=args.displaywidth or pargs.displaywidth or 220
   local float=args.float or pargs.float or nil
   if float then float="float:"..float..";" else float="" end
   local nowiki=args.nowiki or pargs.nowiki
   local i,featurelat,featurelong,featurename,featureimage,featuresize,featuretext=0,{},{},{},{},{},{}
   repeat -- import all feature names, longitude, latitude
       i=i+1
       featurename[i]=args['feature'..i] or pargs['feature'..i]
       featurelat[i]=args['feature'..i..'lat'] or pargs['feature'..i..'lat']
       featurelong[i]=args['feature'..i..'long'] or pargs['feature'..i..'long']
       featureimage[i]=args['feature'..i..'image'] or pargs['feature'..i..'image']
       featuresize[i]=args['feature'..i..'size'] or pargs['feature'..i..'size']
       featuretext[i]=args['feature'..i..'text'] or pargs['feature'..i..'text']
       if (featurelong[i]) then featurelong[i]=processdegrees(featurelong[i]) else featurelat[i]=nil end
       if (featurelat[i]) then featurelat[i]=processdegrees(featurelat[i]) end
   until (not featurelat[i])
   local output=""
    -- first map to display
   local image=mapfiles[yfile+1][xfile+1] or error("Module:MapClip error: "..tostring(yfile)..":"..tostring(xfile).." in "..tostring(mapfile).." not found")
   local scale=displaywidth/(right-left)
   output,errcode=spritedraw(left,right,top,bottom,image,imagewidth,scale,float)
   if right>mapwidthpx then
       local xnew=xfile+2
       if xnew>#mapfiles[1] then xnew=1 end
       if bottom>mapheightpx then
           local ynew=yfile+2
           if ynew>#mapfiles then ynew=1 end
           local image=mapfiles[ynew][xfile+1] or error("Module:MapClip error: "..tostring(yfile)..":"..tostring(xfile).." in "..tostring(mapfile).." not found")
           local output2,errcode2=spritedraw(left,right,top-mapheightpx,bottom-mapheightpx,image,imagewidth,scale,float)
           output=output..output2;errcode=errcode or errcode2
           local image=mapfiles[yfile+1][xnew]
           local output2,errcode2=spritedraw(left-mapwidthpx,right-mapwidthpx,top,bottom,image,imagewidth,scale,float)
           output=output..output2;errcode=errcode or errcode2
           local image=mapfiles[ynew][xnew]
           local output2,errcode2=spritedraw(left-mapwidthpx,right-mapwidthpx,top-mapheightpx,bottom-mapheightpx,image,imagewidth,scale,float)
           output=output..output2;errcode=errcode or errcode2
       else
           local image=mapfiles[yfile+1][xnew]
           local output2,errcode2=spritedraw(left-mapwidthpx,right-mapwidthpx,top,bottom,image,imagewidth,scale,float)
           output=output..output2;errcode=errcode or errcode2
       end
    else if bottom>mapheightpx then
       local ynew=yfile+2
       if ynew>#mapfiles then ynew=1 end
       local image=mapfiles[ynew][xfile+1] or error("Module:MapClip error: "..tostring(yfile)..":"..tostring(xfile).." in "..tostring(mapfile).." not found")
       local output2,errcode2=spritedraw(left,right,top-mapheightpx,bottom-mapheightpx,image,imagewidth,scale,float)
       output=output..output2;errcode=errcode or errcode2
       end
   end
   local grid=args.grid or pargs.grid
   if grid then -- for now only implementing an automagic grid
       md=regionedge[east]-regionedge[west]
       if md<0 then md=md+360 end
       if md<=30 then md=math.abs(md/2) else md=math.abs(md/3) end -- must be at least two divisions
       local pt=10
       if pt<=md then
           if (pt<=md/3) then pt=pt*3 end -- multiples of 30 degrees
           if (pt<=md/3) then pt=pt*3 end -- multiples of 90 degrees
       else while (pt>md) do
               if pt/2<md then pt=pt/2;break end -- first digit 5
               if pt/5<md then pt=pt/5;break end -- first digit 2
               pt=pt/10
               if pt<md then break end -- first digit 1
           end
       end
       local yheight=math.ceil((bottom-top)*scale)
       for gridline=math.ceil(regionedge[west]/pt)*pt,math.floor(regionedge[east]/pt)*pt,pt do
           local xpos=math.floor(((gridline-mapedge[west])*widthratio-xfile*mapwidthpx-left)*scale)
           output=output..'<div style="position:absolute;overflow:visible;border:solid '..grid..';border-width:0 1px 0 0;'..float..'top:15px;width:0px;height:'..yheight..'px;left:'..(xpos+40)..'px;"></div><div style="position:absolute;top:-2px;width:40px;font-size:75%;color:'..grid..';text-align:right;left:'..(xpos+10)..'px;">'..tostring(math.abs(gridline))..((gridline<0) and "W" or "E")..'</div>'
       end
       for gridline=math.floor(regionedge[north]/pt)*pt,math.ceil(regionedge[south]/pt)*pt,-1*pt do
           local ypos=math.floor(((regionedge[north]-gridline)*heightratio)*scale)
           output=output..'<div style="position:absolute;overflow:visible;border:solid '..grid..';border-width:0 0 1px 0;'..float..'top:'..(ypos+15)..'px;height:0px;width:'..displaywidth..'px;left:40px;"></div><div style="position:absolute;top:'..(ypos+6)..'px;width:40px;font-size:75%;color:'..grid..';text-align:right;left:0px;">'..tostring(math.abs(gridline))..((gridline<0) and "S" or "N")..'</div>'
       end
   end
   if featurelat[1] then
       for i=1,#featurelat do
           if featuretext[i] then
               output=output..'<div style="position:absolute;overflow:visible;top:'..math.floor(((regionedge[north]-featurelat[i])*heightratio)*scale+3)..'px;left:'..math.floor(((featurelong[i]-mapedge[west])*widthratio-xfile*mapwidthpx-left)*scale+33)..'px;">'..featuretext[i]..'</div>'
           else
               local linkstring=''
               if featurename[i] then linkstring='|link='..featurename[i]..'|'..featurename[i] end
               output=output..'<div style="position:absolute;overflow:visible;height:15px;width:15px;top:'..math.floor(((regionedge[north]-featurelat[i])*heightratio)*scale+10.5-(featuresize[i] or 15)/2)..'px;left:'..math.floor(((featurelong[i]-mapedge[west])*widthratio-xfile*mapwidthpx-left)*scale+40.5-(featuresize[i] or 15)/2)..'px;">[[File:'..(featureimage[i] or 'Full Star Yellow.svg')..'|'..(featuresize[i] or '15')..'px'..linkstring..']]</div>'
           end
       end
   end
   output = '<div style="position:relative;overflow:hidden;'..float..'width:'..(displaywidth+60)..'px;height:'..math.ceil((bottom-top)*scale+22)..'px;">'..output..'</div>'
   if nowiki or errcode then return frame:preprocess("<nowiki>"..output..debuglog.."</nowiki>") end
   return output
end

return p