Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
sektorsim
CityGML Server
Commits
d7d412aa
Commit
d7d412aa
authored
Jul 16, 2026
by
Matthias Betz
Browse files
initial release
parent
025a7b17
Changes
33
Show whitespace changes
Inline
Side-by-side
src/main/java/de/hft/stuttgart/sektorsim/server/wfs/Query.java
0 → 100644
View file @
d7d412aa
package
de.hft.stuttgart.sektorsim.server.wfs
;
import
jakarta.xml.bind.annotation.XmlAttribute
;
import
jakarta.xml.bind.annotation.XmlElement
;
public
class
Query
{
@XmlAttribute
private
String
typeNames
;
@XmlAttribute
private
String
handle
;
@XmlElement
(
name
=
"Filter"
,
namespace
=
"http://www.opengis.net/fes/2.0"
)
private
Filter
filter
;
public
String
getTypeNames
()
{
return
typeNames
;
}
public
void
setTypeNames
(
String
typeNames
)
{
this
.
typeNames
=
typeNames
;
}
public
String
getHandle
()
{
return
handle
;
}
public
void
setHandle
(
String
handle
)
{
this
.
handle
=
handle
;
}
public
Filter
getFilter
()
{
return
filter
;
}
public
void
setFilter
(
Filter
filter
)
{
this
.
filter
=
filter
;
}
}
src/main/java/de/hft/stuttgart/sektorsim/server/wfs/Vector2d.java
0 → 100644
View file @
d7d412aa
package
de.hft.stuttgart.sektorsim.server.wfs
;
public
class
Vector2d
{
private
double
x
;
private
double
y
;
public
Vector2d
(
double
x
,
double
y
)
{
this
.
x
=
x
;
this
.
y
=
y
;
}
public
double
getX
()
{
return
x
;
}
public
double
getY
()
{
return
y
;
}
}
src/main/java/de/hft/stuttgart/sektorsim/server/wfs/converter/PolygonAdapter.java
0 → 100644
View file @
d7d412aa
package
de.hft.stuttgart.sektorsim.server.wfs.converter
;
import
java.util.ArrayList
;
import
java.util.List
;
import
org.locationtech.jts.geom.Coordinate
;
import
org.locationtech.jts.geom.GeometryFactory
;
import
org.locationtech.jts.geom.Polygon
;
import
de.hft.stuttgart.sektorsim.server.wfs.ExteriorXml
;
import
de.hft.stuttgart.sektorsim.server.wfs.LinearRingValue
;
import
de.hft.stuttgart.sektorsim.server.wfs.PolygonAttributes
;
import
de.hft.stuttgart.sektorsim.server.wfs.PolygonValue
;
import
de.hft.stuttgart.sektorsim.server.wfs.Vector2d
;
import
jakarta.xml.bind.annotation.adapters.XmlAdapter
;
public
class
PolygonAdapter
extends
XmlAdapter
<
PolygonValue
,
Polygon
>
{
private
static
final
GeometryFactory
FACTORY
=
new
GeometryFactory
();
@Override
public
Polygon
unmarshal
(
PolygonValue
polyValue
)
throws
Exception
{
// convert vectors to coordinates and set as exterior ring
return
FACTORY
.
createPolygon
(
polyValue
.
getExterior
().
getLinearRing
().
getPosList
().
stream
().
map
(
v
->
new
Coordinate
(
v
.
getX
(),
v
.
getY
()))
.
toArray
(
size
->
new
Coordinate
[
size
]));
}
@Override
public
PolygonValue
marshal
(
Polygon
polygon
)
throws
Exception
{
PolygonValue
polyValue
=
new
PolygonValue
();
if
(
polygon
.
getUserData
()
!=
null
)
{
Object
userData
=
polygon
.
getUserData
();
PolygonAttributes
attributes
=
(
PolygonAttributes
)
userData
;
polyValue
.
setSrsName
(
attributes
.
getSrsName
());
}
ExteriorXml
exterior
=
new
ExteriorXml
();
LinearRingValue
linearRingValue
=
new
LinearRingValue
();
exterior
.
setLinearRing
(
linearRingValue
);
polyValue
.
setExterior
(
exterior
);
List
<
Vector2d
>
posList
=
new
ArrayList
<>();
for
(
Coordinate
coordinate
:
polygon
.
getExteriorRing
().
getCoordinates
())
{
posList
.
add
(
new
Vector2d
(
coordinate
.
x
,
coordinate
.
y
));
}
linearRingValue
.
setPosList
(
posList
);
return
polyValue
;
}
}
src/main/java/de/hft/stuttgart/sektorsim/server/wfs/converter/PosListAdapter.java
0 → 100644
View file @
d7d412aa
package
de.hft.stuttgart.sektorsim.server.wfs.converter
;
import
java.util.ArrayList
;
import
java.util.List
;
import
java.util.StringJoiner
;
import
de.hft.stuttgart.sektorsim.server.wfs.Vector2d
;
import
jakarta.xml.bind.annotation.adapters.XmlAdapter
;
public
class
PosListAdapter
extends
XmlAdapter
<
String
,
List
<
Vector2d
>>
{
@Override
public
List
<
Vector2d
>
unmarshal
(
String
v
)
throws
Exception
{
if
(
v
==
null
||
v
.
isEmpty
())
{
return
new
ArrayList
<>();
}
String
[]
coordinates
=
v
.
split
(
"\s+"
);
List
<
Vector2d
>
vectorList
=
new
ArrayList
<>();
for
(
int
i
=
0
;
i
<
coordinates
.
length
;
i
+=
2
)
{
double
x
=
Double
.
parseDouble
(
coordinates
[
i
]);
double
y
=
Double
.
parseDouble
(
coordinates
[
i
+
1
]);
vectorList
.
add
(
new
Vector2d
(
x
,
y
));
}
return
vectorList
;
}
@Override
public
String
marshal
(
List
<
Vector2d
>
v
)
throws
Exception
{
return
v
.
stream
().
collect
(()
->
new
StringJoiner
(
" "
),
(
sb
,
vector
)
->
{
sb
.
add
(
Double
.
toString
(
vector
.
getX
())).
add
(
Double
.
toString
(
vector
.
getY
()));
},
(
sj1
,
sj2
)
->
{
sj1
.
add
(
sj2
.
toString
());
}).
toString
();
}
}
src/main/java/de/hft/stuttgart/sektorsim/server/wfs/package-info.java
0 → 100644
View file @
d7d412aa
@jakarta
.
xml
.
bind
.
annotation
.
XmlSchema
(
namespace
=
"http://www.opengis.net/wfs/2.0"
,
elementFormDefault
=
jakarta
.
xml
.
bind
.
annotation
.
XmlNsForm
.
QUALIFIED
,
xmlns
=
{
@XmlNs
(
prefix
=
"gml"
,
namespaceURI
=
"http://www.opengis.net/gml/3.2"
),
@XmlNs
(
prefix
=
"fes"
,
namespaceURI
=
"http://www.opengis.net/fes/2.0"
),
@XmlNs
(
prefix
=
""
,
namespaceURI
=
"http://www.opengis.net/wfs/2.0"
),
@XmlNs
(
prefix
=
"bldg"
,
namespaceURI
=
"http://www.opengis.net/citygml/building/2.0"
)
})
@XmlAccessorType
(
XmlAccessType
.
FIELD
)
package
de.hft.stuttgart.sektorsim.server.wfs
;
import
jakarta.xml.bind.annotation.XmlAccessType
;
import
jakarta.xml.bind.annotation.XmlAccessorType
;
import
jakarta.xml.bind.annotation.XmlNs
;
src/main/resources/application-dev.properties
0 → 100644
View file @
d7d412aa
server.port
=
8110
logging.file.name
=
store-dev.log
logging.level.root
=
info
logging.logback.rollingpolicy.file-name-pattern
=
store-dev-%d{yyyy-MM-dd}.%i.gz
logging.logback.rollingpolicy.max-file-size
=
10MB
wfs.url
=
http://localhost:8080/wfs
conn.databases[0]
.year
=
2019
conn.databases[0]
.host
=
${DB0_HOST:localhost}
conn.databases[0]
.port
=
${DB0_PORT:5432}
conn.databases[0]
.user
=
${DB0_USER:postgres}
conn.databases[0]
.password
=
${DB0_PASSWORD}
conn.databases[0]
.database
=
postgres
conn.databases[0]
.schema
=
citydb
\ No newline at end of file
src/main/resources/application-docker.properties
0 → 100644
View file @
d7d412aa
server.port
=
80
DB_PORT
=
logging.file.name
=
store-docker.log
logging.level.root
=
info
logging.logback.rollingpolicy.file-name-pattern
=
store-docker-%d{yyyy-MM-dd}.%i.gz
logging.logback.rollingpolicy.max-file-size
=
10MB
spring.mvc.async.request-timeout
=
300000
conn.databases[0]
.year
=
2019
conn.databases[0]
.host
=
citygmlstore-citydb5
conn.databases[0]
.port
=
5432
conn.databases[0]
.user
=
${DB0_USER:postgres}
conn.databases[0]
.password
=
${DB0_PASSWORD}
conn.databases[0]
.database
=
postgres
conn.databases[0]
.schema
=
citydb
conn.databases[1]
.year
=
2025
conn.databases[1]
.host
=
citygmlstore-citydb5_2025
conn.databases[1]
.port
=
5432
conn.databases[1]
.user
=
${DB1_USER:postgres}
conn.databases[1]
.password
=
${DB1_PASSWORD}
conn.databases[1]
.database
=
postgres
conn.databases[1]
.schema
=
citydb
\ No newline at end of file
src/main/resources/application.properties
0 → 100644
View file @
d7d412aa
spring.profiles.active
=
${SPRING_PROFILES_ACTIVE:dev}
spring.config.import
=
optional:file:.env[.properties]
\ No newline at end of file
src/main/resources/static/index.html
0 → 100644
View file @
d7d412aa
<!DOCTYPE html>
<html
lang=
"en"
>
<head>
<meta
charset=
"utf-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
>
<title>
Leaflet Polygon Selector (Background Download)
</title>
<link
rel=
"stylesheet"
href=
"https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity=
"sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=
""
/>
<link
rel=
"stylesheet"
href=
"https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css"
/>
<script
src=
"https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity=
"sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=
""
></script>
<script
src=
"https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"
></script>
<style>
/* Basic CSS to make the map fullscreen */
html
,
body
{
height
:
100%
;
margin
:
0
;
padding
:
0
;
overflow
:
hidden
;
/* Prevent scrollbars on body */
}
#map
{
width
:
100%
;
height
:
100%
;
}
/* Style for the WKT output box */
#wkt-output
{
position
:
absolute
;
bottom
:
70px
;
/* Position above the download button */
right
:
10px
;
background
:
rgba
(
255
,
255
,
255
,
0.85
);
padding
:
5px
10px
;
border
:
1px
solid
#ccc
;
border-radius
:
4px
;
z-index
:
1000
;
/* Ensure it's above the map */
font-family
:
monospace
;
font-size
:
0.8em
;
max-width
:
300px
;
max-height
:
150px
;
overflow-y
:
auto
;
word-wrap
:
break-word
;
display
:
none
;
/* Hidden by default */
}
/* Simple instruction text */
#instructions
{
position
:
absolute
;
top
:
10px
;
left
:
50px
;
/* Position relative to default zoom controls */
background
:
rgba
(
255
,
255
,
255
,
0.85
);
padding
:
5px
10px
;
border
:
1px
solid
#ccc
;
border-radius
:
4px
;
z-index
:
1000
;
font-family
:
sans-serif
;
font-size
:
0.9em
;
}
/* Style for the Download button */
#download-button
{
position
:
absolute
;
bottom
:
30px
;
right
:
10px
;
z-index
:
1000
;
padding
:
8px
15px
;
cursor
:
pointer
;
background-color
:
#4CAF50
;
/* Green */
color
:
white
;
border
:
none
;
border-radius
:
4px
;
font-size
:
1em
;
display
:
none
;
/* Hidden by default */
}
#download-button
:hover
{
background-color
:
#45a049
;
}
#download-button
:disabled
{
background-color
:
#cccccc
;
cursor
:
not-allowed
;
}
/* Style for the Clear button */
#clear-button
{
position
:
absolute
;
bottom
:
30px
;
right
:
160px
;
/* Adjusted position */
z-index
:
1000
;
padding
:
8px
15px
;
cursor
:
pointer
;
background-color
:
#f44336
;
/* Red */
color
:
white
;
border
:
none
;
border-radius
:
4px
;
font-size
:
1em
;
display
:
none
;
/* Hidden by default, show while drawing or finished*/
}
#clear-button
:hover
{
background-color
:
#da190b
;
}
/* Ensure geocoder input is usable */
.leaflet-control-geocoder
{
z-index
:
1001
!important
;
/* Ensure search is above other elements */
}
.leaflet-control-geocoder-form
input
{
min-width
:
200px
;
/* Make search input wider */
}
</style>
</head>
<body>
<div
id=
"map"
></div>
<div
id=
"instructions"
>
Click map to start drawing polygon.
</div>
<div
id=
"wkt-output"
></div>
<button
id=
"download-button"
>
Download Data
</button>
<button
id=
"clear-button"
>
Clear Polygon
</button>
<script>
// Wait for the DOM to be fully loaded
document
.
addEventListener
(
'
DOMContentLoaded
'
,
function
()
{
// --- Configuration ---
const
SERVER_BASE_URL
=
"
https://citygml.hft-stuttgart.de/citygml
"
;
// Define server URL here
const
DEFAULT_FILENAME
=
"
data.citygml
"
;
// Fallback filename for download
// --- Initialize Map ---
const
map
=
L
.
map
(
'
map
'
,
{
keyboard
:
true
}).
fitBounds
([[
47.53564
,
7.52587
],
[
49.78976
,
10.4856
]]);
// Center on BW
// }).setView([51.16, 10.45], 6); // Center on Germany
// [[47.53564, 7.52587], [49.78976, 10.4856]]
L
.
tileLayer
(
'
https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
'
,
{
attribution
:
'
© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors
'
}).
addTo
(
map
);
// --- Add Geocoder (Search) Control ---
L
.
Control
.
geocoder
({
defaultMarkGeocode
:
true
}).
addTo
(
map
);
// --- State Variables ---
let
drawingEnabled
=
true
;
let
points
=
[];
let
tempMarkers
=
[];
let
tempPolyline
=
null
;
let
finalPolygon
=
null
;
let
firstMarker
=
null
;
let
currentWktString
=
null
;
// Store WKT for download button
// --- DOM Element References ---
const
wktOutputDiv
=
document
.
getElementById
(
'
wkt-output
'
);
const
downloadButton
=
document
.
getElementById
(
'
download-button
'
);
const
clearButton
=
document
.
getElementById
(
'
clear-button
'
);
const
instructionsDiv
=
document
.
getElementById
(
'
instructions
'
);
// --- Helper Functions ---
function
getFilenameFromHeader
(
header
)
{
if
(
!
header
)
return
null
;
// Simple regex for filename="..."; handles quotes, case-insensitive
const
match
=
header
.
match
(
/filename
\s
*=
\s
*"
?([^
"
]
+
)
"
?
/i
);
return
match
&&
match
[
1
]
?
match
[
1
]
:
null
;
}
function
updateInstructions
()
{
if
(
drawingEnabled
)
{
if
(
points
.
length
===
0
)
{
instructionsDiv
.
textContent
=
'
Click map to start drawing polygon.
'
;
}
else
if
(
points
.
length
<
3
)
{
instructionsDiv
.
textContent
=
`Points:
${
points
.
length
}
. Add
${
3
-
points
.
length
}
more point(s). Click first marker to close (min 3 points).`
;
}
else
{
instructionsDiv
.
textContent
=
`Points:
${
points
.
length
}
. Click first marker or add more points.`
;
}
}
else
{
instructionsDiv
.
textContent
=
'
Polygon finalized. Click Download or Clear.
'
;
}
}
function
resetDrawing
()
{
drawingEnabled
=
true
;
points
=
[];
currentWktString
=
null
;
tempMarkers
.
forEach
(
marker
=>
map
.
removeLayer
(
marker
));
tempMarkers
=
[];
if
(
firstMarker
)
{
firstMarker
.
off
(
'
click
'
);
// Remove specific listener
firstMarker
=
null
;
}
if
(
tempPolyline
)
{
map
.
removeLayer
(
tempPolyline
);
tempPolyline
=
null
;
}
if
(
finalPolygon
)
{
map
.
removeLayer
(
finalPolygon
);
finalPolygon
=
null
;
}
wktOutputDiv
.
style
.
display
=
'
none
'
;
wktOutputDiv
.
textContent
=
''
;
downloadButton
.
style
.
display
=
'
none
'
;
downloadButton
.
disabled
=
false
;
// Re-enable button
downloadButton
.
textContent
=
'
Download Data
'
;
// Reset button text
clearButton
.
style
.
display
=
'
none
'
;
updateInstructions
();
console
.
log
(
"
Drawing reset.
"
);
}
function
finalizePolygon
()
{
if
(
points
.
length
<
3
)
return
;
console
.
log
(
"
Finalizing polygon...
"
);
drawingEnabled
=
false
;
// Remove temporary items
tempMarkers
.
forEach
(
marker
=>
map
.
removeLayer
(
marker
));
tempMarkers
=
[];
if
(
tempPolyline
)
{
map
.
removeLayer
(
tempPolyline
);
tempPolyline
=
null
;
}
if
(
firstMarker
)
{
firstMarker
.
off
(
'
click
'
);
firstMarker
=
null
;
// Ref only needed during drawing
}
if
(
finalPolygon
)
map
.
removeLayer
(
finalPolygon
);
finalPolygon
=
L
.
polygon
(
points
,
{
color
:
'
blue
'
}).
addTo
(
map
);
// Generate and store WKT
let
wktCoords
=
points
.
map
(
p
=>
`
${
p
.
lng
}
${
p
.
lat
}
`
);
wktCoords
.
push
(
`
${
points
[
0
].
lng
}
${
points
[
0
].
lat
}
`
);
// Close ring
currentWktString
=
`POLYGON((
${
wktCoords
.
join
(
'
,
'
)}
))`
;
// Store for download
// Display WKT
wktOutputDiv
.
textContent
=
`WKT (WGS84):\n
${
currentWktString
}
`
;
wktOutputDiv
.
style
.
display
=
'
block
'
;
// Show buttons
downloadButton
.
style
.
display
=
'
block
'
;
clearButton
.
style
.
display
=
'
block
'
;
updateInstructions
();
}
// --- Event Listeners ---
// Map Click: Add points
map
.
on
(
'
click
'
,
function
(
e
)
{
if
(
!
drawingEnabled
)
return
;
const
latlng
=
e
.
latlng
;
points
.
push
(
latlng
);
if
(
points
.
length
>
0
)
{
clearButton
.
style
.
display
=
'
block
'
;
}
const
newMarker
=
L
.
marker
(
latlng
).
addTo
(
map
);
tempMarkers
.
push
(
newMarker
);
if
(
points
.
length
===
1
)
{
firstMarker
=
newMarker
;
// Store reference to the first marker
// Add the specific click listener for closing
firstMarker
.
on
(
'
click
'
,
function
(
ev
)
{
L
.
DomEvent
.
stopPropagation
(
ev
);
// Prevent map click
if
(
drawingEnabled
&&
points
.
length
>=
3
)
{
finalizePolygon
();
}
else
if
(
drawingEnabled
)
{
console
.
log
(
"
Click on first marker, but not enough points (<3).
"
);
// Optional: Provide user feedback here
}
});
}
if
(
tempPolyline
)
map
.
removeLayer
(
tempPolyline
);
if
(
points
.
length
>
1
)
{
tempPolyline
=
L
.
polyline
(
points
,
{
color
:
'
red
'
,
weight
:
2
,
dashArray
:
'
5, 5
'
}).
addTo
(
map
);
}
updateInstructions
();
});
// Download Button Click: Fetch data and trigger download
downloadButton
.
addEventListener
(
'
click
'
,
async
function
()
{
if
(
!
currentWktString
)
{
console
.
error
(
"
Download clicked, but no WKT available.
"
);
return
;
}
const
encodedWkt
=
encodeURIComponent
(
currentWktString
);
// Correctly escapes () etc.
const
fullUrl
=
`
${
SERVER_BASE_URL
}
?wktPolygon=
${
encodedWkt
}
`
;
console
.
log
(
"
Requesting data from:
"
,
fullUrl
);
downloadButton
.
disabled
=
true
;
downloadButton
.
textContent
=
'
Downloading...
'
;
try
{
const
response
=
await
fetch
(
fullUrl
);
if
(
!
response
.
ok
)
{
// Handle HTTP errors (e.g., 404, 500)
throw
new
Error
(
`HTTP error! status:
${
response
.
status
}
${
response
.
statusText
}
`
);
}
// Get data as a Blob
const
blob
=
await
response
.
blob
();
// Determine filename
const
contentDisposition
=
response
.
headers
.
get
(
'
Content-Disposition
'
);
const
filename
=
getFilenameFromHeader
(
contentDisposition
)
||
DEFAULT_FILENAME
;
// Create a temporary link to trigger download
const
tempLink
=
document
.
createElement
(
'
a
'
);
const
objectUrl
=
URL
.
createObjectURL
(
blob
);
tempLink
.
href
=
objectUrl
;
tempLink
.
download
=
filename
;
// Set the desired filename
document
.
body
.
appendChild
(
tempLink
);
// Append necessary for Firefox
tempLink
.
click
();
// Simulate click
// Cleanup
document
.
body
.
removeChild
(
tempLink
);
URL
.
revokeObjectURL
(
objectUrl
);
console
.
log
(
`Download triggered for file:
${
filename
}
`
);
downloadButton
.
textContent
=
'
Download Complete
'
;
// Give feedback
}
catch
(
error
)
{
console
.
error
(
"
Download failed:
"
,
error
);
alert
(
`Failed to download data:
${
error
.
message
}
`
);
// Notify user
downloadButton
.
textContent
=
'
Download Failed
'
;
// Give feedback
}
finally
{
// Re-enable button after a delay, or keep it as 'Complete'/'Failed'
// For simplicity, let's re-enable and reset text on clear/new polygon.
// If keeping status, disable clear until download complete/failed?
// Let's just allow clear anytime.
// Reset button state after a small delay to show status
setTimeout
(()
=>
{
if
(
!
drawingEnabled
)
{
// Only reset if still in finalized state
downloadButton
.
disabled
=
false
;
downloadButton
.
textContent
=
'
Download Data
'
;
}
},
3000
);
// Reset text after 3 seconds
}
});
// Clear Button Click
clearButton
.
addEventListener
(
'
click
'
,
function
()
{
console
.
log
(
"
Clear button clicked.
"
);
resetDrawing
();
});
// Optional: Reset on double-click (if not drawing)
map
.
on
(
'
dblclick
'
,
function
(
e
)
{
if
(
!
drawingEnabled
)
{
resetDrawing
();
}
});
// --- Initial State ---
updateInstructions
();
});
// End DOMContentLoaded
</script>
</body>
</html>
\ No newline at end of file
src/test/java/de/hft/stuttgart/sektorsim/server/ExtractIDList.java
0 → 100644
View file @
d7d412aa
package
de.hft.stuttgart.sektorsim.server
;
import
java.io.ByteArrayInputStream
;
import
java.io.ByteArrayOutputStream
;
import
java.io.FileInputStream
;
import
java.io.IOException
;
import
java.nio.file.Files
;
import
java.nio.file.Path
;
import
java.nio.file.StandardOpenOption
;
import
java.util.ArrayList
;
import
java.util.List
;
import
java.util.zip.ZipEntry
;
import
java.util.zip.ZipInputStream
;
import
org.citygml4j.core.model.core.AbstractCityObject
;
import
org.citygml4j.core.model.core.AbstractFeature
;
import
org.citygml4j.core.model.core.CityModel
;
import
org.citygml4j.xml.CityGMLContext
;
import
org.citygml4j.xml.CityGMLContextException
;
import
org.citygml4j.xml.reader.CityGMLInputFactory
;
import
org.citygml4j.xml.reader.CityGMLReadException
;
import
org.citygml4j.xml.reader.CityGMLReader
;
public
class
ExtractIDList
{
public
static
void
main
(
String
[]
args
)
throws
CityGMLReadException
,
CityGMLContextException
{
String
zipFilePath
=
"part_1131.zip"
;
Path
output
=
Path
.
of
(
"deleteList_1131.txt"
);
try
(
ZipInputStream
zis
=
new
ZipInputStream
(
new
FileInputStream
(
zipFilePath
)))
{
ZipEntry
entry
;
List
<
String
>
ids
=
new
ArrayList
<>();
ids
.
add
(
"GMLID"
);
CityGMLContext
context
=
CityGMLContext
.
newInstance
();
CityGMLInputFactory
in
=
context
.
createCityGMLInputFactory
();
while
((
entry
=
zis
.
getNextEntry
())
!=
null
)
{
System
.
out
.
println
(
"Entry: "
+
entry
.
getName
());
if
(!
entry
.
isDirectory
())
{
ByteArrayOutputStream
baos
=
new
ByteArrayOutputStream
();
byte
[]
buffer
=
new
byte
[
4096
];
int
len
;
while
((
len
=
zis
.
read
(
buffer
))
!=
-
1
)
{
baos
.
write
(
buffer
,
0
,
len
);
}
// Create a new input stream from the entry's content
ByteArrayInputStream
bais
=
new
ByteArrayInputStream
(
baos
.
toByteArray
());
try
(
CityGMLReader
reader
=
in
.
createCityGMLReader
(
bais
))
{
while
(
reader
.
hasNext
())
{
AbstractFeature
citygml
=
reader
.
next
();
if
(
citygml
instanceof
CityModel
cityModel
)
{
cityModel
.
getCityObjectMembers
().
forEach
(
prop
->
{
AbstractCityObject
aco
=
prop
.
getObject
();
ids
.
add
(
aco
.
getId
());
});
}
}
}
}
zis
.
closeEntry
();
}
Files
.
write
(
output
,
ids
,
StandardOpenOption
.
CREATE
,
StandardOpenOption
.
TRUNCATE_EXISTING
);
}
catch
(
IOException
e
)
{
e
.
printStackTrace
();
}
}
}
src/test/java/de/hft/stuttgart/sektorsim/server/controller/CityGMLServerControllerTest.java
0 → 100644
View file @
d7d412aa
package
de.hft.stuttgart.sektorsim.server.controller
;
import
static
org
.
junit
.
jupiter
.
api
.
Assertions
.
assertEquals
;
import
java.io.ByteArrayOutputStream
;
import
java.io.IOException
;
import
java.util.Collections
;
import
org.junit.jupiter.api.Test
;
import
org.locationtech.jts.io.ParseException
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.boot.test.context.SpringBootTest
;
import
org.springframework.http.HttpStatusCode
;
import
org.springframework.http.ResponseEntity
;
import
org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
;
@SpringBootTest
class
CityGMLServerControllerTest
{
@Autowired
CityGMLServerController
controller
;
@Test
void
testGetCityGML
()
throws
ParseException
,
IOException
{
String
wktString
=
"POLYGON ((9.872730080693117 48.58178884030471,"
+
" 9.873389990085997 48.58267166044939,"
+
" 9.874665814913612 48.58201197212753,"
+
" 9.874211210664527 48.583767888178585,"
+
" 9.871630231703818 48.583428351158176,"
+
" 9.872730080693117 48.58178884030471))"
;
// https://owsproxy.lgl-bw.de/owsproxy/wfs/WFS_INSP_BW_Gebauede_3D_LoD2?
// CityGMLServerController controller = new CityGMLServerController("https://owsproxy.lgl-bw.de/owsproxy/wfs/WFS_INSP_BW_Gebauede_3D_LoD2");
// CityGMLServerController controller = new CityGMLServerController(new CityDB5Connector());
ResponseEntity
<
StreamingResponseBody
>
cityGML
=
controller
.
getCityGML
(
wktString
,
Collections
.
emptyList
(),
2019
);
HttpStatusCode
statusCode
=
cityGML
.
getStatusCode
();
assertEquals
(
HttpStatusCode
.
valueOf
(
200
),
statusCode
);
ByteArrayOutputStream
baos
=
new
ByteArrayOutputStream
();
cityGML
.
getBody
().
writeTo
(
baos
);
byte
[]
byteArray
=
baos
.
toByteArray
();
assertEquals
(
1278220
,
byteArray
.
length
);
}
}
src/test/java/de/hft/stuttgart/sektorsim/server/wfs/GetFeatureTest.java
0 → 100644
View file @
d7d412aa
package
de.hft.stuttgart.sektorsim.server.wfs
;
import
static
org
.
junit
.
jupiter
.
api
.
Assertions
.
assertEquals
;
import
java.io.ByteArrayInputStream
;
import
java.io.ByteArrayOutputStream
;
import
java.io.IOException
;
import
java.net.URI
;
import
java.net.URISyntaxException
;
import
java.net.http.HttpClient
;
import
java.net.http.HttpRequest
;
import
java.net.http.HttpRequest.BodyPublishers
;
import
java.net.http.HttpResponse
;
import
java.nio.charset.StandardCharsets
;
import
org.junit.jupiter.api.Test
;
import
org.locationtech.jts.geom.Geometry
;
import
org.locationtech.jts.geom.GeometryFactory
;
import
org.locationtech.jts.geom.Polygon
;
import
org.locationtech.jts.io.ParseException
;
import
org.locationtech.jts.io.WKTReader
;
import
jakarta.xml.bind.JAXBException
;
class
GetFeatureTest
{
@Test
void
testWriteTo
()
throws
JAXBException
,
IOException
,
ParseException
,
URISyntaxException
,
InterruptedException
{
GetFeatureRequest
getFeature
=
new
GetFeatureRequest
();
getFeature
.
setService
(
"WFS"
);
getFeature
.
setVersion
(
"2.0.0"
);
getFeature
.
setHandle
(
"GetFeatureRequest"
);
Query
query
=
new
Query
();
query
.
setTypeNames
(
"bldg:Building"
);
query
.
setHandle
(
"Q01"
);
getFeature
.
setQuery
(
query
);
Filter
filter
=
new
Filter
();
query
.
setFilter
(
filter
);
Intersects
intersects
=
new
Intersects
();
filter
.
setIntersects
(
intersects
);
intersects
.
setValueReference
(
"gml:boundedBy"
);
GeometryFactory
factory
=
new
GeometryFactory
();
WKTReader
wktReader
=
new
WKTReader
(
factory
);
Geometry
geometry
=
wktReader
.
read
(
"POLYGON ((9.872730080693117 48.58178884030471,\r\n"
+
" 9.873389990085997 48.58267166044939,\r\n"
+
" 9.874665814913612 48.58201197212753,\r\n"
+
" 9.874211210664527 48.583767888178585,\r\n"
+
" 9.871630231703818 48.583428351158176,\r\n"
+
" 9.872730080693117 48.58178884030471))"
);
if
(!(
geometry
instanceof
Polygon
polygon
))
{
throw
new
IllegalArgumentException
(
"Geometry is not a polygon"
);
}
polygon
.
setUserData
(
new
PolygonAttributes
(
"id_poly"
,
"EPSG:4326"
));
intersects
.
setPolygon
(
polygon
);
ByteArrayOutputStream
outputStream
=
new
ByteArrayOutputStream
();
getFeature
.
writeTo
(
outputStream
);
outputStream
.
flush
();
outputStream
.
close
();
String
xmlOutput
=
outputStream
.
toString
();
HttpClient
client
=
HttpClient
.
newHttpClient
();
HttpRequest
request
=
HttpRequest
.
newBuilder
()
.
uri
(
new
URI
(
"http://localhost:8080/wfs"
))
.
header
(
"Content-Type"
,
"application/xml"
)
.
POST
(
BodyPublishers
.
ofString
(
xmlOutput
))
.
build
();
HttpResponse
<
String
>
response
=
client
.
send
(
request
,
HttpResponse
.
BodyHandlers
.
ofString
());
System
.
out
.
println
(
"Status code: "
+
response
.
statusCode
());
System
.
out
.
println
(
"Response: "
+
response
.
body
());
}
@Test
void
parseXml
()
throws
JAXBException
{
String
xml
=
"<?xml version=\"1.0\" ?>\r\n"
+
"<GetFeature\r\n"
+
"version=\"2.0.0\"\r\n"
+
"service=\"WFS\"\r\n"
+
"handle=\"Example Query\"\r\n"
+
"xmlns=\"http://www.opengis.net/wfs/2.0\"\r\n"
+
"xmlns:fes=\"http://www.opengis.net/fes/2.0\"\r\n"
+
"xmlns:gml=\"http://www.opengis.net/gml/3.2\"\r\n"
+
"xmlns:myns=\"http://www.someserver.com/myns\"\r\n"
+
"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n"
+
"xsi:schemaLocation=\"http://www.opengis.net/wfs/2.0\r\n"
+
"http://schemas.opengis.net/wfs/2.0.0/wfs.xsd\r\n"
+
"http://www.opengis.net/gml/3.2\r\n"
+
"http://schemas.opengis.net/gml/3.2.1/gml.xsd\">\r\n"
+
"<Query typeNames=\"myns:Roads\" handle=\"Q01\">\r\n"
+
"<fes:Filter>\r\n"
+
"<fes:Intersects>\r\n"
+
"<fes:ValueReference>myns:path</fes:ValueReference>\r\n"
+
"<gml:Polygon srsName=\"urn:ogc:def:crs:EPSG::4326\" gml:id=\"P1\">\r\n"
+
"<gml:exterior>\r\n"
+
"<gml:LinearRing>\r\n"
+
"<gml:posList>-19.06099128723145 -169.9416961669922 -19.0565"
+
"3190612793 -169.9346008300781 -19.0523681640625 -169.9278564453125 -19.047290802"
+
"00195 -169.9230346679688 -19.03918266296387 -169.9215698242188 -19.0405883789062"
+
"5 -169.9138641357422 -19.04656600952148 -169.9136047363281 -19.05992698669434 -1"
+
"69.9196014404297 -19.06432342529297 -169.9275665283203 -19.06826400756836 -169.9"
+
"364929199219 -19.06099128723145 -169.9416961669922</gml:posList>\r\n"
+
"</gml:LinearRing>\r\n"
+
"</gml:exterior>\r\n"
+
"</gml:Polygon>\r\n"
+
"</fes:Intersects>\r\n"
+
"</fes:Filter>\r\n"
+
"</Query>\r\n"
+
"</GetFeature>"
;
GetFeatureRequest
getFeature
=
GetFeatureRequest
.
parse
(
new
ByteArrayInputStream
(
xml
.
getBytes
(
StandardCharsets
.
UTF_8
)));
assertEquals
(
"POLYGON ((-19.06099128723145 -169.9416961669922, -19.05653190612793"
+
" -169.9346008300781, -19.0523681640625 -169.9278564453125, -19.04729080200195"
+
" -169.9230346679688, -19.03918266296387 -169.9215698242188, -19.04058837890625"
+
" -169.9138641357422, -19.04656600952148 -169.9136047363281, -19.05992698669434"
+
" -169.9196014404297, -19.06432342529297 -169.9275665283203, -19.06826400756836"
+
" -169.9364929199219, -19.06099128723145 -169.9416961669922))"
,
getFeature
.
getQuery
().
getFilter
().
getIntersects
().
getPolygon
().
toString
());
}
}
src/test/resources/index_geo.html
0 → 100644
View file @
d7d412aa
<!DOCTYPE html>
<html
lang=
"en"
>
<head>
<meta
charset=
"utf-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
>
<title>
Leaflet Polygon Selector (Background Download)
</title>
<link
rel=
"stylesheet"
href=
"https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity=
"sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=
""
/>
<link
rel=
"stylesheet"
href=
"https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css"
/>
<script
src=
"https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity=
"sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=
""
></script>
<script
src=
"https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"
></script>
<style>
/* Basic CSS to make the map fullscreen */
html
,
body
{
height
:
100%
;
margin
:
0
;
padding
:
0
;
overflow
:
hidden
;
/* Prevent scrollbars on body */
}
#map
{
width
:
100%
;
height
:
100%
;
}
/* Style for the WKT output box */
#wkt-output
{
position
:
absolute
;
bottom
:
70px
;
/* Position above the download button */
right
:
10px
;
background
:
rgba
(
255
,
255
,
255
,
0.85
);
padding
:
5px
10px
;
border
:
1px
solid
#ccc
;
border-radius
:
4px
;
z-index
:
1000
;
/* Ensure it's above the map */
font-family
:
monospace
;
font-size
:
0.8em
;
max-width
:
300px
;
max-height
:
150px
;
overflow-y
:
auto
;
word-wrap
:
break-word
;
display
:
none
;
/* Hidden by default */
}
/* Simple instruction text */
#instructions
{
position
:
absolute
;
top
:
10px
;
left
:
50px
;
/* Position relative to default zoom controls */
background
:
rgba
(
255
,
255
,
255
,
0.85
);
padding
:
5px
10px
;
border
:
1px
solid
#ccc
;
border-radius
:
4px
;
z-index
:
1000
;
font-family
:
sans-serif
;
font-size
:
0.9em
;
}
/* Style for the Download button */
#download-button
{
position
:
absolute
;
bottom
:
30px
;
right
:
10px
;
z-index
:
1000
;
padding
:
8px
15px
;
cursor
:
pointer
;
background-color
:
#4CAF50
;
/* Green */
color
:
white
;
border
:
none
;
border-radius
:
4px
;
font-size
:
1em
;
display
:
none
;
/* Hidden by default */
}
#download-button
:hover
{
background-color
:
#45a049
;
}
#download-button
:disabled
{
background-color
:
#cccccc
;
cursor
:
not-allowed
;
}
/* Style for the Clear button */
#clear-button
{
position
:
absolute
;
bottom
:
30px
;
right
:
160px
;
/* Adjusted position */
z-index
:
1000
;
padding
:
8px
15px
;
cursor
:
pointer
;
background-color
:
#f44336
;
/* Red */
color
:
white
;
border
:
none
;
border-radius
:
4px
;
font-size
:
1em
;
display
:
none
;
/* Hidden by default, show while drawing or finished*/
}
#clear-button
:hover
{
background-color
:
#da190b
;
}
/* Ensure geocoder input is usable */
.leaflet-control-geocoder
{
z-index
:
1001
!important
;
/* Ensure search is above other elements */
}
.leaflet-control-geocoder-form
input
{
min-width
:
200px
;
/* Make search input wider */
}
#searchContainer
{
position
:
absolute
;
top
:
10px
;
left
:
50%
;
transform
:
translateX
(
-50%
);
/* horizontally center */
z-index
:
1000
;
background
:
rgba
(
255
,
255
,
255
,
0.9
);
padding
:
5px
10px
;
border
:
1px
solid
#ccc
;
border-radius
:
4px
;
display
:
flex
;
gap
:
8px
;
align-items
:
center
;
}
#searchContainer
input
{
padding
:
4px
8px
;
font-size
:
0.9em
;
}
#searchContainer
button
{
padding
:
5px
10px
;
font-size
:
0.9em
;
cursor
:
pointer
;
}
</style>
</head>
<body>
<div
id=
"searchContainer"
>
<input
id=
"searchInput"
type=
"text"
placeholder=
"Enter ID to search"
>
<button
id=
"searchButton"
>
Search
</button>
</div>
<div
id=
"map"
></div>
<div
id=
"instructions"
>
Click map to start drawing polygon.
</div>
<div
id=
"wkt-output"
></div>
<button
id=
"download-button"
>
Download Data
</button>
<button
id=
"clear-button"
>
Clear Polygon
</button>
<script>
const
qaColors
=
{
'
QA1
'
:
{
color
:
'
#e41a1c
'
,
fillColor
:
'
#fbb4ae
'
,
text
:
'
QA1 (EFH/2FA)
'
},
'
QA2
'
:
{
color
:
'
#377eb8
'
,
fillColor
:
'
#b3cde3
'
,
text
:
'
QA2 (Mehrgeschossig)
'
},
'
QA3
'
:
{
color
:
'
#4daf4a
'
,
fillColor
:
'
#ccebc5
'
,
text
:
'
QA3 (Innenstadt)
'
},
'
QA4
'
:
{
color
:
'
#984ea3
'
,
fillColor
:
'
#decbe4
'
,
text
:
'
QA4 (Großer Wohnungsbau)
'
},
'
QA5
'
:
{
color
:
'
#ff7f00
'
,
fillColor
:
'
#fed9a6
'
,
text
:
'
QA5 (Wohn- Geschäfts- Bürozentrum)
'
},
'
QA6
'
:
{
color
:
'
#a65628
'
,
fillColor
:
'
#fddbc7
'
,
text
:
'
QA6 (Stadtnahes Gewerbe)
'
},
'
QA7
'
:
{
color
:
'
#f781bf
'
,
fillColor
:
'
#fbb5d3
'
,
text
:
'
QA7 (Industrie- und Gewerbepark)
'
}
};
// Wait for the DOM to be fully loaded
document
.
addEventListener
(
'
DOMContentLoaded
'
,
function
()
{
// --- Configuration ---
const
SERVER_BASE_URL
=
"
https://citygml.hft-stuttgart.de/citygml
"
;
// Define server URL here
const
DEFAULT_FILENAME
=
"
data.gml
"
;
// Fallback filename for download
// --- Initialize Map ---
const
map
=
L
.
map
(
'
map
'
,
{
keyboard
:
true
}).
fitBounds
([[
47.53564
,
7.52587
],
[
49.78976
,
10.4856
]]);
// Center on BW
const
featureLayers
=
{};
let
highlightedLayer
=
null
;
// --- Load GeoJSON file ---
fetch
(
'
test.geojson
'
)
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
// Add the GeoJSON layer
const
geoJsonLayer
=
L
.
geoJSON
(
data
,
{
style
:
feature
=>
{
const
qa
=
feature
.
properties
.
Quarterarchetype
;
const
colors
=
qaColors
[
qa
]
||
{
color
:
'
gray
'
,
fillColor
:
'
lightgray
'
};
return
{
color
:
colors
.
color
,
weight
:
2
,
fillColor
:
colors
.
fillColor
,
fillOpacity
:
0.5
};
},
onEachFeature
:
function
(
feature
,
layer
)
{
// Save each layer by its ID property
if
(
feature
.
properties
&&
feature
.
properties
.
id
)
{
featureLayers
[
feature
.
properties
.
id
]
=
layer
;
}
// Optional: bind popup to show properties
if
(
feature
.
properties
)
{
layer
.
bindPopup
(
"
ID:
"
+
feature
.
properties
.
id
+
"
<br>Archetype:
"
+
feature
.
properties
.
Quarterarchetype
);
}
}
}).
addTo
(
map
);
map
.
fitBounds
(
geoJsonLayer
.
getBounds
());
// --- Add a Legend ---
const
legend
=
L
.
control
({
position
:
'
bottomleft
'
});
legend
.
onAdd
=
function
(
map
)
{
const
div
=
L
.
DomUtil
.
create
(
'
div
'
,
'
info legend
'
);
div
.
style
.
background
=
'
white
'
;
div
.
style
.
padding
=
'
8px
'
;
div
.
style
.
border
=
'
1px solid #ccc
'
;
div
.
style
.
borderRadius
=
'
4px
'
;
div
.
style
.
fontSize
=
'
0.9em
'
;
div
.
innerHTML
+=
'
<strong>QA Legend</strong><br>
'
;
for
(
const
qa
in
qaColors
)
{
const
{
fillColor
,
color
,
text
}
=
qaColors
[
qa
];
div
.
innerHTML
+=
`<i style="
background:
${
fillColor
}
;
border:2px solid
${
color
}
;
display:inline-block;
width:18px;
height:18px;
margin-right:6px;
vertical-align:middle;">
</i>
${
text
}
<br>`
;
}
return
div
;
};
legend
.
addTo
(
map
);
})
.
catch
(
err
=>
console
.
error
(
'
Error loading GeoJSON:
'
,
err
));
document
.
getElementById
(
'
searchButton
'
).
addEventListener
(
'
click
'
,
()
=>
{
const
value
=
document
.
getElementById
(
'
searchInput
'
).
value
.
trim
();
// Reset any previous highlight
if
(
highlightedLayer
)
{
geoJsonLayer
.
resetStyle
(
highlightedLayer
);
highlightedLayer
=
null
;
}
if
(
value
&&
featureLayers
[
value
])
{
highlightedLayer
=
featureLayers
[
value
];
// Highlight style
highlightedLayer
.
setStyle
({
color
:
'
yellow
'
,
weight
:
4
,
fillColor
:
'
gold
'
,
fillOpacity
:
0.7
});
// Bring to front (useful for polygons)
if
(
highlightedLayer
.
bringToFront
)
{
highlightedLayer
.
bringToFront
();
}
// Zoom to the layer
map
.
fitBounds
(
highlightedLayer
.
getBounds
());
// Optionally open its popup
highlightedLayer
.
openPopup
();
}
else
{
alert
(
'
No feature found with ID =
'
+
value
);
}
});
L
.
tileLayer
(
'
https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
'
,
{
attribution
:
'
© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors
'
}).
addTo
(
map
);
// --- Add Geocoder (Search) Control ---
L
.
Control
.
geocoder
({
defaultMarkGeocode
:
true
}).
addTo
(
map
);
// --- State Variables ---
let
drawingEnabled
=
true
;
let
points
=
[];
let
tempMarkers
=
[];
let
tempPolyline
=
null
;
let
finalPolygon
=
null
;
let
firstMarker
=
null
;
let
currentWktString
=
null
;
// Store WKT for download button
// --- DOM Element References ---
const
wktOutputDiv
=
document
.
getElementById
(
'
wkt-output
'
);
const
downloadButton
=
document
.
getElementById
(
'
download-button
'
);
const
clearButton
=
document
.
getElementById
(
'
clear-button
'
);
const
instructionsDiv
=
document
.
getElementById
(
'
instructions
'
);
// --- Helper Functions ---
function
getFilenameFromHeader
(
header
)
{
if
(
!
header
)
return
null
;
// Simple regex for filename="..."; handles quotes, case-insensitive
const
match
=
header
.
match
(
/filename
\s
*=
\s
*"
?([^
"
]
+
)
"
?
/i
);
return
match
&&
match
[
1
]
?
match
[
1
]
:
null
;
}
function
updateInstructions
()
{
if
(
drawingEnabled
)
{
if
(
points
.
length
===
0
)
{
instructionsDiv
.
textContent
=
'
Click map to start drawing polygon.
'
;
}
else
if
(
points
.
length
<
3
)
{
instructionsDiv
.
textContent
=
`Points:
${
points
.
length
}
. Add
${
3
-
points
.
length
}
more point(s). Click first marker to close (min 3 points).`
;
}
else
{
instructionsDiv
.
textContent
=
`Points:
${
points
.
length
}
. Click first marker or add more points.`
;
}
}
else
{
instructionsDiv
.
textContent
=
'
Polygon finalized. Click Download or Clear.
'
;
}
}
function
resetDrawing
()
{
drawingEnabled
=
true
;
points
=
[];
currentWktString
=
null
;
tempMarkers
.
forEach
(
marker
=>
map
.
removeLayer
(
marker
));
tempMarkers
=
[];
if
(
firstMarker
)
{
firstMarker
.
off
(
'
click
'
);
// Remove specific listener
firstMarker
=
null
;
}
if
(
tempPolyline
)
{
map
.
removeLayer
(
tempPolyline
);
tempPolyline
=
null
;
}
if
(
finalPolygon
)
{
map
.
removeLayer
(
finalPolygon
);
finalPolygon
=
null
;
}
wktOutputDiv
.
style
.
display
=
'
none
'
;
wktOutputDiv
.
textContent
=
''
;
downloadButton
.
style
.
display
=
'
none
'
;
downloadButton
.
disabled
=
false
;
// Re-enable button
downloadButton
.
textContent
=
'
Download Data
'
;
// Reset button text
clearButton
.
style
.
display
=
'
none
'
;
updateInstructions
();
console
.
log
(
"
Drawing reset.
"
);
}
function
finalizePolygon
()
{
if
(
points
.
length
<
3
)
return
;
console
.
log
(
"
Finalizing polygon...
"
);
drawingEnabled
=
false
;
// Remove temporary items
tempMarkers
.
forEach
(
marker
=>
map
.
removeLayer
(
marker
));
tempMarkers
=
[];
if
(
tempPolyline
)
{
map
.
removeLayer
(
tempPolyline
);
tempPolyline
=
null
;
}
if
(
firstMarker
)
{
firstMarker
.
off
(
'
click
'
);
firstMarker
=
null
;
// Ref only needed during drawing
}
if
(
finalPolygon
)
map
.
removeLayer
(
finalPolygon
);
finalPolygon
=
L
.
polygon
(
points
,
{
color
:
'
blue
'
}).
addTo
(
map
);
// Generate and store WKT
let
wktCoords
=
points
.
map
(
p
=>
`
${
p
.
lng
}
${
p
.
lat
}
`
);
wktCoords
.
push
(
`
${
points
[
0
].
lng
}
${
points
[
0
].
lat
}
`
);
// Close ring
currentWktString
=
`POLYGON((
${
wktCoords
.
join
(
'
,
'
)}
))`
;
// Store for download
// Display WKT
wktOutputDiv
.
textContent
=
`WKT (WGS84):\n
${
currentWktString
}
`
;
wktOutputDiv
.
style
.
display
=
'
block
'
;
// Show buttons
downloadButton
.
style
.
display
=
'
block
'
;
clearButton
.
style
.
display
=
'
block
'
;
updateInstructions
();
}
// --- Event Listeners ---
// Map Click: Add points
map
.
on
(
'
click
'
,
function
(
e
)
{
if
(
!
drawingEnabled
)
return
;
const
latlng
=
e
.
latlng
;
points
.
push
(
latlng
);
if
(
points
.
length
>
0
)
{
clearButton
.
style
.
display
=
'
block
'
;
}
const
newMarker
=
L
.
marker
(
latlng
).
addTo
(
map
);
tempMarkers
.
push
(
newMarker
);
if
(
points
.
length
===
1
)
{
firstMarker
=
newMarker
;
// Store reference to the first marker
// Add the specific click listener for closing
firstMarker
.
on
(
'
click
'
,
function
(
ev
)
{
L
.
DomEvent
.
stopPropagation
(
ev
);
// Prevent map click
if
(
drawingEnabled
&&
points
.
length
>=
3
)
{
finalizePolygon
();
}
else
if
(
drawingEnabled
)
{
console
.
log
(
"
Click on first marker, but not enough points (<3).
"
);
// Optional: Provide user feedback here
}
});
}
if
(
tempPolyline
)
map
.
removeLayer
(
tempPolyline
);
if
(
points
.
length
>
1
)
{
tempPolyline
=
L
.
polyline
(
points
,
{
color
:
'
red
'
,
weight
:
2
,
dashArray
:
'
5, 5
'
}).
addTo
(
map
);
}
updateInstructions
();
});
// Download Button Click: Fetch data and trigger download
downloadButton
.
addEventListener
(
'
click
'
,
async
function
()
{
if
(
!
currentWktString
)
{
console
.
error
(
"
Download clicked, but no WKT available.
"
);
return
;
}
const
encodedWkt
=
encodeURIComponent
(
currentWktString
);
// Correctly escapes () etc.
const
fullUrl
=
`
${
SERVER_BASE_URL
}
?wktPolygon=
${
encodedWkt
}
`
;
console
.
log
(
"
Requesting data from:
"
,
fullUrl
);
downloadButton
.
disabled
=
true
;
downloadButton
.
textContent
=
'
Downloading...
'
;
try
{
const
response
=
await
fetch
(
fullUrl
);
if
(
!
response
.
ok
)
{
// Handle HTTP errors (e.g., 404, 500)
throw
new
Error
(
`HTTP error! status:
${
response
.
status
}
${
response
.
statusText
}
`
);
}
// Get data as a Blob
const
blob
=
await
response
.
blob
();
// Determine filename
const
contentDisposition
=
response
.
headers
.
get
(
'
Content-Disposition
'
);
const
filename
=
getFilenameFromHeader
(
contentDisposition
)
||
DEFAULT_FILENAME
;
// Create a temporary link to trigger download
const
tempLink
=
document
.
createElement
(
'
a
'
);
const
objectUrl
=
URL
.
createObjectURL
(
blob
);
tempLink
.
href
=
objectUrl
;
tempLink
.
download
=
filename
;
// Set the desired filename
document
.
body
.
appendChild
(
tempLink
);
// Append necessary for Firefox
tempLink
.
click
();
// Simulate click
// Cleanup
document
.
body
.
removeChild
(
tempLink
);
URL
.
revokeObjectURL
(
objectUrl
);
console
.
log
(
`Download triggered for file:
${
filename
}
`
);
downloadButton
.
textContent
=
'
Download Complete
'
;
// Give feedback
}
catch
(
error
)
{
console
.
error
(
"
Download failed:
"
,
error
);
alert
(
`Failed to download data:
${
error
.
message
}
`
);
// Notify user
downloadButton
.
textContent
=
'
Download Failed
'
;
// Give feedback
}
finally
{
// Re-enable button after a delay, or keep it as 'Complete'/'Failed'
// For simplicity, let's re-enable and reset text on clear/new polygon.
// If keeping status, disable clear until download complete/failed?
// Let's just allow clear anytime.
// Reset button state after a small delay to show status
setTimeout
(()
=>
{
if
(
!
drawingEnabled
)
{
// Only reset if still in finalized state
downloadButton
.
disabled
=
false
;
downloadButton
.
textContent
=
'
Download Data
'
;
}
},
3000
);
// Reset text after 3 seconds
}
});
// Clear Button Click
clearButton
.
addEventListener
(
'
click
'
,
function
()
{
console
.
log
(
"
Clear button clicked.
"
);
resetDrawing
();
});
// Optional: Reset on double-click (if not drawing)
map
.
on
(
'
dblclick
'
,
function
(
e
)
{
if
(
!
drawingEnabled
)
{
resetDrawing
();
}
});
// --- Initial State ---
updateInstructions
();
});
// End DOMContentLoaded
</script>
</body>
</html>
\ No newline at end of file
Prev
1
2
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment