Search

Observations with KronoGraph

Integrations

Use MapWeave with KronoGraph for in-depth temporal analysis.

Observations with KronoGraph
View live example →

Interact with the timeline to filter the observations. The observations shown on the map correspond to the time range in the timeline, so narrow the time range with zoom or scroll to move forwards and backwards in time.

MapWeave can be integrated with KronoGraph to visualize individual observations as events and allow filtering of data based on the timeline range. The KronoGraph controls, such as focus and pin, can be used to filter specific entities and their journeys on the map. Clicking on observations on the map can be synced to the KronoGraph ping feature to highlight the corresponding position on the timeline, which is very useful for busy datasets.

import { createTimeline } from "kronograph";
import { ObservationsLayer } from "mapweave/layers";
import { MapWeave } from "mapweave/mapbox";
import {
  backgroundedMapEntities,
  boundsPerEvent,
  mapEntities,
  observations,
  observationsPerEntity,
  observationsPerEvent,
  timelineData,
} from "./data";

const timelineOptions = {
  focus: { backgroundColor: "rgba(255, 255, 255, 0.1)" },
  scales: { showAtBottom: false },
};

const mainLayerOptions = {
  mode: "individual",
};

const highlightLayerOptions = {
  individual: { autoRange: { min: 0 } },
};

let mapweave;
let timeline;
let mainObservationsLayer;
let highlightObservationsLayer;

let state = {
  focusedEntityIds: [],
  pinnedEntityIds: [],
  selectedEventId: null,
};

function updateLayer(layer, entities, observations) {
  const isVisible = entities !== undefined && observations !== undefined;
  layer.visible(isVisible);
  if (isVisible) {
    layer.data({ entities, observations });
  }
}

function observationsForEntities(entityIds) {
  return entityIds.length === 1
    ? observationsPerEntity[entityIds[0]]
    : entityIds.reduce((prev, id) => Object.assign(prev, observationsPerEntity[id]), {});
}

function updateLayersFromState() {
  const { focusedEntityIds, pinnedEntityIds, selectedEventId } = state;
  // For a selected event, highlight that ride only, otherwise all pinned and focused taxis
  const highlightObservations = selectedEventId
    ? observationsPerEvent[selectedEventId]
    : observationsForEntities([...focusedEntityIds, ...pinnedEntityIds]);

  // If no selected event or focused taxi, show all taxis, foregrounding any pinned ones
  let mainLayerEntities;
  if (selectedEventId === null && focusedEntityIds.length === 0) {
    mainLayerEntities = pinnedEntityIds.length > 0 ? backgroundedMapEntities : mapEntities;
  }

  updateLayer(mainObservationsLayer, mainLayerEntities, observations);
  updateLayer(highlightObservationsLayer, mapEntities, highlightObservations);
}

function updateState(changes) {
  state = { ...state, ...changes };
  updateLayersFromState();
}

// Use the start and end properties of the event directly
function handleTimelineRange(timeFilterRange) {
  mainObservationsLayer.options({ timeFilterRange });
  highlightObservationsLayer.options({ timeFilterRange });
}

function timelineClickHandler({ eventIds, targetType }) {
  let selectedEventId = null;
  if (targetType === "event") {
    selectedEventId = eventIds[0];
    // Pan and zoom to the event bounds using the Mapbox fitBounds API
    mapweave.map.fitBounds(boundsPerEvent[selectedEventId], { padding: 50 });
  }

  timeline.selection(selectedEventId ?? []);
  updateState({ selectedEventId });
}

function mapweaveClickHandler(e) {
  // Get the time for a clicked observation or trajectory
  const time = e.item?.time ?? e.item?.observations?.[0].time;
  if (typeof time === "number") {
    // Find the event for the entity that includes the required time
    const eventId = Object.keys(timelineData.events).find((eventId) => {
      const {
        entityIds: [entityId],
        time: { start, end },
      } = timelineData.events[eventId];
      return entityId === e.item.entityId && time >= start && time <= end;
    });
    if (eventId) {
      timeline.ping(eventId);
    }
  }
}

function main() {
  mapweave = new MapWeave({ container: "mw", options: { accessToken: VITE_MAPBOX_API_KEY } });

  mainObservationsLayer = new ObservationsLayer({ options: mainLayerOptions });
  highlightObservationsLayer = new ObservationsLayer({ options: highlightLayerOptions });

  mapweave.layers([mainObservationsLayer, highlightObservationsLayer]);
  mapweave.on("click", mapweaveClickHandler);

  updateLayersFromState();
  mapweave.fitBounds();

  timeline = createTimeline({ container: "kg", options: timelineOptions });
  timeline.set(timelineData);
  timeline.fit();

  timeline.on("range", handleTimelineRange);
  timeline.on("focus", ({ focus }) => updateState({ focusedEntityIds: focus }));
  timeline.on("pin", ({ pin }) => updateState({ pinnedEntityIds: pin }));
  timeline.on("click", timelineClickHandler);
}

