<?xml version="1.0" encoding="utf-8" standalone="yes"?><feed xmlns="http://www.w3.org/2005/Atom">
  <title></title>
  <subtitle></subtitle>
  <id>https://www.endpointdev.com/blog/tags/visionport/</id>
  <link href="https://www.endpointdev.com/blog/tags/visionport/"/>
  <link href="https://www.endpointdev.com/blog/tags/visionport/" rel="self"/>
  <updated>2026-05-18T00:00:00+00:00</updated>
  <author>
    <name>End Point Dev</name>
  </author>
  
    <entry>
      <title>Solving High-Resolution Video Stutter with GStreamer Hardware Acceleration</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/05/solving-hi-res-video-stutter-gstreamer-hardware/"/>
      <id>https://www.endpointdev.com/blog/2026/05/solving-hi-res-video-stutter-gstreamer-hardware/</id>
      <published>2026-05-18T00:00:00+00:00</published>
      <author>
        <name>Alejandro Ramon</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/05/solving-hi-res-video-stutter-gstreamer-hardware/cover.webp&#34; alt=&#34;VisionPort One is the classic VisionPort experience that has proven itself in 70+ installations around the world.&#34;&gt;&lt;br&gt;
Photo from the &lt;a href=&#34;https://www.visionport.com/visionport-platform/&#34;&gt;VisionPort website&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In April, a client at Auburn University reached out to VisionPort Support to ask us to look into an issue with video playback for an upcoming presentation. They were running into performance issues attempting to play a video matching the full resolution of the seven-screen video wall, across all screens.&lt;/p&gt;
&lt;p&gt;We have other clients playing video across all screens normally, on the same hardware and software, with no issue, so this was a strange report to the team. The seemingly simple request ended up leading us down a rabbit hole into how video playback works on the VisionPort platform and ultimately led to some serious modernization of our video rendering stack, the process of which we would like to share with you today.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Why was a system designed to play high-resolution video seamlessly across seven screens performing so poorly?&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;the-system-a-gstreamer-pipeline&#34;&gt;The System: A GStreamer Pipeline&lt;/h3&gt;
&lt;p&gt;The VisionPort video player is a custom application built on top of the &lt;a href=&#34;https://gstreamer.freedesktop.org/&#34;&gt;GStreamer&lt;/a&gt; framework, initially developed to synchronize video across separate computers on our legacy Liquid Galaxy hardware configurations.&lt;/p&gt;
&lt;p&gt;Each instance of the player renders a single video. This can be the full 7560x1920 canvas size of a seven-screen VisionPort, or often various subdivisions of the full resolution that cover two or three displays. Although the host hardware of our modern servers is theoretically sufficient, in practice the pipeline was overwhelmed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/solving-hi-res-video-stutter-gstreamer-hardware/vp-content-playback-flow.webp&#34; alt=&#34;High-level overview of the VisionPort content playback flow, showing users interacting through laptop, tablet, or controllers, with content routed through the VisionPort head node, application layer, NVIDIA card resources, and display wall.&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;High-level overview of the content playback flow for VisionPort systems&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;first-instinct-the-encoding&#34;&gt;First Instinct: The Encoding&lt;/h3&gt;
&lt;p&gt;Our initial hypothesis was that video encoding was to blame. Encoding impacts file size, quality, and playback compatibility, and we have previously encountered issues with certain Apple workflows, videos exported from Final Cut Pro, for example. Although nothing about the original video stood out to our team as improperly configured, we re-encoded the source using an H.264 codec and a well-formed container, and tested reducing the resolutions of the video.&lt;/p&gt;
&lt;p&gt;The stutter persisted consistently across the test videos, ruling out encoding as the root cause. In fact, the lower-resolution videos still had stuttering issues, furthering our confusion.&lt;/p&gt;
&lt;h3 id=&#34;hardware-decoding-take-one&#34;&gt;Hardware Decoding, Take One&lt;/h3&gt;
&lt;p&gt;We next considered that the pipeline was running entirely on the processor (CPU) instead of utilizing the ample graphics card resources (GPU). Although the video player was originally designed to support hardware decode elements, software decode had been the default configuration for a number of years.&lt;/p&gt;
&lt;p&gt;Architecturally, the video player runs from within an application layer deployed to the host via a Docker container. The host features an NVIDIA graphics card, so our logic was that leveraging the GPU for this high-resolution video should, in theory, reduce bottlenecks induced by running everything off of the CPU.&lt;/p&gt;
&lt;p&gt;Implementing hardware decode was a project in itself. We discovered that the original legacy decoders present in the video player, such as &lt;code&gt;vaapidecodebin&lt;/code&gt; and &lt;code&gt;vaapipostproc&lt;/code&gt;, were not available to our server&amp;rsquo;s CPU. NVIDIA&amp;rsquo;s accelerated decoders are not part of GStreamer&amp;rsquo;s default plugin set packaged on the Ubuntu base that we use, meaning we had to identify and install the correct package source and plugin in our Docker images. The necessary plugin package, via &lt;code&gt;apt&lt;/code&gt;, ended up being &lt;code&gt;gstreamer1.0-plugins-bad&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Lastly, we needed to resolve Docker-related resource-visibility issues to ensure the container could access the GPU. That meant using the &lt;a href=&#34;https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html&#34;&gt;NVIDIA Container Toolkit&lt;/a&gt; and passing the correct environment variables to the container so that it could utilize the GPU resources.&lt;/p&gt;
&lt;p&gt;Once properly configured, and by enabling hardware decode in the launch command, we were able to determine that hardware decoding was functional.&lt;/p&gt;
&lt;p&gt;A few useful commands to check whether the expected resources are present in the container:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;nvidia-smi
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Checks if the container can access the GPU at all.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;nvidia-smi dmon -s u
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Provides an ongoing report of what GPU features are being utilized over time,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# so we can see whether the decoder is being used.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gst-inspect-1.0 nvcodec
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Gets a list of supported features present for your current implementation of&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# GStreamer with the NVIDIA codecs. You can also run the command without a&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# specific feature to list every supported feature present.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After finally confirming that hardware decode was functioning, we discovered that playback had improved somewhat, but the stutter was not fully resolved.&lt;/p&gt;
&lt;h3 id=&#34;the-real-culprit-rendering-not-decoding&#34;&gt;The Real Culprit: Rendering, Not Decoding&lt;/h3&gt;
&lt;p&gt;At this point we were stumped, so we tried playing the video in another player we had available on the system, &lt;code&gt;ffplay&lt;/code&gt;. The video appeared to play back somewhat normally.&lt;/p&gt;
&lt;p&gt;We reported this to our developer, who decided to enable GStreamer&amp;rsquo;s debug output. The logs were filled with quality-of-service messages, indicating that one or more pipeline elements could not process frames quickly enough, causing downstream stages to drop or delay them.&lt;/p&gt;
&lt;p&gt;The QoS pattern suggested the bottleneck was in the sink, not the decoder. The video player&amp;rsquo;s GStreamer &lt;code&gt;autovideosink&lt;/code&gt; element was defaulting to &lt;code&gt;xvimagesink&lt;/code&gt;, which relies on the CPU for color-space conversion and video texture handling. At 7560x1920px resolution and 60 fps, the CPU was unable to keep up with rendering the large amount of data. &lt;em&gt;It turns out that the source of the issue had more to do with the framerate of the high-resolution video than with the resolution itself.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The host had &lt;code&gt;glimagesink&lt;/code&gt; available, which is an alternative sink that uses OpenGL for GPU-based color-space conversion and texture upload. By explicitly specifying &lt;code&gt;glimagesink&lt;/code&gt; instead of relying on &lt;code&gt;autovideosink&lt;/code&gt;, we assigned rendering to the appropriate hardware.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This single change restored smooth playback at the target resolution.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;putting-the-pieces-together&#34;&gt;Putting the Pieces Together&lt;/h3&gt;
&lt;p&gt;Even though we had a solution for the issue at hand, there were a few more details to sort out to make this universal for all media going forward. It turns out that the NVIDIA accelerated H.264 decoders for the hardware generation we use top out at 4K, meaning that H.264 encoded videos larger than 3840x2160 would not be able to run on the GPU even if we specified use of the decoder. Luckily, our implementation was robust enough to swap back to decoding on the CPU in the case of a fault.&lt;/p&gt;
&lt;p&gt;Additionally, the H.265 decoder supports higher resolutions, comfortably handling 7560x1920 and allowing for future scalability.&lt;/p&gt;
&lt;p&gt;The complete pipeline now runs the GL-backed sink with the option to enable hardware decode for H.264, H.265, VP8, VP9, or AV1, all common encoding formats. The format is auto-detected and will utilize the decoder if the content is acceptable. The CPU render is no longer a bottleneck for decoding or rendering, allowing videos to play back at the full 60 fps, even at higher resolutions.&lt;/p&gt;
&lt;h3 id=&#34;beyond-one-engagement-broader-implications&#34;&gt;Beyond One Engagement: Broader Implications&lt;/h3&gt;
&lt;p&gt;The Auburn University request prompted us to reevaluate our video player stack, particularly a legacy renderer choice and plugin-installation pattern from prior to our move to containerized deployments. Not mentioned in our process above were bug fixes to fix potential memory bugs and improvements to our testing process to simplify future changes.&lt;/p&gt;
&lt;p&gt;These improvements are now incorporated into the upcoming VisionPort Library MQTT migration of the VisionPort application layer. New deployments will default to hardware-accelerated decoding via NVIDIA decoders and GL-backed rendering via &lt;code&gt;glimagesink&lt;/code&gt;, rather than defaulting to the CPU path with the old renderer.&lt;/p&gt;
&lt;p&gt;Key takeaways include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Instrument first if a video pipeline is stuttering&lt;/strong&gt;. GStreamer&amp;rsquo;s verbose debug output, particularly QoS messages, identifies pipeline bottlenecks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do not assume the autodetected sink is optimal&lt;/strong&gt;. &lt;code&gt;autovideosink&lt;/code&gt; is frequently adequate, but check the available sinks for your operating system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hardware acceleration in a container requires more than package installation&lt;/strong&gt;. The container must have GPU visibility and the correct plugin set, which may be its own task.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decoder and renderer can be independently CPU- or GPU-bound&lt;/strong&gt;. Ensuring both are GPU-accelerated is crucial for high-resolution pipelines.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;working-with-end-point&#34;&gt;Working with End Point&lt;/h3&gt;
&lt;p&gt;If you&amp;rsquo;re facing video playback or multimedia pipeline performance challenges, whether with GStreamer, in a containerized environment, or across a multi-screen VisionPort-style installation, End Point&amp;rsquo;s Immersive and Geospatial division has extensive experience with these issues.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://gstreamer.freedesktop.org/documentation/&#34;&gt;GStreamer documentation&lt;/a&gt; is a valuable resource, and we are available to discuss our insights from similar deployments. &lt;a href=&#34;/contact/&#34;&gt;Get in touch&lt;/a&gt; to set up a consultation.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>New Cesium KML-CZML Editor Features: Custom Data &amp; Styling, Google 3D Tiles, and More</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/06/new-czml-kml-editor-features/"/>
      <id>https://www.endpointdev.com/blog/2025/06/new-czml-kml-editor-features/</id>
      <published>2025-06-13T00:00:00+00:00</published>
      <author>
        <name>Dmitry Kiselev</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/06/new-czml-kml-editor-features/banner.webp&#34; alt=&#34;To the right, a satellite imagery map with blue and red gradient polygons drawn over it. To the left, controls for an app, including import/export, create entities, and a list of entities.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I have made some updates to the &lt;a href=&#34;https://www.visionport.com/cesium-kml-czml-editor/&#34;&gt;Cesium KML-CZML editor&lt;/a&gt; I created and maintain.&lt;/p&gt;
&lt;p&gt;The most important additions and changes are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Support for Google 3D tiles&lt;/li&gt;
&lt;li&gt;Support for writing many more features, including interpolation and time series data for some properties. There are still no editing capabilities for these properties, but while previously the editor would strip these values from the data, it will now copy them into the output file.&lt;/li&gt;
&lt;li&gt;Export to KML and KMZ&lt;/li&gt;
&lt;li&gt;Support for custom data and styling using that custom data&lt;/li&gt;
&lt;li&gt;Switched frontend framework from Vue to React&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Adding support for Google 3D tiles is what caused me to create this major version update. In a nutshell, Cesium has its own way of adding reactivity to Entities and Vue doesn&amp;rsquo;t always play nice with it. If I add Google 3D tiles to the scene, it looks like that Cesium Entities have some references to the scene and that causes Vue to apply reactive getters and setters to the whole scene.&lt;/p&gt;
&lt;p&gt;So, I&amp;rsquo;ve switched to using React because it&amp;rsquo;s easier to control which parts should be reactive, as well as when and how you update Cesium Entities and UI components.&lt;/p&gt;
&lt;p&gt;The next important piece is the CZML exporter. The main issue here is that I don&amp;rsquo;t want to strip away features even if I don&amp;rsquo;t support editing them.&lt;/p&gt;
&lt;p&gt;Even if I don&amp;rsquo;t have the UI to edit certain properties or animations for properties, a user can load a CZML document with a mixture of supported and unsupported features without losing the unsupported features. That means that the exporter should be more robust and feature complete than editor itself.&lt;/p&gt;
&lt;p&gt;I will probably separate the exporter as standalone package because it is valuable on its own — I couldn&amp;rsquo;t find a way to export Cesium entities as CZML within the library.&lt;/p&gt;
&lt;h3 id=&#34;custom-data--styling&#34;&gt;Custom data &amp;amp; styling&lt;/h3&gt;
&lt;p&gt;This part is my favorite. I have some experience creating maps, and when you are working with maps you are not focused on polygons or billboards, you are focused on the data, while graphics are just a tool to represent it.&lt;/p&gt;
&lt;p&gt;The previous iteration of the KML-CZML editor was more oriented toward importing KML and massaging it to have the same look in Cesium as it would in Google Earth. That puts focus on graphical features. This time I wanted to make it easy to edit data and style things based on the data.&lt;/p&gt;
&lt;h4 id=&#34;demo&#34;&gt;Demo&lt;/h4&gt;
&lt;p&gt;We&amp;rsquo;ll load in some demo data from New York elections in &lt;code&gt;Mike4326.geojson&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/06/new-czml-kml-editor-features/map-with-geojson.webp&#34; alt=&#34;The CZML editor with all yellow polygons drawn over a region of New York&#34;&gt;&lt;/p&gt;
&lt;p&gt;From here, we&amp;rsquo;ll click on the &amp;ldquo;Data table &amp;amp; Conditional Styling&amp;rdquo; button. Then, in the menu that opens, we&amp;rsquo;ll set up Color by value so that we can display different regions differently by our selected attribute. Note the &amp;ldquo;Mike_prc&amp;rdquo; column which displays a percent value. The app will select a color from the gradient based on this value.The app will select a color from the gradient based on this value.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/06/new-czml-kml-editor-features/color-by-value-selecting-Mike_prc.webp&#34; alt=&#34;The editor with a full screen popup reading &amp;ldquo;conditional styling&amp;rdquo;. There are three main tabs, &amp;ldquo;Color by value&amp;rdquo;, &amp;ldquo;Labels&amp;rdquo;, and &amp;ldquo;Extrusion and scale&amp;rdquo;. The first is selected. Under the tabs it reads &amp;ldquo;Set entities colors by value of an attribute&amp;rdquo;. Under a dropdown reading &amp;ldquo;Attribute&amp;rdquo;, &amp;ldquo;Mike_prc&amp;rdquo; is selected. A blue to red gradient is below, with a continuous range of colors. At the bottom is a table with polygon 1, polygon 2, etc. Then a style column, all of which have a yellow box. The rightmost column is &amp;ldquo;Mike_prc&amp;rdquo;, and reads a percent number from 0 to 100.&#34;&gt;&lt;/p&gt;
&lt;p&gt;To group values together, we&amp;rsquo;ll toggle the &amp;ldquo;Fixed gradations&amp;rdquo; switch. Then hit the &amp;ldquo;Preview&amp;rdquo; button. By default, there are 9 gradations.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/06/new-czml-kml-editor-features/fixed-gradations-9-preview.webp&#34; alt=&#34;The same view, with the gradient now reduced to only 9 values. There is an additional column next to &amp;ldquo;style&amp;rdquo; which shows colors from the gradient corresponding to the Mike_prc value.&#34;&gt;&lt;/p&gt;
&lt;p&gt;We want fewer gradations than that. Let&amp;rsquo;s try out 6.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/06/new-czml-kml-editor-features/fixed-gradations-6-preview.webp&#34; alt=&#34;The same view, but now there are only 6 values in the gradient. Most of the preview colors are the same, but a few have been changed to match the 6 values.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Not much difference, but we&amp;rsquo;ll go with the 6 gradations. I&amp;rsquo;ll hit apply, and you can see the colors are now displayed in the &amp;ldquo;Style&amp;rdquo; column, while the &amp;ldquo;Preview&amp;rdquo; column has been hidden.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/06/new-czml-kml-editor-features/fixed-gradations-6-applied.webp&#34; alt=&#34;The same view, but now the red to blue colors appear in the &amp;ldquo;style&amp;rdquo; column, and the &amp;ldquo;preview column is hidden.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s see how it looks on the map.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/06/new-czml-kml-editor-features/map-with-6-gradations.webp&#34; alt=&#34;The original view of the CZML editor. The electoral districts now show a gradient from blue to red, corresponding to the polygon styling we saw, rather than all being yellow as it started.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Very nice! Now let&amp;rsquo;s add some labels for a different attribute, the election district.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/06/new-czml-kml-editor-features/label-Election_D-selected.webp&#34; alt=&#34;The conditional styling menu, this time in the &amp;ldquo;Labels&amp;rdquo; tab. Text reads &amp;ldquo;Set Labels and Label text using entities attribute value. The selected attribute is &amp;ldquo;Election_D&amp;rdquo;. There is a box displaying pure black color above the table of polygons.&#34;&gt;&lt;/p&gt;
&lt;p&gt;You can see the text color defaults to black, which won&amp;rsquo;t read well on our colored districts. Let&amp;rsquo;s change it by hitting the &amp;ldquo;edit&amp;rdquo; button next to the color.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/06/new-czml-kml-editor-features/label-color-selected.webp&#34; alt=&#34;The same screen, now with added color sliders next to the color square. The color has been changed to a near-white gray.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I changed it to a light gray. Let&amp;rsquo;s look at the map now.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/06/new-czml-kml-editor-features/map-with-labels.webp&#34; alt=&#34;A closer view of the map with the red-blue colored districts, now with many light gray labels displaying the name of the election district.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Lots of labels! This is quite a dense area, so you would probably want to zoom in further to see the labels more clearly.&lt;/p&gt;
&lt;p&gt;See my previous blog post &lt;a href=&#34;/blog/2020/12/cesium-kml-czml-editor/&#34;&gt;introducing the Cesium CZML-KML Editor&lt;/a&gt; for more about the Cesium CZML-KML editor.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Converting MIDI to KML using AI: Bach’s Notes in the Hills of Greenland</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/05/midi-to-kml-bachs-notes-in-the-hills-of-greenland/"/>
      <id>https://www.endpointdev.com/blog/2025/05/midi-to-kml-bachs-notes-in-the-hills-of-greenland/</id>
      <published>2025-05-02T00:00:00+00:00</published>
      <author>
        <name>Darius Clynes</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/05/midi-to-kml-bachs-notes-in-the-hills-of-greenland/bach-in-kml.webp&#34; alt=&#34;A 3D globe visualization at an oblique angle, with hills, lakes, and mountains, with several pins in the foreground reading &amp;ldquo;acoustic grand piano&amp;rdquo;. There are extruded triangles which are green and red extending away from the viewpoint, regularly spaced in multiple straight lines, and varying in size.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I have always been interested in ways of representing music visually. Aside from conventional music notation, I imagined other cross-modal generation methods that could take a sound and generate an image. In the same vein, I have frequently envisioned a 3D landscape in which you could discover musical “objects”.&lt;/p&gt;
