clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat: lend the planets a standard hand of icons

Dami committed a61fc23
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>
21 files changed, +358 -2
Ainternal/icons/icons.go36+++++++++
Ainternal/icons/icons_test.go27+++++++
Ainternal/icons/standard/maki/LICENSE.txt116++++++++++++++++++++++++++++++
Ainternal/icons/standard/maki/beach.svg4+
Ainternal/icons/standard/maki/caution.svg4+
Ainternal/icons/standard/maki/circle-stroked.svg4+
Ainternal/icons/standard/maki/circle.svg4+
Ainternal/icons/standard/maki/landuse.svg4+
Ainternal/icons/standard/maki/mountain.svg4+
Ainternal/icons/standard/maki/square-stroked.svg4+
Ainternal/icons/standard/maki/square.svg4+
Ainternal/icons/standard/maki/triangle-stroked.svg4+
Ainternal/icons/standard/maki/triangle.svg4+
Ainternal/icons/standard/maki/volcano.svg4+
Ainternal/icons/standard/maki/water.svg4+
Ainternal/icons/standard/maki/waterfall.svg4+
Ainternal/icons/standard/maki/wetland.svg4+
Minternal/trekmap/trekmap.go48++++++++++++-
Minternal/trekmap/trekmap_test.go32++++++++
Mtools/generate/bundles.go3+-
Mtools/generate/main.go42+++++++++++
A internal/icons/icons.go +36 -0
@@ -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+}
A internal/icons/icons_test.go +27 -0
A internal/icons/standard/maki/LICENSE.txt +116 -0
@@ -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/>
A internal/icons/standard/maki/beach.svg +4 -0
A internal/icons/standard/maki/caution.svg +4 -0
A internal/icons/standard/maki/circle-stroked.svg +4 -0
A internal/icons/standard/maki/circle.svg +4 -0
A internal/icons/standard/maki/landuse.svg +4 -0
@@ -0,0 +1,4 @@
1+<?xml version="1.0" encoding="UTF-8"?>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
A internal/icons/standard/maki/mountain.svg +4 -0
A internal/icons/standard/maki/square-stroked.svg +4 -0
A internal/icons/standard/maki/square.svg +4 -0
A internal/icons/standard/maki/triangle-stroked.svg +4 -0
A internal/icons/standard/maki/triangle.svg +4 -0
A internal/icons/standard/maki/volcano.svg +4 -0
A internal/icons/standard/maki/water.svg +4 -0
A internal/icons/standard/maki/waterfall.svg +4 -0
A internal/icons/standard/maki/wetland.svg +4 -0
@@ -0,0 +1,4 @@
1+<?xml version="1.0" encoding="UTF-8"?>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&#xA;&#x9;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&#xA;&#x9;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&#xA;&#x9;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&#xA;&#x9;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&#xA;&#x9;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&#xA;&#x9;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&#xA;&#x9;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&#xA;&#x9;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&#xA;&#x9;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&#xA;&#x9;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&#xA;&#x9;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&#xA;&#x9;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&#xA;&#x9;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&#xA;&#x9;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
M internal/trekmap/trekmap.go +47 -1
@@ -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))
M internal/trekmap/trekmap_test.go +32 -0
M tools/generate/bundles.go +2 -1
M tools/generate/main.go +42 -0
@@ -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] {