main();
// [r, g, b]
const taxiColors = {
  "Taxi-304": [255, 17, 92],
  "Taxi-424": [255, 177, 52],
  "Taxi-529": [0, 161, 113],
};
const taxiBackgroundAlpha = 0.4;

// 15 second interval between positions
const positionInterval = 15000;

const taxiRides = {
  "Taxi-304": [
    {
      time: 1375481298000,
      positions: [
        [41.140899, -8.610084],
        [41.140917, -8.610093],
        [41.140863, -8.610021],
        [41.141007, -8.610165],
        [41.141493, -8.613216],
        [41.141448, -8.613378],
        [41.141403, -8.613504],
        [41.141151, -8.614341],
        [41.140863, -8.61516],
        [41.140773, -8.61543],
        [41.140647, -8.616843],
        [41.141052, -8.617698],
        [41.141367, -8.618391],
        [41.141736, -8.619066],
        [41.142303, -8.619957],
        [41.14269, -8.620461],
        [41.143365, -8.62119],
        [41.143977, -8.622603],
        [41.1444, -8.623935],
        [41.144814, -8.625978],
        [41.144706, -8.628264],
        [41.1453, -8.630262],
        [41.145642, -8.630766],
        [41.145633, -8.630739],
        [41.145966, -8.631225],
        [41.146839, -8.632503],
        [41.147739, -8.632188],
        [41.148981, -8.631099],
        [41.150295, -8.629992],
        [41.151132, -8.629191],
        [41.151222, -8.629146],
        [41.151204, -8.629137],
        [41.152122, -8.628804],
        [41.152347, -8.628723],
        [41.152428, -8.628723],
        [41.152626, -8.630298],
        [41.152734, -8.632404],
        [41.152446, -8.635212],
        [41.152869, -8.637642],
        [41.153526, -8.640468],
        [41.154237, -8.642907],
        [41.154408, -8.645571],
        [41.154273, -8.648289],
        [41.154327, -8.649333],
        [41.154453, -8.649837],
        [41.154426, -8.649828],
        [41.154372, -8.649747],
        [41.15439, -8.650017],
        [41.153796, -8.650341],
        [41.15304, -8.649756],
        [41.15214, -8.649261],
        [41.151078, -8.649738],
        [41.150142, -8.649054],
        [41.148774, -8.648964],
        [41.148243, -8.648478],
      ],
    },
    {
      time: 1375483130000,
      positions: [
        [41.140953, -8.610228],
        [41.140962, -8.610192],
        [41.141214, -8.610678],
        [41.14152, -8.61318],
        [41.141367, -8.613729],
        [41.141106, -8.614566],
        [41.140773, -8.61543],
        [41.140629, -8.615862],
        [41.140962, -8.617545],
        [41.141952, -8.619408],
        [41.143203, -8.620992],
        [41.144139, -8.623071],
        [41.144607, -8.624619],
        [41.144796, -8.626365],
        [41.144796, -8.628975],
        [41.145993, -8.631279],
        [41.147091, -8.632773],
        [41.148207, -8.631738],
        [41.149746, -8.630442],
        [41.151204, -8.629164],
        [41.15232, -8.628741],
        [41.152401, -8.62875],
        [41.152626, -8.6301],
        [41.152689, -8.631594],
        [41.152608, -8.633565],
        [41.152482, -8.635986],
        [41.153031, -8.638389],
        [41.154246, -8.638218],
        [41.156154, -8.637264],
        [41.158044, -8.636328],
        [41.15889, -8.635905],
        [41.159223, -8.6364],
        [41.159556, -8.638551],
        [41.159889, -8.640504],
        [41.159907, -8.640621],
        [41.159934, -8.640783],
        [41.160411, -8.641539],
        [41.161167, -8.640693],
        [41.161212, -8.640621],
        [41.161167, -8.640594],
        [41.161185, -8.640648],
        [41.161266, -8.640756],
        [41.161311, -8.640819],
      ],
    },
  ],
  "Taxi-424": [
    {
      time: 1375482458000,
      positions: [
        [41.153724, -8.662059],
        [41.154426, -8.662572],
        [41.156019, -8.663661],
        [41.157738, -8.664795],
        [41.158368, -8.664399],
        [41.159394, -8.661807],
        [41.160681, -8.65845],
        [41.161896, -8.656056],
        [41.162031, -8.655777],
        [41.162049, -8.655768],
        [41.16213, -8.654733],
        [41.161806, -8.652861],
        [41.1615, -8.651034],
        [41.16042, -8.650269],
        [41.15916, -8.648712],
        [41.15754, -8.646651],
        [41.154228, -8.645076],
        [41.151024, -8.644707],
        [41.149179, -8.641215],
        [41.150682, -8.636409],
        [41.151348, -8.632989],
        [41.15223, -8.632494],
        [41.152221, -8.632521],
        [41.152221, -8.63253],
        [41.152293, -8.632521],
        [41.152626, -8.631603],
        [41.152536, -8.629992],
        [41.152437, -8.626842],
        [41.152554, -8.626419],
        [41.151879, -8.625969],
        [41.150556, -8.625897],
        [41.148756, -8.62551],
        [41.147919, -8.62335],
        [41.147802, -8.622855],
        [41.14737, -8.620794],
        [41.146524, -8.620272],
        [41.146146, -8.620137],
        [41.146308, -8.618382],
        [41.145966, -8.617545],
        [41.14494, -8.61732],
        [41.144976, -8.616726],
        [41.145354, -8.615907],
        [41.145507, -8.615574],
        [41.145876, -8.614521],
        [41.145966, -8.61318],
        [41.146047, -8.612172],
        [41.146794, -8.612262],
        [41.147703, -8.612118],
        [41.147874, -8.612172],
        [41.148144, -8.612478],
        [41.148261, -8.612865],
        [41.14845, -8.612991],
        [41.148468, -8.613036],
        [41.14872, -8.613009],
        [41.148954, -8.612955],
        [41.149125, -8.612964],
        [41.149467, -8.612946],
        [41.149917, -8.612946],
        [41.149935, -8.613792],
        [41.149323, -8.614197],
      ],
    },
    {
      time: 1375483996000,
      positions: [
        [41.157441, -8.628435],
        [41.157495, -8.6283],
        [41.157927, -8.627985],
        [41.158827, -8.628822],
        [41.158602, -8.629992],
        [41.158521, -8.630046],
        [41.157873, -8.630361],
        [41.156982, -8.629362],
        [41.157261, -8.628273],
        [41.157441, -8.626608],
        [41.157207, -8.625132],
        [41.157198, -8.62506],
        [41.156325, -8.625168],
        [41.155551, -8.62506],
        [41.155731, -8.623449],
        [41.156028, -8.621667],
        [41.155434, -8.620047],
        [41.15529, -8.619273],
        [41.155146, -8.618508],
        [41.154705, -8.61615],
        [41.154228, -8.613603],
        [41.153598, -8.612496],
        [41.152977, -8.6121],
        [41.151924, -8.612235],
        [41.15115, -8.612172],
        [41.15052, -8.612109],
        [41.150196, -8.612433],
        [41.14998, -8.613297],
        [41.149989, -8.614224],
      ],
    },
  ],
  "Taxi-529": [
    {
      time: 1375481253000,
      positions: [
        [41.150574, -8.646624],
        [41.149215, -8.646255],
        [41.148387, -8.645904],
        [41.148162, -8.644185],
        [41.148081, -8.64216],
        [41.148459, -8.640144],
        [41.148369, -8.638137],
        [41.148171, -8.636148],
        [41.147667, -8.63433],
        [41.146812, -8.632566],
        [41.146326, -8.63127],
        [41.14647, -8.62965],
        [41.145336, -8.628183],
        [41.145417, -8.626257],
        [41.145624, -8.623971],
        [41.145903, -8.621316],
        [41.146002, -8.620569],
        [41.145984, -8.620569],
        [41.145984, -8.620389],
        [41.146218, -8.618931],
        [41.146065, -8.617599],
        [41.144949, -8.617302],
        [41.145057, -8.616537],
        [41.145255, -8.616141],
        [41.145534, -8.615538],
        [41.145759, -8.614908],
        [41.145984, -8.614233],
        [41.146128, -8.61408],
        [41.146344, -8.614278],
        [41.146353, -8.614269],
      ],
    },
    {
      time: 1375482720000,
      positions: [
        [41.150628, -8.647641],
        [41.150673, -8.646723],
        [41.149206, -8.646201],
        [41.148459, -8.645958],
        [41.148171, -8.644302],
        [41.14809, -8.642088],
        [41.148387, -8.640738],
        [41.148423, -8.639055],
        [41.148324, -8.637318],
        [41.148018, -8.635527],
        [41.147487, -8.633889],
        [41.146533, -8.63217],
        [41.145102, -8.629956],
        [41.14467, -8.627184],
        [41.14476, -8.626104],
        [41.144355, -8.623854],
        [41.143572, -8.621595],
        [41.142357, -8.620065],
        [41.141331, -8.618409],
        [41.141034, -8.617689],
        [41.140818, -8.617275],
        [41.14062, -8.616825],
        [41.140521, -8.616051],
        [41.140728, -8.615421],
        [41.141241, -8.614566],
        [41.14125, -8.614233],
        [41.141403, -8.61399],
        [41.141466, -8.613414],
        [41.141142, -8.610624],
        [41.140818, -8.609985],
        [41.141061, -8.608482],
        [41.141439, -8.606871],
        [41.141808, -8.605125],
        [41.141835, -8.603118],
        [41.142033, -8.600976],
        [41.141538, -8.598798],
        [41.140611, -8.596782],
        [41.139882, -8.594595],
        [41.140053, -8.592633],
        [41.140071, -8.592345],
        [41.140143, -8.591508],
        [41.140809, -8.589231],
        [41.142456, -8.587323],
        [41.143617, -8.584965],
        [41.143734, -8.58168],
        [41.144328, -8.578854],
        [41.144949, -8.577747],
        [41.146191, -8.578989],
        [41.145237, -8.580159],
        [41.144904, -8.582364],
        [41.145975, -8.583966],
        [41.146731, -8.585199],
        [41.147235, -8.586783],
        [41.147793, -8.586414],
        [41.148864, -8.586036],
        [41.149224, -8.58762],
        [41.148531, -8.588466],
        [41.147244, -8.589033],
        [41.14719, -8.588286],
        [41.147505, -8.588061],
        [41.147793, -8.587845],
        [41.148144, -8.587575],
        [41.148225, -8.587584],
        [41.148225, -8.587593],
        [41.148243, -8.587602],
      ],
    },
  ],
};

