Having fun and making something useful with GeoLocation and open APIs

All that matters is that you are making something you love, to the best of your ability, here and now.
— Rick Rubin, The Creative Act: A Way of Being

Working in a modern software development team is very rewarding and a lot of fun. Nowadays it seems most teams are autonomous, working tightly together, moving as one. Often using pair or even mob programming techniques. Scrumming in the morning and turning on a dime shipping to production several times during the day. This fast paced, hands on approach gets a lot of stuff done and I love it. But sometimes I miss the days where I could sit for hours with some good and loud music in my ears in complete flow state just playing around with my own ideas and the computer.

It is when we act freely, for the sake of the action itself rather than for ulterior motives, that we learn to become more than what we were.
— Mihaly Csikszentmihalyi, Flow: The Psychology of Optimal Experience

MX Trax

This is the story of how I made MXTrax, a simple web app that shows you motocross tracks near your location along with other useful information.

It all started after my oldest daughter started riding motocross. We have been to lots of tracks, and finding information about where the tracks are, how to get in contact, when they are open and so on was a bit cumbersome.

I figured that there had to be some place other than facebook groups where this information was available. I was right, there is a lot of information out there, but it is hard to find, and even harder to navigate once it has been found. So I decided I wanted a tool for myself, to be able to find and list the motocross tracks nearby. A perfect opportunity for getting into the flow state while grokking something new.

My daughter on her YZ85

Gathering and curating the data took some time, but once that was done creating the website did not take long. At least that is how it felt. Time flies when you are having fun.

The website is a NextJS app running on a small Heroku dyno. Although I love github actions, there is something to be said for the UX in Heroku. It is perfect for small projects like this. Just link it to your github repo and tada:

For the UI components i ended up using Mantine with Tabler icons. The kit is a well balanced and simple library, with just the right amount of bells and whistles. Nothing fancy, but complete and easy to use. It also looks great IMO.

For the maps I used OpenStreetMap and LeafletJS. Using google maps was not an option for several reasons. The main one being cost.

For the Geo APIs I used open apis at geonorge

Splash images are supplied by Unsplash This is a service I highly recommend.

Weather data provided using open APIs at api.met.no

Digging in

Lets look at some details from the app. Please keep in mind that this code is thrown together in a relative short amount of time. It’s just for fun, so nothing too serious.

GeoLocation

The app is very centered around where you are located. Your location can be changed to a given address of your choice if you want, or if geolocation is not supported. Here is the react hook:

const GeolocationPositionError = {
  [1]: "Vi mangler tilgang til din plassering. Vennligst endre instillinger og forsøk igjen.",
  [2]: "Kan ikke finne din lokasjon akkurat nå. Prøv igjen litt senere.",
  [3]: "Kunne ikke finne din lokasjon pga tidsavbrudd. Vennligst prøv igjen.",
};
const GeoLocationPositionUnsupported =
  "Beklager, lokasjonstjenester støttes ikke av din nettleser. Prøv en annen nettleser";

export const useGeolocation = () => {
  const [position, setPosition] = useState<GeolocationPosition | undefined>(undefined)
  const [pending, setPending] = useState(true)
  const [error, setError] = useState<string | undefined>(undefined)

  useEffect(() => {
    if (!("geolocation" in navigator)) {
      setError(GeoLocationPositionUnsupported);
      return;
    }

    if (!position) {
      setPending(true);
      navigator.geolocation.getCurrentPosition(
        (position) => {
          setPosition(position);
          setPending(false);
        },
        (e) => {
          console.error(e);
          setError(GeolocationPositionError[e.code]);
          setPending(false);
        },
      );
    }
  }, [position]);
  return {
    position,
    error,
    pending,
  };
};

As I mentioned the location can also be set manually. For this I use a simple typeahead component using the Mantine Autocomplete component and a hook backed by the sok API at geonorge.

picture of typeahead address search in the banner

