A source that publishes no artwork no longer settles for initials: the ingestion pipeline now keeps a vendored, public-domain hand of Maki glyphs, and a translator may deal from it by declaring atlas.icon.std on a category. The Gazetteer's own feature-type codes key the curation -- a mons is a mountain, a patera a volcano, a palus literally a marsh, and shape language stands in where no glyph says the thing -- so the Moon's craters will wear the same rim the day a capture arrives, and retuning any choice is one table edit plus a re-translation of captures already on disk. Resolution happens after composition and only where a source's own icon has not won the slot; the asset lands under a name that spells its provenance, and the app never learns the library exists -- it tints one more glyph. A declaration the library cannot answer fails the build, because the promise was made in a translator and is kept here or heard about. Mars climbs from no icons to all twenty-nine categories carrying one, with its packed pins byte-identical.
- commit
- a61fc23562ec4edb82abb51082772a805a4210d5
- parent
- aad407a005521053b4da56ad5220f5610c415aa1
- author
- Dami <dami.etoile@gmail.com>
@@ -0,0 +1,36 @@
1+// Package icons carries the standard icon library the ingestion pipeline2+// enriches icon-less sources from: a curated subset of Mapbox's Maki set,3+// public-domain map glyphs vendored here so the enrichment is as offline as4+// everything else. The app never learns this package exists -- a resolved5+// standard icon is one more asset in a bundle's icons tree, tinted and6+// haloed like any glyph a source shipped itself.7+//8+// Only the names some translator actually speaks are vendored; the embed is9+// the vocabulary, and a name outside it is a build error at resolution10+// time, one table edit old.11+package icons12+13+import (14+ "embed"15+ "fmt"16+ "strings"17+)18+19+//go:embed standard/maki/*.svg standard/maki/LICENSE.txt20+var standard embed.FS21+22+// Standard resolves a set/name reference from the atlas.icon.std attribute23+// to the icon's bytes and the asset name it ships under. The asset name24+// spells its provenance -- std--maki-mountain.svg -- so a bundle listing25+// reads honestly and a source's own artwork can never be shadowed by it.26+func Standard(ref string) ([]byte, string, error) {27+ set, name, found := strings.Cut(ref, "/")28+ if !found || set != "maki" {29+ return nil, "", fmt.Errorf("standard icon %q is not in a vendored set", ref)30+ }31+ data, err := standard.ReadFile("standard/maki/" + name + ".svg")32+ if err != nil {33+ return nil, "", fmt.Errorf("standard icon %q is not vendored", ref)34+ }35+ return data, "std--maki-" + name + ".svg", nil36+}@@ -0,0 +1,27 @@
1+package icons2+3+import (4+ "strings"5+ "testing"6+)7+8+func TestStandardResolvesVendoredNames(t *testing.T) {9+ data, asset, err := Standard("maki/mountain")10+ if err != nil {11+ t.Fatal(err)12+ }13+ if asset != "std--maki-mountain.svg" {14+ t.Fatalf("asset name: %q", asset)15+ }16+ if !strings.Contains(string(data), "<svg") {17+ t.Fatal("resolved bytes are not an svg")18+ }19+}20+21+func TestStandardRefusesStrangers(t *testing.T) {22+ for _, ref := range []string{"maki/skyscraper", "lucide/mountain", "mountain", ""} {23+ if _, _, err := Standard(ref); err == nil {24+ t.Errorf("%q resolved", ref)25+ }26+ }27+}@@ -0,0 +1,116 @@
1+CC0 1.0 Universal2+3+Statement of Purpose4+5+The laws of most jurisdictions throughout the world automatically confer6+exclusive Copyright and Related Rights (defined below) upon the creator and7+subsequent owner(s) (each and all, an "owner") of an original work of8+authorship and/or a database (each, a "Work").9+10+Certain owners wish to permanently relinquish those rights to a Work for the11+purpose of contributing to a commons of creative, cultural and scientific12+works ("Commons") that the public can reliably and without fear of later13+claims of infringement build upon, modify, incorporate in other works, reuse14+and redistribute as freely as possible in any form whatsoever and for any15+purposes, including without limitation commercial purposes. These owners may16+contribute to the Commons to promote the ideal of a free culture and the17+further production of creative, cultural and scientific works, or to gain18+reputation or greater distribution for their Work in part through the use and19+efforts of others.20+21+For these and/or other purposes and motivations, and without any expectation22+of additional consideration or compensation, the person associating CC0 with a23+Work (the "Affirmer"), to the extent that he or she is an owner of Copyright24+and Related Rights in the Work, voluntarily elects to apply CC0 to the Work25+and publicly distribute the Work under its terms, with knowledge of his or her26+Copyright and Related Rights in the Work and the meaning and intended legal27+effect of CC0 on those rights.28+29+1. Copyright and Related Rights. A Work made available under CC0 may be30+protected by copyright and related or neighboring rights ("Copyright and31+Related Rights"). Copyright and Related Rights include, but are not limited32+to, the following:33+34+ i. the right to reproduce, adapt, distribute, perform, display, communicate,35+ and translate a Work;36+37+ ii. moral rights retained by the original author(s) and/or performer(s);38+39+ iii. publicity and privacy rights pertaining to a person's image or likeness40+ depicted in a Work;41+42+ iv. rights protecting against unfair competition in regards to a Work,43+ subject to the limitations in paragraph 4(a), below;44+45+ v. rights protecting the extraction, dissemination, use and reuse of data in46+ a Work;47+48+ vi. database rights (such as those arising under Directive 96/9/EC of the49+ European Parliament and of the Council of 11 March 1996 on the legal50+ protection of databases, and under any national implementation thereof,51+ including any amended or successor version of such directive); and52+53+ vii. other similar, equivalent or corresponding rights throughout the world54+ based on applicable law or treaty, and any national implementations thereof.55+56+2. Waiver. To the greatest extent permitted by, but not in contravention of,57+applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and58+unconditionally waives, abandons, and surrenders all of Affirmer's Copyright59+and Related Rights and associated claims and causes of action, whether now60+known or unknown (including existing as well as future claims and causes of61+action), in the Work (i) in all territories worldwide, (ii) for the maximum62+duration provided by applicable law or treaty (including future time63+extensions), (iii) in any current or future medium and for any number of64+copies, and (iv) for any purpose whatsoever, including without limitation65+commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes66+the Waiver for the benefit of each member of the public at large and to the67+detriment of Affirmer's heirs and successors, fully intending that such Waiver68+shall not be subject to revocation, rescission, cancellation, termination, or69+any other legal or equitable action to disrupt the quiet enjoyment of the Work70+by the public as contemplated by Affirmer's express Statement of Purpose.71+72+3. Public License Fallback. Should any part of the Waiver for any reason be73+judged legally invalid or ineffective under applicable law, then the Waiver74+shall be preserved to the maximum extent permitted taking into account75+Affirmer's express Statement of Purpose. In addition, to the extent the Waiver76+is so judged Affirmer hereby grants to each affected person a royalty-free,77+non transferable, non sublicensable, non exclusive, irrevocable and78+unconditional license to exercise Affirmer's Copyright and Related Rights in79+the Work (i) in all territories worldwide, (ii) for the maximum duration80+provided by applicable law or treaty (including future time extensions), (iii)81+in any current or future medium and for any number of copies, and (iv) for any82+purpose whatsoever, including without limitation commercial, advertising or83+promotional purposes (the "License"). The License shall be deemed effective as84+of the date CC0 was applied by Affirmer to the Work. Should any part of the85+License for any reason be judged legally invalid or ineffective under86+applicable law, such partial invalidity or ineffectiveness shall not87+invalidate the remainder of the License, and in such case Affirmer hereby88+affirms that he or she will not (i) exercise any of his or her remaining89+Copyright and Related Rights in the Work or (ii) assert any associated claims90+and causes of action with respect to the Work, in either case contrary to91+Affirmer's express Statement of Purpose.92+93+4. Limitations and Disclaimers.94+95+ a. No trademark or patent rights held by Affirmer are waived, abandoned,96+ surrendered, licensed or otherwise affected by this document.97+98+ b. Affirmer offers the Work as-is and makes no representations or warranties99+ of any kind concerning the Work, express, implied, statutory or otherwise,100+ including without limitation warranties of title, merchantability, fitness101+ for a particular purpose, non infringement, or the absence of latent or102+ other defects, accuracy, or the present or absence of errors, whether or not103+ discoverable, all to the greatest extent permissible under applicable law.104+105+ c. Affirmer disclaims responsibility for clearing rights of other persons106+ that may apply to the Work or any use thereof, including without limitation107+ any person's Copyright and Related Rights in the Work. Further, Affirmer108+ disclaims responsibility for obtaining any necessary consents, permissions109+ or other rights required for any use of the Work.110+111+ d. Affirmer understands and acknowledges that Creative Commons is not a112+ party to this document and has no duty or obligation with respect to this113+ CC0 or use of the Work.114+115+For more information, please see116+<http://creativecommons.org/publicdomain/zero/1.0/>@@ -0,0 +1,4 @@
1+2+<svg id="beach" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15">3+ <path d="M5.3608,1.67l-.01,4.02a4.4523,4.4523,0,0,0-1.1-.11c-.37.1-.74.63-1.1.76A4.2019,4.2019,0,0,1,5.3608,1.67Zm2.41-.64,2.03,3.45a3.1832,3.1832,0,0,1,.84-.61c.36-.1.94.17,1.34.11A4.2018,4.2018,0,0,0,7.7708,1.03ZM1.0008,13h13c-.66-.66-2.64-1.11-4.34-1.33l-1.87-7c.52-.05,1.15.03,1.53,0l-2.11-3.6h-.01a6.1743,6.1743,0,0,0-.7.14,4.3792,4.3792,0,0,0-.64.22l-.01,4.15c.35-.17.84-.54,1.3-.74l1.8,6.74c-.58-.05-1.09-.08-1.45-.08C6.0308,11.5,2.0008,12,1.0008,13Z"/>4+</svg>\ No newline at end of file@@ -0,0 +1,4 @@
1+2+<svg width="15" height="15" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg" id="caution">3+ <path d="M1.09302 11.8924L6.84044 1.3905C7.12539 0.869834 7.87461 0.869834 8.15956 1.3905L13.907 11.8924C14.1805 12.3922 13.818 13.0019 13.2474 13.0019H1.75258C1.18195 13.0019 0.819505 12.3922 1.09302 11.8924ZM8.29999 8L8.70296 5.5822C8.75375 5.27743 8.51873 5 8.20976 5H6.79022C6.48125 5 6.24623 5.27743 6.29703 5.5822L6.69999 8H8.29999ZM8.59999 9.9C8.59999 9.29249 8.10751 8.8 7.49999 8.8C6.89248 8.8 6.39999 9.29249 6.39999 9.9C6.39999 10.5075 6.89248 11 7.49999 11C8.10751 11 8.59999 10.5075 8.59999 9.9Z"/>4+</svg>\ No newline at end of file@@ -0,0 +1,4 @@
1+2+<svg width="15" height="15" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg" id="circle-stroked">3+ <path d="M7.5 12.3C4.84898 12.3 2.7 10.151 2.7 7.5C2.7 4.84898 4.84898 2.7 7.5 2.7C10.151 2.7 12.3 4.84898 12.3 7.5C12.3 10.151 10.151 12.3 7.5 12.3ZM7.5 14C11.0899 14 14 11.0899 14 7.5C14 3.9101 11.0899 1 7.5 1C3.9101 1 1 3.9101 1 7.5C1 11.0899 3.9101 14 7.5 14Z"/>4+</svg>\ No newline at end of file@@ -0,0 +1,4 @@
1+2+<svg height="15" viewBox="0 0 15 15" width="15" xmlns="http://www.w3.org/2000/svg" id="landuse">3+ <path d="m8.911 2.85401c.02782.02324.05021.0523.06558.08512.01538.03283.02337.06863.02342.10488v3.706c0 .0663-.02634.12989-.07322.17678-.04689.04688-.11048.07322-.17678.07322h-2.5c-.0663 0-.12989-.02634-.17678-.07322-.04688-.04689-.07322-.11048-.07322-.17678v-1.75h-1v1.75c0 .0663-.02634.12989-.07322.17678-.04689.04688-.11048.07322-.17678.07322h-2.5c-.0663 0-.12989-.02634-.17678-.07322-.04688-.04689-.07322-.11048-.07322-.17678v-3.7c.00005-.03625.00804-.07205.02342-.10488.01537-.03282.03776-.06188.06558-.08512l3.254-2.728c.04413-.0370337.09989-.0573335.1575-.0573335s.11337.0202998.1575.0573335zm-1.159 5.646c-.04915.00002-.09718.01464-.138.042l-2.614 1.95799h-1v-1.99999c0-.13261-.05268-.25979-.14645-.35355-.09376-.09377-.22094-.14645-.35355-.14645s-.25979.05268-.35355.14645c-.09377.09376-.14645.22094-.14645.35355v1.99999h-1v-1.99999c0-.13261-.05268-.25979-.14645-.35355-.09376-.09377-.22094-.14645-.35355-.14645s-.25979.05268-.35355.14645c-.09377.09376-.14645.22094-.14645.35355v4.24999c0 .0663.02634.1299.07322.1768.04689.0469.11048.0732.17678.0732h6.5c.0663 0 .12989-.0263.17678-.0732.04688-.0469.07322-.1105.07322-.1768v-3.99999c.00026-.03274-.00596-.0652-.0183-.09552-.01235-.03032-.03057-.0579-.05363-.08114-.02305-.02324-.05048-.04168-.0807-.05427s-.06263-.01907-.09537-.01907zm6.248-3.255v6.49999c0 .0671-.0267.1315-.0741.1789-.0474.0475-.1118.0741-.1789.0741h-3.494c-.0668 0-.1308-.0264-.1782-.0734s-.0743-.1108-.0748-.1776v-6.49699c-.00013-.03287.0062-.06544.0188-.09583.0125-.03039.0309-.058.0541-.08124.0233-.02324.0509-.04165.0813-.05417s.0629-.01889.0958-.01876h.75v-.751c0-.06604.0262-.12937.0729-.17607s.1101-.07293.1761-.07293h1.5c.0668.00053.1306.02742.1776.07481.047.04738.0734.11144.0734.17819v.747h.755c.065 0 .1273.02581.1732.07176.046.04595.0718.10826.0718.17324zm-1 4.75499h-2v1h2zm0-1.99999h-2v1h2zm0-2h-2v1h2z"/>4+</svg>\ No newline at end of file@@ -0,0 +1,4 @@
1+2+<svg height="15" viewBox="0 0 15 15" width="15" xmlns="http://www.w3.org/2000/svg" id="mountain">3+ <path d="m7.5 1c-.3 0-.4.2-.6.4l-5.8 9.5c-.1.1-.1.3-.1.4 0 .5.4.7.7.7h11.6c.4 0 .7-.2.7-.7 0-.2 0-.2-.1-.4l-5.7-9.5c-.2-.2-.4-.4-.7-.4zm0 1.5 3.3 5.5h-.8l-1.5-1.5-1 1.5-1-1.5-1.5 1.5h-.9z"/>4+</svg>\ No newline at end of file@@ -0,0 +1,4 @@
1+2+<svg width="15" height="15" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg" id="square-stroked">3+ <path d="M11.7 3.3V11.7H3.3V3.3H11.7ZM12 2H3C2.4477 2 2 2.4477 2 3V12C2 12.5523 2.4477 13 3 13H12C12.5523 13 13 12.5523 13 12V3C13 2.4477 12.5523 2 12 2Z"/>4+</svg>\ No newline at end of file@@ -0,0 +1,4 @@
1+2+<svg height="15" viewBox="0 0 15 15" width="15" xmlns="http://www.w3.org/2000/svg" id="triangle-stroked">3+ <path d="m7.52427.500405c-.2814-.0091-.5456.1419-.6907.3948l-5.5 9.869195c-.3118.5434.0614 1.2355.6664 1.2356h11.00003c.605-.0001.9782-.6922.6664-1.2355l-5.50003-9.869195c-.1362-.2374-.378-.3861-.6421-.3949zm-.0243 2.398895 4.12673 7.4704h-8.25343z"/>4+</svg>\ No newline at end of file@@ -0,0 +1,4 @@
1+2+<svg height="15" viewBox="0 0 15 15" width="15" xmlns="http://www.w3.org/2000/svg" id="triangle">3+ <path d="m7.5385 1c-.2948 0-.4883.1772-.6154.3846l-5.8462 9.5385c-.0769.0769-.0769.2307-.0769.3846 0 .5385.3846.6923.6923.6923h11.6154c.3846 0 .6923-.1538.6923-.6923 0-.1538 0-.2308-.0769-.3846l-5.7693-9.5385c-.1258-.2081-.3656-.3846-.6153-.3846z"/>4+</svg>\ No newline at end of file@@ -0,0 +1,4 @@
1+2+<svg version="1.1" id="volcano" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15">3+ <path id="path6447" d="M8.4844,1.0002
	c-0.1464,0.005-0.2835,0.0731-0.375,0.1875L6.4492,3.2619L4.8438,1.7385C4.4079,1.3374,3.7599,1.893,4.0899,2.385l1.666,2.4004
	C5.9472,5.061,6.3503,5.0737,6.5586,4.8108C6.7249,4.6009,7,4.133,7.5,4.133s0.7929,0.4907,0.9414,0.6777
	c0.175,0.2204,0.4973,0.2531,0.7129,0.0723l1.668-1.4004c0.4408-0.3741,0.0006-1.0735-0.5273-0.8379L9,3.2268V1.5002
	C9.0002,1.2179,8.7666,0.9915,8.4844,1.0002L8.4844,1.0002z M5,6.0002L2.0762,11.924C1.9993,12.0009,2,12.155,2,12.3088
	c0,0.5385,0.3837,0.6914,0.6914,0.6914h9.6172c0.3846,0,0.6914-0.153,0.6914-0.6914c0-0.1538,0.0008-0.2309-0.0762-0.3848L10,6.0002
	c-0.5,0-1,0.5-1,1v0.5c0,0.277-0.223,0.5-0.5,0.5S8,7.7772,8,7.5002v-0.5c0-0.2761-0.2238-0.5-0.5-0.5S7,6.7241,7,7.0002v2
	c0,0.277-0.223,0.5-0.5,0.5S6,9.2772,6,9.0002v-2C6,6.5002,5.5,6.0002,5,6.0002z"/>4+</svg>\ No newline at end of file@@ -0,0 +1,4 @@