function makeData() {
  for (const [taxiId, [r, g, b]] of Object.entries(taxiColors)) {
    const color = `rgb(${r},${g},${b})`;
    const backgroundColor = `rgba(${r},${g},${b},${taxiBackgroundAlpha})`;
    mapEntities[taxiId] = { color };
    backgroundedMapEntities[taxiId] = { color: backgroundColor };
    timelineEntities[taxiId] = { color, label: taxiId };
  }

  for (const [taxiId, rides] of Object.entries(taxiRides)) {
    observationsPerEntity[taxiId] = {};

    for (const { time: start, positions } of rides) {
      const rideId = `${taxiId}/${start}`;
      const end = start + (positions.length - 1) * positionInterval;

      events[rideId] = {
        entityIds: [taxiId],
        time: { start, end },
      };
      observationsPerEvent[rideId] = {};
      let minLat = Infinity;
      let maxLat = -Infinity;
      let minLon = Infinity;
      let maxLon = -Infinity;

      for (const [index, [latitude, longitude]] of positions.entries()) {
        const observationId = `${rideId}/${index}`;
        const observation = {
          type: "observation",
          entityId: taxiId,
          latitude,
          longitude,
          time: start + index * positionInterval,
        };
        observations[observationId] = observation;
        observationsPerEvent[rideId][observationId] = observation;
        observationsPerEntity[taxiId][observationId] = observation;
        if (latitude < minLat) {
          minLat = latitude;
        }
        if (latitude > maxLat) {
          maxLat = latitude;
        }
        if (longitude < minLon) {
          minLon = longitude;
        }
        if (longitude > maxLon) {
          maxLon = longitude;
        }
      }
      boundsPerEvent[rideId] = [
        [minLon, minLat],
        [maxLon, maxLat],
      ];
    }
  }
}

