Module:Places to check
From Landrace.wiki
More actions
Works out and prints the list on Places to check and the briefs on Places to check/Briefs.
Use
{{#invoke:Places to check|list}}: a sentence giving the number of places, then the sortable table.{{#invoke:Places to check|briefs}}: one brief per listed place, in the list's order.{{#invoke:Places to check|briefs|1=Ganja Mahal}}prints one.
Styles: Template:Places to check/styles.css. A brief's anchor is its place name, so Places to check/Briefs#Ganja_Mahal opens and prints that brief alone; #All_briefs prints them all, one to a page.
What it reads
- the places named on Growing places in historical sources (its subobjects:
Foaf:name,Has country,Has place as recorded,Has description,Has year described,Has date as recorded,Has months as recorded,Has plant names as recorded,Reported in,Has coordinates) - the pages whose
Has growing statusis Historical or No longer grown (Has place type,Has country,Has first grown,Has last grown,Has last checked,Has coordinates) - the accessions filed under those pages (
Has growing area,Has growing region,Has appellation) - the historical sources whose
Has region documentednames one of those pages - for each place with a position, the accessions within 100 km (SMW's distance query on
Has coordinates)
Each query is a plain #ask; the join is done here.
The rule
- An accession counts as recent when it was collected in 2000 or later (
p.CUTOFF). Without a collection date it counts by the year in a standard accession number (XXX-CCC-RRR-NNYYYYNNNN), the year the material is first recorded; otherwise it does not count. - A page with growing status Historical or No longer grown is listed while no recent accession is filed under it.
- A place named only on the census (no page of the same name with such a status) is listed while no recent accession lies within 25 km of its position (
p.RADIUS). A census place with the same name as such a page is shown with that page. - A brief lists up to five recent accessions within 100 km (
p.NEAR,p.NEAR_MAX), and the accessions from before 2000 filed under the page or lying within 25 km.
The page is worked out when it is rendered, so a new accession appears after the page is next rendered (a purge does it at once).
-- Places to check: places that old sources say grew cannabis, and growing places last recorded as
-- Historical or No longer grown, where no accession has been collected since 2000.
-- Two entry points: p.list (the table on "Places to check") and p.briefs (the printable briefs).
-- The join is computed here from plain #ask results (no chain printouts).
local p = {}
p.CUTOFF = 2000 -- an accession collected in this year or later counts as recent
p.RADIUS = 25 -- km: a recent accession this close to a census position counts as "in" the place
p.NEAR = 100 -- km: recent accessions listed as nearby in a brief
p.NEAR_MAX = 5 -- at most this many nearby accessions in a brief
p.CENSUS = 'Growing places in historical sources'
p.BRIEFS = 'Places to check/Briefs'
p.PAST = '[[Has growing status::Historical||No longer grown]]'
local SEP_R, SEP_P, SEP_V = '⟦R⟧', '⟦P⟧', '⟦V⟧'
local function trim(s) return mw.text.trim(s or '') end
local function split(s, sep) return mw.text.split(s, sep, true) end
-- printouts: list of {key, property, format}; returns list of {subject, fields = {key = {values}}}
local function ask(frame, cond, printouts, extra)
local parts = { '{{#ask:', cond }
for _, po in ipairs(printouts) do
parts[#parts + 1] = '|?' .. po[2] .. '#' .. (po[3] or '-') .. '=⟪' .. po[1] .. '⟫'
end
parts[#parts + 1] = '|format=plainlist|headers=plain|link=none|searchlabel=|default='
parts[#parts + 1] = '|sep=' .. SEP_R .. '|propsep=' .. SEP_P .. '|valuesep=' .. SEP_V
parts[#parts + 1] = (extra or '|limit=500') .. '}}'
local out = trim(frame:preprocess(table.concat(parts)))
local rows = {}
if out == '' then return rows end
for _, row in ipairs(split(out, SEP_R)) do
local subject, rest = row:match('^(.-) %((⟪.*)%)%s*$')
if not subject then subject, rest = trim(row), '' end
local fields = {}
for _, part in ipairs(split(rest, SEP_P)) do
local k, v = part:match('^⟪([%w_]+)⟫: (.*)$')
if k then
local vals = {}
for _, x in ipairs(split(v, SEP_V)) do
x = trim(x)
if x ~= '' then vals[#vals + 1] = x end
end
fields[k] = vals
end
end
rows[#rows + 1] = { subject = trim(subject), fields = fields }
end
return rows
end
p._ask = ask
local function one(f, k) local v = f[k]; return v and v[1] or nil end
-- "24° 48' 0.00\" N, 89° 4' 12.00\" E" -> {24.8, 89.07}
local function parseCoord(s)
if not s then return nil end
local function part(txt)
local d, m, sec, h = txt:match('([%d%.]+)°%s*([%d%.]*)\'?%s*([%d%.]*)"?%s*([NSEW])')
if not d then return nil end
local v = tonumber(d) + (tonumber(m) or 0) / 60 + (tonumber(sec) or 0) / 3600
if h == 'S' or h == 'W' then v = -v end
return v
end
local a, b = s:match('^(.-[NS]),%s*(.-[EW])$')
if not a then
local la, lo = s:match('^%s*(%-?[%d%.]+)%s*,%s*(%-?[%d%.]+)%s*$')
if la then return { tonumber(la), tonumber(lo) } end
return nil
end
local la, lo = part(a), part(b)
if la and lo then return { la, lo } end
return nil
end
p._parseCoord = parseCoord
local function km(a, b)
local r = math.pi / 180
local dla, dlo = (b[1] - a[1]) * r, (b[2] - a[2]) * r
local h = math.sin(dla / 2) ^ 2 + math.cos(a[1] * r) * math.cos(b[1] * r) * math.sin(dlo / 2) ^ 2
return 6371 * 2 * math.asin(math.min(1, math.sqrt(h)))
end
p._km = km
-- the year an accession counts for: its collection year, else the year in a standard accession
-- number (XXX-CCC-RRR-NNYYYYNNNN, the year the material is first attested), else nil
local function accYear(title, dateYear)
local y = tonumber(dateYear or '')
if y then return y end
local id = title:match('^%u+%-%u+%-%u+%-%d%d(%d%d%d%d)%d+$')
y = tonumber(id or '')
if y and y > 1000 then return y end
return nil
end
p._accYear = accYear
local lang = mw.language.getContentLanguage()
local function fmtKm(d) return lang:formatNum(math.floor(d + 0.5)) .. ' km' end
local function fmtPos(c)
return string.format('%.2f° %s, %.2f° %s', math.abs(c[1]), c[1] < 0 and 'S' or 'N',
math.abs(c[2]), c[2] < 0 and 'W' or 'E')
end
local function anchor(s) return mw.uri.anchorEncode(s) end
local function grownSpan(first, last)
if first and last then
return first == last and first or (first .. '–' .. last)
elseif last then return 'until ' .. last
elseif first then return 'from ' .. first end
return nil
end
----------------------------------------------------------------------------------------------------
-- data
----------------------------------------------------------------------------------------------------
local function gather0(frame)
-- 1. census places (one subobject per place and source)
local census = ask(frame, '[[-Has subobject::' .. p.CENSUS .. ']]', {
{ 'name', 'Foaf:name' }, { 'country', 'Has country' }, { 'spelled', 'Has place as recorded' },
{ 'desc', 'Has description' }, { 'year', 'Has year described' }, { 'date', 'Has date as recorded' },
{ 'months', 'Has months as recorded' }, { 'names', 'Has plant names as recorded' },
{ 'src', 'Reported in' }, { 'coord', 'Has coordinates' }, { 'pageof', 'Has place page' },
}, '|sort=|limit=500')
-- 2. growing places last recorded as Historical or No longer grown
local hubs = ask(frame, p.PAST, {
{ 'status', 'Has growing status' }, { 'ptype', 'Has place type' }, { 'country', 'Has country' },
{ 'first', 'Has first grown', '-F[Y]' }, { 'last', 'Has last grown', '-F[Y]' },
{ 'checked', 'Has last checked', '-F[Y]' }, { 'coord', 'Has coordinates' },
}, '|sort=|limit=500')
-- 3. accessions linked to those places
local linkProps = { 'Has growing area', 'Has growing region', 'Has appellation' }
local linked = {}
for _, prop in ipairs(linkProps) do
local rows = ask(frame, '[[Category:Accessions]][[' .. prop .. '::<q>' .. p.PAST .. '</q>]]', {
{ 'place', prop }, { 'date', 'Has collection date', '-F[Y]' },
}, '|sort=|limit=500')
for _, r in ipairs(rows) do
for _, pl in ipairs(r.fields.place or {}) do
linked[pl] = linked[pl] or {}
linked[pl][r.subject] = accYear(r.subject, one(r.fields, 'date'))
end
end
end
-- 4. old sources that document a past growing place
local documented = {}
for _, r in ipairs(ask(frame, '[[Has region documented::<q>' .. p.PAST .. '</q>]]', {
{ 'regions', 'Has region documented' }, { 'pub', 'Has publication year', '-F[Y]' },
}, '|sort=|limit=500')) do
for _, reg in ipairs(r.fields.regions or {}) do
documented[reg] = documented[reg] or {}
table.insert(documented[reg], { title = r.subject, year = one(r.fields, 'pub') })
end
end
-- build the places
local places, byName = {}, {}
for _, h in ipairs(hubs) do
local f = h.fields
local pl = {
key = h.subject, name = h.subject, page = h.subject, hub = true,
status = one(f, 'status'), ptype = one(f, 'ptype'), country = one(f, 'country'),
first = one(f, 'first'), last = one(f, 'last'), checked = one(f, 'checked'),
coord = parseCoord(one(f, 'coord')), census = {}, sources = documented[h.subject] or {},
linkedOld = {}, linkedNew = {},
}
for t, y in pairs(linked[h.subject] or {}) do
if y and y >= p.CUTOFF then table.insert(pl.linkedNew, { title = t, year = y })
else table.insert(pl.linkedOld, { title = t, year = y }) end
end
places[#places + 1] = pl
byName[h.subject] = pl
end
for _, c in ipairs(census) do
local f = c.fields
local name = one(f, 'name') or (c.subject:match('#(.+)$')) or c.subject
local row = {
anchor = anchor(c.subject:match('#(.+)$') or name), spelled = one(f, 'spelled'), desc = one(f, 'desc'),
year = one(f, 'year'), date = one(f, 'date'), months = f.months or {}, names = f.names or {},
src = one(f, 'src'),
}
local pl = byName[name]
if not pl then
pl = { key = name, name = name, hub = false, country = one(f, 'country'),
coord = parseCoord(one(f, 'coord')), census = {}, sources = {}, linkedOld = {}, linkedNew = {} }
places[#places + 1] = pl
byName[name] = pl
end
pl.country = pl.country or one(f, 'country')
pl.placePage = pl.placePage or one(f, 'pageof')
pl.coord = pl.coord or parseCoord(one(f, 'coord'))
table.insert(pl.census, row)
end
-- 5. accessions within NEAR km of each place (SMW's distance query; one scan of every accession
-- position costs more time than a page may take)
for _, pl in ipairs(places) do
pl.nearNew, pl.nearOld, pl.nearest = {}, {}, nil
if pl.coord then
local accs = {}
for _, r in ipairs(ask(frame, string.format('[[Category:Accessions]][[Has coordinates::%.4f, %.4f (%d km)]]',
pl.coord[1], pl.coord[2], p.NEAR), {
{ 'date', 'Has collection date', '-F[Y]' }, { 'coord', 'Has coordinates' },
}, '|sort=|limit=500')) do
local c = parseCoord(one(r.fields, 'coord'))
if c then
accs[#accs + 1] = { title = r.subject, year = accYear(r.subject, one(r.fields, 'date')), coord = c }
end
end
for _, a in ipairs(accs) do
local d = km(pl.coord, a.coord)
-- the distance query's box reaches past NEAR at its corners: keep only what lies within NEAR
if d > p.NEAR then
-- outside
elseif a.year and a.year >= p.CUTOFF then
if not pl.nearest or d < pl.nearest.d then pl.nearest = { title = a.title, year = a.year, d = d } end
table.insert(pl.nearNew, { title = a.title, year = a.year, d = d })
elseif d <= p.RADIUS then
table.insert(pl.nearOld, { title = a.title, year = a.year, d = d })
end
end
table.sort(pl.nearNew, function(x, y) return x.d < y.d end)
table.sort(pl.nearOld, function(x, y) return x.d < y.d end)
end
-- the rule: a past growing place counts as checked when an accession collected since the cutoff is
-- linked to it; a place named only in an old source, when one lies within RADIUS of its position
if pl.hub then
pl.toCheck = #pl.linkedNew == 0
else
pl.toCheck = not (pl.nearest and pl.nearest.d <= p.RADIUS)
end
end
table.sort(places, function(a, b)
if (a.country or '') ~= (b.country or '') then return (a.country or '~') < (b.country or '~') end
return a.name < b.name
end)
return places
end
-- one run of the join per page render: the list and the foot table both use it
function p.gather(frame)
if not p._places then p._places = gather0(frame) end
return p._places
end
local function placeLinkU(pl)
return '[[' .. p.CENSUS .. '#' .. pl.census[1].anchor .. '|' .. pl.name .. ']]'
end
-- a place named in an old source whose position cannot be given (a district, a region, a revenue unit) is
-- not a place to visit: it goes in the short list at the foot, with no brief and no distance query
local function unpositioned(pl) return (not pl.hub) and not pl.coord end
local function listed(frame)
local out = {}
for _, pl in ipairs(p.gather(frame)) do
if pl.toCheck and not unpositioned(pl) then out[#out + 1] = pl end
end
return out
end
function p.unpositioned(frame)
local rows = {}
for _, pl in ipairs(p.gather(frame)) do
if unpositioned(pl) then rows[#rows + 1] = pl end
end
if #rows == 0 then return '' end
local t = {
'<div style="overflow-x:auto;">',
'{| class="wikitable sortable lw-ptc-list"',
'! Place !! Country !! Recorded !! Page',
}
for _, pl in ipairs(rows) do
local years, seen = {}, {}
for _, r in ipairs(pl.census) do
if r.year and not seen[r.year] then seen[r.year] = true; years[#years + 1] = r.year end
end
t[#t + 1] = '|-'
t[#t + 1] = '| ' .. placeLinkU(pl) .. ' || ' .. (pl.country and ('[[' .. pl.country .. ']]') or '')
.. ' || ' .. table.concat(years, '; ') .. ' || ' .. (pl.placePage and ('[[' .. pl.placePage .. ']]') or '')
end
t[#t + 1] = '|}'
t[#t + 1] = '</div>'
return table.concat(t, '\n')
end
----------------------------------------------------------------------------------------------------
-- output
----------------------------------------------------------------------------------------------------
local function placeLink(pl)
if pl.hub then return '[[' .. pl.page .. ']]' end
return '[[' .. p.CENSUS .. '#' .. pl.census[1].anchor .. '|' .. pl.name .. ']]'
end
local function kindText(pl)
if pl.hub then
local t = pl.status or ''
if pl.ptype then t = t .. ' (' .. lang:lcfirst(pl.ptype) .. ')' end
return t
end
return 'Named in an old source'
end
local function recordedText(pl)
local bits = {}
if pl.hub then
local g = grownSpan(pl.first, pl.last)
if g then bits[#bits + 1] = 'grown ' .. g end
return table.concat(bits, '; ')
end
for _, r in ipairs(pl.census) do
if r.year then bits[#bits + 1] = r.year end
end
return table.concat(bits, '; ')
end
local function nearestText(pl)
if not pl.coord then return 'no position' end
if not pl.nearest then return 'over ' .. p.NEAR .. ' km' end
return fmtKm(pl.nearest.d)
end
local function plural(n, one, many) return n .. ' ' .. (n == 1 and one or many) end
-- the count sentence and the table, from one run of the join
function p.list(frame)
local rows = listed(frame)
local census, hubs = 0, 0
for _, pl in ipairs(rows) do
if pl.hub then hubs = hubs + 1 else census = census + 1 end
end
local t = {
'The list holds ' .. plural(#rows, 'place', 'places') .. ': ' .. plural(census, 'place', 'places')
.. ' named only in an old source and ' .. plural(hubs, 'growing place', 'growing places') .. '.',
'',
'<div style="overflow-x:auto;">',
'{| class="wikitable sortable lw-ptc-list"',
'! Place !! Country !! Recorded !! Status !! data-sort-type="number" | Nearest accession since ' .. p.CUTOFF .. ' !! Brief',
}
for _, pl in ipairs(rows) do
local sortKm = pl.nearest and math.floor(pl.nearest.d + 0.5) or 999999
t[#t + 1] = '|-'
t[#t + 1] = '| ' .. placeLink(pl) .. ' || ' .. (pl.country and ('[[' .. pl.country .. ']]') or '')
.. ' || ' .. recordedText(pl) .. ' || ' .. kindText(pl)
.. ' || data-sort-value="' .. sortKm .. '" | ' .. nearestText(pl)
.. ' || [[' .. p.BRIEFS .. '#' .. anchor(pl.key) .. '|Brief]]'
end
t[#t + 1] = '|}'
t[#t + 1] = '</div>'
local foot = p.unpositioned(frame)
if foot ~= '' then
t[#t + 1] = ''
t[#t + 1] = '== Named only at district or region level =='
t[#t + 1] = 'Some old sources name only a district, a region or a unit of the old revenue survey, from which no position can be worked out. These are not places to visit, so they have no brief; each links to its row on [[' .. p.CENSUS .. ']].'
t[#t + 1] = ''
t[#t + 1] = foot
end
return table.concat(t, '\n')
end
local function quoteOf(s) return '“' .. s .. '”' end
local function accLine(a, withKm)
local s = '[[' .. a.title .. ']]'
if a.year then s = s .. ' (' .. a.year .. ')' end
if withKm then s = s .. ', ' .. fmtKm(a.d) end
return s
end
local function brief(pl)
local b = {}
local function add(s) b[#b + 1] = s end
add('<div class="lw-ptc-brief" id="' .. anchor(pl.key) .. '">')
add('<div class="lw-ptc-title" role="heading" aria-level="2">' .. pl.name .. '</div>')
local sub = { kindText(pl) }
if pl.country then sub[#sub + 1] = pl.country end
add('<div class="lw-ptc-sub">' .. table.concat(sub, ' · ') .. '</div>')
-- the place
add('{| class="lw-ptc-facts"')
if pl.hub then
add('! Page\n| [[' .. pl.page .. ']]')
add('|-\n! Status\n| ' .. (pl.status or ''))
local g = grownSpan(pl.first, pl.last)
if g then add('|-\n! Recorded grown\n| ' .. g) end
add('|-\n! Last checked\n| ' .. (pl.checked or 'not yet'))
else
add('! Listed on\n| [[' .. p.CENSUS .. '#' .. pl.census[1].anchor .. '|' .. p.CENSUS .. ']]')
end
local spelled = {}
for _, r in ipairs(pl.census) do
if r.spelled and r.spelled ~= pl.name then spelled[#spelled + 1] = "''" .. r.spelled .. "''" end
end
if #spelled > 0 then add('|-\n! Spelled\n| ' .. table.concat(spelled, ', ')) end
if pl.coord then
add('|-\n! Position\n| ' .. fmtPos(pl.coord) .. (pl.hub and '' or ' (the place named, not a field)'))
else
add('|-\n! Position\n| not recorded')
end
add('|}')
-- what the old sources say
if #pl.census > 0 or #pl.sources > 0 then
add('<div class="lw-ptc-h">What the old sources say</div>')
for _, r in ipairs(pl.census) do
local head = "'''" .. (r.src or 'Source') .. "'''"
if r.year then head = head .. ', describing ' .. r.year end
if r.date then head = head .. ' (' .. quoteOf(r.date) .. ')' end
add('* ' .. head .. ': ' .. (r.desc or '') .. (r.desc and not r.desc:match('[%.!?]$') and '.' or ''))
if #r.months > 0 then
local m = {}
for _, x in ipairs(r.months) do m[#m + 1] = quoteOf(x) end
add('*: Months: ' .. table.concat(m, ' '))
end
if #r.names > 0 then add("*: Names for the plant: ''" .. table.concat(r.names, "'', ''") .. "''") end
end
for _, s in ipairs(pl.sources) do
add('* [[' .. s.title .. ']]' .. (s.year and (', published ' .. s.year) or '') .. ', which documents the place')
end
end
-- collections
local old = {}
local seen = {}
for _, a in ipairs(pl.linkedOld) do old[#old + 1] = { a, false }; seen[a.title] = true end
for _, a in ipairs(pl.nearOld) do
if not seen[a.title] then old[#old + 1] = { a, true }; seen[a.title] = true end
end
if #old > 0 then
add('<div class="lw-ptc-h">Collected here before ' .. p.CUTOFF .. '</div>')
for _, o in ipairs(old) do add('* ' .. accLine(o[1], o[2])) end
end
add('<div class="lw-ptc-h">Accessions collected since ' .. p.CUTOFF .. ' nearby</div>')
if not pl.coord then
add('No position is recorded for this place, so nothing is measured.')
elseif #pl.nearNew == 0 then
add('None within ' .. p.NEAR .. ' km.')
else
for i, a in ipairs(pl.nearNew) do
if i > p.NEAR_MAX then break end
add('* ' .. accLine(a, true))
end
end
-- field notes, printed only
add('<div class="lw-ptc-notes">')
add('<div class="lw-ptc-h">Field notes</div>')
for _, l in ipairs({ 'Date of visit', 'Village or field visited', 'Grown there now? Yes / No / Could not tell',
'Names heard for the plant', 'Sowing and harvest months given', 'Seed brought in from elsewhere?',
'Photographs taken', 'Notes' }) do
add('<div class="lw-ptc-line">' .. l .. '</div>')
end
add('<div class="lw-ptc-line"> </div>\n<div class="lw-ptc-line"> </div>')
add('</div>')
add('</div>')
return table.concat(b, '\n')
end
p._brief = brief
function p.briefs(frame)
local out = { '<div class="lw-ptc-briefs" id="All_briefs">' }
local only = trim(frame.args[1])
for _, pl in ipairs(listed(frame)) do
if only == '' or only == pl.key then out[#out + 1] = brief(pl) end
end
out[#out + 1] = '</div>'
return table.concat(out, '\n')
end
return p