MCPcopy Index your code
hub / github.com/arcataroger/openlayers_indoor_map

github.com/arcataroger/openlayers_indoor_map @0.1

Chat with this repo
repository ↗ · DeepWiki ↗ · release 0.1 ↗ · + Follow
532 symbols 1,759 edges 2 files 0 documented · 0%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

openlayers_indoor_map

Example of an interactive, indoor museum map built by the Field Museum using OpenLayers 6.5 and QGIS 3.16. See the live demo used by our museum.

We are releasing this code as an example for other museums and venues to freely use, but unfortunately we will not be able to officially support it due to limited resources. You are welcome to file issues and pull requests, but we cannot guarantee a timely response.

The codebase is messy and poorly modularized/abstracted. Although we use this same code in our production digital map, we consider it an alpha release in need of cleanup and refactoring by more experienced devs. Nonetheless, it works as intended and is relatively well documented.

We hope this will be useful to someone out there!

Licensing

Portions Copyright 2020 Field Museum of Natural History. Based on work by the OpenLayers and QGIS projects.

We are releasing this source code under the 2-clause BSD license, same as OpenLayers itself.

Content in this demo map (geometries, icons, etc.) is also released under Creative Commons 4.0 Attribution (CC-BY-4). Of course, you'd probably want to replace our example content with your own.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Acknowledgments

We'd like to thank the following people for their contributions in time and code to this project. This wouldn't have been possible without you! * Matthew Tarr and Raphael Pelegrino at the American Museum of Natural History for their wisdom and insights regarding indoor mapping * Jenn, Julia, and Lonny at Humboldt State University, who inspired a love of the natural world and the use of digital technology to help protect it * The teams behind OpenLayers and QGIS for making available the tools for amateurs and nonprofits to map their surroundings without breaking the bank * All the contributors to OpenStreetMap who revolutionized open-content, community-driven mapping * All the Field Museum staff who contributed to this project... thank you!!!

FEATURES

  • Different levels of detail depending on zoom, to preserve visual hierarchy and emphasize major exhibitions and important amenities (restrooms) while zoomed in. Minor exhibitions start to appear once you zoom in more.
  • Clickable points of interest (labels, areas, pictograms) that pop up a sidebar with more information (picture, link, description). The map also zooms to the clicked POI, and if it's an exhibition/area, highlights that using a slightly desaturated version of its brand color
  • Permalinks for areas (not yet for pictograms) so that clicked POIs can be shared/bookmarked.
  • Integration with a CMS (Contentful) so non-developer editors can make changes to exhibition text, pictures, colors, and also open/close them depending on COVID social distancing rules or other reasons
  • COVID one-way flows drawn as polylines, with automatic start/end styles (circle and arrowhead)
  • Some icon or text sizes remain constant at certain zoom levels to ensure readability.
  • Automatic thumbnail generation (for the sidebar) using cloud image APIs (Drupal and Cloudinary)

Mobile-specific features

  • Mobile-friendly UI
  • Sidebar becomes a bottom popover
  • Different initial viewport focused on the entrance instead of a whole floor

VISITOR (END-USER) INSTRUCTIONS

We hope our map is intuitively usable by visitors already familiar with Google Maps or Apple Maps, though we don't have the data to prove this. In time, we hope to add more real user monitoring and in-app analytics.

Basic interactions are provided by OpenLayers by default and used like any other web map (panning, zooming, etc.)

In addition, visitors can: * Zoom in and out to see varying levels of detail (minor exhibitions, etc) * Click on specific exhibitions or labels to open a slide-in panel with a basic description, and a link out for more info * Use permalinks to access a specific floor and/or exhibition

Keyboard shortcuts

These are currently undocumented in the UI because most of our visitors are on mobile anyway. We just added these for our own convenience.

  • -/+ - Zoom in/out
  • ESC - Close the sidebar
  • 1/2/3 - Switch floors

DEVELOPER INSTRUCTIONS