export const mapEntities = {};
export const backgroundedMapEntities = {};
export const observations = {};

const timelineEntities = {};
const events = {};

export const observationsPerEvent = {};
export const observationsPerEntity = {};
export const boundsPerEvent = {};

export const timelineData = { entities: timelineEntities, events };

makeData();
<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="@ci/theme/mw/css/examples.css" />
  </head>
  <body>
    <div style="display: flex; flex-direction: column; height: 100vh">
      <div id="mw" style="flex: 1"></div>
      <div id="kg" style="height: 160px"></div>
    </div>
    <script type="module" src="./code.js"></script>
  </body>
</html>
import Timeline from "kronograph/react/Timeline";
import { ObservationsLayer } from "mapweave/react/layers";
import { MapWeave } from "mapweave/react/mapbox";
import React, { useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import {
  backgroundedMapEntities,
  boundsPerEvent,
  mapEntities,
  observations,
  observationsPerEntity,
  observationsPerEvent,
  timelineData,
} from "./data";

const mapWeaveOptions = { accessToken: VITE_MAPBOX_API_KEY };

const timelineOptions = {
  focus: { backgroundColor: "rgba(255, 255, 255, 0.1)" },
  scales: { showAtBottom: false },
};

const mainLayerOptions = {
  mode: "individual",
};

const highlightLayerOptions = {
  individual: { autoRange: { min: 0 } },
};

function observationsForEntities(entityIds) {
  return entityIds.length === 1
    ? observationsPerEntity[entityIds[0]]
    : entityIds.reduce((prev, id) => Object.assign(prev, observationsPerEntity[id]), {});
}

function Demo() {
  const [timeFilterRange, setTimeFilterRange] = useState(null);
  const [focusedEntityIds, setFocusedEntityIds] = useState([]);
  const [pinnedEntityIds, setPinnedEntityIds] = useState([]);
  const [selectedEventId, setSelectedEventId] = useState(null);
  const { events, entities } = timelineData;
  const timelineRef = useRef(null);
  const mapweaveRef = useRef(null);

  // For a selected event, highlight that ride only, otherwise all pinned and focused taxis
  const highlightObservations = selectedEventId
    ? observationsPerEvent[selectedEventId]
    : observationsForEntities([...focusedEntityIds, ...pinnedEntityIds]);

  // If no selected event or focused taxi, show all taxis, foregrounding any pinned ones
  let mainLayerEntities;
  if (selectedEventId === null && focusedEntityIds.length === 0) {
    mainLayerEntities = pinnedEntityIds.length > 0 ? backgroundedMapEntities : mapEntities;
  }

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
      <MapWeave
        style={{ flex: 1 }}
        options={mapWeaveOptions}
        ref={mapweaveRef}
        onClick={(e) => {
          // Get the time for a clicked observation or trajectory
          const time = e.item?.time ?? e.item?.observations?.[0].time;
          if (typeof time === "number") {
            // Find the event for the entity that includes the required time
            const eventId = Object.keys(timelineData.events).find((eventId) => {
              const {
                entityIds: [entityId],
                time: { start, end },
              } = timelineData.events[eventId];
              return entityId === e.item.entityId && time >= start && time <= end;
            });
            if (eventId) {
              timelineRef.current.ping(eventId);
            }
          }
        }}
      >
        <ObservationsLayer
          data={{ entities: mainLayerEntities || {}, observations }}
          visible={mainLayerEntities !== undefined}
          options={{ ...mainLayerOptions, timeFilterRange }}
        />
        <ObservationsLayer
          data={{ entities: mapEntities, observations: highlightObservations || {} }}
          visible={highlightObservations !== undefined}
          options={{ ...highlightLayerOptions, timeFilterRange }}
        />
      </MapWeave>
      <Timeline
        entities={entities}
        events={events}
        style={{ height: "160px" }}
        options={timelineOptions}
        ref={timelineRef}
        selection={selectedEventId || []}
        onTimelineClick={({ eventIds, targetType }) => {
          let clickedEventId = null;
          if (targetType === "event") {
            clickedEventId = eventIds[0];
            // Pan and zoom to the event bounds using the Mapbox fitBounds API
            mapweaveRef.current?.map.fitBounds(boundsPerEvent[clickedEventId], { padding: 50 });
          }
          setSelectedEventId(clickedEventId);
        }}
        onTimelineChange={({ focus, pin, range }) => {
          if (focus !== undefined) {
            setFocusedEntityIds(focus);
          }
          if (pin !== undefined) {
            setPinnedEntityIds(pin);
          }
          if (range !== undefined) {
            setTimeFilterRange(range);
          }
        }}
      ></Timeline>
    </div>
  );
}

