Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions public/content/ogcapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: Connect the Ocotillo OGC API to desktop GIS
deck: Use the Ocotillo OGC API Features endpoint to browse collections in ArcGIS Desktop and in QGIS.
---

> [!WARNING]
> OGC API layers are read-only in desktop GIS. Use them for discovery, map display, querying, and export.

##### Ocotillo OGC landing page URL

```text
{{ ocotillo_api_url }}/ogcapi
```

## ArcGIS Pro / Desktop

1. On the **Insert** tab, in the **Project** group, click **Connections > Server > New OGC API Server**. The **Add OGC API Server Connection** dialog box appears.
2. Enter this URL ([{{ ocotillo_api_url }}/ogcapi]({{ ocotillo_api_url }}/ogcapi)) in the **Server URL** text box.
3. Leave the rest of the options as-is and click OK.
4. In the Catalog pane, expand the “Servers” folder. You should see the Ocotillo OGC API connection. Expand the connection, then expand “Features”. Drag the datasets you want into your map area.
5. When adding a layer, a dialog box will appear with spatial extent options. Click OK to add the entire contents. You can also check the “Use Spatial Extent” box and spatially filter via options in the “Get extent from:” box - e.g. spatially filter by existing layers, selected polygon extents, visible extent, etc.

**Official documentation:**
[How to add OGC API datasets to ArcGIS Pro](https://pro.arcgis.com/en/pro-app/latest/help/data/services/add-ogc-api-services.htm)
Comment thread
TylerAdamMartinez marked this conversation as resolved.
[https://pro.arcgis.com/en/pro-app/latest/help/data/services/use-ogc-api-services.htm](https://pro.arcgis.com/en/pro-app/latest/help/data/services/use-ogc-api-services.htm)

---

## QGIS

1. Open **Data Source Manager**.
2. Choose the WFS / OGC API - Features connection tab.
3. Create a new connection using the Ocotillo landing page URL.
4. Connect to the server, select one or more collections, and add them to the map.
5. For large layers, set paging or feature limits in the connection and layer options.

> [!INFO]
> QGIS expects the OGC API landing page, not a single collection items URL, when you create the server connection.

**Official documentation:**
[https://docs.qgis.org/latest/en/docs/user_manual/working_with_ogc/ogc_client_support.html](https://docs.qgis.org/latest/en/docs/user_manual/working_with_ogc/ogc_client_support.html)

---

## Useful Ocotillo endpoints

### Landing page

[{{ ocotillo_api_url }}/ogcapi]({{ ocotillo_api_url }}/ogcapi)

Use this as the server URL when creating the connection.

### Collections

[{{ ocotillo_api_url }}/ogcapi/collections]({{ ocotillo_api_url }}/ogcapi/collections)

Review available collections before connecting from desktop GIS.

---

## Common collections to look for

- [!CHIPS]
- Water Wells
- Springs
- Latest Depth to Water
- Average TDS
- Latest TDS

Collection names can change by deployment. If you do not see one of these, open the [collections endpoint]({{ ocotillo_api_url }}/ogcapi/collections) and use the names published there.
4 changes: 4 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ const App: React.FC = () => (
path="/report-a-bug"
element={<ContentPage src="/content/report-a-bug.md" />}
/>
<Route
path="/ogcapi"
element={<ContentPage src="/content/ogcapi.md" />}
/>
<Route path="/amp/*" element={<AMPRoutes />} />
<Route path="/ocotillo/*" element={<OcotilloRoutes />} />
<Route path="/st2/*" element={<ST2Routes />} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/sider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ export const ThemedSiderV2: React.FC<RefineThemedLayoutSiderProps> = ({
>
{[
{ to: '/about', label: 'About' },
{ to: '/ocotillo/help', label: 'Connect Desktop GIS' },
{ to: '/ogcapi', label: 'Connect Desktop GIS' },
{ to: '/report-a-bug', label: 'Report a Bug' },
].map(({ to, label }) => (
<RouterLink key={to} to={to} style={{ textDecoration: 'none' }}>
Expand Down
215 changes: 203 additions & 12 deletions src/pages/content/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import React, { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import { Box, CircularProgress, Divider, Typography } from '@mui/material'
import {
Alert,
Box,
CircularProgress,
Divider,
Link,
Typography,
IconButton,
Tooltip,
Stack,
Chip,
} from '@mui/material'
import { ContentCopy } from '@mui/icons-material'
import { Components } from 'react-markdown'
import { settings } from '@/settings'

export type FrontMatter = {
title?: string
Expand All @@ -28,7 +41,10 @@ export function parseFrontmatter(text: string): {
const colonIdx = line.indexOf(':')
if (colonIdx === -1) continue
const key = line.slice(0, colonIdx).trim()
const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, '')
const value = line
.slice(colonIdx + 1)
.trim()
.replace(/^["']|["']$/g, '')
if (key === 'title' || key === 'deck' || key === 'date') {
data[key] = value
}
Expand All @@ -54,22 +70,131 @@ export const markdownComponents: Components = {
</Typography>
),
a: ({ href, children }) => (
<a href={href} style={{ color: 'inherit' }}>
<Link href={href} target="_blank" rel="noreferrer">
{children}
</a>
),
ul: ({ children }) => (
<Box component="ul" sx={{ pl: 3, mb: 2 }}>
{children}
</Box>
</Link>
),
blockquote: ({ children }) => {
const text = React.Children.toArray(children)
.map((child) => {
if (React.isValidElement(child)) {
return React.Children.toArray(child.props.children).join('')
}

return String(child)
})
.join('')
.trim()

const alertMatch = text.match(
/^\[!(WARNING|INFO|ERROR|SUCCESS)\]\s*([\s\S]*)$/i
)

if (alertMatch) {
const severityMap = {
WARNING: 'warning',
INFO: 'info',
ERROR: 'error',
SUCCESS: 'success',
} as const

const alertType = alertMatch[1].toUpperCase() as keyof typeof severityMap
const alertBody = alertMatch[2].trim()

return (
<Alert severity={severityMap[alertType]} sx={{ my: 3 }}>
{alertBody}
</Alert>
)
}

return (
<Box
component="blockquote"
sx={{
borderLeft: 4,
borderColor: 'divider',
pl: 2,
my: 3,
color: 'text.secondary',
fontStyle: 'italic',
}}
>
{children}
</Box>
)
},
code: ({ children, className }) => {
const value = String(children).replace(/\n$/, '')

if (className) {
return <CopyCodeBlock value={value} />
}

return (
<Typography component="code" sx={{ bgcolor: 'action.hover', px: 0.5 }}>
{children}
</Typography>
)
},
ul: ({ children, node }) => {
const getListItemText = (listItem: any): string => {
return (
listItem?.children
?.map((child: any) => child.value ?? '')
?.join('')
?.trim() ?? ''
)
}

const listItems =
node?.children?.filter(
(child: any) => child.type === 'element' && child.tagName === 'li'
) ?? []
const firstItemText = getListItemText(listItems[0])

if (firstItemText === '[!CHIPS]') {
return (
<Stack
direction="row"
spacing={1}
useFlexGap
flexWrap="wrap"
sx={{ mb: 2 }}
>
{listItems.slice(1).map((item: any, index: number) => {
const label = getListItemText(item)

return (
<Chip
key={`${label}-${index}`}
label={label}
variant="outlined"
color="default"
/>
)
})}
</Stack>
)
}

return (
<Box component="ul" sx={{ pl: 3, mb: 2 }}>
{children}
</Box>
)
},
ol: ({ children }) => (
<Box component="ol" sx={{ pl: 3, mb: 2 }}>
{children}
</Box>
),
li: ({ children }) => (
<Typography component="li" variant="body1" sx={{ mb: 0.5, color: 'text.secondary' }}>
<Typography
component="li"
variant="body1"
sx={{ mb: 0.75, color: 'text.secondary' }}
>
{children}
</Typography>
),
Expand Down Expand Up @@ -128,7 +253,10 @@ export const MarkdownPage: React.FC<MarkdownPageProps> = ({
</Typography>
)}
{frontmatter.date && (
<Typography variant="caption" sx={{ display: 'block', mb: 3, color: 'text.disabled' }}>
<Typography
variant="caption"
sx={{ display: 'block', mb: 3, color: 'text.disabled' }}
>
{new Date(frontmatter.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
Expand Down Expand Up @@ -159,7 +287,28 @@ export const ContentPage: React.FC<ContentPageProps> = ({ src }) => {
return res.text()
})
.then((text) => {
const parsed = parseFrontmatter(text)
// Replace template placeholders like {{ key }} in the markdown text
// with corresponding values from the `settings` object.
//
// Example:
// "https://{{ ocotillo_api_url }}/ogcapi"
// → "https://actual-value/ogcapi"
//
const hydratedText = text.replace(
/{{\s*([\w]+)\s*}}/g,
(_, key: string) => {
const value = (settings as Record<string, any>)[key]

if (typeof value === 'string') {
return value.replace(/\/+$/, '')
}

// if key not found or not string → reinsert key name
return `{{ ${key} }}`
}
)

const parsed = parseFrontmatter(hydratedText)
setFrontmatter(parsed.data)
setBody(parsed.content)
})
Expand Down Expand Up @@ -194,3 +343,45 @@ export const ContentPage: React.FC<ContentPageProps> = ({ src }) => {

return <MarkdownPage frontmatter={frontmatter} body={body} />
}

const CopyCodeBlock = ({ value }: { value: string }) => {
const handleCopy = async () => {
await navigator.clipboard.writeText(value)
}

return (
<Box sx={{ position: 'relative', mb: 2 }}>
<Typography
component="code"
variant="body2"
sx={{
px: 1.25,
py: 1.25,
pr: 6,
borderRadius: 1,
bgcolor: 'action.hover',
overflowWrap: 'anywhere',
display: 'block',
color: 'text.primary',
}}
>
{value}
</Typography>

<Tooltip title="Copy">
<IconButton
size="small"
onClick={handleCopy}
sx={{
position: 'absolute',
top: 6,
right: 6,
}}
aria-label="Copy code block"
>
<ContentCopy fontSize="inherit" />
</IconButton>
</Tooltip>
</Box>
)
}
1 change: 0 additions & 1 deletion src/pages/ocotillo/help/index.tsx

This file was deleted.

Loading
Loading