Floor Plan Navigation in Java: Indoor Mapping and Pathfinding

Introduction to Floor Plan Navigation

Floor plan navigation involves creating interactive digital maps of indoor spaces with capabilities for pathfinding, location tracking, and spatial analysis. Java provides excellent tools for building robust floor plan navigation systems that can power applications in healthcare, retail, logistics, and smart buildings.


System Architecture Overview

Floor Plan Navigation System
├── Frontend (Web/Mobile)
│   ├ - Map Rendering (SVG/Canvas/WebGL)
│   ├ - User Interaction
│   └ - Real-time Position Display
├── Java Backend
│   ├ - Floor Plan Parser
│   ├ - Graph-based Pathfinding
│   ├ - Spatial Database
│   ├ - REST API Controllers
│   └ - Real-time Updates (WebSocket)
└── Data Storage
├ - Floor Plan Images (PNG, SVG)
├ - Spatial Data (PostGIS)
â”” - Metadata (JSON/XML)

Core Java Implementation

1. Maven Dependencies

<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.7.0</version>
</dependency>
<!-- Spatial Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.5.0</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-spatial</artifactId>
<version>5.6.10.Final</version>
</dependency>
<!-- Graph Algorithms -->
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-core</artifactId>
<version>1.5.1</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
<!-- Image Processing -->
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.4</version>
</dependency>
</dependencies>

2. Data Models

import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import javax.persistence.*;
@Entity
@Table(name = "floors")
public class Floor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer level;
@Column(name = "image_url")
private String imageUrl;
@Column(name = "floor_dimensions")
private String dimensions; // JSON: {"width": 100, "height": 50, "scale": 0.1}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getLevel() { return level; }
public void setLevel(Integer level) { this.level = level; }
public String getImageUrl() { return imageUrl; }
public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
public String getDimensions() { return dimensions; }
public void setDimensions(String dimensions) { this.dimensions = dimensions; }
}
@Entity
@Table(name = "rooms")
public class Room {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private String type; // OFFICE, CONFERENCE_ROOM, RESTROOM, ELEVATOR, STAIRS
@Column(columnDefinition = "geometry(Polygon, 4326)")
private Polygon boundary;
@ManyToOne
@JoinColumn(name = "floor_id")
private Floor floor;
@Column(name = "room_number")
private String roomNumber;
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public Polygon getBoundary() { return boundary; }
public void setBoundary(Polygon boundary) { this.boundary = boundary; }
public Floor getFloor() { return floor; }
public void setFloor(Floor floor) { this.floor = floor; }
public String getRoomNumber() { return roomNumber; }
public void setRoomNumber(String roomNumber) { this.roomNumber = roomNumber; }
}
@Entity
@Table(name = "navigation_nodes")
public class NavigationNode {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(columnDefinition = "geometry(Point, 4326)")
private Point location;
@ManyToOne
@JoinColumn(name = "floor_id")
private Floor floor;
@Column(name = "node_type")
private String nodeType; // DOOR, ELEVATOR, STAIRS, HALLWAY, ENTRANCE
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Point getLocation() { return location; }
public void setLocation(Point location) { this.location = location; }
public Floor getFloor() { return floor; }
public void setFloor(Floor floor) { this.floor = floor; }
public String getNodeType() { return nodeType; }
public void setNodeType(String nodeType) { this.nodeType = nodeType; }
}

3. Graph-Based Pathfinding System

