New to OpenCode? Start with OpenCode setup, model providers and course budget, then return here for your chosen app.
A dot on a map looks exact, even when the record only says "somewhere near Freiburg"
Occurrence records, such as the ones you download from GBIF, give every find a latitude and longitude. Some come from a phone GPS and are good to a few metres. Others are old herbarium sheets placed at a town centre, good to 10 or 25 kilometres. On an ordinary map both look like the same confident dot. Some rows are simply wrong: a latitude of 94, two values swapped, or 0,0 typed in where nothing was known.
Occurrence Map draws each record as a dot with a halo sized to its stated uncertainty, marks records without a stated uncertainty differently, lists every row it could not map and why, and shows the citation and licence filter for what you see.
You will build it with OpenCode from 30 synthetic records of wild garlic (Allium ursinum) in Baden-Württemberg. The records are invented for this course; the state outline is public-domain Natural Earth data. The app runs locally in your browser, with no map tiles and no internet connection. Step 15 shows how to use a real GBIF download or your own permitted records.
You will finish with: a map that shows 25 of 30 records, names the 5 rows it refused to map, draws a 25 km halo exactly as wide as its 50 km scale bar, and prints a citation line with the date and licence filter.
First watch the 1:48 result tour. It shows where you are going. The numbered steps below are the lesson: what to click in OpenCode, what to paste, and what must appear before you continue.
Silent, with on-screen explanations. OpenCode scenes are captures from the recorded build; app scenes operate the actual OpenCode-generated app. Captions and highlights were added afterwards. Read the transcript (opens in a new tab) · Download the movie (opens in a new tab).
Use: keep this manual beside OpenCode and your browser. Complete each Success check before continuing. Click any screenshot to enlarge it.
Files: reference kit: working app, practice records, outline, four checkpoints and the exact prompts (opens in a new tab). Extract the ZIP first: Windows right-click → Extract All; Mac double-click. Keep it beside your project.
Your personal coach: Step 5 turns your OpenCode session into a one-step-at-a-time guide. For help later, paste the complete coach brief (opens in a new tab) into your project session and tell it your step number.
Files for this journey
Download everything at once as the reference kit (ZIP) (opens in a new tab), or single files as you reach the step that needs them. Save each file into your project folder under exactly this name. If a file opens in the browser instead of downloading, press Ctrl+S (Mac: Command+S) to save it.
| File | First used in |
|---|---|
occurrences.csv (opens in a new tab) | Step 4 · Add the practice files |
bw-outline.json (opens in a new tab) | Step 4 · Add the practice files |
tab-separated.csv (opens in a new tab) | Step 14 · Load another file and find the wrong citation |
The kit also contains the finished app, the recorded checkpoints (checkpoints/…) and the exact prompts.
1. Get OpenCode readyLink to step
Do
- Open an ordinary website. Complete your university Wi-Fi sign-in if prompted.
- Install OpenCode Desktop: follow Step 1 of Set up OpenCode in the L1 book Start With OpenCode.
- Open OpenCode. For English labels, use Settings → Preferences → Language → English. In German: Einstellungen → Präferenzen → Sprache → English.
Success: OpenCode opens. Continue to the project; choose the model in Step 3.
If different: installation or Wi-Fi failure → use the seminar recovery card.
2. Open your projectLink to step
Do
- In Finder or File Explorer, create a folder named
occurrence-mapin Documents. - In OpenCode, click Home → Add project.
- Select your
occurrence-mapfolder. Click Select folder at bottom right. - Click New session.
Success: the new-session screen shows occurrence-map below the message box.
If different: another folder name → return Home and add the correct folder. Open the project before choosing a model.
3. Select a working modelLink to step
Do: click the model name below the message box. Select the model assigned for the seminar. Wait until its name appears below the box.
Success: the model's name is displayed below the box. Step 5 checks that it answers.
If different: no working model → ask the instructor to check the seminar connection. This recording used Space Bunny Free; free availability is not guaranteed. If a model you need is missing from the list, Manage models at the bottom of the menu switches models on and off.
Agent control: if Plan is visible, select Build before requesting edits. In this recorded interface the agent selector is hidden and Build is the default.
4. Add the practice filesLink to step
Do
- Open the extracted reference kit, then its
datafolder. - Copy
occurrences.csv(opens in a new tab) andbw-outline.json(opens in a new tab) into youroccurrence-mapfolder. - Open
occurrences.csvin a text editor or spreadsheet and look at the first line.
Success: the first line lists 16 column names, starting gbifID,occurrenceID,species,scientificName,decimalLatitude,decimalLongitude,coordinateUncertaintyInMeters. There are 30 records below it.
These are Darwin Core names, the shared vocabulary GBIF uses for biodiversity records. The three that matter most here: decimalLatitude and decimalLongitude are the position in degrees; coordinateUncertaintyInMeters is the radius of the smallest circle, around that position, that contains the place where the plant really was. license says how you may reuse the record. The records are synthetic: real places, realistic values, invented finds. bw-outline.json is the border of Baden-Württemberg from Natural Earth, simplified to about 1 km so it fits in the app.
If different: the spreadsheet squeezes everything into one column → open the file in a text editor instead. Do not save it from the spreadsheet.
5. Start your OpenCode coachLink to step
Do: click once inside the message box. Paste this block. Check that occurrence-map still appears below the box, then click the upward-arrow Send button.
Be my patient app-building coach. I am a beginner.
We will build Occurrence Map in this folder, one step at a time.
Use one index.html with inline HTML, CSS and JavaScript,
no packages, server, network calls or map tiles.
occurrences.csv holds synthetic course records; bw-outline.json is
a public-domain Natural Earth outline of Baden-Württemberg.
Explain in at most four short lines: Do, See, Success, If different.
Never claim to see my browser unless you actually inspected it.
Do not create files yet. Reply exactly:
Ready. Next: draw the map.
Wait: while OpenCode answers, Send changes to Stop. Continue only after Stop disappears.
Success: the reply reads Ready. Next: draw the map. No index.html exists yet.
If different: an access or quota error → return to Step 3. Files already appeared → send Stop. Remove only the files you just created, then wait.
6. Request the map and the row checksLink to step
A projection is the rule that turns positions on the round Earth into positions on a flat map. This request uses the simplest honest one for a small region: one degree north is always 111.2 km, and one degree east is 111.2 × cos(48.7°) = 73.4 km, because lines of longitude come closer together towards the pole. 48.7° N runs through the middle of Baden-Württemberg. The map is drawn in kilometres, so later a halo in metres can be converted exactly.
Do: paste this request into the same session and click Send once. Words like SVG (a drawing format browsers understand) are instructions for OpenCode; you do not need to know them.
Read occurrences.csv and bw-outline.json first. Then create only
index.html: a polished, accessible app with the heading "Occurrence Map".
A double-clicked page cannot read files by itself, so embed the exact
CSV rows as the example and the outline ring. Add a "Load example"
button and a "Choose CSV" file input that reads a file locally.
Find columns by their header names; accept comma- or tab-separated
files with quoted fields. Show the species from the species column.
Check every row. Not mapped, with a reason: decimalLatitude or
decimalLongitude missing or not a number; latitude outside -90..90 or
longitude outside -180..180; exactly 0,0 (a placeholder); outside the
map area 47.4-49.9 N, 7.4-10.6 E, but if swapping the two values would
put it inside, say "latitude and longitude look swapped". Never fix a
row silently. List each as "Line N (gbifID ...): reason"; line 1 = header.
Map: one SVG whose units are kilometres. Projection: x = (lon - 7.4) ×
111.195 × cos(48.7°), y = (49.9 - lat) × 111.195. Draw the outline, a
labelled 50 km scale bar and every mapped record as a small dot.
Show "N rows · M mapped · K not mapped". No halos, filters or record
details yet. Save index.html. Tell me how to open it and what to check.
Wait: about seven minutes in the recording. You will see tool activity such as Read, Write and Shell. Continue after Stop turns back into Send.
Success: the reply says index.html was saved and names the counts to expect.
If different: code appears only in the chat → send Save that code as index.html in this project. Tell me the saved path. A permission question → allow writing index.html inside occurrence-map only.
7. Open the app and check the five rows not mappedLink to step
Do
- In Finder/File Explorer, open
occurrence-mapand double-clickindex.html. If a text editor opens, right-click → Open With → your browser. - Click Load example.
Success: 30 rows, 25 mapped, 5 not mapped, species Allium ursinum, and exactly these five lines:
| Line | What is in the file | Why it is not mapped |
|---|---|---|
| 7 | latitude 94.2104 | no latitude is above 90; probably 49.21 typed as 94.21 |
| 13 | latitude 9.0512, longitude 48.5231 | the two values look swapped |
| 18 | 0, 0 | a placeholder: that point is in the Atlantic, off West Africa |
| 24 | 48.1372, 11.5761 | a valid point, but in Munich, outside the map area |
| 28 | longitude blank | position incomplete |
Line 1 is the header, so line 7 is the sixth record. The app reports these rows; it never repairs them. Whether line 13 really is swapped is for you to check in the source, not for the app to guess.
If different: a row is missing from the list or a count differs → send Line [number] should not be mapped because [reason from the table]. I see [what you see]. Fix only the check. Wait, refresh (Ctrl+R / Command+R) and click Load example again.
Keep: copy the whole project folder beside the original and name it occurrence-map-01-map. Continue in the original. Recorded version: checkpoints/05-map/index.html in the kit.
8. Add the uncertainty halosLink to step
Why halos matter: a record with 10 km uncertainty says "the plant was somewhere in this 20 km wide circle". Drawn as a dot, it looks as precise as a GPS point and can put a woodland species into a town or the wrong valley. The halo shows the honest answer. A blank uncertainty is different again: nobody said how precise it is. Drawing it as zero would claim perfect precision, so it gets its own symbol.
Do: paste this request and click Send.
I see 30 rows · 25 mapped · 5 not mapped. Now add uncertainty halos.
coordinateUncertaintyInMeters is the radius of the smallest circle that
contains the true location. Around each dot draw a translucent circle
with radius = coordinateUncertaintyInMeters / 1000 map units, so it is
in kilometres like the scale bar, never in pixels. Dots stay on top.
A blank uncertainty means unknown, not zero: draw that record as a
hollow diamond with no halo. Add a legend: dot with halo = "stated
uncertainty (to scale)", hollow diamond = "uncertainty not stated",
and the scale bar. Keep the checks and the not-mapped list unchanged.
Save index.html. Tell me what to check, then wait.
Wait: about two and a half minutes in the recording. Then refresh the app and click Load example.
Success: OpenCode's reply says the counts stay 30 · 25 · 5, that 22 dots have halos and 3 records are hollow diamonds.
If different: the reply mentions pixels or a fixed circle size → send Halo radius must be coordinateUncertaintyInMeters / 1000 in map units (km), the same units as the scale bar. Change only the halo radius.
9. Check the halos against the scale barLink to step
Do: look at the map. Compare the two largest halos with the 50 km scale bar.
Success
- The big halo in the north-east (the herbarium record for Schwäbisch Hall district, 25,000 m) is as wide as the 50 km scale bar: 25 km radius, 50 km across.
- The Freiburg halo in the south-west (10,000 m) is 0.4 times the bar: 20 km across.
- The mapping-survey records (3,900 m) have small halos, about 8 km across. Phone records (5–65 m) have halos smaller than their dot, so you see only the dot.
- Three hollow diamonds (Ulm, Offenburg, Göppingen): uncertainty not stated.
The dot itself is a symbol about 2.5 km wide on this map; it says nothing about precision. Only the halo does.
If different: the big halo is much smaller or larger than the scale bar → the conversion is wrong. Send The 25000 m halo must be exactly as wide as the 50 km scale bar. It is [smaller/larger]. Use radius = metres / 1000 in the SVG's kilometre units, never pixels.
Keep: copy the project as occurrence-map-02-halos. Recorded version: checkpoints/06-halos/index.html.
10. Add filters, counts and the citationLink to step
Licences in GBIF data: CC0_1_0 means no conditions; CC_BY_4_0 means you must credit the source; CC_BY_NC_4_0 means credit and no commercial use. If your map goes into a commercial report, for example, you would leave the NC records out, and the reader should see how many you left out. This is course guidance, not legal advice.
Do: paste this request and click Send.
Halos and diamonds look right. Now add filters above the map:
"Year from" and "Year to" (whole years, inclusive; defaults = the
earliest and latest year among mapped records) and one checkbox per
licence value found in the license column, all checked.
Draw only mapped records that pass both filters. Show:
"Shown S of M mapped records · E excluded by the current filter ·
K rows not mapped" and "U shown without stated uncertainty".
Under the map add an editable "Dataset citation" field, prefilled with:
Synthetic course data: Allium ursinum occurrences in Baden-Württemberg
(32dots curriculum, 2026). Not GBIF records.
Below it show the full citation line: that text, then "Accessed
YYYY-MM-DD." (today) and "Licence filter: " with the checked licences.
It updates when the filter or the field changes.
Keep everything else. Save index.html. Tell me what to check, then wait.
Wait: about five minutes in the recording. Then refresh and click Load example.
Success: Shown 25 of 25 mapped records · 0 excluded by the current filter · 5 rows not mapped, 3 shown without stated uncertainty, Year from 1987, Year to 2025, three licence boxes ticked, and a citation line ending with today's date and all three licences.
If different: the counts include the 5 rows not mapped as "excluded" → send Excluded by the current filter counts only mapped records that the year or licence filter hides. The 5 rows not mapped are counted separately. Fix only the counts.
11. Check the counts for three filtersLink to step
Do: set the filters one row at a time and read the status line under the checkboxes. Leave Year to at 2025.
| Year from | Licence boxes ticked | Status line | Without stated uncertainty |
|---|---|---|---|
| 2010 | all three | Shown 18 of 25 mapped records · 7 excluded by the current filter | 2 |
| 2010 | CC_BY_4_0 and CC0_1_0 | Shown 11 of 25 mapped records · 14 excluded by the current filter | 1 |
| 1987 | CC0_1_0 only | Shown 6 of 25 mapped records · 19 excluded by the current filter | 1 |
These counts were computed independently from the CSV. OpenCode's reply also suggested checking 2020 to 2024 with all licences: Shown 8 of 25. That is correct too.
Success: all three rows match. With the second filter, the citation line ends Accessed [today's date]. Licence filter: CC_BY_4_0, CC0_1_0.
If different: send With Year from [year] and [licences] ticked I expect Shown [n] of 25 and [n] excluded. I see [your line]. Check that the years are inclusive and that both filters apply together. Change nothing else.
12. Open a recordLink to step
Do: paste this request and click Send.
The counts match. Now make every drawn record clickable and
keyboard-focusable (Tab, then Enter), with an accessible name like
"gbifID 9990000003, 1987". Opening a record shows a panel with its line
number and every column of that row as "name: value" in file order,
blanks shown as "(blank)". Add one line: "Halo radius: 10 km
(coordinateUncertaintyInMeters 10000)", or for a blank value
"Uncertainty not stated: position precision unknown".
The panel has a Close button. The open record is highlighted on the map.
Keep filters, counts and citation unchanged.
Save index.html. Tell me what to check, then wait.
Wait, then refresh, click Load example, and click the dot with the medium halo near Freiburg (south-west). Then click Close, press Tab until a diamond is outlined, and press Enter.
Success: the Freiburg record opens as Line 4, gbifID 9990000003, basisOfRecord: PRESERVED_SPECIMEN, and Halo radius: 10 km (coordinateUncertaintyInMeters 10000). The diamond opens with coordinateUncertaintyInMeters: (blank) and Uncertainty not stated: position precision unknown. The panel appears below the citation; scroll down if you do not see it.
If different: nothing happens on click → send Clicking a dot does not open the record panel. Make each marker open it on click and on Enter. Change nothing else.
Keep: copy the project as occurrence-map-03-details. Recorded version: checkpoints/08-details/index.html.
13. Make it yoursLink to step
Do: send this to OpenCode:
Change only the visible heading to "My Occurrence Map".
Keep the browser title consistent.
Do not change the map, checks, filters, counts, citation or details.
Save index.html and tell me which lines changed.
Refresh the app and click Load example.
Success: your heading reads My Occurrence Map; the counts are still 30 · 25 · 5. In the recording OpenCode read "keep the browser title consistent" as "leave it unchanged", so the browser tab still says Occurrence Map. If you want both the same, ask for it in one sentence.
If different: anything else changed → ask OpenCode to undo that part, or restore index.html from your -03-details copy.
14. Load another file and find the wrong citationLink to step
Do: in the app click Choose CSV and select tab-separated.csv (opens in a new tab) from the kit's data folder. It is laid out like a GBIF "simple" download: tab-separated, 50 columns in a different order, and a different species.
The app read the file correctly: 6 rows, 5 mapped, 1 not mapped (line 4, latitude 147.9912), species Anemone nemorosa. But the citation still names the Allium ursinum course data. A map made from this file would be cited with the wrong dataset. The request in Step 10 never said what happens to the citation when the data change, and OpenCode did not ask.
Do: send this correction:
I chose tab-separated.csv. Species shows Anemone nemorosa and 5 of 6
rows are mapped, which is right. But the citation still names the
Allium ursinum course data, so this map would be cited with the wrong
dataset. The prefilled citation belongs only to the embedded example.
When I choose a file, empty the Dataset citation field and, until I
type a citation, show "Citation needed: paste the citation for
<file name> (for a GBIF download, the DOI citation GBIF gives you)"
instead of the citation line. Load example restores the example
citation. Also write "1 row not mapped", not "1 rows not mapped".
Keep everything else. Save index.html, then wait.
Wait, then refresh and choose tab-separated.csv again.
Success: the Dataset citation field is empty and the line says Citation needed: paste the citation for tab-separated.csv (for a GBIF download, the DOI citation GBIF gives you). The status reads 1 row not mapped. Type any text into the field: the citation line appears with the date and licence filter. Click Load example: the Allium citation returns.
If different: the old citation stays → send the correction again with I still see the Allium citation after choosing tab-separated.csv. at the start.
15. Try a real GBIF download or your own recordsLink to step
Privacy rule: never paste or attach unpublished or personal data to an AI model without permission. The app reads your file inside your browser and sends nothing. Keep your own files outside the project folder, so OpenCode never reads them. Take special care with exact locations of rare or protected species: they are sensitive even when the file is yours.
Do (GBIF):
- On gbif.org, search Occurrences for your species; filter by country Germany and the state, and to records with coordinates. Downloading needs a free GBIF account, which you create yourself.
- Choose Download → Simple. Unzip the file you receive. Its
.csvfile is tab-separated; the app accepts that. - In the app, choose that file. Read the Not mapped list first.
- Paste the citation GBIF shows for your download (it contains a DOI, a permanent link) into Dataset citation.
Do (your own records): save a copy as CSV UTF-8 with at least the columns gbifID, species, decimalLatitude, decimalLongitude, and ideally coordinateUncertaintyInMeters, year and license. Use your own record number in gbifID. Leave an unknown uncertainty blank; never type 0.
Success: you can explain every row in the Not mapped list, and the largest halos belong to the records you expected to be vague. Some publishers blur the location of sensitive species on purpose; their uncertainty then includes that blurring.
If different: everything is "outside the map area" → your records are not in Baden-Württemberg. The map area and outline are fixed in this version; ask OpenCode for another region only together with a public-domain outline for it. Very large files may be slow; filter the download first.
16. Leave a README and continue in a fresh sessionLink to step
Do: send this to OpenCode:
Read index.html, then create README.md with: how to open it
(double-click index.html, then Load example or Choose CSV); the columns
it needs; the checks that stop a row being mapped; the projection and
why halos are drawn in kilometres; what the hollow diamond means; the
practice results (30 rows, 25 mapped, 5 not mapped; Year from 2010
without CC_BY_NC_4_0: shown 11, excluded 14); a "Cite as" line exactly
as the app shows it for that filter today, with the date and the
licence filter; what the app does not do (no tiles, no border check,
no duplicate or taxonomy check); and one next task: export the shown
records as CSV. Do not change index.html. Tell me which file you
created, then wait.
After Stop disappears, click the + (New session) button at the top, next to the session tab. Confirm occurrence-map and your model are shown, then send:
Read README.md and index.html without editing.
In three bullets tell me the app's purpose, how to open it,
and the recorded next task.
Success: the new session names the app's purpose, how to open it, and "export the shown records as CSV" from the files alone. Open README.md: it contains a line like Cite as: Synthetic course data: … Accessed 2026-09-25. Licence filter: CC_BY_4_0, CC0_1_0 with your own date.
If different: it answers about another folder → click Home, choose occurrence-map, click New session and ask again. Wrong README content → ask OpenCode to correct README from index.html only.
17. Make your next improvementLink to step
Do: save a working checkpoint copy first. Then write one request, for example the recorded next task:
Add [one behaviour] so [my user] can [purpose].
Keep the practice result: 30 rows, 25 mapped, 5 not mapped;
Year from 2010 without CC_BY_NC_4_0 shows 11 of 25.
I will know it works when [visible result].
Tell me which part will change, implement only this, then wait.
Give a colleague index.html, README.md and occurrences.csv. Ask them to follow the README without your help.
Success: your feature works, the practice counts are unchanged, and your colleague finds the five rows not mapped and the 25 km halo on their own.
If stuck: paste into your coach session: I am on step [number]. I did: [action]. I expected: [result from the manual]. I see: [exact text]. Give me one next action, then wait.
What this app does not do: no map tiles or background map; it checks the map rectangle, not the state border; the projection is exact only along 48.7° N (east–west distances are up to about 2 % off at the northern and southern edges of the state); no duplicate detection, taxonomy check or date check; halos show only the stated uncertainty, which can itself be wrong. It is a teaching tool, not a validated analysis.
The method behind these steps
Structure it: one species, one region, one question per record: where, how precisely, and may I use it. Understand it: Darwin Core columns, uncertainty as a radius in metres, a projection in kilometres, licences. Choose tools: a bundled public-domain outline and one local HTML file instead of map tiles. Build it: one request at a time, each checked against independently computed counts, including the citation error you found. See T01-L01 · From Idea to Working Result: The Four-Quadrant Method.