const root = createRoot(document.getElementById("mw"));
root.render(<Demo />);
// [r, g, b]
const taxiColors = {
  "Taxi-304": [255, 17, 92],
  "Taxi-424": [255, 177, 52],
  "Taxi-529": [0, 161, 113],
};
const taxiBackgroundAlpha = 0.4;

// 15 second interval between positions
const positionInterval = 15000;

const taxiRides = {
  "Taxi-304": [
    {
      time: 1375481298000,
      positions: [
        [41.140899, -8.610084],
        [41.140917, -8.610093],
        [41.140863, -8.610021],
        [41.141007, -8.610165],
        [41.141493, -8.613216],
        [41.141448, -8.613378],
        [41.141403, -8.613504],
        [41.141151, -8.614341],
        [41.140863, -8.61516],
        [41.140773, -8.61543],
        [41.140647, -8.616843],
        [41.141052, -8.617698],
        [41.141367, -8.618391],
        [41.141736, -8.619066],
        [41.142303, -8.619957],
        [41.14269, -8.620461],
        [41.143365, -8.62119],
        [41.143977, -8.622603],
        [41.1444, -8.623935],
        [41.144814, -8.625978],
        [41.144706, -8.628264],
        [41.1453, -8.630262],
        [41.145642, -8.630766],
        [41.145633, -8.630739],
        [41.145966, -8.631225],
        [41.146839, -8.632503],
        [41.147739, -8.632188],
        [41.148981, -8.631099],
        [41.150295, -8.629992],
        [41.151132, -8.629191],
        [41.151222, -8.629146],
        [41.151204, -8.629137],
        [41.152122, -8.628804],
        [41.152347, -8.628723],
        [41.152428, -8.628723],
        [41.152626, -8.630298],
        [41.152734, -8.632404],
        [41.152446, -8.635212],
        [41.152869, -8.637642],
        [41.153526, -8.640468],
        [41.154237, -8.642907],
        [41.154408, -8.645571],
        [41.154273, -8.648289],
        [41.154327, -8.649333],
        [41.154453, -8.649837],
        [41.154426, -8.649828],
        [41.154372, -8.649747],
        [41.15439, -8.650017],
        [41.153796, -8.650341],
        [41.15304, -8.649756],
        [41.15214, -8.649261],
        [41.151078, -8.649738],
        [41.150142, -8.649054],
        [41.148774, -8.648964],
        [41.148243, -8.648478],
      ],
    },
    {
      time: 1375483130000,
      positions: [
        [41.140953, -8.610228],
        [41.140962, -8.610192],
        [41.141214, -8.610678],
        [41.14152, -8.61318],
        [41.141367, -8.613729],
        [41.141106, -8.614566],
        [41.140773, -8.61543],
        [41.140629, -8.615862],
        [41.140962, -8.617545],
        [41.141952, -8.619408],
        [41.143203, -8.620992],
        [41.144139, -8.623071],
        [41.144607, -8.624619],
        [41.144796, -8.626365],
        [41.144796, -8.628975],
        [41.145993, -8.631279],
        [41.147091, -8.632773],
        [41.148207, -8.631738],
        [41.149746, -8.630442],
        [41.151204, -8.629164],
        [41.15232, -8.628741],
        [41.152401, -8.62875],
        [41.152626, -8.6301],
        [41.152689, -8.631594],
        [41.152608, -8.633565],
        [41.152482, -8.635986],
        [41.153031, -8.638389],
        [41.154246, -8.638218],
        [41.156154, -8.637264],
        [41.158044, -8.636328],
        [41.15889, -8.635905],
        [41.159223, -8.6364],
        [41.159556, -8.638551],
        [41.159889, -8.640504],
        [41.159907, -8.640621],
        [41.159934, -8.640783],
        [41.160411, -8.641539],
        [41.161167, -8.640693],
        [41.161212, -8.640621],
        [41.161167, -8.640594],
        [41.161185, -8.640648],
        [41.161266, -8.640756],
        [41.161311, -8.640819],
      ],
    },
  ],
  "Taxi-424": [
    {
      time: 1375482458000,
      positions: [
        [41.153724, -8.662059],
        [41.154426, -8.662572],
        [41.156019, -8.663661],
        [41.157738, -8.664795],
        [41.158368, -8.664399],
        [41.159394, -8.661807],
        [41.160681, -8.65845],
        [41.161896, -8.656056],
        [41.162031, -8.655777],
        [41.162049, -8.655768],
        [41.16213, -8.654733],
        [41.161806, -8.652861],
        [41.1615, -8.651034],
        [41.16042, -8.650269],
        [41.15916, -8.648712],
        [41.15754, -8.646651],
        [41.154228, -8.645076],
        [41.151024, -8.644707],
        [41.149179, -8.641215],
        [41.150682, -8.636409],
        [41.151348, -8.632989],
        [41.15223, -8.632494],
        [41.152221, -8.632521],
        [41.152221, -8.63253],
        [41.152293, -8.632521],
        [41.152626, -8.631603],
        [41.152536, -8.629992],
        [41.152437, -8.626842],
        [41.152554, -8.626419],
        [41.151879, -8.625969],
        [41.150556, -8.625897],
        [41.148756, -8.62551],
        [41.147919, -8.62335],
        [41.147802, -8.622855],
        [41.14737, -8.620794],
        [41.146524, -8.620272],
        [41.146146, -8.620137],
        [41.146308, -8.618382],
        [41.145966, -8.617545],
        [41.14494, -8.61732],
        [41.144976, -8.616726],
        [41.145354, -8.615907],
        [41.145507, -8.615574],
        [41.145876, -8.614521],
        [41.145966, -8.61318],
        [41.146047, -8.612172],
        [41.146794, -8.612262],
        [41.147703, -8.612118],
        [41.147874, -8.612172],
        [41.148144, -8.612478],
        [41.148261, -8.612865],
        [41.14845, -8.612991],
        [41.148468, -8.613036],
        [41.14872, -8.613009],
        [41.148954, -8.612955],
        [41.149125, -8.612964],
        [41.149467, -8.612946],
        [41.149917, -8.612946],
        [41.149935, -8.613792],
        [41.149323, -8.614197],
      ],
    },
    {
      time: 1375483996000,
      positions: [
        [41.157441, -8.628435],
        [41.157495, -8.6283],
        [41.157927, -8.627985],
        [41.158827, -8.628822],
        [41.158602, -8.629992],
        [41.158521, -8.630046],
        [41.157873, -8.630361],
        [41.156982, -8.629362],
        [41.157261, -8.628273],
        [41.157441, -8.626608],
        [41.157207, -8.625132],
        [41.157198, -8.62506],
        [41.156325, -8.625168],
        [41.155551, -8.62506],
        [41.155731, -8.623449],
        [41.156028, -8.621667],
        [41.155434, -8.620047],
        [41.15529, -8.619273],
        [41.155146, -8.618508],
        [41.154705, -8.61615],
        [41.154228, -8.613603],
        [41.153598, -8.612496],
        [41.152977, -8.6121],
        [41.151924, -8.612235],
        [41.15115, -8.612172],
        [41.15052, -8.612109],
        [41.150196, -8.612433],
        [41.14998, -8.613297],
        [41.149989, -8.614224],
      ],
    },
  ],
  "Taxi-529": [
    {
      time: 1375481253000,
      positions: [
        [41.150574, -8.646624],
        [41.149215, -8.646255],
        [41.148387, -8.645904],
        [41.148162, -8.644185],
        [41.148081, -8.64216],
        [41.148459, -8.640144],
        [41.148369, -8.638137],
        [41.148171, -8.636148],
        [41.147667, -8.63433],
        [41.146812, -8.632566],
        [41.146326, -8.63127],
        [41.14647, -8.62965],
        [41.145336, -8.628183],
        [41.145417, -8.626257],
        [41.145624, -8.623971],
        [41.145903, -8.621316],
        [41.146002, -8.620569],
        [41.145984, -8.620569],
        [41.145984, -8.620389],
        [41.146218, -8.618931],
        [41.146065, -8.617599],
        [41.144949, -8.617302],
        [41.145057, -8.616537],
        [41.145255, -8.616141],
        [41.145534, -8.615538],
        [41.145759, -8.614908],
        [41.145984, -8.614233],
        [41.146128, -8.61408],
        [41.146344, -8.614278],
        [41.146353, -8.614269],
      ],
    },
    {
      time: 1375482720000,
      positions: [
        [41.150628, -8.647641],
        [41.150673, -8.646723],
        [41.149206, -8.646201],
        [41.148459, -8.645958],
        [41.148171, -8.644302],
        [41.14809, -8.642088],
        [41.148387, -8.640738],
        [41.148423, -8.639055],
        [41.148324, -8.637318],
        [41.148018, -8.635527],
        [41.147487, -8.633889],
        [41.146533, -8.63217],
        [41.145102, -8.629956],
        [41.14467, -8.627184],
        [41.14476, -8.626104],
        [41.144355, -8.623854],
        [41.143572, -8.621595],
        [41.142357, -8.620065],
        [41.141331, -8.618409],
        [41.141034, -8.617689],
        [41.140818, -8.617275],
        [41.14062, -8.616825],
        [41.140521, -8.616051],
        [41.140728, -8.615421],
        [41.141241, -8.614566],
        [41.14125, -8.614233],
        [41.141403, -8.61399],
        [41.141466, -8.613414],
        [41.141142, -8.610624],
        [41.140818, -8.609985],
        [41.141061, -8.608482],
        [41.141439, -8.606871],
        [41.141808, -8.605125],
        [41.141835, -8.603118],
        [41.142033, -8.600976],
        [41.141538, -8.598798],
        [41.140611, -8.596782],
        [41.139882, -8.594595],
        [41.140053, -8.592633],
        [41.140071, -8.592345],
        [41.140143, -8.591508],
        [41.140809, -8.589231],
        [41.142456, -8.587323],
        [41.143617, -8.584965],
        [41.143734, -8.58168],
        [41.144328, -8.578854],
        [41.144949, -8.577747],
        [41.146191, -8.578989],
        [41.145237, -8.580159],
        [41.144904, -8.582364],
        [41.145975, -8.583966],
        [41.146731, -8.585199],
        [41.147235, -8.586783],
        [41.147793, -8.586414],
        [41.148864, -8.586036],
        [41.149224, -8.58762],
        [41.148531, -8.588466],
        [41.147244, -8.589033],
        [41.14719, -8.588286],
        [41.147505, -8.588061],
        [41.147793, -8.587845],
        [41.148144, -8.587575],
        [41.148225, -8.587584],
        [41.148225, -8.587593],
        [41.148243, -8.587602],
      ],
    },
  ],
};