import org.jgrapht.Graph;
import org.jgrapht.GraphPath;
import org.jgrapht.alg.shortestpath.AStarShortestPath;
import org.jgrapht.graph.DefaultDirectedWeightedGraph;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Point;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class NavigationService {
private Map<Long, Graph<NavigationNode, DefaultWeightedEdge>> floorGraphs = new HashMap<>();
private AStarShortestPath<NavigationNode, DefaultWeightedEdge> aStar = new AStarShortestPath<>(null);
public void buildFloorGraph(List<NavigationNode> nodes, List<NavigationEdge> edges) {
Graph<NavigationNode, DefaultWeightedEdge> graph = 
new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
// Add nodes to graph
for (NavigationNode node : nodes) {
graph.addVertex(node);
}
// Add edges with weights (distance)
for (NavigationEdge edge : edges) {
NavigationNode source = findNodeById(nodes, edge.getSourceNodeId());
NavigationNode target = findNodeById(nodes, edge.getTargetNodeId());
if (source != null && target != null) {
DefaultWeightedEdge graphEdge = graph.addEdge(source, target);
double distance = calculateDistance(source.getLocation(), target.getLocation());
graph.setEdgeWeight(graphEdge, distance);
}
}
floorGraphs.put(nodes.get(0).getFloor().getId(), graph);
}
public NavigationPath findPath(Long floorId, Long startNodeId, Long endNodeId) {
Graph<NavigationNode, DefaultWeightedEdge> graph = floorGraphs.get(floorId);
if (graph == null) {
throw new IllegalArgumentException("No graph found for floor: " + floorId);
}
NavigationNode startNode = findNodeInGraph(graph, startNodeId);
NavigationNode endNode = findNodeInGraph(graph, endNodeId);
if (startNode == null || endNode == null) {
throw new IllegalArgumentException("Start or end node not found");
}
GraphPath<NavigationNode, DefaultWeightedEdge> path = 
aStar.getPath(graph, startNode, endNode);
return convertToNavigationPath(path);
}
public NavigationPath findMultiFloorPath(Point startPoint, Point endPoint) {
// Find nearest nodes on respective floors
NavigationNode startNode = findNearestNode(startPoint);
NavigationNode endNode = findNearestNode(endPoint);
if (startNode.getFloor().getId().equals(endNode.getFloor().getId())) {
// Same floor pathfinding
return findPath(startNode.getFloor().getId(), startNode.getId(), endNode.getId());
} else {
// Multi-floor pathfinding with elevator/stairs transitions
return findMultiFloorPathWithTransitions(startNode, endNode);
}
}
private double calculateDistance(Point p1, Point p2) {
Coordinate c1 = p1.getCoordinate();
Coordinate c2 = p2.getCoordinate();
return Math.sqrt(Math.pow(c1.x - c2.x, 2) + Math.pow(c1.y - c2.y, 2));
}
private NavigationNode findNodeInGraph(Graph<NavigationNode, DefaultWeightedEdge> graph, Long nodeId) {
return graph.vertexSet().stream()
.filter(node -> node.getId().equals(nodeId))
.findFirst()
.orElse(null);
}
private NavigationNode findNearestNode(Point point) {
// Implementation to find nearest navigation node to given point
// This would query your database or in-memory structure
return null; // Simplified for example
}
private NavigationPath findMultiFloorPathWithTransitions(NavigationNode start, NavigationNode end) {
// Complex implementation for multi-floor navigation
// involving elevators, stairs, and floor transitions
return new NavigationPath(); // Simplified for example
}
private NavigationPath convertToNavigationPath(GraphPath<NavigationNode, DefaultWeightedEdge> path) {
NavigationPath navigationPath = new NavigationPath();
navigationPath.setNodes(path.getVertexList());
navigationPath.setTotalDistance(path.getWeight());
navigationPath.setInstructions(generateTurnByTurnInstructions(path));
return navigationPath;
}
private List<String> generateTurnByTurnInstructions(GraphPath<NavigationNode, DefaultWeightedEdge> path) {
List<String> instructions = new ArrayList<>();
List<NavigationNode> nodes = path.getVertexList();
for (int i = 0; i < nodes.size() - 1; i++) {
NavigationNode current = nodes.get(i);
NavigationNode next = nodes.get(i + 1);
String instruction = generateInstruction(current, next, i == 0);
instructions.add(instruction);
}
instructions.add("You have reached your destination");
return instructions;
}
private String generateInstruction(NavigationNode from, NavigationNode to, boolean isStart) {
if (isStart) {
return String.format("Start from %s, head toward %s", from.getName(), to.getName());
}
// Calculate direction based on coordinates
double angle = calculateBearing(
from.getLocation().getCoordinate(),
to.getLocation().getCoordinate()
);
String direction = getDirectionFromAngle(angle);
return String.format("At %s, turn %s and continue to %s", 
from.getName(), direction, to.getName());
}
private double calculateBearing(Coordinate from, Coordinate to) {
double lat1 = Math.toRadians(from.y);
double lat2 = Math.toRadians(to.y);
double dLon = Math.toRadians(to.x - from.x);
double y = Math.sin(dLon) * Math.cos(lat2);
double x = Math.cos(lat1) * Math.sin(lat2) - 
Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
return (Math.toDegrees(Math.atan2(y, x)) + 360) % 360;
}
private String getDirectionFromAngle(double angle) {
if (angle >= 337.5 || angle < 22.5) return "north";
if (angle >= 22.5 && angle < 67.5) return "northeast";
if (angle >= 67.5 && angle < 112.5) return "east";
if (angle >= 112.5 && angle < 157.5) return "southeast";
if (angle >= 157.5 && angle < 202.5) return "south";
if (angle >= 202.5 && angle < 247.5) return "southwest";
if (angle >= 247.5 && angle < 292.5) return "west";
return "northwest";
}
}