Installation

  • Clone the repo from Github
  • To spin up a dev server, use npm start. This will use Parcel to build a dev environment and start a dev server at localhost:1234. This should also enable autoreload, so editing code or CSS (anything that Parcel sees) should cause the page to automatically refresh
  • To publish for production, use npm build. The built output should be in /docs. We use docs for Github Pages compatibility; you can reconfigure the output directory by editing /package.json.

Folder structure & assets

index.js - The main app, containing all the custom code we built
index.css - Styling for the UI (buttons, logo, etc.)

assets/ - Source files for our assets
  cms/ - Content (colors, descriptions, links, etc.) for our points of interest. A JSON dump from Contentful.
  icons/ - SVG icons used for amenities and pictograms
  images/ - UI images used in index.html
  layers/ - Floor source files (tif, png), outputs (geoJSON), and QGIS project

docs/ - The built and minified production files from 'npm build'. This is what you serve to the public on a real webhost.

.idea/ - Ignore this. It's project-specific configuration used by our IDE, PHPStorm. 
  • We've provided our assets (layer geoJSONs, SVG icons, etc.) as Creative Commons-licensed examples, excluding webfonts that we don't have permission to redistribute. Feel free to use these while you're experimenting with the map, but obviously you'll want to use your own assets once you've built them.
  • We created all our floor layers in QGIS (also FOSS) and exported them as geoJSON files for OpenLayers. The process of converting PDF maps or CAD files into usable geoJSON layers is out of scope of this readme, but in brief, it consists of georeferencing your PDF, using that as a tracing layer, and building vector features on top of it. We've included /assets/layers/qgis-project.qgz as an example so you can see how this was done, and there are numerous tutorials online and on YouTube. At some point we hope to record a video tutorial of our own showing how we did this; if that would be helpful, please let us know.

Basic Workflow

  1. Create vector layers in your GIS software for footprints (polygons), areas of interest (polygons), one-way flows (polylines), labels (points), amenities (points), etc. See /assets/layers/qgis-project.qgz for an example built in QGIS.
  2. Export or save those layers as geoJSON, making sure to save their feature IDs in a geoJSON compatible way (by default QGIS will export IDs inside properties{}, which will NOT work)
  3. Load the geoJSON sources into LayerFiles{}, configure them in LayerSettings{}, style them by layer type in LayerStyles{}, group them into floors in Floors{}
  4. Create the map with tfmMap and view with tfmView. The map is the parent OpenLayers object that controls and draws everything. The view controls the human-visible viewport, along with zoom and rotation constraints.
  5. Use ol-layerswitcher to handle switching between floors (layer groups)
  6. Use interaction events (in our case, tfmMap.on('click')) and plain Javascript logic to handle clicks on different types of features, e.g. an area itself or the label associated (by ID) to that area.
  7. Use various helper functions to provide quality-of-life benefits for the end-user (visitor), such as opening/closing the sidebar, producing thumbnails out of bigger pictures, handling permalinks, basic keyboard shortcuts

Configuring the code