function makeData() {
  for (const [taxiId, [r, g, b]] of Object.entries(taxiColors)) {
    const color = `rgb(${r},${g},${b})`;
    const backgroundColor = `rgba(${r},${g},${b},${taxiBackgroundAlpha})`;
    mapEntities[taxiId] = { color };
    backgroundedMapEntities[taxiId] = { color: backgroundColor };
    timelineEntities[taxiId] = { color, label: taxiId };
  }

  for (const [taxiId, rides] of Object.entries(taxiRides)) {
    observationsPerEntity[taxiId] = {};

    for (const { time: start, positions } of rides) {
      const rideId = `${taxiId}/${start}`;
      const end = start + (positions.length - 1) * positionInterval;

      events[rideId] = {
        entityIds: [taxiId],
        time: { start, end },
      };
      observationsPerEvent[rideId] = {};
      let minLat = Infinity;
      let maxLat = -Infinity;
      let minLon = Infinity;
      let maxLon = -Infinity;

      for (const [index, [latitude, longitude]] of positions.entries()) {
        const observationId = `${rideId}/${index}`;
        const observation = {
          type: "observation",
          entityId: taxiId,
          latitude,
          longitude,
          time: start + index * positionInterval,
        };
        observations[observationId] = observation;
        observationsPerEvent[rideId][observationId] = observation;
        observationsPerEntity[taxiId][observationId] = observation;
        if (latitude < minLat) {
          minLat = latitude;
        }
        if (latitude > maxLat) {
          maxLat = latitude;
        }
        if (longitude < minLon) {
          minLon = longitude;
        }
        if (longitude > maxLon) {
          maxLon = longitude;
        }
      }
      boundsPerEvent[rideId] = [
        [minLon, minLat],
        [maxLon, maxLat],
      ];
    }
  }
}