// geonorge sok api handler
export default async function handler(req, res) {
    const { sok } = await JSON.parse(req.body);
    const api = "https://ws.geonorge.no/adresser/v1/sok?";
    const response = await fetch(
        api +
        new URLSearchParams({
            sok,
            fuzzy: "true",
            utkoordsys: "4258",
            treffPerSide: "25",
            side: "0",
            asciiKompatibel: "true",
        }),
    );
    if (!response.ok) {
        res.status(response.status).send(response.statusText);
    }
    const json = await response.json();
    res.status(200).json(json.adresser ?? []);
}

// hook for searching addresses
export const useAddressSearch = () => {
    const [result, setResult] = useState<{ error: boolean; addresser: Address[] }>({
        error: null,
        addresser: [],
    });
    const [searchAddress, setSearchAddress] = useDebouncedState('', 250);

    useEffect(() => {
        (async () => {
            if (searchAddress === '') {
                setResult({ error: null, addresser: [] });
                return;
            }
            const { error, addresser } = await searchAdresses({
                sok: searchAddress,
            });
            setResult({ error, addresser });
        })();
    }, [searchAddress]);

    const clearSearch = () => {
        setSearchAddress('');
        setResult({ error: null, addresser: [] });
    };

    return {
        setSearchAddress,
        clearSearch,
        searchAddress,
        result,
    };
};

// the "you are here" component displayed in the banner
export function DuErHer() {
    const {isSearch} = useGeoLocationContext();

    return (isSearch ?
            <FraSok/> :
            <FraMinPlassering/>
    );
}

// the typeahead search component
function FraSok() {
    const {setSearchAddress, result, clearSearch} = useAddressSearch();

    return (
        <Paper shadow="xs" p="sm" opacity={0.9}>
            <Autocomplete
                type="search"
                miw="40vw"
                filter={({options}) => options}
                placeholder="Søk etter adresse"
                data={result}
                value={value}
                onChange={handleChange}
                rightSection={value === '' ? null :
                    <ActionIcon size={32} radius="md" variant="subtle" onClick={() => {
                        clearSearch();
                    }}>
                        <IconXboxX stroke={1.5}/>
                    </ActionIcon>}
            />
            <Button
                    variant="filled"
                    onClick={() => {
                        setLocationToCurrentPosition();
                    }}
            >
                Nullstill
            </Button>
        </Paper>
    );
}

OpenStreetMap with Leaflet

picture showing the Leaflet map

The Leaflet map is rendered on the main page. Since this code needs to run on the client I use next/dynamic to tell next to bypass server side rendering. Also notice the little trick of using lat/lng as key in the MapContainer. This hack ensures the leaflet map is rerendered when the location changes.

// .
// ├── components
// │   ├── TrackMap.tsx
// │   ├── TrackMapDynamic.tsx
// ├── pages
// │   ├── index.tsx

// index.tsx
import { TrackMapDynamic } from "../components/TrackMapDynamic";
<TrackMapDynamic />;

// TrackMapDynamic.tsx
import dynamic from "next/dynamic";
export const TrackMapDynamic = dynamic(() => import("./TrackMap"), {
  ssr: false,
});

// TrackMap.tsx
const TrackMap = () => {
  return (
    <MapContainer
      key={`${location.latitude} ${location.longitude}`}
      center={center}
      zoom={9}
      scrollWheelZoom={false}
      style={{ height: "50vh" }}
    >
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <Marker
        position={center}
        icon={icon({
          iconUrl: "/tabler-map-pin-filled.svg",
          iconRetinaUrl: "/tabler-map-pin-filled.svg",
          iconSize: [32, 32],
        })}
      >
        <Popup>Du er her</Popup>
      </Marker>
      {tracks.map((track) => (
        <Marker
          key={track.name}
          position={[track.coordinates.lat, track.coordinates.lng]}
          icon={icon({
            iconUrl: "/dirtbike-icon.png",
            iconRetinaUrl: "/dirtbike-icon.png",
            iconSize: [50, 50],
          })}
        >
          <Popup minWidth={250}>
            <TrackDetails track={track} />
          </Popup>
        </Marker>
      ))}
    </MapContainer>
  );
};