Apologies in advance: This project was built in a hurry, with only one developer, and we did not create the codebase in a very modular manner. Configuration parameters are spread all throughout index.js, though the majority are near the top. We hope to clean this up and refactor it into proper modules and files at a later point. In the meantime, here's the most important configurations to keep track of in index.js:

  • Styling for map features: OpenLayers does not use CSS to control features on the actual canvas. Instead, the look and feel of all our layers an features are defined in the LayerStyles{} object.
  • Point of interest content: Although geometries are hardcoded in the geoJSONs, we opted for a CMS for other POI content, such as titles, descriptions, links, images, etc. Data from that CMS is loaded into the contentfulData{} object. We chose to use Contentful for this project, but you can replace it with any other CMS you like (Drupal, Wordpress with Advanced Custom Forms, Airtable, Google Sheets... anything that can give you a JSON with IDs to work with). Or if you want to skip a CMS and hardcode it all into a JSON file, that's what we did with fallback-data.json (which is our local cache of Contentful's API output; we use this to ensure the map loads even if Contentful is unreachable, albeit that will result in slightly stagnant data).
  • Layers: OpenLayers has a somewhat convoluted layer loading mechanism. First we use Parcel to require geoJSON files into the LayerFiles{} object, then loop through that to populate the actual LayerSources{} object using the OpenLayers API and a simple forEach loop. LayerSources{}. These sources are configured in LayerSettings{} and styled in LayerStyles{}. Phew! Messy, ain't it?
  • Floors: A layer is a set of geoJSON features, configured by you, that OpenLayers renders. A floor is a mental construct we've made up for our visitors, consisting of one or more grouped layers (the floor footprint, exhibition areas, COVID one-way flows, amenities, labels, decorative pictograms, etc.). In our case, the specific layer configuration for each floor is set up in the Floors{} object, which brings together all the layers, settings, and styles for each floor.
  • Various tfmXXXXX{} constants like tfmZooms{} or tfmIcons{}: These are really just for our convenience. Most are self-explanatory. Of particular note is tfmIcons{}, which loads in a bunch of SVGs using Parcel's require. This is a quirk of our particular toolchain; if you were using webpack or something else, you might be able to load in these SVGs another way (e.g. with import).
  • layerSwitcher options: We use ol-layerwitcher to switch between floors (layer groups). Currently, users can deselect all the floors by clicking the same floor twice; we hope to address this with this eventual patch.
  • Feature closed status: We use this in several layer types in order to indicate whether an exhibition is open to the public as normal (closed='Open' or 0), temporarily closed due to COVID (closed='Closed' or 1), or restricted to staff and never open to the public (closed='Staff-Only' or 2). We realize this numbering scheme is inconsistent, and that's something we hope to clean up in the future. You might see some orphaned styling code (commented out) where the temporarily closed areas can be displayed as diagonal gray lines instead of solid gray fill; we opted against that in the released version to maintain visual simplicity.

How user interactions are handled (e.g. clicking on an exhibition)

  • OpenLayers handles the basics: zooming, panning, catching clicks, etc.
  • The click handler (tfmMap.on('click', e=>{})) near line 727 does the rest of the magic. The basic logic is:
  • When a click is detected, first check it against a filter function in the layerFilter parameter of the click handler. In our case only layers marked as tfmClickable=true in their LayerSettings{} template will be considered clickable. In our case, that's labels (text and padding), pictograms (their SVG fills), and areas (geoJSON polygons). OpenLayers is pretty good at evaluating where the click actually happened, though it does struggle a bit with more complex, layered SVG files (which only matters for the pictograms). Our setup is a bit more complex than yours might be, because our branded print map design forced our hand in doing something similar for our exhibitions, giving them labels in addition to their polygonal areas. These labels are defined in another point layer (e.g. upper_area_labels.geojson) and formatted using OpenLayers. They are intended to be the main clickable things on the map, though users can also choose to click on an area if they so choose. ** Once a click is detected, we only want the topmost feature from forEachFeatureAtPixel(). The reason we use that instead of OL's selection handler is due to an apparent bug that sometimes catches label clicks at the edges of padded text (our labels) to register right. We never got to the bottom of that, so we use forEachFeatureAtPixel() instead, but just the first hit
  • The switch(tfmLayerType) stat

Core symbols most depended-on inside this repo

e
called by 192
docs/openlayers_indoor_map.c67e9f72.js
t
called by 169
docs/openlayers_indoor_map.c67e9f72.js
get
called by 161
docs/openlayers_indoor_map.c67e9f72.js
s
called by 158
docs/openlayers_indoor_map.c67e9f72.js
o
called by 137
docs/openlayers_indoor_map.c67e9f72.js
i
called by 131
docs/openlayers_indoor_map.c67e9f72.js
concat
called by 131
docs/openlayers_indoor_map.c67e9f72.js
u
called by 118
docs/openlayers_indoor_map.c67e9f72.js

Shape

Function 267
Method 189
Class 76

Languages

TypeScript100%

Modules by API surface

docs/openlayers_indoor_map.c67e9f72.js520 symbols
index.js12 symbols

For agents

$ claude mcp add openlayers_indoor_map \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page