Maps
Store Locator
View Source
Interactive map with custom markers, popups, and clustering.
Store Locator
Find CoffeeBean locations near you in San Francisco
6Total Stores
3Drive-Thru
324 Hours
1Reserve Bar
🔍
All
Drive-Thru
24 Hours
Reserve
CoffeeBean Financial District0.3 mi
345 Montgomery St
San Francisco, CA 94104
Open until 8 PMDrive-Thru
CoffeeBean Union Square0.5 mi
333 Post St
San Francisco, CA 94108
Open 24 Hours24 Hrs
CoffeeBean Reserve - Ferry Building0.7 mi
1 Ferry Building #7
San Francisco, CA 94111
Open until 7 PMReserve
CoffeeBean Embarcadero0.8 mi
4 Embarcadero Center
San Francisco, CA 94111
Open until 6 PMDrive-Thru
CoffeeBean SOMA1.1 mi
780 Mission St
San Francisco, CA 94103
Open until 9 PMDrive-Thru24 Hrs
CoffeeBean Market Street1.2 mi
901 Market St
San Francisco, CA 94103
Open 24 Hours24 Hrs
Explore More Demos
Oorian Core
Task Manager
Interactive task manager demonstrating pure Oorian features: forms, tables, e...
Oorian Core
Browser APIs
Access browser features from Java: Geolocation, Clipboard, Storage, Notificat...
Maps
Weather Forecast
Real-time weather app with current conditions, 10-day forecast, hourly temper...
StoreLocatorDemo.java
✕
/***********************************************************************************
* Copyright (c) 2025 Corvus Engineering
* All Rights Reserved
***********************************************************************************/
package com.oorian.website.demos;
import com.oorian.css.*;
import com.oorian.html.Element;
import com.oorian.html.elements.*;
import com.oorian.leaflet.*;
import com.oorian.messaging.events.client.InputChangeEvent;
import com.oorian.messaging.events.client.InputCompleteEvent;
import com.oorian.messaging.events.client.InputListener;
import com.oorian.messaging.events.client.MouseClickListener;
import com.oorian.messaging.events.client.MouseClickedEvent;
import java.util.ArrayList;
import java.util.List;
/****************************************************************************************************************
* Store Locator demo component.
* Showcases Leaflet maps with custom markers, popups, and interactive store listings.
* All interactivity is handled server-side using Oorian event handlers.
****************************************************************************************************************/
public class StoreLocatorDemo extends Div implements MouseClickListener, InputListener
{
private static final String MAP_ID = "store-locator-map";
// Store data: id, name, address1, address2, distance, hours, lat, lng, driveThru, is24h, isReserve
private static final String[][] STORES = {
{"store-1", "CoffeeBean Financial District", "345 Montgomery St", "San Francisco, CA 94104", "0.3 mi", "Open until 8 PM", "37.7922", "-122.4029", "true", "false", "false"},
{"store-2", "CoffeeBean Union Square", "333 Post St", "San Francisco, CA 94108", "0.5 mi", "Open 24 Hours", "37.7879", "-122.4074", "false", "true", "false"},
{"store-3", "CoffeeBean Reserve - Ferry Building", "1 Ferry Building #7", "San Francisco, CA 94111", "0.7 mi", "Open until 7 PM", "37.7956", "-122.3933", "false", "false", "true"},
{"store-4", "CoffeeBean Embarcadero", "4 Embarcadero Center", "San Francisco, CA 94111", "0.8 mi", "Open until 6 PM", "37.7946", "-122.3983", "true", "false", "false"},
{"store-5", "CoffeeBean SOMA", "780 Mission St", "San Francisco, CA 94103", "1.1 mi", "Open until 9 PM", "37.7849", "-122.4034", "true", "true", "false"},
{"store-6", "CoffeeBean Market Street", "901 Market St", "San Francisco, CA 94103", "1.2 mi", "Open 24 Hours", "37.7838", "-122.4089", "false", "true", "false"}
};
private LfMap map;
private Div storeList;
private TextInput searchInput;
private String currentFilter = "all";
private String selectedStoreId = null;
private final List<Div> storeCards = new ArrayList<>();
private final List<Div> filterTabs = new ArrayList<>();
public StoreLocatorDemo()
{
createStoreLocator();
}
private void createStoreLocator()
{
setPadding("40px");
setBackgroundColor("#f8fafc");
// Add CSS styles for hover effects
addElement(createStyles());
Div container = new Div();
container.setMaxWidth("1400px");
container.setMargin("0 auto");
container.addElement(createHeader());
container.addElement(createStatsBar());
container.addElement(createMainContent());
addElement(container);
}
private Style createStyles()
{
Style style = new Style();
style.setText("""
.store-card {
background-color: #ffffff;
transition: background-color 0.15s;
}
.store-card:hover {
background-color: #f9fafb;
}
.store-card.selected {
background-color: #f0fdf4 !important;
}
.filter-tab {
transition: all 0.15s;
}
.filter-tab:hover {
background-color: #f3f4f6;
}
.filter-tab.active {
color: #199900 !important;
border-bottom: 2px solid #199900 !important;
background-color: #f0fdf4 !important;
}
""");
return style;
}
private Div createHeader()
{
Div header = new Div();
header.setMarginBottom("24px");
H2 title = new H2();
title.setText("Store Locator");
title.setColor("#1f2937");
title.setFontSize("28px");
title.setFontWeight("700");
title.setMarginTop("0");
title.setMarginBottom("8px");
header.addElement(title);
P subtitle = new P();
subtitle.setText("Find CoffeeBean locations near you in San Francisco");
subtitle.setColor("#6b7280");
subtitle.setFontSize("15px");
subtitle.setMargin("0");
header.addElement(subtitle);
return header;
}
private Div createStatsBar()
{
int totalStores = STORES.length;
int driveThruCount = 0;
int is24HoursCount = 0;
int reserveCount = 0;
for (String[] store : STORES)
{
if (Boolean.parseBoolean(store[8])) driveThruCount++;
if (Boolean.parseBoolean(store[9])) is24HoursCount++;
if (Boolean.parseBoolean(store[10])) reserveCount++;
}
Div stats = new Div();
stats.setDisplay(Display.FLEX);
stats.addStyleAttribute("gap", "24px");
stats.setMarginBottom("24px");
stats.setFlexWrap(FlexWrap.WRAP);
stats.addElement(createStatBadge(String.valueOf(totalStores), "Total Stores"));
stats.addElement(createStatBadge(String.valueOf(driveThruCount), "Drive-Thru"));
stats.addElement(createStatBadge(String.valueOf(is24HoursCount), "24 Hours"));
stats.addElement(createStatBadge(String.valueOf(reserveCount), "Reserve Bar"));
return stats;
}
private Div createStatBadge(String value, String label)
{
Div badge = new Div();
badge.setBackgroundColor("#ffffff");
badge.setBorderRadius("8px");
badge.setPadding("12px 20px");
badge.setDisplay(Display.FLEX);
badge.setAlignItems(AlignItems.CENTER);
badge.addStyleAttribute("gap", "12px");
badge.addStyleAttribute("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)");
Span valueEl = new Span();
valueEl.setText(value);
valueEl.setColor("#199900");
valueEl.setFontSize("24px");
valueEl.setFontWeight("700");
badge.addElement(valueEl);
Span labelEl = new Span();
labelEl.setText(label);
labelEl.setColor("#6b7280");
labelEl.setFontSize("14px");
badge.addElement(labelEl);
return badge;
}
private Div createMainContent()
{
Div content = new Div();
content.setDisplay(Display.GRID);
content.addStyleAttribute("grid-template-columns", "350px 1fr");
content.addStyleAttribute("gap", "24px");
content.setMinHeight("600px");
content.addElement(createSidebar());
content.addElement(createMapSection());
return content;
}
private Div createSidebar()
{
Div sidebar = new Div();
sidebar.setBackgroundColor("#ffffff");
sidebar.setBorderRadius("12px");
sidebar.addStyleAttribute("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)");
sidebar.addStyleAttribute("overflow", "hidden");
sidebar.setDisplay(Display.FLEX);
sidebar.setFlexDirection(FlexDirection.COLUMN);
sidebar.addElement(createSearchBox());
sidebar.addElement(createFilterTabs());
sidebar.addElement(createStoreList());
return sidebar;
}
private Div createSearchBox()
{
Div search = new Div();
search.setPadding("16px");
search.setBorderBottom("1px solid #e5e7eb");
Div inputWrapper = new Div();
inputWrapper.setPosition(Position.RELATIVE);
searchInput = new TextInput();
searchInput.setId("store-search-input");
searchInput.setPlaceholder("Search by city, zip, or address...");
searchInput.setWidth("100%");
searchInput.setPadding("12px 12px 12px 40px");
searchInput.setBorder("1px solid #d1d5db");
searchInput.setBorderRadius("8px");
searchInput.setFontSize("14px");
searchInput.addStyleAttribute("outline", "none");
searchInput.registerListener(this, InputChangeEvent.class);
inputWrapper.addElement(searchInput);
Span icon = new Span();
icon.setText("\uD83D\uDD0D");
icon.setPosition(Position.ABSOLUTE);
icon.setLeft("12px");
icon.setTop("50%");
icon.addStyleAttribute("transform", "translateY(-50%)");
icon.setFontSize("16px");
inputWrapper.addElement(icon);
search.addElement(inputWrapper);
return search;
}
private Div createFilterTabs()
{
Div tabs = new Div();
tabs.setId("filter-tabs");
tabs.setDisplay(Display.FLEX);
tabs.setBorderBottom("1px solid #e5e7eb");
tabs.addElement(createFilterTab("all", "All", true));
tabs.addElement(createFilterTab("drive-thru", "Drive-Thru", false));
tabs.addElement(createFilterTab("24h", "24 Hours", false));
tabs.addElement(createFilterTab("reserve", "Reserve", false));
return tabs;
}
private Div createFilterTab(String filterId, String label, boolean active)
{
Div tab = new Div();
tab.addAttribute("data-filter", filterId);
tab.addClass("filter-tab");
tab.setPadding("12px 16px");
tab.setFontSize("13px");
tab.setFontWeight("500");
tab.addStyleAttribute("cursor", "pointer");
tab.addStyleAttribute("flex", "1");
tab.setTextAlign(TextAlign.CENTER);
tab.setColor("#6b7280");
tab.setBorderBottom("2px solid transparent");
if (active)
{
tab.addClass("active");
}
tab.setText(label);
tab.registerListener(this, MouseClickedEvent.class);
filterTabs.add(tab);
return tab;
}
private Div createStoreList()
{
storeList = new Div();
storeList.setId("store-list");
storeList.addStyleAttribute("overflow-y", "auto");
storeList.addStyleAttribute("flex", "1");
storeList.setMaxHeight("480px");
for (String[] store : STORES)
{
Div card = createStoreCard(
store[0], store[1], store[2], store[3], store[4], store[5],
Double.parseDouble(store[6]), Double.parseDouble(store[7]),
Boolean.parseBoolean(store[8]), Boolean.parseBoolean(store[9]), Boolean.parseBoolean(store[10])
);
storeList.addElement(card);
storeCards.add(card);
}
return storeList;
}
private Div createStoreCard(String id, String name, String address1, String address2,
String distance, String hours, double lat, double lng,
boolean hasDriveThru, boolean is24Hours, boolean isReserve)
{
Div card = new Div();
card.setId(id);
card.addClass("store-card");
card.addAttribute("data-lat", String.valueOf(lat));
card.addAttribute("data-lng", String.valueOf(lng));
card.addAttribute("data-name", name);
card.addAttribute("data-drive-thru", String.valueOf(hasDriveThru));
card.addAttribute("data-24h", String.valueOf(is24Hours));
card.addAttribute("data-reserve", String.valueOf(isReserve));
card.setPadding("16px");
card.setBorderBottom("1px solid #f3f4f6");
card.addStyleAttribute("cursor", "pointer");
Div header = new Div();
header.setDisplay(Display.FLEX);
header.setJustifyContent(JustifyContent.SPACE_BETWEEN);
header.setAlignItems(AlignItems.FLEX_START);
header.setMarginBottom("8px");
Span nameEl = new Span();
nameEl.setText(name);
nameEl.setColor("#1f2937");
nameEl.setFontSize("14px");
nameEl.setFontWeight("600");
header.addElement(nameEl);
Span distanceEl = new Span();
distanceEl.setText(distance);
distanceEl.setColor("#199900");
distanceEl.setFontSize("13px");
distanceEl.setFontWeight("600");
header.addElement(distanceEl);
card.addElement(header);
P addr1 = new P();
addr1.setText(address1);
addr1.setColor("#6b7280");
addr1.setFontSize("13px");
addr1.setMarginTop("0");
addr1.setMarginBottom("2px");
card.addElement(addr1);
P addr2 = new P();
addr2.setText(address2);
addr2.setColor("#6b7280");
addr2.setFontSize("13px");
addr2.setMarginTop("0");
addr2.setMarginBottom("8px");
card.addElement(addr2);
Div footer = new Div();
footer.setDisplay(Display.FLEX);
footer.setAlignItems(AlignItems.CENTER);
footer.addStyleAttribute("gap", "8px");
footer.setFlexWrap(FlexWrap.WRAP);
Span hoursEl = new Span();
hoursEl.setText(hours);
hoursEl.setColor(is24Hours ? "#059669" : "#6b7280");
hoursEl.setFontSize("12px");
footer.addElement(hoursEl);
if (hasDriveThru) footer.addElement(createBadge("Drive-Thru", "#dbeafe", "#2563eb"));
if (is24Hours) footer.addElement(createBadge("24 Hrs", "#d1fae5", "#059669"));
if (isReserve) footer.addElement(createBadge("Reserve", "#fef3c7", "#d97706"));
card.addElement(footer);
card.registerListener(this, MouseClickedEvent.class);
return card;
}
private Span createBadge(String text, String bgColor, String textColor)
{
Span badge = new Span();
badge.setText(text);
badge.setBackgroundColor(bgColor);
badge.setColor(textColor);
badge.setPadding("2px 8px");
badge.setBorderRadius("4px");
badge.setFontSize("11px");
badge.setFontWeight("600");
return badge;
}
private Div createMapSection()
{
Div mapWrapper = new Div();
mapWrapper.setBackgroundColor("#ffffff");
mapWrapper.setBorderRadius("12px");
mapWrapper.addStyleAttribute("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)");
mapWrapper.addStyleAttribute("overflow", "hidden");
map = new LfMap(37.7879, -122.4074, 14)
.setHeight("600px")
.addOpenStreetMapLayer();
map.setId(MAP_ID);
for (String[] store : STORES)
{
double lat = Double.parseDouble(store[6]);
double lng = Double.parseDouble(store[7]);
String name = store[1];
String address1 = store[2];
String address2 = store[3];
String hours = store[5];
String storeId = store[0];
String popupContent = String.format(
"<div style='min-width: 200px;'>" +
"<div style='font-weight: 600; font-size: 14px; color: #1f2937; margin-bottom: 8px;'>%s</div>" +
"<div style='color: #6b7280; font-size: 13px; line-height: 1.5;'>%s<br>%s<br><b>%s</b></div>" +
"<div style='margin-top: 12px;'>" +
"<span style='display: inline-block; background: #199900; color: white; padding: 8px 16px; " +
"border-radius: 6px; font-size: 13px; font-weight: 500;'>Get Directions</span>" +
"</div>" +
"</div>",
name, address1, address2, hours);
LfMarker marker = new LfMarker(storeId, lat, lng)
.setPopupContent(popupContent)
.setTooltip(name);
map.addMarker(marker);
}
LfCircle downtown = new LfCircle(37.7879, -122.4074, 800)
.setColor("#199900")
.setFillColor("#199900")
.setFillOpacity(0.08)
.setWeight(2)
.setOpacity(0.5);
map.addCircle(downtown);
mapWrapper.addElement(map);
return mapWrapper;
}
@Override
public void onEvent(MouseClickedEvent event)
{
// getSource() returns the element that registered the listener
Element source = event.getSource();
// Check if a store card was clicked
for (Div card : storeCards)
{
if (source == card)
{
selectStore(card);
return;
}
}
// Check if a filter tab was clicked
for (Div tab : filterTabs)
{
if (source == tab)
{
String filter = tab.getAttribute("data-filter");
applyFilter(filter);
return;
}
}
}
@Override
public void onEvent(InputChangeEvent event)
{
if (event.getSource() == searchInput)
{
String query = searchInput.getValue();
applySearch(query);
}
}
@Override
public void onEvent(InputCompleteEvent event)
{
// Required by InputListener interface - no action needed for this demo
}
private void selectStore(Div card)
{
// Update selected card styling
for (Div c : storeCards)
{
c.removeClass("selected");
}
card.addClass("selected");
String storeId = card.getId();
selectedStoreId = storeId;
// Get coordinates and pan map
double lat = Double.parseDouble(card.getAttribute("data-lat"));
double lng = Double.parseDouble(card.getAttribute("data-lng"));
map.flyTo(lat, lng, 16);
map.openMarkerPopup(storeId);
}
private void applyFilter(String filter)
{
currentFilter = filter;
// Update tab styling
for (Div tab : filterTabs)
{
String tabFilter = tab.getAttribute("data-filter");
if (tabFilter.equals(filter))
{
tab.addClass("active");
}
else
{
tab.removeClass("active");
}
}
// Filter store cards
for (Div card : storeCards)
{
boolean show = shouldShowCard(card, filter, "");
card.setDisplay(show ? Display.BLOCK : Display.NONE);
}
}
private void applySearch(String query)
{
String lowerQuery = query != null ? query.toLowerCase() : "";
for (Div card : storeCards)
{
boolean show = shouldShowCard(card, currentFilter, lowerQuery);
card.setDisplay(show ? Display.BLOCK : Display.NONE);
}
}
private boolean shouldShowCard(Div card, String filter, String searchQuery)
{
// Check filter
boolean matchesFilter = true;
if (!"all".equals(filter))
{
if ("drive-thru".equals(filter))
{
matchesFilter = "true".equals(card.getAttribute("data-drive-thru"));
}
else if ("24h".equals(filter))
{
matchesFilter = "true".equals(card.getAttribute("data-24h"));
}
else if ("reserve".equals(filter))
{
matchesFilter = "true".equals(card.getAttribute("data-reserve"));
}
}
// Check search query
boolean matchesSearch = true;
if (searchQuery != null && !searchQuery.isEmpty())
{
String name = card.getAttribute("data-name");
matchesSearch = name != null && name.toLowerCase().contains(searchQuery);
}
return matchesFilter && matchesSearch;
}
}