1+2+<svg width="15" height="15" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg" id="waterfall">3+ <path d="M14 1H5C3.34315 1 2 2.34314 2 4V8.87918C1.39704 9.28261 1 9.96937 1 10.75C1 11.9927 2.00736 13 3.25 13C3.7127 13 4.14279 12.8599 4.5 12.6209C4.85721 12.8599 5.2873 13 5.75 13C6.28601 13 6.77826 12.812 7.16413 12.5H7.51743C8.01909 12.8165 8.61403 13 9.25 13C11.0449 13 12.5 11.5449 12.5 9.75002C12.5 8.39777 11.6747 7.23919 10.5 6.74934V5C10.5 3.89543 11.3954 3 12.5 3H14V1ZM11.5 9.75C11.5 10.9926 10.4926 12 9.25 12C8.71418 12 8.2221 11.8127 7.83567 11.5H6.75009C6.52204 11.8036 6.15895 12 5.75 12C5.34105 12 4.97796 11.8036 4.74991 11.5H4.25009C4.02204 11.8036 3.65895 12 3.25 12C2.55964 12 2 11.4404 2 10.75C2 10.1453 2.42944 9.64082 3 9.525V5.75C3 5.33579 3.33579 5 3.75 5C4.16421 5 4.5 5.33579 4.5 5.75V9C4.5 9.27614 4.72386 9.5 5 9.5C5.27614 9.5 5.5 9.27614 5.5 9V6.75C5.5 6.33579 5.83579 6 6.25 6C6.66421 6 7 6.33579 7 6.75V9C7 9.27614 7.22386 9.5 7.5 9.5C7.77614 9.5 8 9.27614 8 9V5.75C8 5.33579 8.33579 5 8.75 5C9.16421 5 9.5 5.33579 9.5 5.75V7.51373C10.625 7.63809 11.5 8.59186 11.5 9.75Z"/>4+</svg>\ No newline at end of file@@ -0,0 +1,4 @@
1+2+<svg version="1.1" id="wetland" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15">3+ <path d="M1.48,4.5C1.905,4.1467,2.4483,3.968,3,4c1.2273-0.0869,2.3154,0.7836,2.5,2l0.78,4.68c-0.6394-0.2893-1.3759-0.2709-2,0.05
	L3.48,6C3.1874,5.1347,2.3926,4.5387,1.48,4.5z M7.48,11.24c0.3816-0.3076,0.825-0.5293,1.3-0.65L10,3
	c0.2926-0.8653,1.0874-1.4613,2-1.5c-0.4198-0.3485-0.955-0.5269-1.5-0.5C9.2727,0.9131,8.1846,1.7836,8,3l-1.3,7.79
	C6.9786,10.9052,7.2408,11.0565,7.48,11.24z M11.8,10.74L11.8,10.74c0.1565-0.1277,0.3238-0.2414,0.5-0.34L13,6
	c0.2926-0.8653,1.0874-1.4613,2-1.5c-0.4198-0.3485-0.955-0.5269-1.5-0.5c-1.2273-0.0869-2.3154,0.7836-2.5,2l-0.67,4
	C10.8731,10.123,11.3778,10.377,11.8,10.74z M14,12L14,12c-0.4346-0.01-0.8579,0.1394-1.19,0.42l-0.47,0.41
	c-0.2847,0.2546-0.7153,0.2546-1,0c-0.15-0.12-0.29-0.26-0.44-0.39c-0.7076-0.5968-1.7424-0.5968-2.45,0
	c-0.16,0.13-0.31,0.28-0.47,0.41c-0.2847,0.2546-0.7153,0.2546-1,0c-0.16-0.13-0.31-0.28-0.47-0.41
	c-0.7059-0.5912-1.7341-0.5912-2.44,0c-0.15,0.13-0.29,0.27-0.44,0.39c-0.0892,0.0715-0.1909,0.1258-0.3,0.16
	c-0.2922,0.0652-0.5969-0.0301-0.8-0.25c-0.2475-0.214-0.5117-0.4079-0.79-0.58C1.5336,12.0421,1.2974,11.9865,1.06,12H1
	c-0.2761,0-0.5,0.2239-0.5,0.5S0.7239,13,1,13l0,0c0.246,0.0145,0.4762,0.126,0.64,0.31L2,13.57
	c0.6717,0.55,1.6308,0.5747,2.33,0.06c0.19-0.14,0.36-0.32,0.55-0.47c0.2847-0.2546,0.7153-0.2546,1,0l0.39,0.35
	c0.6937,0.6189,1.7322,0.6484,2.46,0.07c0.15-0.11,0.27-0.25,0.42-0.37c0.2976-0.3038,0.7851-0.3087,1.0889-0.0111
	c0.0037,0.0037,0.0074,0.0074,0.0111,0.0111l0.39,0.35c0.4866,0.4124,1.1485,0.5516,1.76,0.37c0.3825-0.1036,0.7286-0.3113,1-0.6
	c0.1549-0.1772,0.3674-0.294,0.6-0.33l0,0c0.2761,0,0.5-0.2239,0.5-0.5S14.2761,12,14,12z"/>4+</svg>\ No newline at end of file@@ -256,6 +256,48 @@ func worldPixel(longitude, latitude float64) (x, y float64) {