How’s the weather?

picture showing the weather forecast of a selected track

The weather forecast component is a simple hook combined with a little mapping code to make the results match my UI. I only want to show three icons per day (morning, noon and night) for the next week or so. The results from the API are comprehensive but not a perfect match. So I map the timeseries taking one detail for each period. One nifty detail is that I link to yr.no with the day that was clicked on in the table selected when you land. This is done by passing the index of the series as the ?i= query param in the url. A bit brittle, but worth it. Sure hope it doesn’t break 😅

// the mapping code
const grouped: Record<
    string,
    Record<'morning' | 'noon' | 'evening', WeatherForecastSummary>
> = summary.reduce((acc, e) => {
    const date = new Date(e.time);
    const day = date.toDateString();
    acc[day] = acc[day] || {};
    const time = date.getUTCHours() < 12 ? 'morning' : date.getUTCHours() < 18 ? 'noon' : 'evening';
    acc[day][time] = acc[day][time] || e;
    return acc;
}, {});

// the data fetching hook
export const useWeatherForecast = ({ lat, lng }) => {
  const [forecast, setForecast] = useState(null);
  useEffect(() => {
    const fetchForecast = async () => {
      if (!lat || !lng) {
        return;
      }

      const response = await fetch(
        "https://api.met.no/weatherapi/locationforecast/2.0/compact?" +
          new URLSearchParams({
            lat,
            lon: lng,
          }),
      );
      const json = await response.json();
      setForecast(json);
    };
    fetchForecast().catch(console.error);
  }, [lat, lng]);

  return { forecast };
};

// weather forecast table
export function WeatherForecast({ lat, lng }) {
    return (
        <Table mt={"xl"}>
            <Table.Thead>
                <Table.Tr>
                    <Table.Th></Table.Th>
                    <Table.Th>Morgen</Table.Th>
                    <Table.Th>Middag</Table.Th>
                    <Table.Th>Kveld</Table.Th>
                </Table.Tr>
            </Table.Thead>
            <Table.Tbody>
                {Object.entries(summary).map(([day, items], i) => (
                    <Table.Tr
                        key={day}
                        style={{ cursor: "pointer" }}
                        onClick={() => {
                            router.push(
                                `https://www.yr.no/nb/v%C3%A6rvarsel/timetabell/${lat},${lng}/?i=${i}`,
                            );
                        }}
                    >
                        <Table.Td>
                            <Text size={"xl"}>{formatDateTime(day)}</Text>
                        </Table.Td>
                        <Table.Td>
                            <ForecastCard item={items["morning"]} units={units} textSize={"xl"} />
                        </Table.Td>
                        <Table.Td>
                            <ForecastCard item={items["noon"]} units={units} textSize={"xl"} />
                        </Table.Td>
                        <Table.Td>
                            <ForecastCard item={items["evening"]} units={units} textSize={"xl"} />
                        </Table.Td>
                    </Table.Tr>
                ))}
            </Table.Tbody>
        </Table>
    );
}


// the forecast card, shown in the table cells
function ForecastCard({ item, textSize, units }) {
  if (item === undefined) {
    return null;
  }
  const { temp, symbol_code, precipitation_amount } = item;
  return (
    <Group gap="xs">
      <WeatherIcon symbol_code={symbol_code} size={50} />
      <Stack gap="xs">
        <Text size={textSize} c="dimmed">
          {temp} &deg;{units.air_temperature[0].toUpperCase()}
        </Text>
        <Text size={textSize} c="dimmed">
          {precipitation_amount} {units.precipitation_amount}
        </Text>
      </Stack>
    </Group>
  );
}