4. Supporting Classes

public class NavigationPath {
private List<NavigationNode> nodes;
private double totalDistance;
private List<String> instructions;
private int estimatedTime; // in seconds
// Getters and setters
public List<NavigationNode> getNodes() { return nodes; }
public void setNodes(List<NavigationNode> nodes) { this.nodes = nodes; }
public double getTotalDistance() { return totalDistance; }
public void setTotalDistance(double totalDistance) { this.totalDistance = totalDistance; }
public List<String> getInstructions() { return instructions; }
public void setInstructions(List<String> instructions) { this.instructions = instructions; }
public int getEstimatedTime() { return estimatedTime; }
public void setEstimatedTime(int estimatedTime) { this.estimatedTime = estimatedTime; }
}
@Entity
@Table(name = "navigation_edges")
public class NavigationEdge {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "source_node_id")
private Long sourceNodeId;
@Column(name = "target_node_id")
private Long targetNodeId;
private Double weight;
@Column(name = "is_bidirectional")
private Boolean isBidirectional = true;
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getSourceNodeId() { return sourceNodeId; }
public void setSourceNodeId(Long sourceNodeId) { this.sourceNodeId = sourceNodeId; }
public Long getTargetNodeId() { return targetNodeId; }
public void setTargetNodeId(Long targetNodeId) { this.targetNodeId = targetNodeId; }
public Double getWeight() { return weight; }
public void setWeight(Double weight) { this.weight = weight; }
public Boolean getBidirectional() { return isBidirectional; }
public void setBidirectional(Boolean bidirectional) { isBidirectional = bidirectional; }
}

5. REST API Controllers

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
@RestController
@RequestMapping("/api/navigation")
@CrossOrigin(origins = "*")
public class NavigationController {
@Autowired
private NavigationService navigationService;
@Autowired
private RoomRepository roomRepository;
private GeometryFactory geometryFactory = new GeometryFactory();
@PostMapping("/path/coordinates")
public NavigationPath getPathByCoordinates(
@RequestParam double startX, 
@RequestParam double startY,
@RequestParam double endX, 
@RequestParam double endY,
@RequestParam Long floorId) {
Point startPoint = geometryFactory.createPoint(new Coordinate(startX, startY));
Point endPoint = geometryFactory.createPoint(new Coordinate(endX, endY));
return navigationService.findMultiFloorPath(startPoint, endPoint);
}
@PostMapping("/path/rooms")
public NavigationPath getPathBetweenRooms(
@RequestParam Long startRoomId,
@RequestParam Long endRoomId) {
Room startRoom = roomRepository.findById(startRoomId)
.orElseThrow(() -> new RuntimeException("Start room not found"));
Room endRoom = roomRepository.findById(endRoomId)
.orElseThrow(() -> new RuntimeException("End room not found"));
Point startPoint = startRoom.getBoundary().getCentroid();
Point endPoint = endRoom.getBoundary().getCentroid();
return navigationService.findMultiFloorPath(startPoint, endPoint);
}
@GetMapping("/floor/{floorId}/graph")
public Map<String, Object> getFloorGraph(@PathVariable Long floorId) {
// Return graph data for frontend visualization
return buildGraphData(floorId);
}
@GetMapping("/search/rooms")
public List<Room> searchRooms(@RequestParam String query) {
return roomRepository.findByNameContainingIgnoreCaseOrRoomNumberContainingIgnoreCase(query, query);
}
private Map<String, Object> buildGraphData(Long floorId) {
// Build graph representation for frontend
Map<String, Object> graphData = new HashMap<>();
// Implementation would extract nodes and edges from the graph
return graphData;
}
}