256256 return x, y257257 }258258 259+// standardIcons names a library glyph for each Gazetteer feature-type code,260+// in the set/name vocabulary of atlas.icon.std. The codes are the IAU's own261+// and hold across bodies, so the Moon's craters will wear the same rim the262+// day a capture arrives. The reading leans semantic -- a mons is a mountain,263+// a patera a volcano, a palus literally a marsh -- and falls back to shape264+// language where no glyph says the thing: rimmed circles for depressions,265+// squares for plains, triangles for relief. A code missing here is not an266+// error; its category keeps the viewer's initials until someone curates it,267+// and re-curating is one table edit and a re-translation of captures already268+// on disk.269+var standardIcons = map[string]string{270+ "AA": "maki/circle-stroked", // crater: a rim271+ "CA": "maki/circle-stroked", // catena: a chain of them272+ "CB": "maki/circle-stroked", // cavus: a hollow273+ "AL": "maki/circle", // albedo feature: a patch of tone274+ "MA": "maki/circle", // macula: a dark spot275+ "MO": "maki/mountain", // mons276+ "TH": "maki/mountain", // tholus: a small domical mountain277+ "CO": "maki/triangle-stroked", // collis: small hills278+ "DO": "maki/triangle-stroked", // dorsum: a ridge279+ "RU": "maki/triangle", // rupes: a scarp280+ "SC": "maki/triangle", // scopulus: a lobate scarp281+ "PA": "maki/volcano", // patera: an irregular volcanic crater282+ "CH": "maki/caution", // chaos: broken terrain283+ "LA": "maki/caution", // labes: a landslide284+ "CM": "maki/wetland", // chasma: parallel-walled canyon, read as lines285+ "FO": "maki/wetland", // fossa: a long narrow trench286+ "LB": "maki/wetland", // labyrinthus: intersecting valleys287+ "SU": "maki/wetland", // sulcus: subparallel furrows288+ "PE": "maki/wetland", // palus: literally a marsh289+ "VA": "maki/water", // vallis: a channel something carved290+ "SE": "maki/water", // serpens: a sinuous feature291+ "FL": "maki/waterfall", // fluctus: flow terrain292+ "PL": "maki/square-stroked", // planitia: a low plain293+ "VS": "maki/square-stroked", // vastitas: a vast one294+ "PM": "maki/square", // planum: a plateau295+ "MN": "maki/square", // mensa: a mesa296+ "TA": "maki/landuse", // terra: extensive land297+ "LN": "maki/landuse", // lingula: a tongue of plateau298+ "UN": "maki/beach", // unda: dunes299+}300+259301 // buildGroups arranges the Gazetteer's flat feature list the way the pipeline260302 // expects: one category per feature type, all under one group. The type's261303 // descriptor -- "Crater, craters" names the singular and the plural -- keeps@@ -283,6 +325,10 @@ func buildGroups(capture *Capture, ids *mgdoc.IDSpace, scope string) ([]mgdoc.Gr
283325 if err != nil {284326 return nil, err285327 }328+ attrs := map[string]string{semconv.KeyRenderAs: semconv.RenderAsPin}329+ if standard := standardIcons[byType[featureType][0].Code]; standard != "" {330+ attrs[semconv.KeyIconStd] = standard331+ }286332 category := mgdoc.Category{287333 ID: categoryID,288334 Title: name,@@ -290,7 +336,7 @@ func buildGroups(capture *Capture, ids *mgdoc.IDSpace, scope string) ([]mgdoc.Gr
290336 DisplayType: "markers",291337 Visible: true,292338 Locations: make([]mgdoc.Location, 0, len(byType[featureType])),293- Attrs: map[string]string{semconv.KeyRenderAs: semconv.RenderAsPin},339+ Attrs: attrs,294340 }295341 for _, feature := range byType[featureType] {296342 locationID, err := ids.Claim("trek:feature:" + strconv.FormatInt(feature.ID, 10))@@ -8,6 +8,7 @@ import (
88 "strings"99 "testing"1010 11+ "github.com/FelineStateMachine/atlas/internal/icons"1112 "github.com/FelineStateMachine/atlas/internal/mgdoc"1213 )1314 @@ -272,6 +273,37 @@ func TestTranslateRejectsBadCaptures(t *testing.T) {
272273 }273274 }274275 276+// TestStandardIconsAreVendored holds the curation table to the library: a277+// promise the translator writes into a capture's translation must be one the278+// resolution pass can keep.279+func TestStandardIconsAreVendored(t *testing.T) {280+ for code, ref := range standardIcons {281+ if _, _, err := icons.Standard(ref); err != nil {282+ t.Errorf("code %s names %s: %v", code, ref, err)283+ }284+ }285+}286+287+func TestCategoriesDeclareTheirStandardIcons(t *testing.T) {288+ m := translate(t, fixture())289+ declared := map[string]string{}290+ for _, group := range m.Groups {291+ for _, category := range group.Categories {292+ declared[category.Title] = category.Attrs["atlas.icon.std"]293+ }294+ }295+ want := map[string]string{296+ "Crater": "maki/circle-stroked",297+ "Mons": "maki/mountain",298+ "Planitia": "maki/square-stroked",299+ }300+ for title, ref := range want {301+ if declared[title] != ref {302+ t.Errorf("%s declares %q, want %q", title, declared[title], ref)303+ }304+ }305+}306+275307 func TestMaybeTranslatePassesOthersThrough(t *testing.T) {276308 doc := []byte(`{"id": 5, "title": "As Captured"}`)277309 out, err := MaybeTranslate("map", doc)@@ -24,7 +24,8 @@ import (
2424 // 2 merge resolution: subset names, adoption, one-to-one matches2525 // 3 origin provenance on every map; overlap merges across map slugs2626 // 4 semantic conventions: attributes ride every payload27-const policyRevision = 427+// 5 standard icons resolved for categories that declare one28+const policyRevision = 52829 2930 // writeBundles packs each game into its own .atlas file, named by game,3031 // capture day, and stamp. The directory is a registry, not a mirror: a new@@ -17,6 +17,7 @@ import (
1717 "strings"1818 1919 "github.com/FelineStateMachine/atlas/internal/bundle"20+ "github.com/FelineStateMachine/atlas/internal/icons"2021 "github.com/FelineStateMachine/atlas/internal/ignmap"2122 "github.com/FelineStateMachine/atlas/internal/pbmap"2223 "github.com/FelineStateMachine/atlas/internal/semconv"@@ -413,6 +414,12 @@ func run(source, tileManifestPath, bundleDir string) error {
413414 }414415 out.Games = merged415416 417+ for index := range out.Games {418+ if err := resolveStandardIcons(&out.Games[index]); err != nil {419+ return fmt.Errorf("%s: %w", out.Games[index].Slug, err)420+ }421+ }422+416423 sort.Slice(out.Games, func(i, j int) bool { return out.Games[i].Title < out.Games[j].Title })417424 return writeBundles(out, tiles, filepath.Dir(tileManifestPath), bundleDir)418425 }@@ -523,6 +530,41 @@ func withAttr(attrs map[string]string, key, value string) map[string]string {
523530 return out524531 }525532 533+// resolveStandardIcons makes good on every atlas.icon.std declaration: the534+// named library glyph lands in the game's icon set under its provenance-535+// spelling name, and the category wears it exactly the way it would wear a536+// source's own artwork. It runs after composition so merged-in categories537+// resolve too, and only where a source's own icon has not already won the538+// slot. A declaration the library cannot answer fails the build: the539+// promise was made in a translator, and it is kept here or heard about.540+func resolveStandardIcons(game *catalogGame) error {541+ for mapIndex := range game.Maps {542+ m := &game.Maps[mapIndex]543+ for groupIndex := range m.Groups {544+ categories := m.Groups[groupIndex].Categories545+ for categoryIndex := range categories {546+ category := &categories[categoryIndex]547+ ref := category.Attrs[semconv.KeyIconStd]548+ if ref == "" || category.IconAsset != "" {549+ continue550+ }551+ data, asset, err := icons.Standard(ref)552+ if err != nil {553+ return fmt.Errorf("map %s category %q: %w", m.Slug, category.Title, err)554+ }555+ if game.Icons == nil {556+ game.Icons = make(map[string][]byte)557+ }558+ game.Icons[asset] = data559+ category.IconAsset = asset560+ category.IconPicture = false561+ category.Attrs = withAttr(category.Attrs, semconv.KeyIconKind, semconv.IconKindGlyph)562+ }563+ }564+ }565+ return nil566+}567+526568 func sortGameMaps(gameSlug string, maps []catalogMap) {527569 order := make(map[string]int)528570 for index, slug := range preferredMapOrder[gameSlug] {