function WeatherIcon({ symbol_code, size }) {
  return (
    <Image
      src={`/weathericon/svg/${symbol_code}.svg`}
      alt={symbol_code}
      width={size}
      height={size}
    />
  );
}

Unsplash

Picture of Unsplash image used in the banner

In the page header there is a background image showing a random high quality picture of some motocross content. This is made possible by the Unsplash API.

// the data fetching hook
export const useRandomMxImage = () => {
  const [image, setImage] = useState(undefined);

  useEffect(() => {
    (async () => {
      const response = await fetch("/api/unsplash/random", {
        method: "GET",
      });
      if (!response.ok) {
        return;
      }
      const randomImage = await response.json();
      setImage(randomImage);
    })();
  }, []);

  return { image };
};

// used in the banner
export function Banner() {
  const { image } = useRandomMxImage();
  return (
    <Card
      radius="md"
      className={classes.card}
      style={{
        backgroundImage: image?.urls?.regular
          ? `url(${image?.urls?.regular})`
          : "none",
      }}
    >
      <Overlay className={classes.overlay} opacity={0.55} zIndex={0} />
    ...
    </Card>
  );
}


// and the api handler with some caching
const unsplash = createApi({ accessKey: process.env.UNSPLASH_ACCESS_KEY });

const cache = {
  data: null,
  timestamp: 0,
};

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (cache.data && Date.now() - cache.timestamp < 1_800_000) {
    res.status(200).json(cache.data);
    return;
  }
  const random = await unsplash.photos.getRandom({ query: "motocross" });
  if (random.type === "error") {
    res.status(500).json(random.errors);
  } else {
    cache.data = random.response;
    cache.timestamp = Date.now();
    res.status(200).json(random.response);
  }
}

Conclusion

We are living in the golden age of software development. Never before have we been this efficient.
Never before have so many powerful tools and libraries been at our fingertips, for free or at a very low cost.
In the daily hustle it is easy to loose inspiration and flow.

I hope this article leaves you with some inspiration to go out and find something fun to make, just for the sake of making it.

GLHF

To be happy we need something to solve. Happiness is therefore a form of action
— Mark Manson, The Subtle Art of Not Giving a F*ck: A Counterintuitive Approach to Living a Good Life
Ken Gullaksen

Ken Gullaksen er en erfaren seniorkonsulent og fullstack utvikler med over 17 års erfaring innen systemutvikling. Med en solid bakgrunn i Fullstack utvikling, DevOps, teknisk arkitektur og kontinuerlig leveranser, har Ken vært en viktig bidragsyter i flere store prosjekter i både offentlig og privat sektor. Han er kjent for sin evne til å fjerne flaskehalser og hjelpe teammedlemmer med å overkomme hindringer, noe som fører til økt produktivitet og effektivitet i utviklingsteamene.

Ken er ikke bare teknisk dyktig, men er også hyggelig og lett å samarbeide med, noe som gjør ham til en verdifull ressurs i ethvert team. Han har en sterk forståelse for hva som fungerer i et team, og han er kontinuerlig fokusert på forbedringer, både i kodekvalitet og i arbeidsprosesser. Ken er også en entusiastisk bidragsyter til Open Source-miljøet, hvor hans arbeid har blitt anerkjent og brukt av mange utviklere globalt.

Med sin lange erfaring innen DevOps og autonome team, samt en proaktiv tilnærming til problemløsning, hjelper Ken virksomheter med å modernisere og effektivisere sine IT-løsninger. Hans arbeid har ikke bare forbedret brukeropplevelser, men har også sikret robuste og vedlikeholdbare systemer som tåler tidens tann.

På fritiden er Ken en blid og imøtekommende person som liker å tilbringe tid med sin familie, sykle og kjøre motocross.

Forrige
Forrige

For en helg - for et sted, for et vær, for en gjeng!

Neste
Neste

Velkommen til Erlend!