6. Real-time Position Tracking with WebSocket

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import java.util.concurrent.ConcurrentHashMap;
@Controller
public class PositionTrackingController {
private final SimpMessagingTemplate messagingTemplate;
private final Map<String, UserPosition> userPositions = new ConcurrentHashMap<>();
public PositionTrackingController(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
@MessageMapping("/position.update")
@SendTo("/topic/positions")
public UserPosition updatePosition(UserPosition position) {
userPositions.put(position.getUserId(), position);
return position;
}
@MessageMapping("/navigation.start")
public void startNavigation(NavigationRequest request) {
// Calculate path and send updates
NavigationPath path = navigationService.findMultiFloorPath(
request.getStartPoint(), request.getEndPoint());
messagingTemplate.convertAndSendToUser(
request.getUserId(), 
"/queue/navigation", 
path
);
}
public void broadcastPositionUpdate(String userId, UserPosition position) {
messagingTemplate.convertAndSend("/topic/positions", position);
}
}
public class UserPosition {
private String userId;
private Double x;
private Double y;
private Long floorId;
private Long timestamp;
private Double accuracy; // in meters
// Getters and setters
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public Double getX() { return x; }
public void setX(Double x) { this.x = x; }
public Double getY() { return y; }
public void setY(Double y) { this.y = y; }
public Long getFloorId() { return floorId; }
public void setFloorId(Long floorId) { this.floorId = floorId; }
public Long getTimestamp() { return timestamp; }
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
public Double getAccuracy() { return accuracy; }
public void setAccuracy(Double accuracy) { this.accuracy = accuracy; }
}

7. Floor Plan Image Processing

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
@Service
public class FloorPlanProcessor {
public FloorPlanMetadata processFloorPlanImage(String imagePath) {
try {
BufferedImage image = ImageIO.read(new File(imagePath));
FloorPlanMetadata metadata = new FloorPlanMetadata();
metadata.setWidth(image.getWidth());
metadata.setHeight(image.getHeight());
metadata.setFileSize(new File(imagePath).length());
// Detect rooms, doors, and other features using image processing
detectFeatures(image, metadata);
return metadata;
} catch (IOException e) {
throw new RuntimeException("Failed to process floor plan image", e);
}
}
private void detectFeatures(BufferedImage image, FloorPlanMetadata metadata) {
// Basic feature detection using color analysis and edge detection
// This is a simplified version - real implementation would be more complex
for (int x = 0; x < image.getWidth(); x++) {
for (int y = 0; y < image.getHeight(); y++) {
int rgb = image.getRGB(x, y);
// Analyze pixel color to detect walls, doors, etc.
analyzePixel(rgb, x, y, metadata);
}
}
}
private void analyzePixel(int rgb, int x, int y, FloorPlanMetadata metadata) {
// Implement pixel analysis logic for feature detection
// This could use machine learning models for better accuracy
}
}

Frontend Integration Example

JavaScript Client Code

class FloorPlanNavigator {
constructor(mapCanvasId) {
this.canvas = document.getElementById(mapCanvasId);
this.ctx = this.canvas.getContext('2d');
this.currentFloor = null;
this.navigationPath = null;
}
async navigateToRoom(startRoomId, endRoomId) {
try {
const response = await fetch('/api/navigation/path/rooms', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ startRoomId, endRoomId })
});
this.navigationPath = await response.json();
this.drawNavigationPath();
this.displayInstructions();
} catch (error) {
console.error('Navigation error:', error);
}
}
drawNavigationPath() {
if (!this.navigationPath) return;
this.ctx.strokeStyle = '#007bff';
this.ctx.lineWidth = 3;
this.ctx.beginPath();
this.navigationPath.nodes.forEach((node, index) => {
const point = this.convertToCanvasCoordinates(node.location);
if (index === 0) {
this.ctx.moveTo(point.x, point.y);
} else {
this.ctx.lineTo(point.x, point.y);
}
});
this.ctx.stroke();
}
displayInstructions() {
const instructionsDiv = document.getElementById('instructions');
instructionsDiv.innerHTML = this.navigationPath.instructions
.map(instruction => `<div class="instruction">${instruction}</div>`)
.join('');
}
}