export const mapEntities = {};
export const backgroundedMapEntities = {};
export const observations = {};

const timelineEntities = {};
const events = {};

export const observationsPerEvent = {};
export const observationsPerEntity = {};
export const boundsPerEvent = {};

export const timelineData = { entities: timelineEntities, events };

makeData();
<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="@ci/theme/mw/css/examples.css" />
  </head>
  <body>
    <div id="mw"></div>
    <script type="module" src="./code.jsx"></script>
  </body>
</html>

Terms of use

These terms do not alter or supersede any existing agreements between you (or your employer) and us.

By accessing or using any Content you agree to be bound by these Terms of Use. Please review these terms carefully before using the website.

The contents of this website, including but not limited to any text, code samples, API references, schemas, interactive tools, and other materials (collectively, the 'Content'), are made available for informational and internal evaluation purposes only. All intellectual property rights in the Content are reserved. No licence is granted to use the Content for any commercial purpose, or to copy, distribute, modify, reverse-engineer, or incorporate any part of the Content into any product or service, without our prior written consent.

This Content is provided “as is” and “as available,” without any representations, warranties, or guarantees of any kind, whether express or implied, including but not limited to implied warranties of merchantability, fitness for a particular purpose, non-infringement, or accuracy. To the fullest extent permitted by applicable law, we expressly exclude and disclaim all implied warranties, conditions, and other terms that might otherwise be implied.

We disclaim all liability for any loss or damage, whether direct, indirect, incidental, consequential, or otherwise, arising from any reliance placed on the Content or from your use of it, to the fullest extent permitted by applicable law. By continuing to access or use the Content, you acknowledge and agree to these terms.