&lt;p&gt;Well, now I&amp;rsquo;ve realized a version of this dream — with caveats which will be mentioned later. In this blog I would like to demonstrate how I used AI (in my case ChatGPT using GPT-4 Turbo) to create an interesting JavaScript application from just a few phrases. In this case, we will be making an application that can take as input an existing piece of music represented by a MIDI file and as output, create a KML file that you can view as 3D objects somewhere on the globe.&lt;/p&gt;
&lt;p&gt;Here is how I enlisted ChatGPT to help me:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;please make a javascript application that can take a MIDI file and covert it to extruded polygons in a kml file&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here is a part of its response:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/05/midi-to-kml-bachs-notes-in-the-hills-of-greenland/make-a-javascript-application.webp&#34; alt=&#34;The response to the above ChatGPT query. Chat breaks the process down into 3 steps, &amp;ldquo;Parse the MIDI File&amp;rdquo;, &amp;ldquo;Generate Data for Polygons&amp;rdquo;, and &amp;ldquo;Create a KML File&amp;rdquo;.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I was amazed. It included code to select the MIDI file, convert it to KML, and generate an output file. Plus, ChatGPT correctly interpreted my request despite my “covert” typo. :-)&lt;/p&gt;
&lt;p&gt;Before testing it I was interested in having the color of the extruded polygon be dependent on the pitch of the note. So next I entered:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;please change the color of the extruded polygon dependent on the pitch&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here is what it said:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/05/midi-to-kml-bachs-notes-in-the-hills-of-greenland/change-the-color.webp&#34; alt=&#34;Chat&amp;rsquo;s response to the above query: &amp;ldquo;To change the color of the extruded polygons based on the pitch, we can modify the KML content to include a Style element for each Placemark. We&amp;rsquo;ll map MIDI note pitches to different colors.&#34;&gt;&lt;/p&gt;
&lt;p&gt;It was implemented using npm and Node.js, which seemed excessive for this small application. To make an easier-to-run version in a single html file, I entered:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;please rewrite it so it is in an html page without node.js&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&#34;/blog/2025/05/midi-to-kml-bachs-notes-in-the-hills-of-greenland/rewrite-it.webp&#34; alt=&#34;Chat&amp;rsquo;s response to the above query: &amp;ldquo;To create a web-based application that converts a MIDI file to extruded polygons in a KML file, we can use JavaScript and the Web MIDI API along with a client-side library for parsing MIDI files, such as @tonejs/midi.&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/05/midi-to-kml-bachs-notes-in-the-hills-of-greenland/plain-html-app.webp&#34; alt=&#34;An unstyled HTML app reading &amp;ldquo;MIDI to KML Converter&amp;rdquo;. There is a &amp;ldquo;choose file&amp;rdquo; button, with adjacent text reading &amp;ldquo;bach_846.mid&amp;rdquo;. To the right is a &amp;ldquo;Convert to KML&amp;rdquo; button. Below is a &amp;ldquo;Download KML&amp;rdquo; link. Below that, a &amp;ldquo;play&amp;rdquo; button, then three text inputs reading &amp;ldquo;Longitude&amp;rdquo;, &amp;ldquo;Latitude&amp;rdquo;, and &amp;ldquo;Direction&amp;rdquo;, respectively.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I thought it would be nice to be able to place the polygons, easily, wherever you wanted to, in the world.&lt;/p&gt;
&lt;p&gt;I asked it to take a Google Street View position from Google Maps and decode the latitude, longitude, and heading.&lt;/p&gt;
&lt;p&gt;Next, I asked it to make a nicer user interface. It “understood” the purpose of the application very well and came up with a very nice interface explaining what it does.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/05/midi-to-kml-bachs-notes-in-the-hills-of-greenland/styled-app.webp&#34; alt=&#34;The same app as above, but now styled using simple web design, including making the &amp;ldquo;Convert to KML&amp;rdquo; button Green, and including an explanation paragraph: &amp;ldquo;Upload your MIDI file below, and the application will convert it into a KML file with 3D polygons. Each note in the MIDI file will be represented by an extruded polygon, and different musical instruments will be visually distinct with images on each polygon. Download the KML file and view it in Google Earth.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;how-to-try-out-the-application&#34;&gt;How to try out the application&lt;/h3&gt;
&lt;p&gt;You can try out a version of the application at &lt;a href=&#34;https://darius.endpointdev.com/midi2kml/midi2kml_improved.html&#34;&gt;https://darius.endpointdev.com/midi2kml/midi2kml_improved.html&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/05/midi-to-kml-bachs-notes-in-the-hills-of-greenland/test-coordinates.webp&#34; alt=&#34;The same app as above, now with a &amp;ldquo;Play MIDI&amp;rdquo; button visible, the &amp;ldquo;choose file&amp;rdquo; box filled, and values entered for coordinates.&#34;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find a nice place to have the MIDI data displayed. Copy the URL and paste it into the Street View URL field.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example, here is &lt;a href=&#34;https://www.google.com/maps/@60.8287681,-45.7810281,2a,66.4y,280.22h,98.22t/data=!3m7!1e1!3m5!1sPgzLk0iAbXx_eGh1Z7pS0g!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-8.219999999999999%26panoid%3DPgzLk0iAbXx_eGh1Z7pS0g%26yaw%3D280.22!7i13312!8i6656?entry=ttu&amp;amp;g_ep=EgoyMDI0MTIxMS4wIKXMDSoASAFQAw%3D%3D&#34;&gt;a place&lt;/a&gt; in Greenland:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/05/midi-to-kml-bachs-notes-in-the-hills-of-greenland/greenland-sphere.webp&#34; alt=&#34;A Google Street View spherical image of hills in Greenland, with a Google Maps label reading &amp;ldquo;Hvalsey Church&amp;rdquo;, with a brick building and the ocean visible&#34;&gt;&lt;/p&gt;
&lt;p&gt;And here&amp;rsquo;s one &lt;a href=&#34;https://www.google.com/maps/@53.1000141,4.7522293,3a,75y,270.07h,90t/data=!3m8!1e1!3m6!1sAF1QipO8lOwNwAosMdcm3YTQT2CQleKuRXNRc59MsmA-!2e10!3e11!6shttps:%2F%2Flh3.googleusercontent.com%2Fp%2FAF1QipO8lOwNwAosMdcm3YTQT2CQleKuRXNRc59MsmA-%3Dw900-h600-k-no-pi0-ya271.06580195437505-ro0-fo100!7i8704!8i4352?entry=ttu&amp;amp;g_ep=EgoyMDI1MDMxMi4wIKXMDSoASAFQAw%3D%3D&#34;&gt;on the beach&lt;/a&gt; in Texel in the Netherlands:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/05/midi-to-kml-bachs-notes-in-the-hills-of-greenland/beach-netherlands.webp&#34; alt=&#34;A Google Street View spherical image of a beach, with a sunset, with a Google Maps label reading &amp;ldquo;Beachclub Texel&amp;rdquo;&#34;&gt;&lt;/p&gt;
&lt;ol start=&#34;2&#34;&gt;
&lt;li&gt;
&lt;p&gt;Find a nice MIDI file like &lt;a href=&#34;https://darius.endpointdev.com/midi2kml/bach_846.mid&#34;&gt;Bach’s prelude and fugue in C major&lt;/a&gt;. Download it and put it in the &amp;ldquo;choose MIDI file&amp;rdquo; field&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Press &amp;ldquo;Convert to KML&amp;rdquo;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Click on the &amp;ldquo;download KML&amp;rdquo; link&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open the KML file in a KML viewer or by manually by loading it into a program like Google Earth or Cesium.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/05/midi-to-kml-bachs-notes-in-the-hills-of-greenland/midi-visualization-triangles.webp&#34; alt=&#34;A closer view of the triangles extruded from Google Earth imagery, both labeled &amp;ldquo;acoustic grand piano&amp;rdquo;, and with some triangles smaller on the right, while the ones on the left have some duplicate triangles superimposed on others.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;issues&#34;&gt;Issues&lt;/h3&gt;
&lt;p&gt;This application works at least partially — it puts triangular prisms for each MIDI track, over time, and shows different colors for different notes (though the color–note correlation is not clear). There are plenty of issues and questions, however:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There are lots of overlapping polygons. It&amp;rsquo;s unclear whether these are just errors, or represent different dynamics or articulations&lt;/li&gt;
&lt;li&gt;The &amp;ldquo;Play MIDI&amp;rdquo; button doesn&amp;rsquo;t work — it plays tones of some kind, but in my testing it was either a single bell sound, or computer noise reminiscent of an AOL dial-up modem.&lt;/li&gt;
&lt;li&gt;When I tested it on an orchestral score (Mozart&amp;rsquo;s Requiem, Kyrie), the instrument label pins were off to the side, not indicating which line of notes they corresponded to, and were mostly labeled &amp;ldquo;acoustic grand piano,&amp;rdquo; which is not an instrument included in the MIDI file.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;using-ai-for-blog-post-image-processing&#34;&gt;Using AI for blog post image processing&lt;/h3&gt;
&lt;p&gt;As an aside, I created this blog using Google Docs with PNG pictures embedded in it. Our blog structure requires a Markdown document with separate WebP images. Google Docs has a nice Markdown export function, but it converts the PNG images into embedded Base64-encoded PNG. So I asked ChatGPT to extract the Base64-encoded PNG and create WebP files. It created a Python script that did this perfectly:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;base64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;re&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;PIL&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; Image
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;io&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; BytesIO
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Path to your Markdown file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;md_file_path = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;example.md&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Directory to store extracted and converted WEBP images&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;output_dir = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;converted_images&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;os.makedirs(output_dir, exist_ok=&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Load the Markdown content&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;with&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;open&lt;/span&gt;(md_file_path, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;r&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;as&lt;/span&gt; md_file:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    content = md_file.read()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Regex to find base64-encoded PNGs in the Markdown&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;base64_pattern = re.compile(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;r&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;\[.*?\]:\s*&amp;lt;data:image/png;base64,([A-Za-z0-9+/=]+)&amp;gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Initialize a counter for naming the files&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;counter = &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;match&lt;/span&gt; &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; base64_pattern.finditer(content):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#038&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Match found: &amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    base64_data = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;match&lt;/span&gt;.group(&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#038&#34;&gt;print&lt;/span&gt;(base64_data)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Decode the base64 string to a PNG image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    png_data = base64.b64decode(base64_data)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    png_image = Image.open(BytesIO(png_data))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Save the PNG as a WEBP file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    webp_name = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;image_&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;counter&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;.webp&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    webp_path = os.path.join(output_dir, webp_name)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    png_image.save(webp_path, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;WEBP&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Replace the base64 PNG in the Markdown with a reference to the WEBP file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    content = content.replace(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;match&lt;/span&gt;.group(&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;), &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;![Image &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;counter&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;](&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;webp_path&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;)&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    counter += &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Save the updated Markdown file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;updated_md_path = os.path.splitext(md_file_path)[&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;] + &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;_updated.md&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;with&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;open&lt;/span&gt;(updated_md_path, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;w&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;as&lt;/span&gt; updated_md_file:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    updated_md_file.write(content)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#038&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Updated Markdown file saved: &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;updated_md_path&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#038&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;WEBP images saved in: &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;output_dir&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Unfortunately, when Google Docs embeds the images, it seems to downsize the resolution, so for this post, old-school image processing it is!&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Place Labels in Cesium using Vector Data Tiles</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/02/cesium-place-label-vector-data-tiles/"/>
      <id>https://www.endpointdev.com/blog/2025/02/cesium-place-label-vector-data-tiles/</id>
      <published>2025-02-13T00:00:00+00:00</published>
      <author>
        <name>Dmitry Kiselev</name>
      </author>
      <content type="html">
        &lt;style&gt;
ol &gt; * &gt; ol {
  list-style: lower-alpha;
}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/02/cesium-place-label-vector-data-tiles/banner.webp&#34; alt=&#34;A satellite map of a large area around New York City, with labels for cities scaled to their population.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Cesium is like an IKEA globe: it can do almost anything, but you have to put it together yourself. One major missing feature is labels for cities. Compare the default views of Google Earth and Cesium, and you&amp;rsquo;ll notice this immediately.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/02/cesium-place-label-vector-data-tiles/google-vs-cesium-default-view.webp&#34; alt=&#34;A side-by-side view of satellite imagery of the northeastern United States. On the left, there are state labels and state lines, with some large cities marked. On the right, there are no markings, just the satellite imagery.&#34;&gt;&lt;br&gt;
&lt;small&gt;Left: Google Earth default view, with labels and state lines. Right: Cesium default view, with no labels or markers.&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re reading this post, you might be able to find any major U.S. city without any clues, but most people can’t. You can overlay imagery that includes labels as part of the image, but this approach has a significant drawback: as soon as you rotate the view away from north-up, the labels become difficult to read.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/02/cesium-place-label-vector-data-tiles/imagery-with-labels-in-layer.webp&#34; alt=&#34;A side-by-side comparison of satellite imagery. There are labels for major roads and cities. On the left, these look normal as one would expect on a map, while on the right, the view has been tilted up to a higher angle, and the labels have been tilted as well, making them unreadable.&#34;&gt;&lt;/p&gt;
&lt;p&gt;In Google Earth, this issue is handled using vector data overlays. Labels, borders, and other similar elements are examples of how useful vector overlays can be.&lt;/p&gt;
&lt;p&gt;Cesium also supports vector features such as labels, polylines, and polygons. You can create them manually or use CZML or KML to add such features to a scene. If you use KML, Cesium translates it internally, but the result is the same. Vector features have distinct advantages for 3D geographic data, with labels being the most obvious example.&lt;/p&gt;
&lt;h3 id=&#34;challenges-of-vector-data-in-cesium&#34;&gt;Challenges of Vector Data in Cesium&lt;/h3&gt;
&lt;p&gt;Displaying vector data in Cesium comes with some challenges. Since we cannot load and display all data at once, we have two options: keep it as vector data or rasterize it into imagery. Either way, we need to tile the data, breaking it into small chunks and generalizing or simplifying it.&lt;/p&gt;
&lt;p&gt;Continuing with the example of labels: tiles covering large areas should include only essential labels (e.g., capital cities) to avoid excessive clutter. Similarly, for polylines such as roads or boundaries, we need to prioritize important roads and simplify their geometries.&lt;/p&gt;
&lt;p&gt;Most GeoServers and data providers serve vector tiles encoded as GeoJSON, KML, or other binary formats. However, integrating them into Cesium or Google Earth requires additional steps.&lt;/p&gt;
&lt;p&gt;There are two big considerations that need to be taken into account when integrating tiled vector data into any application:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;How particular geometric features (points, lines, and polygons) are encoded for every tile. Also, what are their attributes and how should they be represented and styled? This involves considering whether a point should have a label or an icon, what color should it be, etc.&lt;/li&gt;
&lt;li&gt;How the data is arranged. This includes how many levels it has, how you fetch new tiles, what the area each tile covers is, and how the software you are integrating with (in our case, Cesium) manages rendering, loading, and unloading the data.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In Google Earth, KML covers both issues in one technology. KML describes features and their styling, while for the arrangement of tiles you can use &lt;a href=&#34;https://www.google.com/earth/outreach/learn/using-network-links-effectively/&#34;&gt;Network Link Regions&lt;/a&gt;. To put it simply, Network Link Regions define an area and as soon as the camera gets close enough to that area, Google Earth loads a new chunk of data for that area. You can read more about that in the &lt;a href=&#34;https://developers.google.com/kml/documentation/regions#smart-loading-of-region-based-network-links&#34;&gt;KML docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In Cesium, this process is covered by multiple technologies, and not all of them work together. We have several encoding and styling options (CZML, KML, GeoJSON), but none of them natively support tiling.&lt;/p&gt;
&lt;p&gt;On the other hand, Cesium 3D Tiles work well for large 3D meshes and point clouds, but they do not support CZML, KML, or GeoJSON as tile content.&lt;/p&gt;
&lt;p&gt;To bridge this gap, Cesium provides &lt;a href=&#34;https://github.com/CesiumGS/3d-tiles/tree/vctr/TileFormats/VectorData&#34;&gt;the Vector Data tile format&lt;/a&gt; and &lt;a href=&#34;https://github.com/CesiumGS/3d-tiles/tree/main/specification/Styling&#34;&gt;3D Tiles Styling&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Although they are still experimental, these features work with the standard Cesium distribution without any hacks or undocumented options.&lt;/p&gt;
&lt;p&gt;Since I previously implemented city labels in Cesium, I decided to revisit the task to do it with this new standard in mind. In my earlier implementation, I had to rely on undocumented Cesium features to determine which tiles should be loaded and unloaded. More details about that approach can be found &lt;a href=&#34;/blog/2023/05/cesium-labels/&#34;&gt;in my previous blog post&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;technical-implementation&#34;&gt;Technical Implementation&lt;/h3&gt;
&lt;p&gt;For this tileset generation, I use two standards:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vector Data tile content with tileset styling
&lt;ul&gt;
&lt;li&gt;Vector Data tile content defines how each tile is encoded&lt;/li&gt;
&lt;li&gt;Tileset styling determines how points, lines, and polygons are rendered&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Implicit tiling
&lt;ul&gt;
&lt;li&gt;Implicit tiling defines tile arrangement and availability&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both standards use a mix of JSON and binary-encoded features. In a nutshell, to encode a point with a label you have to write a vctr file, a subtree file and a tileset.json file.&lt;/p&gt;
&lt;p&gt;Each &lt;strong&gt;vctr&lt;/strong&gt; file consists of the following sections:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Header&lt;/strong&gt;: Defines technical data, like the size of the sections in bytes and their offsets&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feature Table&lt;/strong&gt;: Defines how to read points, lines, or polygons from the binary data section In our case we just specify how many points we have&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch Table&lt;/strong&gt;: Defines data specific attributes, in our case that will be a name for a point&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Binary data with positions and indices&lt;/strong&gt;: Coordinates of the points optimized for storage size&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Subtree&lt;/strong&gt; files encode which child tiles are available, which of them have content, and what are the available subtrees.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tileset.json&lt;/strong&gt; puts all that information together, defining what region the whole tileset covers, specifying that we are using implicit tiling, and defining URL templates for fetching &lt;strong&gt;vctr&lt;/strong&gt; and &lt;strong&gt;subtree&lt;/strong&gt; files.&lt;/p&gt;
&lt;p&gt;Add a style which tells Cesium that it should create a label for every point with its name attribute. And congratulations, we have our first set of labels!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/02/cesium-place-label-vector-data-tiles/earth-with-labels.webp&#34; alt=&#34;Satellite imagery on a globe. There are labels on some of the biggest cities on the globe, such as Los Angeles, New York City, Bogota, etc.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;managing-uneven-label-distribution&#34;&gt;Managing Uneven Label Distribution&lt;/h3&gt;
&lt;p&gt;But as soon as we zoom in we hit a problem: we have too many labels too close to each other.&lt;/p&gt;
&lt;img src=&#34;/blog/2025/02/cesium-place-label-vector-data-tiles/uneven-label-distribution.webp&#34; alt=&#34;The northeastern United States, with labels on the largest cities. Most labels overlap with several others close by, while there are large areas with no labels at all.&#34; style=&#34;max-height:500px&#34;&gt;
&lt;p&gt;In order to avoid having the globe polluted with messy clumps of labels when zooming in, we have to discern how the visibility of 3D tile content is determined by Cesium. A key concept for handling this is assigning a &lt;code&gt;geometricError&lt;/code&gt; value to each tile. The exact value might be complex to calculate, but the general rule is: the larger the &lt;code&gt;geometricError&lt;/code&gt;, the bigger the distance from which a tile should be visible.&lt;/p&gt;
&lt;p&gt;So as you zoom in closer, labels from tiles with bigger &lt;code&gt;geometricError&lt;/code&gt; will appear sooner. Furthermore, for &lt;em&gt;implicit tiling&lt;/em&gt; the &lt;code&gt;geometricError&lt;/code&gt; of children tiles should be half of what their parent tiles’ &lt;code&gt;geometricError&lt;/code&gt; values are. Practically speaking, this means that the &lt;code&gt;geometricError&lt;/code&gt; value assigned to the root tile cascades throughout the system and changes how soon all of the labels become visible. To have some of the labels appear later than the others we have to push them down the tile tree.&lt;/p&gt;
&lt;p&gt;The first thing I tried was to set a minimum tree depth for a label based on city size, but the end result was pretty much the same:&lt;/p&gt;
&lt;img src=&#34;/blog/2025/02/cesium-place-label-vector-data-tiles/uneven-label-distribution-minimum-tree-depth.webp&#34; alt=&#34;The northeastern United States, looking essentially unchanged from the previous image.&#34; style=&#34;max-height:500px&#34;&gt;
&lt;p&gt;The root cause of the points clumping up is not small towns appearing too early, it’s how the populated places themself are distributed. “The Bronx” and “Manhattan” are just big and too close to each other and to “New York City”. If I try to push “Manhattan” deep enough so it doesn’t overlap with “New York City” based just on its size we won’t be able to see Boston or Baltimore or Hamilton, although they have plenty of space to be shown at this zoom level. Hence, we need a way to cluster points and determine point depth based on how much space there is between neighbours.&lt;/p&gt;
&lt;h3 id=&#34;clustering-labels&#34;&gt;Clustering Labels&lt;/h3&gt;
&lt;p&gt;Clustering is a well-established problem in computational geometry, and there are plenty of libraries to solve it, but if I used one of them I would also have to integrate it with the tree structure of the tiles. So instead I’ve implemented a straightforward approach that is effective, although not 100% correct mathematically:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;For every level of the tree, calculate a distance threshold. The threshold gets smaller and smaller with every level of the tree. Let’s follow the same pattern as the tiles&amp;rsquo; &lt;code&gt;geometricError&lt;/code&gt; for this threshold&lt;/li&gt;
&lt;li&gt;For every point to be inserted into the tree, calculate the distance to a closest point in the tree&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;If the distance is more than the threshold, just insert the point. If the distance is smaller than the threshold, let’s calculate how many levels we should skip to get the threshold small enough to insert the point at this level&lt;/li&gt;
&lt;li&gt;If an inserted point is more important than an existing point, then put the inserted point at the current level and push the old one down the tree instead&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And so, there you have it:&lt;/p&gt;
&lt;img src=&#34;/blog/2025/02/cesium-place-label-vector-data-tiles/working-labels.webp&#34; alt=&#34;The same northeastern United States area, with the labels mostly not overlapping, and with more mid-sized towns far from big cities labeled.&#34; style=&#34;max-height:500px&#34;&gt;
&lt;h3 id=&#34;next-steps&#34;&gt;Next Steps&lt;/h3&gt;
&lt;p&gt;Further steps I have in mind to expand on this work with vector tiles include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enhancing styling options&lt;/li&gt;
&lt;li&gt;Encoding polylines&lt;/li&gt;
&lt;li&gt;Encoding polygons&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;notes&#34;&gt;Notes&lt;/h3&gt;
&lt;p&gt;The project is available on GitHub under the Apache-2.0 license: &lt;a href=&#34;https://github.com/EndPointCorp/cesium-vector-data-tiles&#34;&gt;https://github.com/EndPointCorp/cesium-vector-data-tiles&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Place data is from GeoNames.org under the CC-BY license.&lt;/p&gt;
&lt;p&gt;Screenshots use Bing Imagery, ArcGIS World Imagery, and Google Earth.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Exploring Geodatabase Files</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2024/08/exploring-geodatabase-files/"/>
      <id>https://www.endpointdev.com/blog/2024/08/exploring-geodatabase-files/</id>
      <published>2024-08-14T00:00:00+00:00</published>
      <author>
        <name>Constante “Tino” Gonzalez</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2024/08/exploring-geodatabase-files/banner.webp&#34; alt=&#34;The sun shines brightly behind a cloud, casting a half halo of rays to the left of the image, and leaving the right of the image quite dim.&#34;&gt;&lt;/p&gt;
&lt;!-- Image by Jaxson Baerg --&gt;
&lt;p&gt;One of our clients recently provided us with a dataset of real estate properties that they manage, and asked us to generate content based off of the points and polygons in the dataset.&lt;/p&gt;
&lt;p&gt;We will walk through the process of extracting polygons, placemarks, and other info from a geodatabase file and converting them into separate KML files using the &lt;code&gt;ogr2ogr&lt;/code&gt; command-line tool, adding some logic to the data selection to limit the subset of features. We will also explore the GDB file using the GDAL Python library to export the data as JSON for use in other scripts.&lt;/p&gt;
&lt;h3 id=&#34;prerequisites&#34;&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Basic understanding of geospatial data&lt;/li&gt;
&lt;li&gt;Installed versions of the &lt;code&gt;GDAL/OGR&lt;/code&gt; library&lt;/li&gt;
&lt;li&gt;A geodatabase file (&lt;code&gt;.gdb&lt;/code&gt;, &lt;code&gt;.gdb.zip&lt;/code&gt;, or &lt;code&gt;.shp&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;a-first-look-into-the-contents-of-the-gdb-file&#34;&gt;A first look into the contents of the GDB file&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2024/08/exploring-geodatabase-files/pin_label_polygon.webp&#34; alt=&#34;Google earth showing a campus of several buildings, surrounded by a yellow rectangle overlay, and with a blue pin labeled &amp;ldquo;label&amp;rdquo; on one side.&#34;&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ogrinfo example.gdb.zip&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This command will list all layers that are available in the dataset. Add a specific layer as a parameter to the same command, and it will output all the fields, their types, and values for every feature in the layer.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ogrinfo example.gdb.zip a_layer_name&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The parameter &lt;code&gt;-so&lt;/code&gt; can be used to omit the values from the output and get only the layer field names and types:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#$&amp;gt; ogrinfo example.gdb.zip Land_Points -so
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;INFO: Open of `example.gdb.zip&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      using driver `OpenFileGDB&amp;#39; successful.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Layer name: Land_Points
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Geometry: Point
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Feature Count: 219
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Extent: (-122.699891, -23.590280) - (139.763352, 59.622821)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Layer SRS WKT:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;GEOGCRS[&amp;#34;WGS 84&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    DATUM[&amp;#34;World Geodetic System 1984&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ELLIPSOID[&amp;#34;WGS 84&amp;#34;,6378137,298.257223563,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            LENGTHUNIT[&amp;#34;metre&amp;#34;,1]]],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    PRIMEM[&amp;#34;Greenwich&amp;#34;,0,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ANGLEUNIT[&amp;#34;degree&amp;#34;,0.0174532925199433]],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    CS[ellipsoidal,2],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        AXIS[&amp;#34;geodetic latitude (Lat)&amp;#34;,north,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ORDER[2],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ANGLEUNIT[&amp;#34;degree&amp;#34;,0.0174532925199433]],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    USAGE[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        SCOPE[&amp;#34;unknown&amp;#34;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        AREA[&amp;#34;World&amp;#34;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        BBOX[-90,-180,90,180]],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ID[&amp;#34;EPSG&amp;#34;,4326]]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Data axis to CRS axis mapping: 2,1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;FID Column = OBJECTID
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Geometry Column = Shape
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;asset_type: Integer (0.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;asset_id: String (15.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;property_code: String (255.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;full_address_text: String (255.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;land_name: String (255.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;land_address1: String (255.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;land_city: String (255.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;land_sate_code: String (50.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;land_postal_code: String (10.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;land_country_code: String (50.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;division_name: String (255.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;region_name: String (255.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;market_name: String (255.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;submarket_name: String (255.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;supplemental_portfolio_name: String (255.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ownership_name: String (255.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;land_held_for_sale_acre: Real (0.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;land_held_for_sale_hect: Real (0.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;land_held_for_development_acre: Real (0.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;land_held_for_development_hect: Real (0.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;total_land_acre: Real (0.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;total_land_hectare: Real (0.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;buildable_area_sf: Real (0.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;buildable_area_sm: Real (0.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;buildable_area_tsubo: Real (0.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;land_latitude: Real (0.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;land_longitude: Real (0.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;geocoded: Integer (0.0) DEFAULT 0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;globalid: String (0.0) NOT NULL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;created_user: String (255.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;created_date: DateTime (0.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;last_edited_user:  String (255.0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;last_edited_date: DateTime (0.0)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This command shows you information for a single layer, which can be helpful if you are only looking for certain values. However, the geographic data is not much to look at in the terminal. To visualize it, we can convert the data to KML, which applications like Google Earth or Cesium can render while keeping the information as text that can be read:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ogr2ogr -f &amp;#34;KML&amp;#34; example_output.kml example.gdb.zip layer_name&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-f &amp;quot;KML&amp;quot;&lt;/code&gt; specifies the output format&lt;/li&gt;
&lt;li&gt;&lt;code&gt;example_output.kml&lt;/code&gt; is the name of the output KML file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;example.gdb.zip&lt;/code&gt; is the path to the geodatabase file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;layer_name&lt;/code&gt; is the geodatabase layer to export&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This command will export into a KML file:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All the geometries in the layer, be it placemarks or polygons in our case&lt;/li&gt;
&lt;li&gt;All the other fields of information as extended data, which will show up for each feature as a balloon table when visualized in Google Earth&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2024/08/exploring-geodatabase-files/full_label.webp&#34; alt=&#34;Google earth, with a pin selected. A popup dialog displays a table with Land_Points:asset_type, Land_Points:asset_id, and other Land_Points data.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;the-ogr2ogr--sql-option&#34;&gt;The &lt;code&gt;ogr2ogr&lt;/code&gt; &lt;code&gt;-sql&lt;/code&gt; option&lt;/h3&gt;
&lt;p&gt;The default balloon popup was not the outcome we wanted for this KML file. We first used &lt;code&gt;sed&lt;/code&gt; to remove all the extended data from the KML files, but after looking into it a bit further, we found an &lt;code&gt;ogr2ogr&lt;/code&gt; option, &lt;code&gt;-sql&lt;/code&gt;, that made the data filtering easier. This option lets us add a query to the command just like getting the data from a SQL database.&lt;/p&gt;
&lt;h4 id=&#34;1-extract-property-names&#34;&gt;1. Extract property names&lt;/h4&gt;
&lt;p&gt;To create a KML file with just the names of the properties, look up the layers and fields in them using &lt;code&gt;ogrinfo&lt;/code&gt; and find the points layer that has the names—in this case, &lt;code&gt;Layer_Points&lt;/code&gt;. Then, add the desired SQL query to the command.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ogr2ogr -f &amp;#34;KML&amp;#34; output_names.kml example.gdb.zip -sql &amp;#34;SELECT name FROM Layer_Points&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;2-extract-polygons-only&#34;&gt;2. Extract polygons only&lt;/h4&gt;
&lt;p&gt;The polygon geometries are stored in &lt;code&gt;Layer_Polygons&lt;/code&gt;. They can be selected using the special OGR field &lt;code&gt;OGR_GEOMETRY&lt;/code&gt; that refers to the geometry of the selected layer:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ogr2ogr -f &amp;#34;KML&amp;#34; output_polygons.kml example.gdb.zip -sql &amp;#34;SELECT OGR_GEOMETRY FROM Layer_Polygons&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&#34;/blog/2024/08/exploring-geodatabase-files/polygons_only.webp&#34; alt=&#34;Google Earth, with two irregularly shaped red outlines. One has a popup dialog with a table reading &amp;ldquo;Land_Polygons:OGR_GEOMETRY&amp;rdquo;, and a value of &amp;ldquo;MULTIPOLYGON&amp;rdquo;&#34;&gt;&lt;/p&gt;
&lt;h4 id=&#34;3-create-kml-with-pins-and-names&#34;&gt;3. Create KML with pins and names&lt;/h4&gt;
&lt;p&gt;To create a KML file with property names as placemarks with pins, we just select the name. The point placemark seems to be included with all data:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ogr2ogr -f &amp;#34;KML&amp;#34; output_pins.kml input.gdb layer_name -sql &amp;#34;SELECT name FROM layer_name&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;4-create-kml-with-pins-only-no-names&#34;&gt;4. Create KML with pins only, no names&lt;/h4&gt;
&lt;p&gt;To get the points with nothing else, we use the SQLite &lt;code&gt;MakePoint&lt;/code&gt; function, which selects a list of points from the KML.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ogr2ogr -f &amp;#34;KML&amp;#34; output_pins.kml input.gdb layer_name -dialect SQLite -sql &amp;#34;SELECT MakePoint(land_longitude, land_latitude) AS geom FROM Land_Points&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;5-extract-limited-properties&#34;&gt;5. Extract limited properties&lt;/h4&gt;
&lt;p&gt;For extracting a limited number of properties, we can add the &lt;code&gt;WHERE&lt;/code&gt; clause to the SQL command:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ogr2ogr -f &amp;#34;KML&amp;#34; output_pins.kml input.gdb layer_name -sql &amp;#34;SELECT name FROM layer_name&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We can also use the &lt;code&gt;ogr2ogr&lt;/code&gt; &lt;code&gt;-where&lt;/code&gt; flag to use that part of the query only:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ogr2ogr -f &amp;#34;KML&amp;#34; limited_output_polygons.kml input.gdb layer_name -where &amp;#34;ID IN (1, 2, 3)&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;6-run-it-all-in-a-python-script&#34;&gt;6. Run it all in a Python script&lt;/h4&gt;
&lt;p&gt;There is a Python GDAL library, which I will cover the basics of later, but first, here is a simplified example using Python &lt;code&gt;subprocess&lt;/code&gt; to run the ogr2ogr commands we tested in the terminal.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gdb_file = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;example.gdb.zip&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;column_name = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;p_code&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;elem_str = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#39;a1&amp;#39;, &amp;#39;a2&amp;#39;, &amp;#39;b1&amp;#39;, &amp;#39;b3&amp;#39;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sql_land_labels = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;SELECT land_name FROM Land_Points WHERE &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;column_name&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; IN (&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;elem_str&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;)&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;subprocess.run([&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;ogr2ogr&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;-f&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;KML&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;land_labels.kml&amp;#39;&lt;/span&gt;, gdb_file, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;-sql&amp;#39;&lt;/span&gt;, sql_land_labels])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sql_land_points = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;SELECT MakePoint(land_longitude, land_latitude) AS geom FROM Land_Points WHERE &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;column_name&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; IN (&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;elem_str&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;)&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;subprocess.run([&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;ogr2ogr&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;-f&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;KML&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;land_points.kml&amp;#39;&lt;/span&gt;, gdb_file, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;-dialect&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;SQLite&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;-sql&amp;#39;&lt;/span&gt;, sql_land_points])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sql_land_polygons = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;SELECT OGR_GEOMETRY FROM Land_Polygons WHERE &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;column_name&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; IN (&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;elem_str&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;)&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;subprocess.run([&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;ogr2ogr&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;-f&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;KML&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;land_pols.kml&amp;#39;&lt;/span&gt;, gdb_file, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;-sql&amp;#39;&lt;/span&gt;, sql_land_polygons])&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This script will create the three different KML files that we usually need for our presentations. We used variables for the &lt;code&gt;gdb_file&lt;/code&gt;, &lt;code&gt;column_name&lt;/code&gt;, and &lt;code&gt;elem_str&lt;/code&gt; command line parameters to make the script easy to use for selecting other data. We also use them in other scripts to join, apply custom styling, and add regions to the KMLs, which will be covered in another blog post.&lt;/p&gt;
&lt;h4 id=&#34;7-extract-all-data-as-json-from-one-layer-using-the-python-gdal-library&#34;&gt;7. Extract all data as JSON from one layer using the Python GDAL library&lt;/h4&gt;
&lt;p&gt;I probably should have started here, but I only used the Python library later on in the process to join the &lt;code&gt;gdp.zip&lt;/code&gt; file data with data from other sources (spreadsheets, emails, etc.).&lt;/p&gt;
&lt;p&gt;First, install the GDAL library:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pip install gdal&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then you can use the following Python script, changing as necessary the &lt;code&gt;gdb_file&lt;/code&gt;, &lt;code&gt;layer_name&lt;/code&gt;, and the unique field chosen to structure the data. The code is explained in the comments.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;#!/bin/python&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;osgeo&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; ogr
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Open the Geodatabase file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;driver = ogr.GetDriverByName(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;OpenFileGDB&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gdb_file = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;example.gdb.zip&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;data_source = driver.Open(gdb_file, &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Get the layer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;layer_name = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Building_Points&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;layer = data_source.GetLayerByName(layer_name)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; layer:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Get the layer definition&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    layer_defn = layer.GetLayerDefn()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Get the number of fields in the layer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    num_fields = layer_defn.GetFieldCount()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Initialize an empty list to store field names&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    field_names = []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Iterate over each field and add its name to the list&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; i &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;range&lt;/span&gt;(num_fields):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        field_defn = layer_defn.GetFieldDefn(i)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        field_name = field_defn.GetName()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        field_names.append(field_name)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#038&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Field names:&amp;#34;&lt;/span&gt;, field_names)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#038&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Layer &amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;layer_name&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39; not found.&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;all_info = {}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Iterate over all features and organize all field names under a unique one for the dictionary structure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; feature &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; layer:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; feature &lt;span style=&#34;color:#080&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#080&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;None&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        globalid = feature.GetField(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;globalid&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        all_info[globalid] = {}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; fn &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; field_names:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            all_info[globalid][fn] = feature.GetField(fn)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Close the data source&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;data_source = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Save information to a JSON file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;output_file = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;info.json&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;with&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;open&lt;/span&gt;(output_file, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;w&amp;#39;&lt;/span&gt;) &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;as&lt;/span&gt; json_file:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    json.dump(all_info, json_file, indent=&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;4&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#038&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Information saved to &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;output_file&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The script will create a JSON file with all the information on the layer.&lt;/p&gt;
&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Following these steps, we can efficiently manage and visualize geospatial data using &lt;code&gt;ogr2ogr&lt;/code&gt;, SQL, and KML, and in some cases, JSON. These methods allow for a high degree of customization and can be tailored to specific project requirements.&lt;/p&gt;
&lt;h3 id=&#34;additional-resources&#34;&gt;Additional Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://gdal.org&#34;&gt;GDAL/OGR Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://gdal.org/python/&#34;&gt;GDAL Python Bindings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developers.google.com/kml/documentation&#34;&gt;KML Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.sqlite.org/docs.html&#34;&gt;SQLite Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

      </content>
    </entry>
  
    <entry>
      <title>An OpenStreetMap Editor for Adding Public Transit Data Using GTFS</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2024/05/an-openstreetmap-editor-for-incorporating-gtfs-data/"/>
      <id>https://www.endpointdev.com/blog/2024/05/an-openstreetmap-editor-for-incorporating-gtfs-data/</id>
      <published>2024-05-08T00:00:00+00:00</published>
      <author>
        <name>Dmitry Kiselev</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2024/05/an-openstreetmap-editor-for-incorporating-gtfs-data/app-overview.webp&#34; alt=&#34;The OpenStreetMap GTFS editor. The right side is a map with a darkened map, and blue, red, and black dots overlayed along the roads where public transit stops are. In the corner, the map reads &amp;ldquo;Leaflet | copyright OpenStreetMap&amp;rdquo;. It has controls to zoom, change between satellite and OpenStreetMap, darken the map, and open in JOSM. The left side has navigation buttons reading &amp;ldquo;Import&amp;rdquo; (which is selected), &amp;ldquo;Stops&amp;rdquo;, &amp;ldquo;Routes&amp;rdquo;, &amp;ldquo;Trips&amp;rdquo;, and &amp;ldquo;Changes&amp;rdquo;. Below there is text on several lines: &amp;ldquo;gtfs.zip. Loaded 5246 stops&amp;rdquo; then a button reading &amp;ldquo;Query OSM data&amp;rdquo;. Then, &amp;ldquo;OSM Tag with GTFS stop code: &amp;quot; and a text box with &amp;ldquo;gtfs:ref&amp;rdquo; typed inside. Next, bold text reading &amp;ldquo;Possible GTFS stop code tags&amp;rdquo;, followed by a list of tags: &amp;ldquo;ref (6615 objects), route_ref (129 objects), local_ref (127 objects), ref:US-UT:uubus (24 objects), railway:ref (5 objects), loc_ref (7 objects), gtfs:stop_id (2 objects), ref:left (2 objects), ref:right (2 objects), noref (26 objects), crossing_ref (16 objects)&amp;rdquo;. Next, &amp;ldquo;show help&amp;rdquo;. &amp;ldquo;Match stops by name: &amp;quot; and an unchecked checkbox. Next, &amp;ldquo;Match stops by GTFS code in name: &amp;quot; and an unchecked checkbox. Next, bold text reading &amp;ldquo;Template tags for platform&amp;rdquo;. Then, 3 rows in a table. The first two have minus sign buttons on the left, and two columns reading: &amp;ldquo;public_transport&amp;rdquo;,&amp;ldquo;platform&amp;rdquo; and &amp;ldquo;highway&amp;rdquo;,&amp;ldquo;bus_stop&amp;rdquo;. The third row is just a plus sign button.&#34;&gt;&lt;/p&gt;
&lt;p&gt;You can think of OpenStreetMap (OSM) as Wikipedia for maps. While it may not be as well known as Google Maps or Apple Maps, OSM becomes indispensable when you need data, rather than just a visual representation on your phone. The only other real option for data retrieval is to consult a local agency, so if you&amp;rsquo;re in search of a comprehensive and global cartographic dataset, OSM is the go-to choice.&lt;/p&gt;
&lt;p&gt;OSM also excels in providing navigation for pedestrians and cyclists. For the past decade, I&amp;rsquo;ve navigated the US, Canada, and Europe using OSM through the OsmAnd app, a dedicated Android application for OpenStreetMap.&lt;/p&gt;
&lt;p&gt;Overall, my experience has been quite positive, except for one significant weakness: public transportation. Specifically, I&amp;rsquo;ve been missing the convenient access to timetables for buses, trams, or any other form of public transit.&lt;/p&gt;
&lt;p&gt;A significant part of the challenge in making public transportation data readily available on OSM and its associated applications is the fact that the OSM data model isn’t particularly well suited for this type of information. While OSM can store the location of bus and train stops, the actual timetables change so often that keeping them up to date in OSM is essentially impossible.&lt;/p&gt;
&lt;p&gt;Enter the General Transit Feed Specification (GTFS), a widely adopted standard for managing public transportation data. I wanted to explore ways to integrate OSM with GTFS effectively.&lt;/p&gt;
&lt;h3 id=&#34;my-openstreetmap-editor-for-gtfs-data&#34;&gt;My OpenStreetMap editor for GTFS data&lt;/h3&gt;
&lt;p&gt;A simple approach for addressing this challenge would be to overlay an OSM map with stops from GTFS. While this would be relatively straightforward, it doesn&amp;rsquo;t leverage OSM&amp;rsquo;s biggest strength—its vibrant community. One of OSM&amp;rsquo;s most significant advantages is that it dynamically incorporates user contributions: as soon as the community accesses useful data, members enhance the details and accuracy of its mapping data. Therefore, I’ve been working on truly integrating GTFS data with OSM, creating a cohesive mapping experience rather than settling for a disjointed overlay.&lt;/p&gt;
&lt;p&gt;Although the implementation is a bit trickier, I believe the better approach for handling this challenge involves merging GTFS stops with those already present in OSM and incorporating GTFS stop codes into OSM data. Hence, I&amp;rsquo;ve developed a single-page online editor. This tool facilitates the creation, editing, and deletion of stops within OSM. It also highlights stops based on their alignment with stops specified in a given GTFS file.&lt;/p&gt;
&lt;p&gt;Editing the routes wasn’t a priority, yet I found it necessary to match GTFS routes and trips with OSM routes and their variants. Displaying trips shows which part of a street a route uses and also which side of a street bus stops are located on.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve deployed the app on my GitHub, where you can try it out: &lt;a href=&#34;https://kiselev-dv.github.io/osm-gtfs/&#34;&gt;https://kiselev-dv.github.io/osm-gtfs/&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;how-to-use-the-app&#34;&gt;How to use the app&lt;/h3&gt;
&lt;p&gt;To start, download a GTFS data dump. They are usually published by local transit companies. As an example, I&amp;rsquo;ll use data from Halifax, Nova Scotia.&lt;/p&gt;
&lt;p&gt;Open the GTFS ZIP file in the editor. The editor should parse stops from GTFS.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2024/05/an-openstreetmap-editor-for-incorporating-gtfs-data/gtfs-file.webp&#34; alt=&#34;The OSM editing app. The map is not darkened and does not have any blue, red, or black dots. The left bar does not have a list of tags anymore.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Next, query OSM data and select “OSM Tag with GTFS stop code.” After OSM data is loaded you will get OSM tags statistics which might help you to see if there are some popular tag combination for GTFS data in the area. OSM doesn’t have strict data model, but as a rule of thumb for that type of data &lt;code&gt;ref&lt;/code&gt; or &lt;code&gt;gtfs:ref&lt;/code&gt; should work. If there are multiple agencies sharing a stop and each of them publishes its own GTFS data, you can use something like &lt;code&gt;gtfs:&amp;lt;agency_name&amp;gt;:ref&lt;/code&gt;. By default the editor will substitute the most popular tag containing &lt;code&gt;ref&lt;/code&gt; or &lt;code&gt;gtfs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now you can match stops from OSM with stops from GTFS. Matches are color coded:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Blue: matching stops&lt;/li&gt;
&lt;li&gt;Red: stops which are present in GTFS but not mapped in OSM (stops to add)&lt;/li&gt;
&lt;li&gt;Black: stops mapped in OSM but not matched with GTFS stop (probably outdated stops which are no longer in use and may be deleted, or stops not used by the agency whose GTFS data you are using)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2024/05/an-openstreetmap-editor-for-incorporating-gtfs-data/stop-matching.webp&#34; alt=&#34;The OSM editing app. The &amp;ldquo;Stops&amp;rdquo; navigation option is selected. On the map, there are black dots on top of bus stops visible in the actual map texture. Most of these black dots have nearby red circles. There are a couple of blue dots on top of bus stops. The text in the left app bar reads: &amp;ldquo;Filter matches by Route/Trip&amp;rdquo; then a dropdown select box reading &amp;ldquo;Select a route&amp;rdquo;. Then, &amp;ldquo;Show matched: 4844&amp;rdquo; with a checked checkbox. Then, &amp;ldquo;Show unmatched GTFS: 402&amp;rdquo; with a checked checkbox. Then, &amp;ldquo;Show unmatched OSM: 1512&amp;rdquo; with a checked checkbox. Then a button, &amp;ldquo;Open listed stops in JOSM&amp;rdquo;. Then, a list of bus stops: &amp;ldquo;Constitution Blvd @ 3700 S&amp;rdquo; with a blue circle, &amp;ldquo;Constitution Blvd @ 3601 S&amp;rdquo; with a blue circle, etc.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;adding-stops-to-osm&#34;&gt;Adding stops to OSM&lt;/h3&gt;
&lt;p&gt;There are two main cases for adding or matching a stop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There is no stop in OSM at all and we want to create a new one&lt;/li&gt;
&lt;li&gt;There is a stop in OSM and we want to edit it and add GTFS data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Select one of the GTFS stops. On the left panel there will be two buttons: “Create” and “Assign”.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2024/05/an-openstreetmap-editor-for-incorporating-gtfs-data/create-assign-buttons.webp&#34; alt=&#34;The OSM editing app. The &amp;ldquo;Stops&amp;rdquo; tab is selected. In the map, there are red circles on either side of a road. One is larger than the other, indicating its selection. Text in the bar on the left reads: &amp;ldquo;900 E / 300 S (SB)&amp;rdquo;, then &amp;ldquo;GTFS Stop code: 820054, GTFS Stop id: 21317&amp;rdquo;. Then, &amp;ldquo;Routes on this stop: Provo Central Station (831 - PROVO GRANDVIEW)&amp;rdquo; with a filter icon and a rising arrow icon. There is another similar stop. Then &amp;ldquo;This GTFS stop has no matched OSM stop. Select one of the OSM stops nearby to set GTFS code in its tags&amp;rdquo;. Then, &amp;ldquo;OSM Stop platform:&amp;rdquo; and two buttons reading &amp;ldquo;Create&amp;rdquo; and &amp;ldquo;Assign&amp;rdquo;. These buttons are highlighted. Then there are stop filter options.&#34;&gt;&lt;/p&gt;
&lt;p&gt;The “Create” button will switch the map into satellite mode, create a new OSM stop with GTFS code, and add a stop name at the point where you click. When creating a stop or editing its location, pay attention to which side of the road it should be on. To simplify that, you can highlight one of the routes in the “Routes on this stop” section.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2024/05/an-openstreetmap-editor-for-incorporating-gtfs-data/create-stop.webp&#34; alt=&#34;The OSM editing app. The map is showing satellite imagery. A blue stop is selected, with options on the left to move or reassign the stop, as well as OSM data: &amp;ldquo;name: 900 E / 300 S (SB), ref: 820054, public_transport: platform, highway: bus_stop&amp;rdquo;. This data is in a two-column editable table.&#34;&gt;&lt;/p&gt;
&lt;p&gt;The “Assign” button allows you to select one of the existing OSM stops, and set its GTFS code.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2024/05/an-openstreetmap-editor-for-incorporating-gtfs-data/assign-stop.webp&#34; alt=&#34;The OSM editing app. One blue stop is highlighted. It has more OSM data filled out, including &amp;ldquo;network: UTA, network:wikidata: Q7902494&amp;rdquo;, etc.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;demo-videos&#34;&gt;Demo videos&lt;/h3&gt;
&lt;p&gt;You can see a &lt;a href=&#34;https://www.youtube.com/watch?v=fG_RvC7AWfk&#34;&gt;video on my YouTube channel&lt;/a&gt; with the latest updates to the editor, along with a live editing session.&lt;/p&gt;
&lt;p&gt;I have also created a &lt;a href=&#34;https://www.youtube.com/playlist?list=PL0eKSR1VCpIQiTntglaVhKXt4PKWTcEg6&#34;&gt;YouTube playlist&lt;/a&gt; with more demos.&lt;/p&gt;
&lt;h3 id=&#34;an-important-note&#34;&gt;An important note&lt;/h3&gt;
&lt;p&gt;An important note is that I do not upload results directly into OSM. Instead, I generate an XML file for an OSM changeset which you can view and edit in JOSM or another editor. I do this partly so I do not have to deal with authentication, but more importantly, because I see editing public transport relations in OSM as an advanced topic and it makes sense for editors to demonstrate a higher level of sophistication than general users. But I might change my mind about this if I get feedback that it would be desirable.&lt;/p&gt;
&lt;p&gt;My app is built using React and Leaflet. You can find the code on my GitHub: &lt;a href=&#34;https://github.com/kiselev-dv/osm-gtfs&#34;&gt;https://github.com/kiselev-dv/osm-gtfs&lt;/a&gt;&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Making a Loading Spinner with tkinter</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2024/03/making-a-loading-spinner-with-tkinter/"/>
      <id>https://www.endpointdev.com/blog/2024/03/making-a-loading-spinner-with-tkinter/</id>
      <published>2024-03-05T00:00:00+00:00</published>
      <author>
        <name>Matt Vollrath</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2024/03/making-a-loading-spinner-with-tkinter/spiral-stairs.webp&#34; alt=&#34;An overhead shot of a carpeted spiral staircase, with spiraling railings on either side. The staircase is cut off at the bottom by a wall, so that only half of the circle of stairs is visible. The stairs are enclosed by a semicircular wall, and lit by sunlight streaming through a window on the left. On the right is a window whose view is filled with green leaves.&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2023. --&gt;
&lt;p&gt;When you need a loading spinner, you really need a loading spinner. Interested in putting something on the screen without installing a pile of dependencies, I reached deep into the toolbox of the Python standard library, dug around a bit, and pulled out the &lt;a href=&#34;https://docs.python.org/3/library/tkinter.html&#34;&gt;tkinter&lt;/a&gt; module.&lt;/p&gt;
&lt;p&gt;The tkinter module is an interface to the venerable Tcl/Tk GUI toolkit, a cross-platform suite for creating user interfaces in the style of whatever operating system you run it on. It&amp;rsquo;s the only built-in GUI toolkit in Python, but there are many worthy alternatives available (see the end of the post for a list).&lt;/p&gt;
&lt;p&gt;Here I&amp;rsquo;ll demonstrate how to make a loading spinnner with tkinter on Ubuntu 22.04. It should work on any platform that runs Python, with some variations when setting up the system for it.&lt;/p&gt;
&lt;h3 id=&#34;prerequisites&#34;&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;My vision for the loading spinner is some spinning dots and a logo, since this is such a convenient branding opportunity. To accomplish this we&amp;rsquo;ll be extending tkinter with Pillow&amp;rsquo;s ImageTk capability, which can load a PNG with transparency.&lt;/p&gt;
&lt;p&gt;To produce that PNG with transparency, first we may need to rasterize an SVG file, because wise designers work in vectors. This is made trivial by &lt;a href=&#34;https://inkscape.org/&#34;&gt;Inkscape&lt;/a&gt;, a free and complete vector graphics tool:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ inkscape logo.svg -o logo.png&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With the logo in hand, we can move on to setting up our dependencies. Ubuntu&amp;rsquo;s python3 distribution doesn&amp;rsquo;t include tkinter by default, so we&amp;rsquo;ll need to install it explicitly, along with Pillow&amp;rsquo;s separate ImageTk support:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo apt install python3-tk python3-pil.imagetk&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This may occupy up to 75MB, if Python was already installed. This was still the smallest apt footprint of all of the Python GUI libraries in consideration. Pygame was also a strong contender.&lt;/p&gt;
&lt;h3 id=&#34;code&#34;&gt;Code&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s start with something simple: putting the logo on the screen.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;PIL&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; Image, ImageTk
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;tkinter&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; BOTH, Canvas, Tk
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Desired dimensions of our window.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;WIDTH, HEIGHT = &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;500&lt;/span&gt;, &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;500&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;__name__&lt;/span&gt; == &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Create the root window object.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    root = Tk()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Create a canvas for drawing our graphics.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    canvas = Canvas(root, width=WIDTH, height=HEIGHT, background=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;black&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Fill the entire window with the canvas.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    canvas.pack(fill=BOTH, expand=&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Load the logo PNG with regular PIL.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logo_img = Image.open(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;logo.png&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Convert the logo to an ImageTk PhotoImage.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logo_pi = ImageTk.PhotoImage(logo_img)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Add our logo image to the canvas.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    canvas.create_image(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        WIDTH / &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        HEIGHT / &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        image=logo_pi,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Run the tkinter main loop.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    root.mainloop()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This puts the logo in the center of a window, but the logo may be too large or small. Let&amp;rsquo;s scale it according to the window dimensions, let&amp;rsquo;s say to about ⅔ of the width so we have some room for spinning dots:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;PIL&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; Image, ImageTk
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;tkinter&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; BOTH, Canvas, Tk
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Desired dimensions of our window.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;WIDTH, HEIGHT = &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;500&lt;/span&gt;, &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;500&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;__name__&lt;/span&gt; == &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Create the root window object.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    root = Tk()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Create a canvas for drawing our graphics.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    canvas = Canvas(root, width=WIDTH, height=HEIGHT, background=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;black&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Fill the entire window with the canvas.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    canvas.pack(fill=BOTH, expand=&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Load the logo PNG with regular PIL.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logo_img = Image.open(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;logo.png&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Resize the logo to about 2/3 the window width.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    scaled_w = &lt;span style=&#34;color:#038&#34;&gt;round&lt;/span&gt;(WIDTH * &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0.6&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    scaled_h = &lt;span style=&#34;color:#038&#34;&gt;round&lt;/span&gt;(scaled_w / (logo_img.width / logo_img.height))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logo_img = logo_img.resize((scaled_w, scaled_h), Image.LANCZOS)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Convert the logo to an ImageTk PhotoImage.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logo_pi = ImageTk.PhotoImage(logo_img)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Add our logo image to the canvas.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    canvas.create_image(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        WIDTH / &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        HEIGHT / &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        image=logo_pi,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Run the tkinter main loop.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    root.mainloop()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That&amp;rsquo;s better. Now let&amp;rsquo;s add the promised spinning dots. We&amp;rsquo;ll draw some ovals on the canvas and modify our main loop to animate them:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;math&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;time&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;PIL&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; Image, ImageTk
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;tkinter&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; BOTH, Canvas, Tk
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Desired dimensions of our window.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;WIDTH, HEIGHT = &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;500&lt;/span&gt;, &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;500&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Coordinates of the center.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;CENTER_X, CENTER_Y = WIDTH / &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;, HEIGHT / &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# How many spinning dots we want.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;NUM_DOTS = &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;__name__&lt;/span&gt; == &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Create the root window object.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    root = Tk()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Create a canvas for drawing our graphics.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    canvas = Canvas(root, width=WIDTH, height=HEIGHT, background=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;black&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Fill the entire window with the canvas.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    canvas.pack(fill=BOTH, expand=&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Load the logo PNG with regular PIL.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logo_img = Image.open(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;logo.png&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Resize the logo to about 2/3 the window width.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    scaled_w = &lt;span style=&#34;color:#038&#34;&gt;round&lt;/span&gt;(WIDTH * &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0.6&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    scaled_h = &lt;span style=&#34;color:#038&#34;&gt;round&lt;/span&gt;(scaled_w / (logo_img.width / logo_img.height))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logo_img = logo_img.resize((scaled_w, scaled_h), Image.LANCZOS)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Convert the logo to an ImageTk PhotoImage.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logo_pi = ImageTk.PhotoImage(logo_img)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Add our logo image to the canvas.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    canvas.create_image(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        CENTER_X,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        CENTER_Y,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        image=logo_pi,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Radius in pixels of a single dot.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    dot_radius = WIDTH * &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0.05&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Radius of the ring of dots from the center of the window.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    dots_radius = WIDTH / &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt; - dot_radius * &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Helper function to calculate dot position on each update.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;get_dot_coords&lt;/span&gt;(n: &lt;span style=&#34;color:#038&#34;&gt;int&lt;/span&gt;, t: &lt;span style=&#34;color:#038&#34;&gt;float&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Get the x0, y0, x1, y1 coords of dot at index &amp;#39;n&amp;#39; at time &amp;#39;t&amp;#39;.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        angle = (n / NUM_DOTS) * math.pi * &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt; + t
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        x = math.cos(angle) * dots_radius + CENTER_X
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        y = math.sin(angle) * dots_radius + CENTER_Y
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; x - dot_radius, y - dot_radius, x + dot_radius, y + dot_radius
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Create all the dots.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    t0 = time.monotonic()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; n &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;range&lt;/span&gt;(NUM_DOTS):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        coords = get_dot_coords(n, t0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        canvas.create_oval(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            *coords,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            fill=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;#888888&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            width=&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;,  &lt;span style=&#34;color:#888&#34;&gt;# Border width.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            tags=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;dot_&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;n&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Set up a custom main loop to animate the moving dots.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;while&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;True&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# Check the time of this update.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        t = time.monotonic()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; n &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;range&lt;/span&gt;(NUM_DOTS):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888&#34;&gt;# Get the desired coords for this dot at this time.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            coords = get_dot_coords(n, t)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888&#34;&gt;# Move the dot on the canvas, finding it by its tag.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            canvas.coords(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;dot_&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;n&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                *coords,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# Call the required tkinter update function.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        root.update()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# Attempt to stabilize the timing of this loop by targeting 60Hz.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;while&lt;/span&gt; t0 &amp;lt; t:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            t0 += &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt; / &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;60&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        time.sleep(t0 - t)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You may notice that the dots don&amp;rsquo;t look all that great. There&amp;rsquo;s no anti-aliasing when drawing shape primitives in tkinter, so the edges look jagged compared to our well-scaled logo image. One hack is to layer slightly larger and dimmer shapes under each object, which you might do like so:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;math&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;time&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;PIL&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; Image, ImageTk
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;tkinter&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; BOTH, Canvas, Tk
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Desired dimensions of our window.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;WIDTH, HEIGHT = &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;500&lt;/span&gt;, &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;500&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Coordinates of the center.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;CENTER_X, CENTER_Y = WIDTH / &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;, HEIGHT / &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# How many spinning dots we want.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;NUM_DOTS = &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Colors for each layer of fake anti-aliasing around each dot.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Must be in order from back to front.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;COLORS = [&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;#888888&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;#BBBBBB&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;#FFFFFF&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;__name__&lt;/span&gt; == &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Create the root window object.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    root = Tk()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Create a canvas for drawing our graphics.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    canvas = Canvas(root, width=WIDTH, height=HEIGHT, background=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;black&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Fill the entire window with the canvas.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    canvas.pack(fill=BOTH, expand=&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Load the logo PNG with regular PIL.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logo_img = Image.open(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;logo.png&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Resize the logo to about 2/3 the window width.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    scaled_w = &lt;span style=&#34;color:#038&#34;&gt;round&lt;/span&gt;(WIDTH * &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0.6&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    scaled_h = &lt;span style=&#34;color:#038&#34;&gt;round&lt;/span&gt;(scaled_w / (logo_img.width / logo_img.height))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logo_img = logo_img.resize((scaled_w, scaled_h), Image.LANCZOS)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Convert the logo to an ImageTk PhotoImage.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logo_pi = ImageTk.PhotoImage(logo_img)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Add our logo image to the canvas.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    canvas.create_image(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        CENTER_X,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        CENTER_Y,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        image=logo_pi,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Radius in pixels of a single dot.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    dot_radius = WIDTH * &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0.05&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Radius of the ring of dots from the center of the window.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    dots_radius = WIDTH / &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt; - dot_radius * &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Helper function to calculate dot position on each update.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;get_dot_coords&lt;/span&gt;(n: &lt;span style=&#34;color:#038&#34;&gt;int&lt;/span&gt;, t: &lt;span style=&#34;color:#038&#34;&gt;float&lt;/span&gt;, c: &lt;span style=&#34;color:#038&#34;&gt;int&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Get the x0, y0, x1, y1 coords of dot at index &amp;#39;n&amp;#39; at time &amp;#39;t&amp;#39;.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;        Inflate the radius by color index &amp;#39;c&amp;#39;.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        angle = (n / NUM_DOTS) * math.pi * &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt; + t
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        x = math.cos(angle) * dots_radius + CENTER_X
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        y = math.sin(angle) * dots_radius + CENTER_Y
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# Invert the color index and add to the radius.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        radius = dot_radius + (&lt;span style=&#34;color:#038&#34;&gt;len&lt;/span&gt;(COLORS) - c) * &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0.75&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;#radius = dot_radius + c&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; x - radius, y - radius, x + radius, y + radius
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Create all the dots.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    t0 = time.monotonic()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; c, color &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;enumerate&lt;/span&gt;(COLORS):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; n &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;range&lt;/span&gt;(NUM_DOTS):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            coords = get_dot_coords(n, t0, c)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            canvas.create_oval(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                *coords,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                fill=color,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                width=&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;,  &lt;span style=&#34;color:#888&#34;&gt;# Border width.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                tags=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;dot_&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;c&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;_&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;n&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Set up a custom main loop to animate the moving dots.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;while&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;True&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# Check the time of this update.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        t = time.monotonic()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; c, color &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;enumerate&lt;/span&gt;(COLORS):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; n &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;range&lt;/span&gt;(NUM_DOTS):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#888&#34;&gt;# Get the desired coords for this dot at this time.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                coords = get_dot_coords(n, t, c)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#888&#34;&gt;# Move the dot on the canvas, finding it by its tag.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                canvas.coords(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;dot_&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;c&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;_&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;n&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    *coords,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# Call the required tkinter update function.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        root.update()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# Attempt to stabilize the timing of this loop by targeting 60Hz.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;while&lt;/span&gt; t0 &amp;lt; t:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            t0 += &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt; / &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;60&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        time.sleep(t0 - t)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The fake anti-aliasing was a fun exercise, but for this use case you&amp;rsquo;ll probably get better-looking results out of scaling a PNG asset like we did the logo.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2024/03/making-a-loading-spinner-with-tkinter/spinner.webp&#34; alt=&#34;A screenshot of the loading spinner. In the center is a logo reading &amp;ldquo;VisionPort&amp;rdquo;, with the O replaced by a globe with a locator icon in it. Surrounding the logo are animated dots rotating in a circle.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;resources&#34;&gt;Resources&lt;/h3&gt;
&lt;p&gt;If you&amp;rsquo;re interested in learning more about tkinter, see also:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://docs.python.org/3/library/tkinter.html&#34;&gt;Python tkinter docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/index.html&#34;&gt;The tkinter reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Other Python GUI/graphics toolkits you might consider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.pygame.org/news&#34;&gt;Pygame&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://pyglet.org/&#34;&gt;Pyglet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://doc.qt.io/qtforpython-6/&#34;&gt;PySide6&lt;/a&gt; (official Qt binding)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://riverbankcomputing.com/software/pyqt/intro&#34;&gt;PyQt&lt;/a&gt; (unofficial Qt binding)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://wxpython.org/index.html&#34;&gt;wxPython&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

      </content>
    </entry>
  
    <entry>
      <title>Compressed CZML</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2023/06/compressed-czml/"/>
      <id>https://www.endpointdev.com/blog/2023/06/compressed-czml/</id>
      <published>2023-06-13T00:00:00+00:00</published>
      <author>
        <name>Dmitry Kiselev</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2023/06/compressed-czml/crowded-city.webp&#34; alt=&#34;A crowded city on an overcast day. Tall apartment buildings fill the foreground while skyscrapers form a skyline in the background.&#34;&gt;&lt;/p&gt;
&lt;!-- Image by Zed Jensen, 2023. --&gt;
&lt;p&gt;Let’s talk about CZML, Cesium&amp;rsquo;s main language for specifying 3D scenes, and how to incorporate external resources such as billboard graphics, material textures, and 3D models into CZML files.&lt;/p&gt;
&lt;p&gt;For example, let’s look at how we can include glTF models.&lt;/p&gt;
&lt;p&gt;glTF models are composed of multiple files: a single JSON index file along with a variable number of binary buffer files and textures. So, in order to package CZML assets that include glTF models for distribution, you have to read the CZML document itself, then read the referenced glTF files. If they are not binary GLB files, you must also read the glTF files and package all of the files referenced by the glTF models. And if you find this paragraph cumbersome, that&amp;rsquo;s no accident. Indeed, the whole process is quite cumbersome!&lt;/p&gt;
&lt;p&gt;So we are dealing with something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;CZML Document&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;glTF Model&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;buffer1.bin&lt;/li&gt;
&lt;li&gt;buffer2.bin&lt;/li&gt;
&lt;li&gt;texture1.png&lt;/li&gt;
&lt;li&gt;texture2.png&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want to keep the glTF model as a single asset, you can convert the glTF files into binary (GLB) files, and then embed them as base64 data links into CZML.&lt;/p&gt;
&lt;p&gt;Another example would be a set of points with billboards, let’s say a couple hundred points with plenty of different images. Keeping track of what images you have to ship along with a CZML document is very inconvenient, so you might want to embed the images.&lt;/p&gt;
&lt;p&gt;As with 3D models you could use base64 data links, but you will lose readability. You won’t be able to easily edit an image itself; you will have to decode it back into an image file, edit it, re-encode it, and write it back into the CZML file.&lt;/p&gt;
&lt;p&gt;And furthermore, while many billboards can share the same image, if you are going to encode it to base64, you either have to repeat the same base64-encoded string over and over or use &lt;a href=&#34;https://cesium.com/learn/cesiumjs/ref-doc/ReferenceProperty.html&#34;&gt;CZML reference properties&lt;/a&gt;. On the one hand, repeating the same base64 string makes a file terribly bloated, while on the other, with CZML reference properties you have to keep track of Entity IDs. Tracking Entity IDs means you need to be sure that you don’t accidentally delete the Entity which others refer to, and you’ll need to copy it if you want to split the dataset into different files.&lt;/p&gt;
&lt;p&gt;What I really want is to use the same approach that Google Earth uses for KMZ files: KMZ is just a ZIP archive with a KML document and referenced assets packed together. Let’s do the same trick with CZML.&lt;/p&gt;
&lt;p&gt;So the general approach would be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read CZMZ ZIP archive.&lt;/li&gt;
&lt;li&gt;Index ZIP entries as blob objects.&lt;/li&gt;
&lt;li&gt;Find the main CZML document.&lt;/li&gt;
&lt;li&gt;Load it with Cesium CzmlDataSource and proxy all local URLs to blobs from step 2.&lt;/li&gt;
&lt;li&gt;Add destructor to DataSource to revoke blob URLs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To set a proxy we have two main options: read the whole CZML document as a JavaScript object and replace all of the URLs with &lt;a href=&#34;https://cesium.com/learn/cesiumjs/ref-doc/Resource.html&#34;&gt;Cesium.Resource&lt;/a&gt; objects using a proxy, or provide a Cesium.Resource to &lt;a href=&#34;https://cesium.com/learn/cesiumjs/ref-doc/CzmlDataSource.html#load&#34;&gt;CzmlDataSource.load&lt;/a&gt; instead of URLs. In most cases the second option is easier, unless you do some preprocessing on the CZML document before loading.&lt;/p&gt;
&lt;p&gt;You can read zip archives with a library of your choice. I’m using zip.js because Cesium already uses some methods from it.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; data = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; (&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; fetch(assetPath)).blob();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; reader = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; zip.ZipReader(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; zip.BlobReader(data));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; entriesMap = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; Map();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;let&lt;/span&gt; entry &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;of&lt;/span&gt; entries) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; blob = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; entry.getData(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; zip.BlobWriter());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; blobURL = URL.createObjectURL(blob);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   entriesMap.set(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;/&amp;#39;&lt;/span&gt; + entry.filename, blobURL);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now get the document:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; documentEntry = entries.find(e =&amp;gt; /&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;\&lt;/span&gt;.czml$/i.test(e.filename));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; documentBlob = entriesMap.get(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;/&amp;#39;&lt;/span&gt; + documentEntry.filename);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And load the DataSource:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DataSourceInstance.load(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; Cesium.Resource({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   url: documentBlob,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   proxy: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      getURL: URL =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#080;background-color:#fff0ff&#34;&gt;/^blob:/&lt;/span&gt;.test(URL)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;               &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; blobId = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; URL(URL.replace(&lt;span style=&#34;color:#080;background-color:#fff0ff&#34;&gt;/^blob:/&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt;)).pathname;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;               &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; blobURL = entriesMap.get(blobId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;               &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; blobURL ? blobURL : URL;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           console.warn(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;URL not found inside czmz&amp;#39;&lt;/span&gt;, URL);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; URL;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That’s mostly it. We just want to clean up after ourselves; we have to unregister blob URLs to free the resources.&lt;/p&gt;
&lt;p&gt;It’s not documented, but if you remove a DataSource from DataSourceCollection with the &lt;code&gt;destroy&lt;/code&gt; parameter set to true, and DataSource has a destroy method, it will be called.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DataSourceInstance.destroy = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;function&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;let&lt;/span&gt; blobUrl &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;of&lt;/span&gt; entriesMap.values()) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       URL.revokeObjectURL(blobUrl);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


      </content>
    </entry>
  
    <entry>
      <title>VisionPort: The Future of Tourism Marketing</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2023/06/visionport-the-future-of-tourism-marketing/"/>
      <id>https://www.endpointdev.com/blog/2023/06/visionport-the-future-of-tourism-marketing/</id>
      <published>2023-06-13T00:00:00+00:00</published>
      <author>
        <name>Jonathan Perlin</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2023/06/visionport-the-future-of-tourism-marketing/image-1.webp&#34; alt=&#34;A wide screenshot of a presentation on the VisionPort, titled &amp;ldquo;Mexico Market Profile&amp;rdquo;. A 3D view of earth, zoomed so that Mexico takes most of the view, highlighted in orange with its states labeled and separated by border lines. To the left and right of the globe are semi-transparent popups displaying statistics, each with a bar-chart breakdown of data: Visitation and spending forecast with visitation and spending; non-stop seats to California; vacation planning methods; planning to travel; traveler type; barriers to travel.&#34;&gt;&lt;/p&gt;
&lt;p&gt;In a world of rapidly evolving technology, businesses and organizations in the tourism industry must adapt to stay relevant and competitive. VisionPort offers innovative and powerful technological solutions that can help organizations and destinations in tourism enhance the customer experience and reach a wider audience.&lt;/p&gt;
&lt;p&gt;By showcasing destinations and providing immersive and engaging content, VisionPort can help agencies stay ahead of the competition through the use of geographic information systems and customized presentations.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/06/visionport-the-future-of-tourism-marketing/image-2.webp&#34; alt=&#34;Several school kids gather around a VisionPort installation in a space museum, with spacesuits visible in the background. A boy points at an image of a planet displayed on the seven screens arranged in a semicircle.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;showcase-destinations&#34;&gt;Showcase destinations&lt;/h3&gt;
&lt;p&gt;VisionPort&amp;rsquo;s ability to showcase destinations through interactive panoramic views and virtual tours is a game-changer for the tourism industry. By providing immersive and engaging content, potential visitors can get a real sense of what it would be like to experience a particular destination. This can help to spark their interest and inspire them to plan a trip to that location.&lt;/p&gt;
&lt;p&gt;With its support for a variety of media types, including panoramic views, photos, and videos, VisionPort enables businesses to create high-quality content that highlights their unique features and attractions. For instance, a resort or hotel can create panoramic views of its grounds, showing off its pools, restaurants, and other amenities. These views can be further enhanced by overlaying information about the features and services available.&lt;/p&gt;
&lt;p&gt;Moreover, VisionPort allows businesses to create maps and guides that display the different attractions and points of interest within a destination. Visitors can explore the map and click on various locations to learn more about them with photos, videos, and other relevant information. For instance, a map of a city could point to different neighborhoods, museums, and restaurants, allowing visitors to plan their itinerary before arriving at the destination. This way, visitors have a more engaging and personalized experience while exploring the destination.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/06/visionport-the-future-of-tourism-marketing/image-3.webp&#34; alt=&#34;A 3D mouse and a tablet sit on a reflective white table, in front of a VisionPort 7-screen array enclosed in a nice white cabinet. On the screen is displayed a 360-degree panorama of a museum.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Overall, VisionPort’s ability to feature destinations through interactive panoramic views and maps provides an innovative and powerful tool for the tourism industry. It enables businesses to create high-quality and engaging content that can inspire potential visitors to experience a destination for themselves, thereby boosting the tourism industry as a whole.&lt;/p&gt;
&lt;h3 id=&#34;display-a-multitude-of-media&#34;&gt;Display a multitude of media&lt;/h3&gt;
&lt;p&gt;VisionPort’s support for a wide range of media types, including GIS map layers, customizable graphics, videos, and panoramic media, is a key feature that can help destinations create immersive content that illustrates their unique features and attractions. By leveraging these different media types, destinations offer a more personalized and engaging experience to potential visitors, which can be a powerful motivator for them to plan a visit.&lt;/p&gt;
&lt;p&gt;For example, videos can be a great way to exhibit the sights and sounds of a destination, allowing viewers to see the beauty of the landscape, the energy of the local culture, and the excitement of various activities and events that take place there. Videos can also be used to introduce key stakeholders, such as local business owners and tourism officials, and to provide an overview of the destination&amp;rsquo;s history and significance.&lt;/p&gt;
&lt;div style=&#34;position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;&#34;&gt;
      &lt;iframe allow=&#34;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen&#34; loading=&#34;eager&#34; referrerpolicy=&#34;strict-origin-when-cross-origin&#34; src=&#34;https://www.youtube.com/embed/4Ohou5Ek1u4?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0&#34; style=&#34;position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;&#34; title=&#34;VisionPort: 360° Panoramic Video Wall&#34;&gt;&lt;/iframe&gt;
    &lt;/div&gt;

&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Similarly, panoramic media offers a 360° view of a location, allowing viewers to experience the destination in a more immersive and interactive way. This is particularly useful for destinations that have unique or hard-to-reach features, such as mountain ranges, waterfalls, or historic sites, which are difficult to capture in a traditional photograph or video.&lt;/p&gt;
&lt;p&gt;By using these different media types, destinations can create content that is not only informative but also inspiring, helping potential visitors to feel a deeper connection to the destination and motivating them to plan a trip. This can ultimately lead to increased tourism revenue and a stronger local economy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/06/visionport-the-future-of-tourism-marketing/image-4.webp&#34; alt=&#34;A wide screenshot of VisionPort content titled &amp;ldquo;Visitor Share by Origin; Western States&amp;rdquo;. The map is zoomed to show the United States, with the western states highlighted in orange and containing blue circles showing number of visitors, sized proportionally to how many visitors there are. On the right are two bar charts showing overnight visitor distribution by origin state % and share percentage out-of-state for cities. On the left is a card showing a circle graph of California total overnight visitor share by origin market.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;marketing&#34;&gt;Marketing&lt;/h3&gt;
&lt;p&gt;The VisionPort platform helps businesses refine their marketing strategies and create more effective campaigns with engaging, dynamic presentations to present the analytics of their latest marketing campaigns.&lt;/p&gt;
&lt;p&gt;VisionPort also helps businesses collaborate more effectively across departments and teams. Providing a common platform for information sharing and collaboration, VisionPort has facilitated greater communication and teamwork, leading to overall more efficient and effective operations. This translates to increased productivity, streamlined processes, and ultimately, revenue.&lt;/p&gt;
&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;VisionPort is a powerful platform that is helping businesses enhance customer experience and reach wider audiences. By showcasing destinations, creating immersive content, and providing personalized and engaging experiences, agencies are inspiring visitors to plan trips and create unforgettable memories.&lt;/p&gt;
&lt;p&gt;By increasing interaction and developing new methods of presenting and communicating information, VisionPort is a tool that has its place in the future of marketing, especially in the tourism industry.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Cesium Labels</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2023/05/cesium-labels/"/>
      <id>https://www.endpointdev.com/blog/2023/05/cesium-labels/</id>
      <published>2023-05-24T00:00:00+00:00</published>
      <author>
        <name>Dmitry Kiselev</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2023/05/cesium-labels/rome-map.webp&#34; alt=&#34;A 16th-century topographical map of ancient Rome. Buildings are drawn in simple, clear, engraved lines. Streets and important structures like the Pantheon are labeled in Latin.&#34;&gt;&lt;/p&gt;
&lt;!-- Image: Topographical Map of Ancient Rome by Nicolas Beatrizet, 1557. Public domain, acquired from https://www.nga.gov/collection/art-object-page.112707.html --&gt;
&lt;p&gt;Improving place labels in CesiumJS, the open source JavaScript library for 3D globes and maps, has been a longstanding request from some of our VisionPort clients. The display of labels embedded in imagery has not been up to their expectations. The names look upside down as the globe is rotated and there&amp;rsquo;s no option to change the language:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/05/cesium-labels/cesium-old-labels.webp&#34; alt=&#34;The old Cesium labels. The 3D camera is rotated so that the labels reading &amp;ldquo;New York&amp;rdquo;, &amp;ldquo;Kips Bay&amp;rdquo;, etc., are at a 90 degree angle, making it difficult to read.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Cesium has vector labels which allow you to anchor some text to a point on a map which will always be aligned with the camera. However, there is no ready-to-use solution to display city names in particular and no way to load them according to a specific zoom level.&lt;/p&gt;
&lt;p&gt;To improve performance when displaying labels in Cesium it would make sense to load labels as a tile tree and only show some of the top of the tree at different zoom levels in a manner similar to how &lt;a href=&#34;https://developers.google.com/kml/documentation/regions?hl=en#smart-loading-of-region-based-network-links&#34;&gt;KML Regions with NetworkLinks&lt;/a&gt; work. Unfortunately, although Cesium supports KML, it doesn’t support KML Regions and NetworkLinks updates on Region change.&lt;/p&gt;
&lt;p&gt;Another off-the-shelf solution that might conceivably work would be to use Cesium 3D tiles, but unfortunately, tiles do not support 2D Billboards.&lt;/p&gt;
&lt;p&gt;Calculating regions and their level of detail is complicated, but Cesium already does most of that work for us. Cesium already calculates visible tiles and their corresponding levels of detail for ImageryProviders. An ImageryProvider is supposed to load imagery for a given tile’s coordinates and zoom level, which is almost what we want, except that we want to render some 3D primitives for labels, not 2D images for the earth surface.&lt;/p&gt;
&lt;p&gt;We can read what tiles are going to be rendered by Cesium and store them in a variable:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; tilesToRender = viewer.scene.globe._surface._tilesToRender;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then, we get the &lt;a href=&#34;https://wiki.openstreetmap.org/wiki/TMS&#34;&gt;TMS&lt;/a&gt; coordinates out of the visible tiles on screen. Then we calculate the difference between the currently visible tiles and the new ones. We grab all the new tiles that we need from that list, and then we create the &lt;a href=&#34;https://cesium.com/learn/cesiumjs/ref-doc/Entity.html&#34;&gt;Entities&lt;/a&gt; with labels and some other styling properties.&lt;/p&gt;
&lt;h3 id=&#34;backend-and-data-source&#34;&gt;Backend and data source&lt;/h3&gt;
&lt;p&gt;To source the labels we use &lt;a href=&#34;https://www.geonames.org/&#34;&gt;GeoNames&lt;/a&gt;. They have a nice dataset for cities with their population included in the data. We take the city labels they provide and store them into a quadtree. Each node in the quadtree has 10 labels associated with it, starting with the largest population at the top of the data structure and going down. Based on the altitude we traverse a certain distance down the quadtree returning all cities from each node that corresponds to the tile requested.&lt;/p&gt;
&lt;p&gt;Here is a video demonstrating how labels display with this new feature we’ve developed:&lt;/p&gt;
&lt;p&gt;&lt;video type=&#34;video/mp4&#34; controls src=&#34;/blog/2023/05/cesium-labels/cesium-new-labels.mp4&#34; style=&#34;max-height:30rem;width:auto&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;h3 id=&#34;future-plans&#34;&gt;Future Plans&lt;/h3&gt;
&lt;p&gt;Currently, the entity creation is done on the frontend, but we are planning to move this to the backend sometime soon. There is also some crowding of the labels when zooming out since surface tiles aren&amp;rsquo;t immediately updated. The tiles can likely be filtered to remove unneeded ones more aggressively.&lt;/p&gt;
&lt;h3 id=&#34;running-on-your-system&#34;&gt;Running On Your System&lt;/h3&gt;
&lt;p&gt;We have released this code &lt;a href=&#34;https://github.com/EndPointCorp/tiled-city-labels&#34;&gt;on our GitHub&lt;/a&gt; under the Apache license for everyone to use.&lt;/p&gt;
&lt;p&gt;To get this up and running on your local system, start with a Git clone:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git clone https://github.com/EndPointCorp/tiled-city-labels.git&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then within the repo there’s a README to follow. We will give instructions here, but if they differ from the README follow those instead:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#038&#34;&gt;cd&lt;/span&gt; tiled-city-labels
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;docker build -t cesium-labels .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;docker run -p 48088:48088 --rm cesium-labels&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then in a new terminal window still inside the project:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#038&#34;&gt;cd&lt;/span&gt; demo/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python -m SimpleHTTPServer &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;8000&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now you can navigate to http://localhost:8000/ in your browser and see the city labels up and running. To customize this to your own system, you can use the mixin &lt;code&gt;demo/js/CitiesDataSource.js&lt;/code&gt; and add that to your own project. You can also change the port/server used in the mixin by editing the &lt;code&gt;fetch&lt;/code&gt; command inside the &lt;code&gt;queryData&lt;/code&gt; function.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Tennessee Hackathon 2023</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2023/03/tennessee-hackathon-2023/"/>
      <id>https://www.endpointdev.com/blog/2023/03/tennessee-hackathon-2023/</id>
      <published>2023-03-30T00:00:00+00:00</published>
      <author>
        <name>Darius Clynes</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/eptn-1.jpg&#34; alt=&#34;EPTN main room. Several development VisionPort systems are mounted on the back wall. Four End Pointers are seen sitting at a variety of desks and workstations.&#34;&gt;&lt;/p&gt;
&lt;p&gt;We just had our first company gathering in our Tennessee office after a hiatus of several years. About 20 End Pointers came to our Johnson City, Tennessee office to work on various VisionPort projects. For several of us, it also provided an opportunity to meet each other in person for the first time.&lt;/p&gt;
&lt;h3 id=&#34;end-point-tennessee-office-eptn&#34;&gt;End Point Tennessee office (EPTN)&lt;/h3&gt;
&lt;p&gt;Other than our Johnson City-based team, for many of us this was our first look at our Tennessee office from which the VisionPort systems are assembled, tested and shipped.&lt;/p&gt;
&lt;p&gt;Our Content Management System (CMS) team worked on some exciting updates to our VisionPort CMS, including important modifications to our touchscreen systems and improvements to the user interface.&lt;/p&gt;
&lt;p&gt;Meanwhile our Research &amp;amp; Development team worked on upgrades to the VisionPort system itself, focusing on integrating large and small touchscreens for multimedia presentations. One such improvement was support for 8 tabletop touchscreens integrated to serve 16 museum visitors simultaneously.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/eptn-3.jpg&#34; alt=&#34;EPTN main room. Closer image of the left side of the room. Three End Pointers sit side-by side, working on computers. Two of them are checking each others&amp;rsquo; work on a laptop.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Our support team worked on testing and spinning up documentation to bring our inventory up to date and prepare for the next wave of our new VisionPort CMS installations.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/eptn-4.jpg&#34; alt=&#34;EPTN main room. A wider shot. Six End Pointers are hacking away at their workstations.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;casablanca&#34;&gt;Casablanca&lt;/h3&gt;
&lt;p&gt;The luxurious lounge working space at one of our Airbnb locations, nicknamed Casablanca, enabled us to be comfortably spread out as we worked together on various projects.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/cbl-1.jpg&#34; alt=&#34;Casablanca. A spacious lounge room with an exposed brick wall, with part of the kitchen visible in the top right. A grey patterned rug sits on the floor. An End Pointer sits on a leather couch with her laptop. Another laptop sits on the couch.&#34;&gt;&lt;/p&gt;
&lt;p&gt;A morning meeting with plenty of room left:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/cbl-2.jpg&#34; alt=&#34;Casablanca. A wider view of the room, including the kitchen with a counter turned workstation. Several people are seen working in the foreground on a table, as well as the background on couches and other seats.&#34;&gt;&lt;/p&gt;
&lt;p&gt;A quiet comfy corner for planning!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/cbl-3.jpg&#34; alt=&#34;Casablanca. Another corner with a white couch and white seats, a modern wood table, and exposed brick on two walls. Two End Pointers are looking up from their laptops, smiling at the camera.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Working breakfasts:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/cbl-4.jpg&#34; alt=&#34;Casablanca, kitchen area. Food is laid out on the end of the counter, while the other end is taken by two laptops occupied by End Pointers.&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/cbl-5.jpg&#34; alt=&#34;Casablanca, lounge area. Two End Pointers on a leather couch look up from their laptops and smile at the camera.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;pigeon&#34;&gt;Pigeon&lt;/h3&gt;
&lt;p&gt;Just across from Casablanca, we did some relaxing in the &amp;ldquo;Pigeon coop&amp;rdquo;, another luxurious abode:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/pigeon-1.jpg&#34; alt=&#34;Pigeon. Eight End Pointers relax and chat on a modern-looking couch and at a bar&#34;&gt;&lt;/p&gt;
&lt;p&gt;Impromptu roof terrace get together:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/pigeon-2.jpg&#34; alt=&#34;Pigeon. Four End Pointers chat on the roof terrace, which has a view of some city lights and buildings. The terrace is illuminated by a string of lights above.&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/pigeon-3.jpg&#34; alt=&#34;Pigeon. On the other half of the terrace, five more End Pointers gather.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;birthdays-dinner-and-farewell&#34;&gt;Birthdays, dinner, and farewell&lt;/h3&gt;
&lt;p&gt;We also had a couple of employee birthdays to celebrate during the hackathon!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/birthday-1.jpg&#34; alt=&#34;Four pies with lit candles in them sit on a steel countertop, as an End Pointer places the last candle.&#34;&gt;&lt;/p&gt;
&lt;p&gt;And a tasty dinner with everyone the final evening:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/dinner-1.jpg&#34; alt=&#34;Dinner. End Pointers are gathered around a large restaurant table, with lots of hearty food in front of them, midway through the meal. A sign in the background reads &amp;ldquo;Great Oak Brewing&amp;rdquo;.&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/03/tennessee-hackathon-2023/dinner-3.jpg&#34; alt=&#34;The opposite side of the same table. Ben waves with a cowboy hat from the back while other End Pointers smile at the camera, after the meal. Several barrels are visible around the restaurant/brewery.&#34;&gt;&lt;/p&gt;
&lt;p&gt;It was great to see everyone at the Tennessee office. We did plenty of hard work, sometimes sitting elbow to elbow. It certainly was a great opportunity after working together remotely, and helped us experience the camaraderie which is typical of End Point&amp;rsquo;s unique spirit. Until next time, we will continue to improve our VisionPort systems and extend our remote collaboration!&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>University of Denver and VisionPort: The Classroom of the 21st Century</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2022/10/du-visionport-classroom-of-the-21st-century/"/>
      <id>https://www.endpointdev.com/blog/2022/10/du-visionport-classroom-of-the-21st-century/</id>
      <published>2022-10-18T00:00:00+00:00</published>
      <author>
        <name>Samuel Stern</name>
      </author>
      <content type="html">
        &lt;img alt=&#34;The DU VisionPort: Seven vertically-oriented TVs arranged side-by-side in a curved wooden cabinet, with a raised wooden stage in front.&#34; src=&#34;/blog/2022/10/du-visionport-classroom-of-the-21st-century/duinspstage2-sm.webp&#34;&gt;
&lt;!-- photo by Eric Holt --&gt;
&lt;p&gt;VisionPort’s recent installation at the University of Denver is transforming education and has even found itself &lt;a href=&#34;https://www.forbes.com/sites/jennifercastenson/2022/05/19/five-experts-share-data-strategies-to-survive-wild-housing-market/&#34;&gt;featured in Forbes&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Installed in the Marion J. Crean Collaboratory room, VisionPort is doing what it does best: engaging audiences and bringing ideas to life.&lt;/p&gt;
&lt;p&gt;The Collaboratory room gets its name from its mission, to be a collaborative laboratory, specifically for DU’s real estate and construction management students.&lt;/p&gt;
&lt;p&gt;VisionPort was designed with geographic information systems (GIS) in mind, and the content management system we built from the ground up allows experts and novices alike to explore real estate and construction opportunities. From flying around undeveloped land, to exploring cityscapes in three dimensions, the VisionPort is the best platform for presenting GIS data.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“What [the VisionPort] is going to give us is the opportunity to sit in class, and literally, be able to fly to any property, anywhere in the world, and walk that property, see that property.” —Dr. Barbara Jackson&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Seven displays spanning 18 feet, installed in a custom case on top of a beautiful wood stage, the Collaboratory’s design is one of the most unique and intriguing yet.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2022/10/du-visionport-classroom-of-the-21st-century/denverwide.webp&#34; alt=&#34;A close-up of the DU visionport. Two speakers are visible in the front of the cabinet. The seven displays show University of Denver&amp;rsquo;s campus with 3D models in Google Earth.&#34;&gt;&lt;/p&gt;
&lt;p&gt;The world is once again facing new real estate market conditions and the VisionPort platform is the perfect tool to assist in presenting the data needed to understand how the landscape will look over the coming years.&lt;/p&gt;
&lt;p&gt;Developers and real estate professionals are realizing that data is more important than ever to cope with the evolving market. Efficiency is the name of the game, and to be able to collect, analyze, and present that data is VisionPort&amp;rsquo;s bread and butter.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2022/10/du-visionport-classroom-of-the-21st-century/middu-sm.webp&#34; alt=&#34;View of the whole DU Collaboratory room with interactive video wall, VisionPort, 7 round tables and chairs, and couches in the far corner.&#34;&gt;&lt;/p&gt;
&lt;!-- photo by Eric Holt --&gt;
&lt;blockquote&gt;
&lt;p&gt;“Our students will walk out of here with state-of-the-art knowledge about how technology in real estate development and construction management will play in the field.” —Vivek Choudhury, dean of the Daniels College of Business&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;You can hear what Dr. Barbara Jackson has to say about the Collaboratory and VisionPort’s role on DU&amp;rsquo;s &lt;a href=&#34;https://www.youtube.com/watch?v=uAK2W3Utxek&#34;&gt;Burns School YouTube channel&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We are grateful to be able to contribute to the education experience and look forward to seeing the Collaboratory’s VisionPort play its part in what Dr. Jackson has dubbed “the classroom of the 21st century.”&lt;/p&gt;
&lt;p&gt;For more information about VisionPort, email &lt;a href=&#34;mailto:sales@visionport.com&#34;&gt;sales@visionport.com&lt;/a&gt; or visit &lt;a href=&#34;https://www.visionport.com&#34;&gt;www.visionport.com&lt;/a&gt;.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Kansas State University: One Year with VisionPort</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2022/08/kansas-state-university-one-year-with-visionport/"/>
      <id>https://www.endpointdev.com/blog/2022/08/kansas-state-university-one-year-with-visionport/</id>
      <published>2022-08-18T00:00:00+00:00</published>
      <author>
        <name>Sanford Libo</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2022/08/kansas-state-university-one-year-with-visionport/ksu_lg.webp&#34; alt=&#34;A wide view of KSU&amp;rsquo;s campus from above, in Google Earth, showing what might be displayed on KSU&amp;rsquo;s VisionPort.&#34;&gt;&lt;/p&gt;
&lt;p&gt;It has been almost a year since Kansas State University brought the VisionPort platform into their Hale library. I recently had the pleasure of connecting with Jeff Sheldon, Associate Director of the Sunderland Foundation Innovation Lab, to discuss how the school has been using the platform.&lt;/p&gt;
&lt;p&gt;It’s no surprise to hear that the Architecture, Planning and Design (AP) students have taken to VisionPort immediately. Being originally designed around displaying geographic information system (GIS) data, the platform allows users to fly over and through city streets and see buildings in 3D, as well as travel around the world looking for areas of possible real estate development. Many of our clients also use VisionPort to give panoramic, three-dimensional tours of building interiors, to show future tenants properties right from their office and brainstorm design possibilities.&lt;/p&gt;
&lt;p&gt;In addition to the AP Design students, VisionPort has found itself being used to immerse students in their education with an incredible National Geographic presentation that features 360° videos including swimming with sharks and getting up close to sea lions and elephants in their natural habitats, as well as presentations about the moon landing, and even the reconstruction of the Hale library after a fire in 2018.&lt;/p&gt;
&lt;p&gt;&lt;iframe width=&#34;560&#34; height=&#34;315&#34; src=&#34;https://www.youtube.com/embed/EB75SGwKiwM&#34; title=&#34;YouTube video player&#34; frameborder=&#34;0&#34; allow=&#34;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&#34; allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;p&gt;An extremely innovative application KSU has come up with is using the VisionPort system to create a quiet space for students. During exam weeks, the platform is used to create a calming environment, displaying relaxing scenes to help students alleviate stress and promote good mental health.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Ultimately, people don&amp;rsquo;t expect this sort of device in a library and VisionPort has helped change the longstanding narrative that a library is just about books.&amp;rdquo;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;It’s always great to hear about VisionPort being a catalyst for collaboration among students and faculty. Students are encouraged to work together to bring their projects to life on VisionPort and share tips and tricks they’ve learned by working with our content management system.&lt;/p&gt;
&lt;h3 id=&#34;interview&#34;&gt;Interview&lt;/h3&gt;
&lt;h5 id=&#34;end-point-dev-how-has-visionport-aided-the-evolution-of-the-hale-library&#34;&gt;End Point Dev: How has VisionPort aided the evolution of the Hale Library?&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Jeff Sheldon:&lt;/strong&gt; VisionPort, as a feature of the new Sunderland Foundation Innovation Lab in Hale Library, has helped evolve the University&amp;rsquo;s library space in several ways. As a visualization tool, VisionPort allows our students, faculty, and visitors to experience rich immersive presentations in a way they haven&amp;rsquo;t before. Featuring the same Google products we see in classroom instruction, at scale and in high resolution, also affords students an unexpectedly different perspective from what they experience more traditionally on a small screen or in washed-out projections. This has been especially valuable amidst the pandemic, which has transformed expectations to the virtual almost as a default.&lt;/p&gt;
&lt;p&gt;Ultimately, people don&amp;rsquo;t expect this sort of device in a library and VisionPort has helped change the longstanding narrative that a library is just about books.&lt;/p&gt;
&lt;h5 id=&#34;what-types-of-presentations-have-students-and-faculty-been-creating-with-visionport&#34;&gt;What types of presentations have students and faculty been creating with VisionPort?&lt;/h5&gt;
&lt;p&gt;The presentations our students and faculty have been most excited about feature video across our entire 7-panel display and 360° integration for photos and videos. Some have experimented with data visualization and location-to-location journeys, but an overwhelming number have been drawn to the simple navigation controls to use locations and sites as visual aids during group presentations and as a complement to other examples they wheel in.&lt;/p&gt;
&lt;h5 id=&#34;what-are-some-ways-in-which-google-earth-is-being-utilized-with-visionport&#34;&gt;What are some ways in which Google Earth is being utilized with VisionPort?&lt;/h5&gt;
&lt;p&gt;Google Earth&amp;rsquo;s ability to roll onto the screen and zoom into specific locations has the effect of taking viewers on a journey. You can read that in the faces of those watching as a destination unfolds or the promise of a demonstration materializes in front of a tour group. We have quite a few guests visit their hometowns to reminisce and share stories with others, access remote sites to plan for travel, wander the streets of historic figures, and to experience the cultural and socioeconomic influences on a community.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2022/08/kansas-state-university-one-year-with-visionport/ksu_wide.webp&#34; alt=&#34;A photo of Kansas State University&amp;rsquo;s VisionPort in its library. Several modern chairs are set up facing the 7-screen VisionPort, displaying KSU&amp;rsquo;s campus on Google Earth.&#34;&gt;&lt;/p&gt;
&lt;h5 id=&#34;what-content-on-the-visionport-have-students-been-most-excited-about&#34;&gt;What content on the VisionPort have students been most excited about?&lt;/h5&gt;
&lt;p&gt;That&amp;rsquo;s a tough one to narrow down. There have been a few unexpected moments during some of our featured 360° videos where a visitor will turn the camera view around and be charged by an elephant or sniffed by a lion. Seeing such an experience result in a yelp or jump is thrilling.&lt;/p&gt;
&lt;h5 id=&#34;what-fields-of-study-are-being-presented-most-frequently&#34;&gt;What fields of study are being presented most frequently?&lt;/h5&gt;
&lt;p&gt;Kansas State&amp;rsquo;s Architecture, Planning and Design (AP Design) students are regulars, but we&amp;rsquo;ve also fielded student presentations from our history and language departments. A number of ad-hoc classes have worked on presentations as well, but there are many curious about the act of how to visualize and present data to different audiences and who enjoy the process of exploration, but aren&amp;rsquo;t always looking to produce a specific outcome to share. We create accounts for those students to access the system and learn from their peers and the stock examples.&lt;/p&gt;
&lt;h5 id=&#34;how-are-panoramic-images-being-used-on-the-visionport&#34;&gt;How are panoramic images being used on the VisionPort?&lt;/h5&gt;
&lt;p&gt;We&amp;rsquo;re very interested in panoramic and 360° images for the sake of storytelling. One example has been to tell the history of Hale Library in the aftermath of the 2018 fire it endured. Another has been to show the ruins of culturally significant sites some of our students have visited. We&amp;rsquo;ve also worked with faculty members to produce wellness videos during exam weeks, such as a calming aquarium or nature scene.&lt;/p&gt;
&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;We are always happy to hear first-hand experiences universities are having with the VisionPort platform and we are honored to be able to contribute to the education experience.&lt;/p&gt;
&lt;p&gt;Thank you to Mr. Sheldon for the great meeting. We look forward to hearing how KSU innovates with VisionPort over the next year!&lt;/p&gt;
&lt;p&gt;For more information about VisionPort, email &lt;a href=&#34;mailto:sales@visionport.com&#34;&gt;sales@visionport.com&lt;/a&gt; or visit &lt;a href=&#34;https://www.visionport.com&#34;&gt;www.visionport.com&lt;/a&gt;.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Auburn University and VisionPort: How the World Gets Its Water</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2022/07/auburn-university-and-visionport-how-the-world-gets-its-water/"/>
      <id>https://www.endpointdev.com/blog/2022/07/auburn-university-and-visionport-how-the-world-gets-its-water/</id>
      <published>2022-07-28T00:00:00+00:00</published>
      <author>
        <name>Samuel Stern</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2022/07/auburn-university-and-visionport-how-the-world-gets-its-water/auburn-cap-visionport.webp&#34; alt=&#34;A VisionPort presenting about the Central Arizona Project&#34;&gt;&lt;/p&gt;
&lt;p&gt;The IBT Water Project at Auburn University, headed by Associate Professor P.L. Chaney, has done outstanding work illustrating in a GIS format how cities around the world get their water. The Geoscience department has mapped how water is captured and distributed in Australia, Egypt, India, Mexico, Kazakhstan, and the western USA.&lt;/p&gt;
&lt;p&gt;The department chose the Central Arizona Project to turn into an interactive presentation on the VisionPort platform.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2022/07/auburn-university-and-visionport-how-the-world-gets-its-water/capgis.webp&#34; alt=&#34;GIS showing water pumping sites&#34;&gt;&lt;/p&gt;
&lt;p&gt;Starting at the Mark Wilmer Pumping Plant, water is pumped from the Colorado River towards over a dozen plants and lifted up over 2,000 feet in elevation across a series of “stair-steps” before it reaches its final destination near Tucson, where it is then distributed across the state to where it is most needed.&lt;/p&gt;
&lt;p&gt;This data displayed on their VisionPort, installed in a custom wood case in their library, allows students to see the entire journey in a 3D environment spanning seven 65-inch displays. The presenter can take them to each stop and explain the functions of the many plants, check gates, and turnouts along the way.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2022/07/auburn-university-and-visionport-how-the-world-gets-its-water/presentation.webp&#34; alt=&#34;A man giving a presentation with the VisionPort&#34;&gt;&lt;/p&gt;
&lt;p&gt;Numerous departments at Auburn University have had success turning their presentations into engaging experiences on the VisionPort platform and I look forward to seeing and reporting on what their students and faculty do next.&lt;/p&gt;
&lt;p&gt;For more information about VisionPort, email &lt;a href=&#34;mailto:sales@visionport.com&#34;&gt;sales@visionport.com&lt;/a&gt; or visit &lt;a href=&#34;https://www.visionport.com&#34;&gt;www.visionport.com&lt;/a&gt;.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>VisionPort at University of Tokyo, New York office: An Exhibition for Peace on August 6th and 7th</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2022/07/visionport-at-university-of-tokyo-new-york-office/"/>
      <id>https://www.endpointdev.com/blog/2022/07/visionport-at-university-of-tokyo-new-york-office/</id>
      <published>2022-07-26T00:00:00+00:00</published>
      <author>
        <name>Samuel Stern</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2022/07/visionport-at-university-of-tokyo-new-york-office/ground-zero.webp&#34; alt=&#34;3D visualization of Hiroshima with photos pinned throughout&#34;&gt;&lt;br&gt;
Ground Zero, Hiroshima, Japan – August 6th, 1945. &lt;a href=&#34;https://hiroshima.mapping.jp/index_en.html&#34;&gt;Visualized by the lab of Professor Hidenori Watanave&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Technology and education go hand in hand, and the VisionPort platform is being used every day to make that connection.&lt;/p&gt;
&lt;p&gt;We are extremely honored to be able to contribute to the &lt;a href=&#34;https://labo.wtnv.jp/2022/07/convergence2022en.html&#34;&gt;first exhibition at the University of Tokyo’s New York office&lt;/a&gt;, “Convergence of Peace Activities: Connecting and Integrating by Technologies”.&lt;/p&gt;
&lt;p&gt;It is said that those who do not learn from history are condemned to repeat it, and in that vein, the exhibition, drawing from the work of Professor Hidenori Watanave, will be using the VisionPort platform to educate viewers on the realities of the bombings of Hiroshima and Nagasaki, on the date of the 77th anniversary of the first nuclear weapon used in war.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2022/07/visionport-at-university-of-tokyo-new-york-office/utokyo-visionport.webp&#34; alt=&#34;Several women and men in a presentation on the VisionPort&#34;&gt;&lt;/p&gt;
&lt;p&gt;The team has been collecting and colorizing photographic material from the aftermath of the bombings for over 10 years. The exhibition will combine that work with interviews and writings from survivors on a GIS canvas to allow attendees to see what it looked like and to hear from those who were there.&lt;/p&gt;
&lt;p&gt;The lab will also be presenting the work they have been doing covering the ongoing conflict in Ukraine. Day by day they collect the latest images from the war, identify the locations of the events and use geospatial data to map and present it in an interactive, 3D environment.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2022/07/visionport-at-university-of-tokyo-new-york-office/ukraine.webp&#34; alt=&#34;A 3D rendering of bombed buildings in Ukraine&#34;&gt;&lt;br&gt;
&lt;a href=&#34;https://www.u-tokyo.ac.jp/focus/en/features/z1304_00194.html&#34;&gt;Invasion of Ukraine, provided by the labs of Professors Hidenori Watanave and Taichi Furuhashi&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The exhibition will serve to show us where we have been and where we are now, in hopes of being a “convergence,” a place to connect and use all of our available information and technologies so that we may begin a new era of understanding and in turn, peace.&lt;/p&gt;
&lt;p&gt;The exhibition will also feature work done by &lt;a href=&#34;https://coop.archiving.jp/&#34;&gt;Co-Op Peace Map&lt;/a&gt;, &lt;a href=&#34;https://mainichi.jp/english/&#34;&gt;Mainichi Newspaper&lt;/a&gt; and the University of Kyoto.&lt;/p&gt;
&lt;p&gt;The educational work that the University of Tokyo is doing with the VisionPort platform is inspiring and we look forward to being there to see it in memoriam of that fateful day.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://convergence_peace_activities.eventbrite.com/&#34;&gt;Register here&lt;/a&gt; and join us for this viewing on August 6th and 7th at the University of Tokyo’s New York office located at &lt;a href=&#34;https://maps.app.goo.gl/Qoexb1VnrbcEcMmn8&#34;&gt;145 W. 57th St., 21st Floor, New York, NY 10019&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For more information about VisionPort, email &lt;a href=&#34;mailto:sales@visionport.com&#34;&gt;sales@visionport.com&lt;/a&gt; or visit &lt;a href=&#34;https://www.visionport.com/&#34;&gt;visionport.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Images and photography provided by the University of Tokyo, the department of Interfaculty Initiative in Information Studies, the lab of Professor Hidenori Watanave, and the lab of Professor Taichi Furuhashi.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Job opening: VisionPort support engineer (western U.S.)</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2022/04/job-visionport-support-engineer/"/>
      <id>https://www.endpointdev.com/blog/2022/04/job-visionport-support-engineer/</id>
      <published>2022-04-14T00:00:00+00:00</published>
      <author>
        <name>Alejandro Ramon</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2021/06/job-liquid-galaxy-support-engineer/lg-cabinet.jpg&#34; alt=&#34;VisionPort cabinet with 7 HDTV screens in portrait orientation&#34;&gt;&lt;/p&gt;
&lt;p&gt;We are looking for an engineer to join the End Point Immersive and Geospatial Support (I+G) Team—​a small, multidisciplinary team that supports our company’s clients with their &lt;a href=&#34;https://www.visionport.com/&#34;&gt;VisionPort systems&lt;/a&gt; incorporating Liquid Galaxy technology. VisionPort hardware consists of large-panel HD TVs within a curved panoramic environment, supported by a server stack with power, video, audio, and network connections and equipment.&lt;/p&gt;
&lt;p&gt;The candidate will be based out of a home office in the western United States (Washington, Oregon, California, Idaho, Utah, Nevada, Arizona, Montana, Wyoming, New Mexico, and Texas). The engineer will be asked to travel to, perform, and supervise system installations, in addition to day-to-day remote support work from a home office.&lt;/p&gt;
&lt;p&gt;Occasional evenings and weekend on-call shifts are shared amongst the team.&lt;/p&gt;
&lt;p&gt;This is a great entry-level opportunity for people already familiar with light audiovisual, server room, and/or installation handiwork to get experience with all aspects of production computer systems and their deployment. More experienced individuals will have the opportunity to work directly in feature development on production systems and possibly assist with other ongoing consulting projects the I+G team takes on.&lt;/p&gt;
&lt;h3 id=&#34;overview&#34;&gt;Overview&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Job Level&lt;/strong&gt;: Entry-level or experienced, full-time or part-time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Location&lt;/strong&gt;: Remote work with occasional on-site.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment/​Culture&lt;/strong&gt;: Casual, remote management, lots of video meetings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Benefits&lt;/strong&gt;: For full-time employees, paid vacation and holidays, 401(k), health insurance.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;core-responsibilities&#34;&gt;Core responsibilities&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Support clients over email and video calls, troubleshooting system features or content.&lt;/li&gt;
&lt;li&gt;Build server stacks, troubleshoot hardware, and fix software issues during installation.&lt;/li&gt;
&lt;li&gt;Track, monitor, and resolve system issues on production computer systems.&lt;/li&gt;
&lt;li&gt;Report and document issues and fixes.&lt;/li&gt;
&lt;li&gt;Adapt to differing needs day to day!&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;preferred-skills-and-experience&#34;&gt;Preferred skills and experience&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A driver’s license is required for travel.&lt;/li&gt;
&lt;li&gt;Comfortable with basic installation tools: hand drill, cable dressing, cable pulls, crimping, etc.&lt;/li&gt;
&lt;li&gt;Experience on an active construction or office renovation site.&lt;/li&gt;
&lt;li&gt;Working familiarity with computers, general exposure to internal components and software.&lt;/li&gt;
&lt;li&gt;Self-driven, results-, detail-, and team-oriented, follows through on problems.&lt;/li&gt;
&lt;li&gt;Familiarity with Linux basics, or willingness to learn.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;what-work-here-offers&#34;&gt;What work here offers:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Flexible work hours&lt;/li&gt;
&lt;li&gt;Annual bonus opportunity&lt;/li&gt;
&lt;li&gt;Freedom from being tied to an office location&lt;/li&gt;
&lt;li&gt;Collaboration with knowledgeable, friendly, helpful, and diligent co-workers around the world&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;about-end-point&#34;&gt;About End Point&lt;/h3&gt;
&lt;p&gt;End Point is the leading global agency for developing, deploying, and supporting VisionPort systems. With over 200 installations and events worldwide, our team of engineers and content developers has a deep knowledge of the hardware, software, and aesthetics that can make a VisionPort come to life as a dazzling data, business, education, or presentation tool.&lt;/p&gt;
&lt;p&gt;End Point was founded in 1995 and now has over 60 engineers based throughout North America, Europe, and Asia. This team brings decades of experience in systems management, database programming, web platform development, application development, and more specifically, VisionPort development and support.&lt;/p&gt;
&lt;h3 id=&#34;get-in-touch-with-us&#34;&gt;Get in touch with us&lt;/h3&gt;
&lt;p&gt;&lt;del&gt;Please email us an introduction to jobs@visionport.com to apply!&lt;/del&gt;
&lt;strong&gt;(This job has been filled.)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Include your location, resume, LinkedIn URL (if any), and whatever else may help us get to know you. You can expect to interview with the Support Lead and Team Manager.&lt;/p&gt;
&lt;p&gt;We look forward to hearing from you! Direct work seekers only, please—​this role is not for agencies or subcontractors.&lt;/p&gt;
&lt;p&gt;We are an equal opportunity employer and value diversity at our company. We do not discriminate on the basis of sex/​gender, race, religion, color, national origin, sexual orientation, age, marital status, veteran status, or disability status.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>VisionPort Hardware Overview</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2022/01/visionport-hardware-overview/"/>
      <id>https://www.endpointdev.com/blog/2022/01/visionport-hardware-overview/</id>
      <published>2022-01-24T00:00:00+00:00</published>
      <author>
        <name>Alejandro Ramon</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2022/01/visionport-hardware-overview/in-wall-tech-specs.jpg&#34; alt=&#34;A VisionPort system, with labeled servers, wall, displays, and controllers&#34;&gt;&lt;/p&gt;
&lt;p&gt;End Point’s VisionPort has gone through a number of hardware iterations before becoming the system our clients are familiar with today. In this post, we will break down the history of the changes we have made over the years, and how we ended up where we are today.&lt;/p&gt;
&lt;p&gt;The latest server specification used for all current VisionPort installations is known internally as the “LGOne” (Liquid Galaxy One): a node that can support up to eight discrete displays as a single desktop. LGOne is now considered the default specification and will guide the direction and future of VisionPort platform development.&lt;/p&gt;
&lt;h3 id=&#34;a-few-definitions-before-we-continue&#34;&gt;A few definitions before we continue&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Node&lt;/strong&gt;: a single computer/​server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Display node&lt;/strong&gt;: A computer that acts as a source for the displays, and runs applications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Head node&lt;/strong&gt;: A computer that acts as the “brain” of the system.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;some-history&#34;&gt;Some history&lt;/h3&gt;
&lt;p&gt;The original server specification for the Liquid Galaxy (now known as &lt;a href=&#34;https://www.visionport.com/&#34;&gt;VisionPort&lt;/a&gt;), was developed around &lt;strong&gt;2011&lt;/strong&gt; and is now known as “LGClassic”. The LGClassic specification supported one display per node. For the typical seven-screen installation, this meant that eight individual LGClassic display nodes and one head node were required to run our seamless video wall technology with an integrated podium. While the experience was seamless for users, improvements to the performance, flexibility, and overall cost could be made.&lt;/p&gt;
&lt;p&gt;Around &lt;strong&gt;2015&lt;/strong&gt;, advances in graphics and computing technology allowed us to proceed to the next generation: the LGX. LGX nodes supported up to four discrete displays, and thus brought the number of display nodes necessary to run a typical seven-screen installation down to two. This meant that users could experience improved performance and syncing of applications, fewer points of failure, and more modern components. Around this same time, the CMS (Content Management System, known internally as “Roscoe CMS”) was released with a modern interface and tools that made it easier for users to generate content quickly for their system. Still, there were some nuances that affected performance and made content creation less than ideal, like how a seemingly seamless video wall was in actuality two discrete computer desktops.&lt;/p&gt;
&lt;p&gt;Although the LGX line of systems have been updated with newer graphics cards and processors over the years, there have been few significant changes to the standard server specification since 2016. As of 2021, the majority of existing systems in the field continue to utilize the LGX specification.&lt;/p&gt;
&lt;p&gt;Released in early &lt;strong&gt;2020&lt;/strong&gt;, LGOne continued the trend towards improved reliability, performance, and feature support. Systems with an LGOne specification can expect:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Support for 8 (typical) to 16 (advanced) discrete displays running full-resolution application imagery without the need for expensive AV technology such as digital matrix switchers or video wall processors.&lt;/li&gt;
&lt;li&gt;A unified desktop experience, greatly simplifying content creation for CMS users. Consider no rules, borders, or needing to split up images to act a specific way. Simply drag and drop the image anywhere on screen to place it. Images, web pages, and videos can easily span all screens as well, if desired. In addition, support for graphic transparency allows presentation creators flexibility in how they present their content to viewers.&lt;/li&gt;
&lt;li&gt;Increased stability and performance of the system with enterprise grade Nvidia Quadro RTX graphics and Intel Xeon processors.&lt;/li&gt;
&lt;li&gt;Reduced server room footprint allows for simpler installation and maintenance with fewer moving parts.&lt;/li&gt;
&lt;li&gt;Support for technology integrations, like native &lt;a href=&#34;/blog/2021/09/video-conference-integration/&#34;&gt;Video Conferencing&lt;/a&gt; or &lt;a href=&#34;/blog/2021/09/liquid-galaxy-screen-share-integration/&#34;&gt;Screen Sharing solutions&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Support for &lt;a href=&#34;/blog/2021/09/introducing-visionport-remote/&#34;&gt;live view and remote control&lt;/a&gt;, for when a user is not in the office.&lt;/li&gt;
&lt;li&gt;Improved window management tricks that allow for improved scene transitions and smarter content switching.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2022/01/visionport-hardware-overview/lgone-stack.jpg&#34; alt=&#34;A server with: Peripheral equipment, network switch, power distribution unit, head node, display node, all in a section called 9U&#34;&gt;&lt;br&gt;
The LGOne server stack&lt;/p&gt;
&lt;p&gt;In addition to all the above, standardization of the spec allows VisionPort systems with the LGOne specification to support all of our future development. This includes an upcoming rewrite to the CMS with a new interface, improved workflow, and a significant change to how content is previewed. As always, any customer with an up-to-date maintenance agreement will receive all updates and support for the above when made available.&lt;/p&gt;
&lt;p&gt;Over the years we have received plenty of feedback and insight into how the system is used by our client base. We hope that the changes we are making, and will continue to work on, show the effort to respect our users, encourage excitement in using the system, and provide better tools towards creating dynamic and impactful presentations for your customers and consumers.&lt;/p&gt;
&lt;p&gt;Please reach out to &lt;a href=&#34;mailto:sales@visionport.com&#34;&gt;sales@visionport.com&lt;/a&gt; for any questions about pricing and availability or visit &lt;a href=&#34;https://www.visionport.com/&#34;&gt;www.visionport.com&lt;/a&gt; to learn more.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>VisionPort: A Breakdown</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2022/01/visionport-a-breakdown/"/>
      <id>https://www.endpointdev.com/blog/2022/01/visionport-a-breakdown/</id>
      <published>2022-01-01T00:00:00+00:00</published>
      <author>
        <name>Ben Witten</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2022/01/visionport-a-breakdown/image-0.jpg&#34; alt=&#34;A VisionPort-equipped meeting room&#34;&gt;&lt;/p&gt;
&lt;p&gt;In late 2021, End Point Corporation’s immersive technology team &lt;a href=&#34;/blog/2021/11/from-liquid-galaxy-to-visionport/&#34;&gt;officially launched VisionPort&lt;/a&gt;. Evolved from End Point’s Liquid Galaxy, &lt;a href=&#34;https://www.visionport.com&#34;&gt;VisionPort&lt;/a&gt; allows presenters to share content between embedded displays, managed devices, and wireless controllers, all from the tap of a finger. VisionPort was created to incorporate relevant conferencing and office technologies with a powerful and impactful video wall solution.&lt;/p&gt;
&lt;p&gt;Unique to VisionPort is the method that allows for full-resolution applications — synchronized, with geometrically adjusted instances — regardless of the number of displays. This technique avoids the fish-eye distortion commonly seen on conventional video walls created by stretching content beyond its typical aspect ratio, and ensures that the highest quality content can be delivered to viewers.&lt;/p&gt;
&lt;p&gt;The platform also integrates with other displays placed in the room, enabling users to control the experience of the visualizations presented for their participants.&lt;/p&gt;
&lt;p&gt;A person acting as Meeting Director can both interact with and guide multiple inbound video streams via an intuitive interface on an iPad or other tablet controller. With the appropriate setup, directing someone’s laptop image to any screen in the room is as easy as swiping a video thumbnail into the appropriate square on the tablet.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2022/01/visionport-a-breakdown/image-1.jpg&#34; alt=&#34;A VisionPort immersive space, with a 14-screen video wall, three large touch screens, a tablet, and two laptops&#34;&gt;&lt;/p&gt;
&lt;p&gt;Above is a conceptual rendering of how VisionPort can be integrated into a workspace. This rendering features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The base VisionPort product, in this case outfitted with fourteen HD displays split into two rows of seven (although this could just as easily be one row of seven, two rows of five, or one single large screen).&lt;/li&gt;
&lt;li&gt;Two additional side screens, integrated with the base VisionPort system and accessible via the CMS.&lt;/li&gt;
&lt;li&gt;The &lt;a href=&#34;/blog/2021/09/liquid-galaxy-screen-share-integration/&#34;&gt;SSI Kit&lt;/a&gt; incorporated on the left screen, sharing the left side’s laptop display content.&lt;/li&gt;
&lt;li&gt;The &lt;a href=&#34;/blog/2021/09/video-conference-integration/&#34;&gt;VCI Kit&lt;/a&gt; incorporated on the right screen, sharing the video wall content to an active Zoom meeting, also featured on the right laptop.&lt;/li&gt;
&lt;li&gt;Wireless controllers in the front of the room, in addition to an embedded touchscreen.&lt;/li&gt;
&lt;li&gt;Other features not shown above include room audio and support for alternative control methods via USB.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While this particular implementation may seem extravagant, each individual part of the platform is modular, and can be included in the initial installation, or as an enhancement to an existing installation.&lt;/p&gt;
&lt;p&gt;Our VisionPort platform combines custom server hardware with commercial displays and relevant conferencing technology to create the ideal cutting-edge conference room system for enterprises in commercial real estate, logistics, and travel, among other industries.&lt;/p&gt;
&lt;p&gt;Central to VisionPort is End Point’s CMS (Content Management System), which enables clients to quickly and easily build multimedia presentations for the platform, and End Point’s excellent 24×7 support.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2022/01/visionport-a-breakdown/image-2.jpg&#34; alt=&#34;Charts and GIS displayed on the VisionPort&#34;&gt;&lt;/p&gt;
&lt;p&gt;VisionPort’s system architecture is based in Linux and ROS (Robot Operating System), and provides a fundamentally secure, stable, and flexible environment for organizations seeking to display geospatial data sets in a concise and interactive manner. Research universities, multimedia studios, and data laboratories are also well positioned to fully leverage VisionPort, as it allows for multiple data sources and visualization streams to be viewed simultaneously. Museums, aquariums, and science centers use VisionPort to wow their visitors, combining immersive video with interactive displays that surround the guests in a data-immersive environment.&lt;/p&gt;
&lt;p&gt;VisionPort is a huge step forward in End Point’s mission to create the ideal platform for shared immersive presentations, and our team put in a great effort to bring this to life. We are proud to announce that VisionPort is ready for deployment. For more information, contact &lt;a href=&#34;mailto:sales@visionport.com&#34;&gt;sales@visionport.com&lt;/a&gt; or visit &lt;a href=&#34;https://www.visionport.com&#34;&gt;www.visionport.com&lt;/a&gt;.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>VisionPort Success at Auburn University</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2021/12/visionport-success-at-auburn-university/"/>
      <id>https://www.endpointdev.com/blog/2021/12/visionport-success-at-auburn-university/</id>
      <published>2021-12-24T00:00:00+00:00</published>
      <author>
        <name>Ben Witten</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2021/12/visionport-success-at-auburn-university/auburn-1.jpg&#34; alt=&#34;Street view on the Auburn VisionPort&#34;&gt;&lt;/p&gt;
&lt;p&gt;Auburn University’s Liquid Galaxy/​VisionPort installation was the first of its kind, built into a stunning wooden cabinet. A technological showpiece that is a highlight of Auburn’s campus, the VisionPort platform is used to showcase incredible imagery and presentations created by students and faculty alike.&lt;/p&gt;
&lt;p&gt;Auburn’s Liquid Galaxy/​VisionPort is located on the first floor of the RBD (Ralph Brown Draughon) Library. As stated on &lt;a href=&#34;https://lib.auburn.edu/liquidgalaxy/&#34;&gt;Auburn’s Library webpage&lt;/a&gt;, “It provides a one-of-a-kind platform for 3D geospatial visualization, panoramic images, video, tours, and more in a shared immersive environment. Auburn holds the distinction of being one of the few institutions in the Southeast to make this unique and innovative resource available to its patrons.”&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2019/10/liquid-galaxy-cabinets/image-0.jpg&#34; alt=&#34;The Auburn VisionPort in its custom wooden cabinet&#34;&gt;&lt;/p&gt;
&lt;p&gt;Chris Mixon is Auburn University’s Information Technology Specialist in the Media &amp;amp; Digital Resource Lab. He works closely with University groups to develop content, and also works with End Point’s support team as needs arise. As Chris has shared, “we are doing great things with this platform!” Among other projects, Chris is working with instructors and students in Urban Design Studio (CPLN 7200) and Science Communication (ESSI 7150) to display their work on the VisionPort platform.&lt;/p&gt;
&lt;p&gt;Auburn University’s Liquid Galaxy/​VisionPort is also used for Creative Explorations in Technology and Design programs geared toward 7th–11th graders. This program is focused on increasing digital literacy and building valuable technical skills as these middle and high school students prepare for the next stage of their lives.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2021/12/visionport-success-at-auburn-university/wade-hall.jpg&#34; alt=&#34;Wade Hall presentation on the Auburn VisionPort&#34;&gt;
&lt;small&gt;A Liquid Galaxy/​VisionPort exhibit honoring native Alabama author and educator Dr. Wade Hall by Auburn University Libraries’ Special Collections and Archives Department.&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;We look forward to continuing to support Auburn University in leveraging the VisionPort platform for great educational uses.&lt;/p&gt;
&lt;p&gt;To learn more about VisionPort/​Liquid Galaxy installations in custom cabinets, flat floor-mounted grids, two rows of screens, and other unique configurations, see my earlier article &lt;a href=&#34;/blog/2019/10/liquid-galaxy-cabinets/&#34;&gt;Custom cabinets for the Liquid Galaxy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Read more about VisionPort in general at &lt;a href=&#34;https://www.visionport.com&#34;&gt;www.visionport.com&lt;/a&gt;.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>From Liquid Galaxy to VisionPort</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2021/11/from-liquid-galaxy-to-visionport/"/>
      <id>https://www.endpointdev.com/blog/2021/11/from-liquid-galaxy-to-visionport/</id>
      <published>2021-11-23T00:00:00+00:00</published>
      <author>
        <name>Alejandro Ramon</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2021/11/from-liquid-galaxy-to-visionport/banner.jpg&#34; alt=&#34;A VisionPort system&#34;&gt;&lt;/p&gt;
&lt;p&gt;We are rebranding! Meet the future of Liquid Galaxy: &lt;a href=&#34;https://www.visionport.com&#34;&gt;VisionPort&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We are proud to announce the official launch of VisionPort, the next phase for Liquid Galaxy. We have spent the past six months taking steps to rebrand, expand, and reposition our product to combine modern working necessities with the traditional kiosk-style, shared immersive experience familiar to our clients. Through these efforts have come a robust product encompassing an entire room of enhanced features, screens, and conference-enabling applications.&lt;/p&gt;
&lt;p&gt;While our core product will remain consistent with what our clients know and love, current and future clients can look forward to significant updates to the content management system and user experience. We are also proud to announce advanced add-on features that will allow our current and future clients to make their systems more collaborative, interactive, and adaptable to their needs.&lt;/p&gt;
&lt;p&gt;Our core offerings include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extensive preparation and customization of screens, servers, and frames&lt;/li&gt;
&lt;li&gt;Google Earth, Cesium, Street View, etc.&lt;/li&gt;
&lt;li&gt;Content Management System&lt;/li&gt;
&lt;li&gt;Ongoing support service&lt;/li&gt;
&lt;li&gt;Custom installation, system, and content consulting&lt;/li&gt;
&lt;li&gt;Comprehensive system and content training&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Our add-on offerings now include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;/blog/2021/09/video-conference-integration/&#34;&gt;Video Conference Integration&lt;/a&gt;: Allows users to join video conferences or host meetings with a native, software-level view of the Liquid Galaxy.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;/blog/2021/09/liquid-galaxy-screen-share-integration/&#34;&gt;Screen Share Integration&lt;/a&gt;: Allows users to share their content to the main displays. Supports sharing laptop, phone, or tablet’s presentation files and media to any screen on VisionPort.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;/blog/2021/11/liquid-galaxy-media-stream-integration/&#34;&gt;Media Stream Integration&lt;/a&gt;: Allows users to share any HDMI video source onto any of the main displays on VisionPort.&lt;/li&gt;
&lt;li&gt;Support for integrating all the above and the Content Management System with any additional side displays to complement the main screens.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;/blog/2021/09/introducing-visionport-remote/&#34;&gt;VisionPort Remote&lt;/a&gt;: Full remote control of the content and camera view, intended for touch devices. Also supports remote view-only sharing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these enhancements are detailed at our new website, &lt;a href=&#34;https://www.visionport.com&#34;&gt;www.visionport.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Please note that we also recently made a major domain change for our organization: Our new domain name is &lt;a href=&#34;https://www.endpointdev.com&#34;&gt;www.endpointdev.com&lt;/a&gt; — feel free to look through that site as well!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you have any questions for us, please reach out to us at &lt;a href=&#34;mailto:info@visionport.com&#34;&gt;info@visionport.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For sales, &lt;a href=&#34;mailto:sales@visionport.com&#34;&gt;sales@visionport.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For billing, &lt;a href=&#34;mailto:billing@visionport.com&#34;&gt;billing@visionport.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For support, &lt;a href=&#34;mailto:support@visionport.com&#34;&gt;support@visionport.com&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the same vein, all existing and future clients can now reach us at the &lt;a href=&#34;https://www.visionport.com&#34;&gt;www.visionport.com&lt;/a&gt; domain. Our team will be proactively reaching out to all of our clients to inform them of these changes, and will be available to answer any questions about the changes or upgrades available.&lt;/p&gt;
&lt;p&gt;We are very excited about these new advancements, offerings, and rebranding opportunities. We look forward to engagement with current and future clients alike.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Liquid Galaxy Media Stream Integration</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2021/11/liquid-galaxy-media-stream-integration/"/>
      <id>https://www.endpointdev.com/blog/2021/11/liquid-galaxy-media-stream-integration/</id>
      <published>2021-11-11T00:00:00+00:00</published>
      <author>
        <name>Alejandro Ramon</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2021/11/liquid-galaxy-media-stream-integration/banner.jpg&#34; alt=&#34;Media Stream Integration&#34;&gt;&lt;/p&gt;
&lt;p&gt;End Point’s Immersive and Geospatial Division is proud to announce the rollout of our new Media Stream Integration as an extension to the Liquid Galaxy platform’s capabilities. This additional hardware can be added to existing installations or included in a new solution provided by our sales team.&lt;/p&gt;
&lt;p&gt;The Media Stream Integration (“MSI”) is a collection of hardware additions to the Liquid Galaxy that allows a user to stream from any HDMI capable device in up to a 4k (3840×​2160 pixels) window on the main displays of the system.&lt;/p&gt;
&lt;p&gt;With the MSI, a user can connect and share any media source directly to the system through the touchscreen. Examples include a video game console, cable TV box, DVR device, laptop or desktop computer, Plex server, and more. In other words, users can showcase media content that may not be natively supported on the Liquid Galaxy platform.&lt;/p&gt;
&lt;h3 id=&#34;how-it-works&#34;&gt;How it works&lt;/h3&gt;
&lt;p&gt;A user simply ensures that the device is on, and it will appear as an option to share to the Liquid Galaxy screens on the touchscreen. The stream can be overlaid on top of existing content in pre-defined windows on the display wall through which the content will be displayed. When finished, the overlay window can be removed at any time.&lt;/p&gt;
&lt;h3 id=&#34;why-we-created-this&#34;&gt;Why we created this&lt;/h3&gt;
&lt;p&gt;We recognize the value offered in having an impressive, high-resolution display wall, and are often asked during an installation, “Can we watch the game on those screens?” In an effort to expand the flexibility of the system, we asked ourselves, “Why not?” As a dynamic presentation system, we want to equip our clients with all the tools necessary to share their stories, including supporting external content on the system.&lt;/p&gt;
&lt;h3 id=&#34;who-this-benefits&#34;&gt;Who this benefits&lt;/h3&gt;
&lt;p&gt;This is useful to anyone looking for a way to share content from a device not natively supported by the system. We are always looking for new ways to interact with the system and build content, and would love to hear your ideas.&lt;/p&gt;
&lt;p&gt;If you are an existing client and have any questions about this new capability, or if you are considering a Liquid Galaxy platform for your organization and would like to learn more, please &lt;a href=&#34;https://www.visionport.com/contact/&#34;&gt;contact us&lt;/a&gt;!&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>New Jersey Liquid Galaxy Installation</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2021/11/new-jersey-liquid-galaxy-installation/"/>
      <id>https://www.endpointdev.com/blog/2021/11/new-jersey-liquid-galaxy-installation/</id>
      <published>2021-11-10T00:00:00+00:00</published>
      <author>
        <name>Ben Witten</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2021/11/new-jersey-liquid-galaxy-installation/banner.jpg&#34; alt=&#34;Another successful Liquid Galaxy conference room&#34;&gt;&lt;/p&gt;
&lt;p&gt;End Point Dev installed a Liquid Galaxy system at the New Jersey office of one of our clients this past March. This marks the fifth office that our client is using to showcase a Liquid Galaxy, joining offices in 4 other states. This new seven-screen Liquid Galaxy system is built into a conference room wall, and will be used as a technological showpiece to allow their team and clients to view different locations, information, and datasets in an immersive and interactive environment.&lt;/p&gt;
&lt;p&gt;As our team is headquartered in New York City, this was a relatively local installation. Our End Point Dev engineers initially spent three days installing this system at the client’s new office; however, due to unforeseen circumstances there were a couple of return trips made to finalize details and ensure the best possible product. We also provided one day of on-site system training, walking the team through using the system and creating presentations with the Content Management System.&lt;/p&gt;
&lt;p&gt;All Liquid Galaxy content for this client has been prepared by their global marketing team, who build region-focused content for each of the different Liquid Galaxy systems. The team effectively builds interactive content that allows their staff to home in on geographic locations and share in-depth research and datasets on their seven-screen systems.&lt;/p&gt;
&lt;p&gt;Our client has seen great success with its growing fleet of Liquid Galaxy systems. They compare the Liquid Galaxy to being in a helicopter, due to the ability to zoom in and look at real estate properties of interest.&lt;/p&gt;
&lt;p&gt;Our client’s in-house research team also effectively uses Liquid Galaxy to visualize opportunities and trends. The team combines information from their industry dataset, with insights from daily dialogue with leaders in manufacturing and distribution.&lt;/p&gt;
&lt;p&gt;While our client often uses Liquid Galaxy in dedicated conference rooms specifically designed to house and experience the system, they have also found success in using the platform for portable use in exhibits and trade fair expo booths.&lt;/p&gt;
&lt;p&gt;To learn more about Liquid Galaxy, please visit our &lt;a href=&#34;https://www.visionport.com&#34;&gt;VisionPort website&lt;/a&gt;.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Liquid Galaxy Hackathon 2021</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2021/10/liquid-galaxy-hackathon-2021/"/>
      <id>https://www.endpointdev.com/blog/2021/10/liquid-galaxy-hackathon-2021/</id>
      <published>2021-10-02T00:00:00+00:00</published>
      <author>
        <name>Seth Jensen</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2021/10/liquid-galaxy-hackathon-2021/image-0.jpg&#34; alt=&#34;Our NYC hackathon group&#34;&gt;&lt;/p&gt;
&lt;p&gt;A few months ago we had our first company gathering since the pandemic started. About 20 End Pointers came to our New York City office to work on various Liquid Galaxy projects, and for several of us, to meet each other in person for the first time.&lt;/p&gt;
&lt;p&gt;Except for our NYC-based team, this was everyone&amp;rsquo;s first look at the &amp;ldquo;new&amp;rdquo; office; we moved offices in January of 2020, so COVID-19 shut down about 14 months of office use.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2021/10/liquid-galaxy-hackathon-2021/image-1.jpg&#34; alt=&#34;The hackathon group&#34;&gt;&lt;/p&gt;
&lt;p&gt;Our CMS team worked on some exciting updates to our Liquid Galaxy CMS, including implementing new and improved techonologies for the database and user interface.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2021/10/liquid-galaxy-hackathon-2021/image-2.jpg&#34; alt=&#34;The CMS team&#34;&gt;&lt;br&gt;
The CMS team: &lt;a href=&#34;/team/zed-jensen/&#34;&gt;Zed&lt;/a&gt;, &lt;a href=&#34;/team/daniel-gomm/&#34;&gt;Dan&lt;/a&gt;, and &lt;a href=&#34;/team/jeff-laughlin/&#34;&gt;Jeff&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Our Research and Development team worked on upgrades to the Liquid Galaxy itself, focusing on creating smoother transitions for multimedia presentations. This included a custom window manager created by Matt, dubbed &amp;ldquo;Matt WM&amp;rdquo; by the team.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2021/10/liquid-galaxy-hackathon-2021/image-3.jpg&#34; alt=&#34;Neil, Jacob, Matt, and Will working on the Liquid Galaxy&#34;&gt;&lt;br&gt;
&lt;a href=&#34;/team/will-plaut/&#34;&gt;Will&lt;/a&gt;, &lt;a href=&#34;/team/matt-vollrath/&#34;&gt;Matt&lt;/a&gt;, &lt;a href=&#34;/team/jacob-minshall/&#34;&gt;Jacob&lt;/a&gt;, and &lt;a href=&#34;/team/neil-elliott/&#34;&gt;Neil&lt;/a&gt;, hard at work&lt;/p&gt;
&lt;p&gt;Our support team worked on spinning up documentation and data entry to bring our inventory up to date and prepare for the next wave of installations.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2021/10/liquid-galaxy-hackathon-2021/image-4.jpg&#34; alt=&#34;Darius hacking away&#34;&gt;&lt;br&gt;
&lt;a href=&#34;/team/darius-clynes/&#34;&gt;Darius&lt;/a&gt; hacking away&lt;/p&gt;
&lt;p&gt;It was great to see everyone at the NYC office, working elbow to elbow. We saw plenty of the hard work and camaraderie which is End Point&amp;rsquo;s speciality. Until next time, we will continue to improve the Liquid Galaxy remotely!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2021/10/liquid-galaxy-hackathon-2021/image-5.jpg&#34; alt=&#34;View from our new office on the 19th floor&#34;&gt;&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Liquid Galaxy Screen Share Integration</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2021/09/liquid-galaxy-screen-share-integration/"/>
      <id>https://www.endpointdev.com/blog/2021/09/liquid-galaxy-screen-share-integration/</id>
      <published>2021-09-29T00:00:00+00:00</published>
      <author>
        <name>Alejandro Ramon</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;a href=&#34;/blog/2021/09/liquid-galaxy-screen-share-integration/click-large.jpg&#34;&gt;&lt;img src=&#34;/blog/2021/09/liquid-galaxy-screen-share-integration/click-small.jpg&#34; alt=&#34;Screen Share Integration&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;End Point’s Immersive and Geospatial Division is proud to announce the rollout of our new Screen Share Integration as an extension to the Liquid Galaxy platform’s capabilities. The additional hardware and software configuration can be added to existing installations or included in a solution provided by our sales team.&lt;/p&gt;
&lt;p&gt;Screen Share Integration uses &lt;a href=&#34;https://www.barco.com/en/clickshare/wireless-presentation&#34;&gt;ClickShare&lt;/a&gt;, a well-regarded enterprise-grade wireless screen sharing tool already used in the offices of many of our commercial real estate clients. With Screen Share Integration you can push a button to share laptop, desktop, phone, and tablet content directly onto the displays of the Liquid Galaxy or onto an integrated side screen. We expect this to be useful for clients who are interested in sharing videos, spreadsheets, and other ad-hoc interactive media directly from their devices to supplement the main content on screen.&lt;/p&gt;
&lt;h3 id=&#34;why-we-created-this&#34;&gt;Why we created this&lt;/h3&gt;
&lt;p&gt;In an effort to expand the flexibility of the Liquid Galaxy platform, we thought about what tools our current clients are already using. We acknowledge that there are limitations to the interactivity of certain content types on the platform, and ClickShare is a useful tool for wireless sharing already used by our clients. Recognizing the value in supporting more interactivity with content sources and expanding on the possibility of what can be presented on the system, we invested in this feature to provide more options to our users.&lt;/p&gt;
&lt;h3 id=&#34;who-this-benefits&#34;&gt;Who this benefits&lt;/h3&gt;
&lt;p&gt;Any client wishing that they could share and interact with data or media from other sources can now seamlessly integrate these media streams onto the Liquid Galaxy mid-presentation, easily enabling and disabling the share as they go through their presentation.&lt;/p&gt;
&lt;h3 id=&#34;how-it-works&#34;&gt;How it works&lt;/h3&gt;
&lt;p&gt;Clients can use a USB-enabled single button device or an application compatible with iOS, Android, Windows, or macOS devices to share the content to their Liquid Galaxy. Using the touchscreen, the user can then place their content onto the main displays in pre-defined windows, seamlessly overlaid on the Liquid Galaxy.&lt;/p&gt;
&lt;p&gt;If you are an existing client and have any questions about this new capability, please email or call us! If you are considering a Liquid Galaxy platform for your organization and would like to learn more, please &lt;a href=&#34;/contact/&#34;&gt;contact us&lt;/a&gt;.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Introducing VisionPort Remote</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2021/09/introducing-visionport-remote/"/>
      <id>https://www.endpointdev.com/blog/2021/09/introducing-visionport-remote/</id>
      <published>2021-09-17T00:00:00+00:00</published>
      <author>
        <name>Alejandro Ramon</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;a href=&#34;/blog/2021/09/introducing-visionport-remote/vpremote-large.jpg&#34;&gt;&lt;img src=&#34;/blog/2021/09/introducing-visionport-remote/vpremote-small.jpg&#34; alt=&#34;VisionPort Remote screenshot&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Welcome to End Point Liquid Galaxy’s new VisionPort Remote! This application not only allows the user to show their content on screen, but also gives control over Google Earth navigation through a number of touch actions. In addition to serving as a remote touchscreen of the system, Remote doubles as a portal that creators can use to test their content from their own devices. A new feature of Remote permits a shareable “guest view” allowing the presentation host to show their content without the possibility of a guest intervening.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: This interface layout is not final.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;why-we-created-this&#34;&gt;Why we created this&lt;/h3&gt;
&lt;p&gt;The COVID-19 pandemic increased the need for remote work, content, and presentations. Prior to the pandemic, the only way to use the Liquid Galaxy system was if you were in front of the display. The VisionPort Remote provides more flexibility and an ability to experience the system’s benefits from all over the world.&lt;/p&gt;
&lt;h3 id=&#34;who-this-benefits&#34;&gt;Who this benefits&lt;/h3&gt;
&lt;p&gt;The VisionPort remote helps content creators visualize the content that they are making without needing to be in front of the system, enables hands-free control of the Liquid Galaxy from any device, and allows for remote sharing of content to viewers who do not have easy access to a Liquid Galaxy but still want to experience the immersive environment.&lt;/p&gt;
&lt;h3 id=&#34;how-it-works&#34;&gt;How it works&lt;/h3&gt;
&lt;p&gt;The host of Remote holds control of the system and presentation, allowing others to view but not affect the Liquid Galaxy.&lt;/p&gt;
&lt;p&gt;The “Earth preview” displays a live view of content that is being displayed on the Liquid Galaxy, and gives the presenter the ability to navigate the map as one normally would with the SpaceNav, the 3D joystick which supports easy motion in all directions.&lt;/p&gt;
&lt;p&gt;The lower portion of the interface controls the presentation and scene. Scenes, presentations and playlists can be selected to be displayed on the Liquid Galaxy as well as within the preview window.&lt;/p&gt;
&lt;p&gt;To the left of the scene controls is a window with preview display modes. These buttons will show a zoomed-in portion of the displays for a better view of their content. By default, wall is selected and shows an entire view of a presentation, as it would be displayed against the 7 screen wall. The other options show a view of the selected section.&lt;/p&gt;
&lt;p&gt;The share button in the top right corner features a view-only option for the guest viewing the presentation. The guest only has the option to view the different preview display modes as they are presented. Press the share button, copy the link, and send it to your intended audience.&lt;/p&gt;
&lt;p&gt;If you have any questions please &lt;a href=&#34;/contact/&#34;&gt;reach out to us&lt;/a&gt;!&lt;/p&gt;

      </content>
    </entry>
  
</feed>