Conclusion

This Java-based floor plan navigation system provides:

  • Robust pathfinding using graph algorithms
  • Multi-floor navigation with elevator and stair transitions
  • Real-time position tracking via WebSocket
  • RESTful APIs for integration with various clients
  • Spatial database support for complex queries
  • Extensible architecture for adding new features

The system can be enhanced with:

  • Machine learning for better feature detection
  • Bluetooth beacons for indoor positioning
  • Augmented Reality for visual navigation
  • Accessibility features for disabled users
  • Integration with building management systems

This architecture provides a solid foundation for building sophisticated indoor navigation systems suitable for hospitals, shopping malls, corporate campuses, and other large facilities.

Java Observability, Logging Intelligence & AI-Driven Monitoring (APM, Tracing, Logs & Anomaly Detection)

https://macronepal.com/blog/beyond-metrics-observing-serverless-and-traditional-java-applications-with-thundra-apm/
Explains using Thundra APM to observe both serverless and traditional Java applications by combining tracing, metrics, and logs into a unified observability platform for faster debugging and performance insights.

https://macronepal.com/blog/dynatrace-oneagent-in-java-2/
Explains Dynatrace OneAgent for Java, which automatically instruments JVM applications to capture metrics, traces, and logs, enabling full-stack monitoring and root-cause analysis with minimal configuration.

https://macronepal.com/blog/lightstep-java-sdk-distributed-tracing-and-observability-implementation/
Explains Lightstep Java SDK for distributed tracing, helping developers track requests across microservices and identify latency issues using OpenTelemetry-based observability.

https://macronepal.com/blog/honeycomb-io-beeline-for-java-complete-guide-2/
Explains Honeycomb Beeline for Java, which provides high-cardinality observability and deep query capabilities to understand complex system behavior and debug distributed systems efficiently.

https://macronepal.com/blog/lumigo-for-serverless-in-java-complete-distributed-tracing-guide-2/
Explains Lumigo for Java serverless applications, offering automatic distributed tracing, log correlation, and error tracking to simplify debugging in cloud-native environments. (Lumigo Docs)

https://macronepal.com/blog/from-noise-to-signals-implementing-log-anomaly-detection-in-java-applications/
Explains how to detect anomalies in Java logs using behavioral patterns and machine learning techniques to separate meaningful incidents from noisy log data and improve incident response.

https://macronepal.com/blog/ai-powered-log-analysis-in-java-from-reactive-debugging-to-proactive-insights/
Explains AI-driven log analysis for Java applications, shifting from manual debugging to predictive insights that identify issues early and improve system reliability using intelligent log processing.

https://macronepal.com/blog/titliel-java-logging-best-practices/
Explains best practices for Java logging, focusing on structured logs, proper log levels, performance optimization, and ensuring logs are useful for debugging and observability systems.

https://macronepal.com/blog/seeking-a-loguru-for-java-the-quest-for-elegant-and-simple-logging/
Explains the search for simpler, more elegant logging frameworks in Java, comparing modern logging approaches that aim to reduce complexity while improving readability and developer